Feature: Disk Usage via df im Dashboard (LXC-kompatibel)

- get_disk_usage() in system_info.py via /usr/bin/df -P
- GET /api/system/disk-usage Endpoint
- getDiskUsage() im API-Client
- Dashboard zeigt Disk Usage Karten mit Balken + Total/Used/Free
  (sichtbar auf LXC wo /proc/diskstats keine Blockgeräte liefert)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 15:34:18 +02:00
parent 3bc57ef36b
commit c4454a675a
4 changed files with 101 additions and 2 deletions
+10 -1
View File
@@ -168,7 +168,16 @@ async def get_network_traffic():
return result
# ============== DISK I/O ==============
# ============== DISK USAGE + I/O ==============
@router.get("/disk-usage")
async def get_disk_usage():
"""Get filesystem disk usage from df (public)"""
result = system_info.get_disk_usage()
if "error" in result:
raise HTTPException(status_code=500, detail=result["error"])
return result
@router.get("/diskio")
async def get_diskio():
+36
View File
@@ -429,6 +429,42 @@ def get_all_units() -> Dict[str, Any]:
return {"error": str(e)}
def get_disk_usage() -> Dict[str, Any]:
try:
result = subprocess.run(
["/usr/bin/df", "-P", "-x", "tmpfs", "-x", "devtmpfs", "-x", "squashfs", "-x", "overlay"],
capture_output=True, text=True, timeout=5
)
if result.returncode != 0:
return {"error": result.stderr}
filesystems = []
for line in result.stdout.strip().split("\n")[1:]:
parts = line.split()
if len(parts) < 6:
continue
try:
total = int(parts[1]) * 1024
used = int(parts[2]) * 1024
available = int(parts[3]) * 1024
capacity = int(parts[4].rstrip("%"))
filesystems.append({
"filesystem": parts[0],
"mountpoint": parts[5],
"total": total,
"used": used,
"available": available,
"capacity": capacity,
})
except (ValueError, IndexError):
continue
return {"filesystems": filesystems}
except Exception as e:
logger.error(f"Error getting disk usage: {e}")
return {"error": str(e)}
def get_journal_logs(limit: int = 20) -> Dict[str, Any]:
try:
result = subprocess.run(
+50 -1
View File
@@ -24,6 +24,7 @@ export default function Dashboard() {
const [networkInfo, setNetworkInfo] = useState<any>(null)
const [networkTraffic, setNetworkTraffic] = useState<any>(null)
const [diskIO, setDiskIO] = useState<any>(null)
const [diskUsage, setDiskUsage] = useState<any>(null)
// History buffers for sparklines (rolling window of 30 points, ~2.5 minutes at 5s intervals)
const cpuHistoryRef = useRef<number[]>([])
@@ -100,7 +101,7 @@ export default function Dashboard() {
const loadSystemStats = async () => {
try {
const [sysInfo, memInfo, cpuData, uptime, network, traffic, diskio] = await Promise.all([
const [sysInfo, memInfo, cpuData, uptime, network, traffic, diskio, diskusage] = await Promise.all([
api.getSystemInfo().catch(() => null),
api.getMemory().catch(() => null),
api.getCpuInfo().catch(() => null),
@@ -108,6 +109,7 @@ export default function Dashboard() {
api.getNetwork().catch(() => null),
api.getNetworkTraffic().catch(() => null),
api.getDiskIO().catch(() => null),
api.getDiskUsage().catch(() => null),
])
setSystemInfo(sysInfo)
setMemoryInfo(memInfo)
@@ -116,6 +118,7 @@ export default function Dashboard() {
setNetworkInfo(network)
setNetworkTraffic(traffic)
setDiskIO(diskio)
setDiskUsage(diskusage)
// Add to history
if (cpuData?.percent !== undefined) {
@@ -524,6 +527,52 @@ export default function Dashboard() {
</div>
)}
{/* Disk Usage (df-based, immer sichtbar wenn Daten vorhanden) */}
{diskUsage?.filesystems && diskUsage.filesystems.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Disk Usage</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{diskUsage.filesystems.map((fs: any) => {
const pct = fs.capacity
const barColor = pct > 85 ? "bg-red-500" : pct > 70 ? "bg-yellow-500" : "bg-blue-500"
return (
<Card key={fs.mountpoint}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<HardDrive className="w-4 h-4" />
{fs.mountpoint}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div className={`h-full ${barColor} rounded-full`} style={{ width: `${pct}%` }} />
</div>
<span className="text-xs text-muted-foreground w-9 text-right">{pct}%</span>
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<div>
<p className="text-xs text-muted-foreground">Total</p>
<p className="font-medium">{formatBytes(fs.total)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Used</p>
<p className="font-medium">{formatBytes(fs.used)}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Free</p>
<p className="font-medium">{formatBytes(fs.available)}</p>
</div>
</div>
<p className="text-xs text-muted-foreground truncate">{fs.filesystem}</p>
</CardContent>
</Card>
)
})}
</div>
</div>
)}
{/* Disk I/O */}
{diskIO?.disks && diskIO.disks.length > 0 && (
<div className="mb-8">
+5
View File
@@ -426,6 +426,11 @@ export class ZFSManagerAPI {
return response.data
}
async getDiskUsage(): Promise<{ filesystems: { filesystem: string; mountpoint: string; total: number; used: number; available: number; capacity: number }[] }> {
const response = await this.client.get("/api/system/disk-usage")
return response.data
}
async getServices(): Promise<{ services: any[] }> {
const response = await this.client.get("/api/system/services")
return response.data