General refactor to remove stuff being passed around the Tornado handlers when it doesn't need to be. Conditions provider presence is how handled on the JS side without needing special booleans sent to every page handler.

This commit is contained in:
Ian Renton
2026-06-09 11:06:48 +01:00
parent cd30fc765b
commit b725c34f7c
14 changed files with 281 additions and 281 deletions

View File

@@ -3,12 +3,11 @@ from data.band import Band
from data.sig import SIG from data.sig import SIG
# General software # General software
SOFTWARE_NAME = "Spothole by M0TRT"
SOFTWARE_VERSION = "1.4-pre" SOFTWARE_VERSION = "1.4-pre"
# HTTP headers used for spot providers that use HTTP # HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = {"User-Agent": SOFTWARE_NAME + ", v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"} HTTP_HEADERS = {"User-Agent": "Spothole v" + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")"}
HAMQTH_PRG = (SOFTWARE_NAME + " v" + SOFTWARE_VERSION + " operated by " + SERVER_OWNER_CALLSIGN).replace(" ", "_") HAMQTH_PRG = ("Spothole v" + SOFTWARE_VERSION + " operated by " + SERVER_OWNER_CALLSIGN).replace(" ", "_")
# Special Interest Groups # Special Interest Groups
SIGS = [ SIGS = [

View File

@@ -11,12 +11,9 @@ from core.prometheus_metrics_handler import page_requests_counter
class PageTemplateHandler(tornado.web.RequestHandler): class PageTemplateHandler(tornado.web.RequestHandler):
"""Handler for all HTML pages generated from templates""" """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._template_name = template_name
self._web_server_metrics = web_server_metrics 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): def get(self):
# Metrics # Metrics
@@ -28,6 +25,4 @@ class PageTemplateHandler(tornado.web.RequestHandler):
# Load named template, and provide variables used in templates # Load named template, and provide variables used in templates
self.render(self._template_name + ".html", software_version=SOFTWARE_VERSION, self.render(self._template_name + ".html", software_version=SOFTWARE_VERSION,
server_owner_callsign=SERVER_OWNER_CALLSIGN, allow_spotting=ALLOW_SPOTTING, server_owner_callsign=SERVER_OWNER_CALLSIGN, allow_spotting=ALLOW_SPOTTING,
web_ui_options=WEB_UI_OPTIONS, baseurl=BASE_URL, current_path=self.request.path, 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)

View File

@@ -5,12 +5,11 @@ import os
import tornado import tornado
from tornado.web import StaticFileHandler from tornado.web import StaticFileHandler
from core.config import SERVER_OWNER_CALLSIGN, ALLOW_SPOTTING from core.config import ALLOW_SPOTTING, WEB_SERVER_PORT, API_ONLY_MODE
from core.constants import SOFTWARE_VERSION
from core.utils import empty_queue from core.utils import empty_queue
from server.handlers.api.addspot import APISpotHandler 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.alerts import APIAlertsHandler, APIAlertsStreamHandler
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
from server.handlers.api.options import APIOptionsHandler from server.handlers.api.options import APIOptionsHandler
from server.handlers.api.solar_conditions import APISolarConditionsHandler from server.handlers.api.solar_conditions import APISolarConditionsHandler
@@ -23,7 +22,7 @@ from server.handlers.pagetemplate import PageTemplateHandler
class WebServer: class WebServer:
"""Provides the public-facing web server.""" """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""" """Constructor"""
self._spots = spots self._spots = spots
@@ -32,9 +31,8 @@ class WebServer:
self._sse_spot_queues = [] self._sse_spot_queues = []
self._sse_alert_queues = [] self._sse_alert_queues = []
self._status_data = status_data self._status_data = status_data
self._solar_condition_providers = solar_condition_providers self._port = WEB_SERVER_PORT
self._port = port self._api_only_mode = API_ONLY_MODE
self._api_only_mode = api_only_mode
self._shutdown_event = asyncio.Event() self._shutdown_event = asyncio.Event()
self.web_server_metrics = { self.web_server_metrics = {
"last_page_access_time": None, "last_page_access_time": None,
@@ -57,66 +55,60 @@ class WebServer:
async def _start_inner(self): async def _start_inner(self):
"""Start method (async). Sets up the Tornado application.""" """Start method (async). Sets up the Tornado application."""
provider_classes = [type(p).__name__ for p in self._solar_condition_providers if p.enabled] # Prepare a list of common arguments that are passed in to every API & page handler. This is just a basic thing
has_hamqsl = "HamQSL" in provider_classes # to avoid copy-pasting the same thing to every route declaration below.
has_noaa_forecast = "NOAA3dayForecast" in provider_classes handler_opts = {"web_server_metrics": self.web_server_metrics}
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}
# API endpoints are always enabled # API endpoints are always enabled
api_routes = [ api_routes = [
(r"/api/v1/spots", APISpotsHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}), (r"/api/v1/spots", APISpotsHandler, {"spots": self._spots, **handler_opts}),
(r"/api/v1/alerts", APIAlertsHandler, (r"/api/v1/alerts", APIAlertsHandler, {"alerts": self._alerts, **handler_opts}),
{"alerts": self._alerts, "web_server_metrics": self.web_server_metrics}),
(r"/api/v1/spots/stream", APISpotsStreamHandler, (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, (r"/api/v1/alerts/stream", APIAlertsStreamHandler,
{"sse_alert_queues": self._sse_alert_queues, "web_server_metrics": self.web_server_metrics}), {"sse_alert_queues": self._sse_alert_queues, **handler_opts}),
(r"/api/v1/solar", APISolarConditionsHandler, (r"/api/v1/solar", APISolarConditionsHandler, {"solar_conditions": self._solar_conditions, **handler_opts}),
{"solar_conditions": self._solar_conditions, "web_server_metrics": self.web_server_metrics}), (r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._spots, **handler_opts}),
(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, **handler_opts}),
(r"/api/v1/options", APIOptionsHandler, (r"/api/v1/status", APIStatusHandler, {"status_data": self._status_data, **handler_opts}),
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}), (r"/api/v1/lookup/call", APILookupCallHandler, {**handler_opts}),
(r"/api/v1/status", APIStatusHandler, (r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {**handler_opts}),
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}), (r"/api/v1/lookup/grid", APILookupGridHandler, {**handler_opts}),
(r"/api/v1/lookup/call", APILookupCallHandler, {"web_server_metrics": self.web_server_metrics}), (r"/api/v1/spot", APISpotHandler, {"spots": self._spots, **handler_opts}),
(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}),
] ]
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes # If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
if self._api_only_mode: if self._api_only_mode:
logging.info("API-only mode is enabled. Web UI will not be served.") logging.info("API-only mode is enabled. Web UI will not be served.")
ui_routes = [ ui_routes = [
(r"/", PageTemplateHandler, {"template_name": "api_only_home", **page_opts}) (r"/", PageTemplateHandler, {"template_name": "api_only_home", **handler_opts})
] ]
else: else:
ui_routes = [ ui_routes = [
(r"/", PageTemplateHandler, {"template_name": "spots", **page_opts}), (r"/", PageTemplateHandler, {"template_name": "spots", **handler_opts}),
(r"/map", PageTemplateHandler, {"template_name": "map", **page_opts}), (r"/map", PageTemplateHandler, {"template_name": "map", **handler_opts}),
(r"/bands", PageTemplateHandler, {"template_name": "bands", **page_opts}), (r"/bands", PageTemplateHandler, {"template_name": "bands", **handler_opts}),
(r"/alerts", PageTemplateHandler, {"template_name": "alerts", **page_opts}), (r"/alerts", PageTemplateHandler, {"template_name": "alerts", **handler_opts}),
(r"/conditions", PageTemplateHandler, {"template_name": "conditions", **page_opts}), (r"/conditions", PageTemplateHandler, {"template_name": "conditions", **handler_opts}),
(r"/status", PageTemplateHandler, {"template_name": "status", **page_opts}), (r"/status", PageTemplateHandler, {"template_name": "status", **handler_opts}),
(r"/about", PageTemplateHandler, {"template_name": "about", **page_opts}) (r"/about", PageTemplateHandler, {"template_name": "about", **handler_opts})
] ]
# Only allow the Add Spot page if spotting is allowed # Only allow the Add Spot page if spotting is allowed
if ALLOW_SPOTTING: 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. # API docs, Prometheus metrics, and finally static assets are always available regardless of API-only mode.
misc_routes = [ misc_routes = [
(r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **page_opts}), (r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **handler_opts}),
(r"/metrics", PrometheusMetricsHandler), (r"/metrics", PrometheusMetricsHandler),
(r"/(.*)", StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), "../webassets")}) (r"/(.*)", StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), "../webassets")})
] ]
app = tornado.web.Application(api_routes + ui_routes + misc_routes, app = tornado.web.Application(api_routes + ui_routes + misc_routes,
template_path=os.path.join(os.path.dirname(__file__), "../templates"), template_path=os.path.join(os.path.dirname(__file__), "../templates"),
debug=False) debug=False)
app.listen(self._port) app.listen(self._port)
logging.info("Web server running on port " + str(WEB_SERVER_PORT))
await self._shutdown_event.wait() await self._shutdown_event.wait()
def notify_new_spot(self, spot): def notify_new_spot(self, spot):

View File

@@ -9,8 +9,8 @@ from diskcache import Cache
from core.cleanup import CleanupTimer from core.cleanup import CleanupTimer
from data.solar_conditions import SolarConditions from data.solar_conditions import SolarConditions
from core.config import config, WEB_SERVER_PORT, SERVER_OWNER_CALLSIGN, API_ONLY_MODE from core.config import config, SERVER_OWNER_CALLSIGN
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION from core.constants import SOFTWARE_VERSION
from core.lookup_helper import lookup_helper from core.lookup_helper import lookup_helper
from core.status_reporter import StatusReporter from core.status_reporter import StatusReporter
from server.webserver import WebServer from server.webserver import WebServer
@@ -90,7 +90,7 @@ if __name__ == '__main__':
logging.info("Starting...") logging.info("Starting...")
logging.info( 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 # Shut down gracefully on SIGINT
signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGINT, shutdown)
@@ -99,9 +99,7 @@ if __name__ == '__main__':
lookup_helper.start() lookup_helper.start()
# Set up web server # Set up web server
web_server = WebServer(spots=spots, alerts=alerts, solar_conditions=solar_conditions, status_data=status_data, 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)
# Fetch, set up and start spot providers # Fetch, set up and start spot providers
for entry in config["spot-providers"]: for entry in config["spot-providers"]:

View File

@@ -69,7 +69,7 @@
<p>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.</p> <p>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.</p>
</div> </div>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -69,8 +69,8 @@
</div> </div>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/add-spot.js?v=1780997896"></script> <script src="/js/add-spot.js?v=1780999608"></script>
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -70,8 +70,8 @@
</div> </div>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/alerts.js?v=1780997896"></script> <script src="/js/alerts.js?v=1780999608"></script>
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -76,9 +76,9 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/spotsbandsandmap.js?v=1780997896"></script> <script src="/js/spotsbandsandmap.js?v=1780999608"></script>
<script src="/js/bands.js?v=1780997896"></script> <script src="/js/bands.js?v=1780999608"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1780997896" type="text/css"> <link rel="stylesheet" href="/css/style.css?v=1780999608" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" /> <link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
@@ -19,9 +19,9 @@
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn" integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1780997896"></script> <script src="https://misc.ianrenton.com/jsutils/utils.js?v=1780999608"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780997896"></script> <script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1780999608"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780997896"></script> <script src="https://misc.ianrenton.com/jsutils/geo.js?v=1780999608"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
@@ -42,9 +42,7 @@
{% if allow_spotting %} {% if allow_spotting %}
<li class="nav-item ms-4"><a href="/add-spot" class="nav-link" id="nav-link-add-spot"><i class="fa-solid fa-comment"></i> Add&nbsp;Spot</a></li> <li class="nav-item ms-4"><a href="/add-spot" class="nav-link" id="nav-link-add-spot"><i class="fa-solid fa-comment"></i> Add&nbsp;Spot</a></li>
{% end %} {% end %}
{% if has_hamqsl or has_noaa_forecast %}
<li class="nav-item ms-4"><a href="/conditions" class="nav-link" id="nav-link-conditions"><i class="fa-solid fa-sun"></i> Conditions</a></li> <li class="nav-item ms-4"><a href="/conditions" class="nav-link" id="nav-link-conditions"><i class="fa-solid fa-sun"></i> Conditions</a></li>
{% end %}
<li class="nav-item ms-4"><a href="/status" class="nav-link" id="nav-link-status"><i class="fa-solid fa-chart-simple"></i> Status</a></li> <li class="nav-item ms-4"><a href="/status" class="nav-link" id="nav-link-status"><i class="fa-solid fa-chart-simple"></i> Status</a></li>
<li class="nav-item ms-4"><a href="/about" class="nav-link" id="nav-link-about"><i class="fa-solid fa-circle-info"></i> About</a></li> <li class="nav-item ms-4"><a href="/about" class="nav-link" id="nav-link-about"><i class="fa-solid fa-circle-info"></i> About</a></li>
<li class="nav-item ms-4"><a href="/apidocs" class="nav-link" id="nav-link-api"><i class="fa-solid fa-gear"></i> API</a></li> <li class="nav-item ms-4"><a href="/apidocs" class="nav-link" id="nav-link-api"><i class="fa-solid fa-gear"></i> API</a></li>

View File

@@ -1,218 +1,231 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
{% if has_hamqsl %} <div id="hamqsl-section" style="display:none">
<div class="card mt-5"> <div class="card mt-5">
<div class="card-header"> <div class="card-header">
Propagation Conditions Propagation Conditions
</div>
<div class="card-body">
<div class="row row-cols-1 row-cols-md-2 g-3">
<div class="col mt-3 px-3">
<h5>HF</h5>
<table class="table table-sm mt-2">
<thead>
<tr>
<th>Band</th>
<th>Day</th>
<th>Night</th>
</tr>
</thead>
<tbody>
<tr>
<td>80-40m</td>
<td id="hf-conditions-80m-40m-day"></td>
<td id="hf-conditions-80m-40m-night"></td>
</tr>
<tr>
<td>30-20m</td>
<td id="hf-conditions-30m-20m-day"></td>
<td id="hf-conditions-30m-20m-night"></td>
</tr>
<tr>
<td>17-15m</td>
<td id="hf-conditions-17m-15m-day"></td>
<td id="hf-conditions-17m-15m-night"></td>
</tr>
<tr>
<td>12-10m</td>
<td id="hf-conditions-12m-10m-day"></td>
<td id="hf-conditions-12m-10m-night"></td>
</tr>
</tbody>
</table>
</div>
<div class="col mt-3 px-3">
<h5>VHF</h5>
<table class="table table-sm mt-2">
<thead>
<tr>
<th>Propagation Mode</th>
<th>Condition</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sporadic-E 6m (Europe)</td>
<td id="vhf-conditions-es_6m_europe"></td>
</tr>
<tr>
<td>Sporadic-E 4m (Europe)</td>
<td id="vhf-conditions-es_4m_europe"></td>
</tr>
<tr>
<td>Sporadic-E 2m (Europe)</td>
<td id="vhf-conditions-es_2m_europe"></td>
</tr>
<tr>
<td>Sporadic-E 2m (North America)</td>
<td id="vhf-conditions-es_2m_na"></td>
</tr>
<tr>
<td>Aurora (Northern Hemisphere)</td>
<td id="vhf-conditions-vhf_aurora_northern_hemi"></td>
</tr>
<tr>
<td>Aurora Minimum Latitude</td>
<td id="vhf-conditions-aurora-lat"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
</div>
</div> </div>
<div class="card-body">
<div class="row row-cols-1 row-cols-md-2 g-3"> <div class="card mt-5">
<div class="col mt-3 px-3"> <div class="card-header">
<h5>HF</h5> Solar Weather
<table class="table table-sm mt-2"> </div>
<div class="card-body px-3">
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">Solar Flux</div>
<div id="sw-solar-flux-vals" class="col-12 col-md-3 py-2">
<span class="me-3">SFI: <strong id="sw-sfi"></strong></span>
<span>Sunspots: <strong id="sw-sunspots"></strong></span>
</div>
<div id="sw-solar-flux-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">Geomagnetic</div>
<div id="sw-geomag-vals" class="col-12 col-md-3 py-2">
<span class="me-3">K: <strong id="sw-k-index"></strong></span>
<span class="me-3">A: <strong id="sw-a-index"></strong></span>
<span class="me-3"><strong>G</strong><strong id="sw-geomag-storm-scale"></strong></span>
<span>Noise: <strong id="sw-geomag-noise"></strong></span>
</div>
<div id="sw-geomag-desc" class="col-12 col-md-7 py-2">
<span id="sw-geomag-field"></span>. <span id="sw-geomag-storm-desc"></span>
</div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">X-ray Flux</div>
<div id="sw-xray-vals" class="col-12 col-md-3 py-2">
<span class="me-3"><strong id="sw-xray"></strong></span>
<span class="me-3"><strong>R</strong><strong id="sw-radio-blackout-scale"></strong></span></div>
<div id="sw-xray-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">Proton Flux</div>
<div id="sw-proton-vals" class="col-12 col-md-3 py-2">
<span class="me-3"><strong id="sw-proton-flux"></strong> pfu</span>
<span class="me-3"><strong>S</strong><strong id="sw-solar-storm-scale"></strong></span>
</div>
<div id="sw-proton-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 fw-bold py-2">Electron Flux</div>
<div id="sw-electron-vals" class="col-12 col-md-3 py-2"><strong id="sw-electron-flux"></strong> efu
</div>
<div id="sw-electron-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
</div>
</div>
</div>
<div id="noaa-section" style="display:none">
<div class="card mt-5">
<div class="card-header">
Solar Weather Forecast
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col px-3">
<h5>K-index Forecast</h5>
<canvas id="forecast-kp-chart" class="mt-3 mb-3"></canvas>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 g-3">
<div class="col mt-3 px-3">
<h5>Solar Storm Forecast</h5>
<table id="forecast-solar-storm-table" class="table table-sm mt-2">
<thead>
<tr id="forecast-solar-storm-head"></tr>
</thead>
<tbody id="forecast-solar-storm-tbody"></tbody>
</table>
</div>
<div class="col mt-3 px-3">
<h5>Radio Blackout Forecast</h5>
<table id="forecast-blackout-table" class="table table-sm mt-2">
<thead>
<tr id="forecast-blackout-head"></tr>
</thead>
<tbody id="forecast-blackout-tbody"></tbody>
</table>
</div>
</div>
<div class="form-text mt-3">Data from <a href="https://www.swpc.noaa.gov/">NOAA Space Weather Prediction
Center</a>.
</div>
</div>
</div>
</div>
<div id="ionosonde-section" style="display:none">
<div class="card mt-5">
<div class="card-header">
Ionosonde Data
</div>
<div class="card-body">
<div class="mb-3">
<label for="ionosonde-station" class="form-label">Ionosonde station:</label>
<select id="ionosonde-station" class="form-select storeable-select d-inline-block ms-2 w-auto"
oninput="ionosondeStationChanged();">
</select>
</div>
<div id="ionosonde-latest" class="mb-3">
<div id="ionosonde-no-data" class="alert alert-warning mt-2 mb-0 py-2 js-hidden">No data available for
this station.
</div>
<div id="ionosonde-data-rows" class="js-hidden">
<div class="row align-items-center me-0">
<div class="col-12 py-2 text-muted">Latest values as of <span id="ionosonde-latest-time"></span>
</div>
</div>
<div class="row align-items-center me-0">
<div class="col-12 col-md-4 py-2">LUF: <strong id="ionosonde-latest-luf"></strong></div>
<div class="col-12 col-md-4 py-2">foF2: <strong id="ionosonde-latest-fof2"></strong></div>
<div class="col-12 col-md-4 py-2">MUF (3000 km): <strong id="ionosonde-latest-muf"></strong>
</div>
</div>
<div id="ionosonde-stale-warning" class="alert alert-warning mt-2 mb-0 py-2 js-hidden">Data is more
than 12 hours old!
</div>
</div>
</div>
<div id="ionosonde-band-state" class="mb-3 js-hidden">
<table class="table table-sm table-bordered mb-0 d-none d-md-table table-fixed-on-desktop">
<thead> <thead>
<tr> <tr id="ionosonde-band-state-head"></tr>
<th>Band</th>
<th>Day</th>
<th>Night</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr id="ionosonde-band-state-row"></tr>
<td>80-40m</td>
<td id="hf-conditions-80m-40m-day"></td>
<td id="hf-conditions-80m-40m-night"></td>
</tr>
<tr>
<td>30-20m</td>
<td id="hf-conditions-30m-20m-day"></td>
<td id="hf-conditions-30m-20m-night"></td>
</tr>
<tr>
<td>17-15m</td>
<td id="hf-conditions-17m-15m-day"></td>
<td id="hf-conditions-17m-15m-night"></td>
</tr>
<tr>
<td>12-10m</td>
<td id="hf-conditions-12m-10m-day"></td>
<td id="hf-conditions-12m-10m-night"></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> <table class="table table-sm table-bordered mb-0 d-md-none">
<div class="col mt-3 px-3"> <tbody id="ionosonde-band-state-body"></tbody>
<h5>VHF</h5>
<table class="table table-sm mt-2">
<thead>
<tr>
<th>Propagation Mode</th>
<th>Condition</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sporadic-E 6m (Europe)</td>
<td id="vhf-conditions-es_6m_europe"></td>
</tr>
<tr>
<td>Sporadic-E 4m (Europe)</td>
<td id="vhf-conditions-es_4m_europe"></td>
</tr>
<tr>
<td>Sporadic-E 2m (Europe)</td>
<td id="vhf-conditions-es_2m_europe"></td>
</tr>
<tr>
<td>Sporadic-E 2m (North America)</td>
<td id="vhf-conditions-es_2m_na"></td>
</tr>
<tr>
<td>Aurora (Northern Hemisphere)</td>
<td id="vhf-conditions-vhf_aurora_northern_hemi"></td>
</tr>
<tr>
<td>Aurora Minimum Latitude</td>
<td id="vhf-conditions-aurora-lat"></td>
</tr>
</tbody>
</table> </table>
</div> </div>
</div> <canvas id="ionosonde-chart" class="mt-3 mb-3 hideonmobile"></canvas>
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div> <div class="form-text mt-2">Data from the <a href="https://lgdc.uml.edu/">Lowell GIRO Data Center</a> and/or
</div> <a href="https://prop.kc2g.com/">KC2G</a>.
</div>
<div class="card mt-5">
<div class="card-header">
Solar Weather
</div>
<div class="card-body px-3">
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">Solar Flux</div>
<div id="sw-solar-flux-vals" class="col-12 col-md-3 py-2">
<span class="me-3">SFI: <strong id="sw-sfi"></strong></span>
<span>Sunspots: <strong id="sw-sunspots"></strong></span>
</div> </div>
<div id="sw-solar-flux-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">Geomagnetic</div>
<div id="sw-geomag-vals" class="col-12 col-md-3 py-2">
<span class="me-3">K: <strong id="sw-k-index"></strong></span>
<span class="me-3">A: <strong id="sw-a-index"></strong></span>
<span class="me-3"><strong>G</strong><strong id="sw-geomag-storm-scale"></strong></span>
<span>Noise: <strong id="sw-geomag-noise"></strong></span>
</div>
<div id="sw-geomag-desc" class="col-12 col-md-7 py-2">
<span id="sw-geomag-field"></span>. <span id="sw-geomag-storm-desc"></span>
</div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">X-ray Flux</div>
<div id="sw-xray-vals" class="col-12 col-md-3 py-2">
<span class="me-3"><strong id="sw-xray"></strong></span>
<span class="me-3"><strong>R</strong><strong id="sw-radio-blackout-scale"></strong></span></div>
<div id="sw-xray-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 py-2 fw-bold">Proton Flux</div>
<div id="sw-proton-vals" class="col-12 col-md-3 py-2">
<span class="me-3"><strong id="sw-proton-flux"></strong> pfu</span>
<span class="me-3"><strong>S</strong><strong id="sw-solar-storm-scale"></strong></span>
</div>
<div id="sw-proton-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="row border-bottom align-items-start me-0">
<div class="col-12 col-md-2 fw-bold py-2">Electron Flux</div>
<div id="sw-electron-vals" class="col-12 col-md-3 py-2"><strong id="sw-electron-flux"></strong> efu</div>
<div id="sw-electron-desc" class="col-12 col-md-7 py-2"></div>
</div>
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
</div>
</div>
{% end %}
{% if has_noaa_forecast %}
<div class="card mt-5">
<div class="card-header">
Solar Weather Forecast
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col px-3">
<h5>K-index Forecast</h5>
<canvas id="forecast-kp-chart" class="mt-3 mb-3"></canvas>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 g-3">
<div class="col mt-3 px-3">
<h5>Solar Storm Forecast</h5>
<table id="forecast-solar-storm-table" class="table table-sm mt-2">
<thead>
<tr id="forecast-solar-storm-head"></tr>
</thead>
<tbody id="forecast-solar-storm-tbody"></tbody>
</table>
</div>
<div class="col mt-3 px-3">
<h5>Radio Blackout Forecast</h5>
<table id="forecast-blackout-table" class="table table-sm mt-2">
<thead>
<tr id="forecast-blackout-head"></tr>
</thead>
<tbody id="forecast-blackout-tbody"></tbody>
</table>
</div>
</div>
<div class="form-text mt-3">Data from <a href="https://www.swpc.noaa.gov/">NOAA Space Weather Prediction
Center</a>.
</div> </div>
</div> </div>
</div> </div>
{% end %}
{% if has_giro_ionosonde %}
<div class="card mt-5">
<div class="card-header">
Ionosonde Data
</div>
<div class="card-body">
<div class="mb-3">
<label for="ionosonde-station" class="form-label">Ionosonde station:</label>
<select id="ionosonde-station" class="form-select storeable-select d-inline-block ms-2 w-auto"
oninput="ionosondeStationChanged();">
</select>
</div>
<div id="ionosonde-latest" class="mb-3">
<div id="ionosonde-no-data" class="alert alert-warning mt-2 mb-0 py-2 js-hidden">No data available for this station.</div>
<div id="ionosonde-data-rows" class="js-hidden">
<div class="row align-items-center me-0">
<div class="col-12 py-2 text-muted">Latest values as of <span id="ionosonde-latest-time"></span></div>
</div>
<div class="row align-items-center me-0">
<div class="col-12 col-md-4 py-2">LUF: <strong id="ionosonde-latest-luf"></strong></div>
<div class="col-12 col-md-4 py-2">foF2: <strong id="ionosonde-latest-fof2"></strong></div>
<div class="col-12 col-md-4 py-2">MUF (3000 km): <strong id="ionosonde-latest-muf"></strong></div>
</div>
<div id="ionosonde-stale-warning" class="alert alert-warning mt-2 mb-0 py-2 js-hidden">Data is more than 12 hours old!</div>
</div>
</div>
<div id="ionosonde-band-state" class="mb-3 js-hidden">
<table class="table table-sm table-bordered mb-0 d-none d-md-table table-fixed-on-desktop">
<thead><tr id="ionosonde-band-state-head"></tr></thead>
<tbody><tr id="ionosonde-band-state-row"></tr></tbody>
</table>
<table class="table table-sm table-bordered mb-0 d-md-none">
<tbody id="ionosonde-band-state-body"></tbody>
</table>
</div>
<canvas id="ionosonde-chart" class="mt-3 mb-3 hideonmobile"></canvas>
<div class="form-text mt-2">Data from the <a href="https://lgdc.uml.edu/">Lowell GIRO Data Center</a> and/or <a href="https://prop.kc2g.com/">KC2G</a>.</div>
</div>
</div>
{% end %}
<div class="card mt-5"> <div class="card mt-5">
<div class="card-header"> <div class="card-header">
@@ -271,8 +284,8 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/conditions.js?v=1780997896"></script> <script src="/js/conditions.js?v=1780999608"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>

View File

@@ -94,9 +94,9 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/spotsbandsandmap.js?v=1780997896"></script> <script src="/js/spotsbandsandmap.js?v=1780999608"></script>
<script src="/js/map.js?v=1780997896"></script> <script src="/js/map.js?v=1780999608"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -104,9 +104,9 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/spotsbandsandmap.js?v=1780997896"></script> <script src="/js/spotsbandsandmap.js?v=1780999608"></script>
<script src="/js/spots.js?v=1780997896"></script> <script src="/js/spots.js?v=1780999608"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -59,8 +59,8 @@
</div> </div>
</div> </div>
<script src="/js/common.js?v=1780997896"></script> <script src="/js/common.js?v=1780999608"></script>
<script src="/js/status.js?v=1780997896"></script> <script src="/js/status.js?v=1780999608"></script>
<script> <script>
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --> $(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
</script> </script>

View File

@@ -17,6 +17,7 @@ function loadSolarConditions() {
const hfConditionClass = {'Good': 'bg-success-subtle', 'Fair': 'bg-warning-subtle', 'Poor': 'bg-danger-subtle'}; const hfConditionClass = {'Good': 'bg-success-subtle', 'Fair': 'bg-warning-subtle', 'Poor': 'bg-danger-subtle'};
if (jsonData.hf_conditions) { if (jsonData.hf_conditions) {
$('#hamqsl-section').show();
Object.entries(jsonData.hf_conditions).forEach(function ([key, condition]) { Object.entries(jsonData.hf_conditions).forEach(function ([key, condition]) {
const cell = $('#hf-conditions-' + key); const cell = $('#hf-conditions-' + key);
cell.text(condition); cell.text(condition);
@@ -116,6 +117,7 @@ function loadSolarConditions() {
// Ionosonde // Ionosonde
if (jsonData.ionosonde_data && Object.keys(jsonData.ionosonde_data).length > 0) { if (jsonData.ionosonde_data && Object.keys(jsonData.ionosonde_data).length > 0) {
$('#ionosonde-section').show();
ionosondeData = jsonData.ionosonde_data; ionosondeData = jsonData.ionosonde_data;
populateIonosondeDropdown(ionosondeData); populateIonosondeDropdown(ionosondeData);
renderIonosondeData(); renderIonosondeData();
@@ -123,6 +125,9 @@ function loadSolarConditions() {
// Forecast // Forecast
if (jsonData.k_index_forecast) {
$('#noaa-section').show();
}
renderKIndexForecast(jsonData.k_index_forecast); renderKIndexForecast(jsonData.k_index_forecast);
renderSolarStormForecast(jsonData.solar_storm_forecast); renderSolarStormForecast(jsonData.solar_storm_forecast);
renderBlackoutForecast(jsonData.blackout_forecast_r1r2, jsonData.blackout_forecast_r3_or_greater); renderBlackoutForecast(jsonData.blackout_forecast_r1r2, jsonData.blackout_forecast_r3_or_greater);