diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b48f0..6f15d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 (`-xff`, diff --git a/CONTEXT.md b/CONTEXT.md index cb5da25..349e922 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 `-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 diff --git a/FIRST-ADMIN.md b/FIRST-ADMIN.md index c50b962..ec38481 100644 --- a/FIRST-ADMIN.md +++ b/FIRST-ADMIN.md @@ -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//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 | diff --git a/README.md b/README.md index 21f3e61..b2ff7af 100644 --- a/README.md +++ b/README.md @@ -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 (`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.* | diff --git a/REPORT.md b/REPORT.md index 1a42f56..bb94492 100644 --- a/REPORT.md +++ b/REPORT.md @@ -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 ` 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. diff --git a/REUSE.md b/REUSE.md index 586d156..6d8850b 100644 --- a/REUSE.md +++ b/REUSE.md @@ -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://:/, field: , done: ""}` | 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: ""` + `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: `, , 172.16.253.2`; the LAN gives one entry. An app that reads the LEFTMOST entry gets the router reset: `traefik.http.middlewares.-xff.headers.customrequestheaders.X-Forwarded-For=` + `traefik.http.routers..middlewares=-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 (`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. | diff --git a/onboarding/grimmory.md b/onboarding/grimmory.md new file mode 100644 index 0000000..b4db20b --- /dev/null +++ b/onboarding/grimmory.md @@ -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) + + + +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) diff --git a/onboarding/metube.md b/onboarding/metube.md new file mode 100644 index 0000000..fef4d2f --- /dev/null +++ b/onboarding/metube.md @@ -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) + + + +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/ 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) diff --git a/scripts/box_walk.py b/scripts/box_walk.py index 556b175..7927c32 100644 --- a/scripts/box_walk.py +++ b/scripts/box_walk.py @@ -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 diff --git a/scripts/catalog_gates.py b/scripts/catalog_gates.py index 138a18e..3f9c951 100644 --- a/scripts/catalog_gates.py +++ b/scripts/catalog_gates.py @@ -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"} diff --git a/scripts/check-family-gate.py b/scripts/check-family-gate.py new file mode 100644 index 0000000..539f3b4 --- /dev/null +++ b/scripts/check-family-gate.py @@ -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--/` 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=] [--felhom-eu=] +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()) diff --git a/scripts/check-volume-persistence.py b/scripts/check-volume-persistence.py index 2d62598..227339a 100644 --- a/scripts/check-volume-persistence.py +++ b/scripts/check-volume-persistence.py @@ -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(): diff --git a/scripts/copy_freeze/hu.json b/scripts/copy_freeze/hu.json index 3b88296..08c0f2f 100644 --- a/scripts/copy_freeze/hu.json +++ b/scripts/copy_freeze/hu.json @@ -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" } } diff --git a/scripts/test_check_volume_persistence.py b/scripts/test_check_volume_persistence.py index f05d6a7..ccd5eb9 100644 --- a/scripts/test_check_volume_persistence.py +++ b/scripts/test_check_volume_persistence.py @@ -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) diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index ecb5849..345a495 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -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() diff --git a/scripts/upgrade_fixtures_box.py b/scripts/upgrade_fixtures_box.py index 6207e54..9fcb21c 100644 --- a/scripts/upgrade_fixtures_box.py +++ b/scripts/upgrade_fixtures_box.py @@ -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/`. 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/ 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(), diff --git a/templates/grimmory/.felhom.yml b/templates/grimmory/.felhom.yml new file mode 100644 index 0000000..8bad1c5 --- /dev/null +++ b/templates/grimmory/.felhom.yml @@ -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} diff --git a/templates/grimmory/docker-compose.yml b/templates/grimmory/docker-compose.yml new file mode 100644 index 0000000..686e61f --- /dev/null +++ b/templates/grimmory/docker-compose.yml @@ -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: diff --git a/templates/metube/.felhom.yml b/templates/metube/.felhom.yml new file mode 100644 index 0000000..7d7b3a9 --- /dev/null +++ b/templates/metube/.felhom.yml @@ -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} diff --git a/templates/metube/docker-compose.yml b/templates/metube/docker-compose.yml new file mode 100644 index 0000000..b67d422 --- /dev/null +++ b/templates/metube/docker-compose.yml @@ -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