14 Commits

Author SHA1 Message Date
Ian Renton
0ccc2bd15d Minor tweaks 2025-11-17 17:58:52 +00:00
Ian Renton
5724c4c7ea Minor tweaks 2025-11-17 17:50:29 +00:00
Ian Renton
94c0cad769 Improve SIG regexes to specify numbers of digits 2025-11-17 17:41:01 +00:00
Ian Renton
452e4beb29 Fix imports 2025-11-17 17:22:12 +00:00
Ian Renton
b132fe8a39 Fix a bug where SIG API spots could be re-tagged as another SIG e.g. WAB if that was named in the comment. 2025-11-17 17:19:43 +00:00
Ian Renton
e525aaed92 Fix a bug where spothole was too keen on extracting secondary references for xOTA programmes from comments, and was not checking that the "references" it found were surrounded by whitespace. 2025-11-16 17:46:40 +00:00
Ian Renton
92b7110356 Merge remote-tracking branch 'origin/main' 2025-11-16 17:46:05 +00:00
Ian Renton
114eacb9dc Fix a bug where spothole was too keen on extracting secondary references for xOTA programmes from comments, and was not checking that the "references" it found were surrounded by whitespace. 2025-11-16 17:45:58 +00:00
Ian Renton
2a90b17b6b Fix URLs for WOTA outlying fells 2025-11-14 14:37:36 +00:00
Ian Renton
ae075f3ac7 Version number bump 2025-11-13 21:52:13 +00:00
Ian Renton
efa9806c64 Look up K0SWE's dxcc.json rather than using our own tables. Closes #80 2025-11-13 21:51:20 +00:00
Ian Renton
03829831c0 Fix debug code commit 2025-11-13 21:47:05 +00:00
Ian Renton
4f83468309 Add config for "Number of Spots" and "Spot Age" values used in the web UI. Closes #79 2025-11-13 21:18:27 +00:00
Ian Renton
2165ebc103 DXCC 999 2025-11-13 20:10:53 +00:00
40 changed files with 210 additions and 1512 deletions

View File

@@ -196,10 +196,12 @@ Finally, simply add the appropriate config to the `providers` section of `config
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH. As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
The project contains a self-hosted copy of Font Awesome's free library, in the `/webasset/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering. The project contains a self-hosted copy of Font Awesome's free library, in the `/webassets/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the `/webassets/img/flags/` directory.
The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries such as jQuery, Leaflet and Bootstrap. This project would not have been possible without these libraries, so many thanks to their developers. The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries such as jQuery, Leaflet and Bootstrap. This project would not have been possible without these libraries, so many thanks to their developers.
Particular thanks go to QRZCQ country-files.com for providing country lookup data for amateur radio, and to the developers of `pyhamtools` for making it easy to use this data as well as QRZ.com and Clublog lookup. Particular thanks go to country-files.com for providing country lookup data for amateur radio, to K0SWE for [this JSON-formatted DXCC data](https://github.com/k0swe/dxcc-json/), and to the developers of `pyhamtools` for making it easy to use country-files.com data as well as QRZ.com and Clublog lookup.
The project's name was suggested by Harm, DK4HAA. Thanks! The project's name was suggested by Harm, DK4HAA. Thanks!

View File

@@ -1,9 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime
import pytz import pytz
from core.config import SERVER_OWNER_CALLSIGN, MAX_ALERT_AGE from core.config import MAX_ALERT_AGE
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
# Generic alert provider class. Subclasses of this query the individual APIs for alerts. # Generic alert provider class. Subclasses of this query the individual APIs for alerts.

View File

@@ -2,8 +2,8 @@ from datetime import datetime, timedelta
import pytz import pytz
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from alertproviders.http_alert_provider import HTTPAlertProvider from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef

View File

@@ -4,7 +4,6 @@ from datetime import datetime
import pytz import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import pytz import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import pytz import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef

View File

@@ -4,7 +4,6 @@ import pytz
from rss_parser import RSSParser from rss_parser import RSSParser
from alertproviders.http_alert_provider import HTTPAlertProvider from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import pytz import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider from alertproviders.http_alert_provider import HTTPAlertProvider
from core.sig_utils import get_icon_for_sig
from data.alert import Alert from data.alert import Alert
from data.sig_ref import SIGRef from data.sig_ref import SIGRef

View File

@@ -135,4 +135,13 @@ hamqth-password: ""
clublog-api-key: "" clublog-api-key: ""
# Allow submitting spots to the Spothole API? # Allow submitting spots to the Spothole API?
allow-spotting: true allow-spotting: true
# Options for the web UI.
web-ui-options:
spot-count: [10, 25, 50, 100]
spot-count-default: 50
max-spot-age: [5, 10, 30, 60]
max-spot-age-default: 30
alert-count: [25, 50, 100, 200, 500]
alert-count-default: 100

View File

@@ -1,5 +1,5 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime
from threading import Timer from threading import Timer
from time import sleep from time import sleep

View File

@@ -16,4 +16,5 @@ MAX_SPOT_AGE = config["max-spot-age-sec"]
MAX_ALERT_AGE = config["max-alert-age-sec"] MAX_ALERT_AGE = config["max-alert-age-sec"]
SERVER_OWNER_CALLSIGN = config["server-owner-callsign"] SERVER_OWNER_CALLSIGN = config["server-owner-callsign"]
WEB_SERVER_PORT = config["web-server-port"] WEB_SERVER_PORT = config["web-server-port"]
ALLOW_SPOTTING = config["allow-spotting"] ALLOW_SPOTTING = config["allow-spotting"]
WEB_UI_OPTIONS = config["web-ui-options"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
import gzip import gzip
import json
import logging import logging
import re
import urllib.parse import urllib.parse
from datetime import timedelta from datetime import timedelta
@@ -14,7 +16,7 @@ from requests_cache import CachedSession
from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
from core.config import config from core.config import config
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \ from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
QRZCQ_CALLSIGN_LOOKUP_DATA, HTTP_HEADERS, HAMQTH_PRG HTTP_HEADERS, HAMQTH_PRG
# Singleton class that provides lookup functionality. # Singleton class that provides lookup functionality.
@@ -46,6 +48,8 @@ class LookupHelper:
self.CALL_INFO_BASIC = None self.CALL_INFO_BASIC = None
self.LOOKUP_LIB_BASIC = None self.LOOKUP_LIB_BASIC = None
self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = None self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = None
self.DXCC_JSON_DOWNLOAD_LOCATION = None
self.DXCC_DATA = None
def start(self): def start(self):
# Lookup helpers from pyhamtools. We use five (!) of these. The simplest is country-files.com, which downloads # Lookup helpers from pyhamtools. We use five (!) of these. The simplest is country-files.com, which downloads
@@ -84,6 +88,19 @@ class LookupHelper:
filename=self.CLUBLOG_XML_DOWNLOAD_LOCATION) filename=self.CLUBLOG_XML_DOWNLOAD_LOCATION)
self.CLUBLOG_CALLSIGN_DATA_CACHE = Cache('cache/clublog_callsign_lookup_cache') self.CLUBLOG_CALLSIGN_DATA_CACHE = Cache('cache/clublog_callsign_lookup_cache')
# We also get a lookup of DXCC data from K0SWE to use for additional lookups of e.g. flags.
self.DXCC_JSON_DOWNLOAD_LOCATION = "cache/dxcc.json"
success = self.download_dxcc_json()
if success:
with open(self.DXCC_JSON_DOWNLOAD_LOCATION) as f:
tmp_dxcc_data = json.load(f)["dxcc"]
# Reformat as a map for faster lookup
self.DXCC_DATA = {}
for dxcc in tmp_dxcc_data:
self.DXCC_DATA[dxcc["entityCode"]] = dxcc
else:
logging.error("Could not download DXCC data, flags and similar data may be missing!")
# Download the cty.plist file from country-files.com on first startup. The pyhamtools lib can actually download and use # Download the cty.plist file from country-files.com on first startup. The pyhamtools lib can actually download and use
# this itself, but it's occasionally offline which causes it to throw an error. By downloading it separately, we can # this itself, but it's occasionally offline which causes it to throw an error. By downloading it separately, we can
# catch errors and handle them, falling back to a previous copy of the file in the cache, and we can use the # catch errors and handle them, falling back to a previous copy of the file in the cache, and we can use the
@@ -103,6 +120,22 @@ class LookupHelper:
logging.error("Exception when downloading Clublog cty.xml", e) logging.error("Exception when downloading Clublog cty.xml", e)
return False return False
# Download the dxcc.json file on first startup.
def download_dxcc_json(self):
try:
logging.info("Downloading dxcc.json...")
response = SEMI_STATIC_URL_DATA_CACHE.get("https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
headers=HTTP_HEADERS).text
with open(self.DXCC_JSON_DOWNLOAD_LOCATION, "w") as f:
f.write(response)
f.flush()
return True
except Exception as e:
logging.error("Exception when downloading dxcc.json", e)
return False
# Download the cty.xml (gzipped) file from Clublog on first startup, so we can use it in preference to querying the # Download the cty.xml (gzipped) file from Clublog on first startup, so we can use it in preference to querying the
# database live if possible. # database live if possible.
def download_clublog_ctyxml(self): def download_clublog_ctyxml(self):
@@ -175,11 +208,11 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call) clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "Name" in clublog_data: if clublog_data and "Name" in clublog_data:
country = clublog_data["Name"] country = clublog_data["Name"]
# Couldn't get anything from Clublog database, try QRZCQ data # Couldn't get anything from Clublog database, try DXCC data
if not country: if not country:
qrzcq_data = self.get_qrzcq_data_for_callsign(call) dxcc_data = self.get_dxcc_data_for_callsign(call)
if qrzcq_data and "country" in qrzcq_data: if dxcc_data and "name" in dxcc_data:
country = qrzcq_data["country"] country = dxcc_data["name"]
return country return country
# Infer a DXCC ID from a callsign # Infer a DXCC ID from a callsign
@@ -208,11 +241,11 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call) clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "DXCC" in clublog_data: if clublog_data and "DXCC" in clublog_data:
dxcc = clublog_data["DXCC"] dxcc = clublog_data["DXCC"]
# Couldn't get anything from Clublog database, try QRZCQ data # Couldn't get anything from Clublog database, try DXCC data
if not dxcc: if not dxcc:
qrzcq_data = self.get_qrzcq_data_for_callsign(call) dxcc_data = self.get_dxcc_data_for_callsign(call)
if qrzcq_data and "dxcc" in qrzcq_data: if dxcc_data and "entityCode" in dxcc_data:
dxcc = qrzcq_data["dxcc"] dxcc = dxcc_data["entityCode"]
return dxcc return dxcc
# Infer a continent shortcode from a callsign # Infer a continent shortcode from a callsign
@@ -236,11 +269,12 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call) clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "Continent" in clublog_data: if clublog_data and "Continent" in clublog_data:
continent = clublog_data["Continent"] continent = clublog_data["Continent"]
# Couldn't get anything from Clublog database, try QRZCQ data # Couldn't get anything from Clublog database, try DXCC data
if not continent: if not continent:
qrzcq_data = self.get_qrzcq_data_for_callsign(call) dxcc_data = self.get_dxcc_data_for_callsign(call)
if qrzcq_data and "continent" in qrzcq_data: # Some DXCCs are in two continents, if so don't use the continent data as we can't be sure
continent = qrzcq_data["continent"] if dxcc_data and "continent" in dxcc_data and len(dxcc_data["continent"]) == 1:
continent = dxcc_data["continent"][0]
return continent return continent
# Infer a CQ zone from a callsign # Infer a CQ zone from a callsign
@@ -269,11 +303,12 @@ class LookupHelper:
clublog_data = self.get_clublog_api_data_for_callsign(call) clublog_data = self.get_clublog_api_data_for_callsign(call)
if clublog_data and "CQZ" in clublog_data: if clublog_data and "CQZ" in clublog_data:
cqz = clublog_data["CQZ"] cqz = clublog_data["CQZ"]
# Couldn't get anything from Clublog database, try QRZCQ data # Couldn't get anything from Clublog database, try DXCC data
if not cqz: if not cqz:
qrzcq_data = self.get_qrzcq_data_for_callsign(call) dxcc_data = self.get_dxcc_data_for_callsign(call)
if qrzcq_data and "cqz" in qrzcq_data: # Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
cqz = qrzcq_data["cqz"] if dxcc_data and "cq" in dxcc_data and len(dxcc_data["cq"]) == 1:
cqz = dxcc_data["cq"][0]
return cqz return cqz
# Infer a ITU zone from a callsign # Infer a ITU zone from a callsign
@@ -293,13 +328,18 @@ class LookupHelper:
hamqth_data = self.get_hamqth_data_for_callsign(call) hamqth_data = self.get_hamqth_data_for_callsign(call)
if hamqth_data and "itu" in hamqth_data: if hamqth_data and "itu" in hamqth_data:
ituz = hamqth_data["itu"] ituz = hamqth_data["itu"]
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try QRZCQ data # Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try DXCC data
if not ituz: if not ituz:
qrzcq_data = self.get_qrzcq_data_for_callsign(call) dxcc_data = self.get_dxcc_data_for_callsign(call)
if qrzcq_data and "ituz" in qrzcq_data: # Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
ituz = qrzcq_data["ituz"] if dxcc_data and "itu" in dxcc_data and len(dxcc_data["itu"]) == 1:
ituz = dxcc_data["itu"]
return ituz return ituz
# Get an emoji flag for a given DXCC entity ID
def get_flag_for_dxcc(self, dxcc):
return self.DXCC_DATA[dxcc]["flag"] if dxcc in self.DXCC_DATA else None
# Infer an operator name from a callsign (requires QRZ.com/HamQTH) # Infer an operator name from a callsign (requires QRZ.com/HamQTH)
def infer_name_from_callsign_online_lookup(self, call): def infer_name_from_callsign_online_lookup(self, call):
data = self.get_qrz_data_for_callsign(call) data = self.get_qrz_data_for_callsign(call)
@@ -488,11 +528,10 @@ class LookupHelper:
else: else:
return None return None
# Utility method to get QRZCQ data from our constants table, if we can find it # Utility method to get generic DXCC data from our lookup table, if we can find it
def get_qrzcq_data_for_callsign(self, call): def get_dxcc_data_for_callsign(self, call):
# Iterate in reverse order - see comments on the data structure itself for entry in self.DXCC_DATA.values():
for entry in reversed(QRZCQ_CALLSIGN_LOOKUP_DATA): if re.match(entry["prefixRegex"], call):
if call.startswith(entry["prefix"]):
return entry return entry
return None return None

View File

@@ -98,7 +98,12 @@ def get_sig_ref_info(sig, sig_ref_id):
for feature in data["features"]: for feature in data["features"]:
if feature["properties"]["wotaId"] == sig_ref_id: if feature["properties"]["wotaId"] == sig_ref_id:
sig_ref.name = feature["properties"]["title"] sig_ref.name = feature["properties"]["title"]
# Fudge WOTA URLs. Outlying fell (LDO) URLs don't match their ID numbers but require 214 to be
# added to them
sig_ref.url = "https://www.wota.org.uk/MM_" + sig_ref_id sig_ref.url = "https://www.wota.org.uk/MM_" + sig_ref_id
if sig_ref_id.upper().startswith("LDO-"):
number = int(sig_ref_id.upper().replace("LDO-", ""))
sig_ref.url = "https://www.wota.org.uk/MM_LDO-" + str(number + 214)
sig_ref.grid = feature["properties"]["qthLocator"] sig_ref.grid = feature["properties"]["qthLocator"]
sig_ref.latitude = feature["geometry"]["coordinates"][1] sig_ref.latitude = feature["geometry"]["coordinates"][1]
sig_ref.longitude = feature["geometry"]["coordinates"][0] sig_ref.longitude = feature["geometry"]["coordinates"][0]

View File

@@ -6,7 +6,6 @@ from datetime import datetime, timedelta
import pytz import pytz
from core.constants import DXCC_FLAGS
from core.lookup_helper import lookup_helper from core.lookup_helper import lookup_helper
from core.sig_utils import get_icon_for_sig, get_sig_ref_info from core.sig_utils import get_icon_for_sig, get_sig_ref_info
@@ -95,8 +94,8 @@ class Alert:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0]) self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0])
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id: if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0]) self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0])
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag: if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id] self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference # Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs # in its initial call, we use this code to populate the rest of the data. This includes working out grid refs

View File

@@ -9,7 +9,6 @@ from datetime import datetime
import pytz import pytz
from pyhamtools.locator import locator_to_latlong, latlong_to_locator from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.constants import DXCC_FLAGS
from core.lookup_helper import lookup_helper from core.lookup_helper import lookup_helper
from core.sig_utils import get_icon_for_sig, get_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig from core.sig_utils import get_icon_for_sig, get_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
@@ -174,8 +173,8 @@ class Spot:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call) self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
if self.dx_call and not self.dx_dxcc_id: if self.dx_call and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call) self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag: if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id] self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
# Clean up spotter call if it has an SSID or -# from RBN # Clean up spotter call if it has an SSID or -# from RBN
if self.de_call and "-" in self.de_call: if self.de_call and "-" in self.de_call:
@@ -207,8 +206,8 @@ class Spot:
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call) self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
if not self.de_dxcc_id: if not self.de_dxcc_id:
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call) self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
if self.de_dxcc_id and self.de_dxcc_id in DXCC_FLAGS and not self.de_flag: if self.de_dxcc_id and not self.de_flag:
self.de_flag = DXCC_FLAGS[self.de_dxcc_id] self.de_flag = lookup_helper.get_flag_for_dxcc(self.de_dxcc_id)
# Band from frequency # Band from frequency
if self.freq and not self.band: if self.freq and not self.band:
@@ -240,13 +239,17 @@ class Spot:
if self.dx_latitude: if self.dx_latitude:
self.dx_location_source = "SPOT" self.dx_location_source = "SPOT"
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This # Set the top-level "SIG" if it is missing but we have at least one SIG ref.
if not self.sig and self.sig_refs and len(self.sig_refs) > 0:
self.sig = self.sig_refs[0].sig.upper()
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002". # should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
if self.comment and self.sig_refs and len(self.sig_refs) > 0: if self.comment and self.sig_refs and len(self.sig_refs) > 0:
sig = self.sig_refs[0].sig.upper() sig = self.sig_refs[0].sig.upper()
all_comment_refs = re.findall(get_ref_regex_for_sig(sig), self.comment) all_comment_ref_matches = re.finditer(r"(^|\W)(" + get_ref_regex_for_sig(sig) + r")(^|\W)", self.comment, re.IGNORECASE)
for ref in all_comment_refs: for ref_match in all_comment_ref_matches:
self.append_sig_ref_if_missing(SIGRef(id=ref.upper(), sig=sig)) self.append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig))
# See if the comment looks like it contains any SIGs (and optionally SIG references) that we can # See if the comment looks like it contains any SIGs (and optionally SIG references) that we can
# add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA # add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA
@@ -385,7 +388,11 @@ class Spot:
def append_sig_ref_if_missing(self, new_sig_ref): def append_sig_ref_if_missing(self, new_sig_ref):
if not self.sig_refs: if not self.sig_refs:
self.sig_refs = [] self.sig_refs = []
new_sig_ref.id = new_sig_ref.id.strip().upper()
new_sig_ref.sig = new_sig_ref.sig.strip().upper()
if new_sig_ref.id == "":
return
for sig_ref in self.sig_refs: for sig_ref in self.sig_refs:
if sig_ref.id.upper() == new_sig_ref.id.upper() and sig_ref.sig.upper() == new_sig_ref.sig.upper(): if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig:
return return
self.sig_refs.append(new_sig_ref) self.sig_refs.append(new_sig_ref)

View File

@@ -8,7 +8,7 @@ import bottle
import pytz import pytz
from bottle import run, request, response, template from bottle import run, request, response, template
from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING, WEB_UI_OPTIONS
from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS, SOFTWARE_VERSION, UNKNOWN_BAND from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS, SOFTWARE_VERSION, UNKNOWN_BAND
from core.lookup_helper import lookup_helper from core.lookup_helper import lookup_helper
from core.prometheus_metrics_handler import page_requests_counter, get_metrics, api_requests_counter from core.prometheus_metrics_handler import page_requests_counter, get_metrics, api_requests_counter
@@ -469,7 +469,8 @@ class WebServer:
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"]))),
"continents": CONTINENTS, "continents": CONTINENTS,
"max_spot_age": MAX_SPOT_AGE, "max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING} "spot_allowed": ALLOW_SPOTTING,
"web-ui-options": WEB_UI_OPTIONS}
# 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:

View File

@@ -54,20 +54,27 @@ class GMA(HTTPSpotProvider):
match ref_info["reftype"]: match ref_info["reftype"]:
case "Summit": case "Summit":
spot.sig_refs[0].sig = "GMA" spot.sig_refs[0].sig = "GMA"
spot.sig = "GMA"
case "IOTA Island": case "IOTA Island":
spot.sig_refs[0].sig = "IOTA" spot.sig_refs[0].sig = "IOTA"
spot.sig = "IOTA"
case "Lighthouse (ILLW)": case "Lighthouse (ILLW)":
spot.sig_refs[0].sig = "ILLW" spot.sig_refs[0].sig = "ILLW"
spot.sig = "ILLW"
case "Lighthouse (ARLHS)": case "Lighthouse (ARLHS)":
spot.sig_refs[0].sig = "ARLHS" spot.sig_refs[0].sig = "ARLHS"
spot.sig = "ARLHS"
case "Castle": case "Castle":
spot.sig_refs[0].sig = "WCA" spot.sig_refs[0].sig = "WCA"
spot.sig = "WCA"
case "Mill": case "Mill":
spot.sig_refs[0].sig = "MOTA" spot.sig_refs[0].sig = "MOTA"
spot.sig = "MOTA"
case _: case _:
logging.warn("GMA spot found with ref type " + ref_info[ logging.warn("GMA spot found with ref type " + ref_info[
"reftype"] + ", developer needs to add support for this!") "reftype"] + ", developer needs to add support for this!")
spot.sig_refs[0].sig = ref_info["reftype"] spot.sig_refs[0].sig = ref_info["reftype"]
spot.sig = ref_info["reftype"]
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us. # that for us.

View File

@@ -5,7 +5,6 @@ import pytz
import requests import requests
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -53,6 +52,7 @@ class HEMA(HTTPSpotProvider):
freq=float(freq_mode_match.group(1)) * 1000000, freq=float(freq_mode_match.group(1)) * 1000000,
mode=freq_mode_match.group(2).upper(), mode=freq_mode_match.group(2).upper(),
comment=spotter_comment_match.group(2), comment=spotter_comment_match.group(2),
sig="HEMA",
sig_refs=[SIGRef(id=spot_items[3].upper(), sig="HEMA", name=spot_items[4])], sig_refs=[SIGRef(id=spot_items[3].upper(), sig="HEMA", name=spot_items[4])],
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(), time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(spot_items[7]), dx_latitude=float(spot_items[7]),

View File

@@ -4,7 +4,6 @@ from datetime import datetime
import pytz import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -33,6 +32,7 @@ class ParksNPeaks(HTTPSpotProvider):
# Seen PNP spots with empty frequency, and with comma-separated thousands digits # Seen PNP spots with empty frequency, and with comma-separated thousands digits
mode=source_spot["actMode"].upper(), mode=source_spot["actMode"].upper(),
comment=source_spot["actComments"], comment=source_spot["actComments"],
sig=source_spot["actClass"].upper(),
sig_refs=[SIGRef(id=source_spot["actSiteID"], sig=source_spot["actClass"].upper())], sig_refs=[SIGRef(id=source_spot["actSiteID"], sig=source_spot["actClass"].upper())],
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace( time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp()) tzinfo=pytz.UTC).timestamp())

View File

@@ -1,9 +1,7 @@
import re
from datetime import datetime from datetime import datetime
import pytz import pytz
from core.sig_utils import get_icon_for_sig, get_ref_regex_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -31,6 +29,7 @@ class POTA(HTTPSpotProvider):
freq=float(source_spot["frequency"]) * 1000, freq=float(source_spot["frequency"]) * 1000,
mode=source_spot["mode"].upper(), mode=source_spot["mode"].upper(),
comment=source_spot["comments"], comment=source_spot["comments"],
sig="POTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="POTA", name=source_spot["name"])], sig_refs=[SIGRef(id=source_spot["reference"], sig="POTA", name=source_spot["name"])],
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace( time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp(), tzinfo=pytz.UTC).timestamp(),

View File

@@ -3,7 +3,6 @@ from datetime import datetime
import requests import requests
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -45,6 +44,7 @@ class SOTA(HTTPSpotProvider):
freq=(float(source_spot["frequency"]) * 1000000) if (source_spot["frequency"] is not None) else None, # Seen SOTA spots with no frequency! freq=(float(source_spot["frequency"]) * 1000000) if (source_spot["frequency"] is not None) else None, # Seen SOTA spots with no frequency!
mode=source_spot["mode"].upper(), mode=source_spot["mode"].upper(),
comment=source_spot["comments"], comment=source_spot["comments"],
sig="SOTA",
sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"])], sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"])],
time=datetime.fromisoformat(source_spot["timeStamp"]).timestamp(), time=datetime.fromisoformat(source_spot["timeStamp"]).timestamp(),
activation_score=source_spot["points"]) activation_score=source_spot["points"])

View File

@@ -2,8 +2,7 @@ from datetime import datetime
import pytz import pytz
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION from core.config import MAX_SPOT_AGE
from core.config import SERVER_OWNER_CALLSIGN, MAX_SPOT_AGE
# Generic spot provider class. Subclasses of this query the individual APIs for data. # Generic spot provider class. Subclasses of this query the individual APIs for data.

View File

@@ -9,6 +9,7 @@ from requests_sse import EventSource
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from spotproviders.spot_provider import SpotProvider from spotproviders.spot_provider import SpotProvider
# Spot provider using Server-Sent Events. # Spot provider using Server-Sent Events.
class SSESpotProvider(SpotProvider): class SSESpotProvider(SpotProvider):

View File

@@ -1,11 +1,8 @@
import re import re
from datetime import datetime, timedelta from datetime import datetime
import pytz import pytz
from requests_cache import CachedSession
from core.constants import HTTP_HEADERS
from core.sig_utils import get_icon_for_sig, get_ref_regex_for_sig
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider

View File

@@ -13,7 +13,7 @@ from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Wainwrights on the Air # Spot provider for Wainwrights on the Air
class WOTA(HTTPSpotProvider): class WOTA(HTTPSpotProvider):
POLL_INTERVAL_SEC = 120 POLL_INTERVAL_SEC = 120
SPOTS_URL = "http://127.0.0.1:8000/spots_rss.php" SPOTS_URL = "https://www.wota.org.uk/spots_rss.php"
LIST_URL = "https://www.wota.org.uk/mapping/data/summits.json" LIST_URL = "https://www.wota.org.uk/mapping/data/summits.json"
RSS_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %z" RSS_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %z"
@@ -66,6 +66,7 @@ class WOTA(HTTPSpotProvider):
freq=freq_hz, freq=freq_hz,
mode=mode, mode=mode,
comment=comment, comment=comment,
sig="WOTA",
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [], sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
time=time.timestamp()) time=time.timestamp())

View File

@@ -1,7 +1,6 @@
import json import json
from datetime import datetime from datetime import datetime
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.sse_spot_provider import SSESpotProvider from spotproviders.sse_spot_provider import SSESpotProvider
@@ -29,6 +28,7 @@ class WWBOTA(SSESpotProvider):
freq=float(source_spot["freq"]) * 1000000, freq=float(source_spot["freq"]) * 1000000,
mode=source_spot["mode"].upper(), mode=source_spot["mode"].upper(),
comment=source_spot["comment"], comment=source_spot["comment"],
sig="WWBOTA",
sig_refs=refs, sig_refs=refs,
time=datetime.fromisoformat(source_spot["time"]).timestamp(), time=datetime.fromisoformat(source_spot["time"]).timestamp(),
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For # WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For

View File

@@ -2,7 +2,6 @@ from datetime import datetime
import pytz import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -28,6 +27,7 @@ class WWFF(HTTPSpotProvider):
freq=float(source_spot["frequency_khz"]) * 1000, freq=float(source_spot["frequency_khz"]) * 1000,
mode=source_spot["mode"].upper(), mode=source_spot["mode"].upper(),
comment=source_spot["remarks"], comment=source_spot["remarks"],
sig="WWFF",
sig_refs=[SIGRef(id=source_spot["reference"], sig="WWFF", name=source_spot["reference_name"])], sig_refs=[SIGRef(id=source_spot["reference"], sig="WWFF", name=source_spot["reference_name"])],
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(), time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
dx_latitude=source_spot["latitude"], dx_latitude=source_spot["latitude"],

View File

@@ -2,7 +2,6 @@ from datetime import datetime
import pytz import pytz
from core.sig_utils import get_icon_for_sig
from data.sig_ref import SIGRef from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -34,6 +33,7 @@ class ZLOTA(HTTPSpotProvider):
freq=freq_hz, freq=freq_hz,
mode=source_spot["mode"].upper().strip(), mode=source_spot["mode"].upper().strip(),
comment=source_spot["comments"], comment=source_spot["comments"],
sig="ZLOTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="ZLOTA", name=source_spot["name"])], sig_refs=[SIGRef(id=source_spot["reference"], sig="ZLOTA", name=source_spot["name"])],
time=datetime.fromisoformat(source_spot["referenced_time"]).astimezone(pytz.UTC).timestamp()) time=datetime.fromisoformat(source_spot["referenced_time"]).astimezone(pytz.UTC).timestamp())

View File

@@ -101,11 +101,6 @@
<h5 class="card-title">Number of Alerts</h5> <h5 class="card-title">Number of Alerts</h5>
<p class="card-text spothole-card-text">Show up to <p class="card-text spothole-card-text">Show up to
<select id="alerts-to-fetch" class="storeable-select form-select ms-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;"> <select id="alerts-to-fetch" class="storeable-select form-select ms-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;">
<option value="25">25</option>
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
</select> </select>
alerts alerts
</p> </p>

View File

@@ -93,10 +93,6 @@
<h5 class="card-title">Spot Age</h5> <h5 class="card-title">Spot Age</h5>
<p class="card-text spothole-card-text">Last <p class="card-text spothole-card-text">Last
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;"> <select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
<option value="300">5</option>
<option value="600">10</option>
<option value="1800" selected>30</option>
<option value="3600">60</option>
</select> </select>
minutes minutes
</p> </p>

View File

@@ -92,10 +92,6 @@
<h5 class="card-title">Spot Age</h5> <h5 class="card-title">Spot Age</h5>
<p class="card-text spothole-card-text">Last <p class="card-text spothole-card-text">Last
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;"> <select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
<option value="300">5</option>
<option value="600">10</option>
<option value="1800" selected>30</option>
<option value="3600">60</option>
</select> </select>
minutes minutes
</p> </p>

View File

@@ -117,10 +117,6 @@
<h5 class="card-title">Number of Spots</h5> <h5 class="card-title">Number of Spots</h5>
<p class="card-text spothole-card-text">Show up to <p class="card-text spothole-card-text">Show up to
<select id="spots-to-fetch" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;"> <select id="spots-to-fetch" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
<option value="10">10</option>
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select> </select>
spots spots
</p> </p>

View File

@@ -438,7 +438,7 @@ paths:
tags: tags:
- General - General
summary: Get enumeration options summary: Get enumeration options
description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about. description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about. The call also returns a variety of other parameters that may be of use to a web UI, including the contents of the "web-ui-options" config section, which provides guidance for web UI implementations such as the built-in one on sensible configuration options such as the number of spots/alerts to retrieve, or the maximum age of spots to retrieve.
operationId: options operationId: options
responses: responses:
'200': '200':
@@ -490,6 +490,39 @@ paths:
type: boolean type: boolean
description: Whether the POST /spot call, to add spots to the server directly via its API, is permitted on this server. description: Whether the POST /spot call, to add spots to the server directly via its API, is permitted on this server.
example: true example: true
web-ui-options:
type: object
properties:
spot-count:
type: array
description: An array of suggested "spot counts" that the web UI can retrieve from the API
items:
type: integer
example: 50
spot-count-default:
type: integer
example: 50
description: The suggested default "spot count" that the web UI should retrieve from the API
max-spot-age:
type: array
description: An array of suggested "maximum spot ages" that the web UI can retrieve from the API
items:
type: integer
example: 30
max-spot-age-default:
type: integer
example: 30
description: The suggested default "maximum spot age" that the web UI should retrieve from the API
alert-count:
type: array
description: An array of suggested "alert counts" that the web UI can retrieve from the API
items:
type: integer
example: 100
alert-count-default:
type: integer
example: 100
description: The suggested default "alert count" that the web UI should retrieve from the API
/lookup/call: /lookup/call:

BIN
webassets/img/flags/999.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -1,7 +1,7 @@
import os import os
from datetime import timedelta from datetime import timedelta
from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont
from requests_cache import CachedSession from requests_cache import CachedSession
test = os.listdir("./") test = os.listdir("./")
@@ -19,4 +19,7 @@ for dxcc in data["dxcc"]:
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
draw.text((0, -10), flag, font=ImageFont.truetype("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", 109), embedded_color=True) draw.text((0, -10), flag, font=ImageFont.truetype("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", 109), embedded_color=True)
outfile = str(id) + ".png" outfile = str(id) + ".png"
image.save(outfile, "PNG") image.save(outfile, "PNG")
image = Image.new("RGBA", (140, 110), (255, 0, 0, 0))
image.save("999.png", "PNG")

View File

@@ -285,6 +285,13 @@ function loadOptions() {
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_sources"]);
// Populate the Display panel
options["web-ui-options"]["alert-count"].forEach(sc => $("#alerts-to-fetch").append($('<option>', {
value: sc,
text: sc
})));
$("#alerts-to-fetch").val(options["web-ui-options"]["alert-count-default"]);
// Load filters from settings storage // Load filters from settings storage
loadSettings(); loadSettings();

View File

@@ -235,6 +235,13 @@ function loadOptions() {
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]); generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]); generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Populate the Display panel
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
value: sc * 60,
text: sc
})));
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
// Load settings from settings storage now all the controls are available // Load settings from settings storage now all the controls are available
loadSettings(); loadSettings();

View File

@@ -163,6 +163,13 @@ function loadOptions() {
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]); generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]); generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Populate the Display panel
options["web-ui-options"]["max-spot-age"].forEach(sc => $("#max-spot-age").append($('<option>', {
value: sc * 60,
text: sc
})));
$("#max-spot-age").val(options["web-ui-options"]["max-spot-age-default"] * 60);
// Load settings from settings storage now all the controls are available // Load settings from settings storage now all the controls are available
loadSettings(); loadSettings();

View File

@@ -287,6 +287,13 @@ function loadOptions() {
generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]); generateMultiToggleFilterCard("#mode-options", "mode_type", options["mode_types"]);
generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]); generateMultiToggleFilterCard("#source-options", "source", options["spot_sources"]);
// Populate the Display panel
options["web-ui-options"]["spot-count"].forEach(sc => $("#spots-to-fetch").append($('<option>', {
value: sc,
text: sc
})));
$("#spots-to-fetch").val(options["web-ui-options"]["spot-count-default"]);
// Load settings from settings storage now all the controls are available // Load settings from settings storage now all the controls are available
loadSettings(); loadSettings();