fix(qsl): separate the QSL manager from the routing method (#16)
QSL_VIA is the manager. QSL_SENT_VIA and QSL_RCVD_VIA are the ADIF "QSL Via"
enumeration — B bureau, D direct, E electronic, M manager (import-only) —
and say how a card travelled. OpsLog had one column for all three:
- the import folded QSL_SENT_VIA into QSL_VIA whenever QSL_VIA was empty,
which is exactly a Log4OM export (it defaults QSL_SENT_VIA to E), so
OE6CLD saw "E" everywhere OpsLog shows the manager;
- QSL_RCVD_VIA was listed in adifPromoted with no column behind it, so it
was not stored, not kept among the extras, and not exported — dropped
outright on import;
- neither was ever written on export, so an import followed by an export
destroyed both;
- and OpsLog polluted the field itself: the QSL Manager panel wrote
"Bureau" / "Direct" / "Electronic", in full words, into QSL_VIA.
Two columns added (migration 0027), carried through the five places a
promoted ADIF field has to touch, with round-trip tests pinning the reported
case. The QSL panel now offers Bureau / Direct / Electronic for each
direction and stores the enumeration; the manager field is labelled as the
manager and holds only that. M is kept when a file gives it and never
written back out.
Existing logs hold a mixture of the two in one column. The repair is offered,
not performed: the count is shown once per log with a plain question, and a
"no" is remembered. It moves only where QSL_SENT_VIA is still empty, and only
values that normalise to the enumeration — a manager is a callsign and can
never be one of those six words, which a test pins against real manager calls.
This commit is contained in:
@@ -6223,8 +6223,12 @@ func (a *App) BulkUpdateQSL(ids []int64, u QSLBulkUpdate) (int, error) {
|
||||
if v := strings.TrimSpace(u.RcvdDate); v != "" {
|
||||
q.QSLRcvdDate, changed = v, true
|
||||
}
|
||||
if v := strings.TrimSpace(u.Via); v != "" {
|
||||
q.QSLVia, changed = v, true
|
||||
// The QSL Manager panel's "Via" is a routing method (bureau / direct /
|
||||
// electronic), so it belongs in QSL_SENT_VIA, not in QSL_VIA — which
|
||||
// holds the manager. It used to write the manager field, in full words,
|
||||
// which is half of why the two got mixed up in operators' logs.
|
||||
if v := adif.NormaliseQSLVia(u.Via); v != "" {
|
||||
q.QSLSentVia, changed = v, true
|
||||
}
|
||||
if v := strings.TrimSpace(u.Notes); v != "" {
|
||||
q.Notes, changed = v, true
|
||||
@@ -6255,6 +6259,8 @@ var bulkFieldColumns = map[string]string{
|
||||
"qsl_sent": "qsl_sent",
|
||||
"qsl_rcvd": "qsl_rcvd",
|
||||
"qsl_via": "qsl_via",
|
||||
"qsl_sent_via": "qsl_sent_via",
|
||||
"qsl_rcvd_via": "qsl_rcvd_via",
|
||||
"qrz_sent": "qrzcom_qso_upload_status",
|
||||
"qrz_rcvd": "qrzcom_qso_download_status",
|
||||
"clublog_sent": "clublog_qso_upload_status",
|
||||
@@ -10544,6 +10550,84 @@ func (a *App) DownloadULSCounties() error {
|
||||
}
|
||||
|
||||
// BackfillUSCountiesResult summarises a bulk county/grid backfill over the log.
|
||||
// ── QSL routing repair (QSL_VIA vs QSL_SENT_VIA) ───────────────────────
|
||||
|
||||
// keyQSLViaRepairDone records that the operator has answered the offer to move
|
||||
// routing words out of the manager field — whether they accepted or declined.
|
||||
// Asked once per log, never again: a repair that keeps proposing itself after a
|
||||
// "no" is a nag, and the answer does not change.
|
||||
const keyQSLViaRepairDone = "migr.qsl_via_routing.v1"
|
||||
|
||||
// QSLViaRepairStatus counts the QSOs whose QSL_VIA holds a routing method
|
||||
// rather than a manager, so the operator can be shown a number before anything
|
||||
// is touched.
|
||||
type QSLViaRepairStatus struct {
|
||||
Affected int `json:"affected"` // QSOs holding a routing word in qsl_via
|
||||
Asked bool `json:"asked"` // the offer has already been answered
|
||||
}
|
||||
|
||||
// QSLViaRepairStatus reports whether this log needs the QSL_VIA repair.
|
||||
//
|
||||
// OpsLog wrote "Bureau" / "Direct" / "Electronic" into QSL_VIA from its own QSL
|
||||
// Manager panel, and ADIF imports folded QSL_SENT_VIA there as well, so the
|
||||
// manager column in an existing log is a mixture of the two. Moving them is
|
||||
// safe — a manager is a callsign and can never be one of those words — but it
|
||||
// rewrites what an operator sees in their own log, so it is counted and offered
|
||||
// rather than done.
|
||||
func (a *App) QSLViaRepairStatus() (QSLViaRepairStatus, error) {
|
||||
var res QSLViaRepairStatus
|
||||
if a.qso == nil {
|
||||
return res, fmt.Errorf("db not initialized")
|
||||
}
|
||||
if a.settings != nil {
|
||||
if v, _ := a.settings.GetGlobal(a.ctx, keyQSLViaRepairDone); v == "1" {
|
||||
res.Asked = true
|
||||
}
|
||||
}
|
||||
n, err := a.qso.CountQSLViaRouting(a.ctx, adif.IsQSLViaRouting)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Affected = n
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// QSLViaRepairResult reports what the repair moved.
|
||||
type QSLViaRepairResult struct {
|
||||
Moved int `json:"moved"`
|
||||
}
|
||||
|
||||
// RepairQSLVia moves routing words out of QSL_VIA into QSL_SENT_VIA.
|
||||
//
|
||||
// Only where QSL_SENT_VIA is still empty: a QSO that already carries a real
|
||||
// sent-via — from an import made after this was fixed — knows better than a
|
||||
// word left in the manager column, and must not be overwritten by it.
|
||||
func (a *App) RepairQSLVia() (QSLViaRepairResult, error) {
|
||||
var res QSLViaRepairResult
|
||||
if a.qso == nil {
|
||||
return res, fmt.Errorf("db not initialized")
|
||||
}
|
||||
n, err := a.qso.RepairQSLViaRouting(a.ctx, adif.NormaliseQSLVia)
|
||||
res.Moved = n
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
a.markQSLViaRepairAsked()
|
||||
applog.Printf("qsl via repair: moved %d routing values out of the manager field", n)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// DismissQSLViaRepair records a "no" so the offer is not made again.
|
||||
func (a *App) DismissQSLViaRepair() {
|
||||
a.markQSLViaRepairAsked()
|
||||
}
|
||||
|
||||
func (a *App) markQSLViaRepairAsked() {
|
||||
if a.settings != nil {
|
||||
_ = a.settings.SetGlobal(a.ctx, keyQSLViaRepairDone, "1")
|
||||
}
|
||||
}
|
||||
|
||||
// keyDistanceBackfilled marks the one-time distance fill as done.
|
||||
//
|
||||
// A migration, not a setting. It was briefly a button in Preferences, which was
|
||||
|
||||
Reference in New Issue
Block a user