Rehearsal 2026-10-20 B2-B4: bound (operator passphrase), claimed, recovery code, family (accented names refused), second disk initialised, 8 of 9 apps (Jellyfin refused on memory), first uploads froze the box ~1 h (hub said OK); screenshots gallery
gates / gates (push) Successful in 13m8s

This commit is contained in:
2026-10-20 16:34:30 +02:00
parent ad6397308c
commit ee743477b4
62 changed files with 579 additions and 1 deletions
@@ -0,0 +1,9 @@
immich running deployed= True sub= photos gate= True
nextcloud running deployed= True sub= cloud gate= False
paperless-ngx running deployed= True sub= paperless gate= False
vaultwarden running deployed= True sub= vault gate= False
mealie running deployed= True sub= recipes gate= False
actualbudget running deployed= True sub= budget gate= True
radicale running deployed= True sub= calendar gate= False
bookstack running deployed= True sub= wiki gate= False
jellyfin not_deployed deployed= False sub= None gate= False
@@ -0,0 +1,40 @@
14:13:55 diskread=3GB health=200 0.026323
14:14:27 diskread=3GB health=200 0.010294
14:14:59 diskread=4GB health=200 0.011968
14:15:30 diskread=4GB health=200 0.009450
14:16:02 diskread=4GB health=200 0.010128
14:16:33 diskread=4GB health=200 0.009558
14:17:05 diskread=4GB health=200 0.010571
14:17:36 diskread=4GB health=200 0.009807
14:18:08 diskread=4GB health=200 0.010191
14:18:40 diskread=4GB health=200 0.009850
14:19:11 diskread=4GB health=200 0.010203
14:19:43 diskread=4GB health=200 0.011617
14:20:14 diskread=4GB health=200 0.014898
14:20:46 diskread=4GB health=200 0.010567
14:21:17 diskread=4GB health=200 0.011040
14:21:49 diskread=4GB health=200 0.009551
14:22:21 diskread=4GB health=200 0.009957
14:22:52 diskread=4GB health=200 0.010580
14:23:24 diskread=4GB health=200 0.010163
14:23:56 diskread=4GB health=200 0.010898
14:24:27 diskread=4GB health=200 0.010501
14:24:58 diskread=4GB health=200 0.011854
14:25:30 diskread=4GB health=200 0.011567
14:26:01 diskread=4GB health=200 0.010254
14:26:32 diskread=4GB health=200 0.010620
14:27:03 diskread=4GB health=200 0.011295
14:27:35 diskread=4GB health=200 0.010245
14:28:06 diskread=4GB health=200 0.010855
14:28:37 diskread=4GB health=200 0.010371
14:29:09 diskread=4GB health=200 0.015567
14:29:40 diskread=4GB health=200 0.010465
14:30:11 diskread=4GB health=200 0.010190
14:30:42 diskread=4GB health=200 0.011160
14:31:14 diskread=4GB health=200 0.009954
14:31:45 diskread=4GB health=200 0.011131
14:32:16 diskread=4GB health=200 0.011337
14:32:48 diskread=4GB health=200 0.010336
14:33:19 diskread=4GB health=200 0.011064
14:33:50 diskread=4GB health=200 0.010944
14:34:22 diskread=4GB health=200 0.010249
@@ -0,0 +1,16 @@
UTC cpu% load1 mem_GB (the box's own /api/metrics/system history)
12:00 1.4 1.6 0.9384765625
12:07 6.1 0.9 0.93359375
12:14 3.0 0.6 0.970703125
12:21 14.0 1.6 1.5107421875
12:28 1.0 0.3 1.6123046875
12:36 1.5 0.3 1.607421875
12:43 11.8 1.4 1.6298828125
12:50 15.2 1.5 2.056640625
12:57 61.3 8.3 2.318359375
13:33 78.1 139.3 2.2900390625
13:55 78.1 151.6 2.3486328125
14:09 0.0 2.0 1.1875
keys: ['cpu', 'labels', 'load1', 'memory', 'temp']
{"ok": true, "data": {"sync_status": {"last_sync": "2026-10-20T14:13:31.264994519Z", "last_status": "ok", "syncing": false}, "system": {"total_mem_mb": 4096, "used_mem_mb": 3638, "avail_mem_mb": 458, "mem_percent": 88.818359375, "disk_total_gb": 49.03971862792969, "disk_used_gb": 12.562141418457031, "disk_avail_gb": 33.96195983886719, "disk_percent": 27.001362904306276, "disk_known": true, "hdd_total_gb": 195.8027114868164, "hdd_used_gb": 0.10123825073242188, "hdd_avail_gb": 185.68584823608398, "hdd_percent": 0.054491543328877075, "hdd_configured": true, "hdd_known": true, "cpu_percent": 55.314861460957175, "load_avg_1": 3.31, "load_avg_5": 1.24, "load_avg_15": 0.44, "temperature_celsius": 0}}}
{"ok": true, "data": [{"name": "felhom-controller", "cpu_percent": 42.69, "mem_usage_mb": 92.52, "mem_limit_mb": 4096}, {"name": "actualbudget", "cpu_percent": 31.28, "mem_usage_mb": 128.6, "mem_limit_mb": 256}, {"name": "radicale", "cpu_percent": 28.31, "mem_usage_mb": 28.39, "mem_limit_mb": 128}, {"name": "bookstack-db", "cpu_percent": 27.86, "mem_usage_mb": 153.2, "mem_limit_mb": 256}, {"name": "immich-machine-learning", "cpu_percent": 27.57, "mem_usage_mb": 107.2, "mem_limit_mb": 2048}, {"name": "paperless-webserver", "cpu_percent": 27.41, "mem_usage_mb": 72.22, "mem_limit_mb": 1280}, {"name": "mealie", "cpu_percent": 26.57, "mem_usage_mb": 67.21, "mem_limit_mb": 1000}, {"name": "nextcloud-db", "cpu_percent": 26.17, "mem_usage_mb": 116.3, "mem_limit_mb": 512}, {"name": "immich-server", "cpu_percent": 22.66, "mem_usage_mb": 206, "mem_limit_mb": 3072}, {"name": "nextcloud", "cpu_percent": 19.4, "mem_usage_mb": 157.1, "mem_limit_mb": 1024}, {"name": "traefik", "cpu_percent": 10.78, "mem_usage_mb": 186.4, "mem_limit_mb": 4096}, {"name": "bookstack", "cpu_percent": 0.61, "mem_usage_mb": 19.94, "mem_limit_mb": 512}, {"name": "paperless-redis", "cpu_percent": 0.47, "mem_usage_mb": 26.13, "mem_limit_mb": 128}, {"name": "immich-redis", "cpu_percent": 0.43, "mem_usage_mb": 3.16, "mem_limit_mb": 128}, {"name": "nextcloud-redis", "cpu_percent": 0.42, "mem_usage_mb": 10.36, "mem_limit_mb": 128}, {"name": "paperless-postgres", "cpu_percent": 0.03, "mem_usage_mb": 58.27, "mem_limit_mb": 256}, {"name": "immich-postgres", "cpu_percent": 0.03, "mem_usage_mb": 133.1, "mem_limit_mb": 768}, {"name": "vaultwarden", "cpu_percent": 0, "mem_usage_mb": 72.27, "mem_limit_mb": 256}, {"name": "filebrowser", "cpu_percent": 0, "mem_usage_mb": 145, "mem_limit_mb": 256}]}
@@ -0,0 +1,49 @@
12:59:51
Traceback (most recent call last):
File "/root/work/apps.py", line 182, in <module>
fn(*sys.argv[2:])
~~^^^^^^^^^^^^^^^
File "/root/work/apps.py", line 168, in immich
st, out, _ = http("POST", base + "/api/assets", body, dict(tok, **{"Content-Type": ct}))
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/work/apps.py", line 34, in http
with urllib.request.urlopen(req, context=CTX, timeout=timeout) as r:
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/urllib/request.py", line 189, in urlopen
return opener.open(url, data, timeout)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/urllib/request.py", line 489, in open
response = self._open(req, data)
File "/usr/lib/python3.13/urllib/request.py", line 506, in _open
result = self._call_chain(self.handle_open, protocol, protocol +
'_open', req)
File "/usr/lib/python3.13/urllib/request.py", line 466, in _call_chain
result = func(*args)
File "/usr/lib/python3.13/urllib/request.py", line 1367, in https_open
return self.do_open(http.client.HTTPSConnection, req,
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
context=self._context)
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/urllib/request.py", line 1323, in do_open
r = h.getresponse()
File "/usr/lib/python3.13/http/client.py", line 1459, in getresponse
response.begin()
~~~~~~~~~~~~~~^^
File "/usr/lib/python3.13/http/client.py", line 336, in begin
version, status, reason = self._read_status()
~~~~~~~~~~~~~~~~~^^
File "/usr/lib/python3.13/http/client.py", line 297, in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/usr/lib/python3.13/socket.py", line 719, in readinto
return self._sock.recv_into(b)
~~~~~~~~~~~~~~~~~~~~^^^
File "/usr/lib/python3.13/ssl.py", line 1304, in recv_into
return self.read(nbytes, buffer)
~~~~~~~~~^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/ssl.py", line 1138, in read
return self._sslobj.read(len, buffer)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
TimeoutError: The read operation timed out
12:59:52Z immich admin-sign-up 400 b'{"message":"Admin setup is not available"}'
13:05:57
@@ -0,0 +1 @@
14:23:57
@@ -0,0 +1,7 @@
12:57:15
13:12:06Z nextcloud rclone rc=-15
2026/10/20 12:57:17 NOTICE: Config file "/root/.config/rclone/rclone.conf" not found - using defaults
2026/10/20 12:57:18 NOTICE: Csalad/Kulonleges_nevek/link_a_nagy_fajlra: Can't follow symlink without -L/--copy-links
2026/10/20 12:57:18 NOTICE: Csalad/Kulonleges_nevek/Ödön születésnapja – fénykép lista.txt: Duplicate object found in source - ignoring
2026/10/20 12:57:27 ERROR : Csalad/nagy_fajl_1_5GB.bin: Failed to copy: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
13:12:06
@@ -0,0 +1,21 @@
14:23:58 diskread=4GB health=200 0.010208
14:24:30 diskread=4GB health=200 0.010427
14:25:01 diskread=4GB health=200 0.012785
14:25:32 diskread=4GB health=200 0.010109
14:26:04 diskread=4GB health=200 0.009754
14:26:35 diskread=4GB health=200 0.010561
14:27:06 diskread=4GB health=200 0.013033
14:27:37 diskread=4GB health=200 0.010545
14:28:09 diskread=4GB health=200 0.010882
14:28:40 diskread=4GB health=200 0.010998
14:29:11 diskread=4GB health=200 0.009735
14:29:42 diskread=4GB health=200 0.010706
14:30:14 diskread=4GB health=200 0.012557
14:30:45 diskread=4GB health=200 0.011448
14:31:16 diskread=4GB health=200 0.010879
14:31:47 diskread=4GB health=200 0.012360
14:32:19 diskread=4GB health=200 0.009276
14:32:50 diskread=4GB health=200 0.010233
14:33:22 diskread=4GB health=200 0.013962
14:33:53 diskread=4GB health=200 0.011101
14:34:24 diskread=4GB health=200 0.012738
@@ -0,0 +1,6 @@
diskread: 483315769272
diskwrite: 47257374720
mem: 7075991552
memhost: 8218480640
netin: 4738753971
netout: 916386350
@@ -18,3 +18,28 @@
| 09:56 | B2 | the 10-18 17:42 link (used then) → „Ez a hivatkozás érvénytelen vagy lejárt" + an „Új linket kérek" button (not pressed) |
| 09:56:58 | B2 | bind attempt 1: right pairing code + passphrase with one letter changed → „A megadott adatok nem megfelelőek…" — generic, correct; the page does not say how many of the 5 tries are left |
| 09:57:09 | B2 | bind attempt 2: the stored value TESTER4_PASSWORD (from 2026-10-17, before Tester4 was re-created 10-18) → refused: it is stale. 2/5 used. **BLOCKED on the operator: the owner passphrase** |
| 12:00:57 | B2 | the operator's passphrase arrives in sec (intervention 1: an operator act the guide requires — the passphrase is handed over out of band by design) |
| 12:01:3x | B2 | bind attempt 3 (right code + passphrase) → „Sikeres összekötés" (`screens/b2-bind/`) |
| 12:02:25 | B2 | mail „[Felhom] Új beállító kód — újratelepült a szervered" (the guide's reinstall subject); code stored in sec |
| 12:02:43 | B2 | hub: host Tester4-816e21 created (NO REPORT yet); console turns to „a doboz össze van kötve" — the guide's „known bug: the code stays on screen" is FIXED (guide note stale); a stray „V" glyph follows the sentence (R-725 shape) |
| ≤12:13 | B2 | hub: Tester4-816e21 ONLINE, agent 0.161.0, controller 0.336.0 (the golden 0.331.0 → floor by itself); dashboard guest at 192.168.0.133 (DHCP) |
| 12:13 | B2 | harness: the household's address `https://felhom.tester4.test` has no DNS/tunnel (tester4.test is reserved); the browser LXC maps the name to the guest's LAN address in /etc/hosts, TLS not verified |
| 12:14:26 | B2 | claim with a typo in the setup code → „Hibás vagy lejárt kód" |
| 12:14:4x | B2 | claim with the right code + a 12-character password (exactly the minimum) → 302 → /launcher. **Install pressed → claimed: 27 min 50 s, of which 70 min of waiting for the passphrase are excluded (bind attempt 3 at 12:01)** |
| 12:15 | B2 | the yellow bar „A távoli mentés szünetel, amíg nem hozod létre a helyreállítási kódot" shows at once; the hub's Offsite page now lists Tester4 (0 B, no quota shown) and a WireGuard peer 10.77.0.6 → the operator switched off-site ON between 12:01 and 12:15 (intervention 2, an operator prerequisite the guide states) |
| 12:16:22 | B2 | recovery code ceremony through the bar's link: dashboard password → code ready in ~5 s → shown once (10 words) → the page asks words 2 and 7 → accepted → „Befejezés" → /backups/remote „A helyreállítási kód letétbe helyezve" — 16 s in all. The code went page → 0600 file → sec, never printed; every screenshot has the code blacked out (`screens/b2-escrow/`) |
| 12:16 | B2/B9 | the ceremony page shows technical checks to the household („Mentési tároló (PBS)", „Helyreállítási (DR) szint", „/usr/bin/age", „Rendszerjogosultság"); its step numbers jump 1 → 5 → 6; the remote page shows „restic + SFTP" and the Storage Box login `u629488-sub5@…your-storagebox.de /home/felhom-repo` |
| 12:17:13 | B2 | six wrong dashboard passwords from a fresh browser: five „Hibás jelszó.", the sixth „Túl sok hibás próbálkozás erről a címről. Próbáld újra egy perc múlva." (1 minute, per address). The guide's „15 percre zárol" is the SETUP page's rule; the login lock is not in the guide |
| — | B2 | the old (used) bind link: tested 09:56 → „érvénytelen vagy lejárt" + „Új linket kérek" (not pressed) |
| 12:18:09 | B3 | family card: „Ödön" and „Írisz" refused — „A név 1–32 karakter: kisbetű, szám, pont, kötőjel vagy aláhúzás." → added as odon, irisz, kata; each password answered once (→ sec) |
| 12:19 | B3 | the 200 GB second disk is NOT used by itself: the box made a 54.8 GB „Belső tárhely" on the system disk; the storage page offers „Új meghajtó inicializálása". The household pressed it: the first press did nothing (the required „Csatlakoztatási név" — „/mnt/<név>" — was empty; only a browser tooltip); with `hdd_1` → „A meghajtó sikeresen inicializálva és regisztrálva", default for new installs (12:19:48) |
| 12:22–12:55 | B3 | through each app's own install page (real Chromium): **Immich** OK 117 s (2048 MB); **Nextcloud** admin `odon` + a password with spaces and ő/ű: „Időtúllépés — az alkalmazás még indulhat" at ~2 min, running a few minutes later; **Paperless-ngx** admin `irisz` + a 64-character password OK; **Vaultwarden** sign-up switched ON OK; **Mealie** OK; **Actual Budget** OK; **Radicale** OK; **BookStack** OK; **Jellyfin REFUSED**: „Nincs elég memória … telepítés után 4150 MB kellene, a gép 3712 MB-ot tud adni. Távolíts el egy alkalmazást…" (button disabled; no link to a page). 8 of 9 installed; memory meter 3638 / 3712 MB (98 %) |
| 12:22 | B3 | harness slip: my screenshot step blanked every field before the first Nextcloud press, so the form was sent empty and refused (15 min lost); fixed (values restored after each photo) and re-run |
| 12:24–12:55 | B9 | „Első lépések" texts: Immich mixes English („Administration → External Libraries → add /external/photos"); Paperless says „felhasználó \"admin\"" although the household set `irisz`; Vaultwarden says sign-up is closed although the household opened it; **Mealie names `mealie.tester4.test` while the app is at `recipes.tester4.test`** |
| 12:56:33 | B4 | Paperless: 53 PDFs posted through `/api/documents/post_document/` → 53 accepted (processing is the app's background work) |
| 12:57 | B4 | Radicale: calendar „Család" (MKCALENDAR 201) + 2 events (201, one crossing the DST change); Mealie: Gulyásleves + Csirkepörkölt with ingredients, steps and photos (201/200/200) |
| 12:57:15 | B4 | Nextcloud: the 3 393-file tree over WebDAV (rclone, 4 transfers) started; rclone keeps only ONE of the NFC/NFD name pair („Duplicate object found in source") and skips the symlink — tool limits, not the box's; the 1.5 GiB file FAILED at 12:57:27 with an HTML error page |
| 12:58–12:59 | B4 | Immich: the setup gate shows a stranger „Ez az alkalmazás még beállításra vár"; Ödön registered as admin through Immich's own form; the gate opened by itself within 20 s |
| 12:59:52 | B4 | Immich upload through `/api/assets`: the FIRST photo hangs 300 s and times out |
| ~13:05 → | B4 | **THE BOX STOPS ANSWERING.** The dashboard's `/api/health` gives no answer in 10–15 s; the VM reads ~4 GB/min from its disk (158 GB → 209 GB in 15 min) while network traffic stands still; ping fine. 13:12 the upload stopped (the family gives up) — no recovery 10 min later. **The hub reads the box ONLINE / OK all along** (host memory 39 %, controller OK, 13:22). Likely: the apps' own background work after the uploads (Paperless OCR of 53 PDFs, Immich's analysis, Nextcloud) on a guest booked to 98 % of its memory — NOT measured (no shell in the box) |
| 13:24 | B4 | no inside view of the box — tried: root SSH with the install-time password → „Permission denied" (replaced at step 4b, by design); DooPlex's key → denied; the hub-vaulted recovery password → operator-only reveal (the token is refused); the agent's local API token → only on the box. So the stall is described from outside only |
@@ -0,0 +1,142 @@
# Rehearsal 2026-10-20 — every screen the household met
Phone = 390×844, computer = 1366×768 (Chromium 155 + Playwright 1.52.0 in a throwaway container; see `../A4-browser/`). Hungarian and English browser languages. Codes, passwords and links are blacked out or never shown. Console captures are QEMU screendumps of the VM's monitor.
## b1-site
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b1-letoltes` | <img src="b1-site/b1-letoltes-phone-hu.png" width="140"> | <img src="b1-site/b1-letoltes-phone-en.png" width="140"> | <img src="b1-site/b1-letoltes-desktop-hu.png" width="260"> | <img src="b1-site/b1-letoltes-desktop-en.png" width="260"> |
## b2-badpw
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b2-badpw-0-login` | <img src="b2-badpw/b2-badpw-0-login-phone-hu.png" width="140"> | — | — | — |
| `b2-badpw-6-after-six` | <img src="b2-badpw/b2-badpw-6-after-six-phone-hu.png" width="140"> | — | — | — |
## b2-bind
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b2-bind-right-1-open` | — | — | <img src="b2-bind/b2-bind-right-1-open-desktop-hu.png" width="260"> | — |
| `b2-bind-right-2-answer` | — | — | <img src="b2-bind/b2-bind-right-2-answer-desktop-hu.png" width="260"> | — |
| `b2-bind-stored-1-open` | <img src="b2-bind/b2-bind-stored-1-open-phone-hu.png" width="140"> | — | — | — |
| `b2-bind-stored-2-answer` | <img src="b2-bind/b2-bind-stored-2-answer-phone-hu.png" width="140"> | — | — | — |
| `b2-bind-typo-1-open` | <img src="b2-bind/b2-bind-typo-1-open-phone-hu.png" width="140"> | — | — | — |
| `b2-bind-typo-2-answer` | <img src="b2-bind/b2-bind-typo-2-answer-phone-hu.png" width="140"> | — | — | — |
| `b2-oldlink-1-open` | <img src="b2-bind/b2-oldlink-1-open-phone-hu.png" width="140"> | — | — | — |
## b2-claim
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b2-claim-1-open` | <img src="b2-claim/b2-claim-1-open-phone-hu.png" width="140"> | — | — | — |
| `b2-claim-2-typo-answer` | <img src="b2-claim/b2-claim-2-typo-answer-phone-hu.png" width="140"> | — | — | — |
| `b2-claim-3-after-claim` | <img src="b2-claim/b2-claim-3-after-claim-phone-hu.png" width="140"> | — | — | — |
## b2-escrow
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b2-escrow-1-open` | <img src="b2-escrow/b2-escrow-1-open-phone-hu.png" width="140"> | — | — | — |
| `b2-escrow-2-progress` | <img src="b2-escrow/b2-escrow-2-progress-phone-hu.png" width="140"> | — | — | — |
| `b2-escrow-3-reveal-gate` | <img src="b2-escrow/b2-escrow-3-reveal-gate-phone-hu.png" width="140"> | — | — | — |
| `b2-escrow-4-code-shown-REDACTED` | <img src="b2-escrow/b2-escrow-4-code-shown-REDACTED-phone-hu.png" width="140"> | — | — | — |
| `b2-escrow-5-words-ok` | <img src="b2-escrow/b2-escrow-5-words-ok-phone-hu.png" width="140"> | — | — | — |
| `b2-escrow-6-after-finish` | <img src="b2-escrow/b2-escrow-6-after-finish-phone-hu.png" width="140"> | — | — | — |
## b3-apps
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b3-apps-actualbudget-1-form` | — | — | <img src="b3-apps/b3-apps-actualbudget-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-actualbudget-2-result` | — | — | <img src="b3-apps/b3-apps-actualbudget-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-bookstack-1-form` | — | — | <img src="b3-apps/b3-apps-bookstack-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-bookstack-2-result` | — | — | <img src="b3-apps/b3-apps-bookstack-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-immich-1-install-page` | — | — | <img src="b3-apps/b3-apps-immich-1-install-page-desktop-hu.png" width="260"> | — |
| `b3-apps-immich-2-result` | — | — | <img src="b3-apps/b3-apps-immich-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-jellyfin-1-form` | — | — | <img src="b3-apps/b3-apps-jellyfin-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-jellyfin-refused` | <img src="b3-apps/b3-apps-jellyfin-refused-phone-hu.png" width="140"> | — | — | — |
| `b3-apps-mealie-1-form` | — | — | <img src="b3-apps/b3-apps-mealie-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-mealie-2-result` | — | — | <img src="b3-apps/b3-apps-mealie-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-nextcloud-1-form` | — | — | <img src="b3-apps/b3-apps-nextcloud-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-nextcloud-2-result` | — | — | <img src="b3-apps/b3-apps-nextcloud-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-paperless-ngx-1-form` | — | — | <img src="b3-apps/b3-apps-paperless-ngx-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-paperless-ngx-2-result` | — | — | <img src="b3-apps/b3-apps-paperless-ngx-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-radicale-1-form` | — | — | <img src="b3-apps/b3-apps-radicale-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-radicale-2-result` | — | — | <img src="b3-apps/b3-apps-radicale-2-result-desktop-hu.png" width="260"> | — |
| `b3-apps-vaultwarden-1-form` | — | — | <img src="b3-apps/b3-apps-vaultwarden-1-form-desktop-hu.png" width="260"> | — |
| `b3-apps-vaultwarden-2-result` | — | — | <img src="b3-apps/b3-apps-vaultwarden-2-result-desktop-hu.png" width="260"> | — |
| `b3-drives-1-storage` | — | — | <img src="b3-apps/b3-drives-1-storage-desktop-hu.png" width="260"> | — |
| `b3-drives-2-dashboard` | — | — | <img src="b3-apps/b3-drives-2-dashboard-desktop-hu.png" width="260"> | — |
| `b3-drives-3-init-open` | — | — | <img src="b3-apps/b3-drives-3-init-open-desktop-hu.png" width="260"> | — |
| `b3-drives-4-init-config` | — | — | <img src="b3-apps/b3-drives-4-init-config-desktop-hu.png" width="260"> | — |
| `b3-drives-5-init-after-press` | — | — | <img src="b3-apps/b3-drives-5-init-after-press-desktop-hu.png" width="260"> | — |
| `b3-drives-5-init-confirm` | — | — | <img src="b3-apps/b3-drives-5-init-confirm-desktop-hu.png" width="260"> | — |
| `b3-family-1-security` | <img src="b3-apps/b3-family-1-security-phone-hu.png" width="140"> | — | — | — |
## b4-immich
| screen | phone HU | phone EN | computer HU | computer EN |
|---|---|---|---|---|
| `b4-immich-0-stranger` | <img src="b4-immich/b4-immich-0-stranger-phone-hu.png" width="140"> | — | — | — |
| `b4-immich-1-first-open` | <img src="b4-immich/b4-immich-1-first-open-phone-hu.png" width="140"> | — | — | — |
| `b4-immich-2-signup-form` | — | — | <img src="b4-immich/b4-immich-2-signup-form-desktop-hu.png" width="260"> | — |
| `b4-immich-3-signup-filled` | — | — | <img src="b4-immich/b4-immich-3-signup-filled-desktop-hu.png" width="260"> | — |
| `b4-immich-4-after-signup` | — | — | <img src="b4-immich/b4-immich-4-after-signup-desktop-hu.png" width="260"> | — |
## install
**01-boot.png**
<img src="install/01-boot.png" width="520">
**02-eula.png**
<img src="install/02-eula.png" width="520">
**03-disk.png**
<img src="install/03-disk.png" width="520">
**04-location.png**
<img src="install/04-location.png" width="520">
**05-password.png**
<img src="install/05-password.png" width="520">
**06-pw-email.png**
<img src="install/06-pw-email.png" width="520">
**07-network.png**
<img src="install/07-network.png" width="520">
**07b-hostname.png**
<img src="install/07b-hostname.png" width="520">
**08-summary.png**
<img src="install/08-summary.png" width="520">
**09-installing.png**
<img src="install/09-installing.png" width="520">
**10-progress.png**
<img src="install/10-progress.png" width="520">
**11-console.png**
<img src="install/11-console.png" width="520">
**12-after-bind.png**
<img src="install/12-after-bind.png" width="520">
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -90,7 +90,7 @@ def nextcloud(sub, user_env, pw_env):
obscured = subprocess.run(["rclone", "obscure", "-"], input=pw, capture_output=True, text=True).stdout.strip()
env = dict(os.environ, RCLONE_CONFIG_NC_TYPE="webdav", RCLONE_CONFIG_NC_URL=f"https://{sub}.{DOMAIN}/remote.php/dav/files/{user}",
RCLONE_CONFIG_NC_VENDOR="nextcloud", RCLONE_CONFIG_NC_USER=user, RCLONE_CONFIG_NC_PASS=obscured)
r = subprocess.run(["rclone", "copy", "--no-check-certificate", "--transfers", "4", "--stats", "60s", "--stats-one-line",
r = subprocess.run(["rclone", "copy", "--no-check-certificate", "--transfers", os.environ.get("TRANSFERS", "1"), "--stats", "60s", "--stats-one-line",
"-v", os.path.join(DATA, "nextcloud"), "nc:/"], env=env, capture_output=True, text=True)
tail = [l for l in (r.stdout + r.stderr).splitlines() if "ERROR" in l or "Transferred" in l or "Skipping" in l or "NOTICE" in l]
say(f"nextcloud rclone rc={r.returncode}")
@@ -0,0 +1,23 @@
#!/usr/bin/python3
"""counts.py — what each app holds now, read through the app's own API (B4/B5/B7 comparisons). Env names are args."""
import json, os, subprocess, sys
sys.path.insert(0, "/root/work"); import apps
from apps import http, basic
def paperless(u, p):
st, out, _ = http("GET", "https://paperless.tester4.test/api/documents/?page_size=1", None, {"Authorization": basic(os.environ[u], os.environ[p])})
t = json.loads(out).get("count") if st == 200 else out[:120]
st2, out2, _ = http("GET", "https://paperless.tester4.test/api/tasks/?status=FAILURE", None, {"Authorization": basic(os.environ[u], os.environ[p])})
fails = [x.get("task_file_name") for x in json.loads(out2)] if st2 == 200 else out2[:100]
print("paperless documents:", t, "| failed tasks:", fails)
def immich(e, p):
st, out, _ = http("POST", "https://photos.tester4.test/api/auth/login", {"email": os.environ[e], "password": os.environ[p]})
tok = {"Authorization": "Bearer " + json.loads(out)["accessToken"]}
st, out, _ = http("GET", "https://photos.tester4.test/api/assets/statistics", None, tok)
print("immich assets:", out.decode()[:200])
def nextcloud(u, p):
obs = subprocess.run(["rclone", "obscure", "-"], input=os.environ[p], capture_output=True, text=True).stdout.strip()
env = dict(os.environ, RCLONE_CONFIG_NC_TYPE="webdav", RCLONE_CONFIG_NC_URL=f"https://cloud.tester4.test/remote.php/dav/files/{os.environ[u]}",
RCLONE_CONFIG_NC_VENDOR="nextcloud", RCLONE_CONFIG_NC_USER=os.environ[u], RCLONE_CONFIG_NC_PASS=obs)
r = subprocess.run(["rclone", "size", "--no-check-certificate", "nc:/Csalad"], env=env, capture_output=True, text=True)
print("nextcloud Csalad:", (r.stdout + r.stderr).strip().replace("\n", " | ")[:300])
globals()[sys.argv[1]](*sys.argv[2:])
@@ -74,3 +74,18 @@ if __name__ == "__main__":
code, body = call(m, p, d)
print(code)
print(json.dumps(body, ensure_ascii=False, indent=1)[:6000])
def form(path, fields, timeout=600):
"""A form POST (application/x-www-form-urlencoded) with the session and the X-CSRF-Token header."""
import urllib.parse
if _SESS is None:
login()
out = _curl(["-H", "Cookie: " + _SESS, "-H", "X-CSRF-Token: " + _CSRF, "-w", "\n%{http_code}", "-X", "POST",
"--data-binary", "@-", "-H", "Content-Type: application/x-www-form-urlencoded", BASE + path],
inp=urllib.parse.urlencode(fields), timeout=timeout)
body, _, code = out.rpartition("\n")
try:
return code, json.loads(body)
except ValueError:
return code, {"_raw": body[:800]}
@@ -0,0 +1,80 @@
#!/opt/pw/bin/python
"""escrow.py — the household's recovery code ceremony in real Chromium (rehearsal 2026-10-20, B2).
Env: PW (the dashboard password). The recovery code is read off the page into /root/work/rc (0600) — never printed —
and DooPlex moves it into `sec` and shreds the file. Before every screenshot the code block is replaced by black
bars (the page keeps the code in a JS closure, so the check still works).
usage: escrow.py OUTPREFIX [--size phone|desktop]
"""
import os, re, sys, time
from playwright.sync_api import sync_playwright
out = sys.argv[1]
size = sys.argv[sys.argv.index("--size") + 1] if "--size" in sys.argv else "phone"
vp = {"phone": dict(viewport={"width": 390, "height": 844}, device_scale_factor=2, is_mobile=True, has_touch=True),
"desktop": dict(viewport={"width": 1366, "height": 768})}[size]
T0 = time.time()
def say(*a):
print(f"+{time.time() - T0:6.1f}s", *a, flush=True)
def shot(pg, name):
pg.evaluate("""() => { const b = document.getElementById('code-block'); if (b && b.textContent) b.textContent = '█████-█████-█████-█████-█████-█████-█████-█████-█████-█████';
document.querySelectorAll('input').forEach(e => { if (e.type !== 'checkbox') e.value = ''; }); }""")
pg.screenshot(path=f"{out}-{name}-{size}-hu.png", full_page=True)
with sync_playwright() as p:
b = p.chromium.launch(executable_path="/usr/bin/chromium", args=["--no-sandbox"])
ctx = b.new_context(locale="hu-HU", timezone_id="Europe/Budapest", ignore_https_errors=True,
storage_state="/root/work/state-dash.json", **vp)
pg = ctx.new_page()
pg.goto("https://felhom.tester4.test/backup/escrow", wait_until="networkidle")
pg.wait_for_selector("#warn-card", state="visible", timeout=60000)
pg.fill("#reauth-password", os.environ["PW"])
pg.click("#start-btn")
say("start pressed")
pg.wait_for_timeout(1500)
shot(pg, "2-progress")
try:
pg.wait_for_selector("#reveal-card, #void-card", state="visible", timeout=300000)
except Exception:
say("no reveal within 300 s:", re.sub(r"\s+", " ", pg.inner_text("#progress-card"))[:400])
shot(pg, "2b-stuck")
sys.exit(2)
if pg.is_visible("#void-card"):
say("VOID card:", re.sub(r"\s+", " ", pg.inner_text("#void-card"))[:400])
shot(pg, "3-void")
sys.exit(3)
say("reveal card shown")
shot(pg, "3-reveal-gate")
pg.click("#reveal-btn")
pg.wait_for_selector("#reveal-shown", state="visible", timeout=60000)
code = pg.inner_text("#code-block").strip()
fd = os.open("/root/work/rc", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
os.write(fd, code.encode())
os.close(fd)
words = code.split("-")
say(f"code read off the page: {len(words)} words, {len(code)} characters (stored 0600, not printed)")
label = pg.inner_text("#verify-label")
pos = [int(x) for x in re.findall(r"\d+", label)][:2]
say("the page asks for words at positions", pos, "| label:", label)
shot(pg, "4-code-shown-REDACTED")
pg.fill("#verify-w1", words[pos[0] - 1])
pg.fill("#verify-w2", words[pos[1] - 1])
pg.click("#verify-btn")
pg.wait_for_timeout(4000)
if pg.is_visible("#verify-error"):
say("verify error:", pg.inner_text("#verify-error"))
pg.wait_for_selector("#finish-block", state="visible", timeout=60000)
say("words accepted; finish block:", re.sub(r"\s+", " ", pg.inner_text("#finish-block"))[:400])
shot(pg, "5-words-ok")
pg.check("#finish-check")
pg.click("#finish-btn")
pg.wait_for_load_state("networkidle")
pg.wait_for_timeout(3000)
say("after finish:", pg.url, re.sub(r"\s+", " ", pg.inner_text("main") if pg.locator("main").count() else pg.inner_text("body"))[:900])
shot(pg, "6-after-finish")
b.close()
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""family.py — add the household's three family members through the „Család" card's endpoint (B3).
Tries each name first as the family would write it (accented), then the plain form. The box answers each new member's
password ONCE; it goes to a 0600 file per member (DooPlex moves it into sec) and is never printed."""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ctl
outdir = sys.argv[1]
for want, plain in (("Ödön", "odon"), ("Írisz", "irisz"), ("Kata", "kata")):
for name in (want, plain) if want.lower() != plain else (plain,):
code, d = ctl.form("/family/members/add", {"name": name})
pw = (d.get("data") or {}).get("password") if isinstance(d, dict) else None
shown = {k: v for k, v in (d.get("data") or {}).items() if k != "password"} if isinstance(d, dict) and isinstance(d.get("data"), dict) else d
print(f"add {name!r}: {code} {shown if code == '200' else d}" + (" [password answered once → file]" if pw else ""))
if pw:
fd = os.open(os.path.join(outdir, f"fam-{plain}"), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
os.write(fd, pw.encode()); os.close(fd)
break
print("members:", ctl.call("GET", "/family/members")[1])
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
"""install_spec.py APP [FIELD=VALUE ...] [--pw ENVNAME] — writes the ui.py steps for one app's install page:
open it, fill the named fields, fill every password box and its confirmation with the secret ENVNAME, press
„Telepítés indítása", wait for the result card (15 min), screenshot, print the text."""
import json, sys
app = sys.argv[1]; steps = [{"goto": f"https://felhom.tester4.test/apps/{app}/install"}, {"wait": 2000}]
pw = None
for a in sys.argv[2:]:
if a.startswith("--pw="):
pw = a[5:]
elif "=" in a:
k, v = a.split("=", 1)
steps.append({"select" if k.startswith("select:") else "fill": f"[name={k.split(':')[-1]}]", ("option" if k.startswith("select:") else "value"): v})
if pw:
steps += [{"fill_all": "input[data-field-type=password]", "env": pw}, {"fill_all": "input[data-confirm-for]", "env": pw}]
steps += [{"shot": f"{app}-1-form"}, {"click": "#deploy-form button[type=submit]"}, {"wait_for": "#deploy-result", "timeout": 900000},
{"wait": 3000}, {"shot": f"{app}-2-result"}, {"text": True}]
print(json.dumps({"size": "desktop", "lang": "hu", "state": "/root/work/state-dash.json", "out": "/root/shots/b3-apps", "steps": steps}))
@@ -0,0 +1,107 @@
#!/opt/pw/bin/python
"""ui.py — drive the household's screens in real Chromium, step by step (rehearsal 2026-10-20).
usage: ui.py STEPS.json (STEPS may also be '-' to read JSON from the env var UI_STEPS)
STEPS = {"size": "phone"|"desktop", "lang": "hu"|"en", "state": "/root/work/state-dash.json" (load+save cookies),
"out": "/root/shots/<prefix>", "steps": [ ... ]}
step kinds:
{"goto": URL} {"click": SELECTOR} / {"click_text": TEXT}
{"fill": SELECTOR, "env": NAME} — the value comes from env NAME (never printed)
{"fill": SELECTOR, "value": TEXT} — a non-secret value
{"fill": SELECTOR, "env": NAME, "typo": true} — one letter changed (a household's typo)
{"shot": NAME} — blanks every text/password/textarea input first, then screenshots full page
{"wait": MS} {"text": true}
{"wait_for": SELECTOR, "timeout": MS} {"fill_all": SELECTOR, "env": NAME} — every enabled match gets the secret
{"select": SELECTOR, "option": VALUE} — prints the visible page text (trimmed)
{"expect_nav": true} before a click — waits for the navigation the click causes
Every step prints one line: what it did, the URL (any 32+ hex run masked), the HTTP status of navigations.
"""
import json, os, re, sys
from playwright.sync_api import sync_playwright
spec = json.loads(os.environ["UI_STEPS"]) if sys.argv[1] == "-" else json.load(open(sys.argv[1]))
SIZES = {"phone": dict(viewport={"width": 390, "height": 844}, device_scale_factor=2, is_mobile=True, has_touch=True,
user_agent="Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/155.0.0.0 Mobile Safari/537.36"),
"desktop": dict(viewport={"width": 1366, "height": 768})}
LOC = {"hu": "hu-HU", "en": "en-US"}
mask = lambda u: re.sub(r"[0-9a-f]{32,}", "<token>", u)
size, lang = spec.get("size", "phone"), spec.get("lang", "hu")
out = spec.get("out", "/root/shots/x")
def typo(v):
i = len(v) // 2
while i < len(v) and not v[i].isalpha():
i += 1
return v[:i] + ("q" if v[i] != "q" else "w") + v[i + 1:] if i < len(v) else v + "x"
with sync_playwright() as p:
b = p.chromium.launch(executable_path="/usr/bin/chromium", args=["--no-sandbox"])
kw = dict(locale=LOC[lang], extra_http_headers={"Accept-Language": LOC[lang]}, timezone_id="Europe/Budapest",
ignore_https_errors=True, **SIZES[size])
st = spec.get("state")
if st and os.path.exists(st):
kw["storage_state"] = st
ctx = b.new_context(**kw)
pg = ctx.new_page()
resp = {"s": None}
pg.on("response", lambda r: resp.update(s=r.status) if r.request.is_navigation_request() else None)
nav_next = False
for s in spec["steps"]:
try:
if "goto" in s:
r = pg.goto(s["goto"], wait_until="networkidle", timeout=60000)
print(f"goto {mask(pg.url)} -> {r.status if r else '-'} {pg.title()!r}")
elif "fill" in s:
v = os.environ[s["env"]] if "env" in s else s["value"]
if s.get("typo"):
v = typo(v)
pg.fill(s["fill"], v)
print(f"fill {s['fill']} ({'secret ' + s['env'] if 'env' in s else repr(v)}{', with a typo' if s.get('typo') else ''})")
elif "click" in s or "click_text" in s:
target = pg.locator(s["click"]) if "click" in s else pg.get_by_text(s["click_text"], exact=False)
if nav_next:
with pg.expect_navigation(wait_until="networkidle", timeout=60000):
target.first.click()
nav_next = False
else:
target.first.click()
pg.wait_for_load_state("networkidle", timeout=60000)
print(f"click {s.get('click') or s.get('click_text')!r} -> {mask(pg.url)} status={resp['s']}")
elif "expect_nav" in s:
nav_next = True
elif "wait_for" in s:
pg.wait_for_selector(s["wait_for"], state="visible", timeout=s.get("timeout", 600000))
print(f"visible {s['wait_for']}")
elif "fill_all" in s:
n = 0
for el in pg.locator(s["fill_all"]).all():
if el.is_enabled():
el.fill(os.environ[s["env"]]); n += 1
print(f"fill_all {s['fill_all']} ({n} fields, secret {s['env']})")
elif "select" in s:
pg.select_option(s["select"], s["option"])
print(f"select {s['select']} = {s['option']!r}")
elif "wait" in s:
pg.wait_for_timeout(s["wait"])
elif "shot" in s:
# blank every text/password field FOR THE PHOTO ONLY, then put the values back (a blanked form that is
# submitted afterwards refuses — the 2026-10-20 Nextcloud slip)
pg.evaluate("window.__keep = []; document.querySelectorAll('input[type=text],input[type=password],input:not([type]),textarea')"
".forEach(e => { if (e.value) { window.__keep.push([e, e.value]); e.value = ''; } })")
f = f"{out}-{s['shot']}-{size}-{lang}.png"
pg.screenshot(path=f, full_page=True)
pg.evaluate("(window.__keep || []).forEach(([e, v]) => { e.value = v; }); window.__keep = [];")
hs = pg.evaluate("document.documentElement.scrollWidth > window.innerWidth")
print(f"shot {f} hscroll={hs}")
elif "text" in s:
print("text:", re.sub(r"\s+", " ", pg.inner_text("body"))[:1500])
except Exception as e: # noqa: BLE001 — a failing step is a result; record it and stop
print(f"STEP FAILED {s}: {type(e).__name__}: {str(e)[:300]}")
break
if st:
ctx.storage_state(path=st)
os.chmod(st, 0o600)
b.close()