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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s
This commit is contained in:
1 parent
634195b629
commit
1776e87029
5 files changed
+160
-124
No files matched your search
+63
-38
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user