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

17 KiB
Raw Blame History

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.