diff --git a/public/index.html b/public/index.html index f54457b..72ca4ee 100644 --- a/public/index.html +++ b/public/index.html @@ -552,6 +552,34 @@ return { data, loading }; }; + // Solar Indices with History and Kp Forecast Hook + const useSolarIndices = () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/api/solar-indices'); + if (response.ok) { + const result = await response.json(); + setData(result); + } + } catch (err) { + console.error('Solar indices error:', err); + } finally { + setLoading(false); + } + }; + fetchData(); + // Refresh every 15 minutes + const interval = setInterval(fetchData, 15 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + return { data, loading }; + }; + const useBandConditions = (spaceWeather) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -2397,13 +2425,205 @@ ); }; + // ============================================ + // SOLAR INDICES PANEL + // ============================================ + const SolarIndicesPanel = ({ data, loading }) => { + if (loading || !data) { + return ( +
+
☀️ Solar Indices
+
+ Loading solar data... +
+
+ ); + } + + const { sfi, kp, ssn } = data; + + // Get Kp color based on value + const getKpColor = (value) => { + if (value >= 7) return '#ff0000'; // Severe storm + if (value >= 5) return '#ff6600'; // Storm + if (value >= 4) return '#ffcc00'; // Active + if (value >= 3) return '#88cc00'; // Unsettled + return '#00ff88'; // Quiet + }; + + // Get Kp label + const getKpLabel = (value) => { + if (value >= 7) return 'SEVERE'; + if (value >= 5) return 'STORM'; + if (value >= 4) return 'ACTIVE'; + if (value >= 3) return 'UNSETTLED'; + return 'QUIET'; + }; + + // Mini sparkline chart component + const Sparkline = ({ data, color, height = 30, showForecast = false, forecastData = [] }) => { + if (!data || data.length === 0) return null; + + const allData = showForecast ? [...data, ...forecastData] : data; + const values = allData.map(d => d.value); + const max = Math.max(...values, 1); + const min = Math.min(...values, 0); + const range = max - min || 1; + + const width = 100; + const points = allData.map((d, i) => { + const x = (i / (allData.length - 1)) * width; + const y = height - ((d.value - min) / range) * height; + return `${x},${y}`; + }).join(' '); + + // Split point between history and forecast + const historyEndIndex = data.length - 1; + const historyPoints = data.map((d, i) => { + const x = (i / (allData.length - 1)) * width; + const y = height - ((d.value - min) / range) * height; + return `${x},${y}`; + }).join(' '); + + const forecastPoints = showForecast && forecastData.length > 0 ? + [data[data.length - 1], ...forecastData].map((d, i) => { + const x = ((historyEndIndex + i) / (allData.length - 1)) * width; + const y = height - ((d.value - min) / range) * height; + return `${x},${y}`; + }).join(' ') : ''; + + return ( + + {/* History line */} + + {/* Forecast line (dashed) */} + {showForecast && forecastPoints && ( + + )} + + ); + }; + + // Mini bar chart for Kp + const KpBars = ({ history, forecast }) => { + const allData = [...(history || []), ...(forecast || []).slice(0, 8)]; + if (allData.length === 0) return null; + + const barWidth = 100 / allData.length; + const height = 35; + + return ( + + {allData.map((d, i) => { + const isForecast = i >= (history?.length || 0); + const barHeight = (d.value / 9) * height; + return ( + + ); + })} + {/* Divider line between history and forecast */} + {forecast && forecast.length > 0 && history && ( + + )} + + ); + }; + + return ( +
+
+ ☀️ Solar Indices + NOAA/SWPC +
+ +
+ {/* SFI */} +
+
SFI
+
+ {sfi.current || '--'} +
+
+ +
+
30 day history
+
+ + {/* SSN */} +
+
Sunspots
+
+ {ssn.current || '--'} +
+
+ +
+
12 month history
+
+ + {/* Kp Index */} +
+
Kp Index
+
+ + {kp.current !== null ? kp.current.toFixed(1) : '--'} + + + {kp.current !== null ? getKpLabel(kp.current) : ''} + +
+
+ +
+
+ 3 day history + forecast +
+
+
+
+ ); + }; + // ============================================ // LEGACY LAYOUT (Classic HamClock Style) // ============================================ const LegacyLayout = ({ config, currentTime, utcTime, utcDate, localTime, localDate, deGrid, dxGrid, deSunTimes, dxSunTimes, dxLocation, onDXChange, - spaceWeather, bandConditions, potaSpots, dxCluster, dxPaths, contests, propagation, mySpots, satellites, + spaceWeather, bandConditions, solarIndices, potaSpots, dxCluster, dxPaths, contests, propagation, mySpots, satellites, localWeather, use12Hour, onTimeFormatToggle, onSettingsClick, onFullscreenToggle, isFullscreen, mapLayers, toggleDXPaths, togglePOTA, toggleSatellites @@ -2487,27 +2707,107 @@ - {/* TOP RIGHT - Space Weather */} -
-
☀ SOLAR CONDITIONS
-
-
-
SFI
-
{spaceWeather.data?.solarFlux || '--'}
-
-
-
K-Index
-
4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '700' }}>{spaceWeather.data?.kIndex || '-'}
-
-
-
SSN
-
{spaceWeather.data?.sunspotNumber || '--'}
+ {/* TOP RIGHT - Solar Indices with Mini Charts */} +
+
☀ SOLAR INDICES
+ {solarIndices.data ? ( +
+ {/* SFI */} +
+
SFI
+
+ {solarIndices.data.sfi?.current || '--'} +
+ {solarIndices.data.sfi?.history?.length > 0 && ( + + {(() => { + const data = solarIndices.data.sfi.history.slice(-14); + const values = data.map(d => d.value); + const max = Math.max(...values, 1); + const min = Math.min(...values, 0); + const range = max - min || 1; + const points = data.map((d, i) => { + const x = (i / (data.length - 1)) * 50; + const y = 18 - ((d.value - min) / range) * 18; + return `${x},${y}`; + }).join(' '); + return ; + })()} + + )} +
+ {/* SSN */} +
+
SSN
+
+ {solarIndices.data.ssn?.current || '--'} +
+ {solarIndices.data.ssn?.history?.length > 0 && ( + + {(() => { + const data = solarIndices.data.ssn.history; + const values = data.map(d => d.value); + const max = Math.max(...values, 1); + const min = Math.min(...values, 0); + const range = max - min || 1; + const points = data.map((d, i) => { + const x = (i / (data.length - 1)) * 50; + const y = 18 - ((d.value - min) / range) * 18; + return `${x},${y}`; + }).join(' '); + return ; + })()} + + )} +
+ {/* Kp */} +
+
Kp
+
= 5 ? '#ff6600' : solarIndices.data.kp?.current >= 4 ? '#ffcc00' : '#00ff88' + }}> + {solarIndices.data.kp?.current?.toFixed(1) || '--'} +
+ {solarIndices.data.kp?.history?.length > 0 && ( + + {(() => { + const hist = solarIndices.data.kp.history.slice(-12); + const forecast = (solarIndices.data.kp.forecast || []).slice(0, 4); + const allData = [...hist, ...forecast]; + const barWidth = 50 / allData.length; + return allData.map((d, i) => { + const isForecast = i >= hist.length; + const barHeight = (d.value / 9) * 18; + const color = d.value >= 5 ? '#ff6600' : d.value >= 4 ? '#ffcc00' : '#00ff88'; + return ( + + ); + }); + })()} + + )} +
-
-
Conditions
-
{spaceWeather.data?.conditions || '--'}
+ ) : ( +
+
+
SFI
+
{spaceWeather.data?.solarFlux || '--'}
+
+
+
Kp
+
4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '700' }}>{spaceWeather.data?.kIndex || '-'}
+
-
+ )}
{/* LEFT SIDEBAR - DE/DX Info */} @@ -3192,6 +3492,7 @@ const spaceWeather = useSpaceWeather(); const bandConditions = useBandConditions(spaceWeather.data); + const solarIndices = useSolarIndices(); const potaSpots = usePOTASpots(); const dxCluster = useDXCluster(config.dxClusterSource || 'auto'); const dxPaths = useDXPaths(); @@ -3271,6 +3572,7 @@ onDXChange={handleDXChange} spaceWeather={spaceWeather} bandConditions={bandConditions} + solarIndices={solarIndices} potaSpots={potaSpots} dxCluster={dxCluster} dxPaths={dxPaths} @@ -3466,6 +3768,9 @@ {/* VOACAP Propagation - Toggleable */} + + {/* Solar Indices with History */} +
{/* CENTER - MAP */} diff --git a/server.js b/server.js index 7f2e4b0..2d07df9 100644 --- a/server.js +++ b/server.js @@ -67,6 +67,84 @@ app.get('/api/noaa/sunspots', async (req, res) => { } }); +// Solar Indices with History and Kp Forecast +app.get('/api/solar-indices', async (req, res) => { + try { + const [fluxRes, kIndexRes, kForecastRes, sunspotRes] = await Promise.allSettled([ + fetch('https://services.swpc.noaa.gov/json/f107_cm_flux.json'), + fetch('https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json'), + fetch('https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json'), + fetch('https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json') + ]); + + const result = { + sfi: { current: null, history: [] }, + kp: { current: null, history: [], forecast: [] }, + ssn: { current: null, history: [] }, + timestamp: new Date().toISOString() + }; + + // Process SFI data (last 30 days) + if (fluxRes.status === 'fulfilled' && fluxRes.value.ok) { + const data = await fluxRes.value.json(); + if (data?.length) { + // Get last 30 entries + const recent = data.slice(-30); + result.sfi.history = recent.map(d => ({ + date: d.time_tag || d.date, + value: Math.round(d.flux || d.value || 0) + })); + result.sfi.current = result.sfi.history[result.sfi.history.length - 1]?.value || null; + } + } + + // Process Kp history (last 3 days, data comes in 3-hour intervals) + if (kIndexRes.status === 'fulfilled' && kIndexRes.value.ok) { + const data = await kIndexRes.value.json(); + if (data?.length > 1) { + // Skip header row, get last 24 entries (3 days) + const recent = data.slice(1).slice(-24); + result.kp.history = recent.map(d => ({ + time: d[0], + value: parseFloat(d[1]) || 0 + })); + result.kp.current = result.kp.history[result.kp.history.length - 1]?.value || null; + } + } + + // Process Kp forecast + if (kForecastRes.status === 'fulfilled' && kForecastRes.value.ok) { + const data = await kForecastRes.value.json(); + if (data?.length > 1) { + // Skip header row + result.kp.forecast = data.slice(1).map(d => ({ + time: d[0], + value: parseFloat(d[1]) || 0 + })); + } + } + + // Process Sunspot data (last 12 months) + if (sunspotRes.status === 'fulfilled' && sunspotRes.value.ok) { + const data = await sunspotRes.value.json(); + if (data?.length) { + // Get last 12 entries (monthly data) + const recent = data.slice(-12); + result.ssn.history = recent.map(d => ({ + date: `${d['time-tag'] || d.time_tag || ''}`, + value: Math.round(d.ssn || 0) + })); + result.ssn.current = result.ssn.history[result.ssn.history.length - 1]?.value || null; + } + } + + res.json(result); + } catch (error) { + console.error('Solar Indices API error:', error.message); + res.status(500).json({ error: 'Failed to fetch solar indices' }); + } +}); + // NOAA Space Weather - X-Ray Flux app.get('/api/noaa/xray', async (req, res) => { try {