Audit: what a Felhom box needs — floor, 64 apps' memory and CPU, power (N100 bench); R-993..R-995; R-231 due check read green

Measured on demo-felhom (N100, 16 GB): 61 apps deployed and removed through the
product, sampled every 5 s; heavy apps with one real action (Immich 200 photos,
Jellyfin transcode, Paperless OCR, Nextcloud 500 MB, ...). sizing.json for the
estimator (tools/build_sizing.py), SIZING.md with the proposed minimum machine,
METHOD.md. Power is RAPL CPU-package only (no meter). R-993 asks the operator to
approve the minimum machine; R-994 lists catalog limits/requests that are too low;
R-995 records leftover unnamed volumes after removal. R-231's dated check measured:
dooplex-backup.service success, journal green.
This commit is contained in:
2026-10-13 13:23:51 +02:00
parent c2a6cf7a58
commit c885b5acf4
175 changed files with 58633 additions and 5 deletions
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""render_table.py — put the per-app table from evidence/apps-table.json into SIZING.md (__APPS_TABLE__ or between markers)."""
import json, os, re
A = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
t = json.load(open(os.path.join(A, "evidence", "apps-table.json")))
rows = ["<!-- apps-table: generated by tools/render_table.py -->",
"| App | Typical MB | Peak MB | Catalog request / limit | CPU class | CPU rest % | Busiest minute % | Source |",
"|---|---|---|---|---|---|---|---|"]
for a in sorted(t, key=lambda a: -(a["mem_peak_mb"] or 0)):
f = lambda v: "–" if v is None else (f"{v:,}" if isinstance(v, int) else str(v))
src = a["source"] + (" !" if a["sources_disagree"] else "")
if a.get("lifecycle"):
src += f" — {a['lifecycle']}"
rows.append(f"| {a['display_name']} (`{a['slug']}`) | {f(a['mem_typical_mb'])} | {f(a['mem_peak_mb'])} | "
f"{f(a['catalog_request_mb'])} / {f(a['catalog_limit_mb'])} | {a['cpu_class'] or '—'} | "
f"{f(a['cpu_rest_pct'])} | {f(a['cpu_peak_1m_pct'])} | {src} |")
rows.append("<!-- /apps-table -->")
p = os.path.join(A, "SIZING.md"); s = open(p, encoding="utf-8").read()
block = "\n".join(rows)
if "__APPS_TABLE__" in s:
s = s.replace("__APPS_TABLE__", block)
else:
s = re.sub(r"<!-- apps-table:.*?<!-- /apps-table -->", block, s, flags=re.S)
open(p, "w", encoding="utf-8").write(s)
print(len(t), "rows")