From e01b6d5ea98d52ceac18c82fa69658d2262ee2b1 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Wed, 8 Oct 2025 16:09:39 +0100 Subject: [PATCH] Fix some bugginess with how expired alerts and alert max_duration were handled. Closes #34 --- alertproviders/alert_provider.py | 7 ++----- core/cleanup.py | 7 ++----- data/alert.py | 14 +++++++++++--- server/webserver.py | 8 ++++++-- spotproviders/gma.py | 6 +++--- webassets/js/alerts.js | 10 ++++++++-- webassets/js/common.js | 6 +++--- 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/alertproviders/alert_provider.py b/alertproviders/alert_provider.py index 1c6f62c..ad915c7 100644 --- a/alertproviders/alert_provider.py +++ b/alertproviders/alert_provider.py @@ -35,11 +35,8 @@ class AlertProvider: for alert in alerts: # Fill in any blanks alert.infer_missing() - # Add to the list, provided it meets the semsible date test. If alerts have an end time, it must be in the - # future, or if not, then the start date must be at least in the last 24 hours. - if (alert.end_time and alert.end_time > datetime.now(pytz.UTC).timestamp()) or ( - not alert.end_time and alert.start_time > (datetime.now( - pytz.UTC) - timedelta(days=1)).timestamp()): + # Add to the list, provided it heas not already expired. + if not alert.expired(): self.alerts.add(alert.id, alert, expire=MAX_ALERT_AGE) # Stop any threads and prepare for application shutdown diff --git a/core/cleanup.py b/core/cleanup.py index c97a63d..dd3d6ff 100644 --- a/core/cleanup.py +++ b/core/cleanup.py @@ -34,13 +34,10 @@ class CleanupTimer: self.alerts.expire() # Alerts can persist in the system for a while, so we want to explicitly clean up any alerts that have - # definitively ended, or if they have no definite end time, then if the start time was more than 24 hours - # ago. + # expired for id in list(self.alerts.iterkeys()): alert = self.alerts[id] - if (alert.end_time and alert.end_time < datetime.now(pytz.UTC).timestamp()) or ( - not alert.end_time and alert.start_time < (datetime.now( - pytz.UTC) - timedelta(days=1)).timestamp()): + if alert.expired(): self.alerts.evict(id) self.status = "OK" diff --git a/data/alert.py b/data/alert.py index 687f892..26aecc8 100644 --- a/data/alert.py +++ b/data/alert.py @@ -1,9 +1,9 @@ +import copy import hashlib import json from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta -import copy import pytz from core.constants import DXCC_FLAGS @@ -117,4 +117,12 @@ class Alert: # JSON serialise def to_json(self): - return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True) \ No newline at end of file + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True) + + # Decide if this alert has expired (in which case it should not be added to the system in the first place, and not + # returned by the web server if later requested, and removed by the cleanup functions. "Expired" is defined as + # either having an end_time in the past, or if it only has a start_time, then that start time was more than 3 hours + # ago. If it somehow doesn't have a start_time either, it is considered to be expired. + def expired(self): + return not self.start_time or (self.end_time and self.end_time < datetime.now(pytz.UTC).timestamp()) or ( + not self.end_time and self.start_time < (datetime.now(pytz.UTC) - timedelta(hours=3)).timestamp()) diff --git a/server/webserver.py b/server/webserver.py index a95b611..fbb0547 100644 --- a/server/webserver.py +++ b/server/webserver.py @@ -187,6 +187,8 @@ class WebServer: alerts = [] for k in alert_ids: alerts.append(self.alerts.get(k)) + # We never want alerts that seem to be in the past + alerts = list(filter(lambda alert: not alert.expired(), alerts)) alerts = sorted(alerts, key=lambda alert: (alert.start_time if alert and alert.start_time else 0)) for k in query.keys(): match k: @@ -195,8 +197,10 @@ class WebServer: alerts = [a for a in alerts if a.received_time > since] case "max_duration": max_duration = int(query.get(k)) - alerts = [a for a in alerts if (a.end_time and a.end_time - a.start_time <= max_duration) or ( - not a.end_time and datetime.now(pytz.UTC).timestamp() - a.start_time <= max_duration)] + # Check the duration if end_time is provided. If end_time is not provided, assume the activation is + # "short", i.e. it always passes this check. + alerts = [a for a in alerts if (a.end_time and a.end_time - a.start_time <= max_duration) or + not a.end_time] case "source": sources = query.get(k).split(",") alerts = [a for a in alerts if a.source in sources] diff --git a/spotproviders/gma.py b/spotproviders/gma.py index b8b685a..d2c35ac 100644 --- a/spotproviders/gma.py +++ b/spotproviders/gma.py @@ -37,9 +37,9 @@ class GMA(HTTPSpotProvider): sig_refs_names=[source_spot["NAME"]], time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace( tzinfo=pytz.UTC).timestamp(), - latitude=float(source_spot["LAT"]) if (source_spot["LAT"] != "") else None, - # Seen GMA spots with no lat/lon - longitude=float(source_spot["LON"]) if (source_spot["LON"] != "") else None) + latitude=float(source_spot["LAT"]) if (source_spot["LAT"] and source_spot["LAT"] != "") else None, + # Seen GMA spots with no (or empty) lat/lon + longitude=float(source_spot["LON"]) if (source_spot["LON"] and source_spot["LON"] != "") else None) # GMA doesn't give what programme (SIG) the reference is for until we separately look it up. ref_response = self.REF_INFO_CACHE.get(self.REF_INFO_URL_ROOT + source_spot["REF"], diff --git a/webassets/js/alerts.js b/webassets/js/alerts.js index 5e2cd07..4941339 100644 --- a/webassets/js/alerts.js +++ b/webassets/js/alerts.js @@ -48,11 +48,17 @@ function updateTable() { // Split alerts into three types, each of which will get its own table header: On now, next 24h, and later. "On now" // is considered to be events with an end_time where start (a["end_time"] != null && moment.unix(a["end_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore()) - || (a["end_time"] == null && moment.unix(a["start_time"]).utc().add(1, 'hours').isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore())); + onNow = alerts.filter(a => (a["end_time"] != null && a["end_time"] != 0 && moment.unix(a["end_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore()) + || ((a["end_time"] == null || a["end_time"] == 0) && moment.unix(a["start_time"]).utc().add(1, 'hours').isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore())); next24h = alerts.filter(a => moment.unix(a["start_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().subtract(24, 'hours').isBefore()); later = alerts.filter(a => moment.unix(a["start_time"]).utc().subtract(24, 'hours').isSameOrAfter()); + alerts.forEach(a => { + if (!(a in onNow) && !(a in next24h) && !(a in later)) { + console.log(moment.unix(a["start_time"]).format('YYYY-MM-DD hh:mm') + " " + moment.unix(a["end_time"]).format('YYYY-MM-DD hh:mm')); + } + }); + if (onNow.length > 0) { table.find('tbody').append('On Now'); addAlertRowsToTable(table.find('tbody'), onNow); diff --git a/webassets/js/common.js b/webassets/js/common.js index 6b2a9b2..d51115c 100644 --- a/webassets/js/common.js +++ b/webassets/js/common.js @@ -66,13 +66,13 @@ function updateRefreshDisplay() { count = REFRESH_INTERVAL_SEC - secSinceUpdate; if (count <= 60) { var number = count.toFixed(0); - updatingString = "Updating in " + number + " second" + (number != "1" ? "s" : "") + ".  "; + updatingString = "Updating in " + number + " second" + (number != "1" ? "s" : "") + "."; } else { var number = Math.round(count / 60.0).toFixed(0); - updatingString = "Updating in " + number + " minute" + (number != "1" ? "s" : "") + ".  "; + updatingString = "Updating in " + number + " minute" + (number != "1" ? "s" : "") + "."; } } - $("#timing-container").text("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString); + $("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString); } }