Files
felhom.eu/documentation/audits/the-28-2026-09-22/sidejob_630.py
T
admin 186546d562
gates / gates (push) Successful in 27s
THE TWENTY-EIGHT: every app no drill had touched, walked in one night
All 28 walked on scratch guest 9202 against the private drill catalog. 26 deployed, 6 proven,
5 inconclusive, 14 with no upstream edge, 1 failed honestly (outline 1.9.1->1.10.1, HELD with the
right sentence), 2 undeployable - one (plant-it) by design, refused by the lifecycle gate, proven
live for the first time. Each app also got the half the update night skipped: a restore from its own
copy with the seed read back again - 21 restored, 2 correctly REFUSED per 07 6.2.

R-630 RAISED TO P1 by measurement: a stack with NO probe container does not skip verifying - it
waits out the full health timeout and HOLDS, stopping an app whose three containers read healthy.
The controller's own words: "not healthy within 5m0s (last: no probe container)".

R-633 opened: a remove sent during a restore reports success and leaves a container restarting with
a live public route. The product already refuses that clash for update and for restore, naming the
blocker; remove has no such guard.

R-634 opened: an app can be running, healthy and serving while recorded as deployed=false, and is
then unremovable. Reproducible alone on sparkyfitness; concurrency-linked on two others.

R-631 and R-632 CLOSED. Register 321 -> 323. Seven interventions, six of them my own harness -
named, with what each cost. No product code. The live catalog's image: lines are byte-identical to
the start of the night.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 16:00:56 +02:00

63 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""R-630 — what does the guarded Update do when NO probe answers, because none was ever built?
`findProbeContainer` (healthprobe.go:297) takes the container whose name EQUALS the stack name,
else the first with it as a PREFIX. paperless-ngx's containers are `paperless-webserver`,
`paperless-postgres`, `paperless-redis` — none matches `paperless-ngx`. The function returns "",
`:62` counts the stack in `skippedNoContainer` and continues, so no probe is ever built.
The open question this measures, and it is the one that decides which fix is right:
**the `verifying` phase waits on that probe. With no probe at all, does it pass at once, wait out
`update.health_timeout`, or HOLD?**
No edge exists upstream for paperless-ngx tonight, so this presses the Update on the SAME version —
which is exactly what a household does when they press it on an up-to-date app, and it still walks
the whole phase machine. That difference is stated in the record rather than glossed.
"""
import json, os, sys, time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "update-night-2026-09-21"))
import walk as w # noqa: E402
APP, SUB = "paperless-ngx", "paperless"
rec = {"app": APP, "question": "what does `verifying` do with no probe target at all?"}
w.login()
try:
rec["deployed"] = w.deploy(APP, SUB)
w.wait_app(SUB, "/", tries=60)
st = w.stack(APP)
rec["controller_state"] = st.get("state")
rec["front_door"] = w.app_curl(SUB, "/")[1]
rec["containers"] = w.guest(
"docker ps --format '{{.Names}}|{{.Status}}' | grep -i paperless").strip().split("\n")
# what the HEALTH page says for a stack the prober skips
code, d = w.ctl("GET", f"/api/stacks/{APP}")
dd = (d.get("data") or {})
rec["health_detail"] = dd.get("health") or dd.get("health_checks") or dd.get("health_detail")
rec["state"] = dd.get("state")
# the app page, as the household reads it
import re
for lang, suf in (("hu", ""), ("en", "?lang=en")):
h = w.page(f"/apps/{APP}{suf}")
h = re.sub(r"<script.*?</script>|<style.*?</style>", "", h, flags=re.S)
txt = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", h))
i = txt.find("←")
rec[f"page_{lang}"] = txt[i:i + 200] if i >= 0 else None
# THE MEASUREMENT: press Update and time every phase
t0 = time.time()
rec["phases"] = w.press_update(APP)
rec["update_wall_s"] = round(time.time() - t0, 1)
rec["state_after"] = w.stack(APP).get("state")
rec["front_door_after"] = w.app_curl(SUB, "/")[1]
print(json.dumps(rec, ensure_ascii=False, indent=2)[:2500])
finally:
try:
w.remove(APP)
except Exception as e:
rec["remove_error"] = str(e)
json.dump(rec, open(os.path.join(HERE, "sidejobs", "r630.json"), "w"),
ensure_ascii=False, indent=2)
open(os.path.join(HERE, "sidejobs", "r630-log.txt"), "w").write("\n".join(w.LOG) + "\n")
print("\nwritten sidejobs/r630.json")