Grimmory and MeTube published behind the family gate (controller >= 0.287.0, decisions 63/64), with complete records
gates / gates (push) Successful in 3s

- family_gate / family_gate_except / min_controller format (README, REUSE); gate family-gate + decoys
- templates/grimmory (v3.5.0, exceptions OPDS/Kobo/KOReader/Komga) + onboarding/grimmory.md
- templates/metube (2026.09.29, no exceptions; ladder .28 -> .29) + onboarding/metube.md; fixture MeTube
- volume-persistence gate: routed port read from the label NAME (R-801, red-proofed); APP_EXERCISE (R-788)
- box_walk: family_cookie; no cached "not gated" while an app has no router

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-02 09:36:49 +02:00
parent de0a9bd29f
commit 96829d0d0f
20 changed files with 1078 additions and 20 deletions
+29
View File
@@ -1,3 +1,32 @@
## The family gate in the catalog — Grimmory and MeTube published behind it (2026-10-02)
- **New template fields** (controller ≥ 0.287.0, `09` §3 decisions 63/64): `family_gate: true`, `family_gate_except:`
(LITERAL path prefixes; the box anchors each at a segment boundary) and `min_controller:` (an older box refuses the
install instead of publishing the app open). Format: README §family gate; pattern: REUSE.md.
- **New gate `family-gate`** (`scripts/check-family-gate.py`, in `catalog_gates.py --fast`): refuses a non-literal
exception, an exception list without the gate, `family_gate` without `min_controller` ≥ 0.287.0, and `family_gate` while
the newest baked golden (the felhom.eu sibling) is older than 0.287.0; without the sibling its summary says "rule 3 NOT
CHECKED here" (R-797). Decoys: `test_gate_decoys.py` (11 cases, seen red).
- **`templates/grimmory/`** — e-book library (`ghcr.io/grimmory-tools/grimmory:v3.5.0` + `mariadb:11.4`), the setup gate
for the first admin AND the family gate for good, with exceptions for OPDS, Kobo, KOReader and the Komga API (each keeps
Grimmory's own login; a stranger measured on each). Ladder 3.4.1 → 3.5.0 (proven 2026-10-01, re-walked on 9202 today).
R-775's lock can no longer be aimed from outside. Record: `onboarding/grimmory.md`.
- **`templates/metube/`** — video and audio downloads to the household's drive (`ghcr.io/alexta69/metube:2026.09.29`),
behind the family gate with NO exception (it has no login at all; every path, the websocket included, measured). The
own-use sentence on the page in both languages. Ladder 2026.09.28 → 2026.09.29 (bench + 9202). Record:
`onboarding/metube.md`. `FIT.md`'s "stop" became "build behind the family gate" (R-767 closed).
- **Fixture `MeTube`** in `upgrade_fixtures_box.py` (POST /add a 1 MB public test video; history + GET /download; a 404
control). **`box_walk.py`:** passes the family gate as the household (`family_cookie`), and no longer caches "not
gated" from a moment when the app had no router (it read the gate's 401 as the app's for 8 minutes once).
- **`check-volume-persistence.py`: `APP_EXERCISE`** — an app's own write path for an app whose GET pages write nothing
(R-788). MeTube: `POST /add` of a 1 MB public test video. Before it, the gate answered MeTube UNDETERMINED (honest:
nothing was written); an exercise the app refuses writes nothing and keeps the verdict UNDETERMINED.
- **`check-volume-persistence.py`: the routed port is read from the label NAME** (R-801, `routed_ports()`): `compose config
--format json` gives labels as a mapping, and the gate read only the values — it found no port for any template and
never sent a request. Tests `TestRoutedPorts`, red-proofed. A full re-sweep of all templates is owed (R-801).
- README app table, FIRST-ADMIN rows (grimmory class 4 + family gate; metube class 5), copy freeze admits both
(`--add-app`), English for every string.
## The visitor's address (R-753) — 19 router resets, BookStack per visitor, checklist 3.10; SparkyFitness's record (2026-10-01, night)
- **19 apps that read the LEFTMOST `X-Forwarded-For`** carry a router middleware removing the chain (`<router>-xff`,
+5
View File
@@ -2,6 +2,11 @@
> Created with the REUSE.md rollout (2026-07-03). History: `CHANGELOG.md`; format spec: `README.md`.
- **2026-10-02 — THE FAMILY GATE in the catalog (controller v0.287.0, golden 0.287.0):** `family_gate:` /
`family_gate_except:` / `min_controller:`; gate `family-gate`. Grimmory (exceptions OPDS/Kobo/KOReader/Komga) and MeTube
(none) published with complete records. A family app's exceptions must be literal prefixes and each measured as a
stranger + a look-alike; never an exception on an app with no login. Licences of all 58 templates read
(`felhom.eu/documentation/audits/licences-2026-10-02/TABLE.md`); decisions are operator rows R-784, R-789..R-795.
- **2026-10-01 (night, later) — the visitor's address (R-753, controller v0.286.1):** traefik now keeps the tunnel's
X-Forwarded-For chain; readers take it from the RIGHT. Leftmost readers carry `<router>-xff` (19 apps); a right-walking
reader gets `172.16.0.0/12` (bookstack done; kimai, zipline, vikunja, nextcloud open — R-776). Checklist 3.10. SparkyFitness
+2
View File
@@ -51,6 +51,7 @@ asks the probe first where there is one. Open sign-up is closed by the box after
| gokapi | 1 | `GOKAPI_PASSWORD` before first serve | – | fine | R |
| grafana | 1 | `GF_SECURITY_ADMIN_PASSWORD` — **falls back to `admin` if empty** (R-708) | – | fine while the field is set | R |
| **gramps-web** | 4 | first-run onboarding | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); sign-up closed by the box after the setup: `/api/users/<name>/register/`; its status answers HTTP 405 after the setup — the box reads only 200 answers (measured: the press was then refused, fail closed) → button; self-registration answered 500, blocked anyway | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| **grimmory** | 4 | first visitor creates the admin (`POST /api/v1/setup`) | **setup gate**, probe `GET /api/v1/setup/status` → `data` (`false` → `true`), **and the family gate** for good (decision 64): strangers reach nothing but the e-reader exceptions, which keep Grimmory's own login | **NEW 2026-10-02** — catalog, `onboarding/grimmory.md` | **M 9202**: the household made the admin through both gates; a second setup 403; R-775's lock cannot be aimed from outside (`audits/family-gate-2026-10-02/A/items.txt`) |
| **home-assistant** | 4 | onboarding | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); the app itself refuses a stranger's second first-admin / sign-up call; its status is a list (`/api/onboarding`) → button | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| **homebox** | 4 | open registration | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); sign-up closed by the box after the setup: `/api/v1/users/register` | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| homepage | 5 | static start page | – | fine | R |
@@ -60,6 +61,7 @@ asks the probe first where there is one. Open sign-up is closed by the box after
| kimai | 1 | `ADMIN_PASSWORD` → `ADMINPASS` | – | fine | R |
| **komga** | 4 | first visitor claims | **setup gate**, opened by probe `/api/v1/claim` → `isClaimed`; the app itself refuses a stranger's second first-admin / sign-up call | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| **mealie** | 3 | `changeme@example.com / MyPassword` | (b) its own user repository (`update_password`), password as `sys.argv[1]` | **FIXED** — catalog, 2026-09-29 | **M 9202**: fresh install — default 401, generated 200, wrong 401 (`felhom.eu/documentation/audits/login-gate-2026-09-29/D/`) |
| **metube** | 5 | NO login at all, by design | **the family gate** only (decision 64), no exceptions | **NEW 2026-10-02** — catalog, `onboarding/metube.md` | **M 9202**: a stranger's every path, the websocket included, 401/302 from the gate (`audits/family-gate-2026-10-02/A/items.txt`) |
| **n8n** | 4 | owner setup | **setup gate**, probe `GET /rest/settings` → `data.userManagement.showSetupOnFirstLoad` = false | **GATED** — catalog, 2026-09-29 | **M 9202**: as immich; opened by the probe ~20 s after the owner setup |
| **navidrome** | 4 | first user is admin | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); the app itself refuses a stranger's second first-admin / sign-up call | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| nextcloud | 1 | `NEXTCLOUD_ADMIN_PASSWORD` → auto-install | – | fine | R; **M** demo-hp 2026-09-28 |
+28
View File
@@ -223,6 +223,32 @@ any case and by its id. Test every block with `felhom.eu/documentation/audits/si
answers (`false -> true`, or "before … after …"). Gate: `scripts/check-probe-measured.py`. A probe may index a list
(`setup.0.status`) and may count one non-200 status as done (`done_status: 405`, controller ≥ 0.282.0).
### The family gate (`family_gate`, controller ≥ 0.287.0)
`09` §3 decisions 63/64: an app with **no login of its own** (MeTube), or one whose own login cannot be offered to
strangers (Grimmory's 15-minute lock for everyone, R-775), is published ONLY behind the household's family gate. A
stranger reaches nothing; a family member signs in on the box's own page with their **own** name and password (the
dashboard's Biztonság → **Család** card adds, resets and removes members — the password is shown once). The family
login never opens the dashboard.
```yaml
family_gate: true
family_gate_except: # optional — paths a phone or e-reader app calls; each keeps the APP's own login
- "/api/v1/opds" # a LITERAL path prefix; the box anchors it: ^/api/v1/opds(/|$)
min_controller: "0.287.0" # REQUIRED with family_gate — an older box would publish the app OPEN
```
- **An exception is a literal path prefix** (letters, digits, `. _ ~ - /`) — never a regex, a traefik matcher, `..`,
`//` or `/`. The box anchors it at a path-segment boundary, so `/api/v1/opdsx` stays behind the gate (finding F1 of the
spike, `audits/permanent-gate-2026-10-01/`). **Measure every exception on 9202 as a stranger** (the app's own refusal,
401) and a look-alike (`<prefix>x`, the gate's refusal).
- **No exception for an app with no login of its own** — an exception would be an open door.
- The gate is recorded at install (`family_gate:` in `app.yaml`), so a catalog change never gates or un-gates an
installed app; the exceptions follow the current template.
- Gate: `scripts/check-family-gate.py` (in `catalog_gates.py`) refuses a non-literal exception, an exception list
without the gate, `family_gate` without `min_controller` ≥ 0.287.0, and `family_gate` while the newest baked golden is
older than 0.287.0.
### App-email mapping (`smtp_mapping`)
Apps that can send outbound email (password resets, invites, confirmations) get it through
@@ -301,6 +327,7 @@ block + the matching compose `${VAR}` lines.
| Gokapi | None (file) | 30M / 128M | yes | -- | share.* |
| Grafana | None (file) | 100M / 512M | yes | -- | grafana.* |
| Gramps Web | None (file) | 100M / 384M | yes | -- | family.* |
| Grimmory | MariaDB | 600M / 1408M | yes | `${USERDATA_PATH}/media/grimmory/` | library.* |
| Home Assistant | None (file) | 256M / 1024M | yes | -- | ha.* |
| Homebox | None (SQLite) | 50M / 256M | yes | -- | inventory.* |
| Homepage | None (file) | 50M / 256M | yes | -- | home.* |
@@ -310,6 +337,7 @@ block + the matching compose `${VAR}` lines.
| Kimai | MariaDB | 100M / 384M | yes | -- | time.* |
| Komga | None (file) | 200M / 512M | yes | `${HDD_PATH}/media/comics/` | comics.* |
| Mealie | None (SQLite) | 200M / 1000M | yes | -- | recipes.* |
| MeTube | None (file) | 128M / 768M | yes | `${USERDATA_PATH}/media/metube/` | video.* |
| n8n | None (file) | 150M / 512M | no | -- | auto.* |
| Navidrome | None (file) | 50M / 256M | yes | `${HDD_PATH}/media/music/` | music.* |
| Nextcloud | MariaDB + Redis | 256M / 1024M | no | `${HDD_PATH}/storage/nextcloud/` | cloud.* |
+16 -13
View File
@@ -1,15 +1,18 @@
# REPORT — the visitor's address in the catalog (R-753); SparkyFitness's record — 2026-10-01 (night)
# REPORT — the family gate in the catalog: Grimmory and MeTube published (2026-10-02)
Session report: `felhom.eu/REPORT-visitors-2026-10-01.md`. Baseline `94477cba435a`.
Session report: `felhom.eu/REPORT-family-gate-2026-10-02.md`. Evidence: `felhom.eu/documentation/audits/family-gate-2026-10-02/`.
- **19 router resets** (`04e9516`..`50e4fb4`), one commit per app, each `catalog_gates.py --fast <app>` green. Pushed
before controller v0.286.0/.1 (whose traefik keeps the tunnel's chain). Installed copies on demo-hp (adventurelog,
docmost, opengist, paperless-ngx, romm) and demo-felhom (opengist) restarted through the product after the sync.
Measured: docmost on the real tunnel still throttles a rotating forged address at try 11.
- **bookstack `APP_PROXIES=172.16.0.0/12`** (`ca144ea`) — 3.6 re-measured on 9202, before/after
(`felhom.eu/documentation/audits/visitors-2026-10-01/A/bookstack-3.6.txt`).
- **Checklist 3.10** + REUSE pattern (`9b3b859`).
- **`onboarding/sparkyfitness.md`** — measured: bench 5.1/5.2/1.4/2.1/9.1 (LXC 9401 rebuilt and destroyed), 9202 walk
(gate, sign-up locks, 3.5, 3.6, 3.9, 4.3, 4.4, backup/remove/restore, both removes). Open: 0.1 (licence, R-784), 0.5,
0.7, 1.6, 1.7, 5.4, 8.3 (R-786).
- Not done: settings for kimai, zipline, vikunja, nextcloud, Jellyfin (R-776/R-777) — each needs its own 3.6 re-measure.
- **Format:** `family_gate: true`, `family_gate_except:` (literal prefixes, anchored by the box), `min_controller:`
(README §family gate; REUSE row). **Gate `family-gate`** (`check-family-gate.py`): refuses a non-literal exception, an
exception without the gate, a missing/low `min_controller`, and a family app while the newest baked golden < 0.287.0.
Decoys seen red: `B/family-gate-decoys-red.txt`.
- **Grimmory** published (v3.5.0 + mariadb:11.4): setup gate for the first admin + the family gate for good; exceptions
OPDS v1/v2, Kobo, KOReader, Komga API — a stranger measured on each (the app's own 401), look-alikes stay gated.
Record `onboarding/grimmory.md`, all 61 ids. R-775's lock cannot be aimed from outside any more.
- **MeTube** published (2026.09.29): behind the family gate with no exception; the websocket passes the gate for a
member (101) and is refused for a stranger; downloads to `${USERDATA_PATH}/media/metube`; own-use sentence (hu, en).
Ladder 2026.09.28 → .29 (bench + 9202). Record `onboarding/metube.md`, all 61 ids.
- **Gates on the bench:** `catalog_gates.py grimmory` and `metube` exit 0, every gate OK, family-gate against golden 0.287.0.
- **Found and fixed here:** the volume-persistence gate never sent a request to any app (port read from label values;
R-801, `routed_ports()`, red-proofed) — a full re-sweep is owed; `APP_EXERCISE` gives MeTube its own write path
(R-788). `box_walk.py` no longer caches "not gated" from a moment without a router.
+1
View File
@@ -25,6 +25,7 @@ Templates are config; the few script helpers other scripts must REUSE, never re-
| **Open first-run screen → `setup_gate:`** (decision 46, controller ≥ 0.280.0) | `templates/n8n/.felhom.yml` (probe), `templates/uptime-kuma/.felhom.yml` (no probe → the household's button); `FIRST-ADMIN.md` | `setup_gate: true` + optional `setup_done_probe: {url: http://<container_name>:<port>/<path>, field: <dotted.json.path>, done: "<text>"}` | TRAPS: the probe must FLIP on the setup — measure it before and after on 9202; an app with open sign-up after setup (R-711) is not closed by the gate; `url` is read on the docker network, so it names the container, not the subdomain. |
| **Open sign-up after the setup → `signup_block:`** (decision 47, controller ≥ 0.281.0) | `templates/opengist/.felhom.yml`, `templates/calcom/.felhom.yml` | `signup_block: "<traefik matcher>"` + `app_info.add_people` (hu) / `i18n.en.app_info.add_people` | TRAPS: block the app's API sign-up call, not only the page; an app's own invite link often uses the same address (the household's 15-minute window covers it); `add_people` is copy — `--capture-freeze`. |
| **The visitor's address — read from the RIGHT, never the leftmost** (R-753, controller ≥ 0.286.0) | `templates/docmost/docker-compose.yml` (the reset), `templates/home-assistant/` (a right-walking reader) | traefik keeps the tunnel's chain: `<client-written…>, <real visitor>, 172.16.253.2`; the LAN gives one entry. An app that reads the LEFTMOST entry gets the router reset: `traefik.http.middlewares.<router>-xff.headers.customrequestheaders.X-Forwarded-For=` + `traefik.http.routers.<router>.middlewares=<router>-xff` (defined on the router's own container) — it then reads X-Real-Ip or its peer. A right-walking reader gets `172.16.0.0/12` as its trusted proxies. A FIXED count is wrong for one of the two paths — leave count readers alone. **Never turn on** a leftmost switch: glance `proxied`, karakeep `RATE_LIMITING_ENABLED`, vaultwarden `IP_HEADER=X-Forwarded-For`, PocketBase `UseLeftmostIP`, navidrome's reverse-proxy whitelist, Plex `ALLOWED_NETWORKS`. Sweep of all 56: `felhom.eu/documentation/audits/visitors-2026-10-01/A/sweep/`. Checklist 3.10 |
| **No login of its own / a login strangers must not reach → `family_gate:`** (decisions 63/64, controller ≥ 0.287.0) | `templates/metube/.felhom.yml` (no exceptions), `templates/grimmory/.felhom.yml` (`family_gate_except:` for OPDS/Kobo/KOReader/Komga); README.md §family gate | `family_gate: true` + `min_controller: "0.287.0"` (required); optional `family_gate_except:` — LITERAL path prefixes a phone or e-reader app calls, each anchored by the box at a segment boundary and still behind the APP's own login. TRAPS: never an exception on an app with no login of its own (an open door); measure each exception on 9202 as a stranger AND a look-alike (`<prefix>x` → the gate); a websocket passes the gate like any request (MeTube measured 101); gate `check-family-gate.py` refuses an unanchored exception and a family_gate before a golden ≥ 0.287.0 is baked. |
| **The app's own sign-up switch → `after_setup:`** (decisions 47/49, controller ≥ 0.282.0) | `templates/homebox/` (compose `HBOX_OPTIONS_ALLOW_REGISTRATION=${SIGNUP_OPEN:-true}` + `.felhom.yml` `after_setup.env`) | compose default OPEN; `after_setup: {env: {SIGNUP_CLOSED: "true"}}` | TRAPS: the default must be OPEN (an installed app is unchanged by the catalog); several apps' switch also refuses the household's FIRST account, so never set it at install; an app with no env switch (opengist, wishlist) keeps the block alone — make that block case-insensitive. |
| **A same-tag security fix → a RE-TEST step** (`09` decision 52, 2026-09-30) | `scripts/retest-floating.py` (the ONE monthly command), `scripts/retest_box.py` (the box venue), `scripts/box_walk.py` (the 9202 client), `upgrade-test.py --retest` + `--write-ladder` | An entry whose `from` == `to`, with `digest_from` (the tested digest it started from) and `box_evidence`. `--dry-run` lists; `--engines-only` is the ruled start. Runbook `felhom.eu/documentation/runbooks/monthly-floating-retest.md`. TRAPS: never write one by hand (the gates refuse no new digest, a digest the registry stopped serving, a missing box proof, a `digest_from` that is not the previous entry's digest); `image_digest.resolve` IGNORES a `@digest` in its argument — ask the registry by manifest for a digest. |
| **A bench-only environment override** (R-739) | `upgrade-test.py` `BENCH_ENV_OVERRIDES` | Only for an app that cannot run on the bench at all (wanderer: its web server calls the DB at the public https name). Every verdict carries `bench_overrides`. Never a template change. |
+77
View File
@@ -0,0 +1,77 @@
# Onboarding record — grimmory
app: grimmory
opened: 2026-10-02
template_at: uncommitted working tree 2026-10-02 (grimmory v3.5.0 + mariadb:11.4, family_gate)
<!--
Two evidence roots. felhom.eu/documentation/audits/new-apps-2026-10-01/ holds the first build (2026-10-01): the bench
(LXC 9401, swap 0: bench/grimmory/), the box (9202, drill catalog, controller 0.285.0: box/grimmory/) and the proven step
3.4.1 -> 3.5.0 on both venues; the app was HELD there for R-775 (a stranger's 5 wrong sign-ins locked every visitor out).
felhom.eu/documentation/audits/family-gate-2026-10-02/ holds today's build behind the family gate (controller 0.287.0,
`09` §3 decisions 63/64): A/setup.txt and A/items.txt (strangers, family members, each e-reader exception, R-775
re-measured, the controller down), B/box/ (seed, backup, restore, remove on 9202), B/grimmory-reads/ (upstream source and
registry reads at v3.5.0). The spike that measured the gate first: felhom.eu/documentation/audits/permanent-gate-2026-10-01/.
-->
0.1 | done | felhom.eu/documentation/audits/licences-2026-10-02/read-2.md — AGPL-3.0 at v3.5.0 (MariaDB GPL-2.0); upstream's own images, pulled, never redistributed
0.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/registry.txt — 24 releases in 2026, v3.5.0 on 2026-09-21, pushed 2026-10-01, nightlies daily to 2026-10-02
0.3 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/registry.txt — version tags `vX.Y.Z` on ghcr.io; v3.5.0 amd64 + arm64; mariadb:11.4 amd64 + arm64
0.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/checks.txt — no telemetry; a version check asks api.github.com for releases; metadata lookups send titles/ISBNs to the providers the household picks (the first_steps say so); no connection held at rest
0.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — the library works offline; metadata lookups and Kobo sync need the internet (Kobo's first initialization asks Kobo's store, then falls back — 200 measured)
0.6 | n/a | Grimmory serves HTTP on port 6060 only; the database stays internal
0.7 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — every e-reader route through traefik, LAN and the simulated tunnel, no family cookie: OPDS with the OPDS user 200 / wrong 401 / none 401; Kobo with the device token 200 / made-up 401; KOReader with its own user + md5 key 200 / wrong 401; Komga API none 401
0.8 | done | app-catalog-felhom.eu/templates/grimmory/.felhom.yml — tagline + use_cases (hu, en)
0.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S2-grimmory-bench-probe.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt — no public-URL setting is read; it starts, sets up and serves on the bench under no public name
1.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-grimmory.txt — image-pins and image-resolvable
1.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt — upstream's compose runs MariaDB 11.4 (linuxserver 11.4.8); the template runs the official mariadb:11.4, same major
1.3 | done | app-catalog-felhom.eu/templates/grimmory/docker-compose.yml — MARIADB_AUTO_UPGRADE=1 on grimmory-db
1.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/grimmory/evidence/MV-grimmory/verdict.json ; felhom.eu/documentation/audits/new-apps-2026-10-01/bench/grimmory/evidence/MV-grimmory/migration-lines.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/step.txt — Flyway migrates at every start (146 migrations validated); v3.4.1 seeded, stepped INTO v3.5.0 on both venues, read back
1.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/checks.txt — `java -jar /app/app.jar` (Spring Boot, Tomcat); mariadbd
1.6 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt — every env placeholder listed: DATABASE_PASSWORD generated (upstream's fallback would be the DB root password); the token key is generated by the app (1.9); OIDC forced off, remote-auth headers off by default, API docs off by default
1.7 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt — the entrypoint makes the user (USER_ID/GROUP_ID 1000), chowns /app/data /books /bookdrop, drops to it with su-exec; JVM flags fixed in JAVA_TOOL_OPTIONS; migrations by Flyway inside the app; no switch left to decide
1.8 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/checks.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt — an unknown page answers the app's page (200), no stack trace; log level INFO; API docs off
1.9 | n/a | the token-signing key is generated by Grimmory inside its own database and travels with the database backup; no template secret encrypts data
2.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-grimmory.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/volume-persistence-after-R801.txt — volume-persistence CLEAN, also after the R-801 port fix (the gate now sends its requests)
2.2 | done | app-catalog-felhom.eu/templates/grimmory/docker-compose.yml — the database in a named volume (NVMe); covers/app files in ${HDD_PATH}/appdata/grimmory/data; the books in ${USERDATA_PATH}/media/grimmory (the household browses them); the import inbox in ${IMPORT_PATH}/grimmory
2.3 | done | app-catalog-felhom.eu/templates/grimmory/.felhom.yml — books mandatory (the DB points at them), appdata mandatory, the import inbox excluded; tier 1 holds the database volume
2.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S2-grimmory-bench-probe.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-seed.txt — the folders are 1000:1000 from the first start; the uploaded book is written on the household's drive
2.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/life.txt — the night chain's backup, remove keeping backups, the household's restore button, the book read back with the household's own login; the family-gate file and the stranger's refusal come back after the restore
2.6 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/life.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/final.txt — "remove, keep data": the app, its volume and its gate file go, the books and appdata stay on the drive (and the restore brings the app back to them); "remove with data" on 9202 is REFUSED, app kept (R-442: 9202 has no host agent, so the drive path cannot be resolved — fail-closed and right); the with-data half is measured on a demo box after publishing (Part B4) and added here
2.7 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-seed.txt — 169 MB database (mostly the empty schema) + 76 KB on the drive after one book; the books dominate a real household's size
2.8 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-seed.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/shots/grimmory/1.png — the EPUB uploaded through traefik behind the family gate, listed back by the app's own API and shown in its web page
3.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/install.txt — class 4: the household makes the first admin with `POST /api/v1/setup` through the setup gate (200)
3.2 | n/a | no default login exists; the first visitor creates the admin (class 4, gated)
3.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/install.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/checks.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt — the probe reads false before the first admin and true after; the setup gate opened by its probe (~10 s), the family gate stays
3.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/checks.txt — no sign-up route; a stranger's second setup 403; the books API with no token 401
3.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/install.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — before the gate: a stranger's polls of the setup status from the press got 404/401 until the household's admin existed; behind the family gate a stranger gets the gate's answer on all 18 paths × 2 routes
3.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/throttle.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — Grimmory locks a NAME and an ADDRESS for 15 min after 5 wrong tries (hard-coded); behind the family gate a stranger cannot reach the sign-in at all (6 tries: the gate's 401, then the household signs in 200) — only a family member could still lock a name (R-775)
3.7 | done | app-catalog-felhom.eu/templates/grimmory/.felhom.yml ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt — add_people: first the Család card (measured: add, password once), then the account in Grimmory's Users page
3.8 | n/a | no after_install or after_setup command in the template
3.9 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-seed.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — the web client's sign-in (`/api/v1/auth/login`) through traefik over https behind the family gate: right 200, wrong 401; every e-reader route as 0.7
3.10 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt ; felhom.eu/documentation/audits/visitors-2026-10-01/A/sweep/sweep-4.md ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — Tomcat's native forwarded headers read the address from the RIGHT, skipping internal proxies; used for the per-address sign-in lock; the gate's own lock is per visitor and per name (a stranger's guesses never locked a member)
4.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-grimmory-reads.txt ; app-catalog-felhom.eu/templates/grimmory/docker-compose.yml — wget is in the image (curl is not); mariadb's own healthcheck.sh; both dial 127.0.0.1
4.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-grimmory.txt — probe-matches-compose
4.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/install.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/S/S2-grimmory-bench-probe.txt — box: all healthy 99 s after the press (start_period 120 s), 0 restarts; bench: 76 s
4.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/checks.txt — grimmory stopped: the state read degraded within 10 s, the front door 404; running again 45 s after start
4.5 | done | app-catalog-felhom.eu/templates/grimmory/docker-compose.yml — container_name grimmory = the stack; sidecar grimmory-db
5.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/grimmory/mem-grimmory.csv — bench swap 0 (the swap column reads 0), from birth: grimmory 521.6 MiB anon = 50.9 % of 1024M, grimmory-db 108.7 MiB = 28.3 % of 384M; 0 kills
5.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/grimmory/evidence/MV-grimmory/verdict.json ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-mem.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-mem-burst.csv — bench: 606 s soak, 11 880 requests all 200, grimmory 40.8 %, db 27.9 % (anon); box: the household's heaviest ordinary act, 40 EPUBs uploaded at once and read in by the library (41 listed in 8 s) — peak anon 464.6 MiB = 45.4 % of 1024M; 0 kills, 0 restarts
5.3 | done | app-catalog-felhom.eu/templates/grimmory/.felhom.yml — mem_limit 1408M = 1024M + 384M, the header says the same
5.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-mem.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt — the heap sizes itself from the limit (the image's MaxRAMPercentage=60, ExitOnOutOfMemoryError): the JVM's own MaxHeapSize read at three limits with v3.5.0 — 768m → 461 MiB, 1024m → 614 MiB, 1536m → 922 MiB; watched in use at 1024M on two venues (bench 50.9 %, box 49.3 %, burst 45.4 %)
5.5 | done | app-catalog-felhom.eu/templates/grimmory/.felhom.yml ; felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-grimmory-reads.txt — pi_compatible true (both images arm64); the pull is 481 MB + 327 MB
6.1 | done | app-catalog-felhom.eu/scripts/upgrade_fixtures_box.py — Grimmory: first admin, a library, a real EPUB through the app's own API; readback with no-token, wrong-password and second-setup controls
6.2 | done | app-catalog-felhom.eu/templates/grimmory/.felhom.yml ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/box-verdict-grimmory.json ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-step.txt — the ladder step v3.4.1 -> v3.5.0, proven on both venues, written by --write-ladder; walked again on 9202 behind the family gate today (58.5 s, the book read back, the gate and the OPDS exception unchanged)
6.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/undo/box/radicale/step.txt — the box's own undo, proven on a real failed step (Radicale, 2026-10-01); Grimmory's step ran the same guarded Update
6.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/grimmory/evidence/MV-grimmory/verdict.json — files_changed empty, no mark
6.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/registry.txt — `vX.Y.Z` since v2.2.4, plus floating `vX.Y`, `latest` and dated nightlies (never pinned)
7.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt — mail (send a book) is set in the app's own settings and stored in its database; no mail env; a fresh install boots without it
8.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-grimmory.txt — copy-i18n
8.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/grim-seed.txt — the first steps hold on 9202: the admin, the Család card, a library on /books (= userdata/media/grimmory on the drive), the upload, OPDS on with an OPDS user
8.3 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/assets.txt — logo (from the image) and screenshots answer 200 on felhom.eu
8.4 | done | app-catalog-felhom.eu/README.md ; app-catalog-felhom.eu/FIRST-ADMIN.md ; app-catalog-felhom.eu/templates/grimmory/.felhom.yml — README app table + FIRST-ADMIN row (class 4); category media; catalog_since 2026-10-02
8.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/assets.txt — the website's app count read against the templates
9.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-grimmory.txt — catalog_gates.py grimmory exit 0
9.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — a fresh install on 9202 from the drill catalog behind the family gate, as the household, as family members and as a stranger
9.3 | done | felhom.eu/documentation/backlog/OPEN-ITEMS.md — every finding is a register row (R-775 narrowed: only a family member can still lock a name)
9.4 | done | app-catalog-felhom.eu/onboarding/grimmory.md — published in one commit with this record (the onboarding gate)
+76
View File
@@ -0,0 +1,76 @@
# Onboarding record — metube
app: metube
opened: 2026-10-02
template_at: uncommitted working tree 2026-10-02 (ghcr.io/alexta69/metube:2026.09.29, family_gate, no exceptions)
<!--
MeTube was STOPPED on 2026-10-01 for one reason: it has no login at all, by design, and the box had no permanent
household-only door (felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md, R-767). Controller 0.287.0 has one: the family gate (`09` §3 decisions 63/64). Evidence
root: felhom.eu/documentation/audits/family-gate-2026-10-02/ — A/ (the gate on 9202: strangers, family members, the websocket, the controller down), B/box/ (MeTube on 9202,
drill catalog, controller 0.287.0: a fresh install at 2026.09.28, checks, the product's Update to 2026.09.29, backup,
restore, remove), C-metube-bench/ (bench LXC 9401, swap 0, rebuilt and destroyed: birth, the step, the burst, gates),
B/metube-reads/ (the entrypoint and every setting, read at the tag).
-->
0.1 | done | felhom.eu/documentation/audits/licences-2026-10-02/read-2.md — AGPL-3.0 at 2026.09.29; upstream's own image, pulled, never redistributed
0.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/C1b-tag-shape.txt — 80 releases in 2026, the last 2026.09.29; 9 open issues; pushed 2026-09-29
0.3 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/C1-previous-tag.txt — dated version tags; 2026.09.28 and 2026.09.29 resolve, amd64 + arm64
0.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/metube-reads/reads-2026.09.29.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — no telemetry, no version check in the source; yt-dlp self-update runs ONLY if YTDL_NIGHTLY_UPDATE_TIME is set (the template does not set it, so the pinned yt-dlp stays); no outbound connection held at rest; the first-start log names no host but the seeded download
0.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/metube-reads/reads-2026.09.29.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — downloading from the internet is the app's whole job, from the household's address (the page says so); nothing is fetched at the first start; a local YouTube token helper (bgutil-pot) starts with it and talks to YouTube only for a YouTube download
0.6 | n/a | MeTube serves HTTP on port 8081 only, through traefik
0.7 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/metube-reads/reads-2026.09.29.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md — web only on Felhom: upstream's "send to MeTube" browser extensions, bookmarklets and phone apps call /add from another site (CORS_ALLOWED_ORIGINS, empty here) and carry no family login, so behind the gate they do not work — the household pastes the link in the page (first_steps say so); no exception by design, MeTube has no login to fall back on
0.8 | done | app-catalog-felhom.eu/templates/metube/.felhom.yml — tagline + use_cases (hu, en)
0.9 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/metube-reads/reads-2026.09.29.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/birth/evidence/BIRTH/birth.log — no public-URL setting is set (PUBLIC_HOST_URL is a relative path); it starts and downloads on the bench under no public name
1.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/C3-catalog-gates-metube.txt — image-pins and image-resolvable OK on the bench
1.2 | n/a | MeTube has no database engine; its queue and history are files in /state
1.3 | n/a | no MariaDB or PostgreSQL sidecar in this template
1.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/MV-metube.log ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-step.txt — no database to migrate; 2026.09.28 seeded (a download), stepped INTO 2026.09.29 on both venues, the history and the file read back
1.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — `python3 app/main.py` (aiohttp, MeTube's production server) under tini; no development server
1.6 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/metube-reads/reads-2026.09.29.txt — every setting the app reads is listed (Config._DEFAULTS); none is a key, a secret or a password (MeTube has no login); the template sets DOWNLOAD_DIR, STATE_DIR, PUID/PGID, MAX_CONCURRENT_DOWNLOADS=2, ALLOW_PRIVATE_ADDRESSES=false
1.7 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/metube-reads/reads-2026.09.29.txt — the entrypoint chowns the download and state folders to PUID:PGID, upgrades yt-dlp ONLY when YTDL_NIGHTLY_UPDATE_TIME is set (off), starts bgutil-pot and runs the app supervised; nothing else to decide
1.8 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — an unknown page answers 404 with no trace; LOGLEVEL INFO by default
1.9 | n/a | MeTube has no secret at all, so nothing can lock the household out
2.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/volume-persistence-after-R801.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-metube.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/life.txt — CLEAN once the gate really exercised MeTube (a POST /add, APP_EXERCISE; before the R-801 port fix the gate sent no request at all and answered UNDETERMINED, felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-metube-run2.txt); also the history in /state read back after a recreate and after remove + restore
2.2 | done | app-catalog-felhom.eu/templates/metube/docker-compose.yml — the downloads in ${USERDATA_PATH}/media/metube (the household sees them in the file browser and the media player); the queue and history in the named volume metube_state (NVMe)
2.3 | done | app-catalog-felhom.eu/templates/metube/.felhom.yml — the downloads are class optional (they can be downloaded again; large); tier 1 holds metube_state
2.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — the downloaded file is 1000:1000 0644 on the household's drive (PUID/PGID 1000)
2.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/life.txt — the night chain's backup, remove keeping data, the household's restore button (38.5 s), the history and the file read back by a family member; the family-gate file and the stranger's refusal come back after the restore
2.6 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/life.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/final.txt — "remove, keep data": the app, its volume and its gate file go, the downloads stay; "remove with data" on 9202 is REFUSED, app kept (R-442: no host agent on 9202 — fail-closed and right); the with-data half is measured on a demo box after publishing (Part B4) and added here
2.7 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/evidence/MV-metube/burst-metube.log — 12 KB of state + the files; the downloads ARE the size (563 MB for 96 short test clips on the bench); a household's videos are optional in the backup for that reason
2.8 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — the downloaded file opens again through the front door: GET /download/<file> 200 through traefik behind the family gate (a never-downloaded name 404)
3.1 | done | app-catalog-felhom.eu/FIRST-ADMIN.md ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — class 5: no accounts at all; the family gate is the only door, and only family members pass it
3.2 | n/a | no login of its own, so no default login exists
3.3 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/poll-metube.txt — no first-run screen; the family gate's file is written BEFORE the first start: a stranger's 63 polls (GET / and POST /add, 1/s from the press) answered 404 (no router yet) then 401 (the gate) — never the app
3.4 | n/a | MeTube has no sign-up and no accounts to sign up for
3.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/poll-metube.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — from the install press: never a 200 for a stranger (404 → 401)
3.6 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — MeTube has no lock of its own; the family sign-in locks per visitor (5 a minute) and per name (10 in 10 minutes): a stranger's 7 wrong tries locked only the stranger, the family member signed in at once from elsewhere
3.7 | done | app-catalog-felhom.eu/templates/metube/.felhom.yml ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — add_people: the Család card (add: the password shown once; reset and remove end access at the next request, measured)
3.8 | n/a | no after_install or after_setup command in the template
3.9 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/shots/familylogin-video.png — the family sign-in through traefik over https, LAN and the simulated tunnel; in a real browser (headless Chrome) the app's address leads to the sign-in page; the websocket passes the gate (101) for a member
3.10 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — MeTube reads no visitor address; the gate does, from the right (R-753): a stranger's guesses through the simulated tunnel locked only that visitor
4.1 | done | app-catalog-felhom.eu/templates/metube/docker-compose.yml ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/birth/evidence/BIRTH/birth.log — curl is in the image (its own upstream healthcheck uses it); dials 127.0.0.1:8081; healthy on the first probe
4.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/C3-catalog-gates-metube.txt — probe-matches-compose OK
4.3 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/birth/evidence/BIRTH/birth.log — box: healthy 50 s after the press, 0 restarts; bench: 31.2 s
4.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — metube stopped: the state read "stopped" within 10 s; a member at / 404 (no router — never the app ungated); running again 10 s after start
4.5 | done | app-catalog-felhom.eu/templates/metube/docker-compose.yml — container_name metube = the stack; no sidecars
5.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/birth/mem-summary.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/birth/mem-birth.csv — swap 0, from birth: 58.8 MiB anon = 7.7 % of 768M; 0 kills, 0 restarts
5.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/mem-summary.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/evidence/MV-metube/burst-metube.log — 605 s soak + 12 rounds of 8 downloads at once (96 adds, 563 MB): peak anon 270.2 MiB = 35.2 % of 768M; 0 kills, 0 restarts (memory.current touched the limit with page cache, judged on anon — `09` decision 22)
5.3 | done | app-catalog-felhom.eu/templates/metube/.felhom.yml — mem_limit 768M = the compose limit, the header says the same
5.4 | n/a | MeTube is a Python app, no self-sizing heap
5.5 | done | app-catalog-felhom.eu/templates/metube/.felhom.yml ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/C1-previous-tag.txt — pi_compatible true (amd64 + arm64)
6.1 | done | app-catalog-felhom.eu/scripts/upgrade_fixtures_box.py — MeTube: POST /add a 1 MB public test video, the history and GET /download read back; a never-downloaded name must answer 404
6.2 | done | app-catalog-felhom.eu/templates/metube/.felhom.yml ; felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/evidence/MV-metube/verdict.json ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/box-verdict-metube.json — the ladder step 2026.09.28 -> 2026.09.29, proven on both venues, written by --write-ladder
6.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/undo/box/radicale/step.txt — the box's own undo, proven on a real failed step (Radicale, 2026-10-01); MeTube's step ran the same guarded Update (backing-up, verifying, done)
6.4 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/evidence/MV-metube/verdict.json — files_changed empty, no mark
6.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/C1b-tag-shape.txt — dated tags; the shape changed once (YYYY-MM-DD until 2025-07-30, YYYY.MM.DD since); whether upstream re-pushes a tag was not measured
7.1 | n/a | MeTube sends no mail and has no mail settings
8.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-metube.txt — copy-i18n OK (the freeze admits metube; English for every string)
8.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/A/setup.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt — the first steps hold on 9202: members added on the Család card, video.DOMAIN asks for the family name, a pasted link downloads to the drive
8.3 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/assets.txt — logo (from the image) and three screenshots answer 200 on felhom.eu
8.4 | done | app-catalog-felhom.eu/README.md ; app-catalog-felhom.eu/FIRST-ADMIN.md ; app-catalog-felhom.eu/templates/metube/.felhom.yml — README app table + FIRST-ADMIN row (class 5); category media; catalog_since 2026-10-02
8.5 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/assets.txt — the website's app count read against the templates; it holds
9.1 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/catalog-gates-metube.txt — catalog_gates.py metube exit 0 on the bench, every gate OK incl. volume-persistence (after R-801) and family-gate against golden 0.287.0
9.2 | done | felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-fresh.txt ; felhom.eu/documentation/audits/family-gate-2026-10-02/A/items.txt — a fresh install on 9202 from the drill catalog behind the family gate, as the household, as family members and as a stranger
9.3 | done | felhom.eu/documentation/backlog/OPEN-ITEMS.md — every finding is a register row (R-767 closed by the family gate; R-788 and R-801, the volume-persistence gate)
9.4 | done | app-catalog-felhom.eu/onboarding/metube.md — published in one commit with this record (the onboarding gate)
+28 -1
View File
@@ -130,6 +130,14 @@ def gate_cookie(sub):
sess = open(f"{SC}/sess{os.getpid()}.txt").read().strip()
r = sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", "Accept: text/html", "-H", f"Host: {sub}.{DOMAIN}", f"{BASE}/"])
loc = _loc(r.stdout)
st = re.match(r"HTTP/\S+ (\d+)", r.stdout or "")
if not loc and (not st or st.group(1) in ("404", "502", "503", "504")):
# the app is not routed at this moment (a restart, an update's stop): NOT the same as "not gated" — a cached ""
# here sent every later call past nothing and read the gate's 401 as the app's (2026-10-02, grim-step)
say(f" gate: {sub} not routed right now ({st.group(1) if st else 'no answer'}) — not cached, asked again next call")
return ""
if "/__family/start" in loc: # v0.287.0: the FAMILY gate — the household's dashboard session vouches, as for
return family_cookie(sub, loc, sess) # the setup gate (decisions 63/64)
if "/__gate/start" not in loc:
GATE[sub] = ""
return "" # not gated (open, or no gate for this app)
@@ -148,6 +156,25 @@ def gate_cookie(sub):
return GATE[sub]
def family_cookie(sub, loc, sess):
"""Pass the FAMILY gate (controller >= 0.287.0) as the household: /__family/start on the dashboard host with the
dashboard session → the app host's /__felhom_gate/fcb → `felhom_famgate`. Never printed."""
import urllib.parse
u = urllib.parse.urlsplit(loc)
r = sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", "Accept: text/html", "-H", f"Host: {u.hostname}", "-H", f"Cookie: {sess}",
f"{BASE}{u.path}?{u.query}"])
u = urllib.parse.urlsplit(_loc(r.stdout))
if "/__felhom_gate/fcb" not in u.path:
say(f" family gate: the dashboard did not hand back a callback for {sub}")
GATE[sub] = ""
return ""
r = sh(["curl", "-sk", "-D", "-", "-o", "/dev/null", "-H", "Accept: text/html", "-H", f"Host: {u.hostname}", f"{BASE}{u.path}?{u.query}"])
m = re.search(r"(?im)^set-cookie:\s*(felhom_famgate=[^;\r\n]+)", r.stdout or "")
GATE[sub] = m.group(1) if m else ""
say(f" family gate: {sub} is family-gated — passed as the household (cookie {'set' if GATE[sub] else 'NOT set'})")
return GATE[sub]
def app_curl(sub, path, *extra, method=None, data=None, timeout=45, _retry=True):
"""A call to the APP's own front door on 9202 — the household's route, not ours. Carries the setup
gate's cookie when the app is gated, merged into a fixture's own Cookie header (never a second one)."""
@@ -172,7 +199,7 @@ def app_curl(sub, path, *extra, method=None, data=None, timeout=45, _retry=True)
r = sh(args, timeout=timeout + 30, inp=data)
body, _, tail = (r.stdout or "").rpartition("\n")
code, _, redir = tail.strip().partition(" ")
if _retry and "/__gate/start" in redir:
if _retry and ("/__gate/start" in redir or "/__family/start" in redir):
GATE.pop(sub, None) # the gate cookie expired or was never taken — log in as the household again
return app_curl(sub, path, *raw, method=method, data=data, timeout=timeout, _retry=False)
return r.returncode, code.strip(), body
+3
View File
@@ -114,6 +114,9 @@ GATES = [
# exists. Static, files only, so it is --fast and bites in the hook AND in CI. The 53 apps published before
# the checklist are exempt by name inside the script.
("onboarding", "check-onboarding.py", False, True, False),
# 2026-10-02 (`09` §3 decisions 63/64): a family-gated template's exceptions are literal prefixes, it declares
# min_controller >= 0.287.0, and the newest baked golden knows the gate. Static, files only.
("family-gate", "check-family-gate.py", False, True, False),
]
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check-family-gate.py — the family gate's template fields are safe to publish (`09` §3 decisions 63/64, controller 0.287.0).
A template with `family_gate: true` is published ONLY behind the household's family gate. Three ways that goes wrong, each
refused here:
1. an exception (`family_gate_except:`) that is not a LITERAL path prefix — a regex, a traefik matcher, `..`, `//`, or `/`
itself. The controller anchors each prefix at a path-segment boundary (`^/prefix(/|$)`, finding F1 of the spike: an
unanchored `PathPrefix(/api/v1/opds)` let `/api/v1/opdsx` past the gate) and refuses the install on anything else —
this gate refuses it at push time instead of at a household's install;
2. `family_gate: true` without `min_controller: "0.287.0"` (or newer) — the controller refuses a template that needs a
newer box, but only if the template SAYS so;
3. `family_gate: true` while the newest baked golden is older than 0.287.0 — a box installed from that golden runs a
controller that does not know `family_gate` and would publish the app OPEN. Read from the sibling `felhom.eu`
checkout (`documentation/tests/golden-<VER>-<DATE>/` holding a bake log with a `GOLDEN_SHA256=` line — a directory
NAME is not a bake, R-410). The sibling absent (CI's single clone) → printed as NOT CHECKED, never as a pass of rule 3.
Also refused: `family_gate_except:` on a template without `family_gate: true` (an exception list with no gate is a label
without the fact).
Line-based on purpose: the catalog's CI has NO PyYAML. Only TOP-LEVEL, uncommented keys count; a key inside a comment, a
README or a tagline is not the field.
Run from the repo root: python3 scripts/check-family-gate.py [--root=<catalog>] [--felhom-eu=<sibling>]
Exit 0 clean · 1 refused. Decoys: scripts/test_gate_decoys.py (family-gate).
"""
import io
import os
import re
import sys
MIN_VERSION = (0, 287, 0)
LITERAL = re.compile(r"^/[A-Za-z0-9._~/-]*$")
TOP_TRUE = re.compile(r"^family_gate:\s*true\s*(#.*)?$")
TOP_EXCEPT = re.compile(r"^family_gate_except:\s*(#.*)?$")
TOP_MINC = re.compile(r'^min_controller:\s*"?([0-9]+\.[0-9]+\.[0-9]+)"?\s*(#.*)?$')
ITEM = re.compile(r'^\s+-\s*(?:"([^"]*)"|\'([^\']*)\'|(\S+))\s*(#.*)?$')
GOLDEN_DIR = re.compile(r"^golden-(\d+)\.(\d+)\.(\d+)-\d{4}-\d{2}-\d{2}$")
GOLDEN_SHA = re.compile(r"GOLDEN_SHA256=[0-9a-f]{64}")
BAKE_LOGS = ("bake.log", "06-bake.log", "bake-clean.log", "06-bake-clean.log")
def arg(name, default):
for a in sys.argv[1:]:
if a.startswith(name + "="):
return a.split("=", 1)[1]
return default
def parse(text):
"""(family_gate, excepts or None, min_controller tuple or None) from a .felhom.yml text."""
gate, excepts, minc = False, None, None
lines = text.splitlines()
for i, ln in enumerate(lines):
if TOP_TRUE.match(ln):
gate = True
m = TOP_MINC.match(ln)
if m:
minc = tuple(int(x) for x in m.group(1).split("."))
if TOP_EXCEPT.match(ln):
excepts = []
for nxt in lines[i + 1:]:
if not nxt.strip() or nxt.lstrip().startswith("#"):
continue
mi = ITEM.match(nxt)
if not mi:
break
excepts.append(next(g for g in mi.groups()[:3] if g is not None))
return gate, excepts, minc
def literal_ok(p):
if not LITERAL.match(p) or "//" in p or "/../" in p or p.endswith("/..") or p.strip("/") == "":
return False
return True
def newest_golden(sibling):
tests = os.path.join(sibling, "documentation", "tests")
if not os.path.isdir(tests):
return None
best = None
for name in os.listdir(tests):
m = GOLDEN_DIR.match(name)
if not m:
continue
d = os.path.join(tests, name)
baked = False
for log in BAKE_LOGS:
p = os.path.join(d, log)
if os.path.isfile(p) and GOLDEN_SHA.search(io.open(p, encoding="utf-8", errors="replace").read()):
baked = True
break
if baked:
v = tuple(int(x) for x in m.groups())
best = v if best is None or v > best else best
return best
def main():
root = arg("--root", os.getcwd())
sibling = arg("--felhom-eu", os.path.join(os.path.dirname(os.path.abspath(root)), "felhom.eu"))
tdir = os.path.join(root, "templates")
fails, gated, unchecked = [], [], False
for app in sorted(os.listdir(tdir)):
fy = os.path.join(tdir, app, ".felhom.yml")
if not os.path.isfile(fy):
continue
gate, excepts, minc = parse(io.open(fy, encoding="utf-8").read())
if excepts is not None and not gate:
fails.append("%s: family_gate_except without family_gate: true — an exception list with no gate" % app)
if not gate:
continue
gated.append(app)
for p in excepts or []:
if not literal_ok(p):
fails.append("%s: family_gate_except %r is not a literal path prefix (the controller anchors "
"^/prefix(/|$) and refuses anything else — F1)" % (app, p))
if minc is None or minc < MIN_VERSION:
fails.append("%s: family_gate needs min_controller: \"%d.%d.%d\" or newer (got %s)"
% ((app,) + MIN_VERSION + (minc,)))
if gated:
g = newest_golden(sibling)
if g is None:
print("family-gate: rule 3 NOT CHECKED — no felhom.eu sibling with a baked golden at %s" % sibling)
unchecked = True
elif g < MIN_VERSION:
fails.append("family_gate on %s while the newest baked golden is %s — a box installed from it would publish "
"the app OPEN; bake a golden >= %d.%d.%d first" % ((", ".join(gated), "%d.%d.%d" % g) + MIN_VERSION))
else:
print("family-gate: newest baked golden %d.%d.%d >= %d.%d.%d" % (g + MIN_VERSION))
for f in fails:
print(" REFUSED " + f)
if fails:
print("family-gate gate: %d refusal(s)" % len(fails))
return 1
# The summary line carries the gap: an "OK" read alone must not stand for rule 3 when rule 3 was not checked (the
# 2026-10-02 bench run read exactly that). Exit stays 0 — CI's single clone can never check it; the hook does.
print("family-gate gate OK: %d family-gated template(s) %s%s" % (len(gated), gated,
" — rule 3 (golden >= 0.287.0) NOT CHECKED here" if unchecked else ""))
return 0
if __name__ == "__main__":
sys.exit(main())
+48 -6
View File
@@ -583,6 +583,51 @@ def _exercise(cids, ports, deep=False):
return hits
def routed_ports(resolved):
"""The ports traefik routes to, from `docker compose config --format json`.
That output gives `labels` as a MAPPING (`{"traefik.http.services.x.loadbalancer.server.port": "8081"}`), where the
port's NAME is the key — so each label is read as `key=value`, the shape PORT_RE matches. Reading the values alone
found NO port for any template (2026-10-02, R-801): the gate's HTTP exercise never ran, and every verdict came from
what an app writes at start by itself. Pinned by test_routed_ports_reads_the_mapping_form."""
out = set()
for svc in (resolved.get("services") or {}).values():
labels = svc.get("labels") or {}
items = [f"{k}={v}" for k, v in labels.items()] if isinstance(labels, dict) else [str(l) for l in labels]
for lbl in items:
m = PORT_RE.search(lbl)
if m:
out.add(int(m.group(1)))
return sorted(out)
# APP_EXERCISE — the app's OWN write path, for an app whose GET pages write nothing (R-788, 2026-10-02): MeTube writes
# only when it downloads, so a GET-only exercise leaves both of its mounts empty and the honest verdict is UNDETERMINED.
# Each entry is (method, path, json body); it is sent to every running container's routed port, like `_exercise`, and
# the gate then observes where the data landed as for any app. A request the app refuses writes nothing and the verdict
# stays UNDETERMINED — the exercise cannot turn a non-answer into a pass.
APP_EXERCISE = {
"metube": [("POST", "/add", {"url": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4",
"quality": "best", "format": "any", "download_type": "video", "auto_start": True})],
}
def _exercise_app(app, cids, ports):
hits = []
for method, path, body in APP_EXERCISE.get(app, []):
for cid in cids:
info = _inspect(cid) or {}
for net in ((info.get("NetworkSettings") or {}).get("Networks") or {}).values():
ip = net.get("IPAddress")
for port in (ports if ip else []):
code = _sh(["curl", "-sS", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "30", "-X", method,
"-H", "Content-Type: application/json", "--data", json.dumps(body),
f"http://{ip}:{port}{path}"], timeout=60).stdout.strip()
if code and code != "000":
hits.append(f"{method} {ip}:{port}{path} -> {code} (the app's own write path)")
return hits
def docker_prober(app: str, app_dir: Path, settle: int = 45, wait: int = 300) -> dict:
"""Deploy the template, exercise it, and report WHERE the data landed. The Docker seam.
@@ -615,12 +660,7 @@ def docker_prober(app: str, app_dir: Path, settle: int = 45, wait: int = 300) ->
return {"app": app, "error": f"compose config failed: "
f"{(cfg.stderr or cfg.stdout)[:300]}"}
declared = set((resolved.get("volumes") or {}).keys())
ports = sorted({int(m.group(1))
for svc in (resolved.get("services") or {}).values()
for lbl in ((svc.get("labels") or {}).values()
if isinstance(svc.get("labels"), dict)
else (svc.get("labels") or []))
for m in [PORT_RE.search(str(lbl))] if m})
ports = routed_ports(resolved)
up = _sh(base + ["up", "-d"], timeout=1800)
cids = [c for c in _sh(base + ["ps", "-aq"], timeout=120).stdout.split() if c]
@@ -643,6 +683,8 @@ def docker_prober(app: str, app_dir: Path, settle: int = 45, wait: int = 300) ->
running = [c for c in cids
if ((_inspect(c) or {}).get("State") or {}).get("Status") == "running"]
hits = _exercise(running, ports) if (running and ports) else []
if running and ports and app in APP_EXERCISE:
hits += _exercise_app(app, running, ports)
time.sleep(settle)
def observe():
+49
View File
@@ -407,6 +407,32 @@
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Családfa készítő és genealógiai szoftver"
},
"grimmory": {
"app_info.add_people": "A családtagot először a vezérlőpult Beállítások, Biztonság oldalán, a Család kártyán add hozzá; utána a Grimmory Beállítások, Felhasználók oldalán hozd létre a fiókját.",
"app_info.first_steps[0]": "Nyisd meg a library.DOMAIN címet, és hozd létre az admin fiókodat",
"app_info.first_steps[1]": "A családtagjaidat a vezérlőpult Beállítások, Biztonság oldalán, a Család kártyán add hozzá - idegen el sem éri a Grimmoryt",
"app_info.first_steps[2]": "Hozz létre egy könyvtárat a /books mappával - ez a meghajtódon a userdata/media/grimmory mappa",
"app_info.first_steps[3]": "Töltsd fel a könyveidet, vagy másold őket a „Beolvasandó e-könyvek (Grimmory)\" mappába",
"app_info.first_steps[4]": "Az e-könyv-olvasódhoz kapcsold be az OPDS-t a Beállításokban, és hozz létre OPDS-felhasználót",
"app_info.first_steps[5]": "A könyvadatokat a Grimmory külső szolgáltatóktól kéri le (Google Books, Open Library); a Kobo-szinkron a Kobo áruházán át is megy",
"app_info.tagline": "E-könyvtár - olvasd böngészőben, Kobón, KOReaderrel vagy bármely OPDS-olvasóval",
"app_info.use_cases[0]": "E-könyvek és képregények rendezett könyvtárban, borítóval és adatokkal",
"app_info.use_cases[1]": "Olvasás a böngészőben, haladás mentése; Kobo- és KOReader-szinkron",
"app_info.use_cases[2]": "OPDS-katalógus az e-könyv-olvasó alkalmazásoknak (a Beállításokban kapcsold be)",
"app_info.use_cases[3]": "A Calibre-Web helyett vagy mellett: ez modernebb felület, a Calibre-Web a Calibre-könyvtárakhoz jó",
"data_paths[grimmory].label": "Beolvasandó e-könyvek (Grimmory)",
"data_paths[media/grimmory].label": "E-könyvtár (Grimmory)",
"deploy_fields[DB_PASSWORD].label": "Adatbázis jelszó",
"deploy_fields[DB_ROOT_PASSWORD].label": "Adatbázis root jelszó",
"deploy_fields[DOMAIN].description": "A szerver domain neve",
"deploy_fields[DOMAIN].label": "Domain",
"deploy_fields[HDD_PATH].description": "A meghajtó, amelyen a könyveid lesznek",
"deploy_fields[HDD_PATH].label": "E-könyvtár meghajtója",
"deploy_fields[HDD_PATH].placeholder": "/mnt/felhom-drives/hdd_1",
"deploy_fields[SUBDOMAIN].description": "Az alkalmazás aldomainje",
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "E-könyvtár böngészőben olvasóval, OPDS-sel, Kobo és KOReader szinkronnal"
},
"home-assistant": {
"app_info.first_steps[0]": "Nyisd meg a ha.DOMAIN címet a böngészőben",
"app_info.first_steps[1]": "Hozd létre a tulajdonos fiókot az onboarding során",
@@ -600,6 +626,27 @@
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Receptkezelő és étkezéstervező"
},
"metube": {
"app_info.add_people": "A családtagokat a Beállítások, Biztonság oldal Család kártyáján adod hozzá; mindenki a saját nevével és jelszavával lép be.",
"app_info.first_steps[0]": "Add hozzá a családtagjaidat a Beállítások, Biztonság oldal Család kártyáján",
"app_info.first_steps[1]": "Nyisd meg a video.DOMAIN címet, és lépj be a családi neveddel",
"app_info.first_steps[2]": "Illessz be egy linket, válaszd ki a minőséget, és indítsd el a letöltést",
"app_info.first_steps[3]": "Csak saját használatra: csak olyan videót tölts le, amelyhez jogod van (például a sajátodat vagy szabad felhasználásút). A letöltés a háztartásod internetcíméről történik.",
"app_info.prerequisites[0]": "A letöltés a háztartásod internetkapcsolatát használja; egy videószolgáltató ritkán korlátozhatja a sok letöltést egy címről",
"app_info.tagline": "Videók és hanganyagok letöltése egy linkből - csak a családodnak",
"app_info.use_cases[0]": "Egy videó vagy lejátszási lista letöltése a linkjéből, videóként vagy csak hangként",
"app_info.use_cases[1]": "A letöltések a meghajtódra kerülnek, a fájlböngészőben és a médialejátszódban is látod őket",
"app_info.use_cases[2]": "Csak a családtagjaid érik el: idegen nem tud letölteni a háztartásod internetcíméről",
"data_paths[media/metube].label": "Letöltött videók (MeTube)",
"deploy_fields[DOMAIN].description": "A szerver domain neve",
"deploy_fields[DOMAIN].label": "Domain",
"deploy_fields[HDD_PATH].description": "A meghajtó, amelyre a letöltések kerülnek",
"deploy_fields[HDD_PATH].label": "Letöltések meghajtója",
"deploy_fields[HDD_PATH].placeholder": "/mnt/felhom-drives/hdd_1",
"deploy_fields[SUBDOMAIN].description": "Az alkalmazás aldomainje",
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Videók és hanganyagok letöltése a meghajtódra, a család számára"
},
"n8n": {
"app_info.first_steps[0]": "Nyisd meg az auto.DOMAIN címet a böngészőben",
"app_info.first_steps[1]": "Hozd létre az admin fiókot",
@@ -1229,7 +1276,9 @@
},
"reasons": {
"dawarich": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules",
"grimmory": "new app 2026-10-02 (family gate); Hungarian reviewed: informal te, no kerjuk (ASCII scan with a positive control)",
"karakeep": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules",
"metube": "new app 2026-10-02 (family gate); Hungarian reviewed: informal te, no kerjuk (ASCII scan with a positive control)",
"radicale": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules"
}
}
+16
View File
@@ -463,5 +463,21 @@ class TestEnvBuilding(unittest.TestCase):
self.assertEqual([f["env_var"] for f in cvp.parse_deploy_fields(felhom)], ["A"])
class TestRoutedPorts(unittest.TestCase):
def test_routed_ports_reads_the_mapping_form(self):
"""R-801: `compose config --format json` gives labels as a mapping; the port is in the KEY."""
resolved = {"services": {"metube": {"labels": {
"traefik.enable": "true",
"traefik.http.services.metube.loadbalancer.server.port": "8081"}}}}
self.assertEqual(cvp.routed_ports(resolved), [8081])
def test_routed_ports_list_form_still_read(self):
resolved = {"services": {"a": {"labels": ["traefik.http.services.a.loadbalancer.server.port=3000"]}}}
self.assertEqual(cvp.routed_ports(resolved), [3000])
def test_no_routed_port(self):
self.assertEqual(cvp.routed_ports({"services": {"db": {"labels": {"x": "y"}}}}), [])
if __name__ == "__main__":
unittest.main(verbosity=2)
+48
View File
@@ -55,6 +55,7 @@ COVERS = {
"test-record": "a ladder whose newest step is not the compose's images (a move without a record), a gap, a line that is not one JSON entry, a failed verdict - vs a clean ladder (09 decision 13)",
"test-record-move": "an image move with NO entry, with the entry only in a COMMENT or in README, with a failed/backfilled entry, with a digest the registry no longer serves, memory_tight without a raised limit - vs a proven entry that matches; a ref moving in a compose COMMENT is not a move (09 decision 13)",
"probe-measured": "the measurement written in the TAGLINE or another comment block, not directly above setup_done_probe:; a date with no before/after; before/after with no date; 'read upstream' instead of 'measured' - vs a genuine measured comment (R-715)",
"family-gate": "family_gate written only in a COMMENT (not gated, no min_controller owed); min_controller only in a comment; a golden DIRECTORY named 0.287.0 with no bake log (the mkdir shape, R-410); a sibling with no golden (stated NOT CHECKED, never a pass of rule 3) - vs the facts: an unanchorable exception (regex, '/', '..'), an exception list with no gate, min_controller below 0.287.0, the newest baked golden below 0.287.0; and a genuine family app passes (decisions 63/64, finding F1)",
"onboarding": "a NEW template with no record; a record missing an id, or carrying it only inside an HTML comment; a `done` whose path does not exist, is an EMPTY directory (the mkdir shape, R-410) or names an absent sibling-repo file; an `n/a` with an empty or two-word reason; an `open` row; `opened:` backdated before the checklist; the template a new app copies lacking a new id - vs a complete record, an id added after `opened:`, and an exempt app's record with open rows (NEW-APP-CHECKLIST.md)",
"copy-i18n": "Hungarian edited in a COMMENT/README/display_name (label, not copy) vs a real frozen string changed; an English block that is not English, is not matched to a Hungarian twin, or rewrites a credential (R-560). Also the DEGRADED mode CI actually runs — PyYAML shadowed out, freeze only (R-595)",
}
@@ -618,6 +619,52 @@ def onboarding_cases():
finally:
shutil.rmtree(ws, ignore_errors=True)
def family_gate_cases():
"""check-family-gate.py reads FILES: templates/*/.felhom.yml and the sibling felhom.eu's golden bake records."""
global ran
import tempfile
ws = tempfile.mkdtemp(prefix="catalog-familygate-")
try:
cat, sib = os.path.join(ws, "cat"), os.path.join(ws, "felhom.eu")
def golden(ver, baked=True):
d = os.path.join(sib, "documentation", "tests", "golden-%s-2026-10-02" % ver)
os.makedirs(d, exist_ok=True)
if baked:
io.open(os.path.join(d, "bake.log"), "w").write("GOLDEN_SHA256=" + "a" * 64 + "\n")
def app(body, name="famapp"):
d = os.path.join(cat, "templates", name)
os.makedirs(d, exist_ok=True)
io.open(os.path.join(d, ".felhom.yml"), "w").write("display_name: X\n" + body)
GOOD = 'family_gate: true\nfamily_gate_except:\n - "/api/v1/opds" # e-readers\n - "/api/kobo/"\nmin_controller: "0.287.0"\n'
def run(name, setup, expect_rc, must=(), sibling=True):
global ran
shutil.rmtree(cat, ignore_errors=True); shutil.rmtree(sib, ignore_errors=True)
os.makedirs(os.path.join(cat, "templates"))
setup()
args = [sys.executable, os.path.join(ROOT, "scripts", "check-family-gate.py"), "--root=" + cat,
"--felhom-eu=" + (sib if sibling else os.path.join(ws, "absent"))]
r = sh(args, ROOT); out = r.stdout + r.stderr; ran += 1
ok = r.returncode == expect_rc and all(m in out for m in must)
print(" %s %-70s rc=%d (expected %d)" % ("ok" if ok else "XX", name, r.returncode, expect_rc))
if not ok:
fails.append("%s: rc=%d expected %d; missing %s\n%s" % (name, r.returncode, expect_rc, [m for m in must if m not in out], out[-400:]))
print("\n-- family-gate: genuine and decoys")
run("GENUINE: a family app, literal exceptions, min 0.287.0, golden 0.287.0", lambda: (app(GOOD), golden("0.287.0")), 0, ("family-gate gate OK",))
run("DECOY: family_gate only in a COMMENT -> not gated, nothing owed", lambda: (app("# family_gate: true\n"), golden("0.286.1")), 0, ("0 family-gated",))
run("DECOY: no sibling -> rule 3 stated NOT CHECKED", lambda: app(GOOD), 0, ("NOT CHECKED",), sibling=False)
print("-- family-gate: the facts (each MUST be refused)")
run("FACT: an exception that is a regex", lambda: (app(GOOD.replace('"/api/kobo/"', '"/api/(.*)"')), golden("0.287.0")), 1, ("not a literal path prefix",))
run("FACT: the exception '/' (the whole app)", lambda: (app(GOOD.replace('"/api/kobo/"', '"/"')), golden("0.287.0")), 1, ("not a literal",))
run("FACT: an exception with '..'", lambda: (app(GOOD.replace('"/api/kobo/"', '"/api/../admin"')), golden("0.287.0")), 1, ("not a literal",))
run("FACT: an exception list with no gate", lambda: (app('family_gate_except:\n - "/x"\n'), golden("0.287.0")), 1, ("with no gate",))
run("FACT: min_controller only in a comment", lambda: (app(GOOD.replace('min_controller: "0.287.0"', '# min_controller: "0.287.0"')), golden("0.287.0")), 1, ("needs min_controller",))
run("FACT: min_controller below the release", lambda: (app(GOOD.replace('0.287.0', '0.286.1')), golden("0.287.0")), 1, ("needs min_controller",))
run("FACT: newest baked golden below the release", lambda: (app(GOOD), golden("0.286.1")), 1, ("publish the app OPEN",))
run("FACT: a golden DIRECTORY 0.287.0 with no bake log (a name is not a bake)", lambda: (app(GOOD), golden("0.286.1"), golden("0.287.0", baked=False)), 1, ("0.286.1",))
finally:
shutil.rmtree(ws, ignore_errors=True)
def main():
gate = os.path.join(ROOT, "scripts", "check-engine-major.py")
if not os.path.isfile(gate):
@@ -1095,6 +1142,7 @@ i18n:
shutil.rmtree(clone, ignore_errors=True)
onboarding_cases()
family_gate_cases()
if fails:
print()
+54
View File
@@ -2159,12 +2159,66 @@ class Grimmory:
return found
class MeTube:
"""MeTube (2026-10-02, new app through NEW-APP-CHECKLIST.md, published behind the family gate). THE FRONT DOOR is its
own web API, the one its page calls: `POST /add` a link, then `GET /history` until the item is `finished`; the file is
served back at `GET /download/<filename>`. The link is a 1 MB public test video (yt-dlp's generic extractor — no
video-service account, no cookies). Negative control on every readback: a file never downloaded answers 404, so a
read that says "found" cannot be a catch-all. On the box the family gate is passed as the household (box_walk)."""
sub = "video"
URL = "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"
def _hist(self, w, sub):
rc, code, out = w.app_curl(sub, "/history")
try:
return code, json.loads(out)
except Exception:
return code, {}
def seed(self, w, sub, say):
if not w.wait_app(sub, "/", want=("200",), tries=60):
self.tried = "/ never answered 200"
return None
rc, code, out = w.app_curl(sub, "/add", "-H", "Content-Type: application/json", "-H", f"Origin: https://{sub}.{w.DOMAIN}",
data=json.dumps({"url": self.URL, "quality": "best", "format": "any", "auto_start": True}), method="POST")
say(f" metube: POST /add http={code} {(out or '')[:40]}")
if code != "200":
self.tried = f"add -> {code} {(out or '')[:80]}"
return None
for _ in range(60):
code, h = self._hist(w, sub)
done = [d for d in h.get("done", []) if d.get("url") == self.URL and d.get("status") == "finished"]
if done:
fn = done[0].get("filename") or (done[0].get("title", "") + ".mp4")
say(f" metube: the download finished ({done[0].get('size')} bytes)")
return {"filename": fn, "size": done[0].get("size")}
time.sleep(3)
self.tried = "the download never finished"
return None
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/", want=("200",), tries=60):
say(" metube: / never answered")
return False
import urllib.parse
rc, c_absent, _ = w.app_curl(sub, "/download/never-" + secrets.token_hex(4) + ".mp4")
if c_absent != "404":
say(f" metube: READBACK UNUSABLE — a file never downloaded answered {c_absent}")
return False
code, h = self._hist(w, sub)
in_hist = any(d.get("url") == self.URL and d.get("status") == "finished" for d in h.get("done", []))
rc, code, out = w.app_curl(sub, "/download/" + urllib.parse.quote(t["filename"]), "-o", "/dev/null")
say(f" metube: history has it={in_hist}; GET /download/<file> http={code}")
return code == "200" and in_hist
FIXTURES = {
"uptime-kuma": UptimeKuma(),
"crafty-controller": Crafty(),
"wger": Wger(),
"calibre-web": CalibreWeb(),
"sparkyfitness": Sparkyfitness(),
"metube": MeTube(),
"rallly": Rallly(),
"outline": Outline(),
"home-assistant": HomeAssistant(),
+177
View File
@@ -0,0 +1,177 @@
# =============================================================================
# .felhom.yml - App metadata for felhom-controller
# =============================================================================
# Grimmory — ghcr.io/grimmory-tools/grimmory, the community successor of BookLore (FIT.md). Onboarding record:
# onboarding/grimmory.md (NEW-APP-CHECKLIST.md). First admin: CLASS 4 — the first visitor creates the admin
# (`POST /api/v1/setup`); after that the call answers 403 (measured), and there is no sign-up. So: the setup gate
# (decision 46) with a probe on the app's own public setup status. No sign-up block is needed.
# PERMANENTLY behind the family gate (`09` §3 decisions 63/64, controller >= 0.287.0): a stranger never reaches its web
# sign-in, so its hard-coded 15-minute lock (R-775) cannot be aimed at the household from outside. The e-reader paths
# below are exceptions — each keeps Grimmory's OWN login (OPDS user, Kobo device token, KOReader md5 key).
# --- Display info (shown on dashboard) ---
display_name: "Grimmory"
description: "E-könyvtár böngészőben olvasóval, OPDS-sel, Kobo és KOReader szinkronnal"
category: "media"
subdomain: "library"
slug: "grimmory"
# 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-10-02"
# --- Resource hints (displayed on deploy screen) ---
resources:
mem_request: "600M"
mem_limit: "1408M" # grimmory 1024M + grimmory-db 384M = 1408M
pi_compatible: true
needs_hdd: true
# --- Backup classification ---
backup:
userdata:
- path: media/grimmory
class: mandatory # the books — the database points at these files; DB and books are one unit
hdd:
- path: appdata/grimmory/data
class: mandatory # covers, thumbnails and app files the database refers to
import:
- path: grimmory
class: excluded # BookDrop inbox, transient
# --- Customer-facing folder annotation (R-75) ---
data_paths:
- path: grimmory
root: import
role: import
label: "Beolvasandó e-könyvek (Grimmory)"
- path: media/grimmory
root: userdata
role: library
label: "E-könyvtár (Grimmory)"
# --- Deploy wizard fields ---
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: "library"
required: true
locked_after_deploy: true
description: "Az alkalmazás aldomainje"
- env_var: HDD_PATH
label: "E-könyvtár meghajtója"
type: path
required: true
placeholder: "/mnt/felhom-drives/hdd_1"
locked_after_deploy: true
description: "A meghajtó, amelyen a könyveid lesznek"
- env_var: DB_PASSWORD
label: "Adatbázis jelszó"
type: secret
generate: "password:24"
locked_after_deploy: true
- env_var: DB_ROOT_PASSWORD
label: "Adatbázis root jelszó"
type: secret
generate: "password:24"
locked_after_deploy: true
setup_gate: true
# measured 2026-10-01 on 9202 (audits/new-apps-2026-10-01/box/grimmory/install.txt): before the first admin false -> after
# true; again 2026-10-02 behind the family gate (audits/family-gate-2026-10-02/A/setup.txt): the gate opened by this probe
# ~10 s after the household made the admin.
setup_done_probe:
url: "http://grimmory:6060/api/v1/setup/status"
field: "data"
done: "true"
# --- The family gate (controller >= 0.287.0) ---
family_gate: true
family_gate_except:
- "/api/v1/opds" # OPDS catalog (e-reader apps) — Grimmory's own OPDS users
- "/api/v2/opds"
- "/api/kobo" # Kobo sync — the device token in the path
- "/api/koreader" # KOReader sync — its own user + md5 key
- "/komga/api" # the Komga-compatible API (Mihon/Tachiyomi-style readers) — basic auth
min_controller: "0.287.0"
# --- Customer-facing info ---
app_info:
tagline: "E-könyvtár - olvasd böngészőben, Kobón, KOReaderrel vagy bármely OPDS-olvasóval"
add_people: "A családtagot először a vezérlőpult Beállítások, Biztonság oldalán, a Család kártyán add hozzá; utána a Grimmory Beállítások, Felhasználók oldalán hozd létre a fiókját."
docs_url: "https://github.com/grimmory-tools/grimmory"
use_cases:
- 'E-könyvek és képregények rendezett könyvtárban, borítóval és adatokkal'
- 'Olvasás a böngészőben, haladás mentése; Kobo- és KOReader-szinkron'
- 'OPDS-katalógus az e-könyv-olvasó alkalmazásoknak (a Beállításokban kapcsold be)'
- 'A Calibre-Web helyett vagy mellett: ez modernebb felület, a Calibre-Web a Calibre-könyvtárakhoz jó'
first_steps:
- 'Nyisd meg a library.DOMAIN címet, és hozd létre az admin fiókodat'
- 'A családtagjaidat a vezérlőpult Beállítások, Biztonság oldalán, a Család kártyán add hozzá - idegen el sem éri a Grimmoryt'
- 'Hozz létre egy könyvtárat a /books mappával - ez a meghajtódon a userdata/media/grimmory mappa'
- 'Töltsd fel a könyveidet, vagy másold őket a „Beolvasandó e-könyvek (Grimmory)" mappába'
- 'Az e-könyv-olvasódhoz kapcsold be az OPDS-t a Beállításokban, és hozz létre OPDS-felhasználót'
- 'A könyvadatokat a Grimmory külső szolgáltatóktól kéri le (Google Books, Open Library); a Kobo-szinkron a Kobo áruházán át is megy'
# --- Controller health probe (dials what the compose healthcheck dials) ---
healthcheck:
checks:
- type: api
port: 6060
path: "/api/v1/healthcheck"
expect:
status: 200
i18n:
en:
description: 'An e-book library with an in-browser reader, OPDS, Kobo and KOReader sync'
app_info:
tagline: 'An e-book library - read in the browser, on a Kobo, with KOReader or any OPDS reader'
add_people: "First add the family member on the dashboard's Settings, Security page, Family card; then create their account in Grimmory's Settings, Users."
use_cases:
- 'E-books and comics in a tidy library, with covers and details'
- 'Read in the browser with your progress saved; Kobo and KOReader sync'
- 'An OPDS catalog for e-reader apps (switch it on in the settings)'
- 'Instead of or next to Calibre-Web: a more modern interface; Calibre-Web suits Calibre libraries'
first_steps:
- 'Open library.DOMAIN and create your admin account'
- 'Add your family members on the dashboard''s Settings, Security page, Family card - a stranger cannot even reach Grimmory'
- 'Create a library with the /books folder - on your drive that is the userdata/media/grimmory folder'
- 'Upload your books, or copy them into the "Books to import (Grimmory)" folder'
- 'For your e-reader, switch on OPDS in the settings and create an OPDS user'
- 'Grimmory looks book details up with outside services (Google Books, Open Library); Kobo sync also goes through the Kobo store'
data_paths:
- path: 'grimmory'
label: 'E-books to read in (Grimmory)'
- path: 'media/grimmory'
label: 'E-book library (Grimmory)'
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: HDD_PATH
label: 'Library drive'
description: 'The drive your books will live on'
placeholder: '/mnt/felhom-drives/hdd_1'
- env_var: DB_PASSWORD
label: 'Database password'
- env_var: DB_ROOT_PASSWORD
label: 'Database root password'
# update_ladder — the test record: one tested step per line, oldest first (JSON flow mappings,
# `09-update-architecture.md` §6.4 part 4). WRITTEN BY scripts/upgrade-test.py, never by hand;
# gated by scripts/check-test-record.py. An image: move without a proven entry here is refused.
update_ladder:
- {"from": {"grimmory": "ghcr.io/grimmory-tools/grimmory:v3.4.1", "grimmory-db": "mariadb:11.4"}, "to": {"grimmory": "ghcr.io/grimmory-tools/grimmory:v3.5.0", "grimmory-db": "mariadb:11.4"}, "digest": {"grimmory": "sha256:bf6fe21c6e247597f7869664a440b5a34242e2d2940575cae39c17bfaa66dc0b", "grimmory-db": "sha256:1292844148b311e4ed4300022a996d39083f415a963e970cf47cad1b3b18e3a6"}, "verdict": "proven", "tested_at": "2026-10-01T16:58:47Z", "harness_version": 4, "evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/bench/grimmory/evidence/MV-grimmory/verdict.json", "box_evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/box/grimmory/box-verdict-grimmory.json", "memory_peak_pct": 40.8, "marks": {"files_may_change": false, "needs_person": null, "memory_tight": false}, "memory_basis": "anon", "memory_cgroup_peak_pct": 56.1}
+91
View File
@@ -0,0 +1,91 @@
# Grimmory - E-könyvtár (a BookLore közösségi utódja): könyvek, olvasó, OPDS, Kobo és KOReader szinkron
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: MariaDB 11.4 (the major upstream's own compose runs — `09` decision 42's rule; the official image, so
# MARIADB_AUTO_UPGRADE=1 as every catalog MariaDB sidecar, R-459)
# RAM: ~700M (mem_limit: 1408M total — app 1024M + db 384M) | Pi-compatible: Yes (amd64, arm64)
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
# HDD_PATH - A meghajtó, amelyen a könyvtár él
# DB_PASSWORD - Adatbázis jelszó (generált; csak a belső hálón)
# DB_ROOT_PASSWORD - Adatbázis root jelszó (generált; csak a belső hálón)
#
# Folders: the books in ${USERDATA_PATH}/media/grimmory (the household sees and fills it), the drop-in inbox in
# ${IMPORT_PATH}/grimmory (BookDrop: a file put there is offered for import), covers/cache in ${HDD_PATH}/appdata.
# Mirrors upstream's deploy/compose/docker-compose.yml, pinned; OIDC off, Swagger off.
services:
grimmory:
image: ghcr.io/grimmory-tools/grimmory:v3.5.0
container_name: grimmory
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- USER_ID=1000
- GROUP_ID=1000
- DATABASE_URL=jdbc:mariadb://grimmory-db:3306/grimmory
- DATABASE_USERNAME=grimmory
- DATABASE_PASSWORD=${DB_PASSWORD}
- SWAGGER_ENABLED=false
- FORCE_DISABLE_OIDC=true
volumes:
- ${HDD_PATH}/appdata/grimmory/data:/app/data
- ${USERDATA_PATH}/media/grimmory:/books
- ${IMPORT_PATH}/grimmory:/bookdrop
networks:
- traefik-public
- grimmory-internal
depends_on:
grimmory-db:
condition: service_healthy
deploy:
resources:
limits:
memory: 1024M
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:6060/api/v1/healthcheck"]
interval: 30s
timeout: 5s
retries: 5
start_period: 120s
labels:
- "traefik.enable=true"
- "traefik.http.routers.grimmory.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)"
- "traefik.http.routers.grimmory.entrypoints=websecure"
- "traefik.http.routers.grimmory.tls=true"
- "traefik.http.routers.grimmory.tls.certresolver=letsencrypt"
- "traefik.http.services.grimmory.loadbalancer.server.port=6060"
grimmory-db:
image: mariadb:11.4
container_name: grimmory-db
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MARIADB_DATABASE=grimmory
- MARIADB_USER=grimmory
- MARIADB_PASSWORD=${DB_PASSWORD}
- MARIADB_AUTO_UPGRADE=1
volumes:
- grimmory_db:/var/lib/mysql
networks:
- grimmory-internal
deploy:
resources:
limits:
memory: 384M
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
volumes:
grimmory_db:
networks:
traefik-public:
external: true
grimmory-internal:
+125
View File
@@ -0,0 +1,125 @@
# =============================================================================
# .felhom.yml - App metadata for felhom-controller
# =============================================================================
# MeTube — ghcr.io/alexta69/metube, a web interface for yt-dlp. Onboarding record: onboarding/metube.md. It has NO login
# of its own (upstream: "deliberately has no login system"), so it is offered ONLY behind the family gate (`09` §3
# decisions 63/64). FIT.md: was "stop — no login at all"; now "build behind the family gate".
# --- Display info (shown on dashboard) ---
display_name: "MeTube"
description: "Videók és hanganyagok letöltése a meghajtódra, a család számára"
category: "media"
subdomain: "video"
slug: "metube"
# 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-10-02"
# --- The family gate (controller >= 0.287.0) ---
family_gate: true
min_controller: "0.287.0"
# --- Resource hints (displayed on deploy screen) ---
resources:
mem_request: "128M"
mem_limit: "768M" # metube 768M
pi_compatible: true
needs_hdd: true
# --- Backup classification ---
backup:
userdata:
- path: media/metube
class: optional # downloaded videos — can be downloaded again; large
# --- Customer-facing folder annotation (R-75) ---
data_paths:
- path: media/metube
root: userdata
role: library
label: "Letöltött videók (MeTube)"
# --- Deploy wizard fields ---
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: "video"
required: true
locked_after_deploy: true
description: "Az alkalmazás aldomainje"
- env_var: HDD_PATH
label: "Letöltések meghajtója"
type: path
required: true
placeholder: "/mnt/felhom-drives/hdd_1"
locked_after_deploy: true
description: "A meghajtó, amelyre a letöltések kerülnek"
# --- Customer-facing info ---
app_info:
tagline: "Videók és hanganyagok letöltése egy linkből - csak a családodnak"
add_people: "A családtagokat a Beállítások, Biztonság oldal Család kártyáján adod hozzá; mindenki a saját nevével és jelszavával lép be."
docs_url: "https://github.com/alexta69/metube"
use_cases:
- 'Egy videó vagy lejátszási lista letöltése a linkjéből, videóként vagy csak hangként'
- 'A letöltések a meghajtódra kerülnek, a fájlböngészőben és a médialejátszódban is látod őket'
- 'Csak a családtagjaid érik el: idegen nem tud letölteni a háztartásod internetcíméről'
first_steps:
- 'Add hozzá a családtagjaidat a Beállítások, Biztonság oldal Család kártyáján'
- 'Nyisd meg a video.DOMAIN címet, és lépj be a családi neveddel'
- 'Illessz be egy linket, válaszd ki a minőséget, és indítsd el a letöltést'
- 'Csak saját használatra: csak olyan videót tölts le, amelyhez jogod van (például a sajátodat vagy szabad felhasználásút). A letöltés a háztartásod internetcíméről történik.'
prerequisites:
- 'A letöltés a háztartásod internetkapcsolatát használja; egy videószolgáltató ritkán korlátozhatja a sok letöltést egy címről'
# --- Controller health probe (dials what the compose healthcheck dials) ---
healthcheck:
checks:
- type: http
port: 8081
i18n:
en:
description: 'Download videos and audio to your drive, for your family'
app_info:
tagline: 'Download videos and audio from a link - for your family only'
add_people: 'You add family members on the Family card of Settings, Security; everyone signs in with their own name and password.'
use_cases:
- 'Download a video or a playlist from its link, as video or audio only'
- 'The downloads go to your drive; you see them in the file browser and your media player'
- 'Only your family members reach it: a stranger cannot download through your household''s internet address'
first_steps:
- 'Add your family members on the Family card of Settings, Security'
- 'Open video.DOMAIN and sign in with your family name'
- 'Paste a link, pick the quality, and start the download'
- 'For your own use only: download only videos you have the right to (for example your own, or freely licensed ones). Downloads go out from your household''s internet address.'
prerequisites:
- 'Downloads use your household''s internet connection; a video service may now and then limit many downloads from one address'
data_paths:
- path: 'media/metube'
label: 'Downloaded videos (MeTube)'
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: HDD_PATH
label: 'Downloads drive'
description: 'The drive the downloads go to'
placeholder: '/mnt/felhom-drives/hdd_1'
# update_ladder — the test record: one tested step per line, oldest first (JSON flow mappings,
# `09-update-architecture.md` §6.4 part 4). WRITTEN BY scripts/upgrade-test.py, never by hand;
# gated by scripts/check-test-record.py. An image: move without a proven entry here is refused.
update_ladder:
- {"from": {"metube": "ghcr.io/alexta69/metube:2026.09.28"}, "to": {"metube": "ghcr.io/alexta69/metube:2026.09.29"}, "digest": {"metube": "sha256:8e2fe9beeefc55a02f6e1d04cad0fc3c22e8f067d309188f016551dc99ec27b3"}, "verdict": "proven", "tested_at": "2026-10-02T06:31:05Z", "harness_version": 4, "evidence": "felhom.eu/documentation/audits/family-gate-2026-10-02/C-metube-bench/move/evidence/MV-metube/verdict.json", "box_evidence": "felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/box-verdict-metube.json", "memory_peak_pct": 28.1, "marks": {"files_may_change": false, "needs_person": null, "memory_tight": false}, "memory_basis": "anon", "memory_cgroup_peak_pct": 100.5}
+59
View File
@@ -0,0 +1,59 @@
# MeTube - Videoletöltő (yt-dlp webes felülete): videók és hanganyagok letöltése a meghajtódra
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: none (its queue and history are files in the state volume)
# RAM: ~150M idle, more while downloading (mem_limit: 768M) | Pi-compatible: Yes (amd64, arm64)
#
# MeTube has NO login of its own, by design. On Felhom it is published ONLY behind the family gate (`family_gate: true`,
# controller >= 0.287.0, `09` §3 decisions 63/64): a stranger reaches nothing, a family member signs in with their own
# name and password. No exceptions — every path, the websocket included, passes the gate.
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
# HDD_PATH - The drive the downloads go to
#
# Folders: the downloads in ${USERDATA_PATH}/media/metube (the household sees them in the file browser); the queue and
# history in the named volume metube_state (NVMe). ALLOW_PRIVATE_ADDRESSES stays false (upstream default): MeTube will
# not fetch from the household's own network.
services:
metube:
image: ghcr.io/alexta69/metube:2026.09.29
container_name: metube
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- PUID=1000
- PGID=1000
- DOWNLOAD_DIR=/downloads
- STATE_DIR=/state
- MAX_CONCURRENT_DOWNLOADS=2
- ALLOW_PRIVATE_ADDRESSES=false
volumes:
- ${USERDATA_PATH}/media/metube:/downloads
- metube_state:/state
networks:
- traefik-public
deploy:
resources:
limits:
memory: 768M
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8081/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
labels:
- "traefik.enable=true"
- "traefik.http.routers.metube.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)"
- "traefik.http.routers.metube.entrypoints=websecure"
- "traefik.http.routers.metube.tls=true"
- "traefik.http.routers.metube.tls.certresolver=letsencrypt"
- "traefik.http.services.metube.loadbalancer.server.port=8081"
volumes:
metube_state:
networks:
traefik-public:
external: true