Update map and bands pages to use SSE endpoint after initial load. Closes #43.

This commit is contained in:
Ian Renton
2026-06-26 09:09:15 +01:00
parent d1df772649
commit 998b394d2c
14 changed files with 234 additions and 85 deletions

View File

@@ -1,5 +1,9 @@
// How often to query the server?
const REFRESH_INTERVAL_SEC = 60;
// SSE connection for live spot updates
let evtSource;
let restartSSEOnErrorTimeoutId;
// Debounce timer so rapid SSE bursts only trigger one updateBands() call, as these could trigger a flash of the user
// visible content
let updateBandsDebounceId;
// A couple of constants that must match what's in CSS. We need to know them before the content actually renders, so we
// can't just ask the elements themselves for their dimensions.
@@ -12,19 +16,62 @@ BAND_COLUMN_SPOT_DIV_HEIGHT_PX = BAND_COLUMN_FONT_SIZE * 1.6;
// Load spots and populate the bands display.
function loadSpots() {
$.getJSON('/api/v1/spots' + buildQueryString(false), function (jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
// Close any existing SSE connection before fetching fresh data
if (evtSource != null) {
evtSource.close();
}
$.getJSON('/api/v1/spots' + buildQueryString(), function (jsonData) {
// Store data
spots = jsonData;
// Update bands display
updateBands();
// Start the ongoing SSE connection
startSSEConnection();
});
}
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString(includeCredentials) {
// Start an SSE connection to receive new spots as they arrive.
function startSSEConnection() {
if (evtSource != null) {
evtSource.close();
}
evtSource = new EventSource('/api/v1/spots/stream' + buildQueryString());
evtSource.onmessage = function (event) {
const newSpot = JSON.parse(event.data);
// Replace any existing spot for this callsign
spots = spots.filter(s => newSpot["dx_call"] !== s["dx_call"]);
spots.unshift(newSpot);
// Debounce. Wait 500ms after the last message before re-rendering to avoid too many ugly flashes
clearTimeout(updateBandsDebounceId);
updateBandsDebounceId = setTimeout(updateBands, 500);
};
evtSource.onerror = function () {
if (evtSource != null) {
evtSource.close();
}
clearTimeout(restartSSEOnErrorTimeoutId);
restartSSEOnErrorTimeoutId = setTimeout(startSSEConnection, 1000);
};
}
// Remove spots from the display that are older than the selected max age.
function expireOldSpots() {
const maxAgeSeconds = parseInt($("#max-spot-age option:selected").val());
const cutoff = (Date.now() / 1000) - maxAgeSeconds;
const before = spots.length;
spots = spots.filter(s => s["time"] && s["time"] >= cutoff);
if (spots.length !== before) {
updateBands();
}
}
// Build a query string for the API, based on the filters that the user has selected. There's no need for credentials
// in the bands page's version of this, because nothing QRZ.com/HamQTH can provide will affect the display.
function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
@@ -34,9 +81,6 @@ function buildQueryString(includeCredentials) {
str = str + "max_age=" + $("#max-spot-age option:selected").val();
// Additional filters for the bands view: No dupes, no QRT
str = str + "&dedupe=true&allow_qrt=false";
if (includeCredentials) {
str = str + getCredentialQueryString();
}
return str;
}
@@ -257,22 +301,28 @@ function loadOptions() {
// Load settings from settings storage now all the controls are available
loadSettings();
// Load spots and set up the timer
// Load spots and start SSE for live updates
loadSpots();
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000);
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
}
// Method called when any display property is changed to reload the map and persist the display settings.
// Method called when any display property is changed to reload the bands display and persist settings.
function displayUpdated() {
updateMap();
updateBands();
saveSettings();
}
// Startup
$(document).ready(function () {
// Close SSE connection cleanly when navigating away
window.addEventListener('beforeunload', function () {
if (evtSource != null) {
evtSource.close();
}
});
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000);
});