Files
OpsLog/app_dxped.go
T
rouggy 2f1d592497 revert(dxped): no chase badge on the news pane
Tried it, looked at it, took it out. A headline names no band, so the
only honest verdict is entity-level — and a pane where every row reads
NEW DXCC or worked says nothing an operator can act on. Two words
repeated forty times is noise wearing the clothes of information.

The announcements pane keeps its badge, where the announced bands and
modes make the verdict specific enough to be worth a colour. The news
pane keeps what it is good at: what is happening, and a Watch button.
Changelog entry withdrawn with it — it never shipped.
2026-08-31 19:00:44 +02:00

154 lines
4.7 KiB
Go

package main
import (
"strings"
"hamlog/internal/applog"
"hamlog/internal/dxped"
)
// ── DXpeditions (ADXO announcements + DX-World news) ────────────────────
//
// What a logger can say that a news reader cannot: whether the announced
// operation is worth chasing. Every activation is judged against THIS log —
// the same verdict the cluster paints on a spot — so the list reads as "what I
// still need", not "what is on the air".
// DXpedition is one announced operation plus what it is worth here.
type DXpedition struct {
dxped.Activation
// Status is the strongest verdict across the announced callsigns, bands and
// modes: "new" (entity never worked) beats "new-band-mode", which beats
// "new-band", "new-mode", "new-slot", and finally "worked". Empty when the
// callsign resolves to no entity — a prefix ADXO knows and cty.dat does not.
Status string `json:"status_chase"`
// Unconfirmed marks a need that is only a missing QSL, so the badge can be
// drawn dimmed exactly as it is in the cluster and the decode list.
Unconfirmed bool `json:"unconfirmed"`
}
// chaseRank orders the verdicts from "most worth chasing" down. The DXpedition
// list shows ONE badge, so the ranking is the whole decision.
var chaseRank = map[string]int{
"new": 6,
"new-band-mode": 5,
"new-band": 4,
"new-mode": 3,
"new-slot": 2,
"worked": 1,
}
// GetDXpeditions returns the announced operations, freshest feed permitting,
// each carrying its chase verdict.
func (a *App) GetDXpeditions() ([]DXpedition, error) {
if a.dxped == nil {
a.dxped = dxped.New()
}
acts, err := a.dxped.Activations(a.ctx)
if err != nil {
applog.Printf("dxped: adxo fetch: %v", err)
if len(acts) == 0 {
return nil, err
}
// Stale data with a logged error beats an empty tab.
}
out := make([]DXpedition, 0, len(acts))
for _, act := range acts {
out = append(out, DXpedition{Activation: act, Status: "", Unconfirmed: false})
}
a.judgeDXpeditions(out)
return out, nil
}
// judgeDXpeditions fills in the chase verdict, in place.
//
// One ClusterSpotStatuses call for the whole list rather than one per row: the
// worked-index it builds is the expensive part (a full pass over the log), and
// asking it forty times to answer forty rows was the difference between a tab
// that opens and a tab that stalls a remote MySQL for a second.
func (a *App) judgeDXpeditions(list []DXpedition) {
if a.qso == nil || len(list) == 0 {
return
}
type slot struct{ row int }
var queries []SpotQuery
var owners []slot
for i, d := range list {
calls := d.Calls
if len(calls) == 0 {
if c := strings.ToUpper(strings.TrimSpace(d.Callsign)); c != "" {
calls = []string{c}
}
}
for _, call := range calls {
// No announced band/mode: ask the entity-level question alone.
if len(d.Bands) == 0 && len(d.Modes) == 0 {
queries = append(queries, SpotQuery{Call: call})
owners = append(owners, slot{i})
continue
}
bands := d.Bands
if len(bands) == 0 {
bands = []string{""}
}
modes := d.Modes
if len(modes) == 0 {
modes = []string{""}
}
for _, b := range bands {
for _, m := range modes {
queries = append(queries, SpotQuery{Call: call, Band: b, Mode: m})
owners = append(owners, slot{i})
}
}
}
}
if len(queries) == 0 {
return
}
res := a.ClusterSpotStatuses(queries)
for i, r := range res {
if i >= len(owners) {
break
}
row := owners[i].row
if chaseRank[r.Status] > chaseRank[list[row].Status] {
list[row].Status = r.Status
list[row].Unconfirmed = r.UnconfStatus
}
}
}
// GetDXWorldNews returns the DX-World headlines, with the callsigns mined out
// of each one so the reader can watch them.
//
// Deliberately NOT judged against the log. A headline names no band, so the
// only verdict available is entity-level, and a pane of "NEW DXCC" and
// "worked" badges turned out to say nothing an operator could act on — the
// same two words on every row is noise wearing the clothes of information.
// The announcements pane, which knows the bands and modes, is where a chase
// verdict is worth drawing.
func (a *App) GetDXWorldNews() ([]dxped.News, error) {
if a.dxped == nil {
a.dxped = dxped.New()
}
news, err := a.dxped.News(a.ctx)
if err != nil {
applog.Printf("dxped: dx-world fetch: %v", err)
if len(news) == 0 {
return nil, err
}
}
return news, nil
}
// RefreshDXpeditions drops both caches so the next read goes to the network.
// Wired to the tab's refresh button: an operator who has just read of a landing
// on a cluster should not wait out the cache to see it here.
func (a *App) RefreshDXpeditions() {
if a.dxped == nil {
a.dxped = dxped.New()
}
a.dxped.Invalidate()
}