feat(db): fill in the distances nothing ever recorded

The published page derives a distance as it renders, which fixed the empty
column there but not the cause: the field is empty in the database, so it goes
out empty in every ADIF export and leaves the same gap in whoever imports it.

BackfillDistances computes it from the two locators for QSOs that have both.
Only where it is EMPTY: a stored distance came from the log that recorded the
contact, which knew the real positions, and two four-character squares are a
worse answer that must not overwrite a better one. Whole kilometres, for the
same reason the published column is.

In the Database panel rather than beside the county backfill, which lives under
US Counties: this one touches the whole logbook, whatever country the QSOs are in.
This commit is contained in:
2026-08-11 19:49:03 +02:00
parent d3a405f4f6
commit 92a5f30ac0
7 changed files with 109 additions and 5 deletions
+57
View File
@@ -10337,6 +10337,63 @@ func (a *App) DownloadULSCounties() error {
}
// BackfillUSCountiesResult summarises a bulk county/grid backfill over the log.
// BackfillDistancesResult reports what a distance backfill did.
type BackfillDistancesResult struct {
Scanned int `json:"scanned"` // QSOs examined
Filled int `json:"filled"` // QSOs that gained a distance
NoGrid int `json:"no_grid"` // skipped: one of the two locators is missing
}
// BackfillDistances computes DISTANCE for past QSOs that have both locators and
// no distance recorded.
//
// Nothing has ever computed it when logging — the column is only filled by an
// ADIF import that carried one — so a log made in OpsLog has it empty
// throughout. The published web page works around that by deriving the distance
// as it renders, but the column still travels empty into every ADIF export, and
// that is what leaves the gap in someone else's log.
//
// Only fills what is EMPTY. A stored distance came from the log that recorded
// the contact, which knew the real positions; two four-character squares are a
// worse answer and must not overwrite a better one.
func (a *App) BackfillDistances() (BackfillDistancesResult, error) {
var res BackfillDistancesResult
if a.qso == nil {
return res, fmt.Errorf("db not initialized")
}
rows, err := a.qso.List(a.ctx, qso.ListFilter{Limit: 1_000_000})
if err != nil {
return res, err
}
for i := range rows {
q := rows[i]
res.Scanned++
if q.Distance != nil && *q.Distance > 0 {
continue
}
km, ok := geo.DistanceBetweenGrids(q.MyGrid, q.Grid)
if !ok || km <= 0 {
res.NoGrid++
continue
}
// Whole kilometres: the squares are tens of kilometres across and a
// decimal would assert an accuracy the grids do not carry.
v := math.Round(km)
q.Distance = &v
if err := a.qso.Update(a.ctx, q); err != nil {
applog.Printf("backfill distance: QSO %d: %v", q.ID, err)
continue
}
res.Filled++
}
applog.Printf("backfill distance: %d scanned, %d filled, %d without both grids",
res.Scanned, res.Filled, res.NoGrid)
if res.Filled > 0 {
a.invalidateAwardStats()
}
return res, nil
}
type BackfillUSCountiesResult struct {
Scanned int `json:"scanned"` // US QSOs examined
County int `json:"county"` // QSOs that gained a county