Hellmerge #95 branch into 2.0-pre. #119

This commit is contained in:
Ian Renton
2026-08-08 09:52:14 +01:00
parent 4e7c5a05aa
commit ac35a920cf
13 changed files with 109 additions and 93 deletions
+4 -4
View File
@@ -12,7 +12,7 @@ Various approaches exist to writing your own client, but in general:
* Refer to the API docs. These are built on an OpenAPI definition file (`/static/apidocs/openapi.yml`), which you can * Refer to the API docs. These are built on an OpenAPI definition file (`/static/apidocs/openapi.yml`), which you can
automatically use to generate a client skeleton using various software. automatically use to generate a client skeleton using various software.
* Call the main "spots" or "alerts" API endpoints to get the data you want. For example, your app could call * Call the main "spots" or "alerts" API endpoints to get the data you want. For example, your app could call
`https://spothole.app/api/v1/spots` once every few minutes. Apply filters if necessary. `https://spothole.app/api/v2/spots` once every few minutes. Apply filters if necessary.
* Call the "options" API to get an idea of which bands, modes etc. the server knows about. You might want to do that * Call the "options" API to get an idea of which bands, modes etc. the server knows about. You might want to do that
first before calling the spots/alerts APIs, to allow you to populate your filters correctly. first before calling the spots/alerts APIs, to allow you to populate your filters correctly.
* Refer to the provided HTML/JS interface for a reference on different approaches. For example, the "alerts"/"upcoming" * Refer to the provided HTML/JS interface for a reference on different approaches. For example, the "alerts"/"upcoming"
@@ -25,12 +25,12 @@ once every two minutes, so if your client is interested in POTA data there's no
than that. than that.
If you absolutely must be informed within seconds of a spot arriving in Spothole, please use the SSE endpoints instead, If you absolutely must be informed within seconds of a spot arriving in Spothole, please use the SSE endpoints instead,
e.g. `https://spothole.app/api/v1/spots/stream`. e.g. `https://spothole.app/api/v2/spots/stream`.
If you want to handle different types of spot or alert differently within your client, please consider making a single If you want to handle different types of spot or alert differently within your client, please consider making a single
request to the Spothole API to retrieve all the data, then filtering on your side. For example, call request to the Spothole API to retrieve all the data, then filtering on your side. For example, call
`https://spothole.app/api/v1/spots?sig=POTA,SOTA` rather than making two separate calls to `https://spothole.app/api/v2/spots?sig=POTA,SOTA` rather than making two separate calls to
`https://spothole.app/api/v1/spots?sig=POTA` and `https://spothole.app/api/v1/spots?sig=SOTA`. `https://spothole.app/api/v2/spots?sig=POTA` and `https://spothole.app/api/v2/spots?sig=SOTA`.
Remember, here at Spothole Inc. we offer an industry-standard "five nines" uptime on our server, with our own unique Remember, here at Spothole Inc. we offer an industry-standard "five nines" uptime on our server, with our own unique
twist: we don't tell you which side of the decimal point the nines start! (Translation: This is a hobby project. twist: we don't tell you which side of the decimal point the nines start! (Translation: This is a hobby project.
+1 -1
View File
@@ -85,7 +85,7 @@ server {
} }
# SSE endpoints # SSE endpoints
location ~ ^/api/v1/(spots|alerts)/stream/? { location ~ ^/api/v2/(spots|alerts)/stream/? {
proxy_pass http://spothole:8080; proxy_pass http://spothole:8080;
# Remove buffering, remove caching, add suitable timeouts for SSE API calls # Remove buffering, remove caching, add suitable timeouts for SSE API calls
+18 -11
View File
@@ -9,12 +9,17 @@ To navigate your way around the source code, this list may help.
*Python back-end code* *Python back-end code*
* `/core` - Core classes and scripts * `/core` - Core classes and utilities
* `/data` - Data storage classes * `/data` - Data storage classes
* `/spotproviders` - Classes providing spots by accessing the APIs of other services * `/providers/spot` - Classes providing spots by accessing the APIs of other services
* `/alertproviders` - Classes providing alerts by accessing the APIs of other services * `/providers/alert` - Classes providing alerts by accessing the APIs of other services
* `/solarconditionsproviders` - Classes providing solar and propagation by accessing the APIs of other services * `/providers/solarconditions` - Classes providing solar and propagation by accessing the APIs of other services
* `/providers/staticdata` - Classes providing static lookup data by accessing bundled data files or the APIs of other
services
* `/providers/sigrefdata` - Classes providing SIG reference lookup data by accessing bundled data files or the APIs of
other services
* `/server` - Classes for running Spothole's own web server * `/server` - Classes for running Spothole's own web server
* `spothole.py` - Main application script
*Templates* *Templates*
@@ -24,6 +29,7 @@ To navigate your way around the source code, this list may help.
* `/static` - Root for static files served by the web server. These are all served from a path starting `/static/`. * `/static` - Root for static files served by the web server. These are all served from a path starting `/static/`.
* `/static/apidocs` - Contains the OpenAPI spec (`openapi.yml`) * `/static/apidocs` - Contains the OpenAPI spec (`openapi.yml`)
* `/static/audio` - Audio files used by the web front-end
* `/static/css` - CSS files used by the web front-end * `/static/css` - CSS files used by the web front-end
* `/static/img` - image files used by the web front-end * `/static/img` - image files used by the web front-end
* `/static/js` - JavaScript used by the web front-end * `/static/js` - JavaScript used by the web front-end
@@ -31,18 +37,18 @@ To navigate your way around the source code, this list may help.
*Miscellaneous* *Miscellaneous*
* `/` - Main script (`spothole.py`), pip `requirements.txt`, config, README, etc. * `/` - pip `requirements.txt`, config, README, etc.
* `/docs` - Documentation * `/docs` - Documentation
* `/images` - Image sources * `/images` - Image sources
* `/datafiles` - Local data sources (differentiated from the majority of data files which are loaded from URLs and * `/datafiles` - Local data files, used by some providers when the data will never change and/or is not easily available
cached in `/cache`) online in a format Spothole can handle
* `/cache` - Directory where static-ish data downloaded from the internet is cached to avoid rapid re-requests, and * `/cache` - Directory where Spothole stores all the data it uses that should be persisted to disk. Created on first
where spot/alert data is cached so that it survives a software restart. Created on first run. run.
### Extending the server ### Extending the server
Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a Spothole is designed to be easily extensible. If you want to write your own spot provider, for example, simply add a
module to the `spotproviders` package containing your class. (Currently, in order to be loaded correctly, the module ( module to the `providers.spot` package containing your class. (Currently, in order to be loaded correctly, the module (
file) name should be the same as the class name, but lower case.) file) name should be the same as the class name, but lower case.)
Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend " Your class should extend "SpotProvider"; if it operates by polling an HTTP Server on a timer, it can instead extend "
@@ -66,4 +72,5 @@ parameters are optional, but you will at least want to provide a `time` (which m
Finally, simply add the appropriate config to the `spot_providers` section of `config.yml`, and your provider should be Finally, simply add the appropriate config to the `spot_providers` section of `config.yml`, and your provider should be
instantiated on startup. instantiated on startup.
The same approach as above is also used for alert providers. The same approach as above is also used for alerts, and other types of providers. Give me a shout if you need any
advice.
+1 -1
View File
@@ -48,7 +48,7 @@ server {
} }
# SSE endpoints # SSE endpoints
location ~ ^/api/v1/(spots|alerts)/stream/? { location ~ ^/api/v2/(spots|alerts)/stream/? {
proxy_pass http://127.0.0.1:8080; proxy_pass http://127.0.0.1:8080;
# Remove buffering, remove caching, add suitable timeouts for SSE API calls # Remove buffering, remove caching, add suitable timeouts for SSE API calls
+2 -1
View File
@@ -47,7 +47,8 @@ python3 spothole.py
``` ```
The software can take a few seconds to start up, mostly because it is downloading an updated file to match callsigns to The software can take a few seconds to start up, mostly because it is downloading an updated file to match callsigns to
countries. This is normal, don't panic! countries. This is normal, don't panic! Once you see `You can access your copy of Spothole at
http://localhost:8080` in the log, your server is good to go.
If you see some errors on startup, check your configuration, e.g. in case you have specified a port for the web server If you see some errors on startup, check your configuration, e.g. in case you have specified a port for the web server
that is already in use by something else. that is already in use by something else.
+26 -21
View File
@@ -12,12 +12,12 @@ from tornado.web import Application
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
from core.constants import UNKNOWN_BAND from core.constants import UNKNOWN_BAND
from core.utils import infer_band_from_freq
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.sig_utils import get_ref_regex_for_sig from core.sig_utils import get_ref_regex_for_sig
from core.utils import infer_band_from_freq
from core.utils import safe_json_dumps from core.utils import safe_json_dumps
from data.spot import Spot from data.spot import Spot
from spotproviders.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify" RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
@@ -82,19 +82,24 @@ class APISpotHandler(tornado.web.RequestHandler):
upstream_credentials = handling.get("upstream_credentials", {}) upstream_credentials = handling.get("upstream_credentials", {})
captcha_token = handling.get("captcha_token", None) captcha_token = handling.get("captcha_token", None)
# Spothole v2.0 release only: deny upstream spotting. Spothole API breaking changes were in v2.0 but
# functionality is not ready yet. TODO
submit_upstream = False
# Verify CAPTCHA if required # Verify CAPTCHA if required
if RECAPTCHA_SECRET_KEY: if RECAPTCHA_SECRET_KEY:
if not captcha_token: if not captcha_token:
self.set_status(422) self.set_status(422)
self.write(json.dumps("Error - CAPTCHA token is required for spot submission.", self.write(safe_json_dumps("Error - CAPTCHA token is required for spot submission."))
default=serialize_everything))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
if not self._verify_recaptcha(captcha_token): if not self._verify_recaptcha(captcha_token):
self.set_status(422) self.set_status(422)
self.write(json.dumps("Error - CAPTCHA verification failed.", self.write(safe_json_dumps("Error - CAPTCHA verification failed."))
default=serialize_everything))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
@@ -105,7 +110,8 @@ class APISpotHandler(tornado.web.RequestHandler):
# Reject if no timestamp, frequency, dx_call or de_call # Reject if no timestamp, frequency, dx_call or de_call
if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call: if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call:
self.set_status(422) self.set_status(422)
self.write(safe_json_dumps("Error - 'time', 'dx_call', 'freq' and 'de_call' must be provided as a minimum.")) self.write(
safe_json_dumps("Error - 'time', 'dx_call', 'freq' and 'de_call' must be provided as a minimum."))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
@@ -127,7 +133,8 @@ class APISpotHandler(tornado.web.RequestHandler):
# Reject if frequency not in a known band # Reject if frequency not in a known band
if infer_band_from_freq(spot.freq) == UNKNOWN_BAND: if infer_band_from_freq(spot.freq) == UNKNOWN_BAND:
self.set_status(422) self.set_status(422)
self.write(safe_json_dumps("Error - Frequency of " + str(spot.freq / 1000.0) + "kHz is not in a known band.")) self.write(
safe_json_dumps("Error - Frequency of " + str(spot.freq / 1000.0) + "kHz is not in a known band."))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
@@ -137,7 +144,8 @@ class APISpotHandler(tornado.web.RequestHandler):
r"^([A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}|[A-R]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2})$", r"^([A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}|[A-R]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2})$",
spot.dx_grid.upper()): spot.dx_grid.upper()):
self.set_status(422) self.set_status(422)
self.write(safe_json_dumps("Error - '" + spot.dx_grid + "' does not look like a valid Maidenhead grid.")) self.write(
safe_json_dumps("Error - '" + spot.dx_grid + "' does not look like a valid Maidenhead grid."))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
@@ -155,8 +163,7 @@ class APISpotHandler(tornado.web.RequestHandler):
# Reject upstream submission if not permitted # Reject upstream submission if not permitted
if submit_upstream and not ALLOW_UPSTREAM_SPOTTING: if submit_upstream and not ALLOW_UPSTREAM_SPOTTING:
self.set_status(403) self.set_status(403)
self.write(json.dumps("Error - this server does not allow upstream spot submission.", self.write(safe_json_dumps("Error - this server does not allow upstream spot submission."))
default=serialize_everything))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
@@ -165,29 +172,26 @@ class APISpotHandler(tornado.web.RequestHandler):
if submit_upstream and upstream_provider_name: if submit_upstream and upstream_provider_name:
if not spot.sig: if not spot.sig:
self.set_status(422) self.set_status(422)
self.write(json.dumps("Error - a SIG must be selected to submit upstream.", self.write(safe_json_dumps("Error - a SIG must be selected to submit upstream."))
default=serialize_everything))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
if not spot.sig_refs and upstream_provider_name != "Tiles": if not spot.sig_refs and upstream_provider_name != "Tiles":
self.set_status(422) self.set_status(422)
self.write(json.dumps("Error - a SIG reference is required to submit upstream.", self.write(safe_json_dumps("Error - a SIG reference is required to submit upstream."))
default=serialize_everything))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
if not spot.dx_grid and upstream_provider_name == "Tiles": if not spot.dx_grid and upstream_provider_name == "Tiles":
self.set_status(422) self.set_status(422)
self.write(json.dumps("Error - a grid reference is required to submit upstream to Tiles on the Air.", self.write(
default=serialize_everything)) safe_json_dumps("Error - a grid reference is required to submit upstream to Tiles on the Air."))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
if not spot.mode and upstream_provider_name == "Tiles": if not spot.mode and upstream_provider_name == "Tiles":
self.set_status(422) self.set_status(422)
self.write(json.dumps("Error - a mode is required to submit upstream to Tiles on the Air.", self.write(safe_json_dumps("Error - a mode is required to submit upstream to Tiles on the Air."))
default=serialize_everything))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return return
@@ -209,7 +213,8 @@ class APISpotHandler(tornado.web.RequestHandler):
upstream_warning = "Spot was saved locally but upstream submission to " + upstream_provider_name + " failed: " + str( upstream_warning = "Spot was saved locally but upstream submission to " + upstream_provider_name + " failed: " + str(
e) e)
else: else:
upstream_warning = "No enabled provider named '" + upstream_provider_name + "' supports upstream submission for " + spot.sig + " spots." upstream_warning = "No enabled provider named '" + upstream_provider_name + "' supports upstream submission for " + (
spot.sig if spot.sig else "") + " spots."
# If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a # If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or # duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
@@ -219,7 +224,7 @@ class APISpotHandler(tornado.web.RequestHandler):
self._spots.set(spot.id, spot) self._spots.set(spot.id, spot)
if upstream_warning: if upstream_warning:
self.write(json.dumps("Warning - " + upstream_warning, default=serialize_everything)) self.write(safe_json_dumps("Warning - " + upstream_warning))
self.set_status(201) self.set_status(201)
else: else:
self.write(safe_json_dumps("OK")) self.write(safe_json_dumps("OK"))
+8 -6
View File
@@ -37,12 +37,14 @@ class APIOptionsHandler(tornado.web.RequestHandler):
# Build a map of SIG name -> list of provider names that can submit spots for that SIG # Build a map of SIG name -> list of provider names that can submit spots for that SIG
spot_submit_providers = {} spot_submit_providers = {}
for provider in self._spot_providers:
if not provider.enabled: # Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
continue # for provider in self._spot_providers:
for sig in SIGS: # if not provider.enabled:
if provider.can_submit_spot(sig.name): # continue
spot_submit_providers.setdefault(sig.name, []).append(provider.name) # for sig in SIGS:
# if provider.can_submit_spot(sig.name):
# spot_submit_providers.setdefault(sig.name, []).append(provider.name)
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle # 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. # things that aren't even available.
+3 -5
View File
@@ -2,7 +2,7 @@ import json
import tornado import tornado
from core.utils import serialize_everything from core.utils import safe_json_dumps
class V1GoneHandler(tornado.web.RequestHandler): class V1GoneHandler(tornado.web.RequestHandler):
@@ -11,10 +11,8 @@ class V1GoneHandler(tornado.web.RequestHandler):
def post(self): def post(self):
self.set_status(410) self.set_status(410)
self.write(json.dumps( self.write(safe_json_dumps(
"This API endpoint has a breaking change or has been removed in the current version of the Spothole API. Please see /apidocs for details of the current API version and the endpoints available.", "This API endpoint has a breaking change or has been removed in the current version of the Spothole API. Please see /apidocs for details of the current API version and the endpoints available."))
default=serialize_everything
))
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
+6 -4
View File
@@ -1,15 +1,14 @@
import asyncio import asyncio
import logging import logging
import os import os
import threading
import tornado import tornado
from tornado.web import StaticFileHandler from tornado.web import StaticFileHandler
from core.config import ALLOW_SPOTTING, WEB_SERVER_PORT, API_ONLY_MODE, LOG_WEB_REQUESTS, BASE_URL from core.config import ALLOW_SPOTTING, WEB_SERVER_PORT, API_ONLY_MODE, LOG_WEB_REQUESTS, BASE_URL
from core.data_providers import DATA_PROVIDERS
from core.data_store import DATA_STORE from core.data_store import DATA_STORE
from server.handlers.api.addspot import APISpotHandler from server.handlers.api.addspot import APISpotHandler
from server.handlers.api.v1_compatability import V1RedirectHandler, V1GoneHandler
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
from server.handlers.api.dxstats import APIDxStatsHandler from server.handlers.api.dxstats import APIDxStatsHandler
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
@@ -17,6 +16,7 @@ from server.handlers.api.options import APIOptionsHandler
from server.handlers.api.solar_conditions import APISolarConditionsHandler from server.handlers.api.solar_conditions import APISolarConditionsHandler
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
from server.handlers.api.status import APIStatusHandler from server.handlers.api.status import APIStatusHandler
from server.handlers.api.v1_compatability import V1RedirectHandler, V1GoneHandler
from server.handlers.manifesthandler import ManifestHandler from server.handlers.manifesthandler import ManifestHandler
from server.handlers.metrics import PrometheusMetricsHandler from server.handlers.metrics import PrometheusMetricsHandler
from server.handlers.pagetemplate import PageTemplateHandler from server.handlers.pagetemplate import PageTemplateHandler
@@ -32,6 +32,7 @@ class WebServer:
"""Constructor""" """Constructor"""
self._data_store = DATA_STORE self._data_store = DATA_STORE
self._data_providers = DATA_PROVIDERS
self._spot_broadcaster = SSEBroadcaster() self._spot_broadcaster = SSEBroadcaster()
self._alert_broadcaster = SSEBroadcaster() self._alert_broadcaster = SSEBroadcaster()
self._port = WEB_SERVER_PORT self._port = WEB_SERVER_PORT
@@ -87,11 +88,12 @@ class WebServer:
(r"/api/v2/lookup/call", APILookupCallHandler, {**handler_opts}), (r"/api/v2/lookup/call", APILookupCallHandler, {**handler_opts}),
(r"/api/v2/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}), (r"/api/v2/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}),
(r"/api/v2/lookup/grid", APILookupGridHandler, {**handler_opts}), (r"/api/v2/lookup/grid", APILookupGridHandler, {**handler_opts}),
(r"/api/v2/spot", APISpotHandler, {"spots": self._data_store.spots, **handler_opts}), (r"/api/v2/spot", APISpotHandler,
{"spots": self._data_store.spots, "spot_providers": self._data_providers, **handler_opts}),
] ]
# v1 API redirects. Most v1 enpoints are unchanged in v2, and get an HTTP 308 redirect to the v2 API. The ones # v1 API redirects. Most v1 enpoints are unchanged in v2, and get an HTTP 308 redirect to the v2 API. The ones
# that have the actual breaking changes get a bespoke handler. # that have the major breaking changes get a bespoke handler.
v1_compat_routes = [ v1_compat_routes = [
(r"/api/v1/spot", V1GoneHandler), (r"/api/v1/spot", V1GoneHandler),
(r"/api/v1/(.*)", V1RedirectHandler), (r"/api/v1/(.*)", V1RedirectHandler),
+1
View File
@@ -27,6 +27,7 @@ function loadSpots() {
updateBands(); updateBands();
// Start the ongoing SSE connection // Start the ongoing SSE connection
startSSEConnection(); startSSEConnection();
}
}); });
} }
+2 -2
View File
@@ -55,7 +55,7 @@ function loadStatus() {
<div class="col"><strong>${p["sig_name"]}</strong></div> <div class="col"><strong>${p["sig_name"]}</strong></div>
<div class="col">Status: ${p["status"]}</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">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"] > 0) ? p["reference_count"] : "N/A"}</div> <div class="col">References: ${p["enabled"] ? p["reference_count"] : "N/A"}</div>
</div>`); </div>`);
}); });
@@ -65,7 +65,7 @@ function loadStatus() {
<div class="col"><strong>${p["name"]}</strong></div> <div class="col"><strong>${p["name"]}</strong></div>
<div class="col">Status: ${p["status"]}</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">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
<div class="col">Lookups: ${(p["enabled"] && p["lookup_count"] > 0) ? p["lookup_count"] : "N/A"}</div> <div class="col">Lookups: ${p["enabled"] ? p["lookup_count"] : "N/A"}</div>
</div>`); </div>`);
}); });
}); });
+1 -1
View File
@@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div id="add-spot-intro-box" class="permanently-dismissible-box mt-3"> <div id="add-spot-intro-box" class="permanently-dismissible-box mt-3">
<div class="alert alert-primary alert-dismissible fade show" role="alert"> <div class="alert alert-primary alert-dismissible fade show" role="alert"> <!-- TODO Remove when feature available -->
<i class="fa-solid fa-circle-info"></i> <strong>Adding spots to Spothole</strong><br/>This page is implemented <i class="fa-solid fa-circle-info"></i> <strong>Adding spots to Spothole</strong><br/>This page is implemented
as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only as a proof of concept for adding spots to the Spothole system. Currently, spots added in this way are only
visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality visible within Spothole and are not sent "upstream" to DX clusters or xOTA spotting sites. The functionality