Commit graph

71 commits

Author SHA1 Message Date
66d6708f59 feat(costs): $-Tagesbudget pro Nutzer (Stufe 3)
cost_daily_limit_usd (Pref > global; 0 = unbegrenzt) in quota.py
(cost_within_budget, cost_limit). ws.py-Gate: Web-Such- ODER Kostenbudget
aufgebraucht -> Web-Suche fuer den Turn aus + freundlicher Hinweis. Admin:
Schema-Feld + list_users + UI-Eingabe ($/Tag) + globales Live-Setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:17:54 +02:00
d87f0ce424 feat(costs): Pro-Nutzer-Kostenerfassung Stufe 1 (OpenRouter-Ist-Kosten)
CostMeter via ContextVars (app/core/costs.py, multitool-ready). Alle
OpenRouter-Calls (LLM, Sonar, Koreferenz, Fallback) senden usage:{include:true}
und buchen die Ist-Kosten je Kategorie (llm/web_search) — inkl. finalem
Streaming-usage-Chunk. cost_usage-Tabelle (user/day/category) + Store-Methoden;
ws.py/chat.py start_meter pro Turn + persist_costs. Statistik: Spalten
"Kosten (Monat)" + "Ø/Anfrage" + Gesamt. Live: 1 Such-Turn ~ $0,0052
(web_search dominiert, LLM ~ $0,00007). Tests: 318.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:08:45 +02:00
d74e94e6a4 feat(admin): Nutzungsstatistik um "mit Websuche" je Nutzer
get_all_usage liefert total_web_searches (Summe aus web_search_usage);
Statistik-Tabelle + Gesamtzeile zeigen die neue Spalte "mit Websuche".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:51:29 +02:00
4f1e15176b feat: freundliche Hinweise bei abgeschalteter/aufgebrauchter Web-Suche
Ist die Web-Suche bei einer offensichtlich tagesaktuellen Frage
(should_force_search) nicht verfuegbar, sendet ws.py einen {type:"notice"}-
Event mit einer kleinen, lokalisierten Senioren-Systemnachricht ("Web-Suche
aus" bzw. "Tageskontingent aufgebraucht"); Frontend zeigt sie als System-Bubble.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:25:10 +02:00
88c8c9df7f feat: Web-Such-Budget pro Nutzer/Tag
Neue Tabelle web_search_usage + Store-Zaehler (web_search_count /
add_web_search_usage, kaskadiert in delete_user). Pro-Nutzer-Limit
web_search_daily_limit (Pref > global; 0 = unbegrenzt) in quota.py
(web_search_within_budget). ws.py: Gate vor _resolve (Budget aufgebraucht ->
Web-Suche fuer den Turn aus) + Zaehlung nach dem Turn (trace.web_searches,
gezaehlt im Orchestrator je on_citations). Admin: Feld + UI-Eingabe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:22:06 +02:00
5609741039 feat(admin): Web-Suche pro Nutzer ein-/ausschaltbar
web_search_enabled in AdminUserPrefsUpdate (war nicht im Schema -> wurde wie
frueher voice_gender still verworfen) + in der /admin/users-Antwort (Checkbox-
Initialzustand, Default = globaler Wert) + Checkbox im Admin-Nutzer-Bereich
(speichert sofort). resolve_route respektierte das Pref schon -> wirkt jetzt
durchgaengig (AUS = plain Provider, AN = ToolCallingLLM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:12:55 +02:00
69894b1e7b feat(admin): "Verlauf löschen" pro Nutzer (Konto bleibt)
Store.delete_user_history (loescht Sessions + Nachrichten eines Nutzers, ohne
Konto/Erinnerungen/Usage) + DELETE /api/admin/users/{id}/history (audit-geloggt)
+ Button im Admin-Nutzer-Bereich.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:08:43 +02:00
e753788a23 feat(citations): Web-Such-Quellen unter der Antwort-Bubble anzeigen
Sonar liefert Citation-URLs; bisher verworfen, jetzt bis in die UI gereicht.

- ToolCallingLLM: _run_tool/_inject_forced_search geben Citations zurueck;
  stream() sammelt sie ueber einen on_citations-Callback.
- Orchestrator: chat_stream reicht on_citations durch (via _stream_supports)
  und legt die gesammelten URLs in PipelineTrace.citations.
- schemas: PipelineTrace.citations.
- ws.py: Citations im semantic-Event.
- Frontend (app.js): renderCitations() zeigt bis zu 4 Quellen-Links
  (Hostname, neuer Tab) unter der Antwort-Bubble.

Nur ueber den Streaming-Pfad (= Web-UI). Live verifiziert: "Wer ist
Bundeskanzler?" -> 15 Quellen gesammelt (Bundeskanzler.de, Wikipedia, ...).
Tests: 312 gruen; JS-Syntax geprueft. Doc §8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:11:24 +02:00
fdf65e8623 feat(fillers): Beruhigungssätze auch im Geräte-TTS-Modus sprechen
Bisher wurde der Filler im "Im Gerät"-Modus (Web Speech API) nur angezeigt,
aber nicht gesprochen (Server-Audio ist dort aus). Jetzt eigener Kanal:

- Orchestrator: chat_stream bekommt on_filler; _emit_filler bevorzugt den
  dedizierten Filler-Kanal (sonst Fallback on_token). Ephemer wie bisher.
- ws.py: on_filler -> {type:"filler", text}.
- Frontend (app.js): {type:"filler"} -> transient in der STATUS-Zeile (nicht
  in der Antwort-Bubble) und im Geräte-Modus sofort speechSynthesis.speak()
  OHNE cancel (mehrere Filler -> Queue; die Antwort cancelt spaeter und
  uebernimmt). Abgesichert ueber deviceVoicesReady()/deviceVoiceReady(lang) —
  keine Verschaerfung der bekannten deviceVoiceReady-Regression.

Server-TTS-Modus unveraendert (Filler zusaetzlich als "Satz null" gesprochen),
zeigt den Filler nun ebenfalls in der Status-Zeile statt in der Bubble.

Tests: 312 gruen; JS-Syntax geprueft. Doc §5.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:08:48 +02:00
e814b5548d fix(tts): Geräte-TTS-Regression für Sprachen ohne Geschlechtsstimme beheben
Commit 5c63f7b hatte deviceGenderReady als harte Bedingung eingeführt: das
Gerät musste eine Stimme des WAHLgeschlechts haben, sonst Server-Fallback.
Die konservative Geschlechts-Heuristik liefert für viele Geräte-Stimmen null
(z.B. NL am iPhone: nur eine Stimme ohne erkennbares Geschlecht) -> der
Server-Fallback griff und lieferte eine schlechte/fremdsprachige Aussprache.
Für NL kam (im noch nicht neu gestarteten Prozess) die deutsche Default-
Stimme -> "deutscher Akzent".

Lösung: deviceGenderReady -> deviceVoiceReady. Eine sprachrichtige Stimme
reicht (pickVoice wählt cands[0] als Best-Effort-Fallback). Die m/f-Wahl
wirkt am Gerät nur für Sprachen mit mehreren Stimmen (DE, EN), sonst
folgenlos — aber eine falsche Sprache ist schlechter als ein folgenloser
Schalter.

+ Test: NL in test_piper_single_voice_languages_ignore_gender aufgenommen
(serverseitige Komplementär-Logik; sichert voice_for_route NL-Auflösung).
Der client-seitige app.js-Fix ist mangels JS-Test-Framework manuell
verifiziert (Geräte-TTS für NL spricht niederländisch, DE/EN behalten
Geschlechtswahl).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-28 16:15:04 +02:00
3d3973dc16 feat(i18n): Niederländisch (nl) als Sprache aufnehmen
NL war als Halbzustand angelegt (Piper-Modell nl_NL-mls-medium,
Chatterbox-nl.wav, LLM-Sprachname, Cartesia-Ausschluss, leeres
pronunciation.nl.yaml), fehlte aber im Sprachmenü und Backend-Mapping.

- app.js: NL in ALL_LANGS, LANG_BCP47, default_language-Auswahl und
  piper_voice-Liste (jeweils zwischen IT und PT)
- dependencies.py: LANG_TO_PIPER_VOICE nl -> nl_NL-mls-medium
  (kein GENDERED-Eintrag: nur eine Stimme, wie PT/AR)
- safety/emergency.py: NL-Texte in allen 4 Hinweis-Dicts

STT (faster-whisper large-v3) und LLM (_LANG_NAMES) unterstützen nl
bereits automatisch. Offen: deutsche Ordinalzahlen nur für language=de
(NL fällt durch, kosmetisch).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-28 15:13:29 +02:00
37b4c175d2 fix(notruf): ehrlicher Status, wenn niemand verständigt werden kann
Bisher meldete ein Notruf ohne nutzbaren Kanal trotzdem Erfolg
(status="alerted", grüner Haken), obwohl tatsächlich niemand
benachrichtigt wurde — gefährlich für Senioren.

- emergency.py: drei klare Endzustände alerted / no_recipients /
  delivery_failed; notify_readiness() prüft, ob ein Kanal wirklich
  senden würde (SMTP+Empfaenger bzw. Webhook). Mehrsprachige Hinweise
  fuer die Fehlfaelle (nennen Selbsthilfe 112). Antwort traegt jetzt
  notified + notify_ready; Warn-Log, wenn nichts zugestellt wurde.
- /api/emergency/status liefert notify_ready fuer die Vorab-Warnung.
- Frontend: gelbe Vorab-Warnung im SOS-Dialog, wenn kein Empfaenger
  eingerichtet ist; bei no_recipients/delivery_failed kein gruenes
  Erfolgssignal mehr, sondern roter Fehlerzustand (app.js v54).
- Tests an die neue Semantik angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:50:03 +02:00
9eb5c31eb6 release(v0.2.0): Marke „Alexis" + sichtbare Versionsnummer
- Anzeigename Voice Assistant -> Alexis (UI-Titel/Kopfzeile mit Untertitel
  „Ihr Sprachbegleiter", FastAPI-Titel, README/Handbuch). Technische Identifier
  (Dienst, Pfade, Paketname, Konfig-/DB-Dateinamen) bleiben unveraendert.
- Version als EINE Quelle: pyproject.toml = 0.2.0; app.__version__ liest sie
  (pyproject zuerst, Paket-Metadaten als Fallback). Sichtbar in der UI-Kopfzeile,
  unter /api/config (version) und in OpenAPI.
- CHANGELOG.md mit 0.2.0-Eintrag; Tests fuer /api/config.version + Branding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 07:02:02 +02:00
5e128a74ce feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
  (Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
  mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
  (isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
  geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
  serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
  pro Seitenladen).

GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
  gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.

Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
5c63f7b6cc fix(tts): Geräte-TTS folgt der Geschlechtswahl (m/f), sonst Server-Fallback
Der Geräte-TTS-Pfad (Browser-SpeechSynthesis) wählte die Stimme bisher nur
nach Sprache -> der m/f-Schalter blieb dort folgenlos. pickVoice bevorzugt
jetzt die geschlechtspassende Stimme (Heuristik über Name/voiceURI, da die
Web Speech API kein Geschlechtsfeld kennt). Findet das Gerät keine passende
Stimme, weichen deviceSpeak/deviceSpeakBtn auf Server-TTS aus (echte
Piper-m/f-Stimmen), damit die Wahl hörbar wirkt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 02:43:29 +02:00
18ad442c94 feat(admin): Dropdown „Zentrales KI-Modell" — erprobte Presets ruck-zuck umstellen
Neue Karte in Admin › System › Status: ein Dropdown kuratierter, erprobter
Modelle, das Provider + Modell in einem Klick als Laufzeit-Override umstellt
(wirkt sofort, kein Neustart).

- Presets in config/models.yaml (Label, optional note, set:{key:value} aus
  RUNTIME_SETTABLE, default:true = Config-Standard). Loader app/model_presets.py
  (nur erlaubte LLM-Keys: default_llm_provider, openrouter_llm_model,
  local_llm_model).
- Endpunkte: GET /api/admin/models (Presets + aktuell aktives LLM + active_index),
  POST /api/admin/models/select (Default -> Overrides löschen, sonst Override-Keys
  setzen) — nutzt set_/delete_config_override + invalidate_cache, mit Audit-Log.
- Frontend: Karte zeigt Dropdown + „Aktiv: <provider> · <modell>"; Auswahl wirkt
  sofort und lädt den Status neu. Bump app.js v=52 -> v=53.
- Startliste: Mistral Small 24B (Standard), DeepSeek V4 Flash/Pro, Ollama Gemma 3.

Tests: Laden/Default-aktiv/Select-setzt-Override/Default-löscht/404/Auth.
263 Offline-Tests grün. Live via Headless-Chrome verifiziert (Auswahl ->
Override gesetzt, Provider-Karte spiegelt das Modell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:02:44 +02:00
904f7db82c feat(notruf): Alarmpause pro Nutzer im Notfall-Profil (Minuten, 0=aus, leer=global)
Bisher war die Wiederauslöse-Sperre nur global einstellbar. Jetzt zusätzlich
pro Nutzer im Notfall-Profil (Admin), hinter „Standort im Notfall mitsenden":
„Pause bis zum erneuten Alarm: [n] Minuten" (leer = globaler Standard, 0 = aus).

- Auflösung serverseitig: Nutzer-Pref > globale Einstellung > 5
  (_resolve_cooldown_minutes in emergency.py; emergency_cooldown_status nutzt sie,
  die Notruf-UI/der Status-Endpunkt liefern dadurch automatisch den per-User-Wert).
- Schema AdminUserPrefsUpdate.emergency_cooldown_minutes (String) mit Validator
  (leer/0–60, sonst 422). /api/admin/users gibt das Feld jetzt mit aus
  (sonst lädt das Formular leer).
- Frontend: Zahlenfeld + Hinweis im Notfall-Profil, über PROF_KEYS/PROF_VALIDATORS
  verdrahtet. Bump app.js v=51 -> v=52.
- Doku: BEDIENUNGSANLEITUNG § Nutzer › Verwalten.

Tests: per-User-Override/Erben/Aus + Schema-Validierung. 254 Offline-Tests grün.
Live verifiziert: PUT prefs -> /status spiegelt 300->120s, 99 -> 422, Formular
zeigt/lädt das Feld hinter der Standort-Checkbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:10:12 +02:00
8c152ee77c feat(notruf): Wiederauslöse-Sperre + grüner Hinweis; "Neues Gespräch" in die Kopfzeile
1) Notruf-Sperre (Cooldown) gegen Mehrfach-Alarme verwirrter/verängstigter
   Senioren:
   - Neue Einstellung emergency_cooldown_minutes (Default 5), zur Laufzeit im
     Admin-Bereich änderbar (RUNTIME_SETTABLE), 0 = aus.
   - record_manual_emergency unterdrückt innerhalb der Sperre jeden weiteren
     Alarm (kein zweiter Versand, kein Log-Eintrag) und liefert status=cooldown
     mit minutes_ago/remaining_seconds. Neuer Endpunkt GET /api/emergency/status
     für die Anzeige nach einem Seiten-Neuladen. Store: last_manual_emergency_at.
   - UI: Nach dem Alarm wird der Button grün, zeigt „✓" und pulsiert für die
     Dauer der Sperre. Erneuter Druck öffnet einen grünen Hinweis-Dialog:
     „Ich habe Deine Helfer vor <n> Minuten alarmiert. Wahrscheinlich ist Deine
     Hilfe schon unterwegs." Der grüne Zustand übersteht ein Neuladen
     (Status-Abruf beim Start).

2) „Neues Gespräch" als Symbol (Stift) zurück in die Kopfzeile — zwischen
   Sprachauswahl und Menü; löst dieselbe Aktion aus wie der Menüeintrag.

Tests: neue test_emergency_cooldown.py (Status/Suppression/Endpoint); zwei
bestehende Notruf-Tests an die neue Interface-Methode bzw. die Sperre
angepasst. 250 Offline-Tests grün. Frontend via Headless-Chrome verifiziert
(Alarm->grün/pulsierend, Hinweis-Dialog, Reload-Persistenz, Header-Button).
Bump app.js v=50 -> v=51.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:58:17 +02:00
8a6f428c1b fix(ui): SOS-Button auf Quadrat in Höhe der Sprachauswahl verkleinern (32px)
Der prominente SOS-Button war zu groß. Jetzt ein kompaktes 32x32-Quadrat
(h-8 w-8) — exakt so hoch wie die Sprachauswahl daneben (gemessen 32px),
weiterhin rot gefüllt mit weißem „SOS". Der volle Titel passt wieder ohne
Kürzung.

Da ein 32px-Quadrat keine langen Statuswörter fasst, läuft das Feedback jetzt
über Farbe/Symbol: „SOS" (rot) → pulsiert beim Rufen → „✓" (grün) = Alarm
ausgelöst → „!" bei Fehler, dann zurück. Bestätigungsdialog unverändert.

Bump app.js v=49 -> v=50. Verifiziert via Headless-Chrome (SOS 32x32 == lang 32).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:32:42 +02:00
0ef4fe9cb8 feat(ui): SOS-Button prominent (rot gefüllt, großes weißes „SOS") + Bestätigung & Status
Der Notruf-Button war ein kleines 36px-Outline-Icon (🆘) — leicht zu übersehen.
Jetzt ein rot gefüllter Button (bg-red-600) mit großem, fettem weißem „SOS" in
der (immer sichtbaren) Kopfzeile — der Senior erkennt ihn sofort. Sprachwahl
und Menü bleiben unverändert an ihrem Platz.

- Bestätigung: statt native confirm() ein großer eigener Dialog (rote SOS-Kopf-
  zeile, Warnzeile, große Knöpfe „JA, HILFE RUFEN" / „Abbrechen"). Ein Tipp auf
  SOS, ein Tipp auf JA — schnell, aber gegen Fehlalarm geschützt.
- Feedback am Button selbst: setSosState idle|calling|done|error —
  „Rufe …" (pulsierend) → „Gerufen ✓" (grün) → zurück zu „SOS". So sieht der
  Senior unmittelbar am Knopf, dass der Alarm ausgelöst wurde.

Nur Frontend (app/web), live ohne Neustart (No-Cache-Static, Bump auf v=49).
Verifiziert via Headless-Chrome/CDP: Idle/Dialog/Calling/Done-Zustände.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:48:35 +02:00
e4f0ea7199 fix(web): Web-Audio-Kontext bei Nutzergeste freischalten (iOS-Chunk-Wiedergabe)
unlockTTS() schaltete bisher nur SpeechSynthesis frei. Der Web-Audio-Kontext
für die satzweise Chunk-Wiedergabe (Server-TTS-Modi) wurde erst lazy im
WS-Callback erzeugt — auf iOS startet ein so erzeugter AudioContext "suspended"
und resume() außerhalb einer Geste wird abgelehnt -> stumme/blockierte
Chunk-Wiedergabe.

Fix: unlockTTS() legt den AudioContext jetzt in der Nutzergeste an, ruft
resume() und spielt einen 1-Sample-Stille-Buffer (Standard-iOS-Unlock). Beide
Freischaltungen (Geräte-TTS + Web-Audio) laufen unabhängig, auch wenn
SpeechSynthesis fehlt.

Verifiziert (Desktop-Chrome): Kontext wird in der Geste erzeugt, ist "running"
und der Stille-Buffer läuft, bevor der erste Chunk eintrifft; Chunk-Wiedergabe
weiterhin gapless. Echtes iOS Safari hier nicht testbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:55:15 +02:00
5c3deba5ac fix(web): Barge-in — in-flight TTS-Chunks nach interrupt verwerfen
Beim Barge-in konnte ein Audio-Chunk, der im schmalen Fenster zwischen
gesendetem interrupt und dem interrupted-Event/ws.close noch eintraf, weiter
eingeplant und ~0,2-0,3 s abgespielt werden (im Browser verifiziert).

Fix: Beim Senden von interrupt wird die WS als _aborted markiert; der
onmessage-Binär-Handler verwirft danach eintreffende Chunks, statt sie via
scheduleChunk weiter abzuspielen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:41:45 +02:00
5919a74cff feat(web): chunked TTS satzweise abspielen + Barge-in während Wiedergabe
Die satzweise vom Server gestreamten TTS-Chunks (audio-Event + Binär-Frame)
wurden im Frontend nur gepuffert und erst beim done-Event komplett abgespielt
— der Chunked-TTS-Aufwand brachte so keinen Nutzen.

Jetzt:
- scheduleChunk() hängt jeden ankommenden PCM-Chunk lückenlos an den bisher
  geplanten Stream (Web-Audio-Scheduling über streamPlayTime). Die
  Sprachausgabe beginnt, sobald der erste Satz synthetisiert ist.
- case "audio" markiert den folgenden Binär-Frame als Streaming-Chunk.
- done spielt den Gesamtpuffer nur noch ab, wenn nicht bereits gestreamt wurde
  (kein doppeltes Abspielen); Replay-Cache bleibt befüllt.
- Die Chunk-Quellen liegen in activeSources -> Barge-in (stopAudio) stoppt auch
  laufende Streaming-Wiedergabe; streamPlayTime wird dabei zurückgesetzt.
- Geräte-TTS-Modus überspringt die Chunks unverändert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:18:31 +02:00
21ab545d8a ui: Notruf-Bestätigung — Warnzeile umformuliert
"Dann werden sofort Helfer alarmiert!" -> "Ich alarmiere sofort Helfer!"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:15:42 +02:00
9d9b297cd2 feat(ui): Notruf-Bestätigung um Warnzeile ergänzt
Bestätigungsdialog jetzt zweizeilig: "Wirklich Hilfe rufen?" +
"Dann werden sofort Helfer alarmiert!" — macht die Konsequenz klar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:12:21 +02:00
57002dc022 feat(admin): ein Speichern-Button auf Einstellungs-Ebene mit Format-Validierung
- Speichern-Button aus dem einklappbaren „Notfall-Profil" auf die Karten-/
  Einstellungs-Ebene verschoben (immer sichtbar) und in „Speichern" umbenannt.
- Speichert jetzt ALLE Daten: alle Profilfelder + Standort-Freigabe + erlaubte
  Sprachen in einem PUT.
- Vor JEDEM Speichern Format-Validierung pro Feld; bei Verstoß rotes Feld
  (border/ring-red) + Meckertext, Speichern wird abgebrochen. Rote Markierung
  verschwindet beim Nachbessern (input-Listener).
- Regeln: PLZ = 5 Ziffern · Telefon = +/Ziffern/Leer/()/- (6–20) · Geburtsdatum
  = TT.MM.JJJJ (echtes Datum) · Kontakt-E-Mails/-Telefone = CSV gültiger Werte.
  Leere optionale Felder gelten als konform. Validierungslogik in node geprüft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:03:13 +02:00
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
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
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
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
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
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
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
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
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
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