da20722e76
gates / gates (push) Successful in 27s
INTERIM CHECKPOINT — evidence off the machine at the end of the phase that produced it (R-320), not at the end of the session. Phases 2-5 follow in a later commit. Phase 0, all three mechanisms proven with their controls: - the fleet floor to 0.261.0 with its declared MinAgent — both demo boxes in 13 s, the hub logging `managed floor SERVED ... from declared (golden 0.258.0)`. - a PRIVATE DRILL CATALOG (admin/app-catalog-drill), so that broken, dummy, cross-repo and engine-major edges can be measured without the live catalog ever carrying one. Positive control quoted, and two negative controls: the live catalog's main and both real boxes' caches unchanged. - a throwaway image store on the scratch guest, which is what makes an UNATTENDED HOLD measurable at all: an edge that PASSES the within-a-major test and still fails. CompareImageRefs was proven to order host:port/ references by RUNNING it (4 positive cases + 1 negative control), not by reading it. Phase 1: real within-a-major upstream edges walked on guest 9202 through the product's own guarded Update, each app seeded and read back through its OWN front door (R-156), with a per-edge verdict record in 09's shape. `inconclusive` is never collapsed into `failed`. TWO INSTRUMENT FIXES, both in this repo's own evidence code: - 00-api-recipe.md said the app page is /app/<n>; it is /apps/<n>, and every call it described 404s. Corrected, with the session-expiry note that cost the same time. - unattended-caller.py's follow() read update_phase/updating off the API ENVELOPE, so both were always None and EVERY followed update ran to its 900 s timeout and was then recorded `timeout` and never-press-again. Fixed before B1 relied on it. R-623. No controller, agent or hub code was written. The live catalog carries no broken reference. Gates: repo_gates.py --fast — all 15 OK, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
134 lines
6.3 KiB
Python
134 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase 4 — the morning after.
|
|
|
|
1. B1's held app AS A HOUSEHOLD WOULD FIND IT AT BREAKFAST: the app page, the dashboard, the
|
|
backups page, the event list, the mail — in BOTH languages, quoted. Is there ONE sentence that
|
|
says what happened, since when, which copy holds what, and what to press? Scored against
|
|
`09` §3b Q4's recommended option.
|
|
2. Every app still on 9202 healthy through its own front door; every badge true AFTER a rescan
|
|
(R-607 — a badge read before a rescan is not a measurement).
|
|
"""
|
|
import json, os, re, sys, time
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
import walk as w # noqa: E402
|
|
|
|
OUT = os.path.join(HERE, "bad-days", "P4-morning-after")
|
|
HELD = "glance"
|
|
SUBS = {"glance": "dashboard", "uptime-kuma": "status", "wishlist": "wishes",
|
|
"vikunja": "tasks", "bentopdf": "pdf", "privatebin": "paste", "opengist": "gist",
|
|
"docmost": "docs", "tandoor": "recipes", "zipline": "img", "gitea": "git",
|
|
"vaultwarden": "vault", "romm": "arcade", "mealie": "mealie"}
|
|
|
|
|
|
def text_of(html):
|
|
t = re.sub(r"<script.*?</script>", " ", html, flags=re.S)
|
|
t = re.sub(r"<style.*?</style>", " ", t, flags=re.S)
|
|
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", t)).strip()
|
|
|
|
|
|
def quote(page_path, keys, label):
|
|
got = {}
|
|
for lang, sfx in (("hu", ""), ("en", "?lang=en")):
|
|
sep = "&" if "?" in page_path else "?"
|
|
p = page_path + (sfx.replace("?", sep) if sfx else "")
|
|
html = w.page(p)
|
|
t = text_of(html)
|
|
hits = [s.strip() for s in re.split(r"(?<=[.!?]) ", t)
|
|
if any(k in s.lower() for k in keys)]
|
|
got[lang] = hits[:10]
|
|
open(f"{OUT}/{label}-{lang}.html", "w").write(html)
|
|
return got
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT, exist_ok=True)
|
|
w.login()
|
|
w.say("==== Phase 4: the morning after")
|
|
rec = {}
|
|
|
|
st = w.stack(HELD)
|
|
rec["held_app_state"] = {
|
|
"name": HELD, "state": st.get("state"), "updating": st.get("updating"),
|
|
"update_phase": st.get("update_phase"), "update_phase_label": st.get("update_phase_label"),
|
|
"update_error": st.get("update_error"), "hold_reason": st.get("hold_reason"),
|
|
"observables": w.observables(HELD)}
|
|
w.say(f" [1] {HELD}: state={st.get('state')} phase={st.get('update_phase')} "
|
|
f"hold={st.get('hold_reason')!r}")
|
|
w.say(f" error = {st.get('update_error')!r}")
|
|
|
|
KEYS = ("friss", "update", "vissza", "restore", "ment", "backup", "hib", "error",
|
|
"megsz", "nem ", "masolat", "másolat", "copy", "meghajt", "drive")
|
|
w.say(" [1] the four surfaces a household would look at, both languages")
|
|
rec["app_page"] = quote(f"/apps/{HELD}", KEYS, "app-page")
|
|
rec["dashboard"] = quote("/dashboard", KEYS, "dashboard")
|
|
rec["launcher"] = quote("/", KEYS, "launcher")
|
|
rec["backups_page"] = quote("/backups", KEYS, "backups")
|
|
rec["backups_apps_page"] = quote("/backups/apps", KEYS, "backups-apps")
|
|
for k in ("app_page", "dashboard", "launcher", "backups_page", "backups_apps_page"):
|
|
w.say(f" {k:18} HU {rec[k]['hu'][:3]}")
|
|
w.say(f" {k:18} EN {rec[k]['en'][:3]}")
|
|
|
|
# THE FOUR THINGS Q4's recommended option promises the household are told
|
|
page_hu = " ".join(rec["app_page"]["hu"]).lower()
|
|
page_en = " ".join(rec["app_page"]["en"]).lower()
|
|
rec["q4_score"] = {
|
|
"says WHAT happened": any(k in page_hu for k in ("friss", "hib", "megsz")),
|
|
"says SINCE WHEN": bool(re.search(r"\d", " ".join(rec["app_page"]["hu"]))),
|
|
"says WHICH COPY HOLDS WHAT": any(k in page_hu for k in
|
|
("meghajt", "masolat", "másolat", "tavoli", "távoli",
|
|
"sajat", "saját", "helyi")),
|
|
"says WHAT TO PRESS": any(k in page_hu for k in ("vissza", "ment", "nyomd", "gomb")),
|
|
"the same in ENGLISH": bool(page_en) and any(k in page_en for k in
|
|
("update", "restore", "backup", "copy")),
|
|
"IS THE SENTENCE ENGLISH ON THE ENGLISH PAGE (R-606)": None,
|
|
}
|
|
# R-606: the update sentence is a finished Hungarian string, not a key. Look for Hungarian
|
|
# accented words or ASCII Hungarian fragments surviving on the EN page — positive and negative.
|
|
hu_frags = ["friss", "megszak", "alkalmaz", "verzio", "verzió", "vissza"]
|
|
en_only = ["update", "restore", "backup", "version"]
|
|
hu_on_en = [f for f in hu_frags if f in page_en]
|
|
en_on_en = [f for f in en_only if f in page_en]
|
|
rec["q4_score"]["IS THE SENTENCE ENGLISH ON THE ENGLISH PAGE (R-606)"] = {
|
|
"hungarian_fragments_found_on_the_english_page": hu_on_en,
|
|
"english_fragments_found (control)": en_on_en,
|
|
}
|
|
for k, v in rec["q4_score"].items():
|
|
w.say(f" Q4 · {k}: {v}")
|
|
|
|
# 2. every app still on the box, through its own front door, and every badge after a rescan
|
|
w.say(" [2] every app still on the box — front door and badge, after a rescan")
|
|
w.ctl("POST", "/api/sync")
|
|
time.sleep(2)
|
|
w.ctl("POST", "/api/stacks/rescan")
|
|
time.sleep(3)
|
|
_, d = w.ctl("GET", "/api/stacks")
|
|
apps = []
|
|
for s in (d.get("data") or []):
|
|
if not s.get("deployed"):
|
|
continue
|
|
n = s["name"]
|
|
sub = SUBS.get(n, n)
|
|
rc, code, _ = w.app_curl(sub, "/", timeout=20)
|
|
inst = {k: (v.get("ref") if isinstance(v, dict) else v)
|
|
for k, v in ((s.get("app_config") or {}).get("installed_images") or {}).items()}
|
|
cat = s.get("catalog_images") or {}
|
|
bdg = w.badges(n)
|
|
truthful = (inst == cat) == any("naprak" in (x["text"] or "").lower()
|
|
for x in bdg["hu"]) if bdg["hu"] else None
|
|
apps.append({"name": n, "state": s.get("state"), "front_door_http": code,
|
|
"installed": inst, "catalog": cat, "badge_hu": bdg["hu"],
|
|
"badge_en": bdg["en"], "badge_matches_the_refs": truthful})
|
|
w.say(f" {n:16} state={str(s.get('state')):10} door={code:4} "
|
|
f"badge={[x['text'] for x in bdg['hu']]} refs_equal={inst==cat}")
|
|
rec["apps_on_the_box"] = apps
|
|
|
|
json.dump(rec, open(f"{OUT}/result.json", "w"), indent=2, ensure_ascii=False)
|
|
open(f"{OUT}/log.txt", "w").write("\n".join(w.LOG) + "\n")
|
|
w.say(f" written -> {OUT}/result.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|