8f8a64cad7
gates / gates (push) Successful in 24s
MEASURED 2026-09-15 (BIGNIGHT Phase 6): privatebin updated 2.0.5 -> 2.0.6, catalog reverted to 2.0.5, and the box read „Frissítés elérhető — ma" over an Update that would have moved the pin BACKWARDS onto a possibly-migrated datadir. - stacks.CatalogOrder: the comparison gains a fourth answer (Ahead) and moves out of web, so the badge and UpdatePreflight cannot drift apart. - The badge: ahead reads „Naprakész"/"Up to date", tag-ok, with a title saying why. - The refusal: UpdatePreflight returns `downgrade` (409), born as a bundle key; the API now renders update refusals through errText so it reaches English households. - Ahead is narrow: every differing service must be orderable AND newer, else Behind. - Ordering is util.Version.Compare behind a tag normaliser — no second comparator. - Three red-proofs, each seen to fail. R-589 was already fixed in v0.258.0; only its register row was stale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
121 lines
5.5 KiB
Go
121 lines
5.5 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// updateState is the answer to "is this app running what the catalog currently pins?".
|
|
//
|
|
// THE THIRD VALUE IS THE ENTIRE SAFETY PROPERTY — the same shape, and the same lesson, as
|
|
// AppConfig.DesiredState (R-166):
|
|
//
|
|
// ABSENT MEANS UNKNOWN. IT NEVER MEANS "UP TO DATE".
|
|
//
|
|
// Every app.yaml written before v0.233.0 carries no installed_images, so unknown is the common value
|
|
// on upgrade. An implementation that fell through to "Naprakész" would tell every customer on the
|
|
// fleet that their months-old app is current — a confident wrong answer, which is worse than none.
|
|
//
|
|
// THE FOURTH VALUE, updateAhead, ARRIVED IN v0.260.0 (R-524). See stacks.CatalogOrder.
|
|
type updateState int
|
|
|
|
const (
|
|
updateUnknown updateState = iota // nothing recorded, or nothing to compare against
|
|
updateCurrent // every service runs exactly what the template pins
|
|
updateBehind // at least one service does not
|
|
updateAhead // the box runs something NEWER than the catalog offers
|
|
)
|
|
|
|
// compareInstalledToTemplate answers the question WITHOUT touching the network.
|
|
//
|
|
// SINCE v0.260.0 IT IS A THIN WRAPPER OVER stacks.CatalogOrder, and that move is the point of R-524:
|
|
// the badge and the guarded update's downgrade refusal must reach the same verdict, and two
|
|
// implementations of one comparison are two verdicts waiting to disagree. Everything the old body
|
|
// said still holds and now lives in updateorder.go:
|
|
//
|
|
// - NO REGISTRY QUERY, deliberately: a customer's box must not depend on reaching eight upstream
|
|
// registries to render a page. The comparison is reference-to-reference.
|
|
// - ⚠ IT COMPARES AGAINST Stack.CatalogImages, NEVER Stack.TemplateImages, AND v0.235.0 IS WHY.
|
|
// Since the freeze, a pinned app's LIVE compose file is rendered from its own stored definition
|
|
// once the catalog moves past it, so installed would equal template and this function would
|
|
// answer „Naprakész" on precisely the apps that are behind — with every test still green,
|
|
// because the two fields have the same type and shape.
|
|
// - KNOWN LIMITATION: 23 of the catalog's 66 distinct pins FLOAT (postgres:16-alpine,
|
|
// mariadb:11.6, …). For those the reference can be identical while the image behind it has moved
|
|
// upstream. Those apps read „Naprakész" when they may not be — R-446.
|
|
func compareInstalledToTemplate(s stacks.Stack) updateState {
|
|
switch stacks.CatalogOrder(s) {
|
|
case stacks.UpdateOrderCurrent:
|
|
return updateCurrent
|
|
case stacks.UpdateOrderBehind:
|
|
return updateBehind
|
|
case stacks.UpdateOrderAhead:
|
|
return updateAhead
|
|
default:
|
|
return updateUnknown
|
|
}
|
|
}
|
|
|
|
// updateBadgeAt is the pure form: `now` is injected so the age is a testable contract rather than a
|
|
// property of the clock. updateBadge (the funcmap entry) is the one-line wrapper.
|
|
//
|
|
// It returns a *MetaBadge and calls the EXISTING meta_badge partial — no new markup and no new CSS.
|
|
// metabadge.go's own comment asks for exactly that of its second user, and this is it.
|
|
//
|
|
// ⚠ THIS IS THE HUNGARIAN FORM AND IT KEEPS ITS LITERALS ON PURPOSE. The parity guarantee of the
|
|
// localisation arc is that templateFuncMap's Hungarian output is byte-identical to what it was
|
|
// before the bundle existed; the English form is rebuilt from the bundle in
|
|
// i18n_web.go localeFuncs, over the SAME compareInstalledToTemplate, so only the words differ and
|
|
// never the decision. A new branch here needs its twin there, and
|
|
// TestLocaleFuncsHungarianBundleMatchesFuncMap fails if hu.json and these literals disagree.
|
|
func updateBadgeAt(s stacks.Stack, now time.Time) *MetaBadge {
|
|
switch compareInstalledToTemplate(s) {
|
|
case updateCurrent:
|
|
return &MetaBadge{
|
|
Label: "Naprakész",
|
|
Class: "tag-ok",
|
|
Title: "Ez az alkalmazás a legfrissebb elérhető változatot futtatja.",
|
|
}
|
|
case updateAhead:
|
|
// R-524. THE PILL SAYS THE SAME WORD AS updateCurrent, and that is the ruling: an app that
|
|
// runs something newer than the catalog has nothing for the household to do, so it must not
|
|
// wear a warning. The note that says WHY goes in the title, which is the explanation slot
|
|
// this type exists for — a badge that only says a word is a riddle. No version number
|
|
// reaches the customer here either.
|
|
return &MetaBadge{
|
|
Label: "Naprakész",
|
|
Class: "tag-ok",
|
|
Title: "Ez az alkalmazás a katalógusnál újabb változatot futtat, ezért nincs teendőd.",
|
|
}
|
|
case updateBehind:
|
|
label := "Frissítés elérhető"
|
|
if days, ok := s.Meta.CatalogSinceAge(now); ok {
|
|
if days == 0 {
|
|
label += " — ma"
|
|
} else {
|
|
label += fmt.Sprintf(" — %d napja", days)
|
|
}
|
|
}
|
|
return &MetaBadge{
|
|
Label: label,
|
|
Class: "tag-warn",
|
|
Title: "Újabb változat érhető el ehhez az alkalmazáshoz. " +
|
|
"A frissítés indításához nyomd meg a Frissítés gombot.",
|
|
}
|
|
default:
|
|
// UNKNOWN renders NOTHING. Not a grey "ismeretlen" pill: a badge on an app we cannot judge
|
|
// is a question the customer cannot answer, and the record fills itself in on the next
|
|
// restart or update anyway.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// updateBadge is the funcmap entry. NO version number appears in any string it produces — the
|
|
// operator ruled that a household cannot act on "26.05.2", only on "you are behind, by this long".
|
|
// Version strings stay in the logs, the API and the hub.
|
|
//
|
|
// It is INFORMATION ONLY. It is wired to no action, and the Frissítés button is untouched.
|
|
func updateBadge(s stacks.Stack) *MetaBadge { return updateBadgeAt(s, time.Now().UTC()) }
|