docs: update database schema, websocket protocol, workspace outliner spec, flaws backlog, and readme to match current architecture
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s

This commit is contained in:
sandrodach committed 2026-10-26 22:41:50 +01:00
1 parent 634195b629
commit 1776e87029
5 files changed
+160 -124

No files matched your search

+63 -38
View File
@@ -66,55 +66,81 @@ Alle WebSocket-Nachrichten werden als JSON mit einheitlicher Hülle übertragen:
---
## ⚡ 4. CRDT-Echtzeitprotokoll (Yjs Collaborative Editing)
## ⚡ 4. Granulare Workspace-Echtzeitsynchronisation (`/api/v1/ws/docs/{id}`)
Seit Version 2.1 unterstützt MindSift **echte, konfliktfreie Mehrbenutzer-Kollaboration** für Notizen via Yjs CRDT. Clients tauschen binäre Delta-Updates und Live-Awareness-Informationen über dedizierte WebSocket-Nachrichten aus:
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`:
### CRDT-Nachrichtentypen
| Event | Richtung | Zweck / Payload |
```
wss://<domain>/api/v1/ws/docs/<docId>?token=<jwt>&clientId=<cli_...>&userName=<Name>&userColor=<Hex>
```
### 4.1 JSON-Echtzeit-Eventprotokoll
Die Kommunikation erfolgt über leichtgewichtige JSON-Nachrichten ohne binären CRDT-Overhead:
| Event-Typ | Richtung | Zweck / Payload |
|---|---|---|
| `CRDT_JOIN` | Client ➔ Server | Tritt dem Dokumentenraum (`doc:{docId}`) bei. |
| `CRDT_INIT` | Server ➔ Client | Sendet den aktuellen Basiszustand (`stateBlob`) und offene Delta-Updates (`updates[]`) an den Client. |
| `CRDT_UPDATE` | Bidirektional | Überträgt inkrementelle binäre Yjs-Updates (Base64-kodiert). Server speichert in `document_crdt_updates` und verteilt an alle anderen Raumnutzer. |
| `CRDT_COMPACT` | Client ➔ Server | Komprimiert mehrere Updates zu einem konsolidierten Snapshot in `document_crdt_state`. |
| `CRDT_AWARENESS` | Bidirektional | Überträgt Live-Präsenz (Fokus auf `blockId`, Benutzername, Farbe). |
| `CRDT_AWARENESS_LEAVE` | Bidirektional | Signalisiert, dass ein Nutzer den Fokus verloren hat oder das Dokument verlassen hat. |
| `CRDT_LEAVE` | Client ➔ Server | Verlässt den Dokumentenraum explizit. |
| `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 Benutzer A
participant WS as WebSocket Hub & SQLite
actor UserB as Benutzer B
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->>WS: CRDT_JOIN (docId: "doc_123")
WS-->>UserA: CRDT_INIT (stateBlob, updates)
UserB->>WS: CRDT_JOIN (docId: "doc_123")
WS-->>UserB: CRDT_INIT (stateBlob, updates)
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->>WS: CRDT_UPDATE (docId, updateBlob)
WS->>WS: Append in document_crdt_updates
WS->>UserB: CRDT_UPDATE (docId, updateBlob)
Note over UserB: Yjs wendet Delta atomar an (0 Konflikte)
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!
UserA->>WS: CRDT_AWARENESS (docId, focusedBlockId)
WS->>UserB: CRDT_AWARENESS (docId, user, focusedBlockId)
Note over UserB: Avatar-Badge erscheint am Block
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.
## 🔒 5. Abwärtskompatibles Document-Lease-Locking
Vor Version 2.1 nutzte MindSift ein serverseitiges Lease-Locking mit 30-Sekunden-Heartbeats. Durch die CRDT-Engine wurde die blockierende Bearbeitungssperre (`isLockedByOther`) abgelöst:
- **Aktuelles Verhalten:** Mehrere Benutzer mit Bearbeitungsrechten können zeitgleich dasselbe Dokument bearbeiten.
- **Presence & Locks:** Die Events `LOCK_DOC` und `HEARTBEAT_DOC` bleiben abwärtskompatibel erhalten und dienen nun der reinen Präsenzanzeige (z.B. gelber Statusindikator „wird gerade von X angesehen").
- **Schreibschutz:** Ein echter Schreibschutz (`isReadOnly`) greift ausschließlich noch dann, wenn der Nutzer nur über Leserechte (`permission === 'read'`) für die Notiz oder den Ordner verfügt.
### 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. Fallback-Strategie & Proxy-Resilienz (Dual-Transport Architektur)
## 🔒 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).
@@ -153,12 +179,11 @@ MindSift löst dieses Problem durch eine **vollwertige Dual-Transport-Architektu
- 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-Modus
### 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:
- `PUT /api/v1/documents/:id/crdt`: Unterstützt sowohl volles `state` (Compaction) als auch inkrementelles `update` (Delta). Der Hub persistiert das Update und broadcastet es sofort an alle anderen WS- und SSE-Clients im Dokumentenraum.
- `POST /api/v1/documents/:id/awareness`: Überträgt Cursor- und Blockfokus-Präsenzen.
- `DELETE /api/v1/documents/:id/awareness`: Informiert Peers beim Verlassen des Dokuments.
- `POST /api/v1/events/rooms`: Erlaubt SSE-Clients, Dokument- oder Listenräumen dynamisch beizutreten (`join`) oder sie zu verlassen (`leave`).
- `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)`).