diff --git a/server.js b/server.js index 3327460..213b9d0 100644 --- a/server.js +++ b/server.js @@ -285,6 +285,7 @@ const noaaCache = { kindex: { data: null, timestamp: 0 }, sunspots: { data: null, timestamp: 0 }, xray: { data: null, timestamp: 0 }, + aurora: { data: null, timestamp: 0 }, solarIndices: { data: null, timestamp: 0 } }; const NOAA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes @@ -670,15 +671,38 @@ app.get('/api/dxpeditions', async (req, res) => { // NOAA Space Weather - X-Ray Flux app.get('/api/noaa/xray', async (req, res) => { try { - const response = await fetch('https://services.swpc.noaa.gov/json/goes/primary/xrays-7-day.json'); + if (noaaCache.xray.data && (Date.now() - noaaCache.xray.timestamp) < NOAA_CACHE_TTL) { + return res.json(noaaCache.xray.data); + } + const response = await fetch('https://services.swpc.noaa.gov/json/goes/primary/xrays-6-hour.json'); const data = await response.json(); + noaaCache.xray = { data, timestamp: Date.now() }; res.json(data); } catch (error) { console.error('NOAA X-Ray API error:', error.message); + if (noaaCache.xray.data) return res.json(noaaCache.xray.data); res.status(500).json({ error: 'Failed to fetch X-ray data' }); } }); +// NOAA OVATION Aurora Forecast +const AURORA_CACHE_TTL = 10 * 60 * 1000; // 10 minutes (NOAA updates every ~30 min) +app.get('/api/noaa/aurora', async (req, res) => { + try { + if (noaaCache.aurora.data && (Date.now() - noaaCache.aurora.timestamp) < AURORA_CACHE_TTL) { + return res.json(noaaCache.aurora.data); + } + const response = await fetch('https://services.swpc.noaa.gov/json/ovation_aurora_latest.json'); + const data = await response.json(); + noaaCache.aurora = { data, timestamp: Date.now() }; + res.json(data); + } catch (error) { + console.error('NOAA Aurora API error:', error.message); + if (noaaCache.aurora.data) return res.json(noaaCache.aurora.data); + res.status(500).json({ error: 'Failed to fetch aurora data' }); + } +}); + // POTA Spots // POTA cache (2 minutes) let potaCache = { data: null, timestamp: 0 }; diff --git a/src/components/SolarPanel.jsx b/src/components/SolarPanel.jsx index 762d360..cb98a78 100644 --- a/src/components/SolarPanel.jsx +++ b/src/components/SolarPanel.jsx @@ -1,25 +1,81 @@ /** * SolarPanel Component - * Toggleable between live sun image from NASA SDO and solar indices display + * Cycles between: Solar Image → Solar Indices → X-Ray Flux Chart */ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; + +const MODES = ['image', 'indices', 'xray']; +const MODE_LABELS = { image: 'SOLAR', indices: 'SOLAR INDICES', xray: 'X-RAY FLUX' }; +const MODE_ICONS = { image: '📊', indices: '📈', xray: '🖼️' }; +const MODE_TITLES = { image: 'Show solar indices', indices: 'Show X-ray flux', xray: 'Show solar image' }; + +// Flare class from flux value (W/m²) +const getFlareClass = (flux) => { + if (!flux || flux <= 0) return { letter: '?', color: '#666', level: 0 }; + if (flux >= 1e-4) return { letter: 'X', color: '#ff0000', level: 4 }; + if (flux >= 1e-5) return { letter: 'M', color: '#ff6600', level: 3 }; + if (flux >= 1e-6) return { letter: 'C', color: '#ffcc00', level: 2 }; + if (flux >= 1e-7) return { letter: 'B', color: '#00cc88', level: 1 }; + return { letter: 'A', color: '#4488ff', level: 0 }; +}; + +// Format flux value for display +const formatFlux = (flux) => { + if (!flux || flux <= 0) return '--'; + const cls = getFlareClass(flux); + const base = flux >= 1e-4 ? flux / 1e-4 : + flux >= 1e-5 ? flux / 1e-5 : + flux >= 1e-6 ? flux / 1e-6 : + flux >= 1e-7 ? flux / 1e-7 : flux / 1e-8; + return `${cls.letter}${base.toFixed(1)}`; +}; export const SolarPanel = ({ solarIndices }) => { - const [showIndices, setShowIndices] = useState(() => { + const [mode, setMode] = useState(() => { try { const saved = localStorage.getItem('openhamclock_solarPanelMode'); - return saved === 'indices'; - } catch (e) { return false; } + if (MODES.includes(saved)) return saved; + // Migrate old boolean format + if (saved === 'indices') return 'indices'; + return 'image'; + } catch (e) { return 'image'; } }); const [imageType, setImageType] = useState('0193'); + const [xrayData, setXrayData] = useState(null); + const [xrayLoading, setXrayLoading] = useState(false); - const toggleMode = () => { - const newMode = !showIndices; - setShowIndices(newMode); - try { - localStorage.setItem('openhamclock_solarPanelMode', newMode ? 'indices' : 'image'); - } catch (e) {} + const cycleMode = () => { + const nextIdx = (MODES.indexOf(mode) + 1) % MODES.length; + const next = MODES[nextIdx]; + setMode(next); + try { localStorage.setItem('openhamclock_solarPanelMode', next); } catch (e) {} }; + + // Fetch X-ray data when xray mode is active + const fetchXray = useCallback(async () => { + try { + setXrayLoading(true); + const res = await fetch('/api/noaa/xray'); + if (res.ok) { + const data = await res.json(); + // Filter to 0.1-0.8nm (long wavelength, standard for flare classification) + const filtered = data.filter(d => d.energy === '0.1-0.8nm' && d.flux > 0); + setXrayData(filtered); + } + } catch (err) { + console.error('X-ray fetch error:', err); + } finally { + setXrayLoading(false); + } + }, []); + + useEffect(() => { + if (mode === 'xray') { + fetchXray(); + const interval = setInterval(fetchXray, 5 * 60 * 1000); // 5 min refresh + return () => clearInterval(interval); + } + }, [mode, fetchXray]); const imageTypes = { '0193': { name: 'AIA 193Å', desc: 'Corona' }, @@ -40,12 +96,158 @@ export const SolarPanel = ({ solarIndices }) => { return '#00ff88'; }; - // Get K-Index data - server returns 'kp' not 'kIndex' const kpData = solarIndices?.data?.kp || solarIndices?.data?.kIndex; + // X-Ray flux chart renderer + const renderXrayChart = () => { + if (xrayLoading && !xrayData) { + return