Update index.html

pull/1/head
accius 6 days ago
parent 2c243b689e
commit 5f8dd14cf1

@ -265,6 +265,23 @@
font-family: 'JetBrains Mono', monospace !important; font-family: 'JetBrains Mono', monospace !important;
font-size: 12px !important; font-size: 12px !important;
} }
/* Panel styling */
.panel {
background: var(--bg-panel);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
backdrop-filter: blur(10px);
}
.panel-header {
font-size: 10px;
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 8px;
letter-spacing: 0.5px;
}
</style> </style>
</head> </head>
<body> <body>
@ -802,9 +819,11 @@
}; };
// ============================================ // ============================================
// PROPAGATION PANEL COMPONENT // PROPAGATION PANEL COMPONENT (Toggleable views)
// ============================================ // ============================================
const PropagationPanel = ({ propagation, loading }) => { const PropagationPanel = ({ propagation, loading }) => {
const [viewMode, setViewMode] = useState('bars'); // 'bars' or 'chart'
if (loading || !propagation) { if (loading || !propagation) {
return ( return (
<div className="panel"> <div className="panel">
@ -818,7 +837,25 @@
const { solarData, distance, currentBands, currentHour, hourlyPredictions } = propagation; const { solarData, distance, currentBands, currentHour, hourlyPredictions } = propagation;
// Get status color // Get reliability color for heat map (VOACAP style - red=good, green=poor)
const getHeatColor = (rel) => {
if (rel >= 80) return '#ff0000'; // Red - excellent
if (rel >= 60) return '#ff6600'; // Orange - good
if (rel >= 40) return '#ffcc00'; // Yellow - fair
if (rel >= 20) return '#88cc00'; // Yellow-green - poor
if (rel >= 10) return '#00aa00'; // Green - marginal
return '#004400'; // Dark green - closed
};
// Get reliability bar color (standard - green=good)
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';
};
const getStatusColor = (status) => { const getStatusColor = (status) => {
switch (status) { switch (status) {
case 'EXCELLENT': return '#00ff88'; case 'EXCELLENT': return '#00ff88';
@ -830,94 +867,133 @@
} }
}; };
// Get reliability bar color const bands = ['80m', '40m', '30m', '20m', '17m', '15m', '12m', '10m'];
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 ( return (
<div className="panel"> <div className="panel" style={{ cursor: 'pointer' }} onClick={() => setViewMode(v => v === 'bars' ? 'chart' : 'bars')}>
<div className="panel-header"> <div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
📡 HF Propagation to DX <span>📡 VOACAP DE-DX</span>
<span style={{ fontSize: '10px', marginLeft: '8px', color: 'var(--text-muted)' }}> <span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>
{Math.round(distance).toLocaleString()} km {viewMode === 'bars' ? '▦ bars' : '▤ chart'} • click to toggle
</span> </span>
</div> </div>
{/* Solar Conditions Summary */} {viewMode === 'chart' ? (
/* VOACAP Heat Map Chart View */
<div style={{ padding: '4px' }}>
{/* Heat map grid */}
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr', gridTemplateColumns: '28px repeat(24, 1fr)',
gap: '8px', gridTemplateRows: `repeat(${bands.length}, 12px)`,
padding: '8px', gap: '1px',
marginBottom: '8px', fontSize: '8px',
background: 'var(--bg-tertiary)', fontFamily: 'JetBrains Mono, monospace'
borderRadius: '4px',
fontSize: '11px'
}}> }}>
<div style={{ textAlign: 'center' }}> {bands.map((band, bandIdx) => (
<div style={{ color: 'var(--text-muted)' }}>SFI</div> <React.Fragment key={band}>
<div style={{ color: 'var(--accent-primary)', fontWeight: 'bold' }}>{solarData.sfi}</div> {/* Band label */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
paddingRight: '4px',
color: 'var(--text-muted)',
fontSize: '8px'
}}>
{band.replace('m', '')}
</div> </div>
<div style={{ textAlign: 'center' }}> {/* 24 hour cells */}
<div style={{ color: 'var(--text-muted)' }}>SSN</div> {Array.from({ length: 24 }, (_, hour) => {
<div style={{ color: 'var(--accent-secondary)', fontWeight: 'bold' }}>{solarData.ssn}</div> const bandData = hourlyPredictions?.[band];
const hourData = bandData?.find(h => h.hour === hour);
const rel = hourData?.reliability || 0;
return (
<div
key={hour}
style={{
background: getHeatColor(rel),
borderRadius: '1px',
border: hour === currentHour ? '1px solid white' : 'none'
}}
title={`${band} @ ${hour}:00 UTC: ${rel}%`}
/>
);
})}
</React.Fragment>
))}
</div> </div>
<div style={{ textAlign: 'center' }}>
<div style={{ color: 'var(--text-muted)' }}>K</div> {/* Hour labels */}
<div style={{ <div style={{
color: solarData.kIndex >= 4 ? '#ff4444' : solarData.kIndex >= 3 ? '#ffcc00' : '#00ff88', display: 'grid',
fontWeight: 'bold' gridTemplateColumns: '28px repeat(24, 1fr)',
}}>{solarData.kIndex}</div> marginTop: '2px',
</div> fontSize: '7px',
color: 'var(--text-muted)'
}}>
<div>UTC</div>
{[0, '', '', 3, '', '', 6, '', '', 9, '', '', 12, '', '', 15, '', '', 18, '', '', 21, '', ''].map((h, i) => (
<div key={i} style={{ textAlign: 'center' }}>{h}</div>
))}
</div> </div>
{/* Band Predictions */} {/* Legend & Info */}
<div style={{
marginTop: '6px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: '8px'
}}>
<div style={{ display: 'flex', gap: '2px', alignItems: 'center' }}>
<span style={{ color: 'var(--text-muted)' }}>REL:</span>
<div style={{ width: '8px', height: '8px', background: '#004400', borderRadius: '1px' }} />
<div style={{ width: '8px', height: '8px', background: '#00aa00', borderRadius: '1px' }} />
<div style={{ width: '8px', height: '8px', background: '#88cc00', borderRadius: '1px' }} />
<div style={{ width: '8px', height: '8px', background: '#ffcc00', borderRadius: '1px' }} />
<div style={{ width: '8px', height: '8px', background: '#ff6600', borderRadius: '1px' }} />
<div style={{ width: '8px', height: '8px', background: '#ff0000', borderRadius: '1px' }} />
</div>
<div style={{ color: 'var(--text-muted)' }}>
{Math.round(distance/1000)}Kkm, SSN={solarData.ssn}
</div>
</div>
</div>
) : (
/* Bar Chart View */
<div style={{ fontSize: '11px' }}> <div style={{ fontSize: '11px' }}>
{/* Solar quick stats */}
<div style={{ <div style={{
display: 'grid', display: 'flex',
gridTemplateColumns: '45px 1fr 55px', justifyContent: 'space-around',
gap: '4px', padding: '4px',
padding: '4px 0',
borderBottom: '1px solid var(--border-color)',
marginBottom: '4px', marginBottom: '4px',
color: 'var(--text-muted)', background: 'var(--bg-tertiary)',
borderRadius: '4px',
fontSize: '10px' fontSize: '10px'
}}> }}>
<span>BAND</span> <span><span style={{ color: 'var(--text-muted)' }}>SFI </span><span style={{ color: 'var(--accent-amber)' }}>{solarData.sfi}</span></span>
<span>RELIABILITY</span> <span><span style={{ color: 'var(--text-muted)' }}>SSN </span><span style={{ color: 'var(--accent-cyan)' }}>{solarData.ssn}</span></span>
<span style={{ textAlign: 'right' }}>STATUS</span> <span><span style={{ color: 'var(--text-muted)' }}>K </span><span style={{ color: solarData.kIndex >= 4 ? '#ff4444' : '#00ff88' }}>{solarData.kIndex}</span></span>
</div> </div>
{currentBands.slice(0, 8).map((band, idx) => ( {currentBands.slice(0, 8).map((band, idx) => (
<div key={band.band} style={{ <div key={band.band} style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '45px 1fr 55px', gridTemplateColumns: '32px 1fr 40px',
gap: '4px', gap: '4px',
padding: '3px 0', padding: '2px 0',
alignItems: 'center', alignItems: 'center'
borderBottom: idx < 7 ? '1px solid var(--bg-tertiary)' : 'none'
}}> }}>
<span style={{ <span style={{
fontFamily: 'JetBrains Mono, monospace', fontFamily: 'JetBrains Mono, monospace',
color: band.reliability >= 50 ? 'var(--color-primary)' : 'var(--text-muted)' fontSize: '10px',
color: band.reliability >= 50 ? 'var(--accent-green)' : 'var(--text-muted)'
}}> }}>
{band.band} {band.band}
</span> </span>
<div style={{ position: 'relative', height: '12px' }}> <div style={{ position: 'relative', height: '10px', background: 'var(--bg-tertiary)', borderRadius: '2px' }}>
<div style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
background: 'var(--bg-tertiary)',
borderRadius: '2px'
}} />
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@ -925,42 +1001,103 @@
height: '100%', height: '100%',
width: `${band.reliability}%`, width: `${band.reliability}%`,
background: getReliabilityColor(band.reliability), background: getReliabilityColor(band.reliability),
borderRadius: '2px', borderRadius: '2px'
transition: 'width 0.3s ease'
}} /> }} />
<span style={{
position: 'absolute',
right: '4px',
top: '50%',
transform: 'translateY(-50%)',
fontSize: '9px',
color: band.reliability > 50 ? '#000' : 'var(--text-muted)'
}}>
{band.reliability}%
</span>
</div> </div>
<span style={{ <span style={{
textAlign: 'right', textAlign: 'right',
fontSize: '9px', fontSize: '9px',
fontWeight: 'bold',
color: getStatusColor(band.status) color: getStatusColor(band.status)
}}> }}>
{band.status} {band.reliability}%
</span> </span>
</div> </div>
))} ))}
</div> </div>
)}
</div>
);
};
// ============================================
// SOLAR IMAGE COMPONENT
// ============================================
const SolarImage = () => {
const [imageType, setImageType] = useState('0193'); // AIA 193 (corona)
// SDO/AIA image types
const imageTypes = {
'0193': { name: 'AIA 193Å', desc: 'Corona' },
'0304': { name: 'AIA 304Å', desc: 'Chromosphere' },
'0171': { name: 'AIA 171Å', desc: 'Quiet Corona' },
'0094': { name: 'AIA 94Å', desc: 'Flaring' },
'HMIIC': { name: 'HMI Int', desc: 'Visible' }
};
// SDO images update every ~15 minutes
const timestamp = Math.floor(Date.now() / 900000) * 900000; // Round to 15 min
const imageUrl = `https://sdo.gsfc.nasa.gov/assets/img/latest/latest_256_${imageType}.jpg?t=${timestamp}`;
{/* Footer */} return (
<div className="panel" style={{ padding: '8px' }}>
<div style={{ <div style={{
marginTop: '8px', display: 'flex',
paddingTop: '8px', justifyContent: 'space-between',
borderTop: '1px solid var(--border-color)', alignItems: 'center',
marginBottom: '6px'
}}>
<span style={{ fontSize: '10px', color: 'var(--accent-amber)', fontWeight: '700' }}>☀ SOLAR</span>
<select
value={imageType}
onChange={(e) => setImageType(e.target.value)}
onClick={(e) => e.stopPropagation()}
style={{
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
color: 'var(--text-secondary)',
fontSize: '9px', fontSize: '9px',
color: 'var(--text-muted)', padding: '2px 4px',
textAlign: 'center' borderRadius: '3px',
cursor: 'pointer'
}}
>
{Object.entries(imageTypes).map(([key, val]) => (
<option key={key} value={key}>{val.desc}</option>
))}
</select>
</div>
<div style={{
position: 'relative',
width: '100%',
paddingBottom: '100%', // Square aspect ratio
background: '#000',
borderRadius: '50%',
overflow: 'hidden'
}}> }}>
Predictions at {String(currentHour).padStart(2, '0')}:00 UTC • Based on current solar conditions <img
src={imageUrl}
alt="Current Sun"
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
borderRadius: '50%'
}}
onError={(e) => {
e.target.style.display = 'none';
}}
/>
</div>
<div style={{
textAlign: 'center',
marginTop: '4px',
fontSize: '8px',
color: 'var(--text-muted)'
}}>
SDO/AIA • Live from NASA
</div> </div>
</div> </div>
); );
@ -2246,10 +2383,10 @@
</div> </div>
{/* Band Conditions - Compact with color coding */} {/* Band Conditions - Compact with color coding */}
<div className="panel" style={{ padding: '10px', flex: '1 1 auto', overflow: 'hidden', minHeight: 0 }}> <div className="panel" style={{ padding: '10px', flex: '0 0 auto', overflow: 'hidden' }}>
<div style={{ fontSize: '10px', color: 'var(--accent-purple)', fontWeight: '700', marginBottom: '6px' }}>📊 BAND CONDITIONS</div> <div style={{ fontSize: '10px', color: 'var(--accent-purple)', fontWeight: '700', marginBottom: '6px' }}>📊 BAND CONDITIONS</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4px', fontSize: '10px', fontFamily: 'JetBrains Mono' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3px', fontSize: '9px', fontFamily: 'JetBrains Mono' }}>
{bandConditions.data.map(band => { {bandConditions.data.slice(0, 12).map(band => {
const colors = { const colors = {
GOOD: { bg: 'rgba(0,255,136,0.2)', color: '#00ff88', border: 'rgba(0,255,136,0.4)' }, GOOD: { bg: 'rgba(0,255,136,0.2)', color: '#00ff88', border: 'rgba(0,255,136,0.4)' },
FAIR: { bg: 'rgba(255,180,50,0.2)', color: '#ffb432', border: 'rgba(255,180,50,0.4)' }, FAIR: { bg: 'rgba(255,180,50,0.2)', color: '#ffb432', border: 'rgba(255,180,50,0.4)' },
@ -2259,41 +2396,23 @@
return ( return (
<div key={band.band} style={{ <div key={band.band} style={{
textAlign: 'center', textAlign: 'center',
padding: '4px 2px', padding: '3px 1px',
background: style.bg, background: style.bg,
border: `1px solid ${style.border}`, border: `1px solid ${style.border}`,
borderRadius: '3px' borderRadius: '2px'
}}> }}>
<div style={{ fontWeight: '600', color: style.color }}>{band.band}</div> <div style={{ fontWeight: '600', color: style.color, fontSize: '8px' }}>{band.band}</div>
<div style={{ fontSize: '8px', color: style.color, opacity: 0.8 }}>{band.condition}</div>
</div> </div>
); );
})} })}
</div> </div>
</div> </div>
{/* Propagation Compact */} {/* Solar Image */}
<div className="panel" style={{ padding: '10px', flex: '1 1 auto', overflow: 'hidden', minHeight: 0 }}> <SolarImage />
<div style={{ fontSize: '10px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '6px' }}>📡 PROPAGATION TO DX</div>
{propagation.data ? ( {/* VOACAP Propagation - Toggleable */}
<div style={{ fontSize: '10px', fontFamily: 'JetBrains Mono' }}> <PropagationPanel propagation={propagation.data} loading={propagation.loading} />
<div style={{ marginBottom: '6px', color: 'var(--text-muted)', fontSize: '9px' }}>
{Math.round(propagation.data.distance).toLocaleString()} km path
</div>
{propagation.data.currentBands.slice(0, 6).map(b => (
<div key={b.band} style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '3px' }}>
<span style={{ width: '32px', color: b.reliability >= 50 ? 'var(--accent-green)' : 'var(--text-muted)' }}>{b.band}</span>
<div style={{ flex: 1, height: '8px', background: 'var(--bg-tertiary)', borderRadius: '2px', overflow: 'hidden' }}>
<div style={{ width: `${b.reliability}%`, height: '100%', background: b.reliability >= 70 ? '#00ff88' : b.reliability >= 50 ? '#88ff00' : b.reliability >= 30 ? '#ffcc00' : '#ff4444', borderRadius: '2px' }} />
</div>
<span style={{ width: '28px', fontSize: '9px', color: b.reliability >= 50 ? 'var(--accent-green)' : 'var(--text-muted)' }}>{b.reliability}%</span>
</div>
))}
</div>
) : (
<div style={{ color: 'var(--text-muted)', fontSize: '10px' }}>Loading...</div>
)}
</div>
</div> </div>
{/* CENTER - MAP */} {/* CENTER - MAP */}

Loading…
Cancel
Save

Powered by TurnKey Linux.