feat: Super Check Partial + N+1 helper; fix Flex binding to SmartSDR CAT; telemetry callsign wait

- SCP/N+1: new internal/scp downloads the community MASTER.SCP master list and a
  docked two-column widget shows, as you type a call, the known calls containing it
  (Partial) and the calls one edit away (N+1) — click to fix a busted call. Opt-in
  in Settings → General; top-bar toggle; queried debounced on callsign input.
- Flex: OpsLog's GUI-client detection was too loose and could bind to "SmartSDR CAT"
  (or DAX) — both carry "smartsdr" in the program name — instead of the real GUI
  client, making SmartSDR CAT drop and reconnect in a loop while OpsLog was open.
  Now it binds only to a real SmartSDR/Maestro GUI client (e.g. a FLEX-8600M's
  integrated screen) and excludes cat/dax; dropped the risky empty-program fallback.
- Telemetry: on a fresh install the callsign isn't set at launch, so the once-a-day
  heartbeat recorded the machine UUID. Now it waits (~10 min) for the operator to
  enter their callsign before sending, falling back to the UUID only if none appears.
This commit is contained in:
2026-07-25 20:05:33 +02:00
parent 7e08553e6e
commit 51e279887d
12 changed files with 619 additions and 7 deletions
+59 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Activity, AlertCircle, Antenna, Bell, CheckCircle2, Clock, CloudOff, Compass, Database, Ear, Eraser, Gauge, Hash, Loader2, Lock,
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Terminal, Trash2, Unlock, X, Zap,
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
} from 'lucide-react';
import {
@@ -25,6 +25,7 @@ import {
GetUltrabeamStatus, SetUltrabeamDirection,
GetAntGeniusStatus, GetAntGeniusSettings, AntGeniusActivate,
GetTunerGeniusStatus, GetTunerGeniusSettings, TunerGeniusAutotune, TunerGeniusSetBypass, TunerGeniusSetOperate, TunerGeniusActivate,
GetScpStatus, ScpLookup,
OpenExternalURL,
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus, SendClusterCommand,
ListClusterServers, ClusterSpotStatuses, SendClusterSpot,
@@ -71,6 +72,7 @@ import { FlexPanel } from '@/components/FlexPanel';
import { IcomPanel } from '@/components/IcomPanel';
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel';
import { ScpPanel, type ScpResult } from '@/components/ScpPanel';
import { FilterBuilder, type QueryFilter } from '@/components/FilterBuilder';
import { AwardsPanel } from '@/components/AwardsPanel';
import { StatsPanel } from '@/components/StatsPanel';
@@ -473,6 +475,10 @@ export default function App() {
const [agEnabled, setAgEnabled] = useState(false);
const [tgStatus, setTgStatus] = useState<TGStatus>({ connected: false });
const [tgEnabled, setTgEnabled] = useState(false);
// Super Check Partial / N+1
const [scpEnabled, setScpEnabled] = useState(false);
const [scpCount, setScpCount] = useState(0);
const [scpResult, setScpResult] = useState<ScpResult>({});
// Per-port optimistic selection that the status poll must not revert until the
// device confirms it (or it expires) — otherwise a stale poll right after a
// click reverts the UI and the click looks like it did nothing.
@@ -1555,6 +1561,7 @@ export default function App() {
const [showRotor, setShowRotor] = useState(() => localStorage.getItem('opslog.showRotor') !== '0');
const [showAntGenius, setShowAntGenius] = useState(() => localStorage.getItem('opslog.showAntGenius') !== '0');
const [showTuner, setShowTuner] = useState(() => localStorage.getItem('opslog.showTuner') !== '0');
const [showScp, setShowScp] = useState(() => localStorage.getItem('opslog.showScp') !== '0');
const [showBeamOnMap, setShowBeamOnMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
// Award code → scanned field (e.g. POTA→pota_ref, WWFF→wwff). Used to route
@@ -1786,6 +1793,31 @@ export default function App() {
TunerGeniusActivate(ch).catch((e) => setError(String(e?.message ?? e)));
};
// Super Check Partial: poll the enabled flag + list size so the widget shows up
// once the operator turns SCP on and the master list has downloaded.
useEffect(() => {
let alive = true;
const load = async () => {
try { const s: any = await GetScpStatus(); if (alive && s) { setScpEnabled(!!s.enabled); setScpCount(s.count || 0); } } catch {}
};
load();
const id = window.setInterval(load, 3000);
const off = EventsOn('scp:updated', load);
return () => { alive = false; window.clearInterval(id); off(); };
}, []);
// Query SCP/N+1 as the callsign changes (debounced). Skipped when disabled or
// the widget is hidden, so we don't hit the backend for nothing.
useEffect(() => {
if (!scpEnabled || !showScp) { setScpResult({}); return; }
const c = callsign.trim();
if (c.length < 2) { setScpResult({}); return; }
let alive = true;
const id = window.setTimeout(async () => {
try { const r: any = await ScpLookup(c); if (alive && r) setScpResult(r as ScpResult); } catch {}
}, 120);
return () => { alive = false; window.clearTimeout(id); };
}, [callsign, scpEnabled, showScp]);
// RX band auto-follows the TX band (only differs for cross-band work).
useEffect(() => { setBandRx(band); }, [band]);
@@ -4330,6 +4362,20 @@ export default function App() {
{showTuner && tgStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />}
</button>
)}
{scpEnabled && (
<button
type="button"
onClick={() => { const v = !showScp; setShowScp(v); writeUiPref('opslog.showScp', v ? '1' : '0'); }}
title={showScp ? 'Super Check Partial — shown · click to hide' : 'Super Check Partial · click to show'}
className={cn(
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
showScp ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
: 'border-border text-muted-foreground hover:bg-muted',
)}
>
<SpellCheck className="size-4" />
</button>
)}
{chatAvailable && (
<button
type="button"
@@ -4801,7 +4847,7 @@ export default function App() {
{/* Reserved free space to the right. The WinKeyer CW keyer and/or the
Digital Voice Keyer take this slot when enabled (Log4OM-style);
otherwise it shows the QRZ profile photo. */}
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showLiveStations && dbConn?.backend === 'mysql')) && (
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showScp && scpEnabled) || (showLiveStations && dbConn?.backend === 'mysql')) && (
// relative + absolute inner (like the F1-F5 panel): a taller widget (e.g.
// the DVK with Auto CQ) can't grow the row — the row height stays set by
// the entry strip and each widget fills that height, scrolling inside.
@@ -4900,6 +4946,17 @@ export default function App() {
/>
</div>
)}
{showScp && scpEnabled && (
<div className="w-[240px] shrink-0 min-h-0">
<ScpPanel
result={scpResult}
currentCall={callsign}
count={scpCount}
onPick={(c) => onCallsignInput(c, { force: true })}
onClose={() => { setShowScp(false); writeUiPref('opslog.showScp', '0'); }}
/>
</div>
)}
{dvkEnabled && (
<div className="w-[320px] shrink-0 min-h-0">
<DvkPanel