diff --git a/changelog.json b/changelog.json index c27b2ed..673eb08 100644 --- a/changelog.json +++ b/changelog.json @@ -3,6 +3,7 @@ "version": "0.21.1", "date": "2026-07-24", "en": [ + "When a callsign isn't found on QRZ/HamQTH (or you don't use a lookup service), the name, QTH, grid and address are now recovered from the last time you worked that station — provider data still wins when there is a hit.", "Ctrl + mouse wheel zoom is now remembered across restarts (Ctrl+0 resets to 100%).", "Log grid: award column widths are now saved like the other columns, so a width you set survives a restart.", "CW keyer widget: added an F9 macro slot, and empty macros are now hidden (like the voice keyer) — fill them in Settings → CW Keyer.", @@ -18,6 +19,7 @@ "Fixed award references (in the entry strip's Awards tab) not clearing when the callsign changes — clicking one spot then another no longer keeps the previous station's references." ], "fr": [ + "Quand un indicatif est introuvable sur QRZ/HamQTH (ou si tu n'utilises pas de service de lookup), le nom, le QTH, le locator et l'adresse sont maintenant récupérés du dernier QSO avec cette station — les données du service restent prioritaires en cas de résultat.", "Le zoom Ctrl + molette est maintenant conservé après un redémarrage (Ctrl+0 remet à 100 %).", "Grille du log : les largeurs des colonnes de diplômes sont désormais sauvegardées comme les autres colonnes, une largeur réglée survit au redémarrage.", "Widget keyer CW : ajout d'un emplacement de macro F9, et les macros vides sont maintenant masquées (comme le keyer vocal) — remplis-les dans Réglages → Keyer CW.", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ffbb9cc..f7bc4d7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1442,6 +1442,10 @@ export default function App() { // re-populating a field the operator just cleared. const lookupGenRef = useRef(0); const [wb, setWb] = useState(null); + // Live mirror of `wb` so the lookup fallback can read the latest worked-before + // entries synchronously (the two run on separate debounce timers). + const wbRef = useRef(null); + useEffect(() => { wbRef.current = wb; }, [wb]); const [wbBusy, setWbBusy] = useState(false); // Per-award columns for the Recent QSOs / Worked-before grids: load the award @@ -2854,6 +2858,37 @@ export default function App() { catch { setWb(null); } 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 + // lookup service is configured (cty.dat then gives country/zones only). It fills + // 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) { + const last: any = wbRef.current?.entries?.[0]; // entries are qso_date DESC → most recent + if (!last) return; + const ue = userEditedRef.current; + const empty = (v: any) => (v ?? '') === ''; + 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('grid') && empty(r?.grid) && !(r?.lat || r?.lon) && last.grid) setGrid(last.grid); + if (!ue.has('country') && empty(r?.country) && last.country) setCountry(last.country); + setDetails((d) => ({ + ...d, + address: d.address || last.address || '', + state: d.state || last.state || '', + cnty: d.cnty || last.cnty || '', + lat: d.lat ?? (last.lat ?? undefined), + lon: d.lon ?? (last.lon ?? undefined), + dxcc: d.dxcc ?? (last.dxcc || undefined), + cqz: d.cqz ?? (last.cqz || undefined), + ituz: d.ituz ?? (last.ituz || undefined), + cont: d.cont || last.cont || '', + email: d.email || last.email || '', + qsl_via: d.qsl_via || last.qsl_via || '', + })); + if (last.grid || last.lat) setMapZoomSignal((n) => n + 1); + } async function runLookup(call: string) { if (call !== lastLookedUpRef.current) resetAutoFill(); const gen = lookupGenRef.current; // invalidated by ESC / resetEntry @@ -2901,6 +2936,9 @@ export default function App() { email: d.email || (r.email ?? ''), qsl_via: d.qsl_via || (r.qsl_via ?? ''), })); + // 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); 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. @@ -2920,6 +2958,8 @@ export default function App() { if (gen === lookupGenRef.current && call === callsignValRef.current.trim().toUpperCase()) { setLookupResult(null); setLookupError(String(e?.message ?? e)); + // Lookup failed outright — still borrow from the last logged QSO. + fillFromLastQso(); } } finally { // Only clear the spinner if we're still the current lookup — a newer one