11 Commits

Author SHA1 Message Date
admin 836d64efa3 Apps burndown (controller 0.321.0 released): R-1097, R-1102, R-1101, R-1095, R-1098 closed; R-1103 opened; build-hub.sh 1.25.0
gates / gates (push) Successful in 6m52s
build-hub.sh refuses an existing tag and an unanswered registry (test_build_hub_push_once.sh, red-proved).
Register 179 -> 175. R-1067 stays BLOCKED: Tester-2 reports 0.292.0. Design notes in 02 and 07; STATUS;
evidence audits/burndown-apps-2026-10-16/; REPORT-burndown-apps-2026-10-16.md.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 17:27:57 +02:00
admin 11fa13ee38 N100: the file manager's new password set at the operator's yes (controller 0.320.0); STATUS, diary, report
gates / gates (push) Successful in 7m4s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 16:21:54 +02:00
admin ea8e67f341 Logins and folders (controller 0.320.0 released): design, R-1100..R-1102 opened, STATUS, evidence, report
gates / gates (push) Successful in 7m4s
02 "Logins and folders" [DESIGN]: an infra stack is part of the box on the app page; one login answer per
installed app (catalog app_info.login_kind); FileBrowser's new-password press (its CLI, server stopped);
"Fájlok ↗" and the per-drive media folders, a folder with no source is text. Register 176 -> 179.
STATUS: look at FileBrowser, RomM and Plex on demo-hp; say yes for the N100's press; decision D10 (R-1102).

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 15:53:12 +02:00
admin bc9a871391 R-1085, R-1089, R-1090 closed; R-1096..R-1099 opened; STATUS: the N100 RomM check; report
gates / gates (push) Successful in 6m56s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 12:47:19 +02:00
admin 59834feadf app page tuning round 2: P0 cause (Calibre-Web installed before its password field), controller 0.319.0 released, 02 layout rules (style A, login panel, empty-value rule), R-1095 opened, STATUS
gates / gates (push) Successful in 6m54s
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-16 12:41:30 +02:00
admin f1e1c8a60c hub manifest: 0.155.0 (R-1085, operator yes 2026-10-16)
gates / gates (push) Successful in 6m36s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 12:36:39 +02:00
admin b17e9e993c hub v0.155.0: the floor can never deliver a withdrawn controller build (R-1085); RomM/Ghost evidence
gates / gates (push) Successful in 6m44s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 12:24:39 +02:00
admin 920c1aaf84 storage identity build B: live proof, R-1068/R-1079/R-1082/R-1084 closed, R-1093/R-1094 opened, D15, golden 0.318.0 record, backup page
gates / gates (push) Successful in 6m39s
A fresh one-disk install (512 GB VM, installer 1.33.0, agent 0.158.1 + golden 0.318.0 vouched): data volume 100 GiB, data
area 213 GiB registered as the default drive with no press, Jellyfin with no extra step, nothing of the area in a whole-box
copy; partitions formatted and used; the honest backup offer on a VM; /mnt/sys_drive never offered (no box had it).
16 §13 build B status; capability map D15; poster facts (drawing needs a refresh, STATUS); the backup page HU+EN now that
it is true; REPORT-storage-identity-buildB-2026-10-16.md.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-16 10:07:13 +02:00
admin 8b0a4413cd R-1088 and R-1086 closed (token rotated; D11 applied); R-1091 (1)(2) done (D12); STATUS decisions answered
gates / gates (push) Successful in 6m47s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 10:02:01 +02:00
admin ffacccee89 manifests: publish installer 1.33.0 (tag installer-v1.33.0; build B — the internal data place)
gates / gates (push) Successful in 6m33s
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-16 09:41:06 +02:00
admin f51dfe2046 R-1088: credentials restored by the operator; the owed demo-hp guide page check passed (Plex, Sonarr, HU+EN); rotation left open
gates / gates (push) Successful in 6m43s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 09:24:54 +02:00
175 changed files with 5368 additions and 53 deletions
+24
View File
@@ -10,3 +10,27 @@ The full report is `app-catalog-felhom.eu/REPORT.md`. This file is the felhom.eu
- **Rows opened:** R-1085, R-1086, R-1089, R-1090, R-1091, R-1092. R-1088 (another session's row) extended. None closed.
- **Code:** `scripts/read_credential.py` + its test — the same-file guard (R-1088), red-proved.
- **STATUS:** one „works" line, D11 and D12, and the „read the Plex and Sonarr guide" line.
## Morning of 2026-10-16 — the owed rendering check
- The operator restored `~/.config/credentials` from backup. CC checked the key names only (17 keys); no value read.
- **Rendering check ran, 07:20 UTC, through `https://felhom.enkisfelhom.hu` (demo-hp 9201, controller 0.317.0):**
login 302 + session; `/apps/plex/guide` and `/apps/sonarr/guide`, Hungarian and `?lang=en`. Each page: 4
`data-guide-section`, 1 FAQ block with 6 entries, `data-guide-checked` equal to the catalog. Every rendered string
(33 Plex, 35 Sonarr, per language) equals the catalog text on `main` (`ec864b5`); a changed line in memory was seen
to fail the compare. Evidence: `documentation/audits/app-guides-2026-10-15/render-check-2026-10-16.txt`.
- Read only: nothing installed, removed, restarted or set; logged out after. Teardown: provisioned nothing.
- **R-1088 updated, not closed:** restore and check done; the hub token rotation is still the operator's.
- STATUS: the „read the guides" line now says the robot check passed; the R-1088 line now asks for the rotation only.
## 2026-10-16 — the operator's answers applied
- **Token rotated (operator).** CC checked the token in the credentials file works on the hub: `/hosts` and
`/configuration` 200 with it, 302 without it. **R-1088 CLOSED.**
- **D11 → stop advising the change.** Catalog `5cc9e0e`: qBittorrent's first step now says keep the generated password;
qBittorrent and Transmission field descriptions no longer say „change it later". **R-1086 CLOSED.**
- **D12 → reword.** Catalog `5cc9e0e`: Cal.com names Apple Calendar or CalDAV, not Google/Outlook; BentoPDF says the
browser does the work and the file stays on the device. **R-1091 stays open** for its other items; (1) and (2) are
marked done.
- Register: 178 → 176 rows; 0 opened, 2 closed (moved to `CLOSED-ITEMS.md`). STATUS: D11, D12 and the token line removed.
- Gates: catalog `--fast` and 12 test files green; felhom.eu `repo_gates.py --fast` green before the push.
+38
View File
@@ -0,0 +1,38 @@
# REPORT — app page tuning, round 2 (2026-10-16)
Written in ASD-STE100 Simplified Technical English. The full technical report is felhom-controller `REPORT.md`
(v0.319.0). Evidence: `documentation/audits/app-page-tuning-2026-10-16/` (P0, before/, browser/, release/, tools/,
redproof.txt). Design read and updated: `architecture/02-controller-module-map.md` „The app page's layout".
## 1. Why the Calibre-Web password was empty (Part 0)
Calibre-Web on demo-hp was installed on 2026-08-21. Its template got the password field on 2026-09-29. The box adds new
generated values to installed apps, but never a password. So the box never had one. The user name the box shows is the
real one: a session renamed Calibre-Web's admin to it on 2026-10-01. Nothing was removed. No customer box has this state
(the hub lists only demo-hp and demo-felhom; demo-felhom has no Calibre-Web now — checked on the box).
## 2. What changed on the page
The header, the tab bar and every section are panels (style A). The open tab is filled and underlined. One
„Belépés az alkalmazásba" panel shows the name and the password side by side. A value the box does not have is a
sentence, not an eye. „Cím és kulcsok" is rows: no Domain input, no storage list. The install page uses the same panels;
its fields did not change. Checked in a real (headless) Chrome through `https://felhom.enkisfelhom.hu`.
Login pairs: calibre-web, kimai, lubelogger, nextcloud, paperless-ngx, radicale. 14 other apps show the password alone.
## 3. Release
Controller **0.319.0**: Tester 1, demo-hp and demo-felhom took it through their own floor at 10:34 UTC; box walk checks
green on all three; global floor 0.319.0 / MinAgent **0.158.1** (first saved with 0.157.0 by my mistake, corrected 65
seconds later; no box was affected — see the controller report §6). Per-customer floors cleared.
## 4. Register
Rows before 174, after 175; opened 1 (R-1095: `/apps/{slug}/install` for an installed app), closed 0.
## 5. Other session duties
- Instruction files: no edit.
- System poster: no fact in `felhom-system-poster.facts.md` changed.
- `unproven.py --summary`: NOT WALKED 35 of 55 — no number moved.
- Commits: no Co-Authored-By line (the brief's rule).
+82
View File
@@ -0,0 +1,82 @@
# REPORT — apps burndown: RomM saves, the false „shared password" line, FileBrowser, the Data tab, the install address, the build guards, the Pi key (2026-10-16)
Written in ASD-STE100 Simplified Technical English. Operator brief „apps burndown", daytime, operator present.
Design read first: `architecture/02-controller-module-map.md` „The app page" (now with the v0.321.0 notes),
`07-backup-architecture.md` (what an app backup holds — a fact added), `09-update-architecture.md` §0.
Evidence: `documentation/audits/burndown-apps-2026-10-16/`.
**The operator's word before the start:** no RomM saves need to be kept or copied on any box. So Part 1 has no one-time
copy, and no stop for the N100.
## 1. RomM saves (R-1097) — done
- **Measured first (demo-hp 9201):** RomM writes saves to `/romm/assets`; the image keeps `/romm` as an anonymous
volume; the compose mounted nothing there. Every RomM backup on the drive held only `romm_config`, `romm_db_data`,
`romm_redis_data` — no saves. In the code, the app backup takes only the named volumes the compose declares.
`P1/p1-measure.txt`.
- **Fix (catalog `326f846`):** a named volume `romm_assets:/romm/assets`, in the compose and both older step files. A
named volume is the catalog's normal place for app data (07 §6.2), every app backup takes it, and the restore brings it
back. New catalog gate `image-volumes` (a measured list of image data paths that must be mounted).
- **Survey:** no other running app on demo-hp keeps data in an anonymous volume (PrivateBin's are temp folders).
`P1/anon-volumes-demo-hp.txt`.
- **Proof, through the product's endpoints** (`tools/romm_saves.py`, `P1/romm-saves.txt`): sync → restart (the new
volume mounted) → a 32 KiB save file written into `/romm/assets` with `docker exec` → the by-day backup run (night
chain, 5 min 32 s; its update leg did nothing) wrote `romm_romm_assets.tar` → RomM removed, keeping backups (the volume
went) → restored from the backups page (77 s) → **the save back, byte-identical (sha256)**. The dummy file removed after.
- **Boxes:** the hub shows RomM only on demo-hp (the N100 had it once, on 2026-10-13). `P1/hub-romm-boxes.txt`.
## 2. Parts 2–7
| Part | Result |
|---|---|
| 2 — the false „shared password" line (R-1102, D10 = A) | **Done.** `default_creds` removed (hu + en) from the eight; copy freeze re-captured with the reason; a catalog test, red-proved. Live on demo-hp: the line is on none of their app pages or install pages (`live/d10-check.txt`). The user name „admin" stays in the first steps of grafana, gokapi, nzbget, transmission. |
| 3.1 — FileBrowser's stale password (R-1101) | **Done** (controller 0.321.0). The „change it" line is gone for FileBrowser; the page checks the stored password with FileBrowser first and, refused, says so and offers the new-password press. Only FileBrowser's own 401/403 counts. |
| 3.2 — FileBrowser's own banners | **Done.** `server.disableUpdateCheck` (`backend/common/settings/structs.go:51`, read at `backend/cmd/root.go:66`) and `frontend.disablePWAInstall` (`structs.go:294`, sent at `backend/http/static.go:300`) at the pinned tag, set in the rendered config. In the browser, logged in: no „update is available", no „Install app", `disablePWAInstall: true`, `updateAvailable: ""`. Control: the 2026-10-16 screenshot `logins-folders-2026-10-16/browser/cold-romm-2-after-login.png` shows the banner before. |
| 4 — the Data tab's backup part | **Done.** One section, rows, plain words; the value column in the UI font; the export eye is the text „Megjelenítés"/„Elrejtés" with `aria-label`. Changed keys: `tier2_config.a_2_mentes_egy_masik`, `.csak_db_konfiguracio`, `.jelenleg_csak_a_belso_ssd`, `.nincs_elerheto_off_drive_cel`, `.nincs_masik_adatmeghajto_automatikus_cel`, `.rendszerlemez.vedett`, `note.tier2.no_space_ssd`, `note.tier2.reason_no_second`. No eye icon exists in the dashboard's icon set, and adding one would change every page's fixture, so the button is the text the other reveal buttons use. |
| 5 — the install address (R-1095) | **Done** (302 to the settings tab, GET only). **The template clean-up is NOT done:** the installed-only branches of `app_fields.html` are pinned by the R-254/R-709 guard tests, which assert a reveal control on that path; removing them means retiring those tests — filed R-1103. |
| 6 — the build guards (R-1098) | **Done.** `build-hub.sh` 1.25.0 refuses an existing tag and an unanswered registry (6 cases, red-proved). The agent's `release-agent.sh` had the check but let an unreachable registry through; now only a clean 404 proceeds (5 cases, red-proved). Script changes only: **no hub image, no hub deploy, no stop needed.** |
| 7 — the Raspberry Pi key (R-1067) | **Not done, as the brief says:** Tester-2 still reports controller **0.292.0** (newest report 2026-10-04). Its name is in the row. `P7-pi-fleet.txt`. |
## 3. Browser check (headless Chrome through `https://felhom.enkisfelhom.hu`) — `browser/`
`tools/cdp_burndown.py`. Calibre-Web's Data tab: one backup section, rows nightly/second/target, no off-drive/PBS, value
font Plus Jakarta Sans, the export toggle with its label. FileBrowser's page: the reveal, no „change it" line.
`/apps/calibre-web/install` landed on `/apps/calibre-web/settings`. FileBrowser itself: no banners. No console error; no
page wider than the window.
## 4. Versions and commits
- **felhom-controller** `c02c9d8` → `7e791d9` → docs. `0.321.0-rc.1` (demo-hp), release **0.321.0**, MinAgent 0.158.1.
Tester 1, demo-hp and the N100 took it through per-box floors; nothing unhealthy, restarting, exited or created; the
three dashboards 200; https-only 35 names OK; each box's FileBrowser config carries both switches. **Global floor
0.321.0 / MinAgent 0.158.1**, the per-box floors cleared (`release/`).
- **app-catalog-felhom.eu** `5630979` → `326f846` (RomM volume, gate `image-volumes`, D10) → docs.
- **felhom-agent** `3835224` → `dd8b2ac` (script and test only; no release).
- **felhom.eu** `11fa13ee` → this commit (`build-hub.sh` 1.25.0, its test, the register, design notes, STATUS, evidence).
- **Fixtures replaced (13):** `app_data_deployed`, `app_data_export`, `app_data_files_media`, `app_data_not_hdd`,
`_not_hdd_absent`, `_not_hdd_protected`, `_not_hdd_same_disk`, `_notarget_disabled`, `_target_auto`, `_target_ssd`,
`app_settings_deployed`, `app_settings_operator_creds`, `app_settings_infra_filebrowser`. **New (1):**
`app_settings_infra_filebrowser_stale`. Every other fixture, the install page's included, byte-identical.
- **Red-proofs:** `redproof/` (controller 6 + 1 redo, catalog 2 gates + 1 test, hub build, agent).
## 5. Rows
**Before 179, after 175. Closed 5:** R-1097, R-1102, R-1101, R-1095, R-1098. **Opened 1:** R-1103. R-1067 stays
BLOCKED, now naming Tester-2.
**System poster:** no listed fact changed — none of the three was needed.
## 6. Observations
1. The install form keeps branches no page serves any more. FILED: R-1103
2. Reading the hub's database by copying its three files one by one gave a STALE floor (0.320.0, 30 s after 0.321.0 was set); one tar of the three files and the hub's own page both read 0.321.0. NOT-A-FINDING: an evidence technique, not a product defect; the memory recipe is corrected to one tar.
3. The by-day backup run (night chain) also runs the update leg and the off-site push. NOT-A-FINDING: it is the sanctioned trigger, documented as „the night chain, by day"; here the update leg did nothing (kimai held).
4. No live control exists for the „known, shared password" line any more: the eight apps were the only ones that showed it. NOT-A-FINDING: `known_login_test.go` is the mechanism control and passes.
## 7. Teardown (three layers)
- **Machine:** demo-hp 9201 — RomM removed and restored (it runs, healthy, with its new saves volume); the dummy save
file removed. FileBrowser recreated once on each box by the config change (the password survives: its database is in
its volume). Nothing else changed. The N100: only the normal release update.
- **Host:** nothing.
- **Hub:** the three per-box floors set and cleared; the global floor raised to 0.321.0 (the release).
+128
View File
@@ -0,0 +1,128 @@
# REPORT — every app says where its login is · FileBrowser shows its password again · a „Fájlok" button (2026-10-16)
Written in ASD-STE100 Simplified Technical English. Task: „logins and folders" (operator brief, daytime).
Design read first: `architecture/02-controller-module-map.md` „The app page" (now has „Logins and folders", `[DESIGN]`),
`15-media-library.md` §2 and §11, `CLOSED-ITEMS.md` R-513 and R-75, `app-catalog-felhom.eu/FIRST-ADMIN.md`.
Evidence: `documentation/audits/logins-folders-2026-10-16/` (P0, P1b, live, browser, tools).
## 0. Decisions I took (inside the brief; each reversible)
1. **Media folders get no backup sentence.** The brief said „the class-driven backup sentence where a class exists". The
only class there is `excluded`, and its sentence says „this folder is temporary and not backed up". That is false for
the family's films. The class means „not this app's data". So: no sentence (the rule `consequenceFor` already follows
when it cannot be sure).
2. **An infra app has no data tab.** Export, move and the per-app backup are not the household's for a part of the box.
`/apps/filebrowser/data` answers 302 to the page.
3. **A sixth wording inside answer „none".** MeTube and Glance have no login of their own, but the family gate asks for
the family name. „No sign-in needed" is false there. With `family_gate`, „none" says: you sign in with your family
name and password. No new catalog value.
4. **The reset uses FileBrowser's own CLI, not its environment variable** (the variable re-applies at every start).
5. **traefik, cloudflared and samba get no login panel** — nobody signs in to them.
## 1. Part 0 — why FileBrowser showed „Telepítés" and lost its password
- FileBrowser is part of the box. It has no `app.yaml`, so the box marks it „not deployed" (and „protected").
- Every page counted `deployed OR protected` as „on the box". The new app page (v0.315.0, commit `e66d466`) counted
`deployed` only. So it showed the not-installed view: an Install button, no tabs, and no login. The password data was
still there, but only the settings tab shows it, and a not-installed app has no settings tab.
- Worse, found here: the Install button led to a deploy that had **no guard** for a protected stack. A press could have
installed the infra stack as an app. Now refused twice (the deploy API and the deploy itself).
- **State on both boxes:** `operator` on demo-hp (since 2026-09-15) and on the N100 (since 2026-09-16). Nobody knows
either password.
- **Sources:** each box has one drive source (`NVME 1TB` on demo-hp, `Külső tárhely (hdd_1)` on the N100), `Beolvasás`
and `Megőrzött adatok`. The drive source is the drive's `userdata/`, so every `media/<folder>` is reachable.
## 2. What a household sees now (controller 0.320.0)
- **FileBrowser** is part of the box: running state, „Megnyitás ↗" to `files.<domain>`, Restart only, no Install, no
Remove, a two-line guide. Its login: user `admin` and „Megjelenítés". If the box does not know the password: the
sentence „Ezt a jelszót te állítottad be a Fájlkezelőben; a doboz nem ismeri." and the button „Új jelszó
beállítása" (confirm: „A régi jelszó nem fog működni.").
- **Every installed app** has „Belépés az alkalmazásba" with exactly one answer:
- the box made it → „Megjelenítés" (calibre-web, kimai, nextcloud… — every app with a `type: password` field);
- the app wrote it → „Megjelenítés" (crafty-controller; FileBrowser's is this answer too);
- the box never had one → the v0.319.0 sentence;
- first-visit admin → „Az admin fiókot az első megnyitáskor te hozod létre…" (the default: every other app);
- e-mail → rallly, outline; no login → bentopdf, homepage, privatebin, onlyoffice; family sign-in → glance, metube.
- **„Fájlok ↗"** in the header: RomM → `roms`, Calibre-Web → `media/books`, Plex → the first drive's `media`. Immich →
no button (its data is in a named volume). The data tab lists each drive's films and series for Plex, Jellyfin and
Emby, series for Sonarr, films for Radarr, the download folder for qBittorrent and Transmission. A folder on a drive
that is not there now is text, not a link. Under the list: „A Fájlkezelő a saját jelszavát kéri az első alkalommal…"
with a link to the FileBrowser page.
## 3. Part 1b — the „new password" button
- **Spike (demo-hp only):** FileBrowser 1.5.6's CLI `set -u admin,<pw>` (`backend/cmd/cli.go:206-269`) sets the
password with no old one. The server must be stopped first: the CLI opens the database with no lock timeout
(`backend/database/storage/storage.go:26`). Measured: unknown → K1 200; K1 → K2: K2 200, K1 401, admin/admin 401; a
test user, a test share and the settings unchanged; cleaned up. `P1b/`.
- **Built and pressed on demo-hp through the endpoint the button calls** (`live/fb-reset-press.txt`,
`live/fb-reset-verify.txt`): 200 in 3.4 s; the page then offers „Megjelenítés"; the reveal returns a 16-character
password; a second press is refused (409, the box already knows it). Checked from a second channel, FileBrowser's
own login inside the box: new 200, the old 401, admin/admin 401; users, shares and settings unchanged; state
`generated`. The controller log names the act and never the value.
- **The N100: pressed at the operator's yes in chat** (14:19 UTC), after it took 0.320.0 by the normal update path: 200 in
3.5 s; inside the box the new password 200, admin/admin 401, state `generated` (`live/n100-fb-reset-*.txt`). The
operator looked at the pages on demo-hp and confirmed the FileBrowser login works.
## 4. Browser check (headless Chrome through `https://felhom.enkisfelhom.hu`, `browser/`)
Method: `tools/cdp_logins.py` (the last session's CDP client, extended). FileBrowser, RomM, Calibre-Web and Plex pages
and data tabs; screenshots for each. **The cold „Fájlok" click**, with every cookie cleared first: RomM, Calibre-Web
and Plex each went to `files…/login?redirect=<the folder>`; after FileBrowser's login each landed ON the folder
(`/files/NVME%201TB/roms`, `…/media/books`, `…/media`). No console error on any page, except the guide tab's missing
screenshot for FileBrowser (404; the section hides itself, as for every app without screenshots). No page wider than
the window. The FileBrowser line's link was dark blue in rc.1 — fixed in rc.2 / the release.
To be able to show RomM, RomM was installed fresh on demo-hp (its old data kept aside by the product's own „fresh"
choice). It stays for the operator's look.
## 5. Version, commits, fixtures, rows
- **felhom-controller** `39bf562` → `f30a808` (the work) → `1296e7a` (the link style) → docs. Test images
`0.320.0-rc.1`, `rc.2` (demo-hp only); release **0.320.0**, MinAgent 0.158.1.
- **app-catalog-felhom.eu** `806d2d0` → `5630979` (`login_kind` on 8 apps, gate `login-kind`).
- **felhom.eu** `bc9a8713` → this commit (design, register, STATUS, evidence).
- **Fixtures replaced (25):** the 24 `app_settings_*` fixtures and `app_data_deployed` (the login panel; the FileBrowser
line). **New (6):** `app_settings_infra_filebrowser`, `app_guide_infra_filebrowser`, `app_settings_login_email`,
`app_settings_login_none`, `app_settings_login_family`, `app_data_files_media`. Every other fixture, the install page
included, byte-identical.
- **Tests, each red-proved** (the mutation and its failure: `audits/logins-folders-2026-10-16/redproof.txt`):
`TestAppPage_InfraAppHasNoInstall`, `_EveryInstalledAppHasOneLoginAnswer`, `_FilesButtonTarget`,
`_UnknownSourceIsTextNotLink`, `TestFileBrowserReset_SetsVerifiesAndStoresEncrypted`, `_FailedVerifyKeepsOperator`,
`_OnlyInOperatorState`, `TestDeployStack_RefusesProtectedStack`; catalog gate `login-kind` (decoys).
- **Register: before 176, after 179, opened 3 (R-1100, R-1101, R-1102), closed 0.**
**Release (`09` §3 decision 250, `.claude/rules/unprompted-work.md` item 6):** per-box floors 0.320.0 for Tester 1,
demo-hp and the N100; each took 0.320.0 through its normal update path (the N100 and Tester 1 within minutes, demo-hp
at its next 15-minute report). Box checks green on all three (`release/containers.txt`: nothing unhealthy, restarting,
exited or created; `dashboards.txt`: 200 ×3; `https-only.txt`: 35 names → 301, the negative control convicted). Then
the **global floor 0.320.0 / MinAgent 0.158.1** with CC's token (303 `floor_set`), read back from the hub DB with its
WAL (`release/hubdb-floors-after.txt`), and the three per-box floors cleared. On the N100, read only: the FileBrowser
page shows the „operator" sentence and the button (`live/n100-readonly-0.320.0.txt`).
**System poster:** no listed fact changed (no machine, role, path, tier, time, retention or key) — none of the three
was needed. **`unproven.py --summary`:** not walked 35 of 55, unchanged.
## 6. Observations
1. One sign-in for the dashboard and FileBrowser. FILED: R-1100.
2. FileBrowser's stored password goes stale when changed in FileBrowser, and the page says to change it. FILED: R-1101.
3. Eight apps say „starts with a known, shared password" about a generated or typed login. FILED: R-1102 (decision D10).
4. The install button could deploy a protected stack. NOT-A-FINDING: small, fixed in this release with a test.
5. Plex gets the first-visit sentence, but its login is a plex.tv account. NOT-A-FINDING: what the sentence says is
true (the box does not know it; the household makes it at the claim).
6. The build tags every image `latest`, test images too. NOT-A-FINDING: nothing installs or offers `latest`
(searched the hub, the agent and the scripts).
7. The guide tab asks for screenshots FileBrowser does not have (404 in the console). NOT-A-FINDING: the section hides
itself, the same for every app without screenshots.
## 7. Teardown (three layers)
- **Machine:** demo-hp guest 9201 — the spike's files (`/root/fbspike`: test passwords as files, the database backup)
removed; the spike's test user and share deleted. RomM installed fresh and **kept** (for the operator's look).
FileBrowser's password on demo-hp is now the box's own (`generated`), shown by „Megjelenítés". The N100: nothing
changed but the normal release update.
- **Host:** demo-hp `/root/fbspike-tmp` and the pushed password files removed. Nothing else.
- **Hub:** the three per-box floors set and cleared; the global floor raised to 0.320.0 / MinAgent 0.158.1 (this is the
release, not a leftover).
+80
View File
@@ -0,0 +1,80 @@
# REPORT — RomM finds its games · Ghost does not lock the household out · the floor refuses withdrawn builds (2026-10-16)
Daytime, operator present. A second session works in this repo today, so this is a dated report, not `REPORT.md`.
Design read: `architecture/09-update-architecture.md` (§0, decisions 4 and 250), `05-hub-architecture.md` (floor
serving), `02-controller-module-map.md` (app e-mail), `07-backup-architecture.md` (RomM's optional legs).
Evidence: `documentation/audits/romm-ghost-2026-10-16/` (A1, A2, B, C1, C2, teardown).
## 1. RomM (R-1089) — measured, fixed, both scenarios pass
- **A1 (9201):** a dummy file where the first steps said (`roms/gba/`) → „No platforms found”, 0 games. One folder
deeper (`roms/roms/gba/`) → found. Cause: RomM 5.3.1 scans `library/roms/<platform>/`; the drive's `roms/` was
mounted as `library/`.
- **Fix (catalog `fb18471`):** `roms` → `library/roms`; new drive folder `roms-bios` → `library/bios` (operator ruling);
backup and folder labels for it; first steps corrected; stale `admin / admin` comment replaced.
- **Scenario A** (fresh install): the file at `roms/gba/` is found. **Pass.**
- **Scenario B** (old install, correction, restart): the old-steps file is found; the drive is unchanged byte for byte;
the data tab shows both folders backed up. **Pass.** A game uploaded through RomM under the old mapping becomes
„missing” and reappears under a fake platform „roms” (it sits in `roms/roms/<platform>/`).
- **BIOS:** a file in `roms-bios/psx/` is read as psx firmware.
- **Who runs RomM:** demo-felhom (N100) and demo-hp only (hub `/apps/romm`, read only). No customer box.
## 2. Withdrawn controller builds (R-1085) — refused at input and at serve time, live
- **Hub v0.155.0** (`b17e9e99`, deployed `f1e1c8a6` after the operator's yes). One list
(`hub/internal/store/spoiled_tags.go`: `0.315.0`, `0.316.3`). Both floor forms and the impact probe refuse a listed
tag; `ResolveManagedFloor` holds one that arrives any other way (stored value, override, config/env default), with its
own reason and one `[WARN]` per change. The Configuration and customer pages say „withdrawn".
- **The hand deploy:** a marked never-deploy line and a chained pre-check in the `felhom-build-deploy` skill; a test pins
that both equal the hub's list.
- **Tests:** store, forms, probe, both branches of both page notices, the real report handler, the skill parity.
**Ten red-proofs, each seen failing** (`B/hub-redproofs.txt`). Full hub `go build && go vet && go test`: green.
- **Live (`B/hub-live-check.txt`):** ArgoCD Synced/Healthy at `f1e1c8a6`, pod on `felhom-hub:0.155.0`. Impact probe for
`0.316.3` → `valid:false, spoiled:true`; control `0.319.0` → valid. **POST `0.316.3` to the global floor with CC's
token → 303 `flash=floor_spoiled`; the floor read back is still `0.319.0`**; the hub logged one `[WARN] … REFUSED`.
Boxes kept being served `0.319.0`.
## 3. Ghost (R-1090) — measured, fixed, both ways pass
- **C1:** Ghost 6.67.0 skips the e-mailed code only on the first-ever sign-in; the next new device got **500 „Failed to
send email”** — locked out.
- **Fix (catalog `fb18471`):** the device check is off by default and turned on with the mail settings by a new
`smtp_mapping` when app e-mail is on. One FAQ entry.
- **Scenario E, e-mail off:** a new device signs in with the password alone (201).
- **Scenario E, e-mail on:** 403 `2FA_NEW_DEVICE_DETECTED`; the shim relayed (hub 200); the mail arrived; the code from
it was accepted and the device signed in (200).
## 4. Versions, commits, teardown, register
- Catalog: `fb18471` (RomM + Ghost), `806d2d0` (report, box-walk fix). felhom.eu: `b17e9e99` (hub code, skill, `09`,
evidence), `f1e1c8a6` (manifest), and this commit (register, STATUS, report, last evidence). Hub v0.154.0 → **v0.155.0**.
Controller and agent: not touched.
- **CI by job id:** felhom.eu `b17e9e99` job 1934 success; catalog `fb18471` job 1931 success; the later pushes are
listed in the session's final note.
- **Teardown:** machine (9201) — RomM and Ghost removed, the drive's `roms/` as found, app e-mail back off, two empty
helper folders removed; kept on purpose: two `kept/romm/<date>/` folders (the product's „start fresh" result, 8 KB).
The pre-existing broken RomM was removed by operator ruling. Host — nothing. Hub — no record created; the hub
deploy is the only hub change.
- **Register:** **175 rows before, 176 after; 3 closed (R-1085, R-1089, R-1090), 4 opened (R-1096, R-1097, R-1098,
R-1099).** STATUS: one line for the operator (R-1099). System poster: no poster fact changed — none of the three
updates was needed.
## 5. Observations
- The standing RomM on 9201 crash-looped on Redis files damaged by the 2026-09-24 disk-full — FILED: R-1096.
- RomM keeps saves, states and screenshots in an anonymous volume (`/romm/assets`), outside the backup — FILED: R-1097.
- The hub build script does not refuse a version that already exists in the registry (the controller's does) —
FILED: R-1098. Checked by hand for 0.155.0 today (not found; control 0.154.0 found).
- The N100 may hold RomM uploads in the old place, now under a fake „roms" platform — FILED: R-1099 (operator, STATUS).
- The box walk made a drive folder for a value the caller passed — NOT-A-FINDING: small, fixed (catalog `806d2d0`).
- A drill fixture folder (`DRILL-2026-08-21`) on 9201's drive shows as a RomM platform — NOT-A-FINDING: another
session's test file on a test drive, left untouched.
- The task gave the new flash text in Hungarian and English — NOT-A-FINDING: the hub's operator pages are English only,
so only the English text was used, like every neighbouring flash.
- The task asked for a RomM FAQ line if a rescan is needed after the mount change — NOT-A-FINDING: a rescan does not
fix it (the files must move), and no customer runs RomM; R-1099 covers the one box that may.
- RomM's remove answered 502 while another session restarted 9201's controller; the remove had completed —
NOT-A-FINDING: the proxy's answer during a deliberate controller restart.
- 9201's controller moved `0.318.0` → `0.319.0-rc.1` during the test — NOT-A-FINDING: another session's release work.
`unproven.py --summary`: unchanged — 35 of 55 claims not walked.
@@ -0,0 +1,78 @@
# REPORT — storage identity, build B: the one-disk data area and partitions (2026-10-15 → 16)
Brief: „Storage identity, build B — the data area on one-disk boxes (D3, with the space check) and partitions as places
(D5)". Design: `documentation/architecture/16-storage-physical-identity.md` §5–§7, §9 (§13 = what was built).
Evidence: `documentation/audits/storage-identity-buildB-2026-10-15/` (README, SPACE-CHECK, live/). Method: endpoint level
(no browser on DooPlex) and the VM console for the Proxmox install.
## 1. The space check, as approved (operator, 2026-10-16)
| Disk | Thin pool | Data volume | Reserve | Data area |
|---|---|---|---|---|
| 256 GB | ≈ 140 GiB | 50 GiB | 16 GiB | ≈ 42 GiB |
| 512 GB | 349–383 GiB | 100 GiB | 35–38 GiB | ≈ 182–213 GiB |
| 1 TB | ≈ 800 GiB | 130 GiB | 48 GiB | ≈ 590 GiB |
| 2 TB | ≈ 1,720 GiB | 130 GiB | 48 GiB | ≈ 1,510 GiB |
Measured: the heaviest box uses 18 GB of its data volume; the whole catalog needs ≈ 74 GB; a whole-box copy's snapshot used
12 MB. A disk below ≈ 230 GB gets no data area (less than 20 GiB left).
## 2. A fresh one-disk install
Scratch VM on demo-hp, 512 GB disk, the public ISO, the published installer 1.33.0, customer Tester4: data volume **100 GiB**,
data area **213 GiB**, registered as the default drive by itself. **Jellyfin installed with no extra step** (domain and
subdomain only; on 0.314.0 the same request was refused for the missing media path). A whole-box copy holds nothing of the
data area.
## 3. Partitions — what the household can do now
- See each part of a free disk; format an empty part; use a part that has a filesystem; use the free part of a disk whose
other part is already a drive.
- Cannot: format a part while another part of the same disk is in use (the safety net refuses it — on purpose); create,
delete or resize a part (installer only, D5). An empty part still asks for the typed confirmation (R-1093).
## 4. Versions and what waits on the operator
| Component | Version | State |
|---|---|---|
| agent | 0.158.1 (0.158.0 superseded — security review) | vouched by the operator; on the three test boxes with bundle 0.158.1 (through the step bundle `0.158.1-step1`) |
| controller | 0.318.0 | released; the global floor 0.318.0 / MinAgent 0.158.1 (set by the operator) |
| installer | 1.33.0 | published (tag `installer-v1.33.0`, `manifests/webpage.yaml`) |
| golden | 0.318.0 | baked by CC (`documentation/tests/golden-0.318.0-2026-10-16/`), vouched by the operator |
Waits on the operator: discard the two unclaimed scratch registrations (ids 40, 41) and delete Tester4; Tester 1's dashboard
password in the credentials file does not open Tester 1 (the checks ran on the scratch box instead); the poster drawing.
## 5. Rows
Rows before: 178 · after: 176 · opened 2 (R-1093, R-1094) · closed 4 (R-1068, R-1079, R-1082, R-1084).
(Earlier in this session's build B part 1: R-1087, R-1088 opened; R-1088 was closed by another session.)
## 6. Claims in the brief that were wrong
1. „A one-disk scratch VM (200 GB)": under the approved sizes a 200 GB disk gets no data area (15 GiB < 20). Rebuilt at 512 GB.
2. „Format the empty part, use the other" works only in that order: since the security review, a part next to an in-use part
cannot be formatted.
3. „Run after build A is released": build A's agent was never vouched; the vouch needed a fresh golden (the hub refused
agent 0.158.1 next to golden 0.313.0) — CC baked golden 0.318.0 on the operator's word.
4. The installer's design text (installer creates, agent mounts) holds, but the size rule and the only mkfs live in ONE root
helper from the agent's bundle, run by both the installer and the signed job — so the installer makes the area only when
the vouched agent carries that helper.
## 7. Fixed without a row (security review of agent 0.158.0)
The root mkfs wrapper briefly trusted a sibling mount the agent itself could make; the data-area helper read a failed `lvs`
or `blkid` as „empty". Both fixed in 0.158.1, each test seen failing against 0.158.0; 0.158.0 marked „do not vouch".
## 8. Documents
`16` §7 (ruled sizes), §13 (build B status); capability map **D15** (PROVEN-LIVE) + detail; `OPEN-ITEMS` / `CLOSED-ITEMS`;
`STATUS`; the OMV guide's second-disk line; the backup page (HU + EN); **system poster: facts file updated, drawing needs a
refresh** (line added to STATUS).
## 9. Teardown
Machine: scratch VM 342 switched off the household way and destroyed; Tester 1's scratch disk removed. Host: helper scripts
removed. Hub: build A's three per-customer floors cleared; two unclaimed registrations and Tester4 left for the operator.
Secrets: the phrase, the dashboard password and the setup code lived only in private scratch files (shredded at the end);
none in any commit (scanned).
+10 -6
View File
@@ -2,10 +2,12 @@
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
**Updated 2026-10-15 (night).** Storage identity build A: agent 0.157.0 (not vouched yet), controller 0.317.0 and hub 0.154.0 — on the three test boxes only (per-box floors); the global floor stays 0.314.0 until you vouch the agent. Controller 0.317.0 also carries the app page in one column (a version number can now only be pushed once, and only from a clean tree); 0.314.0 is still the floor since 13:11 UTC — raised by CC with its own token after all three test boxes ran it (the first release with no golden; vouched golden 0.313.0); hub 0.153.0 (access tokens — CC uses its own token now, not your password). Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
**Updated 2026-10-16 (midday).** Controller 0.319.0 (the app page in panels, the login in one panel) is the floor, MinAgent 0.158.1 — `REPORT-app-page-tuning-2026-10-16.md`. **Earlier (morning):** Storage identity build B shipped: agent 0.158.1 and golden 0.318.0 vouched by you, controller 0.318.0 is the floor (MinAgent 0.158.1), installer 1.33.0 published — new one-disk boxes get their own data area. Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
`documentation/backlog/STATUS-HISTORY.md`. Reports: `REPORT-night-2026-10-14.md`, `REPORT-immich-rollout-2026-10-14.md`, `REPORT-seo-felho-tarhely-2026-10-14.md`, `REPORT-immich-repair-2026-10-14.md`, `REPORT-power-cut-fixes-2026-10-14.md`, `REPORT-night-power-2026-10-13.md`, `REPORT-poweroff.md`.
## What works
- **RomM game saves are in the backup now** (2026-10-16, catalog): a save survived a backup, a removal and a restore on demo-hp, byte for byte. Eight apps no longer claim „a known, shared password" (your D10 = A). If someone changes the file manager's password in the file manager, the box notices and offers a new one (controller 0.321.0).
- **Every installed app says where its login is, and has a „Fájlok ↗" button to its folder** (controller 0.320.0, 2026-10-16). The file manager is shown as part of the box again, with its password; on a box that does not know that password, one press sets a new one (done on demo-hp and, at your yes, on the N100).
- **Every app but three has a user guide and an FAQ, in Hungarian and English** (catalog, 2026-10-15 night): the „Útmutató” tab shows it on boxes with controller 0.315.0 or newer. Every button name in a guide was checked in the app's own files at the version we ship; a guide that could not be checked stays hidden (plant-it, home-assistant), and Emby has none (R-1092).
- **The N100 is the lived-in box** (since 2026-10-15): used like a household's box, changed only through the product. qBittorrent, Prowlarr, Sonarr and Radarr are installed and linked by the box; films never go off-site (checked before any download). Its diary: `documentation/lived-in/n100.md`.
- A restored app comes back exactly as safe as it was at the backup: closed to strangers until its first account is made (0.314.0, proven on the scratch box). A box on a test build now moves with the floor (Tester 1 did, by itself). The box claims Plex again (demo-hp's Plex claimed through the dashboard, 2026-10-15). A brand-new box from the public installer updated itself to the floor 3 minutes after it was linked (test box, since removed).
@@ -23,6 +25,7 @@
- Felhom runs in a virtual machine on OpenMediaVault 8, tried on an exact copy of the first outside tester's box: install, bind, claim, apps, backup, and back by itself after an OMV restart. The Hungarian guide for him is `documentation/pilot/GUIDE-omv-kvm-hu.md`; open: one-disk Jellyfin (R-1068), NAS films for Jellyfin (R-1071, yours), no disk health in a VM (R-1073, yours). Waits on you: send the guide; delete test customer Tester4 in the hub.
- A second local copy counts only when it is on a different physical disk, and the box can prove it. Inside a virtual machine the box makes the copy but says „A Felhom nem tudja ellenőrizni…" (Tester 1). On real disks nothing changes (demo-hp, N100). A NAS never hides the „only one copy" notice. A box with no drive refuses a media app in words, with a link to the Meghajtók page. Live on the three test boxes; the NAS and no-drive parts are unit-tested only. Next: build B (the internal place on one-disk boxes, R-1079). Your rulings D1–D6 are in the design (`architecture/16-storage-physical-identity.md`).
- **One-disk boxes get their own data area** (2026-10-16): a new install makes a „Belső tárhely" on the disk, outside every whole-box copy, and media apps install there with no extra step (proven on a fresh 512 GB box: 213 GiB area, Jellyfin with no extra step). A disk split into parts can be used part by part. The backup offer no longer promises disk-failure safety when it cannot check the disk, and `/mnt/sys_drive` is no longer offered.
## What is broken
- Several root-level weaknesses wait on your four decisions below (R-954, R-971, R-982, R-985).
@@ -47,13 +50,14 @@
| D7 | R-990, R-989 | Rule yes / no / later on the forum ideas (`audits/forum-research-2026-10-12/FELHOM.md` §5) and the selfh.st ideas (`audits/selfhst-survey-2026-10-12/FELHOM.md` §6) | — | — | nothing changes; the research ages | — |
| D8 | R-206 | Install DooPlex's Docker settings from git (restarts Docker), and prune the build cache weekly: yes or no? | **Yes, in a quiet window** | — | a rebuild of DooPlex still has the settings in git | — |
| D9 | R-1062 | The N100's USB drive is also its whole-box backup drive. Split it in two anyway? | **No — keep one drive**; the lived-in box keeps its nightly whole-box backup | Split: the box loses its local whole-box backup (or add a second drive first) | it stays one drive; multi-drive tests stay on Tester 1 | — |
| D11 | R-1086 | qBittorrent's first steps say „set your own password”, but then the box can no longer link it to Sonarr and Radarr. Stop advising the change, or let the box ask for the new password? | **Stop advising it** — the generated password is long and shown on the dashboard | The box asks: a new card and controller work | a household that follows the advice loses its automatic downloads without being told | — |
| D12 | R-1091 (1), (2) | Cal.com promises Google/Outlook calendar sync that is not set up, and BentoPDF says files are processed „on the server” (they are processed in the browser). Reword both to what is true? | **Yes, reword** | Keep: two promises that are not true | the app pages keep saying it | — |
## Waiting on you — do
- **Olvasd el a Plex és a Sonarr útmutatót a demo-hp-n** (az alkalmazás oldalán az „Útmutató” fül). Ha más formát szeretnél, egy menetben átírom mind a 62-t. Ha nem nézed meg: a szöveg ellenőrzött, de ember még nem olvasta a képernyőn — a robot a DooPlexen nem tud bejelentkezni, mert a jelszófájl üres (R-1088).
- **Rotate CC's hub token and restore CC's credentials file (R-1088).** A session overwrote the file with the token at 22:03; all other keys are gone, and the token was printed once into a transcript. If not: CC cannot log in to any box dashboard, and a printed token stays valid.
- **Vouch agent 0.157.0 and its config bundle** (hub → Configuration → Artifacts). Then CC raises the global floor to controller 0.317.0 with MinAgent 0.157.0 and clears the three per-box floors. If not: households stay on 0.314.0 and keep counting a same-disk copy as a second copy; the three test boxes keep their own floor.
- **Nézd meg a demo-hp-n a Calibre-Web „Adatok és mentés" fülét és a Fájlkezelőt** (`felhom.enkisfelhom.hu`, controller 0.321.0, minden dobozon kiadva). A mentés egy részben, egyszerű szavakkal; a Fájlkezelő nem kér frissítést és nem ajánl „Install app"-ot.
- **Look at `roms/roms/` on the N100's drive in the file manager (R-1099).** RomM now scans `roms/<platform>/`. A game uploaded through RomM before today sits one folder deeper: if there are platform folders in `roms/roms/`, move them up into `roms/` and press „Scan” in RomM. If not: those games show under a platform called „roms”.
- **Nézd meg a Calibre-Web oldalát a demo-hp-n** (`felhom.enkisfelhom.hu`, controller 0.319.0, minden dobozon kiadva). A fejléc, a fülsor és minden rész most külön panel (az „A” stílus). A belépési adatok egy panelben vannak, egymás mellett. A Calibre-Web jelszaváról a doboz nem tud, mert az alkalmazás előbb települt, mint hogy a sablonban jelszó lett volna — ezt most egy mondat mondja ki. Ha nem tetszik, írd meg, mit változtassak. Ha nem nézed meg: semmi nem romlik el.
- **Discard two unclaimed boxes and delete Tester4** (hub, browser only): the build B scratch VMs registered as unclaimed boxes — registration ids 40 (`b43afa10…`) and 41 (`9c78ea30…`); both VMs are destroyed. Tester4 (the throwaway customer) is switched off the household way. If not: two dead entries stay in the Hosts list and Tester4 stays in Customers.
- System poster needs a refresh: one-disk boxes now have a data area („Belső tárhely") on the system disk, outside the whole-guest backups (facts file updated 2026-10-16).
- **Olvasd el a Plex és a Sonarr útmutatót a demo-hp-n** (az alkalmazás oldalán az „Útmutató” fül). Ha más formát szeretnél, egy menetben átírom mind a 62-t. A robot 2026-10-16-án bejelentkezve megnézte: mindkét oldal megjelenik magyarul és angolul, 4 rész és a GYIK, minden sor egyezik a katalógussal. Ha nem nézed meg: csak a forma marad emberi szemmel nem látott.
- **Nézd meg az AdventureLog és a Plex oldalát a demo-hp-n, a rendes címén** (`felhom.enkisfelhom.hu`). A túl nagy keret javítva: a tartalom most 32 képpontra van a bal oszloptól és 36-ra a tetejétől (előtte 80 és 64 volt). Az alkalmazás oldala mostantól egy hasábban van: egy szélesség, vékony vonalak a szakaszok között, aláhúzott fül, és doboz csak ott, ahol az alkalmazás vár rád (kék) vagy ahol valami kockázatos (sárga). A „sikeres frissítés" sáv kis szürke sorrá vált a név alatt. Ha nem nézed meg: a felület él, csak senki nem látta még böngészőben.
- **Nézd meg az új alkalmazás oldalt a demo-hp-n (egy telepített és egy nem telepített app).** Egy alkalmazás mostantól egy oldal négy füllel (Beállítások · Adatok és mentés · Útmutató · Napló), az Indítás/Újraindítás/Leállítás a „Működés" kártyán van, és a „Vissza" az alkalmazáshoz visz, nem a listához. Mondd meg, jó-e így az elrendezés. Ha nem nézed meg: a felület él, csak senki nem látta még böngészőben — a robot a DooPlexen nem tud kattintani.
- **The N100 sheet (about 30 minutes):** `documentation/audits/n100-lived-in-2026-10-15/SHEET.md` — install Plex and claim it, make the admins in Prowlarr/Sonarr/Radarr, add your download sources in Prowlarr, add one show and one film. Then say „done". If not: the media apps wait, safe behind their gate.
@@ -180,6 +180,7 @@
| <a id="d12"></a>[D12](capability-map-detail/D.md#d12) | Boot ordering: automount + networking survive reboot; appliance self-heal watchdog | agent v0.85 | **PROVEN-LIVE** | — | `audits/CAMPAIGN-4-2026-07-13.md` (2026-07-13); `audits/CAMPAIGN-6A-2026-07-14.md` (2026-07-14) |
| <a id="d13"></a>[D13](capability-map-detail/D.md#d13) | Media apps see EVERY drive; a drive added, pulled, back or safely removed is handled per app (notice, real restart, wiring, torrents paused/resumed) | controller v0.313.0, catalog `0188430`, hub v0.151.0 | **PROVEN-LIVE** | Tier-0 boxes (Tester 1, demo-hp 9201); the fleet with golden 0.313.0 + the floor | `audits/media-build-2026-10-15/G/` (2026-10-15); `audits/media-drive-events-2026-10-14/` (2026-10-14) |
| <a id="d14"></a>[D14](capability-map-detail/D.md#d14) | A second local copy COUNTS only on a proven different physical disk: the agent knows each place's disks; a VM box makes the copy but says it cannot check; a same-disk copy is not made; a NAS never hides the one-copy notice; a box with no drive refuses a data-folder app in words | agent v0.157.0, controller v0.317.0, hub v0.154.0 | **PARTIAL** | live: VM `unknown` (Tester 1) and two real-disk boxes `proven` (demo-hp, N100); unit only: the same-disk skip, the NAS notice, the no-drive sentence; `usb_bridge` never seen on a device | `audits/storage-identity-buildA-2026-10-15/README.md` (2026-10-15); `architecture/16-storage-physical-identity.md` §13 |
| <a id="d15"></a>[D15](capability-map-detail/D.md#d15) | A one-disk box gets its own data area („Belső tárhely") outside every whole-box copy, registered as the default drive with no press, so media and data-folder apps install with no extra step; a free disk's partitions can be formatted or used one by one | installer 1.33.0, agent v0.158.1, controller v0.318.0, golden 0.318.0 | **PROVEN-LIVE** | fresh 512 GB one-disk VM on demo-hp; partitions on a scratch disk; the old-box signed job and the D4 sentence unit-proven only; an empty part asks for confirmation (R-1093) | `audits/storage-identity-buildB-2026-10-15/README.md` (2026-10-16) |
## E. Access, networking & household use
@@ -477,26 +477,99 @@ does not get to vouch for a version, so an overlay can neither set nor clear it.
**[DESIGN] Guide text is DATA.** Plain text through `html/template` — no markdown, no raw HTML — so
a step containing a tag is shown as the characters its author typed.
#### The app page's layout (v0.316.0, operator-approved mockup 2026-10-15)
#### The app page's layout (v0.319.0, operator-approved 2026-10-16, section style A)
**[DESIGN]** Five rules; the first three are the mockup, the last two are what they imply.
**[DESIGN]** The mockup canvas „Felhom app oldal — egy oszlop" (Settings screen), operator-approved
2026-10-16 with section style **A — panels**. It replaces v0.316.0's hairline sections: with the hero,
the tab bar and every section on the same page background, the operator found the page flat — nothing
told one section from the next.
1. **One column of one width** (820px; the log tab alone is full width, because log lines are long).
Boxes of differing widths read as a page assembled by accident, which is what v0.315.0 looked like.
2. **A section is a hairline, not a card** — `padding: 28px 0` with a bottom `--line-soft`, no
background, no border, no radius. A card says „this is a thing you act on", and most of an app
page is a thing you READ; a card there is a promise the content cannot keep.
3. **A box means something, and there are exactly two.** Blue (`--blue` on `--blue-dim`) when the app
is WAITING for the household — an after-start value; amber (`--warn` on `--warn-dim`) when
something is RISKY, such as a backup sharing a disk with the data it protects. The
exception-colour principle, applied to shape as well as colour. Nothing informational is boxed.
4. **The hero is not a box**: a 64px logo tile, the name, the tagline, hairline chips (state,
lifecycle, update) and meta text (the address, the last automatic update). The tab bar is links
with a 2px `--blue-bright` underline under the open one — no pills, no fills.
5. **A success is not a banner.** The automatic-update SUCCESS line is meta text in the hero. It
asked nothing of the household, so it is a date. Everything that DOES need a decision — a failed
update, a hold, lost files, a missing drive, an interrupted install — stays a banner above the
tabs, which is R-571's placement rule and is unchanged.
Every panel is the column's full width — no narrower inner card. Boxes of differing widths read as
a page assembled by accident.
2. **The hero is a panel** (`--bg-1`, `--line` border, 20px 24px, 16px above it; the logo tile on
`--bg-2`). Its content is unchanged: the name, the tagline, the chips, the meta text.
3. **The tab bar is a panel** (`--bg-1`, 4px inside, 16px above, 20px below). A tab is 10px 18px,
15px/600 in `--text-2`; the open tab is filled `--bg-2` with a 2px `--blue-bright` underline.
4. **Every section is a panel, ONE style on every surface** — the four tabs and the install page,
never mixed: `--bg-1`, `--line` border, `18px 24px 22px`, 16px apart; the title 17px/700 18px from
the top edge with a `--line-soft` rule under it. The install page's form groups (`.form-section`)
and its progress panel are drawn by the same rule. Pinned by `TestAppPage_OneSectionStyleEverywhere`.
5. **Colour still means something.** A panel is neutral. Blue (`--blue` on `--blue-dim`) is the app
WAITING for the household; amber (`--warn` on `--warn-dim`) is something RISKY. The callouts keep
their colours, inside a panel or as their own.
6. **A success is not a banner.** The automatic-update SUCCESS line is meta text in the hero.
Everything that DOES need a decision — a failed update, a hold, lost files, a missing drive, an
interrupted install — stays a banner above the tabs (R-571's placement rule, unchanged).
**[DESIGN] „Belépés az alkalmazásba" — the login in one panel.** The settings tab shows the app's
login name and password side by side (stacked under 720px). A login field is a `type: password`
field plus, when the template has one, the `secret` or `text` field with the same prefix and
`USER`/`USERNAME`/`EMAIL` (`web/app_login.go`); a password with no clean pair stands alone. Values are
fetched on demand (R-254, R-709) — the page learns only whether one is stored.
**[DESIGN] The empty-value rule.** A login field the box holds no value for is a SENTENCE („A doboz
ennél a telepítésnél nem mentett jelszót. A belépéshez azt használd, amit az alkalmazásban
beállítottál."), never an eye: the reveal endpoint answers 404 for it, a refusal the household could
only meet by pressing. The page and the endpoint decide „stored" with ONE function
(`storedValuePresent`). Measured cause of the case that earned it: an app installed before its
template gained a `type: password` field — `InjectMissingFields` never fills one
(`audits/app-page-tuning-2026-10-16/P0`).
#### Logins and folders (v0.320.0, task 2026-10-16 „logins-folders")
**[DESIGN] A protected infra stack is part of the box, and its app page says so.** FileBrowser (and traefik,
cloudflared, samba) has no `app.yaml`, so `Deployed` is false. Every other surface counts `Deployed || Protected` as
on the box; v0.315.0's app page counted `Deployed` alone and showed FileBrowser „not installed", with „Telepítés" and
without its login (measured on both demo boxes, `audits/logins-folders-2026-10-16/P0`). The app page now treats a
protected stack as installed: settings, guide and log tabs; Restart only (as the app list); the curated name
(`inframeta.go`); no data tab and no install page (both 302 to the page). The deploy API and `DeployStack` refuse a
protected stack — until then the Install press would have deployed the infra compose as an app.
**[DESIGN] Every installed app says where its login is — ONE answer.** The settings tab's „Belépés az alkalmazásba"
panel is on every installed app with exactly one of: the box's login fields (on demand; a field with no stored value
is the v0.319.0 sentence) · the login the app wrote, or FileBrowser's (`InitialCreds`) · a sentence chosen by the
catalog's `app_info.login_kind` — `first_visit` (the default: „you create the admin on the first visit; the box does
not know the password"), `email` (a sign-in link), `none` (no sign-in; behind `family_gate` the sentence says the
family sign-in is the login). `login_kind` is an enum, not copy: the sentence is the controller's. An infra stack with
no login of its own (traefik, cloudflared, samba) gets no panel. Code: `web/app_login.go` `loginAnswerFor`.
**[DESIGN] „Új jelszó beállítása" — FileBrowser's password when the box does not know it.** In R-513's `operator`
state (both demo boxes) the panel says „Ezt a jelszót te állítottad be a Fájlkezelőben; a doboz nem ismeri." and offers
the press. The box uses FileBrowser's own CLI (`set -u admin,<pw>`) with the server STOPPED (the CLI opens the
database with no lock timeout), verifies new=200 and admin/admin refused, stores the value encrypted and records
`generated`; a failed verify leaves `operator`. Not the `FILEBROWSER_ADMIN_PASSWORD` env: it re-applies on every
start. Spike: `audits/logins-folders-2026-10-16/P1b`. FileBrowser still asks for its own login the first time; one
sign-in for both is R-1100.
**[DESIGN] A stored FileBrowser password is checked before it is offered (v0.321.0, R-1101).** In `generated`, the
page asks FileBrowser whether the stored value still logs in; only FileBrowser's own 401/403 counts as refused (a
FileBrowser that does not answer is not a refusal — an unproven refusal must not tell the household its password
changed). Refused → „A Fájlkezelőben megváltoztattad a jelszót, ezért a doboz már nem ismeri." and „Új jelszó
beállítása". The page no longer tells the household to change FileBrowser's password, and FileBrowser's own update
notice and „Install app" prompt are switched off in its config (the box updates it; it is opened from the dashboard).
**[DESIGN] The Data tab's backup part is ONE section (v0.321.0).** „Biztonsági mentés": an „Éjszakai mentés" row, the
2nd copy's state as rows, what the 2nd copy is, its form. Plain words — no „off-drive", no „PBS", no
„DB/konfiguráció" (pinned by `TestAppData_OneBackupSection`).
**[DESIGN] An installed app's install address goes to its settings tab (v0.321.0, R-1095)** — 302, GET only, like the
`/stacks/{name}/deploy` bridge.
**[DESIGN] „Fájlok ↗" — one of the app's folders, in FileBrowser.** The header button opens the folder with the library
role, else the first declared folder, else (a media app) the first present drive's `media` folder; built only with
`fileBrowserLink` (PathEscape). The data tab lists each drive's media folders the app uses (Sonarr `tv`, Radarr
`movies`, the players both, a download client its `media_library.folder`) with NO backup sentence — the bind's class
there means „not this app's data", which is not the class-driven „temporary, not backed up". **A folder whose drive
is not bound into FileBrowser now is text, never a link** (`drivePresent` = the same gates the FileBrowser sync
applies). The button never promises one click: FileBrowser's router brings a cold visitor back to the folder after its
login (measured in the headless browser, `audits/logins-folders-2026-10-16/browser/`).
**[DESIGN] „Cím és kulcsok" is rows, not the install form.** The settings tab does not re-render the
install wizard's fields: the address is the „Cím" row, the storage is a „Tárhely: <név>" row, every
other install answer and generated key is a row (keys keep their eye). The read-only hint sits under
the title. `/apps/{slug}/install` opened for an installed app still renders the form read-only, with
its secret guards.
**[DESIGN] The stylesheet's address changes with every build.** Every shell links
`/static/style.css?v={{.AssetTag}}`, where the tag hashes version + `GitCommit` + `BuildTime` and
@@ -553,6 +553,13 @@ a normal night: nothing. A box off for weeks: one catch-up when it returns; the
running all along. An upgrade from an older release: the ledger is seeded, the first missed night after it is the
first one made up.
**[FACT, 2026-10-16, R-1097] An image's own anonymous VOLUME is in no backup.** The app backup takes the named volumes the
compose DECLARES (`backup.ResolveDockerVolumeNames`) and the drive folders it binds; a path the image keeps in its
`VOLUME` that the compose does not mount lands in an anonymous volume and is taken by nothing. Measured with RomM's saves
(`/romm/assets`) on demo-hp; fixed with a named volume, and catalog gate `image-volumes` keeps a measured registry of such
paths (`audits/burndown-apps-2026-10-16/P1/`). A survey of every running container on demo-hp found no other data path
in an anonymous volume (PrivateBin's are temp folders).
### 6.2 Coverage per app class — and an unresolved count
**[FACT]** Of 53 catalog templates, **52 keep data in Docker named volumes**; exactly **13** carry a
@@ -43,6 +43,24 @@ this closes the half that let it happen.
number. A box already on the bad image is moved by the normal update path — never by re-tagging,
which would be the same defect wearing the fix's clothes.
**[DESIGN] A withdrawn build is never selected (hub v0.155.0, R-1081, R-1085).** The rule above
leaves the spoiled numbers in the registry, so nothing may pick them. Two controller tags hold
other code than their number says: **`0.315.0`** (re-pushed with v0.316.0's code under it) and
**`0.316.3`** (built from a working copy holding another session's uncommitted files). The
household's button and the daily auto-update take the HIGHEST tag and cannot reach them; the
**floor** targets a version exactly, so the floor is where the guard lives, in two halves:
- **at input** — both floor forms (global and per customer, by session or by token) and the
confirm dialog's impact probe refuse a listed tag (`flash=floor_spoiled`; the probe answers
`valid:false, spoiled:true`). The check is its own, NOT inside `normalizeFloorInput`, which also
validates agent and golden numbers;
- **at serve time** — `store.ResolveManagedFloor` HOLDS a listed floor (served as none,
`HeldSpoiled`, its own reason text) however it arrived: a value stored before the refusal, or the
config/env default (which no form checks; the hub also warns about it at start). The stored
value is not rewritten; the Configuration and customer pages say „withdrawn".
The list is `hub/internal/store/spoiled_tags.go`; the hand deploy reads the same list as a marked
line in the `felhom-build-deploy` skill, and a test fails when the two differ. A new spoiled
build is added to both in the same commit.
## 1. How an update works today, as measured
### 1.1 The button
@@ -6,8 +6,8 @@
> whether two storage places sit on the same physical disk." Triggered by the OMV test (`audits/omv-vm-2026-10-15/`,
> R-1068). Spike: `audits/storage-identity-2026-10-15/` (Part B there). Marks as in `01`: **[FACT]** read in a named
> file or measured; **[DESIGN]** a proposal. **Status: ACCEPTED 2026-10-15** — the operator ruled D1–D6 as recommended (§12). **Build A shipped 2026-10-15**
> (§3, §4, §5's D4 sentence, §8, §10); **§5's internal place, §6 and §7 wait for build B** — what is built, and where the
> build differs from this text, is §13.
> (§3, §4, §5's D4 sentence, §8, §10); **build B shipped 2026-10-16** (§5's internal place, §6, §7, §9) — what is built,
> and where the build differs from this text, is §13.
> Paths: A = `felhom-agent/`, C = `felhom-controller/controller/`, E = `felhom.eu/`.
## 1. Today, in one table (Part A)
@@ -216,5 +216,25 @@ fields) · website and guides (the OMV guide's second-disk line; the backup expl
- An older agent (no same-disk call): the controller keeps the pre-build `st_dev` rule and logs it once.
- §11 rejects `sys_drive` for household data, but the drives page still offers `/mnt/sys_drive` to attach (R-280) — R-1082.
**Build B — waits:** §5's internal place, §6 partitions, §7 the installer (and the D3 space check the operator asked for).
**Build B — the internal place, partitions, the installer (2026-10-16)** (agent v0.158.1 — v0.158.0 superseded; controller
v0.318.0; installer 1.33.0; evidence `audits/storage-identity-buildB-2026-10-15/`; the D3 space check: its `SPACE-CHECK.md`,
sizes ruled above in §7):
| Section | Built | Where it differs from the text above |
|---|---|---|
| §5 internal place | the root helper `felhom-internal-place` (bundle) makes `pve/felhom-belso` by the ruled rule; the installer's step 5b (appliance only) and the signed `add_internal_place` (an old box) run it; the agent mounts it at `/mnt/belso` like a drive (kind `internal`); the controller binds and registers it as the default drive | the place is made only in **appliance** mode (a byo host's pool is the owner's); every failing read in the helper refuses (security review of 0.158.0) |
| §6 partitions | each part of a free disk is listed (empty / filesystem / in_use); „format this part", „use this part"; a data-bearing part's wipe id binds disk id + part number + fs-UUID | the „filesystem with no files" state is folded into `filesystem` (no root read-only mount; the typed confirmation covers both). **A part can be formatted only while nothing on its disk is mounted**: the root mkfs wrapper refuses any mounted node and deliberately trusts no „it is Felhom's own drive" claim (the agent could fake it — security review of 0.158.0). So a household formats its free parts before using any part; a free part next to an in-use drive can still be USED, not formatted |
| §7 installer | `step_grows` by the ruled table; step 5b | the host root is left as Proxmox makes it (SPACE-CHECK §3) |
| §9 old boxes | the signed `add_internal_place` (no parameters; the size is the rule's) | — |
**Proven live 2026-10-16** (`audits/storage-identity-buildB-2026-10-15/README.md`): a fresh one-disk install (512 GB VM,
the public installer 1.33.0, agent 0.158.1 and golden 0.318.0 vouched) → data volume 100 GiB, internal place 213 GiB,
registered as the default drive with no press; Jellyfin installed with no extra step onto it; a whole-box copy holds nothing
of it; partitions formatted and used through the wizard; the honest backup offer on a VM. **Not as written:** an empty part
asks for the typed confirmation (the probe reads the parent's partition table — R-1093); a 200 GB disk gets no internal
place under the ruled sizes (15 GiB < 20).
Also in build B: R-1084 (the whole-box backup offer asks the same-disk question) and R-1082 (`/mnt/sys_drive` is never
offered to attach, by device). A bundle that adds a path needs a step bundle on boxes with an older wrapper (R16, R-880):
0.158.1 was delivered through `0.158.1-step1`.
@@ -268,3 +268,29 @@ the installer (R-1079).
---
<a id="d15"></a>
## D15 — The one-disk box's own data area; partitions as places
*Map row: [D15](../00-capability-map.md#d15) · added 2026-10-16.*
**Scenario:** a household with one disk installs a media app and expects it to work with no extra step — and expects its
films not to fill every whole-box copy. A household with a split disk wants to use the free part without wiping the rest.
**Components:** installer 1.33.0 (step 3 sizes the data volume by the ruled table; step 5b runs the bundle's root helper
`felhom-internal-place`), agent v0.158.1 (mounts the place like a drive, kind `internal`; lists partitions; the signed
`add_internal_place` for an old box), controller v0.318.0 (binds and registers it as the default drive; the partition
wizard). Design: `architecture/16-storage-physical-identity.md` §5–§7, §9, §13; sizes: `audits/storage-identity-buildB-2026-10-15/SPACE-CHECK.md`.
**Status:** **PROVEN-LIVE** (2026-10-16) on a fresh 512 GB one-disk VM from the public ISO and installer: data volume 100 GiB,
data area 213 GiB, registered as the default drive with no press, Jellyfin installed with no extra step onto it, a whole-box
copy holds nothing of it; partitions formatted and used through the wizard; formatting the whole disk under a used part
refused in words.
**Unit only:** the old-box signed job (no box with ≥ 20 GiB free thin space was at hand), the D4 sentence (R-1068's other
half). **Known gaps:** an empty part asks for the typed confirmation (R-1093); the whole-disk refusal sentence (R-1094); a
part next to a mounted part can only be used, not formatted (the root wrapper trusts no „Felhom's own" claim, `16` §13).
**Evidence:** `audits/storage-identity-buildB-2026-10-15/README.md`.
---
@@ -66,6 +66,9 @@ Inside, from bottom to top:
- the **system disk** (Proxmox + the guest),
- **data drive 1** (household data and the **Tier-1** backup folder),
- an optional **data drive 2** (the **Tier-2** copy).
- on a **one-disk box**, the **data area** („Belső tárhely", installer 1.33.0) — a part of the system disk the box uses as
data drive 1; it is **not** in the whole-guest backups, and a copy between it and the system data is never counted as a
second copy (it is the same disk).
Note on the drawing: the data drives are attached to the guest from the host; the **whole-guest backups do not include the data drives** (that is what the per-app tiers are for).
Arrows out of the box:
@@ -0,0 +1,230 @@
# Rendering check of the Plex and Sonarr guides on demo-hp — 2026-10-16 07:20 UTC (morning after the night)
# Owed from the night (R-1088: the credentials file was overwritten; the operator restored it from backup).
# Method: endpoint level, no browser on DooPlex. Public address https://felhom.enkisfelhom.hu (demo-hp guest 9201,
# controller 0.317.0 per the page footer). Password read file->file by scripts/read_credential.py, never printed,
# shredded after use. POST /login -> 302 + felhom_session. Pages: /apps/plex/guide, /apps/sonarr/guide (Sonarr is
# not installed: 302 to /apps/sonarr, which is the guide view), each also with ?lang=en (no setting changed).
# Read only: nothing installed, removed, restarted or set. Logged out after (POST /logout 302; the cookie then 302s to login).
# Catalog on main: ec864b5; its plex/sonarr guide equals A/plex-draft.json and A/sonarr-draft.json (hu, en, checked_version).
## 1. Marker count (spec: assert the data-guide-section count)
plex: page lang=hu data-guide-section=4 (want 4) data-guide-faq=1 FAQ entries=6 (want 6) checked=['1.41.4.9463-630c9f557'] (want 1.41.4.9463-630c9f557) titles match draft[hu]=True -> PASS
sonarr: page lang=hu data-guide-section=4 (want 4) data-guide-faq=1 FAQ entries=6 (want 6) checked=['4.0.20'] (want 4.0.20) titles match draft[hu]=True -> PASS
plex: page lang=en data-guide-section=4 (want 4) data-guide-faq=1 FAQ entries=6 (want 6) checked=['1.41.4.9463-630c9f557'] (want 1.41.4.9463-630c9f557) titles match draft[en]=True -> PASS
sonarr: page lang=en data-guide-section=4 (want 4) data-guide-faq=1 FAQ entries=6 (want 6) checked=['4.0.20'] (want 4.0.20) titles match draft[en]=True -> PASS
## 2. Every rendered string vs the catalog text (titles, intros, steps, FAQ q/a)
plex hu: strings on page 33, in catalog 33, mismatched 0, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
sonarr hu: strings on page 35, in catalog 35, mismatched 0, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
plex en: strings on page 33, in catalog 33, mismatched 0, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
sonarr en: strings on page 35, in catalog 35, mismatched 0, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
## 3. Negative control: the same compare with one step and one FAQ answer changed in memory
plex hu: strings on page 33, in catalog 33, mismatched 2, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
sonarr hu: strings on page 35, in catalog 35, mismatched 2, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
plex en: strings on page 33, in catalog 33, mismatched 2, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
sonarr en: strings on page 35, in catalog 35, mismatched 2, <label>.DOMAIN filled 0, unfilled DOMAIN left 0
## The guide block as served: plex.html
<div class="app-guide" data-guide="1" data-guide-checked="1.41.4.9463-630c9f557">
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Nézés tévén vagy telefonon</h3>
<p>A Plex appok a Plex fiókodon keresztül találják meg a szerveredet.</p>
<ol class="app-info-list"><li>Nyisd meg a Plex appot, és jelentkezz be ugyanazzal a Plex fiókkal, amellyel a kódot kérted.</li><li>A „Kezdőképernyő” oldalon megjelennek a könyvtáraid.</li><li>Válassz egy filmet, és kattints a „Lejátszás” gombra.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Új filmek és új meghajtó</h3>
<p>Amit a meghajtód media mappájába teszel, azt a Plex magától észreveszi.</p>
<ol class="app-info-list"><li>A Radarr a media/movies, a Sonarr a media/tv mappába tesz mindent, a Plex innen olvas.</li><li>Új meghajtónál a doboz hozzáadja a meghajtó mappáit a meglévő film- és sorozatkönyvtáraidhoz, és elindít egy beolvasást.</li><li>Ha egy új fájl mégsem jelenik meg, a könyvtár menüjében kattints a „Könyvtárfájlok beolvasása” pontra.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Rossz cím vagy borító javítása</h3>
<p>A Plex a fájl és a mappa nevéből találja ki, melyik film ez.</p>
<ol class="app-info-list"><li>Nyisd meg a filmet, és kattints a „Több” menüre.</li><li>Válaszd a „Találatok pontosítása” pontot, és keresd meg a helyes címet.</li><li>Legközelebb segít, ha a mappa neve a film címe és éve, például: Nosferatu (1922).</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Megosztás a családdal</h3>
<p>Minden családtag a saját Plex fiókjával nézhet.</p>
<ol class="app-info-list"><li>A „Beállítások” oldalon nyisd meg a „Könyvtár hozzáférés kezelése” részt.</li><li>Válaszd ki, kivel és mely könyvtárakat osztod meg.</li><li>A családtag a saját Plex fiókjával lép be az appba.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-faq="1">
<h3>Gyakori kérdések</h3>
<dl class="app-faq">
<dt>Miért nem látom a filmet?</dt>
<dd>Nézd meg, hogy a fájl a media/movies mappában van-e, és hogy a mappa neve a film címe és éve. Utána kattints a „Könyvtárfájlok beolvasása” pontra.</dd>
<dt>Törölhetek filmet a Plexből?</dt>
<dd>Nem: a Plex csak olvassa a meghajtót. Törölni a fájlböngészőben, a Radarrban vagy a Sonarrban tudsz.</dd>
<dt>Kell Plex Pass?</dt>
<dd>Otthoni nézéshez nem. Néhány funkcióhoz igen, például a bevezető átugrásához; ezt a Plex dönti el, nem a doboz.</dd>
<dt>Akadozik a lejátszás. Mit tehetek?</dt>
<dd>A Plex ilyenkor átalakítja a videót, és ez a dobozon lassú lehet. Az appban a „Videó minőség” beállításnál válaszd az „Eredeti” értéket.</dd>
<dt>Elérem otthonon kívül?</dt>
<dd>A doboz beállította, hogy a Plex a saját címeden is elérhető legyen. Otthonon kívüli nézéshez a Plex előfizetést kérhet; ezt a Plex szabja meg.</dd>
<dt>Lejárt a kód. Hol kérek újat?</dt>
<dd>Ugyanott, ahol az elsőt: a címet az első lépések között találod. Az új kódot a vezérlőpulton, a Plex „Beállítások” fülén add meg.</dd>
</dl>
## The guide block as served: sonarr.html
<div class="app-guide" data-guide="1" data-guide-checked="4.0.20">
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Első sorozat hozzáadása</h3>
<p>A Sonarr figyeli a sorozatot, és letölti az új részeket, amint megjelennek.</p>
<ol class="app-info-list"><li>A bal oldali menüben a „Series” (Sorozatok) alatt kattints az „Add New” (Új hozzáadása) pontra.</li><li>Írd be a sorozat címét, és kattints a megfelelő találatra.</li><li>A „Root Folder” (gyökérmappa) mezőben válaszd annak a meghajtónak a media/tv mappáját, ahová a sorozatot szeretnéd.</li><li>A „Tags” (címkék) mezőbe írd ugyanannak a meghajtónak a címkéjét, például hdd-1. Címke nélkül a sorozat nem kap letöltőt.</li><li>Hagyd bejelölve a „Start search for missing episodes” (hiányzó részek keresése) jelölőt, és add hozzá a sorozatot.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>A letöltés követése</h3>
<p>A letöltés a háttérben fut, az oldalt be is zárhatod.</p>
<ol class="app-info-list"><li>Az „Activity” (Tevékenység) menü „Queue” (Sor) lapján látod, mi töltődik éppen.</li><li>Ha egy rész letöltődött, a Sonarr magától átteszi a sorozat mappájába. A Plex és a Jellyfin innen látja.</li><li>A „Calendar” (Naptár) mutatja, mikor jön a következő rész.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Hiányzó részek keresése</h3>
<p>Ha egy régebbi évad hiányzik, egyszerre kerestetheted.</p>
<ol class="app-info-list"><li>Nyisd meg a „Wanted” (Keresett) menü „Missing” (Hiányzó) lapját.</li><li>Jelöld be a kívánt részeket, és kattints a „Search Selected” (kijelöltek keresése) gombra.</li><li>Ha mindet keresnéd, használd a „Search All” (összes keresése) gombot.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Ha valami nem működik</h3>
<p>A Sonarr maga jelzi, ha valami hibás.</p>
<ol class="app-info-list"><li>Nézd meg a „System” (Rendszer) menü „Status” (Állapot) lapját: a „Health” (állapot) rész sorolja a hibákat.</li><li>A „Settings” (Beállítások) menü „Download Clients” (letöltő kliensek) oldalán látod a letöltőket, amelyeket a doboz bekötött.</li><li>Ha nincs találat, nézd meg a Prowlarr „Indexers” (indexerek) oldalát: legalább egy forrás kell.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-faq="1">
<h3>Gyakori kérdések</h3>
<dl class="app-faq">
<dt>Honnan tölt le a Sonarr?</dt>
<dd>Azokról a forrásokról (indexerekről), amelyeket te adsz meg a Prowlarrban. A doboz összeköti a kettőt, de forrást nem ad és nem ajánl: ez a te döntésed.</dd>
<dt>Be kell állítanom a letöltő klienst és a mappákat?</dt>
<dd>Nem. A doboz minden meghajtódhoz bekötötte a telepített letöltőket (qBittorrent, Transmission), a gyökérmappát és a címkét. Neked csak a sorozatot kell hozzáadnod.</dd>
<dt>Miért van ugyanaz a fájl a torrents és a tv mappában is?</dt>
<dd>Nem foglal dupla helyet: ugyanaz a fájl két néven (hardlink). A letöltő tovább oszthatja, a könyvtárad pedig rendezett marad.</dd>
<dt>Letöltötte, de nem került a könyvtárba. Miért?</dt>
<dd>Ha a meghajtón kevesebb mint 10 GB maradna, a Sonarr nem importál. Ezt a doboz állította be: „Settings”, „Media Management”, „Minimum Free Space” (legkevesebb szabad hely). Szabadíts fel helyet, és a következő ellenőrzéskor folytatja.</dd>
<dt>Hol nézhetem meg a sorozatokat?</dt>
<dd>A Plexben vagy a Jellyfinben. A Sonarr csak letölt és rendez, lejátszani nem tud.</dd>
<dt>Mit tölthetek le?</dt>
<dd>Csak olyan tartalmat, amihez jogod van, például szabad licencű műsorokat.</dd>
</dl>
## The guide block as served: en/plex.html
<div class="app-guide" data-guide="1" data-guide-checked="1.41.4.9463-630c9f557">
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Watch on a TV or a phone</h3>
<p>The Plex apps find your server through your Plex account.</p>
<ol class="app-info-list"><li>Open the Plex app and sign in with the same Plex account you got the code with.</li><li>Your libraries appear on the „Home” page.</li><li>Choose a film and select „Play”.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>New films and a new drive</h3>
<p>Whatever you put into your drive&#39;s media folder, Plex notices by itself.</p>
<ol class="app-info-list"><li>Radarr puts everything into media/movies and Sonarr into media/tv; Plex reads from there.</li><li>With a new drive, the box adds its folders to your existing film and series libraries and starts a scan.</li><li>If a new file still does not show up, select „Scan Library Files” in the library&#39;s menu.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Fix a wrong title or cover</h3>
<p>Plex works out which film it is from the names of the file and the folder.</p>
<ol class="app-info-list"><li>Open the film and select the „More” menu.</li><li>Choose „Fix Match” and search for the right title.</li><li>Next time it helps to name the folder after the film&#39;s title and year, for example: Nosferatu (1922).</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Share with the family</h3>
<p>Every family member can watch with their own Plex account.</p>
<ol class="app-info-list"><li>On the „Settings” page, open „Manage Library Access”.</li><li>Choose who gets which libraries.</li><li>The family member signs in to the app with their own Plex account.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-faq="1">
<h3>Frequently asked questions</h3>
<dl class="app-faq">
<dt>Why can I not see the film?</dt>
<dd>Check that the file is in the media/movies folder and that the folder is named after the film&#39;s title and year. Then select „Scan Library Files”.</dd>
<dt>Can I delete a film from Plex?</dt>
<dd>No: Plex only reads the drive. You delete in the file browser, in Radarr or in Sonarr.</dd>
<dt>Do I need Plex Pass?</dt>
<dd>Not for watching at home. Some features need it, for example skipping intros; Plex decides that, not the box.</dd>
<dt>Playback stutters. What can I do?</dt>
<dd>Plex is converting the video, and that can be slow on the box. In the app, set „Video Quality” to „Original”.</dd>
<dt>Can I reach it away from home?</dt>
<dd>The box set Plex up to answer on your own address. To watch away from home, Plex may ask for a subscription; Plex sets that rule.</dd>
<dt>The code ran out. Where do I get a new one?</dt>
<dd>In the same place as the first one: its address is in the first steps. Enter the new code on the dashboard, on Plex&#39;s „Settings” tab.</dd>
</dl>
## The guide block as served: en/sonarr.html
<div class="app-guide" data-guide="1" data-guide-checked="4.0.20">
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Add your first series</h3>
<p>Sonarr watches a series and downloads each new episode as it comes out.</p>
<ol class="app-info-list"><li>In the menu on the left, under „Series”, select „Add New”.</li><li>Type the title of the series and select the right result.</li><li>In „Root Folder”, choose the media/tv folder of the drive you want the series on.</li><li>In „Tags”, type the tag of that same drive, for example hdd-1. Without a tag the series gets no download client.</li><li>Leave „Start search for missing episodes” ticked and add the series.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Follow a download</h3>
<p>Downloads run in the background, so you can close the page.</p>
<ol class="app-info-list"><li>The „Queue” tab of the „Activity” menu shows what is downloading now.</li><li>When an episode is done, Sonarr moves it into the series folder by itself. Plex and Jellyfin see it there.</li><li>The „Calendar” shows when the next episode is due.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>Search for missing episodes</h3>
<p>If an older season is missing, you can search for it in one go.</p>
<ol class="app-info-list"><li>Open the „Missing” tab of the „Wanted” menu.</li><li>Tick the episodes you want and select „Search Selected”.</li><li>To search for all of them, use „Search All”.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1">
<h3>When something does not work</h3>
<p>Sonarr tells you itself when something is wrong.</p>
<ol class="app-info-list"><li>Look at the „Status” tab of the „System” menu: the „Health” part lists the problems.</li><li>The „Download Clients” page of the „Settings” menu shows the download clients the box connected.</li><li>If nothing is found, look at the „Indexers” page in Prowlarr: you need at least one source.</li>
</ol>
</div>
<div class="app-section" style="margin-top:1rem" data-guide-faq="1">
<h3>Frequently asked questions</h3>
<dl class="app-faq">
<dt>Where does Sonarr download from?</dt>
<dd>From the sources (indexers) you add in Prowlarr. The box connects the two, but it neither provides nor suggests a source: that is your choice.</dd>
<dt>Do I have to set up the download client and the folders?</dt>
<dd>No. For every drive, the box connected the installed download clients (qBittorrent, Transmission), the root folder and the tag. You only add the series.</dd>
<dt>Why is the same file in both the torrents and the tv folder?</dt>
<dd>It does not take double space: it is one file with two names (a hardlink). The download client can keep sharing it, and your library stays tidy.</dd>
<dt>It downloaded, but it is not in the library. Why?</dt>
<dd>Sonarr does not import if less than 10 GB would be left on the drive. The box set this: „Settings”, „Media Management”, „Minimum Free Space”. Free up some space and it goes on at the next check.</dd>
<dt>Where do I watch the series?</dt>
<dd>In Plex or Jellyfin. Sonarr only downloads and files; it cannot play anything.</dd>
<dt>What may I download?</dt>
<dd>Only content you have the right to, for example shows under a free licence.</dd>
</dl>
@@ -0,0 +1,57 @@
# Part 0 — why demo-hp's Calibre-Web has no stored password (2026-10-16, read only, no value printed)
Method: SSH to demo-hp (`ssh hp`) → `pct exec 9201`; the controller's own reveal endpoint
(`POST /stacks/calibre-web/auto-field/reveal`, the one the eye button calls) through traefik inside the guest;
Calibre-Web's own user table (`docker exec calibre-web sqlite3 /config/app.db 'select name from user'`), compared
inside the guest. The hub's per-app page (`/apps/calibre-web`) with the CC token; then the box itself
(demo-felhom, `ssh felhom-pve`) as the second channel.
## 1. Keys in calibre-web's app.yaml (9201), names only
```
env: ADMIN_USER DOMAIN HDD_PATH SUBDOMAIN <- no ADMIN_PASSWORD key at all
locked_fields: DOMAIN SUBDOMAIN HDD_PATH ADMIN_USER
deployed_at: 2026-08-21T16:27:08Z
default_login: changed_at 2026-09-29T07:21:06Z by household
app.yaml mtime: 2026-10-01 11:26:10Z <- InjectMissingFields wrote ADMIN_USER here
reveal ADMIN_PASSWORD -> 404, ok false, no value
reveal ADMIN_USER -> 200, value present
```
## 2. Install date vs the template
| what | when |
|---|---|
| calibre-web installed on 9201 | 2026-08-21 16:27Z |
| `ADMIN_PASSWORD` (`type: password`) enters the template | catalog `d0e7e2e`, 2026-09-29 07:19Z |
| `ADMIN_USER` (`type: secret`, `generate: hex:5`) enters the template | catalog `e9f50b5`, 2026-10-01 11:19Z |
| the box injects `ADMIN_USER` into app.yaml | 2026-10-01 11:26:10Z (app.yaml mtime; A3 of `audits/calibre-name-and-prune-2026-10-01`) |
| R-757 guard (no invented after_install-only secret) | controller `1991c74`, v0.298.0, 2026-10-06 |
`InjectMissingFields` (`internal/stacks/deploy.go`) fills only `secret`, `domain` and `subdomain`; a new
`type: password` field falls to the `default:` branch (`requires manual configuration`) and is never stored. So the
**hypothesis holds for the password**: installed before the field existed, never filled, and after_install (which sets
it in the app) never ran for this install.
## 3. Is the stored ADMIN_USER invented?
```
calibre users count 2 admin present False
equal (stored ADMIN_USER is a Calibre-Web user): True
```
**No — on demo-hp the name is NOT invented any more.** On 2026-10-01 11:27Z a session renamed Calibre-Web's admin
user by hand to the name the box had just recorded (`audits/calibre-name-and-prune-2026-10-01/A/A2-demo-hp-rename.txt`,
second block). The name the page shows is the real login name. **So the task's "remove the invented value from
app.yaml on 9201" does not apply: removing it would hide a TRUE login name.** Nothing was removed.
The password that goes with it was set by hand in the app; the operator holds it out-of-band.
## 4. Other boxes (hub, then the box)
The hub's `/apps/calibre-web` says „Deployments 2", affected customers demo-felhom and demo-hp (telemetry). Tester 1
and Tester 2 are not listed. The second channel — the box: demo-felhom guest 9201 has `/opt/docker/stacks/calibre-web/`
with only `.felhom.yml` + `docker-compose.yml` (catalog mirror), **no app.yaml, no calibre-web container** — it is not
installed there now. **No customer box has this state. No register row for customer boxes.**
(The hub's „Deployments 2" counts telemetry history, not what is installed today — a presence-not-success reading;
the box is the truth. NOT-A-FINDING for this task: the hub page labels it telemetry.)
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

@@ -0,0 +1,239 @@
after login url: /launcher
{
"settings": {
"path": "/apps/calibre-web/settings",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 3,
"sectionWidths": [
820,
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16,
16
],
"titleTopGap": 19,
"loginPanel": true,
"loginFields": [
"ADMIN_USER",
"ADMIN_PASSWORD"
],
"notStored": [
"ADMIN_PASSWORD"
],
"reveals": [
"ADMIN_USER"
],
"loginPairCols": "377px 377px",
"domainInput": false,
"deployForm": false,
"logBoxWidth": null,
"console": []
},
"data": {
"path": "/apps/calibre-web/data",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 4,
"sectionWidths": [
820,
820,
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16,
16,
16
],
"titleTopGap": 19,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": false,
"deployForm": false,
"logBoxWidth": null,
"console": []
},
"guide": {
"path": "/apps/calibre-web/guide",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 10,
"sectionWidths": [
820,
820,
820,
820,
820,
820,
820,
820,
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16,
16,
16,
16,
16,
16,
16,
16,
16
],
"titleTopGap": 19,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": false,
"deployForm": false,
"logBoxWidth": null,
"console": [
[
"log-error",
"Failed to load resource: the server responded with a status of 404 ()",
"https://felhom.enkisfelhom.hu/static/assets/calibre-web-screenshot-3.webp"
]
]
},
"logs": {
"path": "/apps/calibre-web/logs",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 1,
"sectionWidths": [
966
],
"columnWidth": 966,
"sectionSpacing": [],
"titleTopGap": null,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": false,
"deployForm": false,
"logBoxWidth": 916,
"console": []
},
"settings_390px": {
"loginPairCols": "324px",
"scrollWidth": 390
}
}
@@ -0,0 +1,42 @@
after login url: /launcher
{
"install": {
"path": "/apps/grocy/install",
"activeTab": null,
"tabBar": null,
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 2,
"sectionWidths": [
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16
],
"titleTopGap": 19,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": true,
"deployForm": true,
"logBoxWidth": null,
"console": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@@ -0,0 +1,231 @@
after login url: /launcher
{
"settings": {
"path": "/apps/paperless-ngx/settings",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 3,
"sectionWidths": [
820,
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16,
16
],
"titleTopGap": 19,
"loginPanel": true,
"loginFields": [
"PAPERLESS_ADMIN_USER",
"PAPERLESS_ADMIN_PASSWORD"
],
"notStored": [],
"reveals": [
"PAPERLESS_ADMIN_PASSWORD"
],
"loginPairCols": "377px 377px",
"domainInput": false,
"deployForm": false,
"logBoxWidth": null,
"console": []
},
"data": {
"path": "/apps/paperless-ngx/data",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 4,
"sectionWidths": [
820,
820,
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16,
16,
16
],
"titleTopGap": 19,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": false,
"deployForm": false,
"logBoxWidth": null,
"console": []
},
"guide": {
"path": "/apps/paperless-ngx/guide",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 10,
"sectionWidths": [
820,
820,
820,
820,
820,
820,
820,
820,
820,
820
],
"columnWidth": 820,
"sectionSpacing": [
16,
16,
16,
16,
16,
16,
16,
16,
16
],
"titleTopGap": 19,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": false,
"deployForm": false,
"logBoxWidth": null,
"console": []
},
"logs": {
"path": "/apps/paperless-ngx/logs",
"activeTab": {
"background-color": "rgb(22, 38, 63)",
"color": "rgb(237, 242, 249)",
"border-bottom-color": "rgb(46, 168, 245)",
"border-bottom-width": "2px",
"padding-top": "10px",
"padding-left": "18px"
},
"tabBar": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "4px"
},
"hero": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"padding-top": "20px",
"padding-left": "24px"
},
"section": {
"background-color": "rgb(15, 27, 46)",
"border-top-color": "rgb(34, 52, 79)",
"border-top-width": "1px",
"padding-top": "18px",
"padding-left": "24px",
"margin-bottom": "16px",
"border-radius": "2px"
},
"visibleSections": 1,
"sectionWidths": [
966
],
"columnWidth": 966,
"sectionSpacing": [],
"titleTopGap": null,
"loginPanel": false,
"loginFields": [],
"notStored": [],
"reveals": [],
"loginPairCols": null,
"domainInput": false,
"deployForm": false,
"logBoxWidth": 916,
"console": []
},
"settings_390px": {
"loginPairCols": "324px",
"scrollWidth": 390
}
}
@@ -0,0 +1,26 @@
## TestAppPage_LoginPairPanel — mutation on internal/web/app_login.go
=== RUN TestAppPage_LoginPairPanel
app_page_tuning_test.go:171: the login panel must hold BOTH halves (user=-1 password=301)
--- FAIL: TestAppPage_LoginPairPanel (0.07s)
## TestAppPage_NoStoredValueShowsSentence — mutation on internal/web/app_login.go
=== RUN TestAppPage_NoStoredValueShowsSentence
app_page_tuning_test.go:217: ADMIN_PASSWORD: no sentence for a field the box holds no value for
app_page_tuning_test.go:220: ADMIN_PASSWORD: an eye is offered for a value the endpoint answers 404 for
app_page_tuning_test.go:217: ADMIN_USER: no sentence for a field the box holds no value for
app_page_tuning_test.go:220: ADMIN_USER: an eye is offered for a value the endpoint answers 404 for
app_page_tuning_test.go:225: the page offers an eye for ADMIN_USER and the endpoint answers 404
## TestAppPage_SettingsHasNoDuplicateDomain — mutation on internal/web/templates/app_settings.html
=== RUN TestAppPage_SettingsHasNoDuplicateDomain
app_page_tuning_test.go:242: the settings tab still renders id="auto-field-DOMAIN"
app_page_tuning_test.go:242: the settings tab still renders id="deploy-form"
app_page_tuning_test.go:242: the settings tab still renders id="field-HDD_PATH"
--- FAIL: TestAppPage_SettingsHasNoDuplicateDomain (0.09s)
## TestAppPage_OneSectionStyleEverywhere — mutation on internal/web/templates/app_logs.html
=== RUN TestAppPage_OneSectionStyleEverywhere
app_page_tuning_test.go:379: app_logs: a block outside the section style: <div data-tab-panel="logs">
--- FAIL: TestAppPage_OneSectionStyleEverywhere (0.59s)
## TestDeployPage_DeployedSelectShowsStoredHDDPath — row reads the DEFAULT drive's label
## TestDeployPage_DeployedSelectShowsStoredHDDPath — row reads the DEFAULT drive's label
networkstub_test.go:191: the storage row must name the STORED path's drive, got: data-app-storage-row="1">Tárhely (felhom-usb)
networkstub_test.go:194: the storage row must NOT name the default drive for a deployed app, got: data-app-storage-row="1">Tárhely (felhom-usb)
--- FAIL: TestDeployPage_DeployedSelectShowsStoredHDDPath (0.07s)
@@ -0,0 +1,9 @@
== tester-1
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.319.0 Up 58 seconds (healthy)
== demo-hp
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.319.0 Up About a minute (healthy)
== demo-felhom
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.319.0 Up About a minute (healthy)
@@ -0,0 +1,3 @@
tester-1 Location: /customers/tester-1?flash=floor_set
demo-hp Location: /customers/demo-hp?flash=floor_set
demo-felhom Location: /customers/demo-felhom?flash=floor_set
@@ -0,0 +1,3 @@
felhom.enkisfelhom.hu 200
felhom.enkicsifelhom.hu 200
felhom.demo-felhom.eu 200
@@ -0,0 +1,5 @@
10:37:01
HTTP/1.1 303 See Other
Location: /configuration?flash=floor_set
('min_controller_version', '0.319.0', '2026-10-16 10:37:02')
('min_controller_version_declared_min_agent', '0.319.0=0.158.1', '2026-10-16 10:37:02')
@@ -0,0 +1,3 @@
10:35:57
HTTP/1.1 303 See Other
Location: /configuration?flash=floor_set
@@ -0,0 +1,39 @@
control NEGATIVE (a stub answering 200 over plain http): convicted, as it must (200 -)
== tester-1 (enkicsifelhom.hu): 13 public name(s)
OK control POSITIVE http://felhom.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkicsifelhom.hu/r1033-check?q=1
OK http://files.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://files.enkicsifelhom.hu/r1033-check?q=1
OK http://media.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://media.enkicsifelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1
OK http://paperless.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paperless.enkicsifelhom.hu/r1033-check?q=1
OK http://paste.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paste.enkicsifelhom.hu/r1033-check?q=1
OK http://plex.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://plex.enkicsifelhom.hu/r1033-check?q=1
OK http://prowlarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://prowlarr.enkicsifelhom.hu/r1033-check?q=1
OK http://radarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://radarr.enkicsifelhom.hu/r1033-check?q=1
OK http://sonarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://sonarr.enkicsifelhom.hu/r1033-check?q=1
OK http://torrent.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://torrent.enkicsifelhom.hu/r1033-check?q=1
OK http://transmission.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://transmission.enkicsifelhom.hu/r1033-check?q=1
OK http://wiki.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkicsifelhom.hu/r1033-check?q=1
== demo-hp (enkisfelhom.hu): 13 public name(s)
OK http://books.enkisfelhom.hu/r1033-check?q=1 -> 301 https://books.enkisfelhom.hu/r1033-check?q=1
OK http://docs.enkisfelhom.hu/r1033-check?q=1 -> 301 https://docs.enkisfelhom.hu/r1033-check?q=1
OK http://felhom.enkisfelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkisfelhom.hu/r1033-check?q=1
OK http://files.enkisfelhom.hu/r1033-check?q=1 -> 301 https://files.enkisfelhom.hu/r1033-check?q=1
OK http://gist.enkisfelhom.hu/r1033-check?q=1 -> 301 https://gist.enkisfelhom.hu/r1033-check?q=1
OK http://ha.enkisfelhom.hu/r1033-check?q=1 -> 301 https://ha.enkisfelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1
OK http://papir.enkisfelhom.hu/r1033-check?q=1 -> 301 https://papir.enkisfelhom.hu/r1033-check?q=1
OK http://paste.enkisfelhom.hu/r1033-check?q=1 -> 301 https://paste.enkisfelhom.hu/r1033-check?q=1
OK http://pdf.enkisfelhom.hu/r1033-check?q=1 -> 301 https://pdf.enkisfelhom.hu/r1033-check?q=1
OK http://plex.enkisfelhom.hu/r1033-check?q=1 -> 301 https://plex.enkisfelhom.hu/r1033-check?q=1
OK http://travel.enkisfelhom.hu/r1033-check?q=1 -> 301 https://travel.enkisfelhom.hu/r1033-check?q=1
OK http://wiki.enkisfelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkisfelhom.hu/r1033-check?q=1
== demo-felhom (demo-felhom.eu): 8 public name(s)
OK http://felhom.demo-felhom.eu/r1033-check?q=1 -> 301 https://felhom.demo-felhom.eu/r1033-check?q=1
OK http://files.demo-felhom.eu/r1033-check?q=1 -> 301 https://files.demo-felhom.eu/r1033-check?q=1
OK http://gist.demo-felhom.eu/r1033-check?q=1 -> 301 https://gist.demo-felhom.eu/r1033-check?q=1
OK http://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1 -> 301 https://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1
OK http://prowlarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://prowlarr.demo-felhom.eu/r1033-check?q=1
OK http://radarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://radarr.demo-felhom.eu/r1033-check?q=1
OK http://sonarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://sonarr.demo-felhom.eu/r1033-check?q=1
OK http://torrent.demo-felhom.eu/r1033-check?q=1 -> 301 https://torrent.demo-felhom.eu/r1033-check?q=1
== every public name redirects http -> https
@@ -0,0 +1,4 @@
['key', 'value', 'updated_at']
('min_controller_version', '0.319.0', '2026-10-16 10:35:57')
('artifact_min_agent', '0.155.0', '2026-10-16 07:23:10')
('min_controller_version_declared_min_agent', '0.319.0=0.157.0', '2026-10-16 10:35:57')
@@ -0,0 +1,6 @@
('demo-felhom', '', '')
('demo-hp', '', '')
('tester-1', '', '')
('Tester-2', '', '')
('min_controller_version', '0.319.0', '2026-10-16 10:35:57')
('min_controller_version_declared_min_agent', '0.319.0=0.157.0', '2026-10-16 10:35:57')
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Minimal CDP client: log in to the controller through its PUBLIC address in headless Chrome, open each
app-page tab, assert computed styles, collect console errors, save screenshots. The password is read
from a 0600 file and only ever sent inside a CDP message; it is never printed."""
import base64, json, os, socket, struct, subprocess, sys, time, urllib.request
CHROME = os.path.expanduser('~/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell')
BASE = sys.argv[1] # https://felhom.enkisfelhom.hu
SLUG = sys.argv[2] # calibre-web
PWFILE = sys.argv[3]
OUT = sys.argv[4]
PORT = 9333
os.makedirs(OUT, exist_ok=True)
prof = os.path.join(OUT, '.profile')
proc = subprocess.Popen([CHROME, f'--remote-debugging-port={PORT}', '--no-sandbox', f'--user-data-dir={prof}',
'--window-size=1280,900', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
for _ in range(50):
try:
tabs = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json'))
break
except Exception:
time.sleep(0.2)
ws_url = [t for t in tabs if t['type'] == 'page'][0]['webSocketDebuggerUrl']
host, path = ws_url[len('ws://'):].split('/', 1)
h, p = host.split(':')
sock = socket.create_connection((h, int(p)))
key = base64.b64encode(os.urandom(16)).decode()
sock.send(f'GET /{path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode())
buf = b''
while b'\r\n\r\n' not in buf:
buf += sock.recv(4096)
st = {'pending': buf.split(b'\r\n\r\n', 1)[1]}
def send(obj):
data = json.dumps(obj).encode()
hdr = bytearray([0x81])
n = len(data)
if n < 126: hdr.append(0x80 | n)
elif n < 65536: hdr.append(0x80 | 126); hdr += struct.pack('>H', n)
else: hdr.append(0x80 | 127); hdr += struct.pack('>Q', n)
mask = os.urandom(4); hdr += mask
sock.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
while len(st['pending']) < n:
st['pending'] += sock.recv(1 << 20)
out, st['pending'] = st['pending'][:n], st['pending'][n:]
return out
def recv():
msg = b''
while True:
b0, b1 = recv_exact(2)
n = b1 & 0x7f
if n == 126: n = struct.unpack('>H', recv_exact(2))[0]
elif n == 127: n = struct.unpack('>Q', recv_exact(8))[0]
msg += recv_exact(n)
if b0 & 0x80: return json.loads(msg)
console = []
mid = [0]
def call(method, **params):
mid[0] += 1; i = mid[0]
send({'id': i, 'method': method, 'params': params})
while True:
m = recv()
if m.get('method') == 'Runtime.exceptionThrown':
console.append(('exception', m['params']['exceptionDetails'].get('text', ''), m['params']['exceptionDetails'].get('exception', {}).get('description', '')[:200]))
elif m.get('method') == 'Runtime.consoleAPICalled' and m['params']['type'] in ('error', 'warning'):
console.append((m['params']['type'], ' '.join(str(a.get('value', a.get('description', ''))) for a in m['params']['args'])[:200]))
elif m.get('method') == 'Log.entryAdded' and m['params']['entry']['level'] in ('error', 'warning'):
e = m['params']['entry']; console.append(('log-' + e['level'], e.get('text', '')[:200], e.get('url', '')))
if m.get('id') == i:
if 'error' in m: raise RuntimeError(f'{method}: {m["error"]}')
return m.get('result', {})
def ev(expr):
r = call('Runtime.evaluate', expression=expr, returnByValue=True, awaitPromise=True)
return r.get('result', {}).get('value')
def goto(url):
call('Page.navigate', url=url)
for _ in range(100):
time.sleep(0.2)
if ev('document.readyState') == 'complete':
break
time.sleep(1.5) # images' onerror, layout
for d in ('Page', 'Runtime', 'Log', 'Network'):
call(d + '.enable')
pw = open(PWFILE).read().strip()
goto(BASE + '/login')
ev("(function(){var f=document.querySelector('input[type=password]'); f.value=%s; f.form.submit(); return 1})()" % json.dumps(pw))
del pw
time.sleep(3)
print('after login url:', ev('location.pathname'))
console.clear() # the login page is not under test
report = {}
for tab in ['settings', 'data', 'guide', 'logs']:
n0 = len(console)
goto(f'{BASE}/apps/{SLUG}/{tab}')
res = ev('''(function(){
function cs(el, props){ if(!el) return null; var s=getComputedStyle(el), o={}; props.forEach(function(p){o[p]=s.getPropertyValue(p)}); return o; }
var secs=[].slice.call(document.querySelectorAll('.app-section')).filter(function(e){return e.offsetParent!==null});
var col=document.querySelector('.app-page');
var t=document.querySelector('.app-section h3'); var first=secs[0];
var gap = (t && first) ? Math.round(t.getBoundingClientRect().top - t.closest('.app-section').getBoundingClientRect().top) : null;
var widths = secs.map(function(e){return Math.round(e.getBoundingClientRect().width)});
var spacing = []; for (var i=1;i<secs.length;i++){ spacing.push(Math.round(secs[i].getBoundingClientRect().top - secs[i-1].getBoundingClientRect().bottom)); }
return {path: location.pathname,
activeTab: cs(document.querySelector('.app-tab-active'), ['background-color','color','border-bottom-color','border-bottom-width','padding-top','padding-left']),
tabBar: cs(document.querySelector('.app-tabs'), ['background-color','border-top-color','padding-top']),
hero: cs(document.querySelector('.app-hero'), ['background-color','border-top-color','padding-top','padding-left']),
section: cs(first, ['background-color','border-top-color','border-top-width','padding-top','padding-left','margin-bottom','border-radius']),
visibleSections: secs.length, sectionWidths: widths, columnWidth: col ? Math.round(col.getBoundingClientRect().width) : null,
sectionSpacing: spacing, titleTopGap: gap,
loginPanel: !!document.querySelector('[data-login-panel]'),
loginFields: [].map.call(document.querySelectorAll('[data-login-field]'), function(e){return e.getAttribute('data-login-field')}),
notStored: [].map.call(document.querySelectorAll('[data-login-not-stored]'), function(e){return e.getAttribute('data-login-not-stored')}),
reveals: [].map.call(document.querySelectorAll('[data-login-reveal]'), function(e){return e.getAttribute('data-login-reveal')}),
loginPairCols: (function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})(),
domainInput: !!document.querySelector('#auto-field-DOMAIN, #field-DOMAIN'), deployForm: !!document.querySelector('#deploy-form'),
logBoxWidth: (function(){var l=document.querySelector('.logs-container'); return l?Math.round(l.getBoundingClientRect().width):null})()
};})()''')
res['console'] = console[n0:]
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
open(os.path.join(OUT, f'{SLUG}-{tab}.png'), 'wb').write(base64.b64decode(shot['data']))
report[tab] = res
# phone width: the login pair stacks
call('Emulation.setDeviceMetricsOverride', width=390, height=844, deviceScaleFactor=1, mobile=True)
goto(f'{BASE}/apps/{SLUG}/settings')
report['settings_390px'] = {'loginPairCols': ev("(function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})()"),
'scrollWidth': ev('document.documentElement.scrollWidth')}
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
open(os.path.join(OUT, f'{SLUG}-settings-390px.png'), 'wb').write(base64.b64decode(shot['data']))
print(json.dumps(report, indent=1, ensure_ascii=False))
finally:
proc.terminate()
subprocess.run(['rm', '-rf', prof])
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Minimal CDP client: log in to the controller through its PUBLIC address in headless Chrome, open each
app-page tab, assert computed styles, collect console errors, save screenshots. The password is read
from a 0600 file and only ever sent inside a CDP message; it is never printed."""
import base64, json, os, socket, struct, subprocess, sys, time, urllib.request
CHROME = os.path.expanduser('~/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell')
BASE = sys.argv[1] # https://felhom.enkisfelhom.hu
SLUG = sys.argv[2] # calibre-web
PWFILE = sys.argv[3]
OUT = sys.argv[4]
PORT = 9333
os.makedirs(OUT, exist_ok=True)
prof = os.path.join(OUT, '.profile')
proc = subprocess.Popen([CHROME, f'--remote-debugging-port={PORT}', '--no-sandbox', f'--user-data-dir={prof}',
'--window-size=1280,900', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
for _ in range(50):
try:
tabs = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json'))
break
except Exception:
time.sleep(0.2)
ws_url = [t for t in tabs if t['type'] == 'page'][0]['webSocketDebuggerUrl']
host, path = ws_url[len('ws://'):].split('/', 1)
h, p = host.split(':')
sock = socket.create_connection((h, int(p)))
key = base64.b64encode(os.urandom(16)).decode()
sock.send(f'GET /{path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode())
buf = b''
while b'\r\n\r\n' not in buf:
buf += sock.recv(4096)
st = {'pending': buf.split(b'\r\n\r\n', 1)[1]}
def send(obj):
data = json.dumps(obj).encode()
hdr = bytearray([0x81])
n = len(data)
if n < 126: hdr.append(0x80 | n)
elif n < 65536: hdr.append(0x80 | 126); hdr += struct.pack('>H', n)
else: hdr.append(0x80 | 127); hdr += struct.pack('>Q', n)
mask = os.urandom(4); hdr += mask
sock.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
while len(st['pending']) < n:
st['pending'] += sock.recv(1 << 20)
out, st['pending'] = st['pending'][:n], st['pending'][n:]
return out
def recv():
msg = b''
while True:
b0, b1 = recv_exact(2)
n = b1 & 0x7f
if n == 126: n = struct.unpack('>H', recv_exact(2))[0]
elif n == 127: n = struct.unpack('>Q', recv_exact(8))[0]
msg += recv_exact(n)
if b0 & 0x80: return json.loads(msg)
console = []
mid = [0]
def call(method, **params):
mid[0] += 1; i = mid[0]
send({'id': i, 'method': method, 'params': params})
while True:
m = recv()
if m.get('method') == 'Runtime.exceptionThrown':
console.append(('exception', m['params']['exceptionDetails'].get('text', ''), m['params']['exceptionDetails'].get('exception', {}).get('description', '')[:200]))
elif m.get('method') == 'Runtime.consoleAPICalled' and m['params']['type'] in ('error', 'warning'):
console.append((m['params']['type'], ' '.join(str(a.get('value', a.get('description', ''))) for a in m['params']['args'])[:200]))
elif m.get('method') == 'Log.entryAdded' and m['params']['entry']['level'] in ('error', 'warning'):
e = m['params']['entry']; console.append(('log-' + e['level'], e.get('text', '')[:200], e.get('url', '')))
if m.get('id') == i:
if 'error' in m: raise RuntimeError(f'{method}: {m["error"]}')
return m.get('result', {})
def ev(expr):
r = call('Runtime.evaluate', expression=expr, returnByValue=True, awaitPromise=True)
return r.get('result', {}).get('value')
def goto(url):
call('Page.navigate', url=url)
for _ in range(100):
time.sleep(0.2)
if ev('document.readyState') == 'complete':
break
time.sleep(1.5) # images' onerror, layout
for d in ('Page', 'Runtime', 'Log', 'Network'):
call(d + '.enable')
pw = open(PWFILE).read().strip()
goto(BASE + '/login')
ev("(function(){var f=document.querySelector('input[type=password]'); f.value=%s; f.form.submit(); return 1})()" % json.dumps(pw))
del pw
time.sleep(3)
print('after login url:', ev('location.pathname'))
console.clear() # the login page is not under test
report = {}
for tab in ['install']:
n0 = len(console)
goto(f'{BASE}/apps/{SLUG}/{tab}')
res = ev('''(function(){
function cs(el, props){ if(!el) return null; var s=getComputedStyle(el), o={}; props.forEach(function(p){o[p]=s.getPropertyValue(p)}); return o; }
var secs=[].slice.call(document.querySelectorAll('.app-section, .deploy-form .form-section')).filter(function(e){return e.offsetParent!==null});
var col=document.querySelector('.app-page');
var t=document.querySelector('.deploy-form .form-section h4'); var first=secs[0];
var gap = (t && first) ? Math.round(t.getBoundingClientRect().top - t.closest('.app-section, .form-section').getBoundingClientRect().top) : null;
var widths = secs.map(function(e){return Math.round(e.getBoundingClientRect().width)});
var spacing = []; for (var i=1;i<secs.length;i++){ spacing.push(Math.round(secs[i].getBoundingClientRect().top - secs[i-1].getBoundingClientRect().bottom)); }
return {path: location.pathname,
activeTab: cs(document.querySelector('.app-tab-active'), ['background-color','color','border-bottom-color','border-bottom-width','padding-top','padding-left']),
tabBar: cs(document.querySelector('.app-tabs'), ['background-color','border-top-color','padding-top']),
hero: cs(document.querySelector('.app-hero'), ['background-color','border-top-color','padding-top','padding-left']),
section: cs(first, ['background-color','border-top-color','border-top-width','padding-top','padding-left','margin-bottom','border-radius']),
visibleSections: secs.length, sectionWidths: widths, columnWidth: col ? Math.round(col.getBoundingClientRect().width) : null,
sectionSpacing: spacing, titleTopGap: gap,
loginPanel: !!document.querySelector('[data-login-panel]'),
loginFields: [].map.call(document.querySelectorAll('[data-login-field]'), function(e){return e.getAttribute('data-login-field')}),
notStored: [].map.call(document.querySelectorAll('[data-login-not-stored]'), function(e){return e.getAttribute('data-login-not-stored')}),
reveals: [].map.call(document.querySelectorAll('[data-login-reveal]'), function(e){return e.getAttribute('data-login-reveal')}),
loginPairCols: (function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})(),
domainInput: !!document.querySelector('#auto-field-DOMAIN, #field-DOMAIN'), deployForm: !!document.querySelector('#deploy-form'),
logBoxWidth: (function(){var l=document.querySelector('.logs-container'); return l?Math.round(l.getBoundingClientRect().width):null})()
};})()''')
res['console'] = console[n0:]
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
open(os.path.join(OUT, f'{SLUG}-{tab}.png'), 'wb').write(base64.b64decode(shot['data']))
report[tab] = res
print(json.dumps(report, indent=1, ensure_ascii=False)); raise SystemExit
call('Emulation.setDeviceMetricsOverride', width=390, height=844, deviceScaleFactor=1, mobile=True)
goto(f'{BASE}/apps/{SLUG}/settings')
report['settings_390px'] = {'loginPairCols': ev("(function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})()"),
'scrollWidth': ev('document.documentElement.scrollWidth')}
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
open(os.path.join(OUT, f'{SLUG}-settings-390px.png'), 'wb').write(base64.b64decode(shot['data']))
print(json.dumps(report, indent=1, ensure_ascii=False))
finally:
proc.terminate()
subprocess.run(['rm', '-rf', prof])
@@ -0,0 +1,2 @@
romm rommapp/romm:5.3.1 ['/redis-data', '/romm']
privatebin privatebin/pdo:2.0.6 ['/run', '/tmp', '/var/lib/nginx/tmp']
@@ -0,0 +1,7 @@
customers reporting RomM telemetry, with their newest report (hub DB + WAL, read 2026-10-16):
demo-felhom rows 1 newest 2026-10-13 09:37:46
demo-hp rows 4450 newest 2026-10-16 16:32:25
customers reporting ANY app in the last 2 hours:
demo-felhom
demo-hp
tester-1
@@ -0,0 +1,18 @@
2026/10/16 13:47:23 main.go:814: [INFO] Self-update enabled (check every 6h, auto-update: false, auto-update time: 04:30)
2026/10/16 14:51:36 night_chain.go:108: [INFO] [night-chain] manual run started from 172.18.0.4:40328: [db-dump tier2 offsite update-leg]
2026/10/16 14:51:36 night_chain.go:84: [INFO] [night-chain] manual run of tonight's chain: dump → second drive → off-site → update leg
2026/10/16 14:51:36 night_chain.go:77: [INFO] [night-chain] db-dump: started
2026/10/16 14:53:42 night_chain.go:82: [INFO] [night-chain] db-dump: done in 2m6s
2026/10/16 14:53:42 night_chain.go:77: [INFO] [night-chain] tier2: started
2026/10/16 14:54:10 night_chain.go:82: [INFO] [night-chain] tier2: done in 28s
2026/10/16 14:54:10 night_chain.go:77: [INFO] [night-chain] offsite: started
2026/10/16 14:57:08 night_chain.go:82: [INFO] [night-chain] offsite: done in 2m58s
2026/10/16 14:57:08 night_chain.go:77: [INFO] [night-chain] update-leg: started
2026/10/16 14:57:08 unattended.go:270: [INFO] [update-leg] started (manual-chain): window 02:30, no step starts at or after 20:12
2026/10/16 14:57:08 unattended.go:316: [INFO] [update-leg] kimai: skipped — held
2026/10/16 14:57:08 unattended.go:248: [INFO] [update-leg] update leg (manual-chain): done=0 undone=0 held=0 failed=0 skipped=1 in 0s [skipped: kimai=held]
2026/10/16 14:57:08 night_chain.go:82: [INFO] [night-chain] update-leg: done in 0s
2026/10/16 14:57:08 night_chain.go:93: [INFO] [night-chain] finished in 5m32s
romm Up About a minute (healthy)
romm-redis Up About a minute (healthy)
romm-db Up About a minute (healthy)
@@ -0,0 +1,40 @@
== mounts of the running romm container
volume <ANONYMOUS 64-hex> -> /redis-data
volume <ANONYMOUS 64-hex> -> /romm
volume romm_romm_config -> /romm/config
bind -> /romm/resources
bind -> /romm/library/bios
bind -> /romm/library/roms
== compose volumes
167:volumes:
168- romm_config:
169- romm_db_data:
170- romm_redis_data:
171-
172-networks:
173- traefik-public:
18:# Cover art etc → ${HDD_PATH}/appdata/romm/resources (app-internal, NOT browsable)
29: image: rommapp/romm:5.3.1@sha256:0d66b4ea152a237c7f24b95e87459f46d3c596a7c0b35a82f26bc971487278b3
40: if [ ! -f /romm/config/config.yml ]; then
42: cat > /romm/config/config.yml << 'CONF'
80: # on demo-hp 9201 (R-1089, felhom.eu audits/romm-ghost-2026-10-16/A1). Now roms/<platform>/ IS
83: - ${USERDATA_PATH}/roms:/romm/library/roms
84: - ${USERDATA_PATH}/roms-bios:/romm/library/bios
85: - ${HDD_PATH}/appdata/romm/resources:/romm/resources
86: - romm_config:/romm/config
== what earlier app backups of romm hold
/mnt/felhom-drives/hdd_1/kept/romm/2026-10-16_113539/unit/volume-dumps
total 203084
drwxr-xr-x 2 root root 4096 Oct 4 05:35 .
drwxr-xr-x 5 root root 4096 Oct 4 11:18 ..
-rw-r--r-- 1 root root 2560 Oct 4 05:35 romm_romm_config.tar
-rw-r--r-- 1 root root 163786752 Oct 4 05:35 romm_romm_db_data.tar
-rw-r--r-- 1 root root 44148736 Oct 4 05:35 romm_romm_redis_data.tar
== anonymous volume contents
assets
config
library
resources
/var/lib/docker/volumes/567f3e5915421b542fc2e67d3195d03b32ef07ef79f2851c6198653cf29e00f6/_data/assets
28K /var/lib/docker/volumes/567f3e5915421b542fc2e67d3195d03b32ef07ef79f2851c6198653cf29e00f6/_data
@@ -0,0 +1,49 @@
== 1. sync the catalog
sync -> 200 {'ok': True, 'data': {'ok': True, 'updated': ['code-server', 'gokapi', 'grafana', 'kimai', 'nextcloud', 'nzbget', 'prowlarr', 'romm', 'transmission'], 'message': 'Sablonok frissítve — frissítve: code-
applied compose names romm_assets: 3
== 2. restart RomM (Újraindítás)
restart -> 200 {'ok': True, 'data': {'state': 'starting'}, 'message': 'Stack romm restart requested — state now: starting'}
state: running
mounts after restart:
volume <ANONYMOUS> -> /redis-data
volume <ANONYMOUS> -> /romm
volume romm_romm_assets -> /romm/assets
volume romm_romm_config -> /romm/config
bind -> /romm/resources
bind -> /romm/library/bios
bind -> /romm/library/roms
== 3. a save where RomM writes saves (dummy bytes, as the app's own user)
drwxr-xr-x 3 root root 4096 Oct 16 16:51 ..
-rw-r--r-- 1 root root 32768 Oct 16 16:51 r1097-proof.sav
4a55d00c3ee6bccd55649e48fd19d84703f755f84474ac0b6a41299b114ee6a5 /romm/assets/romm/users/1/saves/gba/r1097-proof/r1097-proof.sav
== 4. the by-day backup (night chain)
night-chain -> 202 {'data': {'legs': ['db-dump', 'tier2', 'offsite', 'update-leg']}, 'message': 'started', 'ok': True}
night chain last line: 2026/10/16 14:57:08 night_chain.go:93: [INFO] [night-chain] finished in 5m32s after 342s
romm volume dumps now: /mnt/felhom-drives/hdd_1/backups/primary/romm/volume-dumps/:
total 176584
drwxr-xr-x 2 root root 4096 Oct 16 14:56 .
drwxr-xr-x 5 root root 4096 Oct 16 14:56 ..
-rw-r--r-- 1 root root 37888 Oct 16 14:56 romm_romm_assets.tar
-rw-r--r-- 1 root root 2560 Oct 16 14:56 romm_romm_config.tar
-rw-r--r-- 1 root root 162300416 Oct 16 14:56 romm_romm_db_data.tar
-rw-r--r-- 1 root root 18463744 Oct 16 14:56 romm_romm_redis_data.tar
/mnt/felhom-drives/hdd_1/backups/primary/romm/volume-dumps/romm_romm_assets.tar
== 5. remove RomM, KEEPING its backups
remove -> 200 {'ok': True, 'data': {'removed': 'romm', 'volumes_removed': ['romm_romm_assets', 'romm_romm_config', 'romm_romm_db_data', 'romm_romm_redis_data'], 'hdd_paths_removed': [], 'hdd_paths_preserved': ['/mn
volumes left: romm_romm_redis_data.pre-aof-fix-20260924T135937Z
== 6. restore from the backups page
restore: {'ok': True, 'snapshot_id': 'helyi', 'http': 'HTTP/2 302', 'seconds': 77.0}
state: running
mounts after restore:
volume <ANONYMOUS> -> /redis-data
volume <ANONYMOUS> -> /romm
volume romm_romm_assets -> /romm/assets
volume romm_romm_config -> /romm/config
bind -> /romm/resources
bind -> /romm/library/bios
bind -> /romm/library/roms
drwxr-xr-x 3 root root 4096 Oct 16 16:58 ..
-rw-r--r-- 1 root root 32768 Oct 16 16:51 r1097-proof.sav
4a55d00c3ee6bccd55649e48fd19d84703f755f84474ac0b6a41299b114ee6a5 /romm/assets/romm/users/1/saves/gba/r1097-proof/r1097-proof.sav
== VERDICT: the save came back byte-identical: True
@@ -0,0 +1,6 @@
tables with customer: ['customer_notifications', 'customer_configs', 'customer_claims', 'customer_resets']
every customer's NEWEST report (hub DB + WAL):
demo-hp 0.320.0 2026-10-16 14:58:09
tester-1 0.320.0 2026-10-16 14:49:33
demo-felhom 0.320.0 2026-10-16 14:49:28
Tester-2 0.292.0 2026-10-04 18:06:24
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,60 @@
rm: cannot remove '/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/burndown-apps-2026-10-16/browser/.profile/Default': Directory not empty
after login url: /launcher
{
"/apps/calibre-web/data": {
"path": "/apps/calibre-web/data",
"backupSections": 1,
"rows": [
"nightly",
"second",
"target"
],
"offDrivePBS": false,
"valueFont": "\"Plus Jakarta Sans\"",
"exportToggle": [
"Megjelenítés",
"Jelszó mutatása"
],
"fbReveal": false,
"fbStale": false,
"changeItLine": false,
"scrollWidth": 1270,
"console": []
},
"/apps/filebrowser": {
"path": "/apps/filebrowser",
"backupSections": 0,
"rows": [],
"offDrivePBS": false,
"valueFont": null,
"exportToggle": null,
"fbReveal": true,
"fbStale": false,
"changeItLine": false,
"scrollWidth": 1280,
"console": []
},
"/apps/calibre-web/install": {
"path": "/apps/calibre-web/settings",
"backupSections": 0,
"rows": [],
"offDrivePBS": false,
"valueFont": null,
"exportToggle": null,
"fbReveal": false,
"fbStale": false,
"changeItLine": false,
"scrollWidth": 1270,
"console": []
},
"filebrowser": {
"path": "/files/Beolvas%C3%A1s",
"updateBanner": false,
"installApp": false,
"releaseBanners": 0,
"globals": {
"disablePWAInstall": true,
"updateAvailable": ""
}
}
}
@@ -0,0 +1,22 @@
control: bookstack (keeps default_creds):
302 known-login markers: 0
grafana 200 known-login=0 answer=
grafana/install 200 known-login=0 answer=
gokapi 200 known-login=0 answer=
gokapi/install 200 known-login=0 answer=
kimai 200 known-login=0 answer=data-login-answer="fields"
kimai/install 302 known-login=0 answer=
code-server 200 known-login=0 answer=
code-server/install 200 known-login=0 answer=
nzbget 200 known-login=0 answer=
nzbget/install 200 known-login=0 answer=
nextcloud 200 known-login=0 answer=
nextcloud/install 200 known-login=0 answer=
transmission 200 known-login=0 answer=
transmission/install 200 known-login=0 answer=
prowlarr 200 known-login=0 answer=
prowlarr/install 200 known-login=0 answer=
control bookstack: 200 known-login=0 alapertelmezett=0
control wger: 200 known-login=0 alapertelmezett=0
control mealie: 200 known-login=0 alapertelmezett=0
control claper: 200 known-login=0 alapertelmezett=0
@@ -0,0 +1,3 @@
FAIL unreachable (000) -> refused (exit 1)
FAIL server error (500) -> refused (exit 1)
2 of 5 cases FAILED
@@ -0,0 +1,4 @@
XX DECOY: the PARENT /romm mounted rc=0 (expected 1)
XX DECOY: a CHILD /romm/assets/x mounted rc=0 (expected 1)
AssertionError: Lists differ: [' default_creds: "admin / x"\n'] != []
FAILED (failures=1)
@@ -0,0 +1,26 @@
## refused = any non-200 (FileBrowser down reads as refused)
=== RUN TestFileBrowserStoredPassword_RefusedOnlyOnA401
filebrowser_reset_test.go:182: FileBrowser down reads as refused — an unproven refusal must not tell the household its password changed
--- FAIL: TestFileBrowserStoredPassword_RefusedOnlyOnA401 (0.00s)
## no stored-password check on the page
=== RUN TestFileBrowserLogin_StoredPasswordRefusedOffersReset
logins_folders_test.go:365: stale: the page still offers the reveal of a password FileBrowser refuses
--- FAIL: TestFileBrowserLogin_StoredPasswordRefusedOffersReset (0.08s)
## the change-it line back on FileBrowser
=== RUN TestFileBrowserLogin_StoredPasswordRefusedOffersReset
logins_folders_test.go:380: FileBrowser still says to change the password after the first login (R-1101)
--- FAIL: TestFileBrowserLogin_StoredPasswordRefusedOffersReset (0.08s)
## no redirect for an installed app's /install
=== RUN TestInstallAddress_InstalledRedirectsToSettings
logins_folders_test.go:432: installed: 200 → "", want 302 → /apps/calibre-web/settings
--- FAIL: TestInstallAddress_InstalledRedirectsToSettings (0.08s)
## off-drive back in a Data tab string (hu)
=== RUN TestAppData_OneBackupSection
## a second backup section
=== RUN TestAppData_OneBackupSection
logins_folders_test.go:408: hu: 2 backup sections, want 1
logins_folders_test.go:408: en: 2 backup sections, want 1
--- FAIL: TestAppData_OneBackupSection (0.10s)
## (redo) off-drive/PBS back in the always-shown 2nd-copy text (hu)
logins_folders_test.go:415: hu: off-drive/PBS on the Data tab ("off-drive")
--- FAIL: TestAppData_OneBackupSection (0.09s)
@@ -0,0 +1,6 @@
FAIL --push, tag present -> refuses: exit 0, want 1
FAIL --push, tag absent -> proceeds: no 'is not present in the registry'
FAIL --multiarch, tag present -> refuses: exit 0, want 1
FAIL --push, registry unreachable -> refuses: exit 0, want 1
FAIL --push, auth failure -> refuses: exit 0, want 1
5 of 6 cases FAILED
@@ -0,0 +1,15 @@
== tester-1
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.321.0 Up 18 seconds (healthy)
filebrowser Up 16 seconds (healthy)
2
== demo-hp
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.321.0 Up 21 seconds (healthy)
filebrowser Up 3 minutes (healthy)
2
== demo-felhom
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.321.0 Up 25 seconds (healthy)
filebrowser Up 23 seconds (healthy)
2
@@ -0,0 +1,3 @@
tester-1 Location: /customers/tester-1?flash=floor_set
demo-hp Location: /customers/demo-hp?flash=floor_set
demo-felhom Location: /customers/demo-felhom?flash=floor_set
@@ -0,0 +1,3 @@
demo-hp Location: /customers/demo-hp?flash=floor_set
tester-1 Location: /customers/tester-1?flash=floor_set
demo-felhom Location: /customers/demo-felhom?flash=floor_set
@@ -0,0 +1,3 @@
felhom.enkisfelhom.hu 200
felhom.enkicsifelhom.hu 200
felhom.demo-felhom.eu 200
@@ -0,0 +1,3 @@
17:22:20
HTTP/1.1 303 See Other
Location: /configuration?flash=floor_set
@@ -0,0 +1,40 @@
control NEGATIVE (a stub answering 200 over plain http): convicted, as it must (200 -)
== tester-1 (enkicsifelhom.hu): 13 public name(s)
OK control POSITIVE http://felhom.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkicsifelhom.hu/r1033-check?q=1
OK http://files.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://files.enkicsifelhom.hu/r1033-check?q=1
OK http://media.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://media.enkicsifelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1
OK http://paperless.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paperless.enkicsifelhom.hu/r1033-check?q=1
OK http://paste.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paste.enkicsifelhom.hu/r1033-check?q=1
OK http://plex.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://plex.enkicsifelhom.hu/r1033-check?q=1
OK http://prowlarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://prowlarr.enkicsifelhom.hu/r1033-check?q=1
OK http://radarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://radarr.enkicsifelhom.hu/r1033-check?q=1
OK http://sonarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://sonarr.enkicsifelhom.hu/r1033-check?q=1
OK http://torrent.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://torrent.enkicsifelhom.hu/r1033-check?q=1
OK http://transmission.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://transmission.enkicsifelhom.hu/r1033-check?q=1
OK http://wiki.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkicsifelhom.hu/r1033-check?q=1
== demo-hp (enkisfelhom.hu): 14 public name(s)
OK http://arcade.enkisfelhom.hu/r1033-check?q=1 -> 301 https://arcade.enkisfelhom.hu/r1033-check?q=1
OK http://books.enkisfelhom.hu/r1033-check?q=1 -> 301 https://books.enkisfelhom.hu/r1033-check?q=1
OK http://docs.enkisfelhom.hu/r1033-check?q=1 -> 301 https://docs.enkisfelhom.hu/r1033-check?q=1
OK http://felhom.enkisfelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkisfelhom.hu/r1033-check?q=1
OK http://files.enkisfelhom.hu/r1033-check?q=1 -> 301 https://files.enkisfelhom.hu/r1033-check?q=1
OK http://gist.enkisfelhom.hu/r1033-check?q=1 -> 301 https://gist.enkisfelhom.hu/r1033-check?q=1
OK http://ha.enkisfelhom.hu/r1033-check?q=1 -> 301 https://ha.enkisfelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1
OK http://papir.enkisfelhom.hu/r1033-check?q=1 -> 301 https://papir.enkisfelhom.hu/r1033-check?q=1
OK http://paste.enkisfelhom.hu/r1033-check?q=1 -> 301 https://paste.enkisfelhom.hu/r1033-check?q=1
OK http://pdf.enkisfelhom.hu/r1033-check?q=1 -> 301 https://pdf.enkisfelhom.hu/r1033-check?q=1
OK http://plex.enkisfelhom.hu/r1033-check?q=1 -> 301 https://plex.enkisfelhom.hu/r1033-check?q=1
OK http://travel.enkisfelhom.hu/r1033-check?q=1 -> 301 https://travel.enkisfelhom.hu/r1033-check?q=1
OK http://wiki.enkisfelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkisfelhom.hu/r1033-check?q=1
== demo-felhom (demo-felhom.eu): 8 public name(s)
OK http://felhom.demo-felhom.eu/r1033-check?q=1 -> 301 https://felhom.demo-felhom.eu/r1033-check?q=1
OK http://files.demo-felhom.eu/r1033-check?q=1 -> 301 https://files.demo-felhom.eu/r1033-check?q=1
OK http://gist.demo-felhom.eu/r1033-check?q=1 -> 301 https://gist.demo-felhom.eu/r1033-check?q=1
OK http://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1 -> 301 https://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1
OK http://prowlarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://prowlarr.demo-felhom.eu/r1033-check?q=1
OK http://radarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://radarr.demo-felhom.eu/r1033-check?q=1
OK http://sonarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://sonarr.demo-felhom.eu/r1033-check?q=1
OK http://torrent.demo-felhom.eu/r1033-check?q=1 -> 301 https://torrent.demo-felhom.eu/r1033-check?q=1
== every public name redirects http -> https
@@ -0,0 +1,7 @@
FIRST READ (hub.db, -wal, -shm copied ONE BY ONE with kubectl exec cat) — STALE:
('min_controller_version', '0.320.0', '2026-10-16 13:48:34')
('min_controller_version_declared_min_agent', '0.320.0=0.158.1', '2026-10-16 13:48:34')
SECOND READ (one tar of the three files) — 17:23:
('min_controller_version', '0.321.0', '2026-10-16 15:22:20')
('min_controller_version_declared_min_agent', '0.321.0=0.158.1', '2026-10-16 15:22:20')
THIRD CHANNEL — GET /configuration with CC's token: the page shows 0.321.0
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""v0.321.0 apps-burndown browser check (Data tab, FileBrowser page and FileBrowser itself, an install address). Minimal CDP client: log in to the controller through its PUBLIC address in headless Chrome, open each
app-page tab, assert computed styles, collect console errors, save screenshots. The password is read
from a 0600 file and only ever sent inside a CDP message; it is never printed."""
import base64, json, os, socket, struct, subprocess, sys, time, urllib.request
CHROME = os.path.expanduser('~/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell')
BASE = sys.argv[1] # https://felhom.enkisfelhom.hu
PWFILE = sys.argv[2] # the controller password (0600 file)
FBPWFILE = sys.argv[3] # FileBrowser's admin password (0600 file) — for the cold „Fajlok" click
OUT = sys.argv[4]
PORT = 9333
os.makedirs(OUT, exist_ok=True)
prof = os.path.join(OUT, '.profile')
proc = subprocess.Popen([CHROME, f'--remote-debugging-port={PORT}', '--no-sandbox', f'--user-data-dir={prof}',
'--window-size=1280,900', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
for _ in range(50):
try:
tabs = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json'))
break
except Exception:
time.sleep(0.2)
ws_url = [t for t in tabs if t['type'] == 'page'][0]['webSocketDebuggerUrl']
host, path = ws_url[len('ws://'):].split('/', 1)
h, p = host.split(':')
sock = socket.create_connection((h, int(p)))
key = base64.b64encode(os.urandom(16)).decode()
sock.send(f'GET /{path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode())
buf = b''
while b'\r\n\r\n' not in buf:
buf += sock.recv(4096)
st = {'pending': buf.split(b'\r\n\r\n', 1)[1]}
def send(obj):
data = json.dumps(obj).encode()
hdr = bytearray([0x81])
n = len(data)
if n < 126: hdr.append(0x80 | n)
elif n < 65536: hdr.append(0x80 | 126); hdr += struct.pack('>H', n)
else: hdr.append(0x80 | 127); hdr += struct.pack('>Q', n)
mask = os.urandom(4); hdr += mask
sock.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
while len(st['pending']) < n:
st['pending'] += sock.recv(1 << 20)
out, st['pending'] = st['pending'][:n], st['pending'][n:]
return out
def recv():
msg = b''
while True:
b0, b1 = recv_exact(2)
n = b1 & 0x7f
if n == 126: n = struct.unpack('>H', recv_exact(2))[0]
elif n == 127: n = struct.unpack('>Q', recv_exact(8))[0]
msg += recv_exact(n)
if b0 & 0x80: return json.loads(msg)
console = []
mid = [0]
def call(method, **params):
mid[0] += 1; i = mid[0]
send({'id': i, 'method': method, 'params': params})
while True:
m = recv()
if m.get('method') == 'Runtime.exceptionThrown':
console.append(('exception', m['params']['exceptionDetails'].get('text', ''), m['params']['exceptionDetails'].get('exception', {}).get('description', '')[:200]))
elif m.get('method') == 'Runtime.consoleAPICalled' and m['params']['type'] in ('error', 'warning'):
console.append((m['params']['type'], ' '.join(str(a.get('value', a.get('description', ''))) for a in m['params']['args'])[:200]))
elif m.get('method') == 'Log.entryAdded' and m['params']['entry']['level'] in ('error', 'warning'):
e = m['params']['entry']; console.append(('log-' + e['level'], e.get('text', '')[:200], e.get('url', '')))
if m.get('id') == i:
if 'error' in m: raise RuntimeError(f'{method}: {m["error"]}')
return m.get('result', {})
def ev(expr):
r = call('Runtime.evaluate', expression=expr, returnByValue=True, awaitPromise=True)
return r.get('result', {}).get('value')
def goto(url):
call('Page.navigate', url=url)
for _ in range(100):
time.sleep(0.2)
if ev('document.readyState') == 'complete':
break
time.sleep(1.5) # images' onerror, layout
for d in ('Page', 'Runtime', 'Log', 'Network'):
call(d + '.enable')
pw = open(PWFILE).read().strip()
goto(BASE + '/login')
ev("(function(){var f=document.querySelector('input[type=password]'); f.value=%s; f.form.submit(); return 1})()" % json.dumps(pw))
del pw
time.sleep(3)
print('after login url:', ev('location.pathname'))
console.clear() # the login page is not under test
def shot(name):
s = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
open(os.path.join(OUT, name + '.png'), 'wb').write(base64.b64decode(s['data']))
report = {}
for p in ['/apps/calibre-web/data', '/apps/filebrowser', '/apps/calibre-web/install']:
n0 = len(console)
goto(BASE + p)
report[p] = ev("""(function(){ return {path: location.pathname,
backupSections: document.querySelectorAll('[data-backup-section]').length,
rows: [].map.call(document.querySelectorAll('[data-backup-row]'), function(e){return e.getAttribute('data-backup-row')}),
offDrivePBS: /off-drive|\\bPBS\\b/i.test(document.body.innerText),
valueFont: (function(){var v=document.querySelector('[data-backup-row] .app-field-value'); return v?getComputedStyle(v).fontFamily.split(',')[0]:null})(),
exportToggle: (function(){var b=document.getElementById('exportPwToggle'); return b?[b.textContent.trim(), b.getAttribute('aria-label')]:null})(),
fbReveal: !!document.querySelector('#initcred-reveal'), fbStale: !!document.querySelector('[data-fb-stale]'),
changeItLine: document.body.innerText.indexOf('hagyd figyelmen') >= 0,
scrollWidth: document.documentElement.scrollWidth};})()""")
report[p]['console'] = console[n0:]
shot(p.strip('/').replace('/', '-'))
# FileBrowser itself, logged in with the box's own password: no update banner, no install-app prompt.
fbpw = open(FBPWFILE).read().strip()
call('Network.clearBrowserCookies')
goto(BASE.replace('://felhom.', '://files.') + '/')
ev("""(function(pw){var u=document.querySelector('input[type=text],input[name=username],input#username');
var p=document.querySelector('input[type=password]'); if(!u||!p) return 0;
function set(el,v){var d=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value'); d.set.call(el,v); el.dispatchEvent(new Event('input',{bubbles:true}));}
set(u,'admin'); set(p,pw); var b=document.querySelector('form button[type=submit], form input[type=submit], button.button--blue');
if(b){b.click()} else {p.form && p.form.requestSubmit && p.form.requestSubmit()} return 1})(%s)""" % json.dumps(fbpw))
del fbpw
time.sleep(5)
report['filebrowser'] = ev("""(function(){ var t=document.body.innerText;
return {path: location.pathname, updateBanner: /update is available/i.test(t), installApp: /install app/i.test(t),
releaseBanners: document.querySelectorAll('.release-banner').length,
globals: (function(){ try { var g=window.globalVars||{}; return {disablePWAInstall: g.disablePWAInstall, updateAvailable: g.updateAvailable}; } catch(e) { return null; } })()};})()""")
shot('filebrowser-sidebar')
print(json.dumps(report, indent=1, ensure_ascii=False))
finally:
proc.terminate()
subprocess.run(['rm', '-rf', prof])
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""romm_saves.py — R-1097 proof on demo-hp 9201 through the product's endpoints (box_walk helpers, TARGET=9201).
Sync the catalog, restart RomM (the applied compose gains romm_assets), write a save where RomM writes saves, run the
by-day night chain (the sanctioned backup trigger), remove RomM KEEPING its backups, restore from the backups page,
and read the save back. Env: SC (0600 scratch with .ctlpw), EV. The save is a dummy file; no secret is printed."""
import os, sys, time, hashlib
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
APP = "romm"
SAVE_DIR = "/romm/assets/romm/users/1/saves/gba/r1097-proof"
SAVE = SAVE_DIR + "/r1097-proof.sav"
log = open(os.path.join(w.EV, "romm-saves.txt"), "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
def mounts():
return w.guest("docker inspect romm --format '{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{println}}{{end}}' "
"| sed -E 's/volume [0-9a-f]{64}/volume <ANONYMOUS>/'").strip()
def save_state():
return w.guest(f"docker exec romm sh -c 'ls -la {SAVE_DIR} 2>&1; sha256sum {SAVE} 2>&1' | tail -3").strip()
w.login()
say("== 1. sync the catalog")
code, d = w.ctl("POST", "/api/sync"); say("sync ->", code, str(d)[:200])
say("applied compose names romm_assets:", w.guest("grep -c romm_assets /opt/docker/stacks/romm/docker-compose.yml").strip())
say("== 2. restart RomM (Újraindítás)")
code, d = w.ctl("POST", f"/api/stacks/{APP}/restart"); say("restart ->", code, str(d)[:160])
for _ in range(40):
time.sleep(5)
if w.stack(APP).get("state") == "running":
break
say("state:", w.stack(APP).get("state")); say("mounts after restart:\n" + mounts())
say("== 3. a save where RomM writes saves (dummy bytes, written with docker exec into the running container)")
w.guest(f"docker exec romm sh -c 'mkdir -p {SAVE_DIR} && head -c 32768 /dev/urandom > {SAVE}'")
before = save_state(); say(before)
say("== 4. the by-day backup (night chain)")
t0 = time.time(); code, d = w.ctl("POST", "/api/debug/backup/night-chain"); say("night-chain ->", code, str(d)[:160])
for _ in range(180):
time.sleep(10)
tail = w.guest("docker logs --since 60m felhom-controller 2>&1 | grep 'night-chain' | tail -1").strip()
if "finished in" in tail or "REFUSED" in tail:
break
say("night chain last line:", tail[-220:], f"after {round(time.time()-t0)}s")
say("romm volume dumps now:", w.guest("ls -la /mnt/felhom-drives/hdd_1/backups/*/romm/volume-dumps/ /mnt/felhom-drives/hdd_1/*/romm/recovery-unit/volume-dumps/ 2>/dev/null; find /mnt/felhom-drives/hdd_1 -name 'romm_romm_assets.tar' -newer /proc/1 2>/dev/null | head -5"))
say("== 5. remove RomM, KEEPING its backups")
w.ctl("POST", f"/api/stacks/{APP}/stop"); time.sleep(10)
code, d = w.ctl("POST", f"/api/stacks/{APP}/remove", {"remove_hdd_data": False, "remove_backups": False})
say("remove ->", code, str(d)[:200])
time.sleep(5)
say("volumes left:", w.guest("docker volume ls --format '{{.Name}}' | grep -i romm").strip() or "(none)")
say("== 6. restore from the backups page")
r = w.restore(APP); say("restore:", {k: r.get(k) for k in ("ok", "snapshot_id", "http", "seconds")})
for _ in range(60):
time.sleep(5)
if w.stack(APP).get("state") == "running":
break
say("state:", w.stack(APP).get("state")); say("mounts after restore:\n" + mounts())
after = save_state(); say(after)
same = before.split()[-2:] == after.split()[-2:]
say("== VERDICT: the save came back byte-identical:", same)
@@ -0,0 +1,237 @@
=== demo-hp
VMID Status Lock Name
9201 running demo-hp
9202 running demo-hp-scratch
9401 stopped upgrade-harness
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.319.0
filebrowser gtstef/filebrowser:1.5.6-stable
SETTINGS:
config.yaml
docker-compose.yml
- path: "/srv/beolvasas"
name: "Beolvasás"
- path: "/srv/hdd_1"
name: "NVME 1TB"
- path: "/srv/megorzott"
name: "Megőrzött adatok"
- /mnt/felhom-drives/hdd_1/userdata:/srv/hdd_1
- /mnt/sys_drive/felhom-data/userdata/import:/srv/beolvasas
- /mnt/felhom-drives/hdd_1/appdata/romm:/srv/megorzott/hdd_1-romm:ro
- /mnt/felhom-drives/hdd_1/kept/romm/2026-10-16_120728:/srv/megorzott/hdd_1-romm-2026-10-16_120728:ro
- /mnt/felhom-drives/hdd_1/kept/romm/2026-10-16_113539:/srv/megorzott/hdd_1-romm-2026-10-16_113539:ro
=== felhom-pve
VMID Status Lock Name
9201 running demo-felhom
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.319.0
filebrowser gtstef/filebrowser:1.5.6-stable
SETTINGS:
config.yaml
docker-compose.yml
- path: "/srv/beolvasas"
name: "Beolvasás"
- path: "/srv/hdd_1"
name: "Külső tárhely (hdd_1)"
- path: "/srv/megorzott"
name: "Megőrzött adatok"
- /mnt/felhom-drives/hdd_1/userdata:/srv/hdd_1
- /mnt/sys_drive/felhom-data/userdata/import:/srv/beolvasas
- /mnt/felhom-drives/hdd_1/appdata/romm:/srv/megorzott/hdd_1-romm:ro
- /mnt/felhom-drives/hdd_1/appdata/paperless:/srv/megorzott/hdd_1-paperless:ro
- /mnt/felhom-drives/hdd_1/appdata/nextcloud:/srv/megorzott/hdd_1-nextcloud:ro
- /mnt/felhom-drives/hdd_1/appdata/immich:/srv/megorzott/hdd_1-immich:ro
=== demo-hp
/mnt->/mnt /etc/felhom-bootstrap->/etc/felhom-bootstrap /var/lib/docker/volumes/felhom-controller-data/_data->/opt/docker/felhom-controller /opt/docker/stacks->/opt/docker/stacks /var/run/docker.sock->/var/run/docker.sock
/mnt/felhom-drives/hdd_1/userdata/:
documents
downloads
import
media
roms
roms-bios
/mnt/felhom-drives/hdd_1/userdata/media/:
audiobooks
books
comics
grimmory
metube
movies
music
nzbget
photos
podcasts
torrents
transmission
tv
usenet
actualbudget
adventurelog
audiobookshelf
bentopdf
bookstack
calcom
calibre-web
claper
cloudflared
code-server
crafty-controller
dawarich
docmost
emby
filebrowser
ghost
gitea
glance
gokapi
grafana
gramps-web
grimmory
grocy
home-assistant
homebox
homepage
immich
jellyfin
karakeep
kimai
komga
lubelogger
mealie
metube
n8n
navidrome
nextcloud
nzbget
onlyoffice
opengist
outline
paperless-ngx
papra
plant-it
plex
privatebin
prowlarr
qbittorrent
radarr
radicale
rallly
recipe-importer
romm
sabnzbd
seerr
sonarr
sparkyfitness
tandoor
termix
traefik
transmission
uptime-kuma
vaultwarden
vikunja
wanderer
wger
wishlist
zipline
=== felhom-pve
/mnt->/mnt /etc/felhom-bootstrap->/etc/felhom-bootstrap /var/lib/docker/volumes/felhom-controller-data/_data->/opt/docker/felhom-controller /opt/docker/stacks->/opt/docker/stacks /var/run/docker.sock->/var/run/docker.sock
/mnt/felhom-drives/hdd_1/userdata/:
documents
downloads
import
media
roms
roms-bios
/mnt/felhom-drives/hdd_1/userdata/media/:
audiobooks
books
comics
grimmory
metube
movies
music
nzbget
photos
podcasts
torrents
transmission
tv
usenet
actualbudget
adventurelog
audiobookshelf
bentopdf
bookstack
calcom
calibre-web
claper
cloudflared
code-server
crafty-controller
dawarich
docmost
emby
filebrowser
ghost
gitea
glance
gokapi
grafana
gramps-web
grimmory
grocy
home-assistant
homebox
homepage
immich
jellyfin
karakeep
kimai
komga
lubelogger
mealie
metube
n8n
navidrome
nextcloud
nzbget
onlyoffice
opengist
outline
paperless-ngx
papra
plant-it
plex
privatebin
prowlarr
qbittorrent
radarr
radicale
rallly
recipe-importer
romm
sabnzbd
seerr
sonarr
sparkyfitness
tandoor
termix
traefik
transmission
uptime-kuma
vaultwarden
vikunja
wanderer
wger
wishlist
zipline
=== demo-hp
FILE /var/lib/docker/volumes/felhom-controller-data/_data/data/settings.json
"filebrowser_admin_state": "operator"
"filebrowser_admin_decided_at": "2026-09-15T08:52:10Z"
=== felhom-pve
FILE /var/lib/docker/volumes/felhom-controller-data/_data/data/settings.json
"filebrowser_admin_state": "operator"
"filebrowser_admin_decided_at": "2026-09-16T15:37:26Z"
@@ -0,0 +1,48 @@
# P0 — why FileBrowser showed „Telepítés" and lost its password (2026-10-16, read only)
Method: source read (controller `main` 39bf562 = v0.319.0) + the live page through
`https://felhom.enkisfelhom.hu` (curl, logged in) + names/states read with `pct exec 9201` on both boxes.
No value of any secret was read or printed; only the state word of `filebrowser_admin_state`.
## 1. Why `/apps/filebrowser` renders as not installed, with an Install button
- FileBrowser is base infrastructure: `stacks/infra.go` `EnsureBaseStack` writes `/opt/docker/stacks/filebrowser/`
(compose + config.yaml) and starts it. It never writes `app.yaml`.
- `stacks/manager.go` `ScanStacks` lists every dir under the stacks dir with a compose file. FileBrowser is in the list
with `Deployed = appCfg != nil && appCfg.Deployed` = **false** (no app.yaml) and `Protected = true`.
Its metadata is the no-`.felhom.yml` default (`Slug = "filebrowser"`, DisplayName „Filebrowser").
- Every other surface already counts `Deployed || Protected` as „on the box" (dashboard, launcher, app list —
`handlers.go` ~154/315/361, `stacks.html` L30/L76). **The v0.315.0 app page (`e66d466`) uses `found.Deployed` only**
(`web/app_page.go` `appPageHandler`): not deployed → no tab bar, the guide tab is the whole page, and the guide
renders the Install CTA (`app_guide.html` L45, `canInstall` is true for metadata with no lifecycle).
- The R-513 block in `appCommonData` (handlers.go ~L856) still sets `InitialCreds` for FileBrowser — but the new
page shows `InitialCreds` only on the SETTINGS tab, which a „not deployed" app never reaches. The old
`app_info.html` rendered `{{if .InitialCreds}}` without a Deployed gate; that is how it showed before.
- **Commit that changed it: `e66d466` (v0.315.0, one app page with four tabs).**
- Extra finding: `DeployStack` has NO protected-stack guard (it refuses only Deployed / not installable). The Install
button led to a wizard whose POST would have deployed the infra stack as an app. Guarded in this task.
Live confirmation (demo-hp 9201, controller 0.319.0): `demo-hp-apps-filebrowser-markers.txt` — the page carries
`data-install-cta`, „Telepítés", no `data-tab`, no `initcred`.
## 2. FileBrowser admin state (state word only) — `P0-state.txt`
| box | state | decided at |
|---|---|---|
| demo-hp 9201 | `operator` | 2026-09-15T08:52:10Z |
| demo-felhom 9201 (N100, lived-in) | `operator` | 2026-09-16T15:37:26Z |
Both boxes: nobody knows the password (R-513 found admin/admin refused and left it).
## 3. FileBrowser sources (rendered `config.yaml`) — `P0-state.txt`
| box | drive source | import | kept |
|---|---|---|---|
| demo-hp | `NVME 1TB` → `/srv/hdd_1` = `/mnt/felhom-drives/hdd_1/userdata` | `Beolvasás` | `Megőrzött adatok` |
| demo-felhom | `Külső tárhely (hdd_1)` → `/srv/hdd_1` = `/mnt/felhom-drives/hdd_1/userdata` | `Beolvasás` | `Megőrzött adatok` |
Each box has ONE drive. The drive source root is the drive's `userdata/`, so `media/` and every
`media/<movies|tv|music|torrents|transmission|…>` folder is reachable as `<drive source>/media/<sub>` — measured: the
folders exist on both boxes (`ls /mnt/felhom-drives/hdd_1/userdata/media/`). Source names come from the storage
path's label, else the mount basename — the same rule `fbSourceNameForApp` uses, so the link builder and the
sidebar agree by construction.
@@ -0,0 +1,5 @@
1 /install" class="btn btn-primary" data-install-cta="1"
1 Telepítés
1 Telepítés gombbal.
1 Telepítési konfiguráció (
1 app-hero-name">Filebrowser
@@ -0,0 +1,33 @@
# P1b — resetting FileBrowser's admin password WITHOUT the old one (spike, demo-hp guest 9201 only, 2026-10-16)
**Verdict: a safe reset exists. Built.**
## The mechanism, read at the pinned tag
`gtstef/filebrowser:1.5.6-stable` = `gtsteffaniak/filebrowser` tag `v1.5.6-stable` (`5d9b4df`).
- **CLI `filebrowser set -u <user>,<password> -c <config>`** — `backend/cmd/cli.go:60-67` (the flag set), `:117-126`
(dispatch), `:206-269` `setUser`: an existing user gets `user.Password = password`, its TOTP secret cleared, and
`store.Users.Save(user, true, false)` (hashes the password). It needs NO old password.
- It opens the database with `storm.Open(path)` (`backend/database/storage/storage.go:26`) with no timeout: while the
server runs, the bolt lock is held and the CLI would wait forever. So the server is STOPPED for the CLI, and the CLI
runs in a one-shot container with the running container's volumes (`docker run --rm --volumes-from filebrowser`).
- Not chosen: `FILEBROWSER_ADMIN_PASSWORD` / `auth.adminPassword` (`backend/common/settings/config.go:923-926`,
applied in `backend/cmd/user.go:50-57` `validateUserInfo`) — it re-applies on EVERY start (R-513 measured the same on
1.3.3), so it would need a compose change plus two recreates, and the value would sit in the container env.
## Measured (`spike-step1.txt`, `spike-step2.txt`, `spike-step3.txt`, `spike-cleanup.txt`; script `fbspike.sh`)
1. State `operator`, password unknown (R-513 left it 2026-09-15). A DB backup copied first.
2. Reset to K1 with the server stopped: CLI rc 0, „successfully updated user", healthy 7 s after the stop; **K1 → 200**.
3. A test user and a test share made as K1 (user 201, share 200). Settings hash `12a6170ac405fa72`.
4. Reset to K2 without K1: **K2 → 200, K1 → 401, admin/admin → 401**; users `admin` + `fbspike`, the share, and
the settings hash **unchanged**.
5. Test user and share deleted (200/200).
Passwords were generated on DooPlex (16 letters and digits — never a comma, which the CLI's `user,password` form would
split on), sent as files (0600), used from files, and never printed. The CLI carries the password on its command line
for about one second inside the guest; this is the only exposure and it is inside the household's own box.
After the spike demo-hp's FileBrowser password is K2 (kept out-of-band in the session scratchpad) and the controller
still records `operator` — the built button replaces it (Part 1b build).
@@ -0,0 +1,64 @@
#!/bin/sh
# P1b spike — runs INSIDE demo-hp guest 9201. Passwords come from files in $D (0600); never printed.
# Usage: fbspike.sh <step> steps: backup | reset <pwfile> | login <pwfile> | inventory <pwfile> | seed <pwfile> | cleanup <pwfile>
set -u
D=/root/fbspike
IMG=gtstef/filebrowser:1.5.6-stable
IP=$(docker inspect filebrowser --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null)
B="http://$IP:80"
hdr() { printf 'X-Password: %s\n' "$(cat "$1")" > $D/.h; chmod 600 $D/.h; }
login_code() { hdr "$1"; curl -s -o $D/.tok -w '%{http_code}' -X POST -H @$D/.h "$B/api/auth/login?username=admin"; rm -f $D/.h; }
tok() { login_code "$1" >/dev/null; tr -d '"' < $D/.tok; }
case "$1" in
backup)
V=$(docker volume inspect filebrowser_filebrowser_data --format '{{.Mountpoint}}')
cp -a "$V/database.db" $D/database.db.before-spike && ls -l $D/database.db.before-spike | awk '{print "backup bytes",$5}'
;;
reset)
t0=$(date +%s)
docker stop filebrowser >/dev/null && echo "stopped"
P=$(cat "$2")
timeout 60 docker run --rm --volumes-from filebrowser --user 1000:1000 --workdir /home/filebrowser \
--entrypoint /home/filebrowser/filebrowser "$IMG" set -u "admin,$P" -c /home/filebrowser/config.yaml >$D/.cli 2>&1
rc=$?; unset P
echo "cli rc=$rc"; grep -o 'successfully [a-z]* user[^ ]*' $D/.cli; rm -f $D/.cli
docker start filebrowser >/dev/null && echo "started"
for i in $(seq 1 60); do s=$(docker inspect filebrowser --format '{{.State.Health.Status}}'); [ "$s" = healthy ] && break; sleep 2; done
IP=$(docker inspect filebrowser --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'); echo "health=$s after $(( $(date +%s) - t0 ))s"
;;
login)
echo "login $(basename $2): $(login_code "$2")"; rm -f $D/.tok
;;
inventory)
T=$(tok "$2")
echo "users:"; curl -s -H "Authorization: Bearer $T" "$B/api/users" | python3 -c 'import json,sys;[print(" ",u.get("id"),u.get("username"),"admin" if u.get("permissions",{}).get("admin") else "-") for u in json.load(sys.stdin)]'
echo "shares:"; curl -s -H "Authorization: Bearer $T" "$B/api/share/list" | python3 -c 'import json,sys;d=json.load(sys.stdin);d=d if isinstance(d,list) else [];[print(" ",s.get("hash"),s.get("path"),s.get("source")) for s in d];print(" count",len(d))'
echo "settings:"; curl -s -H "Authorization: Bearer $T" "$B/api/settings" | python3 -c 'import json,sys,hashlib;d=json.load(sys.stdin);d.get("auth",{}).pop("key",None);print(" sources",[x.get("name") for x in d.get("server",{}).get("sources",[])]);print(" sha256",hashlib.sha256(json.dumps(d,sort_keys=True).encode()).hexdigest()[:16])'
rm -f $D/.tok
;;
seed)
T=$(tok "$2")
hdr "$2"; curl -s -o /dev/null -w 'create user %{http_code}\n' -X POST -H @$D/.h -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"which":[],"data":{"username":"fbspike","password":"fbspike-'"$(head -c6 /dev/urandom | od -An -tx1 | tr -d ' \n')"'","loginMethod":"password","scopes":[]}}' "$B/api/users"
rm -f $D/.h; [ -n "${3:-}" ] && { rm -f $D/.tok; exit 0; }
curl -s -o /dev/null -w 'create share %{http_code}\n' -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"path":"/media","source":"NVME 1TB","expires":"","unit":"hours","password":null}' "$B/api/share"
rm -f $D/.tok
;;
probe)
T=$(tok "$2"); echo "toklen ${#T}"
for p in api/users api/shares api/settings "api/users?id=self"; do
curl -s -o $D/.o -w "$p %{http_code} " -H "Authorization: Bearer $T" "$B/$p"
head -c 160 $D/.o | sed 's/"password":"[^"]*"/"password":"REDACTED"/g'; echo
done
rm -f $D/.o $D/.tok
;;
cleanup)
T=$(tok "$2")
id=$(curl -s -H "Authorization: Bearer $T" "$B/api/users" | python3 -c 'import json,sys;print(next((u["id"] for u in json.load(sys.stdin) if u["username"]=="fbspike"),""))')
hdr "$2"; [ -n "$id" ] && curl -s -o /dev/null -w "delete user %{http_code}\n" -X DELETE -H @$D/.h -H "Authorization: Bearer $T" "$B/api/users?id=$id"; rm -f $D/.h
for h in $(curl -s -H "Authorization: Bearer $T" "$B/api/share/list" | python3 -c 'import json,sys;d=json.load(sys.stdin);d=d if isinstance(d,list) else [];[print(s["hash"]) for s in d if s.get("path","").rstrip("/")=="/media"]'); do
curl -s -o /dev/null -w "delete share %{http_code}\n" -X DELETE -H "Authorization: Bearer $T" "$B/api/share?hash=$h"; done
rm -f $D/.tok
;;
esac
@@ -0,0 +1,10 @@
## 2026-10-16T12:17:12Z cleanup: the test user and the test share
delete user 200
delete share 200
users:
1 admin admin
shares:
count 0
settings:
sources ['Beolvasás', 'NVME 1TB', 'Megőrzött adatok']
sha256 12a6170ac405fa72
@@ -0,0 +1,33 @@
## 2026-10-16T12:14:17Z step 1: state operator, password unknown; admin/admin and K1 before
login K1: 401
## reset to K1 with the CLI, server stopped
stopped
cli rc=0
successfully updated user:
started
health=healthy after 7s
login K1: 200
users:
Traceback (most recent call last):
File "<string>", line 1, in <module>
import json,sys;[print(" ",u.get("id"),u.get("username"),"admin" if u.get("permissions",{}).get("admin") else "-") for u in json.load(sys.stdin)]
^^^^^
AttributeError: 'str' object has no attribute 'get'
shares:
Traceback (most recent call last):
File "<string>", line 1, in <module>
import json,sys;d=json.load(sys.stdin);d=d if isinstance(d,list) else [];[print(" ",s.get("hash"),s.get("path"),s.get("source")) for s in d];print(" count",len(d))
~~~~~~~~~^^^^^^^^^^^
File "/usr/lib/python3.13/json/__init__.py", line 293, in load
return loads(fp.read(),
cls=cls, object_hook=object_hook,
parse_float=parse_float, parse_int=parse_int,
parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
File "/usr/lib/python3.13/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
~~~~~~~~~~~~~~~~~~~~~~~^^^
File "/usr/lib/python3.13/json/decoder.py", line 348, in decode
raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 1 column 5 (char 4)
settings sources:
keys ['message', 'status']
@@ -0,0 +1,22 @@
## 2026-10-16T12:16:26Z step 2: seed a user and a share as K1, inventory
create user 401
create share 200
users:
1 admin admin
shares:
aoSyMglz7IyGgR9e_WVg4Q /media/ NVME 1TB
count 1
settings:
sources ['Beolvasás', 'NVME 1TB', 'Megőrzött adatok']
sha256 12a6170ac405fa72
## 2026-10-16T12:16:43Z step 2b: seed the user (with X-Password), inventory
create user 201
users:
1 admin admin
2 fbspike -
shares:
aoSyMglz7IyGgR9e_WVg4Q /media/ NVME 1TB
count 1
settings:
sources ['Beolvasás', 'NVME 1TB', 'Megőrzött adatok']
sha256 12a6170ac405fa72
@@ -0,0 +1,19 @@
## 2026-10-16T12:16:51Z step 3: reset to K2 without K1 (CLI, server stopped)
stopped
cli rc=0
successfully updated user:
started
health=healthy after 7s
login K2: 200
login K1: 401
admin/admin:
login ADM: 401
users:
1 admin admin
2 fbspike -
shares:
aoSyMglz7IyGgR9e_WVg4Q /media/ NVME 1TB
count 1
settings:
sources ['Beolvasás', 'NVME 1TB', 'Megőrzött adatok']
sha256 12a6170ac405fa72
Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,219 @@
after login url: /launcher
{
"/apps/filebrowser": {
"path": "/apps/filebrowser",
"title": "FileBrowser",
"tabs": [
"settings",
"guide",
"logs"
],
"install": false,
"loginAnswer": [
"initial"
],
"loginKind": [],
"fbOperator": false,
"fbReset": false,
"reveal": true,
"open": "https://files.enkisfelhom.hu/",
"files": null,
"folders": [],
"fbNote": false,
"scrollWidth": 1280,
"console": []
},
"/apps/filebrowser/guide": {
"path": "/apps/filebrowser/guide",
"title": "FileBrowser",
"tabs": [
"settings",
"guide",
"logs"
],
"install": false,
"loginAnswer": [],
"loginKind": [],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://files.enkisfelhom.hu/",
"files": null,
"folders": [],
"fbNote": false,
"scrollWidth": 1280,
"console": [
[
"log-error",
"Failed to load resource: the server responded with a status of 404 ()",
"https://felhom.enkisfelhom.hu/static/assets/filebrowser-screenshot-3.webp"
]
]
},
"/apps/romm": {
"path": "/apps/romm",
"title": "RomM",
"tabs": [
"settings",
"data",
"guide",
"logs"
],
"install": false,
"loginAnswer": [
"first_visit"
],
"loginKind": [
"first_visit"
],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://arcade.enkisfelhom.hu/",
"files": "https://files.enkisfelhom.hu/files/NVME%201TB/roms",
"folders": [],
"fbNote": false,
"scrollWidth": 1270,
"console": []
},
"/apps/romm/data": {
"path": "/apps/romm/data",
"title": "RomM",
"tabs": [
"settings",
"data",
"guide",
"logs"
],
"install": false,
"loginAnswer": [],
"loginKind": [],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://arcade.enkisfelhom.hu/",
"files": "https://files.enkisfelhom.hu/files/NVME%201TB/roms",
"folders": [
"https://files.enkisfelhom.hu/files/NVME%201TB/roms",
"https://files.enkisfelhom.hu/files/NVME%201TB/roms-bios"
],
"fbNote": true,
"scrollWidth": 1270,
"console": []
},
"/apps/calibre-web": {
"path": "/apps/calibre-web",
"title": "Calibre-Web Automated",
"tabs": [
"settings",
"data",
"guide",
"logs"
],
"install": false,
"loginAnswer": [
"fields"
],
"loginKind": [],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://books.enkisfelhom.hu/",
"files": "https://files.enkisfelhom.hu/files/NVME%201TB/media/books",
"folders": [],
"fbNote": false,
"scrollWidth": 1270,
"console": []
},
"/apps/calibre-web/data": {
"path": "/apps/calibre-web/data",
"title": "Calibre-Web Automated",
"tabs": [
"settings",
"data",
"guide",
"logs"
],
"install": false,
"loginAnswer": [],
"loginKind": [],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://books.enkisfelhom.hu/",
"files": "https://files.enkisfelhom.hu/files/NVME%201TB/media/books",
"folders": [
"https://files.enkisfelhom.hu/files/Beolvas%C3%A1s/calibre",
"https://files.enkisfelhom.hu/files/NVME%201TB/media/books"
],
"fbNote": true,
"scrollWidth": 1270,
"console": []
},
"/apps/plex": {
"path": "/apps/plex",
"title": "Plex",
"tabs": [
"settings",
"data",
"guide",
"logs"
],
"install": false,
"loginAnswer": [
"first_visit"
],
"loginKind": [
"first_visit"
],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://plex.enkisfelhom.hu/",
"files": "https://files.enkisfelhom.hu/files/NVME%201TB/media",
"folders": [],
"fbNote": false,
"scrollWidth": 1270,
"console": []
},
"/apps/plex/data": {
"path": "/apps/plex/data",
"title": "Plex",
"tabs": [
"settings",
"data",
"guide",
"logs"
],
"install": false,
"loginAnswer": [],
"loginKind": [],
"fbOperator": false,
"fbReset": false,
"reveal": false,
"open": "https://plex.enkisfelhom.hu/",
"files": "https://files.enkisfelhom.hu/files/NVME%201TB/media",
"folders": [
"https://files.enkisfelhom.hu/files/NVME%201TB/media/movies",
"https://files.enkisfelhom.hu/files/NVME%201TB/media/tv"
],
"fbNote": true,
"scrollWidth": 1270,
"console": []
},
"cold:romm": {
"target": "https://files.enkisfelhom.hu/files/NVME%201TB/roms",
"first": "/login?redirect=/files/NVME%25201TB/roms",
"after_login": "/files/NVME%201TB/roms"
},
"cold:calibre-web": {
"target": "https://files.enkisfelhom.hu/files/NVME%201TB/media/books",
"first": "/login?redirect=/files/NVME%25201TB/media/books",
"after_login": "/files/NVME%201TB/media/books"
},
"cold:plex": {
"target": "https://files.enkisfelhom.hu/files/NVME%201TB/media",
"first": "/login?redirect=/files/NVME%25201TB/media",
"after_login": "/files/NVME%201TB/media"
}
}
@@ -0,0 +1,16 @@
200 /apps/filebrowser :: 1 data-fb-operator 1 data-fb-reset 1 data-login-answer="initial" 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings"
302 /apps/filebrowser /apps/filebrowser/install ::
302 /apps/filebrowser /apps/filebrowser/data ::
200 /apps/filebrowser/guide :: 1 data-infra-guide 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings"
200 /apps/romm :: 1 data-install-cta
302 /apps/romm /apps/romm/data ::
200 /apps/calibre-web :: 1 data-app-files="1" 1 data-login-answer="fields" 1 data-tab="data" 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings" href="https://files.enkisfelhom.hu/files/NVME%201TB/media/books" target="_blank" rel="noopener" class="btn btn-outline" data-app-files
200 /apps/calibre-web/data :: 1 data-app-files="1" 1 data-fb-login-note 1 data-tab="data" 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings" href="https://files.enkisfelhom.hu/files/NVME%201TB/media/books" target="_blank" rel="noopener" class="btn btn-outline" data-app-files
200 /apps/plex :: 1 data-app-files="1" 1 data-login-answer="first_visit" 1 data-login-kind="first_visit" 1 data-tab="data" 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings" href="https://files.enkisfelhom.hu/files/NVME%201TB/media" target="_blank" rel="noopener" class="btn btn-outline" data-app-files
200 /apps/plex/data :: 1 data-app-files="1" 1 data-fb-login-note 2 data-media-folder 1 data-tab="data" 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings" href="https://files.enkisfelhom.hu/files/NVME%201TB/media" target="_blank" rel="noopener" class="btn btn-outline" data-app-files
200 /apps/immich :: 1 data-install-cta
200 /apps/bentopdf :: 1 data-login-answer="first_visit" 1 data-login-kind="first_visit" 1 data-tab="data" 1 data-tab="guide" 1 data-tab="logs" 1 data-tab="settings"
@@ -0,0 +1,12 @@
## 2026-10-16T13:26:04Z press reset (POST /apps/filebrowser/initial-credentials/reset)
{"data":null,"error":"","ok":true}
HTTP 200 in 3.362612s
## the page after
1 data-login-answer="initial"
1 id="initcred-reveal"
## reveal (value kept out of this file; length only)
HTTP 200
ok True len 16
## a second press (now generated) must be refused
{"data":null,"error":"A Fájlkezelő jelszavát a doboz már ismeri — a Megjelenítés gomb mutatja.","ok":false}
HTTP 409
@@ -0,0 +1,20 @@
login NEWPW: 200
login K2: 401
login ADM: 401
users:
1 admin admin
shares:
count 0
settings:
sources ['Beolvasás', 'NVME 1TB', 'Megőrzött adatok']
sha256 12a6170ac405fa72
e920c91f524e filebrowser filebrowser gtstef/filebrowser:1.5.6-stable
2026/10/16 13:20:19 handlers.go:3869: [INFO] [web] FileBrowser sync — no config/compose change, ensured running without recreate (1 storage path(s))
e920c91f524e filebrowser filebrowser gtstef/filebrowser:1.5.6-stable
2026/10/16 13:26:04 handlers.go:2742: [INFO] [web] filebrowser: new admin password requested from 37.191.56.193
2026/10/16 13:26:04 filebrowser_password.go:220: [INFO] [infra] filebrowser: the household asked for a new admin password — stopping the file browser for its CLI
2026/10/16 13:26:08 filebrowser_password.go:258: [INFO] [infra] filebrowser: admin password replaced at the household's press (value never logged); verified new=200 admin=refused in 3s
2026/10/16 13:26:08 handlers.go:2780: [INFO] [web] filebrowser admin password revealed from 37.191.56.193 (value never logged)
2026/10/16 13:26:08 handlers.go:2742: [INFO] [web] filebrowser: new admin password requested from 37.191.56.193
"filebrowser_admin_state": "generated"
filebrowser Up 19 seconds (healthy)
@@ -0,0 +1,10 @@
## 2026-10-16T14:19:13Z N100 (operator said yes in chat) — before:
data-fb-operator data-fb-reset
## press
{"data":null,"error":"","ok":true}
HTTP 200 in 3.462537s
## after:
data-login-answer="initial" id="initcred-reveal"
## reveal (length only)
HTTP 200
ok True len 16

Some files were not shown because too many files have changed in this diff Show More