Anrufer- und Webhook-Verarbeitung verbessern

This commit is contained in:
Sven committed 2026-11-02 15:58:53 +01:00
1 parent 7b3c187985
commit 9e71fe3a11
18 files changed
+1283 -111

No files matched your search

+305
View File
@@ -0,0 +1,305 @@
# SwyxWeb – Leitfaden für Coding-Agents
## Projektüberblick
SwyxWeb ist eine deutschsprachige Telefonie- und Kundenoberfläche für SwyxTray/SwyxIt!. Das System besteht aus drei zusammenarbeitenden Teilen:
- **Frontend:** React 19, TypeScript und Vite; stellt Dashboard, Entwickleransicht, Anruf-Popups und Aufnahmeverwaltung bereit.
- **Backend:** Spring Boot 4.1 mit Java 21 und Maven Wrapper; stellt REST-API und Server-Sent Events bereit und archiviert Daten in MongoDB.
- **Transkriptionsworker:** FastAPI mit `faster-whisper`; verarbeitet WAV-Dateien lokal, speichert Aufträge in SQLite und erstellt optional Zusammenfassungen über eine OpenAI-kompatible LLM-API.
Die Standardansicht ist ein Dashboard der gespeicherten eingehenden Anrufe. Der Schalter **Dev** blendet die weiteren Bereiche für Telefonie, Adressdaten, Webhooks, Firefox-Tabs und Verbindungsdiagnose ein. Eingehende Anrufe können Kunden- und Kurierdaten sowie zugehörige Jobs anzeigen. Gesprächsaufnahmen werden transkribiert und optional zusammengefasst; eine Sprecherzuordnung ist nicht implementiert.
Die ausführliche fachliche und protokollbezogene Dokumentation steht in `README.md`. Bei Änderungen an Verhalten, Protokoll, Konfiguration oder Deployment müssen README und gegebenenfalls diese Datei mitaktualisiert werden.
## Wichtige Laufzeitgrenzen
Die Verbindungen dürfen nicht verwechselt werden:
1. **Browser ↔ SwyxTray:** Das Frontend öffnet selbst einen WebSocket zur SwyxTray-App, standardmäßig `ws://127.0.0.1:17654/ws`. Das Java-Backend hält keine Verbindung zur echten Telefonanlage.
2. **Browser ↔ Spring Boot:** REST-Aufrufe und der Webhook-Ereignisstrom laufen über `/api/**`, in der Entwicklung zunächst über den Vite-Proxy. Webhook-Ereignisse verwenden SSE unter `/api/webhook/events`, nicht den SwyxTray-WebSocket.
3. **Spring Boot ↔ Whisper-Worker:** Das Backend reicht WAV-Uploads an den FastAPI-Worker weiter. Im Container ist das ausschließlich `http://127.0.0.1:8765`; der Worker-Port wird nicht nach außen veröffentlicht.
4. **Backend ↔ MongoDB:** MongoDB ist ein externer Dienst. `compose.yaml` enthält keinen MongoDB-Container.
Standardports:
| Port | Zweck |
|---|---|
| `5173` | Vite-Entwicklungsserver |
| `8080` | Spring Boot, REST-API und im Produktiv-Image das statische Frontend |
| `8765` | FastAPI-Whisper-Worker, nur containerintern auf Loopback |
| `17654` | SwyxTray-App auf dem Arbeitsplatz, vom Browser aus erreicht |
### Zentrale Datenflüsse
- SwyxTray sendet vollständige Snapshots und bei neueren Protokollen zusätzlich Anrufereignisse. `frontend/src/swyx/protocol.ts` und `useSwyxTray.ts` gleichen Zustände ab, verhindern doppelte Ende-Ereignisse und melden Anrufdaten an `POST /api/calls`.
- Leitungen sind 1-basiert. Bei Protokoll 10 und neuer identifiziert `callId` zusammen mit `line` einen Anruf. Aufnahme- und Auflegebefehle müssen diese Identität beachten, damit verspätete Befehle keinen Folgeanruf auf derselben Leitung treffen.
- Der Adress-Cache der Tray-App wird vom Browser an `POST /api/addresses` gemeldet. Dieser Import aktualisiert nur Name und Beschreibung und erhält Rollen, Kundenkennungen, Job-Zuordnungen und URLs aus vollständigen Webhook-Daten.
- Fremdsysteme liefern Adressen und Jobs über die Webhook-Endpunkte. Das Backend verteilt angenommene Nutzlasten sofort per SSE und schreibt MongoDB-Daten asynchron.
- Bei Protokoll 11 überträgt SwyxTray eine fertige WAV nach bestätigtem Gesprächsende in Blöcken über den bestehenden WebSocket. Der Browser lädt sie an `/api/transcriptions` hoch und bestätigt SwyxTray erst nach Erhalt der dauerhaften Worker-Auftragskennung.
- Der Worker verarbeitet jeweils einen Auftrag, speichert das Transkript vor einem optionalen LLM-Aufruf und setzt unterbrochene Aufträge nach einem Neustart fort. Ein Retry nach einem Zusammenfassungsfehler wiederholt nicht die Whisper-Transkription.
## Repository-Struktur und wichtige Dateien
```text
SwyxWeb/
├── backend/
│ ├── pom.xml Spring Boot 4.1, Java 21, Abhängigkeiten
│ ├── mvnw Maven Wrapper verwenden
│ └── src/
│ ├── main/java/de/appcreation/swyxweb/
│ │ ├── config/ Properties, CORS und Profil-geschützter Mock
│ │ ├── web/ REST-Controller für Config, Calls, Adressen, Jobs, Webhooks
│ │ ├── webhook/ SSE-Verlauf und Verteilung
│ │ ├── storage/ MongoDB-Dokumente, Archiv und Job-Bereinigung
│ │ ├── transcription/ Validierung und Weiterleitung an FastAPI
│ │ └── websocket/ SwyxTray-Mock und dessen Leitungsmodell
│ ├── main/resources/application.properties
│ └── test/java/ JUnit-/Spring-/Mockito-Tests
├── frontend/
│ ├── package.json npm-Skripte und Abhängigkeiten
│ ├── package-lock.json Für reproduzierbare Installation mit npm ci
│ ├── vite.config.ts Dev-Proxy für /api
│ ├── tsconfig*.json Strikter TypeScript-Build
│ └── src/
│ ├── pages/HomePage.tsx Dashboard/Dev-Umschaltung und Gesamtseite
│ ├── components/ React-Panels, Dashboard, Popup, Aufnahmeanzeige
│ ├── hooks/ Tray-, Webhook-, Caller-Lookup- und Upload-Zustand
│ ├── swyx/ WebSocket-Client, Protokoll und Adressabgleich
│ ├── calls.ts Anruf-API und Suche
│ ├── addresses.ts Adress-API und Rufnummernabgleich
│ ├── jobs.ts Typisierte Job-Nutzlasten
│ ├── recordings.ts WAV-Validierung und Transkriptions-API
│ └── *.test.ts(x) Vitest-Tests neben dem getesteten Code
├── transcription/
│ ├── service.py FastAPI, SQLite-Aufträge und Whisper-Verarbeitung
│ ├── summary.py OpenAI-kompatible Zusammenfassungen
│ ├── requirements.txt Fixierte Laufzeitabhängigkeiten
│ ├── requirements-test.txt Laufzeit- plus Testabhängigkeiten
│ └── tests/ pytest-Tests
├── Dockerfile Vierstufiger Gesamt-Build
├── compose.yaml Ein Container plus persistentem Aufnahme-Volume
├── docker/entrypoint.sh Start und gemeinsamer Abbruch von Java und Python
├── docker_push.sh Buildx-Build/Push für linux/amd64
├── .env.example Öffentliche Compose-Konfigurationsvorlage
└── .vscode/launch.json Compound-Starts für Backend und Frontend
```
## Entwicklung und lokale Starts
Voraussetzungen sind Java 21, Node.js/npm und für den lokalen Worker eine Python-Umgebung mit den Projektabhängigkeiten. Der Docker-Build verwendet Node 22 und Python 3.12. Für produktive Datenbankfunktionen muss die konfigurierte MongoDB erreichbar sein.
### Backend
```bash
cd backend
./mvnw spring-boot:run
```
Das Backend lauscht auf `0.0.0.0:8080`. Alternativer Port:
```bash
./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8081"
```
`application.properties` verweist standardmäßig auf eine MongoDB im internen Netz. Schreibvorgänge des Archivs sind asynchron und best-effort; eine nicht erreichbare Datenbank verhindert daher nicht jede Anzeige, wohl aber Datenbankabfragen und dauerhafte Speicherung.
### Frontend
```bash
cd frontend
npm ci
npm run dev
```
Vite läuft auf `http://localhost:5173` und leitet `/api` standardmäßig an `http://localhost:8080` weiter. `VITE_BACKEND_URL` ändert das Proxy-Ziel. Der SwyxTray-WebSocket läuft nicht über diesen Proxy.
In VS Code startet die Compound-Konfiguration **„SwyxWeb starten (Backend + Frontend)"** beide Seiten mit Debugging. Die zweite Compound-Konfiguration verwendet den Mock. Diese Startkonfigurationen lesen die projektweite `.env`; ein reiner Terminalstart aus `backend` tut das nicht automatisch.
### SwyxTray-Mock
Der Mock existiert nur mit Spring-Profil `mock`:
```bash
cd backend
./mvnw spring-boot:run -Dspring-boot.run.profiles=mock
```
Dann stehen `/ws` und `/api/mock/**` zur Verfügung. Für die vorbereitete VS-Code-Konfiguration zeigt `/api/config` automatisch auf `ws://localhost:8080/ws`. Einen Testanruf auslösen:
```bash
curl -X POST "http://localhost:8080/api/mock/incoming-call?number=%2B493012345&name=Muster%20GmbH"
```
Der Mock bildet Telefonie-, Adress-, Tab- und Aufnahmeprotokolle ab, erzeugt aber keine Audiodateien. Ohne das Profil sollen `/ws` und `/api/mock/**` mit 404 antworten; dieses Opt-in nicht durch allgemeine Konfigurationsänderungen aushebeln.
### Lokaler Transkriptionsworker
Ein vollständiges CTranslate2-Whisper-Modell muss bereits lokal vorliegen; der Worker lädt es nicht selbst aus dem Internet:
```bash
python3 -m venv transcription/.venv
transcription/.venv/bin/pip install -r transcription/requirements.txt
WHISPER_MODEL_PATH=/pfad/zum/whisper-modell WHISPER_DATA_DIR=/pfad/zu/auftraegen \
transcription/.venv/bin/python -m uvicorn service:app --app-dir transcription \
--host 127.0.0.1 --port 8765
```
Für den Terminalstart des Backends zusätzlich:
```bash
cd backend
APP_TRANSCRIPTION_BASE_URL=http://127.0.0.1:8765 ./mvnw spring-boot:run
```
Ohne `APP_TRANSCRIPTION_BASE_URL` bleibt die Transkriptionsübergabe außerhalb von Docker deaktiviert.
## Build-Befehle
Es gibt kein übergreifendes Root-Buildskript; die Teilprojekte werden einzeln oder durch das Dockerfile gebaut.
```bash
# Backend: Tests ausführen und target/backend-0.0.1-SNAPSHOT.jar erzeugen
cd backend
./mvnw clean package
# Frontend: Typprüfung und Produktionsbundle in frontend/dist
cd frontend
npm ci
npm run typecheck
npm run build
# Produktionsbundle lokal über Vite ansehen
npm run preview
```
`npm run build` führt `tsc -b && vite build` aus. Um ein lokal gebautes Frontend in das Spring-Jar aufzunehmen, muss `frontend/dist/*` vor dem Maven-Packen nach `backend/src/main/resources/static/` kopiert werden. Der Docker-Build übernimmt diesen Schritt selbst.
## Tests und Prüfstrategie
### Backend
```bash
cd backend
./mvnw test
# Einzelne Testklasse:
./mvnw -Dtest=WebhookControllerTests test
```
- JUnit 5, AssertJ, Mockito, Spring Boot Test und MockMvc.
- Controller- und Profil-Tests ersetzen die Archivschicht mit `@MockitoBean`; MongoDB ist für die Tests nicht erforderlich.
- `ArchiveService` wird mit einem gemockten `MongoTemplate` geprüft.
- Der Transkriptionsclient wird gegen einen lokalen JDK-`HttpServer` getestet, nicht gegen einen echten Whisper-Dienst.
- Der Mock wird einschließlich Profilaktivierung, SSE-Verhalten und Aufnahmeprotokoll getestet.
### Frontend
```bash
cd frontend
npm test
# Einzelne Datei:
npm test -- src/swyx/protocol.test.ts
```
- Vitest-Tests liegen als `*.test.ts` beziehungsweise `*.test.tsx` neben dem Code.
- Die meisten Tests laufen in der Standardumgebung mit gefakten WebSockets, Timern und HTTP-Funktionen. Hook-Tests, die DOM benötigen, kennzeichnen `// @vitest-environment jsdom` direkt in der Datei.
- Schwerpunkte sind Protokoll-Parsing, Snapshot-/Ende-Reihenfolgen, Leitungswiederverwendung, Aufnahme-Timeouts, WAV-Blöcke, Adressabgleich, Job-Parsing und Webhook-Zusammenführung.
- React-Hooks werden mit `react-dom/client` und `act` getestet; es ist keine separate React-Testing-Library konfiguriert.
### Transkriptionsworker
Vom Projektroot:
```bash
python3 -m venv transcription/.venv
transcription/.venv/bin/pip install -r transcription/requirements-test.txt
transcription/.venv/bin/pytest -q transcription/tests
```
- Transkription und Zusammenfassung werden überwiegend durch Funktionen ersetzt; kein Modell-Download und kein externes LLM sind für die Tests nötig.
- Ein Test prüft zusätzlich die echte WAV-Dekodierung mit den installierten `faster-whisper`/PyAV-Abhängigkeiten.
- LLM-Tests verwenden einen lokalen HTTP-Testserver und prüfen auch Timeouts, Redirect-Verbot, Antwortgrenzen und das Vermeiden von Geheimnissen in Fehlermeldungen.
Es ist keine separate Browser-E2E- oder CI-Pipeline im Repository konfiguriert. Reale Aufnahme- und Telefonieänderungen müssen zusätzlich auf einem Windows-Arbeitsplatz mit SwyxTray/SwyxIt! geprüft werden; der Mock und synthetische WAVs ersetzen diesen Praxistest nicht.
## Code- und Projektkonventionen
- Dokumentation, Kommentare, UI-Texte und fachliche Fehlermeldungen sind überwiegend **Deutsch**. Bezeichner und API-Strukturen sind überwiegend Englisch. Diese Sprachtrennung beibehalten.
- TypeScript verwendet zwei Leerzeichen, einfache Anführungszeichen und normalerweise keine Semikolons. `strict`, `noUnusedLocals`, `noUnusedParameters` und `noFallthroughCasesInSwitch` sind aktiv.
- Java-Produktivcode verwendet überwiegend vier Leerzeichen, Records für Datenmodelle und Constructor Injection. Bei Testdateien und älteren Abschnitten die vorhandene Einrückung der jeweiligen Datei beibehalten.
- Python verwendet vier Leerzeichen und für HTTP zum LLM bewusst die Standardbibliothek. Keine neue Bibliothek allein aufgrund ihrer Üblichkeit einführen; vorhandene Manifeste und Idiome sind maßgeblich.
- Es sind keine eigenen Lint-, Formatierungs-, Checkstyle- oder Python-Formatter-Befehle konfiguriert. Typecheck, Compiler und Tests sind die vorhandenen automatischen Prüfungen.
- Spring Boot 4 verwendet im Code `tools.jackson.databind` für JSON-Verarbeitung; einzelne Mapping-Annotationen stammen weiterhin aus `com.fasterxml.jackson.annotation`. Diese Imports nicht vorschnell vereinheitlichen.
- JSON-Feldnamen des Fremdsystems wie `csc_id`, `job_ids` und `map_url` müssen an den API-/MongoDB-Grenzen korrekt auf Java-/TypeScript-Felder abgebildet werden. Unbekannte Jobfelder werden ignoriert; Job-IDs müssen positive Long-Werte sein.
- HTTP 202 bei Archiv- und Upload-Endpunkten bedeutet **angenommen**, nicht bereits vollständig verarbeitet. Die Asynchronität und die best-effort-Semantik des MongoDB-Archivs nicht unbeabsichtigt in synchrone Erfolgsgarantien umwandeln.
- Änderungen am SwyxTray-Protokoll gehören in Parser, Client, Mock und Tests gemeinsam. Ältere Protokollversionen bleiben relevant; insbesondere Snapshot-basierte Abläufe und Deduplizierung nicht entfernen.
- Abhängigkeiten sind in Maven, `package-lock.json` und den Python-Requirements fixiert beziehungsweise versioniert. Die PyAV-Version ist wegen Kompatibilität mit `faster-whisper` ausdrücklich angepinnt.
## Konfiguration und Persistenz
Zentrale Konfigurationsdateien:
- `backend/src/main/resources/application.properties`: Server, MongoDB, WebSocket-Ziel, Webhook und Transkription.
- `frontend/.env.example`: `VITE_WS_URL` und `VITE_BACKEND_URL` für Frontend-/Dev-Builds.
- `.env.example`: Vorlage für Compose und VS-Code-Starts; nach `.env` kopieren und lokale Werte eintragen.
- `compose.yaml`: Übergibt Laufzeitwerte an den Container und legt das Transkriptionsvolume fest.
Die SwyxTray-Adresse ergibt sich aus zuletzt erfolgreich verwendeter Browser-Eingabe beziehungsweise `localStorage`, danach aus `GET /api/config`; ohne Backend-Antwort dient `VITE_WS_URL` und schließlich der Code-Default als Fallback. Laufzeitwerte für den Container sind `APP_WEBSOCKET_HOST`, `APP_WEBSOCKET_PORT`, `APP_WEBSOCKET_PATH` und `APP_WEBSOCKET_SECURE`. Ein festes `VITE_WS_URL` ist normalerweise nicht erforderlich.
Weitere wichtige Werte:
- `SPRING_MONGODB_URI`: externe MongoDB.
- `APP_WEBHOOK_TOKEN`, `app.webhook.history` und `app.webhook.max-size`: Webhook-Schutz und Grenzen.
- `APP_TRANSCRIPTION_BASE_URL`, `WHISPER_MODEL_PATH`, `WHISPER_DATA_DIR`, `WHISPER_THREADS`: Worker-Integration und lokale Verarbeitung.
- `LLM_BASE_URL`, `LLM_MODEL`, `LLM_API_KEY`, `LLM_TIMEOUT_SECONDS`, `LLM_CHUNK_CHARS`: optionale Zusammenfassung. URL und Modell müssen gemeinsam konfiguriert sein; ohne beide bleibt die Zusammenfassung deaktiviert.
Persistenzregeln:
- MongoDB-Sammlungen: `addresses`, `jobs` und `calls`.
- Adressen werden per Rufnummer upserted. Vollständige Webhook-Adressen ersetzen auch Zuordnungen; der Telefonbuchimport erhält sie.
- Jobs werden anhand ihrer externen `id` ersetzt. `JobCleanupService` löscht jede Nacht um 0 Uhr deutscher Zeit Jobs, deren `ordertime` mehr als vier Wochen zurückliegt; Jobs ohne `ordertime` bleiben erhalten.
- Der Webhook-Verlauf umfasst standardmäßig die letzten 50 Nachrichten **nur im Java-Speicher** und ist nach einem Neustart leer. SSE nutzt `Last-Event-ID` zur Nachlieferung und sendet alle 25 Sekunden einen Heartbeat.
- WAVs, `jobs.sqlite3`, Transkripte und Zusammenfassungen liegen im Volume `/data/transcriptions`. Dafür gibt es keine automatische Löschfrist. `docker compose down` erhält das Volume; `docker compose down -v` löscht es.
## Docker und Deployment
```bash
# Frischen Checkout einmalig vorbereiten; vorhandene .env nicht überschreiben
test -f .env || cp .env.example .env
docker compose up -d --build
```
Der mehrstufige `Dockerfile`:
1. baut das Frontend mit Node 22,
2. kopiert `dist` in die statischen Spring-Ressourcen und baut das Java-21-Jar,
3. lädt ein fixiertes `faster-whisper-large-v3-turbo`-Modell in eine eigene Build-Stufe,
4. erzeugt ein Laufzeit-Image mit Java, Python, Whisper und Modell.
Der Container läuft als Benutzer `swyx` mit UID/GID 10001 und veröffentlicht Port 8080. Das Modell ist im Image enthalten; Hugging-Face-Offline-Modus und `local_files_only=True` verhindern Modell-Downloads zur Laufzeit. `WHISPER_MODEL_REPO` und `WHISPER_MODEL_REVISION` sind Build-Argumente. Für den Container sind zunächst mindestens 4 GB RAM einzuplanen; der Java-Heap ist standardmäßig auf 25 % des verfügbaren Speichers begrenzt und über `JAVA_OPTS` änderbar.
`docker/entrypoint.sh` startet Java und Python gemeinsam. Endet einer der beiden Prozesse, wird auch der andere beendet, damit Compose den Container neu starten kann. Der Healthcheck prüft sowohl `/api/config` als auch `/health` des Workers. Der Maven-Schritt im Dockerfile verwendet `-DskipTests`; Tests deshalb vor dem Image-Build separat ausführen.
Registry-Veröffentlichung:
```bash
docker login gitea.appcreation.de
./docker_push.sh 0.9.13
./docker_push.sh 0.9.13 --dry-run
```
Das Skript baut für `linux/amd64` und pusht nach `gitea.appcreation.de/sven/swyxweb:<x.y.z>`. Die Semver-artige Versionsangabe ist Pflicht und wird nicht aus `pom.xml` abgeleitet. `--dry-run` baut ohne Push. Lokale Maven-/npm-Builds sind vorher nicht nötig.
Der Produktiv-Container setzt das Profil `mock` nicht. HTTPS-Seiten benötigen für nicht-lokale SwyxTray-Ziele `wss://`; der WebSocket wird weiterhin vom Arbeitsplatzbrowser aus aufgebaut, nicht vom Docker-Host.
## Sicherheits- und Datenschutzregeln
- `.env`, lokale Frontend-Umgebungen, Build-Ergebnisse und virtuelle Umgebungen sind von Git und Docker-Build-Kontext ausgeschlossen. Keine echten Tokens, API-Schlüssel oder Zugangsdaten in versionierte Dateien oder Beispiele übernehmen.
- Für öffentlich erreichbare Instanzen muss `APP_WEBHOOK_TOKEN` gesetzt sein. Ein leerer Wert lässt alle Webhook-POSTs ohne Token zu und erzeugt eine Start-Warnung. Das Token schützt nur die Webhook-POST-Endpunkte; Verlauf, SSE und die übrigen APIs besitzen keine allgemeine Benutzerauthentifizierung.
- CORS für `/api/**` und Origins des Mocks sind derzeit mit `*` geöffnet. Das ist keine Zugriffskontrolle und muss für eine produktive Exposition gegebenenfalls eingeschränkt werden. Den Mock nicht unbeabsichtigt produktiv aktivieren.
- Die Standard-MongoDB-Konfiguration enthält keine Zugangsdaten und zeigt auf ein internes Netz. Netzwerkzugriff und Datenbankabsicherung müssen zur tatsächlichen Umgebung passen.
- WAV-Uploads werden im Browser, Java-Backend und Worker auf WAV-Dateikopf und höchstens 500 MB geprüft. Diese Grenzen und die Validierung von UUIDs beziehungsweise 32-stelligen Hex-Job-IDs beibehalten. Externe Karten-/Download-URLs dürfen nur vorgesehene HTTP(S)-Formen verwenden.
- Der FastAPI-Worker deaktiviert Dokumentationsendpunkte, beschränkt den `Host`-Header auf Loopback-Namen und soll nicht öffentlich exponiert werden.
- Whisper verarbeitet Audio lokal. An das optionale LLM wird ausschließlich Transkripttext gesendet, nicht die WAV. API-Schlüssel gehören nur in die Worker-Umgebung, nicht ins Frontend, Image oder in Auftragsdaten. LLM-Redirects werden nicht verfolgt; Provider-Antworten und Zugangsdaten dürfen nicht in Logs oder Fehlermeldungen durchsickern.
- Aufnahmen, Rufnummern, Kunden-/Jobdaten, Transkripte und Zusammenfassungen sind sensible Daten. Das persistente Transkriptionsvolume hat keine automatische Aufbewahrungsgrenze; Löschung und Zugriffsschutz müssen betrieblich berücksichtigt werden.