feat(app): auto-call, PSK Reporter analysis and the decode band change

The Wails boundary for the two features above, plus the pieces that
belong to neither:

- decodes are cut into periods PER RECEIVER, and judged 0.8 s after the
  last one arrives rather than a slot and four seconds after the period
  they belong to. That stamp is the START of the slot, thirteen seconds
  before its decodes exist, so every answer left four seconds into the
  next slot and the decoder began its call late.

- WSJT-X colouring can grey out a station already worked on this band
  AND in this mode. Its own switch, off by default: the other three
  verdicts pick out a handful of decodes, this one can match most of a
  period on a full log.

- the watch list's contest auto-add takes a list of callsigns as well
  as a pattern. A pattern collects a fleet that shares a string; it
  cannot collect the station taking part under a callsign that says
  nothing about the event.
This commit is contained in:
2026-09-05 21:45:51 +02:00
parent 470eaf5d80
commit e4a5d42b85
4 changed files with 57 additions and 2 deletions
+23 -2
View File
@@ -753,6 +753,9 @@ type App struct {
// is consulted once per message.
chaseBandsMu sync.RWMutex
chaseBands map[string]bool
// The Chase new panel's OWN band filter, as the set switched off. Read per
// message like the list above, so an atomic rather than a settings lookup.
chaseNewBandsOff atomic.Value // map[string]bool
// pskr is the PSK Reporter MQTT feed, up only while the opening watch is on.
// It is the source that makes VHF detection work at all: the cluster and RBN
// carry a handful of 6 m spots where PSK Reporter carries hundreds.
@@ -773,8 +776,12 @@ type App struct {
// FT8 window and an FT4 one do not even share a period length — and one
// buffer for both cut every slot into fragments, each judged as a period of
// its own. The engine then counted a missed period several times a slot.
acPeriod map[string]string
acAt map[string]time.Time
acPeriod map[string]string
acAt map[string]time.Time
// acFed is when the last decode of the open period ARRIVED, wall clock. The
// period's own timestamp cannot answer "has it gone quiet" — it is the start
// of the slot, thirteen seconds before its decodes exist.
acFed map[string]time.Time
acTR map[string]int
acBuf map[string][]acDecode
acTX autocall.TXState
@@ -782,6 +789,10 @@ type App struct {
acLastJudge time.Time
// acTXPeriod is the last transmit period already counted — see autoCallNoteTX.
acTXPeriod string
// What the decodes panel is SHOWING, and whether it is publishing at all.
// The panel owns the filters; this is its answer, not a second copy of them.
acVisible map[string]bool
acVisibleOn bool
// Self-spot throttle: when and on what frequency we last announced ourselves.
// Held in memory only — a restart legitimately re-announces the station.
selfSpotMu sync.Mutex
@@ -1653,6 +1664,7 @@ func (a *App) startup(ctx context.Context) {
a.startGridCache()
a.chaseNew = newChaseNewStore()
a.refreshChaseNew()
a.refreshChaseNewBands()
// PSK Reporter. After the operator's grid is known: without it there is no
// distance to measure and no receiver squares to filter on, so it stays down.
a.startBandOpenFeed()
@@ -20578,6 +20590,15 @@ func (a *App) SaveChaseSettings(cs ChaseSettings) error {
a.clusterStatusMu.Lock()
a.clusterStatusIdx = nil
a.clusterStatusMu.Unlock()
// Chase new judges each decode AS IT ARRIVES and keeps only what was new at
// that moment, so its list is a set of verdicts reached under the old rule —
// and nothing in it would ever be re-judged. Switching to "new only" left
// rows listed that are merely unconfirmed; switching the other way could not
// bring back what had already been refused. Emptied, it refills from the
// live feed within a period or two, under the rule now in force.
if a.chaseNew != nil {
a.chaseNew.clear()
}
// The frontend caches resolved verdicts and only asks about unknown keys —
// without this it kept judging half the screen under the OLD rules until a
// restart, which read as the new mode simply not working.
+8
View File
@@ -454,6 +454,8 @@ export function GetChangelog():Promise<Array<main.ChangelogEntry>>;
export function GetChaseNew():Promise<boolean>;
export function GetChaseNewBands():Promise<Array<string>>;
export function GetChaseNewGrids():Promise<boolean>;
export function GetChaseNewSpots():Promise<Array<main.ChaseNewSpot>>;
@@ -654,6 +656,8 @@ export function GetYaesuState():Promise<cat.YaesuTXState>;
export function GridSquares(arg1:string):Promise<Array<qso.GridSquare>>;
export function HaltAutoCall():Promise<void>;
export function HaltDecodeTx(arg1:string,arg2:boolean):Promise<void>;
export function HasBuiltinReferences(arg1:string):Promise<boolean>;
@@ -1158,6 +1162,8 @@ export function SetAutoCall(arg1:boolean):Promise<void>;
export function SetAutoCallOnly(arg1:string):Promise<void>;
export function SetAutoCallVisible(arg1:Array<string>,arg2:boolean):Promise<void>;
export function SetCATFrequency(arg1:number):Promise<void>;
export function SetCATMode(arg1:string):Promise<void>;
@@ -1168,6 +1174,8 @@ export function SetCWDecoderPitch(arg1:number):Promise<void>;
export function SetChaseNew(arg1:boolean):Promise<void>;
export function SetChaseNewBands(arg1:Array<string>):Promise<void>;
export function SetChaseNewGrids(arg1:boolean):Promise<void>;
export function SetClublogCtyEnabled(arg1:boolean):Promise<void>;
+16
View File
@@ -842,6 +842,10 @@ export function GetChaseNew() {
return window['go']['main']['App']['GetChaseNew']();
}
export function GetChaseNewBands() {
return window['go']['main']['App']['GetChaseNewBands']();
}
export function GetChaseNewGrids() {
return window['go']['main']['App']['GetChaseNewGrids']();
}
@@ -1242,6 +1246,10 @@ export function GridSquares(arg1) {
return window['go']['main']['App']['GridSquares'](arg1);
}
export function HaltAutoCall() {
return window['go']['main']['App']['HaltAutoCall']();
}
export function HaltDecodeTx(arg1, arg2) {
return window['go']['main']['App']['HaltDecodeTx'](arg1, arg2);
}
@@ -2250,6 +2258,10 @@ export function SetAutoCallOnly(arg1) {
return window['go']['main']['App']['SetAutoCallOnly'](arg1);
}
export function SetAutoCallVisible(arg1, arg2) {
return window['go']['main']['App']['SetAutoCallVisible'](arg1, arg2);
}
export function SetCATFrequency(arg1) {
return window['go']['main']['App']['SetCATFrequency'](arg1);
}
@@ -2270,6 +2282,10 @@ export function SetChaseNew(arg1) {
return window['go']['main']['App']['SetChaseNew'](arg1);
}
export function SetChaseNewBands(arg1) {
return window['go']['main']['App']['SetChaseNewBands'](arg1);
}
export function SetChaseNewGrids(arg1) {
return window['go']['main']['App']['SetChaseNewGrids'](arg1);
}
+10
View File
@@ -1962,6 +1962,8 @@ export namespace main {
misses: number;
max_rounds: number;
rest_min: number;
on_screen_only: boolean;
trace: boolean;
static createFrom(source: any = {}) {
return new AutoCallSettings(source);
@@ -1976,6 +1978,8 @@ export namespace main {
this.misses = source["misses"];
this.max_rounds = source["max_rounds"];
this.rest_min = source["rest_min"];
this.on_screen_only = source["on_screen_only"];
this.trace = source["trace"];
}
}
export class AutoCallStatus {
@@ -1987,6 +1991,7 @@ export namespace main {
misses: number;
max_miss: number;
stopped: boolean;
greylisted: number;
reason: string;
static createFrom(source: any = {}) {
@@ -2003,6 +2008,7 @@ export namespace main {
this.misses = source["misses"];
this.max_miss = source["max_miss"];
this.stopped = source["stopped"];
this.greylisted = source["greylisted"];
this.reason = source["reason"];
}
}
@@ -4962,6 +4968,8 @@ export namespace pskr {
last_err?: string;
broker: string;
bands: string[];
near_km: number;
squares: number;
static createFrom(source: any = {}) {
return new Status(source);
@@ -4975,6 +4983,8 @@ export namespace pskr {
this.last_err = source["last_err"];
this.broker = source["broker"];
this.bands = source["bands"];
this.near_km = source["near_km"];
this.squares = source["squares"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {