Style improvements and fixes #3

This commit is contained in:
Ian Renton
2025-12-22 16:35:28 +00:00
parent 70a7bd4814
commit 1f66da062b
13 changed files with 235 additions and 184 deletions

View File

@@ -10,7 +10,7 @@ from data.sig_ref import SIGRef
# Alert provider for Beaches on the Air
class BOTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://www.beachesontheair.com/"
def __init__(self, provider_config):

View File

@@ -10,7 +10,7 @@ from data.alert import Alert
# Alert provider NG3K DXpedition list
class NG3K(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://www.ng3k.com/adxo.xml"
AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE)

View File

@@ -10,7 +10,7 @@ from data.sig_ref import SIGRef
# Alert provider for Parks n Peaks
class ParksNPeaks(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "http://parksnpeaks.org/api/ALERTS/"
def __init__(self, provider_config):

View File

@@ -9,7 +9,7 @@ from data.sig_ref import SIGRef
# Alert provider for Parks on the Air
class POTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://api.pota.app/activation"
def __init__(self, provider_config):

View File

@@ -9,7 +9,7 @@ from data.sig_ref import SIGRef
# Alert provider for Summits on the Air
class SOTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://api-db2.sota.org.uk/api/alerts/365/all/all"
def __init__(self, provider_config):

View File

@@ -10,7 +10,7 @@ from data.sig_ref import SIGRef
# Alert provider for Wainwrights on the Air
class WOTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://www.wota.org.uk/alerts_rss.php"
RSS_DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S %z"

View File

@@ -9,7 +9,7 @@ from data.sig_ref import SIGRef
# Alert provider for Worldwide Flora and Fauna
class WWFF(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
POLL_INTERVAL_SEC = 1800
ALERTS_URL = "https://spots.wwff.co/static/agendas.json"
def __init__(self, provider_config):

View File

@@ -46,7 +46,7 @@
<p>Spothole is a Progressive Web App, which means you can install it on an Android or iOS device by opening the site in Chrome or Safari respectively, and clicking "Install" on the pop-up panel. It'll only prompt you once, so if you dismiss the prompt and change your mind, you'll find an Install / Add to Home Screen option on your browser's menu.</p>
<p>Installing Spothole on your phone is completely optional, the website works exactly the same way as the "app" does.</p>
<h4 class="mt-4">Why hasn't my spot/alert shown up yet?</h4>
<p>To avoid putting too much load on the various servers that Spothole connects to, the Spothole server only polls them once every two minutes for spots, and once every hour for alerts. (Some sources, such as DX clusters, RBN, APRS-IS and WWBOTA use a non-polling mechanism, and their updates will therefore arrive more quickly.) Then if you are using the web interface, that has its own rate at which it reloads the data from Spothole, which is once a minute for spots or 30 minutes for alerts. So you could be waiting around three minutes to see a newly added spot, or 90 minutes to see a newly added alert.</p>
<p>To avoid putting too much load on the various servers that Spothole connects to, the Spothole server only polls them once every two minutes for spots, and once every 30 minutes for alerts. (Some sources, such as DX clusters, RBN, APRS-IS and WWBOTA use a non-polling mechanism, and their updates will therefore arrive more quickly.) Then if you are using the web interface, that has its own rate at which it fetches the data from Spothole. This is instant for the main spots list, with new spots appearing immediately at the top of the list, while the map and bands displays update once a minute, and the alerts display updates once every 5 minutes. So you could be waiting around three minutes to see a newly added spot, or 40 minutes to see a newly added alert.</p>
<h4 class="mt-4">What licence does Spothole use?</h4>
<p>Spothole's source code is licenced under the Public Domain. You can write a Spothole client, run your own server, modify it however you like, you can claim you wrote it and charge people £1000 for a copy, I don't really mind. (Please don't do the last one. But if you're using my code for something cool, it would be nice to hear from you!)</p>
<h2 class="mt-4">Data Accuracy</h2>

View File

@@ -161,7 +161,9 @@
</div>
</div>
<div id="table-container"></div>
<div id="table-container">
<table id="table" class="table"><thead><tr class="table-primary"></tr></thead><tbody></tbody></table>
</div>
</div>

View File

@@ -204,7 +204,9 @@
</div>
</div>
<div id="table-container"></div>
<div id="table-container">
<table id="table" class="table"><thead><tr class="table-primary"></tr></thead><tbody></tbody></table>
</div>
</div>

View File

@@ -174,6 +174,19 @@ tr.table-faded td span {
text-decoration: line-through !important;
}
/* New spot styles */
tr.new td {
animation: 2s linear newspotanim;
}
@keyframes newspotanim {
0% {
background-color: var(--bs-success-border-subtle);
}
100% {
background-color: intial;
}
}
/* Fudge apply our own "dark primary" and "dark danger" backgrounds as Bootstrap doesn't do this itself */
[data-bs-theme=dark] tr.table-primary {
--bs-table-bg: #053680;

View File

@@ -1,5 +1,5 @@
// How often to query the server?
const REFRESH_INTERVAL_SEC = 60 * 30;
const REFRESH_INTERVAL_SEC = 60 * 10;
// Storage for the alert data that the server gives us.
var alerts = []
@@ -51,7 +51,8 @@ function updateTable() {
var showRef = $("#tableShowRef")[0].checked;
// Populate table with headers
let table = $('<table class="table">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
let table = $("#table");
table.find('thead tr').empty();
if (showStartTime) {
table.find('thead tr').append(`<th>${useLocalTime ? "Start&nbsp;(Local)" : "Start&nbsp;UTC"}</th>`);
}
@@ -74,6 +75,8 @@ function updateTable() {
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
}
table.find('tbody').empty();
// 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.
@@ -100,9 +103,6 @@ function updateTable() {
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

View File

@@ -1,5 +1,7 @@
// SSE event source
let evtSource;
// Table row count, to alternate shading
let rowCount = 0;
// Load spots and populate the table.
function loadSpots() {
@@ -28,12 +30,12 @@ function restartSSEConnection() {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
// Add spot to table
// Add spot to internal data store
newSpot = JSON.parse(event.data);
console.log(newSpot);
spots.unshift(newSpot);
spots = spots.slice(0, -1);
updateTable();
// Add spot to table
addSpotToTopOfTable(newSpot, true);
};
evtSource.onerror = function(err) {
@@ -76,7 +78,8 @@ function updateTable() {
var showDE = $("#tableShowDE")[0].checked;
// Populate table with headers
let table = $('<table class="table">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
let table = $("#table");
table.find('thead tr').empty();
if (showTime) {
table.find('thead tr').append(`<th>${useLocalTime ? "Local" : "UTC"}</th>`);
}
@@ -105,199 +108,230 @@ function updateTable() {
table.find('thead tr').append(`<th class='hideonmobile'>DE</th>`);
}
table.find('tbody').empty();
if (spots.length == 0) {
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>');
}
var count = 0;
spots.forEach(s => {
// Create row
let $tr = $('<tr>');
spots.reverse();
spots.forEach(s => addSpotToTopOfTable(s, false));
}
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
// which cause the table-striped colouring to go awry.
if (count % 2 == 1) {
$tr.addClass("table-active");
}
// Add rows corresponding to a new spot to the top of the table
// highlightNew = false for an initial load, true for new SSE-loaded spots
function addSpotToTopOfTable(s, highlightNew) {
let rows = createNewTableRowsForSpot(s, highlightNew);
$("#table").find('tbody').prepend(rows[1]);
$("#table").find('tbody').prepend(rows[0]);
}
// Show faded out if QRT
if (s["qrt"] == true) {
$tr.addClass("table-faded");
}
// Turn a spot into a set of table rows to represent it. This is actually two table rows because we need a second
// separate row for the mobile view.
// highlightNew = false for an initial load, true for new SSE-loaded spots
function createNewTableRowsForSpot(s, highlightNew) {
// Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local";
// Format a UTC or local time for display
var time = moment.unix(s["time"]).utc();
if (useLocalTime) {
time.local();
}
var time_formatted = time.format("HH:mm");
// Get user grid if valid, this will be null if it's not.
var userPos = latLonForGridCentre($("#userGrid").val());
// Format DX call
var dx_call = s["dx_call"];
if (dx_call == null) {
dx_call = "";
dx_flag = "";
}
if (s["dx_ssid"] != null) {
dx_call = dx_call + "-" + s["dx_ssid"];
}
// Table data toggles
var showTime = $("#tableShowTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked;
var showFreq = $("#tableShowFreq")[0].checked;
var showMode = $("#tableShowMode")[0].checked;
var showComment = $("#tableShowComment")[0].checked;
var showBearing = $("#tableShowBearing")[0].checked && userPos != null;
var showType = $("#tableShowType")[0].checked;
var showRef = $("#tableShowRef")[0].checked;
var showDE = $("#tableShowDE")[0].checked;
// Format dx country
var dx_country = s["dx_country"];
if (dx_country == null) {
dx_country = "Unknown or not a country";
}
// Create row
let $tr = $('<tr>');
if (highlightNew) {
$tr.addClass("new");
}
// Format DX flag
var dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] != 0) {
dx_flag = `<img src="img/flags/${s['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
}
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
// which cause the table-striped colouring to go awry.
if (rowCount % 2 == 1) {
$tr.addClass("table-active");
}
// Format the frequency
var freq_string = "Unknown"
if (s["freq"] != null) {
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)[0] : "";
freq_string = `<span class='freq-mhz freq-mhz-pad'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz hideonmobile'>${hz_string}</span>`
}
// Show faded out if QRT
if (s["qrt"] == true) {
$tr.addClass("table-faded");
}
// Format the mode
mode_string = s["mode"];
if (s["mode"] == null) {
mode_string = "???";
}
if (s["mode_source"] == "BANDPLAN") {
mode_string = mode_string + "<span class='mode-q hideonmobile'><i class='fa-solid fa-circle-question' title='The mode was not reported via the spotting service. This is a guess based on the frequency.'></i></span>";
}
// Format a UTC or local time for display
var time = moment.unix(s["time"]).utc();
if (useLocalTime) {
time.local();
}
var time_formatted = time.format("HH:mm");
// Format comment
var commentText = "";
if (s["comment"] != null) {
commentText = escapeHtml(s["comment"]);
}
// Format DX call
var dx_call = s["dx_call"];
if (dx_call == null) {
dx_call = "";
dx_flag = "";
}
if (s["dx_ssid"] != null) {
dx_call = dx_call + "-" + s["dx_ssid"];
}
// Format bearing text
var bearingText = "---<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service, and we could not determine one. A bearing to this DX is not available.'></i></span>";
if (userPos != null && s["dx_latitude"] != null && s["dx_longitude"] != null) {
var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]);
bearingText = bearing.toFixed(0).padStart(3, '0') + "°";
if (s["dx_location_good"] == null || s["dx_location_good"] == false) {
if (s["dx_location_source"] == "HOME QTH") {
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to a QRZ \"home\" location for a portable/mobile/alternative spot, so this bearing may not be accurate if the DX is close to you..'></i></span>";
} else {
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to just using the centre of a DXCC entity, so this bearing may not be accurate if the DX is close to you.'></i></span>";
}
// Format dx country
var dx_country = s["dx_country"];
if (dx_country == null) {
dx_country = "Unknown or not a country";
}
// Format DX flag
var dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] != 0) {
dx_flag = `<img src="img/flags/${s['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
}
// Format the frequency
var freq_string = "Unknown"
if (s["freq"] != null) {
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)[0] : "";
freq_string = `<span class='freq-mhz freq-mhz-pad'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz hideonmobile'>${hz_string}</span>`
}
// Format the mode
mode_string = s["mode"];
if (s["mode"] == null) {
mode_string = "???";
}
if (s["mode_source"] == "BANDPLAN") {
mode_string = mode_string + "<span class='mode-q hideonmobile'><i class='fa-solid fa-circle-question' title='The mode was not reported via the spotting service. This is a guess based on the frequency.'></i></span>";
}
// Format comment
var commentText = "";
if (s["comment"] != null) {
commentText = escapeHtml(s["comment"]);
}
// Format bearing text
var bearingText = "---<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service, and we could not determine one. A bearing to this DX is not available.'></i></span>";
if (userPos != null && s["dx_latitude"] != null && s["dx_longitude"] != null) {
var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]);
bearingText = bearing.toFixed(0).padStart(3, '0') + "°";
if (s["dx_location_good"] == null || s["dx_location_good"] == false) {
if (s["dx_location_source"] == "HOME QTH") {
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to a QRZ \"home\" location for a portable/mobile/alternative spot, so this bearing may not be accurate if the DX is close to you..'></i></span>";
} else {
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to just using the centre of a DXCC entity, so this bearing may not be accurate if the DX is close to you.'></i></span>";
}
}
}
// Format "type" (Sig or fallback to source)
var typeText = s["source"];
if (s["sig"]) {
typeText = s["sig"];
}
// Format "type" (Sig or fallback to source)
var typeText = s["source"];
if (s["sig"]) {
typeText = s["sig"];
}
// Format sig_refs
var sig_refs = "";
if (s["sig_refs"] != null) {
var items = []
for (var i = 0; i < s["sig_refs"].length; i++) {
if (s["sig_refs"][i]["url"] != null) {
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
} else {
items[i] = `${s["sig_refs"][i]["id"]}`
}
// Format sig_refs
var sig_refs = "";
if (s["sig_refs"] != null) {
var items = []
for (var i = 0; i < s["sig_refs"].length; i++) {
if (s["sig_refs"][i]["url"] != null) {
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
} else {
items[i] = `${s["sig_refs"][i]["id"]}`
}
sig_refs = items.join(", ");
}
sig_refs = items.join(", ");
}
// Format de country
var de_country = s["de_country"];
if (de_country == null) {
de_country = "Unknown or not a country";
}
// Format de country
var de_country = s["de_country"];
if (de_country == null) {
de_country = "Unknown or not a country";
}
// Format DE flag
var de_flag = "<i class='fa-solid fa-circle-question'></i>";
if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] != 0) {
de_flag = `<img src="img/flags/${s['de_dxcc_id']}.png" class="flag" width="24" alt="${de_country}" title="${de_country}"/>`;
}
// Format DE flag
var de_flag = "<i class='fa-solid fa-circle-question'></i>";
if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] != 0) {
de_flag = `<img src="img/flags/${s['de_dxcc_id']}.png" class="flag" width="24" alt="${de_country}" title="${de_country}"/>`;
}
// Format de call
var de_call = s["de_call"];
if (de_call == null) {
de_call = "";
de_flag = "";
}
if (s["de_ssid"] != null) {
de_call = de_call + "-" + s["de_ssid"];
}
// Format de call
var de_call = s["de_call"];
if (de_call == null) {
de_call = "";
de_flag = "";
}
if (s["de_ssid"] != null) {
de_call = de_call + "-" + s["de_ssid"];
}
// Format band name
var bandFullName = s['band'] ? s['band'] + " band": "Unknown band";
// Format band name
var bandFullName = s['band'] ? s['band'] + " band": "Unknown band";
// Populate the row
if (showTime) {
$tr.append(`<td class='nowrap'>${time_formatted}</td>`);
}
if (showDX) {
$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/${s["dx_call"]}' target='_new' title='${s["dx_name"] != null ? s["dx_name"] : ""}'>${dx_call}</a></td>`);
}
if (showFreq) {
$tr.append(`<td class='nowrap'><span class='band-bullet' title='${bandFullName}' style='${(s["freq"] != null) ? "color: " + s["band_color"] : "display: none;"}'>&#9632;</span>${freq_string}</td>`);
}
if (showMode) {
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
}
if (showComment) {
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
}
if (showBearing) {
$tr.append(`<td class='nowrap hideonmobile'>${bearingText}</td>`);
}
if (showType) {
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText}</td>`);
}
if (showRef) {
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
}
if (showDE) {
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
}
table.find('tbody').append($tr);
// Populate the row
if (showTime) {
$tr.append(`<td class='nowrap'>${time_formatted}</td>`);
}
if (showDX) {
$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/${s["dx_call"]}' target='_new' title='${s["dx_name"] != null ? s["dx_name"] : ""}'>${dx_call}</a></td>`);
}
if (showFreq) {
$tr.append(`<td class='nowrap'><span class='band-bullet' title='${bandFullName}' style='${(s["freq"] != null) ? "color: " + s["band_color"] : "display: none;"}'>&#9632;</span>${freq_string}</td>`);
}
if (showMode) {
$tr.append(`<td class='nowrap'>${mode_string}</td>`);
}
if (showComment) {
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
}
if (showBearing) {
$tr.append(`<td class='nowrap hideonmobile'>${bearingText}</td>`);
}
if (showType) {
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText}</td>`);
}
if (showRef) {
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
}
if (showDE) {
$tr.append(`<td class='nowrap hideonmobile'><span class='flag-wrapper' title='${de_country}'>${de_flag}</span>${de_call}</td>`);
}
// Second row for mobile view only, containing type, ref & comment
$tr2 = $("<tr class='hidenotonmobile'>");
if (count % 2 == 1) {
$tr2.addClass("table-active");
}
if (s["qrt"] == true) {
$tr2.addClass("table-faded");
}
$td2 = $("<td colspan='100'>");
if (showType) {
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText} `);
}
if (showRef) {
$td2.append(`${sig_refs} `);
}
if (showBearing) {
$td2.append(` &nbsp; Bearing: ${bearingText} `);
}
if (showComment) {
$td2.append(`<br/>${commentText}`);
}
$tr2.append($td2);
table.find('tbody').append($tr2);
// Second row for mobile view only, containing type, ref & comment
$tr2 = $("<tr class='hidenotonmobile'>");
if (rowCount % 2 == 1) {
$tr2.addClass("table-active");
}
if (s["qrt"] == true) {
$tr2.addClass("table-faded");
}
$td2 = $("<td colspan='100'>");
if (showType) {
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${typeText} `);
}
if (showRef) {
$td2.append(`${sig_refs} `);
}
if (showBearing) {
$td2.append(` &nbsp; Bearing: ${bearingText} `);
}
if (showComment) {
$td2.append(`<br/>${commentText}`);
}
$tr2.append($td2);
count++;
});
rowCount++;
// Update DOM
$('#table-container').html(table);
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