Files
zmb-webui/backend/routers/pages.py
T
patrick 56b1ab9970 Refactor: Tab-Switching von JavaScript auf HTMX + Jinja2 umgestellt
Shares- und Identities-Seite nutzen jetzt ?tab= Query-Parameter statt
clientseitigem JS. Der Server steuert aktiven Tab via Jinja2, kein
<script>-Block mehr nötig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 19:21:29 +02:00

298 lines
11 KiB
Python

"""
HTML Page routes (Jinja2 + HTMX)
Replaces Next.js frontend entirely.
"""
import os
from fastapi import APIRouter, Request, Response, Form, Cookie
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from services import auth as auth_service
from services import system_info, zfs_runner
from services.shares import share_manager
from services.identities import list_users, list_groups
from services.file_manager import FileManager
router = APIRouter()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
_file_manager = FileManager()
def _require_user(token: str | None) -> str | None:
if not token:
return None
return auth_service.verify_token(token)
def _redirect_login():
return RedirectResponse("/login", status_code=303)
def _ctx(**kwargs):
return kwargs or {}
# ── Auth ──────────────────────────────────────────────────────────────────────
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return templates.TemplateResponse(request, "login.html")
@router.post("/login")
async def login_submit(
request: Request,
username: str = Form(...),
password: str = Form(...),
):
if not auth_service.authenticate_user(username, password):
return templates.TemplateResponse(
request, "login.html",
{"error": "Ungültiger Benutzername oder Passwort"},
status_code=401,
)
token = auth_service.create_access_token(username)
resp = RedirectResponse("/", status_code=303)
resp.set_cookie("token", token, httponly=True, samesite="lax", max_age=86400)
return resp
@router.post("/logout")
async def logout():
resp = RedirectResponse("/login", status_code=303)
resp.delete_cookie("token")
return resp
# ── Pages ─────────────────────────────────────────────────────────────────────
@router.get("/", response_class=HTMLResponse)
async def dashboard(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return _redirect_login()
hostname = system_info.get_hostname().get("hostname", "")
return templates.TemplateResponse(request, "dashboard.html", {
"active": "dashboard", "hostname": hostname,
})
@router.get("/zfs", response_class=HTMLResponse)
async def zfs_page(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return _redirect_login()
pools_raw = zfs_runner.list_pools()
pools = []
for p in pools_raw:
datasets = zfs_runner.list_datasets(p["name"])
vdevs = zfs_runner.get_pool_status(p["name"]).get("vdevs", [])
pools.append({**p, "datasets": datasets, "vdevs": vdevs})
return templates.TemplateResponse(request, "zfs.html", {
"active": "zfs", "pools": pools,
})
@router.get("/snapshots", response_class=HTMLResponse)
async def snapshots_page(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return _redirect_login()
snapshots = zfs_runner.list_snapshots()
return templates.TemplateResponse(request, "snapshots.html", {
"active": "snapshots", "snapshots": snapshots,
})
@router.get("/shares", response_class=HTMLResponse)
async def shares_page(request: Request, token: str | None = Cookie(default=None), tab: str = "samba"):
if not _require_user(token):
return _redirect_login()
if tab not in ("samba", "nfs", "config"):
tab = "samba"
return templates.TemplateResponse(request, "shares.html", {"active": "shares", "tab": tab})
@router.get("/navigator", response_class=HTMLResponse)
async def navigator_page(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return _redirect_login()
return templates.TemplateResponse(request, "navigator.html", {"active": "navigator"})
@router.get("/identities", response_class=HTMLResponse)
async def identities_page(request: Request, token: str | None = Cookie(default=None), tab: str = "users"):
if not _require_user(token):
return _redirect_login()
if tab not in ("users", "groups"):
tab = "users"
return templates.TemplateResponse(request, "identities.html", {"active": "identities", "tab": tab})
@router.get("/logs", response_class=HTMLResponse)
async def logs_page(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return _redirect_login()
return templates.TemplateResponse(request, "logs.html", {"active": "logs"})
@router.get("/services", response_class=HTMLResponse)
async def services_page(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return _redirect_login()
return templates.TemplateResponse(request, "services.html", {"active": "services"})
# ── HTMX Fragments ────────────────────────────────────────────────────────────
@router.get("/fragments/quick-stats", response_class=HTMLResponse)
async def frag_quick_stats(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
return templates.TemplateResponse(request, "fragments/quick_stats.html", {
"cpu": system_info.get_cpu_info(),
"mem": system_info.get_memory(),
"uptime": system_info.get_uptime(),
"pools": zfs_runner.list_pools(),
})
@router.get("/fragments/pools", response_class=HTMLResponse)
async def frag_pools(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
return templates.TemplateResponse(request, "fragments/pools.html", {
"pools": zfs_runner.list_pools(),
})
@router.get("/fragments/disk-usage", response_class=HTMLResponse)
async def frag_disk_usage(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
data = system_info.get_disk_usage()
return templates.TemplateResponse(request, "fragments/disk_usage.html", {
"filesystems": data.get("filesystems", []),
})
@router.get("/fragments/net-traffic", response_class=HTMLResponse)
async def frag_net_traffic(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
data = system_info.get_network_traffic()
return templates.TemplateResponse(request, "fragments/net_traffic.html", {
"interfaces": data.get("interfaces", []),
})
@router.get("/fragments/samba-shares", response_class=HTMLResponse)
async def frag_samba_shares(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
return templates.TemplateResponse(request, "fragments/samba_shares.html", {
"shares": share_manager.list_samba_shares(),
})
@router.get("/fragments/nfs-shares", response_class=HTMLResponse)
async def frag_nfs_shares(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
return templates.TemplateResponse(request, "fragments/nfs_shares.html", {
"exports": share_manager.list_nfs_shares(),
})
@router.get("/fragments/samba-config", response_class=HTMLResponse)
async def frag_samba_config(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
data = share_manager.get_samba_global_config()
return templates.TemplateResponse(request, "fragments/samba_config.html", {
"params": data.get("parameters", []),
})
@router.post("/fragments/samba-config/toggle-macos", response_class=HTMLResponse)
async def frag_toggle_macos(
request: Request,
enable: str = "true",
token: str | None = Cookie(default=None),
):
if not _require_user(token):
return HTMLResponse("", status_code=401)
current_data = share_manager.get_samba_global_config()
params = {p["key"]: p["value"] for p in current_data.get("parameters", [])}
fruit = {"fruit:encoding": "native", "fruit:metadata": "stream",
"fruit:zero_file_id": "yes", "fruit:nfs_aces": "no"}
if enable == "true":
params.update(fruit)
else:
for k in fruit:
params.pop(k, None)
share_manager.set_samba_global_config(params)
updated = share_manager.get_samba_global_config()
return templates.TemplateResponse(request, "fragments/samba_config.html", {
"params": updated.get("parameters", []),
})
@router.get("/fragments/users", response_class=HTMLResponse)
async def frag_users(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
return templates.TemplateResponse(request, "fragments/users.html", {
"users": list_users(),
})
@router.get("/fragments/groups", response_class=HTMLResponse)
async def frag_groups(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
return templates.TemplateResponse(request, "fragments/groups.html", {
"groups": list_groups(),
})
@router.get("/fragments/logs", response_class=HTMLResponse)
async def frag_logs(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
data = system_info.get_journal_logs(50)
return templates.TemplateResponse(request, "fragments/logs.html", {
"logs": data.get("logs", []),
})
@router.get("/fragments/services", response_class=HTMLResponse)
async def frag_services(request: Request, token: str | None = Cookie(default=None)):
if not _require_user(token):
return HTMLResponse("", status_code=401)
data = system_info.get_services()
return templates.TemplateResponse(request, "fragments/services.html", {
"services": data.get("services", []),
})
@router.get("/fragments/navigator", response_class=HTMLResponse)
async def frag_navigator(
request: Request,
path: str = "/tank/share",
token: str | None = Cookie(default=None),
):
if not _require_user(token):
return HTMLResponse("", status_code=401)
try:
items = _file_manager.list_directory(path)
parent = str(os.path.dirname(path)) if path != "/" else None
return templates.TemplateResponse(request, "fragments/navigator.html", {
"items": items, "current_path": path, "parent_path": parent, "error": None,
})
except Exception as e:
return templates.TemplateResponse(request, "fragments/navigator.html", {
"items": [], "current_path": path, "parent_path": None, "error": str(e),
})