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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user