controller v0.271.0: automatic app updates — the update leg after the off-site copy, the backup gate waits, the switch (09 6.4 part 7; R-680, R-678, R-643)
gates / gates (push) Successful in 24s

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-24 22:07:24 +02:00
parent 1495ca43fb
commit 9cf13a3add
33 changed files with 1672 additions and 14 deletions
+36
View File
@@ -188,6 +188,12 @@ type Settings struct {
// offsets; overrides yaml when a valid value is present (mirrors PasswordHash precedence).
BackupWindowStart string `json:"backup_window_start,omitempty"`
// AppUpdate (v0.271.0, `09` §3 decision 12) — the per-box switch for AUTOMATIC app updates. The key
// is `app_update.unattended`; ABSENT MEANS ON (the operator's ruling: on by default), so a box that
// never saved the switch, and every box upgraded from before v0.271.0, updates its apps by itself.
// Only an explicit false turns it off. NOT `self_update.auto_update` — that is the controller's own.
AppUpdate *AppUpdateSettings `json:"app_update,omitempty"`
// Storage paths registry
StoragePaths []StoragePath `json:"storage_paths,omitempty"`
@@ -886,6 +892,36 @@ func (s *Settings) SetBackupWindowStart(start string) error {
return s.save()
}
// AppUpdateSettings is settings.json's `app_update` object (v0.271.0).
type AppUpdateSettings struct {
// Unattended nil = the default, ON (decision 12). Pinned by TestAppUpdateUnattended_DefaultOn.
Unattended *bool `json:"unattended,omitempty"`
}
// GetAppUpdateUnattended reports whether this box may update its apps by itself (decision 12).
// Absent = ON.
func (s *Settings) GetAppUpdateUnattended() bool {
s.mu.RLock()
defer s.mu.RUnlock()
if s.AppUpdate == nil || s.AppUpdate.Unattended == nil {
return true
}
return *s.AppUpdate.Unattended
}
// SetAppUpdateUnattended stores the household's choice and saves. It takes effect at the leg's next
// start (and before its next step, if a leg is running) — the leg reads it each time.
func (s *Settings) SetAppUpdateUnattended(on bool) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.AppUpdate == nil {
s.AppUpdate = &AppUpdateSettings{}
}
v := on
s.AppUpdate.Unattended = &v
return s.save()
}
// SetConfigLanguage records the language controller.yaml says this box should start in.
//
// Called once at startup and again after each config pull. It never touches s.Language: a