Update modal lxc

This commit is contained in:
MacRimi
2026-02-02 18:49:18 +01:00
parent f159ee77cd
commit 8d34119e7a
2 changed files with 113 additions and 114 deletions

View File

@@ -1239,14 +1239,15 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
<div className="flex-1 overflow-hidden px-6 py-4"> <div className="flex-1 overflow-hidden px-6 py-4">
{/* Mobile carousel container */} {/* Mobile carousel container */}
<div className="sm:hidden relative overflow-hidden"> <div className="sm:hidden flex flex-col h-full">
<div <div className="flex-1 relative overflow-hidden">
className="flex transition-transform duration-300 ease-in-out w-[200%]" <div
style={{ transform: `translateX(-${modalPage * 50}%)` }} className="flex transition-transform duration-300 ease-in-out h-full"
> style={{ transform: `translateX(-${modalPage * 100}%)` }}
{/* Page 0: Main content */} >
<div className="w-1/2 flex-shrink-0 overflow-y-auto pr-2" style={{ maxHeight: 'calc(100vh - 280px)' }}> {/* Page 0: Main content */}
<div className="space-y-6 pr-1"> <div className="w-full flex-shrink-0 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 320px)' }}>
<div className="space-y-6">
{selectedVM && ( {selectedVM && (
<> <>
<div key={`metrics-mobile-${selectedVM.vmid}`}> <div key={`metrics-mobile-${selectedVM.vmid}`}>
@@ -1365,77 +1366,78 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
) : null} ) : null}
</> </>
)} )}
</div>
</div> </div>
</div>
{/* Page 1: Backups */} {/* Page 1: Backups */}
<div className="w-1/2 flex-shrink-0 overflow-y-auto pl-2" style={{ maxHeight: 'calc(100vh - 280px)' }}> <div className="w-full flex-shrink-0 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 320px)' }}>
<div className="space-y-4"> <div className="space-y-4">
<Card className="border border-border bg-card/50"> <Card className="border border-border bg-card/50">
<CardContent className="p-4"> <CardContent className="p-4">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-4">Create Backup</h3> <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-4">Create Backup</h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="text-xs text-muted-foreground mb-1 block">Storage</label> <label className="text-xs text-muted-foreground mb-1 block">Storage</label>
<Select value={selectedBackupStorage} onValueChange={setSelectedBackupStorage}> <Select value={selectedBackupStorage} onValueChange={setSelectedBackupStorage}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Select storage" /> <SelectValue placeholder="Select storage" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{backupStorages.map((storage) => ( {backupStorages.map((storage) => (
<SelectItem key={storage.storage} value={storage.storage}> <SelectItem key={storage.storage} value={storage.storage}>
{storage.storage} ({storage.avail_human} free) {storage.storage} ({storage.avail_human} free)
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div>
<Button
className="w-full bg-amber-600 hover:bg-amber-700 text-white"
onClick={handleCreateBackup}
disabled={creatingBackup || !selectedBackupStorage}
>
{creatingBackup ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Creating...</>
) : (
<><Archive className="h-4 w-4 mr-2" />Create Backup</>
)}
</Button>
</div> </div>
<Button </CardContent>
className="w-full bg-amber-600 hover:bg-amber-700 text-white" </Card>
onClick={handleCreateBackup}
disabled={creatingBackup || !selectedBackupStorage}
>
{creatingBackup ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Creating...</>
) : (
<><Archive className="h-4 w-4 mr-2" />Create Backup</>
)}
</Button>
</div>
</CardContent>
</Card>
<Card className="border border-border bg-card/50"> <Card className="border border-border bg-card/50">
<CardContent className="p-4"> <CardContent className="p-4">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-4"> <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-4">
Backups ({vmBackups.length}) Backups ({vmBackups.length})
</h3> </h3>
{loadingBackups ? ( {loadingBackups ? (
<div className="text-center py-4 text-muted-foreground"> <div className="text-center py-4 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" /> <Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />
Loading backups... Loading backups...
</div> </div>
) : vmBackups.length === 0 ? ( ) : vmBackups.length === 0 ? (
<div className="text-center py-4 text-muted-foreground text-sm"> <div className="text-center py-4 text-muted-foreground text-sm">
No backups found No backups found
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{vmBackups.map((backup, index) => ( {vmBackups.map((backup, index) => (
<div key={`backup-${backup.volid}-${index}`} className="p-3 rounded-lg bg-muted/50 border border-border"> <div key={`backup-${backup.volid}-${index}`} className="p-3 rounded-lg bg-muted/50 border border-border">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<div className="text-sm font-medium">{backup.date}</div> <div className="text-sm font-medium">{backup.date}</div>
<div className="text-xs text-muted-foreground">{backup.storage}</div> <div className="text-xs text-muted-foreground">{backup.storage}</div>
</div>
<Badge variant="outline" className="text-xs">{backup.size_human}</Badge>
</div> </div>
<Badge variant="outline" className="text-xs">{backup.size_human}</Badge>
</div> </div>
</div> ))}
))} </div>
</div> )}
)} </CardContent>
</CardContent> </Card>
</Card> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -1456,9 +1458,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
{/* Desktop layout */} {/* Desktop layout */}
<div className="hidden sm:block overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}> <div className="hidden sm:block overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
<div className="space-y-6"> <div className="space-y-6">
{selectedVM && ( {selectedVM && (
<> <>
<div key={`metrics-${selectedVM.vmid}`}> <div key={`metrics-${selectedVM.vmid}`}>
<Card <Card
className="cursor-pointer rounded-lg border border-border bg-card hover:bg-black/5 dark:hover:bg-white/5 transition-colors group" className="cursor-pointer rounded-lg border border-border bg-card hover:bg-black/5 dark:hover:bg-white/5 transition-colors group"
onClick={handleMetricsClick} onClick={handleMetricsClick}
@@ -2159,11 +2161,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</Card> </Card>
</> </>
) : null} ) : null}
</> </>
)} )}
</div>
</div> </div>
</div> </div>
</div>
<div className="border-t border-border bg-background px-6 py-4 mt-auto"> <div className="border-t border-border bg-background px-6 py-4 mt-auto">
{/* Terminal button for LXC containers - only when running */} {/* Terminal button for LXC containers - only when running */}

View File

@@ -5666,6 +5666,10 @@ def api_vm_backups(vmid):
try: try:
backups = [] backups = []
# Get current node name
node_result = subprocess.run(['hostname'], capture_output=True, text=True, timeout=5)
node = node_result.stdout.strip() if node_result.returncode == 0 else 'localhost'
# Get list of storage locations # Get list of storage locations
result = subprocess.run(['pvesh', 'get', '/storage', '--output-format', 'json'], result = subprocess.run(['pvesh', 'get', '/storage', '--output-format', 'json'],
capture_output=True, text=True, timeout=10) capture_output=True, text=True, timeout=10)
@@ -5681,9 +5685,11 @@ def api_vm_backups(vmid):
# Only check storages that can contain backups # Only check storages that can contain backups
if 'backup' in content or storage_type == 'pbs': if 'backup' in content or storage_type == 'pbs':
try: try:
# Use --vmid filter to get only backups for this VM
content_result = subprocess.run( content_result = subprocess.run(
['pvesh', 'get', f'/nodes/$(hostname)/storage/{storage_id}/content', '--output-format', 'json'], ['pvesh', 'get', f'/nodes/{node}/storage/{storage_id}/content',
capture_output=True, text=True, timeout=15, shell=True '--vmid', str(vmid), '--output-format', 'json'],
capture_output=True, text=True, timeout=30
) )
if content_result.returncode == 0: if content_result.returncode == 0:
@@ -5691,39 +5697,30 @@ def api_vm_backups(vmid):
for item in contents: for item in contents:
if item.get('content') == 'backup': if item.get('content') == 'backup':
volid = item.get('volid', '') # Get backup type from subtype field (PBS) or parse volid (local)
backup_type = item.get('subtype', '')
if not backup_type:
volid = item.get('volid', '')
if 'vzdump-qemu-' in volid:
backup_type = 'qemu'
elif 'vzdump-lxc-' in volid:
backup_type = 'lxc'
# Check if this backup belongs to the requested vmid size = item.get('size', 0)
backup_vmid = None ctime = item.get('ctime', 0)
backup_type = None notes = item.get('notes', '')
if 'vzdump-qemu-' in volid: backups.append({
backup_type = 'qemu' 'volid': item.get('volid', ''),
try: 'storage': storage_id,
backup_vmid = int(volid.split('vzdump-qemu-')[1].split('-')[0]) 'type': backup_type,
except: 'size': size,
pass 'size_human': format_bytes(size),
elif 'vzdump-lxc-' in volid: 'timestamp': ctime,
backup_type = 'lxc' 'date': datetime.fromtimestamp(ctime).strftime('%Y-%m-%d %H:%M') if ctime else '',
try: 'notes': notes
backup_vmid = int(volid.split('vzdump-lxc-')[1].split('-')[0]) })
except: except Exception as e:
pass
if backup_vmid == vmid:
size = item.get('size', 0)
ctime = item.get('ctime', 0)
backups.append({
'volid': volid,
'storage': storage_id,
'type': backup_type,
'size': size,
'size_human': format_bytes(size),
'timestamp': ctime,
'date': datetime.fromtimestamp(ctime).strftime('%Y-%m-%d %H:%M') if ctime else ''
})
except:
continue continue
# Sort by timestamp (newest first) # Sort by timestamp (newest first)