From 27c065230ac72f50f3cdf584cb4baed6b058af13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?= <cuu508@gmail.com>
Date: Tue, 9 Apr 2024 14:24:43 +0300
Subject: [PATCH] Switch back to using integer timestamps in the log page

The live-updating code still needs float timestamps, but
we only need them for the most recent event (so we know
the lower threshold for fetching new events). We now send
the float timestamp separately:

* in the `/log/` view, we put it in HTML content, in a <script> tag
* in the `/log_events/` view we put it in response header

The main benefit of this is smaller response sizes for the
`/log/` and `/log_events/` views.
---
 hc/front/templatetags/hc_extras.py |  4 ++--
 hc/front/tests/test_log.py         | 38 ------------------------------
 hc/front/tests/test_log_events.py  | 38 ++++++++++++++++++++++++++++++
 hc/front/views.py                  | 21 ++++++++++++++---
 static/js/log.js                   |  9 +++----
 templates/front/log.html           |  7 +++---
 6 files changed, 67 insertions(+), 50 deletions(-)

diff --git a/hc/front/templatetags/hc_extras.py b/hc/front/templatetags/hc_extras.py
index 993434c8..f9df031c 100644
--- a/hc/front/templatetags/hc_extras.py
+++ b/hc/front/templatetags/hc_extras.py
@@ -185,8 +185,8 @@ def now_isoformat() -> str:
 
 
 @register.filter
-def timestamp(dt: datetime) -> float:
-    return dt.timestamp()
+def timestamp(dt: datetime) -> int:
+    return int(dt.timestamp())
 
 
 @register.filter
diff --git a/hc/front/tests/test_log.py b/hc/front/tests/test_log.py
index b3f0d9d2..eead76eb 100644
--- a/hc/front/tests/test_log.py
+++ b/hc/front/tests/test_log.py
@@ -90,44 +90,6 @@ class LogTestCase(BaseTestCase):
         r = self.client.get(self.url)
         self.assertEqual(r.status_code, 404)
 
-    def test_it_shows_email_notification(self) -> None:
-        ch = Channel(kind="email", project=self.project)
-        ch.value = json.dumps({"value": "alice@example.org", "up": True, "down": True})
-        ch.save()
-
-        Notification(owner=self.check, channel=ch, check_status="down").save()
-
-        self.client.login(username="alice@example.org", password="password")
-        r = self.client.get(self.url)
-        self.assertContains(r, "Sent email to alice@example.org", status_code=200)
-
-    def test_it_shows_pushover_notification(self) -> None:
-        ch = Channel.objects.create(kind="po", project=self.project)
-
-        Notification(owner=self.check, channel=ch, check_status="down").save()
-
-        self.client.login(username="alice@example.org", password="password")
-        r = self.client.get(self.url)
-        self.assertContains(r, "Sent a Pushover notification", status_code=200)
-
-    def test_it_shows_webhook_notification(self) -> None:
-        ch = Channel(kind="webhook", project=self.project)
-        ch.value = json.dumps(
-            {
-                "method_down": "GET",
-                "url_down": "foo/$NAME",
-                "body_down": "",
-                "headers_down": {},
-            }
-        )
-        ch.save()
-
-        Notification(owner=self.check, channel=ch, check_status="down").save()
-
-        self.client.login(username="alice@example.org", password="password")
-        r = self.client.get(self.url)
-        self.assertContains(r, "Called webhook foo/$NAME", status_code=200)
-
     def test_it_shows_ignored_nonzero_exitstatus(self) -> None:
         self.ping.kind = "ign"
         self.ping.exitstatus = 123
diff --git a/hc/front/tests/test_log_events.py b/hc/front/tests/test_log_events.py
index 354a25f4..96deace1 100644
--- a/hc/front/tests/test_log_events.py
+++ b/hc/front/tests/test_log_events.py
@@ -141,3 +141,41 @@ class LogTestCase(BaseTestCase):
         r = self.client.get(self.url(flip=False))
         self.assertContains(r, "hello world")
         self.assertNotContains(r, "new ➔ down")
+
+    def test_it_shows_email_notification(self) -> None:
+        ch = Channel(kind="email", project=self.project)
+        ch.value = json.dumps({"value": "alice@example.org", "up": True, "down": True})
+        ch.save()
+
+        Notification(owner=self.check, channel=ch, check_status="down").save()
+
+        self.client.login(username="alice@example.org", password="password")
+        r = self.client.get(self.url())
+        self.assertContains(r, "Sent email to alice@example.org", status_code=200)
+
+    def test_it_shows_pushover_notification(self) -> None:
+        ch = Channel.objects.create(kind="po", project=self.project)
+
+        Notification(owner=self.check, channel=ch, check_status="down").save()
+
+        self.client.login(username="alice@example.org", password="password")
+        r = self.client.get(self.url())
+        self.assertContains(r, "Sent a Pushover notification", status_code=200)
+
+    def test_it_shows_webhook_notification(self) -> None:
+        ch = Channel(kind="webhook", project=self.project)
+        ch.value = json.dumps(
+            {
+                "method_down": "GET",
+                "url_down": "foo/$NAME",
+                "body_down": "",
+                "headers_down": {},
+            }
+        )
+        ch.save()
+
+        Notification(owner=self.check, channel=ch, check_status="down").save()
+
+        self.client.login(username="alice@example.org", password="password")
+        r = self.client.get(self.url())
+        self.assertContains(r, "Called webhook foo/$NAME", status_code=200)
diff --git a/hc/front/views.py b/hc/front/views.py
index 0483300a..62504e34 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -920,16 +920,23 @@ def log(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
     if oldest_ping:
         smin = max(smin, oldest_ping.created)
 
+    events = _get_events(check, 1000, start=smin, end=smax)
     ctx = {
         "page": "log",
         "project": check.project,
         "check": check,
         "min": smin,
         "max": smax,
-        "events": _get_events(check, 1000, start=smin, end=smax),
+        "events": events,
         "oldest_ping": oldest_ping,
     }
 
+    if events:
+        # A full precision timestamp of the most recent event.
+        # This will be used client-side for fetching live updates to specify
+        # "return any events after *this* point".
+        ctx["last_event_timestamp"] = events[0].created.timestamp()
+
     return render(request, "front/log.html", ctx)
 
 
@@ -2750,11 +2757,19 @@ def log_events(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
     if oldest_ping:
         start = max(start, oldest_ping.created)
 
+    events = _get_events(check, 1000, start=start, end=end, kinds=form.kinds())
     ctx = {
-        "events": _get_events(check, 1000, start=start, end=end, kinds=form.kinds()),
+        "events": events,
         "describe_body": True,
     }
-    return render(request, "front/log_rows.html", ctx)
+    response = render(request, "front/log_rows.html", ctx)
+
+    if events:
+        # Include a full precision timestamp of the most recent event in a
+        # response header. This will be used client-side for fetching live updates
+        # to specify "return any events after *this* point".
+        response["X-Last-Event-Timestamp"] = str(events[0].created.timestamp())
+    return response
 
 
 # Forks: add custom views after this line
diff --git a/static/js/log.js b/static/js/log.js
index 228fa112..a0f1baa3 100644
--- a/static/js/log.js
+++ b/static/js/log.js
@@ -87,6 +87,7 @@ $(function () {
     // Once it's ready, set it to visible:
     $("#log").css("visibility", "visible");
 
+    var lastUpdated = document.getElementById("last-event-timestamp").textContent;
     function fetchNewEvents() {
         // Do not fetch updates if the slider is not set to "now"
         // or there's an AJAX request in flight
@@ -97,19 +98,19 @@ $(function () {
         var url = document.getElementById("log").dataset.refreshUrl;
         var qs = $("#filters").serialize();
 
-        var firstRow = $("#log tr").get(0);
-        if (firstRow) {
-            qs += "&u=" + firstRow.dataset.dt;
+        if (lastUpdated) {
+            qs += "&u=" + lastUpdated;
         }
 
         activeRequest = $.ajax({
             url: url + "?" + qs,
             timeout: 2000,
-            success: function(data) {
+            success: function(data, textStatus, xhr) {
                 activeRequest = null;
                 if (!data)
                     return;
 
+                lastUpdated = xhr.getResponseHeader("X-Last-Event-Timestamp");
                 var tbody = document.createElement("tbody");
                 tbody.setAttribute("class", "new");
                 tbody.innerHTML = data;
diff --git a/templates/front/log.html b/templates/front/log.html
index 47cc5bee..770e1189 100644
--- a/templates/front/log.html
+++ b/templates/front/log.html
@@ -71,9 +71,9 @@
             id="end"
             type="range"
             name="end"
-            min="{{ min|timestamp|floatformat:"0" }}"
-            max="{{ max|timestamp|floatformat:"0" }}"
-            value="{{ max|timestamp|floatformat:"0" }}"
+            min="{{ min|timestamp }}"
+            max="{{ max|timestamp }}"
+            value="{{ max|timestamp }}"
             autocomplete="off">
 
         <p id="end-help" class="text-muted">
@@ -123,6 +123,7 @@
 {% endblock %}
 
 {% block scripts %}
+<script id="last-event-timestamp" type="data">{{ last_event_timestamp }}</script>
 {% compress js %}
 <script src="{% static 'js/moment.min.js' %}"></script>
 <script src="{% static 'js/moment-timezone-with-data-10-year-range.min.js' %}"></script>