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
"use strict" ;
const $ = ( sel ) => document . querySelector ( sel ) ;
const messagesEl = $ ( "#messages" ) ;
const statusEl = $ ( "#status" ) ;
const promptEl = $ ( "#prompt" ) ;
const formEl = $ ( "#prompt-form" ) ;
const micBtn = $ ( "#mic" ) ;
2026-06-18 09:49:25 +02:00
const ttsSel = $ ( "#tts" ) ;
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
const langSel = $ ( "#lang-sel" ) ;
2026-06-25 00:49:28 +02:00
const cartesiaHint = $ ( "#cartesia-lang-hint" ) ;
let cartesiaUnsupportedLangs = [ ] ; // von /api/config: Sprachen, die Cartesia nicht spricht
const CARTESIA _LANG _LABELS = { ar : "Arabisch" , it : "Italienisch" , ru : "Russisch" , pl : "Polnisch" , nl : "Niederländisch" , sv : "Schwedisch" , tr : "Türkisch" } ;
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
2026-06-25 02:51:17 +02:00
// Die gewählte Sprache ist immer fest (Flex-Modus wurde entfernt).
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
function langOverrides ( ) {
2026-06-25 02:51:17 +02:00
return { language : ( langSel && langSel . value ) || "de" } ;
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
}
2026-06-18 09:49:25 +02:00
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
// Nutzer-Präferenzen als Request-Overrides für jeden Turn.
2026-06-24 22:01:38 +02:00
// Geschlechtspräferenz: 'm', 'f', oder 'any' (= weiblich bevorzugt)
let _voiceGender = 'f' ;
function applyGenderChoice ( gender ) {
// 'any' aus alten Prefs → weiblich als Standardwert
_voiceGender = ( gender === 'm' ) ? 'm' : 'f' ;
const btnM = document . getElementById ( 'gender-m' ) ;
const btnF = document . getElementById ( 'gender-f' ) ;
if ( ! btnM || ! btnF ) return ;
const mActive = ( _voiceGender === 'm' ) ;
const fActive = ( _voiceGender === 'f' ) ;
// Aktiv: blauer Hintergrund + weiße Schrift
// Inaktiv: kein Hintergrund + dunkle Schrift (explizit, damit kein Vererbungsproblem)
btnM . classList . toggle ( 'bg-blue-600' , mActive ) ;
btnM . classList . toggle ( 'text-white' , mActive ) ;
btnM . classList . toggle ( 'border-blue-600' , mActive ) ;
btnM . classList . toggle ( 'text-slate-700' , ! mActive ) ;
btnF . classList . toggle ( 'bg-blue-600' , fActive ) ;
btnF . classList . toggle ( 'text-white' , fActive ) ;
btnF . classList . toggle ( 'border-blue-600' , fActive ) ;
btnF . classList . toggle ( 'text-slate-700' , ! fActive ) ;
}
2026-06-18 09:49:25 +02:00
function overrides ( ) {
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
const out = { ... langOverrides ( ) } ;
if ( isDeviceMode ( ) ) {
out . text _only = true ; // kein Server-Audio -> Gerät spricht selbst
} else if ( ttsSel && ttsSel . value ) {
out . tts _provider = ttsSel . value ;
}
2026-06-24 22:01:38 +02:00
out . voice _gender = _voiceGender ;
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
return out ;
}
async function savePrefs ( obj ) {
try {
await fetch ( "/api/me/prefs" , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( obj ) ,
} ) ;
} catch ( e ) { console . error ( "savePrefs" , e ) ; }
}
// ---------- Geräte-TTS (Web Speech API) ----------
// "Gerät" liest die Antwort lokal vor -> der Server schickt kein Audio (spart Bytes/Kosten).
const TTS _SUPPORTED = typeof window !== "undefined" && "speechSynthesis" in window ;
const LANG _BCP47 = {
de : "de-DE" , en : "en-US" , fr : "fr-FR" , es : "es-ES" ,
2026-06-24 22:01:38 +02:00
it : "it-IT" , pt : "pt-BR" , pl : "pl-PL" , ar : "ar-SA" , ru : "ru-RU" , zh : "zh-CN" ,
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
} ;
function isMobile ( ) {
const ua = navigator . userAgent || "" ;
return /Android|iPhone|iPad|iPod/i . test ( ua ) ||
( navigator . maxTouchPoints > 1 && /Macintosh/ . test ( ua ) ) ; // iPad ab iOS 13
}
function isDeviceMode ( ) { return ! ! ttsSel && ttsSel . value === "device" ; }
let _voices = [ ] ;
function loadVoices ( ) { _voices = TTS _SUPPORTED ? speechSynthesis . getVoices ( ) : [ ] ; }
if ( TTS _SUPPORTED ) {
loadVoices ( ) ;
2026-06-21 01:51:37 +02:00
// Stimmen laden asynchron -> bei Eintreffen Preset-Zustand neu bewerten (entgrauen).
speechSynthesis . addEventListener ( "voiceschanged" , ( ) => { loadVoices ( ) ; refreshDevicePreset ( ) ; } ) ;
setTimeout ( ( ) => refreshDevicePreset ( ) , 1500 ) ; // Backstop, falls kein voiceschanged kommt
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
}
function pickVoice ( bcp ) {
if ( ! _voices . length ) loadVoices ( ) ;
const lc = bcp . toLowerCase ( ) ;
const two = lc . slice ( 0 , 2 ) ;
return _voices . find ( ( v ) => v . lang && v . lang . toLowerCase ( ) === lc ) ||
_voices . find ( ( v ) => v . lang && v . lang . toLowerCase ( ) . startsWith ( two ) ) || null ;
}
function _makeUtterance ( text , lang ) {
const u = new SpeechSynthesisUtterance ( text ) ;
u . lang = LANG _BCP47 [ lang ] || lang || "de-DE" ;
const v = pickVoice ( u . lang ) ;
if ( v ) u . voice = v ;
return u ;
}
2026-06-21 01:02:03 +02:00
// Hat der Browser nutzbare Sprachstimmen? (Linux-Desktops haben oft KEINE -> stumm.)
function deviceVoicesReady ( ) {
if ( ! TTS _SUPPORTED ) return false ;
if ( ! _voices . length ) loadVoices ( ) ;
return _voices . length > 0 ;
}
// Server-Fallback: holt Audio via /api/speak (ohne tts_provider -> Server-Default,
// niemals "device") und spielt es ab. Greift, wenn Geräte-TTS keine Stimme hat.
async function serverSpeakFallback ( text , lang , btn ) {
try {
const body = { text } ;
if ( lang ) body . language = lang ;
const r = await fetch ( "/api/speak" , {
method : "POST" , headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( body ) ,
} ) ;
if ( ! r . ok ) throw new Error ( "HTTP " + r . status ) ;
startReplay ( [ await r . arrayBuffer ( ) ] , btn ) ;
} catch ( e ) {
setReplayPlaying ( btn , false ) ;
statusEl . textContent = "Vorlesen fehlgeschlagen" ;
}
}
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
// Vorlesen ohne Button-Status (automatisches Vorlesen der Antwort).
function deviceSpeak ( text , lang ) {
2026-06-21 01:02:03 +02:00
if ( ! text ) return ;
if ( deviceVoicesReady ( ) ) {
speechSynthesis . cancel ( ) ;
speechSynthesis . speak ( _makeUtterance ( text , lang ) ) ;
} else {
serverSpeakFallback ( text , lang , null ) ; // keine Stimmen -> Server-Audio
}
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
}
// Vorlesen mit Button-Status (Replay): Icon -> ⏹ während des Sprechens, danach zurück.
function deviceSpeakBtn ( text , lang , btn ) {
2026-06-21 01:02:03 +02:00
if ( ! text ) { setReplayPlaying ( btn , false ) ; return ; }
if ( ! deviceVoicesReady ( ) ) { serverSpeakFallback ( text , lang , btn ) ; return ; }
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
speechSynthesis . cancel ( ) ;
const u = _makeUtterance ( text , lang ) ;
u . onend = u . onerror = ( ) => setReplayPlaying ( btn , false ) ;
setReplayPlaying ( btn , true ) ;
speechSynthesis . speak ( u ) ;
}
// iOS: speechSynthesis darf nur nach einer Nutzergeste starten -> beim ersten Tap freischalten.
let _ttsUnlocked = false ;
function unlockTTS ( ) {
if ( _ttsUnlocked || ! TTS _SUPPORTED ) return ;
try { speechSynthesis . speak ( new SpeechSynthesisUtterance ( "" ) ) ; _ttsUnlocked = true ; } catch ( e ) { }
}
const PLAYBACK _KEY = "va-playback" ; // geräte-lokal: "device" wenn Geräte-TTS gewählt
2026-06-20 22:26:20 +02:00
// Setzt das verborgene Quell-Select (#tts) beim Laden + spiegelt es in die Presets.
// Vorrang: 1. lokale Wahl (localStorage), 2. Server-Pref (tts_provider), 3. Mobil-Default = Gerät.
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
function applyPlaybackChoice ( serverProvider ) {
if ( ! ttsSel ) return ;
const local = localStorage . getItem ( PLAYBACK _KEY ) ;
if ( local === "device" && TTS _SUPPORTED ) {
ttsSel . value = "device" ;
} else if ( serverProvider ) {
ttsSel . value = serverProvider ;
} else if ( TTS _SUPPORTED && isMobile ( ) && local === null ) {
ttsSel . value = "device" ; // Mobil-Default, solange nichts gewählt wurde
localStorage . setItem ( PLAYBACK _KEY , "device" ) ;
2026-06-20 22:26:20 +02:00
} else if ( ! ttsSel . value ) {
ttsSel . value = "piper" ; // Fallback: Server
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
}
2026-06-21 01:51:37 +02:00
refreshDevicePreset ( ) ; // graut „Im Gerät" aus, wenn keine Stimmen + syncPresetUI
}
// Graut „Im Gerät" aus, wenn keine Sprachstimmen vorhanden sind; entgraut automatisch,
// sobald welche da sind. Kein Aufzwingen: device wird nur (wieder) gewählt, wenn es die
// gespeicherte Präferenz ist. Ohne Stimmen NICHT auf der deaktivierten Option sitzen bleiben.
function refreshDevicePreset ( ) {
const btn = document . querySelector ( '.tts-preset[data-tts="device"]' ) ;
if ( ! btn || ! ttsSel ) return ;
const ready = deviceVoicesReady ( ) ;
btn . disabled = ! ready ;
btn . classList . toggle ( "opacity-50" , ! ready ) ;
btn . classList . toggle ( "cursor-not-allowed" , ! ready ) ;
const sub = btn . querySelector ( ".tts-sub" ) ;
if ( sub ) sub . textContent = ready
? "Das Gerät liest vor · spart Daten"
: "keine Sprachstimmen im Browser" ;
if ( ready ) {
if ( localStorage . getItem ( PLAYBACK _KEY ) === "device" && ttsSel . value !== "device" ) {
ttsSel . value = "device" ; // gespeicherte Präferenz wieder aktivieren
}
} else if ( ttsSel . value === "device" ) {
ttsSel . value = "piper" ; // Server statt deaktivierter Option (Präferenz bleibt)
}
2026-06-20 22:26:20 +02:00
syncPresetUI ( ) ;
}
2026-06-24 22:01:38 +02:00
// Graut "Hohe Qualität" (Chatterbox) aus, wenn kein GPU-Backend erreichbar ist.
async function refreshChatterboxPreset ( ) {
const btn = document . querySelector ( '.tts-preset[data-tts="chatterbox"]' ) ;
if ( ! btn || ! ttsSel ) return ;
try {
const cfg = await fetch ( "/api/config" , { cache : "no-store" } ) . then ( r => r . json ( ) ) ;
const available = cfg . available ? . chatterbox _reachable === true ;
btn . disabled = ! available ;
btn . classList . toggle ( "opacity-50" , ! available ) ;
btn . classList . toggle ( "cursor-not-allowed" , ! available ) ;
const sub = btn . querySelector ( ".tts-sub" ) ;
if ( sub ) sub . textContent = available
? "Chatterbox · natürliche Stimme"
: "Chatterbox · kein GPU-Backend erreichbar" ;
if ( ! available && ttsSel . value === "chatterbox" ) {
ttsSel . value = "piper" ;
syncPresetUI ( ) ;
}
} catch ( e ) { /* ignorieren */ }
}
// Graut "Cartesia" aus, wenn API-Key oder Voice-ID fehlt.
async function refreshCartesiaPreset ( ) {
const btn = document . querySelector ( '.tts-preset[data-tts="cartesia"]' ) ;
if ( ! btn || ! ttsSel ) return ;
try {
const cfg = await fetch ( "/api/config" , { cache : "no-store" } ) . then ( r => r . json ( ) ) ;
const available = cfg . available ? . cartesia _available === true ;
2026-06-25 00:49:28 +02:00
cartesiaUnsupportedLangs = cfg . available ? . cartesia _unsupported _languages || [ ] ;
2026-06-24 22:01:38 +02:00
btn . disabled = ! available ;
btn . classList . toggle ( "opacity-50" , ! available ) ;
btn . classList . toggle ( "cursor-not-allowed" , ! available ) ;
const sub = btn . querySelector ( ".tts-sub" ) ;
if ( sub ) sub . textContent = available
? "Cartesia Sonic · natürlich & schnell"
: "Cartesia · kein API-Key konfiguriert" ;
if ( ! available && ttsSel . value === "cartesia" ) {
ttsSel . value = "piper" ;
syncPresetUI ( ) ;
}
2026-06-25 00:49:28 +02:00
updateCartesiaLangHint ( ) ;
2026-06-24 22:01:38 +02:00
} catch ( e ) { /* ignorieren */ }
}
2026-06-25 00:49:28 +02:00
// Zeigt einen Hinweis, wenn Cartesia gewählt ist, die feste Sprache aber nicht
// unterstützt wird — dann übernimmt automatisch die lokale Piper-Stimme (tts_fallback).
function updateCartesiaLangHint ( ) {
if ( ! cartesiaHint ) return ;
const lang = langSel && langSel . value ;
const show = ttsSel && ttsSel . value === "cartesia"
&& lang && lang !== "flex"
&& cartesiaUnsupportedLangs . includes ( lang ) ;
if ( show ) {
const label = CARTESIA _LANG _LABELS [ lang ] || lang . toUpperCase ( ) ;
cartesiaHint . textContent =
` Hinweis: Cartesia spricht ${ label } nicht — es wird automatisch die lokale Stimme (Piper) verwendet. ` ;
cartesiaHint . classList . remove ( "hidden" ) ;
} else {
cartesiaHint . classList . add ( "hidden" ) ;
}
}
2026-06-24 22:01:38 +02:00
// Graut "Cloud" (OpenRouter TTS) aus, wenn das TTS-Modell nicht verfügbar ist.
async function refreshOpenrouterTtsPreset ( ) {
const btn = document . querySelector ( '.tts-preset[data-tts="openrouter"]' ) ;
if ( ! btn || ! ttsSel ) return ;
try {
const cfg = await fetch ( "/api/config" , { cache : "no-store" } ) . then ( r => r . json ( ) ) ;
const available = cfg . available ? . openrouter _tts _available === true ;
btn . disabled = ! available ;
btn . classList . toggle ( "opacity-50" , ! available ) ;
btn . classList . toggle ( "cursor-not-allowed" , ! available ) ;
const sub = btn . querySelector ( ".tts-sub" ) ;
if ( sub ) sub . textContent = available
? "OpenRouter · benötigt Internet"
: "OpenRouter · kein TTS-Modell verfügbar" ;
if ( ! available && ttsSel . value === "openrouter" ) {
ttsSel . value = "piper" ;
syncPresetUI ( ) ;
}
} catch ( e ) { /* ignorieren */ }
}
2026-06-20 22:26:20 +02:00
// Markiert das aktive Ton-Preset (Häkchen + Rahmen) passend zu #tts.
function syncPresetUI ( ) {
const val = ttsSel ? ttsSel . value : "" ;
document . querySelectorAll ( ".tts-preset" ) . forEach ( ( btn ) => {
2026-06-21 01:10:51 +02:00
const active = btn . dataset . tts === val ;
2026-06-20 22:26:20 +02:00
btn . classList . toggle ( "ring-2" , active ) ;
btn . classList . toggle ( "ring-blue-500" , active ) ;
btn . classList . toggle ( "border-blue-500" , active ) ;
const chk = btn . querySelector ( ".check" ) ;
if ( chk ) chk . style . opacity = active ? "1" : "0" ;
} ) ;
}
// ---------- Einstellungs-Menü (⋮) ----------
function openMenu ( ) {
$ ( "#settings-sheet" ) . classList . remove ( "hidden" ) ;
$ ( "#settings-backdrop" ) . classList . remove ( "hidden" ) ;
}
function closeMenu ( ) {
$ ( "#settings-sheet" ) . classList . add ( "hidden" ) ;
$ ( "#settings-backdrop" ) . classList . add ( "hidden" ) ;
2026-06-18 09:49:25 +02:00
}
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
let busy = false ;
2026-06-18 18:38:33 +02:00
let activeWs = null ; // aktive WS-Verbindung (fuer Barge-in von aussen erreichbar)
let activeSources = [ ] ; // laufende AudioBufferSourceNodes (stoppbar per Barge-in)
2026-06-18 08:50:30 +02:00
// Session-ID pro Nutzer (sonst "gehoert einem anderen Nutzer"-Konflikt). Wird aus
// /api/me abgeleitet; bis dahin null -> ensureSession() wartet auf loadMe().
let sessionId = null ;
let mePromise = null ;
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
let currentUserId = null ;
const sessionKey = ( uid ) => "va-session-" + uid ;
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
// ---------- Identitaet / Menue ----------
2026-06-18 08:50:30 +02:00
function loadMe ( ) {
mePromise = ( async ( ) => {
try {
const res = await fetch ( "/api/me" , { headers : { Accept : "application/json" } } ) ;
if ( ! res . ok ) {
$ ( "#identity" ) . textContent = "Nicht angemeldet" ;
return null ;
}
const me = await res . json ( ) ;
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
currentUserId = me . user _id ;
// Zuletzt aktive (ggf. frische) Session bevorzugen, sonst die Basis-Session.
sessionId = localStorage . getItem ( sessionKey ( me . user _id ) ) || ( "web-" + me . user _id ) ;
2026-06-18 08:50:30 +02:00
$ ( "#identity" ) . textContent = "Angemeldet als " + ( me . display _name || me . external _id || "Gast" ) ;
if ( me . sso _logout _url ) {
const logout = $ ( "#logout" ) ;
logout . href = me . sso _logout _url ;
logout . classList . remove ( "hidden" ) ;
}
if ( me . is _admin ) {
2026-06-18 10:20:24 +02:00
$ ( "#admin-toggle" ) . classList . remove ( "hidden" ) ;
2026-06-18 08:50:30 +02:00
}
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
// Gespeicherte Nutzer-Präferenzen in die Dropdowns laden.
const prefs = me . prefs || { } ;
if ( langSel ) {
2026-06-25 02:51:17 +02:00
langSel . value = prefs . language || "de" ;
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
}
applyPlaybackChoice ( prefs . tts _provider ) ;
2026-06-24 22:01:38 +02:00
applyGenderChoice ( prefs . voice _gender || 'f' ) ;
2026-06-18 08:50:30 +02:00
return me ;
} catch ( e ) {
$ ( "#identity" ) . textContent = "Verbindung fehlgeschlagen" ;
return null ;
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 08:50:30 +02:00
} ) ( ) ;
return mePromise ;
}
// Stellt sicher, dass eine nutzerspezifische Session-ID feststeht, bevor ein Turn startet.
async function ensureSession ( ) {
if ( ! sessionId && mePromise ) {
try { await mePromise ; } catch ( e ) { /* ignore */ }
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 08:50:30 +02:00
if ( ! sessionId ) sessionId = "web-anon" ;
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
// loadUsers() ersetzt durch Admin-Panel (siehe unten)
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
// ---------- Nachrichten-UI ----------
2026-06-18 07:39:40 +02:00
// Nach dem naechsten Layout ans Ende scrollen -> die neueste Antwort ist sichtbar.
function scrollToBottom ( ) {
requestAnimationFrame ( ( ) => { messagesEl . scrollTop = messagesEl . scrollHeight ; } ) ;
}
2026-06-18 10:20:24 +02:00
const BUBBLE = {
user : "ml-auto max-w-[80%] rounded-2xl rounded-br-sm bg-blue-600 px-4 py-2 text-white whitespace-pre-wrap" ,
assistant : "mr-auto max-w-[80%] rounded-2xl rounded-bl-sm border border-slate-200 bg-white px-4 py-2 whitespace-pre-wrap dark:border-slate-700 dark:bg-slate-800" ,
system : "mx-auto max-w-[90%] rounded-lg bg-amber-100 px-3 py-1.5 text-xs text-amber-800 dark:bg-amber-900/40 dark:text-amber-200" ,
emergency : "mx-auto max-w-[90%] rounded-lg bg-red-100 px-3 py-1.5 text-sm font-semibold text-red-800 dark:bg-red-900/40 dark:text-red-200" ,
} ;
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
// Lautsprecher-/Stop-Icons als SVG (currentColor → per CSS einfärbbar, anders als Emoji).
const ICON _SPEAKER =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" ' +
'class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" ' +
'd="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" /></svg>' ;
const ICON _STOP =
'<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">' +
'<rect x="6" y="6" width="12" height="12" rx="2" /></svg>' ;
// meta.lang: Sprache, in der diese Bubble vorgelesen werden soll (für Replay).
function addMessage ( role , text , meta = { } ) {
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
const div = document . createElement ( "div" ) ;
2026-06-18 10:20:24 +02:00
div . className = BUBBLE [ role ] || BUBBLE . assistant ;
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
const span = document . createElement ( "span" ) ;
span . className = "bubble-text" ;
2026-06-24 22:01:38 +02:00
span . dir = "auto" ;
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
span . textContent = text ;
div . appendChild ( span ) ;
div . _textEl = span ;
// Vorlese-Symbol nur an Nutzer-/Assistenten-Bubbles (nicht System/Notfall).
if ( role === "user" || role === "assistant" ) {
div . classList . add ( "relative" , "pr-7" ) ;
div . _replay = { lang : meta . lang || null , chunks : null } ; // chunks: gecachtes PCM (Assistent)
const btn = document . createElement ( "button" ) ;
btn . type = "button" ;
btn . innerHTML = ICON _SPEAKER ;
btn . title = "Vorlesen" ;
btn . setAttribute ( "aria-label" , "Vorlesen" ) ;
// Kontrastreiche Farbe je Bubble: weiß auf Blau; im Dark-Mode hell auf Dunkelgrau.
const replayColor = role === "user"
? "text-white/80 hover:text-white"
: "text-slate-500 hover:text-slate-700 dark:text-slate-300 dark:hover:text-white" ;
btn . className = "replay-btn absolute top-1.5 right-1.5 transition-colors " + replayColor ;
btn . addEventListener ( "click" , ( ) => replayBubble ( div ) ) ;
div . _replayBtn = btn ;
div . appendChild ( btn ) ;
}
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
messagesEl . appendChild ( div ) ;
2026-06-18 07:39:40 +02:00
scrollToBottom ( ) ;
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
return div ;
}
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
// ---------- Vorlesen / Replay ----------
function setReplayPlaying ( btn , on ) {
if ( ! btn ) return ;
btn . innerHTML = on ? ICON _STOP : ICON _SPEAKER ;
btn . dataset . playing = on ? "1" : "" ;
}
function startReplay ( chunks , btn ) {
stopAudio ( ) ; // Barge-in: laufendes Audio (auch andere Replays) stoppen
setReplayPlaying ( btn , true ) ;
playPcm ( chunks , 24000 , ( ) => setReplayPlaying ( btn , false ) ) ;
}
async function speakBubble ( text , lang , btn ) {
try {
const body = { text } ;
if ( lang ) body . language = lang ;
if ( ttsSel && ttsSel . value ) body . tts _provider = ttsSel . value ;
const r = await fetch ( "/api/speak" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( body ) ,
} ) ;
if ( ! r . ok ) throw new Error ( "HTTP " + r . status ) ;
startReplay ( [ await r . arrayBuffer ( ) ] , btn ) ;
} catch ( e ) {
setReplayPlaying ( btn , false ) ;
statusEl . textContent = "Vorlesen fehlgeschlagen" ;
}
}
function replayBubble ( div ) {
const btn = div . _replayBtn ;
if ( btn && btn . dataset . playing ) { stopAudio ( ) ; setReplayPlaying ( btn , false ) ; return ; } // Toggle
const info = div . _replay || { } ;
const text = ( div . _textEl && div . _textEl . textContent ) || "" ;
if ( info . chunks && info . chunks . length ) {
startReplay ( info . chunks , btn ) ; // Assistent: gecachtes PCM, sofort & gratis
} else if ( isDeviceMode ( ) ) {
stopAudio ( ) ;
2026-06-20 18:25:45 +02:00
// Geräte-TTS: bereinigtes "spoken" bevorzugen (ohne Markdown/Emojis), sonst Rohtext.
deviceSpeakBtn ( info . spoken || text , info . lang , btn ) ;
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
} else {
2026-06-20 18:25:45 +02:00
speakBubble ( text , info . lang , btn ) ; // Server-TTS via /api/speak (bereinigt selbst)
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
}
}
2026-06-18 18:38:33 +02:00
// ---------- Barge-in-Hilfsfunktionen ----------
function stopAudio ( ) {
activeSources . forEach ( ( s ) => { try { s . stop ( ) ; } catch ( e ) { } } ) ;
activeSources = [ ] ;
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
if ( TTS _SUPPORTED ) { try { speechSynthesis . cancel ( ) ; } catch ( e ) { } } // Geräte-TTS stoppen
2026-06-18 18:38:33 +02:00
}
function setMicIdle ( ) {
micBtn . textContent = "🎤" ;
micBtn . title = "Mikrofon" ;
micBtn . classList . remove ( "bg-red-600" , "animate-pulse" , "bg-amber-500" ) ;
micBtn . classList . add ( "bg-emerald-600" ) ;
}
function setMicRecording ( ) {
micBtn . classList . remove ( "bg-emerald-600" , "bg-amber-500" ) ;
micBtn . classList . add ( "bg-red-600" , "animate-pulse" ) ;
micBtn . title = "Aufnahme stoppen" ;
}
function setMicBusy ( ) {
micBtn . textContent = "⏹" ;
micBtn . title = "KI unterbrechen (Barge-in)" ;
micBtn . classList . remove ( "bg-emerald-600" , "bg-red-600" , "animate-pulse" ) ;
micBtn . classList . add ( "bg-amber-500" ) ;
}
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
// ---------- Audio-Wiedergabe (PCM s16le) ----------
let audioCtx = null ;
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
function playPcm ( chunks , sampleRate , onended ) {
if ( ! chunks . length ) { if ( onended ) onended ( ) ; return ; }
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
const total = chunks . reduce ( ( n , c ) => n + c . byteLength , 0 ) ;
const merged = new Uint8Array ( total ) ;
let off = 0 ;
for ( const c of chunks ) { merged . set ( new Uint8Array ( c ) , off ) ; off += c . byteLength ; }
const view = new DataView ( merged . buffer ) ;
const n = Math . floor ( merged . byteLength / 2 ) ;
audioCtx = audioCtx || new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
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
if ( audioCtx . state === "suspended" ) audioCtx . resume ( ) ;
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
const buf = audioCtx . createBuffer ( 1 , n , sampleRate || 24000 ) ;
const ch = buf . getChannelData ( 0 ) ;
for ( let i = 0 ; i < n ; i ++ ) ch [ i ] = view . getInt16 ( i * 2 , true ) / 32768 ;
const src = audioCtx . createBufferSource ( ) ;
src . buffer = buf ;
src . connect ( audioCtx . destination ) ;
2026-06-18 18:38:33 +02:00
activeSources . push ( src ) ;
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
src . onended = ( ) => {
activeSources = activeSources . filter ( ( s ) => s !== src ) ;
if ( onended ) onended ( ) ;
} ;
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
src . start ( ) ;
}
// ---------- WS-Turn (Text und Sprache teilen die Event-Logik) ----------
function wsUrl ( path ) {
const proto = location . protocol === "https:" ? "wss" : "ws" ;
2026-06-18 08:50:30 +02:00
return ` ${ proto } :// ${ location . host } ${ path } ?session_id= ${ encodeURIComponent ( sessionId ) } ` ;
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
}
// onopen: (ws) => sendet die Eingabe. Liefert ein Promise, das beim done-Event endet.
function runTurn ( path , onopen ) {
return new Promise ( ( resolve ) => {
const ws = new WebSocket ( wsUrl ( path ) ) ;
2026-06-18 18:38:33 +02:00
activeWs = ws ;
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
ws . binaryType = "arraybuffer" ;
const pcm = [ ] ;
let answerEl = null ;
let sampleRate = 24000 ;
2026-06-25 02:51:17 +02:00
// Sprache dieses Turns (für den Replay je Bubble) = feste Routensprache.
let routeLang = null ;
const turnLang = ( ) => routeLang || null ;
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
ws . onopen = ( ) => onopen ( ws ) ;
ws . onerror = ( ) => { statusEl . textContent = "Verbindungsfehler" ; resolve ( ) ; } ;
2026-06-18 18:38:33 +02:00
ws . onclose = ( ) => { activeWs = null ; resolve ( ) ; } ;
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
ws . onmessage = ( event ) => {
if ( typeof event . data !== "string" ) { pcm . push ( event . data ) ; return ; }
let msg ;
try { msg = JSON . parse ( event . data ) ; } catch { return ; }
switch ( msg . type ) {
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
case "ack" :
routeLang = ( msg . route && msg . route . language ) || null ;
break ;
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
case "transcript" :
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
if ( msg . text ) addMessage ( "user" , msg . text , { lang : turnLang ( ) } ) ;
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
break ;
case "token" :
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
if ( ! answerEl ) answerEl = addMessage ( "assistant" , "" , { lang : turnLang ( ) } ) ;
answerEl . _textEl . textContent += msg . text || "" ;
2026-06-18 07:39:40 +02:00
scrollToBottom ( ) ;
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
break ;
case "semantic" :
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
if ( ! answerEl ) answerEl = addMessage ( "assistant" , "" , { lang : turnLang ( ) } ) ;
if ( msg . text ) answerEl . _textEl . textContent = msg . text ;
// Sprache final festhalten (für Replay) und im Geräte-Modus lokal vorlesen.
2026-06-20 18:25:45 +02:00
// Für TTS das bereinigte "spoken" (ohne Markdown/Emojis) bevorzugen, anzeigen
// bleibt der Originaltext.
if ( answerEl . _replay ) {
answerEl . _replay . lang = turnLang ( ) ;
answerEl . _replay . spoken = msg . spoken || msg . text || "" ;
}
if ( isDeviceMode ( ) ) {
const spoken = msg . spoken || msg . text ;
if ( spoken ) deviceSpeak ( spoken , turnLang ( ) ) ;
}
2026-06-18 07:39:40 +02:00
scrollToBottom ( ) ;
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
break ;
case "emergency" :
addMessage ( "emergency" , "⚠ Notfall erkannt (" + ( msg . category || "?" ) + ")" ) ;
break ;
case "done" :
sampleRate = msg . sample _rate || 24000 ;
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
// Geräte-Modus: kein Server-Audio -> nichts abspielen/cachen (Gerät hat schon gesprochen).
if ( ! isDeviceMode ( ) ) {
if ( answerEl && answerEl . _replay ) answerEl . _replay . chunks = pcm . slice ( ) ; // Replay-Cache
playPcm ( pcm , sampleRate ) ;
}
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
statusEl . textContent = "" ;
2026-06-18 07:39:40 +02:00
scrollToBottom ( ) ;
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
ws . close ( ) ;
break ;
case "error" :
addMessage ( "system" , "Fehler: " + ( msg . detail || msg . status || "unbekannt" ) ) ;
ws . close ( ) ;
break ;
2026-06-18 19:03:03 +02:00
case "interrupted" :
statusEl . textContent = "" ;
ws . close ( ) ; // → ws.onclose → resolve() → busy=false → setMicIdle()
break ;
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
}
} ;
} ) ;
}
// ---------- Text senden ----------
async function sendText ( text ) {
if ( busy || ! text . trim ( ) ) return ;
busy = true ;
2026-06-18 18:38:33 +02:00
setMicBusy ( ) ;
2026-06-18 08:50:30 +02:00
await ensureSession ( ) ;
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
const lo = langOverrides ( ) ;
2026-06-25 02:51:17 +02:00
addMessage ( "user" , text , { lang : lo . language || null } ) ;
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
statusEl . textContent = "denkt …" ;
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
const audioStream = ! isDeviceMode ( ) ; // Geräte-Modus: kein Server-Audio anfordern
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
await runTurn ( "/ws/chat" , ( ws ) => {
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
ws . send ( JSON . stringify ( { text , stream : true , audio _stream : audioStream , ... overrides ( ) } ) ) ;
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
} ) ;
busy = false ;
2026-06-18 18:38:33 +02:00
setMicIdle ( ) ;
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
}
formEl . addEventListener ( "submit" , ( e ) => {
e . preventDefault ( ) ;
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
unlockTTS ( ) ; // iOS: Geräte-TTS bei der Nutzergeste freischalten
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
const text = promptEl . value ;
promptEl . value = "" ;
sendText ( text ) ;
} ) ;
// ---------- Mikrofon (Push-to-Talk) ----------
let mediaRecorder = null ;
let recChunks = [ ] ;
2026-06-20 21:28:39 +02:00
async function startRecording ( statusText ) {
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
if ( busy ) return ;
2026-06-18 10:39:02 +02:00
const md = navigator . mediaDevices ;
if ( ! md || ! md . getUserMedia ) {
addMessage ( "system" , "Mikrofon nicht freigegeben: Bitte die Seite direkt in Safari öffnen (nicht im In-App-Browser) und keinen Privat-Modus verwenden." ) ;
return ;
}
if ( typeof MediaRecorder === "undefined" ) {
addMessage ( "system" , "Aufnahme nicht unterstützt (iOS < 14.3?). Bitte iOS aktualisieren." ) ;
return ;
}
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
let stream ;
try {
2026-06-18 10:39:02 +02:00
stream = await md . getUserMedia ( { audio : true } ) ;
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
} catch ( e ) {
2026-06-18 10:39:02 +02:00
addMessage ( "system" , "Mikrofon-Zugriff fehlgeschlagen: " + ( e . name || "" ) +
". Tipp: iPhone → Einstellungen → Safari → Mikrofon = Erlauben/Fragen." ) ;
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
return ;
}
recChunks = [ ] ;
2026-06-18 10:39:02 +02:00
try {
mediaRecorder = new MediaRecorder ( stream ) ;
} catch ( e ) {
stream . getTracks ( ) . forEach ( ( t ) => t . stop ( ) ) ;
addMessage ( "system" , "Aufnahme nicht möglich: " + ( e . name || e . message ) ) ;
return ;
}
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
mediaRecorder . ondataavailable = ( e ) => { if ( e . data . size ) recChunks . push ( e . data ) ; } ;
mediaRecorder . onstop = async ( ) => {
stream . getTracks ( ) . forEach ( ( t ) => t . stop ( ) ) ;
2026-06-18 10:39:02 +02:00
const mime = mediaRecorder . mimeType || "audio/mp4" ;
const blob = new Blob ( recChunks , { type : mime } ) ;
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
const bytes = await blob . arrayBuffer ( ) ;
2026-06-18 10:39:02 +02:00
// Safari -> audio/mp4, andere -> webm; der Server erkennt das Format am Inhalt.
sendVoice ( bytes , mime . includes ( "mp4" ) || mime . includes ( "mpeg" ) ? "mp4" : "webm" ) ;
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
} ;
mediaRecorder . start ( ) ;
2026-06-18 18:38:33 +02:00
setMicRecording ( ) ;
2026-06-20 21:28:39 +02:00
statusEl . textContent = statusText || "Aufnahme … (zum Stoppen erneut tippen)" ;
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
}
function stopRecording ( ) {
if ( mediaRecorder && mediaRecorder . state !== "inactive" ) mediaRecorder . stop ( ) ;
2026-06-18 18:38:33 +02:00
setMicIdle ( ) ;
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:39:02 +02:00
async function sendVoice ( bytes , fmt = "webm" ) {
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
if ( busy ) return ;
busy = true ;
2026-06-18 18:38:33 +02:00
setMicBusy ( ) ;
2026-06-18 08:50:30 +02:00
await ensureSession ( ) ;
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
statusEl . textContent = "verarbeite Sprache …" ;
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
const audioStream = ! isDeviceMode ( ) ; // Geräte-Modus: kein Server-Audio anfordern
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
await runTurn ( "/ws/voice" , ( ws ) => {
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
ws . send ( JSON . stringify ( { type : "start" , format : fmt , stream : true , audio _stream : audioStream , ... overrides ( ) } ) ) ;
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
ws . send ( bytes ) ;
ws . send ( JSON . stringify ( { type : "end" } ) ) ;
} ) ;
busy = false ;
2026-06-18 18:38:33 +02:00
setMicIdle ( ) ;
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
}
micBtn . addEventListener ( "click" , ( ) => {
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
unlockTTS ( ) ; // iOS: Geräte-TTS bei der Nutzergeste freischalten
2026-06-18 18:38:33 +02:00
if ( busy ) {
// Barge-in: laufende KI-Antwort sofort unterbrechen
if ( activeWs ) activeWs . send ( JSON . stringify ( { type : "interrupt" } ) ) ;
stopAudio ( ) ;
statusEl . textContent = "Unterbrochen" ;
// busy + Button-Reset erfolgen sobald die WS schliesst (-> runTurn resolve -> sendVoice/sendText)
} else if ( mediaRecorder && mediaRecorder . state === "recording" ) {
stopRecording ( ) ;
} else {
2026-06-20 22:26:20 +02:00
startRecording ( ) ; // Server-STT (Upload) — überall gleich
2026-06-18 18:38:33 +02:00
}
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-20 22:26:20 +02:00
$ ( "#admin-toggle" ) . addEventListener ( "click" , ( ) => { closeMenu ( ) ; openAdminPanel ( ) ; } ) ;
2026-06-18 10:20:24 +02:00
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
// ---------- Neues Gespräch ----------
// Startet eine frische Session (neuer session_id) -> der bisherige Verlauf wird nicht
// mehr als LLM-Kontext genutzt. Behebt "Sprache springt nicht um", wenn die History
// in einer anderen Sprache dominiert. Nicht-destruktiv: alte Session bleibt in der DB.
function newConversation ( ) {
const base = currentUserId || "anon" ;
sessionId = "web-" + base + "-" + Date . now ( ) ;
if ( currentUserId ) localStorage . setItem ( sessionKey ( currentUserId ) , sessionId ) ;
stopAudio ( ) ;
messagesEl . innerHTML = "" ;
statusEl . textContent = "Neues Gespräch begonnen" ;
setTimeout ( ( ) => {
if ( statusEl . textContent === "Neues Gespräch begonnen" ) statusEl . textContent = "" ;
} , 2500 ) ;
}
2026-06-20 22:26:20 +02:00
$ ( "#new-chat" ) . addEventListener ( "click" , ( ) => { closeMenu ( ) ; newConversation ( ) ; } ) ;
// ---------- Menü (⋮) verdrahten ----------
$ ( "#menu-toggle" ) . addEventListener ( "click" , ( ) =>
$ ( "#settings-sheet" ) . classList . contains ( "hidden" ) ? openMenu ( ) : closeMenu ( ) ) ;
$ ( "#settings-backdrop" ) . addEventListener ( "click" , closeMenu ) ;
document . addEventListener ( "keydown" , ( e ) => { if ( e . key === "Escape" ) closeMenu ( ) ; } ) ;
// Ton-Presets: setzen das verborgene #tts + lösen die bestehende Persistenz-Logik aus.
document . querySelectorAll ( ".tts-preset" ) . forEach ( ( btn ) =>
btn . addEventListener ( "click" , ( ) => {
unlockTTS ( ) ;
ttsSel . value = btn . dataset . tts ;
ttsSel . dispatchEvent ( new Event ( "change" ) ) ;
syncPresetUI ( ) ;
} )
) ;
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
2026-06-18 10:20:24 +02:00
// Tag/Nacht: Umschalter (manuell) + automatisch dem System folgen, solange nichts gewählt.
const themeBtn = $ ( "#theme-toggle" ) ;
2026-06-20 22:26:20 +02:00
const setThemeIcon = ( ) => {
const icon = $ ( "#theme-icon" ) ;
if ( icon ) icon . textContent = document . documentElement . classList . contains ( "dark" ) ? "☀️" : "🌙" ;
} ;
2026-06-18 10:20:24 +02:00
themeBtn . addEventListener ( "click" , ( ) => {
const dark = document . documentElement . classList . toggle ( "dark" ) ;
localStorage . setItem ( "theme" , dark ? "dark" : "light" ) ;
setThemeIcon ( ) ;
} ) ;
matchMedia ( "(prefers-color-scheme: dark)" ) . addEventListener ( "change" , ( e ) => {
if ( ! localStorage . getItem ( "theme" ) ) {
document . documentElement . classList . toggle ( "dark" , e . matches ) ;
setThemeIcon ( ) ;
}
} ) ;
setThemeIcon ( ) ;
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 07:39:40 +02:00
// Wenn die Bildschirmtastatur die sichtbare Hoehe aendert (iOS), unten bleiben.
if ( window . visualViewport ) {
window . visualViewport . addEventListener ( "resize" , scrollToBottom ) ;
}
window . addEventListener ( "resize" , scrollToBottom ) ;
promptEl . addEventListener ( "focus" , ( ) => setTimeout ( scrollToBottom , 300 ) ) ;
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
// Dropdown-Änderungen dauerhaft als Nutzer-Präferenz speichern.
2026-06-25 00:49:28 +02:00
if ( langSel ) langSel . addEventListener ( "change" , ( ) => { savePrefs ( langOverrides ( ) ) ; updateCartesiaLangHint ( ) ; } ) ;
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
if ( ttsSel ) {
ttsSel . addEventListener ( "change" , ( ) => {
unlockTTS ( ) ; // Auswahl ist eine Nutzergeste -> Geräte-TTS gleich freischalten
if ( ttsSel . value === "device" ) {
// Geräte-TTS ist geräte-lokal (nicht server-seitig) -> nur lokal merken.
localStorage . setItem ( PLAYBACK _KEY , "device" ) ;
} else {
localStorage . removeItem ( PLAYBACK _KEY ) ;
savePrefs ( { tts _provider : ttsSel . value || null } ) ; // echter Provider -> Server-Pref
}
2026-06-25 00:49:28 +02:00
updateCartesiaLangHint ( ) ;
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
} ) ;
}
2026-06-24 22:01:38 +02:00
// Gender-Buttons
document . addEventListener ( "DOMContentLoaded" , ( ) => {
const btnM = document . getElementById ( "gender-m" ) ;
const btnF = document . getElementById ( "gender-f" ) ;
if ( btnM ) btnM . addEventListener ( "click" , ( ) => {
applyGenderChoice ( "m" ) ;
savePrefs ( { voice _gender : "m" } ) ;
} ) ;
if ( btnF ) btnF . addEventListener ( "click" , ( ) => {
applyGenderChoice ( "f" ) ;
savePrefs ( { voice _gender : "f" } ) ;
} ) ;
} ) ;
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
loadMe ( ) ;
2026-06-24 22:01:38 +02:00
refreshChatterboxPreset ( ) ;
refreshCartesiaPreset ( ) ;
refreshOpenrouterTtsPreset ( ) ;
2026-06-19 01:04:20 +02:00
// ═══════════════════════════════════════════════════════════════
// ADMIN PANEL
// ═══════════════════════════════════════════════════════════════
// ---------- Hilfsfunktionen ----------
function escHtml ( str ) {
2026-06-19 01:33:49 +02:00
if ( str === null || str === undefined || str === "" ) return "" ;
return String ( str ) . replace ( /&/g , "&" ) . replace ( /</g , "<" ) . replace ( />/g , ">" ) . replace ( /"/g , """ ) ;
2026-06-19 01:04:20 +02:00
}
function fmtDate ( iso ) {
if ( ! iso ) return "—" ;
try {
return new Date ( iso ) . toLocaleString ( "de-DE" , {
day : "2-digit" , month : "2-digit" , year : "2-digit" ,
hour : "2-digit" , minute : "2-digit" ,
} ) ;
} catch { return iso ; }
}
async function adminFetch ( url , method = "GET" , body = null ) {
try {
const opts = { method , headers : { "Content-Type" : "application/json" } } ;
if ( body ) opts . body = JSON . stringify ( body ) ;
const res = await fetch ( url , opts ) ;
if ( method === "DELETE" && res . status === 204 ) return { } ;
const data = await res . json ( ) . catch ( ( ) => null ) ;
if ( ! res . ok ) { console . error ( "Admin API" , res . status , data ) ; return null ; }
return data ;
} catch ( e ) { console . error ( "adminFetch" , e ) ; return null ; }
}
// ---------- Panel öffnen / schließen ----------
function openAdminPanel ( ) {
$ ( "#admin-panel" ) . classList . remove ( "hidden" ) ;
2026-06-20 23:11:57 +02:00
switchAdminTab ( "overview" ) ;
2026-06-19 01:04:20 +02:00
}
$ ( "#admin-close" ) . addEventListener ( "click" , ( ) => $ ( "#admin-panel" ) . classList . add ( "hidden" ) ) ;
2026-06-20 23:11:57 +02:00
// ---------- Tab-Struktur: 5 Bereiche, je mit 1+ Abschnitten ----------
// section = vorhandener Content-Block (#admin-tab-<section>); Bereiche mit mehreren
// Abschnitten bekommen eine Sub-Navigation.
const ADMIN _TABS = {
overview : [ [ "overview" , "Übersicht" ] ] ,
users : [ [ "users" , "Verwalten" ] , [ "chat" , "Gespräche" ] ] ,
emergency : [ [ "emergency" , "Notfälle" ] ] ,
system : [ [ "status" , "Status" ] , [ "metrics" , "Metriken" ] , [ "log" , "Log" ] ] ,
config : [ [ "settings" , "Einstellungen" ] , [ "words" , "Wörterbuch" ] ] ,
} ;
const SECTION _LOADER = {
overview : ( ) => loadOverview ( ) ,
users : ( ) => loadAdminUsers ( ) ,
chat : ( ) => loadChatUsers ( ) ,
emergency : ( ) => loadEmergencyEvents ( ) ,
status : ( ) => loadStatus ( ) ,
metrics : ( ) => loadMetrics ( ) ,
words : ( ) => loadWoerterbuch ( ) ,
settings : ( ) => loadSettings ( ) ,
log : ( ) => { } , // Log: Nutzer verbindet manuell
} ;
function _setTabActive ( btn , active ) {
btn . classList . toggle ( "border-blue-600" , active ) ;
btn . classList . toggle ( "text-blue-600" , active ) ;
btn . classList . toggle ( "dark:border-blue-400" , active ) ;
btn . classList . toggle ( "dark:text-blue-400" , active ) ;
btn . classList . toggle ( "border-transparent" , ! active ) ;
btn . classList . toggle ( "text-slate-500" , ! active ) ;
}
function showAdminSection ( section , top ) {
2026-06-19 01:04:20 +02:00
document . querySelectorAll ( '[id^="admin-tab-"]' ) . forEach ( ( el ) => el . classList . add ( "hidden" ) ) ;
2026-06-20 23:11:57 +02:00
const panel = $ ( "#admin-tab-" + section ) ;
if ( panel ) panel . classList . remove ( "hidden" ) ;
// Sub-Tab markieren
document . querySelectorAll ( ".admin-subtab" ) . forEach ( ( b ) => {
const active = b . dataset . sec === section ;
b . classList . toggle ( "bg-blue-600" , active ) ;
b . classList . toggle ( "text-white" , active ) ;
b . classList . toggle ( "border-blue-600" , active ) ;
b . classList . toggle ( "text-slate-600" , ! active ) ;
b . classList . toggle ( "dark:text-slate-300" , ! active ) ;
b . classList . toggle ( "border-slate-300" , ! active ) ;
b . classList . toggle ( "dark:border-slate-600" , ! active ) ;
2026-06-19 01:04:20 +02:00
} ) ;
2026-06-20 23:11:57 +02:00
const loader = SECTION _LOADER [ section ] ;
if ( loader ) loader ( ) ;
}
function switchAdminTab ( top ) {
const sections = ADMIN _TABS [ top ] || [ [ top , top ] ] ;
document . querySelectorAll ( ".admin-tab" ) . forEach ( ( btn ) => _setTabActive ( btn , btn . dataset . tab === top ) ) ;
2026-06-19 01:04:20 +02:00
2026-06-20 23:11:57 +02:00
const subnav = $ ( "#admin-subnav" ) ;
if ( sections . length <= 1 ) {
subnav . classList . add ( "hidden" ) ;
subnav . innerHTML = "" ;
showAdminSection ( sections [ 0 ] [ 0 ] , top ) ;
} else {
subnav . classList . remove ( "hidden" ) ;
subnav . innerHTML = sections . map ( ( [ sec , label ] ) =>
` <button class="admin-subtab text-xs rounded-lg border px-3 py-1.5 whitespace-nowrap transition-colors border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700" data-sec=" ${ sec } "> ${ escHtml ( label ) } </button> `
) . join ( "" ) ;
subnav . querySelectorAll ( ".admin-subtab" ) . forEach ( ( b ) =>
b . addEventListener ( "click" , ( ) => showAdminSection ( b . dataset . sec , top ) )
) ;
showAdminSection ( sections [ 0 ] [ 0 ] , top ) ;
}
2026-06-19 01:04:20 +02:00
}
document . querySelectorAll ( ".admin-tab" ) . forEach ( ( btn ) =>
btn . addEventListener ( "click" , ( ) => switchAdminTab ( btn . dataset . tab ) )
) ;
2026-06-20 23:11:57 +02:00
$ ( "#load-overview" ) . addEventListener ( "click" , loadOverview ) ;
2026-06-19 01:04:20 +02:00
$ ( "#load-emergency" ) . addEventListener ( "click" , loadEmergencyEvents ) ;
$ ( "#load-status" ) . addEventListener ( "click" , loadStatus ) ;
$ ( "#load-metrics" ) . addEventListener ( "click" , loadMetrics ) ;
2026-06-20 23:11:57 +02:00
// ════════════════════════════════════════
// TAB: ÜBERSICHT (Dashboard)
// ════════════════════════════════════════
async function loadOverview ( ) {
const c = $ ( "#overview-content" ) ;
c . innerHTML = '<p class="text-sm text-slate-400">lade …</p>' ;
const [ users , usage , emergencies , llm ] = await Promise . all ( [
adminFetch ( "/api/admin/users" ) ,
adminFetch ( "/api/admin/usage" ) ,
adminFetch ( "/api/admin/emergency-events" ) ,
adminFetch ( "/api/admin/llm/status" ) ,
] ) ;
const userCount = users ? users . length : "—" ;
const reqTotal = usage ? usage . reduce ( ( s , u ) => s + ( u . total _requests || 0 ) , 0 ) : "—" ;
const emCount = emergencies ? emergencies . length : "—" ;
const backend = llm ? ( { ollama : "Ollama" , llamacpp : "llama.cpp" } [ llm . backend ] || llm . backend ) : "—" ;
const gpu = ( llm && llm . gpus && llm . gpus . length )
? llm . gpus . map ( ( g ) => ` GPU ${ g . index } : ${ g . percent } % ` ) . join ( " · " ) : "—" ;
2026-06-21 00:32:52 +02:00
const hasEmergencies = typeof emCount === "number" && emCount > 0 ;
// Kachel; mit data-jump wird sie anklickbar (springt in den Bereich).
const stat = ( label , value , sub , opts = { } ) => `
< $ { opts . jump ? ` button data-jump=" ${ opts . jump } " ` : "div" }
class = "ov-stat text-left bg-white dark:bg-slate-800 rounded-xl border ${opts.border || " border - slate - 200 dark : border - slate - 700 "} p-4 ${opts.jump ? " hover : bg - slate - 50 dark : hover : bg - slate - 700 / 50 transition - colors " : " "}" >
2026-06-20 23:11:57 +02:00
< p class = "text-xs text-slate-500 dark:text-slate-400" > $ { escHtml ( label ) } < / p >
2026-06-21 00:32:52 +02:00
< p class = "text-2xl font-semibold mt-1 ${opts.accent || " "}" > $ { escHtml ( value ) } < / p >
2026-06-20 23:11:57 +02:00
$ { sub ? ` <p class="text-xs text-slate-400 mt-0.5 truncate"> ${ escHtml ( sub ) } </p> ` : "" }
2026-06-21 00:32:52 +02:00
< / $ { o p t s . j u m p ? " b u t t o n " : " d i v " } > ` ;
2026-06-20 23:11:57 +02:00
const jump = ( to , label ) =>
` <button class="ov-jump text-sm rounded-lg border border-slate-300 dark:border-slate-600 px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors" data-jump=" ${ to } "> ${ escHtml ( label ) } </button> ` ;
c . innerHTML = `
< div class = "grid grid-cols-2 gap-3" >
2026-06-21 00:32:52 +02:00
$ { stat ( "Nutzer" , userCount , "" , { jump : "users" } ) }
$ { stat ( "Anfragen gesamt" , reqTotal , "" , { jump : "system" } ) }
$ { stat ( "Notfälle" , emCount , hasEmergencies ? "ansehen" : "keine" ,
{ jump : "emergency" ,
accent : hasEmergencies ? "text-red-600 dark:text-red-400" : "" ,
border : hasEmergencies ? "border-red-300 dark:border-red-800" : "" } ) }
$ { stat ( "LLM-Backend" , backend , llm ? llm . model : "" , { jump : "system" } ) }
2026-06-20 23:11:57 +02:00
< / d i v >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h4 class = "font-semibold text-sm mb-2" > GPU - Auslastung < / h 4 >
< p class = "text-xs font-mono text-slate-600 dark:text-slate-300" > $ { escHtml ( gpu ) } < / p >
< / d i v >
< div class = "flex flex-wrap gap-2" >
$ { jump ( "users" , "→ Nutzer" ) } $ { jump ( "system" , "→ System" ) } $ { jump ( "emergency" , "→ Notfälle" ) }
< / d i v > ` ;
2026-06-21 00:32:52 +02:00
c . querySelectorAll ( ".ov-jump, .ov-stat[data-jump]" ) . forEach ( ( b ) =>
2026-06-20 23:11:57 +02:00
b . addEventListener ( "click" , ( ) => switchAdminTab ( b . dataset . jump ) ) ) ;
}
2026-06-19 01:04:20 +02:00
// ════════════════════════════════════════
// TAB: NUTZER
// ════════════════════════════════════════
async function loadAdminUsers ( ) {
const container = $ ( "#admin-user-cards" ) ;
container . innerHTML = '<p class="text-sm text-slate-400 text-center py-4">lade …</p>' ;
const users = await adminFetch ( "/api/admin/users" ) ;
if ( ! users ) { container . innerHTML = '<p class="text-sm text-red-500 text-center py-4">Fehler beim Laden.</p>' ; return ; }
if ( ! users . length ) { container . innerHTML = '<p class="text-sm text-slate-400 text-center py-4">Noch keine Nutzer.</p>' ; return ; }
container . innerHTML = "" ;
users . forEach ( ( u ) => container . appendChild ( buildUserCard ( u ) ) ) ;
}
$ ( "#create-user-form" ) . addEventListener ( "submit" , async ( e ) => {
e . preventDefault ( ) ;
const nameEl = $ ( "#new-user-name" ) ;
const name = nameEl . value . trim ( ) ;
if ( ! name ) return ;
const data = await adminFetch ( "/api/admin/users" , "POST" , { display _name : name } ) ;
if ( data ? . token ) {
$ ( "#new-token-value" ) . textContent = data . token ;
$ ( "#new-token-box" ) . classList . remove ( "hidden" ) ;
nameEl . value = "" ;
loadAdminUsers ( ) ;
}
} ) ;
function buildUserCard ( u ) {
const card = document . createElement ( "div" ) ;
card . className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" ;
const badgeSSO = u . external _id
? ` <span class="rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 text-xs px-2 py-0.5">SSO</span> ` : "" ;
card . innerHTML = `
< div class = "flex items-start gap-3 flex-wrap" >
< div class = "flex-1 min-w-0" >
< div class = "flex items-center gap-2 flex-wrap mb-0.5" >
< span class = "font-semibold text-slate-900 dark:text-slate-100 user-dn" > $ { escHtml ( u . display _name ) } < / s p a n >
$ { badgeSSO }
< / d i v >
< p class = "text-xs text-slate-400 font-mono" > $ { u . user _id . slice ( 0 , 8 ) } … $ { u . external _id ? " · " + escHtml ( u . external _id ) : "" } < / p >
< p class = "text-xs text-slate-400" > seit $ { fmtDate ( u . created _at ) } < / p >
< / d i v >
< div class = "flex flex-wrap gap-1.5" >
< button class = "btn-rename text-xs rounded-lg border border-slate-300 dark:border-slate-600 px-2.5 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors" > Umbenennen < / b u t t o n >
< button class = "btn-token text-xs rounded-lg border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 px-2.5 py-1.5 hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors" > Token - Reset < / b u t t o n >
< button class = "btn-delete text-xs rounded-lg border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 px-2.5 py-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" > Löschen < / b u t t o n >
< / d i v >
< / d i v >
<!-- Token - Anzeige nach Reset -- >
< div class = "token-box hidden mt-3 rounded-lg bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700 p-3" >
< p class = "text-xs text-amber-700 dark:text-amber-300 font-medium mb-1" > Neuer Token ( einmalig — jetzt kopieren ! ) : < / p >
< code class = "token-val text-xs font-mono break-all text-amber-900 dark:text-amber-100 select-all" > < / c o d e >
< / d i v >
<!-- Umbenenn - Formular -- >
< div class = "rename-box hidden mt-3" >
< form class = "flex gap-2" >
< input type = "text" class = "rename-inp flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value = "${escHtml(u.display_name)}" / >
< button type = "submit" class = "rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-1.5 transition-colors" > Speichern < / b u t t o n >
< button type = "button" class = "rename-cancel 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" > Abbrechen < / b u t t o n >
< / f o r m >
< / d i v >
<!-- Erinnerungen -- >
< div class = "mt-3 pt-3 border-t border-slate-100 dark:border-slate-700" >
< button class = "btn-mem text-xs text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 flex items-center gap-1 transition-colors" >
< span class = "mem-arrow" > ▶ < / s p a n > E r i n n e r u n g e n
< / b u t t o n >
< div class = "mem-section hidden mt-2 space-y-1.5" >
< div class = "mem-list" > < / d i v >
< form class = "mem-add-form flex gap-2 mt-2" >
< input type = "text" placeholder = "Neue Erinnerung …"
class = "flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs 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-xs px-3 py-1.5 transition-colors" > Hinzufügen < / b u t t o n >
< / f o r m >
< / d i v >
< / d i v >
` ;
const uid = u . user _id ;
// Umbenennen
card . querySelector ( ".btn-rename" ) . addEventListener ( "click" , ( ) =>
card . querySelector ( ".rename-box" ) . classList . toggle ( "hidden" )
) ;
card . querySelector ( ".rename-cancel" ) . addEventListener ( "click" , ( ) =>
card . querySelector ( ".rename-box" ) . classList . add ( "hidden" )
) ;
card . querySelector ( ".rename-box form" ) . addEventListener ( "submit" , async ( e ) => {
e . preventDefault ( ) ;
const name = card . querySelector ( ".rename-inp" ) . value . trim ( ) ;
if ( ! name ) return ;
const res = await adminFetch ( ` /api/admin/users/ ${ uid } ` , "PUT" , { display _name : name } ) ;
if ( res ) {
card . querySelector ( ".user-dn" ) . textContent = name ;
card . querySelector ( ".rename-box" ) . classList . add ( "hidden" ) ;
}
} ) ;
// Token-Reset
card . querySelector ( ".btn-token" ) . addEventListener ( "click" , async ( ) => {
if ( ! confirm ( ` Token für „ ${ u . display _name } " zurücksetzen? \n Der alte Token wird sofort ungültig. ` ) ) return ;
const data = await adminFetch ( ` /api/admin/users/ ${ uid } /token ` , "POST" ) ;
if ( data ? . token ) {
card . querySelector ( ".token-val" ) . textContent = data . token ;
card . querySelector ( ".token-box" ) . classList . remove ( "hidden" ) ;
}
} ) ;
// Löschen
card . querySelector ( ".btn-delete" ) . addEventListener ( "click" , async ( ) => {
if ( ! confirm ( ` Nutzer „ ${ u . display _name } " und alle Daten unwiderruflich löschen? ` ) ) return ;
const res = await adminFetch ( ` /api/admin/users/ ${ uid } ` , "DELETE" ) ;
if ( res !== null ) card . remove ( ) ;
} ) ;
// Erinnerungen auf-/zuklappen
const memBtn = card . querySelector ( ".btn-mem" ) ;
const memSection = card . querySelector ( ".mem-section" ) ;
const memArrow = card . querySelector ( ".mem-arrow" ) ;
memBtn . addEventListener ( "click" , async ( ) => {
const open = ! memSection . classList . contains ( "hidden" ) ;
memSection . classList . toggle ( "hidden" , open ) ;
memArrow . textContent = open ? "▶" : "▼" ;
if ( ! open ) await renderMemories ( uid , card . querySelector ( ".mem-list" ) ) ;
} ) ;
// Erinnerung hinzufügen
card . querySelector ( ".mem-add-form" ) . addEventListener ( "submit" , async ( e ) => {
e . preventDefault ( ) ;
const inp = e . target . querySelector ( "input" ) ;
const content = inp . value . trim ( ) ;
if ( ! content ) return ;
const res = await adminFetch ( ` /api/admin/users/ ${ uid } /memories ` , "POST" , { content } ) ;
if ( res ) { inp . value = "" ; await renderMemories ( uid , card . querySelector ( ".mem-list" ) ) ; }
} ) ;
return card ;
}
async function renderMemories ( uid , container ) {
container . innerHTML = '<p class="text-xs text-slate-400">lade …</p>' ;
const mems = await adminFetch ( ` /api/admin/users/ ${ uid } /memories ` ) ;
if ( ! mems ? . length ) {
container . innerHTML = '<p class="text-xs text-slate-400 italic">Keine Erinnerungen vorhanden.</p>' ;
return ;
}
container . innerHTML = "" ;
mems . forEach ( ( m ) => {
const row = document . createElement ( "div" ) ;
row . className = "flex items-start gap-2 rounded-lg bg-slate-50 dark:bg-slate-700/50 px-3 py-2 text-xs" ;
row . innerHTML = `
< span class = "flex-1 text-slate-700 dark:text-slate-300 leading-relaxed" > $ { escHtml ( m . content ) } < / s p a n >
< button class = "shrink-0 text-slate-400 hover:text-red-500 dark:hover:text-red-400 transition-colors font-bold" title = "Löschen" > ✕ < / b u t t o n >
` ;
row . querySelector ( "button" ) . addEventListener ( "click" , async ( ) => {
await adminFetch ( ` /api/admin/users/ ${ uid } /memories/ ${ m . id } ` , "DELETE" ) ;
row . remove ( ) ;
} ) ;
container . appendChild ( row ) ;
} ) ;
}
// ════════════════════════════════════════
// TAB: GESPRÄCHE
// ════════════════════════════════════════
let _chatUser = null ;
async function loadChatUsers ( ) {
const list = $ ( "#chat-user-list" ) ;
list . innerHTML = '<li class="px-3 py-3 text-sm text-slate-400">lade …</li>' ;
const users = await adminFetch ( "/api/admin/users" ) ;
if ( ! users ) return ;
list . innerHTML = "" ;
users . forEach ( ( u ) => {
const li = document . createElement ( "li" ) ;
li . className = "px-3 py-2.5 text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex flex-col" ;
li . innerHTML = ` <span class="font-medium truncate"> ${ escHtml ( u . display _name ) } </span>
< span class = "text-xs text-slate-400 font-mono truncate" > $ { u . user _id . slice ( 0 , 8 ) } … < / s p a n > ` ;
li . addEventListener ( "click" , ( ) => {
document . querySelectorAll ( "#chat-user-list li" ) . forEach ( ( el ) =>
el . classList . remove ( "bg-blue-50" , "dark:bg-blue-900/20" , "text-blue-700" , "dark:text-blue-300" )
) ;
li . classList . add ( "bg-blue-50" , "dark:bg-blue-900/20" ) ;
_chatUser = u ;
loadChatSessions ( u ) ;
} ) ;
list . appendChild ( li ) ;
} ) ;
}
async function loadChatSessions ( u ) {
const panel = $ ( "#chat-right-panel" ) ;
panel . innerHTML = ` <p class="p-4 text-sm text-slate-400">lade Sessions für ${ escHtml ( u . display _name ) } …</p> ` ;
const sessions = await adminFetch ( ` /api/admin/users/ ${ u . user _id } /sessions ` ) ;
if ( ! sessions ? . length ) {
panel . innerHTML = '<p class="p-4 text-sm text-slate-400">Keine Sessions gefunden.</p>' ;
return ;
}
panel . innerHTML = `
< div class = "px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-800/50" >
< p class = "text-xs font-semibold text-slate-500 uppercase tracking-wide" > Sessions · $ { escHtml ( u . display _name ) } < / p >
< / d i v >
< div id = "session-list" class = "divide-y divide-slate-100 dark:divide-slate-800/60" > < / d i v >
` ;
const list = panel . querySelector ( "#session-list" ) ;
sessions . forEach ( ( s ) => {
const row = document . createElement ( "div" ) ;
row . className = "px-4 py-3 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" ;
row . innerHTML = `
< p class = "text-sm font-mono text-slate-700 dark:text-slate-300 truncate" > $ { escHtml ( s . id ) } < / p >
< p class = "text-xs text-slate-400 mt-0.5" > $ { fmtDate ( s . updated _at ) } · $ { s . msg _count } Nachrichten < / p >
` ;
row . addEventListener ( "click" , ( ) => loadTranscript ( s . id ) ) ;
list . appendChild ( row ) ;
} ) ;
}
async function loadTranscript ( sessionId ) {
const panel = $ ( "#chat-right-panel" ) ;
panel . innerHTML = ` <p class="p-4 text-sm text-slate-400">lade Transkript …</p> ` ;
const msgs = await adminFetch ( ` /api/admin/sessions/ ${ sessionId } /messages ` ) ;
if ( ! msgs ? . length ) {
panel . innerHTML = ` <div class="p-4">
< button class = "back-btn text-xs text-blue-600 hover:underline mb-3 block" > ← zurück zu Sessions < / b u t t o n >
< p class = "text-sm text-slate-400" > Keine Nachrichten . < / p > < / d i v > ` ;
panel . querySelector ( ".back-btn" ) . addEventListener ( "click" , ( ) => _chatUser && loadChatSessions ( _chatUser ) ) ;
return ;
}
const bubbles = msgs . map ( ( m ) => {
const isUser = m . role === "user" ;
return ` <div class="flex ${ isUser ? "justify-end" : "justify-start" } ">
< div class = " max - w - [ 80 % ] rounded - 2 xl px - 4 py - 2 text - sm whitespace - pre - wrap leading - relaxed
$ { isUser
? "rounded-br-sm bg-blue-600 text-white"
: "rounded-bl-sm border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-100" } " >
$ { escHtml ( m . content ) } < / d i v > < / d i v > ` ;
} ) . join ( "" ) ;
panel . innerHTML = `
< div class = "sticky top-0 px-4 py-2.5 border-b border-slate-200 dark:border-slate-700 bg-white/90 dark:bg-slate-800/90 backdrop-blur flex items-center gap-3" >
< button class = "back-btn text-xs text-blue-600 hover:underline" > ← Sessions < / b u t t o n >
< span class = "text-xs text-slate-400 font-mono truncate" > $ { escHtml ( sessionId ) } < / s p a n >
< / d i v >
< div class = "p-4 space-y-3" > $ { bubbles } < / d i v >
` ;
panel . querySelector ( ".back-btn" ) . addEventListener ( "click" , ( ) => _chatUser && loadChatSessions ( _chatUser ) ) ;
}
// ════════════════════════════════════════
// TAB: NOTFÄLLE
// ════════════════════════════════════════
async function loadEmergencyEvents ( ) {
const container = $ ( "#emergency-content" ) ;
container . innerHTML = '<p class="text-sm text-slate-400">lade …</p>' ;
const events = await adminFetch ( "/api/admin/emergency-events" ) ;
if ( ! events ) { container . innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>' ; return ; }
if ( ! events . length ) { container . innerHTML = '<p class="text-sm text-slate-400 py-4 text-center">Keine Notfall-Ereignisse protokolliert.</p>' ; return ; }
container . innerHTML = `
< div class = "overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700" >
< table class = "w-full text-sm" >
< thead class = "bg-slate-50 dark:bg-slate-800/60" >
< tr class = "text-xs text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-700" >
< th class = "text-left px-4 py-2.5" > Zeitpunkt < / t h >
< th class = "text-left px-4 py-2.5" > Nutzer < / t h >
< th class = "text-left px-4 py-2.5" > Kategorie < / t h >
< th class = "text-left px-4 py-2.5" > Textausschnitt < / t h >
< / t r >
< / t h e a d >
< tbody class = "divide-y divide-slate-100 dark:divide-slate-800 bg-white dark:bg-slate-800" >
$ { events . map ( ( ev ) => `
< tr class = "hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors" >
< td class = "px-4 py-2.5 text-xs text-slate-400 whitespace-nowrap" > $ { fmtDate ( ev . created _at ) } < / t d >
< td class = "px-4 py-2.5 text-xs font-medium" > $ { escHtml ( ev . display _name ) } < / t d >
< td class = "px-4 py-2.5" >
< span class = "rounded-full bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 text-xs px-2.5 py-0.5 font-medium" > $ { escHtml ( ev . category ) } < / s p a n >
< / t d >
2026-06-21 00:32:52 +02:00
< td class = "px-4 py-2.5 text-xs text-slate-600 dark:text-slate-300 max-w-[16rem] break-words" > $ { escHtml ( ev . snippet ) } < / t d >
2026-06-19 01:04:20 +02:00
< / t r > ` ) . j o i n ( " " ) }
< / t b o d y >
< / t a b l e >
< / d i v > ` ;
}
// ════════════════════════════════════════
// TAB: STATUS
// ════════════════════════════════════════
2026-06-20 18:33:06 +02:00
// LLM-/GPU-Status-Karte (read-only) für den Status-Tab.
2026-06-20 18:39:59 +02:00
// Wartet, bis das Gateway nach einem Neustart wieder antwortet, dann Status neu laden.
async function pollGatewayBack ( msgEl , label ) {
for ( let i = 0 ; i < 40 ; i ++ ) {
await new Promise ( ( r ) => setTimeout ( r , 1500 ) ) ;
try {
const r = await fetch ( "/api/config" , { cache : "no-store" } ) ;
if ( r . ok ) { if ( msgEl ) msgEl . textContent = label + " ✓" ; loadStatus ( ) ; return ; }
} catch ( e ) { /* noch nicht oben */ }
if ( msgEl ) msgEl . textContent = label + " … (" + ( i + 1 ) + ")" ;
}
if ( msgEl ) msgEl . textContent = label + " — Zeitüberschreitung, bitte Status manuell prüfen." ;
}
2026-06-25 02:27:18 +02:00
function wireRestart ( ) {
2026-06-20 18:39:59 +02:00
const restartBtn = $ ( "#gw-restart-btn" ) ;
const msg = $ ( "#llm-ctrl-msg" ) ;
2026-06-25 02:27:18 +02:00
if ( ! restartBtn ) return ;
2026-06-20 18:39:59 +02:00
restartBtn . addEventListener ( "click" , async ( ) => {
if ( ! confirm ( "Gateway-Dienst jetzt neu starten?" ) ) return ;
restartBtn . disabled = true ;
msg . textContent = "starte neu …" ;
const res = await adminFetch ( "/api/admin/gateway/restart" , "POST" ) ;
if ( res === null ) { msg . textContent = "Fehler beim Neustart" ; restartBtn . disabled = false ; return ; }
pollGatewayBack ( msg , "Neu gestartet" ) ;
} ) ;
}
2026-06-19 01:04:20 +02:00
async function loadStatus ( ) {
const container = $ ( "#status-content" ) ;
container . innerHTML = '<p class="text-sm text-slate-400">lade …</p>' ;
try {
2026-06-25 02:27:18 +02:00
const [ cfg , met , runtimeCfg ] = await Promise . all ( [
2026-06-19 01:04:20 +02:00
fetch ( "/api/config" ) . then ( ( r ) => r . json ( ) ) ,
fetch ( "/api/metrics" ) . then ( ( r ) => r . json ( ) ) ,
2026-06-25 02:27:18 +02:00
adminFetch ( "/api/admin/config" ) ,
2026-06-19 01:04:20 +02:00
] ) ;
2026-06-25 02:27:18 +02:00
const cfgByKey = Object . fromEntries ( ( runtimeCfg || [ ] ) . map ( ( d ) => [ d . key , d . effective _value ] ) ) ;
const llmModel = cfgByKey [ "openrouter_llm_model" ] || "—" ;
2026-06-19 01:04:20 +02:00
const route = cfg . default _route || { } ;
const counters = met . counters || { } ;
const turnsTotal = counters [ "turns_total" ] || 0 ;
const httpTotal = Object . entries ( counters )
. filter ( ( [ k ] ) => k . startsWith ( "http_requests_total" ) )
. reduce ( ( s , [ , v ] ) => s + v , 0 ) ;
const providerRow = ( label , val ) =>
` <dt class="text-slate-500 dark:text-slate-400"> ${ label } </dt>
< dd class = "font-mono text-xs text-slate-700 dark:text-slate-300" > $ { escHtml ( val || "—" ) } < / d d > ` ;
container . innerHTML = `
<!-- Gateway - Status -- >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< div class = "flex items-center gap-2 mb-3" >
< span class = "h-2.5 w-2.5 rounded-full bg-emerald-500 shadow shadow-emerald-300/60" > < / s p a n >
< h3 class = "font-semibold text-sm" > Gateway < / h 3 >
< span class = "text-xs text-emerald-600 dark:text-emerald-400 font-medium" > läuft < / s p a n >
< / d i v >
< dl class = "grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm" >
$ { providerRow ( "Profil" , cfg . profile ) }
$ { providerRow ( "Umgebung" , cfg . app _env ) }
< / d l >
< / d i v >
<!-- Provider -- >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "font-semibold text-sm mb-3" > Provider ( Standard - Route ) < / h 3 >
< dl class = "grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm" >
2026-06-25 02:27:18 +02:00
$ { providerRow ( "STT" , route . stt _provider ) }
$ { providerRow ( "LLM" , route . llm _provider ) }
$ { providerRow ( "LLM-Modell" , llmModel ) }
$ { providerRow ( "TTS" , route . tts _provider ) }
$ { providerRow ( "Sprache" , route . language ) }
2026-06-19 01:04:20 +02:00
< / d l >
< / d i v >
2026-06-25 02:27:18 +02:00
<!-- Wartung -- >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "font-semibold text-sm mb-1" > Wartung < / h 3 >
< p class = "text-xs text-slate-400 mb-3" > Startet den Gateway - Dienst neu ( z . B . nach Einstellungen , die einen Neustart erfordern ) . < / p >
< div class = "flex flex-wrap items-center gap-2" >
< button id = "gw-restart-btn" class = "rounded-lg border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 text-sm px-3 py-1.5 hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors" > Gateway neu starten < / b u t t o n >
< span id = "llm-ctrl-msg" class = "text-xs text-slate-400" > < / s p a n >
< / d i v >
< / d i v >
2026-06-20 18:33:06 +02:00
2026-06-19 01:04:20 +02:00
<!-- Laufzeit - Metriken -- >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "font-semibold text-sm mb-3" > Laufzeit - Metriken < / h 3 >
< dl class = "grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm" >
$ { providerRow ( "Turns gesamt" , turnsTotal ) }
$ { providerRow ( "HTTP-Anfragen" , httpTotal ) }
< / d l >
< / d i v >
<!-- Verfügbare Provider -- >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "font-semibold text-sm mb-3" > Verfügbare Provider < / h 3 >
< dl class = "grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm" >
$ { providerRow ( "STT" , ( cfg . available ? . stt _providers || [ ] ) . join ( ", " ) ) }
$ { providerRow ( "LLM" , ( cfg . available ? . llm _providers || [ ] ) . join ( ", " ) ) }
$ { providerRow ( "TTS" , ( cfg . available ? . tts _providers || [ ] ) . join ( ", " ) ) }
< / d l >
< / d i v >
2026-06-19 01:15:28 +02:00
<!-- Datenbank - Export -- >
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "font-semibold text-sm mb-1" > Datenbank - Backup < / h 3 >
< p class = "text-xs text-slate-400 mb-3" > SQLite - Datenbank als Datei herunterladen ( Backup / Migration ) . < / p >
< a href = "/api/admin/db-export" download = "voice-assistant.db"
class = "inline-flex items-center gap-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-white text-xs px-4 py-2 font-medium transition-colors" >
⬇ voice - assistant . db herunterladen
< / a >
< / d i v >
2026-06-19 01:04:20 +02:00
` ;
2026-06-25 02:27:18 +02:00
wireRestart ( ) ; // Buttons der LLM-Karte verdrahten (nach dem Rendern)
2026-06-19 01:04:20 +02:00
} catch ( e ) {
2026-06-19 01:33:49 +02:00
console . error ( "loadStatus error:" , e ) ;
2026-06-19 01:04:20 +02:00
container . innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>' ;
}
}
// ════════════════════════════════════════
// TAB: METRIKEN
// ════════════════════════════════════════
async function loadMetrics ( ) {
const container = $ ( "#metrics-content" ) ;
container . innerHTML = '<p class="text-sm text-slate-400">lade …</p>' ;
const data = await adminFetch ( "/api/admin/usage" ) ;
if ( ! data ) { container . innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>' ; return ; }
if ( ! data . length ) { container . innerHTML = '<p class="text-sm text-slate-400 py-4 text-center">Noch keine Nutzungsdaten vorhanden.</p>' ; return ; }
const totalReq = data . reduce ( ( s , u ) => s + ( u . total _requests || 0 ) , 0 ) ;
2026-06-19 01:15:28 +02:00
const maxReq = Math . max ( ... data . map ( ( u ) => u . total _requests || 0 ) , 1 ) ;
const bars = data . map ( ( u ) => {
const pct = Math . round ( ( ( u . total _requests || 0 ) / maxReq ) * 100 ) ;
return `
< div class = "flex items-center gap-3" >
< span class = "w-28 shrink-0 text-xs text-slate-600 dark:text-slate-300 truncate" > $ { escHtml ( u . display _name ) } < / s p a n >
< div class = "flex-1 h-4 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden" >
< div class = "h-full bg-blue-500 dark:bg-blue-600 rounded-full" style = "width:${pct}%" > < / d i v >
< / d i v >
< span class = "w-10 text-right text-xs font-mono text-slate-500 dark:text-slate-400" > $ { u . total _requests ? ? 0 } < / s p a n >
< / d i v > ` ;
} ) . join ( "" ) ;
2026-06-19 01:04:20 +02:00
container . innerHTML = `
< div class = "overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700 mb-4" >
< table class = "w-full text-sm" >
< thead class = "bg-slate-50 dark:bg-slate-800/60" >
< tr class = "text-xs text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-700" >
< th class = "text-left px-4 py-2.5" > Nutzer < / t h >
< th class = "text-right px-4 py-2.5" > Anfragen gesamt < / t h >
< th class = "text-right px-4 py-2.5" > Einheiten < / t h >
< th class = "text-right px-4 py-2.5" > Letzte Aktivität < / t h >
< / t r >
< / t h e a d >
< tbody class = "divide-y divide-slate-100 dark:divide-slate-800 bg-white dark:bg-slate-800" >
$ { data . map ( ( u ) => `
< tr class = "hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors" >
< td class = "px-4 py-2.5 font-medium" > $ { escHtml ( u . display _name ) } < / t d >
< td class = "px-4 py-2.5 text-right font-mono text-xs" > $ { u . total _requests ? ? 0 } < / t d >
< td class = "px-4 py-2.5 text-right font-mono text-xs" > $ { u . total _units ? ? 0 } < / t d >
< td class = "px-4 py-2.5 text-right text-xs text-slate-400" > $ { u . last _active || "—" } < / t d >
< / t r > ` ) . j o i n ( " " ) }
< / t b o d y >
< tfoot class = "border-t-2 border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/60" >
< tr class = "text-xs font-semibold text-slate-600 dark:text-slate-300" >
< td class = "px-4 py-2" > Gesamt < / t d >
< td class = "px-4 py-2 text-right font-mono" > $ { totalReq } < / t d >
< td class = "px-4 py-2" > < / t d >
< td class = "px-4 py-2" > < / t d >
< / t r >
< / t f o o t >
< / t a b l e >
< / d i v >
2026-06-19 01:15:28 +02:00
< div class = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" >
< h3 class = "font-semibold text-sm mb-4" > Anfragen je Nutzer < / h 3 >
< div class = "space-y-2.5" > $ { bars } < / d i v >
< / d i v >
` ;
}
// ════════════════════════════════════════
// TAB: WÖRTERBUCH
// ════════════════════════════════════════
async function loadWoerterbuch ( ) {
const lang = ( $ ( "#words-lang" ) || { } ) . value || "de" ;
const container = $ ( "#words-content" ) ;
container . innerHTML = '<p class="text-sm text-slate-400">lade …</p>' ;
const data = await adminFetch ( ` /api/admin/pronunciation/ ${ lang } ` ) ;
if ( ! data ) { container . innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>' ; return ; }
container . innerHTML = "" ;
const sections = [
[ "abbreviations" , "Abkürzungen" ] ,
[ "units" , "Einheiten" ] ,
[ "terms" , "Begriffe / Aussprache" ] ,
] ;
for ( const [ section , label ] of sections ) {
container . appendChild ( buildWortSection ( label , section , data [ section ] || { } , lang ) ) ;
}
}
function buildWortSection ( title , section , entries , lang ) {
const wrap = document . createElement ( "div" ) ;
wrap . className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4" ;
const rows = Object . entries ( entries ) . map ( ( [ k , v ] ) => `
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
< tr class = "row-edit cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors group"
data - key = "${escHtml(k)}" data - val = "${escHtml(v)}" title = "Zum Bearbeiten anklicken" >
2026-06-19 01:15:28 +02:00
< td class = "px-3 py-2 text-xs font-mono text-slate-700 dark:text-slate-300" > $ { escHtml ( k ) } < / t d >
< td class = "px-3 py-2 text-xs text-slate-400" > → < / t d >
< td class = "px-3 py-2 text-xs text-slate-700 dark:text-slate-300 font-medium" > $ { escHtml ( v ) } < / t d >
< td class = "px-3 py-2 text-right" >
< button class = "del-btn text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors text-xs opacity-0 group-hover:opacity-100"
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
data - key = "${escHtml(k)}" title = "Löschen" > ✕ < / b u t t o n >
2026-06-19 01:15:28 +02:00
< / t d >
< / t r > ` ) . j o i n ( " " ) ;
wrap . innerHTML = `
< h4 class = "font-semibold text-sm mb-3 flex items-center justify-between" >
$ { escHtml ( title ) }
< span class = "text-xs font-normal text-slate-400" > $ { Object . keys ( entries ) . length } Einträge < / s p a n >
< / h 4 >
$ { rows ? `
< div class = "overflow-x-auto mb-4" >
< table class = "w-full" >
< tbody class = "divide-y divide-slate-100 dark:divide-slate-700/60" > $ { rows } < / t b o d y >
< / t a b l e >
< / d i v > ` : ' < p c l a s s = " t e x t - x s t e x t - s l a t e - 4 0 0 i t a l i c m b - 4 " > K e i n e E i n t r ä g e . < / p > ' }
< form class = "add-form flex gap-2 flex-wrap" >
< input type = "text" placeholder = "Schlüssel" required
class = "flex-1 min-w-24 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" / >
< input type = "text" placeholder = "Ersetzung" required
class = "flex-1 min-w-24 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs 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-xs px-3 py-1.5 font-medium transition-colors whitespace-nowrap" > + Hinzufügen < / b u t t o n >
< / f o r m >
2026-06-19 01:04:20 +02:00
` ;
2026-06-19 01:15:28 +02:00
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
const form = wrap . querySelector ( ".add-form" ) ;
const [ keyInp , valInp ] = form . querySelectorAll ( "input" ) ;
const submitBtn = form . querySelector ( 'button[type="submit"]' ) ;
// Zeile anklicken -> Begriffspaar in die Editier-Felder laden, Button wird zu „Speichern".
wrap . querySelectorAll ( "tr.row-edit" ) . forEach ( ( tr ) =>
tr . addEventListener ( "click" , ( ) => {
keyInp . value = tr . dataset . key ;
valInp . value = tr . dataset . val ;
form . dataset . editKey = tr . dataset . key ; // Originalschlüssel merken (für Umbenennen)
submitBtn . textContent = "Speichern" ;
valInp . focus ( ) ;
} )
) ;
2026-06-19 01:15:28 +02:00
wrap . querySelectorAll ( ".del-btn" ) . forEach ( ( btn ) =>
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
btn . addEventListener ( "click" , async ( e ) => {
e . stopPropagation ( ) ; // nicht zugleich die Zeile zum Bearbeiten laden
2026-06-19 01:15:28 +02:00
const key = btn . dataset . key ;
if ( ! confirm ( ` Eintrag „ ${ key } " löschen? ` ) ) return ;
const res = await adminFetch (
` /api/admin/pronunciation/ ${ lang } / ${ section } / ${ encodeURIComponent ( key ) } ` , "DELETE"
) ;
if ( res !== null ) btn . closest ( "tr" ) . remove ( ) ;
} )
) ;
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
form . addEventListener ( "submit" , async ( e ) => {
2026-06-19 01:15:28 +02:00
e . preventDefault ( ) ;
const key = keyInp . value . trim ( ) ;
const value = valInp . value . trim ( ) ;
if ( ! key || ! value ) return ;
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
const editKey = form . dataset . editKey || "" ;
// Hinzufügen ODER Aktualisieren (gleicher Schlüssel wird überschrieben; Backend sortiert).
2026-06-19 01:15:28 +02:00
const res = await adminFetch ( ` /api/admin/pronunciation/ ${ lang } ` , "POST" , { section , key , value } ) ;
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
if ( res === null ) return ;
// Umbenannt -> alten Schlüssel entfernen (neuer wurde oben bereits angelegt).
if ( editKey && editKey !== key ) {
await adminFetch (
` /api/admin/pronunciation/ ${ lang } / ${ section } / ${ encodeURIComponent ( editKey ) } ` , "DELETE"
) ;
}
loadWoerterbuch ( ) ; // neu laden -> sortierte Liste, Formular zurück auf „+ Hinzufügen"
2026-06-19 01:15:28 +02:00
} ) ;
return wrap ;
}
$ ( "#load-words" ) . addEventListener ( "click" , loadWoerterbuch ) ;
$ ( "#words-lang" ) . addEventListener ( "change" , loadWoerterbuch ) ;
// ════════════════════════════════════════
// TAB: LOG
// ════════════════════════════════════════
let _logWs = null ;
function connectLog ( ) {
if ( _logWs ) return ;
const proto = location . protocol === "https:" ? "wss" : "ws" ;
_logWs = new WebSocket ( ` ${ proto } :// ${ location . host } /api/admin/log ` ) ;
const output = $ ( "#log-output" ) ;
const logStat = $ ( "#log-status" ) ;
const btnConn = $ ( "#log-connect" ) ;
const btnDisc = $ ( "#log-disconnect" ) ;
btnConn . disabled = true ;
btnDisc . disabled = false ;
btnDisc . classList . remove ( "opacity-40" ) ;
logStat . textContent = "verbinde …" ;
logStat . className = "text-xs text-amber-400" ;
_logWs . onopen = ( ) => {
logStat . textContent = "verbunden" ;
logStat . className = "text-xs text-emerald-400" ;
output . innerHTML = "" ;
} ;
_logWs . onmessage = ( e ) => {
const line = document . createElement ( "div" ) ;
line . textContent = e . data ;
output . appendChild ( line ) ;
output . scrollTop = output . scrollHeight ;
} ;
_logWs . onerror = ( ) => {
logStat . textContent = "Verbindungsfehler" ;
logStat . className = "text-xs text-red-400" ;
} ;
_logWs . onclose = ( ) => {
_logWs = null ;
btnConn . disabled = false ;
btnDisc . disabled = true ;
btnDisc . classList . add ( "opacity-40" ) ;
if ( ! logStat . className . includes ( "red" ) ) {
logStat . textContent = "getrennt" ;
logStat . className = "text-xs text-slate-400" ;
}
} ;
2026-06-19 01:04:20 +02:00
}
2026-06-19 01:15:28 +02:00
function disconnectLog ( ) {
if ( _logWs ) { _logWs . close ( ) ; }
}
$ ( "#log-connect" ) . addEventListener ( "click" , connectLog ) ;
$ ( "#log-disconnect" ) . addEventListener ( "click" , disconnectLog ) ;
$ ( "#log-clear" ) . addEventListener ( "click" , ( ) => { $ ( "#log-output" ) . innerHTML = "" ; } ) ;
2026-06-19 01:39:36 +02:00
// ════════════════════════════════════════
// TAB: EINSTELLUNGEN
// ════════════════════════════════════════
2026-06-19 02:35:40 +02:00
// Metadaten für jedes Feld: UI-Typ, Optionen, Test-Typ
const FIELD _META = {
2026-06-19 12:30:52 +02:00
default _stt _provider : { ui : "combo" , opts : [ "openrouter" , "faster-whisper" ] , test : "stt" } ,
default _llm _provider : { ui : "combo" , opts : [ "openrouter" , "local-openai-compatible" ] , test : "llm" } ,
default _tts _provider : { ui : "combo" , opts : [ "openrouter" , "piper" , "chatterbox" ] , test : "tts" } ,
2026-06-24 22:01:38 +02:00
default _language : { ui : "combo" , opts : [ "de" , "en" , "fr" , "es" , "it" , "pt" , "pl" , "ar" ] ,
labels : { de : "Deutsch" , en : "Englisch" , fr : "Französisch" , es : "Spanisch" , it : "Italienisch" , pt : "Portugiesisch" , pl : "Polnisch" , ar : "Arabisch" } } ,
2026-06-19 02:35:40 +02:00
openrouter _llm _model : { ui : "combo" , opts : [ "google/gemini-3.1-flash-lite" , "openai/gpt-4.1-mini" , "google/gemini-2.5-flash" , "anthropic/claude-haiku-4-5-20251001" , "meta-llama/llama-3.3-70b-instruct" ] , test : "llm" } ,
2026-06-19 12:39:14 +02:00
openrouter _tts _model : { ui : "combo" , opts : [ "google/gemini-3.1-flash-tts-preview" , "openai/gpt-4o-mini-tts" ] , test : "tts" , testProvider : "openrouter" } ,
openrouter _tts _voice : { ui : "combo" , opts : [ "Zephyr" , "Puck" , "Charon" , "Kore" , "Fenrir" , "Leda" , "Orus" , "Aoede" , "Callirrhoe" , "Enceladus" , "Algieba" , "Despina" , "Achernar" ] , test : "tts" , testProvider : "openrouter" } ,
2026-06-24 22:01:38 +02:00
piper _voice : { ui : "combo" , opts : [ "de_DE-thorsten-high" , "de_DE-kerstin-low" , "en_US-lessac-high" , "en_US-amy-medium" , "en_US-ryan-high" , "en_GB-cori-high" , "fr_FR-siwis-medium" , "fr_FR-tom-medium" , "es_ES-sharvard-medium" , "it_IT-paola-medium" , "it_IT-riccardo-x_low" , "pt_BR-faber-medium" , "pl_PL-gosia-medium" , "pl_PL-darkman-medium" , "ar_JO-kareem-medium" , "ru_RU-irina-medium" , "ru_RU-ruslan-medium" , "zh_CN-huayan-medium" ] , test : "tts" , testProvider : "piper" } ,
2026-06-19 02:35:40 +02:00
local _llm _system _prompt : { ui : "textarea" , test : "llm" } ,
local _llm _temperature : { ui : "range" , min : 0 , max : 2 , step : 0.1 , test : "llm" } ,
2026-06-20 18:46:33 +02:00
local _llm _top _p : { ui : "range" , min : 0 , max : 1 , step : 0.05 , test : "llm" } ,
2026-06-19 02:35:40 +02:00
local _llm _max _tokens : { ui : "number" , min : 0 , test : "llm" } ,
tts _normalize _level : { ui : "select" , opts : [ "auto" , "full" , "light" , "off" ] , test : "tts" } ,
audio _stream _default : { ui : "select" , opts : [ "true" , "false" ] ,
labels : { "true" : "Aktiviert (satzweises Streaming)" , "false" : "Deaktiviert (alles auf einmal)" } , test : "tts" } ,
memory _extraction _enabled : { ui : "select" , opts : [ "true" , "false" ] ,
labels : { "true" : "Aktiviert" , "false" : "Deaktiviert" } } ,
memory _extraction _every _n _turns : { ui : "number" , min : 1 , max : 20 } ,
daily _request _limit : { ui : "number" , min : 0 } ,
} ;
const FIELD _GROUPS = [
{ label : "Provider & Sprache" , keys : [ "default_stt_provider" , "default_llm_provider" , "default_tts_provider" , "default_language" ] } ,
{ label : "OpenRouter-Modelle & Stimmen" , keys : [ "openrouter_llm_model" , "openrouter_tts_model" , "openrouter_tts_voice" ] } ,
{ label : "Piper TTS" , keys : [ "piper_voice" ] } ,
2026-06-20 18:46:33 +02:00
{ label : "Lokales LLM" , keys : [ "local_llm_system_prompt" , "local_llm_temperature" , "local_llm_top_p" , "local_llm_max_tokens" ] } ,
2026-06-19 02:35:40 +02:00
{ label : "TTS-Verarbeitung" , keys : [ "tts_normalize_level" , "audio_stream_default" ] } ,
{ label : "Gedächtnis" , keys : [ "memory_extraction_enabled" , "memory_extraction_every_n_turns" ] } ,
{ label : "Limits" , keys : [ "daily_request_limit" ] } ,
] ;
let _settingsData = [ ] ;
2026-06-19 01:39:36 +02:00
async function loadSettings ( ) {
const container = $ ( "#settings-content" ) ;
container . innerHTML = '<p class="text-sm text-slate-400">lade …</p>' ;
const data = await adminFetch ( "/api/admin/config" ) ;
if ( ! data ) { container . innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>' ; return ; }
2026-06-19 02:35:40 +02:00
_settingsData = data ;
2026-06-19 01:39:36 +02:00
container . innerHTML = "" ;
2026-06-19 02:35:40 +02:00
const byKey = Object . fromEntries ( data . map ( ( d ) => [ d . key , d ] ) ) ;
FIELD _GROUPS . forEach ( ( group ) => {
const items = group . keys . map ( ( k ) => byKey [ k ] ) . filter ( Boolean ) ;
if ( ! items . length ) return ;
const section = document . createElement ( "div" ) ;
section . className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden" ;
const header = document . createElement ( "div" ) ;
header . className = "px-4 py-2.5 border-b border-slate-100 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-800/60" ;
header . innerHTML = ` <h4 class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide"> ${ escHtml ( group . label ) } </h4> ` ;
section . appendChild ( header ) ;
items . forEach ( ( item , idx ) => {
const row = buildSettingRow ( item ) ;
if ( idx > 0 ) row . classList . add ( "border-t" , "border-slate-100" , "dark:border-slate-700/60" ) ;
section . appendChild ( row ) ;
} ) ;
container . appendChild ( section ) ;
} ) ;
}
function _buildControl ( item , meta ) {
const val = item . effective _value ;
const id = "cfg-" + item . key ;
const cls = "rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-2 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" ;
if ( meta . ui === "select" ) {
const labels = meta . labels || { } ;
const options = meta . opts . map ( ( o ) =>
` <option value=" ${ escHtml ( o ) } " ${ o === val ? "selected" : "" } > ${ escHtml ( labels [ o ] || o ) } </option> `
) . join ( "" ) ;
return ` <select id=" ${ id } " class=" ${ cls } flex-1"> ${ options } </select> ` ;
}
if ( meta . ui === "combo" ) {
const inList = meta . opts . includes ( val ) ;
2026-06-19 12:30:52 +02:00
const labels = meta . labels || { } ;
2026-06-19 02:35:40 +02:00
const options = meta . opts . map ( ( o ) =>
2026-06-19 12:30:52 +02:00
` <option value=" ${ escHtml ( o ) } " ${ o === val ? "selected" : "" } > ${ escHtml ( labels [ o ] || o ) } </option> `
2026-06-19 02:35:40 +02:00
) . join ( "" ) ;
const customSel = ! inList ? 'selected' : '' ;
return `
< div class = "flex-1 space-y-1.5" >
< select class = "combo-sel w-full ${cls}" >
$ { options }
< option value = "__custom__" $ { customSel } > ✏ Eigener Wert … < / o p t i o n >
< / s e l e c t >
< input id = "${id}" type = "text" value = "${escHtml(val)}" placeholder = "Eigenen Wert eingeben"
class = "combo-inp w-full ${cls} ${inList ? " hidden " : " "}" / >
< / d i v > ` ;
}
if ( meta . ui === "range" ) {
return `
< div class = "flex-1 flex items-center gap-3" >
< input id = "${id}" type = "range" min = "${meta.min}" max = "${meta.max}" step = "${meta.step}" value = "${escHtml(val)}"
class = "flex-1 accent-blue-600" / >
< span class = "range-val text-xs font-mono w-8 text-right" > $ { escHtml ( val ) } < / s p a n >
< / d i v > ` ;
}
if ( meta . ui === "number" ) {
const extra = meta . max !== undefined ? ` max=" ${ meta . max } " ` : "" ;
return ` <input id=" ${ id } " type="number" min=" ${ meta . min ? ? 0 } " ${ extra } value=" ${ escHtml ( val ) } " class=" ${ cls } flex-1" /> ` ;
}
if ( meta . ui === "textarea" ) {
return ` <textarea id=" ${ id } " rows="3" class=" ${ cls } flex-1 resize-y"> ${ escHtml ( val ) } </textarea> ` ;
}
return ` <input id=" ${ id } " type="text" value=" ${ escHtml ( val ) } " class=" ${ cls } flex-1" /> ` ;
2026-06-19 01:39:36 +02:00
}
function buildSettingRow ( item ) {
2026-06-19 02:35:40 +02:00
const meta = FIELD _META [ item . key ] || { ui : "text" } ;
2026-06-19 01:39:36 +02:00
const overridden = item . is _overridden ;
2026-06-19 02:35:40 +02:00
const envName = item . key . toUpperCase ( ) ;
const testType = meta . test ;
const wrap = document . createElement ( "div" ) ;
wrap . className = "px-4 py-3" + ( overridden ? " bg-amber-50/40 dark:bg-amber-900/10" : "" ) ;
2026-06-19 01:39:36 +02:00
2026-06-19 02:35:40 +02:00
const testBtn = testType
? ` <button class="test-btn text-xs rounded-lg border border-slate-300 dark:border-slate-600 px-2.5 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors whitespace-nowrap"> ${ testType === "tts" ? "🔊 Test" : testType === "llm" ? "💬 Test" : "🎤 Test" } </button> `
: "" ;
const resetBtn = overridden
? ` <button class="reset-btn text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors whitespace-nowrap">↩ Reset</button> `
: "" ;
2026-06-19 01:39:36 +02:00
wrap . innerHTML = `
2026-06-19 02:35:40 +02:00
< div class = "flex items-center gap-2 mb-2 flex-wrap" >
< span class = "font-medium text-sm flex-1" > $ { escHtml ( item . label ) } < / s p a n >
$ { overridden ? '<span class="text-xs rounded-full bg-amber-200 dark:bg-amber-800/60 text-amber-800 dark:text-amber-200 px-2 py-0.5 font-medium">überschrieben</span>' : "" }
$ { resetBtn }
< / d i v >
< div class = "flex gap-2 items-start flex-wrap" >
$ { _buildControl ( item , meta ) }
< div class = "flex flex-col gap-1.5 shrink-0" >
< button class = "save-btn rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-2 font-medium transition-colors" > Speichern < / b u t t o n >
$ { testBtn }
2026-06-19 01:39:36 +02:00
< / d i v >
< / d i v >
2026-06-19 02:35:40 +02:00
< div class = "flex items-baseline gap-2 mt-1.5 text-xs text-slate-400 flex-wrap" >
< span class = "font-mono font-medium text-slate-500 dark:text-slate-400" > $ { envName } < / s p a n >
< span class = "text-slate-300 dark:text-slate-600" > · < / s p a n >
< span > . env : < span class = "font-mono" > $ { escHtml ( item . base _value ) || "—" } < / s p a n > < / s p a n >
< span class = "text-slate-300 dark:text-slate-600" > · < / s p a n >
< span > $ { escHtml ( item . hint ) } < / s p a n >
2026-06-19 01:39:36 +02:00
< / d i v >
2026-06-19 02:35:40 +02:00
< div class = "feedback-msg text-xs mt-1 hidden" > < / d i v >
< div class = "test-result text-xs mt-2 hidden rounded-lg bg-slate-100 dark:bg-slate-700/60 px-3 py-2 font-mono whitespace-pre-wrap max-h-28 overflow-y-auto" > < / d i v >
2026-06-19 01:39:36 +02:00
` ;
2026-06-19 02:35:40 +02:00
// Combo-Synchronisation: Dropdown ↔ Textfeld
const comboSel = wrap . querySelector ( ".combo-sel" ) ;
const comboInp = wrap . querySelector ( ".combo-inp" ) ;
if ( comboSel && comboInp ) {
comboSel . addEventListener ( "change" , ( ) => {
if ( comboSel . value === "__custom__" ) {
comboInp . classList . remove ( "hidden" ) ;
comboInp . focus ( ) ;
} else {
comboInp . value = comboSel . value ;
comboInp . classList . add ( "hidden" ) ;
}
} ) ;
comboInp . addEventListener ( "input" , ( ) => {
const match = Array . from ( comboSel . options ) . find ( ( o ) => o . value === comboInp . value ) ;
comboSel . value = match ? match . value : "__custom__" ;
} ) ;
}
2026-06-19 01:39:36 +02:00
2026-06-19 02:35:40 +02:00
// Range-Slider: Wert live anzeigen
const rangeInp = wrap . querySelector ( 'input[type="range"]' ) ;
if ( rangeInp ) {
rangeInp . addEventListener ( "input" , ( ) => {
wrap . querySelector ( ".range-val" ) . textContent = rangeInp . value ;
} ) ;
}
// Aktuellen Wert holen (aus Combo-Textfeld oder normalem Input)
function getCurrentValue ( ) {
if ( comboInp ) return comboInp . value . trim ( ) ;
const inp = wrap . querySelector ( ` #cfg- ${ item . key } ` ) ;
return inp ? ( inp . value ? ? inp . textContent ? ? "" ) . trim ( ) : "" ;
}
// Feedback-Anzeige
function showFeedback ( ok , msg ) {
const fb = wrap . querySelector ( ".feedback-msg" ) ;
fb . textContent = msg ;
fb . className = "feedback-msg text-xs mt-1 " + ( ok ? "text-emerald-600 dark:text-emerald-400" : "text-red-500" ) ;
setTimeout ( ( ) => { fb . className = "feedback-msg text-xs mt-1 hidden" ; } , 3000 ) ;
}
// Speichern
2026-06-19 01:39:36 +02:00
wrap . querySelector ( ".save-btn" ) . addEventListener ( "click" , async ( ) => {
2026-06-19 02:35:40 +02:00
const val = getCurrentValue ( ) ;
2026-06-19 01:39:36 +02:00
const res = await adminFetch ( ` /api/admin/config/ ${ item . key } ` , "PUT" , { value : val } ) ;
2026-06-19 02:35:40 +02:00
if ( res !== null ) {
showFeedback ( true , "✓ Gespeichert" ) ;
item . is _overridden = true ;
item . effective _value = val ;
// Overrides-Badge nachrüsten falls noch nicht da
if ( ! wrap . querySelector ( ".reset-btn" ) ) {
wrap . querySelector ( ".flex.items-center.gap-2" ) . insertAdjacentHTML ( "beforeend" ,
'<button class="reset-btn text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors whitespace-nowrap">↩ Reset</button>' ) ;
wrap . querySelector ( ".reset-btn" ) . addEventListener ( "click" , doReset ) ;
}
} else {
showFeedback ( false , "✗ Fehler beim Speichern" ) ;
}
2026-06-19 01:39:36 +02:00
} ) ;
2026-06-19 02:35:40 +02:00
// Reset
async function doReset ( ) {
if ( ! confirm ( ` Override für „ ${ item . label } " entfernen? \n Der .env-Wert gilt danach wieder. ` ) ) return ;
const res = await adminFetch ( ` /api/admin/config/ ${ item . key } ` , "DELETE" ) ;
if ( res !== null ) loadSettings ( ) ;
}
wrap . querySelector ( ".reset-btn" ) ? . addEventListener ( "click" , doReset ) ;
// Test
const testBtnEl = wrap . querySelector ( ".test-btn" ) ;
if ( testBtnEl ) {
testBtnEl . addEventListener ( "click" , async ( ) => {
const resultEl = wrap . querySelector ( ".test-result" ) ;
resultEl . textContent = "…" ;
resultEl . classList . remove ( "hidden" ) ;
testBtnEl . disabled = true ;
try {
if ( testType === "tts" ) {
2026-06-19 12:39:14 +02:00
const body = { text : "Hallo, ich bin der Sprachassistent." } ;
if ( meta . testProvider ) body . tts _provider = meta . testProvider ;
2026-06-19 02:35:40 +02:00
const r = await fetch ( "/api/speak" , {
method : "POST" , headers : { "Content-Type" : "application/json" } ,
2026-06-19 12:39:14 +02:00
body : JSON . stringify ( body ) ,
2026-06-19 02:35:40 +02:00
} ) ;
if ( ! r . ok ) throw new Error ( "HTTP " + r . status ) ;
const buf = await r . arrayBuffer ( ) ;
const view = new DataView ( buf ) ;
const n = Math . floor ( buf . byteLength / 2 ) ;
audioCtx = audioCtx || new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
const abuf = audioCtx . createBuffer ( 1 , n , 24000 ) ;
const ch = abuf . getChannelData ( 0 ) ;
for ( let i = 0 ; i < n ; i ++ ) ch [ i ] = view . getInt16 ( i * 2 , true ) / 32768 ;
const src = audioCtx . createBufferSource ( ) ;
src . buffer = abuf ; src . connect ( audioCtx . destination ) ; src . start ( ) ;
resultEl . textContent = "▶ Audio spielt …" ;
src . onended = ( ) => { resultEl . classList . add ( "hidden" ) ; } ;
} else if ( testType === "llm" ) {
const r = await fetch ( "/api/chat?debug=true" , {
method : "POST" , headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { text : "Sag kurz Hallo." , tts _provider : "piper" } ) ,
} ) ;
const d = await r . json ( ) ;
resultEl . textContent = d . response || d . semantic || d . trace ? . response || JSON . stringify ( d ) . slice ( 0 , 200 ) ;
} else if ( testType === "stt" ) {
const cfg = await fetch ( "/api/config" ) . then ( ( r ) => r . json ( ) ) ;
const available = cfg . available ? . stt _providers || [ ] ;
const current = item . effective _value ;
resultEl . textContent = available . includes ( current )
? ` ✓ Provider „ ${ current } " ist verfügbar. `
: ` ✗ Provider „ ${ current } " nicht in verfügbaren: ${ available . join ( ", " ) } ` ;
}
} catch ( e ) {
resultEl . textContent = "Fehler: " + e . message ;
}
testBtnEl . disabled = false ;
2026-06-19 01:39:36 +02:00
} ) ;
}
return wrap ;
}
$ ( "#load-settings" ) . addEventListener ( "click" , loadSettings ) ;