fix(distance): compute it where QSOs are made, not in a settings button

The button was the wrong shape twice over. A maintenance chore does not belong
beside the options an operator actually chooses, and nobody should have to be
told their log is missing a field before it gets filled in.

So the distance is computed where QSOs come from: on the logging path and on the
ADIF import. fillDistance is its own function rather than part of
applyStationDefaults, because the import only applies those when the operator
ticks a box — and a distance is not a station default. It is derived from the
QSO's own two grids and is true whatever was chosen about profile fields.

What is already in the log is handled by a one-time migration at startup,
recorded by a settings key. In the background: on a large log over a remote
MySQL that is thousands of row updates and startup must not wait for a tidy-up.
Marked done only on SUCCESS, so a run cut short by a closed program tries again
next time rather than leaving half the log filled for ever.

An imported value still wins. It came from the log that made the contact, which
knew the real positions rather than two four-character squares.
This commit is contained in:
2026-08-11 20:14:27 +02:00
parent deee8c4618
commit 75a2f73992
4 changed files with 65 additions and 28 deletions
+1 -24
View File
@@ -45,7 +45,7 @@ import {
TestLoTWUpload, ListTQSLStationLocations,
DownloadLoTWUsers, GetLoTWUsersStatus,
GetScpStatus, SetScpEnabled, DownloadScp,
DownloadULSCounties, ULSStatus, BackfillUSCounties, BackfillDistances,
DownloadULSCounties, ULSStatus, BackfillUSCounties,
ComputeStationInfo,
GetUIPref, SetUIPref,
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas, GetFlexBandPower, SaveFlexBandPower,
@@ -1495,14 +1495,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
try { await DownloadULSCounties(); }
catch (e: any) { setUlsBusy(false); setUlsProgress(null); setUlsMsg({ ok: false, text: String(e?.message ?? e) }); }
};
const [distBusy, setDistBusy] = useState(false);
const [distMsg, setDistMsg] = useState<string | null>(null);
const runDistBackfill = async () => {
setDistBusy(true); setDistMsg(null);
try { const r: any = await BackfillDistances(); setDistMsg(t("db.distDone", { f: r?.filled ?? 0, s: r?.scanned ?? 0, n: r?.no_grid ?? 0 })); }
catch (e: any) { setDistMsg(String(e?.message ?? e)); }
finally { setDistBusy(false); }
};
const runBackfill = async () => {
setBackfillBusy(true); setBackfillMsg(null);
try { const r: any = await BackfillUSCounties(); setBackfillMsg(t('uscty.backfillDone', { c: r?.county ?? 0, g: r?.grid ?? 0, s: r?.scanned ?? 0 })); }
@@ -5331,21 +5323,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<>
<SectionHeader title={t('sec.database')} />
{/* Distance backfill. Here rather than beside the county one, which lives
in the US Counties panel: this touches the whole logbook, whatever
country the QSOs are in. */}
<div className="rounded-md border border-border p-3 space-y-2 max-w-2xl mb-5">
<div className="text-xs font-medium">{t('db.distTitle')}</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('db.distIntro')}</p>
<div className="flex items-center gap-3">
<Button size="sm" variant="secondary" onClick={runDistBackfill} disabled={distBusy}>
{distBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
{t('db.distRun')}
</Button>
{distMsg && <span className="text-xs text-muted-foreground">{distMsg}</span>}
</div>
</div>
{/* Settings / application database (settings + profiles) always shown,
distinct from the QSO logbook so the two are never confused. */}
<div className="space-y-2 max-w-2xl mb-5 border border-border/60 rounded-md p-3">
+2 -2
View File
@@ -151,7 +151,7 @@ const en: Dict = {
'uscty.download': 'Download',
'uscty.update': 'Update',
'uscty.done': 'County database ready — {n} callsigns.',
'db.distTitle': 'Fill in distances', 'db.distIntro': 'Nothing records a distance when a QSO is logged, so the field is empty in every ADIF you export. This works it out from the two locators, for QSOs that have both and no distance yet. A distance already recorded is left alone.', 'db.distRun': 'Fill distances', 'db.distDone': '{f} filled out of {s} scanned; {n} had no pair of locators.', 'uscty.backfillTitle': 'Fill existing QSOs',
'uscty.backfillTitle': 'Fill existing QSOs',
'uscty.backfillIntro': 'Resolve county (and grid) for US QSOs already in your log that are missing them. Existing values are kept — only blanks are filled.',
'uscty.backfillRun': 'Fill missing counties',
'uscty.backfillDone': '{s} US QSOs scanned · {c} counties, {g} grids filled.',
@@ -578,7 +578,7 @@ const fr: Dict = {
'uscty.download': 'Télécharger',
'uscty.update': 'Mettre à jour',
'uscty.done': 'Base des comtés prête — {n} indicatifs.',
'db.distTitle': 'Compléter les distances', 'db.distIntro': 'Rien n enregistre de distance quand un QSO est journalisé, le champ part donc vide dans chaque ADIF exporté. Ceci la déduit des deux locators, pour les QSO qui ont les deux et pas encore de distance. Une distance déjà enregistrée n est pas touchée.', 'db.distRun': 'Compléter les distances', 'db.distDone': '{f} complétés sur {s} examinés ; {n} sans les deux locators.', 'uscty.backfillTitle': 'Compléter les QSO existants',
'uscty.backfillTitle': 'Compléter les QSO existants',
'uscty.backfillIntro': "Résout le comté (et le locator) pour les QSO US déjà dans ton log qui n'en ont pas. Les valeurs existantes sont conservées — seuls les vides sont remplis.",
'uscty.backfillRun': 'Remplir les comtés manquants',
'uscty.backfillDone': '{s} QSO US analysés · {c} comtés, {g} locators remplis.',