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" / >
2026-06-27 07:02:02 +02:00
< title > Alexis< / title >
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< link rel = "icon" type = "image/svg+xml" href = "/favicon.svg?v=1" / >
< link rel = "icon" type = "image/png" sizes = "32x32" href = "/favicon-32.png?v=1" / >
< link rel = "apple-touch-icon" href = "/apple-touch-icon.png?v=1" / >
2026-06-18 10:20:24 +02:00
< script src = "https://cdn.tailwindcss.com" > < / script >
< script > tailwind . config = { darkMode : "class" } ; < / script >
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< style >
/* Dropdown-Popup (< option > ) hat sonst Browser-Default-Weiß -> im Dark-Mode
helle Schrift auf hellem Grund = unlesbar. Farben explizit setzen. */
select option { background-color: #ffffff; color: #1e293b; } /* slate-800 */
.dark select option { background-color: #1e293b; color: #f1f5f9; } /* slate-100 */
< / style >
2026-06-18 10:20:24 +02:00
< 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 >
2026-06-20 23:11:57 +02:00
<!-- Tab Navigation (5 logische Bereiche) -->
2026-06-19 01:04:20 +02:00
< nav class = "shrink-0 flex border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-x-auto" >
2026-06-20 23:11:57 +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 = "overview" > 📊 Übersicht< / 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 = "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 = "system" > 🖥 System< / 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 = "config" > ⚙ Konfiguration< / button >
2026-06-19 01:04:20 +02:00
< / nav >
2026-06-20 23:11:57 +02:00
<!-- Sub - Navigation (dynamisch, nur bei mehreren Abschnitten) -->
< div id = "admin-subnav" class = "hidden shrink-0 flex gap-1.5 px-3 py-2 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/40 overflow-x-auto" > < / div >
2026-06-19 01:04:20 +02:00
<!-- Tab Content Container -->
< div class = "flex-1 overflow-hidden" >
2026-06-20 23:11:57 +02:00
<!-- ── Übersicht (Dashboard) ── -->
< div id = "admin-tab-overview" 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" > Übersicht< / h3 >
< button id = "load-overview"
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 = "overview-content" class = "space-y-4" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
2026-06-19 01:04:20 +02:00
<!-- ── 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" >
2026-06-25 04:33:33 +02:00
< p class = "text-xs text-emerald-700 dark:text-emerald-400 mb-1 font-medium" > Login-Link (nur einmal sichtbar — jetzt kopieren & an den Nutzer geben!):< / p >
2026-06-19 01:04:20 +02:00
< 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" >
2026-06-25 03:02:52 +02:00
< h3 class = "font-semibold text-base" > Notrufe (vom Nutzer ausgelöst)< / h3 >
2026-06-19 01:04:20 +02:00
< 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 >
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< option value = "fr" > Französisch (fr)< / option >
< option value = "es" > Spanisch (es)< / option >
< option value = "it" > Italienisch (it)< / option >
2026-06-28 15:25:45 +02:00
< option value = "nl" > Niederländisch (nl)< / option >
2026-06-24 22:01:38 +02:00
< option value = "pt" > Portugiesisch (pt)< / option >
< option value = "pl" > Polnisch (pl)< / option >
< option value = "ar" > Arabisch (ar)< / option >
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< option value = "ru" > Russisch (ru)< / option >
< option value = "zh" > Chinesisch (zh)< / option >
2026-06-19 01:15:28 +02:00
< / 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 >
2026-06-25 04:52:09 +02:00
<!-- ── Tab: Wartung ── -->
< div id = "admin-tab-maintenance" class = "hidden h-full overflow-y-auto p-4" >
< div class = "max-w-3xl mx-auto" >
< h3 class = "font-semibold text-base mb-4" > Wartung< / h3 >
< div id = "maintenance-content" class = "space-y-4" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
2026-06-19 01:15:28 +02:00
<!-- ── 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:39:36 +02:00
<!-- ── Tab: Einstellungen ── -->
< div id = "admin-tab-settings" class = "hidden h-full overflow-y-auto p-4" >
< div class = "max-w-3xl mx-auto" >
< div class = "flex items-center justify-between mb-2 flex-wrap gap-2" >
< h3 class = "font-semibold text-base" > Laufzeit-Einstellungen< / h3 >
< button id = "load-settings"
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 >
< p class = "text-xs text-slate-400 mb-4" > Änderungen wirken sofort — kein Server-Neustart nötig. „Zurücksetzen" entfernt den Override und der .env-Wert gilt wieder.< / p >
< div id = "settings-content" class = "space-y-2" >
< p class = "text-sm text-slate-400" > lade …< / p >
< / div >
< / div >
< / div >
2026-06-19 01:04:20 +02:00
< / div > <!-- /Tab Content Container -->
< / div > <!-- /Admin Panel -->
<!-- ═══════════════════ CHAT INTERFACE ═══════════════════ -->
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< header class = "shrink-0 h-14 flex items-center gap-2 px-3 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 backdrop-blur" >
2026-06-20 22:26:20 +02:00
< img src = "/favicon.svg?v=1" alt = "" class = "w-7 h-7 shrink-0" / >
2026-06-27 07:02:02 +02:00
< h1 class = "flex-1 truncate font-semibold text-base" > Alexis< span class = "hidden min-[420px]:inline font-normal text-slate-400 dark:text-slate-500 text-sm" > · Ihr Sprachbegleiter< / span > < / h1 >
2026-06-25 03:26:50 +02:00
< button type = "button" id = "emergency-btn" title = "Hilfe rufen" aria-label = "Hilfe rufen"
2026-06-26 15:32:42 +02:00
class="shrink-0 h-8 w-8 grid place-items-center rounded-lg bg-red-600 hover:bg-red-700 text-white font-extrabold text-sm leading-none shadow-sm transition-colors disabled:opacity-100">SOS< / button >
2026-06-25 02:51:17 +02:00
< select id = "lang-sel" title = "Sprache"
2026-06-18 10:20:24 +02:00
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< option value = "de" > 🇩🇪 DE< / option >
< option value = "en" > 🇬🇧 EN< / option >
< option value = "fr" > 🇫🇷 FR< / option >
< option value = "es" > 🇪🇸 ES< / option >
< option value = "it" > 🇮🇹 IT< / option >
2026-06-28 15:25:45 +02:00
< option value = "nl" > 🇳🇱 NL< / option >
2026-06-24 22:01:38 +02:00
< option value = "pt" > 🇧🇷 PT< / option >
< option value = "pl" > 🇵🇱 PL< / option >
< option value = "ar" > 🇸🇦 AR< / option >
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
< option value = "ru" > 🇷🇺 RU< / option >
< option value = "zh" > 🇨🇳 ZH< / option >
< / select >
2026-06-26 16:35:23 +02:00
<!-- „Neues Gespräch" in der Kopfzeile: ab 384px Viewport sichtbar, darunter
(sehr schmale Phones, z.B. iPhone SE) ausgeblendet, damit der Titel Platz
behält. Das Menü enthält „Neues Gespräch" dauerhaft als Rückfall. -->
2026-06-26 15:58:17 +02:00
< button id = "new-chat-top" type = "button" title = "Neues Gespräch" aria-label = "Neues Gespräch"
2026-06-26 16:35:23 +02:00
class="shrink-0 h-9 w-9 hidden min-[384px]: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">
2026-06-26 15:58:17 +02:00
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.7" class = "w-5 h-5" > < path stroke-linecap = "round" stroke-linejoin = "round" d = "M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" / > < / svg >
< / button >
2026-06-20 22:26:20 +02:00
< button id = "menu-toggle" title = "Menü" aria-label = "Menü"
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 text-xl leading-none 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-21 01:28:21 +02:00
<!-- Identitätsleiste: angemeldeter Nutzer (links) + Abmelden (rechts) -->
< 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 >
2026-06-27 07:02:02 +02:00
< div class = "flex items-center gap-3" >
< span id = "app-version" class = "opacity-70 tabular-nums" > < / span >
< a id = "logout" class = "hidden hover:underline" href = "#" > Abmelden< / a >
< / div >
2026-06-21 01:28:21 +02:00
< / div >
2026-06-20 22:26:20 +02:00
<!-- Verstecktes Quell - Select: speist die Ton - Presets (bewahrt die bestehende Logik) -->
< select id = "tts" class = "hidden" >
< option value = "device" > Im Gerät< / option >
< option value = "piper" > Server< / option >
< option value = "chatterbox" > Beste Qualität< / option >
2026-06-24 22:01:38 +02:00
< option value = "cartesia" > Cartesia< / option >
2026-06-20 22:26:20 +02:00
< option value = "openrouter" > Cloud< / option >
< / select >
<!-- ═══════════════════ EINSTELLUNGS - SHEET ═══════════════════ -->
< div id = "settings-backdrop" class = "hidden fixed inset-0 z-40 bg-black/30" > < / div >
< div id = "settings-sheet"
class="hidden fixed top-14 right-2 z-50 w-[min(20rem,calc(100vw-1rem))] max-h-[80vh] overflow-y-auto
rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shadow-xl p-3 space-y-3">
<!-- Vorlesen (TTS - Presets) -->
< div >
< p class = "text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-1.5 px-1" > Vorlesen< / p >
< div class = "space-y-1.5" >
< button type = "button" data-tts = "device"
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
< span class = "text-lg shrink-0" > 📱< / span >
< span class = "flex-1 min-w-0" > < span class = "block text-sm font-medium" > Im Gerät< / span >
2026-06-21 01:51:37 +02:00
< span class = "tts-sub block text-xs text-slate-500 dark:text-slate-400" > Das Gerät liest vor · spart Daten< / span > < / span >
2026-06-20 22:26:20 +02:00
< span class = "check text-blue-600 dark:text-blue-400 font-bold opacity-0" > ✓< / span >
< / button >
< button type = "button" data-tts = "piper"
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
2026-06-21 01:10:51 +02:00
< span class = "text-lg shrink-0" > ⚡< / span >
< span class = "flex-1 min-w-0" > < span class = "block text-sm font-medium" > Schnell< / span >
< span class = "block text-xs text-slate-500 dark:text-slate-400" > Piper · lokal, zuverlässig< / span > < / span >
2026-06-20 22:26:20 +02:00
< span class = "check text-blue-600 dark:text-blue-400 font-bold opacity-0" > ✓< / span >
< / button >
< button type = "button" data-tts = "chatterbox"
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
< span class = "text-lg shrink-0" > ✨< / span >
2026-06-21 01:10:51 +02:00
< span class = "flex-1 min-w-0" > < span class = "block text-sm font-medium" > Hohe Qualität< / span >
< span class = "block text-xs text-slate-500 dark:text-slate-400" > Chatterbox · natürliche Stimme< / span > < / span >
< span class = "check text-blue-600 dark:text-blue-400 font-bold opacity-0" > ✓< / span >
< / button >
2026-06-24 22:01:38 +02:00
< button type = "button" data-tts = "cartesia"
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
< span class = "text-lg shrink-0" > 🎙< / span >
< span class = "flex-1 min-w-0" > < span class = "block text-sm font-medium" > Cartesia< / span >
< span class = "tts-sub block text-xs text-slate-500 dark:text-slate-400" > Cartesia Sonic · natürlich & schnell< / span > < / span >
< span class = "check text-blue-600 dark:text-blue-400 font-bold opacity-0" > ✓< / span >
< / button >
2026-06-21 01:10:51 +02:00
< button type = "button" data-tts = "openrouter"
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
< span class = "text-lg shrink-0" > ☁< / span >
< span class = "flex-1 min-w-0" > < span class = "block text-sm font-medium" > Cloud< / span >
< span class = "block text-xs text-slate-500 dark:text-slate-400" > OpenRouter · benötigt Internet< / span > < / span >
2026-06-20 22:26:20 +02:00
< span class = "check text-blue-600 dark:text-blue-400 font-bold opacity-0" > ✓< / span >
< / button >
< / div >
2026-06-25 00:49:28 +02:00
< p id = "cartesia-lang-hint" class = "hidden mt-2 px-1 text-xs text-amber-600 dark:text-amber-400" > < / p >
2026-06-20 22:26:20 +02:00
< / div >
2026-06-24 22:01:38 +02:00
<!-- Geschlechtspräferenz (wirkt bei Cartesia) -->
< div id = "gender-pref-row" >
< p class = "text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-1.5 px-1" > Stimme< / p >
< div class = "flex gap-2" >
< button type = "button" id = "gender-m"
class="gender-btn flex-1 rounded-lg border border-slate-200 dark:border-slate-700 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50">
♂ Männlich
< / button >
< button type = "button" id = "gender-f"
class="gender-btn flex-1 rounded-lg border border-slate-200 dark:border-slate-700 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50">
♀ Weiblich
< / button >
< / div >
< / div >
2026-06-20 22:26:20 +02:00
<!-- Aktionen -->
< div class = "border-t border-slate-100 dark:border-slate-700 pt-2 space-y-1" >
< button id = "new-chat" type = "button"
class="w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 text-left transition-colors">
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.7" class = "w-5 h-5 shrink-0" > < path stroke-linecap = "round" stroke-linejoin = "round" d = "M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" / > < / svg >
Neues Gespräch
< / button >
< button id = "theme-toggle" type = "button"
class="w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 text-left transition-colors">
< span id = "theme-icon" class = "w-5 text-center shrink-0" > 🌙< / span >
< span > Tag-/Nachtmodus< / span >
< / button >
2026-06-21 01:28:21 +02:00
< button id = "admin-toggle" type = "button"
class="hidden w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 text-left transition-colors">
< span class = "w-5 text-center shrink-0" > ⚙< / span >
< span > Admin-Bereich< / span >
< / button >
2026-06-20 22:26:20 +02:00
< / div >
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" >
2026-06-20 23:04:35 +02:00
< button type = "button" id = "mic" title = "Mikrofon" aria-label = "Mikrofon"
2026-06-18 10:20:24 +02:00
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤< / button >
2026-06-24 22:01:38 +02:00
< input id = "prompt" type = "text" autocomplete = "off" dir = "auto" placeholder = "Nachricht …"
2026-06-20 23:04:35 +02:00
class="flex-1 min-w-0 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" title = "Senden" aria-label = "Senden"
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-colors">
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.8" class = "w-5 h-5" >
< path stroke-linecap = "round" stroke-linejoin = "round" d = "M6 12 3.27 4.36a.6.6 0 0 1 .82-.74l16.2 7.83a.6.6 0 0 1 0 1.08l-16.2 7.83a.6.6 0 0 1-.82-.74L6 12Zm0 0h7" / >
< / svg >
< / 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 >
feat(ui): SOS-Button prominent (rot gefüllt, großes weißes „SOS") + Bestätigung & Status
Der Notruf-Button war ein kleines 36px-Outline-Icon (🆘) — leicht zu übersehen.
Jetzt ein rot gefüllter Button (bg-red-600) mit großem, fettem weißem „SOS" in
der (immer sichtbaren) Kopfzeile — der Senior erkennt ihn sofort. Sprachwahl
und Menü bleiben unverändert an ihrem Platz.
- Bestätigung: statt native confirm() ein großer eigener Dialog (rote SOS-Kopf-
zeile, Warnzeile, große Knöpfe „JA, HILFE RUFEN" / „Abbrechen"). Ein Tipp auf
SOS, ein Tipp auf JA — schnell, aber gegen Fehlalarm geschützt.
- Feedback am Button selbst: setSosState idle|calling|done|error —
„Rufe …" (pulsierend) → „Gerufen ✓" (grün) → zurück zu „SOS". So sieht der
Senior unmittelbar am Knopf, dass der Alarm ausgelöst wurde.
Nur Frontend (app/web), live ohne Neustart (No-Cache-Static, Bump auf v=49).
Verifiziert via Headless-Chrome/CDP: Idle/Dialog/Calling/Done-Zustände.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:48:35 +02:00
<!-- ═══════════════ SOS - Bestätigung (Notruf) ═══════════════ -->
< div id = "sos-confirm" role = "dialog" aria-modal = "true" aria-labelledby = "sos-confirm-title"
class="hidden fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
< div class = "w-full max-w-sm rounded-2xl bg-white dark:bg-slate-800 shadow-xl overflow-hidden" >
< div class = "bg-red-600 text-white px-5 py-4 flex items-center gap-3" >
< span class = "text-3xl font-extrabold tracking-wide" > SOS< / span >
< span id = "sos-confirm-title" class = "text-lg font-semibold" > Hilfe rufen?< / span >
< / div >
< div class = "px-5 py-4" >
< p class = "text-base text-slate-700 dark:text-slate-200" > Möchten Sie wirklich Hilfe rufen?< / p >
< p class = "mt-1 text-sm font-semibold text-red-600 dark:text-red-400" > Es werden sofort Helfer alarmiert.< / p >
2026-06-27 16:50:03 +02:00
< p id = "sos-confirm-warn"
class="hidden mt-3 rounded-lg bg-amber-100 dark:bg-amber-900/40 px-3 py-2 text-sm font-semibold text-amber-800 dark:text-amber-200">
⚠️ Achtung: Es sind noch keine Notfall-Kontakte eingerichtet. Es kann niemand
automatisch benachrichtigt werden – im Notfall bitte selbst 112 anrufen.
< / p >
feat(ui): SOS-Button prominent (rot gefüllt, großes weißes „SOS") + Bestätigung & Status
Der Notruf-Button war ein kleines 36px-Outline-Icon (🆘) — leicht zu übersehen.
Jetzt ein rot gefüllter Button (bg-red-600) mit großem, fettem weißem „SOS" in
der (immer sichtbaren) Kopfzeile — der Senior erkennt ihn sofort. Sprachwahl
und Menü bleiben unverändert an ihrem Platz.
- Bestätigung: statt native confirm() ein großer eigener Dialog (rote SOS-Kopf-
zeile, Warnzeile, große Knöpfe „JA, HILFE RUFEN" / „Abbrechen"). Ein Tipp auf
SOS, ein Tipp auf JA — schnell, aber gegen Fehlalarm geschützt.
- Feedback am Button selbst: setSosState idle|calling|done|error —
„Rufe …" (pulsierend) → „Gerufen ✓" (grün) → zurück zu „SOS". So sieht der
Senior unmittelbar am Knopf, dass der Alarm ausgelöst wurde.
Nur Frontend (app/web), live ohne Neustart (No-Cache-Static, Bump auf v=49).
Verifiziert via Headless-Chrome/CDP: Idle/Dialog/Calling/Done-Zustände.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:48:35 +02:00
< / div >
< div class = "px-5 pb-5 grid grid-cols-1 gap-3" >
< button type = "button" id = "sos-confirm-yes"
class="h-14 rounded-xl bg-red-600 hover:bg-red-700 text-white text-xl font-extrabold tracking-wide transition-colors">JA, HILFE RUFEN< / button >
< button type = "button" id = "sos-confirm-no"
class="h-12 rounded-xl border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 text-lg font-semibold hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Abbrechen< / button >
< / div >
< / div >
< / div >
2026-06-26 15:58:17 +02:00
<!-- ═══════════════ Notruf bereits ausgelöst (Sperre) ═══════════════ -->
< div id = "sos-cooldown" role = "dialog" aria-modal = "true" aria-labelledby = "sos-cooldown-title"
class="hidden fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
< div class = "w-full max-w-sm rounded-2xl bg-white dark:bg-slate-800 shadow-xl overflow-hidden" >
< div class = "bg-green-600 text-white px-5 py-4 flex items-center gap-3" >
< span class = "text-3xl font-extrabold" > ✓< / span >
< span id = "sos-cooldown-title" class = "text-lg font-semibold" > Hilfe ist unterwegs< / span >
< / div >
< div class = "px-5 py-4" >
< p id = "sos-cooldown-text" class = "text-base text-slate-700 dark:text-slate-200" > < / p >
< / div >
< div class = "px-5 pb-5" >
< button type = "button" id = "sos-cooldown-ok"
class="w-full h-14 rounded-xl bg-green-600 hover:bg-green-700 text-white text-xl font-extrabold transition-colors">Verstanden< / button >
< / div >
< / div >
< / div >
2026-06-27 16:50:03 +02:00
< script src = "/app.js?v=54" > < / 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 >