Der Abmelden-Button blendet sich erst nach dem ersten erfolgreichen
Status-Abruf ein - bei zugestopftem/schmalem Header oder einer alten, nicht
neu geladenen Seite war er dadurch schwer zu finden. ".../?logoff" an die
Leitstand-URL angehaengt loest denselben Ablauf jetzt garantiert aus,
unabhaengig vom Button. Query-String wird danach aus der URL entfernt,
damit ein Reload nicht erneut abmeldet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
HTTP Basic Auth kennt keine Server-Session zum Beenden - der Browser cached
die Zugangsdaten selbst. Der neue Button loescht das Testzugang-Cookie und
schickt einen Request mit falschen Zugangsdaten (per XHR mit explizitem
user/password), was den Basic-Auth-Cache der gaengigen Browser ueberschreibt;
ein Reload danach zeigt wieder den Anmelde-Dialog. Nur sichtbar, wenn
ueberhaupt ein Zugangsschutz aktiv ist (s.nutzer gesetzt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
Mit mehreren Leitstand-Konten (praxis/notdienst/admin) war im UI selbst
nicht erkennbar, als wer man gerade angemeldet ist. /api/status liefert
jetzt "nutzer" (aus self._nutzer, siehe voriger Commit), das Frontend zeigt
ihn im Header neben dem Stand-Zeitstempel ("👤 notdienst").
Nebenbei behoben: dienste.status() gibt ein gecachtes, ueber Requests
geteiltes dict zurueck - der Handler mutierte es bisher direkt
(testzugang-Feld). Bei gleichzeitigen Anfragen verschiedener Nutzer haette
das den Nutzernamen der einen Anfrage in die Antwort einer anderen
durchsickern lassen koennen. Jetzt eine flache Kopie vor dem Mutieren.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
_angemeldet() merkt sich jetzt den eingeloggten Nutzernamen (self._nutzer) -
bei mehreren Leitstand-Konten (LEITSTAND_ZUGAENGE) war bisher nicht
nachvollziehbar, wer eine Karte zwischen den Stapeln verschoben hat. Der
Verlauf-Eintrag zeigt "(von <nutzer>)" in derselben Zeile, "nutzer" steht
zusaetzlich als eigenes Feld im JSONL (Verlauf + Audit-Log).
Getestet: Verschieben per Testanruf + notdienst-Login, Eintrag im Verlauf
verifiziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
Welches Profil (PROFIL) gerade aktiv ist, stand bisher nur in der .env - im
laufenden Leitstand war es unsichtbar, obwohl Kategorien und Ansage sich
je nach Profil komplett unterscheiden (Praxis vs. Aufzug-Notdienst).
kategorien.json bekommt ein "profil_name"-Feld (Anzeigename), neues
kategorien.PROFIL_NAME laedt es. Browsertab-Titel und Header-Untertitel im
Leitstand sowie Titel/Ueberschrift im Dashboard-Export zeigen es jetzt an.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
Bisher gab es genau ein Nutzer/Passwort-Paar (LEITSTAND_USER/LEITSTAND_PASS).
Fuer einen zweiten Einsatzzweck/ein zweites Team am selben Leitstand (z.B.
Notdienst neben Praxis) reicht das nicht. LEITSTAND_ZUGAENGE in der .env
nimmt jetzt zusaetzliche "nutzer:passwort"-Paare (kommagetrennt) an - das
bestehende LEITSTAND_USER/LEITSTAND_PASS bleibt als erstes Konto gueltig,
config.leitstand_konten() buendelt alle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
Kategorien, Prompt und Notfall-Sicherheitsnetz waren fest auf eine
Hausarztpraxis zugeschnitten (categorize.py: KATEGORIEN-Liste, _NOTFALL_MUSTER-
Regex). Fuer einen anderen Einsatzzweck (z.B. 24h-Aufzug-Notdienst) musste
bisher Python-Code geaendert werden.
Neu: profile/<name>/ buendelt prompt.txt, kategorien.json, sicherheitsnetz.txt
und ansage.txt - alles, was fachlich vom Einsatzzweck abhaengt, an einer
Stelle. PROFIL in der .env waehlt den aktiven Ordner (Default "praxis", altes
Verhalten bleibt exakt gleich - per Test verifiziert: identische Kategorien-
Liste und Sicherheitsnetz-Muster). profile/aufzug-notdienst/ liegt als
ausformuliertes Beispielprofil bei (eigene Kategorien, eigenes
Sicherheitsnetz, eigene Ansage).
Dringlichkeitsstufen (niedrig/normal/hoch/notfall) bleiben bewusst fest -
nur die Kategorien sind einsatzzweck-spezifisch. Neues pipe/kategorien.py
laedt das Profil-Schema einmal fuer categorize.py, dashboard.py, deck.py und
server.py (Leitstand-JS: Kategorie-Label kommen jetzt vom Server statt fest
im Frontend zu stehen).
prompts/categorize_de.txt und telefon/ansage.txt sind nach profile/praxis/
umgezogen (git erkennt es als Rename).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
Bisher stand nur config.OLLAMA_MODELL zur Auswahl - ein neu per "ollama pull"
installiertes Modell (z.B. ornith-1.5:9b) tauchte nicht im Leitstand auf, ohne
Code-Aenderung. modellwahl.auswahl() fragt jetzt GET /api/tags live ab und
baut die Ollama-Optionen daraus; reine Embedding-Modelle (capabilities ohne
"completion", z.B. nomic-embed-text) werden ausgefiltert, weil sie nicht
kategorisieren koennen. Die festen OpenRouter-Testeintraege bleiben bestehen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
Dienste-Ampel (Telefon, Verarbeitung, Spracherkennung, Nextcloud) alarmierte
bisher nur im offenen Leitstand. pipe/monitor.py laeuft jetzt als eigener
Dauerprozess, prueft dieselbe Ampel (neu ausgelagert nach pipe/dienste.py)
periodisch und alarmiert bei Stoerung lokal per macOS-Benachrichtigung + Ton
(pipe/alarm.py) - unabhaengig davon, ob pipe.watch oder pipe.server laufen.
Wird ab jetzt von telefon/starten.sh mitgestartet.
Verschluesselung at rest: keine App-eigene Verschluesselung eingebaut,
stattdessen im README als Betreiber-Pflicht dokumentiert (FileVault aktivieren)
- App verlaesst sich auf Festplattenverschluesselung durch das Betriebssystem.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
CSS-Blur (Sicher-Modus) verdeckt nur die Anzeige - im Seitenquelltext,
DevTools oder Netzwerk-Tab standen die echten Rufnummern trotzdem drin,
weil sie ganz normal im JSON von /api/anrufe und /api/verlauf ankamen.
Bei Testzugang (per Token statt Passwort, self._via_token) werden nummer/
rueckrufnummer jetzt vor dem Senden durch einen Platzhalter ersetzt
(_maskiere_anrufe), im Verlauf-Log werden eingebettete Ziffernfolgen ab
9 Stellen ersetzt (_maskiere_verlauf, gleiche Heuristik wie das bestehende
Frontend-telBlur, nur serverseitig und wirklich ersetzend statt nur
verdeckend). Die echten Werte verlassen den Server in diesem Modus nicht
mehr.
Getestet: 13 unmaskierte Rufnummern im Verlauf bei Passwort-Zugriff, 0 bei
Token-Zugriff; _maskiere_anrufe() isoliert verifiziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Wer per "agent-one test"-Link kommt, braucht kein Passwort - also duerfen
fremde Tester keine echten Rufnummern sehen. /api/status liefert jetzt
"testzugang" (true, wenn die Anfrage per Zugangs-Token statt Passwort
authentifiziert wurde), das Frontend erzwingt darauf sicherSetzen(true) und
macht den Sicher-Modus-Button zum No-Op (Toast statt Abschalten).
Mit Playwright verifiziert: Laden mit gueltigem Token setzt Sicher-Modus
sofort, Klick auf den Button aendert daran nichts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Gegenstueck zum "agent-one test"-Kommando in Alice (dillenberg.net,
dilles-agent.php). Kein Live-Aufruf zwischen den Systemen noetig - beide
teilen sich TESTZUGANG_SECRET, das Token ist ein HMAC-signierter
Ablauf-Zeitstempel ("ablauf.signatur"), stateless geprueft.
pipe/testzugang.py: gueltig(token) prueft Signatur (hmac.compare_digest)
und Ablauf. _angemeldet() in server.py akzeptiert das Token zusaetzlich zum
Passwort - per ?zugang=... in der URL oder per zugang-Cookie fuer
Folgeanfragen (fetch()-Aufrufe der Seite tragen die URL nicht mit). Ein
frisches URL-Token setzt das Cookie (HttpOnly, Secure, SameSite=Lax) fuer
den Rest der Stunde.
End-to-End getestet: PHP-generiertes Token wird von Python akzeptiert
(Cross-Language-HMAC-Kompatibilitaet bestaetigt), ungueltige Signatur und
abgelaufenes Token werden korrekt abgelehnt (401), Cookie-Folgeanfrage ohne
erneutes Token/Passwort funktioniert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Modal hat jetzt zwei Dropdowns (Kategorie, Status/Dringlichkeit) oberhalb
des Freitextfelds, vorbelegt mit den aktuellen Karten-Werten - man
korrigiert nur, was falsch ist, statt alles neu waehlen zu muessen.
meldung.oeffnen() nimmt dafuer kategorie/dringlichkeit entgegen, der
generische Rahmen bleibt erhalten (felder:false blendet die Auswahl fuer
kuenftige nicht-kategorisierungsbezogene Meldungen aus).
aufSpeichern bekommt jetzt {text, kategorie, dringlichkeit} statt nur
Freitext - mit Playwright verifiziert (Vorbelegung + tatsaechlicher
POST-Body).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Ueber die Session angesammelte Badges (3x separates "ungeprüft"-Chip fuer
Name/Rueckrufnummer/E-Mail, je eigene .meta-Zeile) sahen zugemuellt aus.
- Ein einziger dezenter Hinweis (.hinweis-klein, kein Rahmen/Chip, kleiner
kursiver Text) statt Border-Pill, direkt am Namen.
- Rueckrufnummer + E-Mail jetzt auf einer gemeinsamen Zeile mit einem
gemeinsamen "ungeprüft"-Hinweis statt zwei einzelnen Chips.
- "vermutete Korrektur" (E-Mail-@-Reparatur) bleibt als eigener, staerkerer
Warnhinweis (.warn-klein, andere Bedeutung: Daten evtl. falsch, nicht nur
ungeprueft).
- Titel jetzt eigene Zeile statt im Kopf mit Badges zu teilen - bessere
Zeilenumbrueche bei langen Namen, Dringlichkeits-Badge + Melde-Button
bilden eine eigene Kopfzeile oben rechts.
Nebenbei Bug behoben: rueckrufZeile/emailZeile hiessen im Template noch
beim alten Namen (kontaktZeile) - haette beim ersten echten Kartenrendering
gecrasht, mit Playwright vor dem Commit abgefangen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
"📝 Text" war ohne den title-Tooltip nicht verstaendlich - und title-
Tooltips funktionieren auf Touch-Geraeten meist gar nicht (kein Hover).
Jetzt "🎙️ ungeprüft" direkt sichtbar, Tooltip bleibt als Zusatzinfo für
Maus-Nutzer erhalten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Auf Wunsch umgebaut: kein 👍 mehr, nur noch ein einzelner, dezenter
👎-Melde-Button oben rechts auf der Karte (bei der Dringlichkeits-Badge)
statt der Bewertungszeile unten. Klick oeffnet ein Modal mit Freitext-
Eingabe statt der bisherigen Kategorie/Dringlichkeit-Dropdowns - fuer
Entwicklungs-Feedback ist Prosa nuetzlicher als starre Felder.
Neues kleines, wiederverwendbares JS-"Framework" (Objekt "meldung" im
Frontend): meldung.oeffnen({titel, kontext, aufSpeichern}) - nicht fest an
den Melde-Button gekoppelt, fuer kuenftige weitere "Melden"-Stellen ohne
neues Modal bauen zu muessen. Escape/Klick-außerhalb schliessen, takt()
pausiert waehrend das Modal offen ist (gleiches Muster wie beim Audio-Fix).
Speicherung laeuft weiter durch die bestehende bewertung.json/Export-
Pipeline (korrektur ist jetzt Freitext statt strukturierter Felder).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Gleiches Muster wie der Audio-Refresh-Bug: zeichneBoard() baut die Karten
alle 3s per innerHTML komplett neu, das riss eine gerade geoeffnete
👎-Korrektur-Auswahl (Kategorie/Dringlichkeit-Dropdowns) wieder weg, bevor
man sie ausfuellen konnte - "erscheint, verschwindet wieder". takt()
pausiert jetzt zusaetzlich, solange .bw-korrektur.zeigen offen ist. Mit
Playwright verifiziert: bleibt jetzt auch nach 7s (mehrere Poll-Zyklen)
sichtbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
"Logpy:AgentOne — Anruf-Leitstand" als eine Zeile war zu lang/unruhig.
Jetzt "Logpy:AgentOne" (h1) mit "Anruf-Leitstand" als kleiner, gedaempfter
Untertitel darunter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Logo (ComfyUI/z-image-turbo generiert: Telefonhoerer + KI-Schaltkreis-Motiv,
docs/logo.png) als Base64 direkt im Header eingebettet - konsistent mit dem
bereits inline eingebetteten Favicon, kein neuer Static-Asset-Serving-Pfad
noetig. Funktioniert in Hell und Dunkel (weisse Kreis-Badge).
a.mail-Klasse fuer den mailto:-Link auf der Karte ergaenzt (vorher
unstyled), gleiche gepunktete Unterstreichung wie bei Telefonnummer-Links.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Whisper transkribiert gesprochenes "at" beim Diktieren einer E-Mail manchmal
als Punkt statt "@" (beobachtet: "gdg at dillenberg punkt net" ->
Transkript "gdg.dillenberg.net", komplett ohne @). Zwei Massnahmen:
1. WHISPER_PROMPT um ein Formatierungsbeispiel ergaenzt (max.mueller@
example.de) - bekannter Whisper-Trick, um Formatierungskonventionen ueber
den initial_prompt zu primen. Reduziert das Problem an der Quelle, bleibt
aber modellabhaengig.
2. Neues Schema-Feld "email" (categorize.py, wie rueckrufnummer). Deter-
ministische Reparatur per Regex: "wort.domain.tld" ohne @ wird zu
"wort@domain.tld" - aber nicht stillschweigend, sondern im Leitstand als
"⚠️ vermutete Korrektur" markiert (koennte auch eine genannte Subdomain
sein, daher pruefbar statt automatisch als Fakt behandelt).
Getestet mit dem realen Fall (Brunski Beat, "gdg.dillenberg.net" ->
"gdg@dillenberg.net" + Korrektur-Flag), bereits gespeicherter Anruf
nachkorrigiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
"Tag archivieren" archiviert jetzt zusaetzlich das Verlauf-Log (Kopie nach
telefon/verlauf-archiv/verlauf_JJJJMMTT-HHMMSS.log) und leert es danach -
frische Verlaufsansicht fuer den neuen Tag. Das dauerhafte, unveraenderliche
audit.log ist davon nicht betroffen, das behaelt ohnehin alles fuer immer.
Live getestet: 530 Zeilen archiviert, Log korrekt geleert (nur noch die
Bestaetigungszeile drin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Vorher Standard-Browser-Scrollbar. Jetzt duenn und in --muted eingefaerbt,
sowohl fuer Firefox (scrollbar-width/-color) als auch WebKit/Chromium
(::-webkit-scrollbar).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Vorher ganz unten nach dem Board - Diensteausfaelle (Telefon/Verarbeitung/
Spracherkennung/Nextcloud) sollten aber auf den ersten Blick sichtbar sein,
nicht erst nach Scrollen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Anruf-IDs im Log ("23-44-01_004915223062462" - Zeitstempel+Nummer aus
ordner.name) waren bisher vom Sicher-Modus nicht erfasst, nur die
Kartenansicht. telBlur() umschliesst gezielt Ziffernfolgen ab 9 Stellen mit
einem blurrbaren Span - Datum/Uhrzeit-Anteile (<=8 Ziffern in diesem Format)
und der restliche Log-Text bleiben unangetastet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
README-Titel, Leitstand-Browsertitel/-Header und Footer angepasst. Technischer
Name bleibt vorerst fonagent-one (Repo-Slug, Python-Package, Ordnerpfade) -
Rename dort waere ein groesserer, schwerer umkehrbarer Schritt (bricht den
bereits geteilten GitHub-Link), dafuer erst Rueckmeldung einholen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Jede Karte im Leitstand bekommt Daumen hoch/runter fuer die Kategorisierung.
Bei 👎 optional Korrektur (richtige Kategorie/Dringlichkeit). Gespeichert
als bewertung.json pro Anruf-Ordner (gleiches Muster wie stapel.json,
pipe/bewertung.py).
Export als JSONL (pipe/export.py, neuer "📤 Bewertungen exportieren"-Knopf
+ automatisch beim "Tag archivieren"): ein Datensatz pro bewertetem Anruf
mit Transkript, Modell-Ausgabe, Backend/Modell, Bewertung und Korrektur -
fuer externe Auswertung (z.B. um Kategorisierungs-Fehler systematisch statt
zufaellig zu finden, wie das Notfall-Sicherheitsnetz vom letzten Fund).
Export-Knopf verlangt Bestaetigung ("Transkripte und Namen verlassen den
Rechner") - bewusst kein automatischer Export ohne Hinweis, das steht sonst
im Widerspruch zum On-Prem-Versprechen. training/ ist neu gitignored (volle
Transkripte/Namen drin, siehe README-Hinweis zu Testdaten).
Nebenbei: process_call.py legt jetzt auch backend/modell/dauer_s der
Kategorisierung dauerhaft in meta.json ab (vorher nur im fluechtigen Log) -
noetig, damit der Export nachvollziehbar ist, welches Modell lief.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Notfall-Sicherheitsnetz (categorize.py): qwen3:8b stufte "starke
Brustschmerzen ... kriege kaum Luft" reproduzierbar (3/3 Testlaeufe) nur als
"hoch" statt "notfall" ein, obwohl es selbst "Brustschmerzen"+"Atemnot" als
Stichworte erkannte und der Prompt genau diese als Notfall-Beispiele nennt.
Bei Notfall-Erkennung darf man sich nicht allein aufs LLM verlassen -
_notfall_sicherheitsnetz() prueft den rohen Transkripttext deterministisch
gegen die im Prompt selbst genannten Symptome (Atemnot, Brustschmerz,
Bewusstlosigkeit, starke Blutung) und eskaliert nur nach oben, nie runter.
Board-Refresh-Bug (server.py): der Audio-Fix aus einem frueheren Commit
("audio:not([paused])") pruefte ein HTML-Attribut, das <audio> nie gesetzt
hat - der Selektor traf auf JEDES Audio-Element zu, egal ob es lief. Das
Board hat sich seitdem praktisch nie mehr neu gezeichnet, sobald irgendeine
Karte eine Aufnahme hatte (also fast immer) - erklaert "Dashboard
aktualisiert manchmal nicht". Jetzt echte .paused-DOM-Eigenschaft geprueft.
Verbindungsverlust-Anzeige: bisher schluckte hole() Netzwerkfehler still,
das Board blieb unbemerkt eingefroren. Neues Banner ab 2 aufeinanderfolgenden
fehlgeschlagenen Poll-Versuchen (~6s), zeigt Zeitpunkt der letzten
erfolgreichen Aktualisierung, verschwindet automatisch bei Erholung.
Bereits gespeicherter Peter-Fuchs-Fall (Brustschmerz+Atemnot) manuell auf
notfall/notfall nachkorrigiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
auswertung.rueckrufnummer wurde schon immer extrahiert, aber im Leitstand
nie angezeigt - nur die CLIP-Nummer war sichtbar. Ruft jemand von fremdem
Anschluss an und nennt eine andere Rueckrufnummer, ging die im UI unter.
Neue Zeile "☎️ Rückruf an: ..." nur wenn sie sich von der CLIP-Nummer
unterscheidet (sonst Redundanz), klickbar (tel:), mit demselben "Text"-
Quellenlabel wie beim Namen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Zweites Kategorisierungs-Backend neben Ollama (pipe/categorize.py), fuer
Vergleichstests mit staerkeren Cloud-Modellen. Umschaltbar per Dropdown oben
rechts im Leitstand (pipe/modellwahl.py, geteilte telefon/modell.json) - der
laufende Watcher braucht dafuer keinen Neustart, die Auswahl wird bei jedem
Anruf frisch gelesen.
Jede Kategorisierung loggt jetzt Backend/Modell/Dauer/Tokens/Kosten mit ins
Leitstand-Verlauf (protokoll.schreibe), nicht nur versteckt in meta.json.
Testergebnis (Nemotron ueber OpenRouter, :free-Tier): stark schwankende
Latenz (8,5s bis 80s je nach Modell/Auslastung), einmal 502 "temporarily
overloaded" - :free-Modelle sind fuer spaeter geplanten Echtzeit-Dialog
ungeeignet, dafuer braucht es ein bezahltes/dediziertes Tier.
telefon/modell.json ist Laufzeit-Zustand, kein Quellcode - neu ignoriert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
Der Kartenname kam bisher unmarkiert vom LLM aus dem Transkript (Vermutung,
kein Abgleich) - jetzt ein "Text"-Label direkt am Namen, analog zum
bestehenden kontakt-badge fuer den echten Rufnummer-Abgleich gegen das
Adressbuch. Grundlage fuer einen spaeteren Abgleich beider Quellen
gegeneinander (geplant, noch nicht umgesetzt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
- Audio-Wiedergabe im Leitstand brach durch Auto-Refresh (setInterval) ab,
Karten-DOM wurde per innerHTML neu gebaut waehrend ein <audio> lief.
takt() pausiert jetzt, solange eine Aufnahme abgespielt wird. Zusaetzlich
HTTP-Range-Unterstuetzung fuer /audio/ nachgeruestet (war vorher immer
volle Datei statt 206 Partial Content).
- Sicher-Modus: Rufnummern per Klick blurren, fuer Screenshots/Aufnahmen.
- Notfall-Alarm: pulsierender Karten-Rahmen + Banner + minuetlicher Ton,
solange ein Notfall nicht im letzten Stapel (Erledigt) liegt.
- Favicon.
- Neues unveraenderliches audit.log (append-only, macOS uappnd-Flag) als
Nachweis fuer den Umgang mit Anrufdaten - protokoll.schreibe() spiegelt
jetzt jedes Ereignis dorthin, zusaetzlich zum rotierenden verlauf.log.
- pipe.loeschen: loescht erledigte Anrufe nach LOESCHFRIST_TAGE (lokal +
Nextcloud), unbearbeitete Anrufe (nicht im letzten Stapel) bleiben immer
unangetastet. Gedacht fuer taeglichen Cron-Lauf.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vtvcyNkapuDh583643hNs
pipe/kontakte.py: CardDAV-Abgleich (stdlib urllib, gleicher Stil wie
store.py) gegen ein konfigurierbares Adressbuch (NEXTCLOUD_ADRESSBUCH,
Default "contacts"). Rufnummer wird normalisiert (+49/0049/0-Schreibweisen,
Leerzeichen/Bindestriche), Adressbuch 5 Minuten gecacht statt pro Anruf neu
geladen. Ohne Nextcloud-Konfiguration oder bei Ausfall: sauber None, kein
Anruf bricht deswegen ab.
process_call.py speichert den Treffer als "bekannter_kontakt" in meta.json;
server.py zeigt ihn als gruenen Badge neben der Rufnummer im Board - fuer den
Abgleich "Anrufer sagt X, Nummer gehoert laut Adressbuch Y" auf einen Blick.
Nextcloud-Zugangsdaten aktivierten bisher automatisch auch den Datei-Upload
(store.hochladen) - fuer den reinen Kontakt-Abgleich waere das ein unge-
wollter Nebeneffekt, der das "Vollstaendig on-prem"-Versprechen des Projekts
bricht. Neuer Schalter NEXTCLOUD_UPLOAD (Default an, bestehende Installa-
tionen unveraendert) trennt beides.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuP3zfgYqhnrwyEne2ejG7
Raeumt Anrufe aus dem letzten Stapel ("Erledigt") vom Board - markiert sie
per archiviert.json im Anrufordner (nur Zeitstempel, simuliert: true), keine
Datei wird verschoben, exportiert oder geloescht. anrufe() filtert markierte
Anrufe aus der Board-Ansicht, ablage/ bleibt unveraendert. Reversibel durch
Loeschen der Marker-Datei.
Bestaetigung vor dem Archivieren, Toast mit Anzahl danach, Eintrag im
Verlauf-Log. Echtes Archivieren (Export/Retention/Loeschfristen) ist noch
offen - siehe MITGABE.md Abschnitt 6.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuP3zfgYqhnrwyEne2ejG7
pipe/server.py rief fs_cli mit fest verdrahtetem Port 8099 auf (Wert vom
Ursprungsrechner) statt den Wert aus der Config zu lesen - auf einer Maschine
mit abweichendem Event-Socket-Port (hier 8022) zeigte der Leitstand "Telefon:
unklar", obwohl der Trunk laengst registriert war. Neue FS_PORT-Variable in
pipe/config.py (Default 8021, wie starten.sh es ueber die .env erwartet).
DECK_STAPEL in .env.example war unquotiert - das Leerzeichen in "In
Bearbeitung" bricht das `source .env` in telefon/freeswitch/einrichten.sh
(bash interpretiert es als zweites Kommando). Wert jetzt in Anführungszeichen.
test/erzeuge_beispiele.sh erzeugt die im README/MITGABE referenzierten
Test-Anrufe (rezept/termin/notfall) lokal per macOS "say" - vorher liefen die
Beispielbefehle aus einem frischen Klon ins Leere, weil test/*.wav bewusst
gitignoriert ist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuP3zfgYqhnrwyEne2ejG7
Der Screenshot zeigt das Board mit allen vier Spalten, dem Verlauf darüber und
den Dienste-Ampeln darunter. Die Anrufe darin sind erfunden — er wurde aus einem
eigens angelegten Beispieldatensatz erzeugt, nicht aus dem Betrieb, damit keine
echten Rufnummern ins Repository gelangen.
Dazu ein Hinweis in der README, dass alle Namen, Nummern und Anliegen im
Repository Platzhalter sind, "Praxis Musterhausen" eingeschlossen.
Außerdem: Der Pfad zum Log der Telefonanlage ist jetzt über FREESWITCH_LOG
konfigurierbar statt fest verdrahtet — nötig, um den Leitstand gegen einen
anderen Datenbestand laufen zu lassen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017V57oXiDLi4nSsyau5qaA2
Nimmt Anrufe einer Hausarztpraxis entgegen, transkribiert sie lokal,
kategorisiert das Anliegen und legt es strukturiert ab. Keine Patientendaten
verlassen den Rechner: Spracherkennung, Sprachmodell und Sprachsynthese laufen
lokal, im Datenpfad steht kein Cloud-Dienst.
Aufbau:
- FreeSWITCH nimmt über einen SIP-Trunk an, spielt die Ansage und nimmt auf.
- pipe.watch erkennt fertige Aufnahmen und schiebt sie durch die Pipe.
- whisper.cpp transkribiert, ein lokales Sprachmodell (Ollama) ordnet in
Kategorie und Dringlichkeit ein — mit erzwungenem JSON-Schema.
- Ablage lokal, optional Nextcloud (WebDAV) und ein Deck-Board.
- Leitstand (pipe.server): Board mit Eingang/In Bearbeitung/Rückfragen/Erledigt,
Aufnahmen zum Anhören, Verlauf und Ampeln für alle beteiligten Dienste.
Zugangsdaten und Anrufdaten liegen außerhalb des Repositorys (.env, ablage/,
telefon/). Konfiguration siehe .env.example, Einrichtung siehe README.