First attempt at converting "sig" to "activity" for v3

This commit is contained in:
Ian Renton
2026-09-24 07:09:37 +01:00
parent 1d0129f7bb
commit d91fa70655
90 changed files with 822 additions and 496 deletions
+67 -49
View File
@@ -15,6 +15,25 @@ info:
## Changelog
### 3.0
The term "SIG" (Special Interest Group), which Spothole inherited from ADIF, has been replaced with "activity" throughout the API.
* **Breaking change:** In spot and alert data, `sig` has been renamed to `activity` and `sig_refs` to `activity_refs`.
* **Breaking change:** In activity reference data (i.e. each entry in `activity_refs`, and the response of the activity reference lookup), `sig` has been renamed to `activity`.
* **Breaking change:** The `dx_location_source` value "SIG REF LOOKUP" has been renamed to "ACTIVITY REF LOOKUP".
* **Breaking change:** The `/spots`, `/spots/stream`, `/alerts` and `/alerts/stream` query parameter `sig` has been renamed to `activity`, and its special value `NO_SIG` to `NO_ACTIVITY`. The `/spots` and `/spots/stream` query parameters `needs_sig` and `needs_sig_ref` have been renamed to `needs_activity` and `needs_activity_ref`. When using the `fields` query parameter, use the new field names `activity` and `activity_refs`.
* **Breaking change:** `/lookup/sigref` has been renamed to `/lookup/activityref`, and its `sig` query parameter has been renamed to `activity`.
* **Breaking change:** POST `/spot` now expects `activity` and `activity_refs` in the `spot` object, rather than `sig` and `sig_refs`.
* **Breaking change:** In the `/options` response, `sigs` has been renamed to `activities`, and within each activity, `sig_type` has been renamed to `activity_type`.
* **Breaking change:** In the `/status` response, `sig_ref_data_providers` has been renamed to `activity_ref_data_providers`, and within each provider, `sig_name` has been renamed to `activity_name`.
#### Upgrading a client from v2 to v3 API endpoints
In v3.0 of Spothole, the `v2` (and `v1`) API endpoints will be maintained for backwards compatibility, so if you have written a client against the `v2` API, it will continue to receive `sig`, `sig_refs` etc. as before. However, you are encouraged to move to the `v3` API endpoints as soon as possible.
To upgrade, replace `v2` with `v3` in the URLs your code calls, then rename any use of the fields, query parameters and values listed above. If you use the activity reference lookup, call `/lookup/activityref?activity=...&id=...` instead of `/lookup/sigref?sig=...&id=...`.
### 2.2
* Renamed AMSAT SIG to "Satellite" as AMSAT is a specific organisation not just a general term for satellite QSOs
@@ -102,10 +121,10 @@ info:
license:
name: The Unlicense
url: https://unlicense.org/#the-unlicense
version: 2.0
version: 3.0
servers:
- url: https://spothole.app/api/v2
- url: https://spothole.app/api/v3
tags:
- name: Spots
@@ -396,7 +415,7 @@ paths:
example: "Failed"
/lookup/sigref:
/lookup/activityref:
get:
tags:
- Utilities
@@ -405,7 +424,7 @@ paths:
Perform a lookup of data about a certain reference, providing the activity and the ID of the
reference. An ActivityRef structure will be returned containing the activity and ID, plus any other
information Spothole could find about it.
operationId: sigref
operationId: activityref
parameters:
- $ref: '#/components/parameters/ActivityRefLookupActivity'
- $ref: '#/components/parameters/ActivityRefLookupId'
@@ -461,7 +480,7 @@ paths:
Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object
containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the
`/options` response to see which activities and providers support upstream submission. cURL example:
`curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v2/spot`"
`curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v3/spot`"
operationId: spot
requestBody:
description: Object containing a "spot" sub-object with the spot data, and an optional "handling" sub-object with server-side instructions of what to do with it.
@@ -559,32 +578,32 @@ components:
schema:
$ref: "#/components/schemas/Source"
SpotActivity:
name: sig
name: activity
in: query
description: >
Limit the spots to only ones from one or more activities provided as an argument.
To select more than one activity, supply a comma-separated list. The special `sig` name `NO_SIG`
matches spots with no activity set. You can use `sig=NO_SIG` to specifically only return generic
To select more than one activity, supply a comma-separated list. The special `activity` name `NO_ACTIVITY`
matches spots with no activity set. You can use `activity=NO_ACTIVITY` to specifically only return generic
spots with no associated activity. You can also use combinations to request for example POTA + no
activity, but reject other activities. If you want to request 'every activity and not No Activity', see the
`needs_sig` query parameter for a shortcut.
`needs_activity` query parameter for a shortcut.
schema:
$ref: "#/components/schemas/ActivityNameIncludingNoSig"
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
SpotNeedsActivity:
name: needs_sig
name: needs_activity
in: query
description: >
Limit the spots to only ones with an activity such as POTA. Because supplying all
known activities as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots
known activities as an `activity` parameter is unwieldy, and leaving `activity` blank will also return spots
with *no* activity, this parameter can be set true to return only spots with an activity, regardless of
what it is, so long as it's not blank. This is the equivalent of supplying the `sig` query
param with a list of every known activity apart from the special `NO_SIG` value. This is what Field
what it is, so long as it's not blank. This is the equivalent of supplying the `activity` query
param with a list of every known activity apart from the special `NO_ACTIVITY` value. This is what Field
Spotter uses to exclude generic cluster spots and only retrieve xOTA things.
schema:
type: boolean
default: false
SpotNeedsActivityRef:
name: needs_sig_ref
name: needs_activity_ref
in: query
description: >
Limit the spots to only ones which have at least one reference (e.g. a park reference) for
@@ -723,14 +742,14 @@ components:
schema:
$ref: "#/components/schemas/Source"
AlertActivity:
name: sig
name: activity
in: query
description: >
Limit the alerts to only ones from one or more activities. To select more than one
activity, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts
specifically without an associated activity (i.e. general DXpeditions).
activity, supply a comma-separated list. The special value 'NO_ACTIVITY' can be included to return alerts
specifically without an associated activity.
schema:
$ref: "#/components/schemas/ActivityNameIncludingNoSig"
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
AlertDxContinent:
name: dx_continent
in: query
@@ -839,9 +858,9 @@ components:
type: string
example: M0TRT
ActivityRefLookupActivity:
name: sig
name: activity
in: query
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
description: Activity, e.g. outdoor activity programme such as POTA
required: true
schema:
$ref: "#/components/schemas/ActivityName"
@@ -938,11 +957,11 @@ components:
- EVENT
example: TRADITIONAL
ActivityNameIncludingNoSig:
ActivityNameIncludingNoActivity:
oneOf:
- $ref: "#/components/schemas/ActivityName"
- type: string
enum: [ NO_SIG ]
enum: [ NO_ACTIVITY ]
example: POTA
ActivityRefType:
@@ -1068,7 +1087,7 @@ components:
type: string
enum:
- SPOT
- "SIG REF LOOKUP"
- "ACTIVITY REF LOOKUP"
- "GRID"
- "HOME QTH"
- DXCC
@@ -1090,8 +1109,8 @@ components:
type: string
description: Activity reference ID.
example: GB-0001
sig:
description: Activity that this reference is in. Still named "sig" in the API for backwards compatibility.
activity:
description: Activity that this reference is in.
$ref: "#/components/schemas/ActivityName"
name:
type: string
@@ -1205,7 +1224,7 @@ components:
itself, or from a lookup of the activity ref (e.g. park) it's likely quite accurate, but if
we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will
be a lot less accurate. "SPOT" indicates the location source was the spot itself from the
spotting service. "SIG REF LOOKUP" indicates that the spot didn't provide a location,
spotting service. "ACTIVITY REF LOOKUP" indicates that the spot didn't provide a location,
but we looked it up from reference data. "GRID" indicates that the spot provided some
location data such as a Maidenhead, UK Ordnance Survey or Irish grid reference, but the
location is likely less accurate than "SPOT". "HOME QTH" indicates we looked up the DX
@@ -1218,7 +1237,7 @@ components:
type: boolean
description: >
Does the software think the location is good enough to put a marker on a map? This is
true if the source is "SPOT", "SIG REF LOOKUP" or "GRID", or alternatively if
true if the source is "SPOT", "ACTIVITY REF LOOKUP" or "GRID", or alternatively if
the source is "HOME QTH" and the callsign doesn't have a slash in it (i.e. operator
likely at home).
example: true
@@ -1315,14 +1334,14 @@ components:
type: string
description: Comment left by the spotter, if any
example: "59 in NY 73"
sig:
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
activity:
description: Activity, e.g. outdoor activity programme such as POTA
$ref: "#/components/schemas/ActivityName"
sig_refs:
activity_refs:
type: array
items:
$ref: '#/components/schemas/ActivityRef'
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility.
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
qrt:
type: boolean
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
@@ -1364,7 +1383,7 @@ components:
type: boolean
description: >
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather
than only adding it to this Spothole server. Requires `sig`, at least one `sig_refs`
than only adding it to this Spothole server. Requires `activity`, at least one `activity_refs`
entry, and `upstream_provider` to be set. Check `spot_submit_providers` in the
/options response to see which activities and providers support this.
default: false
@@ -1503,14 +1522,14 @@ components:
type: string
description: Comment made by the activator, if any
example: "2025 DXpedition to null island"
sig:
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
activity:
description: Activity, e.g. outdoor activity programme such as POTA
$ref: "#/components/schemas/ActivityName"
sig_refs:
activity_refs:
type: array
items:
$ref: '#/components/schemas/ActivityRef'
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility.
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
url:
type: string
description: A URL linking to more information about the alert, e.g. DXpedition or contest info.
@@ -1604,8 +1623,8 @@ components:
Activity:
type: object
description: >
Represents an activity (a term which replaces the older "Special Interest Group" or "SIG" terminology,
though `sig`-prefixed field names remain for API backwards compatibility).
Represents an activity, such as an outdoor activity programme (e.g. POTA), or another kind of operating
(e.g. Contest, DXpedition).
properties:
name:
description: The abbreviated name of the activity
@@ -1614,12 +1633,11 @@ components:
type: string
description: The full name of the activity
example: Parks on the Air
sig_type:
type: boolean
activity_type:
description: >
Whether the activity is traditional (e.g. EME), adventure (e.g. POTA), regional (e.g. WAB), or for a
specific event (e.g. MOTA). Generally for Spothole's own internal use, clients probably won't need this.
Used to group them in the web UI. Still named "sig_type" in the API for backwards compatibility.
Used to group them in the web UI.
$ref: "#/components/schemas/ActivityType"
has_refs:
type: boolean
@@ -1998,7 +2016,7 @@ components:
StaticDataProviderStatus:
type: object
properties:
sig_name:
name:
type: string
description: The name of the provider.
example: K0SWE
@@ -2020,9 +2038,9 @@ components:
ActivityRefDataProviderStatus:
type: object
properties:
sig_name:
activity_name:
type: string
description: The name of the activity. Still named "sig_name" in the API for backwards compatibility.
description: The name of the activity.
example: WWFF
enabled:
type: boolean
@@ -2046,7 +2064,7 @@ components:
CallsignDataProviderStatus:
type: object
properties:
sig_name:
name:
type: string
description: The name of the provider.
example: Country Files
@@ -2185,7 +2203,7 @@ components:
description: An array of all the static reference data providers.
items:
$ref: '#/components/schemas/StaticDataProviderStatus'
sig_ref_data_providers:
activity_ref_data_providers:
type: array
description: An array of all the activity reference data providers.
items:
@@ -2216,7 +2234,7 @@ components:
items:
type: string
example: "PHONE"
sigs:
activities:
type: array
description: An array of all the supported activities.
items:
@@ -2254,7 +2272,7 @@ components:
type: integer
description: >
The maximum age, in seconds, of any spot before it will be deleted by the system. When
querying the /api/v2/spots endpoint and providing a "max_age" or "since" parameter, there
querying the /api/v3/spots endpoint and providing a "max_age" or "since" parameter, there
is no point providing a number larger than this, because the system drops all spots older
than this.
example: 3600
+13 -13
View File
@@ -29,7 +29,7 @@ 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/v2/options', function (jsonData) {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
@@ -42,8 +42,8 @@ function loadOptions() {
});
// Populate activity drop-down
$.each(options["sigs"], function (i, activity) {
$('#sig').append($('<option>', {
$.each(options["activities"], function (i, activity) {
$('#activity').append($('<option>', {
value: activity.name,
text: activity.name
}));
@@ -91,8 +91,8 @@ function updateUpstreamArea() {
return;
}
const sig = $("#sig").val();
const providers = (sig && options["spot_submit_providers"][sig]) ? options["spot_submit_providers"][sig] : [];
const activity = $("#activity").val();
const providers = (activity && options["spot_submit_providers"][activity]) ? options["spot_submit_providers"][activity] : [];
if (providers.length === 0) {
$("#upstream-area").hide();
@@ -131,8 +131,8 @@ function updateCredentialsButton() {
// Get the currently selected upstream provider name
function getSelectedUpstreamProvider() {
const providers = (options && options["spot_submit_providers"] && $("#sig").val())
? (options["spot_submit_providers"][$("#sig").val()] || [])
const providers = (options && options["spot_submit_providers"] && $("#activity").val())
? (options["spot_submit_providers"][$("#activity").val()] || [])
: [];
if (providers.length === 0) return null;
if (providers.length === 1) return providers[0];
@@ -197,8 +197,8 @@ function addSpot() {
const dx = $("#dx-call").val().toUpperCase();
const freqStr = $("#freq").val();
const mode = $("#mode")[0].value;
const sig = $("#sig")[0].value;
const sigRef = $("#sig-ref").val();
const activity = $("#activity")[0].value;
const activityRef = $("#activity-ref").val();
const dxGrid = $("#dx-grid").val();
const comment = $("#comment").val();
const de = $("#de-call").val().toUpperCase();
@@ -208,8 +208,8 @@ function addSpot() {
spot["dx_call"] = dx;
spot["freq"] = parseFloat(freqStr) * 1000;
if (mode !== "") spot["mode"] = mode;
if (sig !== "") spot["sig"] = sig;
if (sigRef !== "") spot["sig_refs"] = [{sig: sig, id: sigRef}];
if (activity !== "") spot["activity"] = activity;
if (activityRef !== "") spot["activity_refs"] = [{activity: activity, id: activityRef}];
if (dxGrid !== "") spot["dx_grid"] = dxGrid;
if (comment !== "") spot["comment"] = comment;
spot["de_call"] = de;
@@ -232,7 +232,7 @@ function addSpot() {
handling["upstream_credentials"] = loadCredentials(upstreamProviderName);
}
$.ajax("/api/v2/spot", {
$.ajax("/api/v3/spot", {
data: JSON.stringify({spot, handling}),
contentType: 'application/json',
type: 'POST',
@@ -291,7 +291,7 @@ $("#mode").change(function () {
});
// Update upstream area and credentials button when activity changes
$("#sig").change(function () {
$("#activity").change(function () {
updateUpstreamArea();
});
+14 -14
View File
@@ -10,7 +10,7 @@ let alerts = [];
// to alerts
function loadAlerts() {
$.ajax({
url: '/api/v2/alerts' + buildQueryString(), dataType: 'json', success: function (jsonData) {
url: '/api/v3/alerts' + buildQueryString(), dataType: 'json', success: function (jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
// Store data
@@ -24,7 +24,7 @@ function loadAlerts() {
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString() {
let str = "?";
["dx_continent", "source", "sig"].forEach(fn => {
["dx_continent", "source", "activity"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -210,14 +210,14 @@ function addAlertRowsToTable(tbody, alerts) {
if (a["dx_calls"] != null) {
dx_calls_html = a["dx_calls"].map(call => `<a class='dx-link' href='https://qrz.com/db/${call}' target='_new'>${call}</a>`).join(", ");
}
if (dx_calls_html === "" && a["sig"] === "Contest") {
if (dx_calls_html === "" && a["activity"] === "Contest") {
// Contest = true and no DX callsigns, so display "Contest"
dx_calls_html = "Contest"
}
// Format DXpedition country
let dx_country_html = "";
if (a["sig"] === "DXpedition" && a["dx_country"] != null && a["dx_country"] !== "") {
if (a["activity"] === "DXpedition" && a["dx_country"] != null && a["dx_country"] !== "") {
dx_country_html = `<br/>${a["dx_country"]}`;
}
@@ -252,23 +252,23 @@ function addAlertRowsToTable(tbody, alerts) {
// Activity or fallback to "General DX"
let activityText = "General DX";
if (a["sig"]) {
activityText = a["sig"];
if (a["activity"]) {
activityText = a["activity"];
}
// Format activity refs
let activityRefs = "";
if (a["sig_refs"] != null) {
if (a["activity_refs"] != null) {
const items = [];
for (let i = 0; i < a["sig_refs"].length; i++) {
if (a["sig_refs"][i]["url"] != null) {
items[i] = `<a href='${encodeURI(a["sig_refs"][i]["url"])}' title='${escapeHtml(a["sig_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(a["sig_refs"][i]["id"])}</a>`
for (let i = 0; i < a["activity_refs"].length; i++) {
if (a["activity_refs"][i]["url"] != null) {
items[i] = `<a href='${encodeURI(a["activity_refs"][i]["url"])}' title='${escapeHtml(a["activity_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(a["activity_refs"][i]["id"])}</a>`
} else {
items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}`
items[i] = `${escapeHtml(a["activity_refs"][i]["id"])}`
}
// If this is a satellite alert the ref will just be the satellite, but DX grid is also important, so
// show that if we can.
if (a["sig_refs"][i]["sig"] === "Satellite" && a["dx_grid"] != null) {
if (a["activity_refs"][i]["activity"] === "Satellite" && a["dx_grid"] != null) {
items[i] += " from " + a["dx_grid"];
}
}
@@ -330,7 +330,7 @@ function addAlertRowsToTable(tbody, alerts) {
// Load server options. Once a successful callback is made from this, we then query alerts.
function loadOptions() {
$.getJSON('/api/v2/options', function (jsonData) {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
@@ -338,7 +338,7 @@ function loadOptions() {
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["sigs"].filter(o => o["alerts_possible"]), false);
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
+5 -5
View File
@@ -20,7 +20,7 @@ function loadSpots() {
sseAbortController.abort();
}
$.ajax({
url: '/api/v2/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
url: '/api/v3/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
// Store data
spots = jsonData;
// Update bands display
@@ -39,7 +39,7 @@ function startSSEConnection() {
sseAbortController = new AbortController();
// No need to include QRZ/HamQTH credentials because the information wouldn't be displayed on the bands panel anyway
fetchEventSource('/api/v2/spots/stream' + buildQueryString(), {
fetchEventSource('/api/v3/spots/stream' + buildQueryString(), {
signal: sseAbortController.signal,
openWhenHidden: true,
@@ -80,7 +80,7 @@ function expireOldSpots() {
// in the bands page's version of this, because nothing QRZ.com/HamQTH can provide will affect the display.
function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
["dx_continent", "de_continent", "mode", "source", "band", "activity"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -281,7 +281,7 @@ function removeDuplicatesForBandPanel(spotList) {
// 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/v2/options', function (jsonData) {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
@@ -295,7 +295,7 @@ function loadOptions() {
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["sigs"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
+24 -9
View File
@@ -33,6 +33,16 @@ function saveSettings() {
// Load settings from local storage and set up the filter selectors. Suppressed if "use local storage" is false.
function loadSettings() {
if (useLocalStorage) {
// Spothole v2 and earlier stored activity filter settings with "sig" in the element IDs, so migrate these to
// their new names.
Object.keys(localStorage).forEach(function (key) {
if (key.startsWith("#filter-button-sig-")) {
const newKey = key.replace("#filter-button-sig-", "#filter-button-activity-").replace("-NO_SIG:", "-NO_ACTIVITY:");
localStorage.setItem(newKey, localStorage.getItem(key));
localStorage.removeItem(key);
}
});
// Find all local storage entries and push their data to the corresponding UI element
Object.keys(localStorage).forEach(function (key) {
if (key.startsWith("#") && key.includes(":")) {
@@ -67,7 +77,12 @@ function loadURLParams() {
updateSelectFromParam(params, "limit", "alerts_to_fetch"); // Only on Alerts page
updateSelectFromParam(params, "max_age", "max_spot_age"); // Only on Map & Bands pages
updateFilterFromParam(params, "band", "band");
updateFilterFromParam(params, "sig", "sig");
updateFilterFromParam(params, "activity", "activity");
// Spothole v2 and earlier called the "activity" param "sig", so support this for existing embeds
if (params.get("activity") == null && params.get("sig") != null) {
params.set("activity", params.get("sig").replace("NO_SIG", "NO_ACTIVITY"));
updateFilterFromParam(params, "activity", "activity");
}
updateFilterFromParam(params, "source", "source");
updateFilterFromParam(params, "mode", "mode");
updateFilterFromParam(params, "dx_continent", "dx_continent");
@@ -156,41 +171,41 @@ 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-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
$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_SIG) option is offered - this doesn't apply on the alerts page, where every alert has an activity.
// 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["sig_type"] === "TRADITIONAL");
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-sig storeable-checkbox" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
$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["sig_type"] === "ADVENTURE");
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["sig_type"] === "REGIONAL");
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["sig_type"] === "EVENT");
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('sig', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('sig', false); return false;">None</a></div>`);
$("#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
+2 -2
View File
@@ -10,7 +10,7 @@ let ionosondeChart = null;
// Load solar conditions
function loadSolarConditions() {
$.getJSON('/api/v2/solar', function (jsonData) {
$.getJSON('/api/v3/solar', function (jsonData) {
// HF
@@ -660,7 +660,7 @@ function dxStatsContientChanged() {
// Fetch DX stats from the API and render
function loadDxStats() {
$.getJSON('/api/v2/dxstats', function (jsonData) {
$.getJSON('/api/v3/dxstats', function (jsonData) {
dxStatsData = jsonData;
renderDxStats();
});
+13 -13
View File
@@ -45,7 +45,7 @@ function loadSpots() {
// 3) Subscribe to the SSE endpoint (with credentials if we have them) so that updates come with augmented
// data if they can.
$.ajax({
url: '/api/v2/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
url: '/api/v3/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
// Store data
spots = jsonData;
// Update map
@@ -58,7 +58,7 @@ function loadSpots() {
// OK, we have credentials and have loaded once without them so the user has a basic map. Now reload
// with the credentials and replace what's on the map, so we can improve the data.
$.ajax({
url: '/api/v2/spots' + buildQueryString(),
url: '/api/v3/spots' + buildQueryString(),
dataType: 'json',
headers: getCredentialHeaders(),
success: function (jsonData2) {
@@ -85,7 +85,7 @@ function startSSEConnection() {
sseAbortController = new AbortController();
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
fetchEventSource('/api/v2/spots/stream' + buildQueryString(), {
fetchEventSource('/api/v3/spots/stream' + buildQueryString(), {
headers: getCredentialHeaders(),
signal: sseAbortController.signal,
openWhenHidden: true,
@@ -183,7 +183,7 @@ function removeSpotFromMap(key) {
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
["dx_continent", "de_continent", "mode", "source", "band", "activity"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -276,19 +276,19 @@ function getTooltipText(s) {
// Activity or fallback to source
let activitySourceText = s["source"];
if (s["sig"]) {
activitySourceText = s["sig"];
if (s["activity"]) {
activitySourceText = s["activity"];
}
// Format activity refs
let activityRefs = "";
if (s["sig_refs"] != null) {
if (s["activity_refs"] != null) {
const items = [];
for (let 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='activity-ref-link'>${s["sig_refs"][i]["id"]}</a>`
for (let i = 0; i < s["activity_refs"].length; i++) {
if (s["activity_refs"][i]["url"] != null) {
items[i] = `<a href='${s["activity_refs"][i]["url"]}' title='${s["activity_refs"][i]["name"]}' target='_new' class='activity-ref-link'>${s["activity_refs"][i]["id"]}</a>`
} else {
items[i] = `${s["sig_refs"][i]["id"]}`
items[i] = `${s["activity_refs"][i]["id"]}`
}
}
activityRefs = items.join(", ");
@@ -325,7 +325,7 @@ function getTooltipText(s) {
// 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/v2/options', function (jsonData) {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
@@ -339,7 +339,7 @@ function loadOptions() {
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["sigs"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
+12 -12
View File
@@ -20,7 +20,7 @@ function loadSpots() {
// Make the new query. No credential headers on the first load to keep things quick
$.ajax({
url: '/api/v2/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
url: '/api/v3/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
// Store data
spots = jsonData;
// Update table
@@ -43,7 +43,7 @@ function startSSEConnection() {
sseAbortController = new AbortController();
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
fetchEventSource('/api/v2/spots/stream' + buildQueryString(), {
fetchEventSource('/api/v3/spots/stream' + buildQueryString(), {
headers: getCredentialHeaders(),
signal: sseAbortController.signal,
openWhenHidden: true,
@@ -100,7 +100,7 @@ function startSSEConnection() {
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
["dx_continent", "de_continent", "mode", "source", "band", "activity"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
@@ -329,19 +329,19 @@ function createNewTableRowsForSpot(s, highlightNew) {
// Format activity
let activityText = "General DX";
if (s["sig"]) {
activityText = s["sig"];
if (s["activity"]) {
activityText = s["activity"];
}
// Format activity refs
let activityRefs = "";
if (s["sig_refs"] != null) {
if (s["activity_refs"] != null) {
const items = [];
for (let i = 0; i < s["sig_refs"].length; i++) {
if (s["sig_refs"][i]["url"] != null) {
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["sig_refs"][i]["url"])}' title='${escapeHtml(s["sig_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(s["sig_refs"][i]["id"])}</a></span>`
for (let i = 0; i < s["activity_refs"].length; i++) {
if (s["activity_refs"][i]["url"] != null) {
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["activity_refs"][i]["url"])}' title='${escapeHtml(s["activity_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(s["activity_refs"][i]["id"])}</a></span>`
} else {
items[i] = `<span style="white-space: nowrap;">${escapeHtml(s["sig_refs"][i]["id"])}</span>`
items[i] = `<span style="white-space: nowrap;">${escapeHtml(s["activity_refs"][i]["id"])}</span>`
}
}
activityRefs = items.join(", ");
@@ -464,7 +464,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
// 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/v2/options', function (jsonData) {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
@@ -478,7 +478,7 @@ function loadOptions() {
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["sigs"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
+4 -4
View File
@@ -1,6 +1,6 @@
// Load server status
function loadStatus() {
$.getJSON('/api/v2/status', function (jsonData) {
$.getJSON('/api/v3/status', function (jsonData) {
$("#software_version").text(jsonData["software_version"]);
$("#server_owner_callsign").text(jsonData["server_owner_callsign"]);
$("#up-since").text(moment().subtract(jsonData["uptime"], 'seconds').fromNow());
@@ -54,10 +54,10 @@ function loadStatus() {
</div>`);
});
jsonData["sig_ref_data_providers"].forEach(p => {
$("#sig_ref_data_providers-status-container").append(`
jsonData["activity_ref_data_providers"].forEach(p => {
$("#activity_ref_data_providers-status-container").append(`
<div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
<div class="col"><strong>${p["sig_name"]}</strong></div>
<div class="col"><strong>${p["activity_name"]}</strong></div>
<div class="col">Status: ${p["status"]}</div>
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
<div class="col">References: ${p["enabled"] ? p["reference_count"] : "N/A"}</div>