Files
felhom.eu/documentation/audits/new-apps-2026-10-10/FIT.md
T
admin 970616b72b
gates / gates (push) Failing after 11m41s
New apps 2026-10-10: Grocy and LubeLogger on the website; Monica stopped
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.
2026-10-10 12:34:05 +02:00

194 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.