|
|
|
@ -1,129 +1,152 @@
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* PSKReporter Panel
|
|
|
|
* PSKReporter Panel
|
|
|
|
* Shows where your digital mode signals are being received
|
|
|
|
* Shows where your digital mode signals are being received
|
|
|
|
|
|
|
|
* Styled to match DXClusterPanel
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
import { usePSKReporter } from '../hooks/usePSKReporter.js';
|
|
|
|
import { usePSKReporter } from '../hooks/usePSKReporter.js';
|
|
|
|
|
|
|
|
import { getBandColor } from '../utils/callsign.js';
|
|
|
|
|
|
|
|
|
|
|
|
const PSKReporterPanel = ({ callsign, onShowOnMap }) => {
|
|
|
|
const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) => {
|
|
|
|
const [timeWindow, setTimeWindow] = useState(15); // minutes
|
|
|
|
const [timeWindow, setTimeWindow] = useState(15);
|
|
|
|
const [activeTab, setActiveTab] = useState('tx'); // 'tx' or 'rx'
|
|
|
|
const [activeTab, setActiveTab] = useState('rx'); // Default to 'rx' (Hearing) - more useful
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
const {
|
|
|
|
txReports,
|
|
|
|
txReports,
|
|
|
|
txCount,
|
|
|
|
txCount,
|
|
|
|
rxReports,
|
|
|
|
rxReports,
|
|
|
|
rxCount,
|
|
|
|
rxCount,
|
|
|
|
stats,
|
|
|
|
|
|
|
|
loading,
|
|
|
|
loading,
|
|
|
|
error,
|
|
|
|
error,
|
|
|
|
lastUpdate,
|
|
|
|
|
|
|
|
refresh
|
|
|
|
refresh
|
|
|
|
} = usePSKReporter(callsign, {
|
|
|
|
} = usePSKReporter(callsign, {
|
|
|
|
minutes: timeWindow,
|
|
|
|
minutes: timeWindow,
|
|
|
|
enabled: callsign && callsign !== 'N0CALL'
|
|
|
|
enabled: callsign && callsign !== 'N0CALL'
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const formatTime = (timestamp) => {
|
|
|
|
const reports = activeTab === 'tx' ? txReports : rxReports;
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
const count = activeTab === 'tx' ? txCount : rxCount;
|
|
|
|
return date.toLocaleTimeString('en-US', {
|
|
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
// Get band color from frequency
|
|
|
|
minute: '2-digit',
|
|
|
|
const getFreqColor = (freqMHz) => {
|
|
|
|
hour12: false
|
|
|
|
if (!freqMHz) return 'var(--text-muted)';
|
|
|
|
}) + 'z';
|
|
|
|
const freq = parseFloat(freqMHz);
|
|
|
|
|
|
|
|
return getBandColor(freq);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Format age
|
|
|
|
const formatAge = (minutes) => {
|
|
|
|
const formatAge = (minutes) => {
|
|
|
|
if (minutes < 1) return 'now';
|
|
|
|
if (minutes < 1) return 'now';
|
|
|
|
if (minutes === 1) return '1m ago';
|
|
|
|
if (minutes < 60) return `${minutes}m`;
|
|
|
|
return `${minutes}m ago`;
|
|
|
|
return `${Math.floor(minutes/60)}h`;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getSnrColor = (snr) => {
|
|
|
|
|
|
|
|
if (snr === null || snr === undefined) return 'var(--text-muted)';
|
|
|
|
|
|
|
|
if (snr >= 0) return '#4ade80'; // Green - excellent
|
|
|
|
|
|
|
|
if (snr >= -10) return '#fbbf24'; // Yellow - good
|
|
|
|
|
|
|
|
if (snr >= -15) return '#f97316'; // Orange - fair
|
|
|
|
|
|
|
|
return '#ef4444'; // Red - weak
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const reports = activeTab === 'tx' ? txReports : rxReports;
|
|
|
|
|
|
|
|
const count = activeTab === 'tx' ? txCount : rxCount;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!callsign || callsign === 'N0CALL') {
|
|
|
|
if (!callsign || callsign === 'N0CALL') {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div className="panel">
|
|
|
|
<div className="panel" style={{ padding: '10px' }}>
|
|
|
|
<div className="panel-header">
|
|
|
|
<div style={{ fontSize: '12px', color: 'var(--accent-primary)', fontWeight: '700', marginBottom: '6px' }}>
|
|
|
|
<span className="panel-icon">📡</span>
|
|
|
|
📡 PSKReporter
|
|
|
|
<h3>PSKReporter</h3>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="panel-content">
|
|
|
|
<div style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '10px', fontSize: '11px' }}>
|
|
|
|
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '20px' }}>
|
|
|
|
Set callsign in Settings
|
|
|
|
Set your callsign in Settings to see PSKReporter data
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div className="panel">
|
|
|
|
<div className="panel" style={{
|
|
|
|
<div className="panel-header">
|
|
|
|
padding: '10px',
|
|
|
|
<span className="panel-icon">📡</span>
|
|
|
|
display: 'flex',
|
|
|
|
<h3>PSKReporter</h3>
|
|
|
|
flexDirection: 'column',
|
|
|
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
|
|
height: '100%',
|
|
|
|
|
|
|
|
overflow: 'hidden'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
{/* Header - matches DX Cluster style */}
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
|
|
color: 'var(--accent-primary)',
|
|
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
|
|
marginBottom: '6px',
|
|
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
|
|
alignItems: 'center'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
<span>📡 PSKReporter</span>
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
|
|
<select
|
|
|
|
<select
|
|
|
|
value={timeWindow}
|
|
|
|
value={timeWindow}
|
|
|
|
onChange={(e) => setTimeWindow(parseInt(e.target.value))}
|
|
|
|
onChange={(e) => setTimeWindow(parseInt(e.target.value))}
|
|
|
|
style={{
|
|
|
|
style={{
|
|
|
|
background: 'var(--bg-tertiary)',
|
|
|
|
background: 'rgba(100, 100, 100, 0.3)',
|
|
|
|
border: '1px solid var(--border-color)',
|
|
|
|
border: '1px solid #666',
|
|
|
|
|
|
|
|
color: '#aaa',
|
|
|
|
|
|
|
|
padding: '2px 4px',
|
|
|
|
borderRadius: '4px',
|
|
|
|
borderRadius: '4px',
|
|
|
|
padding: '2px 6px',
|
|
|
|
fontSize: '10px',
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
fontFamily: 'JetBrains Mono',
|
|
|
|
color: 'var(--text-primary)'
|
|
|
|
cursor: 'pointer'
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<option value={5}>5 min</option>
|
|
|
|
<option value={5}>5m</option>
|
|
|
|
<option value={15}>15 min</option>
|
|
|
|
<option value={15}>15m</option>
|
|
|
|
<option value={30}>30 min</option>
|
|
|
|
<option value={30}>30m</option>
|
|
|
|
<option value={60}>1 hour</option>
|
|
|
|
<option value={60}>1h</option>
|
|
|
|
</select>
|
|
|
|
</select>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={refresh}
|
|
|
|
onClick={refresh}
|
|
|
|
|
|
|
|
disabled={loading}
|
|
|
|
style={{
|
|
|
|
style={{
|
|
|
|
background: 'transparent',
|
|
|
|
background: 'rgba(100, 100, 100, 0.3)',
|
|
|
|
border: 'none',
|
|
|
|
border: '1px solid #666',
|
|
|
|
cursor: 'pointer',
|
|
|
|
color: '#888',
|
|
|
|
fontSize: '0.9rem',
|
|
|
|
padding: '2px 6px',
|
|
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
|
|
fontSize: '10px',
|
|
|
|
|
|
|
|
cursor: loading ? 'not-allowed' : 'pointer',
|
|
|
|
opacity: loading ? 0.5 : 1
|
|
|
|
opacity: loading ? 0.5 : 1
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
disabled={loading}
|
|
|
|
|
|
|
|
title="Refresh"
|
|
|
|
|
|
|
|
>
|
|
|
|
>
|
|
|
|
🔄
|
|
|
|
🔄
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
{onToggleMap && (
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
onClick={onToggleMap}
|
|
|
|
|
|
|
|
style={{
|
|
|
|
|
|
|
|
background: showOnMap ? 'rgba(68, 136, 255, 0.3)' : 'rgba(100, 100, 100, 0.3)',
|
|
|
|
|
|
|
|
border: `1px solid ${showOnMap ? '#4488ff' : '#666'}`,
|
|
|
|
|
|
|
|
color: showOnMap ? '#4488ff' : '#888',
|
|
|
|
|
|
|
|
padding: '2px 8px',
|
|
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
|
|
fontSize: '10px',
|
|
|
|
|
|
|
|
fontFamily: 'JetBrains Mono',
|
|
|
|
|
|
|
|
cursor: 'pointer'
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
🗺️ {showOnMap ? 'ON' : 'OFF'}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Tabs */}
|
|
|
|
{/* Tabs - compact style */}
|
|
|
|
<div style={{
|
|
|
|
<div style={{
|
|
|
|
display: 'flex',
|
|
|
|
display: 'flex',
|
|
|
|
borderBottom: '1px solid var(--border-color)',
|
|
|
|
gap: '4px',
|
|
|
|
background: 'var(--bg-tertiary)'
|
|
|
|
marginBottom: '6px'
|
|
|
|
}}>
|
|
|
|
}}>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
onClick={() => setActiveTab('tx')}
|
|
|
|
onClick={() => setActiveTab('tx')}
|
|
|
|
style={{
|
|
|
|
style={{
|
|
|
|
flex: 1,
|
|
|
|
flex: 1,
|
|
|
|
padding: '8px',
|
|
|
|
padding: '4px 6px',
|
|
|
|
background: activeTab === 'tx' ? 'var(--bg-secondary)' : 'transparent',
|
|
|
|
background: activeTab === 'tx' ? 'rgba(74, 222, 128, 0.2)' : 'rgba(100, 100, 100, 0.2)',
|
|
|
|
border: 'none',
|
|
|
|
border: `1px solid ${activeTab === 'tx' ? '#4ade80' : '#555'}`,
|
|
|
|
borderBottom: activeTab === 'tx' ? '2px solid var(--accent-primary)' : '2px solid transparent',
|
|
|
|
borderRadius: '3px',
|
|
|
|
color: activeTab === 'tx' ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
|
|
color: activeTab === 'tx' ? '#4ade80' : '#888',
|
|
|
|
cursor: 'pointer',
|
|
|
|
cursor: 'pointer',
|
|
|
|
fontSize: '0.8rem',
|
|
|
|
fontSize: '10px',
|
|
|
|
fontWeight: activeTab === 'tx' ? '600' : '400'
|
|
|
|
fontFamily: 'JetBrains Mono'
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
📤 Being Heard ({txCount})
|
|
|
|
📤 Being Heard ({txCount})
|
|
|
|
@ -132,169 +155,101 @@ const PSKReporterPanel = ({ callsign, onShowOnMap }) => {
|
|
|
|
onClick={() => setActiveTab('rx')}
|
|
|
|
onClick={() => setActiveTab('rx')}
|
|
|
|
style={{
|
|
|
|
style={{
|
|
|
|
flex: 1,
|
|
|
|
flex: 1,
|
|
|
|
padding: '8px',
|
|
|
|
padding: '4px 6px',
|
|
|
|
background: activeTab === 'rx' ? 'var(--bg-secondary)' : 'transparent',
|
|
|
|
background: activeTab === 'rx' ? 'rgba(96, 165, 250, 0.2)' : 'rgba(100, 100, 100, 0.2)',
|
|
|
|
border: 'none',
|
|
|
|
border: `1px solid ${activeTab === 'rx' ? '#60a5fa' : '#555'}`,
|
|
|
|
borderBottom: activeTab === 'rx' ? '2px solid var(--accent-primary)' : '2px solid transparent',
|
|
|
|
borderRadius: '3px',
|
|
|
|
color: activeTab === 'rx' ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
|
|
color: activeTab === 'rx' ? '#60a5fa' : '#888',
|
|
|
|
cursor: 'pointer',
|
|
|
|
cursor: 'pointer',
|
|
|
|
fontSize: '0.8rem',
|
|
|
|
fontSize: '10px',
|
|
|
|
fontWeight: activeTab === 'rx' ? '600' : '400'
|
|
|
|
fontFamily: 'JetBrains Mono'
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
📥 Hearing ({rxCount})
|
|
|
|
📥 Hearing ({rxCount})
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="panel-content" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
|
|
{/* Reports list - matches DX Cluster style */}
|
|
|
|
{error ? (
|
|
|
|
{error ? (
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px', color: 'var(--text-muted)' }}>
|
|
|
|
<div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}>
|
|
|
|
<div style={{ marginBottom: '8px' }}>⚠️ PSKReporter temporarily unavailable</div>
|
|
|
|
⚠️ Temporarily unavailable
|
|
|
|
<div style={{ fontSize: '0.7rem' }}>Will retry automatically</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : loading && reports.length === 0 ? (
|
|
|
|
) : loading && reports.length === 0 ? (
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px', color: 'var(--text-muted)' }}>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
|
|
|
|
Loading...
|
|
|
|
<div className="loading-spinner" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : reports.length === 0 ? (
|
|
|
|
) : reports.length === 0 ? (
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px', color: 'var(--text-muted)' }}>
|
|
|
|
<div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}>
|
|
|
|
No {activeTab === 'tx' ? 'reception reports' : 'stations heard'} in the last {timeWindow} minutes
|
|
|
|
No {activeTab === 'tx' ? 'reception reports' : 'stations heard'}
|
|
|
|
<div style={{ fontSize: '0.65rem', marginTop: '8px' }}>
|
|
|
|
|
|
|
|
(Make sure you're transmitting digital modes like FT8)
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
|
|
|
|
{/* Summary stats for TX */}
|
|
|
|
|
|
|
|
{activeTab === 'tx' && txCount > 0 && (
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
<div style={{
|
|
|
|
padding: '8px 12px',
|
|
|
|
flex: 1,
|
|
|
|
background: 'var(--bg-tertiary)',
|
|
|
|
overflow: 'auto',
|
|
|
|
borderRadius: '4px',
|
|
|
|
fontSize: '12px',
|
|
|
|
marginBottom: '8px',
|
|
|
|
fontFamily: 'JetBrains Mono, monospace'
|
|
|
|
fontSize: '0.75rem'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
}}>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px' }}>
|
|
|
|
{reports.slice(0, 20).map((report, i) => {
|
|
|
|
<span>
|
|
|
|
const freqMHz = report.freqMHz || (report.freq ? (report.freq / 1000000).toFixed(3) : '?');
|
|
|
|
<strong style={{ color: 'var(--accent-primary)' }}>{txCount}</strong> stations hearing you
|
|
|
|
const color = getFreqColor(freqMHz);
|
|
|
|
</span>
|
|
|
|
const displayCall = activeTab === 'tx' ? report.receiver : report.sender;
|
|
|
|
{stats.txBands.length > 0 && (
|
|
|
|
const grid = activeTab === 'tx' ? report.receiverGrid : report.senderGrid;
|
|
|
|
<span>
|
|
|
|
|
|
|
|
Bands: {stats.txBands.join(', ')}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{stats.txModes.length > 0 && (
|
|
|
|
|
|
|
|
<span>
|
|
|
|
|
|
|
|
Modes: {stats.txModes.slice(0, 3).join(', ')}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Reports list */}
|
|
|
|
return (
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
|
|
|
|
|
|
{reports.slice(0, 25).map((report, idx) => (
|
|
|
|
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
key={idx}
|
|
|
|
key={`${displayCall}-${report.freq}-${i}`}
|
|
|
|
onClick={() => onShowOnMap && report.lat && report.lon && onShowOnMap(report)}
|
|
|
|
onClick={() => onShowOnMap && report.lat && report.lon && onShowOnMap(report)}
|
|
|
|
style={{
|
|
|
|
style={{
|
|
|
|
display: 'grid',
|
|
|
|
display: 'grid',
|
|
|
|
gridTemplateColumns: '1fr auto auto auto',
|
|
|
|
gridTemplateColumns: '55px 1fr auto',
|
|
|
|
gap: '8px',
|
|
|
|
gap: '6px',
|
|
|
|
padding: '6px 8px',
|
|
|
|
padding: '4px 6px',
|
|
|
|
background: 'var(--bg-tertiary)',
|
|
|
|
borderRadius: '3px',
|
|
|
|
borderRadius: '4px',
|
|
|
|
marginBottom: '2px',
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
background: i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent',
|
|
|
|
cursor: report.lat && report.lon ? 'pointer' : 'default',
|
|
|
|
cursor: report.lat && report.lon ? 'pointer' : 'default',
|
|
|
|
alignItems: 'center'
|
|
|
|
transition: 'background 0.15s',
|
|
|
|
|
|
|
|
borderLeft: '2px solid transparent'
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
|
|
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(68, 136, 255, 0.15)'}
|
|
|
|
|
|
|
|
onMouseLeave={(e) => e.currentTarget.style.background = i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent'}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<div>
|
|
|
|
<div style={{ color, fontWeight: '600', fontSize: '11px' }}>
|
|
|
|
<span style={{
|
|
|
|
{freqMHz}
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
|
|
color: 'var(--accent-primary)',
|
|
|
|
|
|
|
|
fontFamily: 'var(--font-mono)'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
{activeTab === 'tx' ? report.receiver : report.sender}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
{(activeTab === 'tx' ? report.receiverGrid : report.senderGrid) && (
|
|
|
|
|
|
|
|
<span style={{
|
|
|
|
|
|
|
|
marginLeft: '6px',
|
|
|
|
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
|
|
|
fontSize: '0.7rem'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
{activeTab === 'tx' ? report.receiverGrid : report.senderGrid}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
<div style={{
|
|
|
|
color: 'var(--text-secondary)',
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
fontFamily: 'var(--font-mono)'
|
|
|
|
fontWeight: '600',
|
|
|
|
}}>
|
|
|
|
overflow: 'hidden',
|
|
|
|
{report.freqMHz} {report.band}
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
</div>
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
|
|
fontSize: '11px'
|
|
|
|
<div style={{
|
|
|
|
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
|
|
|
minWidth: '40px',
|
|
|
|
|
|
|
|
textAlign: 'center'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
}}>
|
|
|
|
{report.mode}
|
|
|
|
{displayCall}
|
|
|
|
|
|
|
|
{grid && <span style={{ color: 'var(--text-muted)', fontWeight: '400', marginLeft: '4px', fontSize: '9px' }}>{grid}</span>}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
<div style={{
|
|
|
|
display: 'flex',
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'center',
|
|
|
|
alignItems: 'center',
|
|
|
|
gap: '6px',
|
|
|
|
gap: '4px',
|
|
|
|
minWidth: '70px',
|
|
|
|
fontSize: '10px'
|
|
|
|
justifyContent: 'flex-end'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
}}>
|
|
|
|
{report.snr !== null && (
|
|
|
|
<span style={{ color: 'var(--text-muted)' }}>{report.mode}</span>
|
|
|
|
|
|
|
|
{report.snr !== null && report.snr !== undefined && (
|
|
|
|
<span style={{
|
|
|
|
<span style={{
|
|
|
|
color: getSnrColor(report.snr),
|
|
|
|
color: report.snr >= 0 ? '#4ade80' : report.snr >= -10 ? '#fbbf24' : '#f97316',
|
|
|
|
fontFamily: 'var(--font-mono)',
|
|
|
|
|
|
|
|
fontWeight: '600'
|
|
|
|
fontWeight: '600'
|
|
|
|
}}>
|
|
|
|
}}>
|
|
|
|
{report.snr > 0 ? '+' : ''}{report.snr}dB
|
|
|
|
{report.snr > 0 ? '+' : ''}{report.snr}
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
<span style={{
|
|
|
|
<span style={{ color: 'var(--text-muted)', fontSize: '9px' }}>
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
|
|
|
fontSize: '0.65rem'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
{formatAge(report.age)}
|
|
|
|
{formatAge(report.age)}
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
);
|
|
|
|
</div>
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
{reports.length > 25 && (
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
|
|
padding: '8px',
|
|
|
|
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
|
|
|
fontSize: '0.7rem'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
Showing 25 of {reports.length} reports
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer with last update */}
|
|
|
|
|
|
|
|
{lastUpdate && (
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
|
|
padding: '4px 12px',
|
|
|
|
|
|
|
|
borderTop: '1px solid var(--border-color)',
|
|
|
|
|
|
|
|
fontSize: '0.65rem',
|
|
|
|
|
|
|
|
color: 'var(--text-muted)',
|
|
|
|
|
|
|
|
textAlign: 'right'
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
Updated: {lastUpdate.toLocaleTimeString()}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|