v0.147.0 — feedback slice 1: pressing a button says something

The systemic complaint, twice in one evening: you press a button and nothing
happens. No progress, no ETA, no named result. Three worst offenders, fixed on
the two patterns already here (deploy 3-step panel, storage-init status poll).
No new framework — that is a ROADMAP item; three targeted cards ship tonight.

4a — a verification restore names its result. The flash said the app had been
restored "to a verification folder on the drive"; which folder, on which drive,
was invisible, so the customer could not go and look at what they had just asked
for. Full path now. The restore page gained a listing of existing verification
copies (app, size, date, path) — nothing anywhere showed these, so they piled up
and the only way to find them was SSH — each with a double-confirmed delete.

That delete is the only one this release adds, so it names a STACK, never a
path: the Manager resolves the name inside a backups/offsite-restore root it
computed itself and refuses anything landing outside. Red-proofed — neutralise
the name guard and stack:"" resolves to the offsite-restore ROOT and takes every
copy with it. Refusals are asserted as non-effects.

4b — Megosztás enable shows what it is waiting for. Enabling ran ReconcileSamba
synchronously inside the POST handler; on a golden without felhom-samba baked
that is compose pulling ~100MB, i.e. minutes of an apparently-hung form post
followed by "Beállítás mentve." whether or not anything came up. Detached +
polled now, distinguishing "képfájl letöltése" from "indítás" — decided BEFORE
the work starts, since afterwards the image is always present. Success is
probed, not inferred (compose up -d exits 0 on a crash-loop). The password form
starts the same job: with UserSet false reconcile deploys nothing, so on a fresh
box that is where the pull actually happens.

4c — "Távoli mentés most" streams real progress. restic was already reporting
bytes and percent; the runner seam used CombinedOutput() and discarded them. The
manual run now passes --json and scans stdout line-by-line: total bytes, percent,
current app. Manual only — the nightly stays silent, pinned by a test that fails
if it ever passes --json. The poll now arms unconditionally, closing a race the
manual trigger always ran: the redirect rendered before the goroutine wrote
LastStatus=running, so the poll never armed and the page sat static during the
very run just started. Red-proofed twice.

Also closes the golden/controller infra-image drift at the source: infra.Images()
derives from the existing pins and --print-infra-images exposes it, so the golden
bake can stop carrying its own copy. That copy had already drifted — felhom-samba
was never added, so the golden baked 3 of 4, which is why enabling Megosztás
pulled at runtime in the first place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
This commit is contained in:
2026-07-19 09:30:30 +02:00
parent 7f0b41c3e7
commit b5d78d1e0f
22 changed files with 1527 additions and 21 deletions
+33 -6
View File
@@ -221,10 +221,33 @@ func (s *Server) sharingEnableHandler(w http.ResponseWriter, r *http.Request) {
sharingRedirect(w, r, "A hálózati megosztás kikapcsolva. A mappák és a fájlok megmaradtak.")
return
}
if err := s.stackMgr.ReconcileSamba(); err != nil {
s.logger.Printf("[WARN] [sharing] reconcile failed: %v", err)
// v0.147.0 (4b): the bring-up runs DETACHED and the page polls it. Synchronously it was a form
// post that hung for minutes on a first-enable image pull and then flashed „Beállítás mentve."
// regardless of whether anything actually came up.
if !s.startSambaEnsure() {
sharingRedirect(w, r, "A megosztási szolgáltatás előkészítése már folyamatban van.")
return
}
sharingRedirect(w, r, "Beállítás mentve.")
sharingRedirect(w, r, "Beállítás mentve. A megosztási szolgáltatás előkészítése folyamatban…")
}
// sharingStatusHandler is the 4b poll target (GET /sharing/status). Reports the ensure job's phase
// plus the live container state, so a page loaded AFTER the job finished (or after a restart, when
// the in-memory job is gone) still shows the truth.
func (s *Server) sharingStatusHandler(w http.ResponseWriter, r *http.Request) {
phase := sambaPhaseIdle
errMsg := ""
if job := s.sambaEnsure.snapshot(); job != nil {
phase, errMsg = job.Phase, job.Error
}
running := s.stackMgr != nil && s.stackMgr.SambaRunning()
// A stale `idle`/`running` job must never contradict reality: liveness wins on a fresh page.
if phase == sambaPhaseIdle && running {
phase = sambaPhaseRunning
}
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{
"phase": phase, "error": errMsg, "running": running,
})
}
// sharingPasswordHandler sets the household SMB password (POST /sharing/password).
@@ -247,10 +270,14 @@ func (s *Server) sharingPasswordHandler(w http.ResponseWriter, r *http.Request)
sharingRedirect(w, r, "A jelszó beállítása nem sikerült.")
return
}
if err := s.stackMgr.ReconcileSamba(); err != nil {
s.logger.Printf("[WARN] [sharing] reconcile after password failed: %v", err)
// Setting the password is the moment the stack ACTUALLY first comes up: with UserSet false,
// reconcile deliberately deploys nothing, so on a fresh box this — not the enable toggle — is
// where the image pull happens. Same detached job, same card.
if !s.startSambaEnsure() {
sharingRedirect(w, r, "Megosztási jelszó beállítva. Az előkészítés már folyamatban van.")
return
}
sharingRedirect(w, r, "Megosztási jelszó beállítva.")
sharingRedirect(w, r, "Megosztási jelszó beállítva. A megosztási szolgáltatás előkészítése folyamatban…")
}
// sharingShareCreateHandler creates a share (POST /sharing/shares). Either a NEW folder under