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
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:
@@ -839,6 +839,14 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
if found.Deployed && found.AppConfig != nil && found.AppConfig.LastUpdateUndone != nil {
|
||||
data["UpdateUndoneLine"] = s.msg(r, "app_info.update_undone", found.Name, undoneWhen(found.AppConfig.LastUpdateUndone.At))
|
||||
}
|
||||
// v0.271.0 (`09` §6.4 part 7): a SUCCESSFUL automatic update is told on the page only — no mail.
|
||||
// An undone or held automatic step keeps the lines above (they already say it), so this line is for
|
||||
// `done` alone, and only while the app still runs what that step installed (a later manual update or
|
||||
// a restore makes it history, and the page stops saying it).
|
||||
if found.Deployed && found.AppConfig != nil && found.AppConfig.LastAutoUpdate != nil &&
|
||||
found.AppConfig.LastAutoUpdate.Outcome == stacks.LegOutcomeDone && sameImageMap(found.AppConfig.LastAutoUpdate.To, found.AppConfig.PinnedImages) {
|
||||
data["AutoUpdateLine"] = s.msg(r, "app_info.auto_update_done", undoneWhen(found.AppConfig.LastAutoUpdate.At))
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, "app_info", data)
|
||||
}
|
||||
@@ -2144,6 +2152,9 @@ func (s *Server) systemPageData() map[string]interface{} {
|
||||
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
|
||||
data["HubEnabled"] = s.cfg.Hub.Enabled
|
||||
|
||||
// v0.271.0: the automatic app-update switch (absent = ON).
|
||||
data["AppUpdateUnattended"] = s.settings == nil || s.settings.GetAppUpdateUnattended()
|
||||
|
||||
// Self-update status
|
||||
data["SelfUpdateEnabled"] = s.cfg.SelfUpdate.Enabled
|
||||
if s.updater != nil {
|
||||
@@ -2824,6 +2835,37 @@ func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
|
||||
// settingsAppUpdateHandler saves the per-box switch for AUTOMATIC app updates (v0.271.0, `09` §3
|
||||
// decision 12). An unchecked box posts nothing, so absence of the field means OFF here — this is the
|
||||
// household's explicit save, unlike the stored default (absent = ON). The leg reads the switch at its
|
||||
// next start and before every press, so the choice takes effect before the next night.
|
||||
func (s *Server) settingsAppUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
on := r.FormValue("app_update_unattended") == "on" || r.FormValue("app_update_unattended") == "true"
|
||||
if s.settings == nil {
|
||||
data := s.systemPageData()
|
||||
data["AppUpdateError"] = s.msg(r, "settings.app_update_save_error")
|
||||
s.executeTemplate(w, r, "settings_system", data)
|
||||
return
|
||||
}
|
||||
if err := s.settings.SetAppUpdateUnattended(on); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] saving app_update.unattended=%v failed: %v", on, err)
|
||||
data := s.systemPageData()
|
||||
data["AppUpdateError"] = s.msg(r, "settings.app_update_save_error")
|
||||
s.executeTemplate(w, r, "settings_system", data)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] automatic app updates switched %s by the household (app_update.unattended=%v)", map[bool]string{true: "ON", false: "OFF"}[on], on)
|
||||
s.reportTriggerNow()
|
||||
data := s.systemPageData()
|
||||
if on {
|
||||
data["AppUpdateSuccess"] = s.msg(r, "settings.app_update_on")
|
||||
} else {
|
||||
data["AppUpdateSuccess"] = s.msg(r, "settings.app_update_off")
|
||||
}
|
||||
s.executeTemplate(w, r, "settings_system", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.notificationsPageData()
|
||||
|
||||
@@ -3539,6 +3581,19 @@ func generateFileBrowserConfig(paths []settings.StoragePath, importSource bool)
|
||||
|
||||
// undoneWhen renders last_update_undone.at for the page line, in the box's time zone; the raw value
|
||||
// when it does not parse (a record is never hidden for its format).
|
||||
// sameImageMap reports whether two service→image maps are equal (nil equals empty).
|
||||
func sameImageMap(a, b map[string]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, v := range a {
|
||||
if b[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func undoneWhen(rfc3339 string) string {
|
||||
t, err := time.Parse(time.RFC3339, rfc3339)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user