""" 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), })