# 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 `Bejelentkezés \| Grocy` 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**, `Garage - LubeLogger` | | `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 `-xff` reset; both get a line in their record saying why.