up
This commit is contained in:
+53
-9
@@ -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>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user