Part C: R-644, R-763, R-764 closed (wger's fixes proven on 9202), R-762/R-717 stopped with reasons; STATUS, CONTEXT, REPORT (142 -> 137; 0 opened, 5 closed)
gates / gates (push) Successful in 2m43s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 14:03:28 +02:00
parent a29ee9dd33
commit 01d5dbd3e0
10 changed files with 280 additions and 64 deletions
+2
View File
@@ -24,6 +24,8 @@
> five conditions; the invite runs inside the box, the token never leaves it); the Tester 1 box is allowed but the walk > five conditions; the invite runs inside the box, the token never leaves it); the Tester 1 box is allowed but the walk
> has no route there. Vaultwarden's first ladder step 1.36.0-alpine → 1.37.4-alpine is live in the catalog (`ecb8552`). > has no route there. Vaultwarden's first ladder step 1.36.0-alpine → 1.37.4-alpine is live in the catalog (`ecb8552`).
> R-469 re-read: NOT removable — the engine gate is now decision 35's permanent per-app check (operator: close or keep). > R-469 re-read: NOT removable — the engine gate is now decision 35's permanent per-app check (operator: close or keep).
> Part C: R-644, R-763, R-764 closed (wger's two fixes proven on 9202; wger stays hidden for R-762); R-717 needs a lift
> command in `after_setup` first. Register 142 → 137 (0 opened, 5 closed).
> **2026-10-06 (midday) — the operator's ten answers (`09` §3 139–148, „A" for all; CC's own pick differed on 140, 146, > **2026-10-06 (midday) — the operator's ten answers (`09` §3 139–148, „A" for all; CC's own pick differed on 140, 146,
> 147).** Register 150 → 142 (2 opened: R-890 vaultwarden ladder, R-891 a stale CLAUDE.md line; 10 closed). Releases: > 147).** Register 150 → 142 (2 opened: R-890 vaultwarden ladder, R-891 a stale CLAUDE.md line; 10 closed). Releases:
+122 -57
View File
@@ -1,82 +1,147 @@
# REPORT — the operator's ten answers built (2026-10-06, rulings `09` §3 139–148) # REPORT — instruction files kept true; vaultwarden on the ladder; the burn-down continued (2026-10-06 afternoon)
| Part | Result | | Part | Result |
|---|---| |---|---|
| **A** — the box changes (1, 4, 5) | **done** — R-444 weekly trim (measured on demo-hp first, then built, delivered, seen working by itself), R-645 version skip, R-856 crash-boot grace; agent v0.149.0 + bundle, controller v0.300.0 + golden 0.300.0, hub v0.139.0, all on demo-hp, demo-felhom, Tester 1 | | **A** — the instruction-file rule (R-891, R-469, sweep) | **done** — rule in all four copies of `unprompted-work.md` §5 and `PROMPT-TEMPLATE.md` §9 rule 9; R-891 fixed and closed; R-469 re-read: NOT removable, narrowed (an operator question); sweep: 26 factual edits in four repos (list below). No permission prompt or refusal came up. |
| **B** — the backup leftovers (2) | **done** — runbook written; read-only listing of ep0: **no leftover exists**, nothing deleted, real counts unchanged; the box's warning names the runbook | | **B** — vaultwarden on the update ladder (R-890) | **done** — the test-box admin seed built (catalog `6b4877d`, red-proved); 1.36.0-alpine → 1.37.4-alpine proven on bench 9401 and on 9202; written by `--write-ladder` (catalog `ecb8552`); R-890 closed |
| **C** — the page sentences (6, 10) | **done** — mealie and Karakeep, hu + en, live in the catalog | | **C** — the burn-down | **3 closed** (R-644, R-763, R-764), **2 stopped with the reason written** (R-762 medium, R-717 needs a design) |
| **D** — the test tools (3, 7, 8, 9) | **done** — R-618 closed by ruling; R-734 marker list; R-624 bench seed proven on a recreated bench; R-502 full-run gate, first real run green with its decoy convicted |
| Rows before | Rows after | Opened | Closed | | Rows before | Rows after | Opened | Closed |
|---|---|---|---| |---|---|---|---|
| **150** | **142** | **2** (R-890, R-891) | **10** | | **142** | **137** | **0** | **5** (R-890, R-891, R-644, R-763, R-764) |
Closed: R-444, R-99, R-618, R-645, R-856, R-747, R-734, R-624, R-502, R-774.
## Baselines and rulings ## Baselines and rulings
Verified 11:07: felhom.eu `8f40b3ce26` (the operator's own „cleaned reports" commit on top of `fe998b8`), controller Verified at the start: felhom.eu `7d0dffcf34`, controller `36088fd82e` (v0.300.0), agent `cefdc731a4` (v0.149.0),
`13bda270c3` (v0.299.0), agent `37e98f452b` (v0.148.0), catalog `d1a148408f`, register 150. The ten rulings were recorded catalog `65130c6c03`; register 142. Read: every repo's `CLAUDE.md` and `.claude/rules/`, `REPORT.md`, R-890, R-891,
first (`09` §3 139–148, `8c65ff0c`), with the note that CC's own pick differed on 2, 8 and 9. R-469. The two rulings were recorded first as `09` §3 decisions 149 and 150, with the reviewer's error recorded in 150.
Architecture read: `09-update-architecture.md` §3 (decisions 16, 35, 42, 146), §6.4 part 4 (the ladder writer), §6.5
**The operator's cleanup removed every `REPORT*.md`.** Two felhom.eu checks read `REPORT.md`: the decoy test now plants a (the drill catalog).
missing file and removes it again (`8c65ff0c`), and this file is the session's own report, as the repo rule says.
## Part A ## Part A
- **R-444, measured first** (demo-hp, 09:14Z, `audits/ten-answers-2026-10-06/r444-measure.txt`): `pct fstrim 9201` rc 0 in **The rule** — `.claude/rules/unprompted-work.md` §5 „Instruction files", the operator's text verbatim plus the
24.4 s; thin pool 65.53 % → 33.40 %; 18/18 app probes 200, slowest 1.1 s. **Built** (agent `ee71abd`): weekly, due permission-check sentence; identical in felhom.eu, felhom-controller, app-catalog-felhom.eu and the workspace root's
Wednesday from 10:00 host-local, starts only 10:00–20:59, under the one-heavy-op gate (backup, restore-test, OS steps), unversioned copy (md5 `c1e6c881…` for all four). `documentation/PROMPT-TEMPLATE.md` §9, rule 9. felhom-agent has no copy
3 tries a week, persisted, reported as `guest_disk_trim`; ONE sudo rule `/usr/sbin/pct ^fstrim [0-9]+$`. Hub of the shared rule file (it never had one); adding one is a rule change, left for the operator.
(`a411cde7`): System page „Last disk trim". **Live:** `sudo -l` on demo-hp and demo-felhom allows the trim and refuses
`--ignore-mountpoints`, `;x`, a second vmid and `pct destroy`. The job's first catch-up try ran 2 minutes after the agent
update and failed (the bundle had not landed — expected); the hourly retry at 12:56 local logged
`fstrim: guest 9201 trimmed 2.1 GiB in 2.4s` and the hub page read „10 min ago · 2.1 GiB" (`r444-live.txt`).
- **R-645** (controller `2d63714`): every night leg skips an app whose pin is not what it runs; one amber line on the
backups page. Red-proved on the hand-lift shape. **Residual, stated:** once the boot reconciler starts the app on the
new version, pin = running again — a design question, not built.
- **R-856** (controller `c393d85` + agent route `GET /host/crash-guard`): after a crash boot, app mails wait 15 min.
Live through the real route: demo-hp logged the normal 90 s because its last crash boot (2026-10-05) was not this
start; demo-felhom logged a clean boot. **The crash branch was not shown live** (no crash allowed); tests cover it.
## Part B — ep0, read only **R-469 — not met.** R-463 closed (8 of 11 PostgreSQL apps converted by the box; zipline, adventurelog, immich stay on 16
by decision 42), but the engine gate now enforces decision 35: a PostgreSQL major passes only with a two-venue ladder
entry carrying `engine_conversion`. Removing it would let an unproven major ship, and the image refuses to start on the
old datadir. That is a loosened fence. Nothing is left for CC to build; the row asks the operator to close it (CC's pick)
or keep it.
Every snapshot of datastore `felhom-offsite`, both from the server's API and from the directories: 9 snapshots in 5 **Every instruction-file edit** (before → after, why). All factual; none loosens a rule.
namespaces, all ≥ 369,808,250 B, all verification `ok`, every directory with its manifest. **No phantom; nothing deleted.**
Runbook `runbooks/pbs-phantom-cleanup.md` (the list command, the two-part test, one `api delete` per proven phantom, the
before/after count control) + `runbooks/pbs-phantom-list.py`; the agent's WARN now names the runbook (`be398f9`).
## Part C and D felhom.eu
1. `CLAUDE.md` „Gates — ONE entry point": a list of ten gates + „`--fast` … today that is all of them" → the `GATES`
table is the list (nineteen gates); `--fast` skips the full-run-only gates and names them (today `iso-bootstrap`). Why:
`repo_gates.py` imported: 19 gates, `iso-bootstrap` not fast. **R-891.**
2. `CLAUDE.md` design pointer `architecture/01..05-*.md` → `01..11-*.md` (00 = the capability map). Why: 06–11 exist.
3. `.claude/rules/docs.md` „the locked design" `01..05` → `01..11`. Same reason.
4. `.claude/rules/website.md`: the list of what `site_gates.py` asserts gains „no embedded `<style>` blocks". Why: its
gate 7 (`site_gates.py` docstring).
5. `.claude/rules/unprompted-work.md`: §5 added; „Same wording lives in all three repos'" → names the three repos and the
workspace root. Why: four copies exist.
6. `documentation/runbooks/workspace-CLAUDE.md` (the workspace root `CLAUDE.md` links to it): standing rule 3 carried the
R-286 sentence group twice in a row; the second copy removed.
- **R-747, R-774** (catalog `ec72c9d`, live `1938921`): one first-step sentence each, hu + en, freeze updated. felhom-agent
- **R-734** (catalog `b0939cf`): the update test ignores listed marker files (immich's six `.immich`), each with a reason, 7. `CLAUDE.md` gates: „`reuse_refs_check` and `instructions_gate` … today that is all of them" → every gate in `GATES`
only when changed/added and ≤ 64 bytes; an unlisted file still counts (red-proved). (five: three shared, `published`, `release-complete`); `--fast` skips `published`. Why: `agent_gates.py:52-60`.
- **R-624** (catalog `aed80ee`): bench-only vaultwarden seed through its admin invite. The bench LXC 9401 no longer 8. `CLAUDE.md` decoy paragraph: `scripts/decoy_coverage_gate.py`, `documentation/audits/AUDIT-gate-decoys-…` → with
existed; recreated by its recorded recipe (stopped again afterwards). Proven: admin sign-in, invite and registration `felhom.eu/`. Why: neither exists in this repo.
200; the seed read back before and after; `.env` shredded, secret greps 0 with a working control; without the run flag 9. `.claude/rules/health-checks.md` (comment): „The single source is now felhom.eu/CLAUDE.md 'Code quality rules'" → the
the admin route is not tried (`inconclusive`). The move used to drive it (1.36.0-alpine → 1.36.0) failed its health copies are felhom.eu `hub.md` and the controller's `gates.md`. Why: that section holds no health-check rule.
check — the non-alpine image fails the alpine health check; that is the chosen target, not the seed. **New question:
R-890.** felhom-controller
- **R-502** (felhom.eu `9d39faab`): `iso-bootstrap`, full runs only, NOT CHECKED without docker or the image; the image 10. `.claude/rules/gates.md`: `python3 controller/scripts/controller_gates.py` (from `controller/`) →
`felhom-iso-assistant:trixie` was built on DooPlex; **first real run: 73 checks green, the built-in decoy convicted.** `python3 scripts/controller_gates.py` from `controller/` (or the long path from the repo root). Why: the old path does
- **R-618:** closed by the ruling; nothing to build. not exist from `controller/`; the runner's usage line and CI use the new one.
11. `.claude/rules/gates.md`: the list of ten local gates → a pointer to `GATES` (fourteen local + three shared; the
advisory `golden-notice`). Why: `controller_gates.py:71-101`.
12. `.claude/rules/gates.md`: the shared list gains `observations_gate.py`. Why: `controller_gates.py:87`.
13. `.claude/rules/gates.md` (two lines): decoy gate and audit paths with `felhom.eu/`.
14. `CLAUDE.md` Commands table: the same command fix as 10.
15. `CLAUDE.md`: „Protected stacks (traefik, cloudflared, felhom-controller)" → „…, and always samba". Why:
`internal/config/config.go` `alwaysProtectedStacks`.
16. `CLAUDE.md` design pointer `architecture/01/02/03-*.md` → `architecture/NN-*.md` (01–03; 07, 09, 10).
17. `.claude/rules/unprompted-work.md`: as 5.
app-catalog-felhom.eu
18. `CLAUDE.md`: „each holding exactly `docker-compose.yml` + `.felhom.yml`" → without „exactly", plus `steps/<StepKey>.yml`
for a superseded ladder step. Why: 20 templates have `steps/`.
19. „Only the two template files sync" → „Only the template files sync".
20. „60 checks in 10 groups" → 61. Why: `NEW-APP-CHECKLIST.md` row-count table.
21. „runs all four gates … scopes the two gates that accept scoping" → every gate in `GATES` (fourteen); scopes every gate
that accepts an app scope (eight). Why: `catalog_gates.py:88-131`.
22. „CI was rejected for now … R-161 stays open at reduced scope" → CI exists and re-runs `--fast`; R-161 closed 2026-10-05.
23. „`--fast` … is gate 1 and the engine-major gate … the other two" → every gate except image-resolvable and
volume-persistence.
24. The paragraph „There is no gate for this yet and that is a known gap" removed. Why: `check-catalog-since.py` is a
registered gate (R-452, closed); the same section already names it.
25. „The four MariaDB services (…)" → the five (grimmory added), noting the gate judges by glob; „The eleven PostgreSQL
services" → twelve, postgis and immich's image counted. Why: `templates/*/docker-compose.yml`.
26. Decoy gate and audit paths with `felhom.eu/`; `.claude/rules/unprompted-work.md` as 5.
Sweep notes not acted on: word-for-word repeated paragraphs (R-421 in three CLAUDE.md files; „Presence is not success" in
the controller's `backup-paths.md`) — repetition, not a wrong fact.
## Part B — R-890
**Built** (catalog `6b4877d`): `box_admin_seed_allowed(w, app)` allows the admin seed on a box walk only when all five
hold — not the bench venue; `FELHOM_BOX_ADMIN_SEED=1`; the walk targets demo-hp guest 9202 (never 9201, a demo box's
household-shaped guest); THIS run installed the app (`box_walk.DEPLOYED_THIS_RUN`, set on the deploy's 202); the box's
`controller.yaml` names the drill catalog. The invite runs inside the box: the token is read from the container's
environment into a shell variable and handed to curl on stdin; the admin cookie is in a 0600 file that is shredded
(vaultwarden marks it Secure, so it is sent by hand over the container's plain-http address); only HTTP codes come back.
**The Tester 1 box is allowed by the ruling, but the walk has no route to it** (`box_walk.py` drives demo-hp guests
only) — written in the code, not built. Tests `BoxAdminSeedGuard` (6); **red-proof:** a guard returning `(True, "")`
fails three tests (`test_each_condition_alone_refuses`, `test_a_household_shaped_box_is_never_seeded`, and the bench's
`test_the_box_walk_never_signs_in_as_admin`).
**Proven:**
- Bench LXC 9401 (harness v5, 600 s memory watch): verdict `proven`; seed read back before and after; healthy in 30.9 s;
anon peak 16.3 % (cgroup 22.3 %); 0 kills, 0 restarts; no file changed.
- Box 9202 (drill catalog): self-registration 400; the in-box admin sign-in and invite → (200, session yes, 200);
invited registration 200; seed read back; guarded Update backing-up → pulling → copying → verifying → done in 12.3 s;
seed read back; badge „Frissítés elérhető — 80 napja" → „Naprakész"; removed through the product.
- `upgrade-test.py --write-ladder` from both verdicts → `vaultwarden/server:1.37.4-alpine`, `catalog_since` 2026-10-06,
the first ladder entry (catalog `ecb8552`); `check-image-resolvable.py vaultwarden` OK.
- **Who gets it:** no box reports vaultwarden today (hub DB copy with its WAL: last vaultwarden telemetry 2026-09-18,
demo-hp; control: the latest telemetry row of any app 13:51 today; the copy shredded). Read directly: demo-hp 9201 and
demo-felhom 9201 run no vaultwarden container.
## Part C
- **R-644 closed:** no gokapi on 9202 (no container, no volume; control: the six running containers are listed).
- **R-763 closed** (fix from 2026-10-05, proven now): wger installed fresh on 9202; its own process reads
`ALLOW_REGISTRATION False`, `ALLOW_GUEST_USERS False`; straight at the app, a stranger's sign-up GET and a CSRF-valid
POST both redirect to the features page (control: the same CSRF token passes on the login form, 200); three anonymous
dashboard visits → login; users 1 before and after (2 → 4 before the fix).
- **R-764 closed** (proven now): mail off → healthy, 0 restarts, console backend; mail on (the toggle's injection) →
settings load, smtp backend, port 2526, TLS off. Not measured: a real reset mail (9202 has no app-mail).
- **R-762 stopped:** still 404 on wger 2.7; the image has no whitenoise and runs Django's `runserver`, so it needs a
second container for `/static` and `/media` — medium. Note added to the row.
- **R-717 stopped — needs a design:** the controller's `after_setup` command form runs only when the lock is set; the
household's 15-minute window cannot reopen a database switch. Note added to the row.
## Said plainly ## Said plainly
- **CI run 1423 (felhom.eu, the hub-manifest commit) was red** on the golden gate: it read controller v0.300.0 a few minutes - **A merge conflict was committed into the DRILL catalog** (vaultwarden's `.felhom.yml`, by a `commit -am` after a
before the golden 0.300.0 record was committed. Run 1425 on the next push was green. failed merge). Found at once by its conflict markers and fixed in the next drill commit; the drill was then reset to
- The agent update's first trim attempt failed by design (the sudo rule rides the bundle, which came 10 minutes later). live. The live catalog was never touched by it.
On a box that gets the agent but not the bundle, the trim warns weekly and its capability reads degraded. - My first wger user-count command had a quoting bug (it printed a Python NameError); the reads were repeated through a
- **R-891:** felhom.eu `CLAUDE.md` still says `--fast` runs every gate — untrue since `iso-bootstrap`. An instruction file; file. The stranger checks were not affected.
left for the operator.
## CI, last commit of every repo ## CI, last commit of every repo
felhom.eu: this commit (checked by its commit after the push); controller `36088fd` → 1426 success; agent `cefdc73` → 1427 catalog `ecb8552` → 1433 success (and `6b4877d` → 1430 success); agent `3e8ebeb` → 1431 success; controller `3124278`
success; catalog `65130c6` → 1428 success. → 1432 success; felhom.eu `a29ee9dd` → 1434 (checked before the next push), and this commit (checked after the push).
## Teardown ## Teardown
Drill VM: build guest destroyed, secrets shredded, off, `virgin`. Bench 9401: stopped (kept; its evidence copied off). Machine: 9202 back on the live catalog (`controller.yaml` restored byte-identical), vaultwarden and wger removed through
Scratch 9202: not used. ep0: read only. demo-hp / demo-felhom: the deliveries, one measured trim, `sudo -l` checks. the product (no container, no volume), the same six containers as at the start. Bench 9401: 0 containers, `.env` gone,
Hub: the deploy, the vouches, three floors per release. Scratch secrets shredded at the end. stopped. Drill: reset to live (`ecb8552` = `ecb8552`). Host: nothing else. Hub: nothing changed (one read-only DB copy,
shredded). Scratch secrets shredded at the end.
+20 -3
View File
@@ -2,8 +2,25 @@
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.** **Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
**Updated 2026-10-06 13:15: every box of ours healthy on hub 0.139.0, agent 0.149.0, controller 0.300.0. The open-items **Updated 2026-10-06 14:15: every box of ours healthy on hub 0.139.0, agent 0.149.0, controller 0.300.0. The open-items
list is at 142. Report: `REPORT.md`.** list is at 137. Report: `REPORT.md`.**
## Afternoon (2026-10-06): your two answers built; the list at 137
- **New standing rule (your answer):** a session now fixes a wrong fact in an instruction file by itself and lists the
change in its report. It still may not loosen a safety rule. The rule is in all four copies of the shared rule file and
in the brief template. No permission prompt came up.
- **The wrong line is fixed**, and twenty-five more wrong facts across the four repos (gate lists, paths, counts).
Every edit is listed in the report.
- **Vaultwarden is on the update list:** its first step (1.36.0 → 1.37.4) passed on the test bench and on the scratch box,
using the admin seed you allowed. No box runs vaultwarden today, so no household gets it now.
- **wger's two fixes are proven** on the scratch box: a stranger cannot sign up, visits make no guest accounts, and mail
works when switched on. wger stays hidden: it still shows no styles or photos.
**Needs you (none urgent):**
1. **R-469** — the catalog's database-version check cannot be removed any more: it is now how decision 35's per-app
proof is enforced. (A) close the row and keep the check (my pick), or (B) keep the row open. If nothing: the row
stays open; the check works either way.
## Midday (2026-10-06): your ten answers built; the list at 142 ## Midday (2026-10-06): your ten answers built; the list at 142
@@ -15,7 +32,7 @@ list is at 142. Report: `REPORT.md`.**
for the day one appears. for the day one appears.
- **Three of your answers differed from my picks (2, 8 and 9).** Your answer governs, and each is recorded. - **Three of your answers differed from my picks (2, 8 and 9).** Your answer governs, and each is recorded.
**Needs you (none urgent):** **Needs you (none urgent):** *(both answered 2026-10-06 13:25 and done)*
1. **R-890** — a vaultwarden update can still not be written to the update list: the list needs a proof on the scratch box 1. **R-890** — a vaultwarden update can still not be written to the update list: the list needs a proof on the scratch box
too, and your ruling keeps the admin password on the test bench only. Pick: allow it on scratch box 9202 as well. If too, and your ruling keeps the admin password on the test bench only. Pick: allow it on scratch box 9202 as well. If
nothing: vaultwarden updates stay manual. nothing: vaultwarden updates stay manual.
@@ -0,0 +1,45 @@
lifecycle on the box: None deployed=False
settings read by wger's own process:
before: NameError: name 'USERS' is not defined
stranger, straight at the app: GET registration: 302 http://172.18.0.6:8000/en/software/features
stranger, straight at the app: POST registration: 403
stranger, straight at the app: anonymous GET dashboard 1: 302 http://172.18.0.6:8000/user/login?next=/en/dashboard
stranger, straight at the app: anonymous GET dashboard 2: 302 http://172.18.0.6:8000/user/login?next=/en/dashboard
stranger, straight at the app: static css: 404
stranger, straight at the app: static root size: 4.0K /home/wger/static
after: NameError: name 'USERS' is not defined
stranger account exists: STRANGER False
restarts: 0 healthy
read 1: level=INFO ts=2026-10-06 13:56:58,292 module=apps path=/home/wger/.local/lib/python3.12/site-packages/axes/apps.py line=53 message=AXES: BEGIN version 8.3.1, blocking by username
SETTINGS ALLOW_REGISTRATION=False ALLOW_GUEST_USERS=False EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend ENABLE_EMAIL-env=False DEBUG=False
USERS 1 ['admin']
anonymous GET /en/dashboard 1: 302 http://172.18.0.6:8000/user/login?next=/en/dashboard
anonymous GET /en/dashboard 2: 302 http://172.18.0.6:8000/user/login?next=/en/dashboard
anonymous GET /en/dashboard 3: 302 http://172.18.0.6:8000/user/login?next=/en/dashboard
anonymous GET /en/user/demo-entries: 302 http://172.18.0.6:8000/en/software/features
read 2: level=INFO ts=2026-10-06 13:57:06,003 module=apps path=/home/wger/.local/lib/python3.12/site-packages/axes/apps.py line=53 message=AXES: BEGIN version 8.3.1, blocking by username
SETTINGS ALLOW_REGISTRATION=False ALLOW_GUEST_USERS=False EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend ENABLE_EMAIL-env=False DEBUG=False
USERS 1 ['admin']
GET login (for a CSRF cookie): 200
csrf cookie: yes
POST registration with a valid CSRF: 302 http://172.18.0.6:8000/en/software/features
CONTROL - POST login with the same CSRF and a wrong password: 200
USERS 1
settings loaded with the mail toggle's injection (ENABLE_EMAIL=True, EMAIL_HOST set), in the running wger:
/home/wger/src/settings/main.py:117: UserWarning: JWT_PRIVATE_KEY / JWT_PUBLIC_KEY are not set. JWT authentication will not work until you run `./manage.py generate-jwt-keys` and add the output to your environment.
warnings.warn(
MAIL-ON SETTINGS: ENABLE_EMAIL-env=True EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend EMAIL_HOST=felhom-relay.invalid EMAIL_PORT=2526 USE_TLS=False
rc=0
Traceback (most recent call last):
File "<stdin>", line 14, in <module>
File "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/box_walk.py", line 502, in remove
c1, d1 = ctl("POST", f"/api/stacks/{name}/stop")
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/box_walk.py", line 88, in ctl
sess = open(f"{SC}/sess{os.getpid()}.txt").read().strip()
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/da0e2df0-3072-4d3e-bf10-c475ac1e5ae4/scratchpad/sc890/sess4190727.txt'
13:58:17 [X] stop -> 200 {'ok': True, 'message': 'Stack wger stop completed'}
13:58:49 [X] remove (with drive data) -> 200 {'ok': True, 'data': {'removed': 'wger', 'volumes_removed': ['wger_wger_data', 'wger_wger_media'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note
13:58:57 [X] after remove: deployed=False leftovers='/opt/docker/stacks/wger'
remove -> 200
@@ -21,3 +21,9 @@ harness); the bench has 0 containers and is stopped.
## Part C ## Part C
- R-644: `C/R-644-9202-live.txt` — no gokapi on 9202 any more. - R-644: `C/R-644-9202-live.txt` — no gokapi on 9202 any more.
- R-763 / R-764 / R-762: `C/wger.txt` (`tools/wgerwalk.py`; wger un-hidden in the DRILL only, removed after).
## Teardown
`box/T1-repoint-live.txt` (9202 back on the live catalog, byte-identical `controller.yaml`; the same six containers),
`box/T2-drill-reset.txt` (drill = live).
@@ -0,0 +1,11 @@
26: repo_url: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
3
RESTORED-IDENTICAL
0
filebrowser Up 5 hours (healthy)
felhom-controller Up 16 seconds (healthy)
paperless-webserver Up 11 hours (healthy)
paperless-postgres Up 11 hours (healthy)
paperless-redis Up 11 hours (healthy)
traefik Up 28 hours
@@ -0,0 +1 @@
drill=ecb8552 live=ecb8552
@@ -0,0 +1,62 @@
"""wgerwalk.py — R-763 / R-764 / R-762 on scratch 9202 (drill catalog = live + wger un-hidden, DRILL only).
Install wger through the product, then, STRAIGHT AT THE APP inside the box (a stranger who got past the box's own gate —
the strictest case; the family gate would stop him first): the sign-up page and form, two anonymous dashboard visits,
the user count before and after (wger's own ORM). The settings wger read (its own process). The static and media read
(R-762). Removed through the product at the end. Evidence: ../C/wger.txt."""
import os, sys
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
HERE = os.path.dirname(os.path.abspath(__file__))
log = open(os.path.join(HERE, "..", "C", "wger.txt"), "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
ORM = ("cd /home/wger/src && DJANGO_SETTINGS_MODULE=settings.main python3 -c "
"\"import django; django.setup(); from django.contrib.auth.models import User; print('USERS', User.objects.count())\"")
SETTINGS = ("cd /home/wger/src && DJANGO_SETTINGS_MODULE=settings.main python3 -c "
"\"import django; django.setup(); from django.conf import settings as s; "
"print('ALLOW_REGISTRATION', s.WGER_SETTINGS.get('ALLOW_REGISTRATION'), 'ALLOW_GUEST_USERS', s.WGER_SETTINGS.get('ALLOW_GUEST_USERS'), "
"'EMAIL_BACKEND', s.EMAIL_BACKEND, 'DEBUG', s.DEBUG)\"")
def users():
out = w.guest(f"docker exec wger sh -c '{ORM}' 2>&1 | tail -1")
return out.strip()
STRANGER = r"""set -u
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' wger | awk '{print $1}')
H="Host: fitness.enkisfelhom.hu"; P="X-Forwarded-Proto: https"
J=$(mktemp)
echo "GET registration: $(curl -s -o /dev/null -c $J -b $J -H "$H" -H "$P" -w '%{http_code} %{redirect_url}' http://$ip:8000/en/user/registration)"
tok=$(grep csrftoken $J | awk '{print $7}')
echo "POST registration: $(curl -s -o /dev/null -c $J -b $J -H "$H" -H "$P" -H "Referer: https://fitness.enkisfelhom.hu/en/user/registration" -w '%{http_code} %{redirect_url}' --data "csrfmiddlewaretoken=$tok&username=stranger1&email=stranger1@gate.invalid&password1=Str4nger-Pass-991&password2=Str4nger-Pass-991" http://$ip:8000/en/user/registration)"
rm -f $J
for i in 1 2; do echo "anonymous GET dashboard $i: $(curl -s -o /dev/null -H "$H" -H "$P" -w '%{http_code} %{redirect_url}' http://$ip:8000/en/dashboard)"; done
echo "static css: $(curl -s -o /dev/null -H "$H" -H "$P" -w '%{http_code}' http://$ip:8000/static/css/workout-manager.css)"
echo "static root size: $(docker exec wger du -sh /home/wger/static 2>&1 | tail -1)"
"""
w.login()
w.sync_rescan("wger")
st = w.stack("wger")
say(f"lifecycle on the box: {(st.get('metadata') or {}).get('lifecycle')} deployed={st.get('deployed')}")
if not w.deploy("wger", "fitness"):
sys.exit(say("RESULT install did not complete") or 1)
w.wait_app("fitness", "/", tries=60)
say("settings read by wger's own process:", w.guest(f"docker exec wger sh -c \"{SETTINGS}\" 2>&1 | tail -1").strip())
say("before:", users())
for line in w.guest(STRANGER).strip().splitlines():
say(" stranger, straight at the app:", line)
say("after:", users())
say("stranger account exists:", w.guest("docker exec wger sh -c \"cd /home/wger/src && DJANGO_SETTINGS_MODULE=settings.main python3 -c "
"\\\"import django; django.setup(); from django.contrib.auth.models import User; "
"print('STRANGER', User.objects.filter(username='stranger1').exists())\\\"\" 2>&1 | tail -1").strip())
say("restarts:", w.guest("docker inspect -f '{{.RestartCount}} {{.State.Health.Status}}' wger").strip())
if not os.environ.get("KEEP"):
say(f"remove -> {w.remove('wger')}")
+9
View File
@@ -26,6 +26,15 @@
--- ---
## 2026-10-06 (afternoon) — wger's two fixes proven on 9202
The full text of every row below: `git show a29ee9dd33:documentation/backlog/OPEN-ITEMS.md`.
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-764** | **[P3-LOW] wger sends no mail: its mail backend is the console, so a password-reset mail never leaves the box, and the template maps no SMTP.** (P4) | CLOSED 2026-10-06 — PROVEN ON 9202 (the fix pushed 2026-10-05, catalog `db88ee9`) | Mail OFF (a fresh install): healthy, 0 restarts, console backend, ENABLE_EMAIL False. Mail ON (the toggle's injection, ENABLE_EMAIL=True + EMAIL_HOST, in the running wger): settings load, smtp backend, port 2526, TLS off — `audits/r890-instructions-2026-10-06/C/wger.txt`. Not measured: a reset mail through a real relay (9202 has no app-mail). |
| **R-763** | **[P2-MEDIUM] On wger a stranger can make an account after the household's setup, and every anonymous visit to the dashboard creates a guest account.** (P3) | CLOSED 2026-10-06 — PROVEN ON 9202 (the fix pushed 2026-10-05, catalog `1968527`) | wger installed fresh on 9202 (drill = live + un-hidden): wger's own process reads ALLOW_REGISTRATION False, ALLOW_GUEST_USERS False; straight at the app a stranger's sign-up GET and a CSRF-valid POST both redirect to /en/software/features (control: the same CSRF passes on the login form), three anonymous dashboard visits redirect to login, users 1 before and after (was 2 -> 4 before the fix) — `audits/r890-instructions-2026-10-06/C/wger.txt`. Family-member adding (checklist 3.7) not measured: R-759. wger stays hidden (R-762). |
## 2026-10-06 (afternoon) — instruction files kept true; vaultwarden on the ladder ## 2026-10-06 (afternoon) — instruction files kept true; vaultwarden on the ladder
The full text of every row below: `git show 7d0dffcf34:documentation/backlog/OPEN-ITEMS.md`. The full text of every row below: `git show 7d0dffcf34:documentation/backlog/OPEN-ITEMS.md`.
+2 -4
View File
@@ -129,11 +129,10 @@ stopping line that lies.
| **R-562** | Apps & catalog | P3 | **[P3-LOW] Dates and sizes are not formatted for any locale — and the Hungarian pages disagree with themselves.** FOUND 2026-09-17 by the i18n inventory §2.8: the two template date layouts differ (`2006. 01. 02. 15:04` Hungarian vs `2006-01-02 15:04` ISO); 10 layout literals in `internal/web` Go and 25 elsewhere pick formats ad hoc; sizes print a decimal POINT (`%.1f GB`, 4 helpers) where Hungarian uses a comma; `timeAgo`/`nextRunLabel`/`pruneLabel` produce Hungarian words outside the three converted pages. Not changed by v0.247.0 (Hungarian bytes are frozen by the parity rule). **Fix shape:** one date and one size formatter per language in `internal/i18n`, the Hungarian output deliberately changed in ONE reviewed release with the parity fixtures re-captured for that release only and the change named in its CHANGELOG. Needs an operator word on the Hungarian format (comma, date style). | **READY - rank P3-LOW; owner: CC** | — | — | CC | | **R-562** | Apps & catalog | P3 | **[P3-LOW] Dates and sizes are not formatted for any locale — and the Hungarian pages disagree with themselves.** FOUND 2026-09-17 by the i18n inventory §2.8: the two template date layouts differ (`2006. 01. 02. 15:04` Hungarian vs `2006-01-02 15:04` ISO); 10 layout literals in `internal/web` Go and 25 elsewhere pick formats ad hoc; sizes print a decimal POINT (`%.1f GB`, 4 helpers) where Hungarian uses a comma; `timeAgo`/`nextRunLabel`/`pruneLabel` produce Hungarian words outside the three converted pages. Not changed by v0.247.0 (Hungarian bytes are frozen by the parity rule). **Fix shape:** one date and one size formatter per language in `internal/i18n`, the Hungarian output deliberately changed in ONE reviewed release with the parity fixtures re-captured for that release only and the change named in its CHANGELOG. Needs an operator word on the Hungarian format (comma, date style). | **READY - rank P3-LOW; owner: CC** | — | — | CC |
| **R-612** | Apps & catalog | P3 | **[P1-HIGH] `wishlist` cannot be signed up to on a fresh Felhom install, the deploy reports SUCCESS, and the error the customer sees is a LIE.** MEASURED 2026-09-21 on guest 9202 while seeding for the power-cut drill. The image's first-boot `pnpm prisma db seed` is **`Killed` — OOM at the catalog's `mem_limit: 128M`**. Without it the `Role` and `Group` rows are absent, so **every** signup fails. **The message the user is shown is `User with username or email already exists`** while the container log says the real cause: `FOREIGN KEY constraint violated`. A household would conclude the account already exists and try to recover a password that was never created. **The controller reports the app running and HEALTHY throughout, and the deploy reported successful** — so nothing on the box says anything is wrong. Repaired on the scratch guest only, to unblock seeding: memory raised to 512 M, the image's own seed re-run, memory put back to 128 M. **The catalog was NOT changed** — the fix is a memory-limit question for the catalog and is deliberately left to a session that can measure the real ceiling rather than guess it. **Needs: the actual peak RSS of that seed, then a `mem_limit` that clears it, plus a check that the seed's failure is not silent.** **— FIXED 2026-09-23 night (catalog `a5a729a`): 512M.** Measured on the bench: the seed peaks at 312–345M and was OOM-killed at 128M on every run (kernel `oom_kill` 3); running, the app sits at ~115M (90 % of the old limit alone). On 9202 at 128M the kill came AFTER the Role/Group rows this time, so the sign-up lie did NOT reproduce — the kill is timing-dependent, the fault is memory (the brief's claim held). At 512M the seed completes (`The seed command has been executed`), and wishlist then moved v0.66.0 → v0.67.1 with its test record. **Still open:** a failed first-boot seed is invisible to the box — nothing reads the seed's exit. | **READY — P3, narrowed to making a failed seed visible; owner: CC (catalog)** **Re-ranked 2026-10-03: P1->P3: the memory fix shipped (catalog a5a729a); only the silent-seed detection remains.** | — | — | CC | | **R-612** | Apps & catalog | P3 | **[P1-HIGH] `wishlist` cannot be signed up to on a fresh Felhom install, the deploy reports SUCCESS, and the error the customer sees is a LIE.** MEASURED 2026-09-21 on guest 9202 while seeding for the power-cut drill. The image's first-boot `pnpm prisma db seed` is **`Killed` — OOM at the catalog's `mem_limit: 128M`**. Without it the `Role` and `Group` rows are absent, so **every** signup fails. **The message the user is shown is `User with username or email already exists`** while the container log says the real cause: `FOREIGN KEY constraint violated`. A household would conclude the account already exists and try to recover a password that was never created. **The controller reports the app running and HEALTHY throughout, and the deploy reported successful** — so nothing on the box says anything is wrong. Repaired on the scratch guest only, to unblock seeding: memory raised to 512 M, the image's own seed re-run, memory put back to 128 M. **The catalog was NOT changed** — the fix is a memory-limit question for the catalog and is deliberately left to a session that can measure the real ceiling rather than guess it. **Needs: the actual peak RSS of that seed, then a `mem_limit` that clears it, plus a check that the seed's failure is not silent.** **— FIXED 2026-09-23 night (catalog `a5a729a`): 512M.** Measured on the bench: the seed peaks at 312–345M and was OOM-killed at 128M on every run (kernel `oom_kill` 3); running, the app sits at ~115M (90 % of the old limit alone). On 9202 at 128M the kill came AFTER the Role/Group rows this time, so the sign-up lie did NOT reproduce — the kill is timing-dependent, the fault is memory (the brief's claim held). At 512M the seed completes (`The seed command has been executed`), and wishlist then moved v0.66.0 → v0.67.1 with its test record. **Still open:** a failed first-boot seed is invisible to the box — nothing reads the seed's exit. | **READY — P3, narrowed to making a failed seed visible; owner: CC (catalog)** **Re-ranked 2026-10-03: P1->P3: the memory fix shipped (catalog a5a729a); only the silent-seed detection remains.** | — | — | CC |
| **R-676** | Apps & catalog | P3 | **[P3-LOW] Watch: immich's first start restarted 12 times — decision 28's crash-loop stop (6 in 10 min) would stop it.** From the 2026-09-17 chaos night (DB connection dropped during the first-start geocoding import on a 6 GB guest; it did not recover that night). No healthy app in any drill evidence restarts on a first start (1831 samples, 40 live containers), so the threshold stands; this row exists so the first immich install under v0.269.x is watched. `audits/night-2026-09-24/A3/40-first-start-restarts.txt` **2026-09-25 night (read from source, v0.271.0): a DEPLOY's first start is NOT covered by decision 28's suppression** — `Deploying` clears when `compose up -d` returns (`deploy.go` "Clear deploying flag"), and `ObserveUnhealthy` then samples the app; an automatic update's step, verify and undo ARE covered (`Updating`, pinned by `TestD28_NoCrashLoopStopDuringAnAutomaticStep`). So a first start that restarts ≥ 6 times in 10 min is stopped — which R-676 already accepts for a broken first start; a healthy slow first start would be stopped too. **-- 2026-09-30: the first-start restarts are explained.** immich's first-start geodata import OOM-kills its database at 512M on a guest with no swap (R-732, measured: 61–104 kills); the 2026-09-17 chaos-night case (DB connection dropped during the import on a 6 GB guest) fits it. Fixed in the catalog (`56c4888`, 768M). The watch itself (decision 28 on a DEPLOY's first start) is unchanged. | **OPEN — P3; owner: CC (watch)** | — | — | CC | | **R-676** | Apps & catalog | P3 | **[P3-LOW] Watch: immich's first start restarted 12 times — decision 28's crash-loop stop (6 in 10 min) would stop it.** From the 2026-09-17 chaos night (DB connection dropped during the first-start geocoding import on a 6 GB guest; it did not recover that night). No healthy app in any drill evidence restarts on a first start (1831 samples, 40 live containers), so the threshold stands; this row exists so the first immich install under v0.269.x is watched. `audits/night-2026-09-24/A3/40-first-start-restarts.txt` **2026-09-25 night (read from source, v0.271.0): a DEPLOY's first start is NOT covered by decision 28's suppression** — `Deploying` clears when `compose up -d` returns (`deploy.go` "Clear deploying flag"), and `ObserveUnhealthy` then samples the app; an automatic update's step, verify and undo ARE covered (`Updating`, pinned by `TestD28_NoCrashLoopStopDuringAnAutomaticStep`). So a first start that restarts ≥ 6 times in 10 min is stopped — which R-676 already accepts for a broken first start; a healthy slow first start would be stopped too. **-- 2026-09-30: the first-start restarts are explained.** immich's first-start geodata import OOM-kills its database at 512M on a guest with no swap (R-732, measured: 61–104 kills); the 2026-09-17 chaos-night case (DB connection dropped during the import on a 6 GB guest) fits it. Fixed in the catalog (`56c4888`, 768M). The watch itself (decision 28 on a DEPLOY's first start) is unchanged. | **OPEN — P3; owner: CC (watch)** | — | — | CC |
| **R-762** | Apps & catalog | P3 | **[P2-MEDIUM] wger serves no CSS or JavaScript and no uploaded photo: every static file and every `/media/` file answers 404.** MEASURED 2026-10-01 on 9202 (drill catalog, the live template `82fff32`, wger 2.7), found by checklist rows 1.7 and 2.8: the login page links `/static/css/workout-manager.css`, `/static/bootstrap-compiled.css` — both 404 through traefik; the static root inside the container is empty (4 KB). A progress photo posted to `/api/v2/gallery/` answered 201 and the file is on the media volume, but `GET /media/gallery/…png` answers 404 signed in, without a session, and straight at the app inside the container. **Cause, read in the image:** the entrypoint runs `collectstatic` only when `DJANGO_DEBUG == "False"` and the template sets no `DJANGO_DEBUG`; and wger serves `/media/` only in development (`urls.py:393` „served like this during development only”) — upstream's production setup puts nginx in front for `/static` and `/media`. So the household gets an unstyled app and photos that never show. The same lines stand since the template was written (the 2026-09-29 template too). Not checked: whether any box runs wger (on 2026-09-30 none reported to the hub). **Needs:** `DJANGO_DEBUG=False` (collectstatic) and something that serves `/static` + `/media` (upstream's nginx sidecar, or the gunicorn switch of R-755 plus a static server), proven on the bench and on 9202 with a page that loads its CSS and a photo read back. Owner decides together with R-755 (same server question). `audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt`, `C/C4-seed-photo-size.txt` **-- 2026-10-01 (operator):** wger is `lifecycle: hidden` until this and its twin are fixed (catalog `55b8c8a`; read back on 9202: not on the app list, mealie control present). **Merged 2026-10-05 from R-755 (duplicate):** `templates/wger/docker-compose.yml` still sets no `WGER_USE_GUNICORN` — the gunicorn switch is the same server question. | **READY — rank P2-MEDIUM; owner: CC (catalog)** **Re-ranked 2026-10-03: P2→P3: wger is hidden from installs and no box runs it; needed only before it is offered again.** | — | — | CC | | **R-762** | Apps & catalog | P3 | **[P2-MEDIUM] wger serves no CSS or JavaScript and no uploaded photo: every static file and every `/media/` file answers 404.** MEASURED 2026-10-01 on 9202 (drill catalog, the live template `82fff32`, wger 2.7), found by checklist rows 1.7 and 2.8: the login page links `/static/css/workout-manager.css`, `/static/bootstrap-compiled.css` — both 404 through traefik; the static root inside the container is empty (4 KB). A progress photo posted to `/api/v2/gallery/` answered 201 and the file is on the media volume, but `GET /media/gallery/…png` answers 404 signed in, without a session, and straight at the app inside the container. **Cause, read in the image:** the entrypoint runs `collectstatic` only when `DJANGO_DEBUG == "False"` and the template sets no `DJANGO_DEBUG`; and wger serves `/media/` only in development (`urls.py:393` „served like this during development only”) — upstream's production setup puts nginx in front for `/static` and `/media`. So the household gets an unstyled app and photos that never show. The same lines stand since the template was written (the 2026-09-29 template too). Not checked: whether any box runs wger (on 2026-09-30 none reported to the hub). **Needs:** `DJANGO_DEBUG=False` (collectstatic) and something that serves `/static` + `/media` (upstream's nginx sidecar, or the gunicorn switch of R-755 plus a static server), proven on the bench and on 9202 with a page that loads its CSS and a photo read back. Owner decides together with R-755 (same server question). `audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt`, `C/C4-seed-photo-size.txt` **-- 2026-10-01 (operator):** wger is `lifecycle: hidden` until this and its twin are fixed (catalog `55b8c8a`; read back on 9202: not on the app list, mealie control present). **Merged 2026-10-05 from R-755 (duplicate):** `templates/wger/docker-compose.yml` still sets no `WGER_USE_GUNICORN` — the gunicorn switch is the same server question. **-- 2026-10-06 (afternoon), measured again on 9202 (live template, wger 2.7):** `/static/css/workout-manager.css` 404 straight at the app, `/home/wger/static` 4 KB, settings `DEBUG False`; the image has gunicorn but no whitenoise and runs Django's `runserver` (no `WGER_USE_GUNICORN`, R-755). So `DJANGO_DEBUG=False` alone would collect the files and still serve none: the fix needs a server for `/static` + `/media` (a second container) — medium, not taken. `audits/r890-instructions-2026-10-06/C/wger.txt`. | **READY — rank P2-MEDIUM; owner: CC (catalog)** **Re-ranked 2026-10-03: P2→P3: wger is hidden from installs and no box runs it; needed only before it is offered again.** | — | — | CC |
| **R-76** | Apps & catalog | P4 | **FileBrowser-created folders break the setgid chain, and a drop-zone's mode is not stable** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size S, roadmap state `idea (surfaced by the R-75 spike, 2026-07-26)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** | — | Two related findings from `audits/SPIKE-catalog-data-paths-2026-07-26.md` P3/P5, both **pre-existing** and deliberately left alone by that spike. **(a)** FileBrowser Quantum 1.3.3 creates files `0644` and folders `0755` and does **not** propagate the setgid bit — even though the entrypoint wrapper's `umask 002` really is in effect (`/proc/1/status` `Umask: 0002`). Group inheritance itself works (a file uploaded into a 2775 group-100 dir landed group 100, not the process gid 1000), so the convention's *group* half holds and only its *mode* half is lost. The consequence is proven with a control: inside a UI-created `0755` folder a gid-1000 process's file landed group **1000**, while the identical write into the 2775 parent landed group **100**. So **any folder a customer creates through FileBrowser breaks the shared-group chain one level down.** Latent today — every userdata-touching catalog app that declares an identity declares uid/gid **1000**, the same uid FileBrowser runs as, so owner permissions mask it; it bites the day a content app runs as a different non-root uid with gid 1000. The comment at `infra/infra.go:156` is right that the image ignores `-e UMASK` but does not say t | CC | | **R-76** | Apps & catalog | P4 | **FileBrowser-created folders break the setgid chain, and a drop-zone's mode is not stable** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size S, roadmap state `idea (surfaced by the R-75 spike, 2026-07-26)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** | — | Two related findings from `audits/SPIKE-catalog-data-paths-2026-07-26.md` P3/P5, both **pre-existing** and deliberately left alone by that spike. **(a)** FileBrowser Quantum 1.3.3 creates files `0644` and folders `0755` and does **not** propagate the setgid bit — even though the entrypoint wrapper's `umask 002` really is in effect (`/proc/1/status` `Umask: 0002`). Group inheritance itself works (a file uploaded into a 2775 group-100 dir landed group 100, not the process gid 1000), so the convention's *group* half holds and only its *mode* half is lost. The consequence is proven with a control: inside a UI-created `0755` folder a gid-1000 process's file landed group **1000**, while the identical write into the 2775 parent landed group **100**. So **any folder a customer creates through FileBrowser breaks the shared-group chain one level down.** Latent today — every userdata-touching catalog app that declares an identity declares uid/gid **1000**, the same uid FileBrowser runs as, so owner permissions mask it; it bites the day a content app runs as a different non-root uid with gid 1000. The comment at `infra/infra.go:156` is right that the image ignores `-e UMASK` but does not say t | CC |
| **R-577** | Apps & catalog | P4 | **[P3-LOW] A guest SHARE visitor has no way to pick a language, and the household's setting is the wrong default for them.** FOUND 2026-09-18 by localisation slice 2 release C (R-557, controller v0.254.0): every other page a person can reach now carries a language globe — the dashboard (the household's setting), and the sign-in and claim pages (the visitor's own cookie). The two guest share pages (`launcher_shared`, `launcher_share_password`) deliberately do NOT, and `TestGuestSharePagesHaveNoGlobe` pins that so it stays a decision rather than an oversight. **Why it is the operator's and not CC's:** a share visitor is a stranger the household sent a link to, and what language they are shown is a promise the SHARE FEATURE makes, not an implementation detail. The `felhom_lang` cookie already built would fit them exactly (display-only, their own browser, never the household's setting). **Fix shape, if the operator says yes:** add `{{template "lang_globe" .}}` to both shells with the anonymous form, and one render case per page per language. | **READY - rank P3-LOW; owner: operator (the decision), CC (the change)** **Re-ranked 2026-10-03: P3->P4: feature decision for the operator; Hungarian default works today.** | — | — | operator | | **R-577** | Apps & catalog | P4 | **[P3-LOW] A guest SHARE visitor has no way to pick a language, and the household's setting is the wrong default for them.** FOUND 2026-09-18 by localisation slice 2 release C (R-557, controller v0.254.0): every other page a person can reach now carries a language globe — the dashboard (the household's setting), and the sign-in and claim pages (the visitor's own cookie). The two guest share pages (`launcher_shared`, `launcher_share_password`) deliberately do NOT, and `TestGuestSharePagesHaveNoGlobe` pins that so it stays a decision rather than an oversight. **Why it is the operator's and not CC's:** a share visitor is a stranger the household sent a link to, and what language they are shown is a promise the SHARE FEATURE makes, not an implementation detail. The `felhom_lang` cookie already built would fit them exactly (display-only, their own browser, never the household's setting). **Fix shape, if the operator says yes:** add `{{template "lang_globe" .}}` to both shells with the anonymous form, and one render case per page per language. | **READY - rank P3-LOW; owner: operator (the decision), CC (the change)** **Re-ranked 2026-10-03: P3->P4: feature decision for the operator; Hungarian default works today.** | — | — | operator |
| **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC | | **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC |
| **R-764** | Apps & catalog | P4 | **[P3-LOW] wger sends no mail: its mail backend is the console, so a password-reset mail never leaves the box, and the template maps no SMTP.** READ 2026-10-01 inside the app on 9202 (checklist row 7.1): `EMAIL_BACKEND django.core.mail.backends.console.EmailBackend`; the settings read `ENABLE_EMAIL`, `EMAIL_HOST`, `EMAIL_PORT`, `FROM_EMAIL` …; the template carries no `smtp_mapping`. The household's admin can reset another member's password in the app; a member who forgets theirs and asks wger by e-mail gets nothing, and the page does not say so. Not measured: what wger shows after a reset request. **Needs:** an `smtp_mapping` (vaultwarden's shape, a fresh install with mail OFF booting — REUSE.md §2), or the page saying mail is not available. `audits/new-app-checklist-2026-10-01/C/C2-static-reads.txt` | **READY — rank P3-LOW; owner: CC (catalog)** **Re-ranked 2026-10-03: P3→P4: wger is hidden and no box runs it.** **2026-10-06: PUSHED** to the live catalog (`c265b37`; wger stays hidden). | — | — | CC |
| **R-769** | Apps & catalog | P4 | **[P3-LOW] Pinchflat is not built: upstream is paused (no release in 2026, last push 2025-12-16) and its last release has no image tag.** READ 2026-10-01: ghcr's newest version tag `v2025.6.6`, `latest` amd64 only; runs as root by default; an unanswered 30 GB yt-dlp memory report (#866); third parties call it unmaintained (community-scripts #15968). Forks with images exist (Pinchflat-NGX, MorganKryze). **Needs:** the operator's word on a fork (a new upstream), after the YouTube sentence (R-767). `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator | | **R-769** | Apps & catalog | P4 | **[P3-LOW] Pinchflat is not built: upstream is paused (no release in 2026, last push 2025-12-16) and its last release has no image tag.** READ 2026-10-01: ghcr's newest version tag `v2025.6.6`, `latest` amd64 only; runs as root by default; an unanswered 30 GB yt-dlp memory report (#866); third parties call it unmaintained (community-scripts #15968). Forks with images exist (Pinchflat-NGX, MorganKryze). **Needs:** the operator's word on a fork (a new upstream), after the YouTube sentence (R-767). `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator |
| **R-770** | Apps & catalog | P4 | **[P3-LOW] Invidious — fit check only; the recommendation is not to build it.** READ 2026-10-01: playback needs `invidious-companion` (rolling `latest`, no version tags); PostgreSQL 14 (EOL 2026-11); `registration_enabled: true` by default; upstream: a bot check means „your IP is blocked from YouTube”, a 429 can last 24 h, triggered by „someone on your network” — on our boxes that IP is the household's. One bad period in 2026 (March, ~2 weeks). No report found of a family's other devices being bot-checked (inference). **Needs:** the operator's go / no-go. `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator | | **R-770** | Apps & catalog | P4 | **[P3-LOW] Invidious — fit check only; the recommendation is not to build it.** READ 2026-10-01: playback needs `invidious-companion` (rolling `latest`, no version tags); PostgreSQL 14 (EOL 2026-11); `registration_enabled: true` by default; upstream: a bot check means „your IP is blocked from YouTube”, a 429 can last 24 h, triggered by „someone on your network” — on our boxes that IP is the household's. One bad period in 2026 (March, ~2 weeks). No report found of a family's other devices being bot-checked (inference). **Needs:** the operator's go / no-go. `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator |
| **R-771** | Apps & catalog | P4 | **[P3-LOW] moonlight-web — fit check only; not buildable through an HTTP-only tunnel at usable latency.** READ 2026-10-01: two unrelated projects (MrCreativ3001/moonlight-web-stream, the original; linckosz/moonlight-web); both need Sunshine/Apollo/Wolf on a gaming PC on the LAN and WebRTC over UDP (40000-40100/udp; linckosz recommends host networking and sends telemetry by default); both have a WebSocket fallback (high latency, all video through the tunnel); a logged-in user controls the PC's desktop. **Needs:** the operator's go / no-go (LAN-only use would need a different publishing model). `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator | | **R-771** | Apps & catalog | P4 | **[P3-LOW] moonlight-web — fit check only; not buildable through an HTTP-only tunnel at usable latency.** READ 2026-10-01: two unrelated projects (MrCreativ3001/moonlight-web-stream, the original; linckosz/moonlight-web); both need Sunshine/Apollo/Wolf on a gaming PC on the LAN and WebRTC over UDP (40000-40100/udp; linckosz recommends host networking and sends telemetry by default); both have a WebSocket fallback (high latency, all video through the tunnel); a logged-in user controls the PC's desktop. **Needs:** the operator's go / no-go (LAN-only use would need a different publishing model). `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator |
@@ -209,8 +208,7 @@ stopping line that lies.
| **R-255** | Security & access | P3 | **The check that would catch a fourth secret-in-the-body covers 4 of 27 pages, and the cheap gate that covers all 36 templates is blind to the shape that actually shipped.** Filed 2026-08-08 while closing R-254, **because a partial guard reported as complete is worse than no guard — it stops the next person looking.** **Two nets, both measured.** **(1) `scripts/secret_in_markup_gate.py`** reads all 36 templates and convicts any `{{ … }}` naming a secret unless allowlisted with a reason. It catches `{{.RetrievalPassword}}` and `{{.InitialCreds.Password}}`, **and it catches a launder through a local variable** because the assignment itself names the secret (`{{$v := .InitialCreds.Password}}` is convicted — verified). **It is blind to a secret arriving under a NEUTRAL PAGE-DATA KEY** — `data["Tagline"] = creds.Password` then `{{.AppInfo.Tagline}}` passes it cleanly, also verified. **That is exactly the shape of R-254 site two** (`value="{{$val}}"` inside an `{{if eq .Type "secret"}}` branch), so the gate **would not have caught one of the three instances it was written for.** **(2) The runtime body assertion** — render the page and grep the response for a sentinel — catches every shape, including that one (demonstrated on the same planted leak the gate missed). But it needs each page's data to be constructible in a test, and **only 4 of 27 page templates have that today**: `settings_security`, `app_info`, `deploy`, `backups_restore` — the four that were touched by R-249/R-252/R-253/R-254 and therefore got their own tests. **The other 23 pages have no runtime coverage at all.** **What closing this needs, so the cost is not re-estimated:** a per-page data fixture for the remaining 23 (most need a wired `Server` — `stackMgr`, `backupMgr`, agent seams), then one table-driven test that renders each with a sentinel substituted for every string in its data and asserts the sentinel is absent. **That is real scaffolding, which is why it was NOT built inside R-254's session** rather than half-built and declared done. | **READY** — owner Viktor | — | — | operator | | **R-255** | Security & access | P3 | **The check that would catch a fourth secret-in-the-body covers 4 of 27 pages, and the cheap gate that covers all 36 templates is blind to the shape that actually shipped.** Filed 2026-08-08 while closing R-254, **because a partial guard reported as complete is worse than no guard — it stops the next person looking.** **Two nets, both measured.** **(1) `scripts/secret_in_markup_gate.py`** reads all 36 templates and convicts any `{{ … }}` naming a secret unless allowlisted with a reason. It catches `{{.RetrievalPassword}}` and `{{.InitialCreds.Password}}`, **and it catches a launder through a local variable** because the assignment itself names the secret (`{{$v := .InitialCreds.Password}}` is convicted — verified). **It is blind to a secret arriving under a NEUTRAL PAGE-DATA KEY** — `data["Tagline"] = creds.Password` then `{{.AppInfo.Tagline}}` passes it cleanly, also verified. **That is exactly the shape of R-254 site two** (`value="{{$val}}"` inside an `{{if eq .Type "secret"}}` branch), so the gate **would not have caught one of the three instances it was written for.** **(2) The runtime body assertion** — render the page and grep the response for a sentinel — catches every shape, including that one (demonstrated on the same planted leak the gate missed). But it needs each page's data to be constructible in a test, and **only 4 of 27 page templates have that today**: `settings_security`, `app_info`, `deploy`, `backups_restore` — the four that were touched by R-249/R-252/R-253/R-254 and therefore got their own tests. **The other 23 pages have no runtime coverage at all.** **What closing this needs, so the cost is not re-estimated:** a per-page data fixture for the remaining 23 (most need a wired `Server` — `stackMgr`, `backupMgr`, agent seams), then one table-driven test that renders each with a sentinel substituted for every string in its data and asserts the sentinel is absent. **That is real scaffolding, which is why it was NOT built inside R-254's session** rather than half-built and declared done. | **READY** — owner Viktor | — | — | operator |
| **R-338** | Security & access | P3 | **`demo-hp` is not on the R-50 island at all, and `operations/nodes.md` states that it is.** The page records both fleet boxes as island-migrated 2026-07-25. True of `felhom-pve`; **false of `demo-hp`**, whose `agent.json` has `listen_addr: 192.168.0.87:8443` — the customer LAN address — and **no `island_bridge`/`island_guest_addr` keys at all**, whose guest 9201 has `net0` only (no `eth1`), and whose `vmbr9` exists with **zero members**. The controller's `controller.yaml` points at the LAN address, so the box works; this is inventory drift, not breakage. **Two costs.** A session trusting the page addresses the wrong endpoint — that happened on 2026-08-18 and the resulting timeout was briefly read as a fault. And the agent's local API is **bound to the customer LAN on this box** rather than to a point-to-point island, which is the exposure R-50 was built to remove — so a documented security property is claimed for a box that does not have it **Checked from source 2026-10-05 (burn-down round 2):** nodes.md:86-88 still claims demo-hp is on the R-50 island (`local_api` on 169.254.253.1:8443/vmbr9, guest eth1). git blame: that claim dates from e6b5fa1e (2026-07-30); the 2026-09-21 edit bcdd5b20 re-read addresses but only reworded the lan_resolver clause -- the island claim was NOT re-verified after the reprovision. Agent config path /etc/felhom-agent/agent.json (felhom-agent cmd/felhom-agent/main.go:171), island keys island_bridge (internal/config/config.go:246). | **READY (S) — NEW 2026-08-18** | — | Decide which is true: migrate `demo-hp` to the island, or correct `nodes.md`. Leaving both is the one option that keeps the doc lying | Viktor decides; CC executes | | **R-338** | Security & access | P3 | **`demo-hp` is not on the R-50 island at all, and `operations/nodes.md` states that it is.** The page records both fleet boxes as island-migrated 2026-07-25. True of `felhom-pve`; **false of `demo-hp`**, whose `agent.json` has `listen_addr: 192.168.0.87:8443` — the customer LAN address — and **no `island_bridge`/`island_guest_addr` keys at all**, whose guest 9201 has `net0` only (no `eth1`), and whose `vmbr9` exists with **zero members**. The controller's `controller.yaml` points at the LAN address, so the box works; this is inventory drift, not breakage. **Two costs.** A session trusting the page addresses the wrong endpoint — that happened on 2026-08-18 and the resulting timeout was briefly read as a fault. And the agent's local API is **bound to the customer LAN on this box** rather than to a point-to-point island, which is the exposure R-50 was built to remove — so a documented security property is claimed for a box that does not have it **Checked from source 2026-10-05 (burn-down round 2):** nodes.md:86-88 still claims demo-hp is on the R-50 island (`local_api` on 169.254.253.1:8443/vmbr9, guest eth1). git blame: that claim dates from e6b5fa1e (2026-07-30); the 2026-09-21 edit bcdd5b20 re-read addresses but only reworded the lan_resolver clause -- the island claim was NOT re-verified after the reprovision. Agent config path /etc/felhom-agent/agent.json (felhom-agent cmd/felhom-agent/main.go:171), island keys island_bridge (internal/config/config.go:246). | **READY (S) — NEW 2026-08-18** | — | Decide which is true: migrate `demo-hp` to the island, or correct `nodes.md`. Leaving both is the one option that keeps the doc lying | Viktor decides; CC executes |
| **R-616** | Security & access | P3 | **[P3-LOW] The catalog credentials are stored in PLAINTEXT in the box's catalog clone and are printed by an ordinary `git remote -v`.** FOUND 2026-09-21 on guest 9202 while pointing it at a private drill catalog. `Syncer.buildRepoURL` injects `username:token` into the HTTPS URL, and `git clone` persists that URL as the clone's `origin`, so `<data>/catalog-cache/.git/config` holds the token in the clear and **any** diagnostic that prints the remote leaks it — which is what happened in this session's own transcript, and is the same shape as R-580 (`curl -w '%{redirect_url}'`). `maskRepoURL` exists and is used for the LOG lines, so the masking intent is already there; the stored remote is the half that was missed. **INERT ON THE FLEET TODAY** — the live catalog is public and `git.token` is empty on every real box — which is exactly why it should be fixed before it is not: the day the catalog goes private, every box carries a readable credential and every support session that runs `git remote -v` prints it. **Fix shape:** store the remote WITHOUT credentials and supply them per-fetch (a credential helper, `http.extraHeader`, or `GIT_ASKPASS`), and a test asserting the clone's stored `origin` contains no `@`. **Operator action from tonight, unrelated to the fix:** the Gitea `admin` token used for the drill repo was printed by that command and must be rotated. Evidence: `audits/update-night-2026-09-21/05-9202-follows-drill.txt` (redacted). | **READY — rank P3-LOW; owner: CC (controller); one operator action (rotate the Gitea admin token)** **2026-10-05 (burn-down night): FIXED on controller `main`** (`28a5203`; the catalog clone stores no credentials; the token is supplied per fetch; R-615's repo comparison ignores credentials on both sides, so a token never re-clones (`TestR616_TokenSetSameRepoNoRecloneOriginClean`) and a credentialed origin is cleaned at the next pull. The operator's Gitea admin token rotation (the row's second half) is still owed). Ships with the next controller release; close after delivery. **2026-10-06: DELIVERED** in controller v0.298.0 (the clone stores no credentials). Left: the operator's Gitea admin token rotation. | — | — | CC + operator | | **R-616** | Security & access | P3 | **[P3-LOW] The catalog credentials are stored in PLAINTEXT in the box's catalog clone and are printed by an ordinary `git remote -v`.** FOUND 2026-09-21 on guest 9202 while pointing it at a private drill catalog. `Syncer.buildRepoURL` injects `username:token` into the HTTPS URL, and `git clone` persists that URL as the clone's `origin`, so `<data>/catalog-cache/.git/config` holds the token in the clear and **any** diagnostic that prints the remote leaks it — which is what happened in this session's own transcript, and is the same shape as R-580 (`curl -w '%{redirect_url}'`). `maskRepoURL` exists and is used for the LOG lines, so the masking intent is already there; the stored remote is the half that was missed. **INERT ON THE FLEET TODAY** — the live catalog is public and `git.token` is empty on every real box — which is exactly why it should be fixed before it is not: the day the catalog goes private, every box carries a readable credential and every support session that runs `git remote -v` prints it. **Fix shape:** store the remote WITHOUT credentials and supply them per-fetch (a credential helper, `http.extraHeader`, or `GIT_ASKPASS`), and a test asserting the clone's stored `origin` contains no `@`. **Operator action from tonight, unrelated to the fix:** the Gitea `admin` token used for the drill repo was printed by that command and must be rotated. Evidence: `audits/update-night-2026-09-21/05-9202-follows-drill.txt` (redacted). | **READY — rank P3-LOW; owner: CC (controller); one operator action (rotate the Gitea admin token)** **2026-10-05 (burn-down night): FIXED on controller `main`** (`28a5203`; the catalog clone stores no credentials; the token is supplied per fetch; R-615's repo comparison ignores credentials on both sides, so a token never re-clones (`TestR616_TokenSetSameRepoNoRecloneOriginClean`) and a credentialed origin is cleaned at the next pull. The operator's Gitea admin token rotation (the row's second half) is still owed). Ships with the next controller release; close after delivery. **2026-10-06: DELIVERED** in controller v0.298.0 (the clone stores no credentials). Left: the operator's Gitea admin token rotation. | — | — | CC + operator |
| **R-717** | Security & access | P3 | **[P3-LOW] opengist and wishlist keep their sign-up switch only in their own database — the box closes them with the address block alone.** MEASURED 2026-09-29: opengist `disable-signup` is an admin-panel setting (no env, no CLI); wishlist `system_config.enableSignup` (Prisma). Their blocks are case-insensitive and refused every trick shape (`audits/signup-lock-2026-09-29/B/`). **Fix direction:** an `after_setup` command that sets the database value (wishlist: a Node/Prisma one-liner; opengist: needs its sqlite with the app stopped). | **OPEN — P3; owner: CC** | — | — | CC | | **R-717** | Security & access | P3 | **[P3-LOW] opengist and wishlist keep their sign-up switch only in their own database — the box closes them with the address block alone.** MEASURED 2026-09-29: opengist `disable-signup` is an admin-panel setting (no env, no CLI); wishlist `system_config.enableSignup` (Prisma). Their blocks are case-insensitive and refused every trick shape (`audits/signup-lock-2026-09-29/B/`). **Fix direction:** an `after_setup` command that sets the database value (wishlist: a Node/Prisma one-liner; opengist: needs its sqlite with the app stopped). **-- 2026-10-06 (afternoon): NEEDS A DESIGN.** The controller's `after_setup` command form runs only when the lock is SET (`internal/stacks/after_setup.go` `applyNativeLock`, `lock && len(spec.Command) > 0`); the household's 15-minute window lifts only the env form. A database switch closed by a command would stay closed through the window, so the household could not let a family member sign up. Needs a lift command (an `open` twin) in the controller first. | **OPEN — P3; owner: CC** | — | — | CC |
| **R-763** | Security & access | P3 | **[P2-MEDIUM] On wger a stranger can make an account after the household's setup, and every anonymous visit to the dashboard creates a guest account.** MEASURED 2026-10-01 on 9202 (live template `82fff32`), found by checklist row 3.4: after the admin existed, a stranger with no dashboard session `POST /en/user/registration` → 302, signed in with it → 302, read the API → 200; two anonymous `GET /en/dashboard` raised the user count from 2 to 4 (wger's middleware `create_temporary_user`, `utils/middleware.py:69`). Settings read inside the app: `ALLOW_REGISTRATION True`, `ALLOW_GUEST_USERS True` (the image defaults; the template sets neither). `GET /en/user/demo-entries` as a stranger answered 500. wger is FIRST-ADMIN class 3 (a known default login, fixed by `after_install`), so it never got decision 47's sign-up lock — it was not in R-711's list. Every crawler visit adds a user row to the household's database. **Needs:** per decision 47, close it after the first admin: `ALLOW_REGISTRATION=False` and `ALLOW_GUEST_USERS=False` (env switches the settings read — measure that the admin can still add family members, row 3.7), proven on 9202 as a stranger. `audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt` **-- 2026-10-01 (operator):** wger is `lifecycle: hidden` until this and its twin are fixed (catalog `55b8c8a`; read back on 9202: not on the app list, mealie control present). | **READY — rank P2-MEDIUM; owner: CC (catalog)** **Re-ranked 2026-10-03: P2→P3: wger is hidden from installs and no box runs it; needed only before it is offered again.** **2026-10-06: PUSHED** to the live catalog (`c265b37`; wger stays hidden). | — | — | CC |
| **R-775** | Security & access | P3 | **[P2-MEDIUM] Grimmory: a stranger's 5 wrong sign-ins lock EVERY visitor out of the web login for 15 minutes — so Grimmory was not published.** MEASURED 2026-10-01 on 9202 (drill catalog, v3.4.1, through traefik): after 5 wrong tries for `admin` every further sign-in answered 429 — the household's right password AND a different name — and stayed 429 for 10+ minutes of retries. Read in the jar: `AuthRateLimitService` — Caffeine `expireAfterWrite(ofMinutes(15))`, `MAX_ATTEMPTS 5`, keys `login:ip:` and `login:user:`; Spring `forward-headers-strategy: native` takes the address from X-Forwarded-For, and behind the tunnel every visitor is the tunnel container's address (R-753) — the wger shape (R-752), with no setting to change it. Everything else in the checklist passed (bench + box step v3.4.1 → v3.5.0, gate by its own probe, OPDS through traefik); two smaller findings for the publishing session: on a reinstall over the first install's kept books, a new upload was saved to the drive but not added to the library (`box/grimmory/reinstall-c1.txt`, not investigated); and the remove + restore round trip (2.5) cannot be shown on 9202 for a drive app — its backup lives on the scratch drive, which is not a registered drive (R-756). The template waits in `audits/new-apps-2026-10-01/wip/grimmory/`. **Needs (operator):** (A) publish with a sentence on the page that wrong guesses by others can lock the login for 15 minutes (MEASURED: during the lock an e-reader's OPDS feed still answered 200 with its own login, wrong 401 — `box/grimmory/opds-under-lock.txt`), or (B) wait until the box passes each visitor's real address (R-753). `audits/new-apps-2026-10-01/box/grimmory/throttle.txt` **-- 2026-10-01 (evening):** option B's precondition SHIPPED (controller v0.286.1, R-753): Grimmory's Tomcat RemoteIpValve walks from the right and counts `172.16.0.0/12` as a proxy (READ in source, Spring Boot 4.1.1 — not yet measured with Grimmory's own lock), so `login:ip:` becomes per visitor; `login:user:` still lets a stranger lock the public name `admin` 15 min. A third route was spiked and passed: Grimmory behind the permanent family gate with its e-reader paths excepted (R-780). Recommendation: publish behind the family gate if R-780 is built; otherwise B with a measured 3.6. **UPDATE 2026-10-02 — NARROWED, Grimmory PUBLISHED behind the family gate:** a stranger cannot reach Grimmory's web sign-in at all (6 tries through the simulated tunnel: the gate's 401, then the household signs in 200 — `audits/family-gate-2026-10-02/A/items.txt`), and the e-reader exceptions keep Grimmory's own login. The 2.5 round trip now WORKS on 9202 (`audits/family-gate-2026-10-02/B/box/life.txt`). **What is left:** a family member past the gate can still lock a NAME (the admin's) for 15 minutes with 5 wrong tries — hard-coded in Grimmory; and the reinstall-over-kept-books finding (`new-apps-2026-10-01/box/grimmory/reinstall-c1.txt`) is still not investigated. | **WATCHING — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P2→P3: Grimmory is now behind the family gate; only a family member can still lock a name.** | — | — | CC | | **R-775** | Security & access | P3 | **[P2-MEDIUM] Grimmory: a stranger's 5 wrong sign-ins lock EVERY visitor out of the web login for 15 minutes — so Grimmory was not published.** MEASURED 2026-10-01 on 9202 (drill catalog, v3.4.1, through traefik): after 5 wrong tries for `admin` every further sign-in answered 429 — the household's right password AND a different name — and stayed 429 for 10+ minutes of retries. Read in the jar: `AuthRateLimitService` — Caffeine `expireAfterWrite(ofMinutes(15))`, `MAX_ATTEMPTS 5`, keys `login:ip:` and `login:user:`; Spring `forward-headers-strategy: native` takes the address from X-Forwarded-For, and behind the tunnel every visitor is the tunnel container's address (R-753) — the wger shape (R-752), with no setting to change it. Everything else in the checklist passed (bench + box step v3.4.1 → v3.5.0, gate by its own probe, OPDS through traefik); two smaller findings for the publishing session: on a reinstall over the first install's kept books, a new upload was saved to the drive but not added to the library (`box/grimmory/reinstall-c1.txt`, not investigated); and the remove + restore round trip (2.5) cannot be shown on 9202 for a drive app — its backup lives on the scratch drive, which is not a registered drive (R-756). The template waits in `audits/new-apps-2026-10-01/wip/grimmory/`. **Needs (operator):** (A) publish with a sentence on the page that wrong guesses by others can lock the login for 15 minutes (MEASURED: during the lock an e-reader's OPDS feed still answered 200 with its own login, wrong 401 — `box/grimmory/opds-under-lock.txt`), or (B) wait until the box passes each visitor's real address (R-753). `audits/new-apps-2026-10-01/box/grimmory/throttle.txt` **-- 2026-10-01 (evening):** option B's precondition SHIPPED (controller v0.286.1, R-753): Grimmory's Tomcat RemoteIpValve walks from the right and counts `172.16.0.0/12` as a proxy (READ in source, Spring Boot 4.1.1 — not yet measured with Grimmory's own lock), so `login:ip:` becomes per visitor; `login:user:` still lets a stranger lock the public name `admin` 15 min. A third route was spiked and passed: Grimmory behind the permanent family gate with its e-reader paths excepted (R-780). Recommendation: publish behind the family gate if R-780 is built; otherwise B with a measured 3.6. **UPDATE 2026-10-02 — NARROWED, Grimmory PUBLISHED behind the family gate:** a stranger cannot reach Grimmory's web sign-in at all (6 tries through the simulated tunnel: the gate's 401, then the household signs in 200 — `audits/family-gate-2026-10-02/A/items.txt`), and the e-reader exceptions keep Grimmory's own login. The 2.5 round trip now WORKS on 9202 (`audits/family-gate-2026-10-02/B/box/life.txt`). **What is left:** a family member past the gate can still lock a NAME (the admin's) for 15 minutes with 5 wrong tries — hard-coded in Grimmory; and the reinstall-over-kept-books finding (`new-apps-2026-10-01/box/grimmory/reinstall-c1.txt`) is still not investigated. | **WATCHING — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P2→P3: Grimmory is now behind the family gate; only a family member can still lock a name.** | — | — | CC |
| **R-782** | Security & access | P3 | **[P3-LOW] Two side observations of the R-753 sweep, inferred, not measured:** glance's seeded `glance.yml` has no `auth:` block (the dashboard is public to anyone with the address), and homepage's `/api/*` refuses a Host not in `HOMEPAGE_ALLOWED_HOSTS`, which the template does not set (widgets may 400). **Needs:** measure both on 9202; glance: decide whether a public link dashboard is intended (the setup gate does not cover it after setup). | **READY — rank P3-LOW; owner: CC (catalog)** | — | — | CC | | **R-782** | Security & access | P3 | **[P3-LOW] Two side observations of the R-753 sweep, inferred, not measured:** glance's seeded `glance.yml` has no `auth:` block (the dashboard is public to anyone with the address), and homepage's `/api/*` refuses a Host not in `HOMEPAGE_ALLOWED_HOSTS`, which the template does not set (widgets may 400). **Needs:** measure both on 9202; glance: decide whether a public link dashboard is intended (the setup gate does not cover it after setup). | **READY — rank P3-LOW; owner: CC (catalog)** | — | — | CC |
| **R-831** | Security & access | P3 | **The Hetzner storage API token (`HETZNER_TOKEN`, the storage project's token in `Secret/storagebox`) was printed into the 2026-10-03 session transcript** — CC read the gitignored `manifests/storagebox.secret.yaml` and its redaction pattern missed the quoted value. It can create, reset and delete Storage Box sub-accounts. Not rotated by the operator's choice (decision 73). **Rotation, whenever chosen (3 steps):** create a new token in the storage project in the Hetzner console → patch `Secret/storagebox` key `HETZNER_TOKEN` in `felhom-system` and `kubectl rollout restart deployment/hub` → delete the old token in the console. Rule for sessions: never print a file that holds secrets — read the one field needed. | **WAITING-ON-OPERATOR — rotation is his call** **Not rotated by the operator's rulings (2026-10-04 „keep using the current one"; 2026-10-05 option B) — restated 2026-10-05 18:23; the steps stay here.** | — | rotate when chosen | operator | | **R-831** | Security & access | P3 | **The Hetzner storage API token (`HETZNER_TOKEN`, the storage project's token in `Secret/storagebox`) was printed into the 2026-10-03 session transcript** — CC read the gitignored `manifests/storagebox.secret.yaml` and its redaction pattern missed the quoted value. It can create, reset and delete Storage Box sub-accounts. Not rotated by the operator's choice (decision 73). **Rotation, whenever chosen (3 steps):** create a new token in the storage project in the Hetzner console → patch `Secret/storagebox` key `HETZNER_TOKEN` in `felhom-system` and `kubectl rollout restart deployment/hub` → delete the old token in the console. Rule for sessions: never print a file that holds secrets — read the one field needed. | **WAITING-ON-OPERATOR — rotation is his call** **Not rotated by the operator's rulings (2026-10-04 „keep using the current one"; 2026-10-05 option B) — restated 2026-10-05 18:23; the steps stay here.** | — | rotate when chosen | operator |