feat: Add Space Weather mode with real-time solar and geomagnetic monitoring

New mode providing real-time space weather data from NOAA SWPC, NASA SDO,
and HamQSL APIs. Includes Kp index, solar wind, X-ray flux charts, HF band
conditions, D-RAP absorption maps, aurora forecast, solar imagery, flare
probability, and active solar regions. No SDR hardware required.

Bumps version to 2.20.0. Updates all documentation including README, FEATURES,
USAGE, UI_GUIDE, help modal, and GitHub Pages site.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-17 22:10:34 +00:00
parent cae7a0586f
commit 16239c1d31
15 changed files with 1759 additions and 13 deletions

View File

@@ -48,6 +48,7 @@ Support the developer of this open-source project
- **GPS** - Real-time GPS position tracking with live map, speed, altitude, and satellite info - **GPS** - Real-time GPS position tracking with live map, speed, altitude, and satellite info
- **TSCM** - Counter-surveillance with RF baseline comparison and threat detection - **TSCM** - Counter-surveillance with RF baseline comparison and threat detection
- **Meshtastic** - LoRa mesh network integration - **Meshtastic** - LoRa mesh network integration
- **Space Weather** - Real-time solar and geomagnetic data from NOAA SWPC, NASA SDO, and HamQSL (no SDR required)
- **Spy Stations** - Number stations and diplomatic HF network database - **Spy Stations** - Number stations and diplomatic HF network database
- **Remote Agents** - Distributed SIGINT with remote sensor nodes - **Remote Agents** - Distributed SIGINT with remote sensor nodes
- **Offline Mode** - Bundled assets for air-gapped/field deployments - **Offline Mode** - Bundled assets for air-gapped/field deployments

View File

@@ -7,10 +7,21 @@ import os
import sys import sys
# Application version # Application version
VERSION = "2.19.0" VERSION = "2.20.0"
# Changelog - latest release notes (shown on welcome screen) # Changelog - latest release notes (shown on welcome screen)
CHANGELOG = [ CHANGELOG = [
{
"version": "2.20.0",
"date": "February 2026",
"highlights": [
"Space Weather mode: real-time solar and geomagnetic monitoring from NOAA SWPC, NASA SDO, and HamQSL",
"Kp index, solar wind, X-ray flux charts with Chart.js visualization",
"HF band conditions, D-RAP absorption maps, aurora forecast, and solar imagery",
"NOAA Space Weather Scales (G/S/R), flare probability, and active solar regions",
"No SDR hardware required — all data from public APIs with server-side caching",
]
},
{ {
"version": "2.19.0", "version": "2.19.0",
"date": "February 2026", "date": "February 2026",

View File

@@ -165,6 +165,22 @@ Digital Selective Calling (DSC) monitoring on the international maritime distres
- **Real-time JSON output** with meter ID, consumption, and signal data - **Real-time JSON output** with meter ID, consumption, and signal data
- **Multiple meter protocol support** via rtl_tcp integration - **Multiple meter protocol support** via rtl_tcp integration
## Space Weather
- **Real-time solar indices** - Solar Flux Index (SFI), Kp index, A-index, sunspot number
- **NOAA Space Weather Scales** - Geomagnetic storms (G), solar radiation (S), radio blackouts (R)
- **HF band conditions** - Day/night propagation from HamQSL for 80m through 10m bands
- **Solar wind monitoring** - Speed, density, and IMF Bz from DSCOVR satellite
- **X-ray flux chart** - GOES X-ray data with flare class scale (A/B/C/M/X)
- **Flare probability** - 1-day and 3-day C/M/X-class flare forecasts
- **Solar imagery** - NASA SDO 193A, 304A, and magnetogram images
- **D-RAP absorption maps** - HF radio absorption at 5-30 MHz frequency bands
- **Aurora forecast** - OVATION aurora oval visualization
- **SWPC alerts** - Real-time space weather alerts and warnings
- **Active solar regions** - Current sunspot region data with location and area
- **Auto-refresh** - 5-minute polling with manual refresh option
- **No SDR required** - Data fetched from NOAA SWPC, NASA SDO, and HamQSL public APIs
## Satellite Tracking ## Satellite Tracking
- **Full-screen dashboard** - dedicated popout with polar plot and ground track - **Full-screen dashboard** - dedicated popout with polar plot and ground track

View File

@@ -206,14 +206,23 @@ Extended base for full-screen dashboards (maps, visualizations).
| `listening` | Listening post | | `listening` | Listening post |
| `spystations` | Spy stations | | `spystations` | Spy stations |
| `meshtastic` | Mesh networking | | `meshtastic` | Mesh networking |
| `weathersat` | Weather satellites |
| `sstv_general` | HF SSTV |
| `gps` | GPS tracking |
| `websdr` | WebSDR |
| `subghz` | Sub-GHz analyzer |
| `bt_locate` | BT Locate |
| `analytics` | Analytics dashboard |
| `spaceweather` | Space weather |
| `dmr` | DMR/P25 digital voice |
### Navigation Groups ### Navigation Groups
The navigation is organized into groups: The navigation is organized into groups:
- **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic - **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic, WebSDR, SubGHz
- **Wireless**: WiFi, Bluetooth - **Wireless**: WiFi, Bluetooth, BT Locate
- **Security**: TSCM - **Security**: TSCM, Analytics
- **Space**: Satellite, ISS SSTV - **Space**: Satellite, ISS SSTV, Weather Sat, HF SSTV, GPS, Space Weather
--- ---

View File

@@ -239,6 +239,25 @@ Enable the auto-scheduler to automatically capture passes:
- Starts SatDump at the correct time and frequency - Starts SatDump at the correct time and frequency
- Decoded images are saved with timestamps - Decoded images are saved with timestamps
## Space Weather
1. **Switch to Space Weather mode** - Select "Space Weather" from the Space navigation group
2. **View Dashboard** - Solar indices, NOAA scales, band conditions, and charts load automatically
3. **Solar Imagery** - Toggle between SDO 193A, 304A, and Magnetogram views
4. **D-RAP Maps** - Select frequency (5-30 MHz) to view HF radio absorption maps
5. **Aurora Forecast** - View the OVATION aurora oval for the northern hemisphere
6. **Alerts** - Review current SWPC space weather alerts and warnings
7. **Active Regions** - View solar active region data (number, location, area)
8. **Refresh** - Data auto-refreshes every 5 minutes, or click "Refresh Now"
### Tips
- No SDR hardware required — all data comes from public APIs (NOAA SWPC, NASA SDO, HamQSL)
- Check HF band conditions before operating on shortwave frequencies
- Kp >= 5 indicates geomagnetic storm conditions that may affect HF propagation
- D-RAP maps show where HF absorption is highest — useful for path planning
- Solar imagery updates approximately every 15 minutes from NASA SDO
## AIS Vessel Tracking ## AIS Vessel Tracking
1. **Select Hardware** - Choose your SDR type 1. **Select Hardware** - Choose your SDR type

View File

@@ -156,6 +156,11 @@
<h3>GPS Tracking</h3> <h3>GPS Tracking</h3>
<p>Real-time GPS position tracking with live map, speed, heading, altitude, satellite info, and track recording.</p> <p>Real-time GPS position tracking with live map, speed, heading, altitude, satellite info, and track recording.</p>
</div> </div>
<div class="feature-card" data-category="space">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></div>
<h3>Space Weather</h3>
<p>Real-time solar and geomagnetic monitoring. Kp index, HF band conditions, solar imagery, D-RAP maps, and aurora forecasts from NOAA SWPC.</p>
</div>
<div class="feature-card" data-category="wireless"> <div class="feature-card" data-category="wireless">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg></div> <div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg></div>
<h3>WiFi Scanning</h3> <h3>WiFi Scanning</h3>

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "intercept" name = "intercept"
version = "2.19.0" version = "2.20.0"
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"

View File

@@ -36,6 +36,7 @@ def register_blueprints(app):
from .subghz import subghz_bp from .subghz import subghz_bp
from .bt_locate import bt_locate_bp from .bt_locate import bt_locate_bp
from .analytics import analytics_bp from .analytics import analytics_bp
from .space_weather import space_weather_bp
app.register_blueprint(pager_bp) app.register_blueprint(pager_bp)
app.register_blueprint(sensor_bp) app.register_blueprint(sensor_bp)
@@ -71,6 +72,7 @@ def register_blueprints(app):
app.register_blueprint(subghz_bp) # SubGHz transceiver (HackRF) app.register_blueprint(subghz_bp) # SubGHz transceiver (HackRF)
app.register_blueprint(bt_locate_bp) # BT Locate SAR device tracking app.register_blueprint(bt_locate_bp) # BT Locate SAR device tracking
app.register_blueprint(analytics_bp) # Cross-mode analytics dashboard app.register_blueprint(analytics_bp) # Cross-mode analytics dashboard
app.register_blueprint(space_weather_bp) # Space weather monitoring
# Initialize TSCM state with queue and lock from app # Initialize TSCM state with queue and lock from app
import app as app_module import app as app_module

300
routes/space_weather.py Normal file
View File

@@ -0,0 +1,300 @@
"""Space Weather routes - proxies NOAA SWPC, NASA SDO, and HamQSL data."""
from __future__ import annotations
import json
import time
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
from typing import Any
from flask import Blueprint, Response, jsonify
from utils.logging import get_logger
logger = get_logger('intercept.space_weather')
space_weather_bp = Blueprint('space_weather', __name__, url_prefix='/space-weather')
# ---------------------------------------------------------------------------
# TTL Cache
# ---------------------------------------------------------------------------
_cache: dict[str, dict[str, Any]] = {}
# Cache TTLs in seconds
TTL_REALTIME = 300 # 5 min for real-time data
TTL_FORECAST = 1800 # 30 min for forecasts
TTL_DAILY = 3600 # 1 hr for daily summaries
TTL_IMAGE = 600 # 10 min for images
def _cache_get(key: str) -> Any | None:
entry = _cache.get(key)
if entry and time.time() < entry['expires']:
return entry['data']
return None
def _cache_set(key: str, data: Any, ttl: int) -> None:
_cache[key] = {'data': data, 'expires': time.time() + ttl}
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
_TIMEOUT = 15 # seconds
SWPC_BASE = 'https://services.swpc.noaa.gov'
SWPC_JSON = f'{SWPC_BASE}/products'
def _fetch_json(url: str, timeout: int = _TIMEOUT) -> Any | None:
try:
req = urllib.request.Request(url, headers={'User-Agent': 'INTERCEPT/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as exc:
logger.warning('Failed to fetch %s: %s', url, exc)
return None
def _fetch_text(url: str, timeout: int = _TIMEOUT) -> str | None:
try:
req = urllib.request.Request(url, headers={'User-Agent': 'INTERCEPT/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.read().decode()
except Exception as exc:
logger.warning('Failed to fetch %s: %s', url, exc)
return None
def _fetch_bytes(url: str, timeout: int = _TIMEOUT) -> bytes | None:
try:
req = urllib.request.Request(url, headers={'User-Agent': 'INTERCEPT/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.read()
except Exception as exc:
logger.warning('Failed to fetch %s: %s', url, exc)
return None
# ---------------------------------------------------------------------------
# Data source fetchers
# ---------------------------------------------------------------------------
def _fetch_cached_json(cache_key: str, url: str, ttl: int) -> Any | None:
cached = _cache_get(cache_key)
if cached is not None:
return cached
data = _fetch_json(url)
if data is not None:
_cache_set(cache_key, data, ttl)
return data
def _fetch_kp_index() -> Any | None:
return _fetch_cached_json('kp_index', f'{SWPC_JSON}/noaa-planetary-k-index.json', TTL_REALTIME)
def _fetch_kp_forecast() -> Any | None:
return _fetch_cached_json('kp_forecast', f'{SWPC_JSON}/noaa-planetary-k-index-forecast.json', TTL_FORECAST)
def _fetch_scales() -> Any | None:
return _fetch_cached_json('scales', f'{SWPC_JSON}/noaa-scales.json', TTL_REALTIME)
def _fetch_flux() -> Any | None:
return _fetch_cached_json('flux', f'{SWPC_JSON}/10cm-flux-30-day.json', TTL_DAILY)
def _fetch_alerts() -> Any | None:
return _fetch_cached_json('alerts', f'{SWPC_JSON}/alerts.json', TTL_REALTIME)
def _fetch_solar_wind_plasma() -> Any | None:
return _fetch_cached_json('sw_plasma', f'{SWPC_JSON}/solar-wind/plasma-6-hour.json', TTL_REALTIME)
def _fetch_solar_wind_mag() -> Any | None:
return _fetch_cached_json('sw_mag', f'{SWPC_JSON}/solar-wind/mag-6-hour.json', TTL_REALTIME)
def _fetch_xrays() -> Any | None:
return _fetch_cached_json('xrays', f'{SWPC_BASE}/json/goes/primary/xrays-1-day.json', TTL_REALTIME)
def _fetch_xray_flares() -> Any | None:
return _fetch_cached_json('xray_flares', f'{SWPC_BASE}/json/goes/primary/xray-flares-7-day.json', TTL_REALTIME)
def _fetch_flare_probability() -> Any | None:
return _fetch_cached_json('flare_prob', f'{SWPC_BASE}/json/solar_probabilities.json', TTL_FORECAST)
def _fetch_solar_regions() -> Any | None:
return _fetch_cached_json('solar_regions', f'{SWPC_BASE}/json/solar_regions.json', TTL_DAILY)
def _fetch_sunspot_report() -> Any | None:
return _fetch_cached_json('sunspot_report', f'{SWPC_BASE}/json/sunspot_report.json', TTL_DAILY)
def _parse_hamqsl_xml(xml_text: str) -> dict[str, Any] | None:
"""Parse HamQSL solar XML into a dict of band conditions."""
try:
root = ET.fromstring(xml_text)
solar = root.find('.//solardata')
if solar is None:
return None
result: dict[str, Any] = {}
# Scalar fields
for tag in ('sfi', 'aindex', 'kindex', 'kindexnt', 'xray', 'sunspots',
'heliumline', 'protonflux', 'electonflux', 'aurora',
'normalization', 'latdegree', 'solarwind', 'magneticfield',
'calculatedconditions', 'calculatedvhfconditions',
'geomagfield', 'signalnoise', 'fof2', 'muffactor', 'muf'):
el = solar.find(tag)
if el is not None and el.text:
result[tag] = el.text.strip()
# Band conditions
bands: list[dict[str, str]] = []
for band_el in solar.findall('.//calculatedconditions/band'):
bands.append({
'name': band_el.get('name', ''),
'time': band_el.get('time', ''),
'condition': band_el.text.strip() if band_el.text else ''
})
result['bands'] = bands
# VHF conditions
vhf: list[dict[str, str]] = []
for phen_el in solar.findall('.//calculatedvhfconditions/phenomenon'):
vhf.append({
'name': phen_el.get('name', ''),
'location': phen_el.get('location', ''),
'condition': phen_el.text.strip() if phen_el.text else ''
})
result['vhf'] = vhf
return result
except ET.ParseError as exc:
logger.warning('Failed to parse HamQSL XML: %s', exc)
return None
def _fetch_band_conditions() -> dict[str, Any] | None:
cached = _cache_get('band_conditions')
if cached is not None:
return cached
xml_text = _fetch_text('https://www.hamqsl.com/solarxml.php')
if xml_text is None:
return None
data = _parse_hamqsl_xml(xml_text)
if data is not None:
_cache_set('band_conditions', data, TTL_FORECAST)
return data
# ---------------------------------------------------------------------------
# Image proxy whitelist
# ---------------------------------------------------------------------------
IMAGE_WHITELIST: dict[str, dict[str, str]] = {
# D-RAP absorption maps
'drap_global': {
'url': f'{SWPC_BASE}/images/animations/d-rap/global/latest.png',
'content_type': 'image/png',
},
'drap_5': {
'url': f'{SWPC_BASE}/images/d-rap/global_f05.png',
'content_type': 'image/png',
},
'drap_10': {
'url': f'{SWPC_BASE}/images/d-rap/global_f10.png',
'content_type': 'image/png',
},
'drap_15': {
'url': f'{SWPC_BASE}/images/d-rap/global_f15.png',
'content_type': 'image/png',
},
'drap_20': {
'url': f'{SWPC_BASE}/images/d-rap/global_f20.png',
'content_type': 'image/png',
},
'drap_25': {
'url': f'{SWPC_BASE}/images/d-rap/global_f25.png',
'content_type': 'image/png',
},
'drap_30': {
'url': f'{SWPC_BASE}/images/d-rap/global_f30.png',
'content_type': 'image/png',
},
# Aurora forecast
'aurora_north': {
'url': f'{SWPC_BASE}/images/animations/ovation/north/latest.jpg',
'content_type': 'image/jpeg',
},
# SDO solar imagery
'sdo_193': {
'url': 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_0193.jpg',
'content_type': 'image/jpeg',
},
'sdo_304': {
'url': 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_0304.jpg',
'content_type': 'image/jpeg',
},
'sdo_magnetogram': {
'url': 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_HMIBC.jpg',
'content_type': 'image/jpeg',
},
}
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@space_weather_bp.route('/data')
def get_data():
"""Return aggregated space weather data from all sources."""
data = {
'kp_index': _fetch_kp_index(),
'kp_forecast': _fetch_kp_forecast(),
'scales': _fetch_scales(),
'flux': _fetch_flux(),
'alerts': _fetch_alerts(),
'solar_wind_plasma': _fetch_solar_wind_plasma(),
'solar_wind_mag': _fetch_solar_wind_mag(),
'xrays': _fetch_xrays(),
'xray_flares': _fetch_xray_flares(),
'flare_probability': _fetch_flare_probability(),
'solar_regions': _fetch_solar_regions(),
'sunspot_report': _fetch_sunspot_report(),
'band_conditions': _fetch_band_conditions(),
'timestamp': time.time(),
}
return jsonify(data)
@space_weather_bp.route('/image/<key>')
def get_image(key: str):
"""Proxy and cache whitelisted space weather images."""
entry = IMAGE_WHITELIST.get(key)
if not entry:
return jsonify({'error': 'Unknown image key'}), 404
cache_key = f'img_{key}'
cached = _cache_get(cache_key)
if cached is not None:
return Response(cached, content_type=entry['content_type'],
headers={'Cache-Control': 'public, max-age=300'})
img_data = _fetch_bytes(entry['url'])
if img_data is None:
return jsonify({'error': 'Failed to fetch image'}), 502
_cache_set(cache_key, img_data, TTL_IMAGE)
return Response(img_data, content_type=entry['content_type'],
headers={'Cache-Control': 'public, max-age=300'})

View File

@@ -0,0 +1,467 @@
/* Space Weather Mode Styles */
/* Main container */
.sw-visuals-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
height: 100%;
overflow-y: auto;
}
/* Header metrics strip */
.sw-header-strip {
display: flex;
align-items: center;
gap: 2px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px 12px;
flex-wrap: wrap;
}
.sw-header-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 12px;
min-width: 60px;
}
.sw-header-stat + .sw-header-stat {
border-left: 1px solid var(--border-color);
}
.sw-header-value {
font-family: var(--font-mono, 'Roboto Condensed', monospace);
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.sw-header-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.5px;
color: var(--text-dim);
text-transform: uppercase;
}
.sw-header-value.accent-cyan { color: var(--accent-cyan); }
.sw-header-value.accent-green { color: #00ff88; }
.sw-header-value.accent-yellow { color: #ffcc00; }
.sw-header-value.accent-orange { color: #ff8800; }
.sw-header-value.accent-red { color: #ff3366; }
/* Refresh controls in strip */
.sw-strip-controls {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
padding-left: 12px;
}
.sw-refresh-btn {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
transition: border-color 0.2s, color 0.2s;
}
.sw-refresh-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.sw-last-update {
font-size: 10px;
color: var(--text-dim);
font-family: var(--font-mono, 'Roboto Condensed', monospace);
}
/* NOAA G/S/R Scale cards */
.sw-scales-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.sw-scale-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
text-align: center;
}
.sw-scale-label {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
color: var(--text-dim);
text-transform: uppercase;
margin-bottom: 4px;
}
.sw-scale-value {
font-family: var(--font-mono, 'Roboto Condensed', monospace);
font-size: 24px;
font-weight: 700;
line-height: 1.2;
}
.sw-scale-desc {
font-size: 10px;
color: var(--text-dim);
margin-top: 2px;
}
/* Scale severity colors */
.sw-scale-0 { color: #00ff88; border-color: #00ff8833; }
.sw-scale-1 { color: #88ff00; border-color: #88ff0033; }
.sw-scale-2 { color: #ffcc00; border-color: #ffcc0033; }
.sw-scale-3 { color: #ff8800; border-color: #ff880033; }
.sw-scale-4 { color: #ff4400; border-color: #ff440033; }
.sw-scale-5 { color: #ff0044; border-color: #ff004433; }
/* HF Band conditions grid */
.sw-band-panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
}
.sw-band-title {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.sw-band-grid {
display: grid;
grid-template-columns: auto repeat(2, 1fr);
gap: 4px 8px;
font-size: 11px;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
}
.sw-band-header {
font-size: 10px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
padding-bottom: 4px;
border-bottom: 1px solid var(--border-color);
}
.sw-band-name {
color: var(--text-secondary);
font-weight: 500;
}
.sw-band-cond {
text-align: center;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
}
.sw-band-good { color: #00ff88; background: #00ff8815; }
.sw-band-fair { color: #ffcc00; background: #ffcc0015; }
.sw-band-poor { color: #ff3366; background: #ff336615; }
/* 2-column dashboard grid */
.sw-dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
/* Chart containers */
.sw-chart-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
}
.sw-chart-title {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.sw-chart-wrap {
position: relative;
height: 180px;
}
.sw-chart-wrap canvas {
width: 100% !important;
height: 100% !important;
}
/* Flare probability table */
.sw-prob-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
}
.sw-prob-table th {
font-size: 10px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
text-align: left;
padding: 4px 6px;
border-bottom: 1px solid var(--border-color);
}
.sw-prob-table td {
padding: 4px 6px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
/* Solar image gallery */
.sw-image-panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
}
.sw-image-tabs {
display: flex;
gap: 4px;
margin-bottom: 8px;
}
.sw-image-tab {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-dim);
padding: 4px 10px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
transition: all 0.2s;
}
.sw-image-tab:hover {
border-color: var(--text-secondary);
color: var(--text-secondary);
}
.sw-image-tab.active {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: var(--accent-cyan)10;
}
.sw-image-frame {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
background: var(--bg-primary);
border-radius: 4px;
overflow: hidden;
}
.sw-image-frame img {
max-width: 100%;
max-height: 400px;
object-fit: contain;
border-radius: 4px;
}
/* D-RAP frequency selector */
.sw-drap-freqs {
display: flex;
gap: 4px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.sw-drap-freq-btn {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-dim);
padding: 3px 8px;
border-radius: 3px;
font-size: 10px;
cursor: pointer;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
transition: all 0.2s;
}
.sw-drap-freq-btn:hover {
border-color: var(--text-secondary);
color: var(--text-secondary);
}
.sw-drap-freq-btn.active {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
/* Alerts list */
.sw-alerts-panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
max-height: 300px;
overflow-y: auto;
}
.sw-alert-item {
padding: 8px;
border-bottom: 1px solid var(--border-color);
font-size: 11px;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
}
.sw-alert-item:last-child {
border-bottom: none;
}
.sw-alert-type {
font-weight: 600;
color: var(--accent-cyan);
font-size: 10px;
text-transform: uppercase;
margin-bottom: 2px;
}
.sw-alert-time {
font-size: 10px;
color: var(--text-dim);
margin-bottom: 4px;
}
.sw-alert-msg {
color: var(--text-secondary);
line-height: 1.4;
white-space: pre-wrap;
font-size: 10px;
}
/* Active regions table */
.sw-regions-panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px;
max-height: 300px;
overflow-y: auto;
}
.sw-regions-table {
width: 100%;
border-collapse: collapse;
font-size: 10px;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
}
.sw-regions-table th {
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
text-align: left;
padding: 4px 6px;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
background: var(--bg-card);
}
.sw-regions-table td {
padding: 4px 6px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
/* Empty / loading states */
.sw-empty {
text-align: center;
padding: 20px;
color: var(--text-dim);
font-size: 11px;
font-family: var(--font-mono, 'Roboto Condensed', monospace);
}
.sw-loading {
text-align: center;
padding: 20px;
color: var(--text-dim);
font-size: 11px;
}
.sw-loading::after {
content: '';
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-cyan);
border-radius: 50%;
animation: sw-spin 0.8s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
@keyframes sw-spin {
to { transform: rotate(360deg); }
}
/* Full-width card */
.sw-full-width {
grid-column: 1 / -1;
}
/* Responsive */
@media (max-width: 768px) {
.sw-dashboard-grid {
grid-template-columns: 1fr;
}
.sw-scales-row {
grid-template-columns: 1fr;
}
.sw-header-strip {
gap: 0;
}
.sw-header-stat {
padding: 4px 8px;
min-width: 50px;
}
.sw-header-value {
font-size: 14px;
}
}

View File

@@ -0,0 +1,677 @@
/**
* Space Weather Mode — IIFE module
* Polls /space-weather/data every 5 min, renders dashboard with Chart.js
*/
const SpaceWeather = (function () {
'use strict';
let _initialized = false;
let _pollTimer = null;
let _autoRefresh = true;
const POLL_INTERVAL = 5 * 60 * 1000; // 5 min
// Chart.js instances
let _kpChart = null;
let _windChart = null;
let _xrayChart = null;
// Current image selections
let _solarImageKey = 'sdo_193';
let _drapFreq = 'drap_global';
// -------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------
function init() {
if (!_initialized) {
_initialized = true;
}
refresh();
_startAutoRefresh();
}
function destroy() {
_stopAutoRefresh();
_destroyCharts();
_initialized = false;
}
function refresh() {
_fetchData();
}
function selectSolarImage(key) {
_solarImageKey = key;
_updateSolarImageTabs();
const frame = document.getElementById('swSolarImageFrame');
if (frame) {
frame.innerHTML = '<div class="sw-loading">Loading</div>';
const img = new Image();
img.onload = function () { frame.innerHTML = ''; frame.appendChild(img); };
img.onerror = function () { frame.innerHTML = '<div class="sw-empty">Failed to load image</div>'; };
img.src = '/space-weather/image/' + key + '?t=' + Date.now();
img.alt = key;
}
}
function selectDrapFreq(key) {
_drapFreq = key;
_updateDrapTabs();
const frame = document.getElementById('swDrapImageFrame');
if (frame) {
frame.innerHTML = '<div class="sw-loading">Loading</div>';
const img = new Image();
img.onload = function () { frame.innerHTML = ''; frame.appendChild(img); };
img.onerror = function () { frame.innerHTML = '<div class="sw-empty">Failed to load image</div>'; };
img.src = '/space-weather/image/' + key + '?t=' + Date.now();
img.alt = key;
}
}
function toggleAutoRefresh() {
const cb = document.getElementById('swAutoRefresh');
_autoRefresh = cb ? cb.checked : !_autoRefresh;
if (_autoRefresh) _startAutoRefresh();
else _stopAutoRefresh();
}
// -------------------------------------------------------------------
// Polling
// -------------------------------------------------------------------
function _startAutoRefresh() {
_stopAutoRefresh();
if (_autoRefresh) {
_pollTimer = setInterval(_fetchData, POLL_INTERVAL);
}
}
function _stopAutoRefresh() {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
}
function _fetchData() {
fetch('/space-weather/data')
.then(function (r) { return r.json(); })
.then(function (data) {
_renderAll(data);
_updateTimestamp();
})
.catch(function (err) {
console.warn('SpaceWeather fetch error:', err);
});
}
// -------------------------------------------------------------------
// Master render
// -------------------------------------------------------------------
function _renderAll(data) {
_renderHeaderStrip(data);
_renderScales(data);
_renderBandConditions(data);
_renderKpChart(data);
_renderWindChart(data);
_renderXrayChart(data);
_renderFlareProb(data);
_renderSolarImage();
_renderDrapImage();
_renderAuroraImage();
_renderAlerts(data);
_renderRegions(data);
_updateSidebar(data);
}
// -------------------------------------------------------------------
// Header strip
// -------------------------------------------------------------------
function _renderHeaderStrip(data) {
var sfi = '--', kp = '--', aIndex = '--', ssn = '--', wind = '--', bz = '--';
// SFI from band_conditions (HamQSL) or flux
if (data.band_conditions && data.band_conditions.sfi) {
sfi = data.band_conditions.sfi;
} else if (data.flux && data.flux.length > 1) {
var last = data.flux[data.flux.length - 1];
sfi = last[1] || '--';
}
// Kp from kp_index
if (data.kp_index && data.kp_index.length > 1) {
var lastKp = data.kp_index[data.kp_index.length - 1];
kp = lastKp[1] || '--';
}
// A-index from band_conditions
if (data.band_conditions && data.band_conditions.aindex) {
aIndex = data.band_conditions.aindex;
}
// Sunspot number
if (data.band_conditions && data.band_conditions.sunspots) {
ssn = data.band_conditions.sunspots;
}
// Solar wind speed — last non-null entry
if (data.solar_wind_plasma && data.solar_wind_plasma.length > 1) {
for (var i = data.solar_wind_plasma.length - 1; i >= 1; i--) {
if (data.solar_wind_plasma[i][2]) {
wind = Math.round(parseFloat(data.solar_wind_plasma[i][2]));
break;
}
}
}
// IMF Bz — last non-null entry
if (data.solar_wind_mag && data.solar_wind_mag.length > 1) {
for (var j = data.solar_wind_mag.length - 1; j >= 1; j--) {
if (data.solar_wind_mag[j][3]) {
bz = parseFloat(data.solar_wind_mag[j][3]).toFixed(1);
break;
}
}
}
_setText('swStripSfi', sfi);
_setText('swStripKp', kp);
_setText('swStripA', aIndex);
_setText('swStripSsn', ssn);
_setText('swStripWind', wind !== '--' ? wind + ' km/s' : '--');
_setText('swStripBz', bz !== '--' ? bz + ' nT' : '--');
// Color Kp by severity
var kpEl = document.getElementById('swStripKp');
if (kpEl) {
var kpNum = parseFloat(kp);
kpEl.className = 'sw-header-value';
if (kpNum >= 7) kpEl.classList.add('accent-red');
else if (kpNum >= 5) kpEl.classList.add('accent-orange');
else if (kpNum >= 4) kpEl.classList.add('accent-yellow');
else kpEl.classList.add('accent-green');
}
// Color Bz — negative is bad
var bzEl = document.getElementById('swStripBz');
if (bzEl) {
var bzNum = parseFloat(bz);
bzEl.className = 'sw-header-value';
if (bzNum < -10) bzEl.classList.add('accent-red');
else if (bzNum < -5) bzEl.classList.add('accent-orange');
else if (bzNum < 0) bzEl.classList.add('accent-yellow');
else bzEl.classList.add('accent-green');
}
}
// -------------------------------------------------------------------
// NOAA Scales
// -------------------------------------------------------------------
function _renderScales(data) {
if (!data.scales) return;
var s = data.scales;
// Structure: { "0": { R: {Scale, Text}, S: {Scale, Text}, G: {Scale, Text} }, ... }
// Key "0" = current conditions
var current = s['0'];
if (!current) return;
var scaleMap = {
'G': { el: 'swScaleG', label: 'Geomagnetic Storms' },
'S': { el: 'swScaleS', label: 'Solar Radiation' },
'R': { el: 'swScaleR', label: 'Radio Blackouts' }
};
['G', 'S', 'R'].forEach(function (k) {
var info = scaleMap[k];
var scaleData = current[k];
var val = '0', text = info.label;
if (scaleData) {
val = String(scaleData.Scale || '0').replace(/[^0-9]/g, '') || '0';
if (scaleData.Text && scaleData.Text !== 'none') {
text = scaleData.Text;
}
}
var el = document.getElementById(info.el);
if (el) {
el.querySelector('.sw-scale-value').textContent = k + val;
el.querySelector('.sw-scale-value').className = 'sw-scale-value sw-scale-' + val;
var descEl = el.querySelector('.sw-scale-desc');
if (descEl) descEl.textContent = text;
}
});
}
// -------------------------------------------------------------------
// Band conditions
// -------------------------------------------------------------------
function _renderBandConditions(data) {
var grid = document.getElementById('swBandGrid');
if (!grid) return;
if (!data.band_conditions || !data.band_conditions.bands || data.band_conditions.bands.length === 0) {
grid.innerHTML = '<div class="sw-empty" style="grid-column:1/-1">No band data available</div>';
return;
}
// Group by band name, collect day/night
var bands = {};
data.band_conditions.bands.forEach(function (b) {
if (!bands[b.name]) bands[b.name] = {};
bands[b.name][b.time.toLowerCase()] = b.condition;
});
var html = '<div class="sw-band-header">Band</div><div class="sw-band-header" style="text-align:center">Day</div><div class="sw-band-header" style="text-align:center">Night</div>';
Object.keys(bands).forEach(function (name) {
html += '<div class="sw-band-name">' + name + '</div>';
['day', 'night'].forEach(function (t) {
var cond = bands[name][t] || '--';
var cls = 'sw-band-cond';
var cl = cond.toLowerCase();
if (cl === 'good') cls += ' sw-band-good';
else if (cl === 'fair') cls += ' sw-band-fair';
else if (cl === 'poor') cls += ' sw-band-poor';
html += '<div class="' + cls + '">' + cond + '</div>';
});
});
grid.innerHTML = html;
}
// -------------------------------------------------------------------
// Kp bar chart
// -------------------------------------------------------------------
function _renderKpChart(data) {
var canvas = document.getElementById('swKpChart');
if (!canvas) return;
if (!data.kp_index || data.kp_index.length < 2) return;
var rows = data.kp_index.slice(1); // skip header
var labels = [];
var values = [];
var colors = [];
// Take last 24 entries
var subset = rows.slice(-24);
subset.forEach(function (r) {
var dt = r[0] || '';
labels.push(dt.slice(5, 16)); // MM-DD HH:MM
var v = parseFloat(r[1]) || 0;
values.push(v);
if (v >= 7) colors.push('#ff3366');
else if (v >= 5) colors.push('#ff8800');
else if (v >= 4) colors.push('#ffcc00');
else colors.push('#00ff88');
});
if (_kpChart) { _kpChart.destroy(); _kpChart = null; }
_kpChart = new Chart(canvas, {
type: 'bar',
data: {
labels: labels,
datasets: [{
data: values,
backgroundColor: colors,
borderWidth: 0,
barPercentage: 0.8
}]
},
options: _chartOpts('Kp', 0, 9, false)
});
}
// -------------------------------------------------------------------
// Solar wind chart
// -------------------------------------------------------------------
function _renderWindChart(data) {
var canvas = document.getElementById('swWindChart');
if (!canvas) return;
if (!data.solar_wind_plasma || data.solar_wind_plasma.length < 2) return;
var rows = data.solar_wind_plasma.slice(1);
var labels = [];
var speedData = [];
var densityData = [];
// Sample every 3rd point to avoid overcrowding
for (var i = 0; i < rows.length; i += 3) {
var r = rows[i];
labels.push(r[0] ? r[0].slice(11, 16) : '');
speedData.push(r[2] ? parseFloat(r[2]) : null);
densityData.push(r[1] ? parseFloat(r[1]) : null);
}
if (_windChart) { _windChart.destroy(); _windChart = null; }
_windChart = new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Speed (km/s)',
data: speedData,
borderColor: '#00ccff',
backgroundColor: '#00ccff22',
borderWidth: 1.5,
pointRadius: 0,
fill: true,
tension: 0.3,
yAxisID: 'y'
},
{
label: 'Density (p/cm³)',
data: densityData,
borderColor: '#ff8800',
borderWidth: 1,
pointRadius: 0,
borderDash: [4, 2],
tension: 0.3,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: true, position: 'top', labels: { color: '#888', font: { size: 10 }, boxWidth: 12, padding: 8 } }
},
scales: {
x: { display: true, ticks: { color: '#555', font: { size: 9 }, maxTicksLimit: 8 }, grid: { color: '#ffffff08' } },
y: { display: true, position: 'left', ticks: { color: '#00ccff', font: { size: 9 } }, grid: { color: '#ffffff08' }, title: { display: false } },
y1: { display: true, position: 'right', ticks: { color: '#ff8800', font: { size: 9 } }, grid: { drawOnChartArea: false } }
},
interaction: { mode: 'index', intersect: false }
}
});
}
// -------------------------------------------------------------------
// X-ray flux chart
// -------------------------------------------------------------------
function _renderXrayChart(data) {
var canvas = document.getElementById('swXrayChart');
if (!canvas) return;
if (!data.xrays || data.xrays.length < 2) return;
// New format: array of objects with time_tag, flux, energy
// Filter to short-wavelength (0.1-0.8nm) only
var filtered = data.xrays.filter(function (r) {
return r.energy && r.energy === '0.1-0.8nm';
});
if (filtered.length === 0) filtered = data.xrays;
var labels = [];
var values = [];
// Sample every 3rd point
for (var i = 0; i < filtered.length; i += 3) {
var r = filtered[i];
var tag = r.time_tag || '';
labels.push(tag.slice(11, 16));
values.push(r.flux ? parseFloat(r.flux) : null);
}
if (_xrayChart) { _xrayChart.destroy(); _xrayChart = null; }
_xrayChart = new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'X-Ray Flux (W/m²)',
data: values,
borderColor: '#ff3366',
backgroundColor: '#ff336622',
borderWidth: 1.5,
pointRadius: 0,
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: { display: true, ticks: { color: '#555', font: { size: 9 }, maxTicksLimit: 8 }, grid: { color: '#ffffff08' } },
y: {
display: true,
type: 'logarithmic',
ticks: {
color: '#888',
font: { size: 9 },
callback: function (v) {
if (v >= 1e-4) return 'X';
if (v >= 1e-5) return 'M';
if (v >= 1e-6) return 'C';
if (v >= 1e-7) return 'B';
if (v >= 1e-8) return 'A';
return '';
}
},
grid: { color: '#ffffff08' }
}
}
}
});
}
// -------------------------------------------------------------------
// Flare probability
// -------------------------------------------------------------------
function _renderFlareProb(data) {
var el = document.getElementById('swFlareProb');
if (!el) return;
if (!data.flare_probability || data.flare_probability.length === 0) {
el.innerHTML = '<div class="sw-empty">No flare data</div>';
return;
}
// New format: array of objects with date, c_class_1_day, m_class_1_day, x_class_1_day, etc.
var latest = data.flare_probability.slice(-3);
var html = '<table class="sw-prob-table"><thead><tr>';
html += '<th>Date</th><th>C 1-day</th><th>M 1-day</th><th>X 1-day</th><th>Proton</th>';
html += '</tr></thead><tbody>';
latest.forEach(function (row) {
html += '<tr>';
html += '<td>' + _escHtml(row.date || '--') + '</td>';
html += '<td>' + _escHtml(row.c_class_1_day || '--') + '%</td>';
html += '<td>' + _escHtml(row.m_class_1_day || '--') + '%</td>';
html += '<td>' + _escHtml(row.x_class_1_day || '--') + '%</td>';
html += '<td>' + _escHtml(row['10mev_protons_1_day'] || '--') + '%</td>';
html += '</tr>';
});
html += '</tbody></table>';
el.innerHTML = html;
}
// -------------------------------------------------------------------
// Images
// -------------------------------------------------------------------
function _renderSolarImage() {
selectSolarImage(_solarImageKey);
}
function _renderDrapImage() {
selectDrapFreq(_drapFreq);
}
function _renderAuroraImage() {
var frame = document.getElementById('swAuroraFrame');
if (!frame) return;
var img = new Image();
img.onload = function () { frame.innerHTML = ''; frame.appendChild(img); };
img.onerror = function () { frame.innerHTML = '<div class="sw-empty">Failed to load aurora image</div>'; };
img.src = '/space-weather/image/aurora_north?t=' + Date.now();
img.alt = 'Aurora Forecast';
}
function _updateSolarImageTabs() {
document.querySelectorAll('.sw-solar-tab').forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.key === _solarImageKey);
});
}
function _updateDrapTabs() {
document.querySelectorAll('.sw-drap-freq-btn').forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.key === _drapFreq);
});
}
// -------------------------------------------------------------------
// Alerts
// -------------------------------------------------------------------
function _renderAlerts(data) {
var el = document.getElementById('swAlertsList');
if (!el) return;
if (!data.alerts || data.alerts.length === 0) {
el.innerHTML = '<div class="sw-empty">No active alerts</div>';
return;
}
var html = '';
// Show latest 10
var items = data.alerts.slice(0, 10);
items.forEach(function (a) {
var msg = a.message || a.product_text || '';
// Truncate long messages
if (msg.length > 300) msg = msg.substring(0, 300) + '...';
html += '<div class="sw-alert-item">';
html += '<div class="sw-alert-type">' + _escHtml(a.product_id || 'Alert') + '</div>';
html += '<div class="sw-alert-time">' + _escHtml(a.issue_datetime || '') + '</div>';
html += '<div class="sw-alert-msg">' + _escHtml(msg) + '</div>';
html += '</div>';
});
el.innerHTML = html;
}
// -------------------------------------------------------------------
// Active regions
// -------------------------------------------------------------------
function _renderRegions(data) {
var el = document.getElementById('swRegionsBody');
if (!el) return;
if (!data.solar_regions || data.solar_regions.length === 0) {
el.innerHTML = '<tr><td colspan="5" class="sw-empty">No active regions</td></tr>';
return;
}
// New format: array of objects with region, observed_date, location, longitude, area, etc.
// De-duplicate by region number (keep latest observed_date per region)
var byRegion = {};
data.solar_regions.forEach(function (r) {
var key = r.region || '';
if (!byRegion[key] || (r.observed_date > byRegion[key].observed_date)) {
byRegion[key] = r;
}
});
var regions = Object.values(byRegion);
var html = '';
regions.forEach(function (r) {
html += '<tr>';
html += '<td>' + _escHtml(String(r.region || '')) + '</td>';
html += '<td>' + _escHtml(r.observed_date || '') + '</td>';
html += '<td>' + _escHtml(r.location || '') + '</td>';
html += '<td>' + _escHtml(String(r.longitude || '')) + '</td>';
html += '<td>' + _escHtml(String(r.area || '')) + '</td>';
html += '</tr>';
});
el.innerHTML = html;
}
// -------------------------------------------------------------------
// Sidebar quick status
// -------------------------------------------------------------------
function _updateSidebar(data) {
var sfi = '--', kp = '--', aIdx = '--', ssn = '--', wind = '--', bz = '--';
if (data.band_conditions) {
if (data.band_conditions.sfi) sfi = data.band_conditions.sfi;
if (data.band_conditions.aindex) aIdx = data.band_conditions.aindex;
if (data.band_conditions.sunspots) ssn = data.band_conditions.sunspots;
}
if (data.kp_index && data.kp_index.length > 1) {
kp = data.kp_index[data.kp_index.length - 1][1] || '--';
}
if (data.solar_wind_plasma && data.solar_wind_plasma.length > 1) {
for (var i = data.solar_wind_plasma.length - 1; i >= 1; i--) {
if (data.solar_wind_plasma[i][2]) {
wind = Math.round(parseFloat(data.solar_wind_plasma[i][2])) + ' km/s';
break;
}
}
}
if (data.solar_wind_mag && data.solar_wind_mag.length > 1) {
for (var j = data.solar_wind_mag.length - 1; j >= 1; j--) {
if (data.solar_wind_mag[j][3]) {
bz = parseFloat(data.solar_wind_mag[j][3]).toFixed(1) + ' nT';
break;
}
}
}
_setText('swSidebarSfi', sfi);
_setText('swSidebarKp', kp);
_setText('swSidebarA', aIdx);
_setText('swSidebarSsn', ssn);
_setText('swSidebarWind', wind);
_setText('swSidebarBz', bz);
}
// -------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------
function _setText(id, text) {
var el = document.getElementById(id);
if (el) el.textContent = text;
}
function _escHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function _updateTimestamp() {
var el = document.getElementById('swLastUpdate');
if (el) el.textContent = 'Updated: ' + new Date().toLocaleTimeString();
}
function _chartOpts(yLabel, yMin, yMax, showLegend) {
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: !!showLegend, labels: { color: '#888', font: { size: 10 } } }
},
scales: {
x: { display: true, ticks: { color: '#555', font: { size: 9 }, maxRotation: 45, maxTicksLimit: 8 }, grid: { color: '#ffffff08' } },
y: { display: true, min: yMin, max: yMax, ticks: { color: '#888', font: { size: 9 }, stepSize: 1 }, grid: { color: '#ffffff08' } }
}
};
}
function _destroyCharts() {
if (_kpChart) { _kpChart.destroy(); _kpChart = null; }
if (_windChart) { _windChart.destroy(); _windChart = null; }
if (_xrayChart) { _xrayChart.destroy(); _xrayChart = null; }
}
// -------------------------------------------------------------------
// Expose public API
// -------------------------------------------------------------------
return {
init: init,
destroy: destroy,
refresh: refresh,
selectSolarImage: selectSolarImage,
selectDrapFreq: selectDrapFreq,
toggleAutoRefresh: toggleAutoRefresh
};
})();

View File

@@ -66,6 +66,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/gps.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/modes/gps.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9"> <link rel="stylesheet" href="{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/bt_locate.css') }}?v={{ version }}&r=btlocate2"> <link rel="stylesheet" href="{{ url_for('static', filename='css/modes/bt_locate.css') }}?v={{ version }}&r=btlocate2">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/space-weather.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/function-strip.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/function-strip.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}">
@@ -258,6 +259,10 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span> <span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span>
<span class="mode-name">GPS</span> <span class="mode-name">GPS</span>
</button> </button>
<button class="mode-card mode-card-sm" onclick="selectMode('spaceweather')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
<span class="mode-name">Space Wx</span>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -551,6 +556,8 @@
{% include 'partials/modes/gps.html' %} {% include 'partials/modes/gps.html' %}
{% include 'partials/modes/space-weather.html' %}
{% include 'partials/modes/listening-post.html' %} {% include 'partials/modes/listening-post.html' %}
{% include 'partials/modes/tscm.html' %} {% include 'partials/modes/tscm.html' %}
@@ -2903,6 +2910,141 @@
</div> </div>
</div> </div>
<!-- Space Weather Dashboard -->
<div id="spaceWeatherVisuals" class="sw-visuals-container" style="display: none;">
<!-- Header metrics strip -->
<div class="sw-header-strip">
<div class="sw-header-stat">
<span class="sw-header-value accent-cyan" id="swStripSfi">--</span>
<span class="sw-header-label">SFI</span>
</div>
<div class="sw-header-stat">
<span class="sw-header-value" id="swStripKp">--</span>
<span class="sw-header-label">Kp</span>
</div>
<div class="sw-header-stat">
<span class="sw-header-value" id="swStripA">--</span>
<span class="sw-header-label">A-Index</span>
</div>
<div class="sw-header-stat">
<span class="sw-header-value" id="swStripSsn">--</span>
<span class="sw-header-label">SSN</span>
</div>
<div class="sw-header-stat">
<span class="sw-header-value" id="swStripWind">--</span>
<span class="sw-header-label">Wind</span>
</div>
<div class="sw-header-stat">
<span class="sw-header-value" id="swStripBz">--</span>
<span class="sw-header-label">Bz</span>
</div>
</div>
<!-- NOAA G/S/R Scales -->
<div class="sw-scales-row">
<div class="sw-scale-card" id="swScaleG">
<div class="sw-scale-label">Geomagnetic</div>
<div class="sw-scale-value sw-scale-0">G0</div>
<div class="sw-scale-desc">Quiet</div>
</div>
<div class="sw-scale-card" id="swScaleS">
<div class="sw-scale-label">Solar Radiation</div>
<div class="sw-scale-value sw-scale-0">S0</div>
<div class="sw-scale-desc">None</div>
</div>
<div class="sw-scale-card" id="swScaleR">
<div class="sw-scale-label">Radio Blackouts</div>
<div class="sw-scale-value sw-scale-0">R0</div>
<div class="sw-scale-desc">None</div>
</div>
</div>
<!-- HF Band Conditions -->
<div class="sw-band-panel">
<div class="sw-band-title">HF Band Conditions</div>
<div class="sw-band-grid" id="swBandGrid">
<div class="sw-loading">Loading band data</div>
</div>
</div>
<!-- Charts row -->
<div class="sw-dashboard-grid">
<div class="sw-chart-card">
<div class="sw-chart-title">Kp Index (3-hourly)</div>
<div class="sw-chart-wrap"><canvas id="swKpChart"></canvas></div>
</div>
<div class="sw-chart-card">
<div class="sw-chart-title">Solar Wind</div>
<div class="sw-chart-wrap"><canvas id="swWindChart"></canvas></div>
</div>
<div class="sw-chart-card">
<div class="sw-chart-title">X-Ray Flux (GOES)</div>
<div class="sw-chart-wrap"><canvas id="swXrayChart"></canvas></div>
</div>
<div class="sw-chart-card">
<div class="sw-chart-title">Flare Probability</div>
<div id="swFlareProb"><div class="sw-loading">Loading</div></div>
</div>
</div>
<!-- Solar imagery gallery -->
<div class="sw-dashboard-grid">
<div class="sw-image-panel">
<div class="sw-chart-title">Solar Imagery (SDO)</div>
<div class="sw-image-tabs">
<button class="sw-image-tab sw-solar-tab active" data-key="sdo_193" onclick="SpaceWeather.selectSolarImage('sdo_193')">193&#197;</button>
<button class="sw-image-tab sw-solar-tab" data-key="sdo_304" onclick="SpaceWeather.selectSolarImage('sdo_304')">304&#197;</button>
<button class="sw-image-tab sw-solar-tab" data-key="sdo_magnetogram" onclick="SpaceWeather.selectSolarImage('sdo_magnetogram')">Magnetogram</button>
</div>
<div class="sw-image-frame" id="swSolarImageFrame">
<div class="sw-loading">Loading</div>
</div>
</div>
<div class="sw-image-panel">
<div class="sw-chart-title">Aurora Forecast (North)</div>
<div class="sw-image-frame" id="swAuroraFrame">
<div class="sw-loading">Loading</div>
</div>
</div>
</div>
<!-- D-RAP absorption map -->
<div class="sw-image-panel">
<div class="sw-chart-title">D-Region Absorption (D-RAP)</div>
<div class="sw-drap-freqs">
<button class="sw-drap-freq-btn active" data-key="drap_global" onclick="SpaceWeather.selectDrapFreq('drap_global')">Global</button>
<button class="sw-drap-freq-btn" data-key="drap_5" onclick="SpaceWeather.selectDrapFreq('drap_5')">5 MHz</button>
<button class="sw-drap-freq-btn" data-key="drap_10" onclick="SpaceWeather.selectDrapFreq('drap_10')">10 MHz</button>
<button class="sw-drap-freq-btn" data-key="drap_15" onclick="SpaceWeather.selectDrapFreq('drap_15')">15 MHz</button>
<button class="sw-drap-freq-btn" data-key="drap_20" onclick="SpaceWeather.selectDrapFreq('drap_20')">20 MHz</button>
<button class="sw-drap-freq-btn" data-key="drap_25" onclick="SpaceWeather.selectDrapFreq('drap_25')">25 MHz</button>
<button class="sw-drap-freq-btn" data-key="drap_30" onclick="SpaceWeather.selectDrapFreq('drap_30')">30 MHz</button>
</div>
<div class="sw-image-frame" id="swDrapImageFrame">
<div class="sw-loading">Loading</div>
</div>
</div>
<!-- Alerts & Active Regions -->
<div class="sw-dashboard-grid">
<div class="sw-alerts-panel">
<div class="sw-chart-title">Active Alerts</div>
<div id="swAlertsList"><div class="sw-loading">Loading</div></div>
</div>
<div class="sw-regions-panel">
<div class="sw-chart-title">Active Sunspot Regions</div>
<table class="sw-regions-table">
<thead>
<tr><th>Region</th><th>Date</th><th>Loc</th><th>Lo</th><th>Area</th></tr>
</thead>
<tbody id="swRegionsBody">
<tr><td colspan="5" class="sw-loading">Loading</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Device Intelligence Dashboard (above waterfall for prominence) --> <!-- Device Intelligence Dashboard (above waterfall for prominence) -->
<div class="recon-panel collapsed" id="reconPanel"> <div class="recon-panel collapsed" id="reconPanel">
<div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;"> <div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;">
@@ -3029,6 +3171,7 @@
<script src="{{ url_for('static', filename='js/modes/subghz.js') }}?v={{ version }}&r=subghz_layout9"></script> <script src="{{ url_for('static', filename='js/modes/subghz.js') }}?v={{ version }}&r=subghz_layout9"></script>
<script src="{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate2"></script> <script src="{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate2"></script>
<script src="{{ url_for('static', filename='js/modes/analytics.js') }}"></script> <script src="{{ url_for('static', filename='js/modes/analytics.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/space-weather.js') }}"></script>
<script> <script>
// ============================================ // ============================================
@@ -3165,7 +3308,7 @@
'pager', 'sensor', 'rtlamr', 'aprs', 'listening', 'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
'spystations', 'meshtastic', 'wifi', 'bluetooth', 'bt_locate', 'spystations', 'meshtastic', 'wifi', 'bluetooth', 'bt_locate',
'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz', 'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz',
'analytics' 'analytics', 'spaceweather'
]); ]);
function getModeFromQuery() { function getModeFromQuery() {
@@ -3626,7 +3769,7 @@
'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr', 'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr',
'meshtastic': 'sdr', 'meshtastic': 'sdr',
'satellite': 'space', 'sstv': 'space', 'weathersat': 'space', 'sstv_general': 'space', 'gps': 'space', 'satellite': 'space', 'sstv': 'space', 'weathersat': 'space', 'sstv_general': 'space', 'gps': 'space',
'subghz': 'sdr' 'spaceweather': 'space', 'subghz': 'sdr'
}; };
// Remove has-active from all dropdowns // Remove has-active from all dropdowns
@@ -3731,6 +3874,7 @@
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr'); document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz'); document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
document.getElementById('analyticsMode')?.classList.toggle('active', mode === 'analytics'); document.getElementById('analyticsMode')?.classList.toggle('active', mode === 'analytics');
document.getElementById('spaceWeatherMode')?.classList.toggle('active', mode === 'spaceweather');
const pagerStats = document.getElementById('pagerStats'); const pagerStats = document.getElementById('pagerStats');
@@ -3774,7 +3918,8 @@
'dmr': 'DIGITAL VOICE', 'dmr': 'DIGITAL VOICE',
'websdr': 'WEBSDR', 'websdr': 'WEBSDR',
'subghz': 'SUBGHZ', 'subghz': 'SUBGHZ',
'analytics': 'ANALYTICS' 'analytics': 'ANALYTICS',
'spaceweather': 'SPACE WX'
}; };
const activeModeIndicator = document.getElementById('activeModeIndicator'); const activeModeIndicator = document.getElementById('activeModeIndicator');
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase()); if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
@@ -3794,6 +3939,7 @@
const websdrVisuals = document.getElementById('websdrVisuals'); const websdrVisuals = document.getElementById('websdrVisuals');
const subghzVisuals = document.getElementById('subghzVisuals'); const subghzVisuals = document.getElementById('subghzVisuals');
const btLocateVisuals = document.getElementById('btLocateVisuals'); const btLocateVisuals = document.getElementById('btLocateVisuals');
const spaceWeatherVisuals = document.getElementById('spaceWeatherVisuals');
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none'; if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none';
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none'; if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none';
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none'; if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none';
@@ -3810,6 +3956,7 @@
if (websdrVisuals) websdrVisuals.style.display = mode === 'websdr' ? 'flex' : 'none'; if (websdrVisuals) websdrVisuals.style.display = mode === 'websdr' ? 'flex' : 'none';
if (subghzVisuals) subghzVisuals.style.display = mode === 'subghz' ? 'flex' : 'none'; if (subghzVisuals) subghzVisuals.style.display = mode === 'subghz' ? 'flex' : 'none';
if (btLocateVisuals) btLocateVisuals.style.display = mode === 'bt_locate' ? 'flex' : 'none'; if (btLocateVisuals) btLocateVisuals.style.display = mode === 'bt_locate' ? 'flex' : 'none';
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
// Hide sidebar by default for Meshtastic mode, show for others // Hide sidebar by default for Meshtastic mode, show for others
const mainContent = document.querySelector('.main-content'); const mainContent = document.querySelector('.main-content');
@@ -3851,7 +3998,8 @@
'dmr': 'Digital Voice Decoder', 'dmr': 'Digital Voice Decoder',
'websdr': 'HF/Shortwave WebSDR', 'websdr': 'HF/Shortwave WebSDR',
'subghz': 'SubGHz Transceiver', 'subghz': 'SubGHz Transceiver',
'analytics': 'Cross-Mode Analytics' 'analytics': 'Cross-Mode Analytics',
'spaceweather': 'Space Weather Monitor'
}; };
const outputTitle = document.getElementById('outputTitle'); const outputTitle = document.getElementById('outputTitle');
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor'; if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
@@ -3874,11 +4022,16 @@
if (typeof Analytics !== 'undefined' && Analytics.destroy) Analytics.destroy(); if (typeof Analytics !== 'undefined' && Analytics.destroy) Analytics.destroy();
} }
// Initialize/destroy Space Weather mode
if (mode !== 'spaceweather') {
if (typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy) SpaceWeather.destroy();
}
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm) // Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
const reconBtn = document.getElementById('reconBtn'); const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]'); const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
const reconPanel = document.getElementById('reconPanel'); const reconPanel = document.getElementById('reconPanel');
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'analytics') { if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'analytics' || mode === 'spaceweather') {
if (reconPanel) reconPanel.style.display = 'none'; if (reconPanel) reconPanel.style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none'; if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none'; if (intelBtn) intelBtn.style.display = 'none';
@@ -3916,8 +4069,8 @@
// Hide output console for modes with their own visualizations // Hide output console for modes with their own visualizations
const outputEl = document.getElementById('output'); const outputEl = document.getElementById('output');
const statusBar = document.querySelector('.status-bar'); const statusBar = document.querySelector('.status-bar');
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'listening' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'analytics') ? 'none' : 'block'; if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'listening' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'analytics' || mode === 'spaceweather') ? 'none' : 'block';
if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'dmr' || mode === 'subghz') ? 'none' : 'flex'; if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'dmr' || mode === 'subghz' || mode === 'spaceweather') ? 'none' : 'flex';
// Restore sidebar when leaving Meshtastic mode (user may have collapsed it) // Restore sidebar when leaving Meshtastic mode (user may have collapsed it)
if (mode !== 'meshtastic') { if (mode !== 'meshtastic') {
@@ -3983,6 +4136,8 @@
SubGhz.init(); SubGhz.init();
} else if (mode === 'bt_locate') { } else if (mode === 'bt_locate') {
BtLocate.init(); BtLocate.init();
} else if (mode === 'spaceweather') {
SpaceWeather.init();
} }
} }

View File

@@ -57,6 +57,7 @@
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span><span class="desc">Weather Sat - NOAA &amp; Meteor imagery</span></div> <div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span><span class="desc">Weather Sat - NOAA &amp; Meteor imagery</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span><span class="desc">HF SSTV - Shortwave image decoder</span></div> <div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span><span class="desc">HF SSTV - Shortwave image decoder</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span><span class="desc">GPS - GNSS signal analysis</span></div> <div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span><span class="desc">GPS - GNSS signal analysis</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span><span class="desc">Space Weather - Solar &amp; geomagnetic data</span></div>
</div> </div>
</div> </div>
@@ -197,6 +198,15 @@
<li>Useful for evaluating GNSS reception and interference</li> <li>Useful for evaluating GNSS reception and interference</li>
</ul> </ul>
<h3>Space Weather Mode</h3>
<ul class="tip-list">
<li>Real-time solar and geomagnetic data from NOAA SWPC, NASA SDO, and HamQSL</li>
<li>Solar indices (SFI, Kp, A-index, sunspot number) and NOAA G/S/R scales</li>
<li>HF band conditions, X-ray flux, solar wind speed, and flare probability</li>
<li>Solar imagery (SDO 193/304/Magnetogram), D-RAP absorption maps, aurora forecast</li>
<li>No SDR hardware required &mdash; all data from public APIs</li>
</ul>
<h3>WiFi Mode</h3> <h3>WiFi Mode</h3>
<ul class="tip-list"> <ul class="tip-list">
<li>Requires a WiFi adapter capable of monitor mode</li> <li>Requires a WiFi adapter capable of monitor mode</li>
@@ -330,6 +340,7 @@
<li><strong>Weather Sat:</strong> RTL-SDR, SatDump</li> <li><strong>Weather Sat:</strong> RTL-SDR, SatDump</li>
<li><strong>HF SSTV:</strong> RTL-SDR or SoapySDR-compatible hardware, slowrx</li> <li><strong>HF SSTV:</strong> RTL-SDR or SoapySDR-compatible hardware, slowrx</li>
<li><strong>GPS:</strong> RTL-SDR or GPS-capable SDR</li> <li><strong>GPS:</strong> RTL-SDR or GPS-capable SDR</li>
<li><strong>Space Weather:</strong> Internet connection (public APIs)</li>
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li> <li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li> <li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
<li><strong>BT Locate:</strong> Bluetooth adapter, bluez</li> <li><strong>BT Locate:</strong> Bluetooth adapter, bluez</li>

View File

@@ -0,0 +1,71 @@
<!-- SPACE WEATHER MODE -->
<div id="spaceWeatherMode" class="mode-content">
<div class="section">
<h3>Space Weather Monitor</h3>
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 12px;">
Real-time solar and geomagnetic data from NOAA SWPC, NASA SDO, and HamQSL.
No SDR hardware required — data is fetched from public APIs.
</p>
</div>
<div class="section">
<h3>Quick Status</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 11px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; padding: 4px 6px; background: var(--bg-primary); border-radius: 3px;">
<span style="color: var(--text-dim);">SFI</span>
<span id="swSidebarSfi" style="color: var(--accent-cyan); font-weight: 600;">--</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 4px 6px; background: var(--bg-primary); border-radius: 3px;">
<span style="color: var(--text-dim);">Kp</span>
<span id="swSidebarKp" style="color: var(--accent-cyan); font-weight: 600;">--</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 4px 6px; background: var(--bg-primary); border-radius: 3px;">
<span style="color: var(--text-dim);">A-Index</span>
<span id="swSidebarA" style="color: var(--accent-cyan); font-weight: 600;">--</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 4px 6px; background: var(--bg-primary); border-radius: 3px;">
<span style="color: var(--text-dim);">SSN</span>
<span id="swSidebarSsn" style="color: var(--accent-cyan); font-weight: 600;">--</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 4px 6px; background: var(--bg-primary); border-radius: 3px;">
<span style="color: var(--text-dim);">Wind</span>
<span id="swSidebarWind" style="color: var(--accent-cyan); font-weight: 600;">--</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 4px 6px; background: var(--bg-primary); border-radius: 3px;">
<span style="color: var(--text-dim);">Bz</span>
<span id="swSidebarBz" style="color: var(--accent-cyan); font-weight: 600;">--</span>
</div>
</div>
</div>
<div class="section">
<h3>Refresh</h3>
<button class="mode-btn" onclick="SpaceWeather.refresh()" style="width: 100%; margin-bottom: 8px;">
Refresh Now
</button>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="swAutoRefresh" checked onchange="SpaceWeather.toggleAutoRefresh()" style="width: auto;">
Auto-refresh (5 min)
</label>
</div>
<div id="swLastUpdate" style="font-size: 10px; color: var(--text-dim); font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; margin-top: 4px;">
Not yet loaded
</div>
</div>
<div class="section">
<h3>Resources</h3>
<div style="display: flex; flex-direction: column; gap: 6px;">
<a href="https://www.swpc.noaa.gov/" target="_blank" rel="noopener" class="preset-btn" style="text-decoration: none; text-align: center;">
NOAA Space Weather
</a>
<a href="https://sdo.gsfc.nasa.gov/" target="_blank" rel="noopener" class="preset-btn" style="text-decoration: none; text-align: center;">
NASA SDO
</a>
<a href="https://www.hamqsl.com/solar.html" target="_blank" rel="noopener" class="preset-btn" style="text-decoration: none; text-align: center;">
HamQSL Solar Data
</a>
</div>
</div>
</div>

View File

@@ -125,6 +125,7 @@
{{ mode_item('weathersat', 'Weather Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }} {{ mode_item('weathersat', 'Weather Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }}
{{ mode_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }} {{ mode_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
{{ mode_item('gps', 'GPS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }} {{ mode_item('gps', 'GPS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }}
{{ mode_item('spaceweather', 'Space Weather', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>') }}
</div> </div>
</div> </div>
@@ -198,6 +199,7 @@
{{ mobile_item('weathersat', 'WxSat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }} {{ mobile_item('weathersat', 'WxSat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }}
{{ mobile_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }} {{ mobile_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }}
{{ mobile_item('gps', 'GPS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }} {{ mobile_item('gps', 'GPS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }}
{{ mobile_item('spaceweather', 'SpaceWx', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/></svg>') }}
{{ mobile_item('listening', 'Scanner', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }} {{ mobile_item('listening', 'Scanner', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
{{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }} {{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
{{ mobile_item('meshtastic', 'Mesh', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }} {{ mobile_item('meshtastic', 'Mesh', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}