Files
spothole/webassets/js/alerts.js
2025-10-08 16:41:02 +01:00

233 lines
10 KiB
JavaScript

// How often to query the server?
const REFRESH_INTERVAL_SEC = 60 * 30;
// Storage for the alert data that the server gives us.
var alerts = []
// Load alerts and populate the table.
function loadAlerts() {
$.getJSON('/api/alerts' + buildQueryString(), function(jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
// Store data
alerts = jsonData;
// Update table
updateTable();
});
}
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString() {
var str = "?";
["dx_continent", "source"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
});
str = str + "limit=" + $("#alerts-to-fetch option:selected").val();
var maxDur = $("#max-duration option:selected").val();
if (maxDur != "") {
str = str + "&max_duration=" + maxDur;
}
return str;
}
// Update the alerts table
function updateTable() {
// Populate table with headers
let table = $('<table class="table table-striped-custom table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
table.find('thead tr').append(`<th>Start UTC</th>`);
table.find('thead tr').append(`<th>End UTC</th>`);
table.find('thead tr').append(`<th>DX</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Freq<span class='hideonmobile'>uencie</span>s & Modes</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Comment</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Source</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
// Split alerts into three types, each of which will get its own table header: On now, next 24h, and later. "On now"
// is considered to be events with an end_time where start<now<end, or events with no end time that started in the
// last hour.
onNow = alerts.filter(a => (a["end_time"] != null && a["end_time"] != 0 && moment.unix(a["end_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore())
|| ((a["end_time"] == null || a["end_time"] == 0) && moment.unix(a["start_time"]).utc().add(1, 'hours').isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore()));
next24h = alerts.filter(a => moment.unix(a["start_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().subtract(24, 'hours').isBefore());
later = alerts.filter(a => moment.unix(a["start_time"]).utc().subtract(24, 'hours').isSameOrAfter());
if (onNow.length > 0) {
table.find('tbody').append('<tr class="table-primary"><td colspan="100" style="text-align:center;">On Now</td></tr>');
addAlertRowsToTable(table.find('tbody'), onNow);
}
if (next24h.length > 0) {
table.find('tbody').append('<tr class="table-primary"><td colspan="100" style="text-align:center;">Starting within 24 hours</td></tr>');
addAlertRowsToTable(table.find('tbody'), next24h);
}
if (later.length > 0) {
table.find('tbody').append('<tr class="table-primary"><td colspan="100" style="text-align:center;">Starting later </td></tr>');
addAlertRowsToTable(table.find('tbody'), later);
}
if (onNow.length == 0 && next24h.length == 0 && later.length == 0) {
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No alerts match your filters.</td></tr>');
}
// Update DOM
$('#table-container').html(table);
}
// Add a row to tbody for each alert in the provided list
function addAlertRowsToTable(tbody, alerts) {
alerts.forEach(a => {
// Create row
let $tr = $('<tr>');
// Format UTC times for display. Start time is displayed as e.g. 7 Oct 12:34 unless the time is in a different
// year to the current year, in which case the year is inserted between month and hour.
// End time is displayed the same as above, except if the end date is the same as the start date, in which case
// just e.g. 23:45 is used. Finally, if there is no end date set, "---" is displayed.
var start_time = moment.unix(a["start_time"]).utc();
var start_time_formatted = start_time.format("D MMM HH:mm");
if (start_time.format("YYYY") != moment().format("YYYY")) {
start_time_formatted = start_time.format("D MMM YYYY HH:mm");
}
var end_time_unix = moment.unix(a["end_time"]);
var end_time = end_time_unix.utc();
var end_time_formatted = "---";
if (end_time_unix != null && end_time_unix > 0 && end_time != null) {
var end_time_formatted = end_time.format("HH:mm");
if (end_time.format("D MMM") != start_time.format("D MMM")) {
if (end_time.format("YYYY") != moment().format("YYYY")) {
end_time_formatted = end_time.format("D MMM YYYY HH:mm");
} else {
end_time_formatted = end_time.format("D MMM HH:mm");
}
}
}
// Format DX flag
var dx_flag = "<i class='fa-solid fa-circle-question'></i>";
if (a["dx_flag"] && a["dx_flag"] != null && a["dx_flag"] != "") {
dx_flag = a["dx_flag"];
}
// Format dx country
var dx_country = a["dx_country"]
if (dx_country == null) {
dx_country = "Unknown or not a country"
}
// Format freqs & modes
var freqsModesText = "";
if (a["freqs_modes"] != null) {
freqsModesText = escapeHtml(a["freqs_modes"]);
}
// Format comment
var commentText = "";
if (a["comment"] != null) {
commentText = escapeHtml(a["comment"]);
}
// Sig or fallback to source
var sigSourceText = a["source"];
if (a["sig"]) {
sigSourceText = a["sig"];
}
// Format sig_refs
var sig_refs = ""
if (a["sig_refs"]) {
sig_refs = a["sig_refs"].map(a => `<span class='nowrap'>${a}</span>`).join(", ");
}
// Populate the row
$tr.append(`<td class='nowrap'>${start_time_formatted}</td>`);
$tr.append(`<td class='nowrap'>${end_time_formatted}</td>`);
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span><a class='dx-link' href='https://qrz.com/db/${a["dx_call"]}' target='_new'>${a["dx_call"]}</a></td>`);
$tr.append(`<td class='hideonmobile'>${freqsModesText}</td>`);
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sigSourceText}</td>`);
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
tbody.append($tr);
// Second row for mobile view only, containing source, ref, freqs/modes & comment
$tr2 = $("<tr class='hidenotonmobile'>");
$tr2.append(`<td colspan="100"><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sig_refs} ${freqsModesText}<br/>${commentText}</td>`);
tbody.append($tr2);
});
}
// Load server options. Once a successful callback is made from this, we then query alerts.
function loadOptions() {
$.getJSON('/api/options', function(jsonData) {
// Store options
options = jsonData;
// Populate the filters panel
$("#settings-container").append(generateMultiToggleFilterCard("DX Continent", "dx_continent", options["continents"]));
$("#settings-container").append(generateMultiToggleFilterCard("Sources", "source", options["alert_sources"]));
// Options doesn't give us anything for Max Duration as it's a free numeric input, but we generate our own
// filter card for this.
$("#settings-container").append(generateMaxDurationDropdownFilterCard(options["alert_sources"]));
// Load settings from settings storage
loadSettings();
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
}
// Generate maximum duration drop-down filter card. This one is a special case.
function generateMaxDurationDropdownFilterCard(band_options) {
let $col = $("<div class='col'>")
let $card = $("<div class='card'>");
let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>Duration Limit</h5>`);
$p = $("<p class='card-text filter-card-text'>");
$p.append("Hide any alerts lasting more than:<br/>");
$p.append(`<select id="max-duration" class="storeable-select form-select" onclick="filtersUpdated();" style="width: 8em; display: inline-block;">
<option value="10800">3 hours</option>
<option value="43200">12 hours</option>
<option value="86400" selected>24 hours</option>
<option value="604800">1 week</option>
<option value="2419200">4 weeks</option>
<option value="">No limit</option>
</select>`);
$p.append(" <i class='fa-solid fa-circle-question' title='Some users create long-duration alerts for the period they will be generally in and around xOTA references, not just the times they are specifically on the air. Use this control to restrict the maximum duration of spots that the software will display, and exclude any with a long duration.'></i>");
// Compile HTML elements to return
$card_body.append($p);
$card.append($card_body);
$col.append($card);
return $col;
}
// Method called when any filter is changed to reload the alerts and persist the filter settings.
function filtersUpdated() {
loadAlerts();
saveSettings();
}
// Set up UI element event listeners, after the document is ready
function setUpEventListeners() {
$("#settings-button").click(function() {
$("#settings-area").toggle();
});
$("#close-settings-button").click(function() {
$("#settings-button").button("toggle");
$("#settings-area").hide();
});
}
// Startup
$(document).ready(function() {
// Call loadOptions(), this will then trigger loading alerts and setting up timers.
loadOptions();
// Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000);
// Set up event listeners
setUpEventListeners();
});