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:
@@ -219,11 +219,13 @@ func (s *Server) offboxRunHandler(w http.ResponseWriter, r *http.Request) {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Hour)
|
||||
defer cancel()
|
||||
if err := s.backupMgr.RunOffboxBackup(ctx); err != nil {
|
||||
// ...WithProgress: this is the MANUAL trigger, so the page gets live bytes/percent/current app
|
||||
// (4c). The nightly scheduler keeps calling RunOffboxBackup and stays silent.
|
||||
if err := s.backupMgr.RunOffboxBackupWithProgress(ctx); err != nil {
|
||||
s.logger.Printf("[WARN] [web] manual off-box backup failed: %v", err)
|
||||
}
|
||||
}()
|
||||
offboxRedirect(w, r, "A távoli mentés elindult (a futás után az állapot frissül).", false)
|
||||
offboxRedirect(w, r, "A távoli mentés elindult — az állapot itt frissül.", false)
|
||||
}
|
||||
|
||||
// offboxResetHandler is the CLAIMED confirmed orphaned-repo reset (Scenario C): move the old (recovery-
|
||||
@@ -266,6 +268,11 @@ func (s *Server) offboxStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
resp["last_error"] = t.LastError
|
||||
resp["orphaned"] = t.RepoState == "orphaned"
|
||||
}
|
||||
// v0.147.0 (4c): live progress for a MANUAL run. Absent/inactive on the nightly path, so the
|
||||
// page simply keeps its previous „Fut…" behavior there.
|
||||
if s.backupMgr != nil {
|
||||
resp["progress"] = s.backupMgr.OffboxProgressSnapshot()
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -319,11 +326,57 @@ func (s *Server) offboxRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] off-box restore %s completed (full=%v, async)", app, full)
|
||||
s.backupMgr.EndRestoreOp(true, "A(z) "+app+" visszaállítva ellenőrző mappába a meghajtón (a meglévő adatok változatlanok).")
|
||||
// NAME THE RESULT (v0.147.0, 4a). The old message said the app had been restored "to a
|
||||
// verification folder on the drive" — which folder, on which drive, was invisible, so the
|
||||
// customer had no way to look at what they had just asked for. Resolve the real path and say
|
||||
// it. Fall back to the vague wording only if the path can no longer be resolved.
|
||||
where := s.backupMgr.OffsiteRestoreScratchPath(app)
|
||||
msg := "A(z) " + app + " visszaállítva ellenőrző mappába a meghajtón (a meglévő adatok változatlanok)."
|
||||
if where != "" {
|
||||
msg = "A(z) " + app + " visszaállítva ellenőrző mappába: " + where + " (a meglévő adatok változatlanok)."
|
||||
}
|
||||
s.backupMgr.EndRestoreOp(true, msg)
|
||||
}()
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A távoli visszaállítás elindult — az állapot itt frissül.", false)
|
||||
}
|
||||
|
||||
// offboxVerifyCopyDeleteHandler removes ONE verification copy (v0.147.0, 4a).
|
||||
//
|
||||
// The only delete this slice adds, so it is deliberately narrow: it names a STACK, never a path — the
|
||||
// customer cannot hand us a path to remove. The Manager resolves that name inside a
|
||||
// `backups/offsite-restore` root it computed itself and refuses anything that lands outside (see
|
||||
// DeleteOffsiteRestoreCopy). The template double-confirms before POSTing.
|
||||
//
|
||||
// It refuses while a backup/restore op is running: the copy being deleted could be the one currently
|
||||
// being written.
|
||||
func (s *Server) offboxVerifyCopyDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A mentéskezelő nem érhető el.", true)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
stack := strings.TrimSpace(r.FormValue("stack"))
|
||||
if stack == "" {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "Hiányzó ellenőrző másolat.", true)
|
||||
return
|
||||
}
|
||||
if r.FormValue("confirm") != "1" {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A törlés megerősítés nélkül nem hajtható végre.", true)
|
||||
return
|
||||
}
|
||||
if s.backupMgr.IsRunning() {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "Egy mentési/visszaállítási művelet fut — a törlés most nem biztonságos.", true)
|
||||
return
|
||||
}
|
||||
if err := s.backupMgr.DeleteOffsiteRestoreCopy(stack); err != nil {
|
||||
s.logger.Printf("[WARN] [web] verification-copy delete %s: %v", stack, err)
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A másolat törlése nem sikerült: "+err.Error(), true)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] verification copy deleted: %s", stack)
|
||||
offboxRedirectTo(w, r, "/backups/restore", "Az ellenőrző másolat törölve. A tényleges adataid változatlanok.", false)
|
||||
}
|
||||
|
||||
// offboxPlaceHandler places a COMPLETED full-restore scratch into the app's live locations via a
|
||||
// missing-only merge (§7.3). Never overwrites existing files. Async on a background context.
|
||||
func (s *Server) offboxPlaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user