fix(relays): the generic HTTP board never sent its URLs; cluster filters start off
buildDeviceDriver had no case for "httpgen", so the generic board fell through
to the WebSwitch driver: it polled an address it had never been given, reported
itself offline, greyed out every relay button, and sent none of the configured
URLs. Nothing in the interface said so — the board was configured, saved and
listed, and simply did nothing. deviceKey did not cover the URLs either, so once
that is fixed, correcting a typo in one would still have handed back the cached
driver holding the old address until a restart.
Two shapes of home-made switch could not be described at all:
- a bit-mask board whose four URLs differ by one character
(/Set0/1, /Set0/2, /Set0/4, /Set0/8) — {value} in the pattern now takes the
number from the per-relay box, keeping the address in one place;
- a board numbering its channels from zero — {relay-1}, since giving up the
pattern for eight hand-typed URLs was the only alternative.
The pattern decides what the per-relay boxes hold, and the grid says which as
soon as {value} is typed: guessing per box ("does this look like a URL?") would
change meaning on a typo, which is not a thing to do to something wired to an
antenna. A URL typed without a scheme gets http:// like the named boards get
from relayBase; https:// is passed through untouched.
Host and the connection test are gone for this type. It has no address of its
own — its relays may each live on a different box — and no status to read, so a
test could only ever answer "OK, 4 relays". Save was greyed out without a host,
which made a complete configuration of four full URLs impossible to store.
Cluster filters no longer persist across launches. A band lock set weeks earlier
is invisible to whoever set it: the counter reads 76 spots live, the grid is
empty, and the search goes to the cluster instead. Nobody loses work by
re-ticking a chip. Grouping and the panel state still persist — they change how
spots look, never whether they appear.
This commit is contained in:
+34
-32
@@ -1438,14 +1438,18 @@ 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 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.
|
||||
// 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.
|
||||
const lsBool = (k: string, d: boolean) => { const v = localStorage.getItem(k); return v === null ? d : v === '1'; };
|
||||
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 [clusterFilterSource, setClusterFilterSource] = useState<number | ''>('');
|
||||
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
|
||||
@@ -1502,11 +1506,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>>(() => lsSet<string>('opslog.clusterBands'));
|
||||
const [clusterBands, setClusterBands] = useState<Set<string>>(() => new Set());
|
||||
// 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(() => lsBool('opslog.clusterLockBand', false));
|
||||
const [clusterLockMode, setClusterLockMode] = useState(() => lsBool('opslog.clusterLockMode', false));
|
||||
const [clusterLockBand, setClusterLockBand] = useState(false);
|
||||
const [clusterLockMode, setClusterLockMode] = useState(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';
|
||||
@@ -1523,21 +1527,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(() => localStorage.getItem('opslog.clusterLotwOnly') === '1');
|
||||
const [clusterSpotterConts, setClusterSpotterConts] = useState<Set<string>>(() => lsSet<string>('opslog.clusterSpotterCont'));
|
||||
const [clusterLotwOnly, setClusterLotwOnly] = useState(false);
|
||||
const [clusterSpotterConts, setClusterSpotterConts] = useState<Set<string>>(() => new Set());
|
||||
// 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>>(() => lsSet<SpotFilterKey>('opslog.clusterStatusFilter'));
|
||||
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotFilterKey>>(() => new Set());
|
||||
// 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>>(() => lsSet<SpotModeCat>('opslog.clusterModeFilter'));
|
||||
const [clusterSearch, setClusterSearch] = useState(() => localStorage.getItem('opslog.clusterSearch') || '');
|
||||
const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(() => new Set());
|
||||
const [clusterSearch, setClusterSearch] = useState('');
|
||||
// Hide spots already worked (exact call worked, or this band+mode slot done).
|
||||
const [clusterHideWorked, setClusterHideWorked] = useState(() => lsBool('opslog.clusterHideWorked', false));
|
||||
const [clusterHideWorked, setClusterHideWorked] = useState(false);
|
||||
|
||||
// Everything currently narrowing the spot list, in words. Shown when spots
|
||||
// arrived and none survived — the operator needs to know WHICH filter to
|
||||
@@ -1574,22 +1578,20 @@ export default function App() {
|
||||
setClusterSearch('');
|
||||
}, []);
|
||||
|
||||
// Persist every cluster filter selection whenever it changes, so it is still
|
||||
// set after a close/reopen.
|
||||
// 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.
|
||||
useEffect(() => {
|
||||
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]);
|
||||
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, '[]');
|
||||
}, []);
|
||||
// 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 : []; }
|
||||
@@ -5082,7 +5084,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, (v) => { setClusterLotwOnly(v); writeUiPref('opslog.clusterLotwOnly', v ? '1' : '0'); })}
|
||||
{fSwitch(t('clu.lotwOnly'), clusterLotwOnly, setClusterLotwOnly)}
|
||||
</div>
|
||||
|
||||
{/* The SPOTTER's continent, not the DX's: this asks whether anyone near
|
||||
|
||||
Reference in New Issue
Block a user