# Rollout harness (2026-09-29 afternoon, 9202, controller 0.281.0, drill catalog). Evidence, not product. # python3 ro.py deploy app... fresh installs through the product; the gate file + record right after # python3 ro.py stranger app... a stranger: API-style request and a browser request # python3 ro.py household app... the household (dashboard session) opens the app through the gate # python3 ro.py probe app URL read a candidate status URL from inside the box (the controller's own network) # python3 ro.py state app... gate record, gate file, sign-up block file # python3 ro.py press app the household's "Done" press (the product's endpoint) # python3 ro.py after app... a stranger after the gate opened # python3 ro.py remove app... # Household test passwords are generated per run and kept in ro_creds.json (scratch, 0600, deleted at teardown). import json, os, secrets, sys, time sys.path.insert(0, '/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-calcom-claper-2026-09-28/tools') sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import walk as w from browser import Browser, hopstr D = "enkisfelhom.hu" PW = open(os.path.join(w.SC, ".ctlpw")).read().strip() JAR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ro_jar.json") CREDS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ro_creds.json") def sub(app): return "r-" + app def host(app): return f"{sub(app)}.{D}" def household(): b = Browser("household") if os.path.exists(JAR): b.jar = json.load(open(JAR)) if "felhom." + D not in b.jar: b.login_dashboard(D, PW) return b def save(b): old = os.umask(0o077) json.dump(b.jar, open(JAR, "w")) os.umask(old) def creds(app): c = json.load(open(CREDS)) if os.path.exists(CREDS) else {} if app not in c: c[app] = {"user": "family", "email": "family@spike.hu", "pw": "Hh" + secrets.token_hex(10) + "7"} old = os.umask(0o077) json.dump(c, open(CREDS, "w")) os.umask(old) return c[app] def gstate(app): f = w.guest(f"for f in setup-gate-{app}.yml signup-block-{app}.yml; do test -f /opt/docker/stacks/traefik/dynamic/$f && echo -n \"$f \"; done; echo").strip() rec = (w.stack(app).get("app_config") or {}).get("setup_gate") return f"files=[{f}] record={rec}" def main(): cmd, args = sys.argv[1], sys.argv[2:] w.login() if cmd == "deploy": for app in args: v = w.deploy_values(app, sub(app)) code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": v, "kept_data": "fresh"}) w.say(app, "deploy ->", code, "" if code == "202" else str(d)[:200]) for app in args: st = {} for _ in range(120): st = w.stack(app) if st.get("deployed") and (st.get("app_config") or {}).get("pinned_images") and st.get("state") in ("running", "unhealthy", "degraded"): break time.sleep(5) w.say(app, "state", st.get("state"), "|", gstate(app)) elif cmd == "stranger": for app in args: s = Browser("stranger") st, body, hops = s.req(f"https://{host(app)}/") gp = "Jelentkezz be a Felhom" in body or "waiting for its first setup" in body st2, body2, _ = s.req(f"https://{host(app)}/api/x", "POST", body={"a": 1}, accept="application/json") w.say(app, f"STRANGER browser -> {st} {hopstr(hops)} gate-page={gp} | API POST -> {st2} {body2[:52]!r}") elif cmd == "household": b = household() for app in args: t0 = time.time() st, body, hops = b.req(f"https://{host(app)}/") gp = "Jelentkezz be a Felhom" in body or "waiting for its first setup" in body title = body[body.find("