controller v0.264.0: the household is told when an update is undone or held, in its language
gates / gates (push) Successful in 25s

app_update_undone / app_update_held events (09 decision 15), on by
default and seeded once on existing boxes; R-606 update sentences as
key+args rendered per reader; R-646 startup applied-meta backfill for
apps current with the catalog; R-620 a disabled notifier WARNs once per
event type. Needs hub v0.120.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-23 13:51:21 +02:00
parent c3a2aba0d2
commit bc278944a3
27 changed files with 1086 additions and 86 deletions
+31
View File
@@ -75,6 +75,9 @@ type Settings struct {
// existing customer's stored prefs. Persisted so a later opt-out sticks (the 3a-fix getter append
// re-enabled it on every read — this replaces it).
OffboxEnlargeNoticeSeeded bool `json:"offbox_enlarge_notice_seeded,omitempty"`
// AppUpdateEventsSeeded (v0.264.0) guards the ONE-TIME add of app_update_undone / app_update_held
// into an existing household's stored prefs (seedAppUpdateEvents).
AppUpdateEventsSeeded bool `json:"app_update_events_seeded,omitempty"`
// HubEscrowIdentityPresent (v0.199.0, R-204 item 4 / R-193) caches the report ACK's
// `escrow.identity_blob_present` — whether the HUB is holding a sealed recovery package for this
@@ -604,6 +607,10 @@ var DefaultEnabledEvents = []string{
"expected_backup_missed",
"expected_dbdump_missed",
"offbox_enlarge_blocked", // 3a-fix (warning-class): remote enlargement refused by the quota gate
// v0.264.0 (`09` §3 decision 15): the household is told ONCE when an update is undone, and when it
// (or its undo) failed and the app is held. On by default; seeded into existing prefs once below.
"app_update_undone",
"app_update_held",
}
// PendingEvent is an event queued for the next Hub push cycle.
@@ -697,6 +704,7 @@ func Load(path string, logger *log.Logger) (*Settings, error) {
}
s.migrateResticToRsync()
s.seedOffboxEnlargeNotice()
s.seedAppUpdateEvents()
return s, nil
}
@@ -718,6 +726,29 @@ func (s *Settings) seedOffboxEnlargeNotice() {
}
}
// seedAppUpdateEvents runs ONCE (guarded by AppUpdateEventsSeeded, v0.264.0) — the
// seedOffboxEnlargeNotice shape. A household whose stored prefs predate app_update_undone /
// app_update_held gets both appended: they could not have switched off a type that did not exist.
// ADD-ONLY, never removes a choice; persisted, so a LATER opt-out sticks. The hub runs the same one-time
// add on its side — BOTH are needed, because this list is pushed to the hub on every save of the
// notification page and would otherwise take the types away again.
func (s *Settings) seedAppUpdateEvents() {
if s.AppUpdateEventsSeeded {
return
}
s.AppUpdateEventsSeeded = true
if s.Notifications != nil && s.Notifications.EnabledEvents != nil {
s.Notifications.EnabledEvents = appendIfAbsent(s.Notifications.EnabledEvents, "app_update_undone")
s.Notifications.EnabledEvents = appendIfAbsent(s.Notifications.EnabledEvents, "app_update_held")
if s.log != nil {
s.log.Printf("[INFO] [settings] app update events added to the household's notification prefs (one-time, add-only)")
}
}
if err := s.save(); err != nil && s.log != nil {
s.log.Printf("[ERROR] [settings] Failed to save the app-update-events seed: %v", err)
}
}
// migrateResticToRsync converts any cross-drive backup configs using restic to rsync.
// Called once during Load() before the mutex is exposed.
func (s *Settings) migrateResticToRsync() {