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>
This commit is contained in:
Dieter Schlüter 2026-06-18 04:37:16 +02:00
commit 76df695111
16 changed files with 703 additions and 29 deletions

36
app/web/index.html Normal file
View file

@ -0,0 +1,36 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voice Assistant</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<div id="layout">
<aside id="menu">
<h2>Voice Assistant</h2>
<div id="identity" class="muted">lade …</div>
<div id="admin" class="hidden">
<h3>Admin</h3>
<button id="reload-users">Nutzer laden</button>
<ul id="user-list"></ul>
<a href="/api/metrics" target="_blank" rel="noopener">Metriken</a>
</div>
<a id="logout" class="hidden" href="#">Abmelden</a>
</aside>
<main id="chat">
<div id="messages"></div>
<form id="prompt-form">
<button type="button" id="mic" title="Mikrofon" aria-label="Mikrofon">🎤</button>
<input id="prompt" type="text" placeholder="Nachricht eingeben …" autocomplete="off" />
<button type="submit" id="send">Senden</button>
</form>
<div id="status" class="muted"></div>
</main>
</div>
<script src="/app.js"></script>
</body>
</html>