psk updates

pull/38/head
accius 2 days ago
parent 96099dfb69
commit 42debad3b2

@ -13,6 +13,7 @@ import {
ContestPanel, ContestPanel,
SettingsPanel, SettingsPanel,
DXFilterManager, DXFilterManager,
PSKFilterManager,
SolarPanel, SolarPanel,
PropagationPanel, PropagationPanel,
DXpeditionPanel, DXpeditionPanel,
@ -96,6 +97,7 @@ const App = () => {
// UI state // UI state
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showDXFilters, setShowDXFilters] = useState(false); const [showDXFilters, setShowDXFilters] = useState(false);
const [showPSKFilters, setShowPSKFilters] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
// Map layer visibility // Map layer visibility
@ -183,6 +185,20 @@ const App = () => {
} catch (e) {} } catch (e) {}
}, [dxFilters]); }, [dxFilters]);
// PSKReporter Filters
const [pskFilters, setPskFilters] = useState(() => {
try {
const stored = localStorage.getItem('openhamclock_pskFilters');
return stored ? JSON.parse(stored) : {};
} catch (e) { return {}; }
});
useEffect(() => {
try {
localStorage.setItem('openhamclock_pskFilters', JSON.stringify(pskFilters));
} catch (e) {}
}, [pskFilters]);
const dxCluster = useDXCluster(config.dxClusterSource || 'auto', dxFilters); const dxCluster = useDXCluster(config.dxClusterSource || 'auto', dxFilters);
const dxPaths = useDXPaths(); const dxPaths = useDXPaths();
const dxpeditions = useDXpeditions(); const dxpeditions = useDXpeditions();
@ -193,6 +209,25 @@ const App = () => {
const localWeather = useLocalWeather(config.location); const localWeather = useLocalWeather(config.location);
const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' }); const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' });
// Filter PSKReporter spots for map display
const filteredPskSpots = useMemo(() => {
const allSpots = [...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])];
if (!pskFilters?.bands?.length && !pskFilters?.grids?.length && !pskFilters?.modes?.length) {
return allSpots;
}
return allSpots.filter(spot => {
if (pskFilters?.bands?.length && !pskFilters.bands.includes(spot.band)) return false;
if (pskFilters?.modes?.length && !pskFilters.modes.includes(spot.mode)) return false;
if (pskFilters?.grids?.length) {
const grid = spot.receiverGrid || spot.senderGrid;
if (!grid) return false;
const gridPrefix = grid.substring(0, 2).toUpperCase();
if (!pskFilters.grids.includes(gridPrefix)) return false;
}
return true;
});
}, [pskReporter.txReports, pskReporter.rxReports, pskFilters]);
// Computed values // Computed values
const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]); const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]);
const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]); const dxGrid = useMemo(() => calculateGridSquare(dxLocation.lat, dxLocation.lon), [dxLocation]);
@ -461,7 +496,7 @@ const App = () => {
dxPaths={dxPaths.data} dxPaths={dxPaths.data}
dxFilters={dxFilters} dxFilters={dxFilters}
satellites={satellites.data} satellites={satellites.data}
pskReporterSpots={[...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])]} pskReporterSpots={filteredPskSpots}
showDXPaths={mapLayers.showDXPaths} showDXPaths={mapLayers.showDXPaths}
showDXLabels={mapLayers.showDXLabels} showDXLabels={mapLayers.showDXLabels}
onToggleDXLabels={toggleDXLabels} onToggleDXLabels={toggleDXLabels}
@ -599,7 +634,7 @@ const App = () => {
dxPaths={dxPaths.data} dxPaths={dxPaths.data}
dxFilters={dxFilters} dxFilters={dxFilters}
satellites={satellites.data} satellites={satellites.data}
pskReporterSpots={[...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])]} pskReporterSpots={filteredPskSpots}
showDXPaths={mapLayers.showDXPaths} showDXPaths={mapLayers.showDXPaths}
showDXLabels={mapLayers.showDXLabels} showDXLabels={mapLayers.showDXLabels}
onToggleDXLabels={toggleDXLabels} onToggleDXLabels={toggleDXLabels}
@ -648,6 +683,8 @@ const App = () => {
callsign={config.callsign} callsign={config.callsign}
showOnMap={mapLayers.showPSKReporter} showOnMap={mapLayers.showPSKReporter}
onToggleMap={togglePSKReporter} onToggleMap={togglePSKReporter}
filters={pskFilters}
onOpenFilters={() => setShowPSKFilters(true)}
onShowOnMap={(report) => { onShowOnMap={(report) => {
if (report.lat && report.lon) { if (report.lat && report.lon) {
setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender }); setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender });
@ -692,6 +729,12 @@ const App = () => {
isOpen={showDXFilters} isOpen={showDXFilters}
onClose={() => setShowDXFilters(false)} onClose={() => setShowDXFilters(false)}
/> />
<PSKFilterManager
filters={pskFilters}
onFilterChange={setPskFilters}
isOpen={showPSKFilters}
onClose={() => setShowPSKFilters(false)}
/>
</div> </div>
); );
}; };

@ -0,0 +1,405 @@
/**
* PSKFilterManager Component
* Filter modal for PSKReporter spots - Bands, Grids, Modes
*/
import React, { useState } from 'react';
const BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m', '70cm'];
const MODES = ['FT8', 'FT4', 'JS8', 'WSPR', 'JT65', 'JT9', 'MSK144', 'Q65', 'FST4', 'FST4W'];
// Common grid field prefixes by region
const GRID_REGIONS = [
{ name: 'North America East', grids: ['FN', 'FM', 'EN', 'EM', 'DN', 'DM'] },
{ name: 'North America West', grids: ['CN', 'CM', 'DM', 'DN', 'BN', 'BM'] },
{ name: 'Europe', grids: ['JO', 'JN', 'IO', 'IN', 'KO', 'KN', 'LO', 'LN'] },
{ name: 'South America', grids: ['GG', 'GH', 'GI', 'FG', 'FH', 'FI', 'FF', 'FE'] },
{ name: 'Asia', grids: ['PM', 'PL', 'OM', 'OL', 'QL', 'QM', 'NM', 'NL'] },
{ name: 'Oceania', grids: ['QF', 'QG', 'PF', 'PG', 'RF', 'RG', 'OF', 'OG'] },
{ name: 'Africa', grids: ['KH', 'KG', 'JH', 'JG', 'IH', 'IG'] },
];
export const PSKFilterManager = ({ filters, onFilterChange, isOpen, onClose }) => {
const [activeTab, setActiveTab] = useState('bands');
const [customGrid, setCustomGrid] = useState('');
if (!isOpen) return null;
const toggleArrayItem = (key, item) => {
const current = filters[key] || [];
const newArray = current.includes(item)
? current.filter(x => x !== item)
: [...current, item];
onFilterChange({ ...filters, [key]: newArray.length ? newArray : undefined });
};
const selectAll = (key, items) => {
onFilterChange({ ...filters, [key]: [...items] });
};
const clearFilter = (key) => {
const newFilters = { ...filters };
delete newFilters[key];
onFilterChange(newFilters);
};
const clearAllFilters = () => {
onFilterChange({});
};
const addCustomGrid = () => {
if (customGrid.trim() && customGrid.length >= 2) {
const grid = customGrid.toUpperCase().substring(0, 2);
const current = filters?.grids || [];
if (!current.includes(grid)) {
onFilterChange({ ...filters, grids: [...current, grid] });
}
setCustomGrid('');
}
};
const getActiveFilterCount = () => {
let count = 0;
if (filters?.bands?.length) count += filters.bands.length;
if (filters?.grids?.length) count += filters.grids.length;
if (filters?.modes?.length) count += filters.modes.length;
return count;
};
const tabStyle = (active) => ({
padding: '8px 16px',
background: active ? 'var(--bg-tertiary)' : 'transparent',
border: 'none',
borderBottom: active ? '2px solid var(--accent-cyan)' : '2px solid transparent',
color: active ? 'var(--accent-cyan)' : 'var(--text-muted)',
fontSize: '13px',
cursor: 'pointer',
fontFamily: 'inherit'
});
const chipStyle = (selected) => ({
padding: '6px 12px',
background: selected ? 'rgba(0, 221, 255, 0.2)' : 'var(--bg-tertiary)',
border: `1px solid ${selected ? 'var(--accent-cyan)' : 'var(--border-color)'}`,
borderRadius: '4px',
color: selected ? 'var(--accent-cyan)' : 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
fontFamily: 'JetBrains Mono, monospace'
});
const renderBandsTab = () => (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>
Filter by Band
</span>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => selectAll('bands', BANDS)}
style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', fontSize: '12px', cursor: 'pointer' }}
>
Select All
</button>
<button
onClick={() => clearFilter('bands')}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}
>
Clear
</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{BANDS.map(band => (
<button
key={band}
onClick={() => toggleArrayItem('bands', band)}
style={chipStyle(filters?.bands?.includes(band))}
>
{band}
</button>
))}
</div>
<div style={{ marginTop: '15px', fontSize: '11px', color: 'var(--text-muted)' }}>
{filters?.bands?.length
? `Showing only: ${filters.bands.join(', ')}`
: 'Showing all bands (no filter)'}
</div>
</div>
);
const renderGridsTab = () => (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>
Filter by Grid Square
</span>
<button
onClick={() => clearFilter('grids')}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}
>
Clear All
</button>
</div>
{/* Custom grid input */}
<div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
<input
type="text"
placeholder="Add grid (e.g. FN)"
value={customGrid}
onChange={(e) => setCustomGrid(e.target.value.toUpperCase())}
maxLength={2}
onKeyPress={(e) => e.key === 'Enter' && addCustomGrid()}
style={{
flex: 1,
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '4px',
color: 'var(--text-primary)',
fontSize: '13px',
fontFamily: 'JetBrains Mono'
}}
/>
<button
onClick={addCustomGrid}
style={{
padding: '8px 16px',
background: 'var(--accent-cyan)',
border: 'none',
borderRadius: '4px',
color: '#000',
fontSize: '12px',
cursor: 'pointer',
fontWeight: '600'
}}
>
Add
</button>
</div>
{/* Selected grids */}
{filters?.grids?.length > 0 && (
<div style={{ marginBottom: '20px' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '8px' }}>
Active Grid Filters:
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{filters.grids.map(grid => (
<button
key={grid}
onClick={() => toggleArrayItem('grids', grid)}
style={{
...chipStyle(true),
display: 'flex',
alignItems: 'center',
gap: '6px'
}}
>
{grid}
<span style={{ color: 'var(--accent-red)', fontWeight: '700' }}>×</span>
</button>
))}
</div>
</div>
)}
{/* Quick select by region */}
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '10px' }}>
Quick Select by Region:
</div>
{GRID_REGIONS.map(region => (
<div key={region.name} style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '6px' }}>
{region.name}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{region.grids.map(grid => (
<button
key={grid}
onClick={() => toggleArrayItem('grids', grid)}
style={{
...chipStyle(filters?.grids?.includes(grid)),
padding: '4px 8px',
fontSize: '11px'
}}
>
{grid}
</button>
))}
</div>
</div>
))}
</div>
);
const renderModesTab = () => (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>
Filter by Mode
</span>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => selectAll('modes', MODES)}
style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', fontSize: '12px', cursor: 'pointer' }}
>
Select All
</button>
<button
onClick={() => clearFilter('modes')}
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}
>
Clear
</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{MODES.map(mode => (
<button
key={mode}
onClick={() => toggleArrayItem('modes', mode)}
style={chipStyle(filters?.modes?.includes(mode))}
>
{mode}
</button>
))}
</div>
<div style={{ marginTop: '15px', fontSize: '11px', color: 'var(--text-muted)' }}>
{filters?.modes?.length
? `Showing only: ${filters.modes.join(', ')}`
: 'Showing all modes (no filter)'}
</div>
</div>
);
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 2000
}}
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<div style={{
background: 'var(--bg-primary)',
border: '1px solid var(--border-color)',
borderRadius: '8px',
width: '500px',
maxWidth: '95vw',
maxHeight: '85vh',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5)'
}}>
{/* Header */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px 20px',
borderBottom: '1px solid var(--border-color)'
}}>
<div>
<h3 style={{ margin: 0, fontSize: '16px', color: 'var(--text-primary)' }}>
📡 PSKReporter Filters
</h3>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
{getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active
</span>
</div>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'var(--text-muted)',
fontSize: '24px',
cursor: 'pointer',
lineHeight: 1
}}
>
×
</button>
</div>
{/* Tabs */}
<div style={{
display: 'flex',
borderBottom: '1px solid var(--border-color)',
background: 'var(--bg-secondary)'
}}>
<button onClick={() => setActiveTab('bands')} style={tabStyle(activeTab === 'bands')}>
Bands {filters?.bands?.length ? `(${filters.bands.length})` : ''}
</button>
<button onClick={() => setActiveTab('grids')} style={tabStyle(activeTab === 'grids')}>
Grids {filters?.grids?.length ? `(${filters.grids.length})` : ''}
</button>
<button onClick={() => setActiveTab('modes')} style={tabStyle(activeTab === 'modes')}>
Modes {filters?.modes?.length ? `(${filters.modes.length})` : ''}
</button>
</div>
{/* Tab Content */}
<div style={{
flex: 1,
overflow: 'auto',
padding: '20px'
}}>
{activeTab === 'bands' && renderBandsTab()}
{activeTab === 'grids' && renderGridsTab()}
{activeTab === 'modes' && renderModesTab()}
</div>
{/* Footer */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
padding: '16px 20px',
borderTop: '1px solid var(--border-color)',
background: 'var(--bg-secondary)'
}}>
<button
onClick={clearAllFilters}
style={{
padding: '8px 16px',
background: 'transparent',
border: '1px solid var(--accent-red)',
borderRadius: '4px',
color: 'var(--accent-red)',
fontSize: '13px',
cursor: 'pointer'
}}
>
Clear All Filters
</button>
<button
onClick={onClose}
style={{
padding: '8px 24px',
background: 'var(--accent-cyan)',
border: 'none',
borderRadius: '4px',
color: '#000',
fontSize: '13px',
cursor: 'pointer',
fontWeight: '600'
}}
>
Done
</button>
</div>
</div>
</div>
);
};
export default PSKFilterManager;

@ -3,12 +3,18 @@
* Shows where your digital mode signals are being received * Shows where your digital mode signals are being received
* Uses MQTT WebSocket for real-time data * Uses MQTT WebSocket for real-time data
*/ */
import React, { useState } from 'react'; import React, { useState, useMemo } from 'react';
import { usePSKReporter } from '../hooks/usePSKReporter.js'; import { usePSKReporter } from '../hooks/usePSKReporter.js';
import { getBandColor } from '../utils/callsign.js'; import { getBandColor } from '../utils/callsign.js';
const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) => { const PSKReporterPanel = ({
const [timeWindow] = useState(15); // Keep spots for 15 minutes callsign,
onShowOnMap,
showOnMap,
onToggleMap,
filters = {},
onOpenFilters
}) => {
const [activeTab, setActiveTab] = useState('tx'); // Default to 'tx' (Being Heard) const [activeTab, setActiveTab] = useState('tx'); // Default to 'tx' (Being Heard)
const { const {
@ -22,11 +28,44 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
source, source,
refresh refresh
} = usePSKReporter(callsign, { } = usePSKReporter(callsign, {
minutes: timeWindow, minutes: 15,
enabled: callsign && callsign !== 'N0CALL' enabled: callsign && callsign !== 'N0CALL'
}); });
const reports = activeTab === 'tx' ? txReports : rxReports; // Filter reports by band, grid, and mode
const filterReports = (reports) => {
return reports.filter(r => {
// Band filter
if (filters?.bands?.length && !filters.bands.includes(r.band)) return false;
// Grid filter (prefix match)
if (filters?.grids?.length) {
const grid = activeTab === 'tx' ? r.receiverGrid : r.senderGrid;
if (!grid) return false;
const gridPrefix = grid.substring(0, 2).toUpperCase();
if (!filters.grids.includes(gridPrefix)) return false;
}
// Mode filter
if (filters?.modes?.length && !filters.modes.includes(r.mode)) return false;
return true;
});
};
const filteredTx = useMemo(() => filterReports(txReports), [txReports, filters, activeTab]);
const filteredRx = useMemo(() => filterReports(rxReports), [rxReports, filters, activeTab]);
const filteredReports = activeTab === 'tx' ? filteredTx : filteredRx;
// Count active filters
const getActiveFilterCount = () => {
let count = 0;
if (filters?.bands?.length) count++;
if (filters?.grids?.length) count++;
if (filters?.modes?.length) count++;
return count;
};
const filterCount = getActiveFilterCount();
// Get band color from frequency // Get band color from frequency
const getFreqColor = (freqMHz) => { const getFreqColor = (freqMHz) => {
@ -77,7 +116,7 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
height: '100%', height: '100%',
overflow: 'hidden' overflow: 'hidden'
}}> }}>
{/* Header - matches DX Cluster style */} {/* Header */}
<div style={{ <div style={{
fontSize: '12px', fontSize: '12px',
color: 'var(--accent-primary)', color: 'var(--accent-primary)',
@ -89,6 +128,24 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
}}> }}>
<span>📡 PSKReporter {getStatusIndicator()}</span> <span>📡 PSKReporter {getStatusIndicator()}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>
{filteredReports.length}/{activeTab === 'tx' ? txCount : rxCount}
</span>
<button
onClick={onOpenFilters}
style={{
background: filterCount > 0 ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.3)',
border: `1px solid ${filterCount > 0 ? '#ffaa00' : '#666'}`,
color: filterCount > 0 ? '#ffaa00' : '#888',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '10px',
fontFamily: 'JetBrains Mono',
cursor: 'pointer'
}}
>
🔍 Filters
</button>
<button <button
onClick={refresh} onClick={refresh}
disabled={loading} disabled={loading}
@ -126,7 +183,7 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
</div> </div>
</div> </div>
{/* Tabs - compact style */} {/* Tabs */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
gap: '4px', gap: '4px',
@ -146,7 +203,7 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
fontFamily: 'JetBrains Mono' fontFamily: 'JetBrains Mono'
}} }}
> >
📤 Being Heard ({txCount}) 📤 Being Heard ({filterCount > 0 ? `${filteredTx.length}` : txCount})
</button> </button>
<button <button
onClick={() => setActiveTab('rx')} onClick={() => setActiveTab('rx')}
@ -162,29 +219,31 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
fontFamily: 'JetBrains Mono' fontFamily: 'JetBrains Mono'
}} }}
> >
📥 Hearing ({rxCount}) 📥 Hearing ({filterCount > 0 ? `${filteredRx.length}` : rxCount})
</button> </button>
</div> </div>
{/* Reports list - matches DX Cluster style */} {/* Reports list */}
{error && !connected ? ( {error && !connected ? (
<div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}> <div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}>
Connection failed - click 🔄 to retry Connection failed - click 🔄 to retry
</div> </div>
) : loading && reports.length === 0 ? ( ) : loading && filteredReports.length === 0 && filterCount === 0 ? (
<div style={{ textAlign: 'center', padding: '15px', color: 'var(--text-muted)', fontSize: '11px' }}> <div style={{ textAlign: 'center', padding: '15px', color: 'var(--text-muted)', fontSize: '11px' }}>
<div className="loading-spinner" style={{ margin: '0 auto 8px' }} /> <div className="loading-spinner" style={{ margin: '0 auto 8px' }} />
Connecting to MQTT... Connecting to MQTT...
</div> </div>
) : !connected && reports.length === 0 ? ( ) : !connected && filteredReports.length === 0 && filterCount === 0 ? (
<div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}> <div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}>
Waiting for connection... Waiting for connection...
</div> </div>
) : reports.length === 0 ? ( ) : filteredReports.length === 0 ? (
<div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}> <div style={{ textAlign: 'center', padding: '10px', color: 'var(--text-muted)', fontSize: '11px' }}>
{activeTab === 'tx' {filterCount > 0
? 'Waiting for spots... (TX to see reports)' ? 'No spots match filters'
: 'No stations heard yet'} : activeTab === 'tx'
? 'Waiting for spots... (TX to see reports)'
: 'No stations heard yet'}
</div> </div>
) : ( ) : (
<div style={{ <div style={{
@ -193,7 +252,7 @@ const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) =>
fontSize: '12px', fontSize: '12px',
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}}> }}>
{reports.slice(0, 20).map((report, i) => { {filteredReports.slice(0, 20).map((report, i) => {
const freqMHz = report.freqMHz || (report.freq ? (report.freq / 1000000).toFixed(3) : '?'); const freqMHz = report.freqMHz || (report.freq ? (report.freq / 1000000).toFixed(3) : '?');
const color = getFreqColor(freqMHz); const color = getFreqColor(freqMHz);
const displayCall = activeTab === 'tx' ? report.receiver : report.sender; const displayCall = activeTab === 'tx' ? report.receiver : report.sender;

@ -13,6 +13,7 @@ export { ContestPanel } from './ContestPanel.jsx';
export { LocationPanel } from './LocationPanel.jsx'; export { LocationPanel } from './LocationPanel.jsx';
export { SettingsPanel } from './SettingsPanel.jsx'; export { SettingsPanel } from './SettingsPanel.jsx';
export { DXFilterManager } from './DXFilterManager.jsx'; export { DXFilterManager } from './DXFilterManager.jsx';
export { PSKFilterManager } from './PSKFilterManager.jsx';
export { SolarPanel } from './SolarPanel.jsx'; export { SolarPanel } from './SolarPanel.jsx';
export { PropagationPanel } from './PropagationPanel.jsx'; export { PropagationPanel } from './PropagationPanel.jsx';
export { DXpeditionPanel } from './DXpeditionPanel.jsx'; export { DXpeditionPanel } from './DXpeditionPanel.jsx';

Loading…
Cancel
Save

Powered by TurnKey Linux.