MongoDB-Ablage für Adressen, Anrufe und Jobs; Webhook-URLs je Datenart

Adress-, Anruf- und Jobdaten landen dauerhaft in der MongoDB
(ArchiveService, best-effort auf eigenem Thread): Die Startseite meldet
Adress-Cache und Anruf-Ereignisse an POST /api/addresses bzw. /api/calls,
der Webhook legt seine Nutzlasten selbst ab. Adressen tragen eine
generierte Kennung, die Rufnummer erkennt per Upsert die Dublette.

Der Webhook bekommt je Datenart eine eigene URL – /api/webhook/kunden,
/api/webhook/kuriere, /api/webhook/jobs –, die URL bestimmt die Ablage,
ein Kennzeichen in der Nutzlast ist nicht mehr nötig. Die Herkunft steht
als channel am Ereignis. Der generische POST /api/webhook bleibt.

Die Controller-Tests ersetzen den ArchiveService (MockitoBean) und
laufen damit ohne MongoDB. Markenrot laut Styleguide in beiden Farbmodi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 10:24:53 +02:00
co-authored by Claude Fable 5
parent 1598852785
commit 97c3778180
21 changed files with 1011 additions and 26 deletions
+20
View File
@@ -0,0 +1,20 @@
import type { Contact } from './swyx/protocol'
/**
* Meldet den Adress-Cache der SwyxTray-App an das Backend, das die Einträge in
* der MongoDB ablegt (Aktualisierung statt Verdopplung, siehe ArchiveService).
*
* Die WebSocket-Verbindung zur App hält nur der Browser; das Backend käme an
* diese Adressdaten sonst nicht heran. Fire-and-forget wie bei den
* Anrufdaten: Ist das Backend nicht erreichbar, läuft die Anzeige weiter.
*/
export function reportAddresses(contacts: Contact[]): void {
if (contacts.length === 0) return
void fetch('/api/addresses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ addresses: contacts }),
}).catch((e: unknown) => {
console.warn('Adressdaten nicht an das Backend gemeldet:', e)
})
}
+22
View File
@@ -0,0 +1,22 @@
import type { CallEvent } from './swyx/protocol'
/**
* Meldet Anruf-Ereignisse an das Backend, das sie in der MongoDB ablegt.
*
* Die Ereignisse entstehen nur hier im Browser (aus dem Vergleich zweier
* SwyxTray-Snapshots); das Backend sieht die WebSocket-Verbindung nicht.
* Fire-and-forget: Ist das Backend nicht erreichbar, läuft die Anzeige
* unverändert weiter die Speicherung ist Beiwerk, nicht Voraussetzung.
*/
export function reportCallEvents(events: CallEvent[]): void {
if (events.length === 0) return
void fetch('/api/calls', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(events),
// Auch beim Schließen des Tabs noch absetzen, z. B. das "ended" beim Auflegen.
keepalive: true,
}).catch((e: unknown) => {
console.warn('Anrufdaten nicht an das Backend gemeldet:', e)
})
}
+17 -5
View File
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { SwyxTrayClient, type ConnectionStatus, type RawMessage } from '../swyx/SwyxTrayClient'
import { mergeSnapshot, type CallEvent, type Contact, type SnapshotMessage } from '../swyx/protocol'
import { loadDirectory as loadDirectoryFrom, type SweepOptions } from '../swyx/directory'
import { reportCallEvents } from '../calls'
import { reportAddresses } from '../addresses'
export interface LogEntry extends RawMessage {
id: number
@@ -64,7 +66,11 @@ export function useSwyxTray() {
}),
client.on('error', (message) => setError(message)),
// Unaufgefordert: beim Verbinden und wenn die App ihren Cache erneuert.
client.on('addresses', (contacts) => setAddressCache(contacts)),
client.on('addresses', (contacts) => {
setAddressCache(contacts)
// Dauerhaft in die MongoDB, über das Backend die Anzeige wartet nicht darauf.
reportAddresses(contacts)
}),
client.on('raw', (message) => {
setLog((entries) => {
const next = [...entries, { ...message, id: logIdRef.current++ }]
@@ -89,6 +95,8 @@ export function useSwyxTray() {
setLines(next)
if (events.length > 0) {
setHistory((entries) => [...[...events].reverse(), ...entries].slice(0, MAX_HISTORY))
// Dauerhaft in die MongoDB, über das Backend die Anzeige wartet nicht darauf.
reportCallEvents(events)
}
}),
]
@@ -139,15 +147,19 @@ export function useSwyxTray() {
*/
const loadDirectory = useCallback(
(options: SweepOptions = {}) =>
run(() =>
loadDirectoryFrom(
run(async () => {
const directory = await loadDirectoryFrom(
{
listAddresses: async () => (await client.listAddresses()).addresses ?? [],
search: async (query) => (await client.searchContacts(query)).contacts ?? [],
},
options,
),
),
)
// Auch dieser Bestand dauerhaft in die MongoDB der unaufgeforderte
// Adress-Push deckt ältere App-Versionen ohne Cache nicht ab.
reportAddresses(directory.contacts)
return directory
}),
[client, run],
)
+3 -6
View File
@@ -14,11 +14,11 @@
--border-strong: #b4b4b5;
--text: #222222;
--text-muted: #7c7f7e;
/* Vollflächen (Buttons, Balken) immer das Markenrot mit weißer Schrift. */
/* Markenrot laut Styleguide (RGB 202/13/56, Pantone 185 C) in beiden
Farbmodi identisch, das Brandbook kennt nur diesen einen Rotwert. */
--brand: #ca0d38;
--brand-strong: #a30a2d;
--brand-soft: rgb(202 13 56 / 8%);
/* Rot als Text- und Konturfarbe; im Dunkelmodus eine hellere Abstufung. */
--accent: #ca0d38;
--ok: #2f9e6e;
--warn: #b98a1c;
@@ -35,10 +35,7 @@
--border-strong: #55565c;
--text: #f1f1f2;
--text-muted: #a4a7a8;
--brand: #ca0d38;
--brand-strong: #e33e60;
--brand-soft: rgb(227 62 96 / 14%);
--accent: #ef7d95;
--brand-soft: rgb(202 13 56 / 14%);
--ok: #43c78f;
--warn: #e0b341;
--err: #f2665b;
+5
View File
@@ -13,6 +13,11 @@ export interface WebhookEvent {
id: number
/** Eingangszeit im Backend, ISO-8601. */
receivedAt: string
/**
* Über welche URL die Nachricht kam: `kunden`, `kuriere` oder `jobs`;
* fehlt (bzw. `null`) beim generischen `POST /api/webhook`.
*/
channel?: string | null
/** Das empfangene JSON, unverändert. Objekt, Liste oder ein einfacher Wert. */
payload: unknown
}