fix: Added LOTW badge for Lotw users colored depending on their last upload

This commit is contained in:
2026-07-09 19:32:32 +02:00
parent f3bf0b2f5c
commit 16e780c2df
13 changed files with 465 additions and 6 deletions
+70 -1
View File
@@ -41,6 +41,7 @@ import (
"hamlog/internal/netctl"
"hamlog/internal/operating"
"hamlog/internal/pota"
"hamlog/internal/lotwusers"
"hamlog/internal/powergenius"
"hamlog/internal/profile"
"hamlog/internal/qslcard"
@@ -422,6 +423,7 @@ type App struct {
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
solar *solar.Manager // live space-weather (SFI/SSN/A/K) for the header + QSO stamping
lotwUsers *lotwusers.Manager // LoTW user-activity list (badge next to the callsign)
// NET Control: persistent net definitions/rosters (global JSON) + the live
// session (in-memory only — active stations currently in QSO).
@@ -768,6 +770,24 @@ func (a *App) startup(ctx context.Context) {
// cty.dat lacks (DXpeditions). Loaded from cache if present; downloaded on
// demand. Resolution applied only when the user enables it.
a.clublog = clublog.NewManager(clublogAppAPIKey, dataDir)
// LoTW user-activity list (who's a LoTW user + last upload). Loads the cached
// CSV if present, and auto-downloads in the background when it's missing or
// older than a week (the ARRL list changes daily). Manual refresh is in
// Settings → LoTW.
a.lotwUsers = lotwusers.NewManager(dataDir)
go func() {
if a.lotwUsers.Count() == 0 || time.Since(a.lotwUsers.Updated()) > 7*24*time.Hour {
if n, err := a.lotwUsers.Download(context.Background()); err == nil {
applog.Printf("lotwusers: auto-downloaded %d LoTW users", n)
if a.ctx != nil {
wruntime.EventsEmit(a.ctx, "lotwusers:updated")
}
} else {
applog.Printf("lotwusers: auto-download failed: %v", err)
}
}
}()
go func() {
if err := a.clublog.EnsureLoaded(); err == nil {
d, n := a.clublog.Info()
@@ -1734,6 +1754,42 @@ func (a *App) RefreshSolar() {
}
}
// LoTWUserInfo reports whether a callsign is a LoTW user and how recently it
// uploaded — drives the colour-coded LoTW badge in the entry strip.
func (a *App) LoTWUserInfo(callsign string) lotwusers.Info {
if a.lotwUsers == nil {
return lotwusers.Info{DaysAgo: -1}
}
return a.lotwUsers.Lookup(callsign)
}
// LoTWUsersStatus is the loaded-list summary for Settings (count + last refresh).
type LoTWUsersStatus struct {
Count int `json:"count"`
Updated string `json:"updated,omitempty"` // RFC3339, empty if never
}
// GetLoTWUsersStatus returns how many LoTW callsigns are loaded and when.
func (a *App) GetLoTWUsersStatus() LoTWUsersStatus {
if a.lotwUsers == nil {
return LoTWUsersStatus{}
}
st := LoTWUsersStatus{Count: a.lotwUsers.Count()}
if u := a.lotwUsers.Updated(); !u.IsZero() {
st.Updated = u.UTC().Format(time.RFC3339)
}
return st
}
// DownloadLoTWUsers fetches ARRL's LoTW user-activity list and caches it.
// Returns the number of callsigns loaded.
func (a *App) DownloadLoTWUsers() (int, error) {
if a.lotwUsers == nil {
return 0, fmt.Errorf("not initialized")
}
return a.lotwUsers.Download(a.ctx)
}
// StationInfoComputed bundles the data we resolve live from the
// profile's callsign + grid: country, ARRL DXCC#, CQ zone, ITU zone,
// lat/lon. Used by the Settings UI to show the "what will be stamped on
@@ -5572,7 +5628,20 @@ func (a *App) applyClublogException(q *qso.QSO, force bool) bool {
}
e, ok := a.clublog.Resolve(q.Callsign, date)
if !ok {
return false
// No exception COVERS this QSO's date. If the call nonetheless HAS a
// date-ranged exception (e.g. G1T = Scotland only from 2024-02-21), then
// cty.dat's date-blind "=G1T → Scotland" override is WRONG for an older
// QSO — resolve it by ClubLog's date-aware PREFIX table instead (G1 →
// England for a 2012 contact). Ordinary calls (no exception) are left to
// cty.dat.
if a.clublog.HasException(q.Callsign) {
if pe, pok := a.clublog.ResolvePrefix(q.Callsign, date); pok {
e, ok = pe, true
}
}
if !ok {
return false
}
}
q.Country = titleEntity(e.Entity)
if e.Cont != "" {