5 Commits

Author SHA1 Message Date
Ian Renton
fa92657d9c Fix old alerts not getting deleted 2025-11-01 17:25:20 +00:00
Ian Renton
30fc333c8b Fix scrolling map filters panel on mobile 2025-11-01 17:05:47 +00:00
Ian Renton
0570b39e09 Add Spot page to allow sig and sig_ref entries. Closes #71 2025-11-01 12:38:57 +00:00
Ian Renton
1ed543872a Add Spot page to take mode options from API #71 2025-11-01 12:03:11 +00:00
Ian Renton
812d031a2c Fix link 2025-11-01 11:45:21 +00:00
8 changed files with 101 additions and 21 deletions

View File

@@ -38,7 +38,7 @@ class CleanupTimer:
for id in list(self.alerts.iterkeys()): for id in list(self.alerts.iterkeys()):
alert = self.alerts[id] alert = self.alerts[id]
if alert.expired(): if alert.expired():
self.alerts.evict(id) self.alerts.delete(id)
self.status = "OK" self.status = "OK"
self.last_cleanup_time = datetime.now(pytz.UTC) self.last_cleanup_time = datetime.now(pytz.UTC)

View File

@@ -24,11 +24,11 @@ SIGS = [
SIG(name="SIOTA", description="Silos on the Air", icon="wheat-awn", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"), SIG(name="SIOTA", description="Silos on the Air", icon="wheat-awn", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
SIG(name="WCA", description="World Castles Award", icon="chess-rook", ref_regex=r"[A-Z0-9]{1,3}\-\d+"), SIG(name="WCA", description="World Castles Award", icon="chess-rook", ref_regex=r"[A-Z0-9]{1,3}\-\d+"),
SIG(name="ZLOTA", description="New Zealand on the Air", icon="kiwi-bird", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d+"), SIG(name="ZLOTA", description="New Zealand on the Air", icon="kiwi-bird", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d+"),
SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
SIG(name="BOTA", description="Beaches on the Air", icon="water"),
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award", icon="earth-oceania", ref_regex=r""), SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award", icon="earth-oceania", ref_regex=r""),
SIG(name="WAB", description="Worked All Britain", icon="table-cells-large", ref_regex=r"[A-Z]{1,2}[0-9]{2}"), SIG(name="WAB", description="Worked All Britain", icon="table-cells-large", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
SIG(name="WAI", description="Worked All Ireland", icon="table-cells-large", ref_regex=r"[A-Z][0-9]{2}"), SIG(name="WAI", description="Worked All Ireland", icon="table-cells-large", ref_regex=r"[A-Z][0-9]{2}")
SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
SIG(name="BOTA", description="Beaches on the Air", icon="water")
] ]
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA". # Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".

View File

@@ -12,6 +12,8 @@ from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING
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
from core.sig_utils import get_ref_regex_for_sig
from data.sig_ref import SIGRef
from data.spot import Spot from data.spot import Spot
@@ -140,6 +142,14 @@ class WebServer:
json_spot = json.loads(post_data) json_spot = json.loads(post_data)
spot = Spot(**json_spot) spot = Spot(**json_spot)
# Converting to a spot object this way won't have coped with sig_ref objects, so fix that. (Would be nice to
# redo this in a functional style)
if spot.sig_refs:
real_sig_refs = []
for dict_obj in spot.sig_refs:
real_sig_refs.append(json.loads(json.dumps(dict_obj), object_hook=lambda d: SIGRef(**d)))
spot.sig_refs = real_sig_refs
# Reject if no timestamp, frequency, dx_call or de_call # Reject if no timestamp, frequency, dx_call or de_call
if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call: if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call:
response.content_type = 'application/json' response.content_type = 'application/json'
@@ -171,6 +181,13 @@ class WebServer:
response.status = 422 response.status = 422
return json.dumps("Error - '" + spot.dx_grid + "' does not look like a valid Maidenhead grid.", default=serialize_everything) return json.dumps("Error - '" + spot.dx_grid + "' does not look like a valid Maidenhead grid.", default=serialize_everything)
# Reject if sig_ref format incorrect for sig
print(spot.sig_refs[0])
if spot.sig and spot.sig_refs and len(spot.sig_refs) > 0 and spot.sig_refs[0].id and get_ref_regex_for_sig(spot.sig) and not re.match(get_ref_regex_for_sig(spot.sig), spot.sig_refs[0].id):
response.content_type = 'application/json'
response.status = 422
return json.dumps("Error - '" + spot.sig_refs[0].id + "' does not look like a valid reference for " + spot.sig + ".", default=serialize_everything)
# infer missing data, and add it to our database. # infer missing data, and add it to our database.
spot.source = "API" spot.source = "API"
if not spot.sig: if not spot.sig:

View File

@@ -2,7 +2,7 @@
<div id="add-spot-intro-box" class="permanently-dismissible-box mt-3"> <div id="add-spot-intro-box" class="permanently-dismissible-box mt-3">
<div class="alert alert-primary alert-dismissible fade show" role="alert"> <div class="alert alert-primary alert-dismissible fade show" role="alert">
<i class="fa-solid fa-circle-info"></i> <strong>Adding spots to Spothole</strong><br/>This page is implemented as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality might be extended to include this in future if there is demand for it. If you'd like this to be added, please give a thumbs-up on <a href="https://git.ianrenton.com/ian/spothole/issues/70" target="_new" class="alert-link">issue #70</a> or get in touch via email. <i class="fa-solid fa-circle-info"></i> <strong>Adding spots to Spothole</strong><br/>This page is implemented as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality might be extended to include this in future if there is demand for it. If you'd like this to be added, please give a thumbs-up on <a href="https://git.ianrenton.com/ian/spothole/issues/39" target="_new" class="alert-link">issue #39</a> or get in touch via email.
<button type="button" id="add-spot-intro-box-dismiss" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" id="add-spot-intro-box-dismiss" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
</div> </div>
@@ -17,38 +17,52 @@
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="row g-2"> <form class="row g-3">
<div class="col-auto"> <div class="col-auto">
<label for="dx-call" class="form-label">DX Call</label> <label for="dx-call" class="form-label">DX Call *</label>
<input type="text" class="form-control" id="dx-call" placeholder="N0CALL" style="max-width: 8em;"> <input type="text" class="form-control" id="dx-call" placeholder="N0CALL" style="max-width: 8em;">
</div> </div>
<div class="col-auto"> <div class="col-auto">
<label for="freq" class="form-label">Frequency (kHz)</label> <label for="freq" class="form-label">Frequency (kHz) *</label>
<input type="text" class="form-control" id="freq" placeholder="14100" style="max-width: 8em;"> <input type="text" class="form-control" id="freq" placeholder="e.g. 14100" style="max-width: 8em;">
</div> </div>
<div class="col-auto"> <div class="col-auto">
<label for="mode" class="form-label">Mode</label> <label for="mode" class="form-label">Mode</label>
<input type="text" class="form-control" id="mode" placeholder="SSB" style="max-width: 6em;"> <select id="mode" class="form-select">
<option value="" selected></option>
</select>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<label for="dx-grid" class="form-label">DX Grid (Optional)</label> <label for="sig" class="form-label">SIG</label>
<input type="text" class="form-control" id="dx-grid" placeholder="AA00aa" style="max-width: 8em;"> <select id="sig" class="form-select">
<option value="" selected></option>
</select>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<label for="comment" class="form-label">Comment (Optional)</label> <label for="sig-ref" class="form-label">SIG Reference</label>
<input type="text" class="form-control" id="comment" placeholder="59 TNX QSO 73" style="max-width: 12em;"> <input type="text" class="form-control" id="sig-ref" placeholder="e.g. GB-0001" style="max-width: 8em;">
</div> </div>
<div class="col-auto"> <div class="col-auto">
<label for="de-call" class="form-label">Your Call</label> <label for="dx-grid" class="form-label">DX Grid</label>
<input type="text" class="form-control" id="dx-grid" placeholder="e.g. AA00aa" style="max-width: 8em;">
</div>
<div class="col-auto">
<label for="comment" class="form-label">Comment</label>
<input type="text" class="form-control" id="comment" placeholder="e.g. 59 TNX QSO 73" style="max-width: 12em;">
</div>
<div class="col-auto">
<label for="de-call" class="form-label">Your Call *</label>
<input type="text" class="form-control storeable-text" id="de-call" placeholder="N0CALL" style="max-width: 8em;"> <input type="text" class="form-control storeable-text" id="de-call" placeholder="N0CALL" style="max-width: 8em;">
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-primary" style="margin-top: 2em;" onclick="addSpot();">Spot</button> <button type="button" class="btn btn-primary" style="margin-top: 2em;" onclick="addSpot();">Spot</button>
<span id="result-good"></span>
</div> </div>
</form> </form>
<div id="result-good"></div>
<div id="result-bad"></div> <div id="result-bad"></div>
<p class="small mt-4 mb-1">* Required field</p>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
% rebase('webpage_base.tpl') % rebase('webpage_base.tpl')
<div id="map"> <div id="map">
<div class="mt-3 px-3" style="z-index: 1002; position: relative;"> <div id="maptools" class="mt-3 px-3" style="z-index: 1002; position: relative;">
<div class="row"> <div class="row">
<div class="col-auto me-auto pt-3"></div> <div class="col-auto me-auto pt-3"></div>
<div class="col-auto"> <div class="col-auto">

View File

@@ -246,13 +246,24 @@ div.band-spot:hover span.band-spot-info {
/* GENERAL MOBILE SUPPORT */ /* GENERAL MOBILE SUPPORT */
@media (max-width: 991.99px) { @media (max-width: 991.99px) {
/* General "hide this on mobile" class */
.hideonmobile { .hideonmobile {
display: none !important; display: none !important;
} }
/* Make map stretch to horizontal screen edges */
div#map, div#table-container, div#bands-container { div#map, div#table-container, div#bands-container {
margin-left: -1em; margin-left: -1em;
margin-right: -1em; margin-right: -1em;
} }
/* Avoid map page filters panel being larger than the map itself */
#maptools .appearing-panel {
max-height: 30em;
}
#maptools .appearing-panel .card-body {
max-height: 26em;
overflow: scroll;
}
/* Filter/search DX Call field should be smaller on mobile */
input#filter-dx-call { input#filter-dx-call {
max-width: 6em; max-width: 6em;
margin-right: 0; margin-right: 0;

View File

@@ -1,3 +1,31 @@
// Load server options. Once a successful callback is made from this, we can populate the choice boxes in the form and load
// any saved values from local storage.
function loadOptions() {
$.getJSON('/api/v1/options', function(jsonData) {
// Store options
options = jsonData;
// Populate modes drop-down
$.each(options["modes"], function (i, m) {
$('#mode').append($('<option>', {
value: m,
text : m
}));
});
// Populate SIG drop-down
$.each(options["sigs"], function (i, sig) {
$('#sig').append($('<option>', {
value: sig.name,
text : sig.name
}));
});
// Load settings from settings storage now all the controls are available
loadSettings();
});
}
// Method called to add a spot to the server // Method called to add a spot to the server
function addSpot() { function addSpot() {
try { try {
@@ -7,7 +35,9 @@ function addSpot() {
// Unpack the user's entered values // Unpack the user's entered values
var dx = $("#dx-call").val().toUpperCase(); var dx = $("#dx-call").val().toUpperCase();
var freqStr = $("#freq").val(); var freqStr = $("#freq").val();
var mode = $("#mode").val().toUpperCase(); var mode = $("#mode")[0].value;
var sig = $("#sig")[0].value;
var sigRef = $("#sig-ref").val();
var dxGrid = $("#dx-grid").val(); var dxGrid = $("#dx-grid").val();
var comment = $("#comment").val(); var comment = $("#comment").val();
var de = $("#de-call").val().toUpperCase(); var de = $("#de-call").val().toUpperCase();
@@ -28,6 +58,12 @@ function addSpot() {
if (mode != "") { if (mode != "") {
spot["mode"] = mode; spot["mode"] = mode;
} }
if (sig != "") {
spot["sig"] = sig;
}
if (sigRef != "") {
spot["sig_refs"] = [{id: sigRef}];
}
if (dxGrid != "") { if (dxGrid != "") {
spot["dx_grid"] = dxGrid; spot["dx_grid"] = dxGrid;
} }
@@ -48,7 +84,7 @@ function addSpot() {
type : 'POST', type : 'POST',
timeout: 10000, timeout: 10000,
success: async function (result) { success: async function (result) {
$("#result-good").html("<button type='button' class='btn btn-success' style='margin-top: 2em;'><i class='fa-solid fa-check'></i> OK</button>"); $("#result-good").html("<div class='alert alert-success fade show mb-0 mt-4' role='alert'><i class='fa-solid fa-check'></i> Spot submitted. Returning you to the spots list...</div>");
$("#result-bad").html(""); $("#result-bad").html("");
setTimeout(() => { setTimeout(() => {
$("#result-good").hide(); $("#result-good").hide();
@@ -93,8 +129,8 @@ function displayIntroBox() {
// Startup // Startup
$(document).ready(function() { $(document).ready(function() {
// Load settings from settings storage // Load options
loadSettings(); loadOptions();
// Display intro box // Display intro box
displayIntroBox(); displayIntroBox();
}); });

View File

@@ -249,4 +249,6 @@ $(document).ready(function() {
setUpMap(); setUpMap();
// Call loadOptions(), this will then trigger loading spots and setting up timers. // Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions(); loadOptions();
// Prevent scrolling actions in the popup menus being passed through to the map
L.DomEvent.disableScrollPropagation(document.getElementById('maptools'));
}); });