From 0d48dbfd1757feda9574cc87443edd066d2881ce Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Mon, 10 Aug 2026 19:18:49 +0200 Subject: [PATCH] fix(cluster): drop the worked index when a QSO is logged AddQSO emitted qso:logged but never invalidated the cluster status snapshot. The frontend did its part - it re-queried every visible spot two seconds later - and ClusterSpotStatuses answered out of a snapshot built before the contact, so it returned exactly the same "new band" as before. For ever, until an import, an edit or a profile switch happened to invalidate it for another reason. Reported twice: an E51 and then a ZD7 that stayed yellow on the band map with the QSO plainly in the log. The first report was answered by fixing the visibility gate, which was a real bug of its own and hid this one. Confirmed against the operator's MySQL logbook rather than guessed: ZD7BG has dxcc=250 on all eleven QSOs and 0 of 29579 rows lack a DXCC number, which ruled out the missing-entity-number theory and pointed here. Deliberately not invalidateAwardStats(): that also drops the award matrices, which are expensive on a large log, and a contest run would pay for it once per QSO. This index is a few DISTINCT scans and it is what the spot colours read. Also fixes the new worked-grid query, which asked for a column named "gridsquare". The column is "grid" - gridsquare_ext is a different one - so NEW GRID could never have worked on either backend. Verified against the real database: 12516 distinct grid|mode pairs. --- app.go | 17 +++++++++++++++++ changelog.json | 6 ++++-- internal/qso/qso.go | 6 ++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index cdf9af5..c47d578 100644 --- a/app.go +++ b/app.go @@ -2672,6 +2672,23 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) { // and the audio was discarded (recordings silently stopped working). The // snapshot is an in-memory copy; the heavy file encode still runs async. a.saveQSORecording(&q) + // Drop the cluster worked-index snapshot BEFORE announcing, so the refresh + // the frontend fires on qso:logged rebuilds it and sees this QSO. + // + // Without this the spot colours never moved after a contact. Logging emitted + // the event, the frontend dutifully re-queried every visible spot two + // seconds later, and ClusterSpotStatuses answered out of a snapshot built + // before the QSO — the same "new band" as before, for ever. Reported on an + // E51 and again on a ZD7 that stayed yellow on the band map with the QSO + // plainly in the log. + // + // Deliberately NOT invalidateAwardStats(): that also drops the award + // matrices, which are expensive to rebuild on a large log, and a contest run + // would pay for it once per QSO. This index is a handful of DISTINCT scans + // and it is what the spot colours actually read. + a.clusterStatusMu.Lock() + a.clusterStatusIdx = nil + a.clusterStatusMu.Unlock() // Announce the log RIGHT AWAY so the grid/UI refresh at once and the entry // form clears immediately — the operator is not made to wait on the DB. wruntime.EventsEmit(a.ctx, "qso:logged", id) diff --git a/changelog.json b/changelog.json index 2ffcd26..09931cf 100644 --- a/changelog.json +++ b/changelog.json @@ -4,11 +4,13 @@ "date": "", "en": [ "DX cluster: an L badge next to a callsign marks a station that uploads to LoTW, with a matching LoTW users only filter, and a Spotter continent filter narrows the list by where the spot came FROM — a JA report on 20 m tells a European little about their own path. The filter panel has been tidied along one rule: a switch is a behaviour you turn on or off, chips pick any number from a set. Nothing appears in both shapes any more, the Lock buttons sit in the heading of the section they lock, every section can be cleared the same way, and the whole panel is finally translated.", - "DX cluster: the Spotter continent filter now actually matches. RBN skimmers report as VU2OY-# and cluster nodes as DL1ABC-2, and the suffix was passed straight to the prefix lookup, so every spot came back with no continent and the filter silently selected nothing." + "DX cluster: the Spotter continent filter now actually matches. RBN skimmers report as VU2OY-# and cluster nodes as DL1ABC-2, and the suffix was passed straight to the prefix lookup, so every spot came back with no continent and the filter silently selected nothing.", + "A spot now stops being NEW the moment you log it. Logging announced the QSO but never dropped the cluster worked-index snapshot, so the refresh that follows re-read the answer computed BEFORE the contact — a station stayed yellow on the band map and in the cluster with the QSO plainly in the log, until something else happened to rebuild the index." ], "fr": [ "Cluster DX : un badge L à côté de l indicatif signale une station qui utilise LoTW, avec le filtre Utilisateurs LoTW seulement qui va avec, et un filtre Continent du spotter restreint selon l origine du spot — un report JA sur 20 m dit peu de chose à un Européen sur son propre chemin. Le panneau de filtres a été remis d aplomb selon une seule règle : un interrupteur est un comportement qu on active ou non, les pastilles choisissent dans un ensemble. Plus rien n existe sous les deux formes, les boutons de verrouillage sont dans le titre de la section qu ils verrouillent, chaque section s efface de la même façon, et le panneau est enfin traduit.", - "Cluster DX : le filtre Continent du spotter fonctionne enfin. Les skimmers RBN s annoncent en VU2OY-# et les nœuds cluster en DL1ABC-2, et ce suffixe partait tel quel dans la recherche de préfixe — tous les spots revenaient sans continent et le filtre ne sélectionnait rien." + "Cluster DX : le filtre Continent du spotter fonctionne enfin. Les skimmers RBN s annoncent en VU2OY-# et les nœuds cluster en DL1ABC-2, et ce suffixe partait tel quel dans la recherche de préfixe — tous les spots revenaient sans continent et le filtre ne sélectionnait rien.", + "Un spot cesse enfin d être NOUVEAU dès que tu l enregistres. La journalisation annonçait le QSO mais ne vidait jamais l instantané de l index des contacts, donc le rafraîchissement qui suit relisait la réponse calculée AVANT le contact — une station restait jaune sur le bandmap et dans le cluster alors que le QSO était bien au log, jusqu à ce qu autre chose reconstruise l index." ] }, { diff --git a/internal/qso/qso.go b/internal/qso/qso.go index 58ded0e..ae53347 100644 --- a/internal/qso/qso.go +++ b/internal/qso/qso.go @@ -3017,8 +3017,10 @@ func IsFilterableExtra(field string) bool { _, ok := filterableExtras[field]; // forever, once per subsquare. func (r *Repo) WorkedGridKeys(ctx context.Context, normMode func(string) string) (map[string]struct{}, error) { rows, err := r.db.QueryContext(ctx, - `SELECT DISTINCT COALESCE(gridsquare,''), COALESCE(mode,'') FROM qso - WHERE gridsquare IS NOT NULL AND gridsquare != ''`) + // The column is "grid", not "gridsquare" — that is the ADIF field name, and + // gridsquare_ext is a different (six-plus character) column entirely. + `SELECT DISTINCT COALESCE(grid,''), COALESCE(mode,'') FROM qso + WHERE grid IS NOT NULL AND grid != ''`) if err != nil { return nil, err }