feat: ClubLog Most Wanted rank in the entry band matrix
Opt-in (Settings → General). Shows a "MW #rank" pill next to the DXCC entity name in the entry-strip band/slot matrix — ClubLog's Most Wanted ranking (1 = most wanted), personalised to the operator's callsign and refreshed daily. - internal/clublog/mostwanted.go: fetch mostwanted.php?api=1&callsign=<CALL> (rank→DXCC JSON, real User-Agent), invert to dxcc→rank, cache to <dataDir>/clublog_mostwanted.json; NeedsRefresh invalidates on callsign change. - app.go: keyClublogMostWanted setting, a.clublogMW, startup refresh goroutine, activeCallsign() helper, and Get/Set/Download bindings mirroring the cty ones. WorkedBefore now carries MWRank (qso.WorkedBefore.MWRank) when enabled. - BandSlotGrid.tsx: MW pill next to the entity name, colour-tiered by rank. - SettingsModal General: toggle + download button + status, mirroring the ClubLog cty-exceptions block. Also reorganises the changelog: the theme-persistence fix (landed after the v0.20.11 tag) plus this feature go under a new 0.20.12 entry; 0.20.11 keeps only what shipped in its binary.
This commit is contained in:
@@ -136,6 +136,7 @@ const (
|
||||
keyAwardEditsSeeded = "awards.defs.editflag" // one-shot: back-fill the user-edited flag
|
||||
|
||||
keyClublogCtyEnabled = "clublog.cty_exceptions" // "1" → apply ClubLog exceptions
|
||||
keyClublogMostWanted = "clublog.most_wanted" // "1" → show ClubLog Most Wanted rank in the entry matrix
|
||||
|
||||
// E-mail / SMTP — send QSO recordings to the correspondent.
|
||||
keyEmailEnabled = "email.enabled"
|
||||
@@ -464,6 +465,7 @@ type App struct {
|
||||
extsvc *extsvc.Manager
|
||||
winkeyer *winkeyer.Manager
|
||||
clublog *clublog.Manager
|
||||
clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in)
|
||||
motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled
|
||||
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
||||
motorInhibStop chan struct{} // stops the "inhibit TX while moving" loop; nil when off
|
||||
@@ -954,6 +956,30 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
// ClubLog "Most Wanted" DXCC ranking (opt-in, Settings → General). Loaded from
|
||||
// the cached JSON, then refreshed daily and whenever the operator's callsign
|
||||
// changes (the ranking is personalised per callsign). Best-effort.
|
||||
a.clublogMW = clublog.NewMostWanted(dataDir)
|
||||
go func() {
|
||||
if a.settings == nil {
|
||||
return
|
||||
}
|
||||
if v, _ := a.settings.Get(a.ctx, keyClublogMostWanted); v != "1" {
|
||||
return // feature off — don't touch the network
|
||||
}
|
||||
_ = a.clublogMW.LoadFromDisk()
|
||||
call := a.activeCallsign()
|
||||
if a.clublogMW.NeedsRefresh(call, 24*time.Hour) {
|
||||
if err := a.clublogMW.Download(context.Background(), call); err != nil {
|
||||
applog.Printf("clublog most-wanted: auto-refresh failed: %v", err)
|
||||
}
|
||||
}
|
||||
if a.clublogMW.Loaded() {
|
||||
c, d, n := a.clublogMW.Info()
|
||||
applog.Printf("clublog most-wanted: loaded %d entities for %s (%s)", n, c, d)
|
||||
}
|
||||
}()
|
||||
|
||||
// POTA: background poller of api.pota.app so cluster spots can be tagged
|
||||
// when the DX station is currently activating a park. Best-effort.
|
||||
a.pota = pota.New(func(format string, args ...any) { applog.Printf(format, args...) })
|
||||
@@ -5247,7 +5273,13 @@ func (a *App) WorkedBefore(callsign string, dxccHint int) (qso.WorkedBefore, err
|
||||
dxccHint = dxcc.EntityDXCC(m.Entity.Name)
|
||||
}
|
||||
}
|
||||
return a.qso.WorkedBefore(a.ctx, callsign, dxccHint)
|
||||
wb, err := a.qso.WorkedBefore(a.ctx, callsign, dxccHint)
|
||||
// Attach the ClubLog Most Wanted rank for this entity (opt-in) so the entry
|
||||
// matrix can show it next to the country name.
|
||||
if err == nil && wb.DXCC > 0 && a.clublogMW != nil && a.clublogMostWantedEnabled() {
|
||||
wb.MWRank = a.clublogMW.Rank(wb.DXCC)
|
||||
}
|
||||
return wb, err
|
||||
}
|
||||
|
||||
// SetCompactMode toggles a tiny always-on-top window that exposes just the
|
||||
@@ -7327,6 +7359,74 @@ func (a *App) DownloadClublogCty() (ClublogCtyInfo, error) {
|
||||
return a.GetClublogCtyInfo(), nil
|
||||
}
|
||||
|
||||
// activeCallsign is the current profile's callsign (upper-cased), "" if none.
|
||||
func (a *App) activeCallsign() string {
|
||||
if a.profiles == nil {
|
||||
return ""
|
||||
}
|
||||
if p, err := a.profiles.Active(a.ctx); err == nil {
|
||||
return strings.ToUpper(strings.TrimSpace(p.Callsign))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *App) clublogMostWantedEnabled() bool {
|
||||
if a.settings == nil {
|
||||
return false
|
||||
}
|
||||
v, _ := a.settings.Get(a.ctx, keyClublogMostWanted)
|
||||
return v == "1"
|
||||
}
|
||||
|
||||
// ClublogMostWantedInfo is the UI status of the ClubLog Most Wanted data.
|
||||
type ClublogMostWantedInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Loaded bool `json:"loaded"`
|
||||
Callsign string `json:"callsign"`
|
||||
Date string `json:"date"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// GetClublogMostWantedInfo returns the current Most Wanted status.
|
||||
func (a *App) GetClublogMostWantedInfo() ClublogMostWantedInfo {
|
||||
info := ClublogMostWantedInfo{Enabled: a.clublogMostWantedEnabled()}
|
||||
if a.clublogMW != nil {
|
||||
info.Loaded = a.clublogMW.Loaded()
|
||||
info.Callsign, info.Date, info.Count = a.clublogMW.Info()
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// SetClublogMostWantedEnabled toggles the Most Wanted feature. On first enable it
|
||||
// loads the cached list (the UI then calls Download to fetch a fresh one).
|
||||
func (a *App) SetClublogMostWantedEnabled(on bool) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
v := "0"
|
||||
if on {
|
||||
v = "1"
|
||||
}
|
||||
if err := a.settings.Set(a.ctx, keyClublogMostWanted, v); err != nil {
|
||||
return err
|
||||
}
|
||||
if on && a.clublogMW != nil && !a.clublogMW.Loaded() {
|
||||
_ = a.clublogMW.LoadFromDisk() // ok if not downloaded yet
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadClublogMostWanted fetches a fresh Most Wanted list for the active call.
|
||||
func (a *App) DownloadClublogMostWanted() (ClublogMostWantedInfo, error) {
|
||||
if a.clublogMW == nil {
|
||||
return ClublogMostWantedInfo{}, fmt.Errorf("clublog not initialized")
|
||||
}
|
||||
if err := a.clublogMW.Download(a.ctx, a.activeCallsign()); err != nil {
|
||||
return a.GetClublogMostWantedInfo(), err
|
||||
}
|
||||
return a.GetClublogMostWantedInfo(), nil
|
||||
}
|
||||
|
||||
// applyClublogException overrides a QSO's entity fields from a ClubLog
|
||||
// exception matching its callsign at its date. force=true ignores the
|
||||
// enable toggle (used by the explicit "Update from ClubLog" action).
|
||||
|
||||
Reference in New Issue
Block a user