Set up status and start working on filters panel #7

This commit is contained in:
Ian Renton
2025-10-02 19:33:39 +01:00
parent 9f3fc8146b
commit 0e262f68f5
17 changed files with 151 additions and 51 deletions

View File

@@ -19,22 +19,22 @@ MODE_TYPES = ["CW", "PHONE", "DATA"]
# Band definitions # Band definitions
BANDS = [ BANDS = [
Band(name="160m", start_freq=1800, end_freq=2000, color="#7cfc00", contrast_color="black"), Band(name="160m", start_freq=1800000, end_freq=2000000, color="#7cfc00", contrast_color="black"),
Band(name="80m", start_freq=3500, end_freq=4000, color="#e550e5", contrast_color="black"), Band(name="80m", start_freq=3500000, end_freq=4000000, color="#e550e5", contrast_color="black"),
Band(name="60m", start_freq=5250, end_freq=5410, color="#00008b", contrast_color="white"), Band(name="60m", start_freq=5250000, end_freq=5410000, color="#00008b", contrast_color="white"),
Band(name="40m", start_freq=7000, end_freq=7300, color="#5959ff", contrast_color="white"), Band(name="40m", start_freq=7000000, end_freq=7300000, color="#5959ff", contrast_color="white"),
Band(name="30m", start_freq=10100, end_freq=10150, color="#62d962", contrast_color="black"), Band(name="30m", start_freq=10100000, end_freq=10150000, color="#62d962", contrast_color="black"),
Band(name="20m", start_freq=14000, end_freq=14350, color="#f2c40c", contrast_color="black"), Band(name="20m", start_freq=14000000, end_freq=14350000, color="#f2c40c", contrast_color="black"),
Band(name="17m", start_freq=18068, end_freq=18168, color="#f2f261", contrast_color="black"), Band(name="17m", start_freq=18068000, end_freq=18168000, color="#f2f261", contrast_color="black"),
Band(name="15m", start_freq=21000, end_freq=21450, color="#cca166", contrast_color="black"), Band(name="15m", start_freq=21000000, end_freq=21450000, color="#cca166", contrast_color="black"),
Band(name="12m", start_freq=24890, end_freq=24990, color="#b22222", contrast_color="white"), Band(name="12m", start_freq=24890000, end_freq=24990000, color="#b22222", contrast_color="white"),
Band(name="10m", start_freq=28000, end_freq=29700, color="#ff69b4", contrast_color="black"), Band(name="10m", start_freq=28000000, end_freq=29700000, color="#ff69b4", contrast_color="black"),
Band(name="6m", start_freq=50000, end_freq=54000, color="#FF0000", contrast_color="white"), Band(name="6m", start_freq=50000000, end_freq=54000000, color="#FF0000", contrast_color="white"),
Band(name="4m", start_freq=70000, end_freq=70500, color="#cc0044", contrast_color="white"), Band(name="4m", start_freq=70000000, end_freq=70500000, color="#cc0044", contrast_color="white"),
Band(name="2m", start_freq=144000, end_freq=148000, color="#FF1493", contrast_color="black"), Band(name="2m", start_freq=144000000, end_freq=148000000, color="#FF1493", contrast_color="black"),
Band(name="70cm", start_freq=420000, end_freq=450000, color="#999900", contrast_color="white"), Band(name="70cm", start_freq=420000000, end_freq=450000000, color="#999900", contrast_color="white"),
Band(name="23cm", start_freq=1240000, end_freq=1325000, color="#5AB8C7", contrast_color="black"), Band(name="23cm", start_freq=1240000000, end_freq=1325000000, color="#5AB8C7", contrast_color="black"),
Band(name="13cm", start_freq=2300000, end_freq=2450000, color="#FF7F50", contrast_color="black")] Band(name="13cm", start_freq=2300000000, end_freq=2450000000, color="#FF7F50", contrast_color="black")]
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white") UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white")
# Continents # Continents

View File

@@ -38,7 +38,7 @@ def infer_mode_type_from_mode(mode):
logging.warn("Found an unrecognised mode: " + mode + ". Developer should categorise this.") logging.warn("Found an unrecognised mode: " + mode + ". Developer should categorise this.")
return None return None
# Infer a band from a frequency in kHz # Infer a band from a frequency in Hz
def infer_band_from_freq(freq): def infer_band_from_freq(freq):
for b in BANDS: for b in BANDS:
if b.start_freq <= freq <= b.end_freq: if b.start_freq <= freq <= b.end_freq:
@@ -126,11 +126,14 @@ def infer_grid_from_callsign_qrz(call):
# Infer a latitude and longitude from a callsign (using DXCC, probably very inaccurate) # Infer a latitude and longitude from a callsign (using DXCC, probably very inaccurate)
def infer_latlon_from_callsign_dxcc(call): def infer_latlon_from_callsign_dxcc(call):
try:
data = CALL_INFO_BASIC.get_lat_long(call) data = CALL_INFO_BASIC.get_lat_long(call)
if data and "latitude" in data and "longitude" in data: if data and "latitude" in data and "longitude" in data:
return [data["latitude"], data["longitude"]] return [data["latitude"], data["longitude"]]
else: else:
return None return None
except KeyError:
return None
# Infer a grid locator from a callsign (using DXCC, probably very inaccurate) # Infer a grid locator from a callsign (using DXCC, probably very inaccurate)
def infer_grid_from_callsign_dxcc(call): def infer_grid_from_callsign_dxcc(call):
@@ -138,9 +141,12 @@ def infer_grid_from_callsign_dxcc(call):
return latlong_to_locator(latlon[0], latlon[1], 8) return latlong_to_locator(latlon[0], latlon[1], 8)
# Infer a mode from the frequency according to the band plan. Just a guess really. # Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really.
def infer_mode_from_frequency(freq): def infer_mode_from_frequency(freq):
return freq_to_band(freq)["mode"] try:
return freq_to_band(freq / 1000.0)["mode"]
except KeyError:
return None
# Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things. # Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things.

View File

@@ -5,9 +5,9 @@ from dataclasses import dataclass
class Band: class Band:
# Band name # Band name
name: str name: str
# Start frequency, in kHz # Start frequency, in Hz
start_freq: float start_freq: float
# Stop frequency, in kHz # Stop frequency, in Hz
end_freq: float end_freq: float
# Colour to use for this band, as per PSK Reporter # Colour to use for this band, as per PSK Reporter
color: str color: str

View File

@@ -53,7 +53,7 @@ class Spot:
mode_type: str = None mode_type: str = None
# Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE" # Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE"
mode_source: str = "NONE" mode_source: str = "NONE"
# Frequency, in kHz # Frequency, in Hz
freq: float = None freq: float = None
# Band, defined by the frequency, e.g. "40m" or "70cm" # Band, defined by the frequency, e.g. "40m" or "70cm"
band: str = None band: str = None

View File

@@ -67,7 +67,7 @@ class DXCluster(Provider):
spot = Spot(source=self.name, spot = Spot(source=self.name,
dx_call=match.group(3), dx_call=match.group(3),
de_call=match.group(1), de_call=match.group(1),
freq=float(match.group(2)), freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(), comment=match.group(4).strip(),
icon="desktop", icon="desktop",
time=spot_datetime) time=spot_datetime)

View File

@@ -27,7 +27,7 @@ class GMA(HTTPProvider):
spot = Spot(source=self.name, spot = Spot(source=self.name,
dx_call=source_spot["ACTIVATOR"].upper(), dx_call=source_spot["ACTIVATOR"].upper(),
de_call=source_spot["SPOTTER"].upper(), de_call=source_spot["SPOTTER"].upper(),
freq=float(source_spot["QRG"]) if (source_spot["QRG"] != "") else None, # Seen GMA spots with no frequency freq=float(source_spot["QRG"]) * 1000 if (source_spot["QRG"] != "") else None, # Seen GMA spots with no frequency
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None, # Filter out some weird mode strings mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None, # Filter out some weird mode strings
comment=source_spot["TEXT"], comment=source_spot["TEXT"],
sig_refs=[source_spot["REF"]], sig_refs=[source_spot["REF"]],

View File

@@ -48,7 +48,7 @@ class HEMA(HTTPProvider):
spot = Spot(source=self.name, spot = Spot(source=self.name,
dx_call=spot_items[2].upper(), dx_call=spot_items[2].upper(),
de_call=spotter_comment_match.group(1).upper(), de_call=spotter_comment_match.group(1).upper(),
freq=float(freq_mode_match.group(1)) * 1000, 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="HEMA",

View File

@@ -24,7 +24,7 @@ class ParksNPeaks(HTTPProvider):
source_id=source_spot["actID"], source_id=source_spot["actID"],
dx_call=source_spot["actCallsign"].upper(), dx_call=source_spot["actCallsign"].upper(),
de_call=source_spot["actSpoter"].upper(), # typo exists in API de_call=source_spot["actSpoter"].upper(), # typo exists in API
freq=float(source_spot["actFreq"]) * 1000 if (source_spot["actFreq"] != "") else None, # Seen PNP spots with empty frequency! freq=float(source_spot["actFreq"]) * 1000000 if (source_spot["actFreq"] != "") else None, # Seen PNP spots with empty frequency!
mode=source_spot["actMode"].upper(), mode=source_spot["actMode"].upper(),
comment=source_spot["actComments"], comment=source_spot["actComments"],
sig=source_spot["actClass"], sig=source_spot["actClass"],

View File

@@ -23,7 +23,7 @@ class POTA(HTTPProvider):
source_id=source_spot["spotId"], source_id=source_spot["spotId"],
dx_call=source_spot["activator"].upper(), dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency"]), 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="POTA",

View File

@@ -68,7 +68,7 @@ class RBN(Provider):
spot = Spot(source=self.name, spot = Spot(source=self.name,
dx_call=match.group(3), dx_call=match.group(3),
de_call=match.group(1), de_call=match.group(1),
freq=float(match.group(2)), freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(), comment=match.group(4).strip(),
icon="tower-cell", icon="tower-cell",
time=spot_datetime) time=spot_datetime)

View File

@@ -42,7 +42,7 @@ class SOTA(HTTPProvider):
dx_call=source_spot["activatorCallsign"].upper(), dx_call=source_spot["activatorCallsign"].upper(),
dx_name=source_spot["activatorName"], dx_name=source_spot["activatorName"],
de_call=source_spot["callsign"].upper(), de_call=source_spot["callsign"].upper(),
freq=(float(source_spot["frequency"]) * 1000) 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="SOTA",

View File

@@ -26,7 +26,7 @@ class WWBOTA(HTTPProvider):
spot = Spot(source=self.name, spot = Spot(source=self.name,
dx_call=source_spot["call"].upper(), dx_call=source_spot["call"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=float(source_spot["freq"]) * 1000, # MHz to kHz 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="WWBOTA",

View File

@@ -23,7 +23,7 @@ class WWFF(HTTPProvider):
source_id=source_spot["id"], source_id=source_spot["id"],
dx_call=source_spot["activator"].upper(), dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(), de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency_khz"]), 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="WWFF",

View File

@@ -7,10 +7,45 @@
</div> </div>
<div class="col-auto"> <div class="col-auto">
<p class="d-inline-flex gap-1"> <p class="d-inline-flex gap-1">
<button type="button" class="btn btn-primary" data-bs-toggle="button">Filters</button> <button id="filters-button" type="button" class="btn btn-primary">Filters</button>
<button type="button" class="btn btn-primary" data-bs-toggle="button">Status</button> <button id="status-button" type="button" class="btn btn-primary">Status</button>
</p> </p>
</div> </div>
</div> </div>
<div id="status-area" class="appearing-panel card mb-3">
<div class="card-header text-white bg-primary">
<div class="row">
<div class="col-auto me-auto">
Status
</div>
<div class="col-auto d-inline-flex">
<button id="close-status-button" type="button" class="btn-close btn-close-white" aria-label="Close"></button>
</div>
</div>
</div>
<div class="card-body">
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4"></div>
</div>
</div>
<div id="filters-area" class="appearing-panel card mb-3">
<div class="card-header text-white bg-primary">
<div class="row">
<div class="col-auto me-auto">
Filters
</div>
<div class="col-auto d-inline-flex">
<button id="close-filters-button" type="button" class="btn-close btn-close-white" aria-label="Close"></button>
</div>
</div>
</div>
<div class="card-body">
<div id="filters-container" class="row row-cols-1 row-cols-md-2 g-4"></div>
</div>
</div>
<div id="table-container"></div> <div id="table-container"></div>
</div> </div>

View File

@@ -430,8 +430,8 @@ components:
- NONE - NONE
freq: freq:
type: number type: number
description: Frequency, in kHz description: Frequency, in Hz
example: 7150.5 example: 7150500
band: band:
type: string type: string
description: Band, defined by the frequency. description: Band, defined by the frequency.
@@ -579,12 +579,12 @@ components:
example: 40m example: 40m
start_freq: start_freq:
type: int type: int
description: The start frequency of this band, in kHz. description: The start frequency of this band, in Hz.
example: 7000 example: 7000000
end_freq: end_freq:
type: int type: int
description: The end frequency of this band, in kHz. description: The end frequency of this band, in Hz.
example: 7200 example: 7200000
color: color:
type: string type: string
description: The color associated with this mode, as used on PSK Reporter. description: The color associated with this mode, as used on PSK Reporter.

View File

@@ -48,3 +48,11 @@ tr.table-faded td {
color: lightgray; color: lightgray;
text-decoration: line-through !important; text-decoration: line-through !important;
} }
div.appearing-panel {
display: none;
}
div.status-card {
max-width: 18rem;
}

View File

@@ -50,9 +50,9 @@ function updateTable() {
} }
// Format the frequency // Format the frequency
var mhz = Math.floor(s["freq"] / 1000.0); var mhz = Math.floor(s["freq"] / 1000000.0);
var khz = Math.floor(s["freq"] - (mhz * 1000.0)); var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0);
var hz = Math.floor((s["freq"] - Math.floor(s["freq"])) * 1000.0); var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0));
var hz_string = (hz > 0) ? hz.toFixed(0) : ""; var hz_string = (hz > 0) ? hz.toFixed(0) : "";
var freq_string = `<span class='freq-mhz'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz'>${hz_string}</span>` var freq_string = `<span class='freq-mhz'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz'>${hz_string}</span>`
@@ -105,8 +105,42 @@ function updateTable() {
// Load server status // Load server status
function loadStatus() { function loadStatus() {
$.getJSON('/api/status', function(jsonData) { $.getJSON('/api/status', function(jsonData) {
$('#status-container').html(jsonData); // todo implement $("#status-container").append(generateStatusCard("Server Information", [
`Software Version: ${jsonData["software-version"]}`,
`Server Owner Callsign: ${jsonData["server-owner-callsign"]}`,
`Server Uptime: ${jsonData["uptime"]}`,
`Memory Use: ${jsonData["mem_use_mb"]} MB`,
`Total Spots: ${jsonData["num_spots"]}`
]));
$("#status-container").append(generateStatusCard("Web Server", [
`Status: ${jsonData["webserver"]["status"]}`,
`Last API Access: ${moment.utc(jsonData["webserver"]["last_api_access"], moment.ISO_8601).format("HH:mm")}`,
`Last Page Access: ${moment.utc(jsonData["webserver"]["last_page_access"], moment.ISO_8601).format("HH:mm")}`
]));
$("#status-container").append(generateStatusCard("Cleanup Service", [
`Status: ${jsonData["cleanup"]["status"]}`,
`Last Ran: ${moment.utc(jsonData["cleanup"]["last_ran"], moment.ISO_8601).format("HH:mm")}`
]));
jsonData["providers"].forEach(p => {
$("#status-container").append(generateStatusCard("Provider: " + p["name"], [
`Status: ${p["status"]}`,
`Last Updated: ${p["enabled"] ? moment.utc(p["last_updated"], moment.ISO_8601).format("HH:mm") : "N/A"}`,
`Latest Spot: ${p["enabled"] ? moment.utc(p["last_spot"], moment.ISO_8601).format("HH:mm YYYY-MM-DD") : "N/A"}`
]));
}); });
});
}
// Generate a status card
function generateStatusCard(title, textLines) {
let $col = $("<div class='col'>")
let $card = $("<div class='card status-card'>");
let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>${title}</h5>`);
$card_body.append(`<p class='card-text'>${textLines.join("<br/>")}</p>`);
$card.append($card_body);
$col.append($card);
return $col;
} }
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query // Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
@@ -121,6 +155,9 @@ function loadOptions() {
band_colors[m["name"]] = m["color"] band_colors[m["name"]] = m["color"]
}); });
// Populate the filters panel
$("#filters-container").text(JSON.stringify(options));
// Load spots and set up the timer // Load spots and set up the timer
loadSpots(); loadSpots();
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000) setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000)
@@ -162,8 +199,22 @@ function escapeHtml(str) {
return str.replace(/[&<>"'`]/g, escapeCharacter); return str.replace(/[&<>"'`]/g, escapeCharacter);
} }
// Startup
// Startup. Call loadOptions(), this will then trigger loading spots and setting up timers. $(document).ready(function() {
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions(); loadOptions();
// Update the refresh timing display every second // Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000); setInterval(updateRefreshDisplay, 1000);
// Event listeners
$("#status-button").click(function() {
// If we are going to display status, load the data
if (!$("#status-area").is(":visible")) {
loadStatus();
}
$("#status-area").toggle();
});
$("#close-status-button").click(function() { $("#status-area").hide(); });
$("#filters-button").click(function() { $("#filters-area").toggle(); });
$("#close-filters-button").click(function() { $("#filters-area").hide(); });
});