# REPORT — v0.168.0: Customer-configurable backup window ("Mentési időablak") **Date:** 2026-07-24 · **Deployed & verified live on guest 9201; box left configured at 02:30.** ## 1. Baseline | Repo | `main` @ start | version | → | |------|----------------|---------|---| | felhom-controller | `e33c1ae` | v0.167.1 | **v0.168.0** (committed `82c67e3`) | Verified against live Gitea at task start (HEAD == origin == live). No agent coupling; MinAgent unchanged. ## 2. What shipped ONE customer setting — **"Mentési időablak kezdete"** (window start W) — drives every nightly leg at FIXED, never-stored offsets so misordering is impossible: DB dump at **W**, tier-2 at **W+60m**, off-box at **W+105m** (wrap-safe). The whole-guest (agent PBS/vzdump) cycle is gated to **[W+2h, W+6h)** with a safety valve. Precedence: settings > controller.yaml `db_dump_schedule` > "02:30". Changes apply **without restart**. ## 3. Files - **New** `internal/backupwindow/` (backupwindow.go + _test.go) — pure time math: `ParseHHMM`/`FmtHHMM`/ `LegTimes`/`GateWindow`/`EffectiveWindow`/`Valid`; offsets are constants (derived, never stored). - `internal/scheduler/scheduler.go` (+ new _test.go) — `UpdateDaily(name,timeStr) bool`, per-daily-job buffered `resched` chan, a `case <-job.resched` in `runDailyJob`, Schedule read under the mutex. - `internal/settings/settings.go` — `BackupWindowStart` field + Get/Set. - `internal/quiesce/quiesce.go` (+ new quiesce_window_test.go; fakes updated in quiesce_test.go / quiesce_8b2_test.go) — `Backend.Due` now returns the backup age; `Options.WindowStartFn`/`Cadence`; `scheduledRunAllowed` predicate + Budapest helper; gate on the scheduled path in `runOnce`. - `internal/web/backup_handlers.go` (+ new backup_window_test.go), `handlers.go`, `server.go` route, `templates/backups.html` — the "Mentési időablak" card + POST `/backups/window`. - `cmd/controller/main.go` — the three Daily legs + backup-cache + initial-cache + quiesce loop wired from the effective window. - Docs: `CHANGELOG.md`, `CONTEXT.md`, `controller/README.md`, `REUSE.md` (UpdateDaily + backupwindow + 3rd Budapest-loader duplication note), this `REPORT.md`. > Note: a stray `gofmt -w internal/ cmd/` reformatted ~46 unrelated (non-gofmt-clean) files; all were > restored to `origin/main` so the commit is surgical — only the files above are in the diff. ## 4. Tests + red-proofs (all recorded) `go build ./... && go vet ./... && go test ./...` — **27 packages pass, 0 fail.** Template/design gates (template_id / emoji / mojibake / native-confirm) all OK. | Group | Test(s) | Red-proof (mutate → FAIL → restore) | |-------|---------|--------------------------------------| | A | `LegTimes` (02:30/23:30/22:00/00:00, wrap), `LegTimes_InvalidRejected`, `GateWindow` | drop the FmtHHMM modulo → `LegTimes("23:30")` yields "24:30"/"25:15" → **FAIL confirmed** | | B | `EffectiveWindow` precedence table (settings>yaml>"02:30", corrupt falls through) | — | | C | `UpdateDaily_MutatesAndSignals`, `_UnknownAndInvalid`, `_GoroutineConsumesReschedule` | remove `case <-job.resched` → signal stays buffered → immediacy test **FAIL confirmed** | | D | `scheduledRunAllowed` truth table (inside/boundary/outside/valve/wrap/nil-age) + Loop integration (defer outside / run inside / valve runs / **manual TriggerNow never gated**) | invert the valve `>`→`<` → starving-box + no-valve cases **FAIL confirmed** | | E | `BackupWindowSave_Valid` (settings + 3× UpdateDaily + flash), `_Invalid` (nothing stored + error flash) | skip the `Valid()` check → invalid value gets stored → **FAIL confirmed** | Existing quiesce/scheduler/web suites still pass unchanged. ## 5. Deploy + live validation (guest 9201) Built `felhom-controller:0.168.0` (145M) from the clean pushed tree; deployed via bootstrap → `Up (healthy)`, `Controller elindult (0.168.0)`. Startup registered the legs at the default window: `db-dump 02:30 · tier2-backup 03:30 · offbox-backup 04:15` (CEST). Method: endpoint-level (in-guest curl to `172.17.0.2:8080`, `Host: felhom.demo-felhom.eu`; login 302 + scraped session CSRF). **Save a window (POST `/backups/window`, window_start=22:00) → 303:** - Page re-rendered: input `value="22:00"`, derived rows `22:00 / 23:00 / 23:45`, rendszermentés `kb. 00:00–04:00 között` (W+2h..W+6h, wrap-safe). - Logs (NO restart): `db-dump rescheduled 02:30 → 22:00`, `tier2-backup 03:30 → 23:00`, `offbox-backup 04:15 → 23:45`, `backup window set to 22:00 (legs 22:00/23:00/23:45)`. **Restore (window_start=02:30) → 303:** page shows `02:30 / 03:30 / 04:15` + `kb. 04:30–08:30 között`; logs show the three legs rescheduled back. **Box left at 02:30.** ## 6. NOT live-exercised on 9201 (stated method) - **The disk-tier window-gate DEBUG denial** could not be triggered live: the demo host agent is DOWN for the remote-site window (its `localapi` binds `192.168.0.162`, which doesn't exist there), so the quiesce loop's `/backup/due` call errors before the gate is reached. The gate + safety valve + the manual-never-gated property are fully covered by Group D (truth table + Loop integration). Re-verify the live DEBUG line once the agent is reachable again. - Browser visual check of the "Mentési időablak" card layout is Viktor's acceptance step (the rendered HTML strings are confirmed; the pixels are not). ## 7. Observations (out of scope — documented only) - Saving explicitly writes `backup_window_start:"02:30"` — functionally identical to the empty/default state, so the box is correctly "at 02:30" either way. - Cadence for the safety valve defaults to 24h **controller-side** (the agent's `/backup/due` response carries no cadence field); if cadence policy ever becomes hub/agent-driven, thread it through instead of the constant. - Three Budapest-timezone loaders now exist (scheduler, web, quiesce) — noted in REUSE.md §6; kept local to avoid a scheduler↔quiesce import edge.