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
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:
+15
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.'
|
||||
+65
@@ -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
|
||||
|
@@ -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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user