Files
zmb-webui/frontend/components/VdevTree.tsx
T
Claude Code 6d74d874b6 ZMB Webui: Complete Project – Rebrand & Initial Clean Commit
ARCHITECTURE
============
Backend: FastAPI + uvicorn (port 8000)
  - JWT authentication with PAM system users
  - ZFS CLI wrapper with caching (30-60s TTL)
  - WebSocket pool status broadcaster (30s interval)
  - Services: auth, zfs_runner, file_manager, shares, identities, system_info
  - Routers: pools, datasets, snapshots, shares, identities, navigator, system

Frontend: Next.js 15 + TypeScript (static export)
  - Incremental Static Regeneration (ISR) for weak hardware
  - Type-safe API client (lib/api.ts)
  - Dark mode + custom Tailwind theme
  - Pages: Dashboard, Login, Snapshots, Datasets, Shares, etc.

DEPLOYMENT
==========
Test Target: 192.168.1.179:8090 (Debian LXC)
Production: 10.66.120.3:9090 (Raspberry Pi 4GB ARM64)
Updater: Automated Gitea-based deployment (update-test.sh, update-pi.sh)

FEATURES COMPLETED
==================
Phase 3a: Dashboard Quick Stats (System, CPU, Memory, Storage)
  - Real-time stats with color-coded progress bars
  - Responsive grid layout (mobile: 1, tablet: 2, desktop: 4 columns)
  - ISR-optimized for fast loads on weak hardware

REBRANDING
==========
Renamed throughout:
  - Project: 'ZFS Manager' → 'ZMB Webui'
  - Services: 'zfs-manager' → 'zmb-webui'
  - Systemd units: zfs-manager-backend → zmb-webui-backend
  - Configuration files and documentation

Co-Authored-By: Patrick <patrick@perlbach24.de>
2026-04-22 00:43:05 +02:00

98 lines
2.9 KiB
TypeScript

"use client"
import { Vdev } from "@/lib/api"
function stateColor(state: string) {
switch (state?.toUpperCase()) {
case "ONLINE": return "text-green-500"
case "DEGRADED": return "text-yellow-500"
case "FAULTED":
case "OFFLINE":
case "UNAVAIL": return "text-red-500"
default: return "text-muted-foreground"
}
}
function stateIcon(state: string) {
switch (state?.toUpperCase()) {
case "ONLINE": return "●"
case "DEGRADED": return "◑"
default: return "○"
}
}
interface VdevRowProps {
vdev: Vdev
depth?: number
}
function VdevRow({ vdev, depth = 0 }: VdevRowProps) {
const hasErrors =
vdev.read !== 0 || vdev.write !== 0 || vdev.cksum !== 0
return (
<>
<tr className="border-b border-border/50 last:border-0 hover:bg-muted/30">
<td className="py-2 pr-4">
<span style={{ paddingLeft: `${depth * 20}px` }} className="flex items-center gap-2">
<span className={`text-sm ${stateColor(vdev.state)}`}>{stateIcon(vdev.state)}</span>
<span className={`font-mono text-sm ${depth === 0 ? "font-semibold" : ""}`}>
{vdev.name}
</span>
</span>
</td>
<td className={`py-2 px-3 text-sm font-medium ${stateColor(vdev.state)}`}>
{vdev.state}
</td>
<td className={`py-2 px-3 text-sm font-mono text-center ${hasErrors ? "text-red-500 font-bold" : "text-muted-foreground"}`}>
{vdev.read}
</td>
<td className={`py-2 px-3 text-sm font-mono text-center ${hasErrors ? "text-red-500 font-bold" : "text-muted-foreground"}`}>
{vdev.write}
</td>
<td className={`py-2 px-3 text-sm font-mono text-center ${hasErrors ? "text-red-500 font-bold" : "text-muted-foreground"}`}>
{vdev.cksum}
</td>
</tr>
{vdev.children?.map((child) => (
<VdevRow key={child.name} vdev={child} depth={depth + 1} />
))}
</>
)
}
interface VdevTreeProps {
vdevs: Vdev[]
}
export function VdevTree({ vdevs }: VdevTreeProps) {
if (!vdevs || vdevs.length === 0) {
return (
<p className="text-sm text-muted-foreground py-4">
No VDEV information available.
</p>
)
}
return (
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-border text-xs text-muted-foreground uppercase tracking-wide">
<th className="pb-2 pr-4">Name</th>
<th className="pb-2 px-3">State</th>
<th className="pb-2 px-3 text-center">Read</th>
<th className="pb-2 px-3 text-center">Write</th>
<th className="pb-2 px-3 text-center">CkSum</th>
</tr>
</thead>
<tbody>
{vdevs.map((vdev) => (
<VdevRow key={vdev.name} vdev={vdev} depth={0} />
))}
</tbody>
</table>
</div>
)
}