feat: QSO rate meter (10/60 min) in the header
Opt-in via Settings→General (portable pref opslog.showQsoRate). Shows the contest-style QSO rate in QSOs/hour, projected from the trailing 10-minute (count ×6) and 60-minute windows, between the widget icons and propagation. Backend: qso.RecentRate counts QSOs whose start time falls in each trailing window, scanning only the last 400 rows (cheap on a large log); App.GetQSORate exposes the 10/60-min counts. Frontend refreshes on qso:logged and a 30s tick. The meter shares the propagation grid cell — the header is a fixed 6-column grid, so adding it as its own child pushed profile/band-map/compact onto a second row. i18n EN + FR.
This commit is contained in:
+39
-1
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
AlertCircle, Antenna, Bell, CheckCircle2, Clock, CloudOff, Compass, Database, Ear, Eraser, Hash, Loader2, Lock,
|
||||
Activity, AlertCircle, Antenna, Bell, CheckCircle2, Clock, CloudOff, Compass, Database, Ear, Eraser, Hash, Loader2, Lock,
|
||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ListClusterServers, ClusterSpotStatuses, SendClusterSpot,
|
||||
GetCATSettings,
|
||||
GetSolarData,
|
||||
GetQSORate,
|
||||
LoTWUserInfo,
|
||||
OperatingDefaultForBand,
|
||||
LogUDPLoggedADIF,
|
||||
@@ -1085,6 +1086,20 @@ export default function App() {
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
// Re-read the "beam on map" toggle when Preferences closes (it's edited there).
|
||||
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
|
||||
// QSO-rate meter (10/60 min) in the header — opt-in via Settings→General.
|
||||
const [showQsoRate, setShowQsoRate] = useState(() => localStorage.getItem('opslog.showQsoRate') === '1');
|
||||
useEffect(() => { if (!showSettings) setShowQsoRate(localStorage.getItem('opslog.showQsoRate') === '1'); }, [showSettings]);
|
||||
const [qsoRate, setQsoRate] = useState<{ last10: number; last60: number }>({ last10: 0, last60: 0 });
|
||||
useEffect(() => {
|
||||
if (!showQsoRate) return;
|
||||
const load = () => { GetQSORate().then((r) => setQsoRate({ last10: r?.last10 ?? 0, last60: r?.last60 ?? 0 })).catch(() => {}); };
|
||||
load();
|
||||
// Refresh on each logged QSO (immediate feedback) and on a 30s tick so the
|
||||
// trailing windows roll forward even when nothing new is logged.
|
||||
const off = EventsOn('qso:logged', load);
|
||||
const id = window.setInterval(load, 30 * 1000);
|
||||
return () => { off(); window.clearInterval(id); };
|
||||
}, [showQsoRate]);
|
||||
// Optional deep-link: which Preferences section to open. Cleared on
|
||||
// close so the next plain "Preferences" launch reverts to default.
|
||||
const [settingsSection, setSettingsSection] = useState<string | undefined>(undefined);
|
||||
@@ -3738,6 +3753,28 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* QSO-rate meter (opt-in) + propagation share ONE grid cell: the header
|
||||
is a fixed 6-column grid, so adding the meter as its own child pushed
|
||||
the last columns (profile / band map / compact) onto a 2nd row. */}
|
||||
<div className="flex items-center gap-2">
|
||||
{showQsoRate && (
|
||||
<div className="flex items-center gap-2.5 font-mono px-2.5 h-8 rounded-md border border-border/60 bg-muted/40 whitespace-nowrap"
|
||||
title={t('rate.title')}>
|
||||
<Activity className="size-3.5 text-muted-foreground" />
|
||||
{/* Contest-style rate: QSOs/hour projected from each window
|
||||
(10-min count ×6; the 60-min count is already per hour). */}
|
||||
<span className="inline-flex items-baseline gap-1">
|
||||
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">10′</span>
|
||||
<span className="font-bold text-[12px] text-foreground">{qsoRate.last10 * 6}</span>
|
||||
</span>
|
||||
<span className="inline-flex items-baseline gap-1">
|
||||
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">60′</span>
|
||||
<span className="font-bold text-[12px] text-foreground">{qsoRate.last60}</span>
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[9px] uppercase tracking-wider">Q/h</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Space-weather / propagation — compact, in the header. Live from N0NBH
|
||||
(hamqsl.com), auto-refreshed hourly; the same SFI / A / K are stamped
|
||||
onto each logged QSO. Always renders one element so the grid columns
|
||||
@@ -3765,6 +3802,7 @@ export default function App() {
|
||||
})()}
|
||||
</div>
|
||||
) : <span />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground px-2.5 py-1 bg-muted rounded-md border border-border/60">
|
||||
<Clock className="size-3" />
|
||||
|
||||
Reference in New Issue
Block a user