#!/usr/bin/env python3 """table.py — the README's 65-row table: the measured facts from each /record.json, and the VERDICT set by hand after reading that app's evidence (VERDICTS below — the reviewer's call, not the summarize.py hint).""" import json, os, sys EV = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) W, P, B, N, T = "works", "works with a problem", "broken", "not installed", "not testable on this box" # app: (verdict, one line, row/fix) VERDICTS = { "actualbudget": (W, "the seeded item back after the restore", ""), "adventurelog": (P, "works; but the keep-backups → reinstall → restore path breaks it (DB password from the new install over the old data) — reproduced in its re-run", "R-1269, R-1270"), "audiobookshelf": (W, "the seeded item back after the restore", "drive names: R-1275"), "bentopdf": (W, "stateless (nothing to keep); installs, answers, removes clean", ""), "bookstack": (W, "the seeded item back after the restore", ""), "calcom": (W, "the seeded item back after the restore; 2 start-up messages of the image itself", ""), "calibre-web": (W, "the book (its own upload) is listed again after the keep-data reinstall; the fixture's download check gets 401 (its sign-in for /download, not the app); the admin name is the box's generated one (re-run 2)", "R-1277"), "claper": (W, "the seeded item back after the restore", ""), "code-server": (W, "first install refused by the registry's rate limit, told as „exit code 1”; the re-run installed and worked (no seed: the editor has no API)", "R-1273"), "crafty-controller": (W, "the seeded item back after the restore", ""), "dawarich": (W, "the seeded item back after the restore (every-5th: restore also before the reinstall)", ""), "docmost": (W, "the seeded item back after the restore", ""), "emby": (W, "installs, answers, removes clean; no seed (no fixture for its wizard)", ""), "ghost": (W, "the seeded item back after the restore", ""), "gitea": (W, "the seeded item back after the restore", ""), "glance": (W, "family gate holds a stranger; config-only (nothing to keep); the app's own headings are English", ""), "gokapi": (W, "installs, own login, removes clean; no seed (API key only in its UI)", ""), "grafana": (W, "the seeded item back after the restore", ""), "gramps-web": (W, "installs, answers, removes clean; no seed fixture", ""), "grimmory": (P, "first admin, library and an EPUB upload work; setup gate then family gate work as designed; the read-back through the family gate stayed inconclusive (the harness's gate logic, R-1277); remove-with-data leaves the empty parent folder", "R-1276, R-1277"), "grocy": (W, "the seeded item back after the restore", ""), "home-assistant": (W, "the seeded item back after the restore", ""), "homebox": (W, "the seeded item back after the restore", ""), "homepage": (P, "no gate at all: a stranger sees the box's CPU/RAM/disk (and later the family's links); still the upstream sample page", "R-1272"), "immich": (P, "album (new fixture) in; „use my kept data” loaded the rehearsal's future-dated database and Immich never answered again (404, 20 min)", "R-1271"), "jellyfin": (W, "first user (new fixture, its wizard API) back after the restore", ""), "karakeep": (W, "the seeded item back after the restore", ""), "kimai": (W, "own login; a Hungarian browser lands on /hu/ (curl without a language gets /en/)", ""), "komga": (W, "the seeded item back after the restore", "drive names: R-1275"), "lubelogger": (W, "the seeded item back after the restore", ""), "mealie": (W, "the seeded item back after the restore (the fixture raced after_install on the first run)", ""), "metube": (W, "family gate holds a stranger; download queue entry back", ""), "n8n": (P, "the owner back after the restore; on the live template it tries to send usage data out ([Rudder] lines, 6 in the live re-run) — the drill fix: the variable in the running container, 0 error lines", "drill a601950 (morning list)"), "navidrome": (W, "the seeded item back after the restore", "drive names: R-1275"), "nextcloud": (P, "user in; „use my kept data” loaded the rehearsal's future-dated database beside today's files — the user was not there", "R-1271"), "nzbget": (W, "the seeded item back after the restore; „no news server” until the family adds one (the guide says so)", ""), "onlyoffice": (W, "document server (no household data to keep); installs, answers, removes clean (every-5th restore)", ""), "opengist": (P, "the gist owner back after the restore; but 128M is too small: one sign-up + sign-in hit the limit 443 times and swapped 98 MiB (memprobe-128M.txt); at 256M (drill) anon 216 MiB", "drill 405077f (morning list)"), "outline": (W, "the seeded item back after the restore", ""), "paperless-ngx": (P, "account in; „use my kept data” loaded the rehearsal's future-dated database — the account was not there", "R-1271, R-1276"), "papra": (W, "the seeded item back after the restore (Papra 26.6 needs an Origin header — a browser sends it)", ""), "plant-it": (N, "lifecycle: abandoned — not offered for a new install (by design)", ""), "plex": (W, "installs, answers, removes clean; no seed (a server is claimed with a plex.tv account)", ""), "privatebin": (W, "the seeded item back after the restore; D22 put-back PASS", ""), "prowlarr": (W, "the seeded item back after the restore", ""), "qbittorrent": (W, "the seeded item back after the restore", ""), "radarr": (W, "the seeded item back after the restore", ""), "radicale": (W, "the seeded item back after the restore", ""), "rallly": (W, "installs, answers, removes clean; no seed: Rallly 4.15 closed password sign-up (login by e-mail code); the guide's polls need no account", "R-1277"), "recipe-importer": (W, "stateless helper; installs, answers, removes clean (every-5th restore)", ""), "romm": (P, "item back through „use my kept data” (its own copy); remove-with-data left the empty parent folder", "R-1276"), "sabnzbd": (W, "the seeded item back after the restore", ""), "seerr": (W, "installs, answers (307 to its setup), removes clean; no seed (needs a media server)", ""), "sonarr": (W, "the seeded item back after the restore", ""), "sparkyfitness": (W, "the seeded item back after the restore", ""), "tandoor": (P, "works; but the keep-backups → reinstall → restore path breaks it (DB password from the new install over the old data), and the restore still said success — reproduced on its own", "R-1269, R-1270"), "termix": (W, "installs, answers, removes clean (every-5th restore); no seed fixture", ""), "transmission": (W, "the seeded item back after the restore", ""), "uptime-kuma": (W, "the seeded item back after the restore", ""), "vaultwarden": (W, "installs in 25 s, its own login page, sign-up closed (404); the box admin's invite works (run-live-2), but 1.37.4 has no route for the fixture's invited sign-up, so no item was seeded", "R-1277"), "vikunja": (W, "the seeded item back after the restore; D22 put-back PASS", ""), "wanderer": (T, "not testable on this box: its web part calls its database at the PUBLIC name, which tester4.test cannot resolve (known, R-739) — unhealthy, 404", "R-739"), "wger": (N, "lifecycle: hidden — not offered for a new install (by design)", ""), "wishlist": (W, "the seeded item back after the restore", ""), "zipline": (W, "the seeded item back after the restore", ""), } def mem(r): anon = r.get("mem_anon_peak_mib") or {} src = anon if anon else (r.get("mem_peak_mib") or {}) lim = sum((c.get("limit_mib") or 0) for c in (r.get("containers") or {}).values()) return f"{round(sum(src.values()))}{'' if anon else '*'} / {lim}" if src else "–" def main(): rows = ["| App | Verdict | Peak memory / limit (MiB) | Install → answering (s) | What we saw | Row / fix |", "|---|---|---|---|---|---|"] for app in sorted(VERDICTS): v, line, ref = VERDICTS[app] f = os.path.join(EV, app, "record.json") r = json.load(open(f)) if os.path.exists(f) else {} inst = (r.get("install") or {}).get("seconds_to_front_door") rows.append(f"| {app} | **{v or 'PENDING'}** | {mem(r) if r else '–'} | {inst if inst is not None else '–'} | {line} | {ref} |") c = {} for v, _, _ in VERDICTS.values(): c[v or "PENDING"] = c.get(v or "PENDING", 0) + 1 rows.append("\n" + ", ".join(f"{k}: {n}" for k, n in c.items()) + f" (of {len(VERDICTS)})") out = "\n".join(rows) if "--write" in sys.argv: # replace the block between the markers in ../README.md p = os.path.join(EV, "README.md") s = open(p).read() a, b = s.index("") + len(""), s.index("") open(p, "w").write(s[:a] + "\n" + out + "\n" + s[b:]) print(out) if __name__ == "__main__": main()