970616b72b
gates / gates (push) Failing after 11m41s
The catalogue side is app-catalog-felhom.eu b7f0f7c. Here: the evidence, the website, the register and the operator's view. Website - Two cards in the Otthon & Eletmod / Home & Lifestyle section of BOTH apps pages, with assets: grocy-logo.svg is grocy's own icon with its single fill made white like the other logos, lubelogger-logo.png is the app's own icon with its dark background dropped and the mark made white (the rule SparkyFitness's PNG follows), and six screenshots of each app's own UI with a household's own data, taken headless on the bench from the published template. - The app count moved 56 -> 58 in 15 places per language set, both languages, and the open-source tile 49 -> 51. Checked by asking the same patterns for the new number afterwards. marketing/facebook/COPY.md still says 56 and is NOT changed: the post it carries is already scheduled. Evidence - documentation/audits/new-apps-2026-10-10/ — FIT.md (checklist group 0 for all three, with the Hungarian-UI column), the bench and box transcripts, the memory samples, the screenshots and the gate runs. Register: 137 -> 139 rows, 2 opened, 0 closed. - R-926 after a remove-keeping-backups and restore, nobody has checked what the app page shows for an after_install app's generated password. Measured here: LubeLogger is safe (its login is derived from the environment at every start, the new password signs in, the data is back); grocy's install password still signs in from the restored database but the deployed environment no longer carries ADMIN_PASSWORD at all. Seven apps are in the class. - R-927 Monica, stopped at checklist 0.2 with the measurements, waiting on the operator. Gate scripts: the same console trap in nineteen of them and in repo_gates.py itself, where it ABORTED THE WHOLE RUNNER at the first gate — printing a non-ASCII character on this workstation's cp1250 console raised UnicodeEncodeError before the gate had decided anything, and reuse_refs_check.py died while printing a NOTE. All now reconfigure their own streams. Red-proof that the remaining script-test failures are not mine: test_due_checks_gate.py fails the same 5 of 42 with the change reverted.
194 lines
17 KiB
Markdown
194 lines
17 KiB
Markdown
# FIT — checklist group 0 for Grocy, LubeLogger and Monica (2026-10-10)
|
||
|
||
Read and measured 2026-10-10 09:40–11:10 UTC. Sources: the GitHub API, raw upstream source at the pinned tag,
|
||
the Docker Hub / ghcr registry APIs, and **the images themselves started on bench LXC 9401** (demo-hp, Tier 0).
|
||
**[U]** upstream code/docs/registry · **[M]** measured on the bench, this session · **[I]** our inference.
|
||
Group 0 plans the later tests; it closes only what it measured.
|
||
|
||
Raw reads: `A/`. Bench transcripts: `bench/`.
|
||
|
||
**Where the test machines actually are** — a correction to the brief's reading: **both** 9202
|
||
(`demo-hp-scratch`) and the bench 9401 (`upgrade-harness`) live on **demo-hp**, not on felhom-pve.
|
||
felhom-pve carries only 9201 (`demo-felhom`). Measured with `pct list` on both hosts.
|
||
|
||
## The table
|
||
|
||
| app (project → image, and why that image) | licence (file read) | last release · 2026 | tags · arch | phone-home (switch) | internet at runtime | non-HTTP ports | clients / login route | DB + services | memory (rough) | **Hungarian UI** | first-admin class | verdict |
|
||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||
| **Grocy** (grocy/grocy → `lscr.io/linuxserver/grocy`). **There is no image published by grocy itself** — `grocy/grocy` on Docker Hub is 404 [M], and grocy's own README answers "How to run using Docker" with a single link to `hub.docker.com/r/linuxserver/grocy` [U]. So linuxserver **is** the upstream-recommended image, and the catalog already pins four others from the same publisher. | **MIT** — `LICENSE.md`, API `spdx_id: MIT` [U] | **v4.7.1, 2026-09-04** · 3 in 2026 (v4.6.0 03-06, v4.7.0 08-28, v4.7.1 09-04); 83 releases since 2017 | `4.7.1`, `version-v4.7.1`, `v4.7.1-ls343`, `latest` · **amd64 + arm64** [M]. linuxserver rebuilds weekly, so `4.7.1` is **re-pushed** (ls342 2026-09-27 → ls343 2026-10-04) | **none.** The only outbound hosts in the whole codebase are `world.openfoodfacts.org` (the barcode-lookup plugin, on the household's explicit action) and `releases.grocy.info` (demo-data images, only when `MODE` ≠ production) [M, repo-wide grep]. No update check, no analytics. Switch: `GROCY_STOCK_BARCODE_LOOKUP_PLUGIN=""` | **no** | **none** (80 in the image; 443 unused) | PWA only — no native app. Third-party phone apps use the REST API with the header **`GROCY-API-KEY`** (`app.php:102`) [M]; the iCal feed uses `?secret=` | **SQLite**, one container (`/config/data/grocy.db`); nginx + php-fpm 8.5 | **~30 MiB idle** [M] | **yes — ~92 %.** `localization/hu/strings.po` holds 734 strings, 62 untranslated (91.6 %); the login page rendered `<title>Bejelentkezés \| Grocy</title>` with `GROCY_DEFAULT_LOCALE=hu` [M] | **class 3** — `admin`/`admin` exists on a fresh start and signs in [M] | **build** |
|
||
| **LubeLogger** (hargata/lubelog → `ghcr.io/hargata/lubelogger`; upstream's own `docker-compose.yml` names exactly this image. `hargata/lubelogger` on Docker Hub carries the same tags — ghcr chosen because it is the one upstream writes down) | **MIT** — `LICENSE`, API `spdx_id: MIT` [U] | **v1.7.3, 2026-09-12** · **17 in 2026** (v1.6.1 02-26 … v1.7.3 09-12) | `v1.7.3`, `v1.7.2` … `latest`, `edge` · **amd64 + arm64** [M] | **none at start.** Three outbound fetches, each on a page the household opens: the sponsors list (`hargata.github.io`), the release check (`api.github.com`, behind a `checkForUpdate` request flag), and the language-pack download (`hargata.github.io`) [M]. Startup makes no request — the first log lines are the banner and `Now listening` [M] | **no** | **none** (8080). A SignalR **websocket** at `/api/ws` — websockets pass the tunnel (MeTube measured 101) | PWA; no official native app. API takes **HTTP Basic** or `x-api-key` / `?apiKey=` on `/api`, `/kiosk`, `/images`, `/documents`, `/temp` (`GetPathAllowAPIKeyAuth`) — Basic measured 200 right / 401 wrong [M] | **LiteDB**, one container (`/App/data/cartracker.db`). PostgreSQL is optional (`POSTGRES_CONNECTION`); **upstream's default compose runs LiteDB**, so per 1.2 that is what we run | **~50–61 MiB idle** [M] | **yes, after one click — ~72 % coverage.** Nothing ships in the image: `/App/data/translations/` is empty on a fresh start [M]. `hu_HU` exists in the project's directory and the household downloads it in Settings; the file holds 403 keys of which 400 are translated (99.3 %), but German's file holds 559 — so hu_HU covers **~72 %** of the largest key set and the rest falls back to English [M] | **class 1 once we set it** — see below | **build** |
|
||
| **Monica** (monicahq/monica → `monica`, a Docker Official Image) | **AGPL-3.0** — `LICENSE.md`, API `spdx_id: AGPL-3.0` [U] | **none since 2025-04-21**; the last *stable* release is **v4.1.2, 2024-05-04** — 29 months [U] | `4.1.2`, `5.0.0-beta.5`, each × `-apache`/`-fpm`/`-fpm-alpine` · 7 arches; the image is **re-pushed constantly** (2026-10-08) because Docker Official Images rebuild on base-image changes | not reached | not reached | not reached | not reached | MySQL/MariaDB or PostgreSQL + Redis [U] | not measured | not measured | not measured | **stop — 0.2.** The operator decides. |
|
||
|
||
## Monica — the stop, in full, because it is the one decision this run hands back
|
||
|
||
Checklist **0.2** asks for *a release in the last 6 months*. Monica has had **none in 17 months**, and no stable
|
||
release in **29 months**. Four independent reads, all 2026-10-10:
|
||
|
||
1. **Releases.** The newest release of any kind is `v5.0.0-beta.5`, **2025-04-21**, and it is a prerelease. The
|
||
newest stable is `v4.1.2`, **2024-05-04**. The tag list agrees: after `v4.1.2` there is nothing but betas.
|
||
2. **The stable branch is dead.** `4.x` — the branch the README sends you to for "the stable and current
|
||
version" — has its newest commit on **2024-05-04**.
|
||
3. **`main` is the beta**, and says so: *"This branch is in development. It's our beta version."*
|
||
4. **The commit history has a 13-month hole**: 2025-08-30, then nothing until **2026-09-24**, when two commits
|
||
landed. One of them is the telling one — it adds a login-page notice reading *"As we are about to deploy a
|
||
brand new version of Monica, this instance and all its data will be deleted at the end of December 2026"*
|
||
(PR #7978, merged 2026-09-24). The notice is `APP_SHOW_DELETION_NOTICE`, **off by default**, so it would
|
||
never show on a household's box; what it tells us is about the project, not the switch: the author is
|
||
preparing a rewrite and is winding the hosted service down.
|
||
|
||
So the brief's third anticipated risk is not only real, it is worse than "releases slowly": **nothing has been
|
||
released for a year and a half, the stable branch has not moved in two and a half years, and upstream has
|
||
announced a replacement with no date.** 790 issues are open.
|
||
|
||
**What the operator decides.** Keeping Monica out costs the catalog nothing it has today — no app covers a
|
||
family address book, and none did yesterday either. Letting it in means publishing an app whose next version is
|
||
a rewrite with no migration story, on a household's box, with our name on the restore promise. The honest
|
||
recommendation is **keep it out and look again when the rewrite ships**; `lifecycle:` exists precisely so an app
|
||
can be added later without disturbing anything. Radicale already carries contacts over CardDAV for households
|
||
that only want the address book itself.
|
||
|
||
## The three risks the brief named, measured
|
||
|
||
### 1. Grocy starts with `admin` / `admin` — **true**, and the replacement route is proven
|
||
|
||
Upstream says it in its own README: *"Default login is user `admin` with password `admin`, please change the
|
||
password immediately"*. Measured on the bench, fresh volume:
|
||
|
||
- the `users` table holds exactly one row, `id=1 admin`, password hashed `$argon2id$v=19$m=65536,t=4,p=1`;
|
||
- `POST /login` with `username=admin&password_base64=YWRtaW4=` → **302 to `/`**, and the cookie returns a 200
|
||
stock overview. So the default really is `admin`/`admin`;
|
||
- a stranger with no cookie gets **302 → `/login`** on every page and **401** on `/api/system/info`. There is
|
||
**no open first-run screen** — grocy is first-admin **class 3**, the bookstack / mealie / calibre-web shape,
|
||
not a `setup_gate` app.
|
||
|
||
**How the install replaces it (checklist 3.2).** Grocy ships no CLI, no admin env var and no headless setup. It
|
||
does ship the hashing primitive it uses itself: `UsersService` calls `password_hash($password, PASSWORD_ARGON2ID)`,
|
||
and the image's PHP 8.5.6 reports `password_algos() = [2y, argon2i, argon2id]` with `pdo_sqlite` present [M]. So
|
||
the `after_install` command is a `php -r` that takes **the password as its own argument** (`$argv[1]` — measured:
|
||
with `php -r 'code' a b`, `$argv[1]` is `a`), hashes it with grocy's own algorithm, writes it with a prepared
|
||
statement, and then **verifies with `password_verify` before printing its success marker**. Proven on the bench:
|
||
|
||
| after the command ran | result |
|
||
|---|---|
|
||
| `admin` / `admin` | **302 → `/login?invalid=true`** (refused) |
|
||
| `admin` / *generated* | **302 → `/`** (signed in) |
|
||
| `admin` / a wrong password | **302 → `/login?invalid=true`** (refused) |
|
||
|
||
**Lock-out (3.6), measured:** 25 wrong passwords in a row for `admin`, then the right one — **in at once**. Grocy
|
||
has no lock and no throttle. So the *name* need not be generated (the calibre-web reason, R-752, does not apply
|
||
here) and no family member can be locked out by a stranger.
|
||
|
||
**Barcode scanning.** Upstream: camera scanning is ZXing, *"totally offline / client-side camera stream
|
||
processing"*, and *"due to browser security restrictions, this only works when serving Grocy via a secure
|
||
connection (`https://`)"*. Every Felhom app is served `https://` through the tunnel, so it works — and the
|
||
scanning itself sends nothing anywhere. The *lookup* of an unknown barcode is the separate, explicit "External
|
||
barcode lookup" workflow, and that one does reach `world.openfoodfacts.org` with the barcode number.
|
||
|
||
**Overlap with Mealie / Tandoor.** Grocy has recipes and a meal plan too, so the overlap is real but narrow:
|
||
Mealie and Tandoor are recipe libraries (import a recipe, scale it, cook from it), grocy is a stock book (what
|
||
is in the house, what runs out, what to buy, whose turn the chores are). Grocy's recipes exist to subtract
|
||
ingredients from stock. One line goes in `use_cases`.
|
||
|
||
### 2. LubeLogger may have no login at all — **true, and worse than "may"**
|
||
|
||
`Middleware/Authen.cs` reads `bool.Parse(configuration["EnableAuth"] ?? "false")`, and `appsettings.json` ships
|
||
`"EnableAuth": false`. When it is false the middleware **mints a ticket** for every request with
|
||
`ClaimTypes.Name = "admin"` and the role `IsRootUser`. Measured on a default start, as an anonymous stranger:
|
||
|
||
| request | result |
|
||
|---|---|
|
||
| `GET /` | **200**, `<title>Garage - LubeLogger</title>` |
|
||
| `GET /api/vehicles` | **200** |
|
||
| `GET /Home/Settings` | **200** |
|
||
| `POST /Vehicle/SaveVehicle` | **200 `{"success":true}`** — and the vehicle came back from `/api/vehicles` |
|
||
|
||
So on a default install anyone who finds the subdomain reads and **writes** the family's car records as root.
|
||
|
||
**The route that turns it on, and it is better than an `after_install`.** The root login is not a database row:
|
||
`AuthenticateRootUser` compares the submitted name and password, each **SHA-256 hex** (`StaticHelper.GetHash`),
|
||
against the config keys `UserNameHash` and `UserPasswordHash`. ASP.NET's configuration includes environment
|
||
variables, so all three keys can be set at deploy time — which means **auth is on before the app's first byte**,
|
||
with no install window to measure. Measured from the container's creation, polled every second:
|
||
|
||
```
|
||
t+1s /api/vehicles=000 (not listening yet) /=000
|
||
t+2s /api/vehicles=401 /=302 → /Login/Index
|
||
t+3s /api/vehicles=401 /=302 → /Login/Index
|
||
```
|
||
|
||
and then, with auth on:
|
||
|
||
| request | result |
|
||
|---|---|
|
||
| stranger `GET /`, `GET /Home/Settings`, `POST /Vehicle/SaveVehicle` | **302 → `/Login/Index`** |
|
||
| stranger `GET /api/vehicles` | **401** |
|
||
| household's generated login, web form | **`{"success":true}`**, and `/api/vehicles` 200 with the cookie |
|
||
| household's login, wrong password | **`{"success":false,"message":"Invalid Login Credentials"}`** |
|
||
| API Basic auth, right / wrong | **200 / 401** |
|
||
|
||
So the verdict is **build**, and LubeLogger becomes first-admin **class 1** — generated by us at install.
|
||
|
||
Two more things that had to be checked before trusting the env route, both measured:
|
||
|
||
- **No config file undoes it.** `Program.cs` adds `data/config/userConfig.json` to the configuration *after*
|
||
the environment, so a file holding `EnableAuth: false` would win. On a fresh start that file **is not
|
||
written** (`/App/data/config/` was empty). And when it is later written, `SaveUserConfig` deliberately
|
||
re-reads those three keys from the merged configuration rather than from the posted form — so the household
|
||
cannot turn auth off from the UI while the env sets it.
|
||
- **Sign-up is closed by the app itself.** `RegisterNewUser` refuses without a token minted by the root user:
|
||
`POST /Login/Register` answered **`{"success":false,"message":"Invalid Token"}`**. Nothing for `signup_block`
|
||
to do.
|
||
|
||
**Lock-out (3.6), measured:** 25 wrong passwords, then the right one — **in at once**. No lock, no throttle.
|
||
Same reading as grocy, and the same conclusion: a stranger cannot lock the family out.
|
||
|
||
**Database (1.2).** LiteDB, which is upstream's default compose. One container, no engine, so no MariaDB /
|
||
PostgreSQL major ever to cross — the whole engine-major rule is `n/a` for this app by construction.
|
||
|
||
**File uploads (2.2).** `InitMessage` creates `data/images`, `data/documents`, `data/temp`, `data/themes`,
|
||
`data/translations` beside `data/cartracker.db`. Receipts and documents are **household content** → the
|
||
`${USERDATA_PATH}` side; the database and the DataProtection keys are appdata.
|
||
|
||
**One thing the image lacks, and it changes the healthcheck.** `ghcr.io/hargata/lubelogger:v1.7.3` has **no
|
||
curl, no wget, no nc, no python, no node** [M] — none of the four healthcheck families in `REUSE.md` §2 exists
|
||
in it. It does have **bash**, and `/dev/tcp` works there (measured: `Connection refused` against a dead port,
|
||
which is the mechanism answering). So LubeLogger needs a **fifth family**, a bash `/dev/tcp` probe, and that is
|
||
a catalog-wide convention — it goes into `REUSE.md` in the same commit.
|
||
|
||
### 3. Monica's release state — see above. The brief said "releases slowly"; the measurement says "stopped".
|
||
|
||
## Claims in this brief that turned out wrong, or need sharpening
|
||
|
||
1. **"Grocy starts with the login `admin` / `admin` (as far as we know)"** — right, and now measured rather
|
||
than assumed, on a fresh volume and through the login route.
|
||
2. **"LubeLogger *may* have no login at all until someone turns it on in its settings"** — right about the
|
||
default, wrong about "settings": the switch is a configuration key readable from the **environment**, which
|
||
is why the fix needs no post-install step and leaves no window. The brief's worry that "a stranger on the
|
||
internet sees the family's car data" understates it: a stranger also **writes** it, as root.
|
||
3. **"Monica releases slowly"** — too kind. No release in 17 months, no stable release in 29, the stable branch
|
||
untouched since 2024-05-04, and an upstream notice that the hosted instance and its data go away at the end
|
||
of December 2026 ahead of a rewrite.
|
||
4. **"the test machines"** — 9202 and the bench 9401 are both on **demo-hp**. felhom-pve has only 9201.
|
||
5. **Grocy's image.** The brief asks "upstream recommends which?" — the answer is unambiguous and worth writing
|
||
down: upstream publishes **no** image of its own and points at linuxserver.io.
|
||
6. **A near miss worth recording.** My first pass at grocy's Hungarian coverage reported **0 % translated** — a
|
||
broken `.po` parser that mishandled multi-line `msgstr`. The German control (which must be ~100 %) read 0 %
|
||
too, which is what caught it; a second, independent counting method gave German 0 untranslated, French 12,
|
||
**Hungarian 62 of 734 → 91.6 %**, with real Hungarian strings in the samples. Without the control this table
|
||
would have carried a false "no" in its most load-bearing new column.
|
||
|
||
## What group 0 did NOT close
|
||
|
||
- Memory from birth with swap off (5.1), the 10-minute soak (5.2), backup/restore through the product (2.5),
|
||
"remove with data" (2.6), the upload read back through the front door (2.8), the cold-start health timing
|
||
(4.3), the negative health control (4.4), the ladder step (6.2) and the forced-fail undo (6.3) are all
|
||
Part-B work on the bench and on 9202. Nothing above closes them.
|
||
- The forged-`X-Forwarded-For` test (3.10) through the simulated tunnel is Part B. What is **read** so far:
|
||
**grocy reads no client address anywhere** (repo-wide grep for `REMOTE_ADDR` / `HTTP_X_FORWARDED_FOR` /
|
||
`getClientIp` — nothing), and **LubeLogger reads it in one place only**, `LoginController` ~L521: it takes the
|
||
real peer from `Connection.RemoteIpAddress` and then *appends* the whole `X-Forwarded-For` string to a
|
||
`LogWarning` for a failed login. Measured with a forged header, the log line read
|
||
`Failed Login for … from ::ffff:172.17.0.1, 1.2.3.4, 10.9.9.9` — the real peer first, the forged part visible
|
||
and **deciding nothing**. No trusted-proxy list, no `ForwardedHeaders` middleware, no lock and no rights keyed
|
||
on the address. So neither app needs the `<router>-xff` reset; both get a line in their record saying why.
|