- DELETE /api/admin/user-data: loescht fuer ALLE Nutzer Verlauf, Statistik
(usage/web_search_usage/cost_usage) und Erinnerungen; Konten bleiben.
Gefahrenzone-Knopf mit doppelter Bestaetigung, audit-geloggt.
- list_users jetzt ORDER BY LOWER(display_name) -> Nutzerkarten alphabetisch
(ohne Gross-/Kleinschreibung).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DELETE /api/admin/users/{id}/stats (usage/web_search_usage/cost_usage) und
.../memories (alle Erinnerungen) — require_admin, audit-geloggt; Konto, Verlauf
und (jeweils) die andere Datenart bleiben. Store-Methoden delete_user_stats/
delete_user_memories + zwei UI-Buttons je Nutzerkarte.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lokale LLM-Basis-URL ohne Deploy umstellbar — wirkt pro Request, da der
Provider je Anfrage aus runtime_settings gebaut wird. Wert unveraendert
(localhost Ollama); Heim-Host kann kuenftig per Admin-Live-Config gesetzt werden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Strompreis (EUR/kWh), Notruf-Kosten (EUR/Alarm), Cartesia ($/Zeichen) jetzt im
Admin live einstellbar (RUNTIME_SETTABLE).
- OPENING-Beruhigungssaetze 12 -> 27 (random.choice; formale "Sie"-Uebersetzung
ersetzt beim Neustart den alten "dich"-Cache).
- Leer-/kein-Treffer-Antwort: statt Stille eine freundliche lokalisierte
Mitteilung ("Dazu konnte ich keine Informationen finden."), gesprochen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lokales LLM: Call-Dauer gemessen -> Stromkosten je Job ((max-idle)*load*n/
3.6e6*Preis(EUR/kWh)*usd_per_eur), Kategorie llm_local. Cartesia-TTS: Zeichen x
cartesia_usd_per_char (tts). Notruf: pro Webhook-Alarm 0,20€*usd_per_eur
(alert), nur Doku, nie Sperre. Helfer in costs.py; neue Settings (GPU-Watt/Last/
Strompreis/usd_per_eur/cartesia/alert). Statistik: "exakt vs. geschätzt"
(Tooltip + Summenzeile, Kategorien tts/alert/llm_local). Tests: 318.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
1) schedule_hours-Nudge: search_backstop erkennt jetzt auch Oeffnungszeiten,
"hat X geoeffnet", Notdienst und Fahrplaene/naechste Abfahrt (de/en/nl/fr) und
erzwingt dafuer die Suche. +6 Offline-Tests.
2) Zentrale Datum-/Uhrzeit-Auskunft (app/core/clock.py, now_context()): jedes
Modul kann das aktuelle Datum + Uhrzeit (lokale TZ, Wochentag) erfragen.
In die System-Prompts aller antwortenden LLMs injiziert (ToolCallingLLM,
OpenRouter, lokal) -> relative Zeitangaben ("heute", "morgen", "in 3 Stunden",
"naechsten Montag") werden aufgeloest. Live: "Welcher Wochentag/Datum?" ->
korrekt OHNE Web-Suche.
3) Filler-Timing: Mindestabstand 10 s zwischen Opening und Geduldssaetzen
(FILLER_PATIENCE_INTERVAL 4->10). Sobald die Antwort vorliegt, wird ein
anstehender Filler/Geduldssatz NICHT mehr ausgegeben (answer_started-Gate).
Tests: 318 gruen. Doc §5.4/§5.7/§8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waehlt jemand (Admin/Preset) ein Modell ohne Tool-faehigen Endpoint, lieferte
OpenRouter ein 404 "No endpoints found that support tool use" -> bisher 4x Retry
mit Backoff und danach stille, konfident-veraltete Fallback-Antwort.
- ToolCallingLLM erkennt das strukturelle 404 (_looks_tool_unsupported), merkt
es fuer den Turn (_tools_unsupported), deaktiviert Tools und antwortet SOFORT
plain (kein Haenger). Log-Warnung + Metrik tool_unsupported_total.
- Backstop-Inject jetzt als SYSTEM-Kontext statt synthetischem tool_call: das
funktioniert mit tool-faehigen UND tool-unfaehigen Modellen (letztere lehnen
tool-Messages mit 405 ab). Folge: heikle Kategorien bleiben auch auf einem
tool-unfaehigen Modell korrekt; nur nicht-heikle Volatil-Fragen fallen plain.
Live verifiziert gegen mistral-2501 (tool-unfaehig): 3.7s statt Haenger,
Backstop liefert weiter "Merz"; gegen mistral-3.2 kein Regress (1 Suche).
Tests: 312 gruen. Doc §5.7/§5.8/§8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Schliesst die ~5-15 % Erst-Miss bei Fragen, deren veraltete Antwort
konfident-falsch waere (Amtstraeger, Wohnort lebender Personen, "lebt X noch").
- app/pipeline/search_backstop.py: should_force_search() erkennt solche
Phrasierungen per mehrsprachiger Regex (de/en/nl + etwas fr/es) mit
Gegenwarts-Schutz ("wer ist", nicht "wer war"). 22 Offline-Tests.
- ToolCallingLLM: bei Treffer wird die Suche DETERMINISTISCH erzwungen.
Wichtig: NICHT per tool_choice-Forcen (das honoriert das Modell nicht
zuverlaessig -> beobachtet: ignoriert, veraltete Antwort). Stattdessen
fuehrt der Wrapper die Suche selbst aus und speist das Ergebnis als
synthetischen Tool-Turn ein (_inject_forced_search). Metrik
search_forced_total. Filler/Geduld feuern auch hier.
- Haertung: _chat faengt non-JSON-200 ab (war ein Crash-/Hänger-Ausloeser,
betrifft auch Produktion).
Live verifiziert: "Wer ist Bundeskanzler?" sucht jetzt 3/3 -> Merz.
Tests: 312 gruen. Doc: §5.7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ersetzt die statischen 3-Satz-Filler durch einen kreativen, weniger
roboterhaften Mechanismus für schnelles Feedback waehrend der Web-Suche.
- app/pipeline/fillers.py: EINZIGE Pflege-Stelle. Englische Master-Listen
(12 OPENING + 16 PATIENCE). Zur Laufzeit einmal je Sprache uebersetzt und
gecacht (durchgaengig hoefliche Anrede "Sie"/"vous"/"usted"...), Fallback
Englisch. Zufaellige Auswahl statt fester Reihenfolge.
- Geduld-Schleife: bei laengerer Recherche schiebt ein Hintergrund-Task alle
FILLER_PATIENCE_INTERVAL Sekunden einen zufaelligen PATIENCE-Satz nach;
Abbruch beim ersten Antwort-Delta und im finally (keine verwaisten Tasks).
- Ephemer wie bisher: laeuft ueber on_token/_dispatch, nie in
semantic_response/History.
- Standardsprache wird beim Start vorgewaermt (warmup.py) -> erster Such-Turn
ohne Uebersetzungs-Latenz. dependencies: get_fillers()-Singleton.
Doc: Docs/weg2-tool-calling.md §5.4. Tests: 290 gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Schliesst die im Tool-Calling-Eval isolierte Restkante (nl + Pronomen-aus-
History, z. B. "Leeft hij nog?" -> "Leeft Rutger Hauer nog?") und macht die
Web-Suche metrisch beobachtbar.
- Decontextualizer (app/pipeline/decontextualizer.py): loest Pronomen der
letzten Aeusserung anhand des Verlaufs auf. Bewusst gegated (kurze
Folgefrage MIT Pronomen UND History) -> kein Extra-Call im Normalfall;
best effort (bei Fehler Original behalten).
- ToolCallingLLM nutzt die Vorstufe vor complete()/stream() und zaehlt
tool_calls_total{tool=...}.
- SonarTool zaehlt sonar_calls_total{status=ok|error}.
- Verdrahtung: Registry openrouter-tools reicht den Decontextualizer durch.
Damit ist v1 von Weg 2 vollstaendig. Tests: 290 gruen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frische-/Web-Such-Funktion: Das zentrale Modell entscheidet selbst via
web_search-Tool, ob es tagesaktuelle Fakten braucht, holt sie über
perplexity/sonar und formuliert die Antwort in Persona (Augment).
- SonarTool (app/tools/web_search.py): Fakten via perplexity/sonar,
Citations als Metadaten, honest-punt-Sentinel bei Fehler/Timeout.
- ToolCallingLLM (app/providers/llm/tool_calling.py): agentischer Loop als
LLMProvider; complete() + gestreamtes stream() mit SSE-Tool-Assembler;
Persona- + Trigger- + Vorrang-Prompt (Tool-Ergebnis schlaegt Gedaechtnis).
- Verdrahtung: Registry-Eintrag openrouter-tools; web_search_enabled
(global an, pro Nutzer/Profil abschaltbar) via Route-Layering;
build_orchestrator waehlt tool-faehig vs. plain, Fallback-Kette erhalten.
- Filler: ephemerer Beruhigungssatz beim Tool-Start (sofort angezeigt UND
gesprochen als Satz null), nie in semantic_response/History; on_tool_start
defensiv durch die stream()-Kette gefaedelt (kein Bruch bestehender Provider).
- Modellwechsel: Standard auf mistralai/mistral-small-3.2-24b-instruct
(tool-faehig; im Eval einziger Recall-Gate-Passer). 2501 ist tool-unfaehig.
- Eval-Harness (eval/tool_calling/): Datensatz + Runner zur Modellauswahl.
Doc: Docs/weg2-tool-calling.md. Tests: 290 gruen.
Offen (Schritt 5): Koreferenz-Vorstufe (nl-Pronomen) + Metrik-Zaehler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Das Sprach-Dropdown (lang-sel) und die Profil-Sprachauswahl (words-lang)
rendern aus hartcodierten <option>-Elementen in index.html, nicht aus der
ALL_LANGS-Konstante in app.js. NL fehlte in beiden Listen, deshalb tauchte
es nach Hard-Reload nicht im Menü auf (PT schon, weil es dort schon drin
war).
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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>
- 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>
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>
Eigentliche Ursache für 'immer weibliche Stimme, egal welcher Provider':
UserPrefs kannte kein Feld voice_gender, daher verwarf pydantic in
PUT /api/me/prefs die Geschlechtswahl still. resolve_route sah immer 'any'
-> Piper/Cartesia lieferten die weibliche Stimme (kerstin bzw. Alina).
- voice_gender zum Schema hinzugefügt; Frontend sendet es bereits, resolve_route
liest es bereits (ROUTE_KEYS), voice_for_route mappt m/f bereits.
- Regressions-Test: voice_gender persistiert über /api/me/prefs.
- Docs/stimmen-inventar.md: zentrale Übersicht aller Stimmen je Provider/
Sprache/Geschlecht (Name, Datei, UUID) inkl. Fundstellen im Code.
Live verifiziert (Profil jamulix, de): m -> de_DE-thorsten-high,
f -> de_DE-kerstin-low.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Variante 1: Senioren behalten ihren persönlichen Link; zusätzlich ein
Passwort-Einstieg ohne 2FA über Authelia.
- Neuer Endpunkt GET /api/login (app_login): hinter Authelia (nginx-Location
/login, one_factor), erkennt den per Remote-User weitergereichten Nutzer
(Admin ODER normaler Authelia-Nutzer), mintet das va_token-Cookie und leitet
in die App (/). Cookie-Mint in _issue_login_cookie() gemeinsam mit
/api/admin/login refaktoriert.
- "Persönlicher Zugang nötig"-Seite (main.py) bekommt einen großen Knopf
„Mit Benutzername & Passwort anmelden" -> /login. Bare / leitet bewusst NICHT
automatisch um (Senioren ohne Authelia-Konto würden sonst feststecken).
Begleitende Infra (außerhalb des Repos):
- nginx: location = /login (auth_request one_factor -> /api/login).
- Authelia: access_control-Regel voice.jamulix.de -> one_factor (über der
*.jamulix.de-Wildcard); damit sind App-Login UND Admin nur noch 1FA, andere
Subdomains bleiben 2FA.
Tests: /api/login für Nicht-Admin/Admin/ohne-Identität. 257 Offline-Tests grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Das Kopfzeilen-Symbol „Neues Gespräch" wird unterhalb von 384px Viewport-Breite
ausgeblendet (hidden min-[384px]:grid), damit auf sehr schmalen Phones (z.B.
iPhone SE 375px, kleine Androids ≤360px) der Titel Platz behält. Auf gängigen
Phones (iPhone 12/13/14 390px, Pixel 393px) bleibt es sichtbar.
Kein Funktionsverlust: das Menü enthält „Neues Gespräch" dauerhaft als Rückfall
— es muss also nichts dynamisch verschoben werden, nur das Kopfzeilen-Symbol
ausgeblendet. SOS bleibt auf allen Breiten sichtbar. Reine CSS-Lösung (kein JS,
kein Flackern). Grenze empirisch eingemessen (voller Titel passt mit Symbol ab
~420px; 384px hält ihn auf den meisten Geräten sichtbar bei mild gekürztem Titel).
Verifiziert via Headless-Chrome über mehrere Breiten.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
Die App-Oberflaeche (/) ist bewusst nur per persoenlichem Zugangslink
erreichbar (kein SSO, damit Senioren keine Login-Maske sehen). Ein Admin, der
seinen Link verliert, kam bisher nicht mehr in die App-UI.
Neuer Endpunkt GET /api/admin/login: liegt hinter Authelia (nginx-Location
/api/admin/ mit auth_request + error_page-Redirect, Remote-User durchgereicht),
erkennt den SSO-Admin, mintet ein frisches Capability-Token, setzt es als
va_token-Cookie und leitet in die App (/). Damit meldet sich der Admin allein
mit seinem Authelia-Passwort an. Jeder Aufruf rotiert den Token (alte Links
dieses Admins werden ungueltig) - fuer eine Recovery-Funktion korrekt.
Die huebsche URL https://voice.jamulix.de/admin-login wird per nginx-Alias auf
diesen Endpunkt gelegt (nginx-Config liegt ausserhalb des Repos).
Doku: BEDIENUNGSANLEITUNG.md § 7.6 - Selbstanmeldung, Recovery per
ADMIN_API_KEY (SSO-unabhaengig) und Haertung (zweiter Admin in ADMIN_USERS,
Key im Passwortmanager).
Tests: Cookie wird gesetzt + 303 -> /, gesetzter Token authentifiziert,
Token-Rotation, Nicht-Admin/ohne Identitaet -> 403.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Im Streaming-Pfad lief die satzweise TTS-Synthese bisher inline in der
LLM-Leseschleife: pro Satzgrenze wartete `chat_stream` per `await` auf die
Synthese, wodurch das Token-Streaming (Text in der Antwort-Bubble) satzweise
pausierte.
Jetzt Producer/Consumer: Die LLM-Leseschleife legt fertige Sätze in eine
asyncio.Queue und liest sofort weiter; ein einzelner Consumer-Task
synthetisiert sequenziell und ruft `on_audio` in Reihenfolge auf. Ein
einzelner Consumer garantiert die Audio-Reihenfolge (gapless-Invariante im
Client). Sentinel + await am Ende, Exceptions aus dem Consumer werden
propagiert, verwaiste Tasks werden im finally abgeräumt.
Deterministisch gemessen (Fake-LLM + Fake-TTS 3x800ms Synthese):
Token-Spanne 1845ms -> 242ms, größtes Token-Loch 821ms -> 20ms.
Audio-Chunks bleiben in Reihenfolge (seq 0..n). text_only- und
Nicht-Streaming-Pfad unverändert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
Gegen "Blödsinn", v. a. an Handys:
1) Modell-Default base -> small + compute_type int8 (Benchmark auf der VM:
base patzt schon bei sauberem Audio "an Dienstag und 10 Uhr", small korrekt
"am Dienstag um 10 Uhr"; ~4 s/6-s-Clip, gecacht).
2) faster_whisper.transcribe mit vad_filter=True (+min_silence 500ms) und
condition_on_previous_text=False -> weniger Stille-/Kontext-Halluzinationen.
3) stt_force_language (Default True): WS-Pfad gibt die feste Zielsprache vor
statt Auto-Detect -> robust bei kurzen/verrauschten Clips. Auto-Detect +
Fremdsprachen-Übersetzung jetzt opt-in (stt_force_language=False).
Tests: Config-Defaults + VAD/Language-Durchreichung; test_ws-Übersetzung auf
opt-in umgestellt. Suite 253 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
uvicorn konfiguriert nur seine eigenen Logger -> app.*-INFO-Logs (u. a. die
Notruf-Diagnosezeile geo/geo_error/device) wurden durch den Default-Level
(WARNING) verschluckt. main.py setzt jetzt den "app"-Logger auf LOG_LEVEL
(Default INFO) mit eigenem Handler (propagate=False, kein Doppel-Log).
Verifiziert: "INFO:app.safety.emergency:Notruf von … geo=… device=…" erscheint.
+1 Test, Suite 251 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
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>
- 🆘-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>