controller v0.268.0: the undo finds volumes by definition; a held app names only a whole copy; one press = one tested step (R-658, R-659, R-660, R-651; 09 §6.4 part 5)
gates / gates (push) Successful in 27s

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-09-24 08:15:27 +02:00
parent 80e6ad8c47
commit 206b0357d1
36 changed files with 2201 additions and 40 deletions
+26 -12
View File
@@ -167,9 +167,12 @@ type Stack struct {
// HoldReason is the customer sentence of a hold in force on this app (a failed update or a failed
// restore), "" when none. Filled on every read from the ONE hold store, never cached, so the page
// and the API cannot show a hold the gate has already lifted — or miss one it enforces.
HoldReason string `json:"hold_reason,omitempty"`
HealthProbe *HealthProbeResult `json:"health_probe,omitempty"` // controller-side probe result
LastUpdated time.Time `json:"last_updated"`
HoldReason string `json:"hold_reason,omitempty"`
// HoldNoWholeCopy (v0.268.0, R-659): the hold names NO copy — none on this box brings the app back
// whole — so the page offers no restore button beside the sentence (the restore would refuse).
HoldNoWholeCopy bool `json:"hold_no_whole_copy,omitempty"`
HealthProbe *HealthProbeResult `json:"health_probe,omitempty"` // controller-side probe result
LastUpdated time.Time `json:"last_updated"`
// RestartingSince (C9-F2) is when this stack was FIRST observed in StateRestarting during the
// current restarting run; zero whenever the stack is in any other state. It is what turns a brief
// restart (normal: deploy, update, quiesce restart) into a distinguishable crash loop — see
@@ -197,6 +200,11 @@ type Stack struct {
// Nil means CANNOT-TELL — the cache is missing, unreadable, or the app is not in the catalog —
// and the badge then renders NOTHING. Absent is unknown; it is never „Naprakész".
CatalogImages map[string]string `json:"catalog_images,omitempty"`
// LadderStepsLeft (v0.268.0, `09` §3 decision 14) — how many tested steps lie between the app's
// pin and the catalog's head; one press climbs one. 0 = unknown or none. The page shows it while
// the badge says „Frissítés elérhető".
LadderStepsLeft int `json:"ladder_steps_left,omitempty"`
}
// Manager handles all docker compose stack operations.
@@ -602,8 +610,12 @@ func (m *Manager) ScanStacks() error {
// one above (v0.235.0). A missing catalog entry is silent at INFO: an orphaned app has no
// catalog template by definition, and warning once per app per scan would be noise.
var catImages map[string]string
stepsLeft := 0
if deployed && !m.cfg.IsProtectedStack(name) {
catPath := m.CatalogTemplatePath(name, "docker-compose.yml")
if appCfg != nil {
stepsLeft = ladderStepsLeft(filepath.Dir(catPath), appCfg.PinnedImages)
}
if imgs, cerr := ParseComposeImages(catPath); cerr == nil {
catImages = imgs
} else if m.isDebug() {
@@ -622,18 +634,20 @@ func (m *Manager) ScanStacks() error {
existing.AppConfig = appCfg
existing.TemplateImages = tplImages
existing.CatalogImages = catImages
existing.LadderStepsLeft = stepsLeft
}
} else {
m.stacks[name] = &Stack{
Name: name,
Meta: meta,
ComposePath: composePath,
State: StateNotDeployed,
Deployed: deployed,
Protected: m.cfg.IsProtectedStack(name),
AppConfig: appCfg,
TemplateImages: tplImages,
CatalogImages: catImages,
Name: name,
Meta: meta,
ComposePath: composePath,
State: StateNotDeployed,
Deployed: deployed,
Protected: m.cfg.IsProtectedStack(name),
AppConfig: appCfg,
TemplateImages: tplImages,
CatalogImages: catImages,
LadderStepsLeft: stepsLeft,
}
}
}