ca543b8f69
gates / gates (push) Successful in 15s
Eight claims re-checked against the drill and the v0.218.0 fixes; three moved, all downward.
backup.offsite walked -> partial. "18 snapshots, daily, unbroken" was true on
2026-08-09 and false by 2026-08-21: the next snapshot after that date
was put there by hand, twelve days later. The rebuild lost the target
and the per-app switches came back off, so a run reported "backup OK:
0 app(s) backed up".
fail.wiped-reinstalled.data walked -> partial. A real reinstall orphans BOTH off-premises tiers:
restic silently for 12 days (R-193), and the PBS archives from before
the reinstall cannot be opened by the rebuilt box at all (R-366).
backup.fill-warning walked -> partial. The warning fires correctly, but the watcher runs
once a day, so a filesystem that fills at 03:31 goes unannounced for
~24 h. Watched silent while a volume sat at 99%.
Five re-checked and held: backup.tier1 and recover.byte-identical carry the R-355/R-354 story and
their fixes; backup.restore-proof stays grey for a sharper reason (orphaned archives, not an
untested tier); backup.sikeres gains two fresh instances; fail.customer-self-restore records that
R-356 now blocks 40 of 53 apps regardless of who is driving.
render_stands.py: the header's commit shas were hardcoded, so the page cited the August 9th commits
while the YAML said otherwise - the stale-build-product failure the renderer exists to prevent. They
are parsed now. The count beside them said "15 status(es) moved in that pass" when 15 was every
recorded move ever; it now separates the two numbers.
check_stands passes, and was itself proven able to convict first: a claim marked `missing` flipped to
`walked` in a scratch copy fired rule 5 by name (use.dlna), and the real file still passes.
241 lines
12 KiB
Python
241 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""render_stands.py — generate where-felhom-stands.html from where-felhom-stands.yaml.
|
|
|
|
Run by hand, deliberately not wired into CI:
|
|
|
|
python3 scripts/check_stands.py && python3 scripts/render_stands.py
|
|
|
|
WHY THIS EXISTS. The first version of this page was a React bundle: one 154 KB artefact whose
|
|
content sat gzip+base64 inside a JS module map. It rendered, and that was all it could do — it
|
|
could not be edited, a diff of it showed nothing a person could read, and it could not be
|
|
regenerated when a status moved. So it began going stale the moment it was committed, which is the
|
|
one thing a picture of "where we stand" must not do.
|
|
|
|
This emits plain static HTML with inline CSS and NO JavaScript. It is bigger on disk than it needs
|
|
to be and that is the trade: the page is a build product of a file a person can read and diff.
|
|
|
|
The palette and layout deliberately match the 2026-08-09 snapshot — the operator should recognise
|
|
the page, not learn a new one.
|
|
"""
|
|
import html
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SRC = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.yaml")
|
|
OUT = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.html")
|
|
|
|
DOT = {"walked": "#34d399", "built": "#60a5fa", "partial": "#fbbf24", "missing": "#64748b"}
|
|
LEGEND = [
|
|
("walked", "Walked", "done end to end on real hardware, evidence on file"),
|
|
("built", "Built", "shipped and tested, the real path never walked"),
|
|
("partial", "Partial", "some walked, some not — the note says which"),
|
|
("missing", "Missing", "does not exist"),
|
|
]
|
|
STAGES = {1: "Getting a box", 2: "Making it theirs", 3: "Using it", 4: "Drives",
|
|
5: "Backing up", 6: "When something goes wrong", 7: "Getting everything back"}
|
|
|
|
|
|
def load(path):
|
|
claims, cur, sect = [], None, None
|
|
meta = {}
|
|
for raw in open(path, encoding="utf-8"):
|
|
line = raw.rstrip("\n")
|
|
if line.startswith("verified_on:"):
|
|
meta["date"] = line.split(":", 1)[1].strip()
|
|
# The commit shas the pass was verified against. Parsed rather than hardcoded: they were
|
|
# inlined in the header string until 2026-08-22, so the page kept printing the August 9th
|
|
# commits while the YAML said otherwise — a build product silently disagreeing with its own
|
|
# source, which is the exact failure this renderer exists to prevent.
|
|
if line.startswith(" ") and cur is None and ":" in line and not line.strip().startswith("-"):
|
|
k, _, v = line.strip().partition(":")
|
|
if k in ("felhom-agent", "felhom-controller", "hub") and v.strip():
|
|
meta.setdefault("against", {})[k] = v.strip()
|
|
if line.startswith(" - id:"):
|
|
cur = {"id": line.split(":", 1)[1].strip(), "sources": [], "changed": None}
|
|
claims.append(cur)
|
|
sect = None
|
|
continue
|
|
if cur is None:
|
|
continue
|
|
if line.strip() == "sources:":
|
|
sect = "sources"
|
|
continue
|
|
if line.strip() == "changed:":
|
|
sect = "changed"
|
|
cur["changed"] = {}
|
|
continue
|
|
if line.strip() == "verified:":
|
|
sect = "verified"
|
|
continue
|
|
m = re.match(r'\s+- (capability-map|evidence|register): (.*)$', line)
|
|
if sect == "sources" and m:
|
|
cur["sources"].append((m.group(1), m.group(2).strip().strip('"')))
|
|
continue
|
|
m = re.match(r'\s+(\w+): (.*)$', line)
|
|
if not m:
|
|
continue
|
|
k, v = m.group(1), m.group(2).strip().strip('"')
|
|
if sect == "changed":
|
|
cur["changed"][k] = v
|
|
elif sect == "verified":
|
|
cur.setdefault("v_" + k, v)
|
|
else:
|
|
cur.setdefault(k, v)
|
|
return meta, claims
|
|
|
|
|
|
def esc(t):
|
|
return html.escape(t, quote=False)
|
|
|
|
|
|
def dot(status):
|
|
return ('<span style="width:8px;height:8px;border-radius:50%%;background:%s;'
|
|
'flex:0 0 8px;margin-top:5px"></span>' % DOT.get(status, "#64748b"))
|
|
|
|
|
|
def claim_html(c):
|
|
bits = ['<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:9px">', dot(c.get("status"))]
|
|
body = ['<span style="font-size:12px;line-height:1.45;color:#b8c4d6;text-wrap:pretty">',
|
|
esc(c.get("title", ""))]
|
|
if c.get("changed"):
|
|
ch = c["changed"]
|
|
if ch.get("also_moved"):
|
|
# Moved twice. Say so — a second move rendered as a first one hides the fact that a
|
|
# status is unsettled, which is exactly what a reader needs to see.
|
|
label = "moved twice: %s, then 2026-08-10 back to %s" % (
|
|
esc(ch["also_moved"]), esc(c.get("status", "?")))
|
|
else:
|
|
label = "changed 2026-08-09, was %s" % esc(ch.get("from", "?"))
|
|
body.append('<span style="display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;'
|
|
'background:#3a2a12;color:#fbbf24;font-size:10px;white-space:nowrap">%s</span>' % label)
|
|
if c.get("v_verdict") in ("needs-hardware", "contested"):
|
|
col = "#a78bfa" if c["v_verdict"] == "contested" else "#7dd3fc"
|
|
body.append('<span style="display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;'
|
|
'background:#1b2740;color:%s;font-size:10px;white-space:nowrap">%s</span>'
|
|
% (col, esc(c["v_verdict"])))
|
|
if c.get("note"):
|
|
body.append('<span style="display:block;color:#7c8aa3;font-size:11px;margin-top:3px">%s</span>'
|
|
% esc(c["note"]))
|
|
decay = c.get("decay") or (c.get("changed") or {}).get("decay")
|
|
if decay:
|
|
body.append('<span style="display:block;color:#c08a3e;font-size:11px;margin-top:2px">'
|
|
'proof decay: %s</span>' % esc(decay))
|
|
if c.get("changed", {}) and c["changed"].get("reason"):
|
|
body.append('<span style="display:block;color:#9a7b3a;font-size:11px;margin-top:2px">why it moved: %s</span>'
|
|
% esc(c["changed"]["reason"]))
|
|
srcs = " · ".join("%s %s" % (k, esc(v if len(v) < 62 else v[:59] + "…")) for k, v in c["sources"])
|
|
body.append('<span style="display:block;color:#4b5b76;font-size:10px;margin-top:3px;'
|
|
'font-family:ui-monospace,SFMono-Regular,Menlo,monospace">%s</span>' % srcs)
|
|
body.append("</span>")
|
|
bits.append("".join(body))
|
|
bits.append("</div>")
|
|
return "".join(bits)
|
|
|
|
|
|
def panel(title, inner, sub=None):
|
|
h = ['<div style="background:#121b2c;border:1px solid #1f2c44;border-radius:6px;padding:14px 13px">']
|
|
h.append('<div style="font-size:14px;font-weight:500;padding-bottom:9px;margin-bottom:10px;'
|
|
'border-bottom:1px solid #1f2c44">%s</div>' % title)
|
|
if sub:
|
|
h.append('<div style="color:#6b7a91;font-size:11px;margin:-6px 0 10px">%s</div>' % esc(sub))
|
|
h.append(inner)
|
|
h.append("</div>")
|
|
return "".join(h)
|
|
|
|
|
|
def main():
|
|
meta, claims = load(SRC)
|
|
counts = {}
|
|
for c in claims:
|
|
counts[c.get("status")] = counts.get(c.get("status"), 0) + 1
|
|
changed = [c for c in claims if c.get("changed")]
|
|
|
|
o = []
|
|
o.append("<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">")
|
|
o.append('<meta name="viewport" content="width=device-width, initial-scale=1">')
|
|
o.append("<title>Where Felhom stands</title><style>")
|
|
o.append("@page{size:A3 landscape;margin:8mm}"
|
|
"*{margin:0;padding:0;box-sizing:border-box}"
|
|
"body{background:#0b1220;color:#e6edf7;font-family:-apple-system,BlinkMacSystemFont,"
|
|
"'Segoe UI',Roboto,sans-serif;-webkit-font-smoothing:antialiased;padding:26px 20px}"
|
|
".wrap{max-width:1600px;margin:0 auto}"
|
|
".grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:12px;align-items:start}"
|
|
".grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;align-items:start}"
|
|
".grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;align-items:start}"
|
|
"h1{font-size:22px;font-weight:600;letter-spacing:-.2px}"
|
|
"a{color:#60a5fa}"
|
|
"@media print{body{padding:0}.grid7{grid-template-columns:repeat(7,1fr)}}")
|
|
o.append("</style></head><body><div class=\"wrap\">")
|
|
|
|
o.append("<h1>Where Felhom stands</h1>")
|
|
o.append('<div style="color:#8fa0b8;font-size:12.5px;margin:7px 0 4px;max-width:1100px;line-height:1.5">'
|
|
'What we built, what happens when things go wrong, and what is still missing. '
|
|
'<b style="color:#cbd7e8">Generated from '
|
|
'<code>where-felhom-stands.yaml</code></b>, which cites the capability map, register row or '
|
|
'evidence document behind every claim — and which is checked by '
|
|
'<code>scripts/check_stands.py</code>.</div>')
|
|
rechecked = sum(1 for c in claims if c.get("v_date") == meta.get("date"))
|
|
ag = meta.get("against", {})
|
|
against = ", ".join("%s <code>%s</code>" % (esc(k), esc(ag[k]))
|
|
for k in ("felhom-agent", "felhom-controller", "hub") if k in ag)
|
|
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:16px">Verified %s%s. '
|
|
'<b style="color:#fbbf24">%d claim(s) carry a recorded status move</b> (%d re-checked '
|
|
'in this pass) — each is marked on the page.</div>'
|
|
% (esc(meta.get("date", "?")), (" against " + against) if against else "",
|
|
len(changed), rechecked))
|
|
|
|
leg = ['<div style="display:flex;gap:22px;flex-wrap:wrap;background:#121b2c;border:1px solid #1f2c44;'
|
|
'border-radius:6px;padding:11px 14px;margin-bottom:18px">']
|
|
for k, name, desc in LEGEND:
|
|
leg.append('<div style="display:flex;gap:8px;align-items:flex-start">%s'
|
|
'<span style="font-size:11.5px"><b>%s</b> <span style="color:#7c8aa3">(%d)</span>'
|
|
'<span style="display:block;color:#6b7a91;font-size:10.5px">%s</span></span></div>'
|
|
% (dot(k), name, counts.get(k, 0), esc(desc)))
|
|
leg.append('<div style="display:flex;gap:8px;align-items:flex-start"><span style="font-size:11.5px">'
|
|
'<b style="color:#7dd3fc">needs-hardware</b><span style="display:block;color:#6b7a91;'
|
|
'font-size:10.5px">only a running box could settle it</span></span></div>')
|
|
leg.append("</div>")
|
|
o.append("".join(leg))
|
|
|
|
o.append('<div style="font-size:15px;font-weight:600;margin:0 0 9px">The journey</div>')
|
|
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:10px">Seven stages, left to '
|
|
'right, as they happen to a person.</div>')
|
|
o.append('<div class="grid7">')
|
|
for n in range(1, 8):
|
|
inner = "".join(claim_html(c) for c in claims
|
|
if c.get("band") == "journey" and c.get("stage") == str(n))
|
|
o.append(panel('<span style="color:#4b5b76;font-family:ui-monospace,monospace">%d</span> %s'
|
|
% (n, esc(STAGES[n])), inner))
|
|
o.append("</div>")
|
|
|
|
fails = [c for c in claims if c.get("band") == "failures"]
|
|
o.append('<div style="font-size:15px;font-weight:600;margin:22px 0 9px">When things go wrong</div>')
|
|
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:10px">%d situations, and what '
|
|
'actually happens in each.</div>' % len(fails))
|
|
o.append('<div class="grid3">')
|
|
for c in fails:
|
|
o.append(panel(esc(c.get("title", "").split(" — ")[0])[:74], claim_html(c)))
|
|
o.append("</div>")
|
|
|
|
o.append('<div style="margin-top:26px;padding-top:12px;border-top:1px solid #1f2c44;color:#4b5b76;'
|
|
'font-size:10.5px;line-height:1.6">'
|
|
'Generated by <code>scripts/render_stands.py</code> from <code>where-felhom-stands.yaml</code>. '
|
|
'Do not hand-edit this file — edit the data and regenerate. '
|
|
'The 2026-08-09 React bundle is kept as '
|
|
'<code>where-felhom-stands-2026-08-09-snapshot.html</code> and is not maintained.'
|
|
'</div>')
|
|
o.append("</div></body></html>")
|
|
|
|
open(OUT, "w", encoding="utf-8").write("\n".join(o))
|
|
print("wrote %s (%d bytes) — %d claims, %d changed"
|
|
% (os.path.relpath(OUT, ROOT), os.path.getsize(OUT), len(claims), len(changed)))
|
|
print(" statuses: " + ", ".join("%s=%d" % kv for kv in sorted(counts.items())))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|