#!/usr/bin/env python3 """walk_lifecycle.py — capability map B2 („App lifecycle") re-walked on scratch 9202 (night 2026-10-28) through the dashboard's own API: install a throwaway Radicale, stop, start, restart, logs; the protected stacks refuse a stop; remove with its data — and its labelled volume is gone (the re-check the map row asks for after R-1117).""" import json, os, sys, time sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") os.environ.setdefault("SC", "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/rt-9202") os.environ.setdefault("TARGET", "9202") import box_walk as w EV = "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/night/p5" APP = "radicale" w.login() res = {} def state(): return w.stack(APP).get("state") def running_containers(): return w.guest(f"docker ps --filter label=com.docker.compose.project={APP} --format '{{{{.Names}}}} {{{{.Status}}}}'").strip() res["deploy"] = w.deploy(APP, "radicale") res["volumes_installed"] = w.guest(f"docker volume ls --filter label=com.docker.compose.project={APP} --format '{{{{.Name}}}}'").split() for act in ("stop", "start", "restart"): code, d = w.ctl("POST", f"/api/stacks/{APP}/{act}") for _ in range(24): time.sleep(5) s = state() if (act == "stop" and s != "running") or (act != "stop" and s == "running"): break res[act] = {"http": code, "state_after": state(), "containers": running_containers()} w.say(f"B2 {act}: http={code} state={res[act]['state_after']} containers={res[act]['containers']!r}") code, d = w.ctl("GET", f"/api/stacks/{APP}/logs?lines=20") logs = (d.get("data") or {}) if isinstance(d, dict) else {} txt = json.dumps(logs) res["logs"] = {"http": code, "bytes": len(txt), "names_radicale": "radicale" in txt.lower()} w.say(f"B2 logs: http={code}, {len(txt)} bytes, mention radicale: {res['logs']['names_radicale']}") for prot in ("traefik", "felhom-controller", "cloudflared"): code, d = w.ctl("POST", f"/api/stacks/{prot}/stop") alive = w.guest(f"docker ps --filter name=^{prot}$ --format '{{{{.Status}}}}'").strip() res[f"protected_{prot}"] = {"http": code, "msg": str(d)[:160], "still_running": alive} w.say(f"B2 protected stop {prot}: http={code} ({str(d)[:100]}) still running: {alive!r}") code = w.remove(APP) time.sleep(5) res["remove_http"] = code res["volumes_after"] = w.guest(f"docker volume ls --filter label=com.docker.compose.project={APP} --format '{{{{.Name}}}}'").split() res["containers_after"] = w.guest(f"docker ps -a --filter label=com.docker.compose.project={APP} --format '{{{{.Names}}}}'").split() w.say(f"B2 remove with data: http={code}, labelled volumes left {res['volumes_after']}, containers left {res['containers_after']}") json.dump(res, open(os.path.join(EV, "lifecycle-walk.json"), "w"), indent=1) open(os.path.join(EV, "lifecycle-walk.log"), "w").write("\n".join(w.LOG) + "\n")