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}
-
-
-
+