chore: release v0.21.3
This commit is contained in:
+73
-10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user