Flag guests that no backup job covers

The guest table marks with "!" each guest that Proxmox's not-backed-up
list returns. The frame's bottom border shows the count, e.g.
"mainframe · 2 without backup job". This uses Proxmox's own answer, so
it also counts jobs that select guests by pool.

If the coverage call fails, the guests are still listed and a warning
says the coverage is unknown.

Closes #9

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #20.
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-02 23:02:35 +01:00
1 parent 5a3ca5a216
commit 520de16cc6
5 files changed
+97 -6

No files matched your search

+27 -6
View File
@@ -659,6 +659,8 @@ class GuestApp(App):
# bumped on every context switch; replies started earlier come from the old cluster
self.generation = 0
self.switching = False
self.context_name = ""
self.unprotected: set[int] = set() # VMIDs no backup job covers
CSS = """
#guests_section {
@@ -679,6 +681,7 @@ class GuestApp(App):
self.watch(self, "sub_title", self._show_context_frame)
table = self.query_one("#guests", DataTable)
table.add_columns(
"", # "!" = no backup job covers the guest
"VMID",
"Name",
"Type",
@@ -692,9 +695,13 @@ class GuestApp(App):
self.action_reload()
def _show_context_frame(self, sub_title: str) -> None:
self.query_one("#guests_section").border_subtitle = sub_title.removeprefix(
"context: "
)
self.context_name = sub_title.removeprefix("context: ")
self._update_frame()
def _update_frame(self) -> None:
n = len(self.unprotected & {g["vmid"] for g in self.guests.values()})
count = f" · {n} without backup job" if n else ""
self.query_one("#guests_section").border_subtitle = self.context_name + count
@work(thread=True)
def _show_context(self) -> None:
@@ -724,21 +731,26 @@ class GuestApp(App):
table.coordinate_to_cell_key(table.cursor_coordinate).row_key.value
]
def _fill(self, guests: list[dict], gen: int) -> None:
def _fill(
self, guests: list[dict], gen: int, unprotected: set[int] | None = None
) -> None:
if gen != self.generation: # listed before a context switch: old cluster
return
self.unprotected = unprotected or set()
table = self.query_one("#guests", DataTable)
current = self._selected()
table.clear()
self.guests = {str(g["vmid"]): g for g in guests}
for g in guests:
_add_plain(table,
_add_plain(table,
"!" if g["vmid"] in self.unprotected else "",
g["vmid"], g["name"], g["type"], g["node"], g["status"],
g.get("maxcpu", "-"), _scaled(g.get("maxmem"), 2**20), _scaled(g.get("maxdisk"), 2**30),
key=str(g["vmid"]),
) # fmt: skip
if current and str(current["vmid"]) in self.guests:
table.move_cursor(row=table.get_row_index(str(current["vmid"])))
self._update_frame()
def _reload(self, acted: dict | None = None) -> None:
"""Runs in a worker thread. `acted` gets its live status: the listing lags."""
@@ -751,7 +763,16 @@ class GuestApp(App):
except Exception as exc: # noqa: BLE001 - any failure must reach the user, not kill the app
self.call_from_thread(self.notify, _message(exc), severity="error")
return
self.call_from_thread(self._fill, guests, gen)
unprotected = None
try:
unprotected = cli.not_backed_up()
except Exception as exc: # noqa: BLE001 - the listing still works without it
self.call_from_thread(
self.notify,
f"backup coverage unknown: {_message(exc)}",
severity="warning",
)
self.call_from_thread(self._fill, guests, gen, unprotected)
def action_reload(self) -> None:
if self.switching: