From 6281a1c81e48efcb0002ddfd1a0ecaf02a6e1d5c Mon Sep 17 00:00:00 2001 From: accius Date: Fri, 30 Jan 2026 01:23:48 -0500 Subject: [PATCH] Add VOACAP-style HF propagation predictions --- public/index.html | 269 +++++++++++++++++++++++++++++++++++++++++----- server.js | 182 +++++++++++++++++++++++++++++++ 2 files changed, 423 insertions(+), 28 deletions(-) diff --git a/public/index.html b/public/index.html index 7204894..004881a 100644 --- a/public/index.html +++ b/public/index.html @@ -767,6 +767,205 @@ return date; }; + // ============================================ + // PROPAGATION PREDICTION HOOK + // ============================================ + const usePropagation = (deLocation, dxLocation) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPropagation = async () => { + try { + const response = await fetch( + `/api/propagation?deLat=${deLocation.lat}&deLon=${deLocation.lon}&dxLat=${dxLocation.lat}&dxLon=${dxLocation.lon}` + ); + + if (response.ok) { + const result = await response.json(); + setData(result); + } + } catch (err) { + console.error('Propagation fetch error:', err); + } finally { + setLoading(false); + } + }; + + fetchPropagation(); + // Refresh every 15 minutes + const interval = setInterval(fetchPropagation, 900000); + return () => clearInterval(interval); + }, [deLocation.lat, deLocation.lon, dxLocation.lat, dxLocation.lon]); + + return { data, loading }; + }; + + // ============================================ + // PROPAGATION PANEL COMPONENT + // ============================================ + const PropagationPanel = ({ propagation, loading }) => { + if (loading || !propagation) { + return ( +
+
📡 HF Propagation
+
+ Loading predictions... +
+
+ ); + } + + const { solarData, distance, currentBands, currentHour, hourlyPredictions } = propagation; + + // Get status color + const getStatusColor = (status) => { + switch (status) { + case 'EXCELLENT': return '#00ff88'; + case 'GOOD': return '#88ff00'; + case 'FAIR': return '#ffcc00'; + case 'POOR': return '#ff8800'; + case 'CLOSED': return '#ff4444'; + default: return 'var(--text-muted)'; + } + }; + + // Get reliability bar color + const getReliabilityColor = (rel) => { + if (rel >= 70) return '#00ff88'; + if (rel >= 50) return '#88ff00'; + if (rel >= 30) return '#ffcc00'; + if (rel >= 15) return '#ff8800'; + return '#ff4444'; + }; + + return ( +
+
+ 📡 HF Propagation to DX + + {Math.round(distance).toLocaleString()} km + +
+ + {/* Solar Conditions Summary */} +
+
+
SFI
+
{solarData.sfi}
+
+
+
SSN
+
{solarData.ssn}
+
+
+
K
+
= 4 ? '#ff4444' : solarData.kIndex >= 3 ? '#ffcc00' : '#00ff88', + fontWeight: 'bold' + }}>{solarData.kIndex}
+
+
+ + {/* Band Predictions */} +
+
+ BAND + RELIABILITY + STATUS +
+ + {currentBands.slice(0, 8).map((band, idx) => ( +
+ = 50 ? 'var(--color-primary)' : 'var(--text-muted)' + }}> + {band.band} + +
+
+
+ 50 ? '#000' : 'var(--text-muted)' + }}> + {band.reliability}% + +
+ + {band.status} + +
+ ))} +
+ + {/* Footer */} +
+ Predictions at {String(currentHour).padStart(2, '0')}:00 UTC • Based on current solar conditions +
+
+ ); + }; + // ============================================ // LEAFLET MAP COMPONENT // ============================================ @@ -1277,7 +1476,7 @@ const LegacyLayout = ({ config, currentTime, utcTime, utcDate, localTime, localDate, deGrid, dxGrid, deSunTimes, dxSunTimes, dxLocation, onDXChange, - spaceWeather, bandConditions, potaSpots, dxCluster, contests, + spaceWeather, bandConditions, potaSpots, dxCluster, contests, propagation, onSettingsClick }) => { const bearing = calculateBearing(config.location.lat, config.location.lon, dxLocation.lat, dxLocation.lon); @@ -1488,6 +1687,44 @@ ))}
+ + {/* Propagation */} + {propagation.data && ( +
+
📡 PROPAGATION
+
+ {propagation.data.currentBands.slice(0, 6).map((b, i) => ( +
+ = 50 ? 'var(--accent-green)' : 'var(--text-muted)' }}>{b.band} +
+
= 70 ? '#00ff88' : b.reliability >= 50 ? '#88ff00' : b.reliability >= 30 ? '#ffcc00' : '#ff8800', + borderRadius: '2px' + }} /> +
+ = 70 ? '#00ff88' : b.reliability >= 50 ? '#88ff00' : b.reliability >= 30 ? '#ffcc00' : '#ff8800' + }}>{b.status.substring(0, 4)} +
+ ))} +
+
+ )}
{/* BOTTOM - Footer */} @@ -1847,6 +2084,7 @@ const potaSpots = usePOTASpots(); const dxCluster = useDXCluster(); const contests = useContests(); + const propagation = usePropagation(config.location, dxLocation); const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]); const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]); @@ -1896,6 +2134,7 @@ potaSpots={potaSpots} dxCluster={dxCluster} contests={contests} + propagation={propagation} onSettingsClick={() => setShowSettings(true)} /> -
-
- 📊 QUICK STATS -
-
-
- Active Contests - {contests.data.filter(c => c.status === 'active').length} -
-
- POTA Activators - {potaSpots.data.length} -
-
- DX Spots - {dxCluster.data.length} -
-
- Solar Flux - {spaceWeather.data?.solarFlux || '--'} -
-
- Uptime - {uptime} -
-
-
+