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
<!doctype html>
2026-06-18 10:20:24 +02:00
< html lang = "de" class = "h-full" >
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
< head >
< meta charset = "utf-8" / >
2026-06-18 10:20:24 +02:00
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" / >
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
< title > Voice Assistant< / title >
2026-06-18 10:20:24 +02:00
< script src = "https://cdn.tailwindcss.com" > < / script >
< script > tailwind . config = { darkMode : "class" } ; < / script >
< script >
(function () {
try {
var t = localStorage.getItem("theme");
if (t === "dark" || (!t & & matchMedia("(prefers-color-scheme: dark)").matches))
document.documentElement.classList.add("dark");
} catch (e) {}
})();
< / script >
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
< / head >
2026-06-18 10:20:24 +02:00
< body class = "h-[100dvh] flex flex-col bg-slate-50 text-slate-800 dark:bg-slate-900 dark:text-slate-100" >
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
2026-06-19 01:04:20 +02:00
<!-- ═══════════════════ ADMIN PANEL OVERLAY ═══════════════════ -->
< div id = "admin-panel" class = "hidden fixed inset-0 z-50 flex flex-col bg-slate-50 dark:bg-slate-900" >
<!-- Admin Header -->
< header class = "shrink-0 h-14 flex items-center gap-3 px-4 border-b border-slate-200 dark:border-slate-700 bg-white/90 dark:bg-slate-800/90 backdrop-blur" >
< span class = "text-lg" > ⚙️< / span >
< h2 class = "flex-1 font-semibold text-base" > Admin-Bereich< / h2 >
< button id = "admin-close" title = "Schließen"
class="h-9 w-9 grid place-items-center rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 text-xl leading-none transition-colors">✕< / button >
< / header >
<!-- Tab Navigation -->
< nav class = "shrink-0 flex border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-x-auto" >
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400" data-tab = "users" > 👥 Nutzer< / button >
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab = "chat" > 💬 Gespräche< / button >
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab = "emergency" > 🚨 Notfälle< / button >
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab = "status" > 📡 Status< / button >
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab = "metrics" > 📊 Metriken< / button >
2026-06-19 01:15:28 +02:00
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab = "words" > 🔤 Wörterbuch< / button >
< button class = "admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab = "log" > 📋 Log< / button >
2026-06-19 01:04:20 +02:00
< / nav >
<!-- Tab Content Container -->
< div class = "flex-1 overflow-hidden" >
<!-- ── Tab: Nutzer ── -->
< div id = "admin-tab-users" class = "h-full overflow-y-auto p-4" >
< div class = "max-w-3xl mx-auto space-y-4" >
<!-- Nutzer anlegen -->
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "text-sm font-medium text-slate-700 dark:text-slate-300 mb-3" > Nutzer anlegen< / h3 >
< form id = "create-user-form" class = "flex gap-2" >
< input id = "new-user-name" type = "text" placeholder = "Anzeigename" required
class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
< button type = "submit"
class="rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 font-medium transition-colors">Anlegen< / button >
< / form >
< div id = "new-token-box" class = "hidden mt-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 p-3" >
< p class = "text-xs text-emerald-700 dark:text-emerald-400 mb-1 font-medium" > Token (nur einmal sichtbar — jetzt kopieren!):< / p >
< code id = "new-token-value" class = "text-xs font-mono break-all text-emerald-800 dark:text-emerald-200 select-all" > < / code >
< / div >
< / div >
<!-- Nutzerliste -->
< div id = "admin-user-cards" class = "space-y-3" >
< p class = "text-sm text-slate-400 text-center py-4" > lade …< / p >
< / div >
< / div >
< / div >
<!-- ── Tab: Gespräche ── -->
< div id = "admin-tab-chat" class = "hidden h-full flex overflow-hidden" >
<!-- Linke Spalte: Nutzerliste -->
< div class = "w-48 md:w-60 shrink-0 border-r border-slate-200 dark:border-slate-700 flex flex-col" >
< div class = "px-3 py-2.5 border-b border-slate-200 dark:border-slate-700 bg-slate-100/60 dark:bg-slate-800/60" >
< p class = "text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide" > Nutzer< / p >
< / div >
< ul id = "chat-user-list" class = "flex-1 overflow-y-auto divide-y divide-slate-100 dark:divide-slate-800/60" > < / ul >
< / div >
<!-- Rechte Spalte: Sessions & Transkript -->
< div id = "chat-right-panel" class = "flex-1 overflow-y-auto" >
< p class = "p-6 text-sm text-slate-400" > ← Nutzer auswählen< / p >
< / div >
< / div >
<!-- ── Tab: Notfälle ── -->
< div id = "admin-tab-emergency" class = "hidden h-full overflow-y-auto p-4" >
< div class = "max-w-4xl mx-auto" >
< div class = "flex items-center justify-between mb-4" >
< h3 class = "font-semibold text-base" > Notfall-Ereignisse< / h3 >
< button id = "load-emergency"
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren< / button >
< / div >
< div id = "emergency-content" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
<!-- ── Tab: Status ── -->
< div id = "admin-tab-status" class = "hidden h-full overflow-y-auto p-4" >
< div class = "max-w-3xl mx-auto" >
< div class = "flex items-center justify-between mb-4" >
< h3 class = "font-semibold text-base" > System-Status< / h3 >
< button id = "load-status"
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren< / button >
< / div >
< div id = "status-content" class = "space-y-4" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
<!-- ── Tab: Metriken ── -->
< div id = "admin-tab-metrics" class = "hidden h-full overflow-y-auto p-4" >
< div class = "max-w-3xl mx-auto" >
< div class = "flex items-center justify-between mb-4" >
< h3 class = "font-semibold text-base" > Nutzungsstatistik< / h3 >
< button id = "load-metrics"
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren< / button >
< / div >
< div id = "metrics-content" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
2026-06-19 01:15:28 +02:00
<!-- ── Tab: Wörterbuch ── -->
< div id = "admin-tab-words" class = "hidden h-full overflow-y-auto p-4" >
< div class = "max-w-3xl mx-auto" >
< div class = "flex items-center justify-between mb-4 flex-wrap gap-2" >
< h3 class = "font-semibold text-base" > Aussprache-Lexikon< / h3 >
< div class = "flex items-center gap-2" >
< select id = "words-lang"
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
< option value = "de" > Deutsch (de)< / option >
< option value = "en" > Englisch (en)< / option >
< / select >
< button id = "load-words"
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren< / button >
< / div >
< / div >
< p class = "text-xs text-slate-400 mb-4" > Wirkt nur bei lokalem TTS (Piper, Stufe „full"). < strong > Abkürzungen< / strong > ersetzen ganze Token. < strong > Einheiten< / strong > wirken nur direkt nach einer Zahl. < strong > Begriffe< / strong > für Eigennamen, Fachwörter, Aussprache-Korrekturen.< / p >
< div id = "words-content" class = "space-y-6" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
<!-- ── Tab: Log ── -->
< div id = "admin-tab-log" class = "hidden h-full flex flex-col" >
< div class = "shrink-0 flex items-center gap-3 px-4 py-2.5 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/60" >
< button id = "log-connect"
class="rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-xs px-3 py-1.5 font-medium transition-colors">▶ Verbinden< / button >
< button id = "log-disconnect" disabled
class="rounded-lg border border-slate-300 dark:border-slate-600 text-xs px-3 py-1.5 opacity-40 transition-colors">■ Trennen< / button >
< span id = "log-status" class = "text-xs text-slate-400" > getrennt< / span >
< button id = "log-clear" class = "ml-auto text-xs text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors" > Leeren< / button >
< / div >
< div id = "log-output"
class="flex-1 overflow-y-auto bg-slate-950 text-emerald-300 font-mono text-xs p-4 leading-relaxed whitespace-pre-wrap">
< span class = "text-slate-500" > — Verbinden um Log zu starten —< / span >
< / div >
< / div >
2026-06-19 01:04:20 +02:00
< / div > <!-- /Tab Content Container -->
< / div > <!-- /Admin Panel -->
<!-- ═══════════════════ CHAT INTERFACE ═══════════════════ -->
2026-06-18 10:20:24 +02:00
< header class = "shrink-0 h-14 flex items-center gap-3 px-4 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 backdrop-blur" >
< h1 class = "flex-1 truncate font-semibold text-base" > Voice Assistant< / h1 >
< select id = "tts" title = "Stimme"
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
< option value = "" > Schnell< / option >
< option value = "chatterbox" > Hohe Qualität< / option >
< option value = "openrouter" > Cloud< / option >
< / select >
2026-06-19 01:04:20 +02:00
< button id = "admin-toggle" title = "Admin"
class="hidden h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">⚙️< / button >
2026-06-18 10:20:24 +02:00
< button id = "theme-toggle" title = "Tag/Nacht"
2026-06-19 01:04:20 +02:00
class="h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">🌙< / button >
2026-06-18 10:20:24 +02:00
< / header >
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
2026-06-18 10:20:24 +02:00
< div class = "shrink-0 flex items-center justify-between px-4 py-1 text-xs text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-700" >
< span id = "identity" > lade …< / span >
< a id = "logout" class = "hidden hover:underline" href = "#" > Abmelden< / a >
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
< / div >
2026-06-18 10:20:24 +02:00
< main id = "messages" class = "flex-1 overflow-y-auto px-4 py-4 space-y-3 w-full max-w-3xl mx-auto" > < / main >
< footer class = "shrink-0 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-3 pt-3 pb-[calc(env(safe-area-inset-bottom)+0.6rem)]" >
< form id = "prompt-form" class = "flex items-center gap-2 w-full max-w-3xl mx-auto" >
< button type = "button" id = "mic" title = "Mikrofon"
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤< / button >
< input id = "prompt" type = "text" autocomplete = "off" placeholder = "Nachricht …"
class="flex-1 rounded-xl border border-slate-300 dark:border-slate-600 bg-transparent px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500" />
< button type = "submit" id = "send"
2026-06-19 01:04:20 +02:00
class="shrink-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white px-4 py-2.5 font-medium transition-colors">Senden< / button >
2026-06-18 10:20:24 +02:00
< / form >
< div id = "status" class = "w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400" > < / div >
< / footer >
2026-06-19 01:15:28 +02:00
< script src = "/app.js?v=9" > < / script >
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
< / body >
< / html >