Adressdaten-Bereich, Mock hinter einem Profil, Webhook
Drei Stränge, die sich über dieselben Dateien ziehen (HomePage, index.css, README, application.properties) und deshalb nicht getrennt committet werden können, ohne einen nicht übersetzbaren Zwischenstand zu hinterlassen: Adressdaten: Der Bereich zeigt den gesamten Bestand des globalen Telefon- buchs - zuerst aus dem Adress-Cache der App (Protokoll 8), sonst über das Kommando "addresses" und ersatzweise aus rund 110 Einzelabfragen hinter einem Wartedialog. Der Mock gibt denselben Bestand heraus und lässt sich über /api/mock/address-cache leeren, um die Rückfallebene zu prüfen. Mock hinter dem Profil "mock": Seine Bohnen (/ws und /api/mock/**) hängen jetzt an @Profile, sind ohne das Profil also nicht vorhanden. Damit kann der Mock nicht versehentlich in einer Produktivumgebung mitlaufen; das Container-Image setzt das Profil nicht. Je ein Test hält beide Richtungen fest. Webhook: POST /api/webhook nimmt beliebiges JSON eines fremden Systems an und reicht es über Server-Sent Events an die offenen Browser weiter, wo es der neue Bereich "Webhook" unverändert anzeigt. Der WebSocket kam dafür nicht in Frage - er gehört der SwyxTray-App. Das Backend hält die letzten 50 Nachrichten vor und liefert sie beim Wiederverbinden anhand der Last-Event-ID nach; ein Heartbeat und X-Accel-Buffering: no halten die Verbindung durch Reverse Proxys hindurch offen. Ein Token (app.webhook.token) ist vorgesehen, aber nicht voreingestellt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,17 +18,21 @@ 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](backend/src/main/resources/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.
|
||||
2. Es nimmt unter `POST /api/webhook` **Adressdaten fremder Systeme** entgegen und reicht sie
|
||||
an die offenen Browser weiter (siehe [Webhook](#webhook-für-adressdaten-fremder-systeme)).
|
||||
3. Es stellt unter seinem eigenen Port einen **SwyxTray-Mock** bereit, um die Startseite ohne
|
||||
echte Telefonanlage testen zu können (siehe unten). Der läuft **nur mit dem Spring-Profil
|
||||
`mock`**; im Normalbetrieb – und damit auch im Container – gibt es ihn nicht.
|
||||
|
||||
**Zwei getrennte Ports:** `8080` ist der Port dieser Anwendung (REST-API und Mock),
|
||||
**Zwei getrennte Ports:** `8080` ist der Port dieser Anwendung (REST-API und, mit Profil `mock`, der 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](frontend/src/swyx/protocol.ts) und
|
||||
Mit der Tab-Verwaltung meldete die App im `hello` `protocol: 6`, mit der Adresssuche
|
||||
`protocol: 7`, mit dem Adress-Cache **`protocol: 8`**; die älteren Nachrichten sind
|
||||
unverändert geblieben. Implementiert in [protocol.ts](frontend/src/swyx/protocol.ts) und
|
||||
[SwyxTrayClient.ts](frontend/src/swyx/SwyxTrayClient.ts).
|
||||
|
||||
Ein Anruf wird über die **1-basierte Leitungsnummer `line`** identifiziert – so, wie sie auch
|
||||
@@ -95,6 +99,8 @@ Wählbereich darauf hin.
|
||||
| `tabs` | – | Offene Tabs des Firefox-Plugins auflisten | `tabs` |
|
||||
| `opentab` | `url` | Neuen Tab öffnen | `tabId` |
|
||||
| `closetab` | `tabId` | Tab schließen | – |
|
||||
| `contacts` | `query` | Adressdaten des Swyx-Clients durchsuchen | `contacts` |
|
||||
| `addresses`| – | Adress-Cache der App am Stück abrufen | `addresses` |
|
||||
|
||||
Alles andere quittiert die App mit `"Unbekanntes Kommando '…'."`, ein fehlendes `cmd` mit
|
||||
`"Feld 'cmd' fehlt."`.
|
||||
@@ -154,16 +160,122 @@ wartet die Startseite auf diese drei Kommandos 15 statt 10 Sekunden
|
||||
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](frontend/src/swyx/protocol.test.ts) festgehalten –
|
||||
### Adressdaten des Swyx-Clients
|
||||
|
||||
Seit Protokoll 7 gibt die App das globale Telefonbuch der Anlage heraus – dieselben
|
||||
Einträge, die SwyxIt! im Adressbuch zeigt.
|
||||
|
||||
```jsonc
|
||||
// ->
|
||||
{ "id": 20, "cmd": "contacts", "query": "abt" }
|
||||
// <-
|
||||
{ "type": "result", "id": 20, "ok": true, "contacts": [
|
||||
{ "name": "Abt, Bettina", "number": "7587", "description": "S-SB" },
|
||||
{ "name": "Abdul, Rokhsareh", "number": "5215", "description": "" } ] }
|
||||
```
|
||||
|
||||
Wichtig für die Anzeige – alles gegen die laufende App sondiert:
|
||||
|
||||
- **`query` ist Pflicht.** Ein fehlender, leerer oder nur aus Leerzeichen bestehender
|
||||
Begriff und einer über 128 Zeichen ergeben dieselbe Meldung:
|
||||
`"Feld 'query' fehlt oder ist zu lang (max. 128 Zeichen)."` Ein **vollständiges Auflisten
|
||||
gibt es nicht**; der Bereich der Startseite ist deshalb eine Suche.
|
||||
- Gesucht wird als **Teilzeichenkette in Name und Rufnummer**, Groß-/Kleinschreibung egal.
|
||||
Die `description` (Standortkürzel wie „HH-MT") wird **nicht** durchsucht.
|
||||
- Sortiert nach Namen, **höchstens 100 Treffer** – gekürzt wird stillschweigend, es gibt
|
||||
kein Feld dafür. Ein `limit` im Kommando ignoriert die App.
|
||||
- `description` ist bei vielen Einträgen `""`. Namen sind **nicht eindeutig**: dieselbe
|
||||
Person kommt mit mehreren Durchwahlen vor.
|
||||
- `query` muss eine **Zeichenkette** sein; eine Zahl quittiert die App mit
|
||||
`"Ungueltiges JSON."` – und zwar **ohne `id`**, die Antwort lässt sich also keinem
|
||||
Kommando zuordnen. `SwyxTrayClient.searchContacts()` schickt deshalb immer Text.
|
||||
|
||||
Die `number` ist die Durchwahl in der Form, die `call` direkt annimmt – die Einträge im
|
||||
Bereich „Adressdaten" haben darum einen „Anrufen"-Knopf.
|
||||
|
||||
### Adress-Cache (Protokoll 8)
|
||||
|
||||
Seit Protokoll 8 hält die App die Adressdaten selbst vor und gibt sie **am Stück** heraus –
|
||||
ohne Suchbegriff und ohne Deckel. Das ersetzt im Normalfall die 110 Einzelabfragen des
|
||||
nächsten Abschnitts.
|
||||
|
||||
```jsonc
|
||||
// ->
|
||||
{ "id": 5, "cmd": "addresses" }
|
||||
// <-
|
||||
{ "type": "result", "id": 5, "ok": true,
|
||||
"addresses": [ { "name": "Muster GmbH", "number": "+493012345",
|
||||
"description": "Globales Telefonbuch" } ] }
|
||||
```
|
||||
|
||||
Dieselben Daten schickt die App außerdem **unaufgefordert**, direkt nach dem ersten Snapshot
|
||||
und wenn sich ihr Cache ändert:
|
||||
|
||||
```json
|
||||
{ "type": "addresses", "addresses": [ … ] }
|
||||
```
|
||||
|
||||
Diese Push-Nachricht ist die **einzige Nachricht der App ohne `id` und ohne `ok`** – sie ist
|
||||
damit keine Quittung und wird von [parseAddresses](frontend/src/swyx/protocol.ts) gelesen,
|
||||
nicht von `parseResult`. Die Einträge tragen dieselben Felder wie bei `contacts`.
|
||||
|
||||
Ein **leeres** `addresses` ist ein gültiger Zustand und heißt „Cache ist leer" – nicht
|
||||
„keine Antwort". Stand 21.08.2026 meldet die Anlage genau das: das Kommando wird mit
|
||||
`ok: true` beantwortet, der Cache ist aber leer, und der Push beim Verbinden trägt `[]`.
|
||||
Deshalb bleiben die Einzelabfragen als Rückfallebene bestehen.
|
||||
|
||||
### Rückfallebene: den Gesamtbestand aus Einzelabfragen holen
|
||||
|
||||
Kennt die App das Kommando `addresses` nicht oder ist ihr Cache leer, setzt
|
||||
[directory.ts](frontend/src/swyx/directory.ts) den Bestand aus vielen `contacts`-Abfragen
|
||||
zusammen. Der Trick steckt in der Wahl der Suchbegriffe:
|
||||
|
||||
> Jeder Eintrag hat eine Rufnummer, und die ist eine **reine Ziffernfolge**. Eine Nummer mit
|
||||
> mindestens zwei Ziffern enthält deshalb mindestens eines der hundert Paare `"00"`…`"99"`.
|
||||
> Die Vereinigung dieser hundert Abfragen ist damit **nachweislich vollständig** – solange
|
||||
> keine davon am Deckel hängt.
|
||||
|
||||
Dazu kommen die zehn einstelligen Abfragen `"0"`…`"9"`: eine einstellige Rufnummer (etwa `0`
|
||||
für eine Zentrale) käme in keinem Paar vor. Macht **110 Abfragen**.
|
||||
|
||||
Gegen die Anlage gemessen (21.08.2026):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Einträge insgesamt | 404 |
|
||||
| Abfragen | 110 (keine Verfeinerung nötig) |
|
||||
| größte Trefferzahl eines Ziffernpaars | 59 – also weit unter dem Deckel von 100 |
|
||||
| einstellige Abfragen am Deckel | 7 von 10 (deshalb genügen sie allein nicht) |
|
||||
| Dauer | rund 6,7 s |
|
||||
| Rufnummern | durchgehend 4- bis 6-stellig, rein numerisch |
|
||||
|
||||
Die App beantwortet die Abfragen **nacheinander**; mehrere gleichzeitig zu schicken bringt
|
||||
kaum etwas (110 Abfragen: 7,2 s einzeln, 6,3 s zu viert). Deshalb der Wartedialog.
|
||||
|
||||
**Was passiert, wenn doch eine Abfrage gekürzt wird?** Dann verfeinert der Durchlauf sie –
|
||||
vorn *und* hinten je eine Ziffer angehängt, denn eine Teilzeichenkette kann an beiden Enden
|
||||
weitergehen. Das erfasst jeden Eintrag, dessen Rufnummer **länger** ist als die gekappte
|
||||
Abfrage. Bleibt eine Abfrage auch dreistellig noch gekürzt, meldet der Bereich den Bestand
|
||||
ausdrücklich als unvollständig, statt eine gekürzte Liste als vollständig auszugeben.
|
||||
|
||||
Die eine verbleibende Lücke, offen benannt: Ein Eintrag, dessen Rufnummer **genau** einer
|
||||
gekappten Abfrage entspricht, ließe sich nicht nachladen. Bei vier- bis sechsstelligen
|
||||
Rufnummern betrifft das nur den Fall einer einstelligen Rufnummer bei gleichzeitig gekappter
|
||||
einstelliger Abfrage.
|
||||
|
||||
Die Regeln des Protokolls stehen in [protocol.test.ts](frontend/src/swyx/protocol.test.ts),
|
||||
die des Gesamtabrufs in [directory.test.ts](frontend/src/swyx/directory.test.ts) –
|
||||
darunter die wörtlich mitgeschnittene Nachricht eines echten eingehenden Anrufs.
|
||||
|
||||
## Funktionen der Startseite
|
||||
|
||||
Die Seite ist in drei Bereiche aufgeteilt ([Tabs.tsx](frontend/src/components/Tabs.tsx)):
|
||||
Die Seite ist in fünf Bereiche aufgeteilt ([Tabs.tsx](frontend/src/components/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 |
|
||||
| **Adressdaten** | Der **gesamte** Bestand des Telefonbuchs. Zuerst aus dem Adress-Cache, den die App beim Verbinden von selbst schickt – dann steht er sofort und ohne Wartedialog. Sonst über `addresses`, und erst wenn auch das nichts liefert, über rund 110 Einzelabfragen hinter einem Wartedialog mit Fortschritt und „Abbrechen". Die Anzeige nennt die Herkunft. Danach liegt alles im Browser: das Filtern über Name, Durchwahl **und** Standortkürzel läuft ohne weitere Abfrage. Ein Klick auf „Anrufen" wählt die Durchwahl (`call`) |
|
||||
| **Webhook** | Was ein fremdes System an `POST /api/webhook` geschickt hat – jüngste Nachricht zuerst, jede mit ihrem JSON im Original. Die Seite hört über Server-Sent Events mit, **unabhängig vom geöffneten Bereich**: Eine Nachricht erscheint sofort, und ist gerade ein anderer Bereich offen, zeigt der Tab die Zahl der ungesehenen Nachrichten |
|
||||
| **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 |
|
||||
|
||||
@@ -178,6 +290,83 @@ Weiteres:
|
||||
„nicht erkannt" vermerkt statt still verworfen.
|
||||
- Automatischer Reconnect mit exponentiellem Backoff, nur nach unerwartetem Abbruch.
|
||||
|
||||
## Webhook für Adressdaten fremder Systeme
|
||||
|
||||
Ein anderes System kann Adressdaten per POST an die Anwendung schicken; die Startseite zeigt
|
||||
sie im Bereich **Webhook** an. Anders als alles Übrige läuft das **nicht** über die SwyxTray-App,
|
||||
sondern allein über das Backend.
|
||||
|
||||
```bash
|
||||
curl -X POST https://swyxweb.appcreation.de/api/webhook \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Webhook-Token: …" \
|
||||
-d '{"name":"Muster GmbH","number":"+493012345","description":"CRM-Import"}'
|
||||
```
|
||||
|
||||
Die Quittung nennt die vergebene Nummer und die Eingangszeit:
|
||||
|
||||
```json
|
||||
{ "id": 1, "receivedAt": "2026-08-24T12:38:01.563742294Z" }
|
||||
```
|
||||
|
||||
**Angenommen wird beliebiges JSON** – Objekt wie Liste. Das aufrufende System muss sich an kein
|
||||
Schema halten; die Startseite zeigt die Nutzlast unverändert. Nur die Kopfzeile eines Eintrags
|
||||
fasst `name` und `number` zusammen, wenn es sie gibt, und zählt sonst Felder bzw. Einträge
|
||||
([summarize](frontend/src/webhook.ts)).
|
||||
|
||||
| Endpunkt | Zweck |
|
||||
|---|---|
|
||||
| `POST /api/webhook` | Nachricht abliefern. `415` bei falschem Content-Type, `400` bei kaputtem JSON, `413` über der Größengrenze, `401` bei falschem Token |
|
||||
| `GET /api/webhook/events` | Strom für die Startseite (Server-Sent Events) |
|
||||
| `GET /api/webhook/history` | Verlauf, damit ein später geöffneter Bereich die vorigen Nachrichten sieht |
|
||||
| `DELETE /api/webhook/history` | Verlauf leeren – das macht der Knopf „Leeren" |
|
||||
|
||||
### Wie die Nachricht in den Browser kommt
|
||||
|
||||
Über **Server-Sent Events**, nicht über den WebSocket: Der gehört der SwyxTray-App. Die
|
||||
Startseite hält eine `EventSource` auf `/api/webhook/events` offen
|
||||
([useWebhook.ts](frontend/src/hooks/useWebhook.ts)) – einseitig, über dieselbe HTTPS-Verbindung
|
||||
wie die Seite, und der Browser baut sie nach einem Abbruch von selbst wieder auf.
|
||||
|
||||
Zwei Vorkehrungen, damit dabei nichts verloren geht:
|
||||
|
||||
- Das Backend hält die letzten **50 Nachrichten** im Speicher ([WebhookService](backend/src/main/java/de/appcreation/swyxweb/webhook/WebhookService.java)).
|
||||
Beim Wiederverbinden schickt der Browser die `Last-Event-ID` mit, und alles Jüngere wird
|
||||
nachgeliefert. Über einen **Neustart hinaus wird nichts aufgehoben**.
|
||||
- Alle 25 Sekunden geht ein Kommentar (`:ping`) durch die Leitung, damit ein Reverse Proxy die
|
||||
ruhende Verbindung nicht kappt.
|
||||
|
||||
Die Antwort trägt außerdem `X-Accel-Buffering: no` und `Cache-Control: no-cache`. **Ohne den
|
||||
ersten Kopf sammelt nginx die Ereignisse in seinem Puffer** und gibt sie erst später aus – die
|
||||
Nachricht erschiene dann verspätet oder scheinbar gar nicht. nginx wertet ihn aus, andere Proxys
|
||||
ignorieren ihn folgenlos. Steht vor der Anwendung etwas anderes als nginx, muss dort die
|
||||
Pufferung für `/api/webhook/events` abgeschaltet sein (bei Apache `mod_proxy`: `flushpackets=on`).
|
||||
|
||||
Doppelte Nachrichten sind dadurch normal – der Bereich holt beim Start den Verlauf *und* hört
|
||||
auf den Strom. Entschieden wird über die `id` ([mergeEvent](frontend/src/webhook.ts)).
|
||||
|
||||
### Token
|
||||
|
||||
```properties
|
||||
app.webhook.token= # leer ⇒ jeder Aufruf wird angenommen
|
||||
app.webhook.history=50 # so viele Nachrichten bleiben im Speicher
|
||||
app.webhook.max-size=262144 # größte Nutzlast in Byte
|
||||
```
|
||||
|
||||
> **Für die öffentlich erreichbare Instanz gehört hier ein Geheimnis hinein.** `/api/webhook`
|
||||
> liegt unter derselben Adresse wie die Startseite und ist damit aus dem Internet erreichbar;
|
||||
> ohne Token kann jeder beliebiges JSON in die Anzeige schreiben. Ist der Wert leer, weist das
|
||||
> Backend beim Start ausdrücklich darauf hin (`WARN`).
|
||||
|
||||
Im Container:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 -e APP_WEBHOOK_TOKEN=… … gitea.appcreation.de/sven/swyxweb:0.9.13
|
||||
```
|
||||
|
||||
Das Token gilt nur für `POST`. Verlauf und Ereignisstrom bleiben offen – sie liefern dasselbe,
|
||||
was die Seite ohnehin jedem Betrachter zeigt.
|
||||
|
||||
## Starten
|
||||
|
||||
### Aus VS Code
|
||||
@@ -187,8 +376,9 @@ Im Debug-Panel die Compound-Konfiguration **„SwyxWeb starten (Backend + Fronte
|
||||
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.
|
||||
Alternative **„SwyxWeb starten (lokaler Test gegen SwyxTray-Mock)"**: startet das Backend mit dem
|
||||
Profil `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).
|
||||
@@ -223,10 +413,29 @@ diesen Proxy – der Browser verbindet sich direkt mit der SwyxTray-App.
|
||||
|
||||
## Ohne echte Telefonanlage testen
|
||||
|
||||
> **Der Mock ist Opt-in.** Sein WebSocket-Endpunkt `/ws` und seine Steuer-Endpunkte
|
||||
> `/api/mock/**` hängen am Spring-Profil **`mock`**
|
||||
> ([MockProfile](backend/src/main/java/de/appcreation/swyxweb/config/MockProfile.java)) –
|
||||
> ohne das Profil existieren die Bohnen nicht und beide Pfade antworten mit 404. So kann der
|
||||
> Mock nicht versehentlich in einer Produktivumgebung mitlaufen; das Container-Image setzt das
|
||||
> Profil bewusst nicht. Einschalten:
|
||||
>
|
||||
> ```bash
|
||||
> cd backend && ./mvnw spring-boot:run -Dspring-boot.run.profiles=mock
|
||||
> java -jar app.jar --spring.profiles.active=mock # oder SPRING_PROFILES_ACTIVE=mock
|
||||
> ```
|
||||
>
|
||||
> Abgesichert ist beides durch je einen Test in
|
||||
> [BackendApplicationTests](backend/src/test/java/de/appcreation/swyxweb/BackendApplicationTests.java)
|
||||
> (ohne Profil keine Mock-Bohne) und
|
||||
> [MockProfileTests](backend/src/test/java/de/appcreation/swyxweb/MockProfileTests.java)
|
||||
> (mit Profil alle da).
|
||||
|
||||
Der [SwyxTrayMockHandler](backend/src/main/java/de/appcreation/swyxweb/websocket/SwyxTrayMockHandler.java)
|
||||
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:
|
||||
spricht dasselbe Protokoll: `hello` (mit `protocol: 8`), Snapshot und Adress-Cache beim
|
||||
Verbinden, Quittungen auf
|
||||
`call`/`answer`/`hangup`/`ping`/`status`/`focus`/`tabs`/`opentab`/`closetab`/`contacts`/`addresses`
|
||||
und nach jeder Änderung einen vollständigen Snapshot über alle vier Leitungen. Einen eingehenden Anruf auslösen:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/mock/incoming-call?number=%2B493012345&name=Muster%20GmbH"
|
||||
@@ -244,8 +453,30 @@ liefert dessen `tabId`, `closetab` entfernt ihn; eine unbekannte Kennung ergibt
|
||||
Geöffnet wird auch hier nichts. Der Mock bildet den Fall „kein Plugin verbunden" **nicht** nach;
|
||||
er antwortet immer sofort.
|
||||
|
||||
Für `contacts` führt der Mock ein Telefonbuch mit **154 Einträgen** – groß genug, dass der
|
||||
Bereich „Adressdaten" dieselbe Arbeit leisten muss wie gegen die Anlage. Drei Sonderfälle
|
||||
sind absichtlich dabei: zweimal **„Muster, Max"** mit verschiedenen Durchwahlen (Namen sind
|
||||
nicht eindeutig), Einträge **ohne Beschreibung**, und **„Zentrale" mit der Rufnummer `0`** –
|
||||
die steckt in keinem Ziffernpaar und wird nur über die einstelligen Abfragen gefunden.
|
||||
|
||||
Er hält sich an dieselben Regeln wie die echte App: Teilzeichenkette in Name und Rufnummer,
|
||||
Schreibweise egal, nach Namen sortiert, gekappt bei 100 Treffern, und derselbe Fehlertext
|
||||
für einen fehlenden, leeren oder zu langen Suchbegriff.
|
||||
|
||||
Denselben Bestand gibt der Mock als Adress-Cache heraus – auf `addresses` und beim Verbinden
|
||||
als Push. Der Cache lässt sich leeren, um die Rückfallebene zu prüfen; dann verhält er sich
|
||||
wie die Anlage zurzeit:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/mock/address-cache?filled=false" # -> Einzelabfragen
|
||||
curl -X POST "http://localhost:8080/api/mock/address-cache?filled=true" # -> Cache
|
||||
```
|
||||
|
||||
Jedes Umschalten schickt allen Verbundenen sofort einen neuen Push.
|
||||
|
||||
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`
|
||||
eintragen oder das Backend mit
|
||||
`--spring.profiles.active=mock --app.websocket.host=localhost --app.websocket.port=8080`
|
||||
starten (macht die VS-Code-Konfiguration „lokaler Test" automatisch).
|
||||
|
||||
## Konfiguration der WebSocket-Adresse
|
||||
@@ -280,6 +511,58 @@ 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.
|
||||
|
||||
## Deployment als Docker-Container
|
||||
|
||||
Das [Dockerfile](Dockerfile) im Wurzelverzeichnis erledigt genau das in drei Stufen: Frontend
|
||||
mit Node bauen, das Ergebnis in `backend/src/main/resources/static/` legen und mit Maven
|
||||
zusammen zum Jar packen, dieses in ein schlankes JRE-Image kopieren. Herauskommt **ein**
|
||||
Container, der Startseite und REST-API über Port 8080 ausliefert – als Benutzer `swyx`, nicht
|
||||
als `root`, mit einem Healthcheck gegen `/api/config`.
|
||||
|
||||
```bash
|
||||
docker build -t swyxweb .
|
||||
docker run -d -p 8080:8080 \
|
||||
-e APP_WEBSOCKET_HOST=192.168.180.135 \
|
||||
-e APP_WEBSOCKET_PORT=17654 \
|
||||
--name swyxweb swyxweb
|
||||
```
|
||||
|
||||
Alle `app.websocket.*`-Werte sind zur Laufzeit über Umgebungsvariablen setzbar
|
||||
(`APP_WEBSOCKET_HOST`, `_PORT`, `_PATH`, `_SECURE`), das Bundle muss dafür nicht neu gebaut
|
||||
werden. Der Heap wächst über `-XX:MaxRAMPercentage=75` mit dem Speicherlimit des Containers
|
||||
mit; überschreiben lässt sich das mit `-e JAVA_OPTS=…`. Soll die WebSocket-Adresse ausnahmsweise
|
||||
fest ins Bundle, geht das beim Bauen mit `--build-arg VITE_WS_URL=ws://…/ws`.
|
||||
|
||||
In die Registry schieben erledigt [docker_push.sh](docker_push.sh) – es baut für `linux/amd64`
|
||||
und pusht in die Paket-Registry des Gitea nach `gitea.appcreation.de/sven/swyxweb`
|
||||
(`REGISTRY_IMAGE` im Skript). Die **Versionsnummer ist Pflicht** und wird zum Tag
|
||||
des Images; sie wird bewusst nicht aus der `pom.xml` abgeleitet:
|
||||
|
||||
```bash
|
||||
docker login gitea.appcreation.de
|
||||
./docker_push.sh 0.9.13 # baut und pusht :0.9.13
|
||||
./docker_push.sh 0.9.13 --dry-run # nur bauen, nicht pushen
|
||||
```
|
||||
|
||||
Ohne Argument bricht das Skript mit der Kurzhilfe ab. Zulässig ist das Format `x.y.z`.
|
||||
|
||||
Ein lokaler Maven- oder npm-Lauf ist vorher **nicht** nötig; beides passiert im Image-Build.
|
||||
Die beiden Build-Stufen laufen dabei nativ auf der Architektur des bauenden Rechners
|
||||
(`--platform=$BUILDPLATFORM`) – JS-Bundle und Jar sind architekturunabhängig, nur das
|
||||
Laufzeit-Image wird für `linux/amd64` gezogen. Auf einem ARM-Mac spart das die Emulation.
|
||||
|
||||
**Der SwyxTray-Mock ist nicht dabei**: `SPRING_PROFILES_ACTIVE` bleibt leer, also fehlen `/ws`
|
||||
und `/api/mock/**` (siehe [Ohne echte Telefonanlage testen](#ohne-echte-telefonanlage-testen)).
|
||||
Für eine Vorführung ohne Telefonanlage lässt er sich am einzelnen Container einschalten:
|
||||
`-e SPRING_PROFILES_ACTIVE=mock`.
|
||||
|
||||
Zwei Dinge, die der Container nicht lösen kann:
|
||||
|
||||
- **Läuft die Seite hinter HTTPS**, blockiert der Browser die unverschlüsselte Verbindung zur
|
||||
SwyxTray-App. Dann braucht die App selbst TLS und `APP_WEBSOCKET_SECURE=true` (⇒ `wss://`).
|
||||
- **Die WebSocket-Verbindung baut der Browser des Anwenders auf**, nicht der Container. Port
|
||||
17654 muss also vom Arbeitsplatz aus erreichbar sein, nicht vom Docker-Host.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- **HTTPS-Seite ⇒ `wss://`**: Ein über HTTPS ausgelieferter Browser blockiert unverschlüsselte
|
||||
|
||||
Reference in New Issue
Block a user