Surface Proxmox VMs/LXCs with no backup coverage at all

A failing backup run is visible now, but a guest with no backup job
covering it in the first place was still a silent gap. Rather than
depending on Proxmox's /cluster/backup-info/not-backed-up-guests
endpoint (only exists on newer PVE versions), this derives coverage
from data already fetched: a guest counts as covered if any enabled
job either lists its vmid directly, or backs up "all guests" (scoped
to the job's node, if it has one) without excluding it.

Adds a warning banner plus a full table to the Proxmox page's Backups
card, and extends the existing daily "Proxmox backup failed"
notification (relabeled to mention this too) to also list uncovered
guests, gated by the same toggle.

Verified the coverage logic directly (it's a pure function, so no
fake server needed) across 7 cases: no jobs at all, an all-guests job
with an exclude list, a specific-vmids job, a node-scoped job that
shouldn't cover a guest on a different node, a disabled job providing
no real coverage, two jobs whose combined scope covers everything
neither would alone, and a realistic mixed scenario — all passed.
Confirmed the real dev database's mtime was untouched throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-22 21:01:48 +02:00
1 parent 73d649377e
commit ca61f2a915
7 files changed
+98 -7

No files matched your search

+1
View File
@@ -541,6 +541,7 @@ export interface ProxmoxBackupTask {
export interface ProxmoxBackupsResponse {
jobs: ProxmoxBackupJob[];
tasks: ProxmoxBackupTask[];
uncoveredGuests: ProxmoxGuest[];
}
export interface SynologyVolume {
+40
View File
@@ -95,6 +95,7 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
const [backupJobs, setBackupJobs] = useState<ProxmoxBackupJob[] | null>(null);
const [backupTasks, setBackupTasks] = useState<ProxmoxBackupTask[] | null>(null);
const [uncoveredGuests, setUncoveredGuests] = useState<ProxmoxGuest[] | null>(null);
const [backupsError, setBackupsError] = useState<string | null>(null);
const [loadingBackups, setLoadingBackups] = useState(false);
@@ -140,6 +141,7 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
.then((res) => {
setBackupJobs(res.jobs);
setBackupTasks(res.tasks);
setUncoveredGuests(res.uncoveredGuests);
})
.catch((err) => setBackupsError(err instanceof Error ? err.message : String(err)))
.finally(() => setLoadingBackups(false));
@@ -155,6 +157,7 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
setNodeStats(null);
setBackupJobs(null);
setBackupTasks(null);
setUncoveredGuests(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId]);
@@ -349,6 +352,12 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
{backupTasks[0].status}
</div>
)}
{uncoveredGuests && uncoveredGuests.length > 0 && (
<div className="alert alert-warning">
{uncoveredGuests.length} guest{uncoveredGuests.length !== 1 ? "s are" : " is"} not covered by
any enabled backup job: {uncoveredGuests.map((g) => `${g.name} (#${g.vmid})`).join(", ")}
</div>
)}
<div className="text-secondary small mb-1">Scheduled jobs</div>
{backupJobs && backupJobs.length > 0 ? (
@@ -382,6 +391,37 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
!backupsError && <div className="text-secondary small mb-4">No backup jobs configured in Proxmox.</div>
)}
<div className="text-secondary small mb-1">Guests without backup coverage</div>
{uncoveredGuests && uncoveredGuests.length > 0 ? (
<table className="table table-sm table-vcenter mb-4">
<thead>
<tr>
<th>Name</th>
<th>Node</th>
<th>Type</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{uncoveredGuests.map((g) => (
<tr key={`${g.node}:${g.type}:${g.vmid}`}>
<td>
{g.name} <span className="text-secondary">#{g.vmid}</span>
</td>
<td className="text-secondary">{g.node}</td>
<td>
<span className="badge bg-blue-lt">{g.type === "qemu" ? "VM" : "LXC"}</span>
</td>
<td>{proxmoxStatusBadge(g.status)}</td>
</tr>
))}
</tbody>
</table>
) : (
!backupsError &&
uncoveredGuests && <div className="text-secondary small mb-4">Every VM/LXC is covered by at least one enabled backup job.</div>
)}
<div className="text-secondary small mb-1">Recent runs</div>
{backupTasks && backupTasks.length > 0 ? (
<table className="table table-sm table-vcenter mb-0">
@@ -519,7 +519,7 @@ export default function NotificationSettings() {
{ key: "secretCheck" as const, label: "Secret expiry reminder" },
{ key: "tailscaleKeyCheck" as const, label: "Tailscale key expiry reminder" },
{ key: "dockerUpdateCheck" as const, label: "Docker image update available" },
{ key: "proxmoxBackupCheck" as const, label: "Proxmox backup failed" },
{ key: "proxmoxBackupCheck" as const, label: "Proxmox backup failed or a guest has no coverage" },
{ key: "integrationFailureAlerts" as const, label: "Integration/DNS provider failing repeatedly" },
].map(({ key, label }) => (
<label key={key} className="form-check mb-2">