up
This commit is contained in:
+79
-17
@@ -15,9 +15,9 @@ import {
|
||||
SetCompactMode,
|
||||
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
|
||||
GetSecretStatus, UnlockSecrets,
|
||||
RefreshCtyDat,
|
||||
RefreshCtyDat, DownloadAllReferenceLists,
|
||||
RotatorGoTo, RotatorStop, GetRotatorHeading,
|
||||
GetDBConnectionInfo,
|
||||
GetDBConnectionInfo, GetLogbookRevision,
|
||||
GetUltrabeamStatus, SetUltrabeamDirection,
|
||||
OpenExternalURL,
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus, SendClusterCommand,
|
||||
@@ -45,6 +45,7 @@ import { SendEQSLModal } from '@/components/qsl/SendEQSLModal';
|
||||
import { AutoEQSL } from '@/components/qsl/AutoEQSL';
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||
import { SettingsModal } from '@/components/SettingsModal';
|
||||
import { FirstRunModal } from '@/components/FirstRunModal';
|
||||
import { QSOEditModal } from '@/components/QSOEditModal';
|
||||
import { BandMap } from '@/components/BandMap';
|
||||
import { MainMap } from '@/components/MainMap';
|
||||
@@ -435,7 +436,6 @@ export default function App() {
|
||||
const [qsos, setQsos] = useState<QSO[]>([]);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [error, setError] = useState('');
|
||||
const [migratedBanner, setMigratedBanner] = useState(false);
|
||||
// Secret vault (encrypted passwords): prompt to unlock at launch when a
|
||||
// passphrase is configured but not yet entered this session.
|
||||
const [unlockOpen, setUnlockOpen] = useState(false);
|
||||
@@ -667,6 +667,7 @@ export default function App() {
|
||||
const [showDeleteAll, setShowDeleteAll] = useState(false);
|
||||
const [deletingAll, setDeletingAll] = useState(false);
|
||||
const [ctyRefreshing, setCtyRefreshing] = useState(false);
|
||||
const [refsDownloading, setRefsDownloading] = useState(false);
|
||||
|
||||
// === ADIF ===
|
||||
const [importing, setImporting] = useState(false);
|
||||
@@ -732,6 +733,7 @@ export default function App() {
|
||||
callsign: '', operator: '',
|
||||
my_grid: '', my_country: '', my_sota_ref: '', my_pota_ref: '',
|
||||
});
|
||||
const [showFirstRun, setShowFirstRun] = useState(false);
|
||||
myCallRef.current = (station.callsign || '').toUpperCase();
|
||||
|
||||
// Bearing/distance from operator's grid to the DX — used by the entry-strip
|
||||
@@ -864,6 +866,36 @@ export default function App() {
|
||||
// local SQLite file path).
|
||||
useEffect(() => { GetDBConnectionInfo().then((i) => setDbConn(i as any)).catch(() => {}); }, []);
|
||||
|
||||
// The logbook can switch at runtime when the active profile changes (each
|
||||
// profile can target its own SQLite/MySQL database). Refresh the grid and the
|
||||
// status-bar label when that happens.
|
||||
useEffect(() => {
|
||||
const off = EventsOn('logbook:changed', () => {
|
||||
GetDBConnectionInfo().then((i) => setDbConn(i as any)).catch(() => {});
|
||||
refresh();
|
||||
});
|
||||
return () => { off(); };
|
||||
}, [refresh]);
|
||||
|
||||
// Live sync for a SHARED MySQL logbook: poll a cheap revision fingerprint so
|
||||
// QSOs another operator's instance adds/removes show up here within a few
|
||||
// seconds, without a manual refresh. Pointless on local SQLite (single writer).
|
||||
useEffect(() => {
|
||||
if (dbConn?.backend !== 'mysql') return;
|
||||
let alive = true;
|
||||
let last = '';
|
||||
const tick = async () => {
|
||||
try {
|
||||
const rev = await GetLogbookRevision();
|
||||
if (alive && last && rev !== last) await refresh();
|
||||
if (alive) last = rev;
|
||||
} catch { /* logbook briefly unavailable — try again next tick */ }
|
||||
};
|
||||
tick();
|
||||
const id = window.setInterval(tick, 2000);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, [dbConn, refresh]);
|
||||
|
||||
// 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.
|
||||
@@ -986,8 +1018,12 @@ export default function App() {
|
||||
// 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 && tries++ < 360) {
|
||||
// Quick retries at first (normal startup connects in ~2 s); then keep
|
||||
// trying for several minutes, because the very first migration against a
|
||||
// slow remote MySQL can legitimately take that long before the logbook
|
||||
// is ready. Stays silent so no "db not available" flashes meanwhile.
|
||||
timer = window.setTimeout(attempt, tries < 20 ? 500 : 1000);
|
||||
} else if (!ok && alive) {
|
||||
refresh(); // give up quietly retrying; surface the error now
|
||||
}
|
||||
@@ -1000,7 +1036,12 @@ export default function App() {
|
||||
try {
|
||||
const st = await GetStartupStatus();
|
||||
if (!st.ok) { setError(`Startup failed: ${st.err}\nDB path: ${st.db_path}`); return; }
|
||||
if ((st as any).migrated_from_app_data) setMigratedBanner(true);
|
||||
// First launch (or a never-configured profile): collect the mandatory
|
||||
// station identity before anything else.
|
||||
try {
|
||||
const ss = await GetStationSettings();
|
||||
if (!ss.callsign?.trim()) setShowFirstRun(true);
|
||||
} catch {}
|
||||
} catch {}
|
||||
// Prompt to unlock encrypted passwords if a passphrase is configured.
|
||||
try {
|
||||
@@ -1186,6 +1227,16 @@ export default function App() {
|
||||
setWkSendOnType(!!s.send_on_type);
|
||||
} catch { /* keyer not configured */ }
|
||||
}, []);
|
||||
|
||||
// Every setting is per-profile, so when the active profile changes the whole
|
||||
// main UI re-reads its config (station identity, lists, CAT, keyer). The Go
|
||||
// side reloads its managers; this keeps the React state in sync.
|
||||
useEffect(() => {
|
||||
const off = EventsOn('profile:changed', () => {
|
||||
loadStation(); loadLists(); loadCATCfg(); reloadWk();
|
||||
});
|
||||
return () => { off(); };
|
||||
}, [loadStation, loadLists, loadCATCfg, reloadWk]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await reloadWk();
|
||||
@@ -1723,11 +1774,12 @@ export default function App() {
|
||||
// Maintenance — bumped here while we only have one entry. Will move
|
||||
// to a Tools → Maintenance submenu once Clublog + LoTW refresh land.
|
||||
{ type: 'item', label: ctyRefreshing ? 'Refreshing cty.dat…' : 'Refresh cty.dat', action: 'tools.refreshCty', disabled: ctyRefreshing },
|
||||
{ type: 'item', label: refsDownloading ? 'Downloading reference lists…' : 'Download reference lists (IOTA/POTA/WWFF/SOTA)', action: 'tools.downloadRefs', disabled: refsDownloading },
|
||||
]},
|
||||
{ name: 'help', label: 'Help', items: [
|
||||
{ type: 'item', label: 'About OpsLog', action: 'help.about', disabled: true },
|
||||
]},
|
||||
], [total, selectedId, ctyRefreshing, exporting, wkEnabled, dvkEnabled]);
|
||||
], [total, selectedId, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled]);
|
||||
|
||||
function handleMenu(action: string) {
|
||||
switch (action) {
|
||||
@@ -1744,6 +1796,21 @@ export default function App() {
|
||||
case 'tools.winkeyer': wkSetEnabled(!wkEnabled); break;
|
||||
case 'tools.dvk': setDvkEnabled((v) => !v); break;
|
||||
case 'tools.refreshCty': refreshCtyDat(); break;
|
||||
case 'tools.downloadRefs': downloadRefs(); break;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadRefs() {
|
||||
if (refsDownloading) return;
|
||||
setRefsDownloading(true);
|
||||
setError('');
|
||||
try {
|
||||
const summary = await DownloadAllReferenceLists();
|
||||
showToast(`Reference lists updated — ${summary}`);
|
||||
} catch (e: any) {
|
||||
setError(`Reference download failed: ${String(e?.message ?? e)}`);
|
||||
} finally {
|
||||
setRefsDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2366,18 +2433,13 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transient toasts (bottom-right). Errors stack on top of the green
|
||||
success toast; both auto-dismiss. */}
|
||||
{migratedBanner && (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-[110] flex items-start gap-3 rounded-lg border border-emerald-400 bg-emerald-50 text-emerald-900 px-4 py-3 text-sm shadow-xl max-w-lg animate-in fade-in slide-in-from-top-2">
|
||||
<span className="flex-1">
|
||||
<strong>Migration complete.</strong> Your data has been copied to the data folder next to OpsLog.exe.
|
||||
Please <strong>restart OpsLog</strong> to use the new location.
|
||||
</span>
|
||||
<button className="shrink-0 text-emerald-600 hover:text-emerald-800" onClick={() => setMigratedBanner(false)}>×</button>
|
||||
</div>
|
||||
{/* First launch: mandatory station identity. Blocks until filled. */}
|
||||
{showFirstRun && (
|
||||
<FirstRunModal onDone={() => { setShowFirstRun(false); loadStation(); refresh(); }} />
|
||||
)}
|
||||
|
||||
{/* Transient toasts (bottom-right). Errors stack on top of the green
|
||||
success toast; both auto-dismiss. */}
|
||||
{/* Unlock encrypted passwords (set via Settings → Security). Dismissable:
|
||||
skipping leaves lookups/uploads without their passwords until unlocked. */}
|
||||
{unlockOpen && (
|
||||
|
||||
Reference in New Issue
Block a user