feat(appearance): colour log rows by QSL status, and fix UDP QSO numbering

New Settings → Appearance section: whole-row colouring in the log grid driven
by QSL / LoTW state, each rule with its own colour from a palette or a free
picker. What Logger32 does, with one difference that matters — the colour is
applied as a 24% tint, not a fill. Logger32's grid is white; this one is dark,
and a saturated user-picked colour behind white text is unreadable at exactly
the moment the operator is scanning for what still needs sending.

Rules are ORDERED and the first match wins, because a contact is usually
several of these at once: one confirmed on LoTW and by card is confirmed, not
"sent, awaiting reply". The order lives in the data so the panel can show the
rules in the order they actually apply, numbered.

The colour is interpolated into a CSS color-mix(), so anything that is not
plainly #rrggbb is refused on the way in and falls back to the default.

Also fixes the QSO number column, which was empty for contacts logged from
WSJT-X: that path inserts through the repo directly and never reached AddQSO.
Rather than chase each of the remaining bulk-insert paths — the POTA hunter
import and the LoTW/QRZ "add what I was missing" passes, which insert OLD
dates and so shift every number after them — the index now checks its length
against a COUNT and rebuilds when they disagree. One indexed count beats
remembering to invalidate in a place that does not exist yet.
This commit is contained in:
2026-08-13 01:06:21 +02:00
parent 2484cd2515
commit 858c04d267
13 changed files with 378 additions and 7 deletions
+89
View File
@@ -0,0 +1,89 @@
package main
// Row colouring for the log grid, by QSL / LoTW status — the thing Logger32 does
// and the reason an operator can tell at a glance what still needs sending.
//
// Rules are ORDERED and the first match wins, because a contact is usually
// several things at once: one confirmed on LoTW and by card is confirmed, not
// "sent, awaiting reply". Putting the order in the data rather than in a chain
// of ifs is what lets the settings panel show it in the same order it applies.
import (
"encoding/json"
"regexp"
"strings"
)
// RowColorRule is one status and the colour it paints.
type RowColorRule struct {
ID string `json:"id"`
Color string `json:"color"`
Enabled bool `json:"enabled"`
}
// RowColorSettings is the whole appearance block.
type RowColorSettings struct {
Enabled bool `json:"enabled"`
Rules []RowColorRule `json:"rules"`
}
// The rule ids, in priority order. The frontend matches on these and holds the
// labels, so a translated name never has to travel through the settings.
var rowColorOrder = []string{
"confirmed_lotw", // LoTW confirmation received
"confirmed_paper", // card or eQSL received
"sent_waiting", // sent by some route, nothing back yet
"to_send", // a card is requested / queued and has not gone out
}
// Defaults: green for done, amber for waiting, blue for owed. Deliberately
// muted — they are composited at low opacity over a dark grid, and a saturated
// value there reads as an error state rather than a status.
var rowColorDefaults = map[string]string{
"confirmed_lotw": "#16a34a",
"confirmed_paper": "#0ea5e9",
"sent_waiting": "#f59e0b",
"to_send": "#a855f7",
}
// hexColor guards what reaches the stylesheet. The value is interpolated into a
// CSS color-mix() by the grid, so anything that is not plainly a hex colour is
// refused rather than passed through.
var hexColor = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
func normRowColors(s RowColorSettings) RowColorSettings {
byID := map[string]RowColorRule{}
for _, r := range s.Rules {
byID[r.ID] = r
}
out := RowColorSettings{Enabled: s.Enabled}
for _, id := range rowColorOrder {
r := byID[id]
r.ID = id
if !hexColor.MatchString(strings.TrimSpace(r.Color)) {
r.Color = rowColorDefaults[id]
}
out.Rules = append(out.Rules, r)
}
return out
}
// GetRowColors returns the row-colouring configuration, defaults included so the
// panel never has to invent one.
func (a *App) GetRowColors() RowColorSettings {
var s RowColorSettings
if raw := a.settingOr(keyRowColors, ""); raw != "" {
_ = json.Unmarshal([]byte(raw), &s)
}
return normRowColors(s)
}
// SaveRowColors persists it.
func (a *App) SaveRowColors(s RowColorSettings) error {
b, err := json.Marshal(normRowColors(s))
if err != nil {
return err
}
a.setSetting(keyRowColors, string(b))
return nil
}