Replace pages' use of the options call with directly building the HTML components using templates, as the syntax is much nicer. Use a common set of data provided to all modules via pagetemplate.py instead of passing things like web_ui_options around. Closes #153

This commit is contained in:
Ian Renton
2026-09-25 21:22:31 +01:00
parent 4df00c321c
commit 8de163469b
27 changed files with 493 additions and 534 deletions
+14 -47
View File
@@ -26,45 +26,8 @@ const PROVIDER_CREDENTIAL_SCHEMAS = {
]
};
// 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/v3/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 activity drop-down
$.each(options["activities"], function (i, activity) {
$('#activity').append($('<option>', {
value: activity.name,
text: activity.name
}));
});
// Load reCAPTCHA if a site key is configured (key is inlined into page by server)
if (window._recaptchaSiteKey) {
loadRecaptcha(window._recaptchaSiteKey);
}
// Load settings from settings storage now all the controls are available
loadSettings();
// Update the upstream area for any pre-selected activity
updateUpstreamArea();
});
}
// Load and inject the reCAPTCHA script
function loadRecaptcha(siteKey) {
window._recaptchaSiteKey = siteKey;
function loadRecaptcha() {
if (!document.getElementById('recaptcha-script')) {
const script = document.createElement('script');
script.id = 'recaptcha-script';
@@ -79,20 +42,19 @@ function loadRecaptcha(siteKey) {
// Called by reCAPTCHA after its script loads
function renderRecaptcha() {
window._recaptchaWidgetId = grecaptcha.render('recaptcha-widget', {
sitekey: window._recaptchaSiteKey,
sitekey: RECAPTCHA_SITE_KEY,
size: 'normal'
});
}
// Update the "Send spot to..." area based on the currently selected activity
function updateUpstreamArea() {
if (!window._allowUpstreamSpotting || !options || !options["spot_submit_providers"]) {
if (!ALLOW_UPSTREAM_SPOTTING) {
$("#upstream-area").hide();
return;
}
const activity = $("#activity").val();
const providers = (activity && options["spot_submit_providers"][activity]) ? options["spot_submit_providers"][activity] : [];
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
if (providers.length === 0) {
$("#upstream-area").hide();
@@ -131,9 +93,7 @@ function updateCredentialsButton() {
// Get the currently selected upstream provider name
function getSelectedUpstreamProvider() {
const providers = (options && options["spot_submit_providers"] && $("#activity").val())
? (options["spot_submit_providers"][$("#activity").val()] || [])
: [];
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
if (providers.length === 0) return null;
if (providers.length === 1) return providers[0];
return $("#upstream-provider-select").val();
@@ -302,6 +262,13 @@ $("#upstream-provider-select").change(function () {
// Startup
$(document).ready(function () {
// Load options
loadOptions();
// Load reCAPTCHA if a site key is configured
if (RECAPTCHA_SITE_KEY) {
loadRecaptcha();
}
loadSettings();
// Update the upstream area for any pre-selected activity
updateUpstreamArea();
});
+9 -29
View File
@@ -328,33 +328,6 @@ function addAlertRowsToTable(tbody, alerts) {
});
}
// Load server options. Once a successful callback is made from this, we then query alerts.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// Populate the filters panel
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#source-options", "source", options["alert_providers"]);
// Alerts can only ever be tagged with activities that have an alert source, so only offer those as filters here
generateActivitiesMultiToggleFilterCard(options["activities"].filter(o => o["alerts_possible"]), false);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load filters from settings storage
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
}
// Method called when any filter is changed to reload the alerts and persist the filter settings.
function filtersUpdated() {
loadAlerts();
@@ -363,8 +336,15 @@ function filtersUpdated() {
// Startup
$(document).ready(function () {
// Call loadOptions(), this will then trigger loading alerts and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
+14 -42
View File
@@ -108,7 +108,7 @@ function updateBands() {
// Convert to a map of band names to the spots on that band. Bands with no
// spots in view will not be present.
const bandToSpots = new Map();
options["bands"].forEach(function (band) {
BANDS.forEach(function (band) {
const matchingSpots = spotList.filter(function (s) {
return s.band === band.name;
});
@@ -127,7 +127,7 @@ function updateBands() {
table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`);
// Get the band data to fetch start and end frequencies
let band = options["bands"].filter(function (b) {
let band = BANDS.filter(function (b) {
return b.name === bandName;
})[0];
@@ -278,44 +278,6 @@ function removeDuplicatesForBandPanel(spotList) {
return spotList.filter(s => !spotsToRemove.includes(s.uid));
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
}
// Method called when any display property is changed to reload the bands display and persist settings.
function displayUpdated() {
updateBands();
@@ -331,6 +293,16 @@ $(document).ready(function () {
}
});
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
addBandToggleColourCSS();
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
+1 -73
View File
@@ -1,5 +1,3 @@
// Storage for the options that the server gives us. This will define our filters.
let options = {};
// Normally load user settings from local storage, unless embedded mode is in use
let useLocalStorage = true;
@@ -124,75 +122,6 @@ function allFilterOptionsSelected(parameter) {
return filter.length === 0;
}
// Generate a filter card with inline checkboxes plus All/None links.
function generateMultiToggleFilterCard(elementID, filterQuery, options) {
const $row = $('<div>');
options.forEach(o => {
$row.append(`<div class="form-check form-check-inline"><input type="checkbox" class="form-check-input filter-button-${filterQuery} storeable-checkbox" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" for="filter-button-${filterQuery}-${o}">${o}</label></div>`);
});
$(elementID).append($row);
$(elementID).append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('${filterQuery}', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('${filterQuery}', false); return false;">None</a></div>`);
}
// Generate a collapsable section for the activities filter card, and append it to the given list. The section is
// expanded by default if isExpanded is true. If hasContent is false (i.e. there are no activities to show in this
// section), the section is omitted entirely rather than showing an empty collapsible heading.
function appendActivityFilterSection($list, sectionId, heading, isExapanded, hasContent, buildBody) {
if (!hasContent) {
return;
}
const collapseId = `activity-options-${sectionId}-collapse`;
const $li = $('<li class="mb-1">');
$li.append(`<button class="btn-toggle${isExapanded ? '' : ' collapsed'}" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="${isExapanded ? 'true' : 'false'}" aria-controls="${collapseId}">${heading}</button>`);
const $body = $(`<div class="collapse filter-section-body${isExapanded ? ' show' : ''}" id="${collapseId}">`);
buildBody($body);
$li.append($body);
$list.append($li);
}
// Build the grid of activity checkboxes for a given (already filtered) list of activities.
function buildActivityFilterGrid(activity_options) {
const $grid = $('<div class="row row-cols-2 g-1 mb-1">');
activity_options.forEach(o => {
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-${domSafeName}" for="filter-button-activity-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
});
return $grid;
}
// Generate activities filter card. This one is also a special case. includeGeneralDX controls whether the "General
// DX" (NO_ACTIVITY) option is offered - this doesn't apply on the alerts page, where every alert has an activity.
function generateActivitiesMultiToggleFilterCard(activity_options, includeGeneralDX = true) {
const $list = $('<ul class="list-unstyled filter-section-list ps-0">');
const traditional = activity_options.filter(o => o["activity_type"] === "TRADITIONAL");
appendActivityFilterSection($list, 'traditional', 'Traditional', true, includeGeneralDX || traditional.length > 0, $body => {
if (includeGeneralDX) {
$body.append(`<div class="w-100 mb-1"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-NO_ACTIVITY" value="NO_ACTIVITY" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-NO_ACTIVITY" for="filter-button-activity-NO_ACTIVITY"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
}
$body.append(buildActivityFilterGrid(traditional));
});
const adventure = activity_options.filter(o => o["activity_type"] === "ADVENTURE");
appendActivityFilterSection($list, 'adventure', 'Adventure', true, adventure.length > 0, $body => {
$body.append(buildActivityFilterGrid(adventure));
});
const regional = activity_options.filter(o => o["activity_type"] === "REGIONAL");
appendActivityFilterSection($list, 'regional', 'Regional', false, regional.length > 0, $body => {
$body.append(buildActivityFilterGrid(regional));
});
const event = activity_options.filter(o => o["activity_type"] === "EVENT");
appendActivityFilterSection($list, 'event', 'Event', false, event.length > 0, $body => {
$body.append(buildActivityFilterGrid(event));
});
$("#activity-options").append($list);
$("#activity-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('activity', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('activity', false); return false;">None</a></div>`);
}
// Method called when "All" or "None" is clicked
function toggleFilterButtons(filterQuery, state) {
$(".filter-button-" + filterQuery).each(function () {
@@ -238,8 +167,7 @@ function setColorScheme(mode) {
// Startup function to determine whether to use light or dark mode, or leave as auto
function usePreferredTheme() {
// Work out if we have ever explicitly saved the value of our select box. If so, we set our colour scheme now based
// on that. If not, we let the select stay with nothing selected, so that the server sets it to whatever the
// server's default is when the options call is retrieved.
// on that. If not, we leave the colour scheme at the server's default, which is set in the page template.
let val = localStorage.getItem("#color_scheme:value");
if (val != null) {
setColorScheme(JSON.parse(val));
+30 -57
View File
@@ -322,61 +322,6 @@ function getTooltipText(s) {
return ttt;
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
loadMapURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
if (localStorage.getItem("#basemap:value") === null) {
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
}
}
// Apply basemap and overlay settings now that controls have their saved values
setBasemap($("#basemap").val());
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
enableTerminator($("#showTerminator")[0].checked);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
}
// Method called when any display property is changed to reload the map and persist the display settings.
function displayUpdated() {
updateMap();
@@ -639,8 +584,36 @@ $(document).ready(function () {
$(".hideonmap").hide();
// Set up map
setUpMap();
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadMapURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
addBandToggleColourCSS();
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
if (localStorage.getItem("#basemap:value") === null) {
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
}
}
// Apply basemap and overlay settings now that controls have their saved values
setBasemap($("#basemap").val());
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
enableTerminator($("#showTerminator")[0].checked);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
// Display intro box
displayIntroBox();
// Prevent mouse scroll and touch actions in the popup menus being passed through to the map
+21 -48
View File
@@ -461,52 +461,6 @@ function createNewTableRowsForSpot(s, highlightNew) {
return [$tr, $tr2];
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// Extra setting - the toggle for the "bearing" & "distance" columns are disabled if the user has not entered a
// valid grid, and normally this logic is handled on user input to the grid field, but we might have just loaded
// a value direct into the field, so apply the same logic here.
$("#tableShowBearing").prop('disabled', !isUserGridValid());
$("#tableShowDistance").prop('disabled', !isUserGridValid());
if (!isUserGridValid()) {
$("#tableShowBearing").prop('checked', false);
$("#tableShowDistance").prop('checked', false);
}
// Load spots (this will also set up the SSE connection to update them too)
loadSpots();
});
}
// Work out if the user's entered grid is a valid Maidenhead grid
function isUserGridValid() {
const userGrid = $("#userGrid").val().toUpperCase();
@@ -572,8 +526,27 @@ function clearWorked() {
// Startup
$(document).ready(function () {
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
addBandToggleColourCSS();
// Extra setting - the toggle for the "bearing" & "distance" columns are disabled if the user has not entered a
// valid grid, and normally this logic is handled on user input to the grid field, but we might have just loaded
// a value direct into the field, so apply the same logic here.
$("#tableShowBearing").prop('disabled', !isUserGridValid());
$("#tableShowDistance").prop('disabled', !isUserGridValid());
if (!isUserGridValid()) {
$("#tableShowBearing").prop('checked', false);
$("#tableShowDistance").prop('checked', false);
}
// Load spots (this will also set up the SSE connection to update them too)
loadSpots();
// Display intro box
displayIntroBox();
+7 -49
View File
@@ -7,80 +7,38 @@ let spots = [];
let worked = []
// Dynamically add CSS code for the band checkboxes to show in the appropriate colour.
// Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p".
function addBandToggleColourCSS(band_options) {
function addBandToggleColourCSS() {
const $style = $('<style>');
band_options.forEach(o => {
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$style.append(`#filter-button-label-band-${domSafeName} { padding-left: 0.3em; border-left: 5px solid ${bandToColor(o['name'])};}`);
$(".filter-button-band").each(function () {
$style.append(`label[for="${this.id}"] { padding-left: 0.3em; border-left: 5px solid ${bandToColor(this.value)};}`);
});
$('html > head').append($style);
}
// Generate bands filter card. This one is a special case.
function generateBandsMultiToggleFilterCard(band_options) {
const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">');
band_options.forEach(o => {
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-band storeable-checkbox" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked> <label class="form-check-label" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label></div></div>`);
});
$("#band-options").append($grid);
$("#band-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('band', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('band', false); return false;">None</a> &nbsp; <a href="#" onclick="setHamHFBandToggles(); return false;">Ham HF only</a></div>`);
}
// Set the band toggles so that only the amateur radio HF bands are selected. This includes 160m and 6m because that's
// widely expected by hams to be included. Special case of toggleFilterButtons().
// Set the band toggles so that only the amateur radio HF bands are selected. Special case of toggleFilterButtons().
function setHamHFBandToggles() {
const hamHFBands = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"];
$(".filter-button-band").each(function () {
$(this).prop('checked', hamHFBands.includes($(this).val().replace("filter-button-band-", "")));
$(this).prop('checked', $(this).data("ham-hf"));
});
filtersUpdated();
}
// Generate modes filter card. This one is also a special case.
function generateModesMultiToggleFilterCard(mode_options) {
const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">');
mode_options.forEach(o => {
const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label></div></div>`);
});
$("#mode-options").append($grid);
$("#mode-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('mode', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('mode', false); return false;">None</a> &nbsp; <a href="#" onclick="setVoiceModeToggles(); return false;">Voice only</a> &nbsp; <a href="#" onclick="setDigiModeToggles(); return false;">Digimodes only</a></div>`);
}
// Set the mode toggles that relate to Analog Voice.
function setVoiceModeToggles() {
const modes = ["PHONE", "SSB", "AM", "FM", "DV"];
$(".filter-button-mode").each(function () {
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
$(this).prop('checked', $(this).data("mode-type") === "PHONE");
});
filtersUpdated();
}
// Set the mode toggles that relate to Digimodes.
function setDigiModeToggles() {
const modes = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "PSK", "OLIVIA", "PKT", "MSK144"];
$(".filter-button-mode").each(function () {
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
$(this).prop('checked', $(this).data("mode-type") === "DATA");
});
filtersUpdated();
}
// Generate Sources filter card. This one is a minor special case as we create the checkboxes in the normal way, but
// set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise
// names here for HTML elements.
function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) {
const $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
source_options.forEach(o => {
const enable = sources_enabled_by_default.includes(o);
const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-source storeable-checkbox" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="form-check-label" for="filter-button-source-${domSafeName}">${o}</label></div></div>`);
});
$("#source-options").append($grid);
$("#source-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('source', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('source', false); return false;">None</a></div>`);
}
// Method called when any filter is changed to reload the spots and persist the filter settings.
function filtersUpdated() {
loadSpots();
+60 -1
View File
@@ -38,12 +38,18 @@
<label for="mode" class="form-label">Mode</label>
<select id="mode" class="form-select">
<option value="" selected></option>
{% for m in options["modes"] %}
<option value="{{ m }}">{{ m }}</option>
{% end %}
</select>
</div>
<div class="col-auto">
<label for="activity" class="form-label">Activity</label>
<select id="activity" class="form-select">
<option value="" selected></option>
{% for a in options["activities"] %}
<option value="{{ a.name }}">{{ a.name }}</option>
{% end %}
</select>
</div>
<div class="col-auto">
@@ -68,6 +74,32 @@
</div>
</form>
<div id="upstream-area" class="mt-3" style="display:none;">
<div class="row g-2 align-items-center">
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="submit-upstream">
<label class="form-check-label" for="submit-upstream">
Send spot to <span id="upstream-provider-label"></span>
</label>
</div>
</div>
<div class="col-auto" id="upstream-provider-select-col" style="display:none;">
<select id="upstream-provider-select" class="form-select form-select-sm"></select>
</div>
<div class="col-auto">
<button type="button" class="btn btn-sm btn-outline-secondary" id="upstream-credentials-btn"
onclick="showCredentialsModal();" style="display:none;">
<i class="fa-solid fa-key"></i> Credentials
</button>
</div>
</div>
</div>
<div id="recaptcha-area" class="mt-3" style="display:none;">
<div id="recaptcha-widget"></div>
</div>
<div id="result-good"></div>
<div id="result-bad"></div>
@@ -77,7 +109,34 @@
</div>
<script src="/static/js/add-spot.js?v=1790344336"></script>
<div class="modal fade" id="credentials-modal" tabindex="-1" aria-labelledby="credentials-modal-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="credentials-modal-label">Credentials for
<span id="credentials-provider-name"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="small text-muted">Credentials are stored only in your browser and are never saved on the
server.</p>
<div id="credentials-fields"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveCredentials();">Save</button>
</div>
</div>
</div>
</div>
<script>
const SPOT_SUBMIT_PROVIDERS = {% raw safe_json_dumps(options["spot_submit_providers"]) %};
const RECAPTCHA_SITE_KEY = {% raw safe_json_dumps(web_ui_options["recaptcha_site_key"]) %};
const ALLOW_UPSTREAM_SPOTTING = {% raw safe_json_dumps(web_ui_options["allow_upstream_spotting"]) %};
</script>
<script src="/static/js/add-spot.js?v=1790367752"></script>
<script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script>
+18 -16
View File
@@ -7,45 +7,47 @@
</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) %}
{% module Template("widgets/filters_display_data_buttons.html") %}
</div>
</div>
</div>
<div id="filters-area" class="appearing-panel card mb-3">
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/filters_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/dx_continent.html") %}
<div class="mt-4"></div>
{% module Template("cards/duration_limit_alerts.html", web_ui_options=web_ui_options) %}
{% module Template("cards/duration_limit_alerts.html") %}
</div>
<div class="col">
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
{% module Template("cards/activities.html",
activities=[a for a in options["activities"] if a.alerts_possible], include_general_dx=False) %}
</div>
<div class="col">
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
{% module Template("cards/sources.html",
sources=options["alert_providers"], sources_enabled_by_default=options["alert_providers"]) %}
</div>
</div>
</div>
</div>
<div id="display-area" class="appearing-panel card mb-3">
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/display_area_header.html") %}
<div class="card-body">
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
<div class="col">
{% module Template("cards/time_zone.html", web_ui_options=web_ui_options) %}
{% module Template("cards/time_zone.html") %}
</div>
<div class="col">
{% module Template("cards/number_of_alerts.html", web_ui_options=web_ui_options) %}
{% module Template("cards/number_of_alerts.html") %}
</div>
<div class="col">
{% module Template("cards/color_scheme.html", web_ui_options=web_ui_options) %}
{% module Template("cards/color_scheme.html") %}
</div>
<div class="col">
{% module Template("cards/table_columns_alerts.html", web_ui_options=web_ui_options) %}
{% module Template("cards/table_columns_alerts.html") %}
</div>
</div>
</div>
@@ -53,21 +55,21 @@
{% if web_ui_options["qrz_enabled"] or web_ui_options["hamqth_enabled"] %}
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/your_data_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz_enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
{% module Template("cards/qrz.html") %}
</div>
{% end %}
{% if web_ui_options["hamqth_enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
{% module Template("cards/hamqth.html") %}
</div>
{% end %}
<div class="col">
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
{% module Template("cards/wanted_calls.html") %}
</div>
</div>
</div>
@@ -85,7 +87,7 @@
</div>
<script src="/static/js/alerts.js?v=1790344337"></script>
<script src="/static/js/alerts.js?v=1790367751"></script>
<script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script>
+24 -18
View File
@@ -6,44 +6,46 @@
<div class="col-auto me-auto pt-3"></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) %}
{% module Template("widgets/filters_display_data_buttons.html") %}
</div>
</div>
</div>
<div id="filters-area" class="appearing-panel card mb-3">
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/filters_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
<div class="col">
{% module Template("cards/bands.html", web_ui_options=web_ui_options) %}
{% module Template("cards/bands.html") %}
<div class="mt-4"></div>
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/dx_continent.html") %}
<div class="mt-4"></div>
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/de_continent.html") %}
</div>
<div class="col">
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
{% module Template("cards/activities.html",
activities=options["activities"], include_general_dx=True) %}
</div>
<div class="col">
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
{% module Template("cards/sources.html",
sources=options["spot_providers"],
sources_enabled_by_default=options["spot_providers_enabled_by_default"]) %}
<div class="mt-4"></div>
{% module Template("cards/modes.html", web_ui_options=web_ui_options) %}
{% module Template("cards/modes.html") %}
</div>
</div>
</div>
</div>
<div id="display-area" class="appearing-panel card mb-3">
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/display_area_header.html") %}
<div class="card-body">
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
<div class="col">
{% module Template("cards/spot_age.html", web_ui_options=web_ui_options) %}
{% module Template("cards/spot_age.html") %}
</div>
<div class="col">
{% module Template("cards/color_scheme_and_band_color_scheme.html", web_ui_options=web_ui_options)
%}
{% module Template("cards/color_scheme_and_band_color_scheme.html") %}
</div>
</div>
</div>
@@ -51,21 +53,21 @@
{% if web_ui_options["qrz_enabled"] or web_ui_options["hamqth_enabled"] %}
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/your_data_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz_enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
{% module Template("cards/qrz.html") %}
</div>
{% end %}
{% if web_ui_options["hamqth_enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
{% module Template("cards/hamqth.html") %}
</div>
{% end %}
<div class="col">
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
{% module Template("cards/wanted_calls.html") %}
</div>
</div>
</div>
@@ -76,8 +78,12 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1790344336"></script>
<script src="/static/js/bands.js?v=1790344336"></script>
<script>
const BANDS = {% raw safe_json_dumps(options["bands"]) %};
</script>
<script src="/static/js/spotsbandsandmap.js?v=1790367752"></script>
<script src="/static/js/bands.js?v=1790367752"></script>
<script>$(document).ready(function () {
$("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script>
+6 -6
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1790344336" type="text/css">
<link rel="stylesheet" href="/static/css/style.css?v=1790367751" 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=1790344336"></script>
<script src="/static/js/ui-ham.js?v=1790344336"></script>
<script src="/static/js/geo.js?v=1790344336"></script>
<script src="/static/js/common.js?v=1790344336"></script>
<script src="/static/js/utils.js?v=1790367751"></script>
<script src="/static/js/ui-ham.js?v=1790367751"></script>
<script src="/static/js/geo.js?v=1790367751"></script>
<script src="/static/js/common.js?v=1790367751"></script>
{% end %}
{% block body %}
<div class="container">
@@ -43,7 +43,7 @@
class="fa-solid fa-ruler-vertical"></i> Bands</a></li>
<li class="nav-item ms-4"><a href="/alerts" class="nav-link" id="nav-link-alerts"><i
class="fa-solid fa-clock"></i> Upcoming</a></li>
{% if allow_spotting %}
{% if options["spot_allowed"] %}
<li class="nav-item ms-4"><a href="/add-spot" class="nav-link" id="nav-link-add-spot"><i
class="fa-solid fa-comment"></i> Add&nbsp;Spot</a></li>
{% end %}
+54 -2
View File
@@ -1,6 +1,58 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">Activities</h5>
<div id="activity-options" class="card-text spothole-card-text"></div>
<div class="card-text spothole-card-text">
<ul class="list-unstyled filter-section-list ps-0">
{% for section_type, heading, expanded in [("TRADITIONAL", "Traditional", True), ("ADVENTURE",
"Adventure", True), ("REGIONAL", "Regional", False), ("EVENT", "Event", False)] %}
{% set show_general_dx = include_general_dx and section_type == "TRADITIONAL" %}
{% set section_activities = [a for a in activities if a.activity_type == section_type] %}
{% if section_activities or show_general_dx %}
{% set collapse_id = "activity-options-" + section_type.lower() + "-collapse" %}
<li class="mb-1">
<button class="btn-toggle{% if not expanded %} collapsed{% end %}" data-bs-toggle="collapse"
data-bs-target="#{{ collapse_id }}" aria-expanded="{{ 'true' if expanded else 'false' }}"
aria-controls="{{ collapse_id }}">{{ heading }}
</button>
<div class="collapse filter-section-body{% if expanded %} show{% end %}" id="{{ collapse_id }}">
{% if show_general_dx %}
<div class="w-100 mb-1">
<div class="form-check">
<input type="checkbox"
class="form-check-input filter-button-activity storeable-checkbox"
id="filter-button-activity-NO_ACTIVITY" value="NO_ACTIVITY"
autocomplete="off" onClick="filtersUpdated()" checked>
<label class="form-check-label" for="filter-button-activity-NO_ACTIVITY"><i
class="fa-solid fa-tower-cell"></i> General DX</label>
</div>
</div>
{% end %}
<div class="row row-cols-2 g-1 mb-1">
{% for a in section_activities %}
<div class="col">
<div class="form-check">
<input type="checkbox"
class="form-check-input filter-button-activity storeable-checkbox"
id="filter-button-activity-{{ dom_safe_name(a.name) }}" value="{{ a.name }}"
autocomplete="off" onClick="filtersUpdated()" checked>
<label class="form-check-label"
for="filter-button-activity-{{ dom_safe_name(a.name) }}"
title="{{ a.description }}"><i class="fa-solid {{ a.icon }}"></i> {{ a.name
}}
{{ a.region_flag or "" }}</label>
</div>
</div>
{% end %}
</div>
</div>
</li>
{% end %}
{% end %}
</ul>
<div class="mt-1">
<a href="#" onclick="toggleFilterButtons('activity', true); return false;">All</a> &nbsp;
<a href="#" onclick="toggleFilterButtons('activity', false); return false;">None</a>
</div>
</div>
</div>
</div>
</div>
+22 -2
View File
@@ -1,6 +1,26 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">Bands</h5>
<div id="band-options" class="card-text spothole-card-text"></div>
<div class="card-text spothole-card-text">
<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">
{% for b in options["bands"] %}
<div class="col">
<div class="form-check">
<input type="checkbox" class="form-check-input filter-button-band storeable-checkbox"
id="filter-button-band-{{ dom_safe_name(b.name) }}" value="{{ b.name }}"
data-ham-hf="{{ 'true' if b.is_ham_hf else 'false' }}"
autocomplete="off" onClick="filtersUpdated()" checked>
<label class="form-check-label" for="filter-button-band-{{ dom_safe_name(b.name) }}">{{ b.name
}}</label>
</div>
</div>
{% end %}
</div>
<div class="mt-1">
<a href="#" onclick="toggleFilterButtons('band', true); return false;">All</a> &nbsp;
<a href="#" onclick="toggleFilterButtons('band', false); return false;">None</a> &nbsp;
<a href="#" onclick="setHamHFBandToggles(); return false;">Ham HF only</a>
</div>
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="card-body">
<h5 class="card-title">Theme</h5>
<p class="card-text spothole-card-text">
{% module Template("widgets/color_scheme.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/color_scheme.html") %}
</p>
</div>
</div>
@@ -2,10 +2,10 @@
<div class="card-body">
<h5 class="card-title">Theme</h5>
<p class="card-text spothole-card-text">
{% module Template("widgets/color_scheme.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/color_scheme.html") %}
</p>
<p class="card-text spothole-card-text">
{% module Template("widgets/band_color_scheme.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/band_color_scheme.html") %}
</p>
</div>
</div>
+17 -2
View File
@@ -1,6 +1,21 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">DE Continent</h5>
<div id="de_continent_options" class="card-text spothole-card-text"></div>
<div class="card-text spothole-card-text">
<div>
{% for c in options["continents"] %}
<div class="form-check form-check-inline">
<input type="checkbox" class="form-check-input filter-button-de_continent storeable-checkbox"
id="filter-button-de_continent-{{ c }}" value="{{ c }}"
autocomplete="off" onClick="filtersUpdated()" checked>
<label class="form-check-label" for="filter-button-de_continent-{{ c }}">{{ c }}</label>
</div>
{% end %}
</div>
<div class="mt-1">
<a href="#" onclick="toggleFilterButtons('de_continent', true); return false;">All</a> &nbsp;
<a href="#" onclick="toggleFilterButtons('de_continent', false); return false;">None</a>
</div>
</div>
</div>
</div>
</div>
+17 -2
View File
@@ -1,6 +1,21 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">DX Continent</h5>
<div id="dx_continent_options" class="card-text spothole-card-text"></div>
<div class="card-text spothole-card-text">
<div>
{% for c in options["continents"] %}
<div class="form-check form-check-inline">
<input type="checkbox" class="form-check-input filter-button-dx_continent storeable-checkbox"
id="filter-button-dx_continent-{{ c }}" value="{{ c }}"
autocomplete="off" onClick="filtersUpdated()" checked>
<label class="form-check-label" for="filter-button-dx_continent-{{ c }}">{{ c }}</label>
</div>
{% end %}
</div>
<div class="mt-1">
<a href="#" onclick="toggleFilterButtons('dx_continent', true); return false;">All</a> &nbsp;
<a href="#" onclick="toggleFilterButtons('dx_continent', false); return false;">None</a>
</div>
</div>
</div>
</div>
</div>
+22 -2
View File
@@ -1,6 +1,26 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">Modes</h5>
<div id="mode-options" class="card-text spothole-card-text"></div>
<div class="card-text spothole-card-text">
<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">
{% for m in options["modes"] %}
<div class="col">
<div class="form-check">
<input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox"
id="filter-button-mode-{{ dom_safe_name(m) }}" value="{{ m }}"
data-mode-type="{{ infer_mode_type_from_mode(m).value }}"
autocomplete="off" onClick="filtersUpdated()" checked>
<label class="form-check-label" for="filter-button-mode-{{ dom_safe_name(m) }}">{{ m }}</label>
</div>
</div>
{% end %}
</div>
<div class="mt-1">
<a href="#" onclick="toggleFilterButtons('mode', true); return false;">All</a> &nbsp;
<a href="#" onclick="toggleFilterButtons('mode', false); return false;">None</a> &nbsp;
<a href="#" onclick="setVoiceModeToggles(); return false;">Voice only</a> &nbsp;
<a href="#" onclick="setDigiModeToggles(); return false;">Digimodes only</a>
</div>
</div>
</div>
</div>
</div>
+21 -2
View File
@@ -1,6 +1,25 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">Sources</h5>
<div id="source-options" class="card-text spothole-card-text"></div>
<div class="card-text spothole-card-text">
<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">
{% for s in sources %}
<div class="col">
<div class="form-check">
<input type="checkbox" class="form-check-input filter-button-source storeable-checkbox"
id="filter-button-source-{{ dom_safe_name(s) }}" value="{{ s }}"
autocomplete="off" onClick="filtersUpdated()" {% if s in sources_enabled_by_default
%}checked{% end %}>
<label class="form-check-label" for="filter-button-source-{{ dom_safe_name(s) }}">{{ s
}}</label>
</div>
</div>
{% end %}
</div>
<div class="mt-1">
<a href="#" onclick="toggleFilterButtons('source', true); return false;">All</a> &nbsp;
<a href="#" onclick="toggleFilterButtons('source', false); return false;">None</a>
</div>
</div>
</div>
</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=1790344336"></script>
<script src="/static/js/conditions.js?v=1790367751"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -20,7 +20,7 @@
<li><strong><i class="fa-solid fa-clock"></i> Upcoming</strong>, the list of upcoming activations, DXpeditions and
contests
</li>
{% if allow_spotting %}
{% if options["spot_allowed"] %}
<li><strong><i class="fa-solid fa-comment"></i> Add Spot</strong>, which allows users to add new spots to the system
</li>
{% end %}
+22 -20
View File
@@ -20,50 +20,52 @@
<div class="col-auto me-auto pt-3"></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) %}
{% module Template("widgets/filters_display_data_buttons.html") %}
</div>
</div>
</div>
<div id="filters-area" class="appearing-panel card mb-3">
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/filters_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
<div class="col">
{% module Template("cards/bands.html", web_ui_options=web_ui_options) %}
{% module Template("cards/bands.html") %}
<div class="mt-4"></div>
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/dx_continent.html") %}
<div class="mt-4"></div>
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/de_continent.html") %}
</div>
<div class="col">
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
{% module Template("cards/activities.html",
activities=options["activities"], include_general_dx=True) %}
</div>
<div class="col">
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
{% module Template("cards/sources.html",
sources=options["spot_providers"],
sources_enabled_by_default=options["spot_providers_enabled_by_default"]) %}
<div class="mt-4"></div>
{% module Template("cards/modes.html", web_ui_options=web_ui_options) %}
{% module Template("cards/modes.html") %}
</div>
</div>
</div>
</div>
<div id="display-area" class="appearing-panel card mb-3">
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/display_area_header.html") %}
<div class="card-body">
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
<div class="col">
{% module Template("cards/spot_age.html", web_ui_options=web_ui_options) %}
{% module Template("cards/spot_age.html") %}
</div>
<div class="col">
{% module Template("cards/basemap.html", web_ui_options=web_ui_options) %}
{% module Template("cards/basemap.html") %}
</div>
<div class="col">
{% module Template("cards/map_features.html", web_ui_options=web_ui_options) %}
{% module Template("cards/map_features.html") %}
</div>
<div class="col">
{% module Template("cards/color_scheme_and_band_color_scheme.html",
web_ui_options=web_ui_options) %}
{% module Template("cards/color_scheme_and_band_color_scheme.html") %}
</div>
</div>
</div>
@@ -71,21 +73,21 @@
{% if web_ui_options["qrz_enabled"] or web_ui_options["hamqth_enabled"] %}
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/your_data_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz_enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
{% module Template("cards/qrz.html") %}
</div>
{% end %}
{% if web_ui_options["hamqth_enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
{% module Template("cards/hamqth.html") %}
</div>
{% end %}
<div class="col">
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
{% module Template("cards/wanted_calls.html") %}
</div>
</div>
</div>
@@ -113,8 +115,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script>
<script src="/static/js/spotsbandsandmap.js?v=1790344337"></script>
<script src="/static/js/map.js?v=1790344337"></script>
<script src="/static/js/spotsbandsandmap.js?v=1790367751"></script>
<script src="/static/js/map.js?v=1790367751"></script>
<script>$(document).ready(function () {
$("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script>
+27 -25
View File
@@ -30,85 +30,87 @@
<div id="settingsButtonRow" class="row mb-3">
<div class="col-md-4 mb-3 mb-md-0">
<div class="d-inline-flex gap-3">
{% module Template("widgets/run_pause.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/run_pause.html") %}
<div class="d-inline-flex">{% raw web_ui_options["support_button_html"] %}</div>
</div>
</div>
<div class="col-md-8 text-end">
<div class="d-inline-flex gap-3">
{% module Template("widgets/search.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/filters_display_data_buttons.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/search.html") %}
{% module Template("widgets/filters_display_data_buttons.html") %}
</div>
</div>
</div>
<div id="filters-area" class="appearing-panel card mb-3">
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/filters_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
<div class="col">
{% module Template("cards/bands.html", web_ui_options=web_ui_options) %}
{% module Template("cards/bands.html") %}
<div class="mt-4"></div>
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/dx_continent.html") %}
<div class="mt-4"></div>
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
{% module Template("cards/de_continent.html") %}
</div>
<div class="col">
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
{% module Template("cards/activities.html",
activities=options["activities"], include_general_dx=True) %}
</div>
<div class="col">
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
{% module Template("cards/sources.html",
sources=options["spot_providers"],
sources_enabled_by_default=options["spot_providers_enabled_by_default"]) %}
<div class="mt-4"></div>
{% module Template("cards/modes.html", web_ui_options=web_ui_options) %}
{% module Template("cards/modes.html") %}
</div>
</div>
</div>
</div>
<div id="display-area" class="appearing-panel card mb-3">
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/display_area_header.html") %}
<div class="card-body">
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
<div class="col">
{% module Template("cards/time_zone.html", web_ui_options=web_ui_options) %}
{% module Template("cards/time_zone.html") %}
<div class="mt-4"></div>
{% module Template("cards/audio.html", web_ui_options=web_ui_options) %}
{% module Template("cards/audio.html") %}
</div>
<div class="col">
{% module Template("cards/number_of_spots.html", web_ui_options=web_ui_options) %}
{% module Template("cards/number_of_spots.html") %}
</div>
<div class="col">
{% module Template("cards/color_scheme_and_band_color_scheme.html", web_ui_options=web_ui_options)
%}
{% module Template("cards/color_scheme_and_band_color_scheme.html") %}
</div>
<div class="col">
{% module Template("cards/table_columns_spots.html", web_ui_options=web_ui_options) %}
{% module Template("cards/table_columns_spots.html") %}
</div>
</div>
</div>
</div>
<div id="data-area" class="appearing-panel card mb-3">
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
{% module Template("widgets/your_data_area_header.html") %}
<div class="card-body">
<div class="row row-cols-1 row-cols-md-4 g-4">
{% if web_ui_options["qrz_enabled"] %}
<div class="col">
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
{% module Template("cards/qrz.html") %}
</div>
{% end %}
{% if web_ui_options["hamqth_enabled"] %}
<div class="col">
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
{% module Template("cards/hamqth.html") %}
</div>
{% end %}
<div class="col">
{% module Template("cards/location.html", web_ui_options=web_ui_options) %}
{% module Template("cards/location.html") %}
<div class="mt-4"></div>
{% module Template("cards/worked_calls.html", web_ui_options=web_ui_options) %}
{% module Template("cards/worked_calls.html") %}
</div>
<div class="col">
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
{% module Template("cards/wanted_calls.html") %}
</div>
</div>
</div>
@@ -125,8 +127,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1790344336"></script>
<script src="/static/js/spots.js?v=1790344336"></script>
<script src="/static/js/spotsbandsandmap.js?v=1790367751"></script>
<script src="/static/js/spots.js?v=1790367751"></script>
<script>$(document).ready(function () {
$("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -96,7 +96,7 @@
</div>
</div>
<script src="/static/js/status.js?v=1790344336"></script>
<script src="/static/js/status.js?v=1790367751"></script>
<script>
$(document).ready(function () {
$("#nav-link-status").addClass("active");
+52 -62
View File
@@ -1,3 +1,4 @@
import functools
import logging
from typing import Any
@@ -7,6 +8,7 @@ from tornado.web import Application
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
from core.constants import BANDS, PROPAGATION_MODES
from core.data_providers import DATA_PROVIDERS
from core.enums import Continent, Mode, ModeType
from core.utils import safe_json_dumps
from data.activities import ACTIVITIES
@@ -14,6 +16,54 @@ from data.activities import ACTIVITIES
logger = logging.getLogger(__name__)
@functools.cache
def build_options():
"""Build the options data that is returned by the /options API and used to populate the web UI templates. This
doesn't change at runtime, so it is built once on first use and then cached."""
# Build a map of activity name -> list of provider names that can submit spots for that activity
spot_submit_providers = {}
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
# for provider in DATA_PROVIDERS.spot_providers:
# if not provider.enabled:
# continue
# for activity in ACTIVITIES.values():
# if provider.can_submit_spot(activity.name):
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
# things that aren't even available.
spot_providers = [p.name for p in DATA_PROVIDERS.spot_providers if p.enabled]
alert_providers = [p.name for p in DATA_PROVIDERS.alert_providers if p.enabled]
callsign_data_providers = [p.name for p in DATA_PROVIDERS.callsign_data_providers if p.enabled]
spot_providers_enabled_by_default = [
p.name for p in DATA_PROVIDERS.spot_providers if p.enabled and p.enabled_by_default_in_web_ui
]
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our providers.
if ALLOW_SPOTTING:
spot_providers.append("API")
spot_providers_enabled_by_default.append("API")
return {
"bands": BANDS,
"modes": [m.value for m in Mode],
"mode_types": [t.value for t in ModeType],
"activities": list(ACTIVITIES.values()),
"spot_providers": spot_providers,
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
"alert_providers": alert_providers,
"callsign_data_providers": callsign_data_providers,
"continents": [c.value for c in Continent],
"propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING,
"spot_submit_providers": spot_submit_providers,
}
class APIOptionsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v3/options"""
@@ -23,73 +73,13 @@ class APIOptionsHandler(tornado.web.RequestHandler):
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._status_data = None
self._spot_providers = None
super().__init__(application, request, **kwargs)
def initialize(self, status_data, spot_providers=None):
self._status_data = status_data
self._spot_providers = spot_providers or []
def get(self):
try:
# Build a map of activity name -> list of provider names that can submit spots for that activity
spot_submit_providers = {}
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
# for provider in self._spot_providers:
# if not provider.enabled:
# continue
# for activity in ACTIVITIES.values():
# if provider.can_submit_spot(activity.name):
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
# things that aren't even available.
spot_providers: list = [
p["name"] for p in filter(lambda p: p["enabled"], self._status_data["spot_providers"])
]
alert_providers = [p["name"] for p in filter(lambda p: p["enabled"], self._status_data["alert_providers"])]
callsign_data_providers = [
p["name"]
for p in filter(
lambda p: p["enabled"],
self._status_data["callsign_data_providers"],
)
]
spot_providers_enabled_by_default = [
p["name"]
for p in filter(
lambda p: p["enabled"] and p["enabled_by_default_in_web_ui"],
self._status_data["spot_providers"],
)
]
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our providers.
if ALLOW_SPOTTING:
spot_providers.append("API")
spot_providers_enabled_by_default.append("API")
options = {
"bands": BANDS,
"modes": [m.value for m in Mode],
"mode_types": [t.value for t in ModeType],
"activities": list(ACTIVITIES.values()),
"spot_providers": spot_providers,
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
"alert_providers": alert_providers,
"callsign_data_providers": callsign_data_providers,
"continents": [c.value for c in Continent],
"propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING,
"spot_submit_providers": spot_submit_providers,
}
self.write(safe_json_dumps(options))
self.write(safe_json_dumps(build_options()))
self.set_status(200)
self.set_header("Cache-Control", "no-store")
self.set_header("Cache-Control", "max-age=3600")
self.set_header("Content-Type", "application/json")
except Exception:
+26 -8
View File
@@ -1,3 +1,4 @@
import re
from typing import Any
import tornado
@@ -5,7 +6,6 @@ from tornado import httputil
from tornado.web import Application
from core.config import (
ALLOW_SPOTTING,
BASE_URL,
SERVER_OWNER_CALLSIGN,
TELNET_SERVER_ADDRESS,
@@ -14,6 +14,14 @@ from core.config import (
WEB_UI_OPTIONS,
)
from core.constants import SOFTWARE_VERSION
from core.utils import infer_mode_type_from_mode, safe_json_dumps
from webserver.handlers.api.options import build_options
def dom_safe_name(name):
"""Strip characters from a name that are not allowed in HTML element IDs"""
return re.sub(r"^[^A-Za-z0-9]+|\W+", "", name, flags=re.ASCII)
class PageTemplateHandler(tornado.web.RequestHandler):
@@ -31,17 +39,27 @@ class PageTemplateHandler(tornado.web.RequestHandler):
def initialize(self, template_name):
self._template_name = template_name
def get(self):
# Load named template, and provide variables used in templates
self.render(
f"{self._template_name}.html",
def get_template_namespace(self):
"""Provide variables and helper functions to all page templates"""
namespace = super().get_template_namespace()
namespace.update(
# Various options
options=build_options(),
web_ui_options=WEB_UI_OPTIONS,
# Other single static parameters
software_version=SOFTWARE_VERSION,
server_owner_callsign=SERVER_OWNER_CALLSIGN,
allow_spotting=ALLOW_SPOTTING,
web_ui_options=WEB_UI_OPTIONS,
baseurl=BASE_URL,
telnet_server_enabled=TELNET_SERVER_ENABLED,
telnet_server_address=TELNET_SERVER_ADDRESS,
telnet_server_port=TELNET_SERVER_PORT,
current_path=self.request.path,
# Function references that templates need
dom_safe_name=dom_safe_name,
safe_json_dumps=safe_json_dumps,
infer_mode_type_from_mode=infer_mode_type_from_mode,
)
return namespace
def get(self):
self.render(f"{self._template_name}.html")
+3 -15
View File
@@ -161,11 +161,7 @@ class WebServer:
APIDxStatsHandler,
{"spots": self._data_store.spots},
),
(
r"/api/v3/options",
APIOptionsHandler,
{"status_data": self._data_store.status.get()},
),
(r"/api/v3/options", APIOptionsHandler),
(
r"/api/v3/status",
APIStatusHandler,
@@ -216,11 +212,7 @@ class WebServer:
V2APIDxStatsHandler,
{"spots": self._data_store.spots},
),
(
r"/api/v2/options",
V2APIOptionsHandler,
{"status_data": self._data_store.status.get()},
),
(r"/api/v2/options", V2APIOptionsHandler),
(
r"/api/v2/status",
V2APIStatusHandler,
@@ -271,11 +263,7 @@ class WebServer:
V1APIDxStatsHandler,
{"spots": self._data_store.spots},
),
(
r"/api/v1/options",
V1APIOptionsHandler,
{"status_data": self._data_store.status.get()},
),
(r"/api/v1/options", V1APIOptionsHandler),
(
r"/api/v1/status",
V1APIStatusHandler,