pota
This commit is contained in:
@@ -175,6 +175,8 @@ const (
|
||||
keyExtClublogAutoUpload = "extsvc.clublog.auto_upload"
|
||||
keyExtClublogUploadMode = "extsvc.clublog.upload_mode"
|
||||
|
||||
keyExtPotaToken = "extsvc.pota.token" // pota.app session token for hunter-log sync
|
||||
|
||||
keyExtLoTWTQSLPath = "extsvc.lotw.tqsl_path"
|
||||
keyExtLoTWStationLoc = "extsvc.lotw.station_location"
|
||||
keyExtLoTWForceCall = "extsvc.lotw.force_station_callsign" // override STATION_CALLSIGN at sign time (e.g. F4BPO/P on the F4BPO cert)
|
||||
@@ -1550,6 +1552,102 @@ func (a *App) AwardCellQSOs(code, ref, band string) ([]qso.QSO, error) {
|
||||
return out, err
|
||||
}
|
||||
|
||||
// GetPOTAToken returns the stored pota.app session token (for the settings UI).
|
||||
func (a *App) GetPOTAToken() string {
|
||||
if a.settings == nil {
|
||||
return ""
|
||||
}
|
||||
t, _ := a.settings.Get(a.ctx, keyExtPotaToken)
|
||||
return t
|
||||
}
|
||||
|
||||
// SavePOTAToken stores the pota.app session token used to sync the hunter log.
|
||||
func (a *App) SavePOTAToken(token string) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyExtPotaToken, strings.TrimSpace(token))
|
||||
}
|
||||
|
||||
// POTASyncResult summarises a hunter-log sync run for the UI.
|
||||
type POTASyncResult struct {
|
||||
Fetched int `json:"fetched"` // hunter-log entries downloaded
|
||||
Updated int `json:"updated"` // QSOs newly stamped with a park ref
|
||||
AlreadyTagged int `json:"already_tagged"` // matched but already had a pota_ref
|
||||
Unmatched int `json:"unmatched"` // no local QSO matched
|
||||
}
|
||||
|
||||
// SyncPOTAHunterLog downloads the user's POTA hunter log and stamps pota_ref on
|
||||
// matching local QSOs (same callsign + band within ±5 min), filling only QSOs
|
||||
// that don't already carry a park reference.
|
||||
func (a *App) SyncPOTAHunterLog() (POTASyncResult, error) {
|
||||
if a.qso == nil || a.settings == nil {
|
||||
return POTASyncResult{}, fmt.Errorf("db not initialized")
|
||||
}
|
||||
token, _ := a.settings.Get(a.ctx, keyExtPotaToken)
|
||||
entries, err := pota.FetchHunterLog(a.ctx, token, applog.Printf)
|
||||
if err != nil {
|
||||
return POTASyncResult{}, err
|
||||
}
|
||||
var all []qso.QSO
|
||||
if err := a.qso.IterateAll(a.ctx, func(q qso.QSO) error {
|
||||
all = append(all, q)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return POTASyncResult{}, err
|
||||
}
|
||||
idx := map[string][]int{}
|
||||
for i := range all {
|
||||
idx[potaMatchKey(all[i].Callsign, all[i].Band)] = append(idx[potaMatchKey(all[i].Callsign, all[i].Band)], i)
|
||||
}
|
||||
|
||||
const window = 5 * time.Minute
|
||||
res := POTASyncResult{Fetched: len(entries)}
|
||||
toUpdate := map[int]struct{}{}
|
||||
for _, e := range entries {
|
||||
if e.Date.IsZero() {
|
||||
res.Unmatched++
|
||||
continue
|
||||
}
|
||||
best, bestEmpty, found := -1, false, false
|
||||
var bestDiff time.Duration
|
||||
for _, i := range idx[potaMatchKey(e.Worked, e.Band)] {
|
||||
diff := all[i].QSODate.Sub(e.Date)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff > window {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if best < 0 || diff < bestDiff {
|
||||
best, bestDiff, bestEmpty = i, diff, all[i].POTARef == ""
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case !found:
|
||||
res.Unmatched++
|
||||
case !bestEmpty:
|
||||
res.AlreadyTagged++
|
||||
default:
|
||||
all[best].POTARef = e.Reference // also prevents re-using this QSO
|
||||
toUpdate[best] = struct{}{}
|
||||
res.Updated++
|
||||
}
|
||||
}
|
||||
for i := range toUpdate {
|
||||
_ = a.qso.Update(a.ctx, all[i])
|
||||
}
|
||||
applog.Printf("pota: hunter-log sync — %d fetched, %d updated, %d already, %d unmatched",
|
||||
res.Fetched, res.Updated, res.AlreadyTagged, res.Unmatched)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// potaMatchKey indexes a QSO by base callsign + band for hunter-log matching.
|
||||
func potaMatchKey(call, band string) string {
|
||||
return pota.BaseCall(call) + "|" + strings.ToLower(strings.TrimSpace(band))
|
||||
}
|
||||
|
||||
// AwardStatRow is one row of the award statistics matrix (e.g. "CONFIRMED CW"):
|
||||
// distinct-reference counts per band, plus Total (distinct on any band) and
|
||||
// GrandTotal (sum of the per-band band-slots).
|
||||
|
||||
Reference in New Issue
Block a user