feat(cluster): self-spot on the master node while logging
Settings -> DX Cluster: a toggle and an interval. When it is on, logging a QSO announces the station on the master cluster — the QSO's own station callsign as the DX, on the frequency the contact was made on — so callers find the run without waiting for someone else to spot it. The node fills the DE field from the login, so the spotter is us too: a self-spot. It fires on the FIRST QSO of a frequency and then at most once per interval. Both halves matter: announcing every QSO would flood the node and get the station filtered out, while a pure timer would stay silent for minutes after a band change. A drift of up to 500 Hz still counts as the same run, so nudging the VFO mid-pileup does not re-announce. Five minutes is the floor, clamped in SaveSelfSpotSettings as well as in the input: the limit protects the node from us, so it must not depend on the frontend. The interval input keeps raw text and clamps on blur — clamping per keystroke rewrote "10" to "5" as soon as the "1" landed. Wired into both log paths (manual entry and UDP auto-log) on the async side, so a cluster that is slow or down never holds up logging. A send failure restores the previous throttle state, so the next QSO retries instead of sitting out an interval that produced no spot.
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
||||
AudioStartMonitor, AudioStopMonitor, AudioMonitorActive,
|
||||
AudioStartTX, AudioStopTX, AudioTXActive,
|
||||
ListClusterServers, SaveClusterServer, DeleteClusterServer,
|
||||
GetClusterAutoConnect, SetClusterAutoConnect,
|
||||
GetClusterAutoConnect, SetClusterAutoConnect, GetSelfSpotSettings, SaveSelfSpotSettings,
|
||||
ConnectClusterServer, DisconnectClusterServer,
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
||||
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
||||
@@ -1524,19 +1524,28 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
|
||||
const [clusterServers, setClusterServers] = useState<ClusterServer[]>([]);
|
||||
const [clusterAutoConnect, setClusterAutoConnectState] = useState(false);
|
||||
// Self-spot. SELF_SPOT_MIN_MIN mirrors the backend floor — the input clamps on
|
||||
// blur, not per keystroke, or typing "10" would be rewritten to "5" the moment
|
||||
// the "1" landed and the field would fight the operator.
|
||||
const SELF_SPOT_MIN_MIN = 5;
|
||||
const [selfSpot, setSelfSpot] = useState({ enabled: false, minutes: SELF_SPOT_MIN_MIN });
|
||||
const [selfSpotText, setSelfSpotText] = useState(String(SELF_SPOT_MIN_MIN));
|
||||
const [clusterStatuses, setClusterStatuses] = useState<ClusterServerStatus[]>([]);
|
||||
const [editingServer, setEditingServer] = useState<ClusterServer | null>(null);
|
||||
|
||||
async function reloadClusterServers() {
|
||||
try {
|
||||
const [list, ac, st] = await Promise.all([
|
||||
const [list, ac, st, ss] = await Promise.all([
|
||||
ListClusterServers(),
|
||||
GetClusterAutoConnect(),
|
||||
GetClusterStatus(),
|
||||
GetSelfSpotSettings(),
|
||||
]);
|
||||
setClusterServers((list ?? []) as ClusterServer[]);
|
||||
setClusterAutoConnectState(ac);
|
||||
setClusterStatuses((st ?? []) as ClusterServerStatus[]);
|
||||
setSelfSpot(ss as any);
|
||||
setSelfSpotText(String((ss as any).minutes ?? SELF_SPOT_MIN_MIN));
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
@@ -1849,6 +1858,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
await SaveQSLDefaults(qslDefaults as any);
|
||||
await SaveExternalServices(extSvc as any);
|
||||
await SetClusterAutoConnect(clusterAutoConnect);
|
||||
await SaveSelfSpotSettings(selfSpot as any);
|
||||
|
||||
setMsg('Settings saved.');
|
||||
onSaved();
|
||||
@@ -4114,6 +4124,39 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterWorkedSameSlot(v); writeUiPref('opslog.clusterWorkedSameSlot', v ? '1' : '0'); }} />
|
||||
<span>{t('clu.workedSameSlot')} <span className="text-xs text-muted-foreground">{t('clu.workedSameSlotHint')}</span></span>
|
||||
</label>
|
||||
|
||||
{/* Self-spot. The interval only shows once it's on — an interval for
|
||||
something switched off is just a question the operator can't act on. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={selfSpot.enabled}
|
||||
onCheckedChange={(c) => setSelfSpot((s) => ({ ...s, enabled: !!c }))} />
|
||||
{t('clu.selfSpot')}
|
||||
</label>
|
||||
{selfSpot.enabled && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-muted-foreground">{t('clu.selfSpotEvery')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={SELF_SPOT_MIN_MIN}
|
||||
max={240}
|
||||
className="w-20 h-8 font-mono text-xs"
|
||||
value={selfSpotText}
|
||||
onChange={(e) => setSelfSpotText(e.target.value)}
|
||||
onBlur={() => {
|
||||
const n = Math.floor(Number(selfSpotText));
|
||||
const v = Number.isFinite(n) && n > SELF_SPOT_MIN_MIN ? Math.min(n, 240) : SELF_SPOT_MIN_MIN;
|
||||
setSelfSpotText(String(v));
|
||||
setSelfSpot((s) => ({ ...s, minutes: v }));
|
||||
}}
|
||||
/>
|
||||
<span className="text-muted-foreground">{t('clu.selfSpotMinutes')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">{t('clu.selfSpotHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editingServer && (
|
||||
|
||||
@@ -262,6 +262,8 @@ const en: Dict = {
|
||||
'clu.name': 'Name', 'clu.hostPort': 'Host:port', 'clu.status': 'Status', 'clu.actions': 'Actions',
|
||||
'clu.moveUp': 'Move up', 'clu.moveDown': 'Move down', 'clu.edit': 'Edit', 'clu.delete': 'Delete', 'clu.none': 'No cluster nodes saved yet.', 'clu.connect': 'Connect', 'clu.disconnect': 'Disconnect',
|
||||
'clu.add': 'Add cluster', 'clu.connectAll': 'Connect all', 'clu.disconnectAll': 'Disconnect all', 'clu.autoConnect': 'Auto-connect all enabled on app start',
|
||||
'clu.selfSpot': 'Self-spot while I log', 'clu.selfSpotEvery': 'at most every', 'clu.selfSpotMinutes': 'min',
|
||||
'clu.selfSpotHint': 'Announces YOU on the master cluster when you log a QSO — the spot carries your station callsign and the frequency you just worked on, so callers find you without waiting for someone else to spot you. Sent on the first QSO of a frequency, then no more often than the gap below. Five minutes is the floor: a self-spot is traffic every user of the node sees.',
|
||||
'clu.freeNodes': 'Free public nodes:',
|
||||
'clu.workedSameSlot': 'Already worked only on the same slot',
|
||||
'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.',
|
||||
@@ -675,6 +677,8 @@ const fr: Dict = {
|
||||
'clu.name': 'Nom', 'clu.hostPort': 'Hôte:port', 'clu.status': 'Statut', 'clu.actions': 'Actions',
|
||||
'clu.moveUp': 'Monter', 'clu.moveDown': 'Descendre', 'clu.edit': 'Éditer', 'clu.delete': 'Supprimer', 'clu.none': 'Aucun nœud cluster enregistré.', 'clu.connect': 'Connecter', 'clu.disconnect': 'Déconnecter',
|
||||
'clu.add': 'Ajouter cluster', 'clu.connectAll': 'Tout connecter', 'clu.disconnectAll': 'Tout déconnecter', 'clu.autoConnect': 'Connexion auto de tous les activés au démarrage',
|
||||
'clu.selfSpot': "M'auto-spotter quand j'enregistre", 'clu.selfSpotEvery': 'au plus toutes les', 'clu.selfSpotMinutes': 'min',
|
||||
'clu.selfSpotHint': "Annonce TON indicatif sur le cluster maître quand tu enregistres un QSO — le spot porte l'indicatif de station et la fréquence que tu viens de travailler, pour qu'on te trouve sans attendre que quelqu'un te spotte. Envoyé au premier QSO d'une fréquence, puis pas plus souvent que l'intervalle ci-dessous. Cinq minutes est le plancher : un auto-spot est du trafic que voient tous les utilisateurs du nœud.",
|
||||
'clu.freeNodes': 'Nœuds publics gratuits :',
|
||||
'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot',
|
||||
'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.',
|
||||
|
||||
Vendored
+4
@@ -481,6 +481,8 @@ export function GetScpStatus():Promise<main.ScpStatus>;
|
||||
|
||||
export function GetSecretStatus():Promise<main.SecretStatus>;
|
||||
|
||||
export function GetSelfSpotSettings():Promise<main.SelfSpotSettings>;
|
||||
|
||||
export function GetSlotStats():Promise<qso.SlotStats>;
|
||||
|
||||
export function GetSolarData():Promise<solar.Data>;
|
||||
@@ -895,6 +897,8 @@ export function SaveRelayAuto(arg1:main.RelayAutoConfig):Promise<void>;
|
||||
|
||||
export function SaveRotators(arg1:Array<main.RotatorDevice>):Promise<void>;
|
||||
|
||||
export function SaveSelfSpotSettings(arg1:main.SelfSpotSettings):Promise<void>;
|
||||
|
||||
export function SaveStationDevices(arg1:Array<main.StationDevice>):Promise<void>;
|
||||
|
||||
export function SaveStationSettings(arg1:main.StationSettings):Promise<void>;
|
||||
|
||||
@@ -910,6 +910,10 @@ export function GetSecretStatus() {
|
||||
return window['go']['main']['App']['GetSecretStatus']();
|
||||
}
|
||||
|
||||
export function GetSelfSpotSettings() {
|
||||
return window['go']['main']['App']['GetSelfSpotSettings']();
|
||||
}
|
||||
|
||||
export function GetSlotStats() {
|
||||
return window['go']['main']['App']['GetSlotStats']();
|
||||
}
|
||||
@@ -1738,6 +1742,10 @@ export function SaveRotators(arg1) {
|
||||
return window['go']['main']['App']['SaveRotators'](arg1);
|
||||
}
|
||||
|
||||
export function SaveSelfSpotSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveSelfSpotSettings'](arg1);
|
||||
}
|
||||
|
||||
export function SaveStationDevices(arg1) {
|
||||
return window['go']['main']['App']['SaveStationDevices'](arg1);
|
||||
}
|
||||
|
||||
@@ -2914,6 +2914,20 @@ export namespace main {
|
||||
this.unlocked = source["unlocked"];
|
||||
}
|
||||
}
|
||||
export class SelfSpotSettings {
|
||||
enabled: boolean;
|
||||
minutes: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SelfSpotSettings(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.enabled = source["enabled"];
|
||||
this.minutes = source["minutes"];
|
||||
}
|
||||
}
|
||||
export class SpotQuery {
|
||||
call: string;
|
||||
band: string;
|
||||
|
||||
Reference in New Issue
Block a user