#!/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()