fix(bulk): mode and RST were offered but could not be saved

A bulk-editable field passes through THREE tables: the list the dialog shows,
bulkFieldColumns in app.go, and bulkEditableCols in internal/qso. I added mode,
submode and RST to the first and the third and missed the middle one, so the
dialog offered them and every save came back "unknown field".

Nothing warns about that. Each table is perfectly valid on its own, and the
mismatch only surfaces when an operator picks the one field that falls through
the gap — which is exactly how it was found.

So both directions are pinned now: every mapped field must be writable by the
qso layer, and every writable column must have a field mapping to it. A column
nothing maps to looks supported from the inside and cannot be reached from
outside, which is the same fault wearing the other hat.
This commit is contained in:
2026-08-11 20:39:46 +02:00
parent 1b7f8ec9c1
commit 2a6e09a1d7
3 changed files with 58 additions and 0 deletions
+6
View File
@@ -6162,6 +6162,12 @@ var bulkFieldColumns = map[string]string{
"iota": "iota", "iota": "iota",
"sig": "sig", "sig": "sig",
"sig_info": "sig_info", "sig_info": "sig_info",
// The contact itself — repair fields. Setting mode also clears submode, in
// qso.BulkSetField: a submode left over from the old mode contradicts the new.
"mode": "mode",
"submode": "submode",
"rst_sent": "rst_sent",
"rst_rcvd": "rst_rcvd",
// Misc text // Misc text
"comment": "comment", "comment": "comment",
"notes": "notes", "notes": "notes",
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"testing"
"hamlog/internal/qso"
)
// A bulk-editable field passes through THREE tables: the field list in the UI,
// bulkFieldColumns here, and bulkEditableCols in internal/qso. Mode and RST were
// added to the first and the third and not the second, so the dialog offered
// them and the save failed with "unknown field" — reported from the field.
//
// Nothing warns about that: each table is valid on its own. This is the check.
func TestEveryMappedBulkFieldIsWhitelisted(t *testing.T) {
for id, col := range bulkFieldColumns {
if !qso.BulkEditable(col) {
t.Errorf("bulk field %q maps to column %q, which internal/qso refuses to write", id, col)
}
}
}
// And the reverse: a column the qso layer allows but nothing maps to is dead
// weight — it looks supported from the inside and cannot be reached from outside.
func TestNoUnreachableBulkColumn(t *testing.T) {
mapped := map[string]bool{}
for _, col := range bulkFieldColumns {
mapped[col] = true
}
for _, col := range qso.BulkEditableColumns() {
if !mapped[col] {
t.Errorf("column %q is bulk-writable but no field maps to it — unreachable", col)
}
}
}
+17
View File
@@ -896,6 +896,23 @@ var bulkEditableExtras = map[string]string{
// BulkExtraKey maps a frontend field id to its ADIF key in extras_json, or "". // BulkExtraKey maps a frontend field id to its ADIF key in extras_json, or "".
func BulkExtraKey(field string) string { return bulkEditableExtras[field] } func BulkExtraKey(field string) string { return bulkEditableExtras[field] }
// BulkEditable reports whether a COLUMN may be bulk-written. Exported so the
// app layer can check its own field mapping against this whitelist: the two
// lists are separate, valid on their own, and a field present in one and absent
// from the other fails only when an operator tries to use it.
func BulkEditable(column string) bool { return bulkEditableCols[column] }
// BulkEditableColumns lists every bulk-writable column, for the same check from
// the other side: a column nothing maps to looks supported and cannot be used.
func BulkEditableColumns() []string {
out := make([]string, 0, len(bulkEditableCols))
for c := range bulkEditableCols {
out = append(out, c)
}
sort.Strings(out)
return out
}
// BulkSetExtra sets one whitelisted extras_json field on every listed QSO, // BulkSetExtra sets one whitelisted extras_json field on every listed QSO,
// leaving the other extras untouched. An empty value REMOVES the key rather than // leaving the other extras untouched. An empty value REMOVES the key rather than
// storing a blank — an empty extra would otherwise be carried into every export. // storing a blank — an empty extra would otherwise be carried into every export.