New-app checklist: the pilot audit (wger as of 2026-09-29 and now, two 9202 walks), R-758..R-764 opened; STATUS, CONTEXT, report
gates / gates (push) Successful in 27s

The operator's request of 2026-10-01 recorded in CONTEXT with the reviewer's defaults (new apps only; the 53 get a
read-only gap page). The pilot: the draft caught R-752 and R-755, missed R-737 and (for a new app) R-738; the
sharpened and new rows then found R-762 (wger serves no static files or photos), R-763 (strangers sign up, guest
accounts), R-764 (no mail). Also R-758 (8 mem_limit under the sum), R-759 (wger's open record rows), R-760
(vikunja healthcheck), R-761 (logo comment). R-755 note. Register 392 -> 399.

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-01 15:22:05 +02:00
parent b63654a299
commit 2d69c61556
29 changed files with 1570 additions and 14 deletions
+15
View File
@@ -16,6 +16,21 @@
> and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
> **2026-10-01 (evening) — the new-app checklist: in the catalog, a gate, piloted on wger.** Operator request (recorded
> before the work): *before new apps are added, a checklist every new app passes — storage and database needs, whether an
> admin can be created and its password changed, health checks tested, resource limits tested, and more — so a new app is
> mapped and tested before it is offered.* **Reviewer's defaults (operator may reverse):** the gate binds NEW apps only;
> the 53 existing apps get a read-only gap page, not a re-test. Built: `app-catalog-felhom.eu/NEW-APP-CHECKLIST.md` (60
> rows, 10 groups, a `since` date per id); `onboarding/_TEMPLATE.md` (one line per id: done + evidence / n/a + reason /
> open); gate `onboarding` (`scripts/check-onboarding.py`, `--fast`, hook + CI; the 53 exempt BY NAME, not by commit —
> CI has no history; evidence in a sibling repo is checked where the sibling exists and listed as NOT CHECKED where it
> does not); 16 decoys + 5 gate mutants seen red; `onboarding/EXISTING-APPS-GAPS.md` (`scripts/onboarding_gaps.py`).
> Pilot: the draft caught R-752 and R-755 as written, missed R-737 (its "how" logged in on the web form only) and R-738
> for a new app (no update to run at the newest tag) — rows 1.4/1.6 sharpened, 1.7/3.9 added. The pilot's rows then found
> R-762 (wger serves no CSS/JS and no photos), R-763 (strangers sign up / guest accounts), R-764 (no mail); also R-758
> (8 `mem_limit` ≠ sum), R-760, R-761, R-759 (wger's open record rows). Evidence `documentation/audits/new-app-checklist-2026-10-01/`.
> Report: `REPORT-new-app-checklist-2026-10-01.md`.
> **2026-10-01 (late afternoon) — 61 and 62 built.** calibre-web `ADMIN_USER` (catalog `e9f50b5`; after_install renames
> `admin` in app.db and proves it); demo-hp renamed by hand (name in the operator's credentials file). Registry prune rule in
> `admin/misc-scripts` `c9d5ed5` (keep 20 + in use; refuses when unreadable; dry-run only). R-750, R-752 closed; R-756
+60
View File
@@ -0,0 +1,60 @@
# REPORT — the new-app checklist: in the catalog, a gate, piloted on wger (2026-10-01, evening)
Architecture read: `documentation/architecture/09-update-architecture.md` §3 (decisions 13, 22, 37, 42, 45–50, 61) and
§6.5. Evidence: `documentation/audits/new-app-checklist-2026-10-01/README.md` (the full write-up).
## The Part table
| part | result | changed from the brief, and why |
|---|---|---|
| A — checklist in the catalog | **done** — `NEW-APP-CHECKLIST.md` 60 rows / 10 groups; `onboarding/_TEMPLATE.md`; CLAUDE.md, REUSE.md §5, README point to it | 7 rows added, 16 sharpened, 9 wrong claims fixed (below). A `since` column per id (B's date rule) |
| B — the gate | **done** — `scripts/check-onboarding.py`, gate `onboarding` in `--fast` (hook + CI); 16 decoys, all judged right; 5 gate mutants each turn the suite red | the 53 are exempt **by name**, not "new since commit X": CI fetches at depth 1 and cannot diff (R-452). Evidence in `felhom.eu/` is checked where that repo sits beside the catalog (the hook) and printed as NOT CHECKED where it does not (CI). Rows inside an HTML comment do not count; an empty evidence directory does not count |
| C — wger pilot | **done** — both records filled; table below | the restore round trip (2.5) could not be measured: no per-app backup press exists outside an Update (R-648) — open, R-759 |
| D — gap page | **done** — `onboarding/EXISTING-APPS-GAPS.md` from `scripts/onboarding_gaps.py` | none |
**The date rule (B.1):** each checklist id carries `since`; a record answers every id with `since` ≤ its `opened:`.
`opened:` must be on or after 2026-10-01 and not in the future, so a later id binds only apps opened after it. Residual:
an author can date `opened:` back to the cut-off to skip ids added since — the gate cannot see that; review can.
## Claims in the draft that were wrong
3.3 "32 apps" (33 today) · 5.2 "romm OOM at +76 s (decision 22)" (no such figure anywhere; R-635) · 5.3 "gate" (no gate;
8 templates differ, R-758) · 6.2 "35 of 53 update at night" (not reproducible; 38 carry a ladder) · 8.3 logo address
(`.webp` vs the controller's `.svg`/`.png`, R-761) · 1.6's how could not show R-737 · 1.4's how has nothing to run for
an app at its newest tag · 0.4's packet capture is not in our kit · 2.6's R-756 is an unexplained venue case (R-442
added). Duplicates of gates now name the gate (1.1, 2.1, 3.3, 4.2, 6.2, 8.1, 9.4).
## The pilot — the four problems
| problem | caught by | the draft's how? |
|---|---|---|
| R-737 JWT key | 1.6, 3.9 (new), 0.7 | **missed** — web login worked |
| R-738 no migration | 1.4, 1.7 (new), 6.1 | **only if an update existed** — not for a new app |
| R-752 lock-out → everyone | 3.6 | **caught** |
| R-755 dev server | 1.5, 1.7 | **caught** |
**And three more, found by the new rows on the LIVE wger template (9202, drill catalog):** R-762 no CSS/JS and no
uploaded photo is ever served (404); R-763 a stranger signs up after the setup, and every anonymous dashboard visit
creates a guest account; R-764 mail goes to the console. Not fixed: this task changes no template.
## Gap page headline (of 53)
Fit 52 · images/DB 53 · storage 38 · accounts 39 · health 49 · resources 24 · updates 26 · mail — (6 mapped) · text 52.
## Rows
Opened **R-758** (8 `mem_limit` ≠ sum), **R-759** (wger's open record rows), **R-760** (vikunja healthcheck),
**R-761** (logo comment), **R-762** (P2, wger static + media), **R-763** (P2, wger strangers + guests), **R-764**
(wger mail). Narrowed: none. Note added to R-755 (same server question as R-762). Closed: none.
**Register 392 → 399.** STATUS updated.
## Live work and teardown
9202 only, drill catalog `e9f50b5` (repointed, then restored to live `6d72c09` — three controls each way). wger
installed and removed twice through the product. **Machine:** no wger container, volume or image left; sampler files
removed. **Host:** nothing. **Hub:** untouched. Secrets never printed; evidence scanned for their values.
## Gates
Catalog: `catalog_gates.py --fast` all OK (11 gates); `test_gate_decoys.py` 121 cases OK; `test_catalog_gates.py` OK;
`decoy_coverage_gate.py` 0 unaccounted. felhom.eu: `repo_gates.py --fast` — see the commit. No `--no-verify`.
+18 -13
View File
@@ -2,27 +2,32 @@
**Ready for the first real tester (Tester-2): yes. You confirmed the tunnel route and the connect mails (2026-09-30).**
**Updated 2026-10-01 (late afternoon). Both demo boxes run controller 0.285.0 and host agent 0.138.0. Hub 0.126.0. New installs get golden 0.285.0 with agent 0.138.0.**
**Updated 2026-10-01 (evening). Both demo boxes run controller 0.285.0 and host agent 0.138.0. Hub 0.126.0. New installs get golden 0.285.0 with agent 0.138.0.**
**Tester-2 — read only, from the hub.** The customer record exists. Tester-2's box has not registered yet.
## Your two decisions of this afternoon — built
## Your request of today — the new-app checklist — built
- **calibre-web gets a secret login name (your A).** The box makes the name at install and shows it on the app page,
next to the password. Tested on the scratch box: a stranger tried 40 wrong passwords with `admin`, and the household
still logged in at once with its own name. The lock against guessing stays.
**The HP demo box's calibre-web has a new name too.** It is in your credentials file, under the same key as before.
- **The registry clean-up script keeps 20 versions and never one in use (your A).** "In use" means: the golden, the
floor, the approved agent, the running hub. I ran it only in "show me" mode: today it would delete 70 old controller
versions and 8 hub versions. **It deleted nothing.** If it cannot read what is in use, it refuses to run.
- **The checklist is in the app catalog.** 60 checks in 10 groups. Each check says how to test it and why it exists.
- **A new app cannot reach the live catalog without its filled-in copy.** A check on every push and in CI refuses it.
I tested that check with 16 fake cases, and I broke the check on purpose 5 times to prove the fake cases notice.
- **The 53 old apps are not re-tested.** One page shows what each already has. For example: 24 of 53 show a measured
memory check, 26 of 53 show a tested update.
- **The test on wger:** the first draft found 2 of this week's 4 wger problems. It missed the phone-app login and the
missing update step for a brand-new app. I sharpened those checks. Now all 4 are caught.
## What broke, and what I did
- **A box makes up a value when a template gains a new generated field.** On the HP demo box it made up a calibre-web
login name that the app never had. I fixed that box by giving the app that name. Written down. No other box has calibre-web today.
- On the scratch box, removing calibre-web "with its data" was refused, although the folder is there. Written down.
- **The checklist found three more wger problems, on the live catalog.** Nothing fixed today (this task changes no app).
- wger loads no styling and shows no uploaded photo. The pages look broken.
- A stranger can make a wger account after you set it up. Every anonymous visit adds a "guest" account.
- wger cannot send e-mail. A "forgot password" mail never leaves the box.
- Eight apps show a smaller memory figure than they really use. The apps are not affected; the number on the screen is.
- Earlier today, built: your two afternoon decisions — calibre-web's secret login name (the HP demo box's new name is in
your credentials file), and the registry clean-up rule (it deleted nothing). Found: a box makes up a value when a
template gains a new generated field (calibre-web). Written down.
**Rows.** This afternoon: 2 closed, 2 opened. The list went from 390 to 392 rows.
**Rows.** This evening: 7 opened, 0 closed. The list went from 392 to 399 rows.
## What needs you
@@ -0,0 +1,20 @@
-- onboarding: the facts (each MUST be refused)
ok FACT: a new template with NO record rc=1 (expected 1)
ok FACT: a record missing id 1.4 rc=1 (expected 1)
ok FACT: 1.4 answered only inside an HTML comment rc=1 (expected 1)
ok FACT: done with a path that does not exist rc=1 (expected 1)
ok FACT: done with an EMPTY directory (the mkdir shape) rc=1 (expected 1)
ok FACT: done naming an absent file in the sibling repo rc=1 (expected 1)
ok FACT: n/a with an EMPTY reason rc=1 (expected 1)
ok FACT: n/a with a two-word reason rc=1 (expected 1)
ok FACT: an OPEN row rc=1 (expected 1)
ok FACT: opened: backdated before the checklist rc=1 (expected 1)
ok FACT: a checklist id the template a new app copies lacks rc=1 (expected 1)
ok FACT: an exempt app's record with a done that points nowhere rc=1 (expected 1)
-- onboarding: the genuine articles (each MUST pass)
ok GENUINE: a complete record (catalog + sibling evidence) rc=0 (expected 0)
ok GENUINE: an id added AFTER opened: does not bind rc=0 (expected 0)
ok GENUINE: an exempt app's record may say open rc=0 (expected 0)
ok STATED SKIP: sibling repo absent (the CI shape) - printed, not checked rc=0 (expected 0)
catalog gate decoys OK — 121 case(s), every label judged on its fact (R-421)
@@ -0,0 +1,100 @@
-- onboarding: the facts (each MUST be refused)
ok FACT: a new template with NO record rc=1 (expected 1)
ok FACT: a record missing id 1.4 rc=1 (expected 1)
ok FACT: done with a path that does not exist rc=1 (expected 1)
ok FACT: done with an EMPTY directory (the mkdir shape) rc=1 (expected 1)
ok FACT: done naming an absent file in the sibling repo rc=1 (expected 1)
ok FACT: n/a with an EMPTY reason rc=1 (expected 1)
ok FACT: n/a with a two-word reason rc=1 (expected 1)
ok FACT: an OPEN row rc=1 (expected 1)
ok FACT: opened: backdated before the checklist rc=1 (expected 1)
ok FACT: a checklist id the template a new app copies lacks rc=1 (expected 1)
ok FACT: an exempt app's record with a done that points nowhere rc=1 (expected 1)
-- onboarding: the genuine articles (each MUST pass)
ok GENUINE: a complete record (catalog + sibling evidence) rc=0 (expected 0)
ok GENUINE: an id added AFTER opened: does not bind rc=0 (expected 0)
ok GENUINE: an exempt app's record may say open rc=0 (expected 0)
ok STATED SKIP: sibling repo absent (the CI shape) - printed, not checked rc=0 (expected 0)
MUTANT no comment stripping -> suite RED (1 case(s) failed)
FACT: 1.4 answered only inside an HTML comment: rc=0 expected 1; missing ['missing id(s): 1.4']
-- onboarding: the facts (each MUST be refused)
ok FACT: a new template with NO record rc=1 (expected 1)
ok FACT: a record missing id 1.4 rc=1 (expected 1)
ok FACT: 1.4 answered only inside an HTML comment rc=1 (expected 1)
ok FACT: done with a path that does not exist rc=1 (expected 1)
ok FACT: done naming an absent file in the sibling repo rc=1 (expected 1)
ok FACT: n/a with an EMPTY reason rc=1 (expected 1)
ok FACT: n/a with a two-word reason rc=1 (expected 1)
ok FACT: an OPEN row rc=1 (expected 1)
ok FACT: opened: backdated before the checklist rc=1 (expected 1)
ok FACT: a checklist id the template a new app copies lacks rc=1 (expected 1)
ok FACT: an exempt app's record with a done that points nowhere rc=1 (expected 1)
-- onboarding: the genuine articles (each MUST pass)
ok GENUINE: a complete record (catalog + sibling evidence) rc=0 (expected 0)
ok GENUINE: an id added AFTER opened: does not bind rc=0 (expected 0)
ok GENUINE: an exempt app's record may say open rc=0 (expected 0)
ok STATED SKIP: sibling repo absent (the CI shape) - printed, not checked rc=0 (expected 0)
MUTANT exists() instead of non-empty evidence -> suite RED (1 case(s) failed)
FACT: done with an EMPTY directory (the mkdir shape): rc=0 expected 1; missing ['id 5.1 is done but its evidence']
-- onboarding: the facts (each MUST be refused)
ok FACT: a new template with NO record rc=1 (expected 1)
ok FACT: a record missing id 1.4 rc=1 (expected 1)
ok FACT: 1.4 answered only inside an HTML comment rc=1 (expected 1)
ok FACT: done with a path that does not exist rc=1 (expected 1)
ok FACT: done with an EMPTY directory (the mkdir shape) rc=1 (expected 1)
ok FACT: done naming an absent file in the sibling repo rc=1 (expected 1)
ok FACT: n/a with an EMPTY reason rc=1 (expected 1)
ok FACT: n/a with a two-word reason rc=1 (expected 1)
ok FACT: opened: backdated before the checklist rc=1 (expected 1)
ok FACT: a checklist id the template a new app copies lacks rc=1 (expected 1)
ok FACT: an exempt app's record with a done that points nowhere rc=1 (expected 1)
-- onboarding: the genuine articles (each MUST pass)
ok GENUINE: a complete record (catalog + sibling evidence) rc=0 (expected 0)
ok GENUINE: an id added AFTER opened: does not bind rc=0 (expected 0)
ok GENUINE: an exempt app's record may say open rc=0 (expected 0)
ok STATED SKIP: sibling repo absent (the CI shape) - printed, not checked rc=0 (expected 0)
MUTANT open rows ignored -> suite RED (1 case(s) failed)
FACT: an OPEN row: rc=0 expected 1; missing ['id 6.3 is OPEN']
-- onboarding: the facts (each MUST be refused)
ok FACT: a new template with NO record rc=1 (expected 1)
ok FACT: a record missing id 1.4 rc=1 (expected 1)
ok FACT: 1.4 answered only inside an HTML comment rc=1 (expected 1)
ok FACT: done with a path that does not exist rc=1 (expected 1)
ok FACT: done with an EMPTY directory (the mkdir shape) rc=1 (expected 1)
ok FACT: done naming an absent file in the sibling repo rc=1 (expected 1)
ok FACT: an OPEN row rc=1 (expected 1)
ok FACT: opened: backdated before the checklist rc=1 (expected 1)
ok FACT: a checklist id the template a new app copies lacks rc=1 (expected 1)
ok FACT: an exempt app's record with a done that points nowhere rc=1 (expected 1)
-- onboarding: the genuine articles (each MUST pass)
ok GENUINE: a complete record (catalog + sibling evidence) rc=0 (expected 0)
ok GENUINE: an id added AFTER opened: does not bind rc=0 (expected 0)
ok GENUINE: an exempt app's record may say open rc=0 (expected 0)
ok STATED SKIP: sibling repo absent (the CI shape) - printed, not checked rc=0 (expected 0)
MUTANT no reason length -> suite RED (2 case(s) failed)
FACT: n/a with an EMPTY reason: rc=0 expected 1; missing ['id 7.1 is n/a']
FACT: n/a with a two-word reason: rc=0 expected 1; missing ['id 7.1 is n/a']
-- onboarding: the facts (each MUST be refused)
ok FACT: a new template with NO record rc=1 (expected 1)
ok FACT: a record missing id 1.4 rc=1 (expected 1)
ok FACT: 1.4 answered only inside an HTML comment rc=1 (expected 1)
ok FACT: done with a path that does not exist rc=1 (expected 1)
ok FACT: done with an EMPTY directory (the mkdir shape) rc=1 (expected 1)
ok FACT: done naming an absent file in the sibling repo rc=1 (expected 1)
ok FACT: n/a with an EMPTY reason rc=1 (expected 1)
ok FACT: n/a with a two-word reason rc=1 (expected 1)
ok FACT: an OPEN row rc=1 (expected 1)
ok FACT: a checklist id the template a new app copies lacks rc=1 (expected 1)
ok FACT: an exempt app's record with a done that points nowhere rc=1 (expected 1)
-- onboarding: the genuine articles (each MUST pass)
ok GENUINE: a complete record (catalog + sibling evidence) rc=0 (expected 0)
ok GENUINE: an id added AFTER opened: does not bind rc=0 (expected 0)
ok GENUINE: an exempt app's record may say open rc=0 (expected 0)
ok STATED SKIP: sibling repo absent (the CI shape) - printed, not checked rc=0 (expected 0)
MUTANT no cutoff check -> suite RED (1 case(s) failed)
FACT: opened: backdated before the checklist: rc=0 expected 1; missing ['before the checklist existed']
@@ -0,0 +1,35 @@
# upstream reads for wger, 2026-10-01T13:07:39Z (read only: GitHub API + Docker Hub API, anonymous)
repo: wger-project/wger licence: AGPL-3.0 pushed_at: 2026-10-01T02:03:43Z open_issues: 251 archived: False
release: 2.7 2026-09-03T09:33:59Z
release: 2.6 2026-06-17T07:47:47Z
release: 2.5 2026-04-15T20:09:19Z
release: 2.4 2026-01-18T12:12:02Z
release: 2.3 2025-04-05T18:05:36Z
tag: latest 2026-09-29 archs: ['amd64', 'arm64'] size_amd64_MB: [374]
tag: 2.8.0-dev.0 2026-09-29 archs: ['amd64', 'arm64'] size_amd64_MB: [374]
tag: 2.7 2026-09-06 archs: ['amd64', 'arm64'] size_amd64_MB: [375]
tag: 2.7.0 2026-09-06 archs: ['amd64', 'arm64'] size_amd64_MB: [375]
tag: 2.7.0-alpha2 2026-09-02 archs: ['amd64', 'arm64'] size_amd64_MB: [375]
tag: 2.7.0-alpha1 2026-08-11 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [382]
tag: 2.6.0 2026-06-17 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [373]
tag: 2.6 2026-06-17 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [373]
tag: 2.6.0-alpha2 2026-06-17 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [372]
tag: 2.6-dev 2026-05-13 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [371]
tag: 2.5 2026-04-15 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [370]
tag: 2.5-dev 2026-04-15 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [370]
tag: 2.4-dev 2026-01-20 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [270]
tag: 2.4 2026-01-18 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [270]
tag: 2.3-dev 2025-04-05 archs: ['amd64', 'arm', 'arm64', 'unknown'] size_amd64_MB: [152]
tag: routines-beta 2025-03-14 archs: ['amd64', 'arm64', 'unknown'] size_amd64_MB: [247]
tag: routines 2024-11-19 archs: ['arm64'] size_amd64_MB: []
tag: 2.2-dev 2023-12-05 archs: ['amd64', 'arm', 'arm64', 'unknown'] size_amd64_MB: [262]
tag: master 2023-08-07 archs: ['amd64', 'arm', 'arm64', 'unknown'] size_amd64_MB: [251]
tag: 2.1-dev 2022-10-12 archs: ['amd64', 'arm64'] size_amd64_MB: [221]
--- wger Flutter app (phone): repo
wger-project/flutter 2026-09-29T03:14:21Z
--- felhom.eu assets
https://felhom.eu/assets/wger-logo.webp -> 404 153B
https://felhom.eu/assets/wger-logo.png -> 200
https://felhom.eu/assets/wger-screenshot-1.webp -> 200
https://felhom.eu/assets/paperless-ngx-logo.svg -> 200
https://felhom.eu/assets/nosuchapp-logo.png -> 404
@@ -0,0 +1,11 @@
== check-image-pins.py (catalog d0e7e2e, 2026-09-29)
image-pin gate OK — 53 templates, 0 unpinned images
gate exit code = 0
== check-probe-matches-compose.py (catalog d0e7e2e, 2026-09-29)
probe-matches-compose: OK — every probe dials the port (and, where it can fail, the path) that the app's own compose healthcheck dials
gate exit code = 0
== check-copy-i18n.py (catalog d0e7e2e, 2026-09-29)
copy-i18n: translated so far — actualbudget 15/15, adventurelog 17/17, audiobookshelf 21/21, bentopdf 14/14, bookstack 21/21, calcom 21/21, calibre-web 25/25, claper 20/20, code-server 20/20, crafty
copy-i18n: OK
gate exit code = 0
@@ -0,0 +1,22 @@
# wger, catalog 6d72c09 (= live main), 2026-10-01T13:16:34Z
== check-image-pins.py
image-pin gate OK — 53 templates, 0 unpinned images
gate exit code = 0
== check-image-resolvable.py wger
resolving 1 unique image pin(s)…
image-resolvability gate OK — 1 unique pins, all resolve
gate exit code = 0
== check-probe-matches-compose.py wger
probe-matches-compose: OK — every probe dials the port (and, where it can fail, the path) that the app's own compose healthcheck dials
gate exit code = 0
== check-probe-measured.py wger
probe-measured: OK — 0 probes, each with a measured before/after
gate exit code = 0
== check-copy-i18n.py wger
copy-i18n: translated so far — actualbudget 15/15, adventurelog 18/18, audiobookshelf 21/21, bentopdf 14/14, bookstack 21/21, calcom 22/22, calibre-web 27/27, claper 20/20, code-server 20/20, crafty-controller 24/24, d
copy-i18n: OK
gate exit code = 0
== check-test-record.py wger
test-record gate — 1 template(s) read, 1 carry a ladder, 0 convicted
gate exit code = 0
@@ -0,0 +1,125 @@
# =============================================================================
# .felhom.yml - App metadata for felhom-controller
# =============================================================================
# --- Display info (shown on dashboard) ---
display_name: "wger"
description: "Edzésnapló és fitnesz tervező"
category: "home"
subdomain: "fitness"
slug: "wger"
# catalog_since: the date THIS repo last changed this app's pinned images. Any commit that
# changes an image: line must set this to the same day (see CLAUDE.md).
catalog_since: "2026-07-19"
# --- Resource hints (displayed on deploy screen) ---
resources:
mem_request: "100M"
mem_limit: "384M"
pi_compatible: true
needs_hdd: false
# --- Deploy fields (first deployment only) ---
deploy_fields:
- env_var: DOMAIN
label: "Domain"
type: domain
description: "A szerver domain neve"
locked_after_deploy: true
- env_var: SUBDOMAIN
label: "Aldomain"
type: subdomain
default: "fitness"
required: true
locked_after_deploy: true
description: "Az alkalmazás aldomainje"
- env_var: SECRET_KEY
label: "Titkosítási kulcs"
type: secret
generate: "hex:32"
locked_after_deploy: true
# `09` §3 decision 45: wger starts with admin / adminadmin. The box replaces that password with this generated
# one right after the install (after_install below); the app page shows it as the first password.
- env_var: ADMIN_PASSWORD
label: "Admin jelszó (admin)"
type: password
generate: "password:24"
description: "Az első bejelentkezéshez: admin és ez a jelszó. Utána a beállításokban módosítható."
locked_after_deploy: true
# --- App info (info page content) ---
app_info:
tagline: "Edzésnapló - edzéstervek, haladás követés és testsúly napló"
default_creds: "admin / adminadmin"
docs_url: "https://wger.readthedocs.io/"
use_cases:
- 'Edzéstervek létrehozása és követése'
- 'Testsúly és testméretek nyilvántartása grafikonokkal'
- 'Gyakorlatok adatbázisa képekkel és leírásokkal'
- 'Kalória és tápanyag követés'
- 'API támogatás fitnesz alkalmazás integrációkhoz'
first_steps:
- 'Nyisd meg a fitness.DOMAIN címet a böngészőben'
- 'Jelentkezz be: admin és a Beállítások oldalon látható első jelszó'
- 'Add meg az email címedet a beállításokban'
- 'Hozd létre az edzéstervedet'
- 'Kezdd el naplózni az edzéseidet'
# --- After a fresh install (controller >= 0.279.0, decision 45) ---
# Django's own set_password on the seeded admin. Measured on 9202 2026-09-29: afterwards adminadmin no longer signs in
# at /en/user/login, the new one does.
after_install:
service: wger
env: [ADMIN_PASSWORD]
# The password is the LAST ARGUMENT (sys.argv[1]), never pasted into the code: a quote in it cannot break or change
# the program (security review 2026-09-29). Django is set up the way manage.py does it (settings.main).
command: ["python3", "-c", "import os, sys; sys.path.insert(0, '/home/wger/src'); os.chdir('/home/wger/src'); os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.main'); import django; django.setup(); from django.contrib.auth.models import User; u = User.objects.get(username='admin'); u.set_password(sys.argv[1]); u.save(); print('FELHOM_AFTER_INSTALL_OK')", "${ADMIN_PASSWORD}"]
success: "FELHOM_AFTER_INSTALL_OK"
# --- Controller-side health probe ---
healthcheck:
checks:
- type: http
# 8000, not 80: gunicorn listens on 8000 inside the container; 80 is only
# what traefik publishes. wger's own compose healthcheck dials 127.0.0.1:8000 (R-618).
port: 8000
# --- English copy (localisation slice 5, R-560) --------------------------------------------
# The Hungarian above is UNCHANGED. A box on English reads this block field by field; a missing
# field shows the Hungarian one; a controller older than 0.257.0 ignores the block entirely.
i18n:
en:
description: 'A workout log and fitness planner'
app_info:
tagline: 'A workout log - training plans, progress and a weight diary'
default_creds: 'admin / adminadmin'
use_cases:
- 'Build training plans and follow them'
- 'Track your weight and measurements on a graph'
- 'An exercise database with pictures and descriptions'
- 'Track calories and nutrition'
- 'An API, for fitness app integrations'
first_steps:
- 'Open fitness.DOMAIN in your browser'
- 'Sign in: admin and the first password shown on the settings page'
- 'Add your e-mail address in the settings'
- 'Build your training plan'
- 'Start logging your workouts'
deploy_fields:
- env_var: DOMAIN
label: 'Domain'
description: 'The server domain name'
- env_var: SUBDOMAIN
label: 'Subdomain'
description: 'The subdomain this app answers on'
- env_var: SECRET_KEY
label: 'Encryption key'
- env_var: ADMIN_PASSWORD
label: 'Admin password (admin)'
description: 'For the first sign-in: admin and this password. Change it in the settings afterwards.'
@@ -0,0 +1,65 @@
# wger - Edzésnapló és fitnesz tervező
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: None (file-based)
# RAM: ~100M (mem_limit: 384M) | Pi-compatible: Yes
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
# SECRET_KEY - Titkosítási kulcs (auto-generated)
services:
wger:
image: wger/server:2.6
container_name: wger
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- SECRET_KEY=${SECRET_KEY}
# A wger 2.4+ a TELJES DJANGO_DB_* halmazt beolvassa, akkor is, ha az
# engine sqlite -- enélkül indulás nélkül kilép ("Set the DJANGO_DB_USER
# environment variable"). Az USER/PASSWORD/HOST/PORT értékeket az sqlite
# backend figyelmen kívül hagyja, de jelen kell lenniük.
# A DATABASE a wger_data kötetre mutat (/home/wger/db), oda, ahol a wger
# saját alapértelmezett sqlite fájlja is volt -- így meglévő telepítés
# adatai nem "tűnnek el" egy másik útvonalra.
# R-712 (measured 2026-09-29 on 9202): behind traefik wger saw the request as http and refused a browser's
# https Origin with "CSRF verification failed" — nobody could sign in from a browser.
- CSRF_TRUSTED_ORIGINS=https://${SUBDOMAIN}.${DOMAIN}
- X_FORWARDED_PROTO_HEADER_SET=True
- DJANGO_DB_ENGINE=django.db.backends.sqlite3
- DJANGO_DB_DATABASE=/home/wger/db/database.sqlite
- DJANGO_DB_USER=wger
- DJANGO_DB_PASSWORD=wger
- DJANGO_DB_HOST=localhost
- DJANGO_DB_PORT=5432
- SITE_URL=https://${SUBDOMAIN}.${DOMAIN}
volumes:
- wger_data:/home/wger/db
- wger_media:/home/wger/media
networks:
- traefik-public
deploy:
resources:
limits:
memory: 384M
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8000"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
labels:
- "traefik.enable=true"
- "traefik.http.routers.wger.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)"
- "traefik.http.routers.wger.entrypoints=websecure"
- "traefik.http.routers.wger.tls=true"
- "traefik.http.routers.wger.tls.certresolver=letsencrypt"
- "traefik.http.services.wger.loadbalancer.server.port=8000"
volumes:
wger_data:
wger_media:
networks:
traefik-public:
external: true
@@ -0,0 +1,14 @@
saved copy: 1944 /var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml.pre-checklist1001
git:
branch: main
repo_url: https://gitea.dooplex.hu/admin/app-catalog-drill.git
sync_interval: 15m
token: <redacted>
username: "admin"
hub:
CONTROL 1 — the box's catalog clone:
CONTROL 2 — the LIVE catalog main (unchanged by this act): ['6d72c091e039197758bf999ef8154b13530482b1']
CONTROL 3 — expected source: drill
e9f50b5 calibre-web: the admin login name is generated at install (09 decision 61, R-752)
https://gitea.dooplex.hu/admin/app-catalog-drill.git
@@ -0,0 +1,15 @@
##### wger on 9202 — controller gitea.dooplex.hu/admin/felhom-controller:0.285.0
catalog clone: e9f50b5 calibre-web: the admin login name is generated at install (09 decision 61, R-752)
before: stack/volumes named wger: /opt/docker/stacks/wger
guest swap (free -m): Swap: 512 19 492
sampler started
poller started
deploy press at 15:06:36
deploy -> True
controller log (after_install / hold): 2026/10/01 13:08:09 install_hold.go:135: [INFO] [stacks] wger: install hold OPENED by after_install — the app is reached as without a hold
4.3 press -> docker healthy: 107 s; RestartCount: 0; start_period in compose: start_period: 30s
controller state: running
3.5 stranger polls of admin/adminadmin from the press: 92 tries; codes {'404': 90, '401': 1, '400': 1}
first 3: ['13:06:35 404 404 page not found ', '13:06:36 404 404 page not found ', '13:06:37 404 404 page not found ']
last 3: ['13:08:07 404 404 page not found ', '13:08:08 401 {"error":"this app is waiting for its first setup"}', '13:08:10 400 {"status": 400, "errors": [{"message": "The username and/or password you specified are not']
any 200 with a token: False
@@ -0,0 +1,48 @@
1.5 the server process: python3 manage.py runserver 0.0.0.0:8000 | /usr/bin/python3 manage.py runserver 0.0.0.0:8000
1.7 image entrypoint:
1.7 entrypoint switches (if-tests in /home/wger/entrypoint.sh):
10:if [ "$YARN_PROCESS_STATIC" == "True" ];
21:if [ "$DJANGO_CLEAR_STATIC_FIRST" == "False" ]; then
27:if [[ "$DJANGO_DEBUG" == "False" && "${DJANGO_COLLECTSTATIC_ON_STARTUP:-True}" == "True" ]];
34:if [[ "$DJANGO_PERFORM_MIGRATIONS" == "True" ]];
41:if [[ "$SYNC_EXERCISES_ON_STARTUP" == "True" ]];
48:if [[ "$DOWNLOAD_EXERCISE_IMAGES_ON_STARTUP" == "True" ]];
55:if [[ "$DOWNLOAD_EXERCISE_VIDEOS_ON_STARTUP" == "True" ]];
62:if [[ "$LOAD_ONLINE_FIXTURES_ON_STARTUP" == "True" ]];
69:if [[ "$SYNC_INGREDIENTS_ON_STARTUP" == "True" ]];
81:if [[ "$WGER_USE_GUNICORN" == "True" ]];
0.4/1.7 the IMAGE's env defaults for those switches (+ debug/mail/sync):
the RUNNING container's env, the same switches (values: only these named, non-secret ones):
container: SITE_URL=https://fitness.enkisfelhom.hu
container: AXES_COOLOFF_TIME=5
container: DJANGO_PERFORM_MIGRATIONS=True
container: AXES_HANDLER=axes.handlers.database.AxesDatabaseHandler
container: AXES_LOCKOUT_PARAMETERS=username
container: X_FORWARDED_PROTO_HEADER_SET=True
container: TZ=Europe/Budapest
container: CSRF_TRUSTED_ORIGINS=https://fitness.enkisfelhom.hu
container env NAMES (all): APP_BUILD_COMMIT APP_BUILD_DATE AXES_COOLOFF_TIME AXES_HANDLER AXES_LOCKOUT_PARAMETERS CSRF_TRUSTED_ORIGINS DEBIAN_FRONTEND DJANGO_DB_DATABASE DJANGO_DB_ENGINE DJANGO_DB_HOST DJANGO_DB_PASSWORD DJANGO_DB_PORT DJANGO_DB_USER DJANGO_PERFORM_MIGRATIONS DJANGO_SETTINGS_MODULE LANG LANGUAGE LC_ALL PATH PYTHONDONTWRITEBYTECODE PYTHONPATH PYTHONUNBUFFERED SECRET_KEY SITE_URL TZ X_FORWARDED_PROTO_HEADER_SET
1.6 the env names wger's settings READ (settings/main.py, env.*('NAME'...)):
ACCESS_TOKEN_LIFETIME ALLOW_GUEST_USERS ALLOW_REGISTRATION ALLOW_UPLOAD_VIDEOS AUTH_PROXY_CREATE_UNKNOWN_USER AUTH_PROXY_HEADER AUTH_PROXY_TRUSTED_IPS AUTH_PROXY_USER_EMAIL_HEADER AUTH_PROXY_USER_NAME_HEADER AWS_ACCESS_KEY_ID AWS_S3_DOMAIN AWS_S3_REGION_NAME AWS_SECRET_ACCESS_KEY AWS_STORAGE_BUCKET_NAME AXES_COOLOFF_TIME AXES_ENABLED AXES_FAILURE_LIMIT AXES_HANDLER AXES_IPWARE_PROXY_COUNT AXES_LOCKOUT_PARAMETERS CACHE_API_EXERCISES_CELERY CELERY_BACKEND CELERY_BROKER DJANGO_ADMINS DJANGO_CACHE_BACKEND DJANGO_CACHE_CLIENT_CLASS DJANGO_CACHE_CLIENT_PASSWORD DJANGO_CACHE_CLIENT_SSL_CERTFILE DJANGO_CACHE_CLIENT_SSL_CERT_REQS DJANGO_CACHE_CLIENT_SSL_KEYFILE DJANGO_CACHE_LOCATION DJANGO_CACHE_TIMEOUT DJANGO_DB_DATABASE DJANGO_DB_ENGINE DJANGO_DB_HOST DJANGO_DB_PASSWORD DJANGO_DB_PORT DJANGO_DB_USER DJANGO_MEDIA_ROOT DJANGO_STATIC_ROOT DOWNLOAD_INGREDIENTS_FROM EMAIL_HOST EMAIL_HOST_PASSWORD EMAIL_HOST_USER EMAIL_PORT EMAIL_USE_SSL EMAIL_USE_TLS ENABLE_EMAIL EXERCISE_CACHE_TTL EXPORT_INGREDIENTS_BULK_CELERY EXPOSE_PROMETHEUS_METRICS FROM_EMAIL IDP_OIDC_PRIVATE_KEY JWT_PRIVATE_KEY JWT_PUBLIC_KEY LOGIN_REDIRECT_URL LOG_LEVEL_PYTHON MEDIA_URL MIN_ACCOUNT_AGE_TO_TRUST NUMBER_OF_PROXIES POWERSYNC_TOKEN_LIFETIME POWERSYNC_URL POWERSYNC_URL_PATH RECAPTCHA_PRIVATE_KEY RECAPTCHA_PUBLIC_KEY RECAPTCHA_REQUIRED_SCORE REFRESH_TOKEN_LIFETIME S3_MEDIA_FILES_LOCATION S3_STATIC_FILES_LOCATION SECRET_KEY SECURE_PROXY_SSL_HEADER SITE_URL STATIC_URL SYNC_EXERCISE_IMAGES_CELERY SYNC_EXERCISES_CELERY SYNC_EXERCISE_VIDEOS_CELERY SYNC_INGREDIENTS_CELERY SYNC_OFF_DAILY_DELTA_CELERY TIME_ZONE USE_CELERY USE_RECAPTCHA USE_S3_MEDIA_FILES USE_S3_STATIC_FILES USE_S3_URL_FOR_MEDIA USE_S3_URL_FOR_STATIC USE_X_FORWARDED_HOST WGER_MAX_SESSION_LENGTH_HOURS WGER_SHOW_APP_STORE_LINKS WGER_SOCIAL_PROVIDERS X_FORWARDED_PROTO_HEADER_SET
of which key/secret-like: ['ACCESS_TOKEN_LIFETIME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'DJANGO_CACHE_CLIENT_PASSWORD', 'DJANGO_CACHE_CLIENT_SSL_KEYFILE', 'DJANGO_DB_PASSWORD', 'EMAIL_HOST_PASSWORD', 'IDP_OIDC_PRIVATE_KEY', 'JWT_PRIVATE_KEY', 'JWT_PUBLIC_KEY', 'POWERSYNC_TOKEN_LIFETIME', 'RECAPTCHA_PRIVATE_KEY', 'RECAPTCHA_PUBLIC_KEY', 'REFRESH_TOKEN_LIFETIME', 'SECRET_KEY']
set in the container: ['DJANGO_DB_PASSWORD', 'SECRET_KEY'] NOT set: ['ACCESS_TOKEN_LIFETIME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'DJANGO_CACHE_CLIENT_PASSWORD', 'DJANGO_CACHE_CLIENT_SSL_KEYFILE', 'EMAIL_HOST_PASSWORD', 'IDP_OIDC_PRIVATE_KEY', 'JWT_PRIVATE_KEY', 'JWT_PUBLIC_KEY', 'POWERSYNC_TOKEN_LIFETIME', 'RECAPTCHA_PRIVATE_KEY', 'RECAPTCHA_PUBLIC_KEY', 'REFRESH_TOKEN_LIFETIME']
1.8/0.4 Django settings inside the app:
DEBUG False
EMAIL_BACKEND django.core.mail.backends.console.EmailBackend
WGER_SETTINGS sync/download: {'ALLOW_GUEST_USERS': True, 'ALLOW_REGISTRATION': True, 'ALLOW_UPLOAD_VIDEOS': True, 'EMAIL_FROM': 'wger Workout Manager <wger@example.com>', 'DOWNLOAD_INGREDIENTS_FROM': 'WGER', 'SYNC_EXERCISES_CELERY': False, 'SYNC_EXERCISE_IMAGES_CELERY': False, 'SYNC_EXERCISE_VIDEOS_CELERY': False, 'SYNC_INGREDIENTS_CELERY': False, 'SYNC_OFF_DAILY_DELTA_CELERY': False, 'SYNC_INGREDIENTS_DUMP_URL': 'https://wger.de/media/ingredients/ingredients.jsonl.gz', 'TWITTER': False, 'MASTODON': 'https://fosstodon.org/@wger', 'USE_CELERY': False}
JWT key loaded: False (env in this shell is not the server env)
0.4/0.5 the container's first-start log lines about the network (sync/download/http):
Apply all migrations: account, actstream, allauth_idp_oidc, auth, authtoken, axes, config, contenttypes, core, easy_thumbnails, exercises, gallery, gym, mailer, manager, measurements, mfa, nutrition, sessions, sites, socialaccount, token_blacklist, trophies, weight
Applying nutrition.0012_alter_ingredient_license_author... OK
Applying exercises.0001_initial... OK
Applying nutrition.0013_ingredient_image... OK
Applying nutrition.0015_alter_ingredient_creation_date_and_more... OK
Applying nutrition.0024_remove_ingredient_status... OK
Applying nutrition.0028_ingredient_dietary_properties... OK
Applying nutrition.0029_ingredient_nutriscore... OK
Applying manager.0011_remove_set_exercises... OK
Applying exercises.0002_auto_20150307_1841... OK
Applying exercises.0003_auto_20160921_2000... OK
Applying exercises.0004_auto_20170404_0114... OK
outbound TCP connections the wger process holds right now: 1 020012AC:CDBA
1 060012AC:CC06
@@ -0,0 +1,6 @@
3.9 browser form, https Origin + CSRF, RIGHT password: ok
3.9 browser form, WRONG password: POST /en/user/login -> 200 (refused)
3.9/0.7 phone-app route /allauth/app/v1/auth/login, RIGHT password: http=200 keys=['access_token', 'is_authenticated', 'refresh_token', 'session_token']
the API with that token: http=200
3.9/0.7 phone-app route /allauth/app/v1/auth/login, WRONG password: http=400 keys=['errors', 'status']
1.8 unknown page through traefik: http=404 debug-page=False traceback=False
@@ -0,0 +1,8 @@
wger: POST /api/v2/weightentry/ http=201
seed: {'weight': '84.42'}
2.8 POST /api/v2/gallery/ (multipart, through traefik): http=201 {"id":1,"date":"2026-10-01","image":"…","description":"felhom pilot","heig
2.8 GET the uploaded image back through traefik (/media/gallery/1/def0a0d8-3cc7-4384-96df-ddfcdfaefb8d.png): http=404 bytes=2830 same=False
2.8 the same image with NO session: http=404
wger: readback of the seeded weight entry http=200 found=True
verify (fixture readback, with its negative controls): True
2.7 the volumes' size after the seed: wger_wger_data=4.2M wger_wger_media=16K
@@ -0,0 +1,3 @@
4.4 negative control: docker pause wger at 15:09:17 — controller state before: running
states seen while paused (s, (state, health)): [(4, ('stopped', None))]
after unpause, state running again after 40 s
@@ -0,0 +1,2 @@
5.1 wger cgroup from birth: 90 samples over 183 s; limit 402653184
peak anon 266.8 MiB (69.5 % of the limit); peak swap 0 bytes; oom_kill max 0; last: 0 running healthy
@@ -0,0 +1,91 @@
epoch,anon,current,swap,oom_kill,limit,restarts status health
1790860027.558,34242560,37122048,0,0,402653184,0 running starting
1790860029.614,18444288,25464832,0,0,402653184,0 running starting
1790860031.672,90976256,98574336,0,0,402653184,0 running starting
1790860033.723,105848832,113618944,0,0,402653184,0 running starting
1790860035.782,110673920,118657024,0,0,402653184,0 running starting
1790860037.837,111398912,119705600,0,0,402653184,0 running starting
1790860039.892,111403008,119754752,0,0,402653184,0 running starting
1790860041.949,112967680,121237504,0,0,402653184,0 running starting
1790860044.001,115335168,123891712,0,0,402653184,0 running starting
1790860046.057,117252096,125751296,0,0,402653184,0 running starting
1790860048.107,117264384,126058496,0,0,402653184,0 running starting
1790860050.164,119808000,128299008,0,0,402653184,0 running starting
1790860052.220,120111104,128561152,0,0,402653184,0 running starting
1790860054.275,120111104,128749568,0,0,402653184,0 running starting
1790860056.329,120135680,128872448,0,0,402653184,0 running starting
1790860058.379,120135680,128794624,0,0,402653184,0 running starting
1790860060.436,120135680,128815104,0,0,402653184,0 running starting
1790860062.486,120135680,128634880,0,0,402653184,0 running starting
1790860064.545,120143872,128770048,0,0,402653184,0 running starting
1790860066.596,121110528,129777664,0,0,402653184,0 running starting
1790860068.655,121233408,130125824,0,0,402653184,0 running starting
1790860070.707,122281984,131215360,0,0,402653184,0 running starting
1790860072.769,122286080,131518464,0,0,402653184,0 running starting
1790860074.818,28049408,39301120,0,0,402653184,0 running starting
1790860076.874,110399488,121991168,0,0,402653184,0 running starting
1790860078.933,99303424,110919680,0,0,402653184,0 running starting
1790860080.990,109289472,121643008,0,0,402653184,0 running starting
1790860083.042,189288448,202596352,0,0,402653184,0 running starting
1790860085.086,191352832,204468224,0,0,402653184,0 running starting
1790860087.129,192958464,207355904,0,0,402653184,0 running starting
1790860089.200,279789568,304762880,0,0,402653184,0 running healthy
1790860091.244,197033984,210419712,0,0,402653184,0 running healthy
1790860093.290,197033984,210309120,0,0,402653184,0 running healthy
1790860095.334,197505024,211095552,0,0,402653184,0 running healthy
1790860097.384,197505024,211075072,0,0,402653184,0 running healthy
1790860099.438,197505024,210812928,0,0,402653184,0 running healthy
1790860101.495,197505024,211075072,0,0,402653184,0 running healthy
1790860103.545,197505024,210812928,0,0,402653184,0 running healthy
1790860105.590,197505024,211075072,0,0,402653184,0 running healthy
1790860107.649,197505024,210812928,0,0,402653184,0 running healthy
1790860109.691,197505024,210808832,0,0,402653184,0 running healthy
1790860111.735,197505024,211070976,0,0,402653184,0 running healthy
1790860113.790,197505024,210808832,0,0,402653184,0 running healthy
1790860115.844,197505024,210808832,0,0,402653184,0 running healthy
1790860117.893,198356992,212152320,0,0,402653184,0 running healthy
1790860119.953,198356992,211935232,0,0,402653184,0 running healthy
1790860122.009,198356992,211673088,0,0,402653184,0 running healthy
1790860124.075,198356992,211931136,0,0,402653184,0 running healthy
1790860126.124,198356992,212152320,0,0,402653184,0 running healthy
1790860128.175,198356992,211968000,0,0,402653184,0 running healthy
1790860130.231,198356992,211705856,0,0,402653184,0 running healthy
1790860132.288,198356992,211980288,0,0,402653184,0 running healthy
1790860134.342,198356992,211709952,0,0,402653184,0 running healthy
1790860136.398,198356992,211972096,0,0,402653184,0 running healthy
1790860138.453,198356992,211709952,0,0,402653184,0 running healthy
1790860140.506,198356992,212295680,0,0,402653184,0 running healthy
1790860142.560,198356992,211980288,0,0,402653184,0 running healthy
1790860144.608,271659008,285794304,0,0,402653184,0 running healthy
1790860146.664,198356992,211714048,0,0,402653184,0 running healthy
1790860148.718,198365184,211906560,0,0,402653184,0 running healthy
1790860150.769,198365184,211722240,0,0,402653184,0 running healthy
1790860152.816,198483968,212156416,0,0,402653184,0 running healthy
1790860154.870,201388032,215052288,0,0,402653184,0 running healthy
1790860156.928,201388032,214929408,0,0,402653184,0 running healthy
1790860158.982,201392128,215191552,0,0,402653184,0 running healthy
1790860161.025,201392128,215191552,0,0,402653184,0 paused unhealthy
1790860163.070,201392128,215191552,0,0,402653184,0 paused unhealthy
1790860165.119,201392128,215191552,0,0,402653184,0 paused unhealthy
1790860167.178,201392128,215191552,0,0,402653184,0 running unhealthy
1790860169.223,201392128,215191552,0,0,402653184,0 running unhealthy
1790860171.267,201392128,215191552,0,0,402653184,0 running unhealthy
1790860173.325,201392128,215187456,0,0,402653184,0 running unhealthy
1790860175.380,201392128,215187456,0,0,402653184,0 running unhealthy
1790860177.433,201392128,215187456,0,0,402653184,0 running unhealthy
1790860179.477,201392128,214925312,0,0,402653184,0 running unhealthy
1790860181.522,201392128,215187456,0,0,402653184,0 running unhealthy
1790860183.576,201392128,215187456,0,0,402653184,0 running unhealthy
1790860185.623,201359360,215117824,0,0,402653184,0 running unhealthy
1790860187.666,201359360,215379968,0,0,402653184,0 running unhealthy
1790860189.713,201359360,215117824,0,0,402653184,0 running unhealthy
1790860191.758,201359360,215117824,0,0,402653184,0 running unhealthy
1790860193.802,201359360,215117824,0,0,402653184,0 running unhealthy
1790860195.849,201359360,215379968,0,0,402653184,0 running unhealthy
1790860197.894,201752576,215539712,0,0,402653184,0 running healthy
1790860199.943,201752576,215515136,0,0,402653184,0 running healthy
1790860201.993,201752576,215515136,0,0,402653184,0 running healthy
1790860204.044,201752576,215252992,0,0,402653184,0 running healthy
1790860206.096,201752576,215515136,0,0,402653184,0 running healthy
1790860208.144,201752576,215515136,0,0,402653184,0 running healthy
1790860210.194,201752576,215515136,0,0,402653184,0 running healthy
1 epoch anon current swap oom_kill limit restarts status health
2 1790860027.558 34242560 37122048 0 0 402653184 0 running starting
3 1790860029.614 18444288 25464832 0 0 402653184 0 running starting
4 1790860031.672 90976256 98574336 0 0 402653184 0 running starting
5 1790860033.723 105848832 113618944 0 0 402653184 0 running starting
6 1790860035.782 110673920 118657024 0 0 402653184 0 running starting
7 1790860037.837 111398912 119705600 0 0 402653184 0 running starting
8 1790860039.892 111403008 119754752 0 0 402653184 0 running starting
9 1790860041.949 112967680 121237504 0 0 402653184 0 running starting
10 1790860044.001 115335168 123891712 0 0 402653184 0 running starting
11 1790860046.057 117252096 125751296 0 0 402653184 0 running starting
12 1790860048.107 117264384 126058496 0 0 402653184 0 running starting
13 1790860050.164 119808000 128299008 0 0 402653184 0 running starting
14 1790860052.220 120111104 128561152 0 0 402653184 0 running starting
15 1790860054.275 120111104 128749568 0 0 402653184 0 running starting
16 1790860056.329 120135680 128872448 0 0 402653184 0 running starting
17 1790860058.379 120135680 128794624 0 0 402653184 0 running starting
18 1790860060.436 120135680 128815104 0 0 402653184 0 running starting
19 1790860062.486 120135680 128634880 0 0 402653184 0 running starting
20 1790860064.545 120143872 128770048 0 0 402653184 0 running starting
21 1790860066.596 121110528 129777664 0 0 402653184 0 running starting
22 1790860068.655 121233408 130125824 0 0 402653184 0 running starting
23 1790860070.707 122281984 131215360 0 0 402653184 0 running starting
24 1790860072.769 122286080 131518464 0 0 402653184 0 running starting
25 1790860074.818 28049408 39301120 0 0 402653184 0 running starting
26 1790860076.874 110399488 121991168 0 0 402653184 0 running starting
27 1790860078.933 99303424 110919680 0 0 402653184 0 running starting
28 1790860080.990 109289472 121643008 0 0 402653184 0 running starting
29 1790860083.042 189288448 202596352 0 0 402653184 0 running starting
30 1790860085.086 191352832 204468224 0 0 402653184 0 running starting
31 1790860087.129 192958464 207355904 0 0 402653184 0 running starting
32 1790860089.200 279789568 304762880 0 0 402653184 0 running healthy
33 1790860091.244 197033984 210419712 0 0 402653184 0 running healthy
34 1790860093.290 197033984 210309120 0 0 402653184 0 running healthy
35 1790860095.334 197505024 211095552 0 0 402653184 0 running healthy
36 1790860097.384 197505024 211075072 0 0 402653184 0 running healthy
37 1790860099.438 197505024 210812928 0 0 402653184 0 running healthy
38 1790860101.495 197505024 211075072 0 0 402653184 0 running healthy
39 1790860103.545 197505024 210812928 0 0 402653184 0 running healthy
40 1790860105.590 197505024 211075072 0 0 402653184 0 running healthy
41 1790860107.649 197505024 210812928 0 0 402653184 0 running healthy
42 1790860109.691 197505024 210808832 0 0 402653184 0 running healthy
43 1790860111.735 197505024 211070976 0 0 402653184 0 running healthy
44 1790860113.790 197505024 210808832 0 0 402653184 0 running healthy
45 1790860115.844 197505024 210808832 0 0 402653184 0 running healthy
46 1790860117.893 198356992 212152320 0 0 402653184 0 running healthy
47 1790860119.953 198356992 211935232 0 0 402653184 0 running healthy
48 1790860122.009 198356992 211673088 0 0 402653184 0 running healthy
49 1790860124.075 198356992 211931136 0 0 402653184 0 running healthy
50 1790860126.124 198356992 212152320 0 0 402653184 0 running healthy
51 1790860128.175 198356992 211968000 0 0 402653184 0 running healthy
52 1790860130.231 198356992 211705856 0 0 402653184 0 running healthy
53 1790860132.288 198356992 211980288 0 0 402653184 0 running healthy
54 1790860134.342 198356992 211709952 0 0 402653184 0 running healthy
55 1790860136.398 198356992 211972096 0 0 402653184 0 running healthy
56 1790860138.453 198356992 211709952 0 0 402653184 0 running healthy
57 1790860140.506 198356992 212295680 0 0 402653184 0 running healthy
58 1790860142.560 198356992 211980288 0 0 402653184 0 running healthy
59 1790860144.608 271659008 285794304 0 0 402653184 0 running healthy
60 1790860146.664 198356992 211714048 0 0 402653184 0 running healthy
61 1790860148.718 198365184 211906560 0 0 402653184 0 running healthy
62 1790860150.769 198365184 211722240 0 0 402653184 0 running healthy
63 1790860152.816 198483968 212156416 0 0 402653184 0 running healthy
64 1790860154.870 201388032 215052288 0 0 402653184 0 running healthy
65 1790860156.928 201388032 214929408 0 0 402653184 0 running healthy
66 1790860158.982 201392128 215191552 0 0 402653184 0 running healthy
67 1790860161.025 201392128 215191552 0 0 402653184 0 paused unhealthy
68 1790860163.070 201392128 215191552 0 0 402653184 0 paused unhealthy
69 1790860165.119 201392128 215191552 0 0 402653184 0 paused unhealthy
70 1790860167.178 201392128 215191552 0 0 402653184 0 running unhealthy
71 1790860169.223 201392128 215191552 0 0 402653184 0 running unhealthy
72 1790860171.267 201392128 215191552 0 0 402653184 0 running unhealthy
73 1790860173.325 201392128 215187456 0 0 402653184 0 running unhealthy
74 1790860175.380 201392128 215187456 0 0 402653184 0 running unhealthy
75 1790860177.433 201392128 215187456 0 0 402653184 0 running unhealthy
76 1790860179.477 201392128 214925312 0 0 402653184 0 running unhealthy
77 1790860181.522 201392128 215187456 0 0 402653184 0 running unhealthy
78 1790860183.576 201392128 215187456 0 0 402653184 0 running unhealthy
79 1790860185.623 201359360 215117824 0 0 402653184 0 running unhealthy
80 1790860187.666 201359360 215379968 0 0 402653184 0 running unhealthy
81 1790860189.713 201359360 215117824 0 0 402653184 0 running unhealthy
82 1790860191.758 201359360 215117824 0 0 402653184 0 running unhealthy
83 1790860193.802 201359360 215117824 0 0 402653184 0 running unhealthy
84 1790860195.849 201359360 215379968 0 0 402653184 0 running unhealthy
85 1790860197.894 201752576 215539712 0 0 402653184 0 running healthy
86 1790860199.943 201752576 215515136 0 0 402653184 0 running healthy
87 1790860201.993 201752576 215515136 0 0 402653184 0 running healthy
88 1790860204.044 201752576 215252992 0 0 402653184 0 running healthy
89 1790860206.096 201752576 215515136 0 0 402653184 0 running healthy
90 1790860208.144 201752576 215515136 0 0 402653184 0 running healthy
91 1790860210.194 201752576 215515136 0 0 402653184 0 running healthy
@@ -0,0 +1,4 @@
2.6 stop -> 200
2.6 remove, KEEP drive data (wger has no drive data) -> 200 {'ok': True, 'data': {'removed': 'wger', 'volumes_removed': ['wger_wger_data', 'wger_wger_media'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'backup_paths_removed': ['/mnt/sys_drive/felhom-d
left after: stack dir / containers / volumes: /opt/docker/stacks/wger
the image (kept per decision 53 until the sweep):
@@ -0,0 +1,55 @@
##### wger second walk — controller gitea.dooplex.hu/admin/felhom-controller:0.285.0 catalog e9f50b5 calibre-web: the admin login name is generated at install (09 decision 61, R-752)
deploy -> True
household admin signs in (the admin exists, the setup is done): ok
== 3.4 a STRANGER, after the setup — no dashboard session, no gate cookie, through traefik
wger source: where registration and guests are decided:
/home/wger/src/wger/utils/context_processor.py:36: 'allow_registration': settings.WGER_SETTINGS.get('ALLOW_REGISTRATION', False),
/home/wger/src/wger/utils/middleware.py:63: settings.WGER_SETTINGS['ALLOW_GUEST_USERS']
/home/wger/src/wger/core/views/misc.py:55: if not settings.WGER_SETTINGS['ALLOW_GUEST_USERS']:
/home/wger/src/wger/core/views/user.py:291: if not settings.WGER_SETTINGS['ALLOW_REGISTRATION']:
/home/wger/src/wger/core/account_adapter.py:36: return settings.WGER_SETTINGS['ALLOW_REGISTRATION']
/home/wger/src/wger/software/views.py:74: context['allow_registration'] = settings.WGER_SETTINGS['ALLOW_REGISTRATION']
/home/wger/src/wger/software/views.py:75: context['allow_guest_users'] = settings.WGER_SETTINGS['ALLOW_GUEST_USERS']
GET /en/user/registration -> 200 form fields: ['csrfmiddlewaretoken', 'email', 'language', 'next', 'password1', 'password2', 'username', 'viewport']
POST /en/user/registration as stranger272284 -> 302; form errors: []
the stranger then signs in with that account -> 302 (302 = IN)
and reads the API as that user -> 200
the app's own user list now (admin's view, count only): 2 ['admin', 'stranger']
-- guests: wger's own 'try as guest' path, as a stranger
/home/wger/src/wger/core/views/user.py:283: closed" page), and temporary (guest) users may still reach the
/home/wger/src/wger/core/views/user.py:787: allauth's login view, with one wger carve-out: temporary (guest) users are
/home/wger/src/wger/utils/context_processor.py:55: # Flag for guest users
/home/wger/src/wger/utils/middleware.py:29:from wger.core.demo import create_temporary_user
/home/wger/src/wger/utils/middleware.py:43: # Don't create guest users for requests that are accessing the site
/home/wger/src/wger/utils/middleware.py:68: logger.debug('creating a new guest user now')
/home/wger/src/wger/utils/middleware.py:69: user = create_temporary_user(request)
GET /en/user/demo-entries as a stranger -> 500
GET /en/dashboard as a stranger -> 200
users after the guest tries: 4 ['2167c0bc', '792dae77', 'admin', 'stranger']
== 2.8 the uploaded photo: on the volume? who serves /media/?
POST /api/v2/gallery/ -> 201 image path: /media/gallery/1/e91d6ece-866d-44c7-9e3f-1a6db6a6107b.png
the file on the media volume: -rw-r--r-- 1 wger wger 70 Oct 1 15:13 /home/wger/media/gallery/1/e91d6ece-866d-44c7-9e3f-1a6db6a6107b.png
GET it through traefik, signed in -> 404
inside the container, straight at runserver (no traefik): HTTP Error 404: Not Found
wger's urls.py on /media and /static: 350: 'api/v2/min-server-version/',
352: name='min_server_version',
393:# URL for user uploaded files, served like this during development only
396: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
whitenoise / static middleware in settings: /home/wger/src/settings/ci.py:117:MEDIA_ROOT = '/tmp/'
/home/wger/src/settings/main.py:142:MEDIA_ROOT = env.str('DJANGO_MEDIA_ROOT', '/home/wger/media')
/home/wger/src/settings/main.py:143:STATIC_ROOT = env.str('DJANGO_STATIC_ROOT', '/home/wger/static')
/home/wger/src/settings/settings_global.py:48:MEDIA_ROOT = BASE_DIR.parent / 'media'
/home/wger/src/settings/settings_global.py:49:STATIC_ROOT = BASE_DIR.parent / 'static'
upstream's own production compose serves /media with nginx — the image's docs: celery
Dockerfile
entrypoint.sh
README.md
== 1.7 static files (collectstatic needs DJANGO_DEBUG == "False"; the template sets none)
static refs on the login page: ['/static/css/workout-manager.css', '/static/bootstrap-compiled.css', '/static/css/bootstrap-custom.css']
GET /static/css/workout-manager.css -> 404 (2830 chars)
GET /static/bootstrap-compiled.css -> 404 (2830 chars)
static root inside the container: 4.0K /home/wger/static
@@ -0,0 +1,2 @@
remove WITH data -> 200 {'ok': True, 'data': {'removed': 'wger', 'volumes_removed': ['wger_wger_data', 'wger_wger_media'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját adato
left after: stack dir / containers / volumes: /opt/docker/stacks/wger
@@ -0,0 +1,26 @@
git:
branch: main
repo_url: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
sync_interval: 15m
token: <redacted>
username: ""
hub:
CONTROL 1 — the box's catalog clone: 6d72c09 Merge branch 'main' of https://gitea.dooplex.hu/admin/app-catalog-felhom.eu
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
CONTROL 2 — the LIVE catalog main (unchanged by this act): ['6d72c091e039197758bf999ef8154b13530482b1']
CONTROL 3 — expected source: live
leftovers named wger: /opt/docker/stacks/wger
6d72c09 Merge branch 'main' of https://gitea.dooplex.hu/admin/app-catalog-felhom.eu
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
filebrowser Up 7 hours (healthy)
felhom-controller Up 32 seconds (healthy)
paperless-webserver Up 13 hours (healthy)
paperless-redis Up 13 hours (healthy)
paperless-postgres Up 13 hours (healthy)
traefik Up 6 days
/root/wger-mem.csv
/root/wger-poll.txt
0
0
actualbudget adventurelog audiobookshelf bentopdf bookstack calcom calibre-web chaoscrash chaosoom chaosoomb claper code-server crafty-controller docmost emby filebrowser ghost gitea glance gokapi grafana gramps-web home-assistant homebox homepage immich jellyfin kimai komga mealie
@@ -0,0 +1,89 @@
##### wger on 9202 — controller gitea.dooplex.hu/admin/felhom-controller:0.285.0
catalog clone: e9f50b5 calibre-web: the admin login name is generated at install (09 decision 61, R-752)
before: stack/volumes named wger: /opt/docker/stacks/wger
guest swap (free -m): Swap: 512 19 492
sampler started
poller started
deploy press at 15:06:36
15:06:36 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['ADMIN_PASSWORD']
15:06:37 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
15:08:12 [1] deployed, controller state=running, pinned={'wger': 'wger/server:2.7'}
deploy -> True
controller log (after_install / hold): 2026/10/01 13:08:09 install_hold.go:135: [INFO] [stacks] wger: install hold OPENED by after_install — the app is reached as without a hold
4.3 press -> docker healthy: 107 s; RestartCount: 0; start_period in compose: start_period: 30s
controller state: running
3.5 stranger polls of admin/adminadmin from the press: 92 tries; codes {'404': 90, '401': 1, '400': 1}
first 3: ['13:06:35 404 404 page not found ', '13:06:36 404 404 page not found ', '13:06:37 404 404 page not found ']
last 3: ['13:08:07 404 404 page not found ', '13:08:08 401 {"error":"this app is waiting for its first setup"}', '13:08:10 400 {"status": 400, "errors": [{"message": "The username and/or password you specified are not']
any 200 with a token: False
1.5 the server process: python3 manage.py runserver 0.0.0.0:8000 | /usr/bin/python3 manage.py runserver 0.0.0.0:8000
1.7 image entrypoint:
1.7 entrypoint switches (if-tests in /home/wger/entrypoint.sh):
10:if [ "$YARN_PROCESS_STATIC" == "True" ];
21:if [ "$DJANGO_CLEAR_STATIC_FIRST" == "False" ]; then
27:if [[ "$DJANGO_DEBUG" == "False" && "${DJANGO_COLLECTSTATIC_ON_STARTUP:-True}" == "True" ]];
34:if [[ "$DJANGO_PERFORM_MIGRATIONS" == "True" ]];
41:if [[ "$SYNC_EXERCISES_ON_STARTUP" == "True" ]];
48:if [[ "$DOWNLOAD_EXERCISE_IMAGES_ON_STARTUP" == "True" ]];
55:if [[ "$DOWNLOAD_EXERCISE_VIDEOS_ON_STARTUP" == "True" ]];
62:if [[ "$LOAD_ONLINE_FIXTURES_ON_STARTUP" == "True" ]];
69:if [[ "$SYNC_INGREDIENTS_ON_STARTUP" == "True" ]];
81:if [[ "$WGER_USE_GUNICORN" == "True" ]];
0.4/1.7 the IMAGE's env defaults for those switches (+ debug/mail/sync):
the RUNNING container's env, the same switches (values: only these named, non-secret ones):
container: SITE_URL=https://fitness.enkisfelhom.hu
container: AXES_COOLOFF_TIME=5
container: DJANGO_PERFORM_MIGRATIONS=True
container: AXES_HANDLER=axes.handlers.database.AxesDatabaseHandler
container: AXES_LOCKOUT_PARAMETERS=username
container: X_FORWARDED_PROTO_HEADER_SET=True
container: TZ=Europe/Budapest
container: CSRF_TRUSTED_ORIGINS=https://fitness.enkisfelhom.hu
container env NAMES (all): APP_BUILD_COMMIT APP_BUILD_DATE AXES_COOLOFF_TIME AXES_HANDLER AXES_LOCKOUT_PARAMETERS CSRF_TRUSTED_ORIGINS DEBIAN_FRONTEND DJANGO_DB_DATABASE DJANGO_DB_ENGINE DJANGO_DB_HOST DJANGO_DB_PASSWORD DJANGO_DB_PORT DJANGO_DB_USER DJANGO_PERFORM_MIGRATIONS DJANGO_SETTINGS_MODULE LANG LANGUAGE LC_ALL PATH PYTHONDONTWRITEBYTECODE PYTHONPATH PYTHONUNBUFFERED SECRET_KEY SITE_URL TZ X_FORWARDED_PROTO_HEADER_SET
1.6 the env names wger's settings READ (settings/main.py, env.*('NAME'...)):
ACCESS_TOKEN_LIFETIME ALLOW_GUEST_USERS ALLOW_REGISTRATION ALLOW_UPLOAD_VIDEOS AUTH_PROXY_CREATE_UNKNOWN_USER AUTH_PROXY_HEADER AUTH_PROXY_TRUSTED_IPS AUTH_PROXY_USER_EMAIL_HEADER AUTH_PROXY_USER_NAME_HEADER AWS_ACCESS_KEY_ID AWS_S3_DOMAIN AWS_S3_REGION_NAME AWS_SECRET_ACCESS_KEY AWS_STORAGE_BUCKET_NAME AXES_COOLOFF_TIME AXES_ENABLED AXES_FAILURE_LIMIT AXES_HANDLER AXES_IPWARE_PROXY_COUNT AXES_LOCKOUT_PARAMETERS CACHE_API_EXERCISES_CELERY CELERY_BACKEND CELERY_BROKER DJANGO_ADMINS DJANGO_CACHE_BACKEND DJANGO_CACHE_CLIENT_CLASS DJANGO_CACHE_CLIENT_PASSWORD DJANGO_CACHE_CLIENT_SSL_CERTFILE DJANGO_CACHE_CLIENT_SSL_CERT_REQS DJANGO_CACHE_CLIENT_SSL_KEYFILE DJANGO_CACHE_LOCATION DJANGO_CACHE_TIMEOUT DJANGO_DB_DATABASE DJANGO_DB_ENGINE DJANGO_DB_HOST DJANGO_DB_PASSWORD DJANGO_DB_PORT DJANGO_DB_USER DJANGO_MEDIA_ROOT DJANGO_STATIC_ROOT DOWNLOAD_INGREDIENTS_FROM EMAIL_HOST EMAIL_HOST_PASSWORD EMAIL_HOST_USER EMAIL_PORT EMAIL_USE_SSL EMAIL_USE_TLS ENABLE_EMAIL EXERCISE_CACHE_TTL EXPORT_INGREDIENTS_BULK_CELERY EXPOSE_PROMETHEUS_METRICS FROM_EMAIL IDP_OIDC_PRIVATE_KEY JWT_PRIVATE_KEY JWT_PUBLIC_KEY LOGIN_REDIRECT_URL LOG_LEVEL_PYTHON MEDIA_URL MIN_ACCOUNT_AGE_TO_TRUST NUMBER_OF_PROXIES POWERSYNC_TOKEN_LIFETIME POWERSYNC_URL POWERSYNC_URL_PATH RECAPTCHA_PRIVATE_KEY RECAPTCHA_PUBLIC_KEY RECAPTCHA_REQUIRED_SCORE REFRESH_TOKEN_LIFETIME S3_MEDIA_FILES_LOCATION S3_STATIC_FILES_LOCATION SECRET_KEY SECURE_PROXY_SSL_HEADER SITE_URL STATIC_URL SYNC_EXERCISE_IMAGES_CELERY SYNC_EXERCISES_CELERY SYNC_EXERCISE_VIDEOS_CELERY SYNC_INGREDIENTS_CELERY SYNC_OFF_DAILY_DELTA_CELERY TIME_ZONE USE_CELERY USE_RECAPTCHA USE_S3_MEDIA_FILES USE_S3_STATIC_FILES USE_S3_URL_FOR_MEDIA USE_S3_URL_FOR_STATIC USE_X_FORWARDED_HOST WGER_MAX_SESSION_LENGTH_HOURS WGER_SHOW_APP_STORE_LINKS WGER_SOCIAL_PROVIDERS X_FORWARDED_PROTO_HEADER_SET
of which key/secret-like: ['ACCESS_TOKEN_LIFETIME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'DJANGO_CACHE_CLIENT_PASSWORD', 'DJANGO_CACHE_CLIENT_SSL_KEYFILE', 'DJANGO_DB_PASSWORD', 'EMAIL_HOST_PASSWORD', 'IDP_OIDC_PRIVATE_KEY', 'JWT_PRIVATE_KEY', 'JWT_PUBLIC_KEY', 'POWERSYNC_TOKEN_LIFETIME', 'RECAPTCHA_PRIVATE_KEY', 'RECAPTCHA_PUBLIC_KEY', 'REFRESH_TOKEN_LIFETIME', 'SECRET_KEY']
set in the container: ['DJANGO_DB_PASSWORD', 'SECRET_KEY'] NOT set: ['ACCESS_TOKEN_LIFETIME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'DJANGO_CACHE_CLIENT_PASSWORD', 'DJANGO_CACHE_CLIENT_SSL_KEYFILE', 'EMAIL_HOST_PASSWORD', 'IDP_OIDC_PRIVATE_KEY', 'JWT_PRIVATE_KEY', 'JWT_PUBLIC_KEY', 'POWERSYNC_TOKEN_LIFETIME', 'RECAPTCHA_PRIVATE_KEY', 'RECAPTCHA_PUBLIC_KEY', 'REFRESH_TOKEN_LIFETIME']
1.8/0.4 Django settings inside the app:
DEBUG False
EMAIL_BACKEND django.core.mail.backends.console.EmailBackend
WGER_SETTINGS sync/download: {'ALLOW_GUEST_USERS': True, 'ALLOW_REGISTRATION': True, 'ALLOW_UPLOAD_VIDEOS': True, 'EMAIL_FROM': 'wger Workout Manager <wger@example.com>', 'DOWNLOAD_INGREDIENTS_FROM': 'WGER', 'SYNC_EXERCISES_CELERY': False, 'SYNC_EXERCISE_IMAGES_CELERY': False, 'SYNC_EXERCISE_VIDEOS_CELERY': False, 'SYNC_INGREDIENTS_CELERY': False, 'SYNC_OFF_DAILY_DELTA_CELERY': False, 'SYNC_INGREDIENTS_DUMP_URL': 'https://wger.de/media/ingredients/ingredients.jsonl.gz', 'TWITTER': False, 'MASTODON': 'https://fosstodon.org/@wger', 'USE_CELERY': False}
JWT key loaded: False (env in this shell is not the server env)
0.4/0.5 the container's first-start log lines about the network (sync/download/http):
Apply all migrations: account, actstream, allauth_idp_oidc, auth, authtoken, axes, config, contenttypes, core, easy_thumbnails, exercises, gallery, gym, mailer, manager, measurements, mfa, nutrition, sessions, sites, socialaccount, token_blacklist, trophies, weight
Applying nutrition.0012_alter_ingredient_license_author... OK
Applying exercises.0001_initial... OK
Applying nutrition.0013_ingredient_image... OK
Applying nutrition.0015_alter_ingredient_creation_date_and_more... OK
Applying nutrition.0024_remove_ingredient_status... OK
Applying nutrition.0028_ingredient_dietary_properties... OK
Applying nutrition.0029_ingredient_nutriscore... OK
Applying manager.0011_remove_set_exercises... OK
Applying exercises.0002_auto_20150307_1841... OK
Applying exercises.0003_auto_20160921_2000... OK
Applying exercises.0004_auto_20170404_0114... OK
outbound TCP connections the wger process holds right now: 1 020012AC:CDBA
1 060012AC:CC06
3.9 browser form, https Origin + CSRF, RIGHT password: ok
3.9 browser form, WRONG password: POST /en/user/login -> 200 (refused)
3.9/0.7 phone-app route /allauth/app/v1/auth/login, RIGHT password: http=200 keys=['access_token', 'is_authenticated', 'refresh_token', 'session_token']
the API with that token: http=200
3.9/0.7 phone-app route /allauth/app/v1/auth/login, WRONG password: http=400 keys=['errors', 'status']
1.8 unknown page through traefik: http=404 debug-page=False traceback=False
wger: POST /api/v2/weightentry/ http=201
seed: {'weight': '84.42'}
2.8 POST /api/v2/gallery/ (multipart, through traefik): http=201 {"id":1,"date":"2026-10-01","image":"…","description":"felhom pilot","heig
2.8 GET the uploaded image back through traefik (/media/gallery/1/def0a0d8-3cc7-4384-96df-ddfcdfaefb8d.png): http=404 bytes=2830 same=False
2.8 the same image with NO session: http=404
wger: readback of the seeded weight entry http=200 found=True
verify (fixture readback, with its negative controls): True
2.7 the volumes' size after the seed: wger_wger_data=4.2M wger_wger_media=16K
4.4 negative control: docker pause wger at 15:09:17 — controller state before: running
states seen while paused (s, (state, health)): [(4, ('stopped', None))]
after unpause, state running again after 40 s
5.1 wger cgroup from birth: 90 samples over 183 s; limit 402653184
peak anon 266.8 MiB (69.5 % of the limit); peak swap 0 bytes; oom_kill max 0; last: 0 running healthy
2.6 stop -> 200
2.6 remove, KEEP drive data (wger has no drive data) -> 200 {'ok': True, 'data': {'removed': 'wger', 'volumes_removed': ['wger_wger_data', 'wger_wger_media'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'backup_paths_removed': ['/mnt/sys_drive/felhom-d
left after: stack dir / containers / volumes: /opt/docker/stacks/wger
the image (kept per decision 53 until the sweep):
@@ -0,0 +1,60 @@
##### wger second walk — controller gitea.dooplex.hu/admin/felhom-controller:0.285.0 catalog e9f50b5 calibre-web: the admin login name is generated at install (09 decision 61, R-752)
15:11:29 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['ADMIN_PASSWORD']
15:11:29 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
15:13:00 [1] deployed, controller state=running, pinned={'wger': 'wger/server:2.7'}
deploy -> True
household admin signs in (the admin exists, the setup is done): ok
== 3.4 a STRANGER, after the setup — no dashboard session, no gate cookie, through traefik
wger source: where registration and guests are decided:
/home/wger/src/wger/utils/context_processor.py:36: 'allow_registration': settings.WGER_SETTINGS.get('ALLOW_REGISTRATION', False),
/home/wger/src/wger/utils/middleware.py:63: settings.WGER_SETTINGS['ALLOW_GUEST_USERS']
/home/wger/src/wger/core/views/misc.py:55: if not settings.WGER_SETTINGS['ALLOW_GUEST_USERS']:
/home/wger/src/wger/core/views/user.py:291: if not settings.WGER_SETTINGS['ALLOW_REGISTRATION']:
/home/wger/src/wger/core/account_adapter.py:36: return settings.WGER_SETTINGS['ALLOW_REGISTRATION']
/home/wger/src/wger/software/views.py:74: context['allow_registration'] = settings.WGER_SETTINGS['ALLOW_REGISTRATION']
/home/wger/src/wger/software/views.py:75: context['allow_guest_users'] = settings.WGER_SETTINGS['ALLOW_GUEST_USERS']
GET /en/user/registration -> 200 form fields: ['csrfmiddlewaretoken', 'email', 'language', 'next', 'password1', 'password2', 'username', 'viewport']
POST /en/user/registration as stranger272284 -> 302; form errors: []
the stranger then signs in with that account -> 302 (302 = IN)
and reads the API as that user -> 200
the app's own user list now (admin's view, count only): 2 ['admin', 'stranger']
-- guests: wger's own 'try as guest' path, as a stranger
/home/wger/src/wger/core/views/user.py:283: closed" page), and temporary (guest) users may still reach the
/home/wger/src/wger/core/views/user.py:787: allauth's login view, with one wger carve-out: temporary (guest) users are
/home/wger/src/wger/utils/context_processor.py:55: # Flag for guest users
/home/wger/src/wger/utils/middleware.py:29:from wger.core.demo import create_temporary_user
/home/wger/src/wger/utils/middleware.py:43: # Don't create guest users for requests that are accessing the site
/home/wger/src/wger/utils/middleware.py:68: logger.debug('creating a new guest user now')
/home/wger/src/wger/utils/middleware.py:69: user = create_temporary_user(request)
GET /en/user/demo-entries as a stranger -> 500
GET /en/dashboard as a stranger -> 200
users after the guest tries: 4 ['2167c0bc', '792dae77', 'admin', 'stranger']
== 2.8 the uploaded photo: on the volume? who serves /media/?
POST /api/v2/gallery/ -> 201 image path: /media/gallery/1/e91d6ece-866d-44c7-9e3f-1a6db6a6107b.png
the file on the media volume: -rw-r--r-- 1 wger wger 70 Oct 1 15:13 /home/wger/media/gallery/1/e91d6ece-866d-44c7-9e3f-1a6db6a6107b.png
GET it through traefik, signed in -> 404
inside the container, straight at runserver (no traefik): HTTP Error 404: Not Found
wger's urls.py on /media and /static: 350: 'api/v2/min-server-version/',
352: name='min_server_version',
393:# URL for user uploaded files, served like this during development only
396: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
whitenoise / static middleware in settings: /home/wger/src/settings/ci.py:117:MEDIA_ROOT = '/tmp/'
/home/wger/src/settings/main.py:142:MEDIA_ROOT = env.str('DJANGO_MEDIA_ROOT', '/home/wger/media')
/home/wger/src/settings/main.py:143:STATIC_ROOT = env.str('DJANGO_STATIC_ROOT', '/home/wger/static')
/home/wger/src/settings/settings_global.py:48:MEDIA_ROOT = BASE_DIR.parent / 'media'
/home/wger/src/settings/settings_global.py:49:STATIC_ROOT = BASE_DIR.parent / 'static'
upstream's own production compose serves /media with nginx — the image's docs: celery
Dockerfile
entrypoint.sh
README.md
== 1.7 static files (collectstatic needs DJANGO_DEBUG == "False"; the template sets none)
static refs on the login page: ['/static/css/workout-manager.css', '/static/bootstrap-compiled.css', '/static/css/bootstrap-custom.css']
GET /static/css/workout-manager.css -> 404 (2830 chars)
GET /static/bootstrap-compiled.css -> 404 (2830 chars)
static root inside the container: 4.0K /home/wger/static
remove WITH data -> 200 {'ok': True, 'data': {'removed': 'wger', 'volumes_removed': ['wger_wger_data', 'wger_wger_media'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját adato
left after: stack dir / containers / volumes: /opt/docker/stacks/wger
@@ -0,0 +1,80 @@
# The new-app checklist — review, gate, wger pilot, gap page (2026-10-01)
Operator request 2026-10-01: a checklist every new app passes before it reaches the live catalog. Reviewer's draft the
same day (the brief's appendix; pushed by the operator as `app-catalog-felhom.eu/NEW-APP-CHECKLIST.md`, `6f18f74`).
Architecture read: `documentation/architecture/09-update-architecture.md` §3 decisions 13, 22, 37, 42, 45–50, 61;
§6.5 (drill catalog). Baselines: catalog `main` `6d72c09` (the draft's merge on top of `9c5eae9`), 53 templates;
felhom.eu `b63654a`; register 392 rows, highest R-757; controller on 9202 `0.285.0`.
| part | done? | what |
|---|---|---|
| A — the checklist | done, changed | 60 rows in 10 groups (draft: 53 in 10). 7 rows added, 16 "hows" sharpened, 9 citations fixed. `app-catalog-felhom.eu/NEW-APP-CHECKLIST.md`; template `onboarding/_TEMPLATE.md`; pointers in CLAUDE.md, REUSE.md §5, README "Adding a New App" |
| B — the gate | done | `scripts/check-onboarding.py`, gate `onboarding` in `catalog_gates.py --fast` (hook + CI); 16 decoy cases, 5 mutants of the gate each turn the suite red (`A/`) |
| C — the wger pilot | done | the four problems: 2 caught by the draft as written, 2 missed by the draft's "how" and caught by sharpened/new rows. The pilot ALSO found three live wger defects (R-762, R-763, R-764) |
| D — the gap page | done | `onboarding/EXISTING-APPS-GAPS.md` from `scripts/onboarding_gaps.py`, read only |
## 1. Claims in the draft that were wrong
| row | the draft said | what is true |
|---|---|---|
| 3.3 | "32 apps" gated | 33 templates carry `setup_gate: true` today (FIRST-ADMIN's 32 was 2026-09-29) |
| 5.2 | "romm OOM at +76 s (decision 22)" | no "+76 s" exists in R-635, decision 22 or any audit; romm's OOM loop is R-635 (six hours after an update called success). Citation replaced |
| 5.3 | "gate / review" | **no gate checks it**, and 8 templates differ today (R-758). immich's figure was right: `mem_limit` 4096M vs a 4224M sum, header naming a 256M DB that ran at 512M |
| 6.2 | "35 of 53 update at night" | not reproducible from files; 38 of 53 carry a ladder today, and a ladder is not "updates at night" (marks can hold a step for a person) |
| 8.3 | (implicit) the asset URL | the canonical template comment says `-logo.webp`; the controller loads `-logo.svg`/`.png` (R-761) |
| 1.6 | how = "compose + a login on 9202" | **cannot show R-737**: the web login worked; only the phone app's route failed. Sharpened (list the env the settings READ; log in on every route) + new row 3.9 |
| 1.4 | how = "an update on 9202" | a new app pinned at its newest tag has no update to make. Sharpened: install the PREVIOUS release, step INTO the pin |
| 0.4 | how = "one packet capture or log read" | no packet-capture tool is in our kit; replaced by the entrypoint read (1.7) + the first-start log + the held connections |
| 2.6 | why = "R-756 (refused with a folder present)" | R-756's cause is not known (possibly a 9202 venue artefact); R-442 (the inert "remove with data") added as the measured reason |
| — duplicates of gates | 1.1, 2.1, 3.3 (probe flip), 4.2, 6.2, 8.1, 9.4 | each now names its gate: `image-pins`/`image-resolvable`, `volume-persistence`, `probe-measured`, `probe-matches-compose`, `test-record`(+`-move`), `copy-i18n`, `onboarding` |
**Rows added:** 0.9 (no self-call at the public name, R-739), 1.7 (every entrypoint switch read and decided), 1.8
(debug off, R-482), 1.9 (`data_key`), 2.8 (an upload opens again through the front door, R-483), 3.9 (sign in as each
client does — browser CSRF + the phone app's route, R-712/R-737), 8.5 (website app count).
**Rows sharpened (how or why):** 0.4, 0.7, 1.4, 1.6, 2.3 (R-537/538), 2.6 (R-442), 3.1 (R-612), 3.2 (R-702), 3.4
(R-512), 3.6 (a second member), 3.8 (R-713), 4.1 (inspect the image), 4.3 (R-473, R-676), 4.4 (R-613), 4.5 (R-630),
5.1 (cgroup, not Docker's OOMKilled — R-528; R-703), 5.2 (R-635, R-514), 6.1 (R-624, R-738's product gap), 6.2/6.3
(R-742), 6.5 (R-743), 8.2 (R-515, R-498), 8.3 (R-761).
## 2. The pilot — would the checklist have caught this week's four wger problems?
The record against the template as it was on 2026-09-29 (`d0e7e2e`): `wger-as-of-2026-09-29.md` — 27 of 60 rows open,
10 of them FAILS. The record as it is now: `app-catalog-felhom.eu/onboarding/wger.md` — 11 open, each a register row.
| problem | the row that catches it | would the DRAFT's "how" have shown it? | after the review |
|---|---|---|---|
| **R-737** JWT key missing — the phone app's login 500 | 1.6, 3.9, 0.7 | **No.** "compose + a login on 9202" — the web login worked; only `/allauth/app/v1/auth/login` failed. 0.7 needed a real phone client | 1.6 lists the env the settings READ (`JWT_PRIVATE_KEY` is among 15 key-like names, C2); 3.9 calls the phone route with `curl` |
| **R-738** no migration on update | 1.4 (+ 6.1) | **Only if an update was run.** On 2026-09-29 one existed (2.7 since 09-03) and the fixture's read-back caught it on 09-30; for a NEW app at its newest tag, the draft's how has nothing to run | 1.4: step from the previous release INTO the pin; 1.7: the entrypoint read names `DJANGO_PERFORM_MIGRATIONS` statically |
| **R-752** a stranger locks everyone out | 3.6 | **Yes** — "N wrong passwords … who is locked" is exactly R-752's measured control | 3.6 says how to tell "everyone": the household's AND a second member's right password, and cites R-753 |
| **R-755** development server | 1.5 (+ 1.7) | **Yes** — `ps` in the container shows `manage.py runserver` | 1.7 also finds it statically (`WGER_USE_GUNICORN`) |
**Three more found by the new and sharpened rows on the current template (C8):** row 2.8 — a photo uploads (201)
and never opens (404); row 1.7 — `DJANGO_DEBUG` unset, so `collectstatic` never runs and every CSS/JS file is 404
(R-762); row 3.4 — a stranger signs up after the setup and each anonymous visit makes a guest account (R-763);
row 7.1 — mail goes to the console (R-764). None of these is in the draft's four; all were on the live template since
it was written.
## 3. The gap page's headline (`onboarding/EXISTING-APPS-GAPS.md`, of 53)
0 Fit 52 · 1 Images/DB 53 · 2 Storage 38 · 3 Accounts 39 · 4 Health 49 · 5 Resources 24 · 6 Updates 26 · 7 Mail —
(6 mapped) · 8 Text 52. Read from files only; "covered" means a file shows the signal named on the page, not that it was
re-tested. What NO old app has recorded: the entrypoint switches, the production server, the secrets read (1.4–1.9), a
restore round trip, a negative health control, lock-out (except the R-752 apps), a from-birth memory watch (except
immich).
## 4. The live work, and teardown
9202 only, drill catalog (`app-catalog-drill` `e9f50b5`, wger identical to live). C0 repoint (saved
`controller.yaml.pre-checklist1001`; control: the box's clone = drill `e9f50b5`, live `main` unchanged `6d72c09`);
C1–C7 walk 1 (install, reads, logins, seed + photo, pause, memory, remove keeping data); C8/C8b walk 2 (stranger
sign-up, guests, photo cause, static files, remove with data); C9 restore (clone = live `6d72c09`, standing apps
healthy). **Teardown — machine:** wger removed twice through the product, both volumes gone, image deleted by the
remove (decision 53), sampler/poller files removed from `/root`; the stack directory remains (the sync creates one per
template). **Host:** nothing left (temp scripts removed per call). **Hub:** untouched (9202 is unenrolled).
Secrets: the dashboard password and wger's generated password lived in a 0600 scratch directory, never printed;
evidence scanned for both values and for token shapes — none.
## Files
`A/` decoys (green run; red-proof by mutants) · `B/` upstream reads, the 2026-09-29 template copy, gates then and now ·
`C/` the 9202 walks · `tools/` `c_wger.py`, `c_wger2.py` · `wger-as-of-2026-09-29.md` the first-pass record.
@@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""Part C, the pilot: measure on scratch guest 9202 what wger's onboarding record ("as it is now") has no evidence for.
argv[1] = drill | walk | restore
drill — point 9202 at the drill catalog (`09` §6.5; R-615: the cache dir goes too), save the config first
walk — one fresh wger install from the drill catalog and the reads below; then remove it through the product
restore — put 9202 back on the saved config (the live catalog) and print the controls
Reads (each names the checklist row it answers):
0.4/0.5/1.7/1.8 the image's own env defaults, the container's env NAMES (values never printed except the named
non-secret switches), Django's DEBUG, the settings' env reads
1.5 the server process inside the container
3.5 the default login polled once a second from the install press, through traefik, as a stranger
4.3 press → healthy, RestartCount
5.1 the wger container's cgroup from its birth, every 2 s: anon, swap, oom_kill (swap reported, so a
pass on swap is visible)
3.9/0.7 the browser form (https Origin + CSRF) and the phone app's route, right and wrong
1.8 an unknown page through traefik: a Django debug page or not
2.8 a progress photo uploaded through the API and fetched back through traefik
2.7 the volumes' size after the seed
4.4 negative control: the container paused → does the controller read it unhealthy?
2.6 remove through the product → what is left
Secrets: the generated admin password lives in this process and a 0600 file in SC only; nothing prints it.
"""
import json, os, re, sys, time, tempfile, secrets, base64, io, subprocess
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
import upgrade_fixtures_box as fxb
EV = os.environ["EV"]
VOL = "/var/lib/docker/volumes/felhom-controller-data/_data"
DRILL_REPO = "https://gitea.dooplex.hu/admin/app-catalog-drill.git"
SAVE = f"{VOL}/controller.yaml.pre-checklist1001"
SUB = "fitness"
OUT = []
def p(*a):
line = " ".join(str(x) for x in a)
print(line, flush=True)
OUT.append(line)
def dump(name):
with io.open(os.path.join(EV, name), "w", encoding="utf-8") as fh:
fh.write("\n".join(OUT) + "\n")
def creds():
for l in io.open(os.path.expanduser("~/.git-credentials")).read().strip().split("\n"):
m = re.match(r'https://(admin):([^@]+)@gitea\.dooplex\.hu', l)
if m:
return m.group(1), m.group(2)
raise SystemExit("no admin credential")
def to_drill():
u, t = creds()
p(w.guest(f"""
set -e
test -f {SAVE} || cp -p {VOL}/controller.yaml {SAVE}
python3 - <<'PY'
import re
p = "{VOL}/controller.yaml"
s = open(p).read()
s = re.sub(r'(^\\s+repo_url: ).*$', r'\\g<1>{DRILL_REPO}', s, count=1, flags=re.M)
s = re.sub(r'(^git:(?:\\n\\s+.*)*?\\n\\s+token: ).*$', r'\\g<1>"{t}"', s, count=1, flags=re.M)
s = re.sub(r'(^git:(?:\\n\\s+.*)*?\\n\\s+username: ).*$', r'\\g<1>"{u}"', s, count=1, flags=re.M)
open(p, "w").write(s)
PY
rm -rf {VOL}/catalog-cache {VOL}/data/catalog-cache
docker restart felhom-controller >/dev/null
sleep 20
echo "saved copy: $(ls -l {SAVE} | awk '{{print $5, $9}}')"
grep -A6 '^git:' {VOL}/controller.yaml | sed 's/token:.*/token: <redacted>/'
"""))
w.login()
w.sync_rescan()
controls("drill")
dump("C0-repoint-drill.txt")
def controls(want):
head = w.guest(f"cd {VOL}/data/catalog-cache 2>/dev/null && git log --oneline -1 && git remote get-url origin | sed 's#//[^@]*@#//#'").strip()
p("CONTROL 1 — the box's catalog clone:", head)
live = subprocess.run(["git", "ls-remote", "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git", "refs/heads/main"],
capture_output=True, text=True).stdout.split()[:1]
p("CONTROL 2 — the LIVE catalog main (unchanged by this act):", live)
p("CONTROL 3 — expected source:", want)
def restore():
p(w.guest(f"""
set -e
cp -p {SAVE} {VOL}/controller.yaml
rm -rf {VOL}/catalog-cache {VOL}/data/catalog-cache
docker restart felhom-controller >/dev/null
sleep 20
grep -A6 '^git:' {VOL}/controller.yaml | sed 's/token:.*/token: <redacted>/'
"""))
w.login()
w.sync_rescan()
controls("live")
p("leftovers named wger:", w.guest("docker ps -a --format '{{.Names}}' | grep -i wger; docker volume ls -q | grep -i wger; ls -d /opt/docker/stacks/wger 2>/dev/null").strip() or "none")
dump("C9-restore-live.txt")
SAMPLER = r"""
cat > /root/wger-sampler.sh <<'SH'
#!/bin/bash
out=/root/wger-mem.csv; : > $out
t0=$(date +%s)
id=""
while [ $(( $(date +%s) - t0 )) -lt 1200 ]; do
if [ -z "$id" ]; then id=$(docker ps -aq --no-trunc --filter name=^wger$ | head -1); fi
if [ -n "$id" ]; then
cg=$(ls -d /sys/fs/cgroup/system.slice/docker-$id.scope 2>/dev/null || find /sys/fs/cgroup -maxdepth 4 -type d -name "*$id*" 2>/dev/null | head -1)
if [ -n "$cg" ] && [ -f "$cg/memory.stat" ]; then
anon=$(awk '$1=="anon"{print $2}' $cg/memory.stat)
cur=$(cat $cg/memory.current); sw=$(cat $cg/memory.swap.current 2>/dev/null || echo NA)
oom=$(awk '$1=="oom_kill"{print $2}' $cg/memory.events); lim=$(cat $cg/memory.max)
rc=$(docker inspect -f '{{.RestartCount}} {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' $id 2>/dev/null)
echo "$(date +%s.%N | cut -c1-14),$anon,$cur,$sw,$oom,$lim,$rc" >> $out
fi
fi
[ -f /root/wger-sampler.stop ] && break
sleep 2
done
SH
chmod +x /root/wger-sampler.sh; rm -f /root/wger-sampler.stop
nohup /root/wger-sampler.sh >/dev/null 2>&1 &
echo sampler started
"""
POLLER = r"""
cat > /root/wger-poll.sh <<'SH'
#!/bin/bash
# a STRANGER: no dashboard session, no gate cookie — the public default login, once a second, through traefik
out=/root/wger-poll.txt; : > $out
t0=$(date +%s)
while [ $(( $(date +%s) - t0 )) -lt 600 ]; do
r=$(curl -sk --max-time 4 -o /root/wger-poll.body -w '%{http_code}' -H 'Host: fitness.enkisfelhom.hu' \
-H 'Content-Type: application/json' --data '{"username":"admin","password":"adminadmin"}' \
https://127.0.0.1/allauth/app/v1/auth/login)
b=$(head -c 90 /root/wger-poll.body | tr '\n' ' ')
echo "$(date +%H:%M:%S) $r $b" >> $out
# the first answer that is the APP's own JSON (not the gate, not traefik's 404) ends the poll: every further
# wrong try would count toward wger's 5-failure lock (decision 58) and spoil the household's login below
case "$b" in *'"status"'*|*access_token*) break;; esac
sleep 1
done
SH
chmod +x /root/wger-poll.sh
nohup /root/wger-poll.sh >/dev/null 2>&1 &
echo poller started
"""
def walk():
w.login()
p("##### wger on 9202 — controller", w.guest("docker inspect felhom-controller --format {{.Config.Image}}").strip())
p("catalog clone:", w.guest(f"cd {VOL}/data/catalog-cache && git log --oneline -1").strip())
p("before: stack/volumes named wger:", w.guest("ls -d /opt/docker/stacks/wger 2>/dev/null; docker volume ls -q | grep -i wger").strip() or "none")
p("guest swap (free -m):", " ".join(w.guest("free -m | grep -i swap").split()))
p(w.guest(SAMPLER).strip())
p(w.guest(POLLER).strip())
t_press = time.time()
p("deploy press at", time.strftime("%H:%M:%S"))
ok = w.deploy("wger", SUB)
p("deploy ->", ok)
opened = None
for _ in range(120):
time.sleep(5)
lg = w.guest("docker logs --since 30m felhom-controller 2>&1 | grep -E 'wger: install hold OPENED|wger.*after_install' | tail -3")
if "hold OPENED" in lg:
opened = lg.strip()
break
p("controller log (after_install / hold):", opened)
healthy = None
for _ in range(90):
h = w.guest("docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}} {{.RestartCount}}' wger").strip()
if h.startswith("healthy"):
healthy = time.time() - t_press
break
time.sleep(2)
p("4.3 press -> docker healthy: %.0f s; RestartCount: %s; start_period in compose: %s" % (
healthy or -1, w.guest("docker inspect -f '{{.RestartCount}}' wger").strip(),
w.guest("grep -m1 start_period /opt/docker/stacks/wger/docker-compose.yml").strip()))
p(" controller state:", w.stack("wger").get("state"))
time.sleep(8)
w.guest("pkill -f wger-poll.sh || true")
poll = w.guest("cat /root/wger-poll.txt").strip().splitlines()
codes = {}
for l in poll:
c = l.split(" ")[1] if len(l.split(" ")) > 1 else "?"
codes[c] = codes.get(c, 0) + 1
p("3.5 stranger polls of admin/adminadmin from the press: %d tries; codes %s" % (len(poll), codes))
p(" first 3:", poll[:3])
p(" last 3:", poll[-3:])
p(" any 200 with a token:", any(" 200 " in l and "access_token" in l for l in poll))
dump("C1-install.txt")
# --- the static reads inside the container
OUT.clear()
p("1.5 the server process:", " | ".join(w.guest("docker exec wger sh -c \"ps -eo args 2>/dev/null || cat /proc/[0-9]*/cmdline | tr '\\\\0' ' '\" | grep -E '[m]anage.py|[g]unicorn|[u]vicorn|[d]aphne' | head -4").strip().splitlines()))
p("1.7 image entrypoint:", w.guest("docker image inspect wger/server:2.7 --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'").strip())
p("1.7 entrypoint switches (if-tests in /home/wger/entrypoint.sh):")
p(w.guest("docker exec wger grep -nE 'if \\[\\[? *\"?\\$' /home/wger/entrypoint.sh").rstrip())
p("0.4/1.7 the IMAGE's env defaults for those switches (+ debug/mail/sync):")
env_img = w.guest("docker image inspect wger/server:2.7 --format '{{range .Config.Env}}{{println .}}{{end}}'")
keep = re.compile(r"^(DJANGO_DEBUG|DJANGO_PERFORM_MIGRATIONS|WGER_USE_GUNICORN|SYNC_\w+|DOWNLOAD_\w+|LOAD_ONLINE\w*|"
r"DJANGO_COLLECTSTATIC\w*|ENABLE_EMAIL|EXERCISE\w*|ALLOW_\w+|USE_CELERY|DJANGO_CLEAR\w*|YARN\w*|TZ)=")
for l in env_img.splitlines():
if keep.match(l):
p(" image:", l)
p(" the RUNNING container's env, the same switches (values: only these named, non-secret ones):")
env_run = w.guest("docker inspect wger --format '{{range .Config.Env}}{{println .}}{{end}}'")
for l in env_run.splitlines():
if keep.match(l) or l.startswith(("AXES_", "CSRF_TRUSTED", "X_FORWARDED", "SITE_URL")):
p(" container:", l)
names = sorted({l.split("=", 1)[0] for l in env_run.splitlines() if "=" in l})
p(" container env NAMES (all):", " ".join(names))
p("1.6 the env names wger's settings READ (settings/main.py, env.*('NAME'...)):")
reads = w.guest("docker exec wger sh -c \"grep -ohE \\\"env\\\\.[a-z]+\\\\('[A-Z_0-9]+'\\\" /home/wger/src/settings/*.py | sed -E \\\"s/.*\\\\('//; s/'$//\\\" | sort -u\"").split()
p(" ", " ".join(reads))
secretish = [r for r in reads if re.search(r"KEY|SECRET|TOKEN|PASSWORD|PEM", r)]
p(" of which key/secret-like:", secretish)
p(" set in the container:", [r for r in secretish if r in names], " NOT set:", [r for r in secretish if r not in names])
dj = w.guest("""cat > /tmp/djs.py <<'PY'
import os, sys
sys.path.insert(0, '/home/wger/src'); os.chdir('/home/wger/src')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.main')
import django; django.setup()
from django.conf import settings as s
print('DEBUG', s.DEBUG)
print('EMAIL_BACKEND', getattr(s, 'EMAIL_BACKEND', None))
print('WGER_SETTINGS sync/download:', {k: v for k, v in getattr(s, 'WGER_SETTINGS', {}).items() if any(x in k for x in ('SYNC', 'DOWNLOAD', 'USE_CELERY', 'ALLOW', 'EMAIL', 'TWITTER', 'MASTODON'))})
print('JWT key loaded:', bool(os.environ.get('JWT_PRIVATE_KEY')), '(env in this shell is not the server env)')
PY
docker cp /tmp/djs.py wger:/tmp/djs.py && docker exec wger python3 /tmp/djs.py 2>&1 | tail -4""")
p("1.8/0.4 Django settings inside the app:\n" + dj.rstrip())
p("0.4/0.5 the container's first-start log lines about the network (sync/download/http):")
p(w.guest("docker logs wger 2>&1 | grep -iE 'sync|download|http|fixture|ingredient|exercise' | head -12").rstrip())
p(" outbound TCP connections the wger process holds right now:", w.guest(
"docker exec wger sh -c 'cat /proc/net/tcp /proc/net/tcp6 2>/dev/null' | awk 'NR>1 && $4==\"01\"{print $3}' | sort | uniq -c | head").strip() or "none")
dump("C2-static-reads.txt")
# --- logins, as each client does
OUT.clear()
pw = (w.GENERATED.get("wger") or {}).get("ADMIN_PASSWORD") or ""
with io.open(os.path.join(os.environ["SC"], "wger.pw"), "w") as fh:
fh.write(pw)
os.chmod(os.path.join(os.environ["SC"], "wger.pw"), 0o600)
W = fxb.Wger()
jar = tempfile.mktemp(prefix="wger-jar-")
hdr, why = W._login(w, SUB, pw, jar)
p("3.9 browser form, https Origin + CSRF, RIGHT password:", why)
jar2 = tempfile.mktemp(prefix="wger-jar2-")
hdr2, why2 = W._login(w, SUB, pw + "x", jar2)
p("3.9 browser form, WRONG password:", why2, "(refused)" if hdr2 is None else "(LET IN!)")
for label, pwd in (("RIGHT", pw), ("WRONG", pw + "y")):
rc, code, out = w.app_curl(SUB, "/allauth/app/v1/auth/login", "-H", "Content-Type: application/json",
data=json.dumps({"username": "admin", "password": pwd}), method="POST")
keys = []
try:
keys = sorted((json.loads(out).get("meta") or {}).keys()) or sorted(json.loads(out).keys())
except Exception:
pass
p(f"3.9/0.7 phone-app route /allauth/app/v1/auth/login, {label} password: http={code} keys={keys}")
if label == "RIGHT" and code == "200":
tok = (json.loads(out).get("meta") or {}).get("access_token") or ""
rc, c2, _ = w.app_curl(SUB, "/api/v2/weightentry/", "-H", f"Authorization: Bearer {tok}")
p(" the API with that token: http=%s" % c2)
# --- 1.8 an unknown page
rc, code, body = w.app_curl(SUB, "/felhom-no-such-page-xyz/")
p("1.8 unknown page through traefik: http=%s debug-page=%s traceback=%s" % (
code, "DEBUG = True" in body, "Traceback" in body))
dump("C3-logins.txt")
# --- seed (the fixture's own front door) + a photo
OUT.clear()
say = lambda *a: p(*a)
t = W.seed(w, SUB, say)
p("seed:", {k: v for k, v in (t or {}).items() if k != "pw"})
png = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")
fn = tempfile.mktemp(suffix=".png")
open(fn, "wb").write(png)
if hdr:
rc, code, out = w.app_curl(SUB, "/api/v2/gallery/", *hdr, "-F", f"image=@{fn};type=image/png",
"-F", "date=2026-10-01", "-F", "description=felhom pilot", method="POST")
p("2.8 POST /api/v2/gallery/ (multipart, through traefik): http=%s %s" % (code, re.sub(r'"image":"[^"]*"', '"image":"…"', out[:160])))
try:
url = json.loads(out).get("image") or ""
except Exception:
url = ""
if url:
path = re.sub(r"^https?://[^/]+", "", url)
rc, code, body = w.app_curl(SUB, path, *hdr)
p("2.8 GET the uploaded image back through traefik (%s): http=%s bytes=%d same=%s" % (
path, code, len(body.encode("latin-1", "ignore")), "PNG" in body[:8]))
rc, code, body = w.app_curl(SUB, path)
p("2.8 the same image with NO session: http=%s" % code)
p("verify (fixture readback, with its negative controls):", W.verify(w, SUB, t, say) if t else "no seed")
p("2.7 the volumes' size after the seed:", " ".join(w.guest(
"for v in $(docker volume ls -q | grep -i wger); do du -sh $(docker volume inspect -f '{{.Mountpoint}}' $v) | awk -v v=$v '{print v\"=\"$1}'; done").split()))
dump("C4-seed-photo-size.txt")
# --- 4.4 negative control
OUT.clear()
p("4.4 negative control: docker pause wger at", time.strftime("%H:%M:%S"), "— controller state before:", w.stack("wger").get("state"))
w.guest("docker pause wger")
seen = []
for i in range(45):
time.sleep(4)
st = w.stack("wger")
s = (st.get("state"), (st.get("health") or {}).get("status") if isinstance(st.get("health"), dict) else st.get("health"))
if not seen or seen[-1][1] != s:
seen.append((round(4 * (i + 1)), s))
if s[0] in ("unhealthy", "degraded", "stopped", "error"):
break
p(" states seen while paused (s, (state, health)):", seen)
w.guest("docker unpause wger")
back = None
for i in range(45):
time.sleep(4)
if w.stack("wger").get("state") == "running":
back = 4 * (i + 1)
break
p(" after unpause, state running again after %s s" % back)
dump("C5-negative-control.txt")
# --- 5.1 the sampler
OUT.clear()
w.guest("touch /root/wger-sampler.stop")
time.sleep(3)
csv = w.guest("cat /root/wger-mem.csv").strip().splitlines()
rows = [l.split(",") for l in csv if l.count(",") >= 6]
if rows:
anon = [int(r[1]) for r in rows if r[1].isdigit()]
sw = [int(r[3]) for r in rows if r[3].isdigit()]
oom = [int(r[4]) for r in rows if r[4].isdigit()]
lim = rows[0][5]
p("5.1 wger cgroup from birth: %d samples over %.0f s; limit %s" % (len(rows), float(rows[-1][0]) - float(rows[0][0]), lim))
p(" peak anon %.1f MiB (%.1f %% of the limit); peak swap %s bytes; oom_kill max %s; last: %s" % (
max(anon) / 1048576, 100.0 * max(anon) / int(lim) if lim.isdigit() else -1, max(sw) if sw else "NA", max(oom) if oom else "NA", rows[-1][6]))
with io.open(os.path.join(EV, "C6-wger-mem.csv"), "w") as fh:
fh.write("epoch,anon,current,swap,oom_kill,limit,restarts status health\n" + "\n".join(csv) + "\n")
dump("C6-first-start-memory.txt")
# --- 2.6 remove through the product
OUT.clear()
c1, d1 = w.ctl("POST", "/api/stacks/wger/stop")
p("2.6 stop ->", c1)
for _ in range(24):
time.sleep(5)
if w.stack("wger").get("state") != "running":
break
code, d = w.ctl("POST", "/api/stacks/wger/remove", {"remove_hdd_data": False, "remove_backups": True})
p("2.6 remove, KEEP drive data (wger has no drive data) ->", code, str(d)[:200])
time.sleep(6)
p(" left after: stack dir / containers / volumes:", w.guest(
"ls -d /opt/docker/stacks/wger 2>/dev/null; docker ps -a --format '{{.Names}}' | grep -x wger; docker volume ls -q | grep -i wger").strip() or "nothing")
w.guest("rm -f /root/wger-sampler.sh /root/wger-poll.sh /root/wger-poll.body /root/wger-sampler.stop /tmp/djs.py; docker exec felhom-controller true")
p(" the image (kept per decision 53 until the sweep):", w.guest("docker images --format '{{.Repository}}:{{.Tag}}' | grep -i wger").strip())
dump("C7-remove.txt")
for j in (jar, jar2, fn):
if os.path.exists(j):
os.unlink(j)
if __name__ == "__main__":
{"drill": to_drill, "walk": walk, "restore": restore}[sys.argv[1]]()
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Part C, second short walk on 9202 (drill catalog): the three things the first walk's reads raised.
3.4 wger's settings read ALLOW_REGISTRATION=True and ALLOW_GUEST_USERS=True (C2): can a STRANGER make an account
after the household's admin exists — by sign-up, and by "guest"? Through traefik, no dashboard session.
2.8 a photo uploaded through the API answered 201 and then 404 (C4): is the file on the volume, and who would
serve /media/ — the cause, read inside the container.
1.7 the entrypoint runs collectstatic only when DJANGO_DEBUG == "False", and the template sets no DJANGO_DEBUG:
does a static file answer?
Then remove through the product. Secrets never printed.
"""
import io, json, os, re, sys, tempfile, time, secrets
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
import upgrade_fixtures_box as fxb
EV = os.environ["EV"]
SUB = "fitness"
OUT = []
def p(*a):
line = " ".join(str(x) for x in a)
print(line, flush=True)
OUT.append(line)
def dump(name):
with io.open(os.path.join(EV, name), "w", encoding="utf-8") as fh:
fh.write("\n".join(OUT) + "\n")
w.login()
p("##### wger second walk — controller", w.guest("docker inspect felhom-controller --format {{.Config.Image}}").strip(),
"catalog", w.guest("cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache && git log --oneline -1").strip())
p("deploy ->", w.deploy("wger", SUB))
for _ in range(120):
time.sleep(5)
if "hold OPENED" in w.guest("docker logs --since 20m felhom-controller 2>&1 | grep 'wger: install hold OPENED'"):
break
w.wait_app(SUB, "/en/user/login", want=("200",), tries=72)
pw = (w.GENERATED.get("wger") or {}).get("ADMIN_PASSWORD") or ""
W = fxb.Wger()
jar = tempfile.mktemp(prefix="wger-jar-")
hdr, why = W._login(w, SUB, pw, jar)
p("household admin signs in (the admin exists, the setup is done):", why)
# ---- 3.4 a stranger signs up
p("\n== 3.4 a STRANGER, after the setup — no dashboard session, no gate cookie, through traefik")
p("wger source: where registration and guests are decided:")
p(w.guest("docker exec wger sh -c \"grep -rn -E 'ALLOW_REGISTRATION|ALLOW_GUEST_USERS' /home/wger/src/wger --include=*.py | grep -v -E 'tests?/' | head -12\"").rstrip())
sj = tempfile.mktemp(prefix="wger-stranger-")
o = f"https://{SUB}.{w.DOMAIN}"
sh_ = ["-H", f"Origin: {o}", "-H", f"Referer: {o}/en/user/registration", "-c", sj, "-b", sj]
rc, code, page = w.app_curl(SUB, "/en/user/registration", *sh_)
fields = sorted(set(re.findall(r'name="([a-z_0-9]+)"', page or "")))
p("GET /en/user/registration ->", code, "form fields:", fields)
m = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', page or "")
uname = "stranger" + secrets.token_hex(3)
spw = "Str-" + secrets.token_hex(8)
if m:
data = ["--data-urlencode", f"csrfmiddlewaretoken={m.group(1)}", "--data-urlencode", f"username={uname}",
"--data-urlencode", f"email={uname}@example.invalid", "--data-urlencode", f"password1={spw}",
"--data-urlencode", f"password2={spw}"]
rc, code, body = w.app_curl(SUB, "/en/user/registration", *sh_, *data, method="POST")
errs = re.findall(r'class="[^"]*(?:invalid-feedback|errorlist|alert-danger)[^"]*"[^>]*>\s*([^<]{3,120})', body or "")
p(f"POST /en/user/registration as {uname} -> {code}; form errors: {errs[:3]}")
sj2 = tempfile.mktemp(prefix="wger-stranger2-")
sh2 = ["-H", f"Origin: {o}", "-H", f"Referer: {o}/en/user/login", "-c", sj2, "-b", sj2]
rc, code, pg = w.app_curl(SUB, "/en/user/login", *sh2)
m2 = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', pg or "")
rc, code, _ = w.app_curl(SUB, "/en/user/login", *sh2, "--data-urlencode", f"csrfmiddlewaretoken={m2.group(1) if m2 else ''}",
"--data-urlencode", f"login={uname}", "--data-urlencode", f"password={spw}", method="POST")
p(f"the stranger then signs in with that account -> {code} ({'302 = IN' if code == '302' else 'refused'})")
rc, code, out = w.app_curl(SUB, "/api/v2/weightentry/", *sh2)
p(" and reads the API as that user ->", code)
for f in (sj2,):
os.path.exists(f) and os.unlink(f)
p("the app's own user list now (admin's view, count only):", w.guest(
"docker exec wger sh -c \"cd /home/wger/src && python3 manage.py shell -c 'from django.contrib.auth.models import User; print(User.objects.count(), sorted(u.username[:8] for u in User.objects.all()))'\" 2>/dev/null | tail -1").strip())
p("\n-- guests: wger's own 'try as guest' path, as a stranger")
p(w.guest("docker exec wger sh -c \"grep -rn -E 'create_temporary_user|def demo_entries|guest' /home/wger/src/wger/core/urls.py /home/wger/src/wger/core/views/user.py /home/wger/src/wger/utils/*.py 2>/dev/null | head -12\"").rstrip())
gj = tempfile.mktemp(prefix="wger-guest-")
gh = ["-c", gj, "-b", gj]
for path in ("/en/user/demo-entries", "/en/dashboard"):
rc, code, body = w.app_curl(SUB, path, *gh)
p(f"GET {path} as a stranger -> {code}")
p("users after the guest tries:", w.guest(
"docker exec wger sh -c \"cd /home/wger/src && python3 manage.py shell -c 'from django.contrib.auth.models import User; print(User.objects.count(), sorted(u.username[:8] for u in User.objects.all()))'\" 2>/dev/null | tail -1").strip())
os.path.exists(gj) and os.unlink(gj)
os.path.exists(sj) and os.unlink(sj)
# ---- 2.8 the photo
p("\n== 2.8 the uploaded photo: on the volume? who serves /media/?")
import base64
png = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")
fn = tempfile.mktemp(suffix=".png")
open(fn, "wb").write(png)
rc, code, out = w.app_curl(SUB, "/api/v2/gallery/", *hdr, "-F", f"image=@{fn};type=image/png", "-F", "date=2026-10-01",
"-F", "description=felhom pilot 2", method="POST")
url = ""
try:
url = json.loads(out).get("image") or ""
except Exception:
pass
path = re.sub(r"^https?://[^/]+", "", url)
p("POST /api/v2/gallery/ ->", code, "image path:", path)
p("the file on the media volume:", w.guest(f"docker exec wger ls -la /home/wger/media{path.replace('/media', '', 1)} 2>&1").strip())
rc, code, _ = w.app_curl(SUB, path, *hdr)
p("GET it through traefik, signed in ->", code)
p("inside the container, straight at runserver (no traefik):", w.guest(f"docker exec wger sh -c \"python3 -c \\\"import urllib.request as u;\ntry:\n print(u.urlopen('http://127.0.0.1:8000{path}').status)\nexcept Exception as e: print(e)\\\"\"").strip())
p("wger's urls.py on /media and /static:", w.guest("docker exec wger sh -c \"grep -n -E 'MEDIA|static\\(|serve' /home/wger/src/wger/urls.py | head -8\"").rstrip())
p("whitenoise / static middleware in settings:", w.guest("docker exec wger sh -c \"grep -n -i -E 'whitenoise|STATIC_ROOT|MEDIA_ROOT' /home/wger/src/settings/*.py /home/wger/src/wger/settings_global.py 2>/dev/null | head -8\"").rstrip())
p("upstream's own production compose serves /media with nginx — the image's docs:", w.guest("docker exec wger sh -c \"ls /home/wger/src/extras/docker/production 2>/dev/null; grep -rn -l 'location /media' /home/wger/src/extras 2>/dev/null | head -3\"").strip() or "(no extras/docker/production in the image)")
os.unlink(fn)
# ---- 1.7 a static file
p("\n== 1.7 static files (collectstatic needs DJANGO_DEBUG == \"False\"; the template sets none)")
rc, code, body = w.app_curl(SUB, "/en/user/login")
css = re.findall(r'(?:href|src)="(/static/[^"]+\.(?:css|js))"', body or "")
p("static refs on the login page:", css[:3])
for c in css[:2]:
rc, code, b = w.app_curl(SUB, c)
p(f"GET {c} -> {code} ({len(b)} chars)")
p("static root inside the container:", w.guest("docker exec wger sh -c 'ls /home/wger/static 2>&1 | head -5; du -sh /home/wger/static 2>/dev/null'").strip())
dump("C8-signup-guest-media-static.txt")
# ---- remove
OUT.clear()
c1, _ = w.ctl("POST", "/api/stacks/wger/stop")
for _ in range(24):
time.sleep(5)
if w.stack("wger").get("state") != "running":
break
code, d = w.ctl("POST", "/api/stacks/wger/remove", {"remove_hdd_data": True, "remove_backups": True})
p("remove WITH data ->", code, str(d)[:200])
time.sleep(6)
p("left after: stack dir / containers / volumes:", w.guest(
"ls -d /opt/docker/stacks/wger 2>/dev/null; docker ps -a --format '{{.Names}}' | grep -x wger; docker volume ls -q | grep -i wger").strip() or "nothing")
dump("C8b-remove-with-data.txt")
os.path.exists(jar) and os.unlink(jar)
@@ -0,0 +1,74 @@
# Onboarding record — wger, AS IT WAS ON 2026-09-29 (the pilot's first pass)
app: wger
opened: 2026-10-01
template_at: d0e7e2e (2026-09-29 09:19 — the last wger commit before the four fixes; copy in B/template-d0e7e2e/)
<!--
The pilot (TASK Part C): fill the record against the template as it stood BEFORE this week's four wger fixes, using
evidence already in the audits (much of it measured on 2026-09-30/10-01 against this same template) and asking:
would the checklist have caught R-737, R-738, R-752, R-755? A row that FAILS on the old template is written `open`
with "FAILS:" — that is the row catching the defect. A row with no evidence for this template version is `open`
with "not measured". Paths are from the workspace root. Same format as app-catalog-felhom.eu/onboarding/_TEMPLATE.md.
-->
0.1 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt — AGPL-3.0, image pulled from Docker Hub
0.2 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt — 2.7 on 2026-09-03, 2.6 on 2026-06-17; repo pushed 2026-10-01
0.3 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt — plain `x.y` tags, amd64 + arm64
0.4 | open | not measured for this template on 2026-09-29: the entrypoint's start-time network jobs (exercise sync, image downloads) were never listed — see the now-record C2
0.5 | open | not measured for this template on 2026-09-29 (same read as 0.4)
0.6 | n/a | wger serves only HTTP on port 8000; nothing else is published
0.7 | open | FAILS: the phone app's login route `/allauth/app/v1/auth/login` answered 500 on the right password (R-737) — felhom.eu/documentation/audits/more-night-apps-2026-09-30/B/wger-probe.txt
0.8 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/template-d0e7e2e/.felhom.yml — tagline + five use_cases
0.9 | done | felhom.eu/documentation/audits/more-night-apps-2026-09-30/bench/apps/wger/bench/evidence/MV-wger/verdict.json — the bench ran it; SITE_URL builds links only
1.1 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B2-gates-at-d0e7e2e.txt — image-pins exit 0 at d0e7e2e
1.2 | n/a | wger keeps SQLite on its own volume, no database sidecar
1.3 | n/a | no MariaDB or PostgreSQL service in this template
1.4 | open | FAILS: no `DJANGO_PERFORM_MIGRATIONS`; 2.6 -> 2.7 ended `done`, login 500, 12 migrations unapplied (R-738) — felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/wger/step-attempt1-FAILED-R738.txt
1.5 | open | FAILS: the template sets no `WGER_USE_GUNICORN`, so the entrypoint runs `manage.py runserver` (R-755) — felhom.eu/documentation/audits/lockouts-2026-10-01/B/B4-wger-fix-drill.txt
1.6 | open | FAILS: settings read `JWT_PRIVATE_KEY` (settings/main.py:109), the template sets none (R-737) — felhom.eu/documentation/audits/more-night-apps-2026-09-30/B/wger-probe.txt
1.7 | open | FAILS: the entrypoint's switches `DJANGO_PERFORM_MIGRATIONS`, `WGER_USE_GUNICORN` and `DJANGO_DEBUG` were never read or decided — the last one means `collectstatic` never runs and no CSS/JS is served (R-762, measured 2026-10-01 on the current template; this line is unchanged since d0e7e2e) — felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/wger/entrypoint-read.txt
1.8 | open | not measured for this template on 2026-09-29 (the now-record C2 reads DEBUG on the same image line)
1.9 | n/a | SECRET_KEY signs sessions and reset links only; losing it means signing in again, no data is lost
2.1 | done | app-catalog-felhom.eu/audits/persistence-sweep-2026-08-02/state/gate.log — wger CLEAN (2026-08-02, same volumes)
2.2 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/template-d0e7e2e/docker-compose.yml — two named volumes (NVMe), no drive path
2.3 | n/a | needs_hdd false: no drive path to classify; the tier-1 unit holds both named volumes
2.4 | done | felhom.eu/documentation/audits/more-night-apps-2026-09-30/bench/apps/wger/bench/evidence/MV-wger/verdict.json — started and wrote its database on the bench
2.5 | open | not measured: no backup -> remove -> restore -> read back of wger exists in any audit
2.6 | open | not measured for wger
2.7 | open | not measured for wger
2.8 | open | FAILS: an uploaded photo is saved but answers 404 — nothing serves /media/ (R-762, measured 2026-10-01 on the current template; unchanged since d0e7e2e) — felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt
3.1 | done | felhom.eu/documentation/audits/login-gate-2026-09-29/D/D2-live.txt — class 3 measured on 9202 (FIRST-ADMIN row)
3.2 | done | felhom.eu/documentation/audits/login-gate-2026-09-29/D/D2-live.txt — default refused, generated signs in, wrong refused
3.3 | n/a | class 3: a known default login, not an open first-run screen
3.4 | open | FAILS: a stranger signs up after the setup, and each anonymous dashboard visit makes a guest account (R-763, measured 2026-10-01 on the current template; `ALLOW_REGISTRATION`/`ALLOW_GUEST_USERS` unset since d0e7e2e) — felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt
3.5 | open | not measured for wger (R-741 was measured on calibre-web and mealie)
3.6 | open | FAILS: 10 wrong tries by a stranger locked every member out for 30 min — django-axes keyed on the address, and behind the tunnel everyone has one (R-752, R-753) — felhom.eu/documentation/audits/lockouts-2026-10-01/B/B2-wger-control-live-template.txt
3.7 | open | not measured: no `add_people` text in this template
3.8 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/template-d0e7e2e/.felhom.yml — the password is `sys.argv[1]`
3.9 | open | FAILS (half): the browser form works with the https Origin (R-712 fixed in this commit), the phone app's route answers 500 (R-737) — felhom.eu/documentation/audits/more-night-apps-2026-09-30/B/wger-probe.txt
4.1 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/template-d0e7e2e/docker-compose.yml — wget to 127.0.0.1:8000; the image has wget (healthy on the bench)
4.2 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B2-gates-at-d0e7e2e.txt — probe-matches-compose exit 0 at d0e7e2e
4.3 | done | felhom.eu/documentation/audits/more-night-apps-2026-09-30/bench/apps/wger/bench/evidence/MV-wger/run.log — healthy at FROM on the bench
4.4 | open | not measured for wger
4.5 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/template-d0e7e2e/docker-compose.yml — `container_name: wger`, the only service
5.1 | open | not measured: no first-start-from-birth watch of wger exists (the bench watch is after the update)
5.2 | done | felhom.eu/documentation/audits/more-night-apps-2026-09-30/bench/apps/wger/bench/evidence/MV-wger/memory-samples.json — 10 min, peak anon 49.8 % of 384M, 0 kills (at TO, 2.7)
5.3 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/template-d0e7e2e/.felhom.yml — mem_limit 384M = the one service's 384M
5.4 | n/a | wger is a Python (Django) app, no self-sizing heap
5.5 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt — pi_compatible true, arm64 published, ~373 MB
6.1 | open | not done on 2026-09-29: no wger fixture existed; the one written on 2026-09-30 is what caught R-738
6.2 | open | not done on 2026-09-29: no ladder entry
6.3 | open | not measured for wger
6.4 | open | not measured on 2026-09-29 (the 2026-09-30 step marked nothing)
6.5 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt — plain `x.y` plus `x.y.0`; the shape held 2.1 -> 2.7
7.1 | open | FAILS: wger has a mail switch and no `smtp_mapping`; its mail goes to the console (R-764, read 2026-10-01; unchanged since d0e7e2e) — felhom.eu/documentation/audits/new-app-checklist-2026-10-01/C/C2-static-reads.txt
8.1 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B2-gates-at-d0e7e2e.txt — copy-i18n exit 0 at d0e7e2e (wger 20/20)
8.2 | open | not read on 9202's page for this version
8.3 | done | felhom.eu/documentation/audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt — wger-logo.png and screenshot-1 answer 200
8.4 | done | app-catalog-felhom.eu/README.md — row `fitness.*`; FIRST-ADMIN row; category home; catalog_since 2026-07-19
8.5 | n/a | wger is an existing app; the count does not change
9.1 | open | only the static gates were re-run at d0e7e2e (B2); the runtime gates were not
9.2 | done | felhom.eu/documentation/audits/login-gate-2026-09-29/D/D2-live.txt — fresh install on 9202 as household and stranger (2026-09-29)
9.3 | open | 26 other rows are open, 10 of them FAILS
9.4 | n/a | wger is exempt: published 2026-02-15, before the checklist
+8 -1
View File
@@ -866,9 +866,16 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
| **R-752** | **[P3-LOW] Four more catalog apps let a stranger lock the household out with wrong passwords for a known login name — like mealie (R-747).** READ 2026-10-01 in each app's source at its pinned tag (not measured live): **calibre-web-automated v4.0.8** — Flask-Limiter on the login keyed on the lowercased USERNAME, 3/minute and 40/day, checked before the password; the default login is `admin` → up to a day; no env switch (a database setting). **wger 2.7** — django-axes keyed on IP, 10 failures, 30 min, each failure restarts it; behind traefik every client has traefik's IP → everyone is locked out (`AXES_*` env vars exist; `AXES_IPWARE_PROXY_COUNT` 0). **Grafana 13.2.3** — per-account, 5 failures in a sliding 5 minutes; a slow trickle keeps it closed (`GF_SECURITY_*`). **BookStack 26.09.1** — key `email|ip`, 5 tries, 60 s, hard-coded; `APP_PROXIES` empty, so the key is the e-mail alone. gokapi (3 s delay, no lock) and claper (per-IP 10/min, no account lock) cannot. **Needs:** per app, the smallest fix that keeps a guessing guard (calibre-web-automated and wger first — longest and broadest), each proven on 9202 as R-747's was. **-- 2026-10-01 (afternoon):** **Measured on 9202, each through traefik as a stranger with the public name** (`audits/lockouts-2026-10-01/B/`): **wger** — control: 10 wrong on `admin` locked the second member too; FIXED (decision 58, catalog `82fff32`): username, 5 min, database handler — the second member unaffected, admin in again at 7.5 min (each try during a lock restarts it — measured: 8-minute retries kept a 15-minute lock closed 40+ min). **BookStack** — 1.0 min, kept (decision 59). **Grafana** — 5.0 min, kept (decision 60); a trickle did not hold the household out once the burst aged. **calibre-web-automated** — the form locks 3/min (1.2 min measured) and **40/day per name: after 40 wrong tries in 14 min the right password was refused 2 min later still; only an app restart cleared it** (in-memory store); OPDS has its own 3/min per name (`cps/main.py:75`), no daily limit. No knob for the daily length; both fixes have a household cost — operator decision in STATUS. Installed apps: a settings-only change reaches the stack file at the next sync (images equal, ≤15 min) and the running app at the next `compose up -d` — Restart/Start (measured: the env changed only at Restart), an Update, or a backup's restart (`backup.go:972`, read). **-- 2026-10-01 (late afternoon):** calibre-web done by operator ruling `09` §3 decision 61 (catalog `e9f50b5`): a generated `ADMIN_USER` (`secret`, `hex:5`); `after_install` renames `admin` to it and proves it. 9202: 40 wrong tries on `admin`, the household in at once with its own name (form and OPDS). demo-hp renamed by hand; its name is in the operator's credentials file. All four apps answered (wger 58, BookStack 59, Grafana 60, calibre-web 61). An installed calibre-web is given a made-up name by the box — R-757. | **CLOSED 2026-10-01 — decisions 58–61** |
| **R-753** | **[P3-LOW] Behind the tunnel every visitor reaches an app with the SAME address — the tunnel container's — so every per-address guard is an "everyone" guard and every app's log is blind.** MEASURED 2026-10-01 (`audits/lockouts-2026-10-01/A/A1-client-address.txt`): on demo-hp through its real tunnel, a request from DooPlex's public address reached traefik as `172.18.0.5` (cloudflared, in the guest on `traefik-public`) and BookStack as `172.18.0.3` (traefik); on 9202 an echo container showed `X-Forwarded-For`/`X-Real-Ip` = the sending container for the tunnel's hop (traefik DROPS the incoming chain — good: a client cannot forge it) and the real address from the LAN; `CF-Connecting-IP` passes untouched and is FORGEABLE from the LAN. **No box-wide fix taken:** trusting cloudflared in traefik passes Cloudflare's appended chain, whose LEFTMOST entry the client writes — every app reading the leftmost address would believe it; cloudflared's address is docker-assigned; a single-address rewrite needs a traefik plugin (a new dependency). Per-app fixes trust no header (R-752). **Needs (operator):** whether to build a safe version (cloudflared on a fixed-address network + traefik trusting only it + per-app proxy counts), or keep "one address" and fix per app. Only ONE outside address was available (DooPlex has no IPv6); a second was not measured. | **OPEN — rank P3-LOW; owner: operator (direction), CC measures** |
| **R-754** | **[P3-LOW] `01-topology-and-trust.md` §7 says cloudflared runs on the Proxmox HOST as an agent-managed service; on every box it runs INSIDE the guest as a container the controller renders.** READ 2026-10-01: `felhom-controller` `internal/infra/templates/cloudflared-compose.yml.tmpl` (`container_name: cloudflared`, network `traefik-public`); demo-hp's guest 9201 runs `cloudflared` (ingress `*.enkisfelhom.hu -> https://traefik`); R-505 saw the same in VM 331. A design decision that the build does not follow — the document or the build is wrong, and only the operator decides which (R-370: a design decision is not a defect). | **OPEN — rank P3-LOW; owner: operator (which is right)** |
| **R-755** | **[P3-LOW] wger runs Django's DEVELOPMENT server in production: `manage.py runserver`, because the template does not set `WGER_USE_GUNICORN=True`.** MEASURED 2026-10-01 on 9202 (`ps` in the wger container: `python3 manage.py runserver 0.0.0.0:8000`); wger 2.7's `extras/docker/production/entrypoint.sh:81-87` runs gunicorn only with that switch. Django's own documentation says runserver is not for production (one process, not hardened). Not changed this session (a different change from R-752's; needs its own bench + box proof, memory watch included). | **OPEN — rank P3-LOW; owner: CC (catalog)** |
| **R-755** | **[P3-LOW] wger runs Django's DEVELOPMENT server in production: `manage.py runserver`, because the template does not set `WGER_USE_GUNICORN=True`.** MEASURED 2026-10-01 on 9202 (`ps` in the wger container: `python3 manage.py runserver 0.0.0.0:8000`); wger 2.7's `extras/docker/production/entrypoint.sh:81-87` runs gunicorn only with that switch. Django's own documentation says runserver is not for production (one process, not hardened). Not changed this session (a different change from R-752's; needs its own bench + box proof, memory watch included). **-- 2026-10-01 (checklist pilot):** re-measured on 9202 at the live template: still `manage.py runserver` (`audits/new-app-checklist-2026-10-01/C/C2-static-reads.txt`). The same server question now carries R-762 — runserver with DEBUG off serves no `/static` and no `/media`, so the fix for both is one decision (upstream's nginx front, or gunicorn + a static server). | **OPEN — rank P3-LOW; owner: CC (catalog)** |
| **R-756** | **[P3-LOW] On 9202, "remove with drive data" refuses calibre-web with 409 „…/scratch_hdd/userdata/calibre-web tárhely jelenleg nem elérhető", while the controller container lists that folder.** MEASURED twice on 2026-10-01 (`audits/lockouts-2026-10-01/B/B1…`, `audits/calibre-name-and-prune-2026-10-01/A/A1…`): `POST /api/stacks/calibre-web/remove` with `remove_hdd_data` → 409; `docker exec felhom-controller ls -ld /mnt/felhom-drives/scratch_hdd/userdata/calibre-web` → the directory (dated 2026-09-22). The walk then removed the app keeping the data (R-442's fail-closed answer). Either the drive is not a registered drive on this scratch box (a test-venue artefact) or the resolver reads another path than the one it names. Not measured which. | **OPEN — rank P3-LOW; owner: CC** |
| **R-757** | **[P3-LOW] A template that gains a generated `secret` field makes the box INVENT that value for apps already installed — for calibre-web a login name the app never got.** MEASURED 2026-10-01 on demo-hp: 9 minutes after catalog `e9f50b5` (decision 61) synced, the controller logged `InjectMissingFields … injected missing fields: ADMIN_USER` (deploy.go:1337) and the app page's reveal returned a 10-character name that was NOT calibre-web's login (the app had kept its own name; `after_install` runs only after a fresh install). The page did not list the field, but the reveal answers it, and the password field's text now says "the user name above". demo-hp was fixed by renaming the app's user to the box's recorded name (credentials file updated). Any other installed calibre-web gets the same made-up name at its next sync while its login stays `admin` (no other box has one today: N100 none, Tester-2 not registered). **Needs:** InjectMissingFields must not invent a value an app has to have been GIVEN (a field consumed only by `after_install`), or such a field needs an "installed apps: ask" path. `audits/calibre-name-and-prune-2026-10-01/A/A2…, A3…` | **OPEN — rank P3-LOW; owner: CC (controller)** |
| **R-758** | **[P3-LOW] Eight templates declare a `mem_limit` that is not the sum of their compose limits, and no gate checks it.** FOUND 2026-10-01 by `scripts/onboarding_gaps.py` (the new-app checklist's gap page, row 5.3): adventurelog 384M vs 896M, bookstack 512M vs 768M, calcom 768M vs 1792M, claper 384M vs 640M, kimai 384M vs 640M, nextcloud 1024M vs 1664M, outline 768M vs 1152M, zipline 512M vs 768M — every one UNDER the sum, so the deploy screen and the box's capacity figure (`09` decision 22) read less memory than the app may take. REUSE.md §2 says `mem_limit` = the sum. The compose limits are what Docker enforces, so no app is starved by this; the figure the household and the capacity check read is wrong. **Needs:** the eight figures corrected (a template change: needs its own session, no image move), and a static gate (`--fast`) with a decoy, so a new app cannot repeat it. `app-catalog-felhom.eu/onboarding/EXISTING-APPS-GAPS.md` | **READY — rank P3-LOW; owner: CC (catalog)** |
| **R-759** | **[P3-LOW] wger's onboarding record (the checklist pilot) keeps rows open that no other row owns.** 2026-10-01, `app-catalog-felhom.eu/onboarding/wger.md`: **2.5** no backup → remove → restore → read back of wger exists — the box has no per-app backup press outside an Update (R-648) and wger has no newer step to carry one; **3.7** changing the password and adding a family member not measured, and the template has no `add_people` text; **6.3** no forced-fail undo for wger; **8.2** the app page not read on 9202 this session; **9.1** the runtime volume-persistence gate not re-run (last CLEAN 2026-08-02). The other open rows have their own: 1.5 (R-755), 1.7/2.8 (R-762), 3.4 (R-763), 7.1 (R-764). wger is exempt from the onboarding gate (published before the checklist), so nothing blocks; this row is what keeps the record honest. **Needs:** the five measured on 9202 — 2.5 and 6.3 ride wger's next ladder step (the update's backing-up phase is the per-app backup). `audits/new-app-checklist-2026-10-01/` | **READY — rank P3-LOW; owner: CC (catalog)** |
| **R-760** | **[P3-LOW] vikunja's compose has no healthcheck, and nothing says why.** FOUND 2026-10-01 by `scripts/onboarding_gaps.py` (row 4.1): two services in the catalog have no compose `healthcheck:` — `adventurelog-frontend` (deliberate, a comment cites R-655: the image brings its own) and `vikunja` (no comment). Not measured: whether the vikunja image declares its own `HEALTHCHECK`. The controller's probe still runs (`healthcheck.checks` in `.felhom.yml`), so the badge is not blind; Docker's own health state is. **Needs:** read the image's config; either a compose healthcheck of the family the image has (REUSE.md §2), or a comment saying why none. `app-catalog-felhom.eu/onboarding/EXISTING-APPS-GAPS.md` | **READY — rank P3-LOW; owner: CC (catalog)** |
| **R-761** | **[P3-LOW] The canonical example template tells a new app's author the logo is `<slug>-logo.webp`; the controller loads `<slug>-logo.svg`, then `.png`.** READ 2026-10-01 (checklist row 8.3): `templates/paperless-ngx/.felhom.yml` lines 20–24 (the comment block REUSE.md §2 says to copy) name `{assets.base_url}/assets/{slug}-logo.webp`; `felhom-controller` `internal/config/config.go` `AppLogoURL`/`AppLogoPNGURL` ask for `-logo.svg` and `-logo.png`; `https://felhom.eu/assets/wger-logo.webp` answers 404, `wger-logo.png` 200 (negative control `nosuchapp-logo.png` 404). A new app's author following the comment publishes a logo the box never loads. **Needs:** the comment corrected (a comment-only template change, in a session allowed to touch templates). The checklist row 8.3 already names the right files. `audits/new-app-checklist-2026-10-01/B/B1-upstream-reads.txt` | **READY — rank P3-LOW; owner: CC (catalog)** |
| **R-762** | **[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` | **READY — rank P2-MEDIUM; owner: CC (catalog)** |
| **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.** 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` | **READY — rank P2-MEDIUM; owner: CC (catalog)** |
| **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.** 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)** |
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
One row per dated check. The R-number must have a row above. Dates are UTC.