Commit graph

93 commits

Author SHA1 Message Date
5d05e081d4 feat: Notruf — Telefon in SMS, Geo-Diagnose + Geräteerkennung; Sprach-UI-Fix
Notruf (Punkte 1+2):
- SMS enthält jetzt die Telefonnummer(n) der Person (Mobil/Festnetz).
- Geo-Diagnose (P1): captureGeo liefert bei Fehlschlag einen Grund
  (Permission/insecure/Timeout/Code) statt still null; geht als geo_error an
  /api/emergency, landet in E-Mail ("Standort nicht verfügbar (<Grund>)") und
  im Server-Log (logger.info) — Fehlversuche sind jetzt erklärbar.
- Geräteerkennung (P2): device {mobile, platform} via User-Agent Client Hints
  (Fallback UA-String); E-Mail zeigt "Auslösegerät: Mobilgerät/Festgerät (…)".
- schemas: DeviceInfo + EmergencyRequest.geo_error/device; chat reicht durch.
- Tests: Telefon in SMS, geo_error + Geräte-Zeile in E-Mail (240 grün).

Sprach-Bug (Punkt 3): allowed_languages wurde im Menü nur via <option hidden>
gefiltert, was manche Browser bei <select> ignorieren -> Sprachen blieben
wählbar. Jetzt zusätzlich opt.disabled (robust, nicht wählbar); Backend klemmt
ohnehin serverseitig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:24:36 +02:00
2a013ae9fe feat: Notruf-Frontend — Geoposition + Admin-Notfallprofil (Phase 2)
- 🆘-Handler erfasst beim Auslösen die Geoposition (One-Shot, navigator.
  geolocation, enableHighAccuracy) und hängt sie als geo:{lat,lon,accuracy,ts}
  an /api/emergency. Timeout-Race (8 s), damit hängendes GPS den Alarm nie
  blockiert; nur mit Freigabe (location_consent).
- loadMe: Prefs global gehalten; bei location_consent Standort-Erlaubnis vorab
  angefragt (kein Browser-Dialog im Panikmoment).
- Admin-User-Karte: einklappbares „Notfall-Profil" zum Pflegen von Name,
  Adresse, Telefonen, Geburtsdatum, med. Hinweisen, Kontakt-E-Mail/-Telefon
  und der Standort-Freigabe; speichert via PUT /api/admin/users/{id}/prefs.

Nutzt das in Phase 1+3 gebaute Backend; keine Backend-Änderung. app.js per
node --check validiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:48:51 +02:00
5ed4a386bb feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
  Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
  auf SMS/E-Mail

- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
  erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
  emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
  kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
  ohne-Geo-Fallback, Payload). Suite 237 grün.

Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
1816a26734 feat: Notruf-Eskalation per SMS/Anruf (provider-agnostischer Webhook)
Zweiter Benachrichtigungskanal neben der E-Mail: bei einem Notruf geht
zusätzlich ein JSON-POST an EMERGENCY_WEBHOOK_URL mit Kanälen (sms,call),
Kontakt-Telefonnummern und fertigen SMS-/Anruf-Texten. Den konkreten
Gateway (Twilio, seven.io, sipgate, eigener Dienst) verdrahtet der
Empfänger dahinter — kein Provider-Lock-in im Code.

- config: EMERGENCY_WEBHOOK_URL aktiviert + _TOKEN (Bearer), _CONTACT_PHONE
  (CSV/E.164), _WEBHOOK_CHANNELS (default sms,call)
- emergency.py: async httpx-POST (best effort), Telefon-Auflösung analog
  zur E-Mail (Pref emergency_phones > Default), notified = email OR webhook;
  Hinweise kanal-neutral formuliert; Response um webhook_sent + channels ergänzt
- Tests: Webhook-Versand inkl. Payload/Bearer-Header + "ohne URL kein POST"
- Doku: §10 + Env-Var-Tabelle aktualisiert (zwei Kanäle, Payload-Format)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:17:49 +02:00
2bec4e72db feat(3c): OpenRouter-Modelle live & selbst-aktualisierend statt erfunden
- Neuer Endpunkt GET /api/admin/openrouter/models (10 Min gecacht) liefert
  die echte Modell-Liste von OpenRouter (live 339 Modelle).
- Konfiguration: openrouter_llm_model/-tts_model nutzen jetzt ein
  <datalist> aus dieser Live-Liste (tippen/auswählen) — die fest
  verdrahteten, teils erfundenen Namen (z.B. gemini-3.1-flash-lite) sind weg.
- Test mit gemocktem httpx. app.js v=48. 220 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 05:01:05 +02:00
d296bad9f2 feat(3b): Einstellungen vereinfacht — wichtige sichtbar, „Erweitert" einklappbar
Statt 7 technischer Gruppen jetzt 3: „Sprache & Stimme" und
„Antwortverhalten" (die alltagsrelevanten Schalter) immer sichtbar;
alle technischen Regler (STT/LLM-Provider, OpenRouter-Modelle, Temperatur,
top_p, Tokens, Systemprompt, Normalisierung …) unter „Erweitert", das
standardmäßig eingeklappt ist. app.js v=47.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:54:56 +02:00
5b1a36c976 feat(3a): Admin-Struktur aufgeräumt + Restart nach „Wartung"
- 4 ruhige Top-Tabs: Übersicht · Nutzer · System · Konfiguration.
- „Notrufe" wandert unter System; „Metriken" → „Nutzungs-Statistik" unter
  Nutzer einsortiert.
- Status ist jetzt rein lesend (Wartung + DB-Backup raus).
- Neuer „Wartung"-Abschnitt unter Konfiguration mit prominentem
  „Gateway neu starten" + DB-Backup. app.js v=46.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:52:09 +02:00
6e824d8f9b feat(2-Schritt3): nginx-Cutover — App per Token-Link, Admin hinter Authelia
Sicherheits-Architektur umgestellt (live verifiziert):
- App-Routen (/, /ws/) ohne Authelia -> Auth via Capability-Token-Cookie
  (Senioren: Ein-Klick-Link, kein 2FA). Remote-User wird in nginx HART
  geleert (Anti-Spoofing: App vertraut Remote-User von der Proxy-IP).
- /api/admin/* (und /api/admin/log) zusätzlich hinter Authelia (Doppelschutz).
- gate_web_ui: statt SSO-Redirect (würde nach Cutover schleifen) eine
  freundliche "Persönlicher Zugang nötig"-Hinweisseite (401).

Endabnahme durch nginx: Link->303+Cookie, /api/me->200 (kein 2FA),
Admin->302 Authelia, Header-Spoofing->401. DEPLOYMENT.md 2.8 aktualisiert.
Test auf neue Hinweisseite angepasst. 219 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:45:11 +02:00
bd55807a1c feat(2-Schritt2): Login-Link + persistentes is_admin-Flag pro Nutzer
- Nutzerverwaltung zeigt beim Anlegen/„Link erneuern" jetzt den vollen
  Ein-Klick-Link (location.origin/?k=<token>) statt nur des Tokens.
- Neuer Admin-Schalter pro Nutzer (DB-Spalte users.is_admin + Migration).
  is_admin_user() honoriert das Flag ODER ADMIN_USERS (SSO). Dadurch zeigt
  /me den Admin-Button anhand des Flags — Grundlage für den nginx-Cutover.
- admin.py: PUT /admin/users/{id}/admin; list_users liefert is_admin.

Migration live verifiziert (Spalte vorhanden, bestehende Admins via
ADMIN_USERS weiterhin korrekt). app.js v=45. 219 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:33:33 +02:00
47daf918ae feat(2-Schritt1): Additiver Ein-Klick-Login per Capability-Token
Zweiter Auth-Weg NEBEN Authelia (nichts schaltet um, kein Aussperr-Risiko):
- Link https://voice.jamulix.de/?k=<token> -> Token landet im va_token-Cookie
  (HttpOnly, Secure, SameSite=Lax, 1 Jahr), Token wird aus der URL entfernt.
- Cookie/?k= authentifiziert App-Routen (require_user) und WS (/ws/voice,
  /ws/chat). Token = vorhandenes Nutzer-Token (get_user_by_token).
- authenticate(): fehlt der SSO-Header, wird auf Token-Auth durchgefallen
  (statt hartem 401) — Voraussetzung für die spätere nginx-Trennung.

Authelia/Forward-Auth bleibt voll funktionsfähig (Live verifiziert: beide
Wege gehen). Tests: Cookie-, ?k=- und Negativ-Pfad. 217 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:24:52 +02:00
cc354938df feat(2a): Erlaubte Sprachen pro Nutzer (Admin-Vorgabe)
Der Admin schaltet pro Nutzer Sprachen frei (Toggle-Chips in der
Nutzerverwaltung). Die Haupt-App zeigt dann nur diese Sprachen; bei genau
einer erlaubten Sprache wird das Sprachmenü ausgeblendet (kein Stress).

- admin.py: PUT /admin/users/{id}/prefs (allowed_languages), list_users
  liefert allowed_languages mit.
- dependencies.py: resolve_route klemmt die Sprache auf eine erlaubte
  (Backend-Durchsetzung, nicht nur UI).
- app.js: applyAllowedLanguages (Haupt-UI-Filter) + Sprach-Chips je Nutzer
  im Admin. app.js v=44.
- Tests: Klemmung + Admin-Endpunkt. 213 passed.

Login-/nginx-Umbau (Ein-Klick-Link) folgt nach separater Design-Abstimmung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:06:09 +02:00
c35fe6e187 feat(P2): Notruf verschickt E-Mail an Kontaktperson (Testphase)
Beim Notruf-Knopf wird – sofern SMTP konfiguriert ist – eine E-Mail an die
Kontaktperson(en) des Nutzers geschickt (Default: dieter.schlueter@linix.de).
Der Nutzer-Hinweis spiegelt ehrlich wider, ob wirklich etwas rausging:
"Kontaktperson per E-Mail benachrichtigt" vs. "noch keine Benachrichtigung".

- config.py: EMERGENCY_CONTACT_EMAIL + SMTP_* (ohne SMTP_HOST kein Versand);
  veralteten LLM-Klassifikator-Kommentar entfernt.
- emergency.py: SMTP-Versand (best effort, im Thread), lokalisierte Hinweise
  für "gesendet"/"nicht gesendet" (10 Sprachen). Pro-Nutzer-Kontakte via
  prefs.emergency_contacts vorbereitet, Fallback auf Config-Default.
- Test mit gemocktem smtplib. env.example + DEPLOYMENT.md 2.5 dokumentiert.

SMS/Anruf-Eskalation folgt später (eigene Planung). 210 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:37:18 +02:00
a7f718192f feat(P3): Diktat in Fremdsprache → Übersetzung in die Zielsprache
Spricht der Nutzer in einer anderen als der eingestellten Sprache, erkennt
das System die gesprochene Sprache automatisch (transcribe_detect) und
übersetzt die Anfrage per LLM in die Zielsprache — sie wird dann auch in
der Zielsprache angezeigt und verarbeitet. Hilft beim Sprachenlernen und in
der Kommunikation über Sprachgrenzen.

- Orchestrator.translate() (LLM-basiert, nur Übersetzung).
- ws.py Voice-STT: transcribe_detect + Übersetzung bei abweichender Sprache.
- Test-Stubs um transcribe_detect ergänzt; neuer Übersetzungstest. 209 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:30:37 +02:00
40291d318b fix(ui): Notruf-Button dezent in den Header; Log live & durchscrollend
P1: Der prominente rote Footer-Banner war erdrückend ("erinnert ständig an
Not"). Ersatz: kleiner, quadratischer, dezenter 🆘-Button links neben der
Sprachauswahl im Header.

P4: journalctl -f wird via `stdbuf -oL` zeilengepuffert — neue Log-Zeilen
kommen jetzt sofort (statt blockweise) im Browser an und scrollen durch.
app.js-Cache-Bust auf v=43.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:26:50 +02:00
3389f75af0 feat(1b): Automatische Notfallerkennung raus → manueller Notruf-Knopf
Die unzuverlässige/intransparente Auto-Erkennung (Stichwörter + LLM) ist
ersatzlos entfernt: emergency.py neu (nur manueller, vom Nutzer bestätigter
Notruf), llm_classifier.py gelöscht, Aufrufe in chat.py/ws.py raus,
emergency_llm_* aus der Config.

Neu: prominenter "🆘 Hilfe rufen"-Knopf (Bestätigung) → POST /api/emergency
→ protokolliert + liefert Hinweis in Nutzersprache. Eskalation (Benachrichtigung
an Angehörige) folgt später; vorerst Platzhalter "ACHTUNG: Es ist noch keine
Benachrichtigung eingebaut." (10 Sprachen). Admin-Tab "Notfälle" → "Notrufe".

Tests: test_emergency_llm.py entfernt, test_quota_safety.py auf Notruf-Endpunkt
umgeschrieben. 208 passed. Live verifiziert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:02:52 +02:00
be5206c7ed feat(1a): Flex-Sprachmodus vollständig entfernen
Sprache ist immer fest. Entfernt: language_mode aus Route/Schema/Config/
Runtime-Config, die Flex-Verzweigung in Orchestrator (effective_language =
language) und ws.py (immer normales transcribe), die "🔄 Flex"-Option im
Frontend samt zugehöriger app.js-Logik (langOverrides, loadMe, Turn-Sprache).

Die generische STT-Methode transcribe_detect bleibt als wiederverwendbare
Infrastruktur erhalten (nicht mehr aufgerufen). Test: Route trägt kein
language_mode mehr. 215 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 02:51:17 +02:00
f3f9151894 fix(admin): Phase 0 — Log & Restart reparieren, Status auf Wahrheit
Wurzel-Ursache: Dienst läuft als System-Dienst, Code nutzte aber
systemctl/journalctl --user → Log leer, Restart wirkungslos.

- Log-WS (admin.py): --user → System-journalctl. Erfordert voice in
  Gruppe systemd-journal (siehe DEPLOYMENT.md 2.7.1).
- Restart (admin_llm.py): --user → sudo -n systemctl restart über eng
  begrenzte sudoers-Regel; is-active ebenfalls auf System-Dienst.
- Status (app.js): irrelevante Ollama/llama.cpp-Karte + Backend-Wechsel
  entfernt; zeigt jetzt echtes aktives LLM-Modell (aus Runtime-Config);
  Neustart in eigener "Wartung"-Karte (zieht in Phase 3 nach Konfiguration).
- DEPLOYMENT.md 2.7.1: sudoers-Regel + journal-Gruppe dokumentiert.

Live verifiziert: Log lesbar, Restart ohne Passwort, /health nach 3s zurück.
Tests: 214 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 02:27:18 +02:00
284228b5fa fix: audioop-DeprecationWarning beim Resampling unterdrücken
audioop ist seit 3.11 deprecated und ab 3.13 entfernt. Es bleibt der
schnelle Default (stdlib, kein numpy nötig — wichtig fürs schlanke
Test-/CI-venv); der Import wird nur in catch_warnings gekapselt. Ab 3.13
greift weiterhin der bestehende ImportError->numpy-Fallback.

Suite-Warnungen damit von 2 auf 1 (verbleibend: Starlette-TestClient,
bibliotheksintern).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 00:52:25 +02:00
a6866a6e7f feat: UI-Hinweis bei Cartesia + nicht unterstützter Sprache
Wählt der Nutzer Cartesia und eine feste Sprache, die Cartesia nicht
spricht (AR/IT/RU/PL …), erscheint ein Hinweis, dass automatisch die
lokale Piper-Stimme verwendet wird. Macht den tts_fallback sichtbar,
statt lautlos eine andere Stimme zu liefern.

- /api/config liefert neu available.cartesia_unsupported_languages
  (Single Source of Truth: _UNSUPPORTED_LANGS aus cartesia.py).
- app.js zeigt/aktualisiert den Hinweis bei Sprach-/Provider-Wechsel.
- Test: /api/config listet die nicht unterstützten Sprachen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 00:49:28 +02:00
35c08074cf feat: Piper-Fallback für Cartesia-unsupported Sprachen (AR/IT/RU/PL)
PiperTTSProvider.synthesize() erkennt jetzt Gender-Codes ("f"/"m") und
löst sie per language-Parameter in echte Stimmmodell-Namen auf. Dadurch
greift FallbackTTSProvider transparent auf Piper durch, wenn Cartesia
eine Sprache nicht unterstützt.

tts_fallback = "piper" muss in voice-assistant.toml gesetzt sein
(nicht versioniert; siehe DEPLOYMENT.md Abschnitt 2.5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 23:47:36 +02:00
91b9ef3374 Jamulix: Optimierungen übernehmen, lokale Voice-Modelle ignorieren 2026-06-24 22:01:38 +02:00
Dieter Schlüter
b8b2bcddd3 ui(tts): "Im Gerät" ausgrauen statt ausblenden, wenn keine Sprachstimmen
- refreshDevicePreset(): graut das "Im Gerät"-Preset aus (disabled + opacity + Hinweis
  "keine Sprachstimmen im Browser"), wenn getVoices() leer ist; entgraut automatisch
  bei voiceschanged (+ 1,5s-Backstop).
- Kein Aufzwingen: device wird nur (wieder) gewählt, wenn es die gespeicherte Präferenz
  ist; ohne Stimmen wird die aktive Wahl auf piper gesetzt (Präferenz bleibt erhalten).
- Server-Fallback bleibt für fehlende Einzelsprachen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:51:37 +02:00
Dieter Schlüter
53ec2d7ec7 ui: "Angemeldet als …" + "Abmelden" zurück in eine Leiste unter die Kopfzeile
- Identitätsleiste (Nutzer links, Abmelden-Link rechts) wieder direkt unter dem Header
  statt im ⋮-Menü. Logout-Link kommt wie bisher aus SSO_LOGOUT_URL.
- Admin-Button bleibt im ⋮-Menü (als "⚙ Admin-Bereich"); IDs unverändert.
- Doku §5.1.2 angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:28:21 +02:00
Dieter Schlüter
7d30fe9e7c ui(tts): Ton-Presets — Schnell (piper), Hohe Qualität (chatterbox), Cloud (openrouter) zurück
- Vorlesen-Presets wieder nach Tempo/Qualität benannt: 📱 Im Gerät ·  Schnell ·
   Hohe Qualität · ☁ Cloud. Cloud (OpenRouter) wieder als sichtbarer Knopf.
- syncPresetUI: exakte Markierung (openrouter hat eigenen Knopf, kein Sonderfall mehr).
- Doku §5.1.2 angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:10:51 +02:00
Dieter Schlüter
d39fc40e94 fix(tts): Geräte-TTS fällt auf Server-Audio zurück, wenn keine Stimmen vorhanden
Linux-Desktop-Browser haben oft keine speechSynthesis-Stimmen -> "Im Gerät" blieb
stumm (auto + Replay). deviceVoicesReady() prüft getVoices(); fehlen Stimmen, holt
serverSpeakFallback() Audio via /api/speak (Server-Default, nie "device") und spielt es.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:02:03 +02:00
Dieter Schlüter
7bac2952c5 polish(admin): Übersicht-Kacheln klickbar + Notfall-Signal, Notfall-Text umbricht
- Übersicht: Kacheln (Nutzer/Anfragen/Notfälle/LLM) springen per Klick in den Bereich;
  Notfall-Kachel wird rot mit "ansehen", wenn Ereignisse vorliegen, sonst "keine".
- Notfall-Tabelle: Textausschnitt umbricht statt abzuschneiden -> auf Mobil lesbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:32:52 +02:00
Dieter Schlüter
487c8aa3c1 feat(admin): Redesign — 5 Bereiche + Übersicht-Dashboard + Sub-Navigation
Von 8 gemischten Tabs auf 5 logische Bereiche verdichtet:
- 📊 Übersicht (neu, Start): Kennzahlen (Nutzer, Anfragen, Notfälle) + LLM-Backend/GPU
  + Schnell-Sprünge.
- 👥 Nutzer: Sub-Tabs Verwalten + Gespräche.
- 🚨 Notfälle.
- 🖥 System: Sub-Tabs Status (inkl. LLM-Steuerung) + Metriken + Log.
- ⚙ Konfiguration: Sub-Tabs Einstellungen + Wörterbuch.

- Sub-Navigation (Segmented-Buttons) für Bereiche mit mehreren Ansichten.
- Bestehende Content-Blöcke + Loader unverändert wiederverwendet (nur neu gebündelt)
  -> geringes Risiko. loadOverview neu (vorhandene Endpunkte).
- Doku §7.5 auf 5-Bereiche-Struktur aktualisiert; Wörterbuch-Hinweis (alle 8 Sprachen,
  Zeile-anklicken-bearbeiten) korrigiert.

167 grün, JS-Syntax ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:11:57 +02:00
Dieter Schlüter
35fd3a5d15 fix(ui): Senden als runder Icon-Button (Fußzeile überlaufsicher)
- Senden ➤ als runder Icon-Button (gleich groß wie Mikrofon) statt Text -> passt
  auch im Querformat/schmalen Geräten, kein Abschneiden mehr.
- Eingabefeld min-w-0, Buttons shrink-0 -> Reihe kann nicht überlaufen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:04:35 +02:00
Dieter Schlüter
3f6bf53a7f refactor(ui): Geräte-STT entfernt + Menü-Redesign (schlanke Kopfzeile + ⋮-Sheet)
Geräte-STT (Web Speech API) entfernt — auf Android nur Cloud, Qualität < Whisper,
verwirrend. STT läuft jetzt überall serverseitig (Whisper). Geräte-TTS bleibt.
- Backend: allow_cloud_stt (config/runtime_config/api me) + Tests entfernt.
- Frontend: SpeechRecognition-Code, STT-Dropdown, "Lokal"-Button raus.

Menü-Redesign (verständlicher + passt auf Handys):
- Kopfzeile schlank: Mund-Icon + Titel + Sprache + ⋮-Menübutton (kein Überlauf mehr).
- ⋮ öffnet ein Einstellungs-Sheet: Ton als 3 Presets (📱 Im Gerät / ☁ Server /
   Beste Qualität) statt zweier kryptischer "Gerät"-Dropdowns; Neues Gespräch;
  Tag-/Nachtmodus; Konto (Identität, Admin, Abmelden).
- #tts bleibt als verborgenes Quell-Select -> bestehende TTS-Logik unverändert.
- Doku §5.1.2 neu, §6.3 STT-Hinweis. 167 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:26:20 +02:00
Dieter Schlüter
f74d2fadb2 fix(stt): iOS-Live-Vorschau wiederhergestellt + klare Server-Fallback-Meldung
- resolveSttMode behandelt iOS-Safari (keine available()-API) wieder als "local"
  -> on-device SpeechRecognition mit Live-Interim funktioniert auf iPhone wieder.
- Server-Fallback zeigt jetzt "Server-Erkennung. Live-Vorschau nur mit lokalem/
  Cloud-STT" (startRecording optionaler Status-Text) statt generischem Hinweis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:28:39 +02:00
Dieter Schlüter
dc597bd77b fix(stt): processLocally-Feinschliff (Android-Bug "language not supported") + "Alles lokal"
- Geräte-STT prüft vor lokaler Erkennung das On-Device-Sprachpaket via
  SpeechRecognition.available(); fehlt es, install() im Hintergrund + Fallback auf
  Cloud (falls erlaubt) bzw. Server. processLocally wird nur bei "available" gesetzt
  -> behebt "language not supported" auf Android-Chrome.
- onerror/Start-Fehler (language-not-supported/network/service) -> sauberer
  Server-STT-Fallback statt Fehlermeldung.
- Schnellwahl "📴 Lokal": setzt STT + TTS gemeinsam auf Gerät (nur Text).
- Doku §6.3.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:32:17 +02:00
Dieter Schlüter
ab9c4f4938 feat(stt): Geräte-STT auf Mobilgeräten (Web Speech API) — getrennt, Datenschutz-Linie C
- Geräte-STT erkennt Sprache lokal und sendet nur Text über den Text-Turn; spart
  Audio-Upload + Server-STT. Getrennter Schalter (STT ▾) unabhängig vom TTS.
- Linie C: nur bei nachweislich lokaler Erkennung (iOS / Chrome on-device); Cloud
  (z. B. Chrome-Desktop -> Google) nur mit Admin-Flag ALLOW_CLOUD_STT.
  -> config.allow_cloud_stt, RUNTIME_SETTABLE, /api/me, Admin-Toggle.
- Fix-only (SpeechRecognition braucht Sprach-Hint); Flex -> Server-STT-Fallback.
  Kein/instabiles SpeechRecognition (z. B. Firefox) -> Server-STT. Live-Interim im
  Eingabefeld. Terminal/Desktop/Laptop unverändert serverseitig (Option nur sichtbar,
  wenn das Gerät lokale Erkennung bietet).
- Tests: /api/me-Flag + runtime-setzbar (169 grün). Doku §5.1.2 + §6.3.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:08:10 +02:00
Dieter Schlüter
33d7189418 feat(admin): Audit-Logging schreibender Admin-Aktionen — Plan-Schritt 5
- app/audit.py: eigener Logger "va.audit" (eigener stdout-Handler -> Journal/Log-Tab).
  log_admin_action() protokolliert Aktion + Auslöser (SSO-Name oder admin-key).
- Verdrahtet in: config_set/config_reset (PUT/DELETE /admin/config),
  llm_backend_switch (+ _rejected), gateway_restart.
- Tests: caplog prüft Audit-Zeilen für config_set und abgelehnten Backend-Switch.
- Doku §7.5: Audit-Format im Log-Tab.

Schließt den Plan (Admin-gesteuerte LLM-/Gateway-Verwaltung, Schritte 1–5) ab. 167 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:50:55 +02:00
Dieter Schlüter
b2576fd465 feat(llm): Live-Parameter top_p + Live/Restart-Kennzeichnung — Plan-Schritt 4
- local_llm_top_p (Default 0.9) durch Config -> runtime_config (RUNTIME_SETTABLE,
  live ohne Neustart) -> local-openai-compatible Payload (top_p).
- Admin-Einstellungen: top_p als Slider (0–1); Hints "wirkt sofort (kein Neustart)"
  für temperature/top_p.
- Doku: top_p + Klarstellung Live-Parameter (temp/top_p/max_tokens) vs.
  Startup-Wert Kontextfenster (OLLAMA_CONTEXT_LENGTH / llama.cpp -c).

Verifiziert: top_p im Payload, Live-Config-Round-Trip (PUT/DELETE). 165 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:46:33 +02:00
Dieter Schlüter
5b6f0f8ef2 feat(admin): Backend-Wechsel + Gateway-Neustart aus dem Admin-Panel — Plan-Schritt 3
- admin_llm: switch_backend() mit strikter Allowlist (backend ∈ {ollama,llamacpp},
  Modell gegen 'ollama list' + Format-Regex), detached (Self-Restart-sicher),
  niemals shell=True. restart_gateway_detached() für systemd-User-Dienst.
- switch-llm.sh: flock-Lock gegen parallele Backend-Wechsel (Exit 75).
- Endpunkte POST /api/admin/llm/backend (422 bei ungültig) und
  POST /api/admin/gateway/restart (require_admin).
- Status-Tab: Steuerung (Backend-Dropdown + Modell, Wechseln/Neustart) mit
  Poll bis das Gateway wieder antwortet; Hinweis auf systemd-Voraussetzung.
- Tests: Auth + Allowlist (Shell-Metazeichen/unbekanntes Modell -> 422). 165 grün.
- Doku §7.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:39:59 +02:00
Dieter Schlüter
97ae0a5d34 feat(admin): LLM-/GPU-Status-Karte (read-only) — Plan-Schritt 2
- app/admin_llm.py: read-only Statusabfragen ohne sudo (docker ps, ollama ps,
  nvidia-smi, systemctl --user) mit sicheren Defaults bei fehlenden Tools.
- GET /api/admin/llm/status (require_admin): Backend, Modell, Backend-Status,
  geladene Ollama-Modelle, GPU-Auslastung, Gateway-Dienst-Status.
- Admin Status-Tab: LLM-Backend-Karte mit GPU-Balken.
- Tests: Auth-Gate + Antwortschema (160 grün).
- Doku §7.5: Status-Tab um LLM/GPU-Karte ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:33:06 +02:00
Dieter Schlüter
bd20a308de feat(tts): Emojis/Markdown beim Vorlesen entfernen + Autostart/GPU-Doku
- spoken_response_adapter: Emoji-/Symbol-Strip (Emoticons, Symbole, Flaggen,
  Pfeile, Dingbats, ZWJ/Variationsselektoren). Markdown wurde bereits entfernt.
- Geräte-TTS (app.js v28) liest das bereinigte "spoken"-Feld statt des Rohtexts
  (automatisch + beim Replay); Anzeige bleibt Originaltext.
- deploy/ollama-keepalive.conf: Drop-in OLLAMA_KEEP_ALIVE=5m -> GPU im Leerlauf frei.
- Doku §4.10: Dauerbetrieb als Dienst + warum der Gateway die GPU nicht blockiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:25:45 +02:00
Dieter Schlüter
878bf785dd 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
Dieter Schlüter
81d4cf6fd8 fix(llm): LLM antwortet jetzt in der konfigurierten Systemsprache
Bisher wurde `language` zwar an STT/TTS übergeben, aber nie an den LLM.
Lokales Modell antwortete deshalb immer auf Englisch/Deutsch, egal ob
Französisch, Spanisch o.a. eingestellt war.

Lösung: `language`-Parameter durch die gesamte LLM-Schicht gezogen:
- base.py: `lang_instruction()` helper + Signatur erweitert
- local_openai_compatible.py: Sprachanweisung ("Respond in Français.")
  wird als letzter System-Part in den Message-Stack eingefügt
- openrouter.py: Explizite Sprachanweisung ergänzt den bestehenden
  "Answer in the same language as the user"-Prompt
- fallback.py: FallbackLLMProvider leitet `language` durch
- orchestrator.py: alle 3 LLM-Aufrufstellen übergeben `language`
- Tests: alle Stub-LLMs um `language=None` ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:12:16 +02:00
Dieter Schlüter
c3f6206d0c feat(piper): beste Stimmen für 10 Sprachen installiert + low/medium aufgeräumt (v19)
Neu installiert: en_US-lessac-high, en_GB-cori-high, es_ES-sharvard-medium,
ru_RU-irina-medium, zh_CN-huayan-medium, it_IT-paola-medium, nl_NL-mls-medium.
Gelöscht: de_DE-kerstin-low, de_DE-karlsson-low, fr_FR-gilles-low,
es_ES-davefx-medium. Alle verbleibenden Stimmen haben 130–159 Phoneme;
es/fr/it/nl/ru/zh haben kein high — medium ist das Maximum bei piper.
Dropdown und Doku aktualisiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:41:03 +02:00
Dieter Schlüter
389cd5a417 fix: fr_FR-siwis-medium installiert — Missing-phoneme-Warning behoben (v18)
fr_FR-gilles-low (low, 130 Phoneme) fehlt Kombinations-Tilde U+0303
→ Nasalvokale (ã, ɔ̃) werden nicht korrekt ausgesprochen.
fr_FR-siwis-medium (medium, 154 Phoneme) enthält alle nötigen Phoneme.
Dropdown und Doku aktualisiert. espeak-ng-Daten waren bereits vollständig
in piper gebündelt — kein separater Download nötig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:23:42 +02:00
Dieter Schlüter
da561cec09 fix: de_DE-karlsson-low installiert — Dropdown + Doku aktualisiert (v17)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:07:24 +02:00
Dieter Schlüter
b91be9b9fe fix: Piper-Stimmliste korrigieren — karlsson-low nicht installiert (v16)
app.js: de_DE-karlsson-low aus Dropdown entfernt (nicht vorhanden);
stattdessen alle tatsächlich installierten Stimmen aufgeführt
(de_DE-thorsten-high, de_DE-kerstin-low, en_US-ryan-high,
es_ES-davefx-medium, fr_FR-gilles-low).
BEDIENUNGSANLEITUNG § 6.5.2: Tabelle auf tatsächlichen Bestand korrigiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:04:49 +02:00
Dieter Schlüter
11ae66f5ae fix(admin): TTS-Test-Button nutzt jetzt richtigen Provider (v15)
piper_voice, openrouter_tts_model und openrouter_tts_voice bekommen
'testProvider' in FIELD_META. Der Test-Handler schickt diesen Provider
als tts_provider im Request mit — statt immer den System-Default zu
nutzen, der oft openrouter ist und dann mit 502 scheitert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:39:14 +02:00
Dieter Schlüter
4a4cbe643d feat(admin): Provider & Sprache — Eigener-Wert-Eingabe + Tests (v14)
app.js: default_stt/llm/tts_provider und default_language von 'select'
auf 'combo' umgestellt, damit neben den vordefinierten Optionen jederzeit
ein beliebiger eigener Wert eingegeben werden kann (Zukunftssicherheit).
combo-Renderer unterstützt jetzt auch meta.labels (Sprachkürzel → Klarname).
tests/test_admin_settings.py: 28 Tests für GET/PUT/DELETE /api/admin/config —
Auth, bekannte Werte, Custom-Values, Persistenz, Runtime-Wirkung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:30:52 +02:00
Dieter Schlüter
d8b861db26 feat(web): Einstellungen-Tab – typisierte Inputs, Gruppen, Test-Buttons (v13)
- Dropdown-Auswahl für Provider, Bool- und Enum-Felder
- Combo-Widget (Dropdown + Textfeld, synchronisiert) für Modelle/Stimmen
- Range-Slider für Temperature, Number-Input für Token/Limit-Felder
- Textarea für System-Prompt
- Test-Buttons: 🔊 TTS via WebAudio, 💬 LLM via /api/chat?debug=true, 🎤 STT-Verfügbarkeitscheck
- Inline-Save-Feedback (grün ✓ / rot ✗), kein Full-Reload mehr
- ENV-Variablenname in UPPERCASE angezeigt
- Sieben Gruppen-Sektionen statt flacher Liste
- Piper-Stimmentabelle in Doku korrigiert (nur installierte: thorsten/kerstin/karlsson)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 02:35:40 +02:00
Dieter Schlüter
5d022aaf63 feat(admin): Phase 3 — Laufzeit-Konfiguration ohne Server-Neustart
Neue Tabelle config_overrides in SQLite; RuntimeSettings-Wrapper liest
überschreibbare Felder mit 30s TTL-Cache aus der DB und fällt auf
.env-Werte zurück. dependencies.py und quota.py nutzen runtime_settings
als Default statt des statischen Settings-Singletons.

16 Felder überschreibbar: STT/LLM/TTS-Provider, LLM-Modelle, Stimmen,
Systemprompt, Temperatur, Tageskontingent, Normalisierung u.a.

Backend: GET/PUT/DELETE /api/admin/config/{key}
Admin-UI: neuer Tab "⚙ Einstellungen" mit Inline-Edit und Reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:39:36 +02:00
Dieter Schlüter
296138ac1e fix(web): escHtml verarbeitet Zahlen korrekt, Status-Tab Fehlertext verbessert
escHtml() rief .replace() auf Number-Werten auf (httpTotal aus Metriken)
→ TypeError. Fix: immer String(str) konvertieren. Außerdem console.error
im Status-Tab-catch für einfachere Fehlerdiagnose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:33:49 +02:00
Dieter Schlüter
ea923ffc61 fix(auth): require_admin akzeptiert jetzt auch SSO-Admin-Nutzer
Vorher pruefte require_admin nur den X-Admin-Key-Header — SSO-Admins
(aus ADMIN_USERS) bekamen 401. Das brach alle Admin-Panel-Tabs ausser
der Nutzerliste im Browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:24:04 +02:00
Dieter Schlüter
366d71ba26 feat(admin): Admin-Panel Phase 2 — Wörterbuch-CRUD, Live-Log, DB-Export, Metriken-Balken
- Aussprache-Lexikon (de/en) komplett im Browser editierbar: Einträge
  hinzufügen/löschen in Sektionen Abkürzungen/Einheiten/Begriffe;
  LRU-Cache wird nach jedem Schreibvorgang automatisch geleert.
- Live-Log-Tab: WebSocket auf /api/admin/log streamt journalctl
  des voice-assistant.service live im Terminal-Style ins Admin-Panel.
- DB-Export: SQLite-Datenbank über /api/admin/db-export herunterladen.
- Metriken-Tab: CSS-Balkendiagramm (Anfragen je Nutzer) ergänzt.
- Dokumentation: §7.5 Admin-Web-Panel, B.5 API-Endpunkte, Sachregister.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:15:28 +02:00