Family gate session close: B4 on demo-hp (real internet: member in, stranger 401; remove with data), REPORT-family-gate-2026-10-02, STATUS, R-800 (remove-with-data keeps userdata, operator), CONTEXT; register 436
gates / gates (push) Successful in 26s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-02 10:43:10 +02:00
parent e6d1ebd152
commit 2633dc1654
15 changed files with 319 additions and 43 deletions
@@ -0,0 +1,5 @@
##### B4 on demo-hp 9201 — controller gitea.dooplex.hu/admin/felhom-controller:0.287.0 | catalog 96829d0 Grimmory and MeTube published behind the family gate (controller >= 0.287.0, decis | 2026-10-02T07:38:25Z
before: gate files: none | drive: /dev/nvme0n1 938G 74G 817G 9% /mnt/felhom-drives/hdd_1
the public names resolve to: 2a06:98c1:3121::3 video.enkisfelhom.hu
2a06:98c1:3120::3 video.enkisfelhom.hu (Cloudflare — the REAL internet path)
Család card: add b4teszt -> ok=True (password returned once, len 19, not printed)
@@ -0,0 +1,12 @@
deploy grimmory (fields beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'], values not printed) -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
gate files right after the press: family-gate-grimmory.yml setup-gate-grimmory.yml
deployed in 85 s: state=running pinned={'grimmory': 'ghcr.io/grimmory-tools/grimmory:v3.5.0', 'grimmory-db': 'mariadb:11.4'}
family_gate record: family_gate: since: "2026-10-02T07:38:31Z" hosts: - library.enkisfelhom.hu
the household makes the first admin through the setup gate -> 200
setup gate opened by its probe after ~10 s; gate files: family-gate-grimmory.yml
NOTE: attempt 1 stopped on a HARNESS error: after Grimmory's setup gate opened, box_walk still held the SETUP-gate cookie, which the family gate rightly refused (401 on an API path, no redirect, so no retry). The product behaved as designed. Cleaned up through the product (remove with data, member removed) before attempt 2.
cleanup: remove grimmory WITH data -> ('200', {'ok': True, 'data': {'removed': 'grimmory', 'volumes_removed': ['grimmory_grimmory_db'], 'hdd_paths_removed': ['/mnt/felhom-drives/hdd_1/appdata/grimmory/data (4.0K)'], 'hdd_paths_preserved': [], 'backup_paths_removed': ['/mnt/felhom-drives/hdd_1/backups/primary/grimmory (32K)'], 'verified': True}, 'message': 'Stack grimmory removed'})
cleanup: remove member b4teszt -> {"data":{"members":[],"name":"b4teszt"},"error":"","ok":true}
left: /mnt/felhom-drives/hdd_1/appdata/grimmory
/mnt/felhom-drives/hdd_1/userdata/media/grimmory
@@ -0,0 +1,7 @@
##### B4 on demo-hp 9201 — controller gitea.dooplex.hu/admin/felhom-controller:0.287.0 | catalog 96829d0 Grimmory and MeTube published behind the family gate (controller >= 0.287.0, decis | 2026-10-02T08:10:07Z
before: gate files: none | drive: /dev/nvme0n1 938G 74G 817G 9% /mnt/felhom-drives/hdd_1
the public names resolve to: 2a06:98c1:3120::3 video.enkisfelhom.hu
2a06:98c1:3121::3 video.enkisfelhom.hu (Cloudflare — the REAL internet path)
Család card: add b4teszt -> ok=True (password returned once, len 19, not printed)
Család card: remove b4teszt -> True | members now: []
after: gate files: none | stacks: 0
@@ -0,0 +1,17 @@
deploy grimmory (fields beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'], values not printed) -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
gate files right after the press: family-gate-grimmory.yml setup-gate-grimmory.yml
deployed in 80 s: state=running pinned={'grimmory': 'ghcr.io/grimmory-tools/grimmory:v3.5.0', 'grimmory-db': 'mariadb:11.4'}
family_gate record: family_gate: since: "2026-10-02T08:10:13Z" hosts: - library.enkisfelhom.hu
the household makes the first admin through the setup gate -> 200
setup gate opened by its probe after ~10 s; gate files: family-gate-grimmory.yml
grimmory: readback http=200 found=True (controls: no token 401, wrong 401, second setup 403)
a book uploaded -> 204 | read back: True
on the drive: /mnt/felhom-drives/hdd_1/userdata/media/grimmory/Felhom/Felhom Demo 7ca212a4/Felhom Demo 7ca212a4 - Felhom.epub |
a STRANGER / internet 401 | LAN 401
a STRANGER /api/v1/books internet 401 | LAN 401
a STRANGER /api/v1/opdsx internet 401 | LAN 401
a STRANGER /api/v1/opds internet 401 | LAN 401
the family member b4teszt through the REAL internet: the app's address → the family sign-in → back: 403 at library.enkisfelhom.hu/ (the app's own page: False)
2.6 remove WITH data and backups -> 200 {'ok': True, 'data': {'removed': 'grimmory', 'volumes_removed': ['grimmory_grimmory_db'], 'hdd_paths_removed': ['/mnt/felhom-drives/hdd_1/appdata/grimmory/data (4.0K)'], 'hdd_paths_preserved': [], 'backup_paths_removed': ['/mnt/felhom-drives/hdd_1/backups/primary/grimmory (32K)'], 'verified': True},
left after: /mnt/felhom-drives/hdd_1/userdata/media/grimmory/Felhom/Felhom Demo 7ca212a4/Felhom Demo 7ca212a4 - Felhom.epub | /mnt/felhom-drives/hdd_1/appdata/grimmory | /mnt/felhom-drives/hdd_1/userdata/media/grimmory
gate files: family-gate-grimmory.yml family-gate-metube.yml | a stranger at library/ -> internet 404 | LAN 404
@@ -0,0 +1,13 @@
##### B4 member through the real internet — 2026-10-02T08:40:07Z
control: Python's user agent at the edge -> 403 | a browser's -> 404 (MeTube not installed yet: 404 from the box)
deploy metube -> 202
state=running
Család card: add b4teszt -> ok=True (password returned once, not printed)
a member's browser opens video.enkisfelhom.hu (no cookie) -> 200 at https://felhom.enkisfelhom.hu/__family/start (the family sign-in page: True)
the member signs in on the box's own page -> 200 at https://video.enkisfelhom.hu/ (MeTube's own page: True)
the member reads /history -> 200 (True)
the same browser at the DASHBOARD (/launcher) -> 200 at https://felhom.enkisfelhom.hu/login (sent to the dashboard login: True)
logout -> 200; then /history -> 200 at https://felhom.enkisfelhom.hu/__family/start (refused: True)
a STRANGER (no cookie, browser UA) at video.enkisfelhom.hu/history -> 401
remove metube WITH data and backups -> 200 {'ok': True, 'data': {'removed': 'metube', 'volumes_removed': ['metube_metube_state'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját adatot külső megh
Család card: remove b4teszt -> ok=True members now []
@@ -0,0 +1,16 @@
deploy metube (fields beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'], values not printed) -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
gate files right after the press: family-gate-grimmory.yml family-gate-metube.yml
deployed in 40 s: state=running pinned={'metube': 'ghcr.io/alexta69/metube:2026.09.29'}
family_gate record: family_gate: since: "2026-10-02T08:12:13Z" hosts: - video.enkisfelhom.hu
metube: POST /add http=200 {"status": "ok"}
metube: the download finished (991017 bytes)
metube: history has it=True; GET /download/<file> http=200
the seed reads back: True
on the drive: /mnt/felhom-drives/hdd_1/userdata/media/metube/Big_Buck_Bunny_360_10s_1MB.mp4 |
a STRANGER / internet 401 | LAN 401
a STRANGER /history internet 401 | LAN 401
a STRANGER /socket.io/?EIO=4&transport=polling internet 401 | LAN 401
the family member b4teszt through the REAL internet: the app's address → the family sign-in → back: 403 at video.enkisfelhom.hu/ (the app's own page: False)
2.6 remove WITH data and backups -> 200 {'ok': True, 'data': {'removed': 'metube', 'volumes_removed': ['metube_metube_state'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját adatot külső meghajtón, így ott nem volt mit törölni.', 'verified': True}, 'message': 'Stack metube removed'}
left after: /mnt/felhom-drives/hdd_1/userdata/media/metube/Big_Buck_Bunny_360_10s_1MB.mp4 | /mnt/felhom-drives/hdd_1/userdata/media/metube
gate files: none | a stranger at video/ -> internet 404 | LAN 404
@@ -0,0 +1,14 @@
## 2026-10-02T08:41:34Z demo-hp 9201 teardown (three layers)
machine — before the hand tidy:
/mnt/felhom-drives/hdd_1/userdata/media/grimmory/Felhom/Felhom Demo 7ca212a4/Felhom Demo 7ca212a4 - Felhom.epub
/mnt/felhom-drives/hdd_1/userdata/media/metube/Big_Buck_Bunny_360_10s_1MB.mp4
0
0
1
the test files above are this session's own (a test EPUB, the 1 MB test clip) — removed by hand with the empty test folders; the product kept them on purpose (userdata = the household's files, R-800):
audiobooks books comics movies music photos podcasts tv
adventurelog adventurelog-frontend adventurelog-postgres bentopdf bookstack bookstack-db calibre-web cloudflared docmost docmost-postgres docmost-redis felhom-controller filebrowser kimai kimai-db opengist paperless-postgres paperless-redis paperless-webserver privatebin romm romm-db romm-redis traefik
host — no guest, storage or file created on demo-hp itself (9201 only).
hub — read only; the floor and the artifact manifest changed on purpose (E/floor.txt, golden record); no customer or appliance record created.
Család card on demo-hp: members [] (b4teszt removed).
note: the 1 family-gate file counted above was MeTube's, read ~10 s after its removal; the box's tick removed it at 08:41:36 ("removed the family-gate file of an app that is not installed"); video.enkisfelhom.hu answers 404 after. Read 2026-10-02T08:42Z.
@@ -69,6 +69,7 @@ def deploy(app, sub):
w.login()
w.sync_rescan("metube", "ghcr.io/alexta69/metube:2026.09.29")
say(None, f"##### B4 on demo-hp 9201 — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()} | catalog "
+ w.guest("cd /opt/docker/felhom-controller/data/catalog-cache 2>/dev/null || cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache; git log --oneline -1").strip()[:90]
+ f" | {time.strftime('%FT%TZ', time.gmtime())}")
@@ -95,6 +96,7 @@ for app, (sub, fx) in APPS.items():
if "setup-gate-grimmory" not in gate_files(): break
time.sleep(5)
say(app, f" setup gate opened by its probe after ~{5*i} s; gate files: {gate_files()}")
w.GATE.pop(sub, None) # the SETUP-gate cookie is useless now; the family gate wants a family cookie (attempt 1)
code, tok = fx._token(w, sub, "admin", pw)
auth = ["-H", f"Authorization: Bearer {tok}"]
rc, c, out = w.app_curl(sub, "/api/v1/libraries", *auth, "-H", "Content-Type: application/json",
@@ -0,0 +1,54 @@
"""fg_b4_lib.py — the demo-box helpers fg_b4_member.py uses (deploy, the Család card, remove), product endpoints only."""
import json, os, secrets, subprocess, sys, time
os.environ.setdefault("GUEST", "9201"); os.environ.setdefault("BASE", "https://192.168.0.155")
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
w.HP = "hp"
DOM = w.DOMAIN; CTL = f"felhom.{DOM}"; DRIVE = "/mnt/felhom-drives/hdd_1"
def _card(action, name):
sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip(); csrf = open(f"{w.SC}/csrf{os.getpid()}.txt").read().strip()
r = subprocess.run(["curl", "-sk", "-H", f"Host: {CTL}", "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}", "-X", "POST",
"--data", f"name={name}", f"{w.BASE}/family/members/{action}"], capture_output=True, text=True)
return json.loads(r.stdout)
def add_member(name, say):
d = _card("add", name)
say(f"Család card: add {name} -> ok={d['ok']} (password returned once, not printed)")
return d["data"]["password"]
def remove_member(name, say):
d = _card("remove", name)
say(f"Család card: remove {name} -> ok={d['ok']} members now {d['data'].get('members')}")
def deploy(app, sub, say):
code, d = w.ctl("GET", f"/api/stacks/{app}/deploy-fields")
fields = (((d.get("data") or {}).get("metadata") or {}).get("deploy_fields")) or []
values = {"DOMAIN": DOM, "SUBDOMAIN": sub}
for f in fields:
ev, ty = f.get("env_var"), f.get("type")
if ev in values or (not f.get("required") and ty != "password"):
continue
values[ev] = DRIVE if ty == "path" else ("Demo-" + secrets.token_hex(12) if ty in ("secret", "password") else (f.get("default") or app))
code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": values})
say(f"deploy {app} -> {code}")
for _ in range(120):
time.sleep(5)
st = w.stack(app)
if st.get("deployed") and (st.get("app_config") or {}).get("pinned_images") and st.get("state") == "running":
break
say(f" state={st.get('state')}")
def remove(app, say):
w.ctl("POST", f"/api/stacks/{app}/stop")
for _ in range(24):
time.sleep(5)
if w.stack(app).get("state") != "running":
break
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": True, "remove_backups": True})
say(f"remove {app} WITH data and backups -> {code} {str(d)[:200]}")
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""fg_b4_member.py — B4 part 2: a family member signs in through the REAL internet (Cloudflare → demo-hp's tunnel) with a
browser's user agent. Attempt 2 of fg_b4.py used Python's urllib, whose user agent Cloudflare refuses at its edge (403,
"error code 1010") before the box — measured. MeTube is installed through the product, a test member is added on the
Család card, the member signs in with curl (cookie jar, browser UA) on the box's own page, reaches MeTube, then logs out
(and is refused after); a stranger's cookie-less request is refused through the same path. Then MeTube is removed with
its data and the member removed. Secrets never printed. Evidence: $EV/B4/member-internet.txt."""
import html, json, os, re, secrets, subprocess, sys, tempfile, time
os.environ.setdefault("GUEST", "9201"); os.environ.setdefault("BASE", "https://192.168.0.155")
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fg_b4_lib as L
w = L.w
log = open(f"{w.EV}/B4/member-internet.txt", "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36"
jar = tempfile.mktemp(prefix="b4jar-")
def c(url, *a, data=None):
args = ["curl", "-s", "-L", "--max-time", "30", "-A", UA, "-b", jar, "-c", jar, "-H", "Accept: text/html", "-o", "/tmp/b4body",
"-w", "%{http_code} %{url_effective}"]
if data is not None: args += ["--data", data]
r = subprocess.run(args + list(a) + [url], capture_output=True, text=True)
code, _, eff = r.stdout.strip().partition(" ")
body = open("/tmp/b4body", encoding="utf-8", errors="replace").read() if os.path.exists("/tmp/b4body") else ""
return code, eff.split("?")[0], body
w.login()
say(f"##### B4 member through the real internet — {time.strftime('%FT%TZ', time.gmtime())}")
say("control: Python's user agent at the edge ->", subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-A", "Python-urllib/3.13", "https://video.enkisfelhom.hu/"], capture_output=True, text=True).stdout,
"| a browser's ->", subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-A", UA, "https://video.enkisfelhom.hu/"], capture_output=True, text=True).stdout, "(MeTube not installed yet: 404 from the box)")
L.deploy("metube", "video", say)
fpw = L.add_member("b4teszt", say)
code, url, body = c("https://video.enkisfelhom.hu/")
say(f"a member's browser opens video.enkisfelhom.hu (no cookie) -> {code} at {url} (the family sign-in page: {'name=\"_ft\"' in body})")
ft = re.search(r'name="_ft" value="([^"]+)"', body); rd = re.search(r'name="rd" value="([^"]*)"', body)
form = "&".join(f"{k}={__import__('urllib.parse').parse.quote(v)}" for k, v in
(("_ft", ft.group(1) if ft else ""), ("rd", html.unescape(rd.group(1)) if rd else ""), ("name", "b4teszt"), ("password", fpw)))
code, url, body = c("https://felhom.enkisfelhom.hu/__family/login", data=form)
say(f"the member signs in on the box's own page -> {code} at {url} (MeTube's own page: {'MeTube' in body and 'name=\"_ft\"' not in body})")
code, url, body = c("https://video.enkisfelhom.hu/history")
say(f"the member reads /history -> {code} ({'done' in body})")
code, url, body = c("https://felhom.enkisfelhom.hu/launcher")
say(f"the same browser at the DASHBOARD (/launcher) -> {code} at {url} (sent to the dashboard login: {'/login' in url})")
code, url, body = c("https://felhom.enkisfelhom.hu/__family/logout", data="")
code2, url2, _ = c("https://video.enkisfelhom.hu/history")
say(f"logout -> {code}; then /history -> {code2} at {url2} (refused: {'video.' not in url2 or code2 != '200'})")
say("a STRANGER (no cookie, browser UA) at video.enkisfelhom.hu/history ->",
subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-A", UA, "https://video.enkisfelhom.hu/history"], capture_output=True, text=True).stdout)
L.remove("metube", say)
L.remove_member("b4teszt", say)
os.unlink(jar)