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
|
||||
|
||||
@@ -527,16 +527,25 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
||||
const deviceCard = (dev: Device) => {
|
||||
const st = status[dev.id];
|
||||
const relays = st?.relays ?? dev.labels.map((label, i) => ({ number: i + 1, label, on: false }));
|
||||
// The generic HTTP board has no address of its own and nothing to poll: its
|
||||
// relays can each live on a different box, and no status endpoint is read
|
||||
// back. So no host under the name, no online dot, and the buttons are never
|
||||
// greyed out waiting for a connection that is never made.
|
||||
const fireAndForget = dev.type === 'httpgen';
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden h-full">
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
|
||||
<PlugZap className="size-4 text-primary" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold truncate">{dev.name || TYPE_LABEL[dev.type]}</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono truncate">{TYPE_LABEL[dev.type]} · {dev.host}</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono truncate">
|
||||
{TYPE_LABEL[dev.type]}{fireAndForget || !dev.host ? '' : ` · ${dev.host}`}
|
||||
</div>
|
||||
</div>
|
||||
<span className={cn('ml-auto size-2 rounded-full shrink-0', st?.connected ? 'bg-success' : 'bg-muted-foreground/40')}
|
||||
title={st?.connected ? t('station.online') : (st?.error || t('station.offline'))} />
|
||||
{fireAndForget ? <span className="ml-auto" /> : (
|
||||
<span className={cn('ml-auto size-2 rounded-full shrink-0', st?.connected ? 'bg-success' : 'bg-muted-foreground/40')}
|
||||
title={st?.connected ? t('station.online') : (st?.error || t('station.offline'))} />
|
||||
)}
|
||||
<button className="text-muted-foreground hover:text-foreground" title={t('station.edit')}
|
||||
onClick={() => setEditing({ ...dev, labels: [...dev.labels] })}><Pencil className="size-3.5" /></button>
|
||||
<button className="text-muted-foreground hover:text-destructive" title={t('station.delete')}
|
||||
@@ -549,7 +558,7 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
|
||||
const key = `${dev.id}:${r.number}`;
|
||||
const label = r.label || `${t('station.relay')} ${r.number}`;
|
||||
return (
|
||||
<button key={r.number} type="button" disabled={!st?.connected}
|
||||
<button key={r.number} type="button" disabled={!fireAndForget && !st?.connected}
|
||||
title={label}
|
||||
onClick={() => toggle(dev, r.number, !r.on)}
|
||||
className={cn('w-[150px] flex items-center gap-1.5 rounded-md border px-2 py-1 text-left transition-colors disabled:opacity-40',
|
||||
@@ -687,6 +696,11 @@ function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
|
||||
const isDenkovi = device.type === 'denkovi';
|
||||
const isUsbRelay = device.type === 'usbrelay';
|
||||
const isHTTPGen = device.type === 'httpgen';
|
||||
// {value} in a pattern changes what the boxes below hold — a value to drop
|
||||
// into it instead of a whole URL. The grid says which as soon as it is typed,
|
||||
// because the two are indistinguishable once entered and getting it wrong
|
||||
// switches an antenna somewhere unexpected.
|
||||
const usesValue = `${device.on_pattern ?? ''}${device.off_pattern ?? ''}`.includes('{value}');
|
||||
// COM ports for the generic USB-serial relay picker.
|
||||
const [serialPorts, setSerialPorts] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
@@ -795,7 +809,12 @@ function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">{t('station.usbRelayHint')}</p>
|
||||
</div>
|
||||
) : (
|
||||
) : isHTTPGen ? null : (
|
||||
/* No Host for the generic board: its driver never reads one. Each URL
|
||||
below carries its own address, and they need not even share it — one
|
||||
relay can sit on a different box from the next. A field that changes
|
||||
nothing is worse than no field: it reads as the thing to fill in first,
|
||||
and then the URLs look like they should be relative to it. */
|
||||
<div className={cn('grid gap-3', (isKM || isDingtian) ? 'grid-cols-3' : 'grid-cols-1')}>
|
||||
<div className={cn('space-y-1', (isKM || isDingtian) ? '' : 'max-w-xs')}>
|
||||
<Label>{t('station.host')}</Label>
|
||||
@@ -855,12 +874,12 @@ function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">{t('station.patternHint')}</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('station.perRelayUrls')}</Label>
|
||||
<Label>{usesValue ? t('station.perRelayValues') : t('station.perRelayUrls')}</Label>
|
||||
<div className="space-y-1">
|
||||
{device.labels.map((_, i) => (
|
||||
<div key={i} className="grid grid-cols-[2.5rem_1fr_1fr] items-center gap-2">
|
||||
<span className="text-[11px] text-muted-foreground">{i + 1}</span>
|
||||
<Input className="h-8 font-mono text-[11px]" placeholder={t('station.onUrlPh')}
|
||||
<Input className="h-8 font-mono text-[11px]" placeholder={usesValue ? t('station.onValuePh') : t('station.onUrlPh')}
|
||||
value={device.on_urls?.[i] ?? ''}
|
||||
onChange={(e) => {
|
||||
const on_urls = [...(device.on_urls ?? [])];
|
||||
@@ -868,7 +887,7 @@ function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
|
||||
on_urls[i] = e.target.value;
|
||||
onChange({ ...device, on_urls });
|
||||
}} />
|
||||
<Input className="h-8 font-mono text-[11px]" placeholder={t('station.offUrlPh')}
|
||||
<Input className="h-8 font-mono text-[11px]" placeholder={usesValue ? t('station.offValuePh') : t('station.offUrlPh')}
|
||||
value={device.off_urls?.[i] ?? ''}
|
||||
onChange={(e) => {
|
||||
const off_urls = [...(device.off_urls ?? [])];
|
||||
@@ -879,7 +898,7 @@ function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">{t('station.perRelayHint')}</div>
|
||||
<div className="text-[10px] text-muted-foreground">{usesValue ? t('station.perValueHint') : t('station.perRelayHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -901,12 +920,20 @@ function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={testDevice} disabled={testing || !device.host.trim()}>
|
||||
{testing ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <PlugZap className="size-3.5 mr-1" />}
|
||||
{t('station.test')}
|
||||
</Button>
|
||||
{/* No connection test for the generic board, and no host required to
|
||||
save it. There is nothing to test: it has no address of its own and
|
||||
no status to read — its URLs are fired and forgotten. A button that
|
||||
can only ever say "OK, 4 relays" tests nothing, and a Save greyed
|
||||
out for a missing host made a perfectly complete configuration —
|
||||
four full URLs — impossible to store. */}
|
||||
{!isHTTPGen && (
|
||||
<Button size="sm" variant="outline" onClick={testDevice} disabled={testing || !device.host.trim()}>
|
||||
{testing ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <PlugZap className="size-3.5 mr-1" />}
|
||||
{t('station.test')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="ghost" onClick={onCancel}><X className="size-3.5 mr-1" />{t('station.cancel')}</Button>
|
||||
<Button size="sm" onClick={onSave} disabled={!device.host.trim()}><Check className="size-3.5 mr-1" />{t('station.save')}</Button>
|
||||
<Button size="sm" onClick={onSave} disabled={!isHTTPGen && !device.host.trim()}><Check className="size-3.5 mr-1" />{t('station.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,8 @@ const en: Dict = {
|
||||
'uscty.backfillIntro': 'Resolve county (and grid) for US QSOs already in your log that are missing them. Existing values are kept — only blanks are filled.',
|
||||
'uscty.backfillRun': 'Fill missing counties',
|
||||
'uscty.backfillDone': '{s} US QSOs scanned · {c} counties, {g} grids filled.',
|
||||
'station.typeHttpGen': 'HTTP relay (home-made / generic)', 'station.onPattern': 'ON URL pattern', 'station.offPattern': 'OFF URL pattern', 'station.patternHint': '{relay} is replaced by the relay number. Leave blank if every relay has its own URL below.', 'station.perRelayUrls': 'Per-relay URLs (optional)', 'station.perRelayHint': 'Filled in here, these win over the patterns — for a switch whose channels have nothing in common. State is remembered, not read back: after a restart every relay is re-commanded once.', 'station.onUrlPh': 'ON URL', 'station.offUrlPh': 'OFF URL',
|
||||
'station.typeHttpGen': 'HTTP relay (home-made / generic)', 'station.onPattern': 'ON URL pattern', 'station.offPattern': 'OFF URL pattern', 'station.patternHint': 'Optional, http or https. {relay} is replaced by the relay number — {relay-1} if the board counts from zero. Put {value} where the boards differ only by a number, and type that number per relay below. Leave both blank if every relay has its own full URL.', 'station.perRelayUrls': 'Per-relay URLs (optional)', 'station.perRelayHint': 'Filled in here, these win over the patterns — for a switch whose channels have nothing in common. State is remembered, not read back: after a restart every relay is re-commanded once.', 'station.onUrlPh': 'ON URL', 'station.offUrlPh': 'OFF URL',
|
||||
'station.perRelayValues': 'Per-relay values (for {value})', 'station.perValueHint': 'The pattern above contains {value}: these are the values dropped into it, not URLs — e.g. 1, 2, 4, 8 to switch on and 0 to switch off. State is remembered, not read back: after a restart every relay is re-commanded once.', 'station.onValuePh': 'ON value', 'station.offValuePh': 'OFF value',
|
||||
'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotateTo': 'Rotate to {az}°', 'station.rotatorNoRead': 'No heading read', 'station.bands': 'Bands', 'station.nudgeUp': 'Up {n} kHz', 'station.nudgeDown': 'Down {n} kHz', 'station.trackOn': 'Tracking on', 'station.trackOff': 'Tracking off', 'station.trackStepTip': 'Re-tune only when the rig has moved this far', 'station.trackModeTip': 'When the antenna is allowed to re-tune', 'station.trackAlways': 'Every frequency change', 'station.trackStep': 'Past a step', 'station.trackBand': 'Band change only', 'station.trackAlwaysTip': 'Follow every frequency change. Best resonance, but the motors run constantly — and on a SteppIR every move blocks transmit while the elements travel.', 'station.trackStepTipMode': 'Re-tune only once the rig has moved further than the step. Follows a QSY, ignores tuning around.', 'station.trackBandTip': 'Re-tune only when the band changes. The motors move a few times a day and are left alone within a band.', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.setExactLen': 'Click to type the exact current length (fixes the baseline if the auto-read is off).', 'station.atMax': 'Controller refused — the element is likely at its maximum length for this band, so it can\'t extend further.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag the grip handle on the left of a panel to move it. Pick a column count to lay them out in a grid.', 'station.dragMove': 'Drag to move this panel', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.hostHint': 'LAN IP for local use. To reach the board from OUTSIDE, put a full URL here — e.g. https://relay.yourdomain.com — pointing at a reverse proxy (Nginx Proxy Manager…) that fronts the board’s HTTP port.', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.ftdiSerial': 'FTDI serial number', 'station.detect': 'Detect', 'station.ftdiHint': 'The Denkovi board is driven via FTDI bit-bang (not the COM port). Pick its serial (e.g. DAE0006K). Needs the FTDI D2XX driver installed.', 'station.channels': 'Relays', 'station.comPort': 'COM port', 'station.noPorts': 'No ports found', 'station.usbRelayHint': 'Cheap USB-serial relay boards (CH340/LCUS) using the A0 command protocol. If yours does not switch, tell me its model / command set.', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save', 'station.test': 'Test connection', 'station.testOk': 'Connected — {n} relays', 'station.testFail': 'Not connected', 'station.detectNone': 'No FTDI board found — check the cable and that the D2XX driver is installed.', 'station.detectFound': '{n} board(s) detected.',
|
||||
'awards.followHint': 'Awards shown in the Awards tab. Leave the right side empty to show them all.', 'awards.available': 'All awards', 'awards.followed': 'Followed', 'awards.search': 'Filter…', 'awards.addAll': 'Add all', 'awards.clear': 'Clear', 'awards.allTracked': 'All awards are followed.', 'awards.noneFollowed': 'Nothing followed yet — the Awards tab shows all.',
|
||||
'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer',
|
||||
@@ -590,7 +591,8 @@ const fr: Dict = {
|
||||
'uscty.backfillIntro': "Résout le comté (et le locator) pour les QSO US déjà dans ton log qui n'en ont pas. Les valeurs existantes sont conservées — seuls les vides sont remplis.",
|
||||
'uscty.backfillRun': 'Remplir les comtés manquants',
|
||||
'uscty.backfillDone': '{s} QSO US analysés · {c} comtés, {g} locators remplis.',
|
||||
'station.typeHttpGen': 'Relais HTTP (fait main / générique)', 'station.onPattern': 'Modèle d’URL ON', 'station.offPattern': 'Modèle d’URL OFF', 'station.patternHint': '{relay} est remplacé par le numéro du relais. Laissez vide si chaque relais a sa propre URL ci-dessous.', 'station.perRelayUrls': 'URL par relais (optionnel)', 'station.perRelayHint': 'Renseignées ici, elles l’emportent sur les modèles — pour un commutateur dont les voies n’ont rien en commun. L’état est mémorisé, pas relu : après un redémarrage chaque relais est recommandé une fois.', 'station.onUrlPh': 'URL ON', 'station.offUrlPh': 'URL OFF',
|
||||
'station.typeHttpGen': 'Relais HTTP (fait main / générique)', 'station.onPattern': 'Modèle d’URL ON', 'station.offPattern': 'Modèle d’URL OFF', 'station.patternHint': 'Optionnel, http ou https. {relay} est remplacé par le numéro du relais — {relay-1} si la carte compte à partir de zéro. Mets {value} là où les URL ne diffèrent que par un nombre, et saisis ce nombre par relais ci-dessous. Laisse les deux vides si chaque relais a sa propre URL complète.', 'station.perRelayUrls': 'URL par relais (optionnel)', 'station.perRelayHint': 'Renseignées ici, elles l’emportent sur les modèles — pour un commutateur dont les voies n’ont rien en commun. L’état est mémorisé, pas relu : après un redémarrage chaque relais est recommandé une fois.', 'station.onUrlPh': 'URL ON', 'station.offUrlPh': 'URL OFF',
|
||||
'station.perRelayValues': 'Valeurs par relais (pour {value})', 'station.perValueHint': 'Le modèle ci-dessus contient {value} : ce sont les valeurs qui y seront insérées, pas des URL — ex. 1, 2, 4, 8 pour activer et 0 pour couper. L’état est mémorisé, pas relu : après un redémarrage chaque relais est recommandé une fois.', 'station.onValuePh': 'Valeur ON', 'station.offValuePh': 'Valeur OFF',
|
||||
'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotateTo': 'Tourner vers {az}°', 'station.rotatorNoRead': 'Azimut non lu', 'station.bands': 'Bandes', 'station.nudgeUp': 'Monter de {n} kHz', 'station.nudgeDown': 'Descendre de {n} kHz', 'station.trackOn': 'Suivi actif', 'station.trackOff': 'Suivi inactif', 'station.trackStepTip': 'Ne réaccorder que si le rig a bougé d au moins ça', 'station.trackModeTip': "Quand l'antenne a le droit de se réaccorder", 'station.trackAlways': 'À chaque changement', 'station.trackStep': 'Au-delà d un pas', 'station.trackBand': 'Changement de bande', 'station.trackAlwaysTip': "Suivre chaque changement de fréquence. Résonance idéale, mais les moteurs tournent en permanence — et sur une SteppIR chaque déplacement bloque l'émission le temps du mouvement.", 'station.trackStepTipMode': "Ne réaccorder qu'une fois le rig sorti du pas. Suit un QSY, ignore la recherche autour.", 'station.trackBandTip': "Ne réaccorder qu'au changement de bande. Les moteurs bougent quelques fois par jour et restent tranquilles dans une bande.", 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.setExactLen': "Clique pour taper la longueur actuelle exacte (recale la base si la lecture auto est fausse).", 'station.atMax': "Refusé par le contrôleur — l'élément est probablement en butée (longueur max pour cette bande), il ne peut plus s'allonger.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse la poignée à gauche d un panneau pour le déplacer. Choisis un nombre de colonnes pour la disposition.', 'station.dragMove': 'Glisser pour déplacer ce panneau', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.hostHint': "IP du LAN en local. Pour joindre la carte depuis L'EXTÉRIEUR, saisis une URL complète ici — ex. https://relais.tondomaine.com — pointant vers un reverse proxy (Nginx Proxy Manager…) qui expose le port HTTP de la carte.", 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.ftdiSerial': 'Numéro de série FTDI', 'station.detect': 'Détecter', 'station.ftdiHint': "La carte Denkovi se pilote en FTDI bit-bang (pas via le port COM). Choisis son numéro de série (ex. DAE0006K). Nécessite le driver FTDI D2XX installé.", 'station.channels': 'Relais', 'station.comPort': 'Port COM', 'station.noPorts': 'Aucun port', 'station.usbRelayHint': "Cartes USB-série bon marché (CH340/LCUS) protocole A0. Si la tienne ne commute pas, donne-moi le modèle / jeu de commandes.", 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer', 'station.test': 'Tester la connexion', 'station.testOk': 'Connecté — {n} relais', 'station.testFail': 'Non connecté', 'station.detectNone': 'Aucune carte FTDI trouvée — vérifie le câble et que le driver D2XX est installé.', 'station.detectFound': '{n} carte(s) détectée(s).',
|
||||
'awards.followHint': 'Diplômes affichés dans l’onglet Awards. Laisse la colonne de droite vide pour tous les afficher.', 'awards.available': 'Tous les diplômes', 'awards.followed': 'Suivis', 'awards.search': 'Filtrer…', 'awards.addAll': 'Tout ajouter', 'awards.clear': 'Vider', 'awards.allTracked': 'Tous les diplômes sont suivis.', 'awards.noneFollowed': 'Aucun pour l’instant — l’onglet Awards les montre tous.',
|
||||
'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW',
|
||||
|
||||
@@ -34,10 +34,12 @@ 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
|
||||
// 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.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.
|
||||
'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
|
||||
|
||||
Reference in New Issue
Block a user