From 1776e8702986bbce54e8452a247c9e9af33c06f6 Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 26 Oct 2026 22:41:50 +0100 Subject: [PATCH] docs: update database schema, websocket protocol, workspace outliner spec, flaws backlog, and readme to match current architecture --- Doku/api/websocket.md | 101 ++++++++++++++++++++++++-------------- Doku/database.md | 57 ++++++++++++--------- Doku/flaws.md | 37 +++++--------- Doku/modules/workspace.md | 76 ++++++++++++++++------------ README.md | 13 ++--- 5 files changed, 160 insertions(+), 124 deletions(-) diff --git a/Doku/api/websocket.md b/Doku/api/websocket.md index ba172ba..fb211e6 100644 --- a/Doku/api/websocket.md +++ b/Doku/api/websocket.md @@ -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:///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 | |---|---|---| -| `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)`). diff --git a/Doku/database.md b/Doku/database.md index 3b73140..c2478c1 100644 --- a/Doku/database.md +++ b/Doku/database.md @@ -27,7 +27,9 @@ erDiagram lists ||--o{ list_items : "enthält" lists ||--o{ list_shares : "wird geteilt mit" - documents ||--o{ document_blocks : "besteht aus" + documents ||--o{ blocks : "besteht aus" + documents ||--o{ block_tags : "indiziert" + blocks ||--o{ block_tags : "verknüpft" documents ||--o{ doc_shares : "geteilt mit Benutzern" documents ||--o{ doc_team_shares : "geteilt mit Teams" documents ||--o{ doc_user_folders : "eigene Platzierung" @@ -207,45 +209,54 @@ CREATE TABLE IF NOT EXISTS task_list_shares ( ### 2.4 Workspace: Dokumente, Blöcke & Geteilte Ordner -#### `documents` & `document_blocks` (Database-First Outliner) -Dokumente werden als Kopfdatensatz in `documents` gespeichert, während der Inhalt als feingranulare Blöcke in `document_blocks` verwaltet wird. MindSift speichert alle Notizen zu 100% in SQLite (keine physischen `.md`-Dateien auf Festplatte). +#### `documents`, `blocks` & `block_tags` (Database-First Outliner Schema) +Dokumente werden als Kopfdatensatz in `documents` gespeichert, während der Inhalt als feingranulare Blöcke in `blocks` verwaltet wird. Hashtags (#tags) werden per Regex analysiert und in `block_tags` abgelegt. MindSift speichert alle Notizen zu 100% in SQLite (keine physischen `.md`-Dateien auf Festplatte). ```sql CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, -- Präfix "doc_..." - user_id TEXT NOT NULL, -- Eigentümer - title TEXT NOT NULL, -- Dokumenttitel + user_id TEXT NOT NULL DEFAULT '', -- Eigentümer (usr_...) + space_id TEXT DEFAULT '', + title TEXT NOT NULL DEFAULT '', -- Dokumenttitel + folder_id TEXT NOT NULL DEFAULT '', -- Ordner-Zuordnung + markdown_cache TEXT NOT NULL DEFAULT '', -- Kompiliertes Standard-Markdown (automatischer Cache) icon TEXT NOT NULL DEFAULT '📄', -- Emoji-Icon - content TEXT NOT NULL DEFAULT '', -- Plaintext / Text-Repräsentation (Cache) - blocks_json TEXT NOT NULL DEFAULT '[]', -- Legacy JSON-Cache + content TEXT NOT NULL DEFAULT '', -- Plaintext / Text-Repräsentation + blocks_json TEXT NOT NULL DEFAULT '[]', -- Legacy/Zusatz-Cache tags TEXT NOT NULL DEFAULT '[]', -- JSON Array von Tags folder_path TEXT NOT NULL DEFAULT '', -- Relativer Ordnerpfad des Eigentümers - shared_folder_id TEXT NOT NULL DEFAULT '', -- ID aus workspace_shared_folders (wenn in geteiltem Ordner) - sub_folder_path TEXT NOT NULL DEFAULT '', -- Relativer Unterordnerpfad innerhalb des geteilten Ordners is_pinned INTEGER NOT NULL DEFAULT 0, -- Globaler Pin is_archived INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_documents_user ON documents(user_id, is_archived, updated_at DESC); -CREATE INDEX IF NOT EXISTS idx_documents_shared_folder ON documents(shared_folder_id); -CREATE TABLE IF NOT EXISTS document_blocks ( - id TEXT PRIMARY KEY, -- Präfix "blk_..." - doc_id TEXT NOT NULL, - sort_order REAL NOT NULL DEFAULT 0, -- Fließkommazahl für konfliktfreies Einfügen - type TEXT NOT NULL DEFAULT 'bullet', -- 'bullet', 'todo', 'h1', 'h2', 'h3', 'code', 'quote', 'callout', 'divider' +CREATE TABLE IF NOT EXISTS blocks ( + id TEXT PRIMARY KEY, -- Block-ID (z.B. "blk_..." oder "docId-b0") + document_id TEXT NOT NULL, -- Zugehöriges Dokument (doc_...) content TEXT NOT NULL DEFAULT '', -- Textinhalt des Blocks - level INTEGER NOT NULL DEFAULT 0, -- Hierarchieebene (0 = Root, 1 = Kind...) + level INT NOT NULL DEFAULT 0, -- Hierarchieebene (0 = Root, 1 = Kind...) + position REAL NOT NULL DEFAULT 0, -- Fließkommazahl für konfliktfreies Einfügen/Sortieren + type TEXT NOT NULL DEFAULT 'bullet', -- 'bullet', 'todo', 'h1', 'h2', 'h3', 'code', 'quote', 'callout', 'divider' checked INTEGER NOT NULL DEFAULT 0, -- Für Checklisten (0 = offen, 1 = abgehakt) - collapsed INTEGER NOT NULL DEFAULT 0, -- Für eingeklappte Unterbäume + collapsed INTEGER NOT NULL DEFAULT 0, -- Für eingeklappte Unterbäume (1 = zugeklappt) lang TEXT NOT NULL DEFAULT '', -- Programmiersprache für Codeblöcke - updated_at TEXT NOT NULL, - updated_by TEXT NOT NULL DEFAULT '', - FOREIGN KEY(doc_id) REFERENCES documents(id) ON DELETE CASCADE + FOREIGN KEY(document_id) REFERENCES documents(id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_document_blocks_doc_order ON document_blocks(doc_id, sort_order); +CREATE INDEX IF NOT EXISTS idx_blocks_doc_pos ON blocks(document_id, position ASC); + +CREATE TABLE IF NOT EXISTS block_tags ( + tag TEXT NOT NULL, -- Hashtag (z.B. "meeting", ohne führendes '#') + document_id TEXT NOT NULL, -- Zugehöriges Dokument + block_id TEXT NOT NULL, -- Zugehöriger Block + PRIMARY KEY(tag, block_id), + FOREIGN KEY(document_id) REFERENCES documents(id) ON DELETE CASCADE, + FOREIGN KEY(block_id) REFERENCES blocks(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_block_tags_tag ON block_tags(tag); +CREATE INDEX IF NOT EXISTS idx_block_tags_doc ON block_tags(document_id); ``` #### Entkoppelte Ordner-Freigabe (`workspace_shared_folders` & `workspace_folder_mounts`) diff --git a/Doku/flaws.md b/Doku/flaws.md index b00cb7e..f3eaf7b 100644 --- a/Doku/flaws.md +++ b/Doku/flaws.md @@ -8,7 +8,7 @@ Diese Datei dokumentiert eine ehrliche und schonungslose technische Analyse alle | ID | Bereich | Problem / Schwachstelle | Schweregrad | Risiko im Produktivbetrieb | |---|---|---|---|---| -| **FLAW-01** | Workspace Sync | Kein echtes CRDT/OT – Lock-Heartbeat-Modell mit Last-Write-Wins | **Gelöst (v2.1)** | Behoben durch Yjs CRDT-Engine & blockbasierte kollaborative Bearbeitung | +| **FLAW-01** | Workspace Sync | Blockierende Locks / Synchronisations-Konflikte | **Gelöst (v2.2)** | Behoben durch In-Memory DocRooms, Echo-Schutz per `clientId` und 1.5s Debounce-Flush | | **FLAW-02** | Storage | Dual-Storage Drift (SQLite vs. Dateisystem) | **Obsolet / Gelöst** | Kein Risiko: MindSift nutzt Pure-DB (keine `.md`-Dateien auf Platte) | | **FLAW-03** | Netzwerk / WS | Proxy-Inkompatibilität bei WebSocket-Upgrades & Polling-Fallback | **Gelöst (v2.2)** | Behoben durch Dual-Transport SSE-Fallback & globale Singleton-Verbindung | | **FLAW-04** | Datenbank / Suche | Volltextsuche via `LIKE '%query%'` statt SQLite FTS5 | **Mittel** | Performance-Einbrüche bei wachsenden Notizbeständen (>10.000 Blöcke) | @@ -16,25 +16,22 @@ Diese Datei dokumentiert eine ehrliche und schonungslose technische Analyse alle | **FLAW-06** | SQLite Concurrency | Single-Writer-Bottleneck bei hohem parallelem Schreibaufkommen | **Mittel** | `SQLITE_BUSY` Fehler bei vielen gleichzeitigen Team-Nutzern | | **FLAW-07** | Benachrichtigungen | Fehlende Dead-Letter-Queue für Webhook- & E-Mail-Alarme | **Niedrig** | Verlorene Benachrichtigungen bei temporärem Ausfall von Drittsystemen | | **FLAW-08** | Sicherheit / Auth | Statische JWT-Tokens ohne rotierende Refresh-Tokens & Blacklisting | **Niedrig** | Keine sofortige Sitzungsinvalidierung bei Rechteentzug | -| **FLAW-09** | Outliner UX | Kein hierarchischer Undo/Redo-Stack für Strukturmutationen | **Mittel** | Versehentliches Löschen/Einrücken von Blöcken nicht per `Ctrl+Z` rückgängig machbar | --- ## 🔍 Detaillierte Analyse & Maßnahmen -### FLAW-01: [GELÖST / IMPLEMENTIERT] Yjs Outliner CRDT & Multi-User Kollaboration -- **Status:** **Gelöst via Yjs CRDT Engine (Lösung i)** -- **Betroffene Komponenten:** [`api/websocket.md`](./api/websocket.md), [`modules/workspace.md`](./modules/workspace.md), `internal/ws/hub.go`, `internal/storage/sqlite.go`, `frontend/src/lib/crdt.ts`, `frontend/src/features/workspace/WorkspaceView.tsx` -- **Ursache (Historisch):** MindSift nutzte ein kooperatives Dokumenten-Lease-Locking mit Last-Write-Wins. Bei parallelem Zugriff oder Netztrennung konnten Änderungen überschrieben werden. -- **Implementierte Lösung (Yjs CRDT):** - 1. **Block-Level Yjs CRDT:** Implementierung eines vollwertigen CRDT-Modells mit `Y.Doc`. Die Blockreihenfolge wird über ein `Y.Array` (Block-IDs) konfliktfrei mit dem YATA-Algorithmus aufgelöst. Inhalte und Metadaten sind in `Y.Map` gekapselt. - 2. **Vollständige Eliminierung des Lock-Managers:** Alle pessimistischen Dokumentensperren (`isLockedByOther`, Lease-Heartbeats, 423 Locked) wurden restlos entfernt. Mehrere Nutzer können zeitgleich an denselben Absätzen und Blöcken arbeiten. - 3. **Proxy-Resiliente Dual-Transport-Architektur:** - - **Primary:** Gorilla WebSocket (`/api/v1/ws/docs/:id`) mit binärer Übertragung. - - **Fallback:** 4-Sekunden-Watchdog schaltet bei blockiertem WebSocket-Upgrade automatisch auf HTTP-Transport um: Downstream via Server-Sent Events (`GET /api/v1/sync/docs/:id/stream`) und Upstream via HTTP POST (`POST /api/v1/sync/docs/:id/update`). - 4. **Offline-First via IndexedDB:** Lokale Eingaben landen sofort und unterbrechungsfrei in `y-indexeddb`. Daten gehen auch bei Netzwerkabbrüchen nicht verloren. - 5. **Asynchroner DocRoom-Hub & Debounced SQLite-Persistenz:** In-Memory `DocRoom` pro aktivem Dokument; SQLite-Schreibvorgänge laufen debounced (2.5 Sekunden Inaktivität oder Raum-Austritt) rein asynchron im Hintergrund, ohne API-Threads oder Tastenschläge zu blockieren. - 6. **UI Status-Indikator:** Dezent im Header visualisiert: Grün ("Live (WebSocket)"), Gelb ("Eingeschränkt (HTTP-Proxy-Modus)") und Grau ("Offline (Lokal gesichert)"). +### FLAW-01: [GELÖST / IMPLEMENTIERT] Granulare Outliner-Synchronisation & Multi-User Kollaboration +- **Status:** **Gelöst via In-Memory DocRooms, Echo-Schutz & Debounced Persistence** +- **Betroffene Komponenten:** [`api/websocket.md`](./api/websocket.md), [`modules/workspace.md`](./modules/workspace.md), `internal/ws/docroom.go`, `internal/storage/sqlite.go`, `frontend/src/lib/useDocumentSync.ts`, `frontend/src/features/workspace/OutlinerBlock.tsx` +- **Ursache (Historisch):** MindSift nutzte früher pessimistisches Dokumentensperren (Lease-Locks mit Heartbeats). Bei parallelem Bearbeiten blockierten sich Nutzer gegenseitig; unausgereifte Sync-Versuche führten zu Selbstechos und Cursor-Jumps. +- **Implementierte Lösung:** + 1. **In-Memory DocRooms:** Granularer WebSocket-Raum pro Dokument (`/api/v1/ws/docs/:id`) mit Mutex-geschütztem Block-State im Go-Backend. + 2. **Strikter Echo-Schutz per `clientId`:** Jede Verbindung besitzt eine eindeutige `clientId`. Der Hub filtert den Sender heraus und leitet Events nur an Peers weiter. + 3. **Live Presence & Ephemere Cursors:** Cursorposition und Textselektionen (`cursor_moved`) werden flüchtig an Peers gestreamt, ohne SQLite zu belasten. + 4. **Fokus- & Eingabeschutz (`OutlinerBlock.tsx`):** Remote-Updates werden nur blockiert, wenn `document.hasFocus()` vorliegt. Bei `window.onblur` und `input.onblur` wird sofort (0ms) geflusht; Callbacks werden über `useRef` geführt (keine Stale Closures). + 5. **1.5s Inaktivitäts-Debounce-Flush:** Nach 1,5s Inaktivität sichert der Raum persistent in SQLite (`blocks`, Regex `#tags` in `block_tags` und kompiliertes Markdown in `documents.markdown_cache`). + 6. **Vollständige Eliminierung von Locks:** Alle blockierenden Sperren wurden restlos entfernt. Mehrere Nutzer können zeitgleich schreiben. --- @@ -125,16 +122,6 @@ Diese Datei dokumentiert eine ehrliche und schonungslose technische Analyse alle --- -### FLAW-09: Kein hierarchischer Undo/Redo-Stack im Outliner -- **Betroffene Komponenten:** [`modules/workspace.md`](./modules/workspace.md), `frontend/src/features/workspace/WorkspaceView.tsx` -- **Ursache:** Standardmäßiges Browser-`Ctrl+Z` funktioniert nur innerhalb eines einzelnen aktiven Textfelds (`contenteditable` / Textarea). Strukturelle Aktionen – wie das Verschieben von Blöcken via Drag & Drop, Einrücken (`Tab`), Ausrücken (`Shift+Tab`) oder das Löschen ganzer Unterbäume – sind im Standard-Browser-Undo-Stack nicht enthalten. -- **Folgen:** Verschiebt ein Nutzer versehentlich einen Block samt 10 Unterpunkten, kann dies nicht mit `Ctrl+Z` rückgängig gemacht werden. -- **Empfohlene Lösung:** - 1. Implementierung eines expliziten In-Memory Command-Stacks im Frontend (Command-Pattern: `execute()` / `undo()`). - 2. Aufzeichnung aller Block-Mutationen (Insert, Indent, Move, Delete) mit Revert-Payloads. - ---- - ## 🧭 Verwandte Dokumente - 🏗️ [Gesamtarchitektur](architecture.md) - 💾 [Datenbank-Design & Tabellenschemata](database.md) diff --git a/Doku/modules/workspace.md b/Doku/modules/workspace.md index be55f6a..a0d93f7 100644 --- a/Doku/modules/workspace.md +++ b/Doku/modules/workspace.md @@ -1,12 +1,12 @@ # 📝 Kernmodul 3: Workspace Outliner-Engine, Wiki-Links & Pure-DB Storage -Der MindSift Workspace ([`WorkspaceView.tsx`](../../frontend/src/features/workspace/WorkspaceView.tsx)) kombiniert ein **hierarchisches Bullet-Point Notizsystem (inspiriert von Logseq)** mit einem **persönlichen Wiki** und einem **globalen To-Do Rollup Dashboard**. +Der MindSift Workspace ([`WorkspaceView.tsx`](../../frontend/src/features/workspace/WorkspaceView.tsx)) kombiniert ein **hierarchisches Bullet-Point Notizsystem (inspiriert von Logseq)** mit einem **persönlichen Wiki**, **Live-Kollaboration** und einem **globalen To-Do Rollup Dashboard**. --- ## 🌳 1. Die Logseq-Outliner-Engine -Im Gegensatz zu klassischen Fließtext-Editoren basiert der MindSift Workspace auf **Blöcken** (`document_blocks` in SQLite). Jeder Block ist ein gedanklicher Knotenpunkt mit einer flexiblen Einrückungsebene (`level: 0, 1, 2, ...`). +Im Gegensatz zu klassischen Fließtext-Editoren basiert der MindSift Workspace auf einer **flachen, relationalen Liste von Blöcken** (`blocks`-Tabelle in SQLite). Jeder Block ist ein gedanklicher Knotenpunkt mit einer flexiblen Einrückungsebene (`level: 0, 1, 2, ...`) und einer fortlaufenden Fließkommaposition (`position`): ``` • Projekt MindSift v2.0 (level 0) @@ -22,12 +22,20 @@ Im Gegensatz zu klassischen Fließtext-Editoren basiert der MindSift Workspace a - `Tab`: Rückt den aktuellen Block ein (wird Kindknoten des vorherigen Blocks). - `Shift + Tab`: Rückt den Block um eine Ebene aus (verringert die Tiefe). - `Shift + Enter`: Weicher Zeilenumbruch innerhalb desselben Blocks (z.B. für mehrzeilige Notizen oder Code). -- `Backspace` (auf leerem Block): Rückt den Block aus oder löscht ihn, wenn er auf Level 0 ist. -- `/` (Slash-Commands): Öffnet das Umwandlungsmenü (`/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/callout`, `/quote`, `/divider`). +- `Backspace` (auf leerem Block): Löscht den Block sofort. Das Ausrücken erfolgt strikt und kontrolliert über `Shift + Tab`. +- `/` (Slash-Commands): Öffnet das Umwandlungsmenü (`/todo`, `/link`, `/h1`, `/h2`, `/h3`, `/code`, `/callout`, `/quote`, `/divider`). +- **Markdown-Tastatur-Kürzel:** Tippen von `[] ` oder `[ ] ` wandelt den Block sofort in ein Todo um; `# `, `## `, `### ` erzeugt Überschriften. + +### Eingabestabilität & Zombie-Fokus-Schutz (`OutlinerBlock.tsx`) +1. **Echter Fokus-Check:** Remote-Updates (`block_updated`) werden nur dann abgewehrt, wenn der Block tatsächlich aktiv im Betriebssystem editiert wird: + `isFocusedRef.current === true && document.hasFocus()`. +2. **Sofort-Flush bei Fensterwechsel:** Sobald das Browser-Fenster den Fokus verliert (`window.addEventListener('blur', ...)`), wird ein offener 150ms-Debounce sofort gecancelt und der Text augenblicklich per WebSocket übertragen. +3. **Stale Closures eliminiert:** Alle Callbacks werden in `useRef` gehalten, sodass asynchrone Debounce-Timer garantiert die aktuellste Komponenten-Referenz verwenden. ### Zoom-In Hoisting (Fokus-Modus) -Klickt der Benutzer auf den Bullet-Dot (`•`) eines Blocks, blendet der Editor alle übergeordneten und benachbarten Blöcke aus und zoomt **ausschließlich in diesen Ast** hinein. -- Eine dezente Breadcrumb-Leiste oben zeigt den Pfad an (z.B. `Workspace / Projekte / MindSift / Architektur`). +Klickt der Benutzer auf den Bullet-Dot (`•`) eines Blocks, blendet der Editor alle übergeordneten und benachbarten Blöcke aus und zoomt **ausschließlich in diesen Ast** hinein: +- Der gewählte Punkt wird zur prominenten Überschrift im Fokus-Modus. +- Eine klickbare Breadcrumb-Leiste oben zeigt den Pfad an (z.B. `Workspace / Projekte / MindSift / Architektur`). - Ein Klick auf ein beliebiges Breadcrumb-Element zoomt sofort auf die jeweilige Ebene zurück. ### Drag & Drop von Blöcken @@ -35,51 +43,55 @@ Der Bullet-Dot dient gleichzeitig als Drag-Anfasser. Wird ein Block verschoben, --- -## 🔗 2. Wiki-Links (`[[Link]]`) & Hashtags +## 🔗 2. Wiki-Links (`[[Link]]`), Web-Links (`/link`) & Hashtags 1. **Interne Wiki-Links:** - - Text in doppelten eckigen Klammern (z.B. `[[Reiseplanung 2026]]`) wird automatisch als klickbarer Link gerendert. - - Ein Klick darauf durchsucht die Notizen des Benutzers und öffnet das Ziel-Dokument direkt im Editor. -2. **Hashtags (`#tag`):** - - Hashtags im Fließtext werden automatisch indiziert (`doc_block_tags`-Tabelle). + - Text in doppelten eckigen Klammern (z.B. `[[Reiseplanung 2026]]`) wird automatisch als klickbare Notiz-Pille gerendert. + - Klick darauf öffnet die verknüpfte Notiz direkt im Editor. +2. **Slash-Befehl `/link` & Link-Modal:** + - `/link` öffnet ein Modal mit zwei Modi: + - **Notiz verknüpfen:** Durchsucht bestehende Notizen und fügt `[[Titel]]` ein. + - **Web-Link:** Eingabe von Titel und URL, fügt Standard-Markdown `[Titel](url)` ein. +3. **Hashtags (`#tag`):** + - Hashtags im Fließtext werden automatisch indiziert (`block_tags`-Tabelle). - In der linken Leiste listet der Tag-Explorer alle verwendeten Schlagworte auf. Klick auf ein Tag filtert alle Dokumente und Blöcke, die dieses Tag enthalten. --- ---- +## ⚡ 3. Echtzeit-Kollaboration & Automatische Markdown-Kompilierung -## ⚡ 3. Yjs CRDT Real-Time Engine & Dual-Persistence - -Ab MindSift v2.1 setzt der Workspace auf ein **konfliktfreies Replicated Data Type (CRDT) Modell** auf Basis von [Yjs](https://yjs.dev/): +MindSift setzt auf eine latenzfreie In-Memory Kollaborations-Architektur ([`internal/ws/docroom.go`](../../internal/ws/docroom.go)): ### Funktionsweise -1. **CRDT-Struktur im Client:** - - Ein `Y.Doc` pro geöffnetem Dokument. - - `Y.Array` (`blocks`): Führt die Block-IDs in korrekter Reihenfolge. Der YATA-Algorithmus garantiert, dass simultanes Einfügen, Verschieben oder Löschen von Blöcken durch mehrere Nutzer deterministisch und konfliktfrei konvergiert. - - `Y.Map` (`block:`): Speichert Inhalt (`content`), Einrückung (`level`), Erledigungsstatus (`checked`) und Faltungsstatus (`collapsed`). -2. **Kollaborative Bearbeitung ohne Sperren:** - - Nutzer können simultan dieselbe Notiz bearbeiten. - - Fokus- und Cursor-Positionen werden via `CRDT_AWARENESS` übertragen und als farbige Benutzer-Pillen direkt am jeweiligen Block visualisiert. +1. **In-Memory DocRoom pro geöffnetem Dokument:** + - Hält den synchronen Zustand von `title` und `blocks[]` im RAM. + - Granulare Mutations-Events (`block_updated`, `block_created`, `block_deleted`, `block_moved`, `title_updated`). + - Ephemeres Presence-Tracking (`cursor_moved`, `client_left`) zeigt farbige Benutzer-Avatare im Header und Cursors am aktiven Block an. +2. **Echo-Schutz:** + - Jeder Client identifiziert sich mit einer eindeutigen `clientId`. + - Der Server sendet Mutations-Events ausschließlich an andere Clients im selben Raum (`clientId != senderClientId`), um Cursor-Springen und Selbstechos zu verhindern. -### Dual-Persistence in SQLite +### Automatische Markdown-Kompilierung & SQLite Debounce-Flush ``` -Client (Y.Doc Mutation) +Eingabe im OutlinerBlock │ - ├──▶ WebSocket (CRDT_UPDATE Base64 Delta) + ├──▶ WebSocket (block_updated Event) │ │ │ ▼ - │ SQLite: document_crdt_updates / document_crdt_state - │ (Mathematisch exakte CRDT-Konvergenz) + │ Go DocRoom: In-Memory Mutation & Broadcast an Peers │ - └──▶ Periodischer Snapshot / Flush + └──▶ 1.5s Inaktivitäts-Debounce (oder Raum-Exit) │ ▼ - SQLite: document_blocks - (Relationale Indizes für Global To-Dos, Tag-Explorer & FTS) + SQLite Transaktion: + 1. Persistiert Blöcke in `blocks` + 2. Regex-Extraktion von #tags in `block_tags` + 3. Kompiliert Block-Hierarchie zu Standard-Markdown + und speichert Cache in `documents.markdown_cache` ``` -1. **Mathematische CRDT-Garantie:** Binäre Yjs-Updates werden in `document_crdt_updates` und periodisch als Snapshot in `document_crdt_state` gesichert. Auch nach einem Server-Neustart oder Offline-Phasen konvergieren alle Clients verlustfrei. -2. **Relationale Query-Fähigkeit:** Um schnelle SQL-Abfragen für das globale To-Do Rollup Dashboard und Hashtags zu ermöglichen, werden die Blöcke parallel in `document_blocks` materialisiert. Es existieren **keine Markdown-Dateien auf der Festplatte**. +1. **Kompiliertes Markdown:** Jeder Block wird gemäß seiner `level`-Tiefe mit 2 Leerzeichen pro Einrückungsebene als sauberes Markdown formatiert (`documents.markdown_cache`). +2. **Pure Database Architecture:** Alle Daten liegen transaktional und sicher in SQLite. Es werden keine unkontrollierten Dateien auf der Festplatte abgelegt; Notizen können jederzeit per Klick als `.md`-Datei exportiert werden. --- diff --git a/README.md b/README.md index 9ce2f6d..8ef9705 100644 --- a/README.md +++ b/README.md @@ -74,13 +74,14 @@ Der MindSift-Workspace vereint die Übersicht eines klassischen Dokumenten-Explo * Verschiebe Absätze und ganze Gedankenbäume mühelos mit der Maus. * **Unterpunkte per Drop erstellen:** Ziehst du einen Bullet-Point direkt auf einen anderen Aufzählungspunkt, wird er automatisch als Unter-Bullet eingerückt. Eingeklappte Eltern-Punkte klappen beim Hineinlegen automatisch auf. * Untergeordnete Unterpunkte werden beim Ziehen automatisch als zusammenhängender Ast mitbewegt. -* **Echtzeit-Schreib-Lock (Kollisionsschutz):** - * Sobald du einen Bullet-Point per Drag & Drop greifst oder anfängst zu tippen, wird sofort ein Schreib-Lock für das Dokument aktiviert. - * Andere Nutzer sehen live, wer das Dokument gerade bearbeitet, und Kollisionen bei gleichzeitigen Änderungen werden zuverlässig verhindert. -* **Slash-Befehle (`/`):** Tippe einfach `/` für `/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/callout` oder `/divider`. +* **Live-Kollaboration & Presence-Tracking:** + * Gleichzeitiges, latenzfreies Bearbeiten von Dokumenten im Team ohne blockierende Sperren. + * Farbige Avatare im Header und Live-Cursors am aktiven Block visualisieren genau, wer gerade wo tippt. + * Strikter Echo-Schutz verhindert Cursor-Springen und Flackern beim Tippen. +* **Slash-Befehle (`/`):** Tippe einfach `/` für `/todo`, `/link` (Notizen & Web-Links verknüpfen), `/h1`, `/h2`, `/h3`, `/code`, `/callout`, `/quote` oder `/divider`. * **Vernetzung & Hashtags:** Verlinke Notizen untereinander mit Wiki-Links (`[[Notiztitel]]`) und tagge Gedanken mit `#Hashtags`. * **Explorer mit Ordnern & Favoriten:** Organisiere Dokumente hierarchisch in Ordnern, verschiebe Notizen per Drag & Drop und hefte deine wichtigsten Dokumente als Favoriten ganz oben an. -* **100 % Standard-Markdown:** Jedes Dokument wird lokal unter `./data/workspace/...` als saubere `.md`-Datei mit 2-Leerzeichen-Einrückung abgelegt – jederzeit lesbar und kompatibel mit Obsidian, Logseq oder VS Code. +* **100 % Pure-DB & Markdown-Export:** Alle Notizen und Blöcke werden transaktional und sicher in SQLite gespeichert. Beim Speichern wird automatisch ein sauberer Markdown-Cache generiert, und Dokumente können jederzeit mit einem Klick als `.md`-Datei exportiert werden. --- @@ -226,7 +227,7 @@ MindSift erkennt den Erststart automatisch und führt dich durch den Einrichtung ## 🔒 Deine Daten gehören dir * **100 % Self-Hosted:** Keine fremden Cloud-Dienste, keine Telemetrie, kein Tracking. -* **Kein Vendor-Lock-in:** Alle Notizen liegen als lesbare `.md`-Dateien im Ordner `./data/workspace`. Backups können jederzeit exportiert werden. +* **Kein Vendor-Lock-in:** Alle Notizen liegen als relationale Daten in SQLite und werden automatisch als Standard-Markdown kompiliert. Dokumente lassen sich jederzeit mit einem Klick als `.md`-Datei exportieren oder als SQLite-Dump sichern. ---