feat(cluster): persist learnt locators behind a "Chase new grids" option

The callsign->grid map died with the process. Every restart began with an empty
Locator column that took an hour of listening to refill, and everything learnt
the day before was thrown away.

internal/gridcache is its own SQLite file in the data directory, not a table in
the settings database: that one sits wherever the operator chose to put it,
often a synchronised folder, and a store that rewrites itself every minute has
no business there. Deleting the file costs a few days of listening and nothing
else.

Callsign is the primary key and the newest report wins — operators move, go
portable, go on expedition, and a stale locator is worse than none for grid
chasing because it reads as a square already worked. Entries not seen in two
years are pruned at open: that is the one way this cache can be actively wrong
rather than merely empty, and it is what bounds a store that would otherwise
only grow.

Only CHANGES are queued. The feeds repeat themselves, so writing every report
would put the whole stream in the batch instead of the news in it. Batches
flush on a timer and on shutdown — a restart is exactly when the cache is worth
the most.

Rotation is switched off while the store is attached: the cap would discard
callsigns the database still holds and the lookup would then miss what we know.
Age in the store is the bound instead.
This commit is contained in:
2026-08-12 17:01:05 +02:00
parent 1ef9e553f7
commit 82a49150d2
9 changed files with 570 additions and 5 deletions
+14 -1
View File
@@ -52,7 +52,7 @@ import {
GetADIFMonitor, SaveADIFMonitor, PickADIFMonitorFile,
GetRelayAuto, SaveRelayAuto, GetStationDevices,
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
GetBandOpenSettings, SaveBandOpenSettings, GetPSKReporterStatus,
GetBandOpenSettings, SaveBandOpenSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids,
} from '../../wailsjs/go/main/App';
import type { profile as profileModels } from '../../wailsjs/go/models';
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -1552,6 +1552,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
// has side effects there — adding the RBN nodes, bringing the PSK Reporter
// 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 [pskrStatus, setPskrStatus] = useState<any>(null);
const saveBandOpen = async (next: any) => {
setBandOpen(next);
@@ -1560,6 +1561,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
useEffect(() => {
(async () => {
try { setBandOpen(await GetBandOpenSettings()); } catch { /* defaults stand */ }
try { setChaseGrids(await GetChaseNewGrids()); } catch { /* defaults stand */ }
})();
// Poll the feed while the panel is open: a live count is the only thing that
// distinguishes "connected" from "connected and receiving nothing".
@@ -4227,6 +4229,17 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
things set up once. A preferences dialog you reopen every ten minutes
is a filter in the wrong place. */}
{/* Grid chasing. Here rather than in the filter panel because it is set
up once: it decides whether locators learnt from decodes are KEPT
across restarts, not what the list shows right now. */}
<div className="border-t border-border/60 pt-3 space-y-2">
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox checked={chaseGrids} className="mt-0.5"
onCheckedChange={(c) => { setChaseGrids(!!c); SetChaseNewGrids(!!c).catch(() => {}); }} />
<span>{t('clu.chaseGrids')} <span className="text-xs text-muted-foreground">{t('clu.chaseGridsHint')}</span></span>
</label>
</div>
{/* Band-opening watch. It lives HERE, with the cluster nodes, because
switching it on adds two of them the operator should see that
happen where it happens rather than find nodes they did not add. */}