6d74d874b6
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>
98 lines
2.9 KiB
TypeScript
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>
|
|
)
|
|
}
|