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:
+59
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user