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:
Claude Code
2026-04-22 00:26:23 +02:00
committed by Patrick
commit 6d74d874b6
104 changed files with 28836 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
"""
Snapshot management endpoints
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from services.zfs_runner import zfs_runner
from services.auth import auth_service
from models import Snapshot
router = APIRouter(prefix="/api/snapshots", tags=["snapshots"])
security = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify JWT token and return username"""
username = auth_service.verify_token(credentials.credentials)
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
return username
class CreateSnapshotRequest(BaseModel):
dataset: str
name: Optional[str] = None # Auto-generate if not provided
class RollbackSnapshotRequest(BaseModel):
snapshot: str
@router.get("/", response_model=List[Snapshot])
async def list_snapshots(
dataset: Optional[str] = None,
limit: int = 50,
current_user: str = Depends(get_current_user)
):
"""
List snapshots (optionally filtered by dataset)
"""
try:
snapshots = zfs_runner.list_snapshots(dataset, limit=limit)
return [
Snapshot(
name=s["name"],
dataset=s["name"].split("@")[0], # Extract dataset part
created=s["creation"],
used=s["used"],
referenced=s["referenced"],
creation_datetime=datetime.fromtimestamp(s["creation"]).isoformat() + "Z"
)
for s in snapshots
]
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", response_model=dict)
async def create_snapshot(
request: CreateSnapshotRequest,
current_user: str = Depends(get_current_user)
):
"""
Create new snapshot
"""
try:
result = zfs_runner.create_snapshot(request.dataset, request.name)
if result.get("status") == "error":
raise HTTPException(status_code=400, detail=result.get("message"))
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{snapshot_name:path}")
async def delete_snapshot(
snapshot_name: str,
current_user: str = Depends(get_current_user)
):
"""
Delete snapshot
"""
try:
result = zfs_runner.destroy_snapshot(snapshot_name)
if result.get("status") == "error":
raise HTTPException(status_code=400, detail=result.get("message"))
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/rollback")
async def rollback_snapshot(
request: RollbackSnapshotRequest,
current_user: str = Depends(get_current_user)
):
"""
Rollback dataset to snapshot (WARNING: Destroys data after snapshot!)
"""
try:
result = zfs_runner.rollback_snapshot(request.snapshot)
if result.get("status") == "error":
raise HTTPException(status_code=400, detail=result.get("message"))
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))