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.