From 0c2ce9431062caa1a6b5b43a205e8c3e6a6ee8fc Mon Sep 17 00:00:00 2001 From: Hanne Moa Date: Thu, 18 Jun 2026 13:25:22 +0200 Subject: [PATCH 1/2] Store heartbeat frequency in a DurationField This means easier database queries but trickier frontend display. --- changelog.d/1941.added.md | 5 +++-- .../0003_sourcesystem_heartbeat_frequency.py | 5 +++-- src/argus/incident/models.py | 15 ++++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/changelog.d/1941.added.md b/changelog.d/1941.added.md index 72e583006..7fea5d07d 100644 --- a/changelog.d/1941.added.md +++ b/changelog.d/1941.added.md @@ -1,2 +1,3 @@ -Added a way to to store expected heartbeat frequency (in seconds, minimum 60) -to a source system, and made the new field visible in the admin. +Added a way to to store expected heartbeat frequency (as a Postgres interval, +minimum 60 seconds) to a source system, and made the new field visible in the +admin. diff --git a/src/argus/incident/migrations/0003_sourcesystem_heartbeat_frequency.py b/src/argus/incident/migrations/0003_sourcesystem_heartbeat_frequency.py index a0d5fe105..1648159f7 100644 --- a/src/argus/incident/migrations/0003_sourcesystem_heartbeat_frequency.py +++ b/src/argus/incident/migrations/0003_sourcesystem_heartbeat_frequency.py @@ -1,5 +1,6 @@ -# Generated by Django 5.2.13 on 2026-06-15 11:46 +# Generated by Django 5.2.13 on 2026-06-22 12:05 +import datetime import django.core.validators from django.db import migrations, models @@ -14,6 +15,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='sourcesystem', name='heartbeat_frequency', - field=models.IntegerField(blank=True, help_text='Expected to send heartbeat at least every N seconds, lower bound: 60 seconds', null=True, validators=[django.core.validators.MinValueValidator(60)]), + field=models.DurationField(blank=True, help_text="Expected to send heartbeat at least every N seconds. Lower bound: 60 seconds. Upper bound: 1 day. Valid inputs: seconds (as an integer, e.g. 86400), DD HH:MM:SS (1 00:00:00), ISO 8601 periods (P1D, note the T is needed if less than a day), PostgreSQL's day-time interval format (1 day)", null=True, validators=[django.core.validators.MinValueValidator(datetime.timedelta(seconds=60)), django.core.validators.MaxValueValidator(datetime.timedelta(days=1))]), ), ] diff --git a/src/argus/incident/models.py b/src/argus/incident/models.py index 5adf27385..0e134f909 100644 --- a/src/argus/incident/models.py +++ b/src/argus/incident/models.py @@ -9,7 +9,7 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError -from django.core.validators import URLValidator, MinValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator from django.db import models from django.db.models import F, Q from django.utils import timezone @@ -18,9 +18,11 @@ from argus.util.datetime_utils import INFINITY_REPR, get_infinity_repr from .constants import Level from .fields import DateTimeInfinityField -from .validators import validate_lowercase, validate_key +from .validators import validate_key, validate_lowercase +MINIMUM_DURATION = timedelta(seconds=60) +MAXIMUM_DURATION = timedelta(days=1) LOG = logging.getLogger(__name__) User = get_user_model() @@ -165,11 +167,14 @@ class SourceSystem(models.Model): blank=True, ) last_seen = models.DateTimeField(null=True, blank=True) - heartbeat_frequency = models.IntegerField( + heartbeat_frequency = models.DurationField( blank=True, null=True, - help_text="Expected to send heartbeat at least every N seconds, lower bound: 60 seconds", - validators=[MinValueValidator(60)], + help_text="Expected to send heartbeat at least every N seconds. Lower bound: 60 seconds. Upper bound: 1 day. Valid inputs: seconds (as an integer, e.g. 86400), DD HH:MM:SS (1 00:00:00), ISO 8601 periods (P1D, note the T is needed if less than a day), PostgreSQL's day-time interval format (1 day)", + validators=[ + MinValueValidator(MINIMUM_DURATION), + MaxValueValidator(MAXIMUM_DURATION), + ], ) class Meta: From 2526843b48c2bcb0a02fb006eb8a046a6c56e7c6 Mon Sep 17 00:00:00 2001 From: Hanne Moa Date: Thu, 18 Jun 2026 13:27:35 +0200 Subject: [PATCH 2/2] Display heartbeat frequency in source pages --- changelog.d/1962.added.md | 1 + src/argus/htmx/sourcesystem/forms.py | 4 +- .../htmx/sourcesystem/_sourcesystem_row.html | 7 +++ .../htmx/sourcesystem/sourcesystem_form.html | 60 +++++++++++++++++++ .../htmx/sourcesystem/sourcesystem_list.html | 2 + src/argus/htmx/templatetags/argus_htmx.py | 19 ++++++ tests/htmx/test_templatetags.py | 30 +++++++++- 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 changelog.d/1962.added.md diff --git a/changelog.d/1962.added.md b/changelog.d/1962.added.md new file mode 100644 index 000000000..26148c944 --- /dev/null +++ b/changelog.d/1962.added.md @@ -0,0 +1 @@ +Show `last_seen` and `heartbeat_frequency` in the source list and allow altering `heartbeat_frequency`. diff --git a/src/argus/htmx/sourcesystem/forms.py b/src/argus/htmx/sourcesystem/forms.py index 68c61f776..e9e45db33 100644 --- a/src/argus/htmx/sourcesystem/forms.py +++ b/src/argus/htmx/sourcesystem/forms.py @@ -9,16 +9,18 @@ class Meta(AddSourceSystemForm.Meta): widgets = { "name": forms.TextInput, "base_url": forms.TextInput, + "heartbeat_frequency": forms.TextInput(attrs={"placeholder": "00:01:00"}), } class UpdateSourceSystemForm(forms.ModelForm): class Meta: model = SourceSystem - fields = ["name", "type", "base_url"] + fields = ["name", "type", "base_url", "heartbeat_frequency"] widgets = { "name": forms.TextInput, "base_url": forms.TextInput, + "heartbeat_frequency": forms.TextInput(attrs={"placeholder": "00:01:00"}), } diff --git a/src/argus/htmx/templates/htmx/sourcesystem/_sourcesystem_row.html b/src/argus/htmx/templates/htmx/sourcesystem/_sourcesystem_row.html index 1517a40ee..be0379f91 100644 --- a/src/argus/htmx/templates/htmx/sourcesystem/_sourcesystem_row.html +++ b/src/argus/htmx/templates/htmx/sourcesystem/_sourcesystem_row.html @@ -1,3 +1,4 @@ +{% load argus_htmx %} @@ -8,6 +9,12 @@ {{ source.base_url|default:"-" }} + + {{ source.last_seen|default:"Never" }} + + + {{ source.heartbeat_frequency|pretty_timedelta:"Off" }} + {% if source.token_status == "valid" %} diff --git a/src/argus/htmx/templates/htmx/sourcesystem/sourcesystem_form.html b/src/argus/htmx/templates/htmx/sourcesystem/sourcesystem_form.html index 7cee4bf0c..27d101e25 100644 --- a/src/argus/htmx/templates/htmx/sourcesystem/sourcesystem_form.html +++ b/src/argus/htmx/templates/htmx/sourcesystem/sourcesystem_form.html @@ -78,6 +78,66 @@

{% endif %} +
+ + {% render_field form.heartbeat_frequency class+="input input-bordered w-full" %} + {% if form.heartbeat_frequency.help_text %} +
+
+

Expected to send heartbeat at least every N seconds. Lower bound: 60 seconds. Upper bound: 1 day.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Valid inputsShortLongNotes
seconds as an integer100086400
DD HH:MM:SS00:16:401 00:00:00This is the format that is displayed in the form
+ ISO 8601 periods + PT16M40SP1DThe T is needed if less than a day.
+ PostgreSQL’s day-time interval format + 00:16:401 day 00:00:00
+
+
+ {% endif %} + {% if form.heartbeat_frequency.errors %} +
+ {% for error in form.heartbeat_frequency.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +