From 70dc1b495c1dc024d92da216c8e9e5d1430b9a09 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Tue, 23 Dec 2025 22:24:30 +0000 Subject: [PATCH] Fix SSE connections not respecting filters #3 --- server/handlers/api/alerts.py | 15 ++++++++++++++- server/handlers/api/spots.py | 14 +++++++++++++- templates/spots.html | 2 +- webassets/apidocs/openapi.yml | 28 ++++++++++++++++++++++++++-- webassets/css/style.css | 4 ++-- webassets/js/spots.js | 19 +++++++++++-------- 6 files changed, 67 insertions(+), 15 deletions(-) diff --git a/server/handlers/api/alerts.py b/server/handlers/api/alerts.py index 1af3339..cd8cf38 100644 --- a/server/handlers/api/alerts.py +++ b/server/handlers/api/alerts.py @@ -58,6 +58,10 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler): self.web_server_metrics["status"] = "OK" api_requests_counter.inc() + # request.arguments contains lists for each param key because technically the client can supply multiple, + # reduce that to just the first entry, and convert bytes to string + self.query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} + # Create a alert queue and add it to the web server's list. The web server will fill this when alerts arrive self.alert_queue = Queue(maxsize=100) self.sse_alert_queues.append(self.alert_queue) @@ -86,7 +90,10 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler): if self.alert_queue: while not self.alert_queue.empty(): alert = self.alert_queue.get() - self.write_message(msg=json.dumps(alert, default=serialize_everything)) + # If the new alert matches our param filters, send it to the client. If not, ignore it. + if alert_allowed_by_query(alert, self.query_params): + self.write_message(msg=json.dumps(alert, default=serialize_everything)) + if self.alert_queue not in self.sse_alert_queues: logging.error("Web server cleared up a queue of an active connection!") self.close() @@ -155,4 +162,10 @@ def alert_allowed_by_query(alert, query): dx_call_includes = query.get(k).strip() if not alert.dx_call or dx_call_includes.upper() not in alert.dx_call.upper(): return False + case "text_includes": + text_includes = query.get(k).strip() + if (not alert.dx_call or text_includes.upper() not in alert.dx_call.upper()) \ + and (not alert.comment or text_includes.upper() not in alert.comment.upper()) \ + and (not alert.freqs_modes or text_includes.upper() not in alert.freqs_modes.upper()): + return False return True diff --git a/server/handlers/api/spots.py b/server/handlers/api/spots.py index 3c369f1..81a4e31 100644 --- a/server/handlers/api/spots.py +++ b/server/handlers/api/spots.py @@ -60,6 +60,10 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler): self.web_server_metrics["status"] = "OK" api_requests_counter.inc() + # request.arguments contains lists for each param key because technically the client can supply multiple, + # reduce that to just the first entry, and convert bytes to string + self.query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()} + # Create a spot queue and add it to the web server's list. The web server will fill this when spots arrive self.spot_queue = Queue(maxsize=1000) self.sse_spot_queues.append(self.spot_queue) @@ -88,7 +92,10 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler): if self.spot_queue: while not self.spot_queue.empty(): spot = self.spot_queue.get() - self.write_message(msg=json.dumps(spot, default=serialize_everything)) + # If the new spot matches our param filters, send it to the client. If not, ignore it. + if spot_allowed_by_query(spot, self.query_params): + self.write_message(msg=json.dumps(spot, default=serialize_everything)) + if self.spot_queue not in self.sse_spot_queues: logging.error("Web server cleared up a queue of an active connection!") self.close() @@ -206,6 +213,11 @@ def spot_allowed_by_query(spot, query): dx_call_includes = query.get(k).strip() if not spot.dx_call or dx_call_includes.upper() not in spot.dx_call.upper(): return False + case "text_includes": + text_includes = query.get(k).strip() + if (not spot.dx_call or text_includes.upper() not in spot.dx_call.upper()) \ + and (not spot.comment or text_includes.upper() not in spot.comment.upper()): + return False case "allow_qrt": # If false, spots that are flagged as QRT are not returned. prevent_qrt = query.get(k).upper() == "FALSE" diff --git a/templates/spots.html b/templates/spots.html index abecef0..cc36d5e 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -17,7 +17,7 @@

- + diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml index c875c83..3a5485c 100644 --- a/webassets/apidocs/openapi.yml +++ b/webassets/apidocs/openapi.yml @@ -115,7 +115,7 @@ paths: default: false - name: dx_call_includes in: query - description: "Limit the alerts to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches." + description: "Limit the spots to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches." required: false schema: type: string @@ -125,6 +125,12 @@ paths: required: false schema: type: string + - name: text_includes + in: query + description: "Limit the spots to only ones where some significant text (DX callsign or comment) includes the supplied string (case-insensitive)." + required: false + schema: + type: string - name: needs_good_location in: query description: "Return only spots with a 'good' location. (See the spot `dx_location_good` parameter for details. Useful for map-based clients, to avoid spots with 'bad' locations e.g. loads of cluster spots ending up in the centre of the DXCC entitity.)" @@ -215,7 +221,7 @@ paths: $ref: "#/components/schemas/Continent" - name: dx_call_includes in: query - description: "Limit the alerts to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches." + description: "Limit the spots to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches." required: false schema: type: string @@ -225,6 +231,12 @@ paths: required: false schema: type: string + - name: text_includes + in: query + description: "Limit the spots to only ones where some significant text (DX callsign or comment) includes the supplied string (case-insensitive)." + required: false + schema: + type: string - name: needs_good_location in: query description: "Return only spots with a 'good' location. (See the spot `dx_location_good` parameter for details. Useful for map-based clients, to avoid spots with 'bad' locations e.g. loads of cluster spots ending up in the centre of the DXCC entitity.)" @@ -304,6 +316,12 @@ paths: required: false schema: type: string + - name: text_includes + in: query + description: "Limit the alerts to only ones where some significant text (DX callsign, freqs/modes, or comment) includes the supplied string (case-insensitive)." + required: false + schema: + type: string responses: '200': description: Success @@ -359,6 +377,12 @@ paths: required: false schema: type: string + - name: text_includes + in: query + description: "Limit the alerts to only ones where some significant text (DX callsign, freqs/modes, or comment) includes the supplied string (case-insensitive)." + required: false + schema: + type: string responses: '200': description: Success diff --git a/webassets/css/style.css b/webassets/css/style.css index 383c55e..5d3c7d5 100644 --- a/webassets/css/style.css +++ b/webassets/css/style.css @@ -80,7 +80,7 @@ div.container { /* SPOTS/ALERTS PAGES, SETTINGS/STATUS AREAS */ -input#filter-dx-call { +input#search { max-width: 12em; margin-right: 1rem; padding-left: 2em; @@ -337,7 +337,7 @@ div.band-spot:hover span.band-spot-info { overflow: scroll; } /* Filter/search DX Call field should be smaller on mobile */ - input#filter-dx-call { + input#search { max-width: 9em; margin-right: 0; } diff --git a/webassets/js/spots.js b/webassets/js/spots.js index 368917b..e0efe6d 100644 --- a/webassets/js/spots.js +++ b/webassets/js/spots.js @@ -5,6 +5,12 @@ let rowCount = 0; // Load spots and populate the table. function loadSpots() { + // If we have an ongoing SSE connection, stop it so it doesn't interfere with our reload + if (evtSource != null) { + evtSource.close(); + } + + // Make the new query $.getJSON('/api/v1/spots' + buildQueryString(), function(jsonData) { // Store last updated time lastUpdateTime = moment.utc(); @@ -14,17 +20,14 @@ function loadSpots() { // Update table updateTable(); // Start SSE connection to fetch updates in the background - restartSSEConnection(); + startSSEConnection(); }); } // Start an SSE connection (closing an existing one if it exists). This will then be used to add to the table on the // fly. -function restartSSEConnection() { - if (evtSource != null) { - evtSource.close(); - } - evtSource = new EventSource('/api/v1/spots/stream'); +function startSSEConnection() { + evtSource = new EventSource('/api/v1/spots/stream' + buildQueryString()); evtSource.onmessage = function(event) { // Store last updated time @@ -74,8 +77,8 @@ function buildQueryString() { } }); str = str + "limit=" + $("#spots-to-fetch option:selected").val(); - if ($("#filter-dx-call").val() != "") { - str = str + "&dx_call_includes=" + encodeURIComponent($("#filter-dx-call").val()); + if ($("#search").val() != "") { + str = str + "&text_includes=" + encodeURIComponent($("#search").val()); } return str; }