gates / gates (push) Successful in 6m59s
- 09 §3 decision 254 (operator decisions 240/243 written down); target-selection: a "lived-in" tier, Tier 0 = demo-hp alone; nodes.md role + drive; 11-os-updates ring 0 wording; CLAUDE.md + docs.md: the diary. - documentation/lived-in/n100.md (new diary); audits/n100-lived-in-2026-10-15/ (state, drive read, Part C capture sets, installs, wiring, stranger probes, demo-hp clean-up, the operator's SHEET.md). - Register: R-1062 (the split, operator decides), R-1063 (decommission half-state on an agent refusal), R-1064 (path removal silent about cross-drive copies), R-1065 (a claimed Plex gets no libraries until a wiring run). - demo-hp: /mnt/sizing-bench removed through the dashboard; the hub shows demo-hp OK (14:56 UTC). Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
27 lines
1.7 KiB
Python
27 lines
1.7 KiB
Python
"""install_media.py — Part D1: qBittorrent, Prowlarr, Sonarr, Radarr through the dashboard's deploy call, with the fields
|
|
the wizard sends (no HDD_PATH for a media app — the box picks its drive). Admins and indexers are the operator's."""
|
|
import json, os, time, n100
|
|
w = n100.w
|
|
w.login()
|
|
for app in ("qbittorrent", "prowlarr", "sonarr", "radarr"):
|
|
if w.stack(app).get("deployed"):
|
|
print(f"{app}: already installed — skipped"); continue
|
|
c, d = w.ctl("GET", f"/api/stacks/{app}/deploy-fields")
|
|
fields = (((d.get("data") or {}).get("metadata") or {}).get("deploy_fields")) or []
|
|
sub = next((f.get("default") for f in fields if f.get("env_var") == "SUBDOMAIN"), None) or app
|
|
values = {"DOMAIN": w.DOMAIN, "SUBDOMAIN": sub}
|
|
if any(f.get("env_var") == "QBT_ADMIN_PASSWORD" for f in fields):
|
|
values["QBT_ADMIN_PASSWORD"] = open(os.path.expanduser("~/.felhom-demofelhom/qbittorrent-admin")).read().strip()
|
|
t0 = time.time()
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": values})
|
|
print(f"{time.strftime('%H:%M:%S')} {app}: deploy (fields sent: {sorted(values)}) -> {code} {json.dumps(d, ensure_ascii=False)[:120]}")
|
|
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") in ("running", "unhealthy"):
|
|
break
|
|
st = w.stack(app)
|
|
hdd = w.guest(f"grep -E '^\\s*HDD_PATH:' /opt/docker/stacks/{app}/app.yaml | head -1").strip()
|
|
print(f" {app}: state={st.get('state')} after {time.time()-t0:.0f}s; pinned={(st.get('app_config') or {}).get('pinned_images')}; {hdd}")
|
|
print(w.guest("docker ps --format '{{.Names}} {{.Status}}' | sort"))
|