Add beta 1.2.2.1

This commit is contained in:
MacRimi
2026-06-05 17:12:23 +02:00
parent e855fca0b3
commit 3629fe8848
12 changed files with 907 additions and 325 deletions

View File

@@ -271,7 +271,7 @@ export function Login({ onLogin }: LoginProps) {
</form>
</div>
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.2.2</p>
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.2.2.1-beta</p>
</div>
</div>
)

View File

@@ -300,26 +300,49 @@ export function NetworkMetrics() {
return (
<div className="space-y-6">
{/* Network Overview Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Network Traffic</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground hidden md:inline">Received:</span>
<span className="text-base lg:text-xl font-bold text-green-500"> {trafficInFormatted}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground hidden md:inline">Sent:</span>
<span className="text-base lg:text-xl font-bold text-blue-500"> {trafficOutFormatted}</span>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
{/* ── Network Traffic (preview restyle: Down/Up dual headline + stacked bar) ── */}
{(() => {
const downBytes = networkData.traffic.bytes_recv || 0
const upBytes = networkData.traffic.bytes_sent || 0
const totalBytes = downBytes + upBytes
const downPct = totalBytes > 0 ? (downBytes / totalBytes) * 100 : 50
const upPct = totalBytes > 0 ? (upBytes / totalBytes) * 100 : 50
return (
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Network Traffic</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<div className="text-xs font-medium text-muted-foreground mb-1">
<span className="text-green-500"></span> Down
</div>
<div className="text-xl lg:text-2xl font-bold leading-tight text-green-500">{trafficInFormatted}</div>
</div>
<div>
<div className="text-xs font-medium text-muted-foreground mb-1">
<span className="text-blue-500"></span> Up
</div>
<div className="text-xl lg:text-2xl font-bold leading-tight text-blue-500">{trafficOutFormatted}</div>
</div>
</div>
<div className="flex h-1.5 rounded-full overflow-hidden gap-[2px]">
<div style={{ width: `${downPct}%`, background: '#22c55e' }}></div>
<div style={{ width: `${upPct}%`, background: '#3b82f6' }}></div>
</div>
<div className="mt-2 flex justify-between text-xs text-muted-foreground">
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-green-500"></span>Down {Math.round(downPct)}%</span>
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-blue-500"></span>Up {Math.round(upPct)}%</span>
</div>
</CardContent>
</Card>
)
})()}
{/* ── Active Interfaces (preview restyle v2: revertido al original con title uppercase) ── */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Active Interfaces</CardTitle>
@@ -330,10 +353,10 @@ export function NetworkMetrics() {
{(networkData.physical_active_count ?? 0) + (networkData.bridge_active_count ?? 0)}
</div>
<div className="flex flex-wrap items-center gap-2 mt-2">
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-xs">
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20">
Physical: {networkData.physical_active_count ?? 0}/{networkData.physical_total_count ?? 0}
</Badge>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20 text-xs">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Bridges: {networkData.bridge_active_count ?? 0}/{networkData.bridge_total_count ?? 0}
</Badge>
</div>
@@ -343,31 +366,43 @@ export function NetworkMetrics() {
</CardContent>
</Card>
{/* Merged Network Config & Health Card */}
{/* ── Network Status (preview restyle: packet-loss highlight + 2x2 grid) ── */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Network Status</CardTitle>
<Badge variant="outline" className={healthColor}>
{healthStatus}
</Badge>
<Badge variant="outline" className={`${healthColor}`}>{healthStatus === 'Healthy' ? '✓ ' : ''}{healthStatus}</Badge>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Hostname</span>
<span className="text-xs font-medium text-foreground truncate max-w-[120px]">{hostname}</span>
{(() => {
const lossPct = Number.parseFloat(avgPacketLoss) || 0
const lossColor =
lossPct >= 5 ? 'text-red-500' :
lossPct >= 1 ? 'text-orange-500' :
lossPct > 0 ? 'text-yellow-500' :
'text-blue-500'
return (
<div className={`mb-3 text-xl lg:text-2xl font-bold ${lossColor} leading-none`}>
{avgPacketLoss}<span className="text-sm font-normal text-muted-foreground">% </span>
<span className="text-sm font-normal text-muted-foreground">Packet Loss</span>
</div>
)
})()}
<div className="grid grid-cols-2 gap-x-3 gap-y-3 pt-3 border-t border-border/50 text-sm">
<div className="min-w-0">
<div className="text-muted-foreground">Hostname:</div>
<div className="font-medium font-mono truncate">{hostname}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Primary DNS</span>
<span className="text-xs font-medium text-foreground font-mono">{primaryDNS}</span>
<div className="min-w-0">
<div className="text-muted-foreground">DNS:</div>
<div className="font-medium font-mono truncate">{primaryDNS}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Packet Loss</span>
<span className="text-xs font-medium text-foreground">{avgPacketLoss}%</span>
<div className="min-w-0">
<div className="text-muted-foreground">Errors:</div>
<div className="font-medium font-mono">{totalErrors}</div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Errors</span>
<span className="text-xs font-medium text-foreground">{totalErrors}</span>
<div className="min-w-0">
<div className="text-muted-foreground">Domain:</div>
<div className="font-medium font-mono truncate">{networkData.domain || '—'}</div>
</div>
</div>
</CardContent>

View File

@@ -858,7 +858,7 @@ export function ProxmoxDashboard() {
</Tabs>
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
<p className="font-medium mb-2">ProxMenux Monitor v1.2.2</p>
<p className="font-medium mb-2">ProxMenux Monitor v1.2.2.1-beta</p>
<p>
<a
href="https://ko-fi.com/macrimi"

View File

@@ -6,7 +6,7 @@ import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import { X, Sparkles, Thermometer, Activity, HardDrive, Shield, Globe, Cpu, Zap, Sliders, Wrench, RefreshCw, Server, BellOff, Bell } from "lucide-react"
import { Checkbox } from "./ui/checkbox"
const APP_VERSION = "1.2.2" // Sync with AppImage/package.json
const APP_VERSION = "1.2.2.1-beta" // Sync with AppImage/package.json
interface ReleaseNote {
date: string
@@ -217,20 +217,16 @@ export const CHANGELOG: Record<string, ReleaseNote> = {
const CURRENT_VERSION_FEATURES = [
{
icon: <Sliders className="h-5 w-5" />,
text: "Health Monitor Thresholds - Per-category Warning and Critical levels for CPU, memory, temperature, storage and more, configurable from Settings. The same numbers feed the colour ranges of the dashboard widgets, so every green / amber / red state maps to a definite range relative to the configured pair",
icon: <Activity className="h-5 w-5" />,
text: "Header Critical badge now respects dismissals (#228) - Permanently silencing every critical alert in a category used to leave the badge stuck on Critical even though the popup correctly reported 0 critical. The rollup that drives /api/system-info now runs a dismiss-aware pass over every category, so the badge, the popup and any API consumer all see the same view",
},
{
icon: <BellOff className="h-5 w-5" />,
text: "Granular dismiss control - Each Health Monitor alert can now be dismissed for 24 hours, 7 days or Permanently via a per-event dropdown. A new Active Suppressions panel in Settings lists every silenced alert with a Re-enable button, gated by Edit mode. Permanent dismisses can only be reverted from there",
icon: <RefreshCw className="h-5 w-5" />,
text: "Auto-reconcile of stale alerts - Errors for resources that no longer exist now auto-clear within the regular cleanup cycle. New cases: a PVE storage removed via pvesm, an NFS/CIFS share whose mount target is no longer in /proc/mounts (the lazy-umount case reported in the field), and LXC mount-capacity alerts whose CT has been deleted",
},
{
icon: <Bell className="h-5 w-5" />,
text: "Apprise notification channel - One Apprise URL reaches ~80 services (Pushover, ntfy, Slack, Matrix, mailto, signal, ...) with full feature parity to the native channels: per-event toggles, Quiet Hours and Daily Digest all apply",
},
{
icon: <Server className="h-5 w-5" />,
text: "LXC update detection - Per-CT apt list --upgradable / apk list -u scan from Settings, with an automatic cache refresh on long-running containers so months-old metadata no longer hides real upstream backlog",
text: "Notification Send Test buttons unified (#226) - All five channel Send Test buttons (Telegram, Gotify, Discord, Email, Apprise) now sit on the left side and carry their channel's brand colour with white text, instead of Apprise being the right-aligned cyan outlier",
},
]

View File

@@ -690,103 +690,213 @@ export function StorageOverview() {
return (
<div className="space-y-6">
{/* Storage Summary */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Storage</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{storageData.total.toFixed(1)} TB</div>
<p className="text-xs text-muted-foreground mt-1">{storageData.disk_count} physical disks</p>
</CardContent>
</Card>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
{/* ── Total Storage (preview restyle: headline + stacked bar Local·Remote·Free) ── */}
{(() => {
const totalGB = (totalLocalCapacity || 0) + (totalRemoteCapacity || 0)
const localPct = totalGB > 0 ? (totalLocalUsed / totalGB) * 100 : 0
const remotePct = totalGB > 0 ? (totalRemoteUsed / totalGB) * 100 : 0
const freeGB = Math.max(0, totalGB - totalLocalUsed - totalRemoteUsed)
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Storage Used</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{(() => {
const totalUsed = totalLocalUsed + totalRemoteUsed
const usedStr = formatStorage(totalUsed)
return (
<div className="flex items-end justify-between mb-3">
<div>
<span className="text-3xl font-bold leading-none">{usedStr.split(' ')[0]}</span>
<span className="text-base font-medium ml-1 text-muted-foreground">{usedStr.split(' ')[1]}</span>
</div>
<Badge variant="outline" className="bg-muted text-muted-foreground border-border">{storageData.disk_count} disks</Badge>
</div>
)
})()}
<div className="flex h-1.5 rounded-full overflow-hidden gap-[2px]">
<div style={{ width: `${localPct}%`, background: '#3b82f6' }} title={`Local ${formatStorage(totalLocalUsed)}`}></div>
<div style={{ width: `${remotePct}%`, background: '#06b6d4' }} title={`Remote ${formatStorage(totalRemoteUsed)}`}></div>
<div style={{ flex: 1, background: 'rgba(99,102,241,0.15)' }} title={`Free ${formatStorage(freeGB)}`}></div>
</div>
<div className="mt-2 space-y-1 text-sm">
<div className="flex items-center justify-between">
<span className="flex items-center gap-1.5 text-muted-foreground"><span className="w-1.5 h-1.5 rounded-full" style={{ background: '#3b82f6' }}></span>Local</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(totalLocalUsed)}</span>
</div>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1.5 text-muted-foreground"><span className="w-1.5 h-1.5 rounded-full" style={{ background: '#06b6d4' }}></span>Remote</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(totalRemoteUsed)}</span>
</div>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1.5 text-muted-foreground"><span className="w-1.5 h-1.5 rounded-full opacity-50" style={{ background: 'currentColor' }}></span>Free</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(freeGB)}</span>
</div>
</div>
</CardContent>
</Card>
)
})()}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Local Used</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalLocalUsed)}</div>
<p className="text-xs mt-1">
<span className={getUsageColor(Number.parseFloat(localUsagePercent))}>{localUsagePercent}%</span>
<span className="text-muted-foreground"> of </span>
<span className="text-green-500">{formatStorage(totalLocalCapacity)}</span>
</p>
</CardContent>
</Card>
{/* ── Local Used (preview restyle: donut + mini-bars Used/Free) ── */}
{(() => {
const pct = Number.parseFloat(localUsagePercent)
const freeGB = Math.max(0, totalLocalCapacity - totalLocalUsed)
const stroke = pct >= 90 ? '#ef4444' : pct >= 75 ? '#f59e0b' : '#22c55e'
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Local Used</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<svg viewBox="0 0 36 36" className="w-[72px] h-[72px] flex-shrink-0">
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="rgba(99,102,241,0.15)" strokeWidth="3"/>
<circle cx="18" cy="18" r="15.9155" fill="none" stroke={stroke} strokeWidth="3"
strokeDasharray={`${pct} 100`} strokeLinecap="round"
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}/>
<text x="18" y="19.5" textAnchor="middle" fontSize="10" fontWeight="700" fill="currentColor">{Math.round(pct)}%</text>
</svg>
<div className="flex-1 space-y-2">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Used</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(totalLocalUsed)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${pct}%`, background: stroke }}/>
</div>
</div>
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Free</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(freeGB)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${100 - pct}%`, background: 'rgba(99,102,241,0.45)' }}/>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Total</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(totalLocalCapacity)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
)
})()}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Remote Used</CardTitle>
<Archive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">
{remoteStorageCount > 0 ? formatStorage(totalRemoteUsed) : "None"}
</div>
<p className="text-xs mt-1">
{remoteStorageCount > 0 ? (
<>
<span className={getUsageColor(Number.parseFloat(remoteUsagePercent))}>{remoteUsagePercent}%</span>
<span className="text-muted-foreground"> of </span>
<span className="text-green-500">{formatStorage(totalRemoteCapacity)}</span>
</>
) : (
<span className="text-muted-foreground">No remote storage</span>
)}
</p>
</CardContent>
</Card>
{/* ── Remote Used (preview restyle: donut + mini-bars Used/Free) ── */}
{(() => {
const has = remoteStorageCount > 0
const pct = has ? Number.parseFloat(remoteUsagePercent) : 0
const freeGB = has ? Math.max(0, totalRemoteCapacity - totalRemoteUsed) : 0
const stroke = pct >= 90 ? '#ef4444' : pct >= 75 ? '#f59e0b' : '#22c55e'
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Remote Used</CardTitle>
<Archive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{has ? (
<div className="flex items-center gap-4">
<svg viewBox="0 0 36 36" className="w-[72px] h-[72px] flex-shrink-0">
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="rgba(99,102,241,0.15)" strokeWidth="3"/>
<circle cx="18" cy="18" r="15.9155" fill="none" stroke={stroke} strokeWidth="3"
strokeDasharray={`${pct} 100`} strokeLinecap="round"
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}/>
<text x="18" y="19.5" textAnchor="middle" fontSize="10" fontWeight="700" fill="currentColor">{Math.round(pct)}%</text>
</svg>
<div className="flex-1 space-y-2">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Used</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(totalRemoteUsed)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${pct}%`, background: stroke }}/>
</div>
</div>
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Free</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(freeGB)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${100 - pct}%`, background: 'rgba(99,102,241,0.45)' }}/>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Total</span>
<span className="font-medium font-mono whitespace-nowrap">{formatStorage(totalRemoteCapacity)}</span>
</div>
</div>
</div>
) : (
<div className="text-center py-4">
<div className="text-2xl font-bold text-muted-foreground">None</div>
<p className="text-xs text-muted-foreground mt-1">No remote storage</p>
</div>
)}
</CardContent>
</Card>
)
})()}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Physical Disks</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
<div className="space-y-1 mt-1">
<p className="text-xs">
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
{diskTypesBreakdown.ssd > 0 && (
<>
{diskTypesBreakdown.nvme > 0 && ", "}
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
</>
)}
{diskTypesBreakdown.hdd > 0 && (
<>
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
</>
)}
{diskTypesBreakdown.usb > 0 && (
<>
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0 || diskTypesBreakdown.hdd > 0) && ", "}
<span className="text-orange-400">{diskTypesBreakdown.usb} USB</span>
</>
)}
</p>
<p className="text-xs">
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
{diskHealthBreakdown.warning > 0 && (
<>
{", "}
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
</>
)}
{diskHealthBreakdown.critical > 0 && (
<>
{", "}
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
</>
)}
</p>
</div>
</CardContent>
</Card>
{/* ── Physical Disks (preview restyle: headline + type strip + health badge) ── */}
{(() => {
const total = Math.max(1, storageData.disk_count || 0)
const seg = 100 / total
const allHealthy = diskHealthBreakdown.warning === 0 && diskHealthBreakdown.critical === 0
const healthBadge = allHealthy
? <Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20"> all healthy</Badge>
: diskHealthBreakdown.critical > 0
? <Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">{diskHealthBreakdown.critical} critical</Badge>
: <Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">{diskHealthBreakdown.warning} warning</Badge>
const seg_purple = '#a855f7'
const seg_cyan = '#06b6d4'
const seg_blue = '#3b82f6'
const seg_orange = '#f97316'
const segments: Array<{ color: string }> = []
for (let i = 0; i < diskTypesBreakdown.nvme; i++) segments.push({ color: seg_purple })
for (let i = 0; i < diskTypesBreakdown.ssd; i++) segments.push({ color: seg_cyan })
for (let i = 0; i < diskTypesBreakdown.hdd; i++) segments.push({ color: seg_blue })
for (let i = 0; i < diskTypesBreakdown.usb; i++) segments.push({ color: seg_orange })
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Physical Disks</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-end justify-between mb-3">
<div>
<span className="text-3xl font-bold leading-none">{storageData.disk_count}</span>
<span className="text-base font-medium ml-1 text-muted-foreground">disks</span>
</div>
{healthBadge}
</div>
<div className="flex h-1.5 rounded-full overflow-hidden gap-[2px]">
{segments.map((s, i) => (
<div key={i} style={{ width: `${seg}%`, background: s.color }}></div>
))}
</div>
<div className="mt-2 flex flex-wrap justify-between text-sm text-muted-foreground gap-x-2 gap-y-1">
{diskTypesBreakdown.nvme > 0 && <span className="flex items-center gap-1 whitespace-nowrap"><span className="w-1.5 h-1.5 rounded-full" style={{ background: seg_purple }}></span>{diskTypesBreakdown.nvme} NVMe</span>}
{diskTypesBreakdown.ssd > 0 && <span className="flex items-center gap-1 whitespace-nowrap"><span className="w-1.5 h-1.5 rounded-full" style={{ background: seg_cyan }}></span>{diskTypesBreakdown.ssd} SSD</span>}
{diskTypesBreakdown.hdd > 0 && <span className="flex items-center gap-1 whitespace-nowrap"><span className="w-1.5 h-1.5 rounded-full" style={{ background: seg_blue }}></span>{diskTypesBreakdown.hdd} HDD</span>}
{diskTypesBreakdown.usb > 0 && <span className="flex items-center gap-1 whitespace-nowrap"><span className="w-1.5 h-1.5 rounded-full" style={{ background: seg_orange }}></span>{diskTypesBreakdown.usb} USB</span>}
</div>
</CardContent>
</Card>
)
})()}
</div>
{proxmoxStorage && proxmoxStorage.storage && proxmoxStorage.storage.length > 0 && (
@@ -1477,7 +1587,7 @@ export function StorageOverview() {
</div>
)}
{(disk.observations_count ?? 0) > 0 && (
<Badge className="bg-blue-500/10 text-blue-400 border-blue-500/20 gap-1 text-[10px] px-1.5 py-0">
<Badge className="bg-blue-500/10 text-blue-400 border-blue-500/20 gap-1">
<Info className="h-3 w-3" />
{disk.observations_count}
</Badge>
@@ -2150,7 +2260,7 @@ export function StorageOverview() {
<h4 className="font-semibold mb-2 flex items-center gap-2">
<Info className="h-4 w-4 text-blue-400" />
Observations
<Badge className="bg-blue-500/10 text-blue-400 border-blue-500/20 text-[10px] px-1.5 py-0">
<Badge className="bg-blue-500/10 text-blue-400 border-blue-500/20">
{diskObservations.length}
</Badge>
</h4>
@@ -3627,7 +3737,7 @@ ${observationsHtml}
<!-- Footer -->
<div class="rpt-footer">
<div>Report generated by ProxMenux Monitor</div>
<div>ProxMenux Monitor v1.2.2</div>
<div>ProxMenux Monitor v1.2.2.1-beta</div>
</div>
</body>

View File

@@ -21,9 +21,12 @@ interface TempDataPoint {
interface SystemData {
cpu_usage: number
cpu_user?: number // preview restyle
cpu_system?: number // preview restyle
memory_usage: number
memory_total: number
memory_used: number
memory_cached?: number // preview restyle
temperature: number
temperature_sparkline?: TempDataPoint[]
uptime: string
@@ -395,64 +398,121 @@ export function SystemOverview() {
return (
<div className="space-y-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
{/* ── CPU Usage (preview restyle v2: tamaño igual a System Info, bars más anchas) ── */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.cpu_usage}%</div>
<Progress value={systemData.cpu_usage} className="mt-2 [&>div]:bg-blue-500" />
<p className="text-xs text-muted-foreground mt-2">Real-time usage</p>
<div className="flex items-center gap-4">
<svg viewBox="0 0 36 36" className="w-[72px] h-[72px] flex-shrink-0">
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="rgba(99,102,241,0.15)" strokeWidth="3"/>
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="#3b82f6" strokeWidth="3"
strokeDasharray={`${systemData.cpu_usage} 100`} strokeLinecap="round"
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}/>
<text x="18" y="19.5" textAnchor="middle" fontSize="10" fontWeight="700" fill="currentColor">{Math.round(systemData.cpu_usage)}%</text>
</svg>
<div className="flex-1 space-y-2 min-w-0">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">User</span>
<span className="font-medium font-mono whitespace-nowrap">{systemData.cpu_user !== undefined ? `${Math.round(systemData.cpu_user)}%` : '—'}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full bg-blue-500 rounded-full" style={{ width: `${systemData.cpu_user ?? 0}%` }}/>
</div>
</div>
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">System</span>
<span className="font-medium font-mono whitespace-nowrap">{systemData.cpu_system !== undefined ? `${Math.round(systemData.cpu_system)}%` : '—'}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${systemData.cpu_system ?? 0}%`, background: 'rgba(99,102,241,0.55)' }}/>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Cores</span>
<span className="font-medium font-mono whitespace-nowrap">{systemData.cpu_cores ?? '—'}{systemData.cpu_threads ? `/${systemData.cpu_threads}` : ''}</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* ── Memory (preview restyle v2: tamaño igual a System Info, bars más anchas) ── */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Memory Usage</CardTitle>
<CardTitle className="text-sm font-medium text-muted-foreground">Memory</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.memory_used.toFixed(1)} GB</div>
<Progress value={systemData.memory_usage} className="mt-2 [&>div]:bg-blue-500" />
<p className="text-xs text-muted-foreground mt-2">
<span className="text-green-500 font-medium">{systemData.memory_usage.toFixed(1)}%</span> of{" "}
{systemData.memory_total} GB
</p>
<div className="flex items-center gap-4">
<svg viewBox="0 0 36 36" className="w-[72px] h-[72px] flex-shrink-0">
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="rgba(99,102,241,0.15)" strokeWidth="3"/>
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="#3b82f6" strokeWidth="3"
strokeDasharray={`${systemData.memory_usage} 100`} strokeLinecap="round"
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}/>
<text x="18" y="19.5" textAnchor="middle" fontSize="10" fontWeight="700" fill="currentColor">{Math.round(systemData.memory_usage)}%</text>
</svg>
<div className="flex-1 space-y-2 min-w-0">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Used</span>
<span className="font-medium font-mono whitespace-nowrap">{systemData.memory_used.toFixed(1)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full bg-blue-500 rounded-full" style={{ width: `${systemData.memory_usage}%` }}/>
</div>
</div>
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Cached</span>
<span className="font-medium font-mono whitespace-nowrap">{systemData.memory_cached !== undefined ? systemData.memory_cached.toFixed(1) : '—'}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${systemData.memory_cached !== undefined && systemData.memory_total > 0 ? (systemData.memory_cached / systemData.memory_total) * 100 : 0}%`, background: 'rgba(99,102,241,0.55)' }}/>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Total</span>
<span className="font-medium font-mono whitespace-nowrap">{systemData.memory_total.toFixed(0)} GB</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* ── Active VM & LXC (preview restyle v2: pills mismo tamaño que "X running") ── */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
Active VM & LXC
</CardTitle>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Active VM &amp; LXC</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loadingStates.vms ? (
<div className="space-y-2 animate-pulse">
<div className="h-8 bg-muted rounded w-12"></div>
<div className="h-5 bg-muted rounded w-24"></div>
<div className="h-4 bg-muted rounded w-32"></div>
<div className="h-10 bg-muted rounded w-20"></div>
<div className="h-5 bg-muted rounded w-32"></div>
</div>
) : (
<>
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
<div className="mt-2 flex flex-wrap gap-1">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{vmStats.running} Running
</Badge>
<div className="flex items-end justify-between">
<div>
<span className="text-4xl font-bold leading-none text-foreground">{vmStats.running}</span>
<span className="text-lg font-medium ml-1 text-muted-foreground">/ {vmStats.vms + vmStats.lxc}</span>
</div>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">{vmStats.running} running</Badge>
</div>
<div className="mt-3 flex gap-1 flex-wrap">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">{vmStats.vms} VMs</Badge>
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20">{vmStats.lxc} LXC</Badge>
{vmStats.stopped > 0 && (
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">
{vmStats.stopped} Stopped
</Badge>
<Badge variant="outline" className="bg-muted text-muted-foreground border-border">{vmStats.stopped} stopped</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Total: {vmStats.vms} VMs, {vmStats.lxc} LXC
</p>
</>
)}
</CardContent>
@@ -471,7 +531,7 @@ export function SystemOverview() {
<span className="text-xl lg:text-2xl font-bold text-foreground">
{systemData.temperature === 0 ? "N/A" : `${Math.round(systemData.temperature * 10) / 10}°C`}
</span>
<Badge variant="outline" className={tempStatus.color}>
<Badge variant="outline" className={`${tempStatus.color}`}>
{tempStatus.status}
</Badge>
</div>

View File

@@ -48,6 +48,7 @@ interface VMData {
status: string
type: string
cpu: number
maxcpu?: number
mem: number
maxmem: number
disk: number
@@ -418,13 +419,13 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
/>
<h3 className="font-mono font-semibold truncate">{mp.target}</h3>
{mp.mp_index && (
<Badge variant="outline" className="font-mono text-xs">
<Badge variant="outline" className="font-mono">
{mp.mp_index}
</Badge>
)}
<Badge className={typeBadgeClass[mp.type]}>{typeLabel[mp.type]}</Badge>
{mp.runtime_fstype && (
<Badge variant="outline" className="font-mono text-xs">
<Badge variant="outline" className="font-mono">
{mp.runtime_fstype}
</Badge>
)}
@@ -524,7 +525,7 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
if (configEntries.length === 0) return null
return (
<div className="mt-3">
<p className="text-xs uppercase tracking-wider text-muted-foreground mb-1.5">
<p className="text-xs text-muted-foreground mb-1.5">
Mount attributes (LXC config)
</p>
<div className="flex flex-wrap gap-1.5">
@@ -550,7 +551,7 @@ function MountPointCard({ mp }: { mp: LxcMountPoint }) {
exist. */}
{(mp.runtime_mounted === true) && (keyValues.length > 0 || flags.length > 0) && (
<div className="mt-3">
<p className="text-xs uppercase tracking-wider text-muted-foreground mb-1.5">
<p className="text-xs text-muted-foreground mb-1.5">
Runtime mount options
</p>
<div className="flex flex-wrap gap-1.5 mb-2">
@@ -1300,139 +1301,178 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
}
`}</style>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total VMs & LXCs</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{safeVMData.length}</div>
<div className="vm-badges mt-2">
<Badge variant="outline" className="vm-badge bg-green-500/10 text-green-500 border-green-500/20">
{safeVMData.filter((vm) => vm.status === "running").length} Running
</Badge>
<Badge variant="outline" className="vm-badge bg-red-500/10 text-red-500 border-red-500/20">
{safeVMData.filter((vm) => vm.status === "stopped").length} Stopped
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2 hidden lg:block">Virtual machines configured</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total CPU</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">
{(safeVMData.reduce((sum, vm) => sum + (vm.cpu || 0), 0) * 100).toFixed(0)}%
</div>
<p className="text-xs text-muted-foreground mt-2">Allocated CPU usage</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Memory</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent className="space-y-3">
{/* Memory Usage (current) */}
{physicalMemoryGB !== null && usedMemoryGB !== null && memoryUsagePercent !== null ? (
<div>
<div className="text-xl lg:text-2xl font-bold text-foreground">{usedMemoryGB.toFixed(1)} GB</div>
<div className="text-xs text-muted-foreground mt-1">
<span className={getMemoryPercentTextColor(memoryUsagePercent)}>
{memoryUsagePercent.toFixed(1)}%
</span>{" "}
of {physicalMemoryGB.toFixed(1)} GB
</div>
<Progress value={memoryUsagePercent} className="h-2 [&>div]:bg-blue-500" />
</div>
) : (
<div>
<div className="text-xl lg:text-2xl font-bold text-muted-foreground">--</div>
<div className="text-xs text-muted-foreground mt-1">Loading memory usage...</div>
</div>
)}
{/* Allocated RAM (configured) - Split into Running and Total */}
<div className="pt-3 border-t border-border">
{/* Layout para desktop */}
<div className="hidden lg:flex items-center justify-between">
<div className="flex gap-6">
{/* Running allocation - most important */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* ── Total VMs & LXCs (preview restyle: B-headline + pills, matching Overview) ── */}
{(() => {
const running = safeVMData.filter((vm) => vm.status === "running").length
const stopped = safeVMData.filter((vm) => vm.status === "stopped").length
const total = safeVMData.length
const vms = safeVMData.filter((vm) => vm.type === "qemu" || vm.type === "vm").length
const lxc = safeVMData.filter((vm) => vm.type === "lxc").length
return (
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total VMs &amp; LXCs</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-end justify-between">
<div>
<div className="text-lg font-semibold text-foreground">{runningAllocatedMemoryGB} GB</div>
<div className="text-xs text-muted-foreground">Running Allocated</div>
<span className="text-4xl font-bold leading-none text-foreground">{running}</span>
<span className="text-lg font-medium ml-1 text-muted-foreground">/ {total}</span>
</div>
{/* Total allocation */}
<div>
<div className="text-lg font-semibold text-muted-foreground">{totalAllocatedMemoryGB} GB</div>
<div className="text-xs text-muted-foreground">Total Allocated</div>
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">{running} running</Badge>
</div>
<div className="mt-3 flex gap-1 flex-wrap">
{vms > 0 && (
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">{vms} VMs</Badge>
)}
{lxc > 0 && (
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20">{lxc} LXC</Badge>
)}
{stopped > 0 && (
<Badge variant="outline" className="bg-muted text-muted-foreground border-border">{stopped} stopped</Badge>
)}
</div>
</CardContent>
</Card>
)
})()}
{/* ── Total CPU Allocated (preview restyle: donut + Used/Configured/In use) ── */}
{(() => {
const allocPct = safeVMData.reduce((sum, vm) => sum + (vm.cpu || 0), 0) * 100
const configuredVCPU = safeVMData.reduce((sum, vm) => sum + (vm.maxcpu || 0), 0)
const inUseVCPU = safeVMData
.filter((vm) => vm.status === "running")
.reduce((sum, vm) => sum + (vm.maxcpu || 0), 0)
const stroke = allocPct >= 90 ? '#ef4444' : allocPct >= 75 ? '#f59e0b' : '#3b82f6'
return (
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total CPU Allocated</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<svg viewBox="0 0 36 36" className="w-[72px] h-[72px] flex-shrink-0">
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="rgba(99,102,241,0.15)" strokeWidth="3"/>
<circle cx="18" cy="18" r="15.9155" fill="none" stroke={stroke} strokeWidth="3"
strokeDasharray={`${Math.min(100, allocPct)} 100`} strokeLinecap="round"
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}/>
<text x="18" y="19.5" textAnchor="middle" fontSize="10" fontWeight="700" fill="currentColor">{Math.round(allocPct)}%</text>
</svg>
<div className="flex-1 space-y-2">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Used</span>
<span className="font-medium font-mono whitespace-nowrap">{Math.round(allocPct)}%</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${Math.min(100, allocPct)}%`, background: stroke }}/>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Configured</span>
<span className="font-medium font-mono whitespace-nowrap">{configuredVCPU || '—'} vCPU</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">In use</span>
<span className="font-medium font-mono whitespace-nowrap">{inUseVCPU || '—'} vCPU</span>
</div>
</div>
</div>
{physicalMemoryGB !== null && (
<div>
{isMemoryOvercommit ? (
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
Exceeds Physical
</Badge>
) : (
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Within Limits
</Badge>
)}
</div>
)}
</div>
</CardContent>
</Card>
)
})()}
{/* Layout para movil */}
<div className="lg:hidden space-y-2">
<div className="flex gap-4">
{/* Running allocation */}
<div>
<div className="text-lg font-semibold text-foreground">{runningAllocatedMemoryGB} GB</div>
<div className="text-xs text-muted-foreground">Running</div>
</div>
{/* Total allocation */}
<div>
<div className="text-lg font-semibold text-muted-foreground">{totalAllocatedMemoryGB} GB</div>
<div className="text-xs text-muted-foreground">Total</div>
{/* ── Total Memory (preview restyle: donut + mini-bars Used/Allocated) ── */}
{(() => {
const usedPct = memoryUsagePercent ?? 0
const usedGB = usedMemoryGB ?? 0
const totalGB = physicalMemoryGB ?? 0
const allocPct = totalGB > 0 ? (allocatedMemoryGB / totalGB) * 100 : 0
const stroke = usedPct >= 90 ? '#ef4444' : usedPct >= 75 ? '#f59e0b' : '#3b82f6'
return (
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Memory</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<svg viewBox="0 0 36 36" className="w-[72px] h-[72px] flex-shrink-0">
<circle cx="18" cy="18" r="15.9155" fill="none" stroke="rgba(99,102,241,0.15)" strokeWidth="3"/>
<circle cx="18" cy="18" r="15.9155" fill="none" stroke={stroke} strokeWidth="3"
strokeDasharray={`${usedPct} 100`} strokeLinecap="round"
style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%' }}/>
<text x="18" y="19.5" textAnchor="middle" fontSize="10" fontWeight="700" fill="currentColor">{Math.round(usedPct)}%</text>
</svg>
<div className="flex-1 space-y-2">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Used</span>
<span className="font-medium font-mono whitespace-nowrap">{usedGB.toFixed(1)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${usedPct}%`, background: stroke }}/>
</div>
</div>
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Alloc</span>
<span className="font-medium font-mono whitespace-nowrap">{allocatedMemoryGB.toFixed(1)}</span>
</div>
<div className="mt-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full rounded-full" style={{ width: `${Math.min(100, allocPct)}%`, background: isMemoryOvercommit ? '#f59e0b' : 'rgba(99,102,241,0.55)' }}/>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Total</span>
<span className="font-medium font-mono whitespace-nowrap">{totalGB.toFixed(0)} GB</span>
</div>
</div>
</div>
{physicalMemoryGB !== null && (
<div>
{isMemoryOvercommit ? (
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
Exceeds Physical
</Badge>
) : (
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Within Limits
</Badge>
)}
</div>
)}
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
)
})()}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Disk</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">
{formatStorage(safeVMData.reduce((sum, vm) => sum + (vm.maxdisk || 0), 0) / 1024 ** 3)}
</div>
<p className="text-xs text-muted-foreground mt-2">Allocated disk space</p>
</CardContent>
</Card>
{/* ── Total Disk (preview restyle: headline + 2-segment stacked bar Used/Alloc-not-Used) ── */}
{(() => {
const usedGB = safeVMData.reduce((sum, vm) => sum + (vm.disk || 0), 0) / 1024 ** 3
const allocGB = safeVMData.reduce((sum, vm) => sum + (vm.maxdisk || 0), 0) / 1024 ** 3
const utilPct = allocGB > 0 ? (usedGB / allocGB) * 100 : 0
const idleGB = Math.max(0, allocGB - usedGB)
const stroke = utilPct >= 90 ? '#ef4444' : utilPct >= 75 ? '#f59e0b' : '#3b82f6'
const usedSeg = allocGB > 0 ? (usedGB / allocGB) * 100 : 0
return (
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Disk</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-end justify-between mb-3">
<div>
<span className="text-xl lg:text-2xl font-bold leading-none">{formatStorage(usedGB)}</span>
<span className="text-sm font-medium ml-1 text-muted-foreground">used</span>
</div>
<Badge variant="outline" className="bg-muted text-muted-foreground border-border">{Math.round(utilPct)}% util</Badge>
</div>
<div className="flex h-1.5 rounded-full overflow-hidden gap-[2px]">
<div style={{ width: `${usedSeg}%`, background: stroke }} title={`Used ${formatStorage(usedGB)}`}></div>
<div style={{ flex: 1, background: 'rgba(168,85,247,0.45)' }} title={`Idle ${formatStorage(idleGB)}`}></div>
</div>
<div className="mt-2 flex justify-between text-sm text-muted-foreground">
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full" style={{ background: stroke }}></span>Used {formatStorage(usedGB)}</span>
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full" style={{ background: 'rgba(168,85,247,0.55)' }}></span>Alloc {formatStorage(allocGB)}</span>
</div>
</CardContent>
</Card>
)
})()}
</div>
<Card className="bg-card border-border">
@@ -1465,11 +1505,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
onClick={() => handleVMClick(vm)}
>
<div className="flex items-center gap-2 flex-wrap mb-3">
<Badge variant="outline" className={`text-xs flex-shrink-0 ${getStatusColor(vm.status)}`}>
<Badge variant="outline" className={`flex-shrink-0 ${getStatusColor(vm.status)}`}>
{getStatusIcon(vm.status)}
{vm.status.toUpperCase()}
</Badge>
<Badge variant="outline" className={`text-xs flex-shrink-0 ${typeBadge.color}`}>
<Badge variant="outline" className={`flex-shrink-0 ${typeBadge.color}`}>
{typeBadge.icon}
{typeBadge.label}
</Badge>
@@ -2835,7 +2875,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
{backup.storage}
</Badge>
</div>
<Badge variant="outline" className="text-xs font-mono ml-2 flex-shrink-0">
<Badge variant="outline" className="font-mono ml-2 flex-shrink-0">
{backup.size_human}
</Badge>
</div>