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
+8
View File
@@ -94,6 +94,8 @@ type Server struct {
// netAgentFn nil → the shared agentClient(); netProbeFn nil → runNetProbe (the uid-1000 re-exec).
netAdd netAddState
storageInit storageInitState
// sambaEnsure is the Megosztás bring-up progress slot (v0.147.0, 4b) — same single-flight shape.
sambaEnsure sambaEnsureState
netAgentFn func() (netAgent, error)
// fabUpload is the chunked browser .fab upload single-flight slot (v0.128.0).
fabUpload uploadState
@@ -359,6 +361,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.sharingPageHandler(w, r)
case path == "/sharing/enable" && r.Method == http.MethodPost:
s.sharingEnableHandler(w, r)
case path == "/sharing/status" && r.Method == http.MethodGet:
s.sharingStatusHandler(w, r)
case path == "/sharing/password" && r.Method == http.MethodPost:
s.sharingPasswordHandler(w, r)
case path == "/sharing/shares" && r.Method == http.MethodPost:
@@ -418,6 +422,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.offboxRestoreHandler(w, r)
case path == "/backup/offbox/place" && r.Method == http.MethodPost:
s.offboxPlaceHandler(w, r)
// v0.147.0 (4a): remove ONE verification copy. The only delete path this slice adds — see
// DeleteOffsiteRestoreCopy for the prefix guard.
case path == "/backup/offbox/verify-copy/delete" && r.Method == http.MethodPost:
s.offboxVerifyCopyDeleteHandler(w, r)
// R-7b: „Megosztások" restore — a sibling of the per-app pair above.
case path == "/backup/shares/restore" && r.Method == http.MethodPost:
s.sharesRestoreHandler(w, r)