|
|
|
|
@ -1205,24 +1205,34 @@
|
|
|
|
|
// ============================================
|
|
|
|
|
// PROPAGATION PANEL COMPONENT (Toggleable views)
|
|
|
|
|
// ============================================
|
|
|
|
|
const PropagationPanel = ({ propagation, loading }) => {
|
|
|
|
|
const PropagationPanel = ({ propagation, loading, bandConditions }) => {
|
|
|
|
|
// Load view mode preference from localStorage, default to 'chart'
|
|
|
|
|
const [viewMode, setViewMode] = useState(() => {
|
|
|
|
|
try {
|
|
|
|
|
const saved = localStorage.getItem('openhamclock_voacapViewMode');
|
|
|
|
|
return saved === 'bars' ? 'bars' : 'chart'; // Default to chart
|
|
|
|
|
if (saved === 'bars' || saved === 'bands') return saved;
|
|
|
|
|
return 'chart'; // Default to chart
|
|
|
|
|
} catch (e) { return 'chart'; }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Save view mode preference when changed
|
|
|
|
|
const toggleViewMode = () => {
|
|
|
|
|
const newMode = viewMode === 'bars' ? 'chart' : 'bars';
|
|
|
|
|
// Cycle through view modes: chart -> bars -> bands -> chart
|
|
|
|
|
const cycleViewMode = () => {
|
|
|
|
|
const modes = ['chart', 'bars', 'bands'];
|
|
|
|
|
const currentIdx = modes.indexOf(viewMode);
|
|
|
|
|
const newMode = modes[(currentIdx + 1) % modes.length];
|
|
|
|
|
setViewMode(newMode);
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem('openhamclock_voacapViewMode', newMode);
|
|
|
|
|
} catch (e) { console.error('Failed to save VOACAP view mode:', e); }
|
|
|
|
|
} catch (e) { console.error('Failed to save view mode:', e); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get band condition color/style
|
|
|
|
|
const getBandStyle = (condition) => ({
|
|
|
|
|
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)' },
|
|
|
|
|
POOR: { bg: 'rgba(255,68,102,0.2)', color: '#ff4466', border: 'rgba(255,68,102,0.4)' }
|
|
|
|
|
}[condition] || { bg: 'rgba(255,180,50,0.2)', color: '#ffb432', border: 'rgba(255,180,50,0.4)' });
|
|
|
|
|
|
|
|
|
|
if (loading || !propagation) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="panel">
|
|
|
|
|
@ -1270,183 +1280,224 @@
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const bands = ['80m', '40m', '30m', '20m', '17m', '15m', '12m', '10m'];
|
|
|
|
|
|
|
|
|
|
const viewModeLabels = {
|
|
|
|
|
chart: '▤ chart',
|
|
|
|
|
bars: '▦ bars',
|
|
|
|
|
bands: '◫ bands'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="panel" style={{ cursor: 'pointer' }} onClick={toggleViewMode}>
|
|
|
|
|
<div className="panel" style={{ cursor: 'pointer' }} onClick={cycleViewMode}>
|
|
|
|
|
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
<span>📡 VOACAP {hasRealData && <span style={{ color: '#00ff88', fontSize: '10px' }}>●</span>}</span>
|
|
|
|
|
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
|
|
|
|
{viewMode === 'bars' ? '▦ bars' : '▤ chart'} • click to toggle
|
|
|
|
|
<span>
|
|
|
|
|
{viewMode === 'bands' ? '📊 BAND CONDITIONS' : '📡 VOACAP'}
|
|
|
|
|
{hasRealData && viewMode !== 'bands' && <span style={{ color: '#00ff88', fontSize: '10px', marginLeft: '4px' }}>●</span>}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* MUF/LUF and Data Source Info */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
padding: '4px 8px',
|
|
|
|
|
background: hasRealData ? 'rgba(0, 255, 136, 0.1)' : 'var(--bg-tertiary)',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
fontSize: '11px'
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px' }}>
|
|
|
|
|
<span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}>MUF </span>
|
|
|
|
|
<span style={{ color: '#ff8800', fontWeight: '600' }}>{muf || '?'}</span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}> MHz</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}>LUF </span>
|
|
|
|
|
<span style={{ color: '#00aaff', fontWeight: '600' }}>{luf || '?'}</span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}> MHz</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{ color: hasRealData ? '#00ff88' : 'var(--text-muted)', fontSize: '10px' }}>
|
|
|
|
|
{hasRealData ? `📡 ${ionospheric?.source || 'ionosonde'}` : '⚡ estimated'}
|
|
|
|
|
<span style={{ fontSize: '10px', color: 'var(--text-muted)' }}>
|
|
|
|
|
{viewModeLabels[viewMode]} • click to toggle
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{viewMode === 'chart' ? (
|
|
|
|
|
/* VOACAP Heat Map Chart View */
|
|
|
|
|
{viewMode === 'bands' ? (
|
|
|
|
|
/* Band Conditions Grid View */
|
|
|
|
|
<div style={{ padding: '4px' }}>
|
|
|
|
|
{/* Heat map grid */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '28px repeat(24, 1fr)',
|
|
|
|
|
gridTemplateRows: `repeat(${bands.length}, 12px)`,
|
|
|
|
|
gap: '1px',
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
fontFamily: 'JetBrains Mono, monospace'
|
|
|
|
|
}}>
|
|
|
|
|
{bands.map((band, bandIdx) => (
|
|
|
|
|
<React.Fragment key={band}>
|
|
|
|
|
{/* Band label */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
paddingRight: '4px',
|
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
fontSize: '12px'
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4px' }}>
|
|
|
|
|
{(bandConditions?.data || []).slice(0, 12).map((band, idx) => {
|
|
|
|
|
const style = getBandStyle(band.condition);
|
|
|
|
|
return (
|
|
|
|
|
<div key={idx} style={{
|
|
|
|
|
background: style.bg,
|
|
|
|
|
border: `1px solid ${style.border}`,
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
padding: '6px 2px',
|
|
|
|
|
textAlign: 'center'
|
|
|
|
|
}}>
|
|
|
|
|
{band.replace('m', '')}
|
|
|
|
|
<div style={{ fontFamily: 'Orbitron, monospace', fontSize: '13px', fontWeight: '700', color: style.color }}>
|
|
|
|
|
{band.band}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ fontSize: '9px', fontWeight: '600', color: style.color, marginTop: '2px', opacity: 0.8 }}>
|
|
|
|
|
{band.condition}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* 24 hour cells */}
|
|
|
|
|
{Array.from({ length: 24 }, (_, hour) => {
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
{/* Hour labels */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '28px repeat(24, 1fr)',
|
|
|
|
|
marginTop: '2px',
|
|
|
|
|
fontSize: '9px',
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
{/* Legend & Info */}
|
|
|
|
|
<div style={{
|
|
|
|
|
marginTop: '6px',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
fontSize: '11px'
|
|
|
|
|
}}>
|
|
|
|
|
<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)}km • {ionospheric?.foF2 ? `foF2=${ionospheric.foF2}` : `SSN=${solarData.ssn}`}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginTop: '6px', fontSize: '10px', color: 'var(--text-muted)', textAlign: 'center' }}>
|
|
|
|
|
SFI {solarData.sfi} • K {solarData.kIndex} • General conditions for all paths
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
/* Bar Chart View */
|
|
|
|
|
<div style={{ fontSize: '13px' }}>
|
|
|
|
|
{/* Solar quick stats */}
|
|
|
|
|
<>
|
|
|
|
|
{/* MUF/LUF and Data Source Info */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-around',
|
|
|
|
|
padding: '4px',
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
background: 'var(--bg-tertiary)',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
padding: '4px 8px',
|
|
|
|
|
background: hasRealData ? 'rgba(0, 255, 136, 0.1)' : 'var(--bg-tertiary)',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
fontSize: '11px'
|
|
|
|
|
}}>
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>SFI </span><span style={{ color: 'var(--accent-amber)' }}>{solarData.sfi}</span></span>
|
|
|
|
|
{ionospheric?.foF2 ? (
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>foF2 </span><span style={{ color: '#00ff88' }}>{ionospheric.foF2}</span></span>
|
|
|
|
|
) : (
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>SSN </span><span style={{ color: 'var(--accent-cyan)' }}>{solarData.ssn}</span></span>
|
|
|
|
|
)}
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>K </span><span style={{ color: solarData.kIndex >= 4 ? '#ff4444' : '#00ff88' }}>{solarData.kIndex}</span></span>
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px' }}>
|
|
|
|
|
<span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}>MUF </span>
|
|
|
|
|
<span style={{ color: '#ff8800', fontWeight: '600' }}>{muf || '?'}</span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}> MHz</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}>LUF </span>
|
|
|
|
|
<span style={{ color: '#00aaff', fontWeight: '600' }}>{luf || '?'}</span>
|
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}> MHz</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{ color: hasRealData ? '#00ff88' : 'var(--text-muted)', fontSize: '10px' }}>
|
|
|
|
|
{hasRealData ? `📡 ${ionospheric?.source || 'ionosonde'}` : '⚡ estimated'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{currentBands.slice(0, 8).map((band, idx) => (
|
|
|
|
|
<div key={band.band} style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '32px 1fr 40px',
|
|
|
|
|
gap: '4px',
|
|
|
|
|
padding: '2px 0',
|
|
|
|
|
alignItems: 'center'
|
|
|
|
|
}}>
|
|
|
|
|
<span style={{
|
|
|
|
|
fontFamily: 'JetBrains Mono, monospace',
|
|
|
|
|
{viewMode === 'chart' ? (
|
|
|
|
|
/* VOACAP Heat Map Chart View */
|
|
|
|
|
<div style={{ padding: '4px' }}>
|
|
|
|
|
{/* Heat map grid */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '28px repeat(24, 1fr)',
|
|
|
|
|
gridTemplateRows: `repeat(${bands.length}, 12px)`,
|
|
|
|
|
gap: '1px',
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
color: band.reliability >= 50 ? 'var(--accent-green)' : 'var(--text-muted)'
|
|
|
|
|
fontFamily: 'JetBrains Mono, monospace'
|
|
|
|
|
}}>
|
|
|
|
|
{band.band}
|
|
|
|
|
</span>
|
|
|
|
|
<div style={{ position: 'relative', height: '10px', background: 'var(--bg-tertiary)', borderRadius: '2px' }}>
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
height: '100%',
|
|
|
|
|
width: `${band.reliability}%`,
|
|
|
|
|
background: getReliabilityColor(band.reliability),
|
|
|
|
|
borderRadius: '2px'
|
|
|
|
|
}} />
|
|
|
|
|
{bands.map((band, bandIdx) => (
|
|
|
|
|
<React.Fragment key={band}>
|
|
|
|
|
{/* Band label */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
paddingRight: '4px',
|
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
fontSize: '12px'
|
|
|
|
|
}}>
|
|
|
|
|
{band.replace('m', '')}
|
|
|
|
|
</div>
|
|
|
|
|
{/* 24 hour cells */}
|
|
|
|
|
{Array.from({ length: 24 }, (_, hour) => {
|
|
|
|
|
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>
|
|
|
|
|
<span style={{
|
|
|
|
|
textAlign: 'right',
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
color: getStatusColor(band.status)
|
|
|
|
|
|
|
|
|
|
{/* Hour labels */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '28px repeat(24, 1fr)',
|
|
|
|
|
marginTop: '2px',
|
|
|
|
|
fontSize: '9px',
|
|
|
|
|
color: 'var(--text-muted)'
|
|
|
|
|
}}>
|
|
|
|
|
{band.reliability}%
|
|
|
|
|
</span>
|
|
|
|
|
<div>UTC</div>
|
|
|
|
|
{[0, '', '', 3, '', '', 6, '', '', 9, '', '', 12, '', '', 15, '', '', 18, '', '', 21, '', ''].map((h, i) => (
|
|
|
|
|
<div key={i} style={{ textAlign: 'center' }}>{h}</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Legend & Info */}
|
|
|
|
|
<div style={{
|
|
|
|
|
marginTop: '6px',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
fontSize: '11px'
|
|
|
|
|
}}>
|
|
|
|
|
<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)}km • {ionospheric?.foF2 ? `foF2=${ionospheric.foF2}` : `SSN=${solarData.ssn}`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
/* Bar Chart View */
|
|
|
|
|
<div style={{ fontSize: '13px' }}>
|
|
|
|
|
{/* Solar quick stats */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-around',
|
|
|
|
|
padding: '4px',
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
background: 'var(--bg-tertiary)',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
fontSize: '11px'
|
|
|
|
|
}}>
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>SFI </span><span style={{ color: 'var(--accent-amber)' }}>{solarData.sfi}</span></span>
|
|
|
|
|
{ionospheric?.foF2 ? (
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>foF2 </span><span style={{ color: '#00ff88' }}>{ionospheric.foF2}</span></span>
|
|
|
|
|
) : (
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>SSN </span><span style={{ color: 'var(--accent-cyan)' }}>{solarData.ssn}</span></span>
|
|
|
|
|
)}
|
|
|
|
|
<span><span style={{ color: 'var(--text-muted)' }}>K </span><span style={{ color: solarData.kIndex >= 4 ? '#ff4444' : '#00ff88' }}>{solarData.kIndex}</span></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{currentBands.slice(0, 8).map((band, idx) => (
|
|
|
|
|
<div key={band.band} style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: '32px 1fr 40px',
|
|
|
|
|
gap: '4px',
|
|
|
|
|
padding: '2px 0',
|
|
|
|
|
alignItems: 'center'
|
|
|
|
|
}}>
|
|
|
|
|
<span style={{
|
|
|
|
|
fontFamily: 'JetBrains Mono, monospace',
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
color: band.reliability >= 50 ? 'var(--accent-green)' : 'var(--text-muted)'
|
|
|
|
|
}}>
|
|
|
|
|
{band.band}
|
|
|
|
|
</span>
|
|
|
|
|
<div style={{ position: 'relative', height: '10px', background: 'var(--bg-tertiary)', borderRadius: '2px' }}>
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
height: '100%',
|
|
|
|
|
width: `${band.reliability}%`,
|
|
|
|
|
background: getReliabilityColor(band.reliability),
|
|
|
|
|
borderRadius: '2px'
|
|
|
|
|
}} />
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{
|
|
|
|
|
textAlign: 'right',
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
color: getStatusColor(band.status)
|
|
|
|
|
}}>
|
|
|
|
|
{band.reliability}%
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
@ -2404,30 +2455,6 @@
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const BandConditionsPanel = ({ bands, loading }) => {
|
|
|
|
|
const getStyle = (c) => ({ GOOD: { bg: 'rgba(0,255,136,0.15)', color: 'var(--accent-green)', border: 'rgba(0,255,136,0.3)' }, FAIR: { bg: 'rgba(255,180,50,0.15)', color: 'var(--accent-amber)', border: 'rgba(255,180,50,0.3)' }, POOR: { bg: 'rgba(255,68,102,0.15)', color: 'var(--accent-red)', border: 'rgba(255,68,102,0.3)' } }[c] || { bg: 'rgba(255,180,50,0.15)', color: 'var(--accent-amber)', border: 'rgba(255,180,50,0.3)' });
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '20px', backdropFilter: 'blur(10px)' }}>
|
|
|
|
|
<div style={{ fontSize: '12px', fontWeight: '600', color: 'var(--accent-cyan)', letterSpacing: '2px', marginBottom: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
|
|
|
<span>📡 BAND CONDITIONS</span>
|
|
|
|
|
{loading && <div className="loading-spinner" />}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px' }}>
|
|
|
|
|
{bands.map((b, i) => {
|
|
|
|
|
const s = getStyle(b.condition);
|
|
|
|
|
return (
|
|
|
|
|
<div key={i} style={{ background: s.bg, border: `1px solid ${s.border}`, borderRadius: '6px', padding: '10px', textAlign: 'center' }}>
|
|
|
|
|
<div style={{ fontFamily: 'Orbitron, monospace', fontSize: '14px', fontWeight: '700', color: s.color }}>{b.band}</div>
|
|
|
|
|
<div style={{ fontSize: '12px', fontWeight: '600', color: s.color, marginTop: '4px', opacity: 0.8 }}>{b.condition}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DXClusterPanel = ({ spots, loading, activeSource, showOnMap, onToggleMap }) => (
|
|
|
|
|
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '20px', backdropFilter: 'blur(10px)', maxHeight: '280px', overflow: 'hidden' }}>
|
|
|
|
|
<div style={{ fontSize: '12px', fontWeight: '600', color: 'var(--accent-cyan)', letterSpacing: '2px', marginBottom: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
|
|
|
@ -3722,37 +3749,11 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Band Conditions - Compact with color coding */}
|
|
|
|
|
<div className="panel" style={{ padding: '10px', flex: '0 0 auto', overflow: 'hidden' }}>
|
|
|
|
|
<div style={{ fontSize: '12px', color: 'var(--accent-purple)', fontWeight: '700', marginBottom: '6px' }}>📊 BAND CONDITIONS</div>
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3px', fontSize: '13px', fontFamily: 'JetBrains Mono' }}>
|
|
|
|
|
{bandConditions.data.slice(0, 12).map(band => {
|
|
|
|
|
const colors = {
|
|
|
|
|
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)' },
|
|
|
|
|
POOR: { bg: 'rgba(255,68,102,0.2)', color: '#ff4466', border: 'rgba(255,68,102,0.4)' }
|
|
|
|
|
};
|
|
|
|
|
const style = colors[band.condition] || colors.FAIR;
|
|
|
|
|
return (
|
|
|
|
|
<div key={band.band} style={{
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
padding: '3px 1px',
|
|
|
|
|
background: style.bg,
|
|
|
|
|
border: `1px solid ${style.border}`,
|
|
|
|
|
borderRadius: '2px'
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ fontWeight: '600', color: style.color, fontSize: '12px' }}>{band.band}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Solar Panel (toggleable between image and indices) */}
|
|
|
|
|
<SolarPanel solarIndices={solarIndices} />
|
|
|
|
|
|
|
|
|
|
{/* VOACAP Propagation - Toggleable */}
|
|
|
|
|
<PropagationPanel propagation={propagation.data} loading={propagation.loading} />
|
|
|
|
|
{/* VOACAP/Propagation/Band Conditions - Toggleable */}
|
|
|
|
|
<PropagationPanel propagation={propagation.data} loading={propagation.loading} bandConditions={bandConditions} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* CENTER - MAP */}
|
|
|
|
|
|