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.
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:
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
+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
# 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):
@@ -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):
+2 -2
View File
@@ -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:
+2 -1
View File
@@ -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
@@ -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):
+1 -1
View File
@@ -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):
@@ -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):
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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
+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._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
+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
# 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):
+1 -1
View File
@@ -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
+46 -27
View File
@@ -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
+1 -1
View File
@@ -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()
@@ -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):
+5 -3
View File
@@ -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)
+19 -3
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+6 -6
View File
@@ -14,7 +14,7 @@ function loadStatus() {
jsonData["spot_providers"].forEach(p => {
$("#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">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>
@@ -24,7 +24,7 @@ function loadStatus() {
jsonData["alert_providers"].forEach(p => {
$("#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">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>
@@ -33,7 +33,7 @@ function loadStatus() {
jsonData["solar_condition_providers"].forEach(p => {
$("#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">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>
@@ -42,7 +42,7 @@ function loadStatus() {
jsonData["static_data_providers"].forEach(p => {
$("#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">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>
@@ -51,7 +51,7 @@ function loadStatus() {
jsonData["sig_ref_data_providers"].forEach(p => {
$("#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">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>
@@ -61,7 +61,7 @@ function loadStatus() {
jsonData["callsign_data_providers"].forEach(p => {
$("#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">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>
+8 -1
View File
@@ -8,7 +8,8 @@
</div>
<div class="col-auto">
<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>
@@ -50,19 +51,25 @@
</div>
</div>
{% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
</div>
</div>
</div>
{% end %}
<div id="table-container">
<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">
<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>
@@ -54,19 +55,25 @@
</div>
</div>
{% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
</div>
</div>
</div>
{% end %}
<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">
<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>
@@ -74,19 +75,25 @@
</div>
</div>
{% if web_ui_options["qrz-enabled"] or web_ui_options["hamqth-enabled"] %}
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
</div>
</div>
</div>
{% end %}
</div>
</div>
+6 -1
View File
@@ -25,7 +25,8 @@
<div class="col-md-8 text-end">
<div class="d-inline-flex gap-3">
{% 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>
@@ -86,12 +87,16 @@
{% module Template("widgets/data-area-header.html", web_ui_options=web_ui_options) %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz-enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
{% if web_ui_options["hamqth-enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
</div>
{% end %}
<div class="col">
{% module Template("cards/location.html", web_ui_options=web_ui_options) %}
</div>
+3 -3
View File
@@ -6,19 +6,19 @@
Spothole
</div>
<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">Software Version: <span id="software-version"></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>
<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">Memory Use: <span id="memory-use"></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>
<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">Status: <span id="web-server-status"></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"
onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i><span
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"
onclick="toggleDataPanel();"><i class="fa-solid fa-database"></i><span
class="hideonmobile">&nbsp;Your data</span></button>
{% end %}
</div>