feat(cluster): the spot list size is a setting

The list is a ring buffer that held a thousand spots, and on a busy
evening a thousand arrive in a couple of minutes. So the buffer decided
how long a spot lived, and the spot LIFETIME setting never got the chance
to expire anything: fifteen minutes meant nothing when the oldest spot
was pushed out after two.

Now settable (Preferences → Cluster, beside the lifetime, since between
them they decide the same thing), 100 to 10 000. The ceiling is a real
limit rather than a round number: every spot is matched against the
worked index and the alert rules, and is a row the cluster grid and every
open band map re-render.
This commit is contained in:
2026-08-24 19:15:32 +02:00
parent 4e9b1eebe9
commit 27d0952d01
7 changed files with 103 additions and 9 deletions
+45
View File
@@ -312,6 +312,7 @@ const (
keyClusterSelfSpot = "cluster.self_spot" // "1" → announce ourselves on the cluster as we log keyClusterSelfSpot = "cluster.self_spot" // "1" → announce ourselves on the cluster as we log
keyClusterSelfSpotMin = "cluster.self_spot_minutes" // shortest gap between two self-spots keyClusterSelfSpotMin = "cluster.self_spot_minutes" // shortest gap between two self-spots
keyClusterSpotTTL = "cluster.spot_ttl_min" // drop spots older than this; 0 = keep them keyClusterSpotTTL = "cluster.spot_ttl_min" // drop spots older than this; 0 = keep them
keyClusterSpotMax = "cluster.spot_max" // how many spots the list holds at once
keyScpEnabled = "scp.enabled" // Super Check Partial / N+1 suggestions on keyScpEnabled = "scp.enabled" // Super Check Partial / N+1 suggestions on
@@ -20294,6 +20295,50 @@ func (a *App) SetKenwoodKeySpeed(wpm int) error {
// and exists only so a mistyped value cannot mean "never". // and exists only so a mistyped value cannot mean "never".
const spotTTLMax = 720 const spotTTLMax = 720
// The spot list is a ring buffer, and its size decided the lifetime far more
// often than the lifetime setting did: on a busy evening a thousand spots
// arrive in a couple of minutes, so a fifteen-minute lifetime never got the
// chance to expire anything. Hence a setting.
//
// The ceiling is a real limit, not a round number. Every spot is matched against
// the worked index and the alert rules, and each one is a row the cluster grid
// and every open band map re-render; past ten thousand that work starts to show
// on the very evenings the list is worth having.
const (
spotMaxDefault = 1000
spotMaxCeiling = 10000
spotMaxFloor = 100
)
// GetSpotMax returns how many spots the list holds.
func (a *App) GetSpotMax() int {
n, _ := strconv.Atoi(a.settingOr(keyClusterSpotMax, ""))
if n <= 0 {
return spotMaxDefault
}
return clampSpotMax(n)
}
// SetSpotMax sets it. Clamped here as well as in the UI, for the same reason as
// the lifetime: the value decides how much work arrives on every spot, and a
// stale frontend must not be able to widen it past the ceiling.
func (a *App) SetSpotMax(n int) error {
a.setSetting(keyClusterSpotMax, strconv.Itoa(clampSpotMax(n)))
return nil
}
func clampSpotMax(n int) int {
if n < spotMaxFloor {
return spotMaxFloor
}
if n > spotMaxCeiling {
return spotMaxCeiling
}
return n
}
// GetSpotTTLMinutes returns how long a spot stays in the list, in minutes. // GetSpotTTLMinutes returns how long a spot stays in the list, in minutes.
// 0 means spots are kept until the count cap pushes them out, which is what // 0 means spots are kept until the count cap pushes them out, which is what
// OpsLog always did. // OpsLog always did.
+4 -2
View File
@@ -11,7 +11,8 @@
"Yaesu console: the NAR button (narrow IF filter) is shown only when the radio answers the command, and says what it is. A button that did nothing when pressed read as a fault in the rig.", "Yaesu console: the NAR button (narrow IF filter) is shown only when the radio answers the command, and says what it is. A button that did nothing when pressed read as a fault in the rig.",
"The frequency readout now steps the Hz digits under the mouse wheel too, not just the kHz ones — 100, 10 and 1 Hz. Zero-beating a CW signal is a few tens of Hz, and it was the one move the display would not make.", "The frequency readout now steps the Hz digits under the mouse wheel too, not just the kHz ones — 100, 10 and 1 Hz. Zero-beating a CW signal is a few tens of Hz, and it was the one move the display would not make.",
"The split-pile-up chaser only appears in CW: the marker comes from a skimmer decoding a report, so on SSB or FT8 there is nothing for it to chase.", "The split-pile-up chaser only appears in CW: the marker comes from a skimmer decoding a report, so on SSB or FT8 there is nothing for it to chase.",
"Cluster: a single click now only fills the callsign, and a DOUBLE click works the spot — QSY, mode and the rest. Running down the list used to drag the radio along with every line looked at." "Cluster: a single click now only fills the callsign, and a DOUBLE click works the spot — QSY, mode and the rest. Running down the list used to drag the radio along with every line looked at.",
"Cluster: how many spots the list keeps is now a setting (Preferences → Cluster, beside the spot lifetime). It was fixed at a thousand, and on a busy evening that fills in a couple of minutes — so the cap, not the lifetime, decided when a spot disappeared and a 15-minute lifetime never expired anything."
], ],
"fr": [ "fr": [
"Quand TQSL refuse un envoi, le journal contient désormais l'enregistrement ADIF exact qui lui a été remis et l'emplacement de station demandé pour la signature. « No QSOs processed » recouvre plusieurs causes sans rapport et le fichier temporaire est supprimé dès que TQSL rend la main : la seule pièce à conviction utile était justement invisible.", "Quand TQSL refuse un envoi, le journal contient désormais l'enregistrement ADIF exact qui lui a été remis et l'emplacement de station demandé pour la signature. « No QSOs processed » recouvre plusieurs causes sans rapport et le fichier temporaire est supprimé dès que TQSL rend la main : la seule pièce à conviction utile était justement invisible.",
@@ -22,7 +23,8 @@
"Console Yaesu : le bouton NAR (filtre FI étroit) n'apparaît que si la radio répond à la commande, et indique ce qu'il fait. Un bouton sans effet passait pour une panne de la radio.", "Console Yaesu : le bouton NAR (filtre FI étroit) n'apparaît que si la radio répond à la commande, et indique ce qu'il fait. Un bouton sans effet passait pour une panne de la radio.",
"L'affichage de fréquence fait maintenant défiler aussi les chiffres des Hz à la molette, pas seulement ceux des kHz — 100, 10 et 1 Hz. Se caler au zéro-beat sur un signal CW se joue à quelques dizaines de Hz, et c'était le seul geste que l'affichage refusait.", "L'affichage de fréquence fait maintenant défiler aussi les chiffres des Hz à la molette, pas seulement ceux des kHz — 100, 10 et 1 Hz. Se caler au zéro-beat sur un signal CW se joue à quelques dizaines de Hz, et c'était le seul geste que l'affichage refusait.",
"Le chasseur de pile-up en split n'apparaît qu'en CW : le marqueur vient d'un skimmer qui décode un report, donc en SSB ou en FT8 il n'a rien à chasser.", "Le chasseur de pile-up en split n'apparaît qu'en CW : le marqueur vient d'un skimmer qui décode un report, donc en SSB ou en FT8 il n'a rien à chasser.",
"Cluster : un clic simple ne remplit plus que l'indicatif, et le DOUBLE clic travaille le spot — QSY, mode et le reste. Parcourir la liste entraînait la radio à chaque ligne regardée." "Cluster : un clic simple ne remplit plus que l'indicatif, et le DOUBLE clic travaille le spot — QSY, mode et le reste. Parcourir la liste entraînait la radio à chaque ligne regardée.",
"Cluster : le nombre de spots conservés devient un réglage (Préférences → Cluster, à côté de la durée de vie). Il était fixé à mille, et un soir chargé remplit ça en quelques minutes — c'était donc le plafond, et non la durée de vie, qui décidait de la disparition d'un spot, et une durée de 15 minutes n'expirait jamais rien."
] ]
}, },
{ {
+15 -4
View File
@@ -95,7 +95,7 @@ import { ShutdownProgress } from '@/components/ShutdownProgress';
import { ClusterGrid } from '@/components/ClusterGrid'; import { ClusterGrid } from '@/components/ClusterGrid';
import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot'; import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot';
import { applySpotDisplay, readSpotDisplayOptions, spotIsWorked, SPOT_DISPLAY_OPTIONS_EXPOSED } from '@/lib/spotDisplay'; import { applySpotDisplay, readSpotDisplayOptions, spotIsWorked, SPOT_DISPLAY_OPTIONS_EXPOSED } from '@/lib/spotDisplay';
import { AnswerDecode, HaltDecodeTx, LogUIError, FlexTXOnBand, GetMatrixColors, GetRotorPresets, GetRowColors, GetSpotTTLMinutes, IsNewUSCounty } from '../wailsjs/go/main/App'; import { AnswerDecode, HaltDecodeTx, LogUIError, FlexTXOnBand, GetMatrixColors, GetRotorPresets, GetRowColors, GetSpotTTLMinutes, GetSpotMax, IsNewUSCounty } from '../wailsjs/go/main/App';
import { applyMatrixColors } from '@/lib/matrixColors'; import { applyMatrixColors } from '@/lib/matrixColors';
import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid'; import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid';
import { NetControlPanel } from '@/components/NetControlPanel'; import { NetControlPanel } from '@/components/NetControlPanel';
@@ -1663,7 +1663,14 @@ export default function App() {
const [clusterServers, setClusterServers] = useState<{ id: number; name: string; enabled: boolean; sort_order: number }[]>([]); const [clusterServers, setClusterServers] = useState<{ id: number; name: string; enabled: boolean; sort_order: number }[]>([]);
// Ring buffer — only keep the last N spots; cluster firehose can be heavy. // Ring buffer — only keep the last N spots; cluster firehose can be heavy.
const [spots, setSpots] = useState<ClusterSpot[]>([]); const [spots, setSpots] = useState<ClusterSpot[]>([]);
const SPOTS_CAP = 1000; // How many spots the list holds. A setting rather than a constant: at a
// thousand, a busy evening filled the buffer in a couple of minutes, so the
// spot LIFETIME never had anything left to expire — the cap was deciding the
// lifetime. Kept in a ref as well: the append path runs inside a state updater
// that must not close over a stale value.
const [spotsCap, setSpotsCap] = useState(1000);
const spotsCapRef = useRef(1000);
useEffect(() => { spotsCapRef.current = spotsCap; }, [spotsCap]);
// Cluster filter selections persist across restarts (writeUiPref → localStorage // Cluster filter selections persist across restarts (writeUiPref → localStorage
// + DB, so they also travel with a copied data/ folder). Loaders read the cache // + 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. // synchronously at first render; a single effect below writes them back.
@@ -1977,7 +1984,7 @@ export default function App() {
useEffect(() => { useEffect(() => {
setSpotStatus((prev) => { setSpotStatus((prev) => {
const keys = Object.keys(prev); const keys = Object.keys(prev);
if (keys.length <= SPOTS_CAP * 2) return prev; if (keys.length <= spotsCapRef.current * 2) return prev;
const live = new Set(spots.map((x) => spotStatusKey(x.dx_call, x.band ?? '', x.comment ?? '', x.freq_hz))); const live = new Set(spots.map((x) => spotStatusKey(x.dx_call, x.band ?? '', x.comment ?? '', x.freq_hz)));
// Decoded stations count as live too. They share this cache, and pruning // Decoded stations count as live too. They share this cache, and pruning
// to the cluster spots alone would evict every one of them — on a busy // to the cluster spots alone would evict every one of them — on a busy
@@ -2488,6 +2495,9 @@ export default function App() {
// same thing everywhere — and it gives the memory back. // same thing everywhere — and it gives the memory back.
const [spotTTLMin, setSpotTTLMin] = useState(0); const [spotTTLMin, setSpotTTLMin] = useState(0);
useEffect(() => { GetSpotTTLMinutes().then(setSpotTTLMin).catch(() => {}); }, [showSettings]); useEffect(() => { GetSpotTTLMinutes().then(setSpotTTLMin).catch(() => {}); }, [showSettings]);
// Same beat as the lifetime: re-read when Preferences closes, since the two
// settings decide between them how long a spot survives.
useEffect(() => { GetSpotMax().then((n: number) => { if (n > 0) setSpotsCap(n); }).catch(() => {}); }, [showSettings]);
useEffect(() => { useEffect(() => {
if (spotTTLMin <= 0) return; // 0 = keep until the count cap pushes them out if (spotTTLMin <= 0) return; // 0 = keep until the count cap pushes them out
const sweep = () => { const sweep = () => {
@@ -3478,7 +3488,8 @@ export default function App() {
const filtered = hist(sp) ? next : next.filter((x) => hist(x) || key(x) !== k); const filtered = hist(sp) ? next : next.filter((x) => hist(x) || key(x) !== k);
next = [sp, ...filtered]; next = [sp, ...filtered];
} }
return next.length > SPOTS_CAP ? next.slice(0, SPOTS_CAP) : next; const cap = spotsCapRef.current;
return next.length > cap ? next.slice(0, cap) : next;
}); });
}; };
const unsubSpot = EventsOn('cluster:spot', (sp: ClusterSpot) => { const unsubSpot = EventsOn('cluster:spot', (sp: ClusterSpot) => {
+25 -1
View File
@@ -58,7 +58,7 @@ import {
GetFolderSync, SaveFolderSync, PickFolderSyncFolder, GetFolderSyncStatus, SyncFolderNow, GetFolderSync, SaveFolderSync, PickFolderSyncFolder, GetFolderSyncStatus, SyncFolderNow,
GetRelayAuto, SaveRelayAuto, GetStationDevices, GetRelayAuto, SaveRelayAuto, GetStationDevices,
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards, GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax,
} from '../../wailsjs/go/main/App'; } from '../../wailsjs/go/main/App';
import type { profile as profileModels } from '../../wailsjs/go/models'; import type { profile as profileModels } from '../../wailsjs/go/models';
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types'; import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -1942,6 +1942,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [chaseNew, setChaseNew] = useState(false); const [chaseNew, setChaseNew] = useState(false);
const [spotTTL, setSpotTTL] = useState(0); const [spotTTL, setSpotTTL] = useState(0);
const [spotTTLText, setSpotTTLText] = useState('0'); const [spotTTLText, setSpotTTLText] = useState('0');
const [spotMaxText, setSpotMaxText] = useState('1000');
const [gridStat, setGridStat] = useState<any>(null); const [gridStat, setGridStat] = useState<any>(null);
const [pskrStatus, setPskrStatus] = useState<any>(null); const [pskrStatus, setPskrStatus] = useState<any>(null);
const saveBandOpen = async (next: any) => { const saveBandOpen = async (next: any) => {
@@ -1955,6 +1956,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
try { setChaseNew(await GetChaseNew()); } catch { /* defaults stand */ } try { setChaseNew(await GetChaseNew()); } catch { /* defaults stand */ }
try { setLinkedAmps((await GetLinkedAmps()) ?? []); } catch { /* defaults stand */ } try { setLinkedAmps((await GetLinkedAmps()) ?? []); } catch { /* defaults stand */ }
try { const n = await GetSpotTTLMinutes(); setSpotTTL(n); setSpotTTLText(String(n)); } catch { /* defaults stand */ } try { const n = await GetSpotTTLMinutes(); setSpotTTL(n); setSpotTTLText(String(n)); } catch { /* defaults stand */ }
try { const n = await GetSpotMax(); setSpotMaxText(String(n)); } catch { /* defaults stand */ }
})(); })();
// Poll the feed while the panel is open: a live count is the only thing that // Poll the feed while the panel is open: a live count is the only thing that
// distinguishes "connected" from "connected and receiving nothing". // distinguishes "connected" from "connected and receiving nothing".
@@ -4973,6 +4975,28 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
}} /> }} />
<span className="text-xs text-muted-foreground">{t('clu.spotTtlHint')}</span> <span className="text-xs text-muted-foreground">{t('clu.spotTtlHint')}</span>
</div> </div>
{/* The list size, beside the lifetime because between them they decide
the same thing: whichever runs out first removes the spot. */}
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm">{t('clu.spotMax')}</span>
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs">
{[500, 1000, 2500, 5000].map((v) => (
<button key={v} type="button"
onClick={() => { setSpotMaxText(String(v)); SetSpotMax(v).catch(() => {}); }}
className={cn('px-2.5 py-1.5 font-medium', Number(spotMaxText) === v ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
{v}
</button>
))}
</div>
<Input className="h-8 w-24" value={spotMaxText}
onChange={(e) => {
const raw = e.target.value.replace(/[^0-9]/g, '');
setSpotMaxText(raw);
const n = parseInt(raw, 10);
if (Number.isFinite(n) && n > 0) SetSpotMax(Math.min(10000, n)).catch(() => {});
}} />
<span className="text-xs text-muted-foreground">{t('clu.spotMaxHint')}</span>
</div>
</div> </div>
<div className="border-t border-border/60 pt-3 space-y-2"> <div className="border-t border-border/60 pt-3 space-y-2">
+2 -2
View File
@@ -313,7 +313,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.', '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', '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.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.spotTtlHint': 'minutes — spots older than this are removed from the list and the band maps. 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': 'The list is a ring buffer: past this, the oldest spot drops out. It is usually this, not the lifetime, that decides — a thousand spots arrive in a few minutes on a busy evening.', 'clu.spotTtlHint': 'minutes — spots older than this are removed from the list and the band maps. 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',
'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.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.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. Combines with digital-mode grouping (Settings → General): with it on, a call worked on 20m FT8 also counts as worked for a 20m FT4 spot; with it off, FT8 and FT4 are separate slots.', 'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere. Combines with digital-mode grouping (Settings → General): with it on, a call worked on 20m FT8 also counts as worked for a 20m FT4 spot; with it off, FT8 and FT4 are separate slots.',
@@ -795,7 +795,7 @@ const fr: Dict = {
'bo.nearKm': 'Compter les récepteurs à moins de', 'bo.nearKmHint': 'Un report ne prouve TON chemin que sil 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 na 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.nearKm': 'Compter les récepteurs à moins de', 'bo.nearKmHint': 'Un report ne prouve TON chemin que sil 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 na 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', '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 nest pas affiché.', '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 nest pas affiché.',
'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotTtlHint': 'minutes — les spots plus vieux que ça sont retirés de la liste et des bandmaps. 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': "La liste est un tampon circulaire : au-delà, le plus ancien sort. C'est souvent lui, et non la durée de vie, qui décide — mille spots arrivent en quelques minutes un soir chargé.", 'clu.spotTtlHint': 'minutes — les spots plus vieux que ça sont retirés de la liste et des bandmaps. 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.workedSameSlotHint': '— un spot n\'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode, pas juste n\'importe où. Se combine avec le groupage des modes numériques (Réglages → Général) : activé, un indicatif contacté en 20m FT8 compte aussi comme contacté pour un spot 20m FT4 ; désactivé, FT8 et FT4 sont des slots distincts.', 'clu.workedSameSlotHint': '— un spot n\'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode, pas juste n\'importe où. Se combine avec le groupage des modes numériques (Réglages → Général) : activé, un indicatif contacté en 20m FT8 compte aussi comme contacté pour un spot 20m FT4 ; désactivé, FT8 et FT4 sont des slots distincts.',
+4
View File
@@ -563,6 +563,8 @@ export function GetSolarData():Promise<solar.Data>;
export function GetSpotColors():Promise<main.SpotColors>; export function GetSpotColors():Promise<main.SpotColors>;
export function GetSpotMax():Promise<number>;
export function GetSpotTTLMinutes():Promise<number>; export function GetSpotTTLMinutes():Promise<number>;
export function GetStartupStatus():Promise<main.StartupStatus>; export function GetStartupStatus():Promise<main.StartupStatus>;
@@ -1149,6 +1151,8 @@ export function SetPassphrase(arg1:string):Promise<void>;
export function SetScpEnabled(arg1:boolean):Promise<void>; export function SetScpEnabled(arg1:boolean):Promise<void>;
export function SetSpotMax(arg1:number):Promise<void>;
export function SetSpotTTLMinutes(arg1:number):Promise<void>; export function SetSpotTTLMinutes(arg1:number):Promise<void>;
export function SetTelemetryEnabled(arg1:boolean):Promise<void>; export function SetTelemetryEnabled(arg1:boolean):Promise<void>;
+8
View File
@@ -1066,6 +1066,10 @@ export function GetSpotColors() {
return window['go']['main']['App']['GetSpotColors'](); return window['go']['main']['App']['GetSpotColors']();
} }
export function GetSpotMax() {
return window['go']['main']['App']['GetSpotMax']();
}
export function GetSpotTTLMinutes() { export function GetSpotTTLMinutes() {
return window['go']['main']['App']['GetSpotTTLMinutes'](); return window['go']['main']['App']['GetSpotTTLMinutes']();
} }
@@ -2238,6 +2242,10 @@ export function SetScpEnabled(arg1) {
return window['go']['main']['App']['SetScpEnabled'](arg1); return window['go']['main']['App']['SetScpEnabled'](arg1);
} }
export function SetSpotMax(arg1) {
return window['go']['main']['App']['SetSpotMax'](arg1);
}
export function SetSpotTTLMinutes(arg1) { export function SetSpotTTLMinutes(arg1) {
return window['go']['main']['App']['SetSpotTTLMinutes'](arg1); return window['go']['main']['App']['SetSpotTTLMinutes'](arg1);
} }