# Echtzeit-Server (SSE)

*Source: https://lxcars.de/technik/echtzeit-sse/*

> Node.js SSE-Server für Live-Updates in allen Modulen

## Prinzip

Der Echtzeit-Server ist ein eigenständiger Node.js-Dienst, der zwischen PostgreSQL und dem Browser vermittelt. Wenn sich in der Datenbank etwas ändert, sendet PostgreSQL über `NOTIFY` ein Ereignis. Der SSE-Server empfängt es per `LISTEN` und leitet es über eine offene HTTP-Verbindung an den Browser weiter.

<div class="w-full px-3 px-0 my-4">
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-4 md:col-span-2"><strong>Datenbank</strong></div>
    <div class="col-span-8 md:col-span-10">PostgreSQL löst <code>pg_notify()</code> bei relevanten Änderungen aus</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-4 md:col-span-2"><strong>SSE-Server</strong></div>
    <div class="col-span-8 md:col-span-10">Node.js empfängt die Benachrichtigung per <code>LISTEN</code> und schreibt sie in den HTTP-Stream</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-4 md:col-span-2"><strong>Browser</strong></div>
    <div class="col-span-8 md:col-span-10"><code>EventSource</code>-API empfängt das Ereignis und aktualisiert die Oberfläche</div>
  </div>
</div>

Kein Polling, keine Verzögerung. Änderungen erscheinen innerhalb von Millisekunden.

## Kanäle

Der Server lauscht auf sechs PostgreSQL-Kanäle pro Mandant:

<div class="w-full px-3 px-0 my-4">
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong><code>crmti_change</code></strong></div>
    <div class="col-span-7 md:col-span-9">Neue und verpasste Anrufe, Kontaktänderungen</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong><code>whatsapp_message</code></strong></div>
    <div class="col-span-7 md:col-span-9">Ein- und ausgehende WhatsApp-Nachrichten</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong><code>calendar_change</code></strong></div>
    <div class="col-span-7 md:col-span-9">Neue, verschobene oder gelöschte Kalendereinträge</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong><code>faktura_change</code></strong></div>
    <div class="col-span-7 md:col-span-9">Änderungen an Angeboten, Aufträgen, Rechnungen und Positionen</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong><code>weroni_question</code></strong></div>
    <div class="col-span-7 md:col-span-9">Offene Rückfragen der KI-Assistentin Weroni</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong><code>camera_event</code></strong></div>
    <div class="col-span-7 md:col-span-9">Erkannte Objekte und Alarme der Videoüberwachung</div>
  </div>
</div>

## Module mit Echtzeit-Updates

Die folgenden Bereiche reagieren auf SSE-Ereignisse und aktualisieren sich ohne Seitenreload:

- **InfoBar** — Zähler für ungelesene Anrufe, E-Mails, WhatsApp-Nachrichten und Weroni-Rückfragen
- **Kalender** — Neue oder verschobene Termine erscheinen sofort
- **Faktura** — Belegstatus-Änderungen werden live synchronisiert
- **WhatsApp** — Eingehende Nachrichten erscheinen direkt im Chat
- **Kamera** — Neue Erkennungen und Alarme in der Live-Ansicht
- **Wall-Display** — Großbildschirm-Modus mit Echtzeit-Aktualisierung
- **Mechaniker-Modus** — Auftragszuweisungen und Statuswechsel in Echtzeit

## Mandantentrennung

Jeder Mandant erhält eine eigene PostgreSQL-LISTEN-Verbindung. Die Zuordnung erfolgt über die Session:

1. Der Browser sendet den Session-Cookie mit der SSE-Anfrage.
2. Der SSE-Server ermittelt über die Auth-Datenbank den zugehörigen Mandanten.
3. Eine dedizierte LISTEN-Verbindung zur Mandanten-Datenbank wird hergestellt (oder wiederverwendet).
4. Ereignisse erreichen ausschließlich die Benutzer des jeweiligen Mandanten.

LISTEN-Verbindungen werden automatisch aufgebaut, wenn sich der erste Benutzer eines Mandanten verbindet, und geschlossen, wenn der letzte Benutzer die Verbindung trennt.

## Stabilität

<div class="w-full px-3 px-0 my-4">
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Heartbeat</strong></div>
    <div class="col-span-7 md:col-span-9">Alle 30 Sekunden ein Kommentar-Frame — hält die Verbindung durch Proxies und Load-Balancer offen</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Auto-Reconnect</strong></div>
    <div class="col-span-7 md:col-span-9">Browser stellt die Verbindung bei Abbruch automatisch wieder her (<code>EventSource</code>-Standard)</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>DB-Reconnect</strong></div>
    <div class="col-span-7 md:col-span-9">PostgreSQL-LISTEN-Verbindungen werden bei Abbruch nach 5 Sekunden neu aufgebaut</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Graceful Shutdown</strong></div>
    <div class="col-span-7 md:col-span-9">SIGTERM/SIGINT schließen alle Verbindungen und Pools sauber</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Polling-Fallback</strong></div>
    <div class="col-span-7 md:col-span-9">E-Mail (60 Sek.) und WhatsApp (120 Sek.) haben zusätzliche Polling-Intervalle als Absicherung</div>
  </div>
</div>

## Auto-Reload bei Deployment

Der SSE-Server überwacht die Datei `build-id.txt` im Build-Verzeichnis. Ändert sich die Build-ID nach einem Deployment, erhalten alle verbundenen Browser ein `build_changed`-Ereignis und laden die Anwendung automatisch neu.

## Deployment

Der SSE-Server läuft als Hintergrundprozess im Docker-Container neben Apache und PHP-FPM. Apache leitet `/sse/`-Anfragen per Reverse-Proxy an Port 3001 weiter.

<div class="w-full px-3 px-0 my-4">
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Runtime</strong></div>
    <div class="col-span-7 md:col-span-9">Node.js 22 (native <code>http</code>-Modul, keine Frameworks)</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Abhängigkeit</strong></div>
    <div class="col-span-7 md:col-span-9"><code>pg</code> (PostgreSQL-Client) — einzige externe Abhängigkeit</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Port</strong></div>
    <div class="col-span-7 md:col-span-9">3001 (konfigurierbar über <code>SSE_PORT</code>)</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Health-Check</strong></div>
    <div class="col-span-7 md:col-span-9"><code>GET /health</code> — gibt <code>ok</code> zurück</div>
  </div>
  <div class="grid grid-cols-12 gap-x-6 mb-1">
    <div class="col-span-5 md:col-span-3"><strong>Reverse-Proxy</strong></div>
    <div class="col-span-7 md:col-span-9">Apache: <code>ProxyPass /sse/ http://127.0.0.1:3001/</code></div>
  </div>
</div>

## Häufig gestellte Fragen

### Brauche ich WebSockets?

Nein. LxCars nutzt Server-Sent Events (SSE) — ein einfacheres Protokoll, das über Standard-HTTP läuft und von jedem Reverse-Proxy ohne Sonderkonfiguration durchgelassen wird.

### Was passiert bei einem Verbindungsabbruch?

Der Browser stellt die SSE-Verbindung automatisch wieder her. Auf Serverseite werden getrennte PostgreSQL-Listener nach 5 Sekunden neu aufgebaut.

### Werden Daten zwischen Mandanten getrennt?

Ja. Jeder Mandant erhält eine eigene PostgreSQL-LISTEN-Verbindung. Ereignisse eines Mandanten erreichen ausschließlich dessen Benutzer.

### Wie hoch ist die Serverlast?

Gering. Der SSE-Server hält offene HTTP-Verbindungen und leitet PostgreSQL-Benachrichtigungen weiter. Es gibt keine Polling-Schleifen und keine eigene Geschäftslogik.

