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. | | `doc_state` | Server ➔ Client | Wird direkt beim Verbinden gesendet: Liefert den aktuellen In-Memory-Zustand (`title`, `blocks[]`, `activeCursors[]`). |
| `CRDT_INIT` | Server ➔ Client | Sendet den aktuellen Basiszustand (`stateBlob`) und offene Delta-Updates (`updates[]`) an den Client. | | `block_updated` | Bidirektional | Feingranulare Inhalts- oder Statusänderung eines Blocks (`blockId`, `content`, `level`, `type`, `checked`, `collapsed`). |
| `CRDT_UPDATE` | Bidirektional | Überträgt inkrementelle binäre Yjs-Updates (Base64-kodiert). Server speichert in `document_crdt_updates` und verteilt an alle anderen Raumnutzer. | | `block_created` | Bidirektional | Erzeugt einen neuen Block (`block`, `afterBlockId`, `newIndex`). |
| `CRDT_COMPACT` | Client ➔ Server | Komprimiert mehrere Updates zu einem konsolidierten Snapshot in `document_crdt_state`. | | `block_deleted` | Bidirektional | Löscht einen Block anhand seiner `blockId`. |
| `CRDT_AWARENESS` | Bidirektional | Überträgt Live-Präsenz (Fokus auf `blockId`, Benutzername, Farbe). | | `block_moved` | Bidirektional | Verschiebt einen Block / Unterbaum (`blockId`, `newIndex`, `level`). |
| `CRDT_AWARENESS_LEAVE` | Bidirektional | Signalisiert, dass ein Nutzer den Fokus verloren hat oder das Dokument verlassen hat. | | `title_updated` | Bidirektional | Aktualisiert den Dokumententitel (`title`). |
| `CRDT_LEAVE` | Client ➔ Server | Verlässt den Dokumentenraum explizit. | | `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 ```mermaid
sequenceDiagram sequenceDiagram
autonumber autonumber
actor UserA as Benutzer A actor UserA as Admin (Fenster A, cli_1)
participant WS as WebSocket Hub & SQLite participant Room as DocRoom (Go RAM)
actor UserB as Benutzer B actor UserB as User (Fenster B, cli_2)
participant DB as SQLite (blocks, block_tags, documents)
UserA->>WS: CRDT_JOIN (docId: "doc_123") UserA->>Room: Connect (clientId="cli_1", userName="Admin")
WS-->>UserA: CRDT_INIT (stateBlob, updates) Room-->>UserA: doc_state (title, blocks, activeCursors)
UserB->>WS: CRDT_JOIN (docId: "doc_123") UserB->>Room: Connect (clientId="cli_2", userName="User")
WS-->>UserB: CRDT_INIT (stateBlob, updates) Room-->>UserB: doc_state (title, blocks, activeCursors)
UserA->>WS: CRDT_UPDATE (docId, updateBlob) UserA->>Room: block_updated (clientId="cli_1", blockId="b1", content="Neu")
WS->>WS: Append in document_crdt_updates Note over Room: Mutiert RAM-Slice & resetet 1.5s Timer
WS->>UserB: CRDT_UPDATE (docId, updateBlob) Room-->>UserB: block_updated (clientId="cli_1", blockId="b1", content="Neu")
Note over UserB: Yjs wendet Delta atomar an (0 Konflikte) Note over UserA: Echo Filter: cli_1 erhält kein Selbstecho!
UserA->>WS: CRDT_AWARENESS (docId, focusedBlockId) UserB->>Room: cursor_moved (clientId="cli_2", blockId="b1", selStart=3)
WS->>UserB: CRDT_AWARENESS (docId, user, focusedBlockId) Room-->>UserA: cursor_moved (clientId="cli_2", blockId="b1")
Note over UserB: Avatar-Badge erscheint am Block 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 ### 4.3 1.5-Sekunden Debounce-Flush in SQLite
- Das Tippen im Editor erfolgt latenzfrei direkt im Arbeitsspeicher des `DocRoom`.
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: - 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:
- **Aktuelles Verhalten:** Mehrere Benutzer mit Bearbeitungsrechten können zeitgleich dasselbe Dokument bearbeiten. 1. Persistiert alle Blöcke in `blocks`.
- **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"). 2. Extrahiert alle Hashtags (`#tag`) per Regex und aktualisiert `block_tags`.
- **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. 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). 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. - 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)`). - 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: 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. - `PATCH /api/v1/documents/:id/blocks`: Akzeptiert Block-Mutationen (`upserted`, `deletedIds`, `blockOrder`) und synchronisiert den In-Memory-Raum sowie SQLite.
- `POST /api/v1/documents/:id/awareness`: Überträgt Cursor- und Blockfokus-Präsenzen. - `PUT /api/v1/documents/:id`: Aktualisiert Dokumenttitel und Metadaten.
- `DELETE /api/v1/documents/:id/awareness`: Informiert Peers beim Verlassen des Dokuments. - `POST /api/v1/events/rooms`: Erlaubt SSE-Clients, Räumen dynamisch beizutreten (`join`) oder sie zu verlassen (`leave`).
- `POST /api/v1/events/rooms`: Erlaubt SSE-Clients, Dokument- oder Listenräumen dynamisch beizutreten (`join`) oder sie zu verlassen (`leave`).
### 3. Verbindungs-Indikator ### 3. Verbindungs-Indikator
- **Grün pulsierend:** Live verbunden über WebSockets (`Live (WebSocket)`). - **Grün pulsierend:** Live verbunden über WebSockets (`Live (WebSocket)`).
+34 -23
View File
@@ -27,7 +27,9 @@ erDiagram
lists ||--o{ list_items : "enthält" lists ||--o{ list_items : "enthält"
lists ||--o{ list_shares : "wird geteilt mit" 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_shares : "geteilt mit Benutzern"
documents ||--o{ doc_team_shares : "geteilt mit Teams" documents ||--o{ doc_team_shares : "geteilt mit Teams"
documents ||--o{ doc_user_folders : "eigene Platzierung" 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 ### 2.4 Workspace: Dokumente, Blöcke & Geteilte Ordner
#### `documents` & `document_blocks` (Database-First Outliner) #### `documents`, `blocks` & `block_tags` (Database-First Outliner Schema)
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). 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 ```sql
CREATE TABLE IF NOT EXISTS documents ( CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY, -- Präfix "doc_..." id TEXT PRIMARY KEY, -- Präfix "doc_..."
user_id TEXT NOT NULL, -- Eigentümer user_id TEXT NOT NULL DEFAULT '', -- Eigentümer (usr_...)
title TEXT NOT NULL, -- Dokumenttitel 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 icon TEXT NOT NULL DEFAULT '📄', -- Emoji-Icon
content TEXT NOT NULL DEFAULT '', -- Plaintext / Text-Repräsentation (Cache) content TEXT NOT NULL DEFAULT '', -- Plaintext / Text-Repräsentation
blocks_json TEXT NOT NULL DEFAULT '[]', -- Legacy JSON-Cache blocks_json TEXT NOT NULL DEFAULT '[]', -- Legacy/Zusatz-Cache
tags TEXT NOT NULL DEFAULT '[]', -- JSON Array von Tags tags TEXT NOT NULL DEFAULT '[]', -- JSON Array von Tags
folder_path TEXT NOT NULL DEFAULT '', -- Relativer Ordnerpfad des Eigentümers 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_pinned INTEGER NOT NULL DEFAULT 0, -- Globaler Pin
is_archived INTEGER NOT NULL DEFAULT 0, is_archived INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE 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_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 ( CREATE TABLE IF NOT EXISTS blocks (
id TEXT PRIMARY KEY, -- Präfix "blk_..." id TEXT PRIMARY KEY, -- Block-ID (z.B. "blk_..." oder "docId-b0")
doc_id TEXT NOT NULL, document_id TEXT NOT NULL, -- Zugehöriges Dokument (doc_...)
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'
content TEXT NOT NULL DEFAULT '', -- Textinhalt des Blocks 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) 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 lang TEXT NOT NULL DEFAULT '', -- Programmiersprache für Codeblöcke
updated_at TEXT NOT NULL, FOREIGN KEY(document_id) REFERENCES documents(id) ON DELETE CASCADE
updated_by TEXT NOT NULL DEFAULT '',
FOREIGN KEY(doc_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`) #### Entkoppelte Ordner-Freigabe (`workspace_shared_folders` & `workspace_folder_mounts`)
+12 -25
View File
@@ -8,7 +8,7 @@ Diese Datei dokumentiert eine ehrliche und schonungslose technische Analyse alle
| ID | Bereich | Problem / Schwachstelle | Schweregrad | Risiko im Produktivbetrieb | | 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-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-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) | | **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-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-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-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 ## 🔍 Detaillierte Analyse & Maßnahmen
### FLAW-01: [GELÖST / IMPLEMENTIERT] Yjs Outliner CRDT & Multi-User Kollaboration ### FLAW-01: [GELÖST / IMPLEMENTIERT] Granulare Outliner-Synchronisation & Multi-User Kollaboration
- **Status:** **Gelöst via Yjs CRDT Engine (Lösung i)** - **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/hub.go`, `internal/storage/sqlite.go`, `frontend/src/lib/crdt.ts`, `frontend/src/features/workspace/WorkspaceView.tsx` - **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 ein kooperatives Dokumenten-Lease-Locking mit Last-Write-Wins. Bei parallelem Zugriff oder Netztrennung konnten Änderungen überschrieben werden. - **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 (Yjs CRDT):** - **Implementierte Lösung:**
1. **Block-Level Yjs CRDT:** Implementierung eines vollwertigen CRDT-Modells mit `Y.Doc`. Die Blockreihenfolge wird über ein `Y.Array<string>` (Block-IDs) konfliktfrei mit dem YATA-Algorithmus aufgelöst. Inhalte und Metadaten sind in `Y.Map` gekapselt. 1. **In-Memory DocRooms:** Granularer WebSocket-Raum pro Dokument (`/api/v1/ws/docs/:id`) mit Mutex-geschütztem Block-State im Go-Backend.
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. 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. **Proxy-Resiliente Dual-Transport-Architektur:** 3. **Live Presence & Ephemere Cursors:** Cursorposition und Textselektionen (`cursor_moved`) werden flüchtig an Peers gestreamt, ohne SQLite zu belasten.
- **Primary:** Gorilla WebSocket (`/api/v1/ws/docs/:id`) mit binärer Übertragung. 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).
- **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`). 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`).
4. **Offline-First via IndexedDB:** Lokale Eingaben landen sofort und unterbrechungsfrei in `y-indexeddb`. Daten gehen auch bei Netzwerkabbrüchen nicht verloren. 6. **Vollständige Eliminierung von Locks:** Alle blockierenden Sperren wurden restlos entfernt. Mehrere Nutzer können zeitgleich schreiben.
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)").
--- ---
@@ -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 ## 🧭 Verwandte Dokumente
- 🏗️ [Gesamtarchitektur](architecture.md) - 🏗️ [Gesamtarchitektur](architecture.md)
- 💾 [Datenbank-Design & Tabellenschemata](database.md) - 💾 [Datenbank-Design & Tabellenschemata](database.md)
+44 -32
View File
@@ -1,12 +1,12 @@
# 📝 Kernmodul 3: Workspace Outliner-Engine, Wiki-Links & Pure-DB Storage # 📝 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 ## 🌳 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) • 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). - `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 + 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). - `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. - `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`, `/h1`, `/h2`, `/h3`, `/code`, `/callout`, `/quote`, `/divider`). - `/` (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) ### 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. 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`). - 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. - Ein Klick auf ein beliebiges Breadcrumb-Element zoomt sofort auf die jeweilige Ebene zurück.
### Drag & Drop von Blöcken ### 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:** 1. **Interne Wiki-Links:**
- Text in doppelten eckigen Klammern (z.B. `[[Reiseplanung 2026]]`) wird automatisch als klickbarer Link gerendert. - Text in doppelten eckigen Klammern (z.B. `[[Reiseplanung 2026]]`) wird automatisch als klickbare Notiz-Pille gerendert.
- Ein Klick darauf durchsucht die Notizen des Benutzers und öffnet das Ziel-Dokument direkt im Editor. - Klick darauf öffnet die verknüpfte Notiz direkt im Editor.
2. **Hashtags (`#tag`):** 2. **Slash-Befehl `/link` & Link-Modal:**
- Hashtags im Fließtext werden automatisch indiziert (`doc_block_tags`-Tabelle). - `/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. - 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 MindSift setzt auf eine latenzfreie In-Memory Kollaborations-Architektur ([`internal/ws/docroom.go`](../../internal/ws/docroom.go)):
Ab MindSift v2.1 setzt der Workspace auf ein **konfliktfreies Replicated Data Type (CRDT) Modell** auf Basis von [Yjs](https://yjs.dev/):
### Funktionsweise ### Funktionsweise
1. **CRDT-Struktur im Client:** 1. **In-Memory DocRoom pro geöffnetem Dokument:**
- Ein `Y.Doc` pro geöffnetem Dokument. - Hält den synchronen Zustand von `title` und `blocks[]` im RAM.
- `Y.Array<string>` (`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. - Granulare Mutations-Events (`block_updated`, `block_created`, `block_deleted`, `block_moved`, `title_updated`).
- `Y.Map` (`block:<id>`): Speichert Inhalt (`content`), Einrückung (`level`), Erledigungsstatus (`checked`) und Faltungsstatus (`collapsed`). - Ephemeres Presence-Tracking (`cursor_moved`, `client_left`) zeigt farbige Benutzer-Avatare im Header und Cursors am aktiven Block an.
2. **Kollaborative Bearbeitung ohne Sperren:** 2. **Echo-Schutz:**
- Nutzer können simultan dieselbe Notiz bearbeiten. - Jeder Client identifiziert sich mit einer eindeutigen `clientId`.
- Fokus- und Cursor-Positionen werden via `CRDT_AWARENESS` übertragen und als farbige Benutzer-Pillen direkt am jeweiligen Block visualisiert. - 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 │ Go DocRoom: In-Memory Mutation & Broadcast an Peers
│ (Mathematisch exakte CRDT-Konvergenz)
│ │
└──▶ Periodischer Snapshot / Flush └──▶ 1.5s Inaktivitäts-Debounce (oder Raum-Exit)
│ │
▼ ▼
SQLite: document_blocks SQLite Transaktion:
(Relationale Indizes für Global To-Dos, Tag-Explorer & FTS) 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. 1. **Kompiliertes Markdown:** Jeder Block wird gemäß seiner `level`-Tiefe mit 2 Leerzeichen pro Einrückungsebene als sauberes Markdown formatiert (`documents.markdown_cache`).
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**. 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.
--- ---
+7 -6
View File
@@ -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. * 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. * **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. * Untergeordnete Unterpunkte werden beim Ziehen automatisch als zusammenhängender Ast mitbewegt.
* **Echtzeit-Schreib-Lock (Kollisionsschutz):** * **Live-Kollaboration & Presence-Tracking:**
* Sobald du einen Bullet-Point per Drag & Drop greifst oder anfängst zu tippen, wird sofort ein Schreib-Lock für das Dokument aktiviert. * Gleichzeitiges, latenzfreies Bearbeiten von Dokumenten im Team ohne blockierende Sperren.
* Andere Nutzer sehen live, wer das Dokument gerade bearbeitet, und Kollisionen bei gleichzeitigen Änderungen werden zuverlässig verhindert. * Farbige Avatare im Header und Live-Cursors am aktiven Block visualisieren genau, wer gerade wo tippt.
* **Slash-Befehle (`/`):** Tippe einfach `/` für `/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/callout` oder `/divider`. * 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`. * **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. * **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 ## 🔒 Deine Daten gehören dir
* **100 % Self-Hosted:** Keine fremden Cloud-Dienste, keine Telemetrie, kein Tracking. * **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.
--- ---