// How often to query the server? const REFRESH_INTERVAL_SEC = 60; // Storage for the spot data that the server gives us. var spots = [] // Storage for the options that the server gives us. This will define our filters. var options = {}; // Last time we updated the spots list on display. var lastUpdateTime; // Options-based lookups for band colours band_colors = {} // Load spots and populate the table. function loadSpots() { $.getJSON('/api/spots', function(jsonData) { // Store last updated time lastUpdateTime = moment.utc(); updateRefreshDisplay(); // Store data spots = jsonData; // Update table updateTable(); }); } // Update the spots table function updateTable() { // Populate table with headers let headers = Object.keys(spots[0]); let table = $('').append(''); ["UTC", "DX", "Frequency", "Mode", "Comment", "Source", "Ref.", "DE"].forEach(header => table.find('thead tr').append(``)); spots.forEach(s => { // Create row let $tr = $(''); // Show in red if QRT if (s["qrt"] == true) { $tr.addClass("table-faded"); } // Format a UTC time for display var time = moment.utc(s["time"], moment.ISO_8601); var time_formatted = time.format("HH:mm") // Format dx country var dx_country = s["dx_country"] if (dx_country == null) { dx_country = "Unknown or not a country" } // Format the frequency 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 = `${mhz.toFixed(0)}${khz.toFixed(0).padStart(3, '0')}${hz_string}` // Format the mode mode_string = s["mode"]; if (s["mode_source"] == "BANDPLAN") { mode_string = mode_string + "" } // Band-based colour var band_dot_style = "" if (band_colors[s["band"]]) { band_dot_style = `color: ${band_colors[s["band"]]}; ` } // Format sig_refs var sig_refs = "" if (s["sig_refs"]) { sig_refs = s["sig_refs"].join(", ") } // Format DE flag var de_flag = ""; if (s["de_flag"] && s["de_flag"] != "") { de_flag = s["de_flag"]; } // Format de country var de_country = s["de_country"] if (de_country == null) { de_country = "Unknown or not a country" } // Populate the row $tr.append(``); $tr.append(``); $tr.append(``); $tr.append(``); $tr.append(''); $tr.append(``); $tr.append(``); $tr.append(``); table.find('tbody').append($tr); }); // Update DOM $('#table-container').html(table); } // Load server status function loadStatus() { $.getJSON('/api/status', function(jsonData) { $("#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 = $("
") let $card = $("
"); let $card_body = $("
"); $card_body.append(`
${title}
`); $card_body.append(`

${textLines.join("
")}

`); $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 // spots repeatedly. function loadOptions() { $.getJSON('/api/options', function(jsonData) { // Store options options = jsonData; // Separately store colour lookups for bands options["bands"].forEach(m => { 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) }); } // Update the refresh timing display function updateRefreshDisplay() { if (lastUpdateTime != null) { let count = REFRESH_INTERVAL_SEC; let secSinceUpdate = moment.duration(moment().diff(lastUpdateTime)).asSeconds(); updatingString = "Updating..." if (secSinceUpdate < REFRESH_INTERVAL_SEC) { count = REFRESH_INTERVAL_SEC - secSinceUpdate; updatingString = "Updating in " + count.toFixed(0) + " seconds..."; } $("#timing-container").text("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString); } } // Utility function to escape HTML characters from a string. function escapeHtml(str) { if (typeof str !== 'string') { return ''; } const escapeCharacter = (match) => { switch (match) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case '\'': return '''; case '`': return '`'; default: return match; } }; 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); // 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(); }); });
${header}
${time_formatted}${s["dx_flag"]}${s["dx_call"]}${freq_string}${mode_string}' + escapeHtml(`${s["comment"]}`) + ' ${s["source"]}${sig_refs}${de_flag}${s["de_call"]}