diff --git a/alertproviders/bota.py b/alertproviders/bota.py index 0e34e49..6c19bc5 100644 --- a/alertproviders/bota.py +++ b/alertproviders/bota.py @@ -10,7 +10,7 @@ from data.sig_ref import SIGRef # Alert provider for Beaches on the Air class BOTA(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://www.beachesontheair.com/" def __init__(self, provider_config): diff --git a/alertproviders/ng3k.py b/alertproviders/ng3k.py index f684b1a..fa487b1 100644 --- a/alertproviders/ng3k.py +++ b/alertproviders/ng3k.py @@ -10,7 +10,7 @@ from data.alert import Alert # Alert provider NG3K DXpedition list class NG3K(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://www.ng3k.com/adxo.xml" AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE) diff --git a/alertproviders/parksnpeaks.py b/alertproviders/parksnpeaks.py index d05462c..84cd2a7 100644 --- a/alertproviders/parksnpeaks.py +++ b/alertproviders/parksnpeaks.py @@ -10,7 +10,7 @@ from data.sig_ref import SIGRef # Alert provider for Parks n Peaks class ParksNPeaks(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "http://parksnpeaks.org/api/ALERTS/" def __init__(self, provider_config): diff --git a/alertproviders/pota.py b/alertproviders/pota.py index d4e2a8b..8d1b8a1 100644 --- a/alertproviders/pota.py +++ b/alertproviders/pota.py @@ -9,7 +9,7 @@ from data.sig_ref import SIGRef # Alert provider for Parks on the Air class POTA(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://api.pota.app/activation" def __init__(self, provider_config): diff --git a/alertproviders/sota.py b/alertproviders/sota.py index 38acf05..0792f3e 100644 --- a/alertproviders/sota.py +++ b/alertproviders/sota.py @@ -9,7 +9,7 @@ from data.sig_ref import SIGRef # Alert provider for Summits on the Air class SOTA(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://api-db2.sota.org.uk/api/alerts/365/all/all" def __init__(self, provider_config): diff --git a/alertproviders/wota.py b/alertproviders/wota.py index ccf0d5b..3f40caa 100644 --- a/alertproviders/wota.py +++ b/alertproviders/wota.py @@ -10,7 +10,7 @@ from data.sig_ref import SIGRef # Alert provider for Wainwrights on the Air class WOTA(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://www.wota.org.uk/alerts_rss.php" RSS_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %z" diff --git a/alertproviders/wwff.py b/alertproviders/wwff.py index 30c8319..5da3f2e 100644 --- a/alertproviders/wwff.py +++ b/alertproviders/wwff.py @@ -9,7 +9,7 @@ from data.sig_ref import SIGRef # Alert provider for Worldwide Flora and Fauna class WWFF(HTTPAlertProvider): - POLL_INTERVAL_SEC = 3600 + POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://spots.wwff.co/static/agendas.json" def __init__(self, provider_config): diff --git a/views/webpage_about.tpl b/views/webpage_about.tpl index 2722e3a..16fe182 100644 --- a/views/webpage_about.tpl +++ b/views/webpage_about.tpl @@ -46,7 +46,7 @@

Spothole is a Progressive Web App, which means you can install it on an Android or iOS device by opening the site in Chrome or Safari respectively, and clicking "Install" on the pop-up panel. It'll only prompt you once, so if you dismiss the prompt and change your mind, you'll find an Install / Add to Home Screen option on your browser's menu.

Installing Spothole on your phone is completely optional, the website works exactly the same way as the "app" does.

Why hasn't my spot/alert shown up yet?

-

To avoid putting too much load on the various servers that Spothole connects to, the Spothole server only polls them once every two minutes for spots, and once every hour for alerts. (Some sources, such as DX clusters, RBN, APRS-IS and WWBOTA use a non-polling mechanism, and their updates will therefore arrive more quickly.) Then if you are using the web interface, that has its own rate at which it reloads the data from Spothole, which is once a minute for spots or 30 minutes for alerts. So you could be waiting around three minutes to see a newly added spot, or 90 minutes to see a newly added alert.

+

To avoid putting too much load on the various servers that Spothole connects to, the Spothole server only polls them once every two minutes for spots, and once every 30 minutes for alerts. (Some sources, such as DX clusters, RBN, APRS-IS and WWBOTA use a non-polling mechanism, and their updates will therefore arrive more quickly.) Then if you are using the web interface, that has its own rate at which it fetches the data from Spothole. This is instant for the main spots list, with new spots appearing immediately at the top of the list, while the map and bands displays update once a minute, and the alerts display updates once every 5 minutes. So you could be waiting around three minutes to see a newly added spot, or 40 minutes to see a newly added alert.

What licence does Spothole use?

Spothole's source code is licenced under the Public Domain. You can write a Spothole client, run your own server, modify it however you like, you can claim you wrote it and charge people £1000 for a copy, I don't really mind. (Please don't do the last one. But if you're using my code for something cool, it would be nice to hear from you!)

Data Accuracy

diff --git a/views/webpage_alerts.tpl b/views/webpage_alerts.tpl index 7548ce9..82cb5f4 100644 --- a/views/webpage_alerts.tpl +++ b/views/webpage_alerts.tpl @@ -161,7 +161,9 @@ -
+
+
+
diff --git a/views/webpage_spots.tpl b/views/webpage_spots.tpl index f6e8e63..153d095 100644 --- a/views/webpage_spots.tpl +++ b/views/webpage_spots.tpl @@ -204,7 +204,9 @@ -
+
+
+
diff --git a/webassets/css/style.css b/webassets/css/style.css index dd7f314..7da2b7d 100644 --- a/webassets/css/style.css +++ b/webassets/css/style.css @@ -174,6 +174,19 @@ tr.table-faded td span { text-decoration: line-through !important; } +/* New spot styles */ +tr.new td { + animation: 2s linear newspotanim; +} +@keyframes newspotanim { + 0% { + background-color: var(--bs-success-border-subtle); + } + 100% { + background-color: intial; + } +} + /* Fudge apply our own "dark primary" and "dark danger" backgrounds as Bootstrap doesn't do this itself */ [data-bs-theme=dark] tr.table-primary { --bs-table-bg: #053680; diff --git a/webassets/js/alerts.js b/webassets/js/alerts.js index 533ece4..55b01bc 100644 --- a/webassets/js/alerts.js +++ b/webassets/js/alerts.js @@ -1,5 +1,5 @@ // How often to query the server? -const REFRESH_INTERVAL_SEC = 60 * 30; +const REFRESH_INTERVAL_SEC = 60 * 10; // Storage for the alert data that the server gives us. var alerts = [] @@ -51,7 +51,8 @@ function updateTable() { var showRef = $("#tableShowRef")[0].checked; // Populate table with headers - let table = $('').append(''); + let table = $("#table"); + table.find('thead tr').empty(); if (showStartTime) { table.find('thead tr').append(``); } @@ -74,6 +75,8 @@ function updateTable() { table.find('thead tr').append(``); } + table.find('tbody').empty(); + // 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'); } - - // Update DOM - $('#table-container').html(table); } // Add a row to tbody for each alert in the provided list diff --git a/webassets/js/spots.js b/webassets/js/spots.js index e261783..219fbc7 100644 --- a/webassets/js/spots.js +++ b/webassets/js/spots.js @@ -1,5 +1,7 @@ // SSE event source let evtSource; +// Table row count, to alternate shading +let rowCount = 0; // Load spots and populate the table. function loadSpots() { @@ -28,12 +30,12 @@ function restartSSEConnection() { // Store last updated time lastUpdateTime = moment.utc(); updateRefreshDisplay(); - // Add spot to table + // Add spot to internal data store newSpot = JSON.parse(event.data); - console.log(newSpot); spots.unshift(newSpot); spots = spots.slice(0, -1); - updateTable(); + // Add spot to table + addSpotToTopOfTable(newSpot, true); }; evtSource.onerror = function(err) { @@ -76,7 +78,8 @@ function updateTable() { var showDE = $("#tableShowDE")[0].checked; // Populate table with headers - let table = $('
${useLocalTime ? "Start (Local)" : "Start UTC"}Ref.No alerts match your filters.
').append(''); + let table = $("#table"); + table.find('thead tr').empty(); if (showTime) { table.find('thead tr').append(``); } @@ -105,199 +108,230 @@ function updateTable() { table.find('thead tr').append(``); } + table.find('tbody').empty(); if (spots.length == 0) { table.find('tbody').append(''); } - var count = 0; - spots.forEach(s => { - // Create row - let $tr = $(''); + spots.reverse(); + spots.forEach(s => addSpotToTopOfTable(s, false)); +} - // Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of - // extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners - // which cause the table-striped colouring to go awry. - if (count % 2 == 1) { - $tr.addClass("table-active"); - } +// Add rows corresponding to a new spot to the top of the table +// highlightNew = false for an initial load, true for new SSE-loaded spots +function addSpotToTopOfTable(s, highlightNew) { + let rows = createNewTableRowsForSpot(s, highlightNew); + $("#table").find('tbody').prepend(rows[1]); + $("#table").find('tbody').prepend(rows[0]); +} - // Show faded out if QRT - if (s["qrt"] == true) { - $tr.addClass("table-faded"); - } +// Turn a spot into a set of table rows to represent it. This is actually two table rows because we need a second +// separate row for the mobile view. +// highlightNew = false for an initial load, true for new SSE-loaded spots +function createNewTableRowsForSpot(s, highlightNew) { + // Use local time instead of UTC? + var useLocalTime = $("#timeZone")[0].value == "local"; - // Format a UTC or local time for display - var time = moment.unix(s["time"]).utc(); - if (useLocalTime) { - time.local(); - } - var time_formatted = time.format("HH:mm"); + // Get user grid if valid, this will be null if it's not. + var userPos = latLonForGridCentre($("#userGrid").val()); - // Format DX call - var dx_call = s["dx_call"]; - if (dx_call == null) { - dx_call = ""; - dx_flag = ""; - } - if (s["dx_ssid"] != null) { - dx_call = dx_call + "-" + s["dx_ssid"]; - } + // Table data toggles + var showTime = $("#tableShowTime")[0].checked; + var showDX = $("#tableShowDX")[0].checked; + var showFreq = $("#tableShowFreq")[0].checked; + var showMode = $("#tableShowMode")[0].checked; + var showComment = $("#tableShowComment")[0].checked; + var showBearing = $("#tableShowBearing")[0].checked && userPos != null; + var showType = $("#tableShowType")[0].checked; + var showRef = $("#tableShowRef")[0].checked; + var showDE = $("#tableShowDE")[0].checked; - // Format dx country - var dx_country = s["dx_country"]; - if (dx_country == null) { - dx_country = "Unknown or not a country"; - } + // Create row + let $tr = $(''); + if (highlightNew) { + $tr.addClass("new"); + } - // Format DX flag - var dx_flag = ""; - if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] != 0) { - dx_flag = `${dx_country}`; - } + // Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of + // extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners + // which cause the table-striped colouring to go awry. + if (rowCount % 2 == 1) { + $tr.addClass("table-active"); + } - // Format the frequency - var freq_string = "Unknown" - if (s["freq"] != null) { - var mhz = Math.floor(s["freq"] / 1000000.0); - var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0); - var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0)); - var hz_string = (hz > 0) ? hz.toFixed(0)[0] : ""; - freq_string = `${mhz.toFixed(0)}${khz.toFixed(0).padStart(3, '0')}${hz_string}` - } + // Show faded out if QRT + if (s["qrt"] == true) { + $tr.addClass("table-faded"); + } - // Format the mode - mode_string = s["mode"]; - if (s["mode"] == null) { - mode_string = "???"; - } - if (s["mode_source"] == "BANDPLAN") { - mode_string = mode_string + ""; - } + // Format a UTC or local time for display + var time = moment.unix(s["time"]).utc(); + if (useLocalTime) { + time.local(); + } + var time_formatted = time.format("HH:mm"); - // Format comment - var commentText = ""; - if (s["comment"] != null) { - commentText = escapeHtml(s["comment"]); - } + // Format DX call + var dx_call = s["dx_call"]; + if (dx_call == null) { + dx_call = ""; + dx_flag = ""; + } + if (s["dx_ssid"] != null) { + dx_call = dx_call + "-" + s["dx_ssid"]; + } - // Format bearing text - var bearingText = "---"; - if (userPos != null && s["dx_latitude"] != null && s["dx_longitude"] != null) { - var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]); - bearingText = bearing.toFixed(0).padStart(3, '0') + "°"; - if (s["dx_location_good"] == null || s["dx_location_good"] == false) { - if (s["dx_location_source"] == "HOME QTH") { - bearingText = bearingText + ""; - } else { - bearingText = bearingText + ""; - } + // Format dx country + var dx_country = s["dx_country"]; + if (dx_country == null) { + dx_country = "Unknown or not a country"; + } + + // Format DX flag + var dx_flag = ""; + if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] != 0) { + dx_flag = `${dx_country}`; + } + + // Format the frequency + var freq_string = "Unknown" + if (s["freq"] != null) { + var mhz = Math.floor(s["freq"] / 1000000.0); + var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0); + var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0)); + var hz_string = (hz > 0) ? hz.toFixed(0)[0] : ""; + freq_string = `${mhz.toFixed(0)}${khz.toFixed(0).padStart(3, '0')}${hz_string}` + } + + // Format the mode + mode_string = s["mode"]; + if (s["mode"] == null) { + mode_string = "???"; + } + if (s["mode_source"] == "BANDPLAN") { + mode_string = mode_string + ""; + } + + // Format comment + var commentText = ""; + if (s["comment"] != null) { + commentText = escapeHtml(s["comment"]); + } + + // Format bearing text + var bearingText = "---"; + if (userPos != null && s["dx_latitude"] != null && s["dx_longitude"] != null) { + var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]); + bearingText = bearing.toFixed(0).padStart(3, '0') + "°"; + if (s["dx_location_good"] == null || s["dx_location_good"] == false) { + if (s["dx_location_source"] == "HOME QTH") { + bearingText = bearingText + ""; + } else { + bearingText = bearingText + ""; } } + } - // Format "type" (Sig or fallback to source) - var typeText = s["source"]; - if (s["sig"]) { - typeText = s["sig"]; - } + // Format "type" (Sig or fallback to source) + var typeText = s["source"]; + if (s["sig"]) { + typeText = s["sig"]; + } - // Format sig_refs - var sig_refs = ""; - if (s["sig_refs"] != null) { - var items = [] - for (var i = 0; i < s["sig_refs"].length; i++) { - if (s["sig_refs"][i]["url"] != null) { - items[i] = `${s["sig_refs"][i]["id"]}` - } else { - items[i] = `${s["sig_refs"][i]["id"]}` - } + // Format sig_refs + var sig_refs = ""; + if (s["sig_refs"] != null) { + var items = [] + for (var i = 0; i < s["sig_refs"].length; i++) { + if (s["sig_refs"][i]["url"] != null) { + items[i] = `${s["sig_refs"][i]["id"]}` + } else { + items[i] = `${s["sig_refs"][i]["id"]}` } - sig_refs = items.join(", "); } + sig_refs = items.join(", "); + } - // Format de country - var de_country = s["de_country"]; - if (de_country == null) { - de_country = "Unknown or not a country"; - } + // Format de country + var de_country = s["de_country"]; + if (de_country == null) { + de_country = "Unknown or not a country"; + } - // Format DE flag - var de_flag = ""; - if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] != 0) { - de_flag = `${de_country}`; - } + // Format DE flag + var de_flag = ""; + if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] != 0) { + de_flag = `${de_country}`; + } - // Format de call - var de_call = s["de_call"]; - if (de_call == null) { - de_call = ""; - de_flag = ""; - } - if (s["de_ssid"] != null) { - de_call = de_call + "-" + s["de_ssid"]; - } + // Format de call + var de_call = s["de_call"]; + if (de_call == null) { + de_call = ""; + de_flag = ""; + } + if (s["de_ssid"] != null) { + de_call = de_call + "-" + s["de_ssid"]; + } - // Format band name - var bandFullName = s['band'] ? s['band'] + " band": "Unknown band"; + // Format band name + var bandFullName = s['band'] ? s['band'] + " band": "Unknown band"; - // Populate the row - if (showTime) { - $tr.append(``); - } - if (showDX) { - $tr.append(``); - } - if (showFreq) { - $tr.append(``); - } - if (showMode) { - $tr.append(``); - } - if (showComment) { - $tr.append(``); - } - if (showBearing) { - $tr.append(``); - } - if (showType) { - $tr.append(``); - } - if (showRef) { - $tr.append(``); - } - if (showDE) { - $tr.append(``); - } - table.find('tbody').append($tr); + // Populate the row + if (showTime) { + $tr.append(``); + } + if (showDX) { + $tr.append(``); + } + if (showFreq) { + $tr.append(``); + } + if (showMode) { + $tr.append(``); + } + if (showComment) { + $tr.append(``); + } + if (showBearing) { + $tr.append(``); + } + if (showType) { + $tr.append(``); + } + if (showRef) { + $tr.append(``); + } + if (showDE) { + $tr.append(``); + } - // Second row for mobile view only, containing type, ref & comment - $tr2 = $(""); - if (count % 2 == 1) { - $tr2.addClass("table-active"); - } - if (s["qrt"] == true) { - $tr2.addClass("table-faded"); - } - $td2 = $(""); + if (rowCount % 2 == 1) { + $tr2.addClass("table-active"); + } + if (s["qrt"] == true) { + $tr2.addClass("table-faded"); + } + $td2 = $("
${useLocalTime ? "Local" : "UTC"}DE
No spots match your filters.
${time_formatted}${dx_flag}${dx_call}${freq_string}${mode_string}${commentText}${bearingText} ${typeText}${sig_refs}${de_flag}${de_call}${time_formatted}${dx_flag}${dx_call}${freq_string}${mode_string}${commentText}${bearingText} ${typeText}${sig_refs}${de_flag}${de_call}
"); - if (showType) { - $td2.append(` ${typeText} `); - } - if (showRef) { - $td2.append(`${sig_refs} `); - } - if (showBearing) { - $td2.append(`   Bearing: ${bearingText} `); - } - if (showComment) { - $td2.append(`
${commentText}`); - } - $tr2.append($td2); - table.find('tbody').append($tr2); + // Second row for mobile view only, containing type, ref & comment + $tr2 = $("
"); + if (showType) { + $td2.append(` ${typeText} `); + } + if (showRef) { + $td2.append(`${sig_refs} `); + } + if (showBearing) { + $td2.append(`   Bearing: ${bearingText} `); + } + if (showComment) { + $td2.append(`
${commentText}`); + } + $tr2.append($td2); - count++; - }); + rowCount++; - // Update DOM - $('#table-container').html(table); + return [$tr, $tr2]; } // Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query