from __future__ import annotations import re from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi.responses import HTMLResponse from sqlalchemy import func, select from sqlalchemy.orm import Session from app.db import get_db from app.deps import tenant_or_404 from app.models import AuditLog, Deployment, Secret, Setting, Tenant from app.services import deploy, guard, onyva_login, preview, store from app.services.importer import DEFAULT_SETTINGS from app.templating import templates router = APIRouter() def _logo(db, t: Tenant, source="current") -> str | None: _, files = store.content(db, t, source) for name in ("logo.svg", "logo.png"): if name in files: return f"/api/tenants/{t.id}/files/{name}?source={source}" return None @router.get("/", response_class=HTMLResponse) def dashboard(request: Request, db: Session = Depends(get_db)): rows = [] for t in db.scalars(select(Tenant).order_by(Tenant.id)): cfg, files = store.content(db, t, "current") missing = db.scalar(select(func.count()).select_from(Secret).where(Secret.tenant_id == t.id, Secret.value_enc.is_(None))) or 0 rows.append({ "t": t, "cfg": cfg, "accent": (cfg.get("colors") or {}).get("accent", "#327F24"), "logo": _logo(db, t), "host": deploy.host_for(db, t), "outdated": t.deployed_version != t.current_version, "missing_secrets": missing, "stt": (cfg.get("stt") or {}).get("provider", "mistral"), "has_code": "overrides.py" in files, "custom_prompt": "prompt.txt" in files, "custom_pdf": "protocol.html" in files or "protocol.css" in files, }) stats = { "tenants": len(rows), "admin": sum(1 for r in rows if r["t"].source == "admin"), "drafts": sum(1 for r in rows if r["t"].draft), "outdated": sum(1 for r in rows if r["outdated"]), "secrets": sum(1 for r in rows if r["missing_secrets"]), } last = db.scalars(select(Deployment).order_by(Deployment.id.desc()).limit(5)).all() return templates.TemplateResponse(request, "dashboard.html", { "rows": rows, "stats": stats, "last": last, "core": preview.core_version(), "tag": deploy.setting(db, "core_image_tag", "latest"), "nav": "dashboard"}) @router.get("/tenants/{tenant_id}", response_class=HTMLResponse) def tenant_page(request: Request, t: Tenant = Depends(tenant_or_404), db: Session = Depends(get_db)): return templates.TemplateResponse(request, "tenant.html", {"t": t, "nav": "dashboard", "logo": _logo(db, t, "draft")}) @router.get("/tenants/{tenant_id}/bundle.tar.gz") def bundle_download(t: Tenant = Depends(tenant_or_404), db: Session = Depends(get_db), source: str = "current"): data, _ = store.bundle_tar(db, t, source) return Response(data, media_type="application/gzip", headers={"Content-Disposition": f'attachment; filename="{t.id}-v{t.current_version}.tar.gz"'}) @router.get("/preview/{tenant_id}/{kind}") def preview_view(kind: str, source: str = "draft", t: Tenant = Depends(tenant_or_404), db: Session = Depends(get_db)): if kind not in ("web", "pdf"): raise HTTPException(404) try: data = preview.run(db, t, source, "index" if kind == "web" else "pdf") except (preview.PreviewError, preview.SourceGone) as exc: return Response(str(exc), status_code=422, media_type="text/plain; charset=utf-8") if kind == "pdf": return Response(data, media_type="application/pdf", headers={"Content-Disposition": "inline"}) html = data.decode("utf-8") # PNG-Logos referenziert der Core über /brand/logo → auf die Admin-Datei umbiegen html = html.replace('src="/brand/logo"', f'src="/api/tenants/{t.id}/files/logo.png?source={source}"') html = html.replace("", "", 1) return HTMLResponse(html) @router.get("/deployments", response_class=HTMLResponse) def deployments_page(request: Request, db: Session = Depends(get_db)): tenants = db.scalars(select(Tenant).where(Tenant.status == "active").order_by(Tenant.id)).all() return templates.TemplateResponse(request, "deployments.html", { "tenants": tenants, "tag": deploy.setting(db, "core_image_tag", "latest"), "nav": "deployments"}) @router.get("/settings", response_class=HTMLResponse) def settings_page(request: Request, db: Session = Depends(get_db)): values = {k: deploy.setting(db, k, "") for k in DEFAULT_SETTINGS} shared = {s.key: bool(s.value_enc) for s in db.scalars(select(Secret).where(Secret.tenant_id.is_(None)))} from app.services import llm return templates.TemplateResponse(request, "settings.html", { "v": values, "shared": shared, "core": preview.core_version(), "nav": "settings", "llm_ready": llm.configured(db), "mode": {"live": deploy.live_enabled(), "token": deploy.token_present(), "allowed": guard.describe()}, "oidc": {"api_key": onyva_login.key_present(), "writes_allowed": onyva_login.writes_allowed(), "api_base": onyva_login.base_url(db)}}) @router.get("/audit", response_class=HTMLResponse) def audit_page(request: Request, db: Session = Depends(get_db)): rows = db.scalars(select(AuditLog).order_by(AuditLog.id.desc()).limit(200)).all() return templates.TemplateResponse(request, "audit.html", {"rows": rows, "nav": "audit"}) @router.get("/core", response_class=HTMLResponse) def core_page(request: Request): return templates.TemplateResponse(request, "core.html", {"nav": "core"})