Refactor of caching & data storage part 17 #118

This commit is contained in:
Ian Renton
2026-08-04 22:11:34 +01:00
parent 459d999f57
commit 62b3414d29
31 changed files with 135 additions and 67 deletions
+2
View File
@@ -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. # 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 ( 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"])] "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 # 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. # one of our proviers. We set that to also be enabled by default.
if ALLOW_SPOTTING: if ALLOW_SPOTTING:
+1 -1
View File
@@ -92,7 +92,7 @@ class LiveDataCache:
time.sleep(interval) time.sleep(interval)
self.save_snapshot() 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() t.start()
def close(self): def close(self):
+1 -1
View File
@@ -30,7 +30,7 @@ class StatusReporter:
def start(self): def start(self):
"""Start the reporter thread""" """Start the reporter thread"""
self._thread = Thread(target=self._run, daemon=True) self._thread = Thread(target=self._run, name="StatusReporter")
self._thread.start() self._thread.start()
def stop(self): def stop(self):
+1 -1
View File
@@ -25,7 +25,7 @@ class HTTPAlertProvider(AlertProvider):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # 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.") 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() self._thread.start()
def stop(self): def stop(self):
@@ -32,7 +32,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
# subsequent polls, so start() returns immediately and the application can continue starting. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info( logging.info(
"Set up query of " + self.name + " callsign reference data every " + str(self._poll_interval) + " days.") "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() self._thread.start()
def stop(self): def stop(self):
+2 -2
View File
@@ -77,11 +77,11 @@ class HamQTH(APIQueryCallsignDataProvider):
self._HAMQTH_BASE_URL + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus( self._HAMQTH_BASE_URL + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
lookup_call) + "&prg=" + self._PRG, headers=HTTP_HEADERS, timeout=10) lookup_call) + "&prg=" + self._PRG, headers=HTTP_HEADERS, timeout=10)
if response.ok: if response.ok:
# Found data, convert it to our object and return it
data = xmltodict.parse(response.content)["HamQTH"]["search"]
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
self.lookup_count += 1 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) return self.hamqth_response_to_callsign(callsign, data)
elif not response.from_cache: elif not response.from_cache:
+2 -1
View File
@@ -79,11 +79,12 @@ class QRZ(APIQueryCallsignDataProvider):
qrz_response = xmltodict.parse(response.content).get("QRZDatabase", {}) qrz_response = xmltodict.parse(response.content).get("QRZDatabase", {})
if qrz_response: if qrz_response:
if "Callsign" in qrz_response: if "Callsign" in qrz_response:
qrz_data = qrz_response.get("Callsign")
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
self.lookup_count += 1 self.lookup_count += 1
# Found data, convert it to our object and return it # 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"): 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 # Errors here are normally just "callsign not in database", no need to log that ourselves
@@ -28,7 +28,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
# subsequent polls, so start() returns immediately and the application can continue starting. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info( logging.info(
"Set up query of " + self.sig_name + " SIG ref data every " + str(self._poll_interval) + " days.") "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() self._thread.start()
def stop(self): def stop(self):
+1 -1
View File
@@ -58,7 +58,7 @@ class GIROIonosonde(SolarConditionsProvider):
def start(self): def start(self):
logging.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.") 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() self._thread.start()
def stop(self): def stop(self):
@@ -24,7 +24,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
def start(self): def start(self):
logging.info( logging.info(
"Set up query of " + self.name + " solar conditions API every " + str(self._poll_interval) + " seconds.") "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() self._thread.start()
def stop(self): def stop(self):
+1 -1
View File
@@ -30,7 +30,7 @@ class KC2GProp(SolarConditionsProvider):
def start(self): def start(self):
logging.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.") 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() self._thread.start()
def stop(self): def stop(self):
+1 -1
View File
@@ -15,7 +15,7 @@ class APRSIS(SpotProvider):
def __init__(self, provider_config): def __init__(self, provider_config):
super().__init__("APRS-IS", 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._thread.daemon = True
self._aprsis = None self._aprsis = None
+1 -1
View File
@@ -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._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._spot_line_pattern = self._LINE_PATTERN_ALLOW_RBN if self._allow_rbn_spots else self._LINE_PATTERN_EXCLUDE_RBN
self._telnet = None 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._thread.daemon = True
self._running = True self._running = True
+1 -1
View File
@@ -26,7 +26,7 @@ class HTTPSpotProvider(SpotProvider):
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between # 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. # 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.") 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() self._thread.start()
def stop(self): def stop(self):
+1 -1
View File
@@ -27,7 +27,7 @@ class RBN(SpotProvider):
super().__init__(name, provider_config) super().__init__(name, provider_config)
self._port = provider_config["port"] self._port = provider_config["port"]
self._telnet = None 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._thread.daemon = True
self._running = True self._running = True
+46 -27
View File
@@ -1,7 +1,6 @@
import logging import logging
from datetime import datetime from datetime import datetime
from threading import Thread from threading import Event, Lock, Thread
from time import sleep
import pytz import pytz
from requests_sse import EventSource from requests_sse import EventSource
@@ -16,24 +15,35 @@ class SSESpotProvider(SpotProvider):
def __init__(self, name, provider_config, url): def __init__(self, name, provider_config, url):
super().__init__(name, provider_config) super().__init__(name, provider_config)
self._url = url self._url = url
self._event_source = None
self._thread = None self._thread = None
self._stopped = False
self._last_event_id = None self._last_event_id = None
self._stop_event = Event()
self._event_source_lock = Lock()
self._event_source = None
def start(self): def start(self):
logging.info("Set up SSE connection to " + self.name + " spot API.") logging.info("Set up SSE connection to " + self.name + " spot API.")
self._stopped = False self._stop_event.clear()
self._thread = Thread(target=self._run) self._thread = Thread(target=self._run, name=f"SSESpotProvider-{self.name}")
self._thread.daemon = True self._thread.daemon = True
self._thread.start() self._thread.start()
def stop(self): def stop(self):
self._stopped = True self._stop_event.set()
if self._event_source:
self._event_source.close() 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: 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): def _on_open(self):
self.status = "Waiting for Data" self.status = "Waiting for Data"
@@ -41,36 +51,45 @@ class SSESpotProvider(SpotProvider):
def _on_error(self): def _on_error(self):
self.status = "Connecting" self.status = "Connecting"
def _set_event_source(self, event_source):
with self._event_source_lock:
self._event_source = event_source
def _run(self): def _run(self):
while not self._stopped: while not self._stop_event.is_set():
try: try:
logging.debug("Connecting to " + self.name + " spot API...") logging.debug("Connecting to " + self.name + " spot API...")
self.status = "Connecting" 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: on_open=self._on_open, on_error=self._on_error) as event_source:
self._event_source = event_source self._set_event_source(event_source)
for event in self._event_source: try:
if event.type == 'message': for event in event_source:
try: if self._stop_event.is_set():
self._last_event_id = event.last_event_id break
new_spot = self._sse_message_to_spot(event.data) if event.type == 'message':
if new_spot: try:
self._submit(new_spot) 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.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Received data from " + self.name + " spot API.") logging.debug("Received data from " + self.name + " spot API.")
except Exception: except Exception:
logging.exception( logging.exception(
"Exception processing message from SSE Spot Provider (" + self.name + ")") "Exception processing message from SSE Spot Provider (" + self.name + ")")
finally:
self._set_event_source(None)
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in SSE Spot Provider (" + self.name + ")") logging.exception("Exception in SSE Spot Provider (" + self.name + ")")
else: else:
self.status = "Disconnected" 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): 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 """Convert an SSE message received from the API into a spot. The whole message data is provided here so the subclass
+1 -1
View File
@@ -24,7 +24,7 @@ class WebsocketSpotProvider(SpotProvider):
def start(self): def start(self):
logging.info("Set up websocket connection to " + self.name + " spot API.") logging.info("Set up websocket connection to " + self.name + " spot API.")
self._stopped = False 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.daemon = True
self._thread.start() self._thread.start()
@@ -29,7 +29,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
# subsequent polls, so start() returns immediately and the application can continue starting. # subsequent polls, so start() returns immediately and the application can continue starting.
logging.info( logging.info(
"Set up query of " + self.name + " static reference data every " + str(self._poll_interval) + " days.") "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() self._thread.start()
def stop(self): def stop(self):
+5 -3
View File
@@ -38,10 +38,12 @@ class APIOptionsHandler(tornado.web.RequestHandler):
"mode_types": MODE_TYPES, "mode_types": MODE_TYPES,
"sigs": SIGS, "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/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"]))), 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"]))), 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, "continents": CONTINENTS,
"propagation_modes": list(PROPAGATION_MODES.values()), "propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE, "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 # If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our proviers. # one of our proviers.
if ALLOW_SPOTTING: if ALLOW_SPOTTING:
options["spot_sources"].append("API") options["spot_providers"].append("API")
self.write(safe_json_dumps(options)) self.write(safe_json_dumps(options))
self.set_status(200) self.set_status(200)
+19 -3
View File
@@ -17,7 +17,11 @@ info:
### 2.0 ### 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 ### 1.4
@@ -1934,12 +1938,24 @@ components:
description: An array of all the supported Special Interest Groups. description: An array of all the supported Special Interest Groups.
items: items:
$ref: '#/components/schemas/SIG' $ref: '#/components/schemas/SIG'
sources: spot_providers:
type: array type: array
description: An array of all the supported data sources. description: An array of all the supported spot data sources.
items: items:
type: string type: string
example: "Cluster" 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: continents:
type: array type: array
description: An array of all the supported continents. description: An array of all the supported continents.
+1 -1
View File
@@ -288,7 +288,7 @@ function loadOptions() {
// Populate the filters panel // Populate the filters panel
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); 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 // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
+1 -1
View File
@@ -291,7 +291,7 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]); 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 // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
+1 -1
View File
@@ -322,7 +322,7 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]); 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 // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
+1 -1
View File
@@ -436,7 +436,7 @@ function loadOptions() {
generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]); generateMultiToggleFilterCard("#dx-continent-options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]); generateMultiToggleFilterCard("#de-continent-options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]); 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 // 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 // to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
+6 -6
View File
@@ -14,7 +14,7 @@ function loadStatus() {
jsonData["spot_providers"].forEach(p => { jsonData["spot_providers"].forEach(p => {
$("#spot-providers-status-container").append(` $("#spot-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["name"]}</strong></div> <div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div> <div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div> <div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
@@ -24,7 +24,7 @@ function loadStatus() {
jsonData["alert_providers"].forEach(p => { jsonData["alert_providers"].forEach(p => {
$("#alert-providers-status-container").append(` $("#alert-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["name"]}</strong></div> <div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div> <div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div> <div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
@@ -33,7 +33,7 @@ function loadStatus() {
jsonData["solar_condition_providers"].forEach(p => { jsonData["solar_condition_providers"].forEach(p => {
$("#condition-providers-status-container").append(` $("#condition-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["name"]}</strong></div> <div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div> <div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div> <div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
@@ -42,7 +42,7 @@ function loadStatus() {
jsonData["static_data_providers"].forEach(p => { jsonData["static_data_providers"].forEach(p => {
$("#static-data-providers-status-container").append(` $("#static-data-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["name"]}</strong></div> <div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div> <div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div> <div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
@@ -51,7 +51,7 @@ function loadStatus() {
jsonData["sig_ref_data_providers"].forEach(p => { jsonData["sig_ref_data_providers"].forEach(p => {
$("#sig-ref-data-providers-status-container").append(` $("#sig-ref-data-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["sig_name"]}</strong></div> <div class="col"><strong>${p["sig_name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div> <div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div> <div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
@@ -61,7 +61,7 @@ function loadStatus() {
jsonData["callsign_data_providers"].forEach(p => { jsonData["callsign_data_providers"].forEach(p => {
$("#callsign-data-providers-status-container").append(` $("#callsign-data-providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["name"]}</strong></div> <div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div> <div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div> <div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
+8 -1
View File
@@ -8,7 +8,8 @@
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="d-inline-flex gap-1"> <div class="d-inline-flex gap-1">
{% 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"]) %}
</div> </div>
</div> </div>
</div> </div>
@@ -50,19 +51,25 @@
</div> </div>
</div> </div>
{% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
<div id="data-area" class="appearing-panel card mb-3"> <div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body"> <div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4"> <div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
</div> </div>
</div> </div>
</div> </div>
{% end %}
<div id="table-container"> <div id="table-container">
<table id="table" class="table"> <table id="table" class="table">
+8 -1
View File
@@ -6,7 +6,8 @@
<div class="col-auto me-auto pt-3"></div> <div class="col-auto me-auto pt-3"></div>
<div class="col-auto"> <div class="col-auto">
<div class="d-inline-flex gap-1"> <div class="d-inline-flex gap-1">
{% 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"]) %}
</div> </div>
</div> </div>
</div> </div>
@@ -54,19 +55,25 @@
</div> </div>
</div> </div>
{% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
<div id="data-area" class="appearing-panel card mb-3"> <div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body"> <div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4"> <div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
</div> </div>
</div> </div>
</div> </div>
{% end %}
<div id="bands-container"></div> <div id="bands-container"></div>
+8 -1
View File
@@ -20,7 +20,8 @@
<div class="col-auto me-auto pt-3"></div> <div class="col-auto me-auto pt-3"></div>
<div class="col-auto"> <div class="col-auto">
<div class="d-inline-flex gap-1"> <div class="d-inline-flex gap-1">
{% 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"]) %}
</div> </div>
</div> </div>
</div> </div>
@@ -74,19 +75,25 @@
</div> </div>
</div> </div>
{% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
<div id="data-area" class="appearing-panel card mb-3"> <div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body"> <div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4"> <div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
</div> </div>
</div> </div>
</div> </div>
{% end %}
</div> </div>
</div> </div>
+6 -1
View File
@@ -25,7 +25,8 @@
<div class="col-md-8 text-end"> <div class="col-md-8 text-end">
<div class="d-inline-flex gap-3"> <div class="d-inline-flex gap-3">
{% module Template("widgets/search.html", web_ui_options=web_ui_options) %} {% 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"]) %}
</div> </div>
</div> </div>
</div> </div>
@@ -86,12 +87,16 @@
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %} {% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body"> <div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4"> <div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %} {% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col"> <div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %} {% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div> </div>
{% end %}
<div class="col"> <div class="col">
{% module Template("cards/location.html", web_ui_options=web_ui_options) %} {% module Template("cards/location.html", web_ui_options=web_ui_options) %}
</div> </div>
+3 -3
View File
@@ -6,19 +6,19 @@
Spothole Spothole
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>Metadata</strong></div> <div class="col"><strong>Metadata</strong></div>
<div class="col">Software Version: <span id="software-version"></span></div> <div class="col">Software Version: <span id="software-version"></span></div>
<div class="col">Owner Callsign: <span id="server-owner-callsign"></span></div> <div class="col">Owner Callsign: <span id="server-owner-callsign"></span></div>
<div class="col">Up since: <span id="up-since"></span></div> <div class="col">Up since: <span id="up-since"></span></div>
</div> </div>
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>Performance</strong></div> <div class="col"><strong>Performance</strong></div>
<div class="col">Memory Use: <span id="memory-use"></span></div> <div class="col">Memory Use: <span id="memory-use"></span></div>
<div class="col">Total Spots: <span id="total-spots"></span></div> <div class="col">Total Spots: <span id="total-spots"></span></div>
<div class="col">Total Alerts: <span id="total-alerts"></span></div> <div class="col">Total Alerts: <span id="total-alerts"></span></div>
</div> </div>
<div class="row row-cols-1 row-cols-md-4 g-4 mb-2"> <div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>Web Server</strong></div> <div class="col"><strong>Web Server</strong></div>
<div class="col">Status: <span id="web-server-status"></span></div> <div class="col">Status: <span id="web-server-status"></span></div>
<div class="col">Last API call: <span id="web-server-last-api"></span></div> <div class="col">Last API call: <span id="web-server-last-api"></span></div>
@@ -5,7 +5,9 @@
<button id="display-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" <button id="display-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button"
onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i><span onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i><span
class="hideonmobile">&nbsp;Display</span></button> class="hideonmobile">&nbsp;Display</span></button>
{% if show_data_button %}
<button id="data-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" <button id="data-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button"
onclick="toggleDataPanel();"><i class="fa-solid fa-database"></i><span onclick="toggleDataPanel();"><i class="fa-solid fa-database"></i><span
class="hideonmobile">&nbsp;Your data</span></button> class="hideonmobile">&nbsp;Your data</span></button>
{% end %}
</div> </div>