Files
admin ca543b8f69
gates / gates (push) Successful in 15s
where-felhom-stands: bring the picture up to 2026-08-22, and stop the page disagreeing with its source
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.
2026-08-22 10:36:42 +02:00

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> &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())