mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-05 18:11:41 +00:00
Refactor of caching & data storage part 17 #118
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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">
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> 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"> Your data</span></button>
|
||||
{% end %}
|
||||
</div>
|
||||
Reference in New Issue
Block a user