revert(cluster): keep the filter selections across restarts

They were made session-only on a misreading: the ask was about the FIRST launch
of a fresh install, not every launch. Restored as they were — restored on
reopen, and travelling with a copied data/ folder.

A first-ever launch already starts with nothing filtered: every selection
defaults to empty or off and there is no code that switches one on. The one
route by which a new installation can come up filtered is a copied data/ folder,
which carries the previous machine's selections by design.

The filtered-out message and the clear-every-filter button stay: they are what
makes a filter that IS set visible, whenever it was set.
This commit is contained in:
2026-08-15 23:58:40 +02:00
parent fe2affc72f
commit c2ff0f714a
3 changed files with 36 additions and 42 deletions
-2
View File
@@ -6,14 +6,12 @@
"WinKeyer: the opening probe goes out as one write, matching a capture of a client that talks to the same K3NG keyer, and the handshake bytes are always logged so a keyer that stays silent can be diagnosed.",
"DX cluster: when spots arrive and every one is filtered out, the panel says so, names the filters doing it and offers to clear them — it used to say “waiting for spots” beside a counter reading 76 live.",
"DX cluster: the log times the connection and the first spot, so a slow first launch can be told apart from a quiet node.",
"DX cluster: filters now start off at every launch — a band lock left on from a previous session hid every spot.",
"Generic HTTP relay: its URLs were never actually sent — fixed. Adds {value} and {relay-1}, https, and no host or test needed."
],
"fr": [
"WinKeyer : la sonde douverture part en un seul envoi, calquée sur la capture dun client qui dialogue avec le même manipulateur K3NG, et les octets de la poignée de main sont toujours journalisés pour diagnostiquer un manipulateur muet.",
"Cluster DX : quand des spots arrivent et que tout est filtré, le panneau le dit, nomme les filtres responsables et propose de les effacer — il affichait « en attente de spots » à côté dun compteur à 76 en direct.",
"Cluster DX : le journal chronomètre la connexion et le premier spot, pour distinguer un premier lancement lent dun nœud silencieux.",
"Cluster DX : les filtres démarrent désactivés à chaque lancement — un verrou de bande oublié masquait tous les spots.",
"Relais HTTP générique : ses URL n’étaient jamais envoyées — corrigé. Ajoute {value} et {relay-1}, https, sans hôte ni test."
]
},
+32 -34
View File
@@ -1438,18 +1438,14 @@ export default function App() {
// Ring buffer — only keep the last N spots; cluster firehose can be heavy.
const [spots, setSpots] = useState<ClusterSpot[]>([]);
const SPOTS_CAP = 1000;
// Cluster filters start OFF at every launch, and are deliberately not restored.
//
// They used to persist, which produced the least diagnosable report there is:
// the counter says 76 spots LIVE, the grid is empty, and the cause is a band
// lock switched on weeks ago. A filter you did not just set is invisible — you
// go looking for a broken cluster instead. Nobody loses work by re-ticking a
// chip; people lose an evening to an empty spot list.
//
// Only the DISPLAY choices below (grouping, panel shown) still persist: they
// change how spots look, never whether they appear at all.
// Cluster filter selections persist across restarts (writeUiPref → localStorage
// + DB, so they also travel with a copied data/ folder). Loaders read the cache
// synchronously at first render; a single effect below writes them back.
const lsBool = (k: string, d: boolean) => { const v = localStorage.getItem(k); return v === null ? d : v === '1'; };
const [clusterFilterSource, setClusterFilterSource] = useState<number | ''>('');
const lsSet = <T,>(k: string): Set<T> => { try { const a = JSON.parse(localStorage.getItem(k) || '[]'); return new Set(Array.isArray(a) ? a : []); } catch { return new Set<T>(); } };
const [clusterFilterSource, setClusterFilterSource] = useState<number | ''>(() => {
const v = localStorage.getItem('opslog.clusterFilterSource'); const n = v ? parseInt(v, 10) : NaN; return Number.isFinite(n) ? n : '';
});
const [clusterGroup, setClusterGroup] = useState(() => lsBool('opslog.clusterGroup', true));
const [clusterCmd, setClusterCmd] = useState('');
// Cluster console: the raw traffic. Spots are parsed out of the stream into the
@@ -1506,11 +1502,11 @@ export default function App() {
if (atBottom) el.scrollTop = el.scrollHeight;
}, [clusterLines]);
// Multi-band filter: empty set = all bands. The user toggles chips.
const [clusterBands, setClusterBands] = useState<Set<string>>(() => new Set());
const [clusterBands, setClusterBands] = useState<Set<string>>(() => lsSet<string>('opslog.clusterBands'));
// Lock-to-entry: when on, the band filter follows the entry's current
// band and the mode filter follows the entry's current mode.
const [clusterLockBand, setClusterLockBand] = useState(false);
const [clusterLockMode, setClusterLockMode] = useState(false);
const [clusterLockBand, setClusterLockBand] = useState(() => lsBool('opslog.clusterLockBand', false));
const [clusterLockMode, setClusterLockMode] = useState(() => lsBool('opslog.clusterLockMode', false));
// Status filter chips. Empty set = show every status (including
// already-worked). Otherwise only matching spots pass.
type SpotStatusKey = 'new' | 'new-band' | 'new-mode' | 'new-slot' | 'worked';
@@ -1527,21 +1523,21 @@ export default function App() {
// LoTW-only, and the spotter's continent. Both narrow the list by a property
// of the station rather than by what the spot is worth, which is why they sit
// beside Hide worked and not among the status chips.
const [clusterLotwOnly, setClusterLotwOnly] = useState(false);
const [clusterSpotterConts, setClusterSpotterConts] = useState<Set<string>>(() => new Set());
const [clusterLotwOnly, setClusterLotwOnly] = useState(() => localStorage.getItem('opslog.clusterLotwOnly') === '1');
const [clusterSpotterConts, setClusterSpotterConts] = useState<Set<string>>(() => lsSet<string>('opslog.clusterSpotterCont'));
// Read through lib/spotDisplay, not straight from localStorage: while the two
// options are withdrawn it answers false, so the cluster list cannot end up
// applying an option the operator can no longer see or switch off.
const [clusterMuteWorked, setClusterMuteWorked] = useState(() => readSpotDisplayOptions().muteWorked);
const [clusterSlotHighlight, setClusterSlotHighlight] = useState(() => readSpotDisplayOptions().slotHighlight);
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotFilterKey>>(() => new Set());
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotFilterKey>>(() => lsSet<SpotFilterKey>('opslog.clusterStatusFilter'));
// Mode filter chips. Empty set = show every mode. Categories map the
// inferred per-spot mode onto SSB (phone) / CW / DATA (digital).
type SpotModeCat = 'SSB' | 'CW' | 'DATA';
const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(() => new Set());
const [clusterSearch, setClusterSearch] = useState('');
const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(() => lsSet<SpotModeCat>('opslog.clusterModeFilter'));
const [clusterSearch, setClusterSearch] = useState(() => localStorage.getItem('opslog.clusterSearch') || '');
// Hide spots already worked (exact call worked, or this band+mode slot done).
const [clusterHideWorked, setClusterHideWorked] = useState(false);
const [clusterHideWorked, setClusterHideWorked] = useState(() => lsBool('opslog.clusterHideWorked', false));
// Everything currently narrowing the spot list, in words. Shown when spots
// arrived and none survived — the operator needs to know WHICH filter to
@@ -1578,20 +1574,22 @@ export default function App() {
setClusterSearch('');
}, []);
// Grouping is a display choice, not a filter — it stays remembered.
useEffect(() => { writeUiPref('opslog.clusterGroup', clusterGroup ? '1' : '0'); }, [clusterGroup]);
// Once, at startup: erase the filter values earlier versions stored, in the
// local cache AND in the mirrored settings. Without this an upgrade would
// leave rows nothing reads — and a downgrade would bring the invisible band
// lock straight back.
// Persist every cluster filter selection whenever it changes, so it is still
// set after a close/reopen.
useEffect(() => {
for (const k of ['opslog.clusterFilterSource', 'opslog.clusterSearch']) writeUiPref(k, '');
for (const k of ['opslog.clusterLockBand', 'opslog.clusterLockMode',
'opslog.clusterHideWorked', 'opslog.clusterLotwOnly']) writeUiPref(k, '0');
for (const k of ['opslog.clusterBands', 'opslog.clusterStatusFilter',
'opslog.clusterModeFilter', 'opslog.clusterSpotterCont']) writeUiPref(k, '[]');
}, []);
writeUiPref('opslog.clusterFilterSource', clusterFilterSource === '' ? '' : String(clusterFilterSource));
writeUiPref('opslog.clusterGroup', clusterGroup ? '1' : '0');
writeUiPref('opslog.clusterBands', JSON.stringify([...clusterBands]));
writeUiPref('opslog.clusterLockBand', clusterLockBand ? '1' : '0');
writeUiPref('opslog.clusterLockMode', clusterLockMode ? '1' : '0');
writeUiPref('opslog.clusterStatusFilter', JSON.stringify([...clusterStatusFilter]));
writeUiPref('opslog.clusterSpotterCont', JSON.stringify([...clusterSpotterConts]));
writeUiPref('opslog.clusterModeFilter', JSON.stringify([...clusterModeFilter]));
writeUiPref('opslog.clusterSearch', clusterSearch);
writeUiPref('opslog.clusterHideWorked', clusterHideWorked ? '1' : '0');
}, [clusterFilterSource, clusterGroup, clusterBands, clusterLockBand, clusterLockMode,
clusterStatusFilter, clusterModeFilter, clusterSearch, clusterHideWorked,
clusterLotwOnly, clusterSpotterConts]);
// Bands shown side-by-side in the Band Map tab (portable).
const [bandMapBands, setBandMapBands] = useState<string[]>(() => {
try { const v = JSON.parse(localStorage.getItem('opslog.bandMapBands') || '[]'); return Array.isArray(v) ? v : []; }
@@ -5084,7 +5082,7 @@ export default function App() {
there is one place to flip when they come back. */}
{SPOT_DISPLAY_OPTIONS_EXPOSED && fSwitch(t('clu.muteWorkedShort'), clusterMuteWorked, (v) => { setClusterMuteWorked(v); writeUiPref('opslog.clusterMuteWorked', v ? '1' : '0'); })}
{SPOT_DISPLAY_OPTIONS_EXPOSED && fSwitch(t('clu.slotHighlightShort'), clusterSlotHighlight, (v) => { setClusterSlotHighlight(v); writeUiPref('opslog.clusterSlotHighlight', v ? '1' : '0'); })}
{fSwitch(t('clu.lotwOnly'), clusterLotwOnly, setClusterLotwOnly)}
{fSwitch(t('clu.lotwOnly'), clusterLotwOnly, (v) => { setClusterLotwOnly(v); writeUiPref('opslog.clusterLotwOnly', v ? '1' : '0'); })}
</div>
{/* The SPOTTER's continent, not the DX's: this asks whether anyone near
+4 -6
View File
@@ -34,12 +34,10 @@ const PORTABLE_KEYS = [
'opslog.dateFormat', // how dates are DISPLAYED (iso / fr / us); storage stays ISO
'opslog.mapGreyline', // world map: grey line (day/night terminator) shown
'opslog.awardRefSort', 'opslog.awardRefSortDir', // award reference table: sort column and direction
'opslog.clusterGroup', // cluster: group spots by callsign
// NOTE: the cluster FILTERS (band, mode, the two rig locks, status, search,
// LoTW-only, spotter continent, source) are deliberately absent. They start
// off at every launch — a filter restored from a previous session is invisible
// to whoever set it, and an empty spot list beside a live counter reads as a
// broken cluster. See the comment on their state in App.tsx.
// Cluster filter selections — restored on reopen.
'opslog.clusterFilterSource', 'opslog.clusterGroup', 'opslog.clusterBands',
'opslog.clusterLockBand', 'opslog.clusterLockMode', 'opslog.clusterStatusFilter',
'opslog.clusterModeFilter', 'opslog.clusterSearch', 'opslog.clusterHideWorked',
'opslog.activeTab', // last selected tab
'opslog.mainSplit', // Main tab: width share of the left pane (percent)
'opslog.clusterMuteWorked', // cluster/band map: no colour or badge on worked spots