chore: release v0.21.3

This commit is contained in:
2026-07-26 16:57:19 +02:00
parent 4fd70f6a9d
commit 91b5af1c7b
46 changed files with 2491 additions and 355 deletions
+73 -10
View File
@@ -22,7 +22,7 @@ import {
RefreshCtyDat, DownloadAllReferenceLists,
RotatorGoTo, RotatorStop, GetRotatorHeading,
GetDBConnectionInfo, GetLogbookRevision,
GetUltrabeamStatus, SetUltrabeamDirection,
GetUltrabeamStatus, SetUltrabeamDirection, UILog,
GetAntGeniusStatus, GetAntGeniusSettings, AntGeniusActivate,
GetTunerGeniusStatus, GetTunerGeniusSettings, TunerGeniusAutotune, TunerGeniusSetBypass, TunerGeniusSetOperate, TunerGeniusActivate,
GetScpStatus, ScpLookup,
@@ -249,6 +249,13 @@ function FreqWheelDisplay({ mhz, onNudge, className, placeholder = '—.——
function shortCatError(err?: string): string {
if (!err) return '';
const e = err.toLowerCase();
// Checked BEFORE the not-found cases: a privilege mismatch used to be condensed
// into "OmniRig not found", which is actively misleading — OmniRig is running,
// visibly, and the operator goes looking for a driver or COM-port fault. The
// full explanation is in the tooltip and the log.
if (e.includes('privilege level') || e.includes('elevation') || e.includes('élévation')) {
return 'OmniRig: run as admin?';
}
if (e.includes('not registered') || e.includes('not available')) return 'OmniRig not found';
if (e.includes('not connected')) return 'not connected';
if (e.includes('coinitialize')) return 'COM error';
@@ -1452,6 +1459,17 @@ export default function App() {
// entries synchronously (the two run on separate debounce timers).
const wbRef = useRef<WB | null>(null);
useEffect(() => { wbRef.current = wb; }, [wb]);
// Which callsign wbRef.current actually belongs to. Worked-before is replaced
// only when its query RESOLVES, so between two calls the ref still holds the
// previous station — and the backfill below would happily copy that station's
// name, QTH and grid onto the new one.
const wbCallRef = useRef('');
// A backfill the lookup asked for before the history had arrived. Parked here
// and replayed by runWorkedBefore: with no QRZ/HamQTH configured the lookup
// answers from local cty.dat almost instantly, while the history query — a
// round trip to a possibly remote MySQL logbook — is still in flight, so the
// backfill found nothing and nothing ever ran it again.
const pendingBackfillRef = useRef<{ call: string; r?: any } | null>(null);
const [wbBusy, setWbBusy] = useState(false);
// Per-award columns for the Recent QSOs / Worked-before grids: load the award
@@ -2886,9 +2904,26 @@ export default function App() {
async function runWorkedBefore(call: string, dxccHint: number = 0) {
setWbBusy(true);
try { setWb(await WorkedBefore(call, dxccHint)); }
catch { setWb(null); }
finally { setWbBusy(false); }
try {
const w = await WorkedBefore(call, dxccHint);
setWb(w);
// Mirrored synchronously rather than through the effect above: a backfill
// parked by the lookup has to read this on the very next line, not a
// render later.
wbRef.current = w;
wbCallRef.current = call;
// The lookup finished before this history did and parked its backfill —
// run it now that we can answer "who did we work last?".
const p = pendingBackfillRef.current;
if (p && p.call === call) {
pendingBackfillRef.current = null;
fillFromLastQso(p.r, call);
}
} catch {
setWb(null);
wbRef.current = null;
wbCallRef.current = '';
} finally { setWbBusy(false); }
}
// fillFromLastQso enriches the entry from the LAST QSO we logged with this call
// when the live lookup came up short — the callsign isn't on QRZ/HamQTH, or no
@@ -2896,11 +2931,32 @@ export default function App() {
// ONLY the fields the provider left empty and the operator hasn't edited, so a
// real QRZ/HamQTH hit is never overridden. `r` is the provider result (omitted
// on a lookup error, where every provider field counts as empty).
function fillFromLastQso(r?: any) {
function fillFromLastQso(r: any, call: string) {
// Only ever fill from THIS callsign's history. If worked-before hasn't
// resolved for it yet, park the request instead of reading whatever the ref
// happens to hold — that would be the previously entered station.
if (wbCallRef.current !== call) {
pendingBackfillRef.current = { call, r };
UILog(`backfill ${call}: history not in yet (have "${wbCallRef.current}") — parked`).catch(() => {});
return;
}
// Parked backfills can land late; drop it if the operator has moved on.
if (call !== callsignValRef.current.trim().toUpperCase()) {
UILog(`backfill ${call}: abandoned, entry now holds "${callsignValRef.current}"`).catch(() => {});
return;
}
const last: any = wbRef.current?.entries?.[0]; // entries are qso_date DESC → most recent
if (!last) return;
if (!last) {
UILog(`backfill ${call}: no prior QSO in the log (count=${wbRef.current?.count ?? 0}, entries=${wbRef.current?.entries?.length ?? 0})`).catch(() => {});
return;
}
const ue = userEditedRef.current;
const empty = (v: any) => (v ?? '') === '';
// One line saying what we found and what blocked each field, so this can be
// diagnosed from another operator's log instead of guessed at.
UILog(`backfill ${call}: last QSO ${last.qso_date ?? '?'} name="${last.name ?? ''}" qth="${last.qth ?? ''}" grid="${last.grid ?? ''}"`
+ ` | provider name="${r?.name ?? ''}" grid="${r?.grid ?? ''}" src=${r?.source ?? 'none'}`
+ ` | edited=[${[...ue].join(',')}]`).catch(() => {});
if (!ue.has('name') && empty(r?.name) && last.name) setName(last.name);
if (!ue.has('qth') && empty(r?.qth) && last.qth) setQth(last.qth);
if (!ue.has('country') && empty(r?.country) && last.country) setCountry(last.country);
@@ -2985,7 +3041,7 @@ export default function App() {
}));
// Backfill anything the provider didn't supply from the last time we worked
// this call (call not found on QRZ/HamQTH, or lookup off → cty.dat only).
fillFromLastQso(r);
fillFromLastQso(r, call);
if (r.dxcc && r.dxcc > 0) runWorkedBefore(call, r.dxcc);
// The DX location is now known (grid set above) — force the world map to
// auto-zoom right away, so it doesn't lag behind the resolved QSO.
@@ -3006,7 +3062,7 @@ export default function App() {
setLookupResult(null);
setLookupError(String(e?.message ?? e));
// Lookup failed outright — still borrow from the last logged QSO.
fillFromLastQso();
fillFromLastQso(undefined, call);
}
} finally {
// Only clear the spinner if we're still the current lookup — a newer one
@@ -3021,6 +3077,9 @@ export default function App() {
const call = value.trim().toUpperCase();
if (call.length < 3) {
setLookupResult(null); setWb(null);
// Drop the history and any backfill waiting on it, so clearing the field
// can't let a late one repopulate the next callsign typed.
wbRef.current = null; wbCallRef.current = ''; pendingBackfillRef.current = null;
if (lastLookedUpRef.current !== '') resetAutoFill();
return;
}
@@ -4082,7 +4141,9 @@ export default function App() {
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
<WorkedBeforeGrid wb={wbWithAwards as any} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog}
onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} onDelete={(ids) => setDeletingIds(ids)} />
onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields}
onExportCabrilloSelected={exportSelectedCabrillo} onDelete={(ids) => setDeletingIds(ids)} />
</div>
);
case 'flex':
@@ -5491,7 +5552,9 @@ export default function App() {
<TabsContent value="worked" className="mt-0 flex flex-col min-h-0 flex-1">
<WorkedBeforeGrid wb={wbWithAwards as any} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording}
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} onDelete={(ids) => setDeletingIds(ids)} />
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields}
onExportCabrilloSelected={exportSelectedCabrillo} onDelete={(ids) => setDeletingIds(ids)} />
</TabsContent>
{/* Opened on demand from Tools QSL Manager; closable via the