"""Live proofs for controller v0.272.0 on scratch 9202 (drill catalog). Evidence, not product; every act is the endpoint the UI invokes. R-670 + R-671: navidrome installed at 0.64.0 with a WRONG probe port (9999) so a step to 0.64.1 fails AND its undo fails (the old probe is wrong too) -> HOLD with the undo copies kept; then the backup page's restore lifts the hold -> the copies must be gone, and no undo may log the false 'unreadable' ERROR. R-677: the probe put right, navidrome climbed to the head, then the drill RE-TESTS the head at a new digest -> the badge must say '— ma' / '— today', not the template's catalog_since age.""" import json, re, subprocess, time, os import walk as w, fixtures EVD = w.EV.replace("night-2026-09-25", "retire-peti-2026-09-25") + "/B/live" os.makedirs(EVD, exist_ok=True) T = f"{w.DRILL}/templates/navidrome" LOG = [] def say(s): w.say(s); LOG.append(time.strftime("%H:%M:%S ") + s); open(EVD + "/run.log", "w").write("\n".join(LOG) + "\n") def drill(edit, msg): w.sh(["git", "-C", w.DRILL, "pull", "-q", "--rebase", "origin", "main"], timeout=120) edit() w.sh(["git", "-C", w.DRILL, "commit", "-qam", "LIVE-272 " + msg]); w.sh(["git", "-C", w.DRILL, "push", "-q", "origin", "main"], timeout=120) head = w.sh(["git", "-C", w.DRILL, "rev-parse", "--short", "HEAD"]).stdout.strip() for k in range(40): w.sync_rescan() if w.guest("cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache && git rev-parse --short HEAD").strip() == head: say(f"drill: {msg} (box at {head})"); return time.sleep(8) raise SystemExit("box never caught up") def sub(path, a, b): s = open(path).read(); assert a in s, (path, a); open(path, "w").write(s.replace(a, b, 1)) def copies(): return w.guest("docker volume ls -q --filter label=felhom.undo-copy-of=navidrome").split() w.login() since = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) # --- R-670 / R-671 ------------------------------------------------------------------------------- drill(lambda: (sub(f"{T}/docker-compose.yml", "image: deluan/navidrome:0.64.1", "image: deluan/navidrome:0.64.0"), sub(f"{T}/.felhom.yml", " port: 4533", " port: 9999")), "navidrome at 0.64.0, probe port 9999 (wrong on purpose)") ok = w.deploy("navidrome", "c-navi"); say(f"deploy navidrome -> {ok}; state={w.stack('navidrome').get('state')}") tok = fixtures.FIXTURES["navidrome"].seed(w, "c-navi", say); say(f"seed ok={tok is not None}") say(f"C1 read before: {fixtures.FIXTURES['navidrome'].verify(w, 'c-navi', tok, say)}") drill(lambda: sub(f"{T}/docker-compose.yml", "image: deluan/navidrome:0.64.0", "image: deluan/navidrome:0.64.1"), "navidrome head 0.64.1 (probe still wrong)") w.sync_rescan(expect_app="navidrome", expect_ref="deluan/navidrome:0.64.1") res = w.press_update("navidrome"); st = w.stack("navidrome") say(f"update -> final {res.get('final_phase')} hold={st.get('hold_reason')!r}") c1 = copies(); say(f"undo copies kept by the hold: {c1}") lg = w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'UNDO|the UNDO failed|HOLDING|backup block rejected|unreadable|removed .* undo cop'") open(EVD + "/r670-r671-log-before-restore.txt", "w").write(lg) say(f"R-670: 'docker-compose.yml unreadable' lines during the update+undo: {lg.count('unreadable')}; UNDO lines: {lg.count('UNDO')}") r = w.restore("navidrome"); say(f"restore -> {json.dumps({k: r.get(k) for k in ('ok','http','seconds','state_after','hold_after')}, ensure_ascii=False)}") c2 = copies(); say(f"undo copies AFTER the restore: {c2}") lg2 = w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'update hold .* CLEARED|removed .* undo cop|unreadable'") open(EVD + "/r671-log-after-restore.txt", "w").write(lg2); say("after-restore lines:\n" + lg2) say(f"read after restore: {fixtures.FIXTURES['navidrome'].verify(w, 'c-navi', tok, say)}") # --- R-677 ------------------------------------------------------------------------------------------- drill(lambda: sub(f"{T}/.felhom.yml", " port: 9999", " port: 4533"), "navidrome probe put right") w.sync_rescan(expect_app="navidrome", expect_ref="deluan/navidrome:0.64.1") res2 = w.press_update("navidrome"); say(f"update to the head -> {res2.get('final_phase')}") time.sleep(5) def retest(): p = f"{T}/.felhom.yml"; lines = open(p).read().split("\n") idx = max(i for i, l in enumerate(lines) if l.startswith(" - {") and '"deluan/navidrome:0.64.1"}, "digest"' in l) e = json.loads(lines[idx][4:]); e["digest"] = {"navidrome": "sha256:" + "b" * 64} e["tested_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()); lines[idx] = " - " + json.dumps(e, ensure_ascii=False) open(p, "w").write("\n".join(lines)) drill(retest, "navidrome head re-tested at a new digest (tested now)") for i in range(6): w.sync_rescan(); b = w.badges("navidrome") if "Frissítés elérhető" in json.dumps(b, ensure_ascii=False): break time.sleep(10) say(f"R-677 badges: {json.dumps(b, ensure_ascii=False)}") json.dump({"update": res, "restore": r, "copies_kept": c1, "copies_after": c2, "update2": res2, "badges": b}, open(EVD + "/live272.json", "w"), indent=2, ensure_ascii=False, default=str)