SvenandClaude Opus 5 6e9de54119 Start-Konfiguration für VS Code versionieren
Die README beschreibt die Compound-Konfiguration "SwyxWeb starten
(Backend + Frontend)" als Weg, das Projekt zu starten – dann muss sie
auch im Repo liegen. Persönliche Einstellungen (.vscode/settings.json)
bleiben ausgeschlossen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:04:31 +02:00
2026-08-20 11:03:34 +02:00
2026-08-20 11:03:34 +02:00
2026-08-20 11:03:34 +02:00

SwyxWeb

React-Frontend mit Spring-Boot-Backend. Die Startseite verbindet sich aus dem Browser heraus per WebSocket mit der SwyxTray-App (ws://192.168.180.135:17654/ws), meldet eingehende Anrufe und startet ausgehende Anrufe.

SwyxWeb/
├── backend/    Spring Boot 4.1 (Java 21, Maven Wrapper)
└── frontend/   React 19 + TypeScript + Vite

Wer verbindet sich mit wem?

Die WebSocket-Verbindung wird ausschließlich im Browser aufgebaut in SwyxTrayClient.ts über new WebSocket(url). Das Backend baut keine eigene WebSocket-Verbindung auf; es hat zwei Aufgaben:

  1. GET /api/config liefert dem Frontend die Zieladresse, damit sie nicht im JS-Bundle fest verdrahtet ist (konfigurierbar in application.properties).
  2. Es stellt unter seinem eigenen Port einen SwyxTray-Mock bereit, um die Startseite ohne echte Telefonanlage testen zu können (siehe unten). Im Normalbetrieb wird er nicht benutzt.

Zwei getrennte Ports: 8080 ist der Port dieser Anwendung (REST-API und Mock), 17654 der Port der SwyxTray-App auf 192.168.180.135. Sie sind unabhängig voneinander.

Protokoll der SwyxTray-App

Quelle: Mitschnitt gegen die laufende App (Version 1.0.0.0, protocol: 1) am 13.08.2026. Seit der Tab-Verwaltung meldet die App im hello protocol: 6; die älteren Nachrichten sind unverändert geblieben. Implementiert in protocol.ts und SwyxTrayClient.ts.

Ein Anruf wird über die 1-basierte Leitungsnummer line identifiziert so, wie sie auch in SwyxIt! erscheint. Eine Anruf-Id gibt es nicht.

Die App schickt keine Ereignisnachrichten. Jede Zustandsänderung auch ein eingehender Anruf kommt als vollständiger snapshot über alle vier Leitungen. Die Anrufmeldung der Startseite entsteht deshalb aus dem Vergleich zweier Snapshots (mergeSnapshot).

Begrüßung (App → Seite, beim Verbinden)

{ "app": "SwyxTray", "version": "1.0.0.0", "protocol": 1, "session": 7, "type": "hello" }

Direkt danach folgt der erste Snapshot.

Snapshot (App → Seite, bei jeder Änderung)

{ "connected": true, "serverUp": true, "overall": "Ringing", "statusText": "Eingehender Ruf",
  "user": "Muster, M.", "server": "127.0.0.1", "type": "snapshot",
  "lines": [
    { "line": 1, "state": "LSRinging", "stateCode": 3, "stateText": "klingelt",
      "peer": "001602107449", "peerNumber": "001602107449", "peerName": "",
      "busy": true, "selected": true },
    { "line": 2, "state": "LSInactive", "stateCode": 0, "stateText": "frei",
      "peer": "unbekannt", "peerNumber": "", "peerName": "", "busy": false, "selected": false }
  ] }

state trägt die Namen der CLMgr-Aufzählung LineState, stateText den Klartext aus SwyxIt!. Belegt sind bisher LSInactive / 0 / "frei" und LSRinging / 3 / "klingelt". callStateOf bildet beides auf incoming · outgoing · connected ab und wertet dabei Name und Klartext aus; ein unbekannter Zustand gilt als belegt, damit eine klingelnde Leitung nie stillschweigend verschwindet. peer ist die fertige Anzeigeform, "unbekannt" der Platzhalter für „keine Gegenstelle". Freie Leitungen schickt die App immer mit; die Startseite blendet sie aus.

Kommandos (Seite → App)

// ->
{ "id": 7, "cmd": "call", "number": "+49 30 1234567" }
// <- Quittung, id gespiegelt
{ "id": 7, "ok": true, "line": 2, "type": "result" }
// <- Fehlerfall (ungültige Nummer, keine Verbindung zu SwyxIt!)
{ "id": 7, "ok": false, "error": "…", "type": "result" }

ok: true heißt nur, dass CLMgr den Auftrag angenommen hat gewählt wird asynchron. Der tatsächliche Verlauf kommt danach über die folgenden Snapshots. Die Startseite weist im Wählbereich darauf hin.

Kommando Felder Zweck Antwort
call number Wählvorgang starten line
answer line Anruf annehmen
hangup line Anruf beenden/ablehnen
ping Verbindungstest
status Snapshot anfordern
focus title, url Fenster in den Vordergrund holen focused
tabs Offene Tabs des Firefox-Plugins auflisten tabs
opentab url Neuen Tab öffnen tabId
closetab tabId Tab schließen

Alles andere quittiert die App mit "Unbekanntes Kommando '…'.", ein fehlendes cmd mit "Feld 'cmd' fehlt.".

Fenster in den Vordergrund holen

// ->
{ "id": 9, "cmd": "focus", "title": "Kundenakte Muster GmbH", "url": "https://crm.example.local/kunden/4711" }
// <- Fenster war da und ist jetzt vorn
{ "type": "result", "id": 9, "ok": true, "focused": true }
// <- kein Fenster gefunden, stattdessen Browser mit der URL gestartet
{ "type": "result", "id": 9, "ok": true, "focused": false }

title wird als Teilzeichenkette gesucht, unabhängig von der Groß-/Kleinschreibung "swyxit" findet also das Fenster „SwyxIt!". url ist optional und wird nur benutzt, wenn kein Fenster passt; fehlt sie in diesem Fall, antwortet die App mit ok: false und "Kein Fenster mit Titel '…' gefunden und keine URL angegeben.". Ein fehlender oder leerer Titel ergibt "Feld 'title' fehlt.".

Seit der Umstellung des Tab-Bereichs benutzt die Startseite focus nicht mehr; das Kommando bleibt aber im Protokoll und in SwyxTrayClient.focusWindow() erhalten.

Tabs des Firefox-Plugins

Die drei Tab-Kommandos reicht die SwyxTray-App unverändert an das Firefox-Plugin durch gemeint sind also die Tabs des Firefox auf dem Rechner der App, nicht die Bereiche der Startseite. Es sind dieselben Kanäle, die auch die Tray-Menüpunkte Lese Tabs, Tab öffnen und Tab schliessen benutzen.

// ->
{ "id": 12, "cmd": "tabs" }
// <-
{ "type": "result", "id": 12, "ok": true, "tabs": [
    { "id": 43, "title": "Kundenakte", "url": "https://crm.example.local/kunden/4711", "active": true },
    { "id": 44, "title": "SwyxWeb", "url": "http://localhost:5173/", "active": false } ] }

// ->
{ "id": 13, "cmd": "opentab", "url": "https://crm.example.local/kunden/4711" }
// <-
{ "type": "result", "id": 13, "ok": true, "tabId": 43 }

// ->  tabId stammt aus einer tabs-Antwort
{ "id": 14, "cmd": "closetab", "tabId": 43 }
// <-
{ "type": "result", "id": 14, "ok": true }

url muss eine absolute http(s)-Adresse sein dieselbe Prüfung wie bei focus; sonst antwortet die App mit ok: false. Ist kein Plugin verbunden, endet jedes der drei Kommandos nach dem Zeitfenster von rund fünf Sekunden ebenfalls mit ok: false. Deshalb wartet die Startseite auf diese drei Kommandos 15 statt 10 Sekunden (SwyxTrayClient.ts).

Auch hier gilt: die App meldet Tab-Änderungen nicht von selbst. Die Liste wird nach jedem Öffnen und Schließen neu über tabs geholt.

Alle Regeln sind in protocol.test.ts festgehalten darunter die wörtlich mitgeschnittene Nachricht eines echten eingehenden Anrufs.

Funktionen der Startseite

Die Seite ist in drei Bereiche aufgeteilt (Tabs.tsx):

Tab Inhalt
Anrufe Rufnummer wählen (call), belegte Leitungen mit Aktionen und dem Zustand der App (statusText, angemeldeter Benutzer, Warnung bei connected: false), Verlauf der Anrufereignisse
Tabs Offene Tabs des Firefox-Plugins anzeigen (tabs), einzeln schließen (closetab) und eine URL als neuen Tab öffnen (opentab); die Liste wird beim ersten Öffnen des Bereichs und nach jeder Änderung geholt, die eingetippte URL übersteht einen Reload (localStorage)
Verbindung Adresse, Verbinden/Trennen, Benachrichtigungen erlauben, Diagnose: Rohnachrichten-Log, Status abfragen, Freitext senden

Die Meldung eines eingehenden Anrufs steht bewusst über den Tabs mit Leitungsnummer, Name und Rufnummer, „Annehmen"/„Ablehnen" , damit sie in keinem Bereich untergeht. Liegt der Browser-Tab im Hintergrund, kommt zusätzlich eine System-Benachrichtigung (nach Erlaubnis).

Weiteres:

  • Der Verlauf entsteht aus dem Vergleich aufeinanderfolgender Snapshots.
  • Eine Nachricht, die keinem der drei Typen entspricht, wird im Log ausdrücklich als „nicht erkannt" vermerkt statt still verworfen.
  • Automatischer Reconnect mit exponentiellem Backoff, nur nach unerwartetem Abbruch.

Starten

Aus VS Code

Im Debug-Panel die Compound-Konfiguration „SwyxWeb starten (Backend + Frontend)" wählen (launch.json). Sie startet das Backend im Debugger, danach den Vite-Dev-Server und öffnet Chrome, sobald dieser bereit ist Breakpoints funktionieren auf beiden Seiten. „Stop" beendet beides zusammen.

Alternative „SwyxWeb starten (lokaler Test gegen SwyxTray-Mock)": /api/config liefert dann ws://localhost:8080/ws, die Startseite spricht also mit dem SwyxTray-Mock.

Benötigte Extensions: Extension Pack for Java (Backend) und JavaScript Debugger (im VS Code enthalten, für Vite und Chrome).

Backend

cd backend
./mvnw spring-boot:run

Läuft auf http://0.0.0.0:8080. Achtung: Auf diesem Rechner belegt bereits ein anderes Projekt Port 8080 dann auf einen freien Port ausweichen:

./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8081"

Der Port der SwyxTray-App (17654) ändert sich dadurch nicht.

Frontend

cd frontend
npm install
npm run dev

Öffnet http://localhost:5173. Der Vite-Dev-Server leitet /api an http://localhost:8080 weiter (überschreibbar per VITE_BACKEND_URL). Die WebSocket-Verbindung läuft nicht über diesen Proxy der Browser verbindet sich direkt mit der SwyxTray-App.

Ohne echte Telefonanlage testen

Der SwyxTrayMockHandler spricht dasselbe Protokoll: hello (mit protocol: 6) und Snapshot beim Verbinden, Quittungen auf call/answer/hangup/ping/status/focus/tabs/opentab/closetab und nach jeder Änderung einen vollständigen Snapshot über alle vier Leitungen. Einen eingehenden Anruf auslösen:

curl -X POST "http://localhost:8080/api/mock/incoming-call?number=%2B493012345&name=Muster%20GmbH"

Für focus täuscht der Mock die Fenster „SwyxIt!", „SwyxWeb Google Chrome" und „Kundenakte Muster GmbH" vor (Teilzeichenkette, Schreibweise egal). Ein passender Titel ergibt focused: true, ein unbekannter mit URL focused: false, ein unbekannter ohne URL denselben Fehler wie die echte App. Geöffnet wird natürlich nichts der Mock protokolliert nur.

Für die Tab-Kommandos spielt der Mock das Firefox-Plugin und führt eine Tabliste im Speicher anfangs „SwyxWeb" und „Kundenakte Muster GmbH". opentab hängt einen Tab an und liefert dessen tabId, closetab entfernt ihn; eine unbekannte Kennung ergibt "Kein Tab mit der Kennung …", eine relative Adresse denselben Fehler wie die echte App. Geöffnet wird auch hier nichts. Der Mock bildet den Fall „kein Plugin verbunden" nicht nach; er antwortet immer sofort.

Die Startseite muss dazu auf ws://localhost:8080/ws zeigen entweder im Adressfeld eintragen oder das Backend mit --app.websocket.host=localhost --app.websocket.port=8080 starten (macht die VS-Code-Konfiguration „lokaler Test" automatisch).

Konfiguration der WebSocket-Adresse

Es gilt die erste Quelle, die etwas liefert:

Priorität Quelle Beispiel
1 Eingabefeld auf der Startseite zur Laufzeit änderbar
2 GET /api/config vom Backend app.websocket.host in application.properties
3 VITE_WS_URL aus frontend/.env siehe .env.example
4 Default im Code ws://192.168.180.135:17654/ws

Backend-seitig:

server.port=8080                     # Port dieser Anwendung

app.websocket.host=192.168.180.135   # SwyxTray-App
app.websocket.port=17654
app.websocket.path=/ws
app.websocket.secure=false           # true ⇒ wss://

Build und Tests

cd backend  && ./mvnw clean package     # erzeugt target/backend-0.0.1-SNAPSHOT.jar
cd frontend && npm test && npm run build

Um das Frontend aus dem Backend auszuliefern, frontend/dist/* nach backend/src/main/resources/static/ kopieren und neu packen.

Hinweise

  • HTTPS-Seite ⇒ wss://: Ein über HTTPS ausgelieferter Browser blockiert unverschlüsselte ws://-Verbindungen (Mixed Content). Im Dev-Betrieb über http:// ist ws:// in Ordnung.
  • CORS/Origin: Der Mock akzeptiert alle Origins (setAllowedOriginPatterns("*")), damit der Vite-Dev-Server auf Port 5173 sich verbinden kann. Für die Produktion einschränken siehe WebSocketConfig.java und CorsConfig.java.
  • Firewall: Port 17654 muss auf 192.168.180.135 eingehend freigegeben sein, sonst scheitert der Verbindungsaufbau ohne aussagekräftige Browser-Fehlermeldung.
  • System-Benachrichtigungen verlangen eine Benutzerinteraktion zur Erlaubniserteilung und funktionieren nur über https:// oder localhost.
S
Description
No description provided
Readme
417 KiB
Languages
TypeScript 53.7%
Java 37.7%
CSS 6.1%
Dockerfile 1.1%
Shell 0.9%
Other 0.5%