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>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { HardDrive, Menu, LogOut } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { api } from "@/lib/api"
|
||||
|
||||
export function Header() {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [zfsAvailable, setZfsAvailable] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkZfsAvailability = async () => {
|
||||
const status = await api.getSystemStatus()
|
||||
setZfsAvailable(status.zfs_available)
|
||||
}
|
||||
checkZfsAvailability()
|
||||
}, [])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await api.logout()
|
||||
router.push("/login")
|
||||
}
|
||||
|
||||
const isActive = (path: string) => pathname === path
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-2 font-bold text-lg">
|
||||
<HardDrive className="w-6 h-6" />
|
||||
<span>ZMB Webui</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
<Link
|
||||
href="/"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
{zfsAvailable && (
|
||||
<>
|
||||
<Link
|
||||
href="/snapshots"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/snapshots")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Snapshots
|
||||
</Link>
|
||||
<Link
|
||||
href="/datasets"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/datasets")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Datasets
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href="/navigator"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/navigator")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Navigator
|
||||
</Link>
|
||||
<Link
|
||||
href="/shares"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/shares")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Shares
|
||||
</Link>
|
||||
<Link
|
||||
href="/file-sharing"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/file-sharing")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
File Sharing
|
||||
</Link>
|
||||
<Link
|
||||
href="/identities"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/identities")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Identities
|
||||
</Link>
|
||||
<Link
|
||||
href="/logs"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/logs")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Logs
|
||||
</Link>
|
||||
<Link
|
||||
href="/services"
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive("/services")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Services
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Logout Button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleLogout}>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
</Button>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="md:hidden p-2"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{isMenuOpen && (
|
||||
<nav className="md:hidden pb-4 space-y-1">
|
||||
<Link
|
||||
href="/"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
{zfsAvailable && (
|
||||
<>
|
||||
<Link
|
||||
href="/snapshots"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/snapshots")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Snapshots
|
||||
</Link>
|
||||
<Link
|
||||
href="/datasets"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/datasets")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Datasets
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href="/files"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/files")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Files
|
||||
</Link>
|
||||
<Link
|
||||
href="/shares"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/shares")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Shares
|
||||
</Link>
|
||||
<Link
|
||||
href="/file-sharing"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/file-sharing")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
File Sharing
|
||||
</Link>
|
||||
<Link
|
||||
href="/identities"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/identities")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Identities
|
||||
</Link>
|
||||
<Link
|
||||
href="/logs"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/logs")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Logs
|
||||
</Link>
|
||||
<Link
|
||||
href="/services"
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
isActive("/services")
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Services
|
||||
</Link>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user