#!/usr/bin/env python3 """Part B spike — does the controller tolerate a top-level `update_ladder:` key in .felhom.yml? The drill catalog's navidrome carries the block (drill commit given as argv[1]). The app must sync, deploy, probe and badge exactly as without it. Positive observables: the sync reaches the drill commit, the metadata the page needs is read (display_name, deploy_fields), the deploy reaches `running`, the controller's own health-probe line for navidrome appears, the badge renders in both languages.""" import sys, time, json, re sys.path.insert(0, ".") import walk as w want = sys.argv[1] tag = sys.argv[2] if len(sys.argv) > 2 else "v?" w.login() w.say(f"SPIKE on controller {tag}, drill commit {want}") w.sync_rescan() time.sleep(3) head = w.guest("cat /var/lib/docker/volumes/felhom-controller-data/_data/catalog-cache/.git/HEAD 2>/dev/null; git -C /var/lib/docker/volumes/felhom-controller-data/_data/catalog-cache rev-parse --short=12 HEAD 2>/dev/null; ls /var/lib/docker/volumes/felhom-controller-data/_data/ | head -30") w.say("catalog cache: " + head.replace("\n", " | ")) has = w.guest("grep -c '^update_ladder:' /opt/docker/stacks/navidrome/.felhom.yml") w.say(f"update_ladder lines in the box's navidrome/.felhom.yml: {has.strip()}") c, d = w.ctl("GET", "/api/stacks/navidrome/deploy-fields") meta = ((d.get("data") or {}).get("metadata") or {}) w.say(f"deploy-fields -> {c}; display_name={meta.get('display_name')!r} fields={[f.get('env_var') for f in meta.get('deploy_fields') or []]}") ok = w.deploy("navidrome", "navidrome") w.wait_app("navidrome", "/") probe = [] for _ in range(40): c2, d2 = w.ctl("GET", "/api/debug/logs?level=DEBUG&lines=3000") ents = ((d2.get("data") or {}).get("entries") or []) if isinstance(d2, dict) else [] probe = [e.get("timestamp", "") + " " + e.get("message", "") for e in ents if "navidrome" in e.get("message", "") and "probe" in e.get("message", "").lower()] if any("healthy=true" in p or "-> 200" in p or "→ 200" in p or " ok" in p for p in probe): break time.sleep(10) w.say("probe lines for navidrome:\n " + "\n ".join(probe[-6:])) st = w.stack("navidrome") w.say(f"state={st.get('state')} deployed={st.get('deployed')} health={st.get('health')}") import re as _re b = {} for i in range(12): for lang, suf in (("hu", ""), ("en", "?lang=en")): h = w.page(f"/apps/navidrome{suf}") b[lang] = _re.findall(r'([^<]*)<', h)[:4] if b["hu"] and b["en"]: w.say(f"badge present after {i} rescan round(s)") break w.ctl("POST", "/api/stacks/rescan") time.sleep(5) w.say("badges: " + json.dumps(b, ensure_ascii=False)) logs = w.guest("docker logs --since 10m felhom-controller 2>&1 | grep -iE 'navidrome' | grep -iE 'probe|yaml|felhom.yml|error|WARN' | tail -15") w.say("controller log lines for navidrome:\n" + logs) errs = w.guest("docker logs --since 10m felhom-controller 2>&1 | grep -iE 'yaml: |unmarshal|cannot parse' | tail -5") w.say("yaml/unmarshal errors in the controller log (want none): " + (errs.strip() or "(none)")) w.remove("navidrome") out = {"controller": tag, "drill_commit": want, "ladder_on_box": has.strip(), "deploy_fields_http": c, "display_name": meta.get("display_name"), "deployed": ok, "state": st.get("state"), "badges": b, "probe_lines": probe[-6:], "yaml_errors": errs.strip()} json.dump(out, open(f"B1-spike-{tag}.json", "w"), indent=2, ensure_ascii=False) open(f"B1-spike-{tag}.log", "w").write("\n".join(w.LOG) + "\n")