fix: theme sometimes reverts on reopen — gate per-profile UI prefs on scope

The theme (and other per-profile UI prefs) are read via a.settings.Get, which is
scoped to the active profile. GetUIPref only guarded on a.settings==nil, so in
the startup window AFTER NewStore but BEFORE SetProfile(active) it resolved with
the wrong (empty) scope. The frontend theme self-heal treats a resolved ""  as
"answered, unset → keep default, stop retrying", so a race landed on the light
default and stayed. Add a settingsScoped atomic flag set right after
SetProfile; GetUIPref/SetUIPref return "not ready" until then, so the frontend
keeps retrying and restores the real theme. Also prevents seeding prefs into the
wrong profile scope. Changelog entry added to 0.21.0 (EN+FR).
This commit is contained in:
2026-07-24 19:14:26 +02:00
parent fd097a647f
commit 3b1a8ef01a
2 changed files with 13 additions and 7 deletions
+9 -5
View File
@@ -515,6 +515,7 @@ type App struct {
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
startupErr string // captured for surfacing to the frontend
settingsScoped atomic.Bool // true once a.settings is scoped to the active profile — GetUIPref/SetUIPref (per-profile) must wait for it, else an early call reads the wrong scope and e.g. resets the theme
dbPath string // settings/config database file (settings + profiles); may be a user-chosen location
logbookPath string // default SQLite logbook file (QSOs), next to the settings db — used when a profile doesn't point elsewhere
logDb *sql.DB // QSO logbook connection — MySQL, a per-profile SQLite file, or the default logbook.db (never the settings db, except on fallback)
@@ -848,6 +849,7 @@ func (a *App) startup(ctx context.Context) {
}
}
a.settings.SetProfile(active.ID)
a.settingsScoped.Store(true) // per-profile settings reads (GetUIPref…) are now safe
// US county resolver — its own local SQLite (data/uls.db), populated on demand
// by DownloadULSCounties. Opening (creating an empty store) is cheap and never
// fatal: county resolution simply stays inert until the operator downloads it.
@@ -2013,10 +2015,12 @@ func (a *App) groupDigitalSlots() bool {
}
func (a *App) GetUIPref(key string) (string, error) {
if a.settings == nil {
if a.settings == nil || !a.settingsScoped.Load() {
// Distinct from a genuinely-empty pref: the (LOCAL SQLite) settings store
// isn't wired yet. There's a brief window at launch where the frontend can
// call this before OnStartup has opened the DB and built the store. The UI
// isn't wired AND scoped to the active profile yet. There's a brief window at
// launch where the frontend can call this before it's ready — reading then
// returns the wrong profile's (empty) value, which stopped the theme
// self-heal early ("dark theme reverts to light on reopen"). The UI
// uses the error to keep RETRYING rather than treat it as "unset" and fall
// back to a default — the "dark theme reverts to light after an update" bug
// (the update cleared localStorage, and the DB read gave up too early while
@@ -2027,8 +2031,8 @@ func (a *App) GetUIPref(key string) (string, error) {
}
func (a *App) SetUIPref(key, value string) error {
if a.settings == nil {
return fmt.Errorf("db not initialized")
if a.settings == nil || !a.settingsScoped.Load() {
return fmt.Errorf("settings store not ready") // avoid seeding the wrong profile scope
}
return a.settings.Set(a.ctx, "ui."+key, value)
}