diff --git a/core/constants.py b/core/constants.py index 3d6a47c..ae8a00a 100644 --- a/core/constants.py +++ b/core/constants.py @@ -3,12 +3,11 @@ from data.band import Band from data.sig import SIG # General software -SOFTWARE_NAME = "Spothole by M0TRT" SOFTWARE_VERSION = "1.4-pre" # HTTP headers used for spot providers that use HTTP -HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"} -HAMQTH_PRG = (SOFTWARE_NAME + " v" + SOFTWARE_VERSION + " operated by " + SERVER_OWNER_CALLSIGN).replace(" ", "_") +HTTP_HEADERS = {"User-Agent": "Spothole v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"} +HAMQTH_PRG = ("Spothole v" + SOFTWARE_VERSION + " operated by " + SERVER_OWNER_CALLSIGN).replace(" ", "_") # Special Interest Groups SIGS = [ diff --git a/server/handlers/pagetemplate.py b/server/handlers/pagetemplate.py index 69150b0..30898d9 100644 --- a/server/handlers/pagetemplate.py +++ b/server/handlers/pagetemplate.py @@ -11,12 +11,9 @@ from core.prometheus_metrics_handler import page_requests_counter class PageTemplateHandler(tornado.web.RequestHandler): """Handler for all HTML pages generated from templates""" - def initialize(self, template_name, web_server_metrics, has_hamqsl=False, has_noaa_forecast=False, has_giro_ionosonde=False): + def initialize(self, template_name, web_server_metrics): self._template_name = template_name self._web_server_metrics = web_server_metrics - self._has_hamqsl = has_hamqsl - self._has_noaa_forecast = has_noaa_forecast - self._has_giro_ionosonde = has_giro_ionosonde def get(self): # Metrics @@ -28,6 +25,4 @@ class PageTemplateHandler(tornado.web.RequestHandler): # Load named template, and provide variables used in templates self.render(self._template_name + ".html", software_version=SOFTWARE_VERSION, server_owner_callsign=SERVER_OWNER_CALLSIGN, allow_spotting=ALLOW_SPOTTING, - web_ui_options=WEB_UI_OPTIONS, baseurl=BASE_URL, current_path=self.request.path, - has_hamqsl=self._has_hamqsl, has_noaa_forecast=self._has_noaa_forecast, - has_giro_ionosonde=self._has_giro_ionosonde) \ No newline at end of file + web_ui_options=WEB_UI_OPTIONS, baseurl=BASE_URL, current_path=self.request.path) \ No newline at end of file diff --git a/server/webserver.py b/server/webserver.py index 50faa1c..d204fb6 100644 --- a/server/webserver.py +++ b/server/webserver.py @@ -5,12 +5,11 @@ import os import tornado from tornado.web import StaticFileHandler -from core.config import SERVER_OWNER_CALLSIGN, ALLOW_SPOTTING -from core.constants import SOFTWARE_VERSION +from core.config import ALLOW_SPOTTING, WEB_SERVER_PORT, API_ONLY_MODE from core.utils import empty_queue from server.handlers.api.addspot import APISpotHandler -from server.handlers.api.dxstats import APIDxStatsHandler from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler +from server.handlers.api.dxstats import APIDxStatsHandler from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler from server.handlers.api.options import APIOptionsHandler from server.handlers.api.solar_conditions import APISolarConditionsHandler @@ -23,7 +22,7 @@ from server.handlers.pagetemplate import PageTemplateHandler class WebServer: """Provides the public-facing web server.""" - def __init__(self, spots, alerts, solar_conditions, status_data, solar_condition_providers, port, api_only_mode=False): + def __init__(self, spots, alerts, solar_conditions, status_data): """Constructor""" self._spots = spots @@ -32,9 +31,8 @@ class WebServer: self._sse_spot_queues = [] self._sse_alert_queues = [] self._status_data = status_data - self._solar_condition_providers = solar_condition_providers - self._port = port - self._api_only_mode = api_only_mode + self._port = WEB_SERVER_PORT + self._api_only_mode = API_ONLY_MODE self._shutdown_event = asyncio.Event() self.web_server_metrics = { "last_page_access_time": None, @@ -57,66 +55,60 @@ class WebServer: async def _start_inner(self): """Start method (async). Sets up the Tornado application.""" - provider_classes = [type(p).__name__ for p in self._solar_condition_providers if p.enabled] - has_hamqsl = "HamQSL" in provider_classes - has_noaa_forecast = "NOAA3dayForecast" in provider_classes - has_giro_ionosonde = "GIROIonosonde" in provider_classes or "KC2GProp" in provider_classes - page_opts = {"web_server_metrics": self.web_server_metrics, "has_hamqsl": has_hamqsl, - "has_noaa_forecast": has_noaa_forecast, "has_giro_ionosonde": has_giro_ionosonde} + # Prepare a list of common arguments that are passed in to every API & page handler. This is just a basic thing + # to avoid copy-pasting the same thing to every route declaration below. + handler_opts = {"web_server_metrics": self.web_server_metrics} # API endpoints are always enabled api_routes = [ - (r"/api/v1/spots", APISpotsHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}), - (r"/api/v1/alerts", APIAlertsHandler, - {"alerts": self._alerts, "web_server_metrics": self.web_server_metrics}), + (r"/api/v1/spots", APISpotsHandler, {"spots": self._spots, **handler_opts}), + (r"/api/v1/alerts", APIAlertsHandler, {"alerts": self._alerts, **handler_opts}), (r"/api/v1/spots/stream", APISpotsStreamHandler, - {"sse_spot_queues": self._sse_spot_queues, "web_server_metrics": self.web_server_metrics}), + {"sse_spot_queues": self._sse_spot_queues, **handler_opts}), (r"/api/v1/alerts/stream", APIAlertsStreamHandler, - {"sse_alert_queues": self._sse_alert_queues, "web_server_metrics": self.web_server_metrics}), - (r"/api/v1/solar", APISolarConditionsHandler, - {"solar_conditions": self._solar_conditions, "web_server_metrics": self.web_server_metrics}), - (r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}), - (r"/api/v1/options", APIOptionsHandler, - {"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}), - (r"/api/v1/status", APIStatusHandler, - {"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}), - (r"/api/v1/lookup/call", APILookupCallHandler, {"web_server_metrics": self.web_server_metrics}), - (r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {"web_server_metrics": self.web_server_metrics}), - (r"/api/v1/lookup/grid", APILookupGridHandler, {"web_server_metrics": self.web_server_metrics}), - (r"/api/v1/spot", APISpotHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}), + {"sse_alert_queues": self._sse_alert_queues, **handler_opts}), + (r"/api/v1/solar", APISolarConditionsHandler, {"solar_conditions": self._solar_conditions, **handler_opts}), + (r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._spots, **handler_opts}), + (r"/api/v1/options", APIOptionsHandler, {"status_data": self._status_data, **handler_opts}), + (r"/api/v1/status", APIStatusHandler, {"status_data": self._status_data, **handler_opts}), + (r"/api/v1/lookup/call", APILookupCallHandler, {**handler_opts}), + (r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}), + (r"/api/v1/lookup/grid", APILookupGridHandler, {**handler_opts}), + (r"/api/v1/spot", APISpotHandler, {"spots": self._spots, **handler_opts}), ] # If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes if self._api_only_mode: logging.info("API-only mode is enabled. Web UI will not be served.") ui_routes = [ - (r"/", PageTemplateHandler, {"template_name": "api_only_home", **page_opts}) + (r"/", PageTemplateHandler, {"template_name": "api_only_home", **handler_opts}) ] else: ui_routes = [ - (r"/", PageTemplateHandler, {"template_name": "spots", **page_opts}), - (r"/map", PageTemplateHandler, {"template_name": "map", **page_opts}), - (r"/bands", PageTemplateHandler, {"template_name": "bands", **page_opts}), - (r"/alerts", PageTemplateHandler, {"template_name": "alerts", **page_opts}), - (r"/conditions", PageTemplateHandler, {"template_name": "conditions", **page_opts}), - (r"/status", PageTemplateHandler, {"template_name": "status", **page_opts}), - (r"/about", PageTemplateHandler, {"template_name": "about", **page_opts}) + (r"/", PageTemplateHandler, {"template_name": "spots", **handler_opts}), + (r"/map", PageTemplateHandler, {"template_name": "map", **handler_opts}), + (r"/bands", PageTemplateHandler, {"template_name": "bands", **handler_opts}), + (r"/alerts", PageTemplateHandler, {"template_name": "alerts", **handler_opts}), + (r"/conditions", PageTemplateHandler, {"template_name": "conditions", **handler_opts}), + (r"/status", PageTemplateHandler, {"template_name": "status", **handler_opts}), + (r"/about", PageTemplateHandler, {"template_name": "about", **handler_opts}) ] # Only allow the Add Spot page if spotting is allowed if ALLOW_SPOTTING: - ui_routes += [(r"/add-spot", PageTemplateHandler, {"template_name": "add_spot", **page_opts})] + ui_routes += [(r"/add-spot", PageTemplateHandler, {"template_name": "add_spot", **handler_opts})] # API docs, Prometheus metrics, and finally static assets are always available regardless of API-only mode. misc_routes = [ - (r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **page_opts}), + (r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **handler_opts}), (r"/metrics", PrometheusMetricsHandler), (r"/(.*)", StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), "../webassets")}) ] app = tornado.web.Application(api_routes + ui_routes + misc_routes, - template_path=os.path.join(os.path.dirname(__file__), "../templates"), - debug=False) + template_path=os.path.join(os.path.dirname(__file__), "../templates"), + debug=False) app.listen(self._port) + logging.info("Web server running on port " + str(WEB_SERVER_PORT)) await self._shutdown_event.wait() def notify_new_spot(self, spot): diff --git a/spothole.py b/spothole.py index e839659..f5455be 100644 --- a/spothole.py +++ b/spothole.py @@ -9,8 +9,8 @@ from diskcache import Cache from core.cleanup import CleanupTimer from data.solar_conditions import SolarConditions -from core.config import config, WEB_SERVER_PORT, SERVER_OWNER_CALLSIGN, API_ONLY_MODE -from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION +from core.config import config, SERVER_OWNER_CALLSIGN +from core.constants import SOFTWARE_VERSION from core.lookup_helper import lookup_helper from core.status_reporter import StatusReporter from server.webserver import WebServer @@ -90,7 +90,7 @@ if __name__ == '__main__': logging.info("Starting...") logging.info( - "This is " + SOFTWARE_NAME + " version " + SOFTWARE_VERSION + ". This instance is run by " + SERVER_OWNER_CALLSIGN + ".") + "This is Spothole version " + SOFTWARE_VERSION + ". This instance is run by " + SERVER_OWNER_CALLSIGN + ".") # Shut down gracefully on SIGINT signal.signal(signal.SIGINT, shutdown) @@ -99,9 +99,7 @@ if __name__ == '__main__': lookup_helper.start() # Set up web server - web_server = WebServer(spots=spots, alerts=alerts, solar_conditions=solar_conditions, status_data=status_data, - solar_condition_providers=solar_condition_providers, port=WEB_SERVER_PORT, - api_only_mode=API_ONLY_MODE) + web_server = WebServer(spots=spots, alerts=alerts, solar_conditions=solar_conditions, status_data=status_data) # Fetch, set up and start spot providers for entry in config["spot-providers"]: diff --git a/templates/about.html b/templates/about.html index bfe8993..6f60b8f 100644 --- a/templates/about.html +++ b/templates/about.html @@ -69,7 +69,7 @@

This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.

- + {% end %} \ No newline at end of file diff --git a/templates/add_spot.html b/templates/add_spot.html index 9d31a2a..f3752b5 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -69,8 +69,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/alerts.html b/templates/alerts.html index 0634f06..7175443 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -70,8 +70,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/bands.html b/templates/bands.html index 80dea96..ae5986d 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,9 +76,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 1537f70..da1f35c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -19,9 +19,9 @@ integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn" crossorigin="anonymous"> - - - + + + {% end %} {% block body %}
@@ -42,9 +42,7 @@ {% if allow_spotting %} {% end %} - {% if has_hamqsl or has_noaa_forecast %} - {% end %} diff --git a/templates/conditions.html b/templates/conditions.html index b924c46..891b7c5 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -1,218 +1,231 @@ {% extends "base.html" %} {% block content %} -{% if has_hamqsl %} -
-
- Propagation Conditions + +
+ + - -
-
- Solar Weather -
-
-
-
Solar Flux
-
- SFI: - Sunspots: + +
Data from the Lowell GIRO Data Center and/or + KC2G.
-
-
-
-
Geomagnetic
-
- K: - A: - G - Noise: -
-
- . -
-
-
-
X-ray Flux
-
- - R
-
-
-
-
Proton Flux
-
- pfu - S -
-
-
-
-
Electron Flux
-
efu
-
-
-
Data from HamQSL.com.
-
-
-{% end %} - -{% if has_noaa_forecast %} -
-
- Solar Weather Forecast -
-
-
-
-
K-index Forecast
- -
-
-
-
-
Solar Storm Forecast
- - - - - -
-
-
-
Radio Blackout Forecast
- - - - - -
-
-
-
-{% end %} - -{% if has_giro_ionosonde %} -
-
- Ionosonde Data -
-
-
- - -
-
-
No data available for this station.
-
-
-
Latest values as of
-
-
-
LUF:
-
foF2:
-
MUF (3000 km):
-
-
Data is more than 12 hours old!
-
-
-
- - - -
- - -
-
- -
Data from the Lowell GIRO Data Center and/or KC2G.
-
-
-{% end %}
@@ -271,8 +284,8 @@
- - + + diff --git a/templates/map.html b/templates/map.html index d9a9353..7fc8628 100644 --- a/templates/map.html +++ b/templates/map.html @@ -94,9 +94,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/spots.html b/templates/spots.html index 4f3aedf..04e65c8 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -104,9 +104,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index 811415b..eb14006 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,8 +59,8 @@
- - + + diff --git a/webassets/js/conditions.js b/webassets/js/conditions.js index 194697e..c81ea54 100644 --- a/webassets/js/conditions.js +++ b/webassets/js/conditions.js @@ -17,6 +17,7 @@ function loadSolarConditions() { const hfConditionClass = {'Good': 'bg-success-subtle', 'Fair': 'bg-warning-subtle', 'Poor': 'bg-danger-subtle'}; if (jsonData.hf_conditions) { + $('#hamqsl-section').show(); Object.entries(jsonData.hf_conditions).forEach(function ([key, condition]) { const cell = $('#hf-conditions-' + key); cell.text(condition); @@ -116,6 +117,7 @@ function loadSolarConditions() { // Ionosonde if (jsonData.ionosonde_data && Object.keys(jsonData.ionosonde_data).length > 0) { + $('#ionosonde-section').show(); ionosondeData = jsonData.ionosonde_data; populateIonosondeDropdown(ionosondeData); renderIonosondeData(); @@ -123,6 +125,9 @@ function loadSolarConditions() { // Forecast + if (jsonData.k_index_forecast) { + $('#noaa-section').show(); + } renderKIndexForecast(jsonData.k_index_forecast); renderSolarStormForecast(jsonData.solar_storm_forecast); renderBlackoutForecast(jsonData.blackout_forecast_r1r2, jsonData.blackout_forecast_r3_or_greater);