feat(cluster): chase switches for POTA and SOTA

An operator who does not chase parks does not want NEW POTA shouting from every
activator spot. Two switches (Settings → DX Cluster, on by default): off, the
marker is withdrawn at the display layer — applySpotDisplay, the same chokepoint
the grid, the band map and the filter read through — so the badge, the colour,
the filter chip and the reference column all go quiet together, and a new-band +
new-POTA spot reads NEW BAND alone. The facts keep being computed; only the
telling stops, so ticking the box back on needs no rescan.
This commit is contained in:
2026-08-28 23:48:22 +02:00
parent 5e38319379
commit bd8719ce99
8 changed files with 63 additions and 14 deletions
+2 -2
View File
@@ -272,11 +272,11 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
// nothing until the next poll happened to hand over a fresh object.
const dispOpts = readSpotDisplayOptions();
const spotStatus = useMemo(() => {
if (!dispOpts.muteWorked && !dispOpts.slotHighlight) return spotStatusRaw;
if (!dispOpts.muteWorked && !dispOpts.slotHighlight && dispOpts.chasePota) return spotStatusRaw;
const out: Record<string, SpotStatusEntry> = {};
for (const k of Object.keys(spotStatusRaw)) out[k] = applySpotDisplay(spotStatusRaw[k], dispOpts) as SpotStatusEntry;
return out;
}, [spotStatusRaw, dispOpts.muteWorked, dispOpts.slotHighlight]);
}, [spotStatusRaw, dispOpts.muteWorked, dispOpts.slotHighlight, dispOpts.chasePota]);
// Re-render when the operator changes their IARU region in Settings.
const [, setRegionTick] = useState(0);
useEffect(() => subscribeIaruRegion(() => setRegionTick((n) => n + 1)), []);
+6 -3
View File
@@ -13,7 +13,7 @@ import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { cleanSpotter, inferSpotMode, spotStatusKey } from '@/lib/spot';
import { markerColour } from '@/lib/spotMarkers';
import { applySpotDisplay, readSpotDisplayOptions } from '@/lib/spotDisplay';
import { applySpotDisplay, chasePota, chaseSota, readSpotDisplayOptions } from '@/lib/spotDisplay';
import { loadLocal, loadRemote, saveState, seedLocal, whenGridPrefsReady } from '@/lib/gridPrefs';
import { distanceUnit, distanceValue, subscribeDistanceUnit } from '@/lib/units';
import { useI18n } from '@/lib/i18n';
@@ -343,7 +343,9 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
},
{
group: 'Spot', label: t('clg2.c.pota'), colId: 'pota',
headerName: t('clg2.c.pota'), field: 'pota_ref' as any, width: 92, cellClass: 'font-mono',
headerName: t('clg2.c.pota'), width: 92, cellClass: 'font-mono',
// Through a valueGetter so the chase switch empties the column live.
valueGetter: (p: any) => (chasePota() ? p.data?.pota_ref ?? '' : ''),
defaultVisible: true,
cellStyle: (p: any) => (statusFor(p)?.new_pota
? fillStyle(markerColour('new_pota'))
@@ -356,7 +358,8 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
// which is why the column is off by default rather than an empty column for
// everyone who does not watch summits.
group: 'Spot', label: t('clg2.c.sota'), colId: 'sota',
headerName: t('clg2.c.sota'), field: 'sota_ref' as any, width: 100, cellClass: 'font-mono',
headerName: t('clg2.c.sota'), width: 100, cellClass: 'font-mono',
valueGetter: (p: any) => (chaseSota() ? p.data?.sota_ref ?? '' : ''),
defaultVisible: false,
cellStyle: () => ({ color: 'var(--success)' }) as any,
},
+15
View File
@@ -2068,6 +2068,8 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
try { await SaveGridScopeSettings(next); } catch { /* the panel keeps the choice either way */ }
};
const [chaseGrids, setChaseGrids] = useState(false);
const [chasePotaOn, setChasePotaOn] = useState(() => localStorage.getItem('opslog.chasePota') !== '0');
const [chaseSotaOn, setChaseSotaOn] = useState(() => localStorage.getItem('opslog.chaseSota') !== '0');
const [chaseNew, setChaseNew] = useState(false);
const [spotTTL, setSpotTTL] = useState(0);
const [spotTTLText, setSpotTTLText] = useState('0');
@@ -5380,6 +5382,19 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
</div>
<div className="border-t border-border/60 pt-3 space-y-2">
{/* Chase switches: off, the marker stops being TOLD, everywhere at
once badge, colour, filter chip, reference column. A "new band
+ new POTA" spot then reads NEW BAND alone. */}
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chasePotaHint')}>
<Checkbox checked={chasePotaOn}
onCheckedChange={(c) => { const v = !!c; setChasePotaOn(v); writeUiPref('opslog.chasePota', v ? '1' : '0'); }} />
{t('clu.chasePota')}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chaseSotaHint')}>
<Checkbox checked={chaseSotaOn}
onCheckedChange={(c) => { const v = !!c; setChaseSotaOn(v); writeUiPref('opslog.chaseSota', v ? '1' : '0'); }} />
{t('clu.chaseSota')}
</label>
<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(() => {}); }} />