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:
@@ -98,7 +98,7 @@ import { ExportFieldsDialog } from '@/components/ExportFieldsDialog';
|
||||
import { ShutdownProgress } from '@/components/ShutdownProgress';
|
||||
import { ClusterGrid } from '@/components/ClusterGrid';
|
||||
import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot';
|
||||
import { applySpotDisplay, readSpotDisplayOptions, spotIsWorked, SPOT_DISPLAY_OPTIONS_EXPOSED } from '@/lib/spotDisplay';
|
||||
import { applySpotDisplay, chasePota, readSpotDisplayOptions, spotIsWorked, SPOT_DISPLAY_OPTIONS_EXPOSED } from '@/lib/spotDisplay';
|
||||
import { AnswerDecode, HaltDecodeTx, LogUIError, FlexTXOnBand, GetMatrixColors, GetRotorPresets, GetRowColors, GetSpotTTLMinutes, GetSpotMax, IsNewUSCounty } from '../wailsjs/go/main/App';
|
||||
import { applyMatrixColors } from '@/lib/matrixColors';
|
||||
import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid';
|
||||
@@ -6127,7 +6127,8 @@ export default function App() {
|
||||
// worked spots; the separate "Hide worked" checkbox drops them — they
|
||||
// are opposite controls, so don't use both at once.
|
||||
{ k: 'worked' as SpotFilterKey, label: 'WORKED', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||
]).map((s: any) => fChip(s.k, s.label, s.cls, clusterStatusFilter.has(s.k),
|
||||
]).filter((s: any) => s.k !== 'new-pota' || chasePota())
|
||||
.map((s: any) => fChip(s.k, s.label, s.cls, clusterStatusFilter.has(s.k),
|
||||
() => setClusterStatusFilter((cur) => { const n = new Set(cur); if (n.has(s.k)) n.delete(s.k); else n.add(s.k); return n; }), s.style))}
|
||||
</div>,
|
||||
clusterStatusFilter.size > 0 ? (
|
||||
|
||||
@@ -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)), []);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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(() => {}); }} />
|
||||
|
||||
@@ -333,7 +333,7 @@ const en: Dict = {
|
||||
'gsc.scope': 'Match a square by', 'gsc.hunt': 'Chase', 'gsc.huntNew': 'New — never worked', 'gsc.huntUnconf': 'New and unconfirmed', 'gsc.scope_band_digi': 'This band + any digital mode', 'gsc.scope_band_mode': 'This band + this exact mode', 'gsc.scope_band_ftx': 'This band + any FT mode (FT8/FT4/FT2)', 'gsc.scope_mix_digi': 'Any band + any digital mode', 'gsc.scope_mix_mode': 'Any band + this exact mode', 'gsc.scope_mix_ftx': 'Any band + any FT mode (FT8/FT4/FT2)', 'gsc.hint': 'Decides when a square stops being NEW. Narrower means more squares to chase: per band and per exact mode is the most demanding, any band and any digital mode the least. Chasing unconfirmed as well keeps a square wanted until a QSL, LoTW or eQSL confirmation arrives — it is still missing from the award until then.',
|
||||
'gsm.basemap': 'Basemap', 'gsm.title': 'Grid squares', 'gsm.all': 'All', 'gsm.phone': 'Phone', 'gsm.cw': 'CW', 'gsm.digital': 'Digital', 'gsm.ftx': 'FTx', 'gsm.confirmed': 'confirmed', 'gsm.worked': 'worked', 'gsm.colConfirmed': 'Colour for confirmed squares', 'gsm.colWorked': 'Colour for worked (unconfirmed) squares', 'gsm.colReset': 'Back to the theme colours', 'gsm.refresh': 'Recount from the log', 'gsm.count': '{n} squares · {c} confirmed',
|
||||
'bo.nearKm': 'Count receivers within', 'bo.nearKmHint': 'A report proves YOUR path only if it was collected near you. Smaller is more local but leaves fewer receivers listening — too small and the watch has nothing to look at. 300 km borrows a whole region; 100 km suits 2 m, where a duct is narrow.',
|
||||
'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.spotTtl': 'Spot lifetime', 'clu.spotTtlNever': 'Keep', 'clu.spotMax': 'Spots kept', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 keeps them.', 'clu.chaseGrids': 'Chase new grids', 'clu.chaseGridsHint': '(learns locators from your own WSJT-X decodes AND from PSK Reporter, and keeps them in their own database so the cluster shows them from the first second)', 'clu.chaseGridsStat': '{n} locators known — {p} waiting to be written', 'clu.workedSameSlot': 'Already worked only on the same slot',
|
||||
'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.spotTtl': 'Spot lifetime', 'clu.spotTtlNever': 'Keep', 'clu.spotMax': 'Spots kept', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 keeps them.', 'clu.chasePota': 'I chase POTA', 'clu.chasePotaHint': 'Off: no NEW POTA badge or filter, and the POTA column stays empty — a new-band + new-POTA spot reads NEW BAND alone.', 'clu.chaseSota': 'I chase SOTA', 'clu.chaseSotaHint': 'Off: the SOTA column stays empty.', 'clu.chaseGrids': 'Chase new grids', 'clu.chaseGridsHint': '(learns locators from your own WSJT-X decodes AND from PSK Reporter, and keeps them in their own database so the cluster shows them from the first second)', 'clu.chaseGridsStat': '{n} locators known — {p} waiting to be written', 'clu.workedSameSlot': 'Already worked only on the same slot',
|
||||
'clu.macros': 'Command buttons', 'clu.macrosHint': 'A named button beside the cluster command box. Leave the command empty and the button is not shown.',
|
||||
'clu.macroLabel': 'Button', 'clu.macroCmd': 'Command',
|
||||
'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere.',
|
||||
@@ -835,7 +835,7 @@ const fr: Dict = {
|
||||
'bo.nearKm': 'Compter les récepteurs à moins de', 'bo.nearKmHint': 'Un report ne prouve TON chemin que s’il a été collecté près de chez toi. Plus petit est plus local, mais laisse moins de récepteurs à l’écoute — trop petit, la veille n’a plus rien à observer. 300 km emprunte les oreilles de toute une région ; 100 km convient au 2 m, où un conduit est étroit.',
|
||||
'bo.open': 'ouvert', 'bo.liveTip': '{band} est ouvert — {n} stations, ~{km} km, {sector}{season}. Cliquer pour le bandmap.', 'bo.enable': 'Surveiller les ouvertures de bande', 'bo.enableHint': "(10, 12, 6, 4 et 2 m. Activer ajoute les deux nœuds RBN et souscrit au flux PSK Reporter — la détection a besoin de bien plus d oreilles qu un cluster ne peut en fournir.)", 'bo.feedUp': 'Flux PSK Reporter actif — {n} décodages vus', 'bo.feedDown': 'Flux PSK Reporter inactif — il faut ton locator, et un instant pour se connecter', 'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot',
|
||||
'clu.macros': 'Boutons de commande', 'clu.macrosHint': 'Un bouton nommé à côté du champ de commande du cluster. Laisse la commande vide et le bouton n’est pas affiché.',
|
||||
'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotMax': 'Nombre de spots conservés', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 les conserve.', 'clu.chaseGrids': 'Chasser les nouveaux locators', 'clu.chaseGridsHint': '(apprend les locators depuis TES propres décodages WSJT-X ET depuis PSK Reporter, et les garde dans leur propre base pour que le cluster les affiche dès la première seconde)', 'clu.chaseGridsStat': '{n} locators connus — {p} en attente d’écriture',
|
||||
'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotMax': 'Nombre de spots conservés', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 les conserve.', 'clu.chasePota': 'Je chasse le POTA', 'clu.chasePotaHint': "Décoché : plus de badge ni de filtre NOUVEAU POTA, et la colonne POTA reste vide — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE.", 'clu.chaseSota': 'Je chasse le SOTA', 'clu.chaseSotaHint': 'Décoché : la colonne SOTA reste vide.', 'clu.chaseGrids': 'Chasser les nouveaux locators', 'clu.chaseGridsHint': '(apprend les locators depuis TES propres décodages WSJT-X ET depuis PSK Reporter, et les garde dans leur propre base pour que le cluster les affiche dès la première seconde)', 'clu.chaseGridsStat': '{n} locators connus — {p} en attente d’écriture',
|
||||
'clu.workedSameSlotHint': "— un spot n'est « contacté » que si cet indicatif a été fait sur la MÊME bande et le même mode, pas n'importe où.",
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,25 @@
|
||||
//
|
||||
// They compose deliberately: mute what is done, light up what is not.
|
||||
|
||||
export type SpotDisplayOptions = { muteWorked: boolean; slotHighlight: boolean };
|
||||
export type SpotDisplayOptions = {
|
||||
muteWorked: boolean; slotHighlight: boolean;
|
||||
// Chase switches (Settings → DX Cluster), ON by default. An operator who does
|
||||
// not chase parks does not want NEW POTA shouting from every activator spot:
|
||||
// off, the marker is withdrawn at the display layer — a "new band + new POTA"
|
||||
// spot simply reads NEW BAND — and the reference column goes quiet. The facts
|
||||
// keep being computed; only the telling stops, so ticking the box back on
|
||||
// needs no rescan.
|
||||
chasePota: boolean; chaseSota: boolean;
|
||||
};
|
||||
|
||||
// chasePota/chaseSota read the switches directly — for the places that show a
|
||||
// REFERENCE rather than a status (the POTA and SOTA columns).
|
||||
export function chasePota(): boolean {
|
||||
try { return localStorage.getItem('opslog.chasePota') !== '0'; } catch { return true; }
|
||||
}
|
||||
export function chaseSota(): boolean {
|
||||
try { return localStorage.getItem('opslog.chaseSota') !== '0'; } catch { return true; }
|
||||
}
|
||||
|
||||
// Both options are withdrawn from the filter panel for now. The machinery below
|
||||
// is deliberately kept whole — it is correct and hard-won — so putting the two
|
||||
@@ -23,14 +41,19 @@ export type SpotDisplayOptions = { muteWorked: boolean; slotHighlight: boolean }
|
||||
export const SPOT_DISPLAY_OPTIONS_EXPOSED = false;
|
||||
|
||||
export function readSpotDisplayOptions(): SpotDisplayOptions {
|
||||
if (!SPOT_DISPLAY_OPTIONS_EXPOSED) return { muteWorked: false, slotHighlight: false };
|
||||
// The EXPOSED flag only withdraws the two original switches; the chase
|
||||
// switches are live regardless.
|
||||
if (!SPOT_DISPLAY_OPTIONS_EXPOSED) {
|
||||
return { muteWorked: false, slotHighlight: false, chasePota: chasePota(), chaseSota: chaseSota() };
|
||||
}
|
||||
try {
|
||||
return {
|
||||
muteWorked: localStorage.getItem('opslog.clusterMuteWorked') === '1',
|
||||
slotHighlight: localStorage.getItem('opslog.clusterSlotHighlight') === '1',
|
||||
chasePota: chasePota(), chaseSota: chaseSota(),
|
||||
};
|
||||
} catch {
|
||||
return { muteWorked: false, slotHighlight: false };
|
||||
return { muteWorked: false, slotHighlight: false, chasePota: true, chaseSota: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +95,9 @@ export function applySpotDisplay<T extends Entry>(s: T, o: SpotDisplayOptions):
|
||||
if (o.muteWorked) {
|
||||
e = { ...e, worked_call: false } as NonNullable<T>;
|
||||
}
|
||||
if (!o.chasePota && e.new_pota) {
|
||||
e = { ...e, new_pota: false } as NonNullable<T>;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ const PORTABLE_KEYS = [
|
||||
'opslog.clusterModeFilter', 'opslog.clusterSearch', 'opslog.clusterHideWorked',
|
||||
'opslog.distanceMiles', // distances shown in statute miles rather than km
|
||||
'opslog.iaruRegion', // IARU region (1/2/3) — band edges and segments on the band maps
|
||||
'opslog.chasePota', // show POTA references and the NEW POTA marker on spots
|
||||
'opslog.chaseSota', // show SOTA references on spots
|
||||
'opslog.activeTab', // last selected tab
|
||||
'opslog.mainSplit', // Main tab: width share of the left pane (percent) — legacy, read once to seed mainShares
|
||||
'opslog.mainShares', // Main tab: column shares per column count, as {2:[..],3:[..],4:[..]}
|
||||
|
||||
Reference in New Issue
Block a user