From 481848a749fce0d92f89a491ebbf496974c129eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Tue, 18 Dec 2018 22:57:12 +0200 Subject: [PATCH] Add "/ping//start" API endpoint --- CHANGELOG.md | 1 + hc/api/management/commands/sendalerts.py | 10 +- hc/api/migrations/0046_auto_20181218_1245.py | 23 ++++ hc/api/models.py | 126 ++++++++++++------- hc/api/tests/test_check_alert_after.py | 30 +++++ hc/api/tests/test_check_model.py | 39 ++++-- hc/api/tests/test_list_checks.py | 23 ++-- hc/api/tests/test_pause.py | 15 +++ hc/api/tests/test_ping.py | 63 +++++++++- hc/api/urls.py | 4 +- hc/api/views.py | 5 +- hc/front/tests/test_pause.py | 24 ++-- hc/front/tests/test_ping_details.py | 24 +++- hc/front/views.py | 6 +- static/css/base.css | 10 +- static/css/icomoon.css | 27 ++-- static/fonts/icomoon.eot | Bin 9948 -> 10348 bytes static/fonts/icomoon.svg | 40 +++--- static/fonts/icomoon.ttf | Bin 9784 -> 10184 bytes static/fonts/icomoon.woff | Bin 9860 -> 10260 bytes templates/emails/summary-html.html | 2 + templates/front/details_events.html | 2 + templates/front/log.html | 2 + templates/front/log_status_text.html | 2 + templates/front/ping_details.html | 2 + 25 files changed, 348 insertions(+), 132 deletions(-) create mode 100644 hc/api/migrations/0046_auto_20181218_1245.py create mode 100644 hc/api/tests/test_check_alert_after.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4195dbe3..bc164f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Allow simultaneous access to checks from different teams - Add CORS support to API endpoints - Flip model, for tracking status changes of the Check objects +- Add "/ping//start" API endpoint ### Bug Fixes - Fix after-login redirects (the "?next=" query parameter) diff --git a/hc/api/management/commands/sendalerts.py b/hc/api/management/commands/sendalerts.py index 431d0357..d263044f 100644 --- a/hc/api/management/commands/sendalerts.py +++ b/hc/api/management/commands/sendalerts.py @@ -85,16 +85,16 @@ class Command(BaseCommand): # In PostgreSQL, add this index to run the below query efficiently: # CREATE INDEX api_check_up ON api_check (alert_after) WHERE status = 'up' - q = Check.objects.filter(alert_after__lt=now, status="up") + q = Check.objects.filter(alert_after__lt=now).exclude(status="down") # Sort by alert_after, to avoid unnecessary sorting by id: check = q.order_by("alert_after").first() if check is None: return False - q = Check.objects.filter(id=check.id, status="up") + old_status = check.status + q = Check.objects.filter(id=check.id, status=old_status) - current_status = check.get_status() - if current_status != "down": + if not check.is_down(): # It is not down yet. Update alert_after q.update(alert_after=check.get_alert_after()) return True @@ -107,7 +107,7 @@ class Command(BaseCommand): flip = Flip(owner=check) flip.created = check.get_alert_after() - flip.old_status = "up" + flip.old_status = old_status flip.new_status = "down" flip.save() diff --git a/hc/api/migrations/0046_auto_20181218_1245.py b/hc/api/migrations/0046_auto_20181218_1245.py new file mode 100644 index 00000000..33c359ae --- /dev/null +++ b/hc/api/migrations/0046_auto_20181218_1245.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.4 on 2018-12-18 12:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0045_flip'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='last_start', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='ping', + name='start', + field=models.NullBooleanField(default=False), + ), + ] diff --git a/hc/api/models.py b/hc/api/models.py index dd2100f7..73dfda84 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -25,6 +25,7 @@ STATUSES = ( ) DEFAULT_TIMEOUT = td(days=1) DEFAULT_GRACE = td(hours=1) +NEVER = datetime(3000, 1, 1, tzinfo=pytz.UTC) CHECK_KINDS = (("simple", "Simple"), ("cron", "Cron")) @@ -55,7 +56,9 @@ PO_PRIORITIES = { def isostring(dt): """Convert the datetime to ISO 8601 format with no microseconds. """ - return dt.replace(microsecond=0).isoformat() + + if dt: + return dt.replace(microsecond=0).isoformat() class Check(models.Model): @@ -73,6 +76,7 @@ class Check(models.Model): tz = models.CharField(max_length=36, default="UTC") n_pings = models.IntegerField(default=0) last_ping = models.DateTimeField(null=True, blank=True) + last_start = models.DateTimeField(null=True, blank=True) last_ping_was_fail = models.NullBooleanField(default=False) has_confirmation_link = models.BooleanField(default=False) alert_after = models.DateTimeField(null=True, blank=True, editable=False) @@ -110,34 +114,58 @@ class Check(models.Model): return errors def get_grace_start(self): - """ Return the datetime when grace period starts. """ + """ Return the datetime when the grace period starts. - # The common case, grace starts after timeout - if self.kind == "simple": - return self.last_ping + self.timeout + If the check is currently new, paused or down, return None. - # The complex case, next ping is expected based on cron schedule. - # Don't convert to naive datetimes (and so avoid ambiguities around - # DST transitions). - # croniter does handle timezone-aware datetimes. + """ - zone = pytz.timezone(self.tz) - last_local = timezone.localtime(self.last_ping, zone) - it = croniter(self.schedule, last_local) - return it.next(datetime) + # NEVER is a constant sentinel value (year 3000). + # Using None instead would make the logic clunky. + result = NEVER - def get_status(self, now=None): - """ Return "up" if the check is up or in grace, otherwise "down". """ + if self.kind == "simple" and self.status == "up": + result = self.last_ping + self.timeout + elif self.kind == "cron" and self.status == "up": + # The complex case, next ping is expected based on cron schedule. + # Don't convert to naive datetimes (and so avoid ambiguities around + # DST transitions). Croniter will handle the timezone-aware datetimes. - if self.status in ("new", "paused"): - return self.status + zone = pytz.timezone(self.tz) + last_local = timezone.localtime(self.last_ping, zone) + it = croniter(self.schedule, last_local) + result = it.next(datetime) - if self.last_ping_was_fail: - return "down" + if self.last_start: + result = min(result, self.last_start) + + if result != NEVER: + return result + + def is_down(self): + """ Return True if the check is currently in alert state. """ + + alert_after = self.get_alert_after() + if alert_after is None: + return False + + return timezone.now() >= self.get_alert_after() + + def get_status(self, now=None): + """ Return current status for display. """ if now is None: now = timezone.now() + if self.last_start: + if now >= self.last_start + self.grace: + return "down" + else: + return "started" + + if self.status in ("new", "paused", "down"): + return self.status + grace_start = self.get_grace_start() grace_end = grace_start + self.grace if now >= grace_end: @@ -151,12 +179,9 @@ class Check(models.Model): def get_alert_after(self): """ Return the datetime when check potentially goes down. """ - # For "fail" pings, sendalerts should the check right - # after receiving the ping, without waiting for the grace time: - if self.last_ping_was_fail: - return self.last_ping - - return self.get_grace_start() + self.grace + grace_start = self.get_grace_start() + if grace_start is not None: + return grace_start + self.grace def assign_all_channels(self): if self.user: @@ -183,7 +208,9 @@ class Check(models.Model): "grace": int(self.grace.total_seconds()), "n_pings": self.n_pings, "status": self.get_status(), - "channels": ",".join(sorted(channel_codes)) + "channels": ",".join(sorted(channel_codes)), + "last_ping": isostring(self.last_ping), + "next_ping": isostring(self.get_grace_start()) } if self.kind == "simple": @@ -192,38 +219,40 @@ class Check(models.Model): result["schedule"] = self.schedule result["tz"] = self.tz - if self.last_ping: - result["last_ping"] = isostring(self.last_ping) - result["next_ping"] = isostring(self.get_grace_start()) - else: - result["last_ping"] = None - result["next_ping"] = None - return result - def ping(self, remote_addr, scheme, method, ua, body, is_fail=False): - self.n_pings = models.F("n_pings") + 1 - self.last_ping = timezone.now() - self.last_ping_was_fail = is_fail - self.has_confirmation_link = "confirm" in str(body).lower() - self.alert_after = self.get_alert_after() + def ping(self, remote_addr, scheme, method, ua, body, action): + if action == "start": + # If we receive multiple start events in a row, + # we remember the first one, not the last one + if self.last_start is None: + self.last_start = timezone.now() + # DOn't update "last_ping" field. + else: + self.last_start = None + self.last_ping = timezone.now() + self.last_ping_was_fail = action == "fail" - new_status = "down" if is_fail else "up" - if self.status != new_status: - flip = Flip(owner=self) - flip.created = self.last_ping - flip.old_status = self.status - flip.new_status = new_status - flip.save() + new_status = "down" if action == "fail" else "up" + if self.status != new_status: + flip = Flip(owner=self) + flip.created = self.last_ping + flip.old_status = self.status + flip.new_status = new_status + flip.save() - self.status = new_status + self.status = new_status + self.alert_after = self.get_alert_after() + self.n_pings = models.F("n_pings") + 1 + self.has_confirmation_link = "confirm" in str(body).lower() self.save() self.refresh_from_db() ping = Ping(owner=self) ping.n = self.n_pings - ping.fail = is_fail + ping.start = action == "start" + ping.fail = action == "fail" ping.remote_addr = remote_addr ping.scheme = scheme ping.method = method @@ -238,6 +267,7 @@ class Ping(models.Model): n = models.IntegerField(null=True) owner = models.ForeignKey(Check, models.CASCADE) created = models.DateTimeField(auto_now_add=True) + start = models.NullBooleanField(default=False) fail = models.NullBooleanField(default=False) scheme = models.CharField(max_length=10, default="http") remote_addr = models.GenericIPAddressField(blank=True, null=True) diff --git a/hc/api/tests/test_check_alert_after.py b/hc/api/tests/test_check_alert_after.py new file mode 100644 index 00000000..a04e11fb --- /dev/null +++ b/hc/api/tests/test_check_alert_after.py @@ -0,0 +1,30 @@ +from datetime import timedelta as td + +from django.test import TestCase +from django.utils import timezone +from hc.api.models import Check + + +class CheckModelTestCase(TestCase): + + def test_it_handles_new_check(self): + check = Check() + self.assertEqual(check.get_alert_after(), None) + + def test_it_handles_paused_check(self): + check = Check() + check.last_ping = timezone.now() - td(days=2) + self.assertEqual(check.get_alert_after(), None) + + def test_it_handles_up(self): + check = Check(status="up") + check.last_ping = timezone.now() - td(hours=1) + expected_aa = check.last_ping + td(days=1, hours=1) + self.assertEqual(check.get_alert_after(), expected_aa) + + def test_it_handles_paused_then_started_check(self): + check = Check(status="paused") + check.last_start = timezone.now() - td(days=2) + + expected_aa = check.last_start + td(hours=1) + self.assertEqual(check.get_alert_after(), expected_aa) diff --git a/hc/api/tests/test_check_model.py b/hc/api/tests/test_check_model.py index 8c019528..f92bb336 100644 --- a/hc/api/tests/test_check_model.py +++ b/hc/api/tests/test_check_model.py @@ -27,7 +27,7 @@ class CheckModelTestCase(TestCase): self.assertEqual(check.get_status(), "grace") - def test_get_stauts_handles_paused_check(self): + def test_get_status_handles_paused_check(self): check = Check() check.status = "up" @@ -82,6 +82,33 @@ class CheckModelTestCase(TestCase): now = dt + timedelta(days=1, minutes=60) self.assertEqual(check.get_status(now), "down") + def test_get_status_handles_past_grace(self): + check = Check() + check.status = "up" + check.last_ping = timezone.now() - timedelta(days=2) + + self.assertEqual(check.get_status(), "down") + + def test_get_status_obeys_down_status(self): + check = Check() + check.status = "down" + check.last_ping = timezone.now() - timedelta(minutes=1) + + self.assertEqual(check.get_status(), "down") + + def test_get_status_handles_started(self): + check = Check() + check.last_ping = timezone.now() - timedelta(hours=2) + check.last_start = timezone.now() - timedelta(minutes=5) + for status in ("new", "paused", "up", "down"): + check.status = status + self.assertEqual(check.get_status(), "started") + + def test_get_status_handles_started_and_mia(self): + check = Check() + check.last_start = timezone.now() - timedelta(hours=2) + self.assertEqual(check.get_status(), "down") + def test_next_ping_with_cron_syntax(self): dt = timezone.make_aware(datetime(2000, 1, 1), timezone=timezone.utc) @@ -95,13 +122,3 @@ class CheckModelTestCase(TestCase): d = check.to_dict() self.assertEqual(d["next_ping"], "2000-01-01T01:00:00+00:00") - - def test_status_checks_the_fail_flag(self): - check = Check() - check.status = "up" - check.last_ping = timezone.now() - timedelta(minutes=5) - check.last_ping_was_fail = True - - # The computed status should be "down" because last_ping_was_fail - # is set. - self.assertEqual(check.get_status(), "down") diff --git a/hc/api/tests/test_list_checks.py b/hc/api/tests/test_list_checks.py index 0b289954..d2543d97 100644 --- a/hc/api/tests/test_list_checks.py +++ b/hc/api/tests/test_list_checks.py @@ -17,8 +17,7 @@ class ListChecksTestCase(BaseTestCase): self.a1 = Check(user=self.alice, name="Alice 1") self.a1.timeout = td(seconds=3600) self.a1.grace = td(seconds=900) - self.a1.last_ping = self.now - self.a1.n_pings = 1 + self.a1.n_pings = 0 self.a1.status = "new" self.a1.tags = "a1-tag a1-additional-tag" self.a1.save() @@ -45,19 +44,16 @@ class ListChecksTestCase(BaseTestCase): doc = r.json() self.assertEqual(len(doc["checks"]), 2) - a1 = None - a2 = None + by_name = {} for check in doc["checks"]: - if check["name"] == "Alice 1": - a1 = check - if check["name"] == "Alice 2": - a2 = check + by_name[check["name"]] = check + a1 = by_name["Alice 1"] self.assertEqual(a1["timeout"], 3600) self.assertEqual(a1["grace"], 900) self.assertEqual(a1["ping_url"], self.a1.url()) - self.assertEqual(a1["last_ping"], self.now.isoformat()) - self.assertEqual(a1["n_pings"], 1) + self.assertEqual(a1["last_ping"], None) + self.assertEqual(a1["n_pings"], 0) self.assertEqual(a1["status"], "new") self.assertEqual(a1["channels"], str(self.c1.code)) @@ -66,13 +62,16 @@ class ListChecksTestCase(BaseTestCase): self.assertEqual(a1["update_url"], update_url) self.assertEqual(a1["pause_url"], pause_url) - next_ping = self.now + td(seconds=3600) - self.assertEqual(a1["next_ping"], next_ping.isoformat()) + self.assertEqual(a1["next_ping"], None) + a2 = by_name["Alice 2"] self.assertEqual(a2["timeout"], 86400) self.assertEqual(a2["grace"], 3600) self.assertEqual(a2["ping_url"], self.a2.url()) self.assertEqual(a2["status"], "up") + next_ping = self.now + td(seconds=86400) + self.assertEqual(a2["last_ping"], self.now.isoformat()) + self.assertEqual(a2["next_ping"], next_ping.isoformat()) def test_it_handles_options(self): r = self.client.options("/api/v1/checks/") diff --git a/hc/api/tests/test_pause.py b/hc/api/tests/test_pause.py index e6c3d414..4a86518a 100644 --- a/hc/api/tests/test_pause.py +++ b/hc/api/tests/test_pause.py @@ -1,3 +1,4 @@ +from django.utils.timezone import now from hc.api.models import Check from hc.test import BaseTestCase @@ -55,3 +56,17 @@ class PauseTestCase(BaseTestCase): HTTP_X_API_KEY="X" * 32) self.assertEqual(r.status_code, 404) + + def test_it_clears_last_start(self): + check = Check(user=self.alice, status="up", last_start=now()) + check.save() + + url = "/api/v1/checks/%s/pause" % check.code + r = self.client.post(url, "", content_type="application/json", + HTTP_X_API_KEY="X" * 32) + + self.assertEqual(r.status_code, 200) + self.assertEqual(r["Access-Control-Allow-Origin"], "*") + + check.refresh_from_db() + self.assertEqual(check.last_start, None) diff --git a/hc/api/tests/test_ping.py b/hc/api/tests/test_ping.py index 0697ff73..a3cb45f5 100644 --- a/hc/api/tests/test_ping.py +++ b/hc/api/tests/test_ping.py @@ -1,7 +1,8 @@ +from datetime import timedelta as td + from django.test import Client, TestCase from django.utils.timezone import now - -from hc.api.models import Check, Ping +from hc.api.models import Check, Flip, Ping class PingTestCase(TestCase): @@ -31,6 +32,16 @@ class PingTestCase(TestCase): self.check.refresh_from_db() self.assertEqual(self.check.status, "up") + def test_it_clears_last_start(self): + self.check.last_start = now() + self.check.save() + + r = self.client.get("/ping/%s/" % self.check.code) + self.assertEqual(r.status_code, 200) + + self.check.refresh_from_db() + self.assertEqual(self.check.last_start, None) + def test_post_works(self): csrf_client = Client(enforce_csrf_checks=True) r = csrf_client.post("/ping/%s/" % self.check.code, "hello world", @@ -132,7 +143,51 @@ class PingTestCase(TestCase): self.check.refresh_from_db() self.assertTrue(self.check.last_ping_was_fail) - self.assertTrue(self.check.alert_after <= now()) + self.assertEqual(self.check.status, "down") + self.assertEqual(self.check.alert_after, None) - ping = Ping.objects.latest("id") + ping = Ping.objects.get() self.assertTrue(ping.fail) + + flip = Flip.objects.get() + self.assertEqual(flip.owner, self.check) + self.assertEqual(flip.new_status, "down") + + def test_start_endpoint_works(self): + last_ping = now() - td(hours=2) + self.check.last_ping = last_ping + self.check.save() + + r = self.client.get("/ping/%s/start" % self.check.code) + self.assertEqual(r.status_code, 200) + + self.check.refresh_from_db() + self.assertTrue(self.check.last_start) + self.assertEqual(self.check.last_ping, last_ping) + + ping = Ping.objects.get() + self.assertTrue(ping.start) + + def test_start_does_not_change_status_of_paused_check(self): + self.check.status = "paused" + self.check.save() + + r = self.client.get("/ping/%s/start" % self.check.code) + self.assertEqual(r.status_code, 200) + + self.check.refresh_from_db() + self.assertTrue(self.check.last_start) + self.assertEqual(self.check.status, "paused") + + def test_start_does_not_overwrite_last_start(self): + first_start = now() - td(hours=2) + + self.check.last_start = first_start + self.check.save() + + r = self.client.get("/ping/%s/start" % self.check.code) + self.assertEqual(r.status_code, 200) + + self.check.refresh_from_db() + # Should still be the original value + self.assertEqual(self.check.last_start, first_start) diff --git a/hc/api/urls.py b/hc/api/urls.py index 068a28e3..923042a8 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -5,8 +5,8 @@ from hc.api import views urlpatterns = [ path('ping//', views.ping, name="hc-ping-slash"), path('ping/', views.ping, name="hc-ping"), - path('ping//fail', views.ping, {"is_fail": True}, - name="hc-fail"), + path('ping//fail', views.ping, {"action": "fail"}, name="hc-fail"), + path('ping//start', views.ping, {"action": "start"}, name="hc-start"), path('api/v1/checks/', views.checks), path('api/v1/checks/', views.update, name="hc-api-update"), diff --git a/hc/api/views.py b/hc/api/views.py index bcc32cee..1a2a620e 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -19,7 +19,7 @@ from hc.lib.badges import check_signature, get_badge_svg @csrf_exempt @never_cache -def ping(request, code, is_fail=False): +def ping(request, code, action="success"): check = get_object_or_404(Check, code=code) headers = request.META @@ -30,7 +30,7 @@ def ping(request, code, is_fail=False): ua = headers.get("HTTP_USER_AGENT", "") body = request.body.decode() - check.ping(remote_addr, scheme, method, ua, body, is_fail) + check.ping(remote_addr, scheme, method, ua, body, action) response = HttpResponse("OK") response["Access-Control-Allow-Origin"] = "*" @@ -188,6 +188,7 @@ def pause(request, code): return HttpResponseForbidden() check.status = "paused" + check.last_start = None check.save() return JsonResponse(check.to_dict()) diff --git a/hc/front/tests/test_pause.py b/hc/front/tests/test_pause.py index 5c38023b..4c64a4b6 100644 --- a/hc/front/tests/test_pause.py +++ b/hc/front/tests/test_pause.py @@ -1,3 +1,4 @@ +from django.utils.timezone import now from hc.api.models import Check from hc.test import BaseTestCase @@ -9,28 +10,35 @@ class PauseTestCase(BaseTestCase): self.check = Check(user=self.alice, status="up") self.check.save() - def test_it_pauses(self): - url = "/checks/%s/pause/" % self.check.code + self.url = "/checks/%s/pause/" % self.check.code + def test_it_pauses(self): self.client.login(username="alice@example.org", password="password") - r = self.client.post(url) + r = self.client.post(self.url) self.assertRedirects(r, "/checks/") self.check.refresh_from_db() self.assertEqual(self.check.status, "paused") def test_it_rejects_get(self): - url = "/checks/%s/pause/" % self.check.code self.client.login(username="alice@example.org", password="password") - r = self.client.get(url) + r = self.client.get(self.url) self.assertEqual(r.status_code, 405) def test_it_allows_cross_team_access(self): self.bobs_profile.current_team = None self.bobs_profile.save() - url = "/checks/%s/pause/" % self.check.code - self.client.login(username="bob@example.org", password="password") - r = self.client.post(url) + r = self.client.post(self.url) self.assertRedirects(r, "/checks/") + + def test_it_clears_last_start(self): + self.check.last_start = now() + self.check.save() + + self.client.login(username="alice@example.org", password="password") + self.client.post(self.url) + + self.check.refresh_from_db() + self.assertEqual(self.check.last_start, None) diff --git a/hc/front/tests/test_ping_details.py b/hc/front/tests/test_ping_details.py index 17e86751..3c9108ad 100644 --- a/hc/front/tests/test_ping_details.py +++ b/hc/front/tests/test_ping_details.py @@ -14,6 +14,26 @@ class LastPingTestCase(BaseTestCase): r = self.client.get("/checks/%s/last_ping/" % check.code) self.assertContains(r, "this is body", status_code=200) + def test_it_shows_fail(self): + check = Check(user=self.alice) + check.save() + + Ping.objects.create(owner=check, fail=True) + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/checks/%s/last_ping/" % check.code) + self.assertContains(r, "/fail", status_code=200) + + def test_it_shows_start(self): + check = Check(user=self.alice) + check.save() + + Ping.objects.create(owner=check, start=True) + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/checks/%s/last_ping/" % check.code) + self.assertContains(r, "/start", status_code=200) + def test_it_requires_user(self): check = Check.objects.create() r = self.client.get("/checks/%s/last_ping/" % check.code) @@ -24,8 +44,8 @@ class LastPingTestCase(BaseTestCase): check.save() # remote_addr, scheme, method, ua, body: - check.ping("1.2.3.4", "http", "post", "tester", "foo-123") - check.ping("1.2.3.4", "http", "post", "tester", "bar-456") + check.ping("1.2.3.4", "http", "post", "tester", "foo-123", "success") + check.ping("1.2.3.4", "http", "post", "tester", "bar-456", "success") self.client.login(username="alice@example.org", password="password") diff --git a/hc/front/views.py b/hc/front/views.py index 71a7b442..1358197e 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -301,11 +301,10 @@ def update_timeout(request, code): check.alert_after = check.get_alert_after() # Changing timeout can change check's status: - is_up = check.get_status() in ("up", "grace") - if is_up and check.status != "up": + if not check.is_down() and check.status == "down": flip = Flip(owner=check) flip.created = timezone.now() - flip.old_status = check.status + flip.old_status = "down" flip.new_status = "up" flip.save() @@ -365,6 +364,7 @@ def pause(request, code): check = _get_check_for_user(request, code) check.status = "paused" + check.last_start = None check.save() if "/details/" in request.META.get("HTTP_REFERER", ""): diff --git a/static/css/base.css b/static/css/base.css index 932c5899..3cc3cadf 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -63,11 +63,17 @@ body { width: 24px; } -.status.icon-up { color: #5cb85c; } +.status.icon-up, .status.icon-started { color: #5cb85c; } .status.icon-new, .status.icon-paused { color: #CCC; } .status.icon-grace { color: #f0ad4e; } .status.icon-down { color: #d9534f; } +.label-start { + background-color: #FFF; + color: #117a3f;; + border: 1px solid #117a3f; +} + .hc-dialog { background: #FFF; padding: 2em; @@ -99,4 +105,4 @@ pre { text-align: center; font-size: small; padding: 2px 0; -} \ No newline at end of file +} diff --git a/static/css/icomoon.css b/static/css/icomoon.css index 37163a2c..a2cb3377 100644 --- a/static/css/icomoon.css +++ b/static/css/icomoon.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('../fonts/icomoon.eot?b4dy0b'); - src: url('../fonts/icomoon.eot?b4dy0b#iefix') format('embedded-opentype'), - url('../fonts/icomoon.ttf?b4dy0b') format('truetype'), - url('../fonts/icomoon.woff?b4dy0b') format('woff'), - url('../fonts/icomoon.svg?b4dy0b#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?swifyd'); + src: url('../fonts/icomoon.eot?swifyd#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?swifyd') format('truetype'), + url('../fonts/icomoon.woff?swifyd') format('woff'), + url('../fonts/icomoon.svg?swifyd#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -24,6 +24,9 @@ -moz-osx-font-smoothing: grayscale; } +.icon-started:before { + content: "\e038"; +} .icon-pagertree:before { content: "\e90d"; color: #33ade2; @@ -80,6 +83,10 @@ content: "\e911"; color: #0079bf; } +.icon-whatsapp:before { + content: "\e902"; + color: #25d366; +} .icon-zendesk:before { content: "\e907"; } @@ -92,9 +99,6 @@ .icon-up:before, .icon-new:before, .icon-ok:before { content: "\e86c"; } -.icon-close:before { - content: "\e5cd"; -} .icon-grace:before { content: "\e000"; } @@ -107,9 +111,6 @@ .icon-asc:before { content: "\e316"; } -.icon-mail:before { - content: "\e0e1"; -} .icon-dots:before { content: "\e5d3"; } @@ -120,8 +121,8 @@ content: "\e7f6"; } .icon-settings:before { - content: "\e902"; + content: "\e912"; } .icon-delete:before { - content: "\e900"; + content: "\e913"; } diff --git a/static/fonts/icomoon.eot b/static/fonts/icomoon.eot index bf911d45d81639d0a5d20c408690eb60e70761b2..7e5fcff0ab577607f716e95301485c3f24314f91 100644 GIT binary patch delta 3971 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 delta 3642 zcma)932Yo!8Giqp-Dbpnq7PCwfFF@lXYS@brQ!RPSOOYkfU*%LTH^s zC4!U!n?kr0M4L`Q6bgu_2#5*-0;nJkqe4XmRH)E$Ra{jC6sVw_rOldu972N-5>GSp z{^MQ$@xJ%wcRQZgCO+X4GU1;UTl`Pn(k-5yer>l*2>BAe2X8pCc>K04C$|v-Ddg{c z<>IT4V;w{O0j#B0F5UWyQ|Yr2Jx-R4e{f_KG+OKa36FQPoW@%%i@SifDHT`e*TLmttZ{59)9XOPkrxKfBW^) zZ;X|TD=T3?)RO`>d&cXlhes{$%(k@o%3`Y6@s1Rz6-nPn_w?T z7RXDv^BfRL81ZmX#Vi;?(SW+^7`B+t=KOJ^*=kS%3m13O`+Mhm=jr{qdM$r(cld?J z9`6x2|NZax7U)7V*CY8xBTq@LNrFlKu?CRG$?e<_eT;-&WgaWBQoGdl+T74H&rDu^ z=slMy6kUi3*U@0AxIw0bto)KE+y{f#uuc5td31D;a;nUt;)UUY z0Wu4H$(`bsgL&n?%ru=|hUug~zIE%kpS*~apFBmU{bWca{lJLym59pyg?j_B$ScSf z$iswKIT0P^cED+8*8}>@&u6_f1Rsfvv4-d(=><WC1TiWrjv9%Hq9_PL zR8}n}FjI-dqY+z{G=UdTp)0bdswTF<9@t2;R~spwrrNTs2?GEBjMV-AhT#wacmf$SkCFb%wRiH!@)b43<~QqCX6#8{>CRX{olSeN|P6qK%{ z{oo<(v)$jGuD-bnZ+17@Wd;;1V`gNdQ(_drifwdJV#knmPrIuUB)u8@Fw5Ck4ZASg z+34cuQkzQzJazh-UE4UWTxvA#SX?~)pIm4_kz}|RxWi~b@LY7$=w{1-GTd{~iz>HX z*ZZ|;mFN$~%F*6AYgnCy-4R(uvk8yyB-%X_9Mxa8d6IT6exp{YOk1XDO;;-S(#{oJ zZPib(@-2Rb`y6SJ*fc7DKin=W?kF;-{YR^OJ3XQTqVWW)yI;aojfwX@u(ivVhQ8Xh&7RG z*T!aD$8|T4j@HM=#y-d+#k(`v4R5^jPjpbp#M z`H9=qb8uuLc6*5ZRM(!pb zCZ8nFg9HZXeeiMkJUj^Bfgi)K;19t+yn|u~yhhy(4#qvB(e|8Gr#I@YHpa9TygfdM z$H8afJ~V?L#Y?~fNNkTaT-@Y7#<3splx$$_^!K{Yt;iU}uZ)-X+G^3E1loi>Q z3{!9z=M~oA1hN?6!8A-fPaH$rHu6r8Wvd{`Jl-8PRXsd!G*{(iM=%Ye!`lfZ z;>tI|$wJ#_0PDzLLJ?W_3?zX-VIg+IMJTE?JoMGPZX8_PNd1 zT(8*~Z?=$}n>d6@eQBw)N4=SdmuCr*1J~0k1aBO8cWV-_XACiup3}@_B{Z=&}AB|Ii+Jm-{9=5HaWI~LH zT|XL^qlPG`0;e$1RIRexX?V>{a!^%dgkp_Zifa^YOnP}>OT{V243F=9$jqcfDv8)Z zP3ftc6XQHZPe&sf&ms(`m6Ap{zlezo$&ZL)LRO&UrZknu8>Pe2iYZYuGc+-hDPyX> zqi-;x*t~>^ENG*dLQ|m}FN?OK4EE{aFU!9|8=T7@gyn!M3{?=d!&2yKH%AX8>#t2b}P15A$_-pqx z>46Em;V9e(f29ZMz4R;GEcac0gRmqP#Dn5Fc1{wdZ_7Vb?$Q>bGtmcPua7;1e^&U9 M2Y - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/fonts/icomoon.ttf b/static/fonts/icomoon.ttf index 4a702973f8fd39558ee0606b2d7ee4d37e36b02f..d1d8551d67c27d784f299f5500e259830c2ef30b 100644 GIT binary patch delta 4065 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 XyHEHG`tR^z4h6%j)w&Y&J%#=c$-%V9 delta 3563 zcma(UYiwK9`F!Wxx9|1!efipsW5;&3HJU3B+#wwV`I;Ju)6vT#LKs9x|#3aUG6^sqEvGEer+2^z!-48Ub?!D)G zobR0PJKuM{@0_Ps9=~9k03pOj4iHAVSFY_U>86lF%47I#-L`9D>e^-dE+hn^h~It1 z#5Ggc1`t1vt#rlY{>u-<4*L+FAtZ6*j>{&tTZ0Q9A|!PI@+&)#APHO^@q>uB?%1_= z-~MOB*ARaI@q?3lw)Ib}UAN(CNp%W%tCc%kCJHKSlf&oaEb6 zd#>4wisF+A9X`Gkj9Z~LaRAXMD#eJ3#&@V)XHGqT>JM*y^!DUmjoH() zvkfWalK*#~^qxGZo!DOkJI=!y(hZkR} z6FB<93-xh2-jl48RJoj@B-ul}+u4g{ATN>YSv!3Zy+S~A#jKdEW~*+MwZHe?z`2*+ zaSlS#u}N+V_3mn2C{sda-{1)Qp?6YP67KEpDfU#V#Zq^%6m(-yF3y8;2?<_N+&1cQ zXm5ILSE0CJ^{RcTWHPmH)#?o!SFPTcOz}KTm72})vX&T07*l|{dB?7JxgWU;Zl>;Rkbj>`ANhw1PP3`a7(%a`|NBBv3`L=Mp5Or!xsGTttw zDN9uL0lSV^=xDd74AYv5E9gMdqzNhMHe4}9IJve3}qMcoIXTFG=l0kQ?? zMP%+RC(iV1`G5Vq)xH>w@AsyBKcBvCMPL2u>WWuh5tmlsroI)w0iGB9vf`+|fFKAw z&--Q75_!>7d?CNjmL-kncx32`?5e7XeNX~BY4%(v#nn_>mNlN|zRXC|Uv4Pw#EJU* z8>gN-dF^`eH6b0Sc%jFHa8PFAD3qE;K|$ z+gTjM->Y=^hTzpQJicKWx)e16z6O=~!xznj+1!-{i@0<+I@ zBkU1xg&yK6r6KTk>feNt_pW|yni~Cks(QpMj%#YA+;IVh@Q=HuM!MG~TP#aflyE4S zT$yjr398t+GU-OC(Dk}2mJMo3WA=H{)#LrWG6 z4Q`9YKJCwDYJIs}f5WBxf8(}7Hj!!KJ!FM1sgx2erN+j_*LE@11jpZqSbP)7R6C?H zDXX_!DV^En)k@F&#Vg@_Px_XT+E@N&{gX%J^%xUnBNi^htW+rCfXXi`IhcZ-`RS5Y)UIT3xLSE~iml@8D2R;ABqFu#3Pc zvY;y+x*kQ<=2xq$-B4DCUnfFtGU zpvUVCWzj{OLMsF1VC9ThGhnSN2ffEattv)B9 zm`xO>B|hMCe9YGL%g_nYYN}}XqN*EKcuS@Vr}$vi{JhCwz|eP5FG-{#$Dia>ky6Y~ z5rz){lkm^g%dBZ+({iv4!v=6nHSE*%U}l9V3qWILz$a6Yp%5s7%9(o1jPS~|s8wjE z8KbON+}mBG5*47G!#L*{P`aI08P(}}P7-;JtM6bv6Su*vsT*$8kU#3L1<;Jv@PMqy zwq%&RBQm$OA19Cn9|xvkVl8kC?ZVC*L6)snN#^hnu%zH(tLj4 zphTE`ntjZEMTRjt2{g){D2UFP*0J4~_iWU{VEa=vgaP((^b_e(P__2vSgW_EIXPYJ z>DkiWmWpHES7dk+MNQQg(e=1t@*Xvf`a9rLl#s3`OfzNr97QoCQN^G@ z5lhqJmemoGRGvpa@f0|aWGUvj?Y5oL^^h#fsOh;{-9(n7rpbn;-kq`UubIiU9op2$ z=bH|-<<5uKT&1qBO-||?qZ_yPjYV2V-MFn?x^isknoBi1?vA!b#`?A+Q=goytyixi z!nrBDuf;E=1@K;t|G*r;Bb11{QpCd%Hr=J7cYRCHKL}+^wDGf<^Vt0@1(^QABA}*K zER7oDRlh$UmRnM~K)H%f)I`~mL?<9wJ|pXJ)v+csh6V>Bs=*5&NwVwn1!YbI#xve0 zin?ex-B&hSg;c03M0skr*+#6xw%Q|M!6!^-{2|$I2)xQOMHEcc$^~m>wiR4ypszUE?57N8IKB75>P;m>d~STV6LLa{638neIiaPMU2M$0!AQ2 z?iYlxtUxvx)zn5RaKxBmO4N+E_jSf|7JCt*nPT35WaN1=?91zSe!WBLV8S}s4fnx^bSu4^KF&tj@41Eiq|hpC s6^@8UBtd#keoeVm8~2a+j|aXHcxvu4mwyvqPcO{OAdH}tY@~$G;`f7hvHcL?F7#S!l_Mut z&LD)3!N8Zp`t5%`Qu)Nn*-&-``r@@3{lx0scOXPKg#4bc9%!uf9J_txC_?!X=nRF` zdhDQc41^#%41Lf;JtE_($4=gL4rJ@l@6@}0Ac(7{kAU7pA$-A9SdTpN$yZOVoP!q^ z_CUY8Q=9mmQ!6KL2iYnt=!;?flR4=(XHK8J3pVopg)0l#3ijZ!gJ2&eP!v%l!*07@ z!>h2F(ATrA`S9mB5(?XWx4p1|R6r~utHsD1_eIR+Yz?Rwz~ zul+T*6vIi>&lT!eXhVCj)tNld+)~`qckJ*T%*Ub4V!`jcl7;qY9_%$briKPvozWtB zTb>prov|){jcJlRoKNL>#&}-lc%D*1NS7!^7_TwG5~ZFxPmJhDvDokVx>q*LaZx0& zY57e&a;gz|yefBRv$?bL^S4w>d1i=uQmElFD-$&m<-&JU?Sulu2@ znaay@sdV4c^6~3GKeF+`Oge)T&|h9UKG0|!ELXzog9Py5+w>lKF?ddTGE*g!m+@qx zFt~g7U?FjJGMUdO?;(@j0o0;sac|1&9gkWw-RY|3AIpkdnq<4L+wlvvzEuy>VxI z;gwe~n{DF<+Y8_08L!Kdqsj&goyv4cH3eQU6*;CWwj^pi92(CelI&_&F&Pthp4TPK zhL#~Ko~GJ>4xZ!j?_p`j?`ewu=tmpx9sJY_FMjp_*28oD=&l4mS6$z)VZgWNhSy*V z>+tN{mhHH>B@2q$->tp8Uc0x3?{%l0qChaVqSLi0rv&%Wj!wIwz<>dF9h!DDY=wG) zZHHRWu$*ZJ{-EjI>k78xlo)>F-WzY6r(Cf#(EQTM%KG|^`{yYw6$b{w-qu3%NTOHh zjfh9Viat|snl3KlN3MFh^qJc>eq!21@=AY6-}r{rtM0-#sgf2>SvdYvn-2 zVtlnbo3GYui-kfmr7Dtb`_Z&fV?(vi^tEWSeQMZrIbk&4Gp=b?%4?;>sJt;Sx|H^! zqTo7{aEk_;eYIVQgzVMa>L;%s8`HF72QbEplcU+}q3#UL3Q_5|yo!>j7fl6+jT#;Z z9yG>Rnt$FCH_kT=qlwQn z4cFV**H^Fg_RYAiVc=&BBRt`c5QLiOLG-ntWIl@h;Ctq$){4K|@HZ2QkByG)5ToMt zr{bQKI5|IaQrER;+p;{f^O)>ES)_bDLq1 zC>C0STahb@Yc2&7VMjj(&qhY+#uBZTuYm2mAtl75_W_S#Ya&n0W&kJc@(Mo@cZNye*G} z{tXE}c+)GxJuqVh+#ig@ih$FV*h18U;h_Tfr3(wiw$}_U$9mL;j;pvBrU!*w@J_5R zNXK0@Ul?S2#bS*D~qC*!1*;xm{DOzTWA0ToVO)9qs@8Bk4I^GkIQ6 zg`^+}o*V(s_L?bzMLxRbax7zOIz9?GNDb#2Q8lT037NMfBJ+}p7q(R`3P2$LK`15y zqm1KT;#3IR;NdvsC`gG2sG}P#nw(mmvrNOr#5UE09ErI31yN*}q|K-z5s?7HD@^4K zpsfTa3tD>(_nB!z8;!df{V>jOnFpxCw#n_h0yrQCcoE_nw{eVXtpsG#j2K?ZkbgNm zX262;@iAGFZNV^jN8sF^5s*nt;jkG6a>tIL-B5b~i?Y=tiJT&r}S6BX)h@K1o!ylJ3gq ziiO2`t=Vq1ZmX32;A~g+j_(QrgnA_riF8NpV#xH-q%6xnBc`$O7EvTQ9?`R=<(rBv z%cdYgro{^avsJCzvZ^sr<6tqACy?Svl9+bfif#8qA~8t?UVqaGE_A#4P_qp@mkBd!pwdgv zc0&^VJHv>Y*JLTKQC`#aN?gwRIwRbW!Zbm$M8S!Qwqh1sZg?(XrY+cIQjPMwBucKL zx*{)N!gHJ=2vNas`cHJ3^<1nTBRsLowvq0&?Q$~46joS^c#;mumcmn65De8Sx#KOb zl}VIUStOjM36|_e^A0Case4z|DH_oj-FV#0q!@UenW~zKq#CY4Vsa#{DLThgFeZp` zlQ6;<#))q+mXH)&a&S^p078J;F2xjynW+wE%6$N~`;ui@u{dxYr`Iz*Ed|yt@wOtD zl9A~DiI#!xAI2Ah(|+aBqhb^X)5260cs4A*e*M!IZi|fn0x4nkg3zmJW#xaL@M$vz zH2jbLLFZ{059@(zwUg}x<2~a`AUF#3<<5K=+3?MFEBX$42Xi=vZ^h^FkI5eLN7Sb8 zb9eCP*(_UU&kN6sz2bW^lmAvbr{AIfOZ2hmo5mUAA^5IDD2sm%OdJ*h--Xttf9v@c F{vV5BrrZDk delta 3661 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 H{vP-@DlC#` diff --git a/templates/emails/summary-html.html b/templates/emails/summary-html.html index 205aade6..3218186c 100644 --- a/templates/emails/summary-html.html +++ b/templates/emails/summary-html.html @@ -21,6 +21,8 @@ LATE {% elif check.get_status == "up" %} UP + {% elif check.get_status == "started" %} + STARTED {% elif check.get_status == "down" %} DOWN {% endif %} diff --git a/templates/front/details_events.html b/templates/front/details_events.html index 1dc9e3c9..c1e9f0f9 100644 --- a/templates/front/details_events.html +++ b/templates/front/details_events.html @@ -12,6 +12,8 @@ {% if event.fail %} Failure + {% elif event.start %} + Started {% else %} OK {% endif %} diff --git a/templates/front/log.html b/templates/front/log.html index 217befbf..5adb629a 100644 --- a/templates/front/log.html +++ b/templates/front/log.html @@ -46,6 +46,8 @@ {% if event.fail %} Failure + {% elif event.start %} + Started {% else %} OK {% endif %} diff --git a/templates/front/log_status_text.html b/templates/front/log_status_text.html index 7ecc1385..4b9c31b6 100644 --- a/templates/front/log_status_text.html +++ b/templates/front/log_status_text.html @@ -10,5 +10,7 @@ This check is paused. {% elif status == "new" %} This check has never received a ping. + {% elif status == "started" %} + This check is currently running. Started {{ check.last_start|naturaltime }}. {% endif %} {% endwith %} \ No newline at end of file diff --git a/templates/front/ping_details.html b/templates/front/ping_details.html index 22cededa..d273657c 100644 --- a/templates/front/ping_details.html +++ b/templates/front/ping_details.html @@ -2,6 +2,8 @@

Ping #{{ ping.n }} {% if ping.fail %} (received via the /fail endpoint) + {% elif ping.start %} + (received via the /start endpoint) {% endif %}