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
BANDS = [
Band(name="160m", start_freq=1800, end_freq=2000, color="#7cfc00", contrast_color="black"),
Band(name="80m", start_freq=3500, end_freq=4000, color="#e550e5", contrast_color="black"),
Band(name="60m", start_freq=5250, end_freq=5410, color="#00008b", contrast_color="white"),
Band(name="40m", start_freq=7000, end_freq=7300, color="#5959ff", contrast_color="white"),
Band(name="30m", start_freq=10100, end_freq=10150, color="#62d962", contrast_color="black"),
Band(name="20m", start_freq=14000, end_freq=14350, color="#f2c40c", contrast_color="black"),
Band(name="17m", start_freq=18068, end_freq=18168, color="#f2f261", contrast_color="black"),
Band(name="15m", start_freq=21000, end_freq=21450, color="#cca166", contrast_color="black"),
Band(name="12m", start_freq=24890, end_freq=24990, color="#b22222", contrast_color="white"),
Band(name="10m", start_freq=28000, end_freq=29700, color="#ff69b4", contrast_color="black"),
Band(name="6m", start_freq=50000, end_freq=54000, color="#FF0000", contrast_color="white"),
Band(name="4m", start_freq=70000, end_freq=70500, color="#cc0044", contrast_color="white"),
Band(name="2m", start_freq=144000, end_freq=148000, color="#FF1493", contrast_color="black"),
Band(name="70cm", start_freq=420000, end_freq=450000, color="#999900", contrast_color="white"),
Band(name="23cm", start_freq=1240000, end_freq=1325000, color="#5AB8C7", contrast_color="black"),
Band(name="13cm", start_freq=2300000, end_freq=2450000, color="#FF7F50", contrast_color="black")]
Band(name="160m", start_freq=1800000, end_freq=2000000, color="#7cfc00", contrast_color="black"),
Band(name="80m", start_freq=3500000, end_freq=4000000, color="#e550e5", contrast_color="black"),
Band(name="60m", start_freq=5250000, end_freq=5410000, color="#00008b", contrast_color="white"),
Band(name="40m", start_freq=7000000, end_freq=7300000, color="#5959ff", contrast_color="white"),
Band(name="30m", start_freq=10100000, end_freq=10150000, color="#62d962", contrast_color="black"),
Band(name="20m", start_freq=14000000, end_freq=14350000, color="#f2c40c", contrast_color="black"),
Band(name="17m", start_freq=18068000, end_freq=18168000, color="#f2f261", contrast_color="black"),
Band(name="15m", start_freq=21000000, end_freq=21450000, color="#cca166", contrast_color="black"),
Band(name="12m", start_freq=24890000, end_freq=24990000, color="#b22222", contrast_color="white"),
Band(name="10m", start_freq=28000000, end_freq=29700000, color="#ff69b4", contrast_color="black"),
Band(name="6m", start_freq=50000000, end_freq=54000000, color="#FF0000", contrast_color="white"),
Band(name="4m", start_freq=70000000, end_freq=70500000, color="#cc0044", contrast_color="white"),
Band(name="2m", start_freq=144000000, end_freq=148000000, color="#FF1493", contrast_color="black"),
Band(name="70cm", start_freq=420000000, end_freq=450000000, color="#999900", contrast_color="white"),
Band(name="23cm", start_freq=1240000000, end_freq=1325000000, color="#5AB8C7", 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")
# 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.")
return None
# Infer a band from a frequency in kHz
# Infer a band from a frequency in Hz
def infer_band_from_freq(freq):
for b in BANDS:
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)
def infer_latlon_from_callsign_dxcc(call):
try:
data = CALL_INFO_BASIC.get_lat_long(call)
if data and "latitude" in data and "longitude" in data:
return [data["latitude"], data["longitude"]]
else:
return None
except KeyError:
return None
# Infer a grid locator from a callsign (using DXCC, probably very inaccurate)
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)
# 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):
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.

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ class GMA(HTTPProvider):
spot = Spot(source=self.name,
dx_call=source_spot["ACTIVATOR"].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
comment=source_spot["TEXT"],
sig_refs=[source_spot["REF"]],

View File

@@ -48,7 +48,7 @@ class HEMA(HTTPProvider):
spot = Spot(source=self.name,
dx_call=spot_items[2].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(),
comment=spotter_comment_match.group(2),
sig="HEMA",

View File

@@ -24,7 +24,7 @@ class ParksNPeaks(HTTPProvider):
source_id=source_spot["actID"],
dx_call=source_spot["actCallsign"].upper(),
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(),
comment=source_spot["actComments"],
sig=source_spot["actClass"],

View File

@@ -23,7 +23,7 @@ class POTA(HTTPProvider):
source_id=source_spot["spotId"],
dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency"]),
freq=float(source_spot["frequency"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="POTA",

View File

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

View File

@@ -42,7 +42,7 @@ class SOTA(HTTPProvider):
dx_call=source_spot["activatorCallsign"].upper(),
dx_name=source_spot["activatorName"],
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(),
comment=source_spot["comments"],
sig="SOTA",

View File

@@ -26,7 +26,7 @@ class WWBOTA(HTTPProvider):
spot = Spot(source=self.name,
dx_call=source_spot["call"].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(),
comment=source_spot["comment"],
sig="WWBOTA",

View File

@@ -23,7 +23,7 @@ class WWFF(HTTPProvider):
source_id=source_spot["id"],
dx_call=source_spot["activator"].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(),
comment=source_spot["remarks"],
sig="WWFF",

View File

@@ -7,10 +7,45 @@
</div>
<div class="col-auto">
<p class="d-inline-flex gap-1">
<button type="button" class="btn btn-primary" data-bs-toggle="button">Filters</button>
<button type="button" class="btn btn-primary" data-bs-toggle="button">Status</button>
<button id="filters-button" type="button" class="btn btn-primary">Filters</button>
<button id="status-button" type="button" class="btn btn-primary">Status</button>
</p>
</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>

View File

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

View File

@@ -48,3 +48,11 @@ tr.table-faded td {
color: lightgray;
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
var mhz = Math.floor(s["freq"] / 1000.0);
var khz = Math.floor(s["freq"] - (mhz * 1000.0));
var hz = Math.floor((s["freq"] - Math.floor(s["freq"])) * 1000.0);
var mhz = Math.floor(s["freq"] / 1000000.0);
var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0);
var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.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>`
@@ -105,8 +105,42 @@ function updateTable() {
// Load server status
function loadStatus() {
$.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
@@ -121,6 +155,9 @@ function loadOptions() {
band_colors[m["name"]] = m["color"]
});
// Populate the filters panel
$("#filters-container").text(JSON.stringify(options));
// Load spots and set up the timer
loadSpots();
setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000)
@@ -162,8 +199,22 @@ function escapeHtml(str) {
return str.replace(/[&<>"'`]/g, escapeCharacter);
}
// Startup
$(document).ready(function() {
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000);
// Startup. Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Update the refresh timing display every second
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(); });
});