#!/usr/bin/env python3 """Build the report's table FROM the verdict records, so it cannot drift from the evidence.""" import glob, json, io, os HERE = os.path.dirname(os.path.abspath(__file__)) THE28 = ("calcom calibre-web claper code-server crafty-controller emby ghost gokapi gramps-web " "homebox homepage immich jellyfin kimai komga onlyoffice outline paperless-ngx plant-it " "plex radarr rallly recipe-importer seerr sonarr sparkyfitness termix wanderer").split() # CLASSES READ FROM `07-backup-architecture.md` §6.2, not re-derived tonight (the brief says so, # and re-deriving is exactly how the brief's own list went wrong). A = at least one readable file # leg; B = data entirely in named volumes. Of the 28, four are A and the rest are B. CLASS_A = {"calibre-web", "immich", "komga", "paperless-ngx"} # which apps carry a database/search SERVICE of their own, read from each compose file ENGINES = {"calcom": "postgres", "claper": "postgres", "outline": "postgres+redis", "paperless-ngx": "postgres+redis", "rallly": "postgres", "sparkyfitness": "postgres", "kimai": "mariadb", "immich": "postgres+redis", "wanderer": "meilisearch"} def klass(a): c = "A file-leg" if a in CLASS_A else "B volumes-only" return c + (" + " + ENGINES[a] if a in ENGINES else "") recs = {} for f in glob.glob(os.path.join(HERE, "apps", "*", "verdict.json")): d = json.load(open(f, encoding="utf-8")) recs[d["app"]] = d rows, tot = [], {} rows.append("| app | class | deployed | seeded | backup | edge | update | restore | removed clean | s | evidence |") rows.append("|---|---|---|---|---|---|---|---|---|---|---|") for a in THE28: d = recs.get(a) if not d: rows.append(f"| `{a}` | — | **NOT WALKED** | — | — | — | — | — | — | — | — |") tot["not-walked"] = tot.get("not-walked", 0) + 1 continue e = d.get("edge") edge = (f"`{e['from'].rsplit(':',1)[1]}` → `{e['to'].rsplit(':',1)[1]}`" if e else "none upstream") seeded = ("yes" if d["seed_read_before"] else ("no route" if d.get("seed_route") in (None, "none written") or not d["deployed"] else "route failed")) upd = d.get("update_phase_final") or ("—" if not e else "not reached") bk = (str(d["backup"]["snapshots_offered"]) + " copy" if d.get("backup") else "—") rows.append("| `%s` | %s | %s | %s | %s | %s | %s | %s | %s | %s | `apps/%s/` |" % ( a, klass(a), "yes" if d["deployed"] else "**no**", seeded, bk, edge, upd, d.get("restore_verdict", "—"), {True: "yes", False: "**no**", None: "—"}.get(d.get("removed_clean"), "—"), d.get("duration_s"), a)) tot[d["verdict"]] = tot.get(d["verdict"], 0) + 1 out = "\n".join(rows) + "\n\n**Totals:** " + " · ".join( f"**{v}** {k}" for k, v in sorted(tot.items(), key=lambda kv: -kv[1])) + \ f" — {sum(tot.values())} of 28 recorded.\n" io.open(os.path.join(HERE, "TABLE.md"), "w", encoding="utf-8").write(out) print(out)