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:
@@ -168,7 +168,16 @@ async def get_network_traffic():
|
|||||||
return result
|
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")
|
@router.get("/diskio")
|
||||||
async def get_diskio():
|
async def get_diskio():
|
||||||
|
|||||||
@@ -429,6 +429,42 @@ def get_all_units() -> Dict[str, Any]:
|
|||||||
return {"error": str(e)}
|
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]:
|
def get_journal_logs(limit: int = 20) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
+50
-1
@@ -24,6 +24,7 @@ export default function Dashboard() {
|
|||||||
const [networkInfo, setNetworkInfo] = useState<any>(null)
|
const [networkInfo, setNetworkInfo] = useState<any>(null)
|
||||||
const [networkTraffic, setNetworkTraffic] = useState<any>(null)
|
const [networkTraffic, setNetworkTraffic] = useState<any>(null)
|
||||||
const [diskIO, setDiskIO] = 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)
|
// History buffers for sparklines (rolling window of 30 points, ~2.5 minutes at 5s intervals)
|
||||||
const cpuHistoryRef = useRef<number[]>([])
|
const cpuHistoryRef = useRef<number[]>([])
|
||||||
@@ -100,7 +101,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const loadSystemStats = async () => {
|
const loadSystemStats = async () => {
|
||||||
try {
|
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.getSystemInfo().catch(() => null),
|
||||||
api.getMemory().catch(() => null),
|
api.getMemory().catch(() => null),
|
||||||
api.getCpuInfo().catch(() => null),
|
api.getCpuInfo().catch(() => null),
|
||||||
@@ -108,6 +109,7 @@ export default function Dashboard() {
|
|||||||
api.getNetwork().catch(() => null),
|
api.getNetwork().catch(() => null),
|
||||||
api.getNetworkTraffic().catch(() => null),
|
api.getNetworkTraffic().catch(() => null),
|
||||||
api.getDiskIO().catch(() => null),
|
api.getDiskIO().catch(() => null),
|
||||||
|
api.getDiskUsage().catch(() => null),
|
||||||
])
|
])
|
||||||
setSystemInfo(sysInfo)
|
setSystemInfo(sysInfo)
|
||||||
setMemoryInfo(memInfo)
|
setMemoryInfo(memInfo)
|
||||||
@@ -116,6 +118,7 @@ export default function Dashboard() {
|
|||||||
setNetworkInfo(network)
|
setNetworkInfo(network)
|
||||||
setNetworkTraffic(traffic)
|
setNetworkTraffic(traffic)
|
||||||
setDiskIO(diskio)
|
setDiskIO(diskio)
|
||||||
|
setDiskUsage(diskusage)
|
||||||
|
|
||||||
// Add to history
|
// Add to history
|
||||||
if (cpuData?.percent !== undefined) {
|
if (cpuData?.percent !== undefined) {
|
||||||
@@ -524,6 +527,52 @@ export default function Dashboard() {
|
|||||||
</div>
|
</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 */}
|
{/* Disk I/O */}
|
||||||
{diskIO?.disks && diskIO.disks.length > 0 && (
|
{diskIO?.disks && diskIO.disks.length > 0 && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
@@ -426,6 +426,11 @@ export class ZFSManagerAPI {
|
|||||||
return response.data
|
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[] }> {
|
async getServices(): Promise<{ services: any[] }> {
|
||||||
const response = await this.client.get("/api/system/services")
|
const response = await this.client.get("/api/system/services")
|
||||||
return response.data
|
return response.data
|
||||||
|
|||||||
Reference in New Issue
Block a user