From 1ef8b36cb19e5abc58943825f1a58252cbf3fb81 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Sat, 9 May 2026 16:52:48 +0100 Subject: [PATCH] Modify the front so that it allows QRZ.com and HamQTH credentials to be provided by the client (if none are provided, the lookups do not occur.) --- templates/about.html | 8 +- templates/add_spot.html | 4 +- templates/alerts.html | 18 +++- templates/bands.html | 20 ++++- templates/base.html | 8 +- templates/cards/hamqth.html | 27 ++++++ templates/cards/qrz.html | 27 ++++++ templates/conditions.html | 4 +- templates/map.html | 20 ++++- templates/spots.html | 32 +++++-- templates/status.html | 4 +- templates/widgets/data-area-header.html | 10 +++ .../widgets/filters-display-buttons.html | 1 + webassets/js/alerts.js | 1 + webassets/js/bands.js | 1 + webassets/js/common.js | 83 +++++++++++++++---- webassets/js/map.js | 1 + webassets/js/spots.js | 1 + 18 files changed, 224 insertions(+), 46 deletions(-) create mode 100644 templates/cards/hamqth.html create mode 100644 templates/cards/qrz.html create mode 100644 templates/widgets/data-area-header.html diff --git a/templates/about.html b/templates/about.html index dc8c707..010d2cd 100644 --- a/templates/about.html +++ b/templates/about.html @@ -28,6 +28,7 @@

Spothole can retrieve spots from: Telnet-based DX clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, LLOTA, WWTOTA, Tiles on the Air, the UK Packet Repeater Network, and any site based on the xOTA software by nischu.

Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, WOTA and BOTA.

Spothole can retrieve solar and propagation condition data from HamQSL.

+

Spothole can also perform lookups for callsign data on behalf of the user from QRZ.com and HamQTH.

Note that the server owner has not necessarily enabled all these data sources. In particular it is common to disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so that the server only displays stations where there is likely to be an operator physically present for a QSO.

Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air (POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air (WWTOTA), Tiles on the Air, Worked All Britain (WAB), Worked All Ireland (WAI), and Toilets on the Air (TOTA).

As of the time of writing in November 2025, I think Spothole captures essentially all outdoor radio programmes that have a defined reference list, and almost certainly those that have a spotting/alerting API. If you know of one I've missed, please let me know!

@@ -54,9 +55,10 @@

Data Accuracy

Please note that the data coming out of Spothole is only as good as the data going in. People mis-hear and make typos when spotting callsigns all the time. There are also plenty of cases where Spothole's data, particularly location data, may be inaccurate. For example, there are POTA parks that span multiple US states, countries that span multiple CQ zones, portable operators with no requirement to sign /P, etc. If you are doing something where accuracy is important, such as contesting, you should not rely on Spothole's data to fill in any gaps in your log.

Privacy

-

Spothole collects no data about you, and there is no way to enter personally identifying information into the site apart from by spotting and alerting through Spothole or the various services it connects to. All spots and alerts are "timed out" and deleted from the system after a set interval, which by default is one hour for spots and one week for alerts.

+

Spothole collects no data about you on a permanent basis. All spots and alerts are "timed out" and deleted from the system after a set interval, which by default is one hour for spots and one week for alerts.

Settings you select from Spothole's menus are sent to the server, in order to provide the data with the requested filters. They are also stored in your browser's local storage, so that your preferences are remembered between sessions.

-

There are no trackers, no ads, and no cookies.

+

The data you provide can optionally include your login credentials for QRZ.com and HamQTH. You can provide these in the "Data" menu of most pages. If you do, Spothole will augment the data it produces with lookups from these services, which can for example provide more accurate markers on the map tab, and operator names when you mouse over a DX callsign. Spothole will still work fine if you don't provide these. The values you enter are sent to Spothole via HTTPS so are protected in transit, though of course you do have to trust Spothole with this sensitive data in order to use this feature.

+

Spothole uses no trackers, no ads, and no cookies.

{% if len(web_ui_options["support-button-html"]) > 0 %}

Caveat: The owner of this server has chosen to inject their own content into the "spots" page. This is designed for a "donate" or "support this server" button. The functionality of this injected content is the responsibility of the server owner, rather than the Spothole software.

{% end %} @@ -67,7 +69,7 @@

This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.

- + {% end %} \ No newline at end of file diff --git a/templates/add_spot.html b/templates/add_spot.html index 1db935c..d881d1b 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -69,8 +69,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/alerts.html b/templates/alerts.html index 1d46221..4eb2dd7 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -50,14 +50,28 @@ +
+ {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} +
+
+
+ {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} +
+
+ {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} +
+
+
+
+
- - + + {% end %} \ No newline at end of file diff --git a/templates/bands.html b/templates/bands.html index 2f1da40..a5ed41c 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -55,6 +55,20 @@ +
+ {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} +
+
+
+ {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} +
+
+ {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} +
+
+
+
+
@@ -62,9 +76,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 8674e44..d379c93 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,7 +24,7 @@ Spothole - + @@ -46,9 +46,9 @@ crossorigin="anonymous"> - - - + + + diff --git a/templates/cards/hamqth.html b/templates/cards/hamqth.html new file mode 100644 index 0000000..3742113 --- /dev/null +++ b/templates/cards/hamqth.html @@ -0,0 +1,27 @@ +
+
+
HamQTH
+
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ See Privacy for more information. +
+
+
+
diff --git a/templates/cards/qrz.html b/templates/cards/qrz.html new file mode 100644 index 0000000..0f72bb6 --- /dev/null +++ b/templates/cards/qrz.html @@ -0,0 +1,27 @@ +
+
+
QRZ.com
+
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ See Privacy for more information. +
+
+
+
diff --git a/templates/conditions.html b/templates/conditions.html index f532dd4..8c2632a 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -230,8 +230,8 @@ - - + + diff --git a/templates/map.html b/templates/map.html index 1d2cab8..ebc21f3 100644 --- a/templates/map.html +++ b/templates/map.html @@ -59,6 +59,20 @@ + +
+ {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} +
+
+
+ {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} +
+
+ {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} +
+
+
+
@@ -79,9 +93,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/spots.html b/templates/spots.html index 8001cd5..11b2cfd 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -62,12 +62,6 @@
{% module Template("cards/number-of-spots.html", web_ui_options=web_ui_options) %}
-
- {% module Template("cards/location.html", web_ui_options=web_ui_options) %} -
-
- {% module Template("cards/worked-calls.html", web_ui_options=web_ui_options) %} -
{% module Template("cards/color-scheme-and-band-color-scheme.html", web_ui_options=web_ui_options) %}
@@ -81,6 +75,26 @@ +
+ {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} +
+
+
+ {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} +
+
+ {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} +
+
+ {% module Template("cards/location.html", web_ui_options=web_ui_options) %} +
+
+ {% module Template("cards/worked-calls.html", web_ui_options=web_ui_options) %} +
+
+
+
+
@@ -90,9 +104,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index 772d453..ec26f90 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,8 +59,8 @@ - - + + diff --git a/templates/widgets/data-area-header.html b/templates/widgets/data-area-header.html new file mode 100644 index 0000000..628bfaf --- /dev/null +++ b/templates/widgets/data-area-header.html @@ -0,0 +1,10 @@ +
+
+
+ Data +
+
+ +
+
+
diff --git a/templates/widgets/filters-display-buttons.html b/templates/widgets/filters-display-buttons.html index f6cf612..54b021e 100644 --- a/templates/widgets/filters-display-buttons.html +++ b/templates/widgets/filters-display-buttons.html @@ -1,4 +1,5 @@
+
\ No newline at end of file diff --git a/webassets/js/alerts.js b/webassets/js/alerts.js index 725de28..d73c6f2 100644 --- a/webassets/js/alerts.js +++ b/webassets/js/alerts.js @@ -33,6 +33,7 @@ function buildQueryString() { if ($("#dxpeditions_skip_max_duration_check")[0].checked) { str = str + "&dxpeditions_skip_max_duration_check=true"; } + str = str + getCredentialQueryString(); return str; } diff --git a/webassets/js/bands.js b/webassets/js/bands.js index 9c227df..568a7eb 100644 --- a/webassets/js/bands.js +++ b/webassets/js/bands.js @@ -34,6 +34,7 @@ function buildQueryString() { 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"; + str = str + getCredentialQueryString(); return str; } diff --git a/webassets/js/common.js b/webassets/js/common.js index 861e444..3706724 100644 --- a/webassets/js/common.js +++ b/webassets/js/common.js @@ -10,15 +10,25 @@ function saveSettings() { if (useLocalStorage) { // Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that // property. For a checkbox, that's the "checked" property. - $(".storeable-checkbox").each(function() { + $(".storeable-checkbox").each(function () { localStorage.setItem("#" + $(this)[0].id + ":checked", JSON.stringify($(this)[0].checked)); }); - $(".storeable-select").each(function() { + $(".storeable-select").each(function () { localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value)); }); - $(".storeable-text").each(function() { + $(".storeable-text").each(function () { localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value)); }); + // Password fields are only saved if the corresponding "remember password" checkbox is ticked. + $(".password-field").each(function () { + var pwKey = "#" + $(this)[0].id + ":value"; + var rememberCheckboxId = $(this).data("remember-checkbox"); + if (rememberCheckboxId && $("#" + rememberCheckboxId)[0] && $("#" + rememberCheckboxId)[0].checked) { + localStorage.setItem(pwKey, JSON.stringify($(this)[0].value)); + } else { + localStorage.removeItem(pwKey); + } + }); } } @@ -26,7 +36,7 @@ function saveSettings() { function loadSettings() { if (useLocalStorage) { // Find all local storage entries and push their data to the corresponding UI element - Object.keys(localStorage).forEach(function(key) { + Object.keys(localStorage).forEach(function (key) { if (key.startsWith("#") && key.includes(":")) { // Split the key back into an element ID and a property var split = key.split(":"); @@ -108,9 +118,9 @@ function getQueryStringFor(parameter) { // For a parameter, such as dx_continent, get the filter options that are currently selected in the UI. function getSelectedFilterOptions(parameter) { - return $(".filter-button-" + parameter).filter(function() { + return $(".filter-button-" + parameter).filter(function () { return this.checked; - }).map(function() { + }).map(function () { return this.value; }).get().join(","); } @@ -118,7 +128,7 @@ function getSelectedFilterOptions(parameter) { // For a parameter, such as dx_continent, return true if all possible options are enabled. (In this case, we don't need // to bother sending this as one of the query parameters to the API; no parameter provided implies "send everything".) function allFilterOptionsSelected(parameter) { - var filter = $(".filter-button-" + parameter).filter(function() { + var filter = $(".filter-button-" + parameter).filter(function () { return !this.checked; }).get(); return filter.length == 0; @@ -137,7 +147,7 @@ function generateMultiToggleFilterCard(elementID, filterQuery, options) { // Method called when "All" or "None" is clicked function toggleFilterButtons(filterQuery, state) { - $(".filter-button-" + filterQuery).each(function() { + $(".filter-button-" + filterQuery).each(function () { $(this).prop('checked', state); }); filtersUpdated(); @@ -218,8 +228,9 @@ function listenForOSThemeChange() { // Panel toggle functions const PANELS = [ - { area: "#filters-area",button: "#filters-button" }, - { area: "#display-area", button: "#display-button" }, + {area: "#filters-area", button: "#filters-button"}, + {area: "#display-area", button: "#display-button"}, + {area: "#data-area", button: "#data-button"}, ]; // Toggle a panel open or closed. If opening, all other visible panels are closed first. @@ -239,18 +250,58 @@ function togglePanel(areaId) { // Close a panel and deactivate its toggle button. function closePanel(areaId) { const panel = PANELS.find(p => p.area === areaId); - if (panel) { $(panel.button).button("toggle"); } + if (panel) { + $(panel.button).button("toggle"); + } $(areaId).hide(); } -function toggleFiltersPanel() { togglePanel("#filters-area"); } -function closeFiltersPanel() { closePanel("#filters-area"); } -function toggleDisplayPanel() { togglePanel("#display-area"); } -function closeDisplayPanel() { closePanel("#display-area"); } +function toggleFiltersPanel() { + togglePanel("#filters-area"); +} + +function closeFiltersPanel() { + closePanel("#filters-area"); +} + +function toggleDisplayPanel() { + togglePanel("#display-area"); +} + +function closeDisplayPanel() { + closePanel("#display-area"); +} + +function toggleDataPanel() { + togglePanel("#data-area"); +} + +function closeDataPanel() { + closePanel("#data-area"); +} + +// Build a query string fragment containing any QRZ.com / HamQTH credentials the user has supplied, +// provided the corresponding "enabled" checkbox is ticked. +function getCredentialQueryString() { + var str = ""; + if ($("#qrz-enabled")[0] && $("#qrz-enabled")[0].checked) { + var qrzUsername = $("#qrz-username").val(); + var qrzPassword = $("#qrz-password").val(); + if (qrzUsername) str += "&qrz_username=" + encodeURIComponent(qrzUsername); + if (qrzPassword) str += "&qrz_password=" + encodeURIComponent(qrzPassword); + } + if ($("#hamqth-enabled")[0] && $("#hamqth-enabled")[0].checked) { + var hamqthUsername = $("#hamqth-username").val(); + var hamqthPassword = $("#hamqth-password").val(); + if (hamqthUsername) str += "&hamqth_username=" + encodeURIComponent(hamqthUsername); + if (hamqthPassword) str += "&hamqth_password=" + encodeURIComponent(hamqthPassword); + } + return str; +} // Startup -$(document).ready(function() { +$(document).ready(function () { usePreferredTheme(); listenForOSThemeChange(); }); diff --git a/webassets/js/map.js b/webassets/js/map.js index 785a6c3..9db5466 100644 --- a/webassets/js/map.js +++ b/webassets/js/map.js @@ -49,6 +49,7 @@ function buildQueryString() { str = str + "max_age=" + $("#max-spot-age option:selected").val(); // Additional filters for the map view: No dupes, no QRT, only spots with good locations str = str + "&dedupe=true&allow_qrt=false&needs_good_location=true"; + str = str + getCredentialQueryString(); return str; } diff --git a/webassets/js/spots.js b/webassets/js/spots.js index 93720e4..85e4068 100644 --- a/webassets/js/spots.js +++ b/webassets/js/spots.js @@ -89,6 +89,7 @@ function buildQueryString() { if ($("#search").val() != "") { str = str + "&text_includes=" + encodeURIComponent($("#search").val()); } + str = str + getCredentialQueryString(); return str; }