feat(cluster): feed the locator store from PSK Reporter, filtered at the broker

The store shipped without its main source. Locators came only from this
station's own WSJT-X decodes, which is what the whole MQTT discussion was
about.

PSK Reporter now feeds it through a new OnGrid callback, fired before any
geographic filtering: what the store wants is "which square is this callsign
in", and that is true whoever happened to hear the report.

The subscription filters on the RECEIVER's square, a level the v2 topic
exposes. Measured on the live feed: the four opening bands unfiltered are 83
messages a second, of which roughly one in a hundred survived the NearKm test
that already existed here — the rest was received, TLS-decrypted, JSON-parsed
and discarded. One ring of squares is 0.2 to 1.2 a second.

By square rather than by DXCC, which was the obvious alternative: one country
measured 1.2 messages a second (OH) against 72.5 (K) on a single band, because
a DXCC can be a continent. By square the same measurement is 0.2 to 1.2, so the
load follows distance — what the feed is actually about — and is the same for
every operator.

Grid chasing subscribes with the "+" band wildcard, so one subscription per
square covers every band instead of one per band per square.

The store gains a source column (decode | mqtt), migrated in place on an
existing file.
This commit is contained in:
2026-08-12 17:16:47 +02:00
parent 82a49150d2
commit 7d33379fe1
15 changed files with 352 additions and 81 deletions
+9 -1
View File
@@ -52,7 +52,7 @@ import {
GetADIFMonitor, SaveADIFMonitor, PickADIFMonitorFile,
GetRelayAuto, SaveRelayAuto, GetStationDevices,
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
GetBandOpenSettings, SaveBandOpenSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids,
GetBandOpenSettings, SaveBandOpenSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetGridCacheStatus,
} from '../../wailsjs/go/main/App';
import type { profile as profileModels } from '../../wailsjs/go/models';
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -1553,6 +1553,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
// feed up or down — so the write has to go where those live.
const [bandOpen, setBandOpen] = useState<any>({ enabled: false, bands: [], available: [] });
const [chaseGrids, setChaseGrids] = useState(false);
const [gridStat, setGridStat] = useState<any>(null);
const [pskrStatus, setPskrStatus] = useState<any>(null);
const saveBandOpen = async (next: any) => {
setBandOpen(next);
@@ -1567,6 +1568,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
// distinguishes "connected" from "connected and receiving nothing".
const t = window.setInterval(async () => {
try { setPskrStatus(await GetPSKReporterStatus()); } catch { /* ignore */ }
try { setGridStat(await GetGridCacheStatus()); } catch { /* ignore */ }
}, 3000);
return () => window.clearInterval(t);
}, []);
@@ -4238,6 +4240,12 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
onCheckedChange={(c) => { setChaseGrids(!!c); SetChaseNewGrids(!!c).catch(() => {}); }} />
<span>{t('clu.chaseGrids')} <span className="text-xs text-muted-foreground">{t('clu.chaseGridsHint')}</span></span>
</label>
{chaseGrids && (
<p className="pl-6 text-xs text-muted-foreground">
{t('clu.chaseGridsStat', { n: gridStat?.known ?? 0, p: gridStat?.pending ?? 0 })}
{pskrStatus?.running ? ` · ${t('bo.feedUp', { n: pskrStatus.received ?? 0 })}` : ''}
</p>
)}
</div>
{/* Band-opening watch. It lives HERE, with the cluster nodes, because