docs: rewrite documentation and README from current codebase, add flaw analysis
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s

Remove outdated claims (CRDT, DocRooms, presence, .md files on disk) and
document the actual architecture, API, permissions and known flaws.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Sonnet 5.5 committed 2026-10-27 23:09:05 +01:00
1 parent 4e9c4ffa8c
commit bc13235da6
14 files changed
+733 -1618

No files matched your search

+119 -175
View File
@@ -1,185 +1,129 @@
# 🔌 REST-API Referenz
# REST-API
Die MindSift REST-API ermöglicht den vollständigen Zugriff auf Gedanken, Aufgaben, Workspace-Notizen, Listen, Benutzerkonten und Smarthome-Automationen.
Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}`. Authentifizierung über `Authorization: Bearer <JWT>` oder `Bearer mindsift_sec_…` bzw. Header `X-API-Key`. Zeitstempel sind ISO-8601 (UTC). Quelle der Wahrheit sind die Routen in [server.go](../../internal/api/server.go).
---
## Öffentlich
## 🔐 1. Authentifizierung
| Methode | Pfad | Zweck |
|---|---|---|
| GET | `/health` | Lebenszeichen |
| GET | `/auth/status` | `needsSetup`, `allowRegistration`, `instanceName`, `demoMode` |
| POST | `/auth/setup` | Erstes Administratorkonto (`email`, `password`, `displayName`, `instanceName`, `allowRegistration`, `sharingMode`, `seedSampleData`); nur solange es keine Konten gibt |
| POST | `/auth/register` | Registrierung (`email`, `password`, `displayName`, `inviteCode`); nur mit Code oder wenn freigegeben |
| POST | `/auth/login` | Anmeldung (`login`/`email`/`username`, `password`) → `{token, user}` |
| POST | `/auth/quickstart` | Immer `403` (Gastzugang deaktiviert) |
| POST | `/webhooks/ingest` | Aufgabe per Webhook anlegen, Token als `?token=`, `X-API-Key` oder Bearer. Body: `title`/`content`/`text`/`message`, `priority`, `source`, `tags`, `dueDate`; Klartext-Body wird als Inhalt genommen |
| GET | `/ws`, `/ws/spaces/{id}`, `/ws/lists/{id}` | WebSocket, siehe [websocket.md](websocket.md) |
| GET | `/events` | SSE-Fallback |
| POST | `/events/rooms` | SSE-Räume beitreten/verlassen ([FLAW-02](../flaws.md)) |
MindSift unterstützt zwei Authentifizierungsmethoden:
## Konto und Allgemeines (angemeldet)
1. **JSON Web Token (JWT Bearer):**
Wird beim Login oder Setup erzeugt und im HTTP-Header mitgesendet:
`Authorization: Bearer <jwt-token>`
2. **Persönliche API-Schlüssel (Smarthome / Automatisierung):**
Werden unter *Einstellungen ➔ Private Webhooks* erzeugt (Präfix `mindsift_sec_...`):
- Per HTTP-Header: `X-API-Key: mindsift_sec_xxxxxxxxxxxx`
- Per Authorization-Header: `Authorization: Bearer mindsift_sec_xxxxxxxxxxxx`
- Per Query-Parameter: `?token=mindsift_sec_xxxxxxxxxxxx`
| Methode | Pfad | Zweck |
|---|---|---|
| GET | `/auth/me` | Benutzer und Kennzahlen |
| POST | `/auth/logout` | Token sperren |
| POST | `/auth/revoke-all` | Alle Sitzungen beenden (`token_version` +1) |
| PATCH | `/auth/settings` | `displayName`, `email`, `password`, `avatarUrl`, `alarmChannels`, `alarmEmail`, `discordWebhookUrl`, `customWebhookUrl`, `notifyBeforeMinutes` |
| POST | `/auth/alarms/test` | Test-Alarm (`channel`: `email`/`discord`/`webhook`, optional `target`) |
| GET | `/users/directory`, `/collaborators` | Benutzerverzeichnis für Freigaben ([FLAW-10](../flaws.md)) |
| GET | `/stats` | Zähler fürs Dashboard |
| GET | `/search?q=` | Suche in eigenen Gedanken und Notizen |
| GET | `/export` | Datenexport als JSON |
| GET/POST/DELETE | `/keys`, `/keys/{id}` | API-Keys verwalten (Klartext nur bei Erstellung) |
---
## Gedanken, Triage, Aufgaben
## 🚦 2. Status- & Fehler-Codes
| Methode | Pfad | Zweck |
|---|---|---|
| POST | `/inbox` | Schnellerfassung. Body: `content`, `priority`, `source`, `dueDate`, `target` (`inbox`/`task`/`list_item`/`document`), `targetListId`, `taskListId` (snake_case-Varianten werden akzeptiert) |
| GET | `/thoughts?status=` | Gedanken (`inbox`, `kept`, `converted`, `discarded`, `all`) |
| GET/PATCH/DELETE | `/thoughts/{id}` | Einzelabruf; Teiländerung (`content`, `isCompleted`, `dueDate`, `priority`, `taskListId`, `kanbanStatus`, `notes`, `subtasks`, `tags`); Löschen |
| DELETE | `/thoughts/discarded/purge` | Papierkorb leeren |
| GET | `/triage/deck` | Posteingang (max. 100) |
| PATCH | `/triage/{id}` | `action`: `keep`/`discard`/`restore`; mit `convertTo`: `task`/`document`/`list_item` |
| GET | `/tasks?taskListId=` | Aufgaben (optional einer Liste) |
| POST | `/tasks/reorder`, `/task-lists/{id}/tasks/reorder` | Reihenfolge/Spalte ändern (`items[{id, sortOrder, kanbanStatus}]`) |
| POST | `/tasks/{id}/unlink` | Verknüpfung zu Notiz-Block lösen |
- `200 OK`: Erfolgreiche Abfrage oder Aktualisierung.
- `201 Created`: Ressource erfolgreich angelegt.
- `400 Bad Request`: Ungültiges JSON oder fehlende Pflichtfelder.
- `401 Unauthorized`: Token ungültig, abgelaufen oder fehlend.
- `403 Forbidden`: Unzureichende Berechtigungen (z.B. nur Server-Admin oder nur Lesezugriff).
- `404 Not Found`: Ressource existiert nicht oder gehört einem anderen Benutzer.
- `409 Conflict`: Ressource gesperrt (z.B. Dokument wird bereits von anderem Benutzer bearbeitet).
## Aufgabenlisten
Fehlerrückgaben erfolgen im einheitlichen Format:
```json
{
"error": "Fehlermeldung im Klartext"
}
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST | `/task-lists` | Listen (`permission` je Liste: `owner`/`editor`/`viewer`); anlegen (`title`, `icon`, `color`, `teamId`) |
| PUT/DELETE | `/task-lists/{id}` | Ändern (`title`, `icon`, `color`, `teamId`, `columns`) – nicht für Leser; Löschen nur Eigentümer |
| GET | `/task-lists/{id}/shares` | Freigaben |
| POST | `/task-lists/{id}/shares` | Nur Eigentümer. Body: `targetType` (`team`/`user`), `targetId`, `permission` (`editor`/`viewer`, Standard `editor`). Erneutes Senden für dasselbe Ziel ändert die Stufe |
| DELETE | `/task-lists/{id}/shares/{shareId}` | Nur Eigentümer |
Schreibzugriffe von Lesern auf Listen oder deren Aufgaben liefern `403` („Keine Schreibrechte für diese Aufgabenliste (nur Lesen)“).
## Workspace
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST/DELETE | `/folders` | Ordnerliste, anlegen (`path`), löschen |
| POST | `/folders/move` | `sourcePath`, `targetParentPath` |
| GET/POST | `/workspace/folders/shared` | Geteilte Ordner; anlegen (`name`, `mountPath`) |
| GET/DELETE | `/workspace/folders/shared/{id}` | Abruf; Löschen |
| PUT | `/workspace/folders/shared/{id}/mount` | Eigenen Einhängepfad ändern (`mountPath`) |
| GET/POST/DELETE | `/workspace/folders/shared/{id}/shares[/{shareId}]` | Freigaben (nur Lesen wirksam) |
| GET | `/workspace/tags`, `/workspace/tags/{tag}` | Tag-Übersicht und -Details |
| PUT | `/workspace/tags/{tag}/note` | Tag-Seite speichern (`content`) |
| GET/POST | `/documents` | Notizen auflisten; anlegen (`title`, `icon`, `folderPath`, `blocks`, `tags`, `isPinned`) |
| GET/PUT/DELETE | `/documents/{id}` | Abruf; Ändern (`title`, `icon`, `folderPath`, `blocks`, `tags`, `isPinned`); Löschen |
| PATCH | `/documents/{id}/blocks` | Teil-Update der Blöcke (`upserted`, `deletedIds`, Reihenfolge); löst `DOC_BLOCKS_PATCHED` aus |
| PUT | `/documents/{id}/move` | Ordner wechseln (`folderPath`) |
| POST | `/documents/{id}/blocks/{blockId}/enhanced-task` | Aus einem Block eine verknüpfte Aufgabe machen (`content`, `checked`) |
| GET/POST/DELETE | `/documents/{id}/shares[/{userId}]` | Notiz-Freigaben (`userId`/`email`/`teamId`; wirksam nur Lesen) |
Nur der Eigentümer schreibt in eine Notiz; Empfänger erhalten `403`/„nur Lesezugriff“ ([FLAW-17](../flaws.md)).
## Geteilte Listen
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST | `/lists` | Listen; anlegen (`title`, `icon`, `listType`) |
| GET/DELETE | `/lists/{id}` | Abruf mit Einträgen; Löschen |
| GET/POST/DELETE | `/lists/{id}/shares[/{shareId}]` | Freigaben (`targetType`, `targetId`, `role`: `editor`/`viewer`) |
| POST | `/lists/{id}/items` | Eintrag (`content`, `category`, `quantity`) |
| PATCH | `/lists/{id}/items/{itemId}` | Abhaken (`isCompleted`) |
| PUT | `/lists/{id}/items/{itemId}` | Ändern (`content`, `category`, `quantity`) |
| DELETE | `/lists/{id}/items/{itemId}` | Löschen |
| POST/PUT | `/lists/{id}/items/reorder` | Reihenfolge/Kategorie (`items[{id, sortOrder, category, content}]`) |
| POST | `/lists/{id}/clear-completed` | Erledigte entfernen |
## Gruppen
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST | `/teams` | Eigene Gruppen; anlegen (`name`, `description`) – Ersteller wird Gruppen-Admin |
| PUT/DELETE | `/teams/{id}` | Nur Gruppen-Admin |
| POST | `/teams/{id}/members` | Mitglied hinzufügen (`userId` oder `email`, `role`) |
| PUT/DELETE | `/teams/{id}/members/{userId}` | Rolle ändern (`admin`/`member`); entfernen |
| GET/POST/DELETE | `/organizations`, `/organizations/{id}[/teams]` | Veraltet, ungeschützt ([FLAW-24](../flaws.md)) |
## Administration (`role = admin`)
| Methode | Pfad | Zweck |
|---|---|---|
| GET/POST | `/admin/users` | Benutzer auflisten, anlegen (`email`, `password`, `displayName`, `role`) |
| PATCH/DELETE | `/admin/users/{id}` | `displayName`, `email`, `role`, `isActive`, `discordWebhookUrl`; löschen |
| PUT | `/admin/users/{id}/password` | Passwort zurücksetzen |
| GET/POST/DELETE | `/admin/invites[/{id}]` | Einladungen (`email`, `role`, `teamId`, `validDays`) |
| GET/PUT | `/admin/settings` | `instanceName`, `allowRegistration`, `sharingMode`, SMTP-Felder |
| POST | `/admin/smtp/test` | Testmail (`toEmail`) |
## Beispiele
```bash
# Gedanken per API-Key erfassen
curl -X POST https://mindsift.example.com/api/v1/webhooks/ingest \
-H "X-API-Key: mindsift_sec_…" -H "Content-Type: application/json" \
-d '{"title":"Milch kaufen #einkauf","priority":"high","dueDate":"2026-11-01"}'
# Aufgabenliste nur lesend freigeben
curl -X POST https://mindsift.example.com/api/v1/task-lists/tkl_…/shares \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"targetType":"user","targetId":"usr_…","permission":"viewer"}'
```
---
## 📋 3. Endpunkte nach Modulen
### 3.1 Authentifizierung & System-Status
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `GET` | `/api/v1/health` | Öffentlich | Liveness-Check (`{"status":"ok"}`) |
| `GET` | `/api/v1/auth/status` | Öffentlich | Prüft ob Ersteinrichtung nötig ist (`needsSetup: true/false`) |
| `POST` | `/api/v1/auth/setup` | Öffentlich | Führt First-Boot Setup durch (legt ersten Admin an) |
| `POST` | `/api/v1/auth/register` | Öffentlich | Registrierung über Einladungscode (`inviteCode`) |
| `POST` | `/api/v1/auth/login` | Öffentlich | Login via E-Mail oder Benutzername + Passwort |
| `GET` | `/api/v1/auth/me` | JWT | Gibt das aktuelle Benutzerprofil zurück |
| `PATCH` | `/api/v1/auth/settings` | JWT | Aktualisiert Profil, Passwort, Theme oder Alarm-Kanäle |
| `POST` | `/api/v1/auth/alarms/test` | JWT | Sendet Test-Alarm an E-Mail, Discord oder Custom Webhook |
#### Beispiel: `POST /api/v1/auth/login`
```json
// Request
{
"login": "sandro",
"password": "geheimesPasswort123"
}
// Response (200 OK)
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "usr_abc123",
"email": "sandro@example.com",
"username": "sandro",
"displayName": "Sandro",
"role": "admin",
"alarmChannels": ["discord", "webhook"]
}
}
```
---
### 3.2 Triage & Instant Capture
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `POST` | `/api/v1/inbox` | JWT / API-Key | Erfasst einen neuen Quick Dump oder leitet direkt weiter |
| `GET` | `/api/v1/triage/deck` | JWT | Liefert den Stapel aller noch nicht triagierten Gedanken (`status: 'inbox'`) |
| `PATCH` | `/api/v1/triage/{id}` | JWT | Führt Triage-Aktion aus (`keep`, `discard`, `restore`) |
#### Beispiel: `POST /api/v1/inbox`
Ermöglicht flexible Direkt-Zuordnungen:
```json
// Request (Quick Dump in Triage-Deck)
{
"content": "Kellerfeuchtigkeit über 68% gestiegen #smarthome #alarm",
"priority": "urgent"
}
// Request (Direkt als Aufgabe anlegen)
{
"content": "Server Backup prüfen #devops",
"priority": "high",
"target": "task",
"dueDate": "2026-10-30"
}
```
*Hinweis:* Wenn `/api/v1/inbox` über einen API-Key aufgerufen wird und kein `source` definiert ist, wird die Quelle automatisch als `"webhook"` markiert!
---
### 3.3 Aufgaben & Kanban-System
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `GET` | `/api/v1/tasks` | JWT | Liefert alle Aufgaben des Benutzers (inkl. geteilter Aufgaben) |
| `POST` | `/api/v1/tasks/reorder` | JWT | Speichert Kanban-Status und Reihenfolge (`sort_order`) |
| `GET` | `/api/v1/task-lists` | JWT | Liefert alle Aufgabenlisten (eigene & geteilte) |
| `POST` | `/api/v1/task-lists` | JWT | Erstellt eine neue Aufgabenliste (mit Farbe & Spalten) |
| `PUT` | `/api/v1/task-lists/{id}` | JWT | Ändert Titel, Farbe, Icon oder Kanban-Spalten |
| `DELETE` | `/api/v1/task-lists/{id}` | JWT | Löscht Aufgabenliste |
| `POST` | `/api/v1/task-lists/{id}/shares` | JWT | Teilt Liste mit Team oder Benutzer |
---
### 3.4 Workspace & Dokumente
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `GET` | `/api/v1/documents` | JWT | Liefert Liste aller Dokumente des Benutzers (Metadaten) |
| `POST` | `/api/v1/documents` | JWT | Erstellt neues Notiz-Dokument (mit Blocks oder Markdown) |
| `GET` | `/api/v1/documents/{id}` | JWT | Lädt komplettes Dokument inklusive aller Blöcke |
| `PUT` | `/api/v1/documents/{id}` | JWT | Aktualisiert Dokumenttitel, Icon oder Ordnerpfad |
| `PATCH` | `/api/v1/documents/{id}/blocks` | JWT | Granulares Patching von Dokument-Blöcken (Database-First) |
| `DELETE` | `/api/v1/documents/{id}` | JWT | Löscht Dokument (Datenbank & physische Datei) |
| `POST` | `/api/v1/documents/{id}/shares` | JWT | Notiz mit anderem Benutzer teilen (`read` / `write`) |
| `GET` | `/api/v1/documents/{id}/lock` | JWT | Fragt Bearbeitungs-Sperrstatus ab |
| `POST` | `/api/v1/documents/{id}/lock` | JWT | Sperrt Dokument für Bearbeitung (30s Lease) |
| `DELETE` | `/api/v1/documents/{id}/lock` | JWT | Gibt Bearbeitungssperre wieder frei |
---
### 3.5 Workspace Ordner & Geteilte Ordner
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `GET` | `/api/v1/folders` | JWT | Liefert alle Ordnerpfade des Benutzers |
| `POST` | `/api/v1/folders` | JWT | Legt neuen (auch leeren) Ordner an |
| `POST` | `/api/v1/folders/move` | JWT | Benennt Ordner um oder verschiebt ihn hierarchisch |
| `DELETE` | `/api/v1/folders` | JWT | Löscht Ordner und verschiebt Notizen |
| `GET` | `/api/v1/workspace/folders/shared` | JWT | Liefert für den Benutzer freigegebene geteilte Ordner |
| `POST` | `/api/v1/workspace/folders/shared` | JWT | Erstellt neuen geteilten Ordner |
| `PUT` | `/api/v1/workspace/folders/shared/{id}/mount` | JWT | Ändert den lokalen Einhängepfad beim Empfänger |
| `POST` | `/api/v1/workspace/folders/shared/{id}/shares` | JWT | Gibt Ordner für Teams oder Benutzer frei |
---
### 3.6 Geteilte Listen (Listen & Einkauf)
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `GET` | `/api/v1/lists` | JWT | Liefert alle geteilten Listen |
| `POST` | `/api/v1/lists` | JWT | Erstellt neue geteilte Liste |
| `GET` | `/api/v1/lists/{id}` | JWT | Lädt Liste inklusive aller Einträge |
| `POST` | `/api/v1/lists/{id}/items` | JWT | Fügt Eintrag hinzu |
| `PATCH` | `/api/v1/lists/{id}/items/{itemId}` | JWT | Hakt Eintrag ab (`isCompleted: true/false`) |
| `POST` | `/api/v1/lists/{id}/items/reorder` | JWT | Speichert sortierte Einträge |
| `POST` | `/api/v1/lists/{id}/clear-completed`| JWT | Löscht alle abgehakten Einträge |
| `POST` | `/api/v1/lists/{id}/shares` | JWT | Teilt Liste mit Team oder Benutzer |
---
### 3.7 Smarthome Ingestion & Externe Webhooks
| Methode | Pfad | Auth | Beschreibung |
| :--- | :--- | :--- | :--- |
| `POST` | `/api/v1/webhooks/ingest` | Token / Key | Empfängt externe Webhook-Payloads von Home Assistant / n8n |
#### Beispiel: `POST /api/v1/webhooks/ingest?token=mindsift_sec_...`
Unterstützt flexible Payloads (JSON oder Plain Text):
```json
{
"title": "Waschmaschine Keller fertig",
"priority": "high",
"tags": ["smarthome", "keller"]
}
```
*Live-Aktualisierung:* Sendet automatisch ein `TASK_CREATED` WebSocket-Event an alle Clients des Benutzers, sodass die neue Karte sofort ohne Neuladen im Triage-Deck erscheint!
+37 -177
View File
@@ -1,195 +1,55 @@
# 📡 WebSocket & Echtzeit-Synchronisation
# Echtzeit: WebSocket und SSE
MindSift implementiert eine bidirektionale Echtzeit-Synchronisationsschicht auf Basis von WebSockets ([`internal/ws/hub.go`](../../internal/ws/hub.go)). Sie sorgt dafür, dass Änderungen an Aufgaben, Triage-Karten, Notizen und geteilten Einkaufslisten ohne manuelles Neuladen sofort auf allen geöffneten Geräten und bei allen Kollaborationspartnern sichtbar werden.
Der Server verteilt Änderungen als JSON-Ereignisse an verbundene Clients. Die Ereignisse entstehen **nach** einer REST-Änderung; es gibt keinen Schreibkanal über die Verbindung. Implementierung: [hub.go](../../internal/ws/hub.go), Client: [api.ts](../../frontend/src/lib/api.ts).
---
> Keine CRDTs, keine Sperren, keine Cursor-Presence. Frühere Fassungen dieser Doku beschrieben DocRooms, `clientId`-Echo-Schutz und Yjs; das existiert nicht (mehr).
## 🔌 1. Verbindung & Handshake
## Transporte
### Verbindungs-Endpunkte
```
wss://<domain>/api/v1/ws?token=<jwt-token>
wss://<domain>/api/v1/ws/lists/<listId>?token=<jwt-token>
```
| Transport | Endpunkt | Hinweise |
|---|---|---|
| WebSocket | `GET /api/v1/ws` (auch `/ws/lists/{id}`, `/ws/spaces/{id}`) | Ping alle 30 s, Lesefrist 60 s, Nachrichten bis 4 MB |
| SSE | `GET /api/v1/events` | Kommentar-Ping alle 15 s, Header `X-Accel-Buffering: no` |
### Authentifizierung & Upgrade
- Die Authentifizierung erfolgt über das JWT-Token im Query-Parameter `?token=...` oder den `Authorization: Bearer`-Header während des HTTP-Upgrade-Requests.
- Der Server extrahiert die `userID` und registriert die Verbindung im internen `Hub`.
- Der `Upgrader` akzeptiert alle Origins (`CheckOrigin: return true`), um volle Kompatibilität mit Reverse-Proxies (Traefik, Nginx, Caddy, Cloudflare) und Docker-Netzwerken zu gewährleisten.
Der Client versucht zuerst WebSocket. Gelingt der Aufbau nicht innerhalb von 2,5 s oder bricht die Verbindung unsauber ab, wechselt er auf SSE und bleibt dabei (Firmen-Proxys). Dort liefert der Server `SSE_CONNECTED` mit einer `sseId`; Räume werden über `POST /api/v1/events/rooms` (`sseId`, `room`, `action`: `join`/`leave`) abonniert.
---
**Authentifizierung:** Token als `?token=<JWT|API-Key>` oder `Authorization`-Header. Ohne gültiges Token wird die Verbindung als `anonymous` akzeptiert ([FLAW-01](../flaws.md), [FLAW-02](../flaws.md)). Das Token wird nur beim Verbinden geprüft.
## 🏛️ 2. Hub-Architektur & Rooms
Der WebSocket-Hub unterteilt Verbindungen in drei Abstraktionsebenen:
```
┌─────────────────────────────────────────────────────────────────┐
│ WebSocket Hub │
├─────────────────┬─────────────────────────────┬─────────────────┤
│ allClients │ userRooms │ rooms │
│ (Alle aktiven │ (Pro Benutzer-ID) │ (Pro Liste/ │
│ Verbindungen) │ Cross-Device Sync für │ Space) │
│ │ Tasks, Triage & Workspace │ Geteilte Liste │
└────────┬────────┴──────────────┬──────────────┴────────┬────────┘
▼ ▼ ▼
Presence Stats TASK_CREATED ITEM_TOGGLED
DOC_LOCKED TASK_UPDATED ITEM_CREATED
DOC_UNLOCKED DOC_UPDATED LIST_UPDATED
```
1. **`allClients` (Global):** Hält jede aktive Client-Verbindung. Ermöglicht Broadcasts systemweiter Ereignisse (z.B. globale Dokument-Sperrungen).
2. **`userRooms[userID]` (Benutzer-Räume):** Sendet Events an alle Geräte desselben Benutzers (z.B. Desktop und Smartphone). Erzeugt ein Webhook oder Quick Dump auf dem Smartphone einen Eintrag, taucht dieser zeitgleich auf dem Desktop auf.
3. **`rooms[listID]` (Kollaborations-Räume):** Multiplexing für geteilte Listen. Alle Mitglieder einer Liste empfangen Aktionen (wie Abhaken, Löschen, Umsortieren) in Echtzeit.
---
## 📨 3. Nachrichten-Format (Envelope)
Alle WebSocket-Nachrichten werden als JSON mit einheitlicher Hülle übertragen:
## Nachrichtenformat
```json
{
"event": "ITEM_TOGGLED",
"listId": "lst_12345",
"payload": {
"itemId": "itm_987",
"isCompleted": true,
"completedBy": "usr_sandro",
"completedByName": "Sandro"
},
"timestamp": "2026-10-26T13:30:00Z"
}
{ "event": "TASK_UPDATED", "listId": "", "payload": { … }, "timestamp": "2026-10-27T12:00:00Z" }
```
---
Client → Server versteht der Server nur: `{"event":"JOIN_ROOM","room":"<id>"}` und `{"event":"LEAVE_ROOM","room":"<id>"}`. Räume heißen `doc:<docId>` für Notizen oder `<listId>` für geteilte Listen.
---
## Zustellung
## ⚡ 4. Granulare Workspace-Echtzeitsynchronisation (`/api/v1/ws/docs/{id}`)
| Kanal | Wer erhält es |
|---|---|
| Benutzer-Kanal (`BroadcastToUser`) | alle Verbindungen eines Benutzers |
| Raum (`BroadcastRoom`, `Broadcast`) | Verbindungen im Raum |
| Alle (`BroadcastToAll`) | **jede** Verbindung, auch unangemeldete |
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`:
## Ereignisse
```
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 |
| Ereignis | Auslöser | Kanal |
|---|---|---|
| `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. |
| `TASK_CREATED` | `POST /inbox` | Benutzer |
| `TASK_UPDATED` | `PATCH /thoughts/{id}` (Triage: Benutzer) | alle |
| `TASK_DELETED` | `DELETE /thoughts/{id}` | alle |
| `TASK_REORDERED` | Aufgaben umsortieren | alle |
| `TASK_LIST_UPDATED` / `TASK_LIST_DELETED` | Liste ändern, (un)teilen, löschen | alle |
| `DOC_CREATED`, `DOC_UPDATED`, `DOC_DELETED` | Notiz anlegen/ändern/löschen, Freigaben | berechtigte Benutzer |
| `DOC_BLOCKS_PATCHED` | `PATCH /documents/{id}/blocks` (`docId`, `updatedBy`, `upserted`, `deletedIds`, `blockOrder`) | Raum `doc:<id>` und berechtigte Benutzer |
| `WORKSPACE_FOLDERS_UPDATED` | Ordner/Mounts ändern | Benutzer |
| `LIST_CREATED`, `LIST_UPDATED`, `LIST_DELETED`, `LIST_SHARED`, `LIST_UNSHARED`, `LIST_CLEARED` | Listenverwaltung | Benutzer bzw. Raum der Liste |
| `ITEM_CREATED`, `ITEM_UPDATED`, `ITEM_TOGGLED`, `ITEM_DELETED`, `ITEM_REORDERED` | Listeneinträge | Raum der Liste |
| `USER_PRESENCE` | Verbindung auf-/abgebaut (`onlineCount`) | Raum |
| `SSE_CONNECTED` | Nur SSE, direkt nach Verbindung | Verbindung |
```mermaid
sequenceDiagram
autonumber
actor UserA as Admin (Fenster A, cli_1)
participant Room as DocRoom (Go RAM)
actor UserB as User (Fenster B, cli_2)
participant DB as SQLite (blocks, block_tags, documents)
UserA->>Room: Connect (clientId="cli_1", userName="Admin")
Room-->>UserA: doc_state (title, blocks, activeCursors)
UserB->>Room: Connect (clientId="cli_2", userName="User")
Room-->>UserB: doc_state (title, blocks, activeCursors)
UserA->>Room: block_updated (clientId="cli_1", blockId="b1", content="Neu")
Note over Room: Mutiert RAM-Slice & resetet 1.5s Timer
Room-->>UserB: block_updated (clientId="cli_1", blockId="b1", content="Neu")
Note over UserA: Echo Filter: cli_1 erhält kein Selbstecho!
UserB->>Room: cursor_moved (clientId="cli_2", blockId="b1", selStart=3)
Room-->>UserA: cursor_moved (clientId="cli_2", blockId="b1")
Note over UserA: Farbiger Cursor-Avatar erscheint an b1
Note over Room: 1.5s Inaktivität ohne neue Tastenanschläge
Room->>DB: Debounce-Flush (blocks, Regex #tags -> block_tags, markdown_cache)
```
### 4.2 Strikter Echo-Schutz (`clientId`)
- Jeder Client generiert beim Start einen flüchtigen Schlüssel (`clientIdRef`, z. B. `cli_a8f9_l0z3`).
- Der Go-Hub filtert beim Broadcasten im Raum alle Clients heraus, deren Verbindung oder `clientId` mit dem Absender übereinstimmt:
```go
if evt.ClientID != "" && client.clientID == evt.ClientID {
continue
}
```
- **Vorteil:** Verhindert Cursor-Resets, Fokus-Verluste und Race Conditions durch Selbstechos während des lokalen Tippens.
### 4.3 1.5-Sekunden Debounce-Flush in SQLite
- Das Tippen im Editor erfolgt latenzfrei direkt im Arbeitsspeicher des `DocRoom`.
- Sobald 1,5 Sekunden lang keine neuen Tastenanschläge oder Mutationen eintreffen (oder wenn der letzte Client den Raum verlässt), stößt der Raum einen synchronisierten Flush an:
1. Persistiert alle Blöcke in `blocks`.
2. Extrahiert alle Hashtags (`#tag`) per Regex und aktualisiert `block_tags`.
3. Kompiliert die Blockliste (anhand der `level`-Tiefe) in Standard-Markdown und speichert sie in `documents.markdown_cache`.
---
## 🔒 5. Berechtigungen & Bearbeitungsrechte
- **Kollaboration ohne blockierende Locks:** Alle Nutzer mit Schreibrechten (`permission === 'owner'` oder `'write'`) können zeitgleich im selben Dokument tippen. Pessimistische Locks oder Lease-Heartbeats wurden vollständig entfernt.
- **Schreibschutz:** Verfügt ein Nutzer nur über Leserechte (`permission === 'read'`), ist das Eingabefeld gesperrt (`readOnly`). Er kann das Dokument jedoch in Echtzeit mitlesen und sieht die Cursors der Bearbeiter.
---
## 🔄 6. Fallback-Strategie & Proxy-Resilienz (Dual-Transport Architektur)
In restriktiven Firmennetzwerken, hinter Unternehmens-Proxies (z.B. Zscaler, BlueCoat) oder fehlerhaft konfigurierten Reverse-Proxies wird der `Upgrade: websocket`-Header häufig verworfen (`NS_ERROR_CONNECTION_REFUSED`, Code 1006 / 1002 / 1008).
MindSift löst dieses Problem durch eine **vollwertige Dual-Transport-Architektur** ([FLAW-03](../flaws.md)):
```
┌───────────────────────────────┐
│ MindSift Frontend │
└───────────────┬───────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
Standard: WebSockets Fallback: SSE + REST
(wss://.../api/v1/ws) (https://.../api/v1/events)
│ │
│ [Proxy Upgrade Block (Code 1006/Timeout)]│
│ ────────────────────────────────────────> │
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ Gorilla WS Conn │ │ Server-Sent Events │
│ Bidirektionaler │ │ (text/event-stream) │
│ TCP-WebSocket │ │ Standard HTTPS Port │
└────────────┬───────────┘ └────────────┬───────────┘
│ │
└─────────────────────┬─────────────────────┘
▼
┌─────────────────────────────┐
│ Realtime Hub │
│ (Multiplexing WS & SSE) │
└─────────────────────────────┘
```
### 1. Automatischer SSE-Fallback (`GET /api/v1/events`)
- Scheitert der WebSocket-Handshake oder blockiert ein Proxy das Upgrade, wechselt MindSift innerhalb von 3,5 Sekunden automatisch auf einen **Server-Sent Events (SSE) Stream**.
- Da SSE ein standardmäßiger HTTP/1.1- bzw. HTTP/2-GET-Request über Standard-Port 443/80 ist (`Content-Type: text/event-stream`), wird er von jedem Proxy und jeder Firewall ungehindert durchgeleitet.
- Header `X-Accel-Buffering: no` und `Cache-Control: no-cache` verhindern Pufferung im Proxy; periodische Ping-Kommentare (`: ping\n\n`) alle 15 Sekunden halten Timeouts fern.
- Der Client bleibt dadurch **vollständig echtzeitfähig** (`Live (SSE)`).
### 2. REST-Endpunkte für Upstream-Aktionen im SSE-Fallback-Modus
Da SSE unidirektional (Server ➔ Client) ist, stellt MindSift für Schreibvorgänge im SSE-Modus REST-Äquivalente bereit:
- `PATCH /api/v1/documents/:id/blocks`: Akzeptiert Block-Mutationen (`upserted`, `deletedIds`, `blockOrder`) und synchronisiert den In-Memory-Raum sowie SQLite.
- `PUT /api/v1/documents/:id`: Aktualisiert Dokumenttitel und Metadaten.
- `POST /api/v1/events/rooms`: Erlaubt SSE-Clients, Räumen dynamisch beizutreten (`join`) oder sie zu verlassen (`leave`).
### 3. Verbindungs-Indikator
- **Grün pulsierend:** Live verbunden über WebSockets (`Live (WebSocket)`).
- **Türkis pulsierend:** Live verbunden über Server-Sent Events (`Live (Server-Sent Events / Proxy-Stream)`).
- **Gelb/Amber:** Temporäre Unterbrechung mit automatischem HTTP-Polling-Schutz.
Ausführliche Konfigurationsbeispiele für Nginx, Traefik, Caddy, Cloudflare und Apache finden sich im separaten Handbuch:
👉 **[Reverse-Proxy-Konfigurationshandbuch](../reverse-proxy.md)**
## Verhalten im Frontend
- Die globale Verbindung (ein Singleton) leitet jede Nachricht als Browser-Ereignis `mindsift:ws` weiter; Ansichten (`TasksView`, `WorkspaceView`, `SharedListsView`) hören darauf und laden bzw. patchen ihren Zustand.
- Nach einem Abbruch verbindet sich der Client mit Verzögerung neu.
- Weil Notizen nur vom Eigentümer geschrieben werden, dient `DOC_BLOCKS_PATCHED` vor allem dazu, Lesenden und weiteren Geräten des Eigentümers Änderungen zu zeigen. Es gibt keine Konfliktauflösung (letzter Schreiber gewinnt).