mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-07 22:25:35 +00:00
parent
9977789cac
commit
3dcc7d60a2
23 changed files with 588 additions and 23 deletions
|
@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Update Mattermost setup instructions
|
||||
- Add support for specifying a run ID via a "rid" query parameter (#722)
|
||||
- Add last ping body in Slack notifications (#735)
|
||||
- Add ntfy integration (#728)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix the most recent ping lookup in the "Ping Details" dialog
|
||||
|
|
|
@ -53,6 +53,7 @@ CHANNEL_KINDS = (
|
|||
("matrix", "Matrix"),
|
||||
("mattermost", "Mattermost"),
|
||||
("msteams", "Microsoft Teams"),
|
||||
("ntfy", "ntfy"),
|
||||
("opsgenie", "Opsgenie"),
|
||||
("pagerteam", "Pager Team"),
|
||||
("pagertree", "PagerTree"),
|
||||
|
@ -82,6 +83,15 @@ PO_PRIORITIES = {
|
|||
2: "emergency",
|
||||
}
|
||||
|
||||
NTFY_PRIORITIES = {
|
||||
5: "max",
|
||||
4: "high",
|
||||
3: "default",
|
||||
2: "low",
|
||||
1: "min",
|
||||
0: "disabled",
|
||||
}
|
||||
|
||||
|
||||
def isostring(dt) -> str | None:
|
||||
"""Convert the datetime to ISO 8601 format with no microseconds."""
|
||||
|
@ -611,7 +621,7 @@ class Channel(models.Model):
|
|||
return {"id": str(self.code), "name": self.name, "kind": self.kind}
|
||||
|
||||
def is_editable(self):
|
||||
return self.kind in ("email", "webhook", "sms", "signal", "whatsapp")
|
||||
return self.kind in ("email", "webhook", "sms", "signal", "whatsapp", "ntfy")
|
||||
|
||||
def assign_all_checks(self):
|
||||
checks = Check.objects.filter(project=self.project)
|
||||
|
@ -672,6 +682,8 @@ class Channel(models.Model):
|
|||
return transports.Mattermost(self)
|
||||
elif self.kind == "msteams":
|
||||
return transports.MsTeams(self)
|
||||
elif self.kind == "ntfy":
|
||||
return transports.Ntfy(self)
|
||||
elif self.kind == "opsgenie":
|
||||
return transports.Opsgenie(self)
|
||||
elif self.kind == "pagertree":
|
||||
|
@ -1024,6 +1036,34 @@ class Channel(models.Model):
|
|||
doc = json.loads(self.value)
|
||||
return doc["token"]
|
||||
|
||||
@property
|
||||
def ntfy_topic(self):
|
||||
assert self.kind == "ntfy"
|
||||
doc = json.loads(self.value)
|
||||
return doc["topic"]
|
||||
|
||||
@property
|
||||
def ntfy_url(self):
|
||||
assert self.kind == "ntfy"
|
||||
doc = json.loads(self.value)
|
||||
return doc["url"]
|
||||
|
||||
@property
|
||||
def ntfy_priority(self):
|
||||
assert self.kind == "ntfy"
|
||||
doc = json.loads(self.value)
|
||||
return doc["priority"]
|
||||
|
||||
@property
|
||||
def ntfy_priority_up(self):
|
||||
assert self.kind == "ntfy"
|
||||
doc = json.loads(self.value)
|
||||
return doc["priority_up"]
|
||||
|
||||
@property
|
||||
def ntfy_priority_display(self):
|
||||
return NTFY_PRIORITIES[self.ntfy_priority]
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
code = models.UUIDField(default=uuid.uuid4, null=True, editable=False)
|
||||
|
|
100
hc/api/tests/test_notify_ntfy.py
Normal file
100
hc/api/tests/test_notify_ntfy.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import timedelta as td
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.utils.timezone import now
|
||||
|
||||
from hc.api.models import Channel, Check, Notification
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class NotifyNtfyTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.check = Check(project=self.project)
|
||||
self.check.name = "Foo"
|
||||
self.check.status = "down"
|
||||
self.check.last_ping = now() - td(minutes=61)
|
||||
self.check.save()
|
||||
|
||||
self.channel = Channel(project=self.project)
|
||||
self.channel.kind = "ntfy"
|
||||
self.channel.value = json.dumps(
|
||||
{
|
||||
"url": "https://example.org",
|
||||
"topic": "foo",
|
||||
"priority": 5,
|
||||
"priority_up": 1,
|
||||
}
|
||||
)
|
||||
self.channel.save()
|
||||
self.channel.checks.add(self.check)
|
||||
|
||||
@patch("hc.api.transports.curl.request")
|
||||
def test_it_works(self, mock_post):
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
self.channel.notify(self.check)
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["json"]
|
||||
self.assertEqual(payload["title"], "Foo is DOWN")
|
||||
self.assertEqual(payload["actions"][0]["url"], self.check.cloaked_url())
|
||||
self.assertNotIn("All the other checks are up.", payload["message"])
|
||||
|
||||
@patch("hc.api.transports.curl.request")
|
||||
def test_it_shows_all_other_checks_up_note(self, mock_post):
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
other = Check(project=self.project)
|
||||
other.name = "Foobar"
|
||||
other.status = "up"
|
||||
other.last_ping = now() - td(minutes=61)
|
||||
other.save()
|
||||
|
||||
self.channel.notify(self.check)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["json"]
|
||||
self.assertIn("All the other checks are up.", payload["message"])
|
||||
|
||||
@patch("hc.api.transports.curl.request")
|
||||
def test_it_lists_other_down_checks(self, mock_post):
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
other = Check(project=self.project)
|
||||
other.name = "Foobar"
|
||||
other.status = "down"
|
||||
other.last_ping = now() - td(minutes=61)
|
||||
other.save()
|
||||
|
||||
self.channel.notify(self.check)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["json"]
|
||||
self.assertIn("The following checks are also down", payload["message"])
|
||||
self.assertIn("Foobar", payload["message"])
|
||||
|
||||
@patch("hc.api.transports.curl.request")
|
||||
def test_it_does_not_show_more_than_10_other_checks(self, mock_post):
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
for i in range(0, 11):
|
||||
other = Check(project=self.project)
|
||||
other.name = f"Foobar #{i}"
|
||||
other.status = "down"
|
||||
other.last_ping = now() - td(minutes=61)
|
||||
other.save()
|
||||
|
||||
self.channel.notify(self.check)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["json"]
|
||||
self.assertNotIn("Foobar", payload["message"])
|
||||
self.assertIn("11 other checks are also down.", payload["message"])
|
|
@ -1000,3 +1000,33 @@ class Gotify(HttpTransport):
|
|||
}
|
||||
|
||||
self.post(url, json=payload)
|
||||
|
||||
|
||||
class Ntfy(HttpTransport):
|
||||
def priority(self, check):
|
||||
if check.status == "up":
|
||||
return self.channel.ntfy_priority_up
|
||||
|
||||
return self.channel.ntfy_priority
|
||||
|
||||
def is_noop(self, check) -> bool:
|
||||
return self.priority(check) == 0
|
||||
|
||||
def notify(self, check, notification=None) -> None:
|
||||
ctx = {"check": check, "down_checks": self.down_checks(check)}
|
||||
payload = {
|
||||
"topic": self.channel.ntfy_topic,
|
||||
"priority": self.priority(check),
|
||||
"title": tmpl("ntfy_title.html", **ctx),
|
||||
"message": tmpl("ntfy_message.html", **ctx),
|
||||
"tags": ["red_circle" if check.status == "down" else "green_circle"],
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
"label": f"View on {settings.SITE_NAME}",
|
||||
"url": check.cloaked_url(),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
self.post(self.channel.ntfy_url, json=payload)
|
||||
|
|
|
@ -341,6 +341,17 @@ class AddGotifyForm(forms.Form):
|
|||
return json.dumps(dict(self.cleaned_data), sort_keys=True)
|
||||
|
||||
|
||||
class NtfyForm(forms.Form):
|
||||
error_css_class = "has-error"
|
||||
topic = forms.CharField(max_length=50)
|
||||
url = forms.URLField(max_length=1000, validators=[WebhookValidator()])
|
||||
priority = forms.IntegerField(initial=3, min_value=0, max_value=5)
|
||||
priority_up = forms.IntegerField(initial=3, min_value=0, max_value=5)
|
||||
|
||||
def get_value(self):
|
||||
return json.dumps(dict(self.cleaned_data), sort_keys=True)
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
q = forms.RegexField(regex=r"^[0-9a-zA-Z\s]{3,100}$")
|
||||
|
||||
|
|
72
hc/front/tests/test_add_ntfy.py
Normal file
72
hc/front/tests/test_add_ntfy.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class AddNtfyTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.check = Check.objects.create(project=self.project)
|
||||
self.url = f"/projects/{self.project.code}/add_ntfy/"
|
||||
|
||||
def test_instructions_work(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "simple HTTP-based pub-sub")
|
||||
|
||||
def test_it_creates_channel(self):
|
||||
form = {
|
||||
"topic": "foo",
|
||||
"url": "https://example.org",
|
||||
"priority": "5",
|
||||
"priority_up": "1",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, self.channels_url)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.kind, "ntfy")
|
||||
self.assertEqual(c.ntfy_topic, "foo")
|
||||
self.assertEqual(c.ntfy_url, "https://example.org")
|
||||
self.assertEqual(c.ntfy_priority, 5)
|
||||
self.assertEqual(c.ntfy_priority_up, 1)
|
||||
self.assertEqual(c.project, self.project)
|
||||
|
||||
# Make sure it calls assign_all_checks
|
||||
self.assertEqual(c.checks.count(), 1)
|
||||
|
||||
def test_it_requires_topic(self):
|
||||
form = {
|
||||
"url": "https://example.org",
|
||||
"priority": "5",
|
||||
"priority_up": "1",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertContains(r, "This field is required")
|
||||
|
||||
def test_it_validates_url(self):
|
||||
form = {
|
||||
"topic": "foo",
|
||||
"url": "this is not an url",
|
||||
"priority": "5",
|
||||
"priority_up": "1",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertContains(r, "Enter a valid URL")
|
||||
|
||||
def test_it_requires_rw_access(self):
|
||||
self.bobs_membership.role = "r"
|
||||
self.bobs_membership.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 403)
|
61
hc/front/tests/test_edit_ntfy.py
Normal file
61
hc/front/tests/test_edit_ntfy.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class EditNtfyTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.check = Check.objects.create(project=self.project)
|
||||
|
||||
self.channel = Channel(project=self.project, kind="ntfy")
|
||||
self.channel.value = json.dumps(
|
||||
{
|
||||
"topic": "foo-bar-baz",
|
||||
"url": "https://example.org",
|
||||
"priority": 3,
|
||||
"priority_up": 0,
|
||||
}
|
||||
)
|
||||
self.channel.save()
|
||||
|
||||
self.url = f"/integrations/{self.channel.code}/edit/"
|
||||
|
||||
def test_instructions_work(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Save Integration")
|
||||
self.assertContains(r, "https://example.org")
|
||||
self.assertContains(r, "foo-bar-baz")
|
||||
|
||||
def test_it_updates_channel(self):
|
||||
form = {
|
||||
"topic": "updated-topic",
|
||||
"url": "https://example.com",
|
||||
"priority": "4",
|
||||
"priority_up": "1",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, self.channels_url)
|
||||
|
||||
self.channel.refresh_from_db()
|
||||
self.assertEqual(self.channel.ntfy_topic, "updated-topic")
|
||||
self.assertEqual(self.channel.ntfy_url, "https://example.com")
|
||||
self.assertEqual(self.channel.ntfy_priority, 4)
|
||||
self.assertEqual(self.channel.ntfy_priority_up, 1)
|
||||
|
||||
# Make sure it does not call assign_all_checks
|
||||
self.assertFalse(self.channel.checks.exists())
|
||||
|
||||
def test_it_requires_rw_access(self):
|
||||
self.bobs_membership.role = "r"
|
||||
self.bobs_membership.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 403)
|
|
@ -66,6 +66,7 @@ project_urls = [
|
|||
path("add_matrix/", views.add_matrix, name="hc-add-matrix"),
|
||||
path("add_mattermost/", views.add_mattermost, name="hc-add-mattermost"),
|
||||
path("add_msteams/", views.add_msteams, name="hc-add-msteams"),
|
||||
path("add_ntfy/", views.ntfy_form, name="hc-add-ntfy"),
|
||||
path("add_opsgenie/", views.add_opsgenie, name="hc-add-opsgenie"),
|
||||
path("add_pagertree/", views.add_pagertree, name="hc-add-pagertree"),
|
||||
path("add_pd/", views.add_pd, name="hc-add-pd"),
|
||||
|
|
|
@ -1210,6 +1210,8 @@ def edit_channel(request: HttpRequest, code: UUID) -> HttpResponse:
|
|||
return signal_form(request, channel=channel)
|
||||
if channel.kind == "whatsapp":
|
||||
return whatsapp_form(request, channel=channel)
|
||||
if channel.kind == "ntfy":
|
||||
return ntfy_form(request, channel=channel)
|
||||
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -2304,6 +2306,43 @@ def add_gotify(request, code):
|
|||
return render(request, "integrations/add_gotify.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def ntfy_form(request, channel=None, code=None):
|
||||
is_new = channel is None
|
||||
if is_new:
|
||||
project = _get_rw_project_for_user(request, code)
|
||||
channel = Channel(project=project, kind="ntfy")
|
||||
|
||||
if request.method == "POST":
|
||||
form = forms.NtfyForm(request.POST)
|
||||
if form.is_valid():
|
||||
channel.value = form.get_value()
|
||||
channel.save()
|
||||
|
||||
if is_new:
|
||||
channel.assign_all_checks()
|
||||
return redirect("hc-channels", channel.project.code)
|
||||
elif is_new:
|
||||
form = forms.NtfyForm()
|
||||
else:
|
||||
form = forms.NtfyForm(
|
||||
{
|
||||
"topic": channel.ntfy_topic,
|
||||
"url": channel.ntfy_url,
|
||||
"priority": channel.ntfy_priority,
|
||||
}
|
||||
)
|
||||
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
"project": channel.project,
|
||||
"form": form,
|
||||
"profile": channel.project.owner_profile,
|
||||
"is_new": is_new,
|
||||
}
|
||||
return render(request, "integrations/ntfy_form.html", ctx)
|
||||
|
||||
|
||||
@require_setting("SIGNAL_CLI_SOCKET")
|
||||
@login_required
|
||||
def signal_captcha(request: HttpRequest) -> HttpResponse:
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
#add-pushover .help {
|
||||
opacity: 0.6;
|
||||
}
|
|
@ -270,3 +270,8 @@ input[type=number]::-webkit-inner-spin-button {
|
|||
background-color: var(--btn-remove-hover);
|
||||
color: var(--btn-remove-color);
|
||||
}
|
||||
|
||||
/* Greyed out help text in bootstrap-select dropdowns */
|
||||
.dropdown .help {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
|
|
@ -240,3 +240,7 @@ body.dark .icon.mattermost,
|
|||
body.dark .icon.matrix {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
img.kind-ntfy {
|
||||
filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.2));
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src:
|
||||
url('../fonts/icomoon.ttf?tkwenv') format('truetype'),
|
||||
url('../fonts/icomoon.woff?tkwenv') format('woff'),
|
||||
url('../fonts/icomoon.svg?tkwenv#icomoon') format('svg');
|
||||
url('../fonts/icomoon.ttf?bncoc2') format('truetype'),
|
||||
url('../fonts/icomoon.woff?bncoc2') format('woff'),
|
||||
url('../fonts/icomoon.svg?bncoc2#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -23,6 +23,15 @@
|
|||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.ic-ntfy:before {
|
||||
content: "\e927";
|
||||
color: rgb(51, 133, 116);
|
||||
}
|
||||
.ic-ntfy:after {
|
||||
content: "\e928";
|
||||
margin-left: -1em;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.ic-gotify:before {
|
||||
content: "\e925";
|
||||
|
|
File diff suppressed because one or more lines are too long
Before (image error) Size: 50 KiB After (image error) Size: 50 KiB |
Binary file not shown.
Binary file not shown.
BIN
static/img/integrations/ntfy.png
Normal file
BIN
static/img/integrations/ntfy.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.6 KiB |
|
@ -29,7 +29,6 @@
|
|||
|
||||
<link rel="stylesheet" href="{% static 'css/add_credential.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/appearance.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/badges.css' %}" type="text/css">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
{% for ch in channels %}
|
||||
<tr class="channel-row kind-{{ ch.kind }}">
|
||||
<td class="icon-cell">
|
||||
<img src="{% static ch.icon_path %}" alt="{{ ch.get_kind_display }}" />
|
||||
<img src="{% static ch.icon_path %}" alt="{{ ch.get_kind_display }}" class="kind-{{ ch.kind }}" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="edit-name" data-toggle="modal" data-target="#name-{{ ch.code }}">
|
||||
|
@ -104,6 +104,9 @@
|
|||
{% if ch.signal_notify_up and not ch.signal_notify_down %}
|
||||
(up only)
|
||||
{% endif %}
|
||||
{% elif ch.kind == "ntfy" %}
|
||||
ntfy topic <span>{{ ch.ntfy_topic }}</span>,
|
||||
{{ ch.ntfy_priority_display }} priority
|
||||
{% else %}
|
||||
{{ ch.get_kind_display }}
|
||||
{% endif %}
|
||||
|
@ -299,6 +302,15 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<img src="{% static 'img/integrations/ntfy.png' %}"
|
||||
class="icon kind-ntfy" alt="ntfy" />
|
||||
|
||||
<h2>ntfy</h2>
|
||||
<p>Send push notifications to your phone or desktop via PUT/POST.</p>
|
||||
<a href="{% url 'hc-add-ntfy' project.code %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
|
||||
{% if enable_opsgenie %}
|
||||
<li>
|
||||
<img src="{% static 'img/integrations/opsgenie.png' %}"
|
||||
|
|
|
@ -32,12 +32,12 @@
|
|||
</option>
|
||||
<option
|
||||
value="-2"
|
||||
data-content="Lowest Priority. <span class='help'>Generates no notification on your device.</span>">
|
||||
data-content="Lowest priority. <span class='help'>Generates no notification on your device.</span>">
|
||||
Lowest Priority
|
||||
</option>
|
||||
<option
|
||||
value="-1"
|
||||
data-content="Low Priority. <span class='help'>Sends a quiet notification.</span>">
|
||||
data-content="Low priority. <span class='help'>Sends a quiet notification.</span>">
|
||||
Low Priority
|
||||
</option>
|
||||
<option value="0" selected="selected">
|
||||
|
@ -45,12 +45,12 @@
|
|||
</option>
|
||||
<option
|
||||
value="1"
|
||||
data-content="High Priority. <span class='help'>Bypasses user's quiet hours.</span>">
|
||||
data-content="High priority. <span class='help'>Bypasses user's quiet hours.</span>">
|
||||
High Priority
|
||||
</option>
|
||||
<option
|
||||
value="2"
|
||||
data-content="Emergency Priority. <span class='help'>Repeated every {{po_retry_delay|hc_duration }} for at most {{ po_expiration|hc_duration }} until you acknowledge them.</span>">
|
||||
data-content="Emergency priority. <span class='help'>Repeated every {{po_retry_delay|hc_duration }} for at most {{ po_expiration|hc_duration }} until you acknowledge them.</span>">
|
||||
Emergency Priority
|
||||
</option>
|
||||
</select>
|
||||
|
@ -68,26 +68,26 @@
|
|||
</option>
|
||||
<option
|
||||
value="-2"
|
||||
data-content="Lowest Priority. <span class='help'>Generates no notification on your device.</span>">
|
||||
Lowest Priority
|
||||
data-content="Lowest priority. <span class='help'>Generates no notification on your device.</span>">
|
||||
Lowest priority
|
||||
</option>
|
||||
<option
|
||||
value="-1"
|
||||
data-content="Low Priority. <span class='help'>Sends a quiet notification.</span>">
|
||||
Low Priority
|
||||
data-content="Low priority. <span class='help'>Sends a quiet notification.</span>">
|
||||
Low priority
|
||||
</option>
|
||||
<option value="0" selected="selected">
|
||||
Normal Priority
|
||||
Normal priority
|
||||
</option>
|
||||
<option
|
||||
value="1"
|
||||
data-content="High Priority. <span class='help'>Bypasses user's quiet hours.</span>">
|
||||
High Priority
|
||||
data-content="High priority. <span class='help'>Bypasses user's quiet hours.</span>">
|
||||
High priority
|
||||
</option>
|
||||
<option
|
||||
value="2"
|
||||
data-content="Emergency Priority. <span class='help'>Repeated every {{po_retry_delay|hc_duration }} for at most {{ po_expiration|hc_duration }} until you acknowledge them.</span>">
|
||||
Emergency Priority
|
||||
data-content="Emergency priority. <span class='help'>Repeated every {{po_retry_delay|hc_duration }} for at most {{ po_expiration|hc_duration }} until you acknowledge them.</span>">
|
||||
Emergency priority
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
159
templates/integrations/ntfy_form.html
Normal file
159
templates/integrations/ntfy_form.html
Normal file
|
@ -0,0 +1,159 @@
|
|||
{% extends "base.html" %}
|
||||
{% load compress humanize static hc_extras %}
|
||||
|
||||
{% block title %}ntfy Integration for {{ site_name }}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>ntfy</h1>
|
||||
<div class="jumbotron">
|
||||
<p>
|
||||
<a href="https://ntfy.sh/">ntfy</a> is is a simple HTTP-based pub-sub
|
||||
notification service. If you use or plan on using ntfy,
|
||||
you can can integrate it with your {{ site_name }} account in few simple steps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Integration Settings</h2>
|
||||
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="form-group {{ form.topic.css_classes }}">
|
||||
<label for="topic" class="col-sm-3 control-label">Topic</label>
|
||||
<div class="col-sm-6">
|
||||
<input
|
||||
id="topic"
|
||||
name="topic"
|
||||
type="text"
|
||||
class="form-control"
|
||||
value="{{ form.topic.value|default:'' }}"
|
||||
required>
|
||||
{% if form.topic.errors %}
|
||||
<div class="help-block">{{ form.topic.errors|join:"" }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group {{ form.url.css_classes }}">
|
||||
<label for="url" class="col-sm-3 control-label">Server URL</label>
|
||||
<div class="col-sm-6">
|
||||
<input
|
||||
id="url"
|
||||
name="url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="https://"
|
||||
value="{{ form.url.value|default:'https://ntfy.sh' }}"
|
||||
required>
|
||||
|
||||
{% if form.url.errors %}
|
||||
<div class="help-block">{{ form.url.errors|join:"" }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Priority for "down" events</label>
|
||||
<div class="col-sm-8">
|
||||
<select name="priority" class="selectpicker form-control">
|
||||
<option
|
||||
value="5"
|
||||
{% if form.priority.value == 5 %}selected="selected"{% endif %}
|
||||
data-content="Max priority. <span class='help'>Really long vibration bursts, default notification sound with a pop-over notification.</span>">
|
||||
Max priority
|
||||
</option>
|
||||
<option
|
||||
value="4"
|
||||
{% if form.priority.value == 4 %}selected="selected"{% endif %}
|
||||
data-content="High priority. <span class='help'>Long vibration burst, default notification sound with a pop-over notification.</span>">
|
||||
High priority
|
||||
</option>
|
||||
<option value="3"
|
||||
{% if form.priority.value == 3 %}selected="selected"{% endif %}
|
||||
data-content="Default priority. <span class='help'>Short default vibration and sound. Default notification behavior.</span>">
|
||||
Default priority
|
||||
</option>
|
||||
<option
|
||||
value="2"
|
||||
{% if form.priority.value == 2 %}selected="selected"{% endif %}
|
||||
data-content="Low priority. <span class='help'>No vibration or sound. Notification will not visibly show up until notification drawer is pulled down.</span>">
|
||||
Low priority
|
||||
</option>
|
||||
<option
|
||||
value="1"
|
||||
{% if form.priority.value == 1 %}selected="selected"{% endif %}
|
||||
data-content="Min priority. <span class='help'>No vibration or sound. The notification will be under the fold in 'Other notifications'.</span>">
|
||||
Min priority
|
||||
</option>
|
||||
<option
|
||||
value="0"
|
||||
{% if form.priority.value == 0 %}selected="selected"{% endif %}
|
||||
data-content="Disabled. <span class='help'>Does not notify about Down events.</span>">
|
||||
Disabled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Priority for "up" events</label>
|
||||
<div class="col-sm-8">
|
||||
<select name="priority_up" class="selectpicker form-control">
|
||||
<option
|
||||
value="5"
|
||||
{% if form.priority_up.value == 5 %}selected="selected"{% endif %}
|
||||
data-content="Max priority. <span class='help'>Really long vibration bursts, default notification sound with a pop-over notification.</span>">
|
||||
Max priority
|
||||
</option>
|
||||
<option
|
||||
value="4"
|
||||
{% if form.priority_up.value == 4 %}selected="selected"{% endif %}
|
||||
data-content="High priority. <span class='help'>Long vibration burst, default notification sound with a pop-over notification.</span>">
|
||||
High priority
|
||||
</option>
|
||||
<option value="3"
|
||||
{% if form.priority_up.value == 3 %}selected="selected"{% endif %}
|
||||
data-content="Default priority. <span class='help'>Short default vibration and sound. Default notification behavior.</span>">
|
||||
Default priority
|
||||
</option>
|
||||
<option
|
||||
value="2"
|
||||
{% if form.priority_up.value == 2 %}selected="selected"{% endif %}
|
||||
data-content="Low priority. <span class='help'>No vibration or sound. Notification will not visibly show up until notification drawer is pulled down.</span>">
|
||||
Low priority
|
||||
</option>
|
||||
<option
|
||||
value="1"
|
||||
{% if form.priority_up.value == 1 %}selected="selected"{% endif %}
|
||||
data-content="Min priority. <span class='help'>No vibration or sound. The notification will be under the fold in 'Other notifications'.</span>">
|
||||
Min priority
|
||||
</option>
|
||||
<option
|
||||
value="0"
|
||||
{% if form.priority_up.value == 0 %}selected="selected"{% endif %}
|
||||
data-content="Disabled. <span class='help'>Does not notify about Up events.</span>">
|
||||
Disabled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap-select.min.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
21
templates/integrations/ntfy_message.html
Normal file
21
templates/integrations/ntfy_message.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% load humanize linemode %}{% linemode %}
|
||||
{% if check.status == "down" %}
|
||||
{% line %}The last ping was {{ check.last_ping|naturaltime }}.{% endline %}
|
||||
{% endif %}
|
||||
|
||||
{% if down_checks is not None %}
|
||||
{% line %}{% endline %}
|
||||
{% if down_checks %}
|
||||
{% if down_checks|length > 10 %}
|
||||
{% line %}{{ down_checks|length }} other checks are {% if check.status == "down" %}also{% else %}still{% endif %} down.{% endline %}
|
||||
{% else %}
|
||||
{% line %}The following checks are {% if check.status == "down" %}also{% else %}still{% endif %} down:{% endline %}
|
||||
{% for c in down_checks %}
|
||||
{% line %}- {{ c.name_then_code|safe }} (last ping: {{ c.last_ping|naturaltime }}){% endline %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% line %}All the other checks are up.{% endline %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endlinemode %}
|
1
templates/integrations/ntfy_title.html
Normal file
1
templates/integrations/ntfy_title.html
Normal file
|
@ -0,0 +1 @@
|
|||
{{ check.name_then_code|safe }} is {{ check.status|upper }}
|
Loading…
Add table
Reference in a new issue