Browse Source

Add ntfy integration

Fixes: #728
pull/761/head
Pēteris Caune 2 months ago
parent
commit
3dcc7d60a2
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
  1. 1
      CHANGELOG.md
  2. 42
      hc/api/models.py
  3. 100
      hc/api/tests/test_notify_ntfy.py
  4. 30
      hc/api/transports.py
  5. 11
      hc/front/forms.py
  6. 72
      hc/front/tests/test_add_ntfy.py
  7. 61
      hc/front/tests/test_edit_ntfy.py
  8. 1
      hc/front/urls.py
  9. 39
      hc/front/views.py
  10. 3
      static/css/add_pushover.css
  11. 5
      static/css/base.css
  12. 4
      static/css/channels.css
  13. 15
      static/css/icomoon.css
  14. 5
      static/fonts/icomoon.svg
  15. BIN
      static/fonts/icomoon.ttf
  16. BIN
      static/fonts/icomoon.woff
  17. BIN
      static/img/integrations/ntfy.png
  18. 1
      templates/base.html
  19. 14
      templates/front/channels.html
  20. 26
      templates/integrations/add_pushover.html
  21. 159
      templates/integrations/ntfy_form.html
  22. 21
      templates/integrations/ntfy_message.html
  23. 1
      templates/integrations/ntfy_title.html

1
CHANGELOG.md

@ -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

42
hc/api/models.py

@ -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

@ -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"])

30
hc/api/transports.py

@ -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)

11
hc/front/forms.py

@ -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

@ -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

@ -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)

1
hc/front/urls.py

@ -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"),

39
hc/front/views.py

@ -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:

3
static/css/add_pushover.css

@ -1,3 +0,0 @@
#add-pushover .help {
opacity: 0.6;
}

5
static/css/base.css

@ -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;
}

4
static/css/channels.css

@ -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));
}

15
static/css/icomoon.css

@ -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";

5
static/fonts/icomoon.svg
File diff suppressed because it is too large
View File

BIN
static/fonts/icomoon.ttf

BIN
static/fonts/icomoon.woff

BIN
static/img/integrations/ntfy.png

After

Width: 128  |  Height: 112  |  Size: 2.6 KiB

1
templates/base.html

@ -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">

14
templates/front/channels.html

@ -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' %}"

26
templates/integrations/add_pushover.html

@ -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

@ -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

@ -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

@ -0,0 +1 @@
{{ check.name_then_code|safe }} is {{ check.status|upper }}
Loading…
Cancel
Save