docs: v0.77.0 open_path — REPORT + README

This commit is contained in:
2026-06-23 12:05:29 +02:00
parent 904e787dda
commit 66d84627b8
2 changed files with 33 additions and 44 deletions
+31 -44
View File
@@ -1,49 +1,36 @@
# REPORT — controller v0.76.0: campaign-#3 hardening (3 fixes)
# REPORT — controller v0.77.0: per-app `open_path` for the "Megnyitás" link
## Baseline → target
`felhom-controller` `main` v0.75.0 → **v0.76.0**. Controller-only; agent/hub untouched.
## Problem
The dashboard / deploy / app-info **"Megnyitás"** (open app) button was hardcoded to the bare subdomain
root `https://{subdomain}.{domain}` for every app. Apps whose useful UI isn't at `/` opened to the wrong
place — e.g. Gokapi's `/` redirects away (its admin is `/admin`), Ghost's `/` is the public blog (admin
is `/ghost/`).
## What changed (commit `b0dd131`)
Three findings from chaos campaign #3, severity-ordered:
## Change (commit `904e787`)
- `internal/stacks/metadata.go`: new optional `Metadata.OpenPath` (`open_path` in `.felhom.yml`),
appended to the open-app URL. Empty = bare root (unchanged behaviour for all other apps).
- Wired into all three link sites via `.Meta.OpenPath`**no handler changes** (all three templates
already carry `.Meta`):
- `dashboard.html:173` (range over stacks → `.Meta.OpenPath`)
- `deploy.html:18` (`.Meta` from the deploy handler)
- `app_info.html:13` (`.Meta` from the app-info handler)
- **S1 [MEDIUM] — corrupt `settings.json` no longer crash-loops the controller.**
`internal/settings/settings.go`: `save()` writes a last-known-good `<path>.bak` **after** the primary
rename succeeds (best-effort). `Load()` on a parse error recovers from `.bak` (re-promotes to primary),
else preserves the corrupt file as `*.corrupt-<ts>` and starts on safe defaults — never returns the
error that drove `main.go`'s `Fatalf`/crash-loop. New `Settings.LoadWarning` → dashboard banner
(`handlers.go` + `dashboard.html`). `main.go` `Fatalf` retained (now only the IO-unreadable path).
- **F2 [MEDIUM, defense-in-depth] — `stack_name` validated against path traversal.**
`internal/web/validate.go` `validStackName` (single segment; rejects `/ \ .. NUL`), gated in
`backupRestoreHandler` (`handlers.go`) and `apiExportStart` (`handler_export.go`) before any
restore/export work.
- **S3 [LOW] — corrupt quiesce marker quarantined, not silently dropped.**
`internal/quiesce/quiesce.go` `readMarker` logs a `[WARN]` + renames a bad-JSON marker to
`*.corrupt-<ts>` (still returns "no marker" → no recovery, the correct contract).
## Catalog (app-catalog-felhom.eu, commits `6a71c5a`)
Audited all 53 templates (subagent + cross-check). No Traefik `PathPrefix` anywhere; the only apps whose
bare `/` is not a usable landing:
- `gokapi``open_path: "/admin"` (file-share index redirects away)
- `ghost``open_path: "/ghost/"` (bare `/` is the public blog; admin/setup at `/ghost/`)
All other 51 serve or redirect usefully at `/` (e.g. plex `/``/web/`, radarr/sonarr/gitea/vaultwarden
`/``/login`). `onlyoffice` is API-only (status page at `/`) — left at root.
## Tests (green; all red-proofed)
`go build ./... && go vet ./... && go test ./...` — green.
- T-S1a recover-from-bak, T-S1b unrecoverable→safe-defaults+preserve, T-S1c save-writes-bak, T-S1d happy.
Red-proof: revert `Load` to error-on-parse → T-S1a/b FAIL.
- T-F2a `validStackName` (pure), T-F2b restore handler rejects traversal (no nil-deref), T-F2c export
handler → 400. Red-proof: drop the restore gate → traversal falls through to the nil-backupMgr
redirect → T-F2b FAIL.
- T-S3a quarantine+log, T-S3b happy. Red-proof: revert `readMarker` to silent → T-S3a FAIL.
## Validation (live, guest 9201, v0.77.0 healthy)
- Rendered `Megnyitás` href on the gokapi **Beállítások** page: `https://share.demo-felhom.eu/admin`
- Dashboard hrefs: `share.demo-felhom.eu/admin` for gokapi; **all other apps unchanged** at bare root
(arcade, audiobooks, books, budget, komga, media, paperless, paste, photos, radarr, recipes, status,
tasks) — no regression ✓
- (Earlier, same session: gokapi's index `RedirectUrl` was also repointed `/``/admin` so direct-URL
visitors to the bare root reach the login too.)
## Deploy
Built+pushed `:0.76.0` on 192.168.0.180 (build-server `git pull` done first), deployed to 9201 via
bootstrap. `docker inspect`: `image=:0.76.0 running healthy`.
## LIVE re-test — the three campaign-#3 scenarios, now fixed
1. **S1:** ensured `.bak`, truncated `settings.json` → restart → `status=running RestartCount=0`
(no crash-loop), logs `[ERROR] primary settings corrupt … — attempting recovery from .bak`
`[WARN] recovered settings from .bak; re-promoting to primary`; config intact (3 storage_paths), 25/0.
2. **F2:** `POST /backup/restore stack_name=../../../etc``flash_error=Érvénytelen alkalmazásnév`,
log `[WARN] restore rejected: invalid stack_name "../../../etc"`, **no "Starting app-data restore"**
(gate fired before the restore logic); `/etc/passwd` intact (24 lines).
3. **S3:** planted a corrupt `quiesce-state.json` → restart → `[WARN] [quiesce] marker … is corrupt …
— quarantining; stacks not auto-recovered`, original renamed to `*.corrupt-<ts>`, `RestartCount=0`,
25/0 (stacks not stranded).
## Still-open campaign-#3 deferrals (NOT in this task)
Time-chaos on a dedicated VM / injectable clock; host-reboot (supervised); `.fab` import
compose-fuzzing. Agent/hub untouched.
## Notes
- `open_path` must start with `/`. It's purely cosmetic (the open link); routing is unchanged.
- Build/deploy: build-server `git pull` first (build.sh doesn't pull), `:0.77.0` via bootstrap.