feat: While importing ADIF, update MY fields

This commit is contained in:
2026-06-20 15:48:21 +02:00
parent e1b3f0faf3
commit 95d37da3bb
11 changed files with 647 additions and 79 deletions
+81 -18
View File
@@ -25,6 +25,7 @@ import (
"hamlog/internal/backup"
"hamlog/internal/cat"
"hamlog/internal/clublog"
"hamlog/internal/cwdecode"
"hamlog/internal/cluster"
"hamlog/internal/db"
"hamlog/internal/dxcc"
@@ -378,8 +379,10 @@ type App struct {
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
cwMu sync.Mutex // guards the CW decoder lifecycle
cwStop chan struct{} // stops the CW decoder capture loop; nil when off
cwMu sync.Mutex // guards the CW decoder lifecycle
cwStop chan struct{} // stops the CW decoder capture loop; nil when off
cwDecoder *cwdecode.Decoder // live decoder (for retargeting the pitch)
cwPitchHz int // manual pitch override (0 = auto / follow Flex)
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
pttMu sync.Mutex
@@ -1481,7 +1484,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
}
}
}()
a.applyStationDefaults(&q)
a.applyStationDefaults(&q, true)
a.applyDXCCNumber(&q)
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
@@ -1604,7 +1607,7 @@ func (a *App) refineDistrictZones(q *qso.QSO) {
// currently-active profile's values. Multi-profile support means a user
// can be /P with a different callsign + grid + SOTA ref than home — the
// QSO carries whichever profile was selected at log time.
func (a *App) applyStationDefaults(q *qso.QSO) {
func (a *App) applyStationDefaults(q *qso.QSO, includeIdentity bool) {
if a.profiles == nil {
return
}
@@ -1612,15 +1615,17 @@ func (a *App) applyStationDefaults(q *qso.QSO) {
if err != nil {
return
}
if q.StationCallsign == "" {
// STATION_CALLSIGN drives upload routing, so only stamp it on NEW QSOs — on
// import backfill, stamping the active call onto a QSO that lacked one could
// misroute it in a mixed-call log.
if includeIdentity && q.StationCallsign == "" {
q.StationCallsign = p.Callsign
}
// OPERATOR and OWNER_CALLSIGN are descriptive (not used for routing), so fill
// them whenever empty — including on import.
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{}
@@ -3477,17 +3482,19 @@ func (a *App) OpenADIFFile() (string, error) {
// cty.dat for every record, overriding what the file carries — corrects the
// wrong COUNTRY that contest software often exports (e.g. RG2Y as Asiatic
// Russia). Everything else in the ADIF is still preserved verbatim.
func (a *App) ImportADIF(path string, dupMode string, applyCty bool) (adif.ImportResult, error) {
func (a *App) ImportADIF(path string, dupMode string, applyCty bool, applyStation bool) (adif.ImportResult, error) {
if a.qso == nil {
return adif.ImportResult{}, fmt.Errorf("db not initialized")
}
if path == "" {
return adif.ImportResult{}, fmt.Errorf("empty path")
}
// Import preserves the ADIF verbatim — NO station / confirmation defaults
// are applied. Defaults are for NEW QSOs only (manual entry + UDP auto-log);
// stamping them on a historical import would, e.g., flag old QSOs as
// "LoTW requested" and try to re-upload them.
// Import preserves the ADIF verbatim by default — confirmation/sent-status
// defaults are NEVER applied (they'd flag old QSOs "LoTW requested" and try to
// re-upload). When applyStation is on, we DO backfill empty MY_* station
// fields (grid/rig/antenna/QTH/address…) from the active profile — those are
// descriptive metadata and safe to fill (identity fields are still left
// alone, see applyStationDefaults).
im := &adif.Importer{Repo: a.qso}
switch dupMode {
case "update":
@@ -3508,11 +3515,18 @@ func (a *App) ImportADIF(path string, dupMode string, applyCty bool) (adif.Impor
_ = a.clublog.EnsureLoaded()
}
clLoaded := a.clublog != nil && a.clublog.Loaded()
if applyCty {
if applyCty || applyStation {
im.Enrich = func(q *qso.QSO) {
a.enrichContactedFromCtyForce(q)
if clLoaded {
a.applyClublogException(q, true) // force: explicit import-time correction
if applyCty {
a.enrichContactedFromCtyForce(q)
if clLoaded {
a.applyClublogException(q, true) // force: explicit import-time correction
}
}
if applyStation {
// Backfill empty MY_* descriptive fields from the active profile
// (identity fields left alone to keep mixed-call routing intact).
a.applyStationDefaults(q, false)
}
}
}
@@ -6328,7 +6342,7 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
// (station callsign, grid, country, zones, and the profile's default
// MY_RIG / MY_ANTENNA) from the active profile. Without this a UDP /
// WSJT-X auto-logged QSO carried none of the operator's own data.
a.applyStationDefaults(&q)
a.applyStationDefaults(&q, true)
// ── DXCC# + QSL defaults ──
// applyDXCCNumber stamps the contacted-station DXCC# from the
@@ -6910,6 +6924,55 @@ func (a *App) FlexSetANFLevel(l int) error {
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetANFLevel(l) })
}
func (a *App) FlexSetAPF(on bool) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetAPF(on) })
}
func (a *App) FlexSetAPFLevel(l int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetAPFLevel(l) })
}
func (a *App) FlexSetCWSpeed(wpm int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWSpeed(wpm) })
}
func (a *App) FlexSetCWPitch(hz int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWPitch(hz) })
}
func (a *App) FlexSetCWBreakInDelay(ms int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWBreakInDelay(ms) })
}
func (a *App) FlexSetCWSidetone(on bool) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWSidetone(on) })
}
func (a *App) FlexSetSidetoneLevel(l int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetSidetoneLevel(l) })
}
func (a *App) FlexSetCWFilter(bw int) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWFilter(bw) })
}
// SwitchCATRig hot-swaps the active OmniRig slot (Rig1 ↔ Rig2) without
// requiring a trip through the full Settings panel. Persists the choice
// so it survives restart.