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
This commit is contained in:
2026-10-16 15:53:12 +02:00
parent bc9a871391
commit ea8e67f341
41 changed files with 1173 additions and 1 deletions
@@ -516,6 +516,41 @@ only meet by pressing. The page and the endpoint decide „stored" with ONE func
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] „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
@@ -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,5 @@
200 /apps/filebrowser :: data-fb-operator data-fb-reset data-login-answer="initial"
302 /apps/filebrowser/install ::
200 /apps/romm :: data-install-cta
200 /apps/plex :: data-install-cta
200 /apps/immich :: data-install-cta
@@ -0,0 +1,2 @@
{"ok":true,"message":"Telepítés elindítva – az állapot a kártyán követhető"}
HTTP 202
@@ -0,0 +1,50 @@
## infra counted as not installed (v0.315.0 line)
=== RUN TestAppPage_InfraAppHasNoInstall
logins_folders_test.go:193: FileBrowser offers Install — an infrastructure app is part of the box
--- FAIL: TestAppPage_InfraAppHasNoInstall (0.09s)
## no fallback login answer (v0.319.0 behaviour)
=== RUN TestAppPage_EveryInstalledAppHasOneLoginAnswer
logins_folders_test.go:256: immich: 0 login answers, want exactly 1
logins_folders_test.go:256: rallly: 0 login answers, want exactly 1
logins_folders_test.go:256: bentopdf: 0 login answers, want exactly 1
--- FAIL: TestAppPage_EveryInstalledAppHasOneLoginAnswer (0.10s)
## no library preference in the Files target
## absent drive still linked
=== RUN TestAppPage_UnknownSourceIsTextNotLink
logins_folders_test.go:315: romm links a folder on a drive that is not on the box
--- FAIL: TestAppPage_UnknownSourceIsTextNotLink (0.08s)
## plaintext stored
=== RUN TestFileBrowserReset_SetsVerifiesAndStoresEncrypted
filebrowser_reset_test.go:56: the stored value is the plaintext password (or empty)
--- FAIL: TestFileBrowserReset_SetsVerifiesAndStoresEncrypted (0.00s)
## verify of the new password removed
=== RUN TestFileBrowserReset_FailedVerifyKeepsOperator
filebrowser_reset_test.go:73: a reset whose new password does not log in reported success
--- FAIL: TestFileBrowserReset_FailedVerifyKeepsOperator (0.00s)
## state check removed
=== RUN TestFileBrowserReset_OnlyInOperatorState
filebrowser_reset_test.go:95: state "": err = <nil>, want ErrFileBrowserNotOperator
filebrowser_reset_test.go:98: state "": the apply ran
filebrowser_reset_test.go:101: state "" changed to "generated"
filebrowser_reset_test.go:95: state "generated": err = <nil>, want ErrFileBrowserNotOperator
filebrowser_reset_test.go:98: state "generated": the apply ran
## protected deploy guard removed
=== RUN TestDeployStack_RefusesProtectedStack
--- FAIL: TestDeployStack_RefusesProtectedStack (0.00s)
## no library preference in the Files target (rerun)
=== RUN TestAppPage_FilesButtonTarget
logins_folders_test.go:292: calibre-web: Files → https://files.example.hu/files/Beolvas%C3%A1s/calibre, want https://files.example.hu/files/M%C3%A9dia%20&amp;%20k%C3%B6nyvt%C3%A1r/media/books
--- FAIL: TestAppPage_FilesButtonTarget (0.08s)
## protected deploy guard removed (rerun)
=== RUN TestDeployStack_RefusesProtectedStack
--- FAIL: TestDeployStack_RefusesProtectedStack (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
panic({0xac07c0?, 0x116c870?})
/usr/local/go/src/runtime/panic.go:860 +0x13a
filebrowser_reset_test.go:128: filebrowser (Protected=true): err = went on into the deploy (panicked on the bare manager), want ErrProtectedStack
filebrowser_reset_test.go:131: filebrowser: left Deploying set
filebrowser_reset_test.go:128: filebrowser (Protected=false): err = went on into the deploy (panicked on the bare manager), want ErrProtectedStack
filebrowser_reset_test.go:131: filebrowser: left Deploying set
filebrowser_reset_test.go:128: samba (Protected=false): err = went on into the deploy (panicked on the bare manager), want ErrProtectedStack
XX DECOY: a bad value only in the English block rc=1 (expected 0)
XX DECOY: the key under ANOTHER mapping rc=1 (expected 0)
@@ -0,0 +1,12 @@
== tester-1
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.320.0 Up 13 minutes (healthy)
filebrowser Up 11 hours (healthy)
== demo-hp
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.320.0 Up 34 seconds (healthy)
filebrowser Up 15 minutes (healthy)
== demo-felhom
unhealthy=0 restarting=0 exited=0 created=0
controller gitea.dooplex.hu/admin/felhom-controller:0.320.0 Up 13 minutes (healthy)
filebrowser Up 11 hours (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,3 @@
15:48:34
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,3 @@
('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')
customers: no such table: customers
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""v0.320.0 logins & folders browser check. 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 = {}
MARK = """(function(){ function all(sel,attr){return [].map.call(document.querySelectorAll(sel),function(e){return e.getAttribute(attr)})}
return {path: location.pathname, title: (document.querySelector('.app-hero-name')||{}).textContent,
tabs: all('[data-tab]','data-tab'), install: !!document.querySelector('[data-install-cta]'),
loginAnswer: all('[data-login-answer]','data-login-answer'), loginKind: all('[data-login-kind]','data-login-kind'),
fbOperator: !!document.querySelector('[data-fb-operator]'), fbReset: !!document.querySelector('[data-fb-reset]'),
reveal: !!document.querySelector('#initcred-reveal'), open: (document.querySelector('[data-app-open]')||{}).href || null,
files: (document.querySelector('[data-app-files]')||{}).href || null,
folders: [].map.call(document.querySelectorAll('.datapath-row'), function(r){var a=r.querySelector('a'); return (a?a.href:'TEXT:'+r.textContent.trim().slice(0,60))}),
fbNote: !!document.querySelector('[data-fb-login-note]'), scrollWidth: document.documentElement.scrollWidth};})()"""
for p in ['/apps/filebrowser', '/apps/filebrowser/guide', '/apps/romm', '/apps/romm/data', '/apps/calibre-web',
'/apps/calibre-web/data', '/apps/plex', '/apps/plex/data']:
n0 = len(console)
goto(BASE + p)
r = ev(MARK); r['console'] = console[n0:]
report[p] = r
shot(p.strip('/').replace('/', '-'))
# The cold „Fajlok" click: this browser has NO FileBrowser session. Follow each app's button target.
fbpw = open(FBPWFILE).read().strip()
for slug in ['romm', 'calibre-web', 'plex']:
target = report['/apps/' + slug].get('files')
if not target:
report['cold:' + slug] = 'no Files button'
continue
call('Network.clearBrowserCookies') # cold: no FileBrowser session (the controller one is re-made below)
goto(target)
first = ev('location.pathname + location.search')
shot('cold-' + slug + '-1-login')
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))
time.sleep(4)
after = ev('location.pathname')
shot('cold-' + slug + '-2-after-login')
report['cold:' + slug] = {'target': target, 'first': first, 'after_login': after}
del fbpw
print(json.dumps(report, indent=1, ensure_ascii=False))
finally:
proc.terminate()
subprocess.run(['rm', '-rf', prof])
+4 -1
View File
@@ -124,7 +124,7 @@ stopping line that lies.
| **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC |
| **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator |
## Apps & catalog — 22 rows (P2 1, P3 12, P4 9)
## Apps & catalog — 25 rows (P2 1, P3 14, P4 10)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -148,8 +148,11 @@ stopping line that lies.
| **R-1043** | Apps & catalog | P4 | **Sonarr, Radarr and Prowlarr build their login redirect as an absolute `http://` URL.** They trust forwarded headers only from their „Trusted networks” (the household set `192.168.0.0/24`; traefik is `172.18.x`), so they ignore `X-Forwarded-Proto: https` and answer `Location: http://<name>/login` to an https visitor. Since controller v0.312.0 that hop is sent back to https by traefik (a GET, no password), so it is cosmetic; a template could seed traefik's network as trusted. (`audits/https-only-2026-10-14/``1A-3-4.txt`) | **OPEN** — filed 2026-10-14 | — | Seed the docker network as a trusted proxy in the *arr templates (with the R-1030 seed), measure the redirect | CC |
| **R-1095** | Apps & catalog | P4 | **`/apps/{slug}/install` still opens for an app that is INSTALLED, and shows the install form read-only.** Found 2026-10-16 (app page tuning, controller v0.319.0): the settings tab stopped rendering the install form, but `appInstallPageHandler` renders `deploy` for any app, so a typed or stale `/install` address shows „<app> — Telepítés" with disabled fields and no button. Its secret guards hold (R-254/R-709 tests; parity cases `deploy_installed*`). | OPEN | the task forbade route changes | a 302 from `/apps/{slug}/install` to `/apps/{slug}/settings` for an installed app (the `redirectStackDeployGET` shape); then the deployed-only branches of `app_fields.html` can go | CC |
| **R-1096** | Apps & catalog | P3 | **After a disk-full, an app's damaged Redis file keeps the app down for good, and nothing says what to do.** FOUND 2026-10-16 on demo-hp 9201: the standing RomM (installed 2026-08-21) was stopped; the dashboard's Start re-created it, `romm-redis` crash-looped (unhealthy), the box stopped it again (`crash_loop`, `09` §3 decision 28) and mailed `app_start_failed`. Its Redis volume dates from the 2026-09-24 thin-pool fill (R-672): Docmost's damaged append-only file was repaired by hand that day with `redis-check-aof --fix` (`audits/r672-2026-09-24/B7-redis-aof-fix.txt`); RomM's was not (the copy `romm_romm_redis_data.pre-aof-fix-20260924T135937Z` is still on the box). The Redis log was not read: the box removed the containers first. That RomM was then removed by operator ruling (`audits/romm-ghost-2026-10-16/A1/`). | OPEN — filed 2026-10-16 | — | List the catalog apps whose Redis runs with `--appendonly yes` (romm does); for each, decide whether its Redis needs persistence at all; reproduce on 9202 by damaging an append-only file, and make the box repair a copy or say what to do | CC |
| **R-1100** | Apps & catalog | P3 | **One sign-in for the dashboard and FileBrowser (proxy auth) — spike first.** Since controller 0.320.0 every app with a folder has „Fájlok ↗", and the box can show FileBrowser's password — but FileBrowser still asks for its OWN login the first time, so the household signs in twice. FileBrowser Quantum has a proxy-auth method (`auth.methods.proxyAuth`, header-trusted) that could take the dashboard session through traefik; nobody has measured it. Design: `architecture/02-controller-module-map.md` „Logins and folders". | OPEN — filed 2026-10-16 (task logins-folders). | — | Spike on demo-hp: proxy auth at the pinned tag (source + measured), a forwardAuth in traefik that answers only for a dashboard session, what a stranger reaching `files.<domain>` with a forged header gets, and what happens to FileBrowser's shares. | CC |
| **R-1102** | Apps & catalog | P3 | **Eight apps say „starts with a known, shared password" about a login that is generated, set at install, or made on the first visit.** `app_info.default_creds` with no `after_install` keeps the known-login sentence in effect (`web/known_login.go`), so the install dialog and the app page say „Ez az alkalmazás egy ismert, közös jelszóval indul: <default_creds>. Telepítés után azonnal változtasd meg." for grafana, gokapi, kimai, code-server, nzbget, nextcloud, transmission (a password the box generated or the household typed, shown in the login panel) and prowlarr („Az első belépéskor te hozod létre"). Found 2026-10-16 while adding the login panel's answers. | OPEN | — | Catalog: drop `default_creds` on these eight (hu + `i18n.en`, `check-copy-i18n.py --capture-freeze`) now that the login panel says where the login is — or keep the hint and give the controller a „hint, not a known password" form. An operator word on which. | CC |
| **R-1097** | Apps & catalog | P3 | **RomM keeps the household's saves, save states and screenshots in an anonymous Docker volume, outside the backup.** FOUND 2026-10-16 from RomM 5.3.1's source and the running container: `ASSETS_BASE_PATH = /romm/assets` (`backend/config/__init__.py:39`); the image declares `VOLUME /romm`; the compose mounts nothing at `/romm/assets`, and `docker inspect` on 9201 showed an anonymous volume at `/romm`. Compose keeps an anonymous volume across a recreate, so an update does not lose it; a remove + reinstall or a restore does, and the app backup (named volumes + drive folders) probably does not hold it — NOT measured. Moving it to a named volume or the drive would hide what a box already has, so a fix needs a one-time copy. | OPEN — filed 2026-10-16 | — | Measure on 9202: a save in a browser game, back up, restore, read the save; then a named `romm_assets` volume with a one-time copy from the anonymous one | CC |
| **R-1099** | Apps & catalog | P4 | **The N100 may hold RomM games in the old upload place, now shown under a fake „roms” platform.** Until 2026-10-16 RomM's own upload wrote to the drive's `roms/roms/<platform>/`; after the mapping fix (catalog `fb18471`, R-1089) those files are at RomM's `library/roms/roms/…`. Measured on 9201: such a game showed as missing in its platform and again under a platform „roms” (`audits/romm-ghost-2026-10-16/B/b-after.txt`). Files put in with the file manager are not affected — they are now found. The N100 runs RomM (hub `/apps/romm`); CC does not log in to the lived-in box. | OPEN — filed 2026-10-16 | Operator: look at `roms/roms/` on the N100's drive in the file manager | If it holds platform folders: move them up one level into `roms/`, then run „Scan” in RomM; if it is empty, close | Operator |
| **R-1101** | Apps & catalog | P4 | **FileBrowser's stored password goes stale when the household changes it in FileBrowser — and the page tells them to.** In the `generated` state the panel shows „Az első bejelentkezés után azonnal változtasd meg!" under FileBrowser's login (R-513 copy). Changing it in FileBrowser leaves the box's copy wrong: „Megjelenítés" then shows a password that is refused, and the state stays `generated`, so „Új jelszó beállítása" is not offered. Found 2026-10-16. | OPEN | — | Either drop the „change it" line for FileBrowser, or have the reveal (or a tick) check the stored value against FileBrowser's login and fall back to `operator` when it is refused. | CC |
## App updates — 4 rows (P3 4)