85 lines
5.8 KiB
Python
85 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""rows.py [--apply] — this run's register changes in OPEN-ITEMS.md: R-1269/R-1270/R-1271 to VERIFY (fixed on main,
|
|
wait for the release), two new rows (R-1278, R-1279) in severity order with the heading counts, and a cross-reference
|
|
on R-1206. Without --apply it prints and changes nothing."""
|
|
import os, re, sys
|
|
|
|
FE = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../.."))
|
|
OPEN = os.path.join(FE, "documentation/backlog/OPEN-ITEMS.md")
|
|
CLOSED = os.path.join(FE, "documentation/backlog/CLOSED-ITEMS.md")
|
|
EV = "`audits/restore-truth-2026-10-25/"
|
|
SEV = {"P1": 1, "P2": 2, "P3": 3, "P4": 4}
|
|
VERIFY = ("**VERIFY** — fixed on main, waits for the release", "the next controller release")
|
|
FIXED = {
|
|
"R-1269": (" **2026-10-25 (D47, controller `02daebe` + `1f9c863`):** every install has an identity, the unit records it, and "
|
|
"every unit writer sets another install's unit aside whole (`backups/set-aside/<app>/<stamp>`, listed with „Visszaállítás”). "
|
|
f"Proven on 9202 ({EV}after/`): the old unit set aside byte for byte, restored with its own password, the account back.",
|
|
"release the controller (Monday list); re-run `tools/unitprobe2.py tandoor` on the released image"),
|
|
"R-1270": (" **2026-10-25 (controller `b680db8`):** a restore whose app does not reach running is never a success on any path; "
|
|
"the household reads that the app has not started yet; the undo copy is kept; a late start turns it into a success. "
|
|
f"Proven on 9202 ({EV}after/_pullprobe/`).",
|
|
"release the controller (Monday list); D49 asks whether to pour the old data back instead"),
|
|
"R-1271": (" **2026-10-25 (controller `bd9f4f7`):** cause — the off-site copy's date was the other box's snapshot time, "
|
|
"uncapped, and „newer than every local copy” took it; the „another drive” check compares a mount path (the same on "
|
|
"every box). Now a copy more than 10 minutes in the future never counts as the newest; alone it is offered, said "
|
|
f"and not recommended. Immich's 404 was the copy's old subdomain. Proven on 9202 ({EV}after/_keptprobe/`).",
|
|
"release the controller (Monday list); re-run `tools/keptprobe.py` on the released image"),
|
|
}
|
|
NEW = [
|
|
("R-1278", "Backup & restore", "P3",
|
|
"**A volume app's reinstall does not offer its kept backup.** „Remove — keep my data” removes a volume app's named "
|
|
"volumes and keeps only its backup; the kept-data question is about drive folders only (`07` §6.5, decision 36), so "
|
|
"the reinstall starts empty and says nothing. Since D47 (2026-10-25) the earlier install's unit is set aside and listed "
|
|
f"on the restore page („Megőrzött másolatok”, „Visszaállítás”) — but the install page does not point to it ({EV}README.md`).",
|
|
"OPEN", "—", "Design: at install, when a set-aside or removed-app unit of the app exists, say so and offer it beside a fresh "
|
|
"start; a test", "CC"),
|
|
("R-1279", "Backup & restore", "P3",
|
|
"**The `.fab` import removes a running app's volumes before its images are pulled — D48 does not cover it.** "
|
|
"`felhom-controller/controller/internal/appexport/restore.go` removes the live volumes (`RemoveStackVolumes`, step 1) "
|
|
"and pulls at the database start later; a bundle whose version cannot be downloaded then leaves the app gone (the "
|
|
"bundle itself stays, so a later import works). Found reading every restore path for D48 (2026-10-25).",
|
|
"OPEN", "—", "Pull the bundle's images before step 1 (the D48 rule and its two sentences); a test with an unpullable image", "CC"),
|
|
]
|
|
R1206_NOTE = (" **2026-10-25:** the kept-data choice no longer takes such a future-dated copy as the newest (R-1271, "
|
|
"controller `bd9f4f7`); retention is unchanged — this row.")
|
|
|
|
|
|
def main(apply):
|
|
text = open(OPEN).read()
|
|
ids = [int(x) for x in re.findall(r"\bR-(\d+)\b", text + open(CLOSED).read())]
|
|
for rid, *_ in NEW:
|
|
assert int(rid[2:]) > max(i for i in ids if i < int(rid[2:])) and f"**{rid}**" not in text, rid
|
|
lines = text.split("\n")
|
|
for rid, (note, action) in FIXED.items():
|
|
i = next(i for i, l in enumerate(lines) if l.startswith(f"| **{rid}** |"))
|
|
cells = lines[i].split(" | ")
|
|
assert len(cells) == 8, (rid, len(cells))
|
|
if "2026-10-25" not in cells[3]:
|
|
cells[3] += note
|
|
cells[4], cells[5], cells[6] = VERIFY[0], VERIFY[1], action
|
|
lines[i] = " | ".join(cells)
|
|
i = next(i for i, l in enumerate(lines) if l.startswith("| **R-1206** |"))
|
|
cells = lines[i].split(" | ")
|
|
if "R-1271" not in lines[i]:
|
|
cells[4] = cells[4] + R1206_NOTE
|
|
lines[i] = " | ".join(cells)
|
|
for rid, cat, sev, what, state, blocked, action, owner in NEW:
|
|
row = f"| **{rid}** | {cat} | {sev} | {what} | {state} | {blocked} | {action} | {owner} |"
|
|
h = next(i for i, l in enumerate(lines) if re.match(rf"^## {re.escape(cat)} — \d+ rows", l))
|
|
end = next((i for i in range(h + 1, len(lines)) if lines[i].startswith("## ")), len(lines))
|
|
rows_at = [i for i in range(h, end) if lines[i].startswith("| **R-")]
|
|
pos = next((i for i in rows_at if SEV.get(lines[i].split(" | ")[2].strip(), 9) > SEV[sev]), rows_at[-1] + 1)
|
|
lines.insert(pos, row)
|
|
m = re.match(r"^(## .+? — )(\d+) rows \((.*)\)$", lines[h])
|
|
counts = {p: int(n) for p, n in re.findall(r"(P\d) (\d+)", m.group(3))}
|
|
counts[sev] = counts.get(sev, 0) + 1
|
|
lines[h] = f"{m.group(1)}{int(m.group(2)) + 1} rows ({', '.join(f'{k} {counts[k]}' for k in sorted(counts))})"
|
|
out = "\n".join(lines)
|
|
print("rows:", sum(1 for l in out.split("\n") if l.startswith("| **R-")))
|
|
if apply:
|
|
open(OPEN, "w").write(out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main("--apply" in sys.argv)
|