feat(instances): add VM owner assignment and snapshot archive view
All checks were successful
Deploy to Production / Trigger production update (push) Successful in 8s

Surface two previously-unused admin endpoints: assign/clear a VM's owner from
the instance Settings tab, and a /instances/snapshot-archives page listing
snapshots preserved from deleted guests (with config detail), linked from the
Instances header for admins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 18:55:47 +05:00
parent dc95d39465
commit bc462c9f8e
7 changed files with 282 additions and 5 deletions

View File

@@ -213,6 +213,73 @@ export function useRollbackSnapshot(serverId: number, vmid: number, type: string
});
}
// ==================== Owner (admin) ====================
export interface VMOwnerInfo {
owner_id: number | null;
owner: { id: number; username: string; full_name?: string } | null;
users: { id: number; username: string; full_name?: string }[];
}
export function useVMOwner(serverId: number, vmid: number, enabled = true) {
return useQuery<VMOwnerInfo>({
queryKey: ['vm-owner', serverId, vmid],
queryFn: () => apiClient.get(`/proxmox/api/${serverId}/vm/${vmid}/owner`),
enabled: enabled && serverId > 0 && vmid > 0,
retry: false,
});
}
export function useSetVMOwner(serverId: number, vmid: number) {
const qc = useQueryClient();
return useMutation({
mutationFn: (user_id: number | null) =>
apiClient.put(`/proxmox/api/${serverId}/vm/${vmid}/owner`, { user_id }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['vm-owner', serverId, vmid] }),
});
}
// ==================== Snapshot archives (admin audit) ====================
export interface SnapshotArchive {
id: number;
server_id: number;
server_name?: string;
vmid: number;
vm_name?: string;
vm_type?: string;
node?: string;
snapname: string;
description?: string;
snaptime?: number;
parent?: string;
vmstate?: number;
snapshot_config?: string;
deleted_by?: string;
deletion_reason?: string;
archived_at?: string;
}
export function useSnapshotArchives(params?: { server_id?: number; vmid?: number; limit?: number; offset?: number }) {
const qs = new URLSearchParams();
if (params?.server_id) qs.set('server_id', String(params.server_id));
if (params?.vmid) qs.set('vmid', String(params.vmid));
if (params?.limit) qs.set('limit', String(params.limit));
if (params?.offset) qs.set('offset', String(params.offset));
return useQuery<{ total: number; offset: number; limit: number; archives: SnapshotArchive[] }>({
queryKey: ['snapshot-archives', params],
queryFn: () => apiClient.get(`/proxmox/api/snapshot-archives?${qs}`),
});
}
export function useSnapshotArchiveDetail(archiveId: number | null) {
return useQuery<SnapshotArchive>({
queryKey: ['snapshot-archive', archiveId],
queryFn: () => apiClient.get(`/proxmox/api/snapshot-archives/${archiveId}`),
enabled: !!archiveId,
});
}
// ==================== Power Actions ====================
interface PowerActionResult {
status: string;