awards
This commit is contained in:
@@ -21,7 +21,9 @@ import (
|
||||
"hamlog/internal/audio"
|
||||
"hamlog/internal/cat"
|
||||
"hamlog/internal/clublog"
|
||||
"hamlog/internal/award"
|
||||
"hamlog/internal/cluster"
|
||||
"hamlog/internal/pota"
|
||||
"hamlog/internal/db"
|
||||
"hamlog/internal/email"
|
||||
"hamlog/internal/extsvc"
|
||||
@@ -89,6 +91,8 @@ const (
|
||||
keyAudioFromGain = "audio.from_gain" // From Radio (RX) mix level, percent
|
||||
keyAudioMicGain = "audio.mic_gain" // mic mix level, percent
|
||||
|
||||
keyAwardDefs = "awards.defs" // JSON array of award definitions (editable)
|
||||
|
||||
keyClublogCtyEnabled = "clublog.cty_exceptions" // "1" → apply ClubLog exceptions
|
||||
|
||||
// E-mail / SMTP — send QSO recordings to the correspondent.
|
||||
@@ -324,6 +328,7 @@ type App struct {
|
||||
cat *cat.Manager
|
||||
dxcc *dxcc.Manager
|
||||
cluster *cluster.Manager
|
||||
pota *pota.Cache
|
||||
operating *operating.Repo
|
||||
udp *udp.Manager
|
||||
udpRepo *udp.Repo
|
||||
@@ -561,6 +566,11 @@ func (a *App) startup(ctx context.Context) {
|
||||
})
|
||||
a.reloadCAT()
|
||||
|
||||
// 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...) })
|
||||
go a.pota.Run(a.ctx)
|
||||
|
||||
// DX Cluster (multi-server): the spot callback enriches each spot
|
||||
// with country + continent via cty.dat BEFORE emitting it, so the UI
|
||||
// renders the row with all metadata already filled (no flicker of
|
||||
@@ -581,6 +591,13 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// POTA: tag the spot when the DX station is currently activating a park.
|
||||
if a.pota != nil {
|
||||
if info, ok := a.pota.Lookup(s.DXCall); ok {
|
||||
s.POTARef = info.Reference
|
||||
s.POTAName = info.ParkName
|
||||
}
|
||||
}
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "cluster:spot", s)
|
||||
}
|
||||
@@ -1155,6 +1172,17 @@ func (a *App) applyStationDefaults(q *qso.QSO) {
|
||||
if q.Operator == "" {
|
||||
q.Operator = p.Operator
|
||||
}
|
||||
// OWNER_CALLSIGN is a valid ADIF field but not a promoted column, so it
|
||||
// lives in Extras (exported verbatim, round-trips, and is filterable via
|
||||
// json_extract). Stamp it from the active profile when set.
|
||||
if strings.TrimSpace(p.OwnerCallsign) != "" {
|
||||
if q.Extras == nil {
|
||||
q.Extras = map[string]string{}
|
||||
}
|
||||
if q.Extras["OWNER_CALLSIGN"] == "" {
|
||||
q.Extras["OWNER_CALLSIGN"] = p.OwnerCallsign
|
||||
}
|
||||
}
|
||||
if q.MyGrid == "" {
|
||||
q.MyGrid = p.MyGrid
|
||||
}
|
||||
@@ -1275,6 +1303,115 @@ func (a *App) CountQSO() (int64, error) {
|
||||
return a.qso.Count(a.ctx)
|
||||
}
|
||||
|
||||
// awardDefs returns the user's stored award definitions, seeding the built-in
|
||||
// defaults on first use.
|
||||
func (a *App) awardDefs() []award.Def {
|
||||
if a.settings != nil {
|
||||
if s, _ := a.settings.Get(a.ctx, keyAwardDefs); strings.TrimSpace(s) != "" {
|
||||
var defs []award.Def
|
||||
if json.Unmarshal([]byte(s), &defs) == nil && len(defs) > 0 {
|
||||
return defs
|
||||
}
|
||||
}
|
||||
}
|
||||
return award.Defaults()
|
||||
}
|
||||
|
||||
// GetAwardDefs returns the (editable) award definitions.
|
||||
func (a *App) GetAwardDefs() []award.Def { return a.awardDefs() }
|
||||
|
||||
// AwardFields lists the scannable QSO fields for the award editor.
|
||||
func (a *App) AwardFields() []string { return award.Fields() }
|
||||
|
||||
// SaveAwardDefs persists edited award definitions.
|
||||
func (a *App) SaveAwardDefs(defs []award.Def) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
b, err := json.Marshal(defs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyAwardDefs, string(b))
|
||||
}
|
||||
|
||||
// ResetAwardDefs restores the built-in defaults.
|
||||
func (a *App) ResetAwardDefs() ([]award.Def, error) {
|
||||
d := award.Defaults()
|
||||
if err := a.SaveAwardDefs(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// GetAwards computes award progress (worked/confirmed) across the whole log.
|
||||
func (a *App) GetAwards() ([]award.Result, error) {
|
||||
if a.qso == nil {
|
||||
return nil, fmt.Errorf("db not initialized")
|
||||
}
|
||||
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 nil, err
|
||||
}
|
||||
nameOf := func(field, ref string) string {
|
||||
switch field {
|
||||
case "dxcc":
|
||||
if n, err := strconv.Atoi(ref); err == nil {
|
||||
return dxcc.NameForDXCC(n)
|
||||
}
|
||||
case "cont":
|
||||
return continentName(ref)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return award.Compute(a.awardDefs(), all, nameOf), nil
|
||||
}
|
||||
|
||||
func continentName(code string) string {
|
||||
switch strings.ToUpper(code) {
|
||||
case "AF":
|
||||
return "Africa"
|
||||
case "AN":
|
||||
return "Antarctica"
|
||||
case "AS":
|
||||
return "Asia"
|
||||
case "EU":
|
||||
return "Europe"
|
||||
case "NA":
|
||||
return "North America"
|
||||
case "OC":
|
||||
return "Oceania"
|
||||
case "SA":
|
||||
return "South America"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ListQSOFiltered returns QSOs matching the advanced filter builder.
|
||||
func (a *App) ListQSOFiltered(f qso.QueryFilter) ([]qso.QSO, error) {
|
||||
if a.qso == nil {
|
||||
return nil, fmt.Errorf("db not initialized")
|
||||
}
|
||||
return a.qso.ListFiltered(a.ctx, f)
|
||||
}
|
||||
|
||||
// CountQSOFiltered returns how many QSOs match the filter (ignoring the row
|
||||
// limit) so the UI can show "showing 500 of 1,234 matches".
|
||||
func (a *App) CountQSOFiltered(f qso.QueryFilter) (int64, error) {
|
||||
if a.qso == nil {
|
||||
return 0, fmt.Errorf("db not initialized")
|
||||
}
|
||||
return a.qso.CountFiltered(a.ctx, f)
|
||||
}
|
||||
|
||||
// FilterFields exposes the whitelisted filterable columns to the frontend.
|
||||
func (a *App) FilterFields() []string {
|
||||
return qso.FilterableFields()
|
||||
}
|
||||
|
||||
func (a *App) GetQSO(id int64) (qso.QSO, error) {
|
||||
if a.qso == nil {
|
||||
return qso.QSO{}, fmt.Errorf("db not initialized")
|
||||
@@ -1444,6 +1581,35 @@ func (a *App) ExportADIF(path string, includeAppFields bool) (adif.ExportResult,
|
||||
return ex.ExportFile(a.ctx, path)
|
||||
}
|
||||
|
||||
// ExportADIFFiltered writes the QSOs matching the current filter to path, with
|
||||
// NO row limit (the on-screen list is capped by the threshold; this is not).
|
||||
func (a *App) ExportADIFFiltered(path string, includeAppFields bool, f qso.QueryFilter) (adif.ExportResult, error) {
|
||||
if a.qso == nil {
|
||||
return adif.ExportResult{}, fmt.Errorf("db not initialized")
|
||||
}
|
||||
if path == "" {
|
||||
return adif.ExportResult{}, fmt.Errorf("empty path")
|
||||
}
|
||||
ex := &adif.Exporter{Repo: a.qso, AppName: "OpsLog", AppVersion: "0.1", IncludeAppFields: includeAppFields}
|
||||
return ex.ExportFileFiltered(a.ctx, path, f)
|
||||
}
|
||||
|
||||
// ExportADIFSelected writes only the QSOs whose ids are given (the rows the
|
||||
// operator highlighted in the grid).
|
||||
func (a *App) ExportADIFSelected(path string, includeAppFields bool, ids []int64) (adif.ExportResult, error) {
|
||||
if a.qso == nil {
|
||||
return adif.ExportResult{}, fmt.Errorf("db not initialized")
|
||||
}
|
||||
if path == "" {
|
||||
return adif.ExportResult{}, fmt.Errorf("empty path")
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return adif.ExportResult{}, fmt.Errorf("no QSOs selected")
|
||||
}
|
||||
ex := &adif.Exporter{Repo: a.qso, AppName: "OpsLog", AppVersion: "0.1", IncludeAppFields: includeAppFields}
|
||||
return ex.ExportFileByIDs(a.ctx, path, ids)
|
||||
}
|
||||
|
||||
// --- Lookup bindings ---
|
||||
|
||||
// LookupCallsign returns the cached or freshly-fetched info for a callsign.
|
||||
|
||||
Reference in New Issue
Block a user