v0.308.0: controller batch 2026-10-12 — clear-address warning (R-922), secret sweep over every page (R-255), read_failed (R-266), no NVMe heat band (R-333), share globe (R-577), Hungarian dates and numbers (R-562), lost-drive option pinned (R-521)

Operator rulings 2026-10-12, 09 §3 decisions 196, 204-207, 223, 227. MinAgent 0.131.0 unchanged.
R-255's sweep found one leak, fixed here: an optional-config secret_input was rendered into its
settings page; it is now revealed on demand. Hungarian parity fixtures re-captured once for R-562.

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-10-12 15:13:59 +02:00
parent dcd89364a4
commit fd28d70bff
183 changed files with 2671 additions and 411 deletions
+51 -25
View File
@@ -567,7 +567,7 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
dp.NotAllowedNote = s.msg(r, "deploy.path_not_allowed")
}
if di := system.GetDiskUsage(sp.Path); di != nil {
dp.FreeHuman = formatFreeSpace(di.AvailGB)
dp.FreeHuman = formatFreeSpace(s.langFor(r), di.AvailGB)
if di.TotalGB > 0 {
dp.FreePercent = 100 - system.DFUsedPercent(di.UsedGB, di.AvailGB) // R-889: df's number
}
@@ -601,8 +601,8 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
if !alreadyDeployed {
if hr := system.GetDockerVolumeHeadroom(); hr.OK {
data["DockerBelowReserve"] = hr.BelowReserve
data["DockerFreeHuman"] = formatFreeSpace(hr.AvailGB)
data["DockerReserveHuman"] = formatFreeSpace(hr.ReserveGB)
data["DockerFreeHuman"] = formatFreeSpace(s.langFor(r), hr.AvailGB)
data["DockerReserveHuman"] = formatFreeSpace(s.langFor(r), hr.ReserveGB)
}
}
@@ -889,7 +889,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
// in the REQUEST's language (a finished sentence passed as page data must be rendered per request,
// never taken from a Hungarian store — the composed-sentence class, R-573/R-590/R-596/R-598).
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))
data["UpdateUndoneLine"] = s.msg(r, "app_info.update_undone", found.Name, undoneWhen(s.langFor(r), 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
@@ -897,7 +897,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
// 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))
data["AutoUpdateLine"] = s.msg(r, "app_info.auto_update_done", undoneWhen(s.langFor(r), found.AppConfig.LastAutoUpdate.At))
}
// R-621: a held app's own log, kept by the hold before its containers were removed, is reachable
@@ -1037,7 +1037,7 @@ func (s *Server) backupsOffboxData(data map[string]interface{}, lang string) {
}
if s.backupMgr != nil && offboxTgt != nil && offboxTgt.QuotaGB > 0 && s.backupMgr.OffboxConfigured() {
if f := s.backupMgr.OffsiteFitForPage(); !f.At.IsZero() && !f.Fits {
data["OffsiteFitTotal"] = appbackup.HumanizeBytes(f.TotalBytes)
data["OffsiteFitTotal"] = i18n.LocalizeDecimals(lang, appbackup.HumanizeBytes(f.TotalBytes))
data["OffsiteFitQuota"] = offboxTgt.QuotaGB
var top []string
for i, a := range f.Apps {
@@ -1048,7 +1048,7 @@ func (s *Server) backupsOffboxData(data map[string]interface{}, lang string) {
if st, ok := s.stackMgr.GetStack(a.Stack); ok && st.Meta.DisplayName != "" {
name = st.Meta.DisplayName
}
top = append(top, name+" ("+appbackup.HumanizeBytes(a.Bytes)+")")
top = append(top, name+" ("+i18n.LocalizeDecimals(lang, appbackup.HumanizeBytes(a.Bytes))+")")
}
data["OffsiteFitTop"] = strings.Join(top, ", ")
}
@@ -1164,7 +1164,7 @@ func (s *Server) backupsRemoteHandler(w http.ResponseWriter, r *http.Request) {
if st := s.backupMgr.AbandonStatus(); st.Active {
data["AbandonActive"] = true
data["AbandonDaysLeft"] = st.DaysLeft
data["AbandonDate"] = st.DueAt.Format("2006-01-02")
data["AbandonDate"] = i18n.FormatDate(s.langFor(r), st.DueAt)
// R-365: DaysLeft is a ceiling, so 0 means the due moment has PASSED (the terminal step waits
// for the next daily sweep, up to ~29 h). The future-tense „… napján véglegesen töröljük" then
// named a date in the past; the overdue sentence says the deletion is due and when it runs.
@@ -1685,7 +1685,7 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus, lang string
row.Tier2CopyDate, row.Tier2CopyDateProven = pkgDate, rp.CopyDateProven
row.Tier2UnitConfirm = s.tier2UnitConfirmWithStaleness(pkgDate, row.Tier2CopyDateProven, stale)
if stale && pkgDate != "" {
row.Tier2UnitStaleNotice = s.msgLang(lang, tier2UnitStaleNoticeFmtKey, fmtRFC3339Local(pkgDate))
row.Tier2UnitStaleNotice = s.msgLang(lang, tier2UnitStaleNoticeFmtKey, fmtRFC3339Local(lang, pkgDate))
}
}
switch cd.LastStatus {
@@ -1927,7 +1927,7 @@ func (s *Server) restoredVersionPrefix(app string, changed bool, pins []string,
if !climbable {
key = restoredOlderVersionNoStepKey
}
return s.note(key, app, at.In(getTimezone()).Format("2006-01-02 15:04"), backup.PinsVersion(pins)) + " "
return s.note(key, app, fmtLocalTime(s.boxLang(), at), backup.PinsVersion(pins)) + " "
}
const (
@@ -2065,9 +2065,9 @@ func (s *Server) tier2UnitConfirmWithStaleness(copyDate string, proven bool, sta
msg := s.note(tier2UnitConfirmBaseKey)
if copyDate != "" {
if proven {
msg += s.note(tier2UnitConfirmDateFmtKey, fmtRFC3339Local(copyDate))
msg += s.note(tier2UnitConfirmDateFmtKey, fmtRFC3339Local(s.boxLang(), copyDate))
} else {
msg += s.note(tier2UnitConfirmDateUnprovenFmtKey, fmtRFC3339Local(copyDate))
msg += s.note(tier2UnitConfirmDateUnprovenFmtKey, fmtRFC3339Local(s.boxLang(), copyDate))
}
}
msg += s.note(tier2UnitConfirmContrastKey)
@@ -2093,9 +2093,9 @@ func (s *Server) tier2UnitSourceMsg(cov backup.Tier2Coverage) string {
return ""
}
if proven {
return s.note(tier2UnitRestoreSourceKey, fmtRFC3339Local(date))
return s.note(tier2UnitRestoreSourceKey, fmtRFC3339Local(s.boxLang(), date))
}
return s.note(tier2UnitRestoreSourceUnprovenKey, fmtRFC3339Local(date))
return s.note(tier2UnitRestoreSourceUnprovenKey, fmtRFC3339Local(s.boxLang(), date))
}
// backupTier2RestoreHandler (C2, closes F2) restores an app's MISSING user files in place from its
@@ -2330,7 +2330,7 @@ func (s *Server) systemPageData() map[string]interface{} {
// R-724: the box's local time, not a raw RFC3339 stamp.
data["LastCheckTime"] = status.LastCheck.CheckedAt
if t, err := time.Parse(time.RFC3339, status.LastCheck.CheckedAt); err == nil {
data["LastCheckTime"] = t.In(getTimezone()).Format("2006-01-02 15:04")
data["LastCheckTime"] = fmtLocalTime(s.boxLang(), t)
}
data["LastCheckError"] = status.LastCheck.Error
}
@@ -2593,6 +2593,18 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
}
}
}
// R-255 (v0.308.0): an optional-config `secret_input` (a key the household typed in for the app, e.g. a
// metadata provider's API key) is no longer rendered into the settings page; it is revealed here on demand,
// behind the same session + CSRF as the auto-generated secrets.
if !allowed {
for _, g := range stack.Meta.OptionalConfig {
for _, f := range g.Fields {
if f.EnvVar == envVar && f.Type == "secret_input" {
allowed = true
}
}
}
}
if !allowed {
s.logger.Printf("[WARN] [web] auto-field reveal refused for %s/%s: not an auto-generated secret field", stackName, envVar)
escrowJSON(w, http.StatusForbidden, nil, s.msg(r, "escrow.field_not_revealable"))
@@ -2945,6 +2957,24 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
return
}
// R-922 warning (operator ruling 2026-10-12, S1, `09` §3 decision 196): clearing a STORED address is said
// first — „you will get no more notification mails" — and nothing is saved or sent until the household
// confirms with the page's own button (confirm_clear=1). Server-side, so it holds without JavaScript and
// for any client. A box whose address is already empty has nothing to warn about. Pinned by
// TestR922_ClearWarnsFirst.
if cur := s.settings.GetNotificationPrefs(); email == "" && cur != nil && cur.Email != "" && r.FormValue("confirm_clear") != "1" {
s.logger.Printf("[INFO] [web] Notification address clear: asking the household to confirm first (nothing saved)")
data := s.notificationsPageData()
data["NotificationPrefs"] = &settings.NotificationPrefs{
Email: email,
EnabledEvents: enabledEvents,
CooldownHours: cooldownHours,
}
data["NotificationClearConfirm"] = true
s.executeTemplate(w, r, "settings_notifications", data)
return
}
// R-922 (operator ruling 2026-10-09, option A): an empty address where one was stored is the household's
// deliberate clear — persisted, so this push AND every later one carry `email_cleared` while the address
// stays empty, and the hub deletes the address it holds. A box that never had one does not set it.
@@ -3278,7 +3308,7 @@ func (s *Server) appDetailsForPath(storagePath string) []StorageAppDetail {
}
}
if any {
detail.SizeHuman = humanizeDirBytes(total)
detail.SizeHuman = fmtSizeBytes(s.boxLang(), total)
}
details = append(details, detail)
}
@@ -3322,11 +3352,11 @@ func dirSizeHuman(path string) string {
return humanizeDirBytes(dirSizeBytesWalk(path))
}
func formatFreeSpace(gb float64) string {
func formatFreeSpace(lang string, gb float64) string {
if gb >= 1000 {
return fmt.Sprintf("%.1f TB", gb/1024)
return i18n.FormatDecimal(lang, gb/1024, 1) + " TB"
}
return fmt.Sprintf("%.1f GB", gb)
return i18n.FormatDecimal(lang, gb, 1) + " GB"
}
func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Request) {
@@ -3819,10 +3849,6 @@ func sameImageMap(a, b map[string]string) bool {
return true
}
func undoneWhen(rfc3339 string) string {
t, err := time.Parse(time.RFC3339, rfc3339)
if err != nil {
return rfc3339
}
return t.In(getTimezone()).Format("2006-01-02 15:04")
func undoneWhen(lang, rfc3339 string) string {
return fmtRFC3339Local(lang, rfc3339)
}