Support RSGB contests

This commit is contained in:
Ian Renton
2026-09-05 09:00:32 +01:00
parent d3d1d20821
commit d6e53c14e9
32 changed files with 256 additions and 98 deletions
+6
View File
@@ -165,6 +165,12 @@ alert_providers:
- class: "NG3K"
enabled: true
- class: "RSGBHFContests"
enabled: true
- class: "RSGBVHFContests"
enabled: true
# Solar condition providers to use. These poll external APIs for solar propagation data (SFI, A/K indices, band
# conditions, etc.) and make it available via the /api/v2/solar endpoint.
+11 -6
View File
@@ -30,7 +30,6 @@ class Mode(str, Enum):
OLIVIA = "OLIVIA"
PKT = "PKT"
MSK144 = "MSK144"
UNKNOWN = "UNKNOWN"
@property
def is_cw(self) -> bool:
@@ -42,14 +41,14 @@ class Mode(str, Enum):
@property
def is_data(self) -> bool:
return not (self.is_cw or self.is_phone or self == Mode.UNKNOWN)
return not (self.is_cw or self.is_phone)
@staticmethod
def from_name(name):
"""Convert a string to an enum mode using the alias table."""
if not name:
return Mode.UNKNOWN
return None
try:
return Mode(name.upper())
@@ -57,14 +56,13 @@ class Mode(str, Enum):
try:
return Mode(MODE_ALIASES[name.upper()])
except (KeyError, ValueError):
return Mode.UNKNOWN
return None
class ModeType(str, Enum):
PHONE = "PHONE"
CW = "CW"
DATA = "DATA"
UNKNOWN = "UNKNOWN"
class ModeSource(str, Enum):
@@ -115,7 +113,14 @@ class SIGRefType(str, Enum):
REGION = "REGION"
GRID = "GRID"
TOILET = "TOILET"
UNKNOWN = "UNKNOWN"
class AlertType(str, Enum):
"""Type of an alert."""
XOTA = "XOTA"
DXPEDITION = "DXPEDITION"
CONTEST = "CONTEST"
class SIGType(str, Enum):
+6 -6
View File
@@ -20,11 +20,11 @@ def safe_json_dumps(obj):
return simplejson.dumps(obj, ensure_ascii=False, ignore_nan=True, default=lambda o: o.__dict__)
def infer_mode_from_comment(comment: str) -> Mode:
def infer_mode_from_comment(comment: str) -> Mode | None:
"""Infer a mode from the comment"""
if not comment:
return Mode.UNKNOWN
return None
for mode in Mode:
if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
@@ -33,14 +33,14 @@ def infer_mode_from_comment(comment: str) -> Mode:
if re.search(r"(^|\W)" + alias + r"($|\W)", comment, re.IGNORECASE):
return Mode(MODE_ALIASES[alias])
return Mode.UNKNOWN
return None
def infer_mode_type_from_mode(mode: str) -> ModeType:
def infer_mode_type_from_mode(mode: str) -> ModeType | None:
"""Infer a "mode family" from a mode ."""
if not mode:
return ModeType.UNKNOWN
return None
if mode in MODE_ALIASES:
mode = MODE_ALIASES[mode]
@@ -56,7 +56,7 @@ def infer_mode_type_from_mode(mode: str) -> ModeType:
except ValueError:
if mode.upper() != "OTHER" and mode != "?":
logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.")
return ModeType.UNKNOWN
return None
def infer_band_from_freq(freq):
+5 -3
View File
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
import pytz
from core.call_lookup_helper import get_call_info
from core.enums import Continent
from core.enums import AlertType, Continent
from core.sig_lookup_helper import populate_missing_sig_ref_info
from core.utils import get_flag_for_dxcc
@@ -58,8 +58,10 @@ class Alert:
sig: str | None = None
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
sig_refs: list | None = None
# Whether this alert is for a DXpedition, as opposed to e.g. an xOTA programme.
is_dxpedition: bool = False
# The type of alert this is: xOTA, DXpedition, or Contest.
alert_type: AlertType | None = None
# A URL link to more information, if any
url: str | None = None
# Where we got the alert from, e.g. "POTA", "SOTA"...
source: str | None = None
# The ID the source gave it, if any.
+1 -1
View File
@@ -15,7 +15,7 @@ class SIGRef:
# Name of the reference, e.g. "Null Country Park", if known.
name: str | None = None
# Type of the reference, e.g. "Park", if known.
ref_type: SIGRefType = SIGRefType.UNKNOWN
ref_type: SIGRefType | None = None
# URL to look up more information about the reference, if known.
url: str | None = None
# Latitude of the reference, in degrees, if known.
+6 -10
View File
@@ -103,9 +103,9 @@ class Spot:
# General QSO info
# Reported mode, such as SSB, PHONE, CW, FT8...
mode: Mode = Mode.UNKNOWN
mode: Mode | None = None
# Inferred mode "family".
mode_type: ModeType = ModeType.UNKNOWN
mode_type: ModeType | None = None
# Source of the mode information.
mode_source: ModeSource = ModeSource.NONE
# Frequency, in Hz
@@ -239,21 +239,17 @@ class Spot:
self.band = band.name
# Mode from comments or bandplan
if not self.mode:
self.mode = Mode.UNKNOWN
if self.mode != Mode.UNKNOWN:
if self.mode:
self.mode_source = ModeSource.SPOT
if self.comment and self.mode == Mode.UNKNOWN:
if self.comment and not self.mode:
self.mode = infer_mode_from_comment(self.comment)
self.mode_source = ModeSource.COMMENT
if self.freq and self.mode == Mode.UNKNOWN:
if self.freq and not self.mode:
self.mode = infer_mode_from_frequency(self.freq)
self.mode_source = ModeSource.BANDPLAN
# Mode type from mode
if not self.mode_type:
self.mode_type = ModeType.UNKNOWN
if self.mode != Mode.UNKNOWN and self.mode_type == ModeType.UNKNOWN:
if self.mode and not self.mode_type:
self.mode_type = infer_mode_type_from_mode(self.mode)
# If we have a latitude or grid at this point, it can only have been provided by the spot itself
+2 -1
View File
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
import pytz
from bs4 import BeautifulSoup
from core.enums import AlertType
from data.alert import Alert
from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider
@@ -57,7 +58,7 @@ class BOTA(HTTPAlertProvider):
dx_calls=[dx_call],
sig_refs=[SIGRef(id=ref_name, sig="BOTA")],
start_time=date_time.timestamp(),
is_dxpedition=False,
alert_type=AlertType.XOTA,
)
new_alerts.append(alert)
+5 -4
View File
@@ -6,19 +6,20 @@ import pytz
from rss_parser import Parser
from rss_parser.models.rss import RSS
from core.enums import AlertType
from data.alert import Alert
from providers.alert.http_alert_provider import HTTPAlertProvider
class NG3K(HTTPAlertProvider):
"""Alert provider NG3K DXpedition list"""
"""Alert provider for NG3K DXpedition list"""
POLL_INTERVAL_SEC = 1800
POLL_INTERVAL_DAYS = 1
ALERTS_URL = "https://www.ng3k.com/adxo.xml"
AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE)
def __init__(self, provider_config):
super().__init__("NG3K", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
super().__init__("NG3K", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60)
def _http_response_to_alerts(self, http_response):
new_alerts = []
@@ -88,7 +89,7 @@ class NG3K(HTTPAlertProvider):
comment=f"{by}; {comment}; {qsl_info}",
start_time=start_timestamp,
end_time=end_timestamp,
is_dxpedition=True,
alert_type=AlertType.DXPEDITION,
)
# Add to our list.
+2 -1
View File
@@ -3,6 +3,7 @@ from datetime import datetime
import pytz
from core.enums import AlertType
from data.alert import Alert
from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider
@@ -51,7 +52,7 @@ class ParksNPeaks(HTTPAlertProvider):
comment=source_alert["Comments"],
sig_refs=sigrefs,
start_time=start_time,
is_dxpedition=False,
alert_type=AlertType.XOTA,
)
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
+2 -1
View File
@@ -2,6 +2,7 @@ from datetime import datetime
import pytz
from core.enums import AlertType
from data.alert import Alert
from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider
@@ -44,7 +45,7 @@ class POTA(HTTPAlertProvider):
end_time=datetime.strptime(source_alert["endDate"] + source_alert["endTime"], "%Y-%m-%d%H:%M")
.replace(tzinfo=pytz.UTC)
.timestamp(),
is_dxpedition=False,
alert_type=AlertType.XOTA,
)
# Add to our list, but exclude any old spots that POTA can sometimes give us where even the end time is
+101
View File
@@ -0,0 +1,101 @@
import re
from datetime import datetime, time
from typing import cast
import pytz
from icalendar import Calendar, Event
from core.enums import AlertType, Continent
from data.alert import Alert
from providers.alert.http_alert_provider import HTTPAlertProvider
class RSGBICALAlertProvider(HTTPAlertProvider):
"""Generic alert provider for RSGB contest ICAL calendars."""
def __init__(self, name, provider_config, url, poll_interval):
super().__init__(name, provider_config, url, poll_interval)
FREQ_PATTERN = re.compile(r"([\d.]+(?:MHz|GHz))|SHF")
def _http_response_to_alerts(self, http_response):
new_alerts = []
cal = Calendar.from_ical(http_response.content)
# Iterate through events
for component in cal.walk():
if component.name != "VEVENT":
continue
event = cast(Event, component)
summary = str(event.get("summary", "")).strip()
# Ensure summaries start with "RSGB" to avoid any confusion
if not summary.startswith("RSGB "):
summary = "RSGB " + summary
# Extract freq from summary
freqs_modes = ""
# HF contests don't give frequencies, all VHF ones do
match = self.FREQ_PATTERN.search(summary)
if match:
freqs_modes = match.group()
else:
freqs_modes = "HF bands"
freqs_modes = freqs_modes + ", "
# Extract mode from summary
if "FMAC" in summary:
freqs_modes = freqs_modes + "FM"
elif "CW" in summary:
freqs_modes = freqs_modes + "CW"
elif "SSB" in summary:
freqs_modes = freqs_modes + "SSB"
elif "FT8" in summary:
freqs_modes = freqs_modes + "FT8"
elif "FT4" in summary:
freqs_modes = freqs_modes + "FT4"
elif "DATA" in summary:
freqs_modes = freqs_modes + "Data modes"
else:
freqs_modes = freqs_modes + "All modes"
dtstart = event.get("dtstart")
dtend = event.get("dtend")
start_timestamp = self._to_utc_timestamp(dtstart.dt)
end_timestamp = self._to_utc_timestamp(dtend.dt) - 1 if dtend is not None else start_timestamp
# Convert to our alert format
alert = Alert(
source=self.name,
dx_calls=[],
dx_country="United Kingdom",
dx_dxcc_id=235,
dx_continent=Continent.EU,
dx_cq_zone=14,
dx_itu_zone=27,
dx_flag="🇬🇧",
freqs_modes=freqs_modes,
comment=summary,
start_time=start_timestamp,
end_time=end_timestamp,
alert_type=AlertType.CONTEST,
)
# Add to our list.
new_alerts.append(alert)
return new_alerts
@staticmethod
def _to_utc_timestamp(value):
"""Convert a date or datetime value from an iCal field into a UTC UNIX timestamp."""
# Datetime object so we can treat it as-is, check if it has a non-UTC tz and convert it if necessary
if isinstance(value, datetime):
if value.tzinfo is None:
value = pytz.UTC.localize(value)
return value.astimezone(pytz.UTC).timestamp()
# Date object so this is an all day event
return pytz.UTC.localize(datetime.combine(value, time.min)).timestamp()
+11
View File
@@ -0,0 +1,11 @@
from providers.alert.rsgb_ical_alert_provider import RSGBICALAlertProvider
class RSGBHFContests(RSGBICALAlertProvider):
"""Alert provider for RSGB HF Contest calendar"""
POLL_INTERVAL_DAYS = 30
ALERTS_URL = "https://calendar.google.com/calendar/ical/a5ff31ebb1b4834dc7fff4c5415ae8251c6a9aa11f98c6af6e472b6c552b1915%40group.calendar.google.com/public/basic.ics"
def __init__(self, provider_config):
super().__init__("RSGB HF Contests", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60)
+11
View File
@@ -0,0 +1,11 @@
from providers.alert.rsgb_ical_alert_provider import RSGBICALAlertProvider
class RSGBVHFContests(RSGBICALAlertProvider):
"""Alert provider for RSGB VHF Contest calendar"""
POLL_INTERVAL_DAYS = 30
ALERTS_URL = "https://calendar.google.com/calendar/ical/40f3552bff39a016f1cdca205864177070dcad68d55be17eb061cb021f39f96c%40group.calendar.google.com/public/basic.ics"
def __init__(self, provider_config):
super().__init__("RSGB VHF Contests", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60)
+2 -1
View File
@@ -2,6 +2,7 @@ from datetime import datetime
import pytz
from core.enums import AlertType
from data.alert import Alert
from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider
@@ -44,7 +45,7 @@ class SOTA(HTTPAlertProvider):
start_time=datetime.strptime(source_alert["dateActivated"], "%Y-%m-%dT%H:%M:%SZ")
.replace(tzinfo=pytz.UTC)
.timestamp(),
is_dxpedition=False,
alert_type=AlertType.XOTA,
)
# Add to our list
+2 -1
View File
@@ -2,6 +2,7 @@ from datetime import datetime
import pytz
from core.enums import AlertType
from data.alert import Alert
from data.sig_ref import SIGRef
from providers.alert.http_alert_provider import HTTPAlertProvider
@@ -34,7 +35,7 @@ class WWFF(HTTPAlertProvider):
end_time=datetime.strptime(source_alert["utc_end"], "%Y-%m-%d %H:%M:%S")
.replace(tzinfo=pytz.UTC)
.timestamp(),
is_dxpedition=False,
alert_type=AlertType.XOTA,
)
# Add to our list
+1 -1
View File
@@ -30,7 +30,7 @@ class ZLOTA(FileDownloadSIGRefDataProvider):
try:
ref_type = SIGRefType(ref["asset_type"].title().upper())
except ValueError:
ref_type = SIGRefType.UNKNOWN
ref_type = None
new_ref = SIGRef(
sig=self.SIG,
+1 -3
View File
@@ -66,9 +66,7 @@ class GMA(HTTPSpotProvider):
if (source_spot["QRG"] != "" and source_spot["QRG"] != "QRT")
else None,
# Filter out some weird mode strings
mode=Mode.from_name(source_spot["MODE"].upper())
if "<>" not in source_spot["MODE"]
else Mode.UNKNOWN,
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
comment=source_spot["TEXT"],
sig_refs=[
SIGRef(
+2 -2
View File
@@ -27,7 +27,7 @@ class WWBOTA(SSESpotProvider):
name=ref["name"],
latitude=ref["lat"],
longitude=ref["long"],
ref_type=SIGRefType.BUNKER
ref_type=SIGRefType.BUNKER,
)
refs.append(sigref)
@@ -36,7 +36,7 @@ class WWBOTA(SSESpotProvider):
dx_call=source_spot["call"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["freq"]) * 1000000,
mode=Mode.from_name(source_spot["mode"].upper()) if "mode" in source_spot else Mode.UNKNOWN,
mode=Mode.from_name(source_spot["mode"].upper()) if "mode" in source_spot else None,
comment=source_spot["comment"],
sig="WWBOTA",
sig_refs=refs,
+1
View File
@@ -25,3 +25,4 @@ ruff~=0.16.3
pdfplumber~=0.11.10
xlrd~=2.0.2
openpyxl~=3.1.5
icalendar~=7.3.0
+13 -5
View File
@@ -9,6 +9,7 @@ import tornado_eventsource.handler
from tornado import httputil
from tornado.web import Application
from core.enums import AlertType
from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
from data.lookup_credentials import extract_credentials
@@ -169,11 +170,18 @@ def alert_allowed_by_query(alert, query):
max_duration = int(query.get(k))
# Check the duration if end_time is provided. If end_time is not provided, assume the activation is
# "short", i.e. it always passes this check. If dxpeditions_skip_max_duration_check is true and
# the alert is a dxpedition, it also always passes the check.
if alert.is_dxpedition and (
query.get("dxpeditions_skip_max_duration_check").upper() == "TRUE"
if "dxpeditions_skip_max_duration_check" in query
else False
# the alert is a dxpedition, or contests_skip_max_duration_check and the alert is a contest, it also
# always passes the check.
if (
alert.alert_type == AlertType.DXPEDITION
and "dxpeditions_skip_max_duration_check" in query
and query.get("dxpeditions_skip_max_duration_check").upper() == "TRUE"
):
continue
if (
alert.alert_type == AlertType.CONTEST
and "contests_skip_max_duration_check" in query
and query.get("contests_skip_max_duration_check").upper() == "TRUE"
):
continue
if alert.end_time and alert.start_time and alert.end_time - alert.start_time > max_duration:
+32 -7
View File
@@ -19,8 +19,9 @@ info:
* Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
* Added `sig_type`, `icon`, `region_flag` and `refs_globally_unique` to SIG information
* Unknown modes and mode types now return "UNKNOWN" not null
* Added `sig_type`, `icon`, `region_flag` and `refs_globally_unique` to SIG data
* Added `alert_type` and `url` to alert data
* Added `contests_skip_max_duration_check` to alert query parameters
* SIG reference types (e.g. "Park") are now capitalised to match other enums
### 2.0
@@ -209,6 +210,7 @@ paths:
- $ref: '#/components/parameters/AlertReceivedSince'
- $ref: '#/components/parameters/AlertMaxDuration'
- $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck'
- $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck'
- $ref: '#/components/parameters/AlertSource'
- $ref: '#/components/parameters/AlertSig'
- $ref: '#/components/parameters/AlertDxContinent'
@@ -243,6 +245,7 @@ paths:
parameters:
- $ref: '#/components/parameters/AlertMaxDuration'
- $ref: '#/components/parameters/AlertDxpeditionsSkipMaxDurationCheck'
- $ref: '#/components/parameters/AlertContestsSkipMaxDurationCheck'
- $ref: '#/components/parameters/AlertSource'
- $ref: '#/components/parameters/AlertSig'
- $ref: '#/components/parameters/AlertDxContinent'
@@ -661,8 +664,8 @@ components:
time minus start time, if end time is set, otherwise the activation is assumed to be short and
therefore to always pass this check. This is useful to filter out people who alert POTA
activations lasting months or even years, but note it will also include multi-day or multi-week
DXpeditions that you might otherwise be interested in. See the
dxpeditions_skip_max_duration_check parameter for the workaround.
DXpeditions or contests that you might otherwise be interested in. See the
dxpeditions_skip_max_duration_check and contests_skip_max_duration_check parameters for the workaround.
schema:
type: integer
AlertDxpeditionsSkipMaxDurationCheck:
@@ -675,6 +678,16 @@ components:
on the air most of the time.
schema:
type: boolean
AlertContestsSkipMaxDurationCheck:
name: contests_skip_max_duration_check
in: query
description: >
Return contest alerts even if they last longer than max_duration. This allows the user to
filter out multi-day/multi-week POTA alerts where the operator likely won't be on the air most
of the time, but keep multi-day/multi-week contests where contesters likely *will* be
on the air most of the time.
schema:
type: boolean
AlertSource:
name: source
in: query
@@ -903,9 +916,16 @@ components:
- REGION
- GRID
- TOILET
- UNKNOWN
example: PARK
AlertType:
type: string
enum:
- XOTA
- DXPEDITION
- CONTEST
example: XOTA
Continent:
type: string
enum:
@@ -970,7 +990,6 @@ components:
- FSK
- PKT
- MSK144
- UNKNOWN
example: SSB
ModeType:
@@ -979,7 +998,6 @@ components:
- CW
- PHONE
- DATA
- UNKNOWN
example: CW
ModeSource:
@@ -1433,6 +1451,13 @@ components:
items:
$ref: '#/components/schemas/SIGRef'
description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
alert_type:
description: "The type of alert this is: xOTA, DXpedition, or Contest."
$ref: "#/components/schemas/AlertType"
url:
type: string
description: A URL linking to more information about the alert, e.g. DXpedition or contest info.
example: "https://www.rsgbcc.org/cgi-bin/contest_rules.pl?year=2026&contest=144backpack1"
source:
type: string
description: Where we got the alert from.
+11 -25
View File
@@ -13,7 +13,6 @@ function loadAlerts() {
url: '/api/v2/alerts' + buildQueryString(), dataType: 'json', success: function (jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
// Store data
alerts = jsonData;
// Update table
@@ -38,6 +37,9 @@ function buildQueryString() {
if ($("#dxpeditions_skip_max_duration_check")[0].checked) {
str = str + "&dxpeditions_skip_max_duration_check=true";
}
if ($("#contests_skip_max_duration_check")[0].checked) {
str = str + "&contests_skip_max_duration_check=true";
}
return str;
}
@@ -208,10 +210,14 @@ function addAlertRowsToTable(tbody, alerts) {
if (a["dx_calls"] != null) {
dx_calls_html = a["dx_calls"].map(call => `<a class='dx-link' href='https://qrz.com/db/${call}' target='_new'>${call}</a>`).join(", ");
}
if (dx_calls_html === "" && a["alert_type"] === "CONTEST") {
// Contest = true and no DX callsigns, so display "Contest"
dx_calls_html = "Contest"
}
// Format DXpedition country
let dx_country_html = "";
if (a["is_dxpedition"] === true && a["dx_country"] != null && a["dx_country"] !== "") {
if (a["alert_type"] === "DXPEDITION" && a["dx_country"] != null && a["dx_country"] !== "") {
dx_country_html = `<br/>${a["dx_country"]}`;
}
@@ -226,6 +232,9 @@ function addAlertRowsToTable(tbody, alerts) {
if (a["comment"] != null) {
commentText = escapeHtml(a["comment"]);
}
if (a["url"] != null) {
commentText = `<a href="${escapeHtml(a['url'])}" target="_new">${commentText}</a>`;
}
// Sig or fallback to source
let sigSourceText = a["source"];
@@ -331,33 +340,10 @@ function filtersUpdated() {
saveSettings();
}
// Update the refresh timing display
function updateRefreshDisplay() {
if (lastUpdateTime != null) {
let secSinceUpdate = moment.duration(moment().diff(lastUpdateTime)).asSeconds();
let count = REFRESH_INTERVAL_SEC;
let updatingString = "Updating..."
if (secSinceUpdate < REFRESH_INTERVAL_SEC) {
count = REFRESH_INTERVAL_SEC - secSinceUpdate;
let number;
if (count <= 60) {
number = count.toFixed(0);
updatingString = "<span class='nowrap'>Updating in " + number + " second" + (number !== "1" ? "s" : "") + ".</span>";
} else {
number = Math.round(count / 60.0).toFixed(0);
updatingString = "<span class='nowrap'>Updating in " + number + " minute" + (number !== "1" ? "s" : "") + ".</span>";
}
}
$("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString);
}
}
// Startup
$(document).ready(function () {
// Call loadOptions(), this will then trigger loading alerts and setting up timers.
loadOptions();
// Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000);
});
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
+1 -1
View File
@@ -77,7 +77,7 @@
</div>
<script src="/static/js/add-spot.js?v=1788546948"></script>
<script src="/static/js/add-spot.js?v=1788595232"></script>
<script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -4
View File
@@ -3,9 +3,6 @@
<div class="mt-3">
<div id="settingsButtonRow" class="row mb-3">
<div class="col-auto me-auto pt-3">
{% module Template("widgets/refresh_timer.html", web_ui_options=web_ui_options) %}
</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) %}
@@ -84,7 +81,7 @@
</div>
<script src="/static/js/alerts.js?v=1788546948"></script>
<script src="/static/js/alerts.js?v=1788595233"></script>
<script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -76,8 +76,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1788546948"></script>
<script src="/static/js/bands.js?v=1788546948"></script>
<script src="/static/js/spotsbandsandmap.js?v=1788595232"></script>
<script src="/static/js/bands.js?v=1788595232"></script>
<script>$(document).ready(function () {
$("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1788546948" type="text/css">
<link rel="stylesheet" href="/static/css/style.css?v=1788595232" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -16,10 +16,10 @@
window.fetchEventSource = fetchEventSource;
</script>
<script src="/static/js/utils.js?v=1788546948"></script>
<script src="/static/js/ui-ham.js?v=1788546948"></script>
<script src="/static/js/geo.js?v=1788546948"></script>
<script src="/static/js/common.js?v=1788546948"></script>
<script src="/static/js/utils.js?v=1788595232"></script>
<script src="/static/js/ui-ham.js?v=1788595232"></script>
<script src="/static/js/geo.js?v=1788595232"></script>
<script src="/static/js/common.js?v=1788595232"></script>
{% end %}
{% block body %}
<div class="container">
@@ -21,5 +21,11 @@
for="dxpeditions_skip_max_duration_check">Allow
DXpeditions that are longer</label>
</p>
<p class='card-text spothole-card-text' style='line-height: 1.5em !important;'>
<input class="form-check-input storeable-checkbox" type="checkbox" value="" onclick="filtersUpdated();"
id="contests_skip_max_duration_check" checked><label class="form-check-label ms-2"
for="contests_skip_max_duration_check">Allow
DXpeditions that are longer</label>
</p>
</div>
</div>
+1 -1
View File
@@ -284,7 +284,7 @@
</div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1788546948"></script>
<script src="/static/js/conditions.js?v=1788595232"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script>
<script src="/static/js/spotsbandsandmap.js?v=1788546948"></script>
<script src="/static/js/map.js?v=1788546948"></script>
<script src="/static/js/spotsbandsandmap.js?v=1788595233"></script>
<script src="/static/js/map.js?v=1788595233"></script>
<script>$(document).ready(function () {
$("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1788546948"></script>
<script src="/static/js/spots.js?v=1788546948"></script>
<script src="/static/js/spotsbandsandmap.js?v=1788595232"></script>
<script src="/static/js/spots.js?v=1788595232"></script>
<script>$(document).ready(function () {
$("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -86,7 +86,7 @@
</div>
</div>
<script src="/static/js/status.js?v=1788546948"></script>
<script src="/static/js/status.js?v=1788595232"></script>
<script>
$(document).ready(function () {
$("#nav-link-status").addClass("active");
-1
View File
@@ -1 +0,0 @@
<div id="timing-container">Loading...</div>