2026-06-17 05:19:07 +02:00
|
|
|
import time
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
from pathlib import Path
|
2026-06-17 05:19:07 +02:00
|
|
|
|
|
|
|
|
from fastapi import FastAPI, Request
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
from fastapi.staticfiles import StaticFiles
|
2026-06-17 05:19:07 +02:00
|
|
|
|
|
|
|
|
from app.metrics import metrics
|
2026-06-17 01:48:56 +02:00
|
|
|
from app.api.health import router as health_router
|
|
|
|
|
from app.api.chat import router as chat_router
|
|
|
|
|
from app.api.transcribe import router as transcribe_router
|
|
|
|
|
from app.api.speak import router as speak_router
|
|
|
|
|
from app.api.devices import router as devices_router
|
|
|
|
|
from app.api.sessions import router as sessions_router
|
|
|
|
|
from app.api.config import router as config_router
|
2026-06-17 02:14:25 +02:00
|
|
|
from app.api.admin import router as admin_router
|
|
|
|
|
from app.api.me import router as me_router
|
2026-06-17 05:19:07 +02:00
|
|
|
from app.api.metrics import router as metrics_router
|
2026-06-17 04:26:55 +02:00
|
|
|
from app.api.ws import router as ws_router
|
2026-06-17 01:48:56 +02:00
|
|
|
|
|
|
|
|
app = FastAPI(title="Voice Assistant Gateway")
|
2026-06-17 05:19:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.middleware("http")
|
|
|
|
|
async def record_metrics(request: Request, call_next):
|
|
|
|
|
start = time.perf_counter()
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
duration = time.perf_counter() - start
|
|
|
|
|
# Route-Template (z. B. /api/sessions/{session_id}/route) statt konkreter URL,
|
|
|
|
|
# um die Label-Kardinalitaet niedrig zu halten.
|
|
|
|
|
route = request.scope.get("route")
|
|
|
|
|
path = getattr(route, "path", request.url.path)
|
|
|
|
|
labels = {"method": request.method, "path": path}
|
|
|
|
|
metrics.inc("http_requests_total", {**labels, "status": response.status_code})
|
|
|
|
|
metrics.observe("http_request_duration_seconds", duration, labels)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 01:48:56 +02:00
|
|
|
app.include_router(health_router)
|
|
|
|
|
app.include_router(chat_router, prefix="/api")
|
|
|
|
|
app.include_router(transcribe_router, prefix="/api")
|
|
|
|
|
app.include_router(speak_router, prefix="/api")
|
|
|
|
|
app.include_router(devices_router, prefix="/api")
|
|
|
|
|
app.include_router(sessions_router, prefix="/api")
|
|
|
|
|
app.include_router(config_router, prefix="/api")
|
2026-06-17 02:14:25 +02:00
|
|
|
app.include_router(admin_router, prefix="/api")
|
|
|
|
|
app.include_router(me_router, prefix="/api")
|
2026-06-17 05:19:07 +02:00
|
|
|
app.include_router(metrics_router, prefix="/api")
|
2026-06-17 04:26:55 +02:00
|
|
|
app.include_router(ws_router)
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
|
|
|
|
|
# Statische Web-UI (same-origin -> kein CORS). Muss NACH allen API-/WS-Routen
|
|
|
|
|
# gemountet werden, damit "/" nur die uebrigen Pfade abfaengt.
|
|
|
|
|
_WEB_DIR = Path(__file__).resolve().parent / "web"
|
|
|
|
|
if _WEB_DIR.is_dir():
|
|
|
|
|
app.mount("/", StaticFiles(directory=str(_WEB_DIR), html=True), name="web")
|