From 62b3414d29c830bcad1a097d0507e478529f8af3 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Tue, 4 Aug 2026 22:11:34 +0100 Subject: [PATCH] Refactor of caching & data storage part 17 #118 --- core/config.py | 2 + core/live_data_cache.py | 2 +- core/status_reporter.py | 2 +- providers/alert/http_alert_provider.py | 2 +- .../file_download_callsign_data_provider.py | 2 +- providers/callsigndata/hamqth.py | 4 +- providers/callsigndata/qrz.py | 3 +- .../file_download_sig_ref_data_provider.py | 2 +- providers/solarconditions/giroionosonde.py | 2 +- .../http_solar_conditions_provider.py | 2 +- providers/solarconditions/kc2gprop.py | 2 +- providers/spot/aprsis.py | 2 +- providers/spot/dxcluster.py | 2 +- providers/spot/http_spot_provider.py | 2 +- providers/spot/rbn.py | 2 +- providers/spot/sse_spot_provider.py | 73 ++++++++++++------- providers/spot/websocket_spot_provider.py | 2 +- .../file_download_static_data_provider.py | 2 +- server/handlers/api/options.py | 8 +- static/apidocs/openapi.yml | 22 +++++- static/js/alerts.js | 2 +- static/js/bands.js | 2 +- static/js/map.js | 2 +- static/js/spots.js | 2 +- static/js/status.js | 12 +-- templates/alerts.html | 9 ++- templates/bands.html | 9 ++- templates/map.html | 9 ++- templates/spots.html | 7 +- templates/status.html | 6 +- .../widgets/filters-display-data-buttons.html | 2 + 31 files changed, 135 insertions(+), 67 deletions(-) diff --git a/core/config.py b/core/config.py index 804b4a8..128ac96 100644 --- a/core/config.py +++ b/core/config.py @@ -30,6 +30,8 @@ LOG_WEB_REQUESTS = config.get("log-web-requests", False) # but for consistency we provide this to the front-end in web-ui-options because it has no impact outside of the web UI. WEB_UI_OPTIONS["spot-providers-enabled-by-default"] = [p["name"] for p in config["spot-providers"] if p["enabled"] and ( "enabled-by-default-in-web-ui" not in p or p["enabled-by-default-in-web-ui"])] +WEB_UI_OPTIONS["qrz-enabled"] = any(p["class"] == "QRZ" and p["enabled"] for p in config["callsign-data-providers"]) +WEB_UI_OPTIONS["hamqth-enabled"] = any(p["class"] == "HamQTH" and p["enabled"] for p in config["callsign-data-providers"]) # If spotting to this server is enabled, "API" is another valid spot source even though it does not come from # one of our proviers. We set that to also be enabled by default. if ALLOW_SPOTTING: diff --git a/core/live_data_cache.py b/core/live_data_cache.py index 6afbcec..935078b 100644 --- a/core/live_data_cache.py +++ b/core/live_data_cache.py @@ -92,7 +92,7 @@ class LiveDataCache: time.sleep(interval) self.save_snapshot() - t = threading.Thread(target=loop, daemon=True, name=f"snapshot-{self._snapshot_dir}") + t = threading.Thread(target=loop, name=f"LiveDataCache-Snapshot-{self._snapshot_dir}") t.start() def close(self): diff --git a/core/status_reporter.py b/core/status_reporter.py index 47211d5..7da2e4f 100644 --- a/core/status_reporter.py +++ b/core/status_reporter.py @@ -30,7 +30,7 @@ class StatusReporter: def start(self): """Start the reporter thread""" - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name="StatusReporter") self._thread.start() def stop(self): diff --git a/providers/alert/http_alert_provider.py b/providers/alert/http_alert_provider.py index 1585741..1994a60 100644 --- a/providers/alert/http_alert_provider.py +++ b/providers/alert/http_alert_provider.py @@ -25,7 +25,7 @@ class HTTPAlertProvider(AlertProvider): # Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # subsequent polls, so start() returns immediately and the application can continue starting. logging.info("Set up query of " + self.name + " alert API every " + str(self._poll_interval) + " seconds.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name=f"HTTPAlertProvider-{self.name}") self._thread.start() def stop(self): diff --git a/providers/callsigndata/file_download_callsign_data_provider.py b/providers/callsigndata/file_download_callsign_data_provider.py index ddab6cb..4145147 100644 --- a/providers/callsigndata/file_download_callsign_data_provider.py +++ b/providers/callsigndata/file_download_callsign_data_provider.py @@ -32,7 +32,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider): # subsequent polls, so start() returns immediately and the application can continue starting. logging.info( "Set up query of " + self.name + " callsign reference data every " + str(self._poll_interval) + " days.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name=f"FileDownloadCallsignDataProvider-{self.name}") self._thread.start() def stop(self): diff --git a/providers/callsigndata/hamqth.py b/providers/callsigndata/hamqth.py index 596f513..e214cf9 100644 --- a/providers/callsigndata/hamqth.py +++ b/providers/callsigndata/hamqth.py @@ -77,11 +77,11 @@ class HamQTH(APIQueryCallsignDataProvider): self._HAMQTH_BASE_URL + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus( lookup_call) + "&prg=" + self._PRG, headers=HTTP_HEADERS, timeout=10) if response.ok: + # Found data, convert it to our object and return it + data = xmltodict.parse(response.content)["HamQTH"]["search"] self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) self.lookup_count += 1 - # Found data, convert it to our object and return it - data = xmltodict.parse(response.content)["HamQTH"]["search"] return self.hamqth_response_to_callsign(callsign, data) elif not response.from_cache: diff --git a/providers/callsigndata/qrz.py b/providers/callsigndata/qrz.py index a1ea0f5..7e6a08a 100644 --- a/providers/callsigndata/qrz.py +++ b/providers/callsigndata/qrz.py @@ -79,11 +79,12 @@ class QRZ(APIQueryCallsignDataProvider): qrz_response = xmltodict.parse(response.content).get("QRZDatabase", {}) if qrz_response: if "Callsign" in qrz_response: + qrz_data = qrz_response.get("Callsign") self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) self.lookup_count += 1 # Found data, convert it to our object and return it - return self.qrz_response_to_callsign(callsign, qrz_response.get("Callsign")) + return self.qrz_response_to_callsign(callsign, qrz_data) elif "Session" in qrz_response and "Error" in qrz_response.get("Session"): # Errors here are normally just "callsign not in database", no need to log that ourselves diff --git a/providers/sigrefdata/file_download_sig_ref_data_provider.py b/providers/sigrefdata/file_download_sig_ref_data_provider.py index b902742..aafe82d 100644 --- a/providers/sigrefdata/file_download_sig_ref_data_provider.py +++ b/providers/sigrefdata/file_download_sig_ref_data_provider.py @@ -28,7 +28,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider): # subsequent polls, so start() returns immediately and the application can continue starting. logging.info( "Set up query of " + self.sig_name + " SIG ref data every " + str(self._poll_interval) + " days.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name=f"FileDownloadSIGRefDataProvider-{self.sig_name}") self._thread.start() def stop(self): diff --git a/providers/solarconditions/giroionosonde.py b/providers/solarconditions/giroionosonde.py index 8a32891..d832df1 100644 --- a/providers/solarconditions/giroionosonde.py +++ b/providers/solarconditions/giroionosonde.py @@ -58,7 +58,7 @@ class GIROIonosonde(SolarConditionsProvider): def start(self): logging.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name="GIROIonosondeDataProvider") self._thread.start() def stop(self): diff --git a/providers/solarconditions/http_solar_conditions_provider.py b/providers/solarconditions/http_solar_conditions_provider.py index 74cc5e3..342e459 100644 --- a/providers/solarconditions/http_solar_conditions_provider.py +++ b/providers/solarconditions/http_solar_conditions_provider.py @@ -24,7 +24,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider): def start(self): logging.info( "Set up query of " + self.name + " solar conditions API every " + str(self._poll_interval) + " seconds.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name=f"HTTPSolarConditionsProvider-{self.name}") self._thread.start() def stop(self): diff --git a/providers/solarconditions/kc2gprop.py b/providers/solarconditions/kc2gprop.py index 9753c47..0330c70 100644 --- a/providers/solarconditions/kc2gprop.py +++ b/providers/solarconditions/kc2gprop.py @@ -30,7 +30,7 @@ class KC2GProp(SolarConditionsProvider): def start(self): logging.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name="KC2GPropProvider") self._thread.start() def stop(self): diff --git a/providers/spot/aprsis.py b/providers/spot/aprsis.py index b9e2b31..0173632 100644 --- a/providers/spot/aprsis.py +++ b/providers/spot/aprsis.py @@ -15,7 +15,7 @@ class APRSIS(SpotProvider): def __init__(self, provider_config): super().__init__("APRS-IS", provider_config) - self._thread = Thread(target=self._connect) + self._thread = Thread(target=self._connect, name="APRSISSpotProvider") self._thread.daemon = True self._aprsis = None diff --git a/providers/spot/dxcluster.py b/providers/spot/dxcluster.py index f14c0a5..6f39b26 100644 --- a/providers/spot/dxcluster.py +++ b/providers/spot/dxcluster.py @@ -36,7 +36,7 @@ class DXCluster(SpotProvider): self._allow_rbn_spots = provider_config["allow_rbn_spots"] if "allow_rbn_spots" in provider_config else False self._spot_line_pattern = self._LINE_PATTERN_ALLOW_RBN if self._allow_rbn_spots else self._LINE_PATTERN_EXCLUDE_RBN self._telnet = None - self._thread = Thread(target=self._handle) + self._thread = Thread(target=self._handle, name=f"DXClusterSpotProvider-{self.name}") self._thread.daemon = True self._running = True diff --git a/providers/spot/http_spot_provider.py b/providers/spot/http_spot_provider.py index 4edb2c5..bbe1477 100644 --- a/providers/spot/http_spot_provider.py +++ b/providers/spot/http_spot_provider.py @@ -26,7 +26,7 @@ class HTTPSpotProvider(SpotProvider): # Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # subsequent polls, so start() returns immediately and the application can continue starting. logging.info("Set up query of " + self.name + " spot API every " + str(self._poll_interval) + " seconds.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name=f"HTTPSpotProvider-{self.name}") self._thread.start() def stop(self): diff --git a/providers/spot/rbn.py b/providers/spot/rbn.py index 9c958e6..050c4a8 100644 --- a/providers/spot/rbn.py +++ b/providers/spot/rbn.py @@ -27,7 +27,7 @@ class RBN(SpotProvider): super().__init__(name, provider_config) self._port = provider_config["port"] self._telnet = None - self._thread = Thread(target=self._handle) + self._thread = Thread(target=self._handle, name=f"RBNSpotProvider-{self.name}") self._thread.daemon = True self._running = True diff --git a/providers/spot/sse_spot_provider.py b/providers/spot/sse_spot_provider.py index 991b107..4ed48ad 100644 --- a/providers/spot/sse_spot_provider.py +++ b/providers/spot/sse_spot_provider.py @@ -1,7 +1,6 @@ import logging from datetime import datetime -from threading import Thread -from time import sleep +from threading import Event, Lock, Thread import pytz from requests_sse import EventSource @@ -16,24 +15,35 @@ class SSESpotProvider(SpotProvider): def __init__(self, name, provider_config, url): super().__init__(name, provider_config) self._url = url - self._event_source = None self._thread = None - self._stopped = False self._last_event_id = None + self._stop_event = Event() + self._event_source_lock = Lock() + self._event_source = None def start(self): logging.info("Set up SSE connection to " + self.name + " spot API.") - self._stopped = False - self._thread = Thread(target=self._run) + self._stop_event.clear() + self._thread = Thread(target=self._run, name=f"SSESpotProvider-{self.name}") self._thread.daemon = True self._thread.start() def stop(self): - self._stopped = True - if self._event_source: - self._event_source.close() + self._stop_event.set() + + with self._event_source_lock: + event_source = self._event_source + if event_source: + try: + event_source.close() + except Exception: + logging.exception( + "Exception closing SSE connection for " + self.name + " during stop()") + if self._thread: - self._thread.join() + self._thread.join(timeout=15) + if self._thread.is_alive(): + logging.warning(self.name + " SSE worker thread did not exit on time and will be killed.") def _on_open(self): self.status = "Waiting for Data" @@ -41,36 +51,45 @@ class SSESpotProvider(SpotProvider): def _on_error(self): self.status = "Connecting" + def _set_event_source(self, event_source): + with self._event_source_lock: + self._event_source = event_source + def _run(self): - while not self._stopped: + while not self._stop_event.is_set(): try: logging.debug("Connecting to " + self.name + " spot API...") self.status = "Connecting" - with EventSource(self._url, headers=HTTP_HEADERS, latest_event_id=self._last_event_id, timeout=30, + with EventSource(self._url, headers=HTTP_HEADERS, latest_event_id=self._last_event_id, timeout=10, on_open=self._on_open, on_error=self._on_error) as event_source: - self._event_source = event_source - for event in self._event_source: - if event.type == 'message': - try: - self._last_event_id = event.last_event_id - new_spot = self._sse_message_to_spot(event.data) - if new_spot: - self._submit(new_spot) + self._set_event_source(event_source) + try: + for event in event_source: + if self._stop_event.is_set(): + break + if event.type == 'message': + try: + self._last_event_id = event.last_event_id + new_spot = self._sse_message_to_spot(event.data) + if new_spot: + self._submit(new_spot) - self.status = "OK" - self.last_update_time = datetime.now(pytz.UTC) - logging.debug("Received data from " + self.name + " spot API.") + self.status = "OK" + self.last_update_time = datetime.now(pytz.UTC) + logging.debug("Received data from " + self.name + " spot API.") - except Exception: - logging.exception( - "Exception processing message from SSE Spot Provider (" + self.name + ")") + except Exception: + logging.exception( + "Exception processing message from SSE Spot Provider (" + self.name + ")") + finally: + self._set_event_source(None) except Exception: self.status = "Error" logging.exception("Exception in SSE Spot Provider (" + self.name + ")") else: self.status = "Disconnected" - sleep(5) # Wait before trying to reconnect + self._stop_event.wait(timeout=5) # Wait before trying to reconnect def _sse_message_to_spot(self, message_data): """Convert an SSE message received from the API into a spot. The whole message data is provided here so the subclass diff --git a/providers/spot/websocket_spot_provider.py b/providers/spot/websocket_spot_provider.py index 48ee86a..d0362b9 100644 --- a/providers/spot/websocket_spot_provider.py +++ b/providers/spot/websocket_spot_provider.py @@ -24,7 +24,7 @@ class WebsocketSpotProvider(SpotProvider): def start(self): logging.info("Set up websocket connection to " + self.name + " spot API.") self._stopped = False - self._thread = Thread(target=self._run) + self._thread = Thread(target=self._run, name=f"WebsocketSpotProvider-{self.name}") self._thread.daemon = True self._thread.start() diff --git a/providers/staticdata/file_download_static_data_provider.py b/providers/staticdata/file_download_static_data_provider.py index ef10c98..d4cd403 100644 --- a/providers/staticdata/file_download_static_data_provider.py +++ b/providers/staticdata/file_download_static_data_provider.py @@ -29,7 +29,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider): # subsequent polls, so start() returns immediately and the application can continue starting. logging.info( "Set up query of " + self.name + " static reference data every " + str(self._poll_interval) + " days.") - self._thread = Thread(target=self._run, daemon=True) + self._thread = Thread(target=self._run, name=f"FileDownloadStaticDataProvider-{self.name}") self._thread.start() def stop(self): diff --git a/server/handlers/api/options.py b/server/handlers/api/options.py index 1ec6c00..a351f64 100644 --- a/server/handlers/api/options.py +++ b/server/handlers/api/options.py @@ -38,10 +38,12 @@ class APIOptionsHandler(tornado.web.RequestHandler): "mode_types": MODE_TYPES, "sigs": SIGS, # Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle things that aren't even available. - "spot_sources": list( + "spot_providers": list( map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["spot_providers"]))), - "alert_sources": list( + "alert_providers": list( map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["alert_providers"]))), + "callsign_data_providers": list( + map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["callsign_data_providers"]))), "continents": CONTINENTS, "propagation_modes": list(PROPAGATION_MODES.values()), "max_spot_age": MAX_SPOT_AGE, @@ -49,7 +51,7 @@ class APIOptionsHandler(tornado.web.RequestHandler): # If spotting to this server is enabled, "API" is another valid spot source even though it does not come from # one of our proviers. if ALLOW_SPOTTING: - options["spot_sources"].append("API") + options["spot_providers"].append("API") self.write(safe_json_dumps(options)) self.set_status(200) diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 2c60731..2a235b2 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -17,7 +17,11 @@ info: ### 2.0 - * Added `sig_ref_data_providers`, `static_data_providers` and `callsign_data_providers` to status and removed `cleanup` + * Added `sig_ref_data_providers`, `static_data_providers` and `callsign_data_providers` to `/status` response + * Added `callsign_data_providers` to `/options` response + * BREAKING: Removed `cleanup` from `/status` response + * BREAKING: in the `/options` response, renamed `spot_sources` and `alert_sources` to `spot_providers` and + `alert_providers` ### 1.4 @@ -1934,12 +1938,24 @@ components: description: An array of all the supported Special Interest Groups. items: $ref: '#/components/schemas/SIG' - sources: + spot_providers: type: array - description: An array of all the supported data sources. + description: An array of all the supported spot data sources. items: type: string example: "Cluster" + alert_providers: + type: array + description: An array of all the supported alert data sources. + items: + type: string + example: "POTA" + callsign_data_providers: + type: array + description: An array of all the supported callsign lookup providers. + items: + type: string + example: "QRZ.com" continents: type: array description: An array of all the supported continents. diff --git a/static/js/alerts.js b/static/js/alerts.js index 463dc56..fd18480 100644 --- a/static/js/alerts.js +++ b/static/js/alerts.js @@ -288,7 +288,7 @@ function loadOptions() { // Populate the filters panel generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); - generateMultiToggleFilterCard("#source-options", "source", options["alert_sources"]); + generateMultiToggleFilterCard("#source-options", "source", options["alert_providers"]); // Load URL params. These may select things from the various filter & display options, so the function needs // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress diff --git a/static/js/bands.js b/static/js/bands.js index 9166ce5..b83662c 100644 --- a/static/js/bands.js +++ b/static/js/bands.js @@ -291,7 +291,7 @@ function loadOptions() { generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateModesMultiToggleFilterCard(options["modes"]); - generateSourcesMultiToggleFilterCard(options["spot_sources"], spotProvidersEnabledByDefault); + generateSourcesMultiToggleFilterCard(options["spot_providers"], spotProvidersEnabledByDefault); // Load URL params. These may select things from the various filter & display options, so the function needs // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress diff --git a/static/js/map.js b/static/js/map.js index c6b0f1e..2420889 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -322,7 +322,7 @@ function loadOptions() { generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateModesMultiToggleFilterCard(options["modes"]); - generateSourcesMultiToggleFilterCard(options["spot_sources"], spotProvidersEnabledByDefault); + generateSourcesMultiToggleFilterCard(options["spot_providers"], spotProvidersEnabledByDefault); // Load URL params. These may select things from the various filter & display options, so the function needs // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress diff --git a/static/js/spots.js b/static/js/spots.js index c0d4375..d0f14a7 100644 --- a/static/js/spots.js +++ b/static/js/spots.js @@ -436,7 +436,7 @@ function loadOptions() { generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateModesMultiToggleFilterCard(options["modes"]); - generateSourcesMultiToggleFilterCard(options["spot_sources"], spotProvidersEnabledByDefault); + generateSourcesMultiToggleFilterCard(options["spot_providers"], spotProvidersEnabledByDefault); // Load URL params. These may select things from the various filter & display options, so the function needs // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress diff --git a/static/js/status.js b/static/js/status.js index 5994265..5a380ed 100644 --- a/static/js/status.js +++ b/static/js/status.js @@ -14,7 +14,7 @@ function loadStatus() { jsonData["spot_providers"].forEach(p => { $("#spot-providers-status-container").append(` -
+
${p["name"]}
Status: ${p["status"]}
Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}
@@ -24,7 +24,7 @@ function loadStatus() { jsonData["alert_providers"].forEach(p => { $("#alert-providers-status-container").append(` -
+
${p["name"]}
Status: ${p["status"]}
Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}
@@ -33,7 +33,7 @@ function loadStatus() { jsonData["solar_condition_providers"].forEach(p => { $("#condition-providers-status-container").append(` -
+
${p["name"]}
Status: ${p["status"]}
Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}
@@ -42,7 +42,7 @@ function loadStatus() { jsonData["static_data_providers"].forEach(p => { $("#static-data-providers-status-container").append(` -
+
${p["name"]}
Status: ${p["status"]}
Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}
@@ -51,7 +51,7 @@ function loadStatus() { jsonData["sig_ref_data_providers"].forEach(p => { $("#sig-ref-data-providers-status-container").append(` -
+
${p["sig_name"]}
Status: ${p["status"]}
Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}
@@ -61,7 +61,7 @@ function loadStatus() { jsonData["callsign_data_providers"].forEach(p => { $("#callsign-data-providers-status-container").append(` -
+
${p["name"]}
Status: ${p["status"]}
Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}
diff --git a/templates/alerts.html b/templates/alerts.html index 453ffd0..ede5a6c 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -8,7 +8,8 @@
- {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options) %} + {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options, + show_data_button=web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"]) %}
@@ -50,19 +51,25 @@
+ {% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
+ {% if web_ui_options["qrz-enabled"] %}
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
+ {% end %} + {% if web_ui_options["hamqth-enabled"] %}
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
+ {% end %}
+ {% end %}
diff --git a/templates/bands.html b/templates/bands.html index 5d839d4..d7b0857 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -6,7 +6,8 @@
- {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options) %} + {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options, + show_data_button=web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"]) %}
@@ -54,19 +55,25 @@ + {% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
+ {% if web_ui_options["qrz-enabled"] %}
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
+ {% end %} + {% if web_ui_options["hamqth-enabled"] %}
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
+ {% end %}
+ {% end %}
diff --git a/templates/map.html b/templates/map.html index 0b94bfd..cb58df4 100644 --- a/templates/map.html +++ b/templates/map.html @@ -20,7 +20,8 @@
- {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options) %} + {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options, + show_data_button=web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"]) %}
@@ -74,19 +75,25 @@ + {% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
+ {% if web_ui_options["qrz-enabled"] %}
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
+ {% end %} + {% if web_ui_options["hamqth-enabled"] %}
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
+ {% end %}
+ {% end %} diff --git a/templates/spots.html b/templates/spots.html index e33c181..6314eb6 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -25,7 +25,8 @@
{% module Template("widgets/search.html", web_ui_options=web_ui_options) %} - {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options) %} + {% module Template("widgets/filters-display-data-buttons.html", web_ui_options=web_ui_options, + show_data_button=web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"]) %}
@@ -86,12 +87,16 @@ {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
+ {% if web_ui_options["qrz-enabled"] %}
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
+ {% end %} + {% if web_ui_options["hamqth-enabled"] %}
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
+ {% end %}
{% module Template("cards/location.html", web_ui_options=web_ui_options) %}
diff --git a/templates/status.html b/templates/status.html index f396a9d..bd06dcc 100644 --- a/templates/status.html +++ b/templates/status.html @@ -6,19 +6,19 @@ Spothole
-
+
Metadata
Software Version:
Owner Callsign:
Up since:
-
+
Performance
Memory Use:
Total Spots:
Total Alerts:
-
+
Web Server
Status:
Last API call:
diff --git a/templates/widgets/filters-display-data-buttons.html b/templates/widgets/filters-display-data-buttons.html index 9f66f30..f48f807 100644 --- a/templates/widgets/filters-display-data-buttons.html +++ b/templates/widgets/filters-display-data-buttons.html @@ -5,7 +5,9 @@ + {% if show_data_button %} + {% end %}
\ No newline at end of file