v0.233.0: record what each compose service actually installed, and badge whether it is current
gates / gates (push) Successful in 12s

Update arc slices 1 and 2. NEITHER CHANGES ANY BEHAVIOUR — no new endpoint, no
auto-update, the three lifecycle buttons byte-identical.

Slice 1 — app.yaml gains installed_images, keyed by compose SERVICE name, each
entry carrying ref + repo digest + first-seen timestamp. Written by
Manager.recordInstalledImages after a successful compose up from StartStack,
RestartStack, UpdateStack and runComposeDeploy. Read from the CONTAINER, never
from docker-compose.yml: the syncer overwrites a deployed app's compose on a
15-minute cycle and the two disagreed for 25 minutes in the spike's own
measurement. A failed write NEVER refuses the action - the deliberate opposite
of SetDesiredState, because this is an observation and that is an intent. Not
called from StartStackServices (the R-47 DB-only window). Its own docker seam
with a context and a 30s timeout, which neither existing exec helper has.

Slice 2 — .felhom.yml gains optional catalog_since; web.updateBadge compares the
recorded ref per service against what the current template pins and returns a
*MetaBadge through the EXISTING meta_badge partial. No new markup, no new CSS.
NO RECORD RENDERS NOTHING: absent means unknown and never means current. No
version number reaches the customer and no registry is queried.

Known limitation, filed not hidden: 23 catalog pins float, so those apps can read
Naprakesz when the image behind the tag has moved.

+17 tests (1707 -> 1724), 28 packages green. Wiring proven through a real
RestartStack plus an AST walk of the four call sites. Three companion red-proofs
run and reverted.
This commit is contained in:
2026-09-02 20:18:01 +02:00
parent 960d29b061
commit 8025304acc
14 changed files with 1637 additions and 9 deletions
+6
View File
@@ -473,6 +473,12 @@ func (s *Server) templateFuncMap() template.FuncMap {
// there is nothing to say. Pair it with the `meta_badge` partial, which no-ops on nil.
// R-56's difficulty badge is meant to be a sibling entry returning the same *MetaBadge.
"lifecycleBadge": lifecycleBadge,
// updateBadge is the SECOND *MetaBadge-returning entry the type was built for: it says
// whether a deployed app is running what the catalog currently pins, and for how long it
// has not been. Pair it with the same `meta_badge` partial. Renders NOTHING when there is
// no record — absent means unknown, never "up to date". Information only: it is wired to
// no action and changes no button.
"updateBadge": updateBadge,
// canInstall reports whether a catalog template may be OFFERED for a new install. The
// server-side deploy gate uses the same stacks.Metadata.CanInstall, so the button and the
// endpoint can never disagree.