Files
OpsLog/app_gate.go
T
rouggy 03e71bfdf2 feat: docked watch-list panel, and auto-call learns the orthogonal markers
The watch list was a tab, and an operator working FT8 lives on the decodes
one: a station they had asked to be told about turned up on a screen they
were not looking at. The same answer is now docked in the widget strip,
above the tabs, reduced to what is worth acting on — on the air and still
needed, one row per band and mode, with the cluster's own NEW DXCC /
NEW BAND / NEW SLOT badge and a click that tunes. Off by default. The
"active and needed" answer costs a debounced query per visible slot, so it
is written once (lib/watchlistSpots) and the tab uses it too.

Auto-call:

- It answers a new prefix, county, state, square or park. Those markers
  are orthogonal to the entity, they ranked as nothing-needed, and the
  engine sat through a never-worked WPX prefix calling CQ. New rung at
  the foot of the ladder, gated by the chase switches the badges use —
  which meant making those switches portable, since the backend cannot
  read localStorage.
- It calls THROUGH a pileup. Giving up the moment the DX answered
  somebody else is precisely how a queue is not worked; the call and miss
  counters already bound the effort, and a station in mid-exchange is
  still never chosen as a new target.

The PSK Reporter panel now follows the station auto-call is waiting for:
the analysis takes a history query and a period or two to fill, so
starting it when the DX comes free is starting it too late.

Callbook lookup: a compound callsign with a page of its OWN keeps that
page's location. QRZ files HP/WE9G under exactly that form, with the
Panama square the station is operating from, and the rule that drops a
home address from a portable call was throwing it away. The record's own
country tells an operation's page from a home page.

Changelog: entries may open with [NEW], drawn as a pill in the What's new
dialog — a release is mostly fixes and the two or three genuinely new
things should not have to be found by reading all of it.
2026-09-06 00:08:55 +02:00

66 lines
2.3 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"os"
"strings"
)
// deniedCallHashes lists station callsigns not permitted to run this build,
// stored as SHA-256 hex of the base call so the calls themselves appear nowhere
// in the source or the compiled binary. Enforcement is best-effort by design —
// the call is entered by the operator and can be changed — so this only turns
// away straightforward use, not a determined one.
var deniedCallHashes = map[string]struct{}{
"2282a88b3e5e4eebc6b8174d005bb46d32025758b7741bdcf34f2b3621c02205": {},
"0ee09fe60817a2a4982f5e5b14a60b8dbb11cff55b3d36661213c4cbdd0933ea": {},
"46fb61e71afb40627cb6493a6a59c9d4d0231ee3cad1a2bf3fcc4cdfce36fabc": {},
"d1ae6212ec057f9d5c6f379fb57acedac7c94142c9d49667dc04b2016d03d1b6": {},
"0741c9e394b42f43191899105553b47155ddc3026da12b5360701f9c181ff123": {},
"ab4926a3a0ab76d41b5b99cd3ad0683584970c341c29427c1dfa4b3c329ce415": {},
"9d17c9c213a6cc89c12d7520bcf21c86a0cf43d17e82e74f33f3a77cb865d28a": {},
"94ee059335e587e501cc4bf90613e0814f00a7b08bc7c648fd865a2af6a22cc2": {},
}
// callDenied reports whether a callsign is on deniedCallHashes. The call is
// reduced to its base form (upper-cased, portable prefix/suffix dropped) the
// same way extsvc does, so F4XYZ/P matches F4XYZ.
func callDenied(call string) bool {
base := denyBaseCall(call)
if base == "" {
return false
}
sum := sha256.Sum256([]byte(base))
_, bad := deniedCallHashes[hex.EncodeToString(sum[:])]
return bad
}
// denyBaseCall mirrors extsvc.baseCall: for a slashed form it returns the
// longest token (the real call), otherwise the call itself, upper-cased.
func denyBaseCall(s string) string {
s = strings.ToUpper(strings.TrimSpace(s))
if !strings.Contains(s, "/") {
return s
}
best := ""
for _, part := range strings.Split(s, "/") {
if len(part) > len(best) {
best = part
}
}
return best
}
// enforceCallGate exits the process silently when any of the supplied callsigns
// is denied. Called once at startup, after the profiles are loaded: the first
// launch is where the operator enters and saves the call, so a denied build
// simply does not come back up on the next launch. No window, no message.
func enforceCallGate(calls ...string) {
for _, c := range calls {
if callDenied(c) {
os.Exit(0)
}
}
}