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:
2026-08-08 20:34:10 +02:00
parent 90c6458af0
commit 1f55cfe9fa
7 changed files with 236 additions and 30 deletions
+4
View File
@@ -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>;
+8
View File
@@ -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);
}
+14
View File
@@ -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;