feat: Super Check Partial + N+1 helper; fix Flex binding to SmartSDR CAT; telemetry callsign wait
- SCP/N+1: new internal/scp downloads the community MASTER.SCP master list and a docked two-column widget shows, as you type a call, the known calls containing it (Partial) and the calls one edit away (N+1) — click to fix a busted call. Opt-in in Settings → General; top-bar toggle; queried debounced on callsign input. - Flex: OpsLog's GUI-client detection was too loose and could bind to "SmartSDR CAT" (or DAX) — both carry "smartsdr" in the program name — instead of the real GUI client, making SmartSDR CAT drop and reconnect in a loop while OpsLog was open. Now it binds only to a real SmartSDR/Maestro GUI client (e.g. a FLEX-8600M's integrated screen) and excludes cat/dax; dropped the risky empty-program fallback. - Telemetry: on a fresh install the callsign isn't set at launch, so the once-a-day heartbeat recorded the machine UUID. Now it waits (~10 min) for the operator to enter their callsign before sending, falling back to the UUID only if none appears.
This commit is contained in:
@@ -53,6 +53,7 @@ import (
|
||||
"hamlog/internal/rotator/gs232"
|
||||
"hamlog/internal/rotator/pst"
|
||||
"hamlog/internal/rotgenius"
|
||||
"hamlog/internal/scp"
|
||||
"hamlog/internal/settings"
|
||||
"hamlog/internal/solar"
|
||||
"hamlog/internal/spe"
|
||||
@@ -234,6 +235,8 @@ const (
|
||||
|
||||
keyClusterAutoConnect = "cluster.auto_connect" // open every enabled server at app start
|
||||
|
||||
keyScpEnabled = "scp.enabled" // Super Check Partial / N+1 suggestions on
|
||||
|
||||
keyBackupEnabled = "backup.enabled"
|
||||
keyBackupFolder = "backup.folder"
|
||||
keyBackupRotation = "backup.rotation"
|
||||
@@ -493,6 +496,7 @@ type App struct {
|
||||
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)
|
||||
scp *scp.Manager // Super Check Partial / N+1 callsign master list
|
||||
|
||||
// NET Control: persistent net definitions/rosters (global JSON) + the live
|
||||
// session (in-memory only — active stations currently in QSO).
|
||||
@@ -985,6 +989,25 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Super Check Partial / N+1: load the cached MASTER.SCP; when the feature is
|
||||
// enabled, auto-(re)download it if missing or older than a week.
|
||||
a.scp = scp.NewManager(dataDir)
|
||||
go func() {
|
||||
if v, _ := a.settings.Get(a.ctx, keyScpEnabled); v != "1" {
|
||||
return
|
||||
}
|
||||
if a.scp.Count() == 0 || time.Since(a.scp.Updated()) > 7*24*time.Hour {
|
||||
if n, err := a.scp.Download(context.Background()); err == nil {
|
||||
applog.Printf("scp: auto-downloaded %d callsigns", n)
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "scp:updated")
|
||||
}
|
||||
} else {
|
||||
applog.Printf("scp: auto-download failed: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
_ = a.clublog.EnsureLoaded()
|
||||
// Auto-refresh a missing/stale country file (ClubLog adds date-ranged
|
||||
@@ -2311,6 +2334,76 @@ func (a *App) DownloadLoTWUsers() (int, error) {
|
||||
return a.lotwUsers.Download(a.ctx)
|
||||
}
|
||||
|
||||
// ── Super Check Partial / N+1 ────────────────────────────────────────────────
|
||||
|
||||
// ScpStatus is the loaded-list summary for Settings + the widget gate.
|
||||
type ScpStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Count int `json:"count"`
|
||||
Updated string `json:"updated,omitempty"` // RFC3339, empty if never
|
||||
}
|
||||
|
||||
// GetScpStatus returns whether SCP is enabled and how many calls are loaded.
|
||||
func (a *App) GetScpStatus() ScpStatus {
|
||||
st := ScpStatus{}
|
||||
if a.settings != nil {
|
||||
v, _ := a.settings.Get(a.ctx, keyScpEnabled)
|
||||
st.Enabled = v == "1"
|
||||
}
|
||||
if a.scp != nil {
|
||||
st.Count = a.scp.Count()
|
||||
if u := a.scp.Updated(); !u.IsZero() {
|
||||
st.Updated = u.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// SetScpEnabled turns Super Check Partial on/off. Enabling triggers a background
|
||||
// download when the list is missing or stale.
|
||||
func (a *App) SetScpEnabled(on bool) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
if err := a.settings.Set(a.ctx, keyScpEnabled, boolStr(on)); err != nil {
|
||||
return err
|
||||
}
|
||||
if on && a.scp != nil && (a.scp.Count() == 0 || time.Since(a.scp.Updated()) > 7*24*time.Hour) {
|
||||
go func() {
|
||||
if n, err := a.scp.Download(context.Background()); err == nil {
|
||||
applog.Printf("scp: downloaded %d callsigns", n)
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "scp:updated")
|
||||
}
|
||||
} else {
|
||||
applog.Printf("scp: download failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadScp fetches the MASTER.SCP master file and caches it. Returns the
|
||||
// number of callsigns loaded.
|
||||
func (a *App) DownloadScp() (int, error) {
|
||||
if a.scp == nil {
|
||||
return 0, fmt.Errorf("not initialized")
|
||||
}
|
||||
return a.scp.Download(a.ctx)
|
||||
}
|
||||
|
||||
// ScpLookup returns the Super Check Partial (substring) and N+1 (one-edit)
|
||||
// suggestions for a typed fragment. Empty when SCP is disabled.
|
||||
func (a *App) ScpLookup(fragment string) scp.Result {
|
||||
if a.scp == nil || a.settings == nil {
|
||||
return scp.Result{}
|
||||
}
|
||||
if v, _ := a.settings.Get(a.ctx, keyScpEnabled); v != "1" {
|
||||
return scp.Result{}
|
||||
}
|
||||
return a.scp.Lookup(fragment, 60)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user