diff --git a/changelog.json b/changelog.json
index c853e94..9189d0b 100644
--- a/changelog.json
+++ b/changelog.json
@@ -6,13 +6,15 @@
"Switching the settings database no longer shows “OpsLog is already running”: the automatic relaunch now waits for the closing instance to release its lock instead of racing it.",
"HamQTH upload: 8th external service — real-time QSO upload to the HamQTH online logbook with your callbook credentials (auto-upload on log, “Send to…” right-click, QSL Manager backlog upload, connection test).",
"Fixed: the right-click “Send to HAMLOG.online” was uploading the selection to QRZ.com with the QRZ key — it now goes to HAMLOG.online.",
- "LoTW: TQSL no longer refuses non-US stations over MY_CNTY — the field is stripped before signing unless it is the US “XX,County” shape LoTW actually validates (a Canadian “ONTARIO,Kawartha” was rejecting the whole record). Exports also stop gluing a full state name onto the county."
+ "LoTW: TQSL no longer refuses non-US stations over MY_CNTY — the field is stripped before signing unless it is the US “XX,County” shape LoTW actually validates (a Canadian “ONTARIO,Kawartha” was rejecting the whole record). Exports also stop gluing a full state name onto the county.",
+ "DX Cluster: two new chase switches — Chase US counties and Chase new prefixes — and unchecking Chase new grids now also withdraws the NEW GRID badge. Each switch removes its badge from the spots AND its chip from the status filters, like Chase POTA always did."
],
"fr": [
"Changer de base de réglages n’affiche plus « OpsLog is already running » : la relance automatique attend désormais que l’instance qui se ferme libère son verrou au lieu de la prendre de vitesse.",
"Upload HamQTH : 8e service externe — envoi des QSO en temps réel vers le logbook HamQTH avec vos identifiants du lookup (upload auto au log, « Envoyer vers… » au clic droit, rattrapage via le QSL Manager, test de connexion).",
"Corrigé : le clic droit « Envoyer vers HAMLOG.online » envoyait la sélection à QRZ.com avec la clé QRZ — elle part maintenant vers HAMLOG.online.",
- "LoTW : TQSL ne refuse plus les stations hors US à cause de MY_CNTY — le champ est retiré avant signature sauf s’il a la forme US « XX,County » que LoTW valide réellement (un « ONTARIO,Kawartha » canadien rejetait tout l’enregistrement). L’export cesse aussi de coller un nom d’état complet devant le comté."
+ "LoTW : TQSL ne refuse plus les stations hors US à cause de MY_CNTY — le champ est retiré avant signature sauf s’il a la forme US « XX,County » que LoTW valide réellement (un « ONTARIO,Kawartha » canadien rejetait tout l’enregistrement). L’export cesse aussi de coller un nom d’état complet devant le comté.",
+ "DX Cluster : deux nouvelles cases — Chasser les comtés US et Chasser les nouveaux préfixes — et décocher Chasser les nouveaux locators retire désormais aussi le badge NEW GRID. Chaque case enlève son badge des spots ET sa puce des filtres de statut, comme Chase POTA le faisait déjà."
]
},
{
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1c66eea..4b8177a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -100,7 +100,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, chasePota, readSpotDisplayOptions, spotIsWorked, SPOT_DISPLAY_OPTIONS_EXPOSED } from '@/lib/spotDisplay';
+import { applySpotDisplay, chaseCounty, chaseGrid, chasePfx, 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';
@@ -6261,7 +6261,10 @@ 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' },
- ]).filter((s: any) => s.k !== 'new-pota' || chasePota())
+ ]).filter((s: any) => (s.k !== 'new-pota' || chasePota())
+ && (s.k !== 'new-county' || chaseCounty())
+ && (s.k !== 'new-pfx' || chasePfx())
+ && (s.k !== 'new-grid' || chaseGrid()))
.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))}
,
diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx
index 4f546f0..61fdbb0 100644
--- a/frontend/src/components/SettingsModal.tsx
+++ b/frontend/src/components/SettingsModal.tsx
@@ -2078,6 +2078,8 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
};
const [chaseGrids, setChaseGrids] = useState(false);
const [chasePotaOn, setChasePotaOn] = useState(() => localStorage.getItem('opslog.chasePota') !== '0');
+ const [chaseCountyOn, setChaseCountyOn] = useState(() => localStorage.getItem('opslog.chaseCounty') !== '0');
+ const [chasePfxOn, setChasePfxOn] = useState(() => localStorage.getItem('opslog.chasePfx') !== '0');
const [chaseSotaOn, setChaseSotaOn] = useState(() => localStorage.getItem('opslog.chaseSota') !== '0');
const [chaseNew, setChaseNew] = useState(false);
const [spotTTL, setSpotTTL] = useState(0);
@@ -2099,7 +2101,13 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
useEffect(() => {
(async () => {
try { setBandOpen(await GetBandOpenSettings()); } catch { /* defaults stand */ }
- try { setChaseGrids(await GetChaseNewGrids()); } catch { /* defaults stand */ }
+ try {
+ const g = await GetChaseNewGrids();
+ setChaseGrids(g);
+ // Mirror for the display layer (spotDisplay.chaseGrid) — the cluster
+ // gates NEW GRID synchronously from localStorage.
+ writeUiPref('opslog.chaseGrids', g ? '1' : '0');
+ } catch { /* defaults stand */ }
try { setChaseNew(await GetChaseNew()); } catch { /* defaults stand */ }
try { setLinkedAmps((await GetLinkedAmps()) ?? []); } catch { /* defaults stand */ }
try { const n = await GetSpotTTLMinutes(); setSpotTTL(n); setSpotTTLText(String(n)); } catch { /* defaults stand */ }
@@ -5468,9 +5476,19 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
onCheckedChange={(c) => { const v = !!c; setChaseSotaOn(v); writeUiPref('opslog.chaseSota', v ? '1' : '0'); }} />
{t('clu.chaseSota')}
+
+
{chaseGrids && (
diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx
index 37cc01c..34854c7 100644
--- a/frontend/src/lib/i18n.tsx
+++ b/frontend/src/lib/i18n.tsx
@@ -347,7 +347,7 @@ const en: Dict = {
'chg.mode': 'Chase', 'chg.sources': 'Confirmed by', 'chg.card': 'QSL card', 'dec.unconfTip': 'Worked but not confirmed — a QSL to chase', '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.chasePota': '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': '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',
+ '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': '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': 'Chase SOTA', 'clu.chaseSotaHint': 'Off: the SOTA column stays empty.', 'clu.chaseCounty': 'Chase US counties', 'clu.chaseCountyHint': 'Off: no NEW COUNTY badge or filter in the cluster.', 'clu.chasePfx': 'Chase new prefixes', 'clu.chasePfxHint': 'Off: no NEW PFX badge or filter in the cluster.', '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.',
@@ -863,7 +863,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.chasePota': 'Chasser 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': 'Chasser 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.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': 'Chasser 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': 'Chasser le SOTA', 'clu.chaseSotaHint': 'Décoché : la colonne SOTA reste vide.', 'clu.chaseCounty': 'Chasser les comtés US', 'clu.chaseCountyHint': 'Décoché : plus de badge ni de filtre NOUVEAU COMTÉ dans le cluster.', 'clu.chasePfx': 'Chasser les nouveaux préfixes', 'clu.chasePfxHint': 'Décoché : plus de badge ni de filtre NOUVEAU PFX dans le cluster.', '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ù.",
diff --git a/frontend/src/lib/spotDisplay.ts b/frontend/src/lib/spotDisplay.ts
index 4a82e60..848205d 100644
--- a/frontend/src/lib/spotDisplay.ts
+++ b/frontend/src/lib/spotDisplay.ts
@@ -20,6 +20,7 @@ export type SpotDisplayOptions = {
// keep being computed; only the telling stops, so ticking the box back on
// needs no rescan.
chasePota: boolean; chaseSota: boolean;
+ chaseCounty: boolean; chasePfx: boolean; chaseGrid: boolean;
};
// chasePota/chaseSota read the switches directly — for the places that show a
@@ -30,6 +31,18 @@ export function chasePota(): boolean {
export function chaseSota(): boolean {
try { return localStorage.getItem('opslog.chaseSota') !== '0'; } catch { return true; }
}
+export function chaseCounty(): boolean {
+ try { return localStorage.getItem('opslog.chaseCounty') !== '0'; } catch { return true; }
+}
+export function chasePfx(): boolean {
+ try { return localStorage.getItem('opslog.chasePfx') !== '0'; } catch { return true; }
+}
+// chaseGrid mirrors the backend "chase new grids" setting (Settings writes the
+// mirror on load and on toggle) so the display layer can gate NEW GRID without
+// an async round-trip per row.
+export function chaseGrid(): boolean {
+ try { return localStorage.getItem('opslog.chaseGrids') !== '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
@@ -44,16 +57,17 @@ export function readSpotDisplayOptions(): SpotDisplayOptions {
// 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() };
+ return { muteWorked: false, slotHighlight: false, chasePota: chasePota(), chaseSota: chaseSota(), chaseCounty: chaseCounty(), chasePfx: chasePfx(), chaseGrid: chaseGrid() };
}
try {
return {
muteWorked: localStorage.getItem('opslog.clusterMuteWorked') === '1',
slotHighlight: localStorage.getItem('opslog.clusterSlotHighlight') === '1',
chasePota: chasePota(), chaseSota: chaseSota(),
+ chaseCounty: chaseCounty(), chasePfx: chasePfx(), chaseGrid: chaseGrid(),
};
} catch {
- return { muteWorked: false, slotHighlight: false, chasePota: true, chaseSota: true };
+ return { muteWorked: false, slotHighlight: false, chasePota: true, chaseSota: true, chaseCounty: true, chasePfx: true, chaseGrid: true };
}
}
@@ -98,6 +112,17 @@ export function applySpotDisplay(s: T, o: SpotDisplayOptions):
if (!o.chasePota && e.new_pota) {
e = { ...e, new_pota: false } as NonNullable;
}
+ // Same withdrawal for the other extra markers: the facts keep being
+ // computed, only the telling stops — re-ticking a box needs no rescan.
+ if (!o.chaseCounty && e.new_county) {
+ e = { ...e, new_county: false } as NonNullable;
+ }
+ if (!o.chasePfx && e.new_pfx) {
+ e = { ...e, new_pfx: false } as NonNullable;
+ }
+ if (!o.chaseGrid && e.new_grid) {
+ e = { ...e, new_grid: false } as NonNullable;
+ }
return e;
}