Update index.html

pull/27/head
accius 4 days ago
parent 11d5c2c224
commit 293bfd91ab

@ -1455,9 +1455,25 @@
// ============================================ // ============================================
// SOLAR IMAGE COMPONENT // SOLAR IMAGE COMPONENT
// ============================================ // ============================================
const SolarImage = () => { // Combined Solar Panel - toggleable between image and indices
const SolarPanel = ({ solarIndices }) => {
const [showIndices, setShowIndices] = useState(() => {
try {
const saved = localStorage.getItem('openhamclock_solarPanelMode');
return saved === 'indices';
} catch (e) { return false; }
});
const [imageType, setImageType] = useState('0193'); // AIA 193 (corona) const [imageType, setImageType] = useState('0193'); // AIA 193 (corona)
// Save preference
const toggleMode = () => {
const newMode = !showIndices;
setShowIndices(newMode);
try {
localStorage.setItem('openhamclock_solarPanelMode', newMode ? 'indices' : 'image');
} catch (e) {}
};
// SDO/AIA image types // SDO/AIA image types
const imageTypes = { const imageTypes = {
'0193': { name: 'AIA 193Å', desc: 'Corona' }, '0193': { name: 'AIA 193Å', desc: 'Corona' },
@ -1468,18 +1484,40 @@
}; };
// SDO images update every ~15 minutes // SDO images update every ~15 minutes
const timestamp = Math.floor(Date.now() / 900000) * 900000; // Round to 15 min const timestamp = Math.floor(Date.now() / 900000) * 900000;
const imageUrl = `https://sdo.gsfc.nasa.gov/assets/img/latest/latest_256_${imageType}.jpg?t=${timestamp}`; const imageUrl = `https://sdo.gsfc.nasa.gov/assets/img/latest/latest_256_${imageType}.jpg?t=${timestamp}`;
// Kp color helper
const getKpColor = (value) => {
if (value >= 7) return '#ff0000';
if (value >= 5) return '#ff6600';
if (value >= 4) return '#ffcc00';
if (value >= 3) return '#88cc00';
return '#00ff88';
};
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';
};
return ( return (
<div className="panel" style={{ padding: '8px' }}> <div className="panel" style={{ padding: '8px' }}>
{/* Header with toggle */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: '6px' marginBottom: '6px'
}}> }}>
<span style={{ fontSize: '12px', color: 'var(--accent-amber)', fontWeight: '700' }}>☀ SOLAR</span> <span style={{ fontSize: '12px', color: 'var(--accent-amber)', fontWeight: '700' }}>
☀ {showIndices ? 'SOLAR INDICES' : 'SOLAR'}
</span>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
{!showIndices && (
<select <select
value={imageType} value={imageType}
onChange={(e) => setImageType(e.target.value)} onChange={(e) => setImageType(e.target.value)}
@ -1488,7 +1526,7 @@
background: 'var(--bg-tertiary)', background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)', border: '1px solid var(--border-color)',
color: 'var(--text-secondary)', color: 'var(--text-secondary)',
fontSize: '11px', fontSize: '10px',
padding: '2px 4px', padding: '2px 4px',
borderRadius: '3px', borderRadius: '3px',
cursor: 'pointer' cursor: 'pointer'
@ -1498,11 +1536,148 @@
<option key={key} value={key}>{val.desc}</option> <option key={key} value={key}>{val.desc}</option>
))} ))}
</select> </select>
)}
<button
onClick={toggleMode}
style={{
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
color: 'var(--text-secondary)',
fontSize: '10px',
padding: '2px 6px',
borderRadius: '3px',
cursor: 'pointer'
}}
title={showIndices ? 'Show solar image' : 'Show solar indices'}
>
{showIndices ? '🖼️' : '📊'}
</button>
</div>
</div>
{showIndices ? (
/* Solar Indices View */
<div>
{solarIndices?.data ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{/* SFI Row */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: '6px', padding: '8px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ minWidth: '60px' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>SFI</div>
<div style={{ fontSize: '22px', fontWeight: '700', color: '#ff8800', fontFamily: 'Orbitron, monospace' }}>
{solarIndices.data.sfi?.current || '--'}
</div>
</div>
<div style={{ flex: 1 }}>
{solarIndices.data.sfi?.history?.length > 0 && (
<svg width="100%" height="30" viewBox="0 0 100 30" preserveAspectRatio="none">
{(() => {
const data = solarIndices.data.sfi.history.slice(-20);
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)) * 100;
const y = 30 - ((d.value - min) / range) * 28;
return `${x},${y}`;
}).join(' ');
return <polyline points={points} fill="none" stroke="#ff8800" strokeWidth="2" />;
})()}
</svg>
)}
<div style={{ fontSize: '9px', color: 'var(--text-muted)' }}>30 day history</div>
</div>
</div>
{/* SSN Row */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: '6px', padding: '8px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ minWidth: '60px' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Sunspots</div>
<div style={{ fontSize: '22px', fontWeight: '700', color: '#ffcc00', fontFamily: 'Orbitron, monospace' }}>
{solarIndices.data.ssn?.current || '--'}
</div>
</div>
<div style={{ flex: 1 }}>
{solarIndices.data.ssn?.history?.length > 0 && (
<svg width="100%" height="30" viewBox="0 0 100 30" preserveAspectRatio="none">
{(() => {
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)) * 100;
const y = 30 - ((d.value - min) / range) * 28;
return `${x},${y}`;
}).join(' ');
return <polyline points={points} fill="none" stroke="#ffcc00" strokeWidth="2" />;
})()}
</svg>
)}
<div style={{ fontSize: '9px', color: 'var(--text-muted)' }}>12 month history</div>
</div>
</div>
{/* Kp Row */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: '6px', padding: '8px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ minWidth: '60px' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Kp Index</div>
<div style={{ fontSize: '22px', fontWeight: '700', fontFamily: 'Orbitron, monospace', color: getKpColor(solarIndices.data.kp?.current) }}>
{solarIndices.data.kp?.current?.toFixed(1) || '--'}
</div>
<div style={{ fontSize: '9px', color: getKpColor(solarIndices.data.kp?.current) }}>
{solarIndices.data.kp?.current !== null ? getKpLabel(solarIndices.data.kp?.current) : ''}
</div>
</div>
<div style={{ flex: 1 }}>
{solarIndices.data.kp?.history?.length > 0 && (
<svg width="100%" height="35" viewBox="0 0 100 35" preserveAspectRatio="none">
{(() => {
const hist = solarIndices.data.kp.history.slice(-16);
const forecast = (solarIndices.data.kp.forecast || []).slice(0, 8);
const allData = [...hist, ...forecast];
const barWidth = 100 / allData.length;
return allData.map((d, i) => {
const isForecast = i >= hist.length;
const barHeight = (d.value / 9) * 32;
return (
<rect
key={i}
x={i * barWidth + 0.5}
y={35 - barHeight}
width={barWidth - 1}
height={barHeight}
fill={getKpColor(d.value)}
opacity={isForecast ? 0.4 : 0.9}
rx="1"
/>
);
});
})()}
</svg>
)}
<div style={{ fontSize: '9px', color: 'var(--text-muted)' }}>3 day history + forecast</div>
</div>
</div>
</div> </div>
) : (
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-muted)' }}>
Loading solar data...
</div>
)}
<div style={{ textAlign: 'center', marginTop: '4px', fontSize: '9px', color: 'var(--text-muted)' }}>
NOAA/SWPC
</div>
</div>
) : (
/* Solar Image View */
<div>
<div style={{ <div style={{
position: 'relative', position: 'relative',
width: '100%', width: '100%',
paddingBottom: '100%', // Square aspect ratio paddingBottom: '100%',
background: '#000', background: '#000',
borderRadius: '50%', borderRadius: '50%',
overflow: 'hidden' overflow: 'hidden'
@ -1533,6 +1708,8 @@
SDO/AIA • Live from NASA SDO/AIA • Live from NASA
</div> </div>
</div> </div>
)}
</div>
); );
}; };
@ -2425,198 +2602,6 @@
); );
}; };
// ============================================
// SOLAR INDICES PANEL
// ============================================
const SolarIndicesPanel = ({ data, loading }) => {
if (loading || !data) {
return (
<div className="panel">
<div className="panel-header">☀️ Solar Indices</div>
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-muted)' }}>
Loading solar data...
</div>
</div>
);
}
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 (
<svg width={width} height={height} style={{ display: 'block' }}>
{/* History line */}
<polyline
points={historyPoints}
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Forecast line (dashed) */}
{showForecast && forecastPoints && (
<polyline
points={forecastPoints}
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="4,2"
opacity="0.6"
/>
)}
</svg>
);
};
// 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 (
<svg width={100} height={height} style={{ display: 'block' }}>
{allData.map((d, i) => {
const isForecast = i >= (history?.length || 0);
const barHeight = (d.value / 9) * height;
return (
<rect
key={i}
x={i * barWidth + 1}
y={height - barHeight}
width={barWidth - 2}
height={barHeight}
fill={getKpColor(d.value)}
opacity={isForecast ? 0.5 : 1}
rx="1"
/>
);
})}
{/* Divider line between history and forecast */}
{forecast && forecast.length > 0 && history && (
<line
x1={(history.length / allData.length) * 100}
y1="0"
x2={(history.length / allData.length) * 100}
y2={height}
stroke="#666"
strokeWidth="1"
strokeDasharray="2,2"
/>
)}
</svg>
);
};
return (
<div className="panel" style={{ padding: '12px' }}>
<div className="panel-header" style={{ marginBottom: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>☀️ Solar Indices</span>
<span style={{ fontSize: '10px', color: 'var(--text-muted)' }}>NOAA/SWPC</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px' }}>
{/* SFI */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: '6px', padding: '8px' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '4px' }}>SFI</div>
<div style={{ fontSize: '20px', fontWeight: '700', color: '#ff8800', fontFamily: 'Orbitron, monospace' }}>
{sfi.current || '--'}
</div>
<div style={{ marginTop: '6px' }}>
<Sparkline data={sfi.history} color="#ff8800" height={25} />
</div>
<div style={{ fontSize: '9px', color: 'var(--text-muted)', marginTop: '2px' }}>30 day history</div>
</div>
{/* SSN */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: '6px', padding: '8px' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '4px' }}>Sunspots</div>
<div style={{ fontSize: '20px', fontWeight: '700', color: '#ffcc00', fontFamily: 'Orbitron, monospace' }}>
{ssn.current || '--'}
</div>
<div style={{ marginTop: '6px' }}>
<Sparkline data={ssn.history} color="#ffcc00" height={25} />
</div>
<div style={{ fontSize: '9px', color: 'var(--text-muted)', marginTop: '2px' }}>12 month history</div>
</div>
{/* Kp Index */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: '6px', padding: '8px' }}>
<div style={{ fontSize: '10px', color: 'var(--text-muted)', marginBottom: '4px' }}>Kp Index</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
<span style={{ fontSize: '20px', fontWeight: '700', color: getKpColor(kp.current), fontFamily: 'Orbitron, monospace' }}>
{kp.current !== null ? kp.current.toFixed(1) : '--'}
</span>
<span style={{ fontSize: '10px', color: getKpColor(kp.current) }}>
{kp.current !== null ? getKpLabel(kp.current) : ''}
</span>
</div>
<div style={{ marginTop: '6px' }}>
<KpBars history={kp.history} forecast={kp.forecast} />
</div>
<div style={{ fontSize: '9px', color: 'var(--text-muted)', marginTop: '2px' }}>
3 day history + forecast
</div>
</div>
</div>
</div>
);
};
// ============================================ // ============================================
// LEGACY LAYOUT (Classic HamClock Style) // LEGACY LAYOUT (Classic HamClock Style)
// ============================================ // ============================================
@ -3763,14 +3748,11 @@
</div> </div>
</div> </div>
{/* Solar Image */} {/* Solar Panel (toggleable between image and indices) */}
<SolarImage /> <SolarPanel solarIndices={solarIndices} />
{/* VOACAP Propagation - Toggleable */} {/* VOACAP Propagation - Toggleable */}
<PropagationPanel propagation={propagation.data} loading={propagation.loading} /> <PropagationPanel propagation={propagation.data} loading={propagation.loading} />
{/* Solar Indices with History */}
<SolarIndicesPanel data={solarIndices.data} loading={solarIndices.loading} />
</div> </div>
{/* CENTER - MAP */} {/* CENTER - MAP */}

Loading…
Cancel
Save

Powered by TurnKey Linux.