Hide backups of earlier guests that reused the VMID

Proxmox ties backups to the VMID only, so a deleted guest's backups
were listed under the new guest with that VMID. CT 104 showed planka
backups next to debian's, and VM 108 showed vaultwarden backups.

Backups whose notes name another guest are now hidden by default. The
notes come from the job's {{guestname}}, so "debian (manual)" still
counts as debian. A line says how many are hidden, and the a key shows
them, marked "(earlier guest)". Backups without notes are always
shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-02 21:15:49 +01:00
1 parent 32809b6743
commit 3efd028708
4 files changed
+91 -12

No files matched your search

+40 -11
View File
@@ -167,6 +167,7 @@ class InfoScreen(ModalScreen):
("escape", "dismiss", "Close"),
("g", "reload", "Refresh"),
("b", "backup", "Backup now"),
("a", "toggle_all", "All backups"),
]
DEFAULT_CSS = "InfoScreen DataTable { height: auto; max-height: 12; }"
@@ -175,12 +176,15 @@ class InfoScreen(ModalScreen):
self.guest = guest
self.storages: list[str] = []
self.jobs: list[dict] = []
self.backups: list[dict] = []
self.show_all = False
def compose(self) -> ComposeResult:
with Vertical():
yield Static(f"{self.guest['name']} ({self.guest['vmid']})", markup=False)
yield Static("", id="jobs", markup=False)
yield DataTable(id="backups")
yield Static("", id="hidden", markup=False)
yield DataTable(id="nets")
yield Static("", id="unavailable", markup=False)
yield DataTable(id="addresses")
@@ -252,17 +256,8 @@ class InfoScreen(ModalScreen):
"; ".join(_job_text(j) for j in covering)
or "No backup job covers this guest"
)
table = self.query_one("#backups", DataTable).clear()
for b in backups:
table.add_row(
datetime.fromtimestamp(b["ctime"]).strftime("%Y-%m-%d %H:%M")
if b.get("ctime")
else "-",
b["storage"],
f"{b['size'] / 2**30:.1f}" if b.get("size") is not None else "-",
b.get("format") or "-",
b.get("notes") or "",
)
self.backups = backups
self._show_backups()
table = self.query_one("#nets", DataTable).clear()
for name, net in nets.items():
if cli.kind(self.guest) == "vm": # IP settings are cloud-init's ipconfigN
@@ -286,6 +281,34 @@ class InfoScreen(ModalScreen):
i["name"], i["mac"] or "-", ", ".join(i["ipv4"]), ", ".join(i["ipv6"])
)
def _show_backups(self) -> None:
name = self.guest["name"]
shown = [
b for b in self.backups if self.show_all or not _earlier_guest(b, name)
]
table = self.query_one("#backups", DataTable).clear()
for b in shown:
table.add_row(
datetime.fromtimestamp(b["ctime"]).strftime("%Y-%m-%d %H:%M")
if b.get("ctime")
else "-",
b["storage"],
f"{b['size'] / 2**30:.1f}" if b.get("size") is not None else "-",
b.get("format") or "-",
(b.get("notes") or "")
+ (" (earlier guest)" if _earlier_guest(b, name) else ""),
)
hidden = len(self.backups) - len(shown)
self.query_one("#hidden", Static).update(
f"{hidden} backup(s) of an earlier guest with this VMID hidden - press a to show"
if hidden
else ""
)
def action_toggle_all(self) -> None:
self.show_all = not self.show_all
self._show_backups()
def action_backup(self) -> None:
if not self.storages:
self.app.notify("No backup storage available", severity="warning")
@@ -301,6 +324,12 @@ class InfoScreen(ModalScreen):
)
def _earlier_guest(backup: dict, name: str) -> bool:
"""VMIDs are reused: notes (the job's {{guestname}}) naming another guest mean a deleted one."""
notes = backup.get("notes") or ""
return bool(notes) and notes != name and not notes.startswith(f"{name} ")
def _job_text(job: dict) -> str:
keep = (job.get("prune-backups") or {}).get("keep-last")
return f"{job['schedule']} to {job['storage']}" + (