This commit is contained in:
2026-06-06 01:21:24 +02:00
parent 922a185208
commit b4e104f5a2
9 changed files with 381 additions and 42 deletions
+70 -10
View File
@@ -94,7 +94,8 @@ const (
keyAwardDefs = "awards.defs" // JSON array of award definitions (editable)
keyAwardRefsUpdated = "awards.refs.updated." // + CODE → last list-update timestamp
keyAwardRefsSeeded = "awards.refs.seeded" // "1" once built-in lists were seeded
keyAwardRefsSeeded = "awards.refs.seeded" // built-in reference-list seed version
keyAwardDefsFixed = "awards.defs.fixed" // built-in award def correction version
keyClublogCtyEnabled = "clublog.cty_exceptions" // "1" → apply ClubLog exceptions
@@ -1072,6 +1073,7 @@ func (a *App) AddQSO(q qso.QSO) (int64, error) {
a.applyStationDefaults(&q)
a.applyDXCCNumber(&q)
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
a.applyQSLDefaults(&q)
// Fill the contacted operator's e-mail from the (cached) lookup so the
// recording can be auto-sent. Cheap: the entry already looked the call up.
@@ -1141,6 +1143,11 @@ func (a *App) ComputeStationInfo(callsign, grid string) StationInfoComputed {
out.Lat = m.Lat
out.Lon = m.Lon
out.DXCC = dxcc.EntityDXCC(m.Entity.Name)
// Refine zones by call district (W6 → CQ3/ITU6) so the entry strip
// shows what will be logged.
if cqz, ituz, ok := dxcc.ZoneByCallDistrict(out.DXCC, callsign); ok {
out.CQZ, out.ITUZ = cqz, ituz
}
}
}
// Grid wins on lat/lon — it's user-set, finer than the DXCC centroid.
@@ -1167,6 +1174,20 @@ func (a *App) applyDXCCNumber(q *qso.QSO) {
}
}
// refineDistrictZones sets the CQ/ITU zone from the call district for
// zone-split countries (USA, Australia), so every entry point — manual,
// UDP, import, re-stamp, award scan — agrees on W6 = CQ3/ITU6 instead of the
// coarse per-entity default. Call AFTER the DXCC is finalised.
func (a *App) refineDistrictZones(q *qso.QSO) {
if q.DXCC == nil {
return
}
if cqz, ituz, ok := dxcc.ZoneByCallDistrict(*q.DXCC, q.Callsign); ok {
zc, zi := cqz, ituz
q.CQZ, q.ITUZ = &zc, &zi
}
}
// applyStationDefaults fills any empty MY_* / station field on q with the
// currently-active profile's values. Multi-profile support means a user
// can be /P with a different callsign + grid + SOTA ref than home — the
@@ -1354,12 +1375,31 @@ func (a *App) migrateAwardDefs() {
return
}
migrated, changed := award.Migrate(defs)
// Version-gated correction of the built-in awards' Validate sources, which
// an earlier version wrongly set equal to Confirm (so VALIDATED == CONFIRMED
// even for paper-QSL-only entities). Re-apply the canonical Confirm/Validate
// from Defaults to protected/built-in awards once.
const defsFixVersion = "2"
if v, _ := a.settings.Get(a.ctx, keyAwardDefsFixed); v != defsFixVersion {
byCode := map[string]award.Def{}
for _, d := range award.Defaults() {
byCode[strings.ToUpper(d.Code)] = d
}
for i := range migrated {
if d, ok := byCode[strings.ToUpper(migrated[i].Code)]; ok && (migrated[i].Builtin || migrated[i].Protected) {
migrated[i].Confirm = d.Confirm
migrated[i].Validate = d.Validate
changed = true
}
}
_ = a.settings.Set(a.ctx, keyAwardDefsFixed, defsFixVersion)
}
if !changed {
return
}
if b, err := json.Marshal(migrated); err == nil {
_ = a.settings.Set(a.ctx, keyAwardDefs, string(b))
applog.Printf("awards: migrated %d legacy definitions to the new model", len(migrated))
applog.Printf("awards: migrated/fixed %d definitions", len(migrated))
}
}
@@ -1716,6 +1756,10 @@ func (a *App) enrichQSOForAwards(q *qso.QSO) {
q.DXCC = &n
}
}
// Zone-split countries (USA, Australia): the per-entity default zone is too
// coarse (W6 = CQ5 instead of 3). Apply the call-district rule so awards
// (WAZ / WITUZ) match Log4OM. This OVERRIDES a stored entity-default zone.
a.refineDistrictZones(q)
}
// awardBandPlan maps a frequency (Hz) to its ADIF band. Used to recover the
@@ -1932,25 +1976,37 @@ func (a *App) HasBuiltinReferences(code string) bool {
return ok
}
// seedBuiltinReferences populates the reference lists of built-in awards on
// first run (idempotent: only seeds an award that currently has none, and only
// once overall, tracked by a settings flag so a user who clears a list is not
// overruled on the next launch).
// builtinRefsVersion is bumped whenever the built-in reference data changes
// (e.g. the West Malaysia 155→299 fix) so existing installs re-seed the
// derived lists. Bump this after correcting BuiltinRefs / the DXCC name table.
const builtinRefsVersion = "2"
// seedBuiltinReferences populates the reference lists of built-in awards.
// - First run (no version stored): seed only awards that have NO references
// yet, so an online-loaded list (POTA…) or a user list isn't clobbered.
// - Version bump (stored != current): RE-SEED the derived built-in lists
// (DXCC, WAZ, WAC, WAS, DDFM) to push data corrections to existing installs.
// These lists are canonical, not user-maintained, so overwriting is safe.
func (a *App) seedBuiltinReferences() {
if a.awardRefs == nil || a.settings == nil {
return
}
if done, _ := a.settings.Get(a.ctx, keyAwardRefsSeeded); done == "1" {
ver, _ := a.settings.Get(a.ctx, keyAwardRefsSeeded)
if ver == builtinRefsVersion {
return
}
firstRun := ver == "" || ver == "1" // "1" was the old boolean flag
if ver == "1" {
firstRun = false // already seeded once → treat as a version upgrade
}
counts, err := a.awardRefs.Counts(a.ctx)
if err != nil {
return
}
for _, d := range a.awardDefs() {
code := strings.ToUpper(d.Code)
if counts[code] > 0 {
continue
if firstRun && counts[code] > 0 {
continue // don't overwrite an existing list on a fresh install
}
if refs, ok := awardref.BuiltinRefs(code); ok {
if n, err := a.awardRefs.ReplaceAll(a.ctx, code, refs); err == nil {
@@ -1958,7 +2014,7 @@ func (a *App) seedBuiltinReferences() {
}
}
}
_ = a.settings.Set(a.ctx, keyAwardRefsSeeded, "1")
_ = a.settings.Set(a.ctx, keyAwardRefsSeeded, builtinRefsVersion)
}
// ImportAwardReferencesText parses pasted lines or CSV into references and
@@ -4100,6 +4156,9 @@ func (a *App) enrichContactedFromCtyForce(q *qso.QSO) bool {
v := m.ITUZone
q.ITUZ = &v
}
// Zone-split countries (USA, Australia): refine the per-entity default zone
// to the call-district zone (W6 → CQ3/ITU6), matching Log4OM/DXKeeper.
a.refineDistrictZones(q)
return true
}
@@ -4469,6 +4528,7 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
// fields (or what the lookup gave us) always win.
a.applyDXCCNumber(&q)
a.applyClublogException(&q, false) // date-ranged DXpedition override
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
a.applyQSLDefaults(&q)
// ── Dedup ──