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
+105 -77
View File
@@ -11,6 +11,7 @@ import (
"unicode"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
@@ -99,7 +100,7 @@ func isOperationalState(state stacks.ContainerState) bool {
func (s *Server) templateFuncMap() template.FuncMap {
loc := getTimezone()
return template.FuncMap{
fm := template.FuncMap{
// stateColor maps container states to design-system-v2 semantic tokens,
// consumed as class suffixes (tag-run, stack-state-run, state-text-run...).
// Exception-color principle: a customer-stopped app is neutral, NOT red —
@@ -200,25 +201,6 @@ func (s *Server) templateFuncMap() template.FuncMap {
}
return "nominal"
},
"fmtMB": func(mb uint64) string {
if mb >= 1024 {
gb := float64(mb) / 1024.0
if gb >= 10 {
return fmt.Sprintf("%.0f GB", gb)
}
return fmt.Sprintf("%.1f GB", gb)
}
return fmt.Sprintf("%d MB", mb)
},
"fmtGB": func(gb float64) string {
if gb >= 100 {
return fmt.Sprintf("%.0f GB", gb)
}
if gb >= 10 {
return fmt.Sprintf("%.1f GB", gb)
}
return fmt.Sprintf("%.2f GB", gb)
},
"subtract": func(a, b int) int {
r := a - b
if r < 0 {
@@ -249,9 +231,6 @@ func (s *Server) templateFuncMap() template.FuncMap {
"fmtTemp": func(celsius float64) string {
return fmt.Sprintf("%.0f°C", celsius)
},
"fmtLoad": func(load float64) string {
return fmt.Sprintf("%.2f", load)
},
"filterCategory": func(state stacks.ContainerState, deployed bool) string {
switch state {
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
@@ -309,37 +288,8 @@ func (s *Server) templateFuncMap() template.FuncMap {
return fmt.Sprintf("%d napja", int(d.Hours()/24))
}
},
// fmtTimeStr renders an RFC3339 STRING as an absolute Budapest-local date-time. R-101: the
// Tier-2 restore confirm dialog printed a raw RFC3339 stamp ("2026-07-28T01:30:00Z") at the
// moment the customer decides whether to restore — a UTC machine timestamp is not something a
// customer can reason about. Absolute rather than relative here on purpose: "3 napja" is fine on
// a status card, but a restore decision deserves the actual date.
//
// R-102: it delegates to the package-level fmtRFC3339Local so a HANDLER can render the same
// date the same way. The Tier-2 unit restore names the copy's date in its confirm (template)
// and again in its outcome (Go); two renderings of one decision that could disagree is how a
// customer ends up confirming one date and being told another.
"fmtTimeStr": fmtRFC3339Local,
"fmtTime": func(t time.Time) string {
if t.IsZero() {
return "–"
}
return t.In(loc).Format("2006-01-02 15:04")
},
// humanBytes (v0.200.0, R-193) renders a size the same way every other surface does — the
// recovery page's listing shares the backup package's formatter rather than growing a second one.
"humanBytes": backup.HumanizeBytes,
"fmtTimeShort": func(t time.Time) string {
if t.IsZero() {
return "–"
}
lt := t.In(loc)
now := time.Now().In(loc)
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
return lt.Format("15:04")
}
return lt.Format("01-02 15:04")
},
// fmtTime / fmtTimeStr / fmtTimeShort / fmtMB / fmtGB / fmtBytes / fmtLoad / humanBytes:
// formatFuncs below (R-562 — per language).
"dbTypeLabel": func(t backup.DBType) string {
switch t {
case backup.DBTypePostgres:
@@ -363,7 +313,7 @@ func (s *Server) templateFuncMap() template.FuncMap {
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay()+1 {
return "holnap " + timeStr
}
return lt.Format("2006-01-02") + " " + timeStr
return i18n.FormatDate(i18n.Default, lt) + " " + timeStr
},
// R-572 (v0.258.0) — `pruneLabel` and `nextPruneLabel` WERE HERE, and they are deleted
// rather than translated. The row said "a template calling either renders Hungarian on an
@@ -384,23 +334,6 @@ func (s *Server) templateFuncMap() template.FuncMap {
}
return fmt.Sprintf("%dm %ds", int(d.Minutes()), int(d.Seconds())%60)
},
"fmtBytes": func(b int64) string {
const (
kb = 1024
mb = 1024 * kb
gb = 1024 * mb
)
switch {
case b >= int64(gb):
return fmt.Sprintf("%.1f GB", float64(b)/float64(gb))
case b >= int64(mb):
return fmt.Sprintf("%.1f MB", float64(b)/float64(mb))
case b >= int64(kb):
return fmt.Sprintf("%.1f KB", float64(b)/float64(kb))
default:
return fmt.Sprintf("%d B", b)
}
},
"shortID": func(id string) string {
if len(id) > 8 {
return id[:8]
@@ -485,17 +418,112 @@ func (s *Server) templateFuncMap() template.FuncMap {
return false
},
}
// R-562: the date, size and number formatters, in Hungarian here; loadTemplates swaps in each
// language's own set (formatFuncs).
for name, fn := range formatFuncs(i18n.Default) {
fm[name] = fn
}
return fm
}
// fmtRFC3339Local renders an RFC3339 STRING as an absolute Budapest-local date-time, returning the
// input unchanged when it does not parse. The ONE implementation behind the `fmtTimeStr` template
// helper and every Go-side caller. See the comment on the funcmap entry for why it is shared.
func fmtRFC3339Local(s string) string {
// fmtRFC3339Local renders an RFC3339 STRING as an absolute Budapest-local date-time in lang (R-562),
// returning the input unchanged when it does not parse. The ONE implementation behind the
// `fmtTimeStr` template helper and every Go-side caller.
//
// R-101: the Tier-2 restore confirm dialog printed a raw RFC3339 stamp at the moment the customer
// decides whether to restore — a UTC machine timestamp is not something a customer can reason about.
// R-102: a HANDLER renders the same date the same way (the confirm in the template and the outcome in
// Go name one copy's date; two renderings that could disagree is how a customer confirms one date and
// is told another).
func fmtRFC3339Local(lang, s string) string {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
// A date another package already wrote as "2006-01-02 15:04" (appexport's bundle list) is
// re-laid in lang, its clock untouched.
if lt, err2 := time.Parse("2006-01-02 15:04", s); err2 == nil {
return i18n.FormatDateTime(lang, lt)
}
return s
}
return t.In(getTimezone()).Format("2006-01-02 15:04")
return i18n.FormatDateTime(lang, t.In(getTimezone()))
}
// fmtLocalTime renders a time as a Budapest-local date-time in lang (R-562). "–" for a zero time.
func fmtLocalTime(lang string, t time.Time) string {
if t.IsZero() {
return "–"
}
return i18n.FormatDateTime(lang, t.In(getTimezone()))
}
// fmtSizeBytes is the dashboard's B/KB/MB/GB size, one decimal, in lang's decimal separator.
func fmtSizeBytes(lang string, b int64) string {
const (
kb = 1024
mb = 1024 * kb
gb = 1024 * mb
)
switch {
case b >= int64(gb):
return i18n.FormatDecimal(lang, float64(b)/float64(gb), 1) + " GB"
case b >= int64(mb):
return i18n.FormatDecimal(lang, float64(b)/float64(mb), 1) + " MB"
case b >= int64(kb):
return i18n.FormatDecimal(lang, float64(b)/float64(kb), 1) + " KB"
default:
return fmt.Sprintf("%d B", b)
}
}
// formatFuncs (R-562, `09` §3 decision 223) — every template func that prints a date, a size or a
// decimal number, for ONE language: Hungarian „2026. 10. 10. 15:04" and a decimal comma, English
// „2026-10-10 15:04" and a decimal point. The layouts live in internal/i18n (FormatDateTime and
// siblings); this map only binds them to the names the templates call. Pinned by
// TestFormatFuncs_PerLanguage.
func formatFuncs(lang string) template.FuncMap {
loc := getTimezone()
return template.FuncMap{
"fmtMB": func(mb uint64) string {
if mb >= 1024 {
gb := float64(mb) / 1024.0
if gb >= 10 {
return i18n.FormatDecimal(lang, gb, 0) + " GB"
}
return i18n.FormatDecimal(lang, gb, 1) + " GB"
}
return fmt.Sprintf("%d MB", mb)
},
"fmtGB": func(gb float64) string {
if gb >= 100 {
return i18n.FormatDecimal(lang, gb, 0) + " GB"
}
if gb >= 10 {
return i18n.FormatDecimal(lang, gb, 1) + " GB"
}
return i18n.FormatDecimal(lang, gb, 2) + " GB"
},
"fmtLoad": func(load float64) string { return i18n.FormatDecimal(lang, load, 2) },
"fmtTimeStr": func(s string) string { return fmtRFC3339Local(lang, s) },
"fmtTime": func(t time.Time) string { return fmtLocalTime(lang, t) },
// humanBytes (v0.200.0, R-193) shares the backup package's formatter so the recovery page's
// listing reads like every other size; its decimal point follows the language here.
"humanBytes": func(n int64) string { return i18n.LocalizeDecimals(lang, backup.HumanizeBytes(n)) },
"fmtTimeShort": func(t time.Time) string {
if t.IsZero() {
return "–"
}
lt := t.In(loc)
now := time.Now().In(loc)
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
return lt.Format("15:04")
}
return i18n.FormatMonthDayTime(lang, lt)
},
"fmtBytes": func(b int64) string { return fmtSizeBytes(lang, b) },
// numText rewrites the decimal point of a size another package already formatted („1.5 GB",
// the *Human fields) into this language's separator.
"numText": func(s string) string { return i18n.LocalizeDecimals(lang, s) },
}
}
// stackOf lets the update-path template funcs take a stack the way each page holds it (app_info: a