Files
felhom.eu/scripts/render_stands.py
T
admin c04f933d0b
gates / gates (push) Successful in 13s
The census answers no, three receipts found, and the prune was on file all along
CENSUS (read-only, hub store, tester's machine not contacted): no machine that is
not ours can be in the state that cost demo-felhom its history. The hub holds
escrow for three hosts; both demo boxes lost their pre-fix key in the same four
hours on 2026-08-04; peti-felhom and david have no host row and no escrow at all.
A control ran FIRST and had to pass -- the query returned "present (572 bytes)"
for a host known to have material and "absent (NULL)" for one known not to.
Corrected my own instrument on the way: a date-only comparison mislabelled both
losses as after the fix, so the in-force moment is now pinned from the hub's first
post-fix escrow row (11:11:37Z), which independently agrees with the register.

PART 1 ESTABLISHED. The prune is recorded inside R-267 -- the row about the
Configuration page being slow -- because pruning artifacts is what made that page
fast. Arithmetic checks (23+7=30, plus three versions that only surfaced after the
first thirty moved them onto page one = 33) and the PAGINATED listing shows both
generics at exactly ten. R-291's blocking condition is released: the operator was
being asked to establish something already written down. And my counter-argument
yesterday was wrong in exactly the way R-267 warns about -- "containers hold 19"
came from an unpaginated query; paginated they hold 270 and 169.

RECEIPTS: three restored (drives.enrol, backup.tier1, fail.lost-recovery-code),
each citing the document that walked it; the map already read PROVEN-LIVE for all
three, so this follows the map rather than raising a status in the view. NINE
HONEST GREYS. fault.selfheal's best hit argues against it -- an incident recording
self-heal's absence through a 1h15m outage.

THE DECAY RULE FIRED FOR THE FIRST TIME. backup.restore-proof has a receipt from
28 July and is superseded anyway: demo-hp's restore-test failed 5 August and the
box has since been rebuilt. A claim about a continuing behaviour cannot rest on an
old observation. The capability map still reads PROVEN-LIVE and is now the thing
out of step -- recorded, not silently rewritten.

PART 4 specified, not implemented. The orphan card promises restorability the box
rendering it cannot evaluate: the discriminator is on the hub and no wire field
carries it. A conditional promise the system cannot evaluate is the same defect as
an unconditional false one, so the copy stops promising, says what happens, and
names a route. Ships with the next controller change so one bake covers both.
2026-08-10 11:15:37 +02:00

228 lines
11 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()
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>')
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:16px">Verified %s against '
'felhom-agent <code>28ba8593b8</code>, felhom-controller <code>c732fe1283</code>, hub '
'<code>56f8aa611c</code>. <b style="color:#fbbf24">%d status(es) moved in that pass</b> — '
'each is marked on the page.</div>' % (meta.get("date", "?"), len(changed)))
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> &nbsp;%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())