This commit is contained in:
2026-06-14 01:35:40 +02:00
parent 67203cd4a8
commit 29fd832bcd
7 changed files with 290 additions and 58 deletions
+53 -9
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
AlertCircle, Antenna, CheckCircle2, Clock, Compass, Eraser, Hash, Loader2, Lock,
AlertCircle, Antenna, CheckCircle2, Clock, Compass, Database, Eraser, Hash, Loader2, Lock,
Maximize2, Minimize2, Mic, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Trash2, Unlock, X, Zap,
} from 'lucide-react';
@@ -17,6 +17,7 @@ import {
GetSecretStatus, UnlockSecrets,
RefreshCtyDat,
RotatorGoTo, RotatorStop, GetRotatorHeading,
GetDBConnectionInfo,
GetUltrabeamStatus, SetUltrabeamDirection,
OpenExternalURL,
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus, SendClusterCommand,
@@ -320,6 +321,7 @@ export default function App() {
const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 });
const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false });
const [dbConn, setDbConn] = useState<{ backend: string; label: string } | null>(null);
// Mode OpsLog shows when the rig reports generic DIG_U/DIG_L. OmniRig
// can't tell us if it's FT8 vs FT4 vs RTTY, so the user picks the default
// in Preferences > Hardware > CAT interface.
@@ -798,7 +800,11 @@ export default function App() {
offset: 0,
}), [filterCallsign, activeFilter, qsoLimit]);
const refresh = useCallback(async () => {
// refresh reloads the grid. Returns false on failure. When silent, it doesn't
// surface the error — used by the startup retry, since the logbook DB (a remote
// MySQL especially) can take a few seconds to connect while the UI is already
// mounted, and we don't want to flash "db not available" during that window.
const refresh = useCallback(async (silent = false): Promise<boolean> => {
try {
const f = buildActiveFilter();
const list = await ListQSOFiltered(f as any);
@@ -809,8 +815,10 @@ export default function App() {
setTotal(n);
setMatchCount(matched);
setError('');
return true;
} catch (e: any) {
setError(String(e?.message ?? e));
if (!silent) setError(String(e?.message ?? e));
return false;
}
}, [buildActiveFilter]);
@@ -852,6 +860,10 @@ export default function App() {
// RX band auto-follows the TX band (only differs for cross-band work).
useEffect(() => { setBandRx(band); }, [band]);
// Logbook connection label for the status bar (MySQL host:port/db, or the
// local SQLite file path).
useEffect(() => { GetDBConnectionInfo().then((i) => setDbConn(i as any)).catch(() => {}); }, []);
// RX freq mirrors TX freq on every TX change (unless the rig is in split,
// where the RX freq is genuinely different). It stays editable by hand:
// a manual RX edit sticks until the next TX-freq change re-syncs it.
@@ -959,7 +971,30 @@ export default function App() {
if (s.dx_call?.trim()) restartRecordingForNewTarget(s.dx_call);
}
useEffect(() => { refresh(); }, [refresh]);
// Initial grid load. The logbook (remote MySQL) may still be connecting while
// the UI is mounted, so retry quietly until the first load succeeds rather
// than leaving the grid empty until the next manual refresh.
useEffect(() => {
let alive = true;
let tries = 0;
let timer = 0;
const attempt = async () => {
if (!alive) return;
const ok = await refresh(true);
if (ok && alive) {
// The logbook is now connected — refresh the status-bar label too, in
// case its one-shot fetch ran during the startup race (before the
// backend was determined) and grabbed the wrong/stale value.
GetDBConnectionInfo().then((i) => { if (alive) setDbConn(i as any); }).catch(() => {});
} else if (!ok && alive && tries++ < 30) {
timer = window.setTimeout(attempt, 500);
} else if (!ok && alive) {
refresh(); // give up quietly retrying; surface the error now
}
};
attempt();
return () => { alive = false; if (timer) window.clearTimeout(timer); };
}, [refresh]);
useEffect(() => {
(async () => {
try {
@@ -1629,10 +1664,8 @@ export default function App() {
if (!path) return;
setExporting(true);
const res = await ExportADIF(path, includeAppFields);
// Reuse the error banner area for a brief success note (4s auto-dismiss).
const msg = `ADIF exported${includeAppFields ? ' (full)' : ' (standard)'}: ${res.count.toLocaleString()} QSOs → ${res.path} (${res.size_kb.toLocaleString()} KB)`;
setError(msg);
setTimeout(() => setError((e) => e === msg ? '' : e), 4000);
// Green success toast (auto-dismiss) — not the red error banner.
showToast(`ADIF exported${includeAppFields ? ' (full)' : ' (standard)'}: ${res.count.toLocaleString()} QSOs → ${res.path} (${res.size_kb.toLocaleString()} KB)`);
} catch (e: any) {
setError(`ADIF export failed: ${String(e?.message ?? e)}`);
} finally {
@@ -2621,7 +2654,7 @@ export default function App() {
value={filterCallsign}
onChange={(e) => setFilterCallsign(e.target.value)}
/>
<Button variant="outline" size="sm" onClick={refresh}>
<Button variant="outline" size="sm" onClick={() => refresh()}>
<RefreshCw className="size-3.5" /> Refresh
</Button>
</div>
@@ -3152,6 +3185,17 @@ export default function App() {
onClick={() => { setSettingsSection('rotator'); setShowSettings(true); }}
/>
<div className="flex-1" />
{dbConn && (
<button
type="button"
onClick={() => { setSettingsSection('database'); setShowSettings(true); }}
title={dbConn.backend === 'mysql' ? `Shared MySQL logbook — ${dbConn.label}` : `Local SQLite logbook — ${dbConn.label}`}
className="inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground max-w-[340px]"
>
<Database className={cn('size-3 shrink-0', dbConn.backend === 'mysql' ? 'text-emerald-600' : 'text-muted-foreground')} />
<span className="font-mono truncate">{dbConn.label}</span>
</button>
)}
</footer>
);
})()}