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
This commit is contained in:
2026-10-16 17:27:57 +02:00
parent 11fa13ee38
commit 836d64efa3
35 changed files with 739 additions and 10 deletions
@@ -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)