Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/DockableApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RotatorPanel,
DXpeditionPanel,
PSKReporterPanel,
PSKReporterBandActivityPanel,
APRSPanel,
MapDataListView,
MeshComPanel,
Expand Down Expand Up @@ -442,7 +443,8 @@ export const DockableApp = ({
'propagation-bars': { name: 'VOACAP Bars', icon: '📊', group: 'Propagation' },
'band-conditions': { name: 'Band Conditions', icon: '📶', group: 'Propagation' },
'band-health': { name: 'Band Health', icon: '📶' },
'band-activity': { name: 'Band Activity', icon: '🔥' },
'band-activity': { name: 'Band Activity (Continent)', icon: '🔥' },
'psk-bands': { name: 'Band Activity (PSKR)', icon: '📡' },
ibp: { name: 'IBP Beacons', icon: '📡', group: 'Propagation' },
'dx-cluster': { name: 'DX Cluster', icon: '📻' },
'psk-reporter': { name: 'PSK Reporter', icon: '📡' },
Expand Down Expand Up @@ -904,6 +906,10 @@ export const DockableApp = ({
content = <BandActivityHeatmap dxSpots={dxClusterData.spots} userCallsign={config.callsign} />;
break;

case 'psk-bands':
content = <PSKReporterBandActivityPanel pskReporter={pskReporter} />;
break;

case 'dx-cluster':
content = (
<DXClusterPanel
Expand Down
166 changes: 166 additions & 0 deletions src/components/PSKReporterBandActivityPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* PSK Reporter Band Activity Panel
*
* Dockable panel showing a compact horizontal bar chart of spot counts
* per HF band from PSKReporter data.
*
* Data source: pskReporter prop (txReports + rxReports from usePSKReporter)
* Filtering: user's ohc_psk_age localStorage value
*/
import React, { useState, useEffect, useMemo } from 'react';
import { DEFAULT_BAND_COLORS } from '../utils/bandColors.js';

const BAND_ORDER = [
'160m',
'80m',
'60m',
'40m',
'30m',
'20m',
'17m',
'15m',
'12m',
'10m',
'8m',
'6m',
'4m',
'2m',
'70cm',
];

const PSKReporterBandActivityPanel = ({ pskReporter = {} }) => {
const [pskAge, setPskAge] = useState(() => {
try {
return parseInt(localStorage.getItem('ohc_psk_age')) || 15;
} catch {
return 15;
}
});

// Listen for localStorage changes to pskAge
useEffect(() => {
const sync = () => {
try {
const v = parseInt(localStorage.getItem('ohc_psk_age'));
if (Number.isFinite(v) && v > 0) setPskAge(v);
} catch {}
};
window.addEventListener('ohc-psk-age-changed', sync);
return () => window.removeEventListener('ohc-psk-age-changed', sync);
}, []);

const { txReports = [], rxReports = [] } = pskReporter;

// Compute band counts from reports, filtered by pskAge
const { bandCounts, totalSpots, maxCount } = useMemo(() => {
const cutoff = Date.now() - pskAge * 60 * 1000;
const counts = {};
let total = 0;

const allReports = [...txReports, ...rxReports];
for (const report of allReports) {
if (report.timestamp <= cutoff) continue;
const band = report.band;
if (!band || band === 'Unknown') continue;
counts[band] = (counts[band] || 0) + 1;
}

const entries = Object.entries(counts);
entries.forEach(([, c]) => {
total += c;
});
const sorted = entries.sort((a, b) => b[1] - a[1]);
const max = sorted.length > 0 ? sorted[0][1] : 0;

return { bandCounts: sorted, totalSpots: total, maxCount: max };
}, [txReports, rxReports, pskAge]);

const countsMap = new Map(bandCounts);

return (
<div className="panel" style={{ padding: '12px 14px' }}>
<div style={{ padding: '0 12px' }}>
{BAND_ORDER.map((band) => {
const count = countsMap.get(band) || 0;
const active = count > 0;
const color = DEFAULT_BAND_COLORS[band] || '#888888';
const barWidth = maxCount > 0 ? Math.max((count / maxCount) * 100, 1.5) : 0;
return (
<div
key={band}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
marginBottom: '4px',
fontSize: '11px',
fontFamily: 'var(--font-mono)',
opacity: active ? 1 : 0.4,
}}
>
<span
style={{
width: '32px',
textAlign: 'right',
fontWeight: 700,
color: active ? 'var(--text-primary)' : 'var(--text-muted)',
}}
>
{band}
</span>
<div
style={{
flex: 1,
background: 'var(--bg-tertiary)',
borderRadius: '2px',
height: '12px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${barWidth}%`,
height: '100%',
background: color,
borderRadius: '2px',
}}
/>
</div>
<span
style={{
width: '32px',
textAlign: 'right',
color: active ? 'var(--text-secondary)' : 'var(--text-muted)',
minWidth: '32px',
}}
>
{count}
</span>
</div>
);
})}
</div>
<div
style={{
marginTop: '6px',
padding: '8px 12px 4px',
fontSize: '10px',
color: 'var(--text-muted)',
fontFamily: 'var(--font-mono)',
textAlign: 'center',
}}
>
<hr
style={{
border: 'none',
borderTop: '1px solid var(--border-color)',
margin: '0 0 4px',
}}
/>
Total: {totalSpots} · Last {pskAge} min
</div>
</div>
);
};

export default PSKReporterBandActivityPanel;
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { SolarPanel } from './SolarPanel.jsx';
export { PropagationPanel } from './PropagationPanel.jsx';
export { DXpeditionPanel } from './DXpeditionPanel.jsx';
export { PSKReporterPanel } from './PSKReporterPanel.jsx';
export { default as PSKReporterBandActivityPanel } from './PSKReporterBandActivityPanel.jsx';
export { DXNewsTicker } from './DXNewsTicker.jsx';
export { WeatherPanel } from './WeatherPanel.jsx';
export { AnalogClockPanel } from './AnalogClockPanel.jsx';
Expand Down
14 changes: 14 additions & 0 deletions src/hooks/usePSKReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ export const usePSKReporter = (callsign, options = {}) => {
if (txChanged || rxChanged) {
setLastUpdate(new Date());
}

// Compute band counts filtered by the user's time window and broadcast
const now = Date.now();
const cutoff = now - minutes * 60 * 1000;
const counts = {};
const allReports = [...txReportsRef.current, ...rxReportsRef.current].filter((r) => r.timestamp > cutoff);
for (const report of allReports) {
const band = report.band;
if (!band || band === 'Unknown') continue;
counts[band] = (counts[band] || 0) + 1;
}
const bands = Object.entries(counts).sort((a, b) => b[1] - a[1]);
const total = bands.reduce((sum, [, c]) => sum + c, 0);
window.dispatchEvent(new CustomEvent('psk-band-activity-changed', { detail: { bands, total } }));
},
[identifier, minutes, maxSpots, cleanOldSpots],
);
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/layerRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import * as IBPLayerPlugin from './layers/useIBPLayer.js';
import * as WinlinkGatewaysPlugin from './layers/useWinlinkGateways.js';
import * as AircraftPlugin from './layers/useAircraft.js';
import * as ATCSectorsPlugin from './layers/useATCSectors.js';
import * as PSKReporterBandActivityPlugin from './layers/usePSKReporterBandActivity.js';

// Auto-discover local/custom plugins (gitignored — survive updates)
const localPluginModules = import.meta.glob('./local/*.js', { eager: true });
Expand Down Expand Up @@ -73,6 +74,7 @@ const layerPlugins = [
WinlinkGatewaysPlugin,
AircraftPlugin,
ATCSectorsPlugin,
PSKReporterBandActivityPlugin,
...localPlugins,
];

Expand Down Expand Up @@ -105,6 +107,7 @@ const PINNED_SHORTCUTS = {
'winlink-gateways': 'k',
aircraft: 'x',
'atc-sectors': 'z',
'psk-band-activity': 'b',
};

export function getAllLayers() {
Expand Down
Loading