# πŸ“‘ WebSocket & Echtzeit-Synchronisation MindSift implementiert eine bidirektionale Echtzeit-Synchronisationsschicht auf Basis von WebSockets ([`internal/ws/hub.go`](../../internal/ws/hub.go)). Sie sorgt dafΓΌr, dass Γ„nderungen an Aufgaben, Triage-Karten, Notizen und geteilten Einkaufslisten ohne manuelles Neuladen sofort auf allen geΓΆffneten GerΓ€ten und bei allen Kollaborationspartnern sichtbar werden. --- ## πŸ”Œ 1. Verbindung & Handshake ### Verbindungs-Endpunkte ``` wss:///api/v1/ws?token= wss:///api/v1/ws/lists/?token= ``` ### Authentifizierung & Upgrade - Die Authentifizierung erfolgt ΓΌber das JWT-Token im Query-Parameter `?token=...` oder den `Authorization: Bearer`-Header wΓ€hrend des HTTP-Upgrade-Requests. - Der Server extrahiert die `userID` und registriert die Verbindung im internen `Hub`. - Der `Upgrader` akzeptiert alle Origins (`CheckOrigin: return true`), um volle KompatibilitΓ€t mit Reverse-Proxies (Traefik, Nginx, Caddy, Cloudflare) und Docker-Netzwerken zu gewΓ€hrleisten. --- ## πŸ›οΈ 2. Hub-Architektur & Rooms Der WebSocket-Hub unterteilt Verbindungen in drei Abstraktionsebenen: ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ WebSocket Hub β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ allClients β”‚ userRooms β”‚ rooms β”‚ β”‚ (Alle aktiven β”‚ (Pro Benutzer-ID) β”‚ (Pro Liste/ β”‚ β”‚ Verbindungen) β”‚ Cross-Device Sync fΓΌr β”‚ Space) β”‚ β”‚ β”‚ Tasks, Triage & Workspace β”‚ Geteilte Liste β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β–Ό β–Ό Presence Stats TASK_CREATED ITEM_TOGGLED DOC_LOCKED TASK_UPDATED ITEM_CREATED DOC_UNLOCKED DOC_UPDATED LIST_UPDATED ``` 1. **`allClients` (Global):** HΓ€lt jede aktive Client-Verbindung. ErmΓΆglicht Broadcasts systemweiter Ereignisse (z.B. globale Dokument-Sperrungen). 2. **`userRooms[userID]` (Benutzer-RΓ€ume):** Sendet Events an alle GerΓ€te desselben Benutzers (z.B. Desktop und Smartphone). Erzeugt ein Webhook oder Quick Dump auf dem Smartphone einen Eintrag, taucht dieser zeitgleich auf dem Desktop auf. 3. **`rooms[listID]` (Kollaborations-RΓ€ume):** Multiplexing fΓΌr geteilte Listen. Alle Mitglieder einer Liste empfangen Aktionen (wie Abhaken, LΓΆschen, Umsortieren) in Echtzeit. --- ## πŸ“¨ 3. Nachrichten-Format (Envelope) Alle WebSocket-Nachrichten werden als JSON mit einheitlicher HΓΌlle ΓΌbertragen: ```json { "event": "ITEM_TOGGLED", "listId": "lst_12345", "payload": { "itemId": "itm_987", "isCompleted": true, "completedBy": "usr_sandro", "completedByName": "Sandro" }, "timestamp": "2026-10-26T13:30:00Z" } ``` --- --- ## ⚑ 4. Granulare Workspace-Echtzeitsynchronisation (`/api/v1/ws/docs/{id}`) MindSift nutzt fΓΌr die Outliner-Synchronisation eine schlanke, hochgradig reaktive In-Memory-Raumarchitektur (`DocRoom` in [`internal/ws/docroom.go`](../../internal/ws/docroom.go)). Clients verbinden sich mit ihrer eindeutigen `clientId`: ``` wss:///api/v1/ws/docs/?token=&clientId=&userName=&userColor= ``` ### 4.1 JSON-Echtzeit-Eventprotokoll Die Kommunikation erfolgt ΓΌber leichtgewichtige JSON-Nachrichten ohne binΓ€ren CRDT-Overhead: | Event-Typ | Richtung | Zweck / Payload | |---|---|---| | `doc_state` | Server βž” Client | Wird direkt beim Verbinden gesendet: Liefert den aktuellen In-Memory-Zustand (`title`, `blocks[]`, `activeCursors[]`). | | `block_updated` | Bidirektional | Feingranulare Inhalts- oder StatusΓ€nderung eines Blocks (`blockId`, `content`, `level`, `type`, `checked`, `collapsed`). | | `block_created` | Bidirektional | Erzeugt einen neuen Block (`block`, `afterBlockId`, `newIndex`). | | `block_deleted` | Bidirektional | LΓΆscht einen Block anhand seiner `blockId`. | | `block_moved` | Bidirektional | Verschiebt einen Block / Unterbaum (`blockId`, `newIndex`, `level`). | | `title_updated` | Bidirektional | Aktualisiert den Dokumententitel (`title`). | | `cursor_moved` | Bidirektional | **Ephemere Live-PrΓ€senz & Cursor:** Sendet Benutzer-Avatar, Cursorposition und Textselektion (`clientId`, `userId`, `userName`, `userColor`, `blockId`, `selectionStart`, `selectionEnd`). Wird nicht in SQLite gespeichert! | | `client_left` | Server βž” Client | Wird automatisch ausgelΓΆst, wenn ein Peer die Verbindung trennt, damit Remote-Cursor sofort verschwinden. | ```mermaid sequenceDiagram autonumber actor UserA as Admin (Fenster A, cli_1) participant Room as DocRoom (Go RAM) actor UserB as User (Fenster B, cli_2) participant DB as SQLite (blocks, block_tags, documents) UserA->>Room: Connect (clientId="cli_1", userName="Admin") Room-->>UserA: doc_state (title, blocks, activeCursors) UserB->>Room: Connect (clientId="cli_2", userName="User") Room-->>UserB: doc_state (title, blocks, activeCursors) UserA->>Room: block_updated (clientId="cli_1", blockId="b1", content="Neu") Note over Room: Mutiert RAM-Slice & resetet 1.5s Timer Room-->>UserB: block_updated (clientId="cli_1", blockId="b1", content="Neu") Note over UserA: Echo Filter: cli_1 erhΓ€lt kein Selbstecho! UserB->>Room: cursor_moved (clientId="cli_2", blockId="b1", selStart=3) Room-->>UserA: cursor_moved (clientId="cli_2", blockId="b1") Note over UserA: Farbiger Cursor-Avatar erscheint an b1 Note over Room: 1.5s InaktivitΓ€t ohne neue TastenanschlΓ€ge Room->>DB: Debounce-Flush (blocks, Regex #tags -> block_tags, markdown_cache) ``` ### 4.2 Strikter Echo-Schutz (`clientId`) - Jeder Client generiert beim Start einen flΓΌchtigen SchlΓΌssel (`clientIdRef`, z. B. `cli_a8f9_l0z3`). - Der Go-Hub filtert beim Broadcasten im Raum alle Clients heraus, deren Verbindung oder `clientId` mit dem Absender ΓΌbereinstimmt: ```go if evt.ClientID != "" && client.clientID == evt.ClientID { continue } ``` - **Vorteil:** Verhindert Cursor-Resets, Fokus-Verluste und Race Conditions durch Selbstechos wΓ€hrend des lokalen Tippens. ### 4.3 1.5-Sekunden Debounce-Flush in SQLite - Das Tippen im Editor erfolgt latenzfrei direkt im Arbeitsspeicher des `DocRoom`. - Sobald 1,5 Sekunden lang keine neuen TastenanschlΓ€ge oder Mutationen eintreffen (oder wenn der letzte Client den Raum verlΓ€sst), stâßt der Raum einen synchronisierten Flush an: 1. Persistiert alle BlΓΆcke in `blocks`. 2. Extrahiert alle Hashtags (`#tag`) per Regex und aktualisiert `block_tags`. 3. Kompiliert die Blockliste (anhand der `level`-Tiefe) in Standard-Markdown und speichert sie in `documents.markdown_cache`. --- ## πŸ”’ 5. Berechtigungen & Bearbeitungsrechte - **Kollaboration ohne blockierende Locks:** Alle Nutzer mit Schreibrechten (`permission === 'owner'` oder `'write'`) kΓΆnnen zeitgleich im selben Dokument tippen. Pessimistische Locks oder Lease-Heartbeats wurden vollstΓ€ndig entfernt. - **Schreibschutz:** VerfΓΌgt ein Nutzer nur ΓΌber Leserechte (`permission === 'read'`), ist das Eingabefeld gesperrt (`readOnly`). Er kann das Dokument jedoch in Echtzeit mitlesen und sieht die Cursors der Bearbeiter. --- ## πŸ”„ 6. Fallback-Strategie & Proxy-Resilienz (Dual-Transport Architektur) In restriktiven Firmennetzwerken, hinter Unternehmens-Proxies (z.B. Zscaler, BlueCoat) oder fehlerhaft konfigurierten Reverse-Proxies wird der `Upgrade: websocket`-Header hΓ€ufig verworfen (`NS_ERROR_CONNECTION_REFUSED`, Code 1006 / 1002 / 1008). MindSift lΓΆst dieses Problem durch eine **vollwertige Dual-Transport-Architektur** ([FLAW-03](../flaws.md)): ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MindSift Frontend β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό Standard: WebSockets Fallback: SSE + REST (wss://.../api/v1/ws) (https://.../api/v1/events) β”‚ β”‚ β”‚ [Proxy Upgrade Block (Code 1006/Timeout)]β”‚ β”‚ ────────────────────────────────────────> β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Gorilla WS Conn β”‚ β”‚ Server-Sent Events β”‚ β”‚ Bidirektionaler β”‚ β”‚ (text/event-stream) β”‚ β”‚ TCP-WebSocket β”‚ β”‚ Standard HTTPS Port β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Realtime Hub β”‚ β”‚ (Multiplexing WS & SSE) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### 1. Automatischer SSE-Fallback (`GET /api/v1/events`) - Scheitert der WebSocket-Handshake oder blockiert ein Proxy das Upgrade, wechselt MindSift innerhalb von 3,5 Sekunden automatisch auf einen **Server-Sent Events (SSE) Stream**. - Da SSE ein standardmÀßiger HTTP/1.1- bzw. HTTP/2-GET-Request ΓΌber Standard-Port 443/80 ist (`Content-Type: text/event-stream`), wird er von jedem Proxy und jeder Firewall ungehindert durchgeleitet. - Header `X-Accel-Buffering: no` und `Cache-Control: no-cache` verhindern Pufferung im Proxy; periodische Ping-Kommentare (`: ping\n\n`) alle 15 Sekunden halten Timeouts fern. - Der Client bleibt dadurch **vollstΓ€ndig echtzeitfΓ€hig** (`Live (SSE)`). ### 2. REST-Endpunkte fΓΌr Upstream-Aktionen im SSE-Fallback-Modus Da SSE unidirektional (Server βž” Client) ist, stellt MindSift fΓΌr SchreibvorgΓ€nge im SSE-Modus REST-Γ„quivalente bereit: - `PATCH /api/v1/documents/:id/blocks`: Akzeptiert Block-Mutationen (`upserted`, `deletedIds`, `blockOrder`) und synchronisiert den In-Memory-Raum sowie SQLite. - `PUT /api/v1/documents/:id`: Aktualisiert Dokumenttitel und Metadaten. - `POST /api/v1/events/rooms`: Erlaubt SSE-Clients, RΓ€umen dynamisch beizutreten (`join`) oder sie zu verlassen (`leave`). ### 3. Verbindungs-Indikator - **GrΓΌn pulsierend:** Live verbunden ΓΌber WebSockets (`Live (WebSocket)`). - **TΓΌrkis pulsierend:** Live verbunden ΓΌber Server-Sent Events (`Live (Server-Sent Events / Proxy-Stream)`). - **Gelb/Amber:** TemporΓ€re Unterbrechung mit automatischem HTTP-Polling-Schutz. AusfΓΌhrliche Konfigurationsbeispiele fΓΌr Nginx, Traefik, Caddy, Cloudflare und Apache finden sich im separaten Handbuch: πŸ‘‰ **[Reverse-Proxy-Konfigurationshandbuch](../reverse-proxy.md)**