MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 50s
Phase 1 of the collaboration plan (server side):
- internal/collab: block tree with parent IDs and fractional order keys,
operations with conflict rules (version check, block leases, conflict
copies instead of lost text, restore on late edits, cycle checks),
in-memory leases/sessions and long-poll notifier.
- Storage: per-note lock + transaction, append-only effect log (doc_ops),
idempotent results per opId (doc_op_results), snapshot, log paging,
central DocRole. Old flat block data is converted to the tree.
- The existing whole-document save (PUT/PATCH) runs as an adapter through
the same engine, so the current editor keeps working and writes to the log.
- API: GET /docs/{id}/snapshot, POST /docs/{id}/sync, GET /docs/{id}/changes
(long poll), POST /docs/{id}/leave; DOC_SEQ poke for WebSocket/SSE rooms.
- Backend accepts editor (write) shares for notes and folders; the forced
read-only migration is gone. The share dialog still offers read only.
- Tests: conflict matrix, order keys, leases, storage adapter round trip,
idempotency, API protocol incl. long poll, and a randomized simulation
(4 clients, lost/duplicated requests and responses, offline phases,
lease expiry) checking convergence, no lost text and exactly-once results.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
6.5 KiB
Markdown
86 lines
6.5 KiB
Markdown
# Datenbank
|
|
|
|
SQLite-Datei `mindsift.db` (Standard: `$DATA_DIR/mindsift.db`), geöffnet mit `journal_mode=WAL`, `synchronous=NORMAL`, `foreign_keys=ON`, `busy_timeout=10000` und höchstens 8 Verbindungen. Das Schema entsteht in `Store.migrate()` ([sqlite.go](../internal/storage/sqlite.go)): zuerst `CREATE TABLE IF NOT EXISTS`, danach eine Liste idempotenter `ALTER TABLE`/`CREATE`-Anweisungen für ältere Datenbanken. IDs sind Text mit Präfix (`usr_…`, `th_…`, `tkl_…`, `lst_…`, `tls_…`, `blk_…`, `doc_…`).
|
|
|
|
Die Datei `migrations/001_init.sql` wird vom Code **nicht** gelesen und ist veraltet ([FLAW-13](flaws.md)).
|
|
|
|
## Tabellen
|
|
|
|
### Benutzer und Zugang
|
|
| Tabelle | Zweck | Wichtige Spalten |
|
|
|---|---|---|
|
|
| `users` | Konten | `email` (eindeutig), `username`, `password_hash`, `display_name`, `role` (`admin`/`member`), `is_active`, `token_version`, `alarm_channels` (kommagetrennt), `alarm_email`, `discord_webhook_url`, `custom_webhook_url`, `notify_before_minutes`, `avatar_url` |
|
|
| `user_invites` | Einladungscodes | `code`, `email`, `role`, `team_id`, `expires_at`, `used_by` |
|
|
| `revoked_tokens` | Sperrliste ausgeloggter JWTs | `token_hash`, `expires_at` |
|
|
| `api_keys` | Webhook-/API-Schlüssel | `key_prefix`, `key_hash` (SHA-256), `scopes` (nicht ausgewertet), `last_used_at` |
|
|
| `system_settings` | Schlüssel/Wert für Instanz-Einstellungen | `sharing_mode`, `allow_registration`, `instance_name`, `smtp_*` |
|
|
|
|
### Gedanken und Aufgaben
|
|
| Tabelle | Zweck | Wichtige Spalten |
|
|
|---|---|---|
|
|
| `thoughts` | Gedanken **und** Aufgaben in einer Tabelle | `status` (`inbox`/`kept`/`converted`/`discarded`), `converted_type` (`task`/`document`/`list_item`/`document_block`), `converted_ref_id`, `priority`, `is_completed`, `due_date` (nur Datum), `task_list_id`, `kanban_status`, `notes`, `subtasks` (JSON), `tags` (JSON), `alarm_sent`, `sort_order`, `source` (`manual`, `voice`, `webhook` …) |
|
|
| `task_lists` | Aufgabenlisten | `user_id` (Eigentümer), `team_id` (Altfeld), `title`, `icon`, `color`, `columns` (JSON, Kanban-Spalten) |
|
|
| `task_list_shares` | Freigaben von Aufgabenlisten | `target_type` (`team`/`user`), `target_id`, **`permission`** (`editor`/`viewer`, Standard `editor`) |
|
|
|
|
Eine Aufgabe ist ein `thought` mit `status='kept'` oder `status='converted' AND converted_type='task'`. Aufgaben, die aus einem Notiz-Block entstehen, tragen `converted_type='document_block'` und `converted_ref_id='<docId>:<blockId>'`.
|
|
|
|
### Workspace
|
|
| Tabelle | Zweck |
|
|
|---|---|
|
|
| `documents` | Notizen: `title`, `icon`, `folder_path`, `tags` (JSON), `is_archived`, `markdown_cache`, `shared_folder_id`, `sub_folder_path`, Altspalten `content`, `blocks_json` |
|
|
| `blocks` | Outliner-Blöcke als Baum: `document_id`, `parent_id` (`''` = oberste Ebene), `order_key` (Sortierschlüssel unter Geschwistern), `type`, `content`, `checked`, `collapsed`, `lang`, `version` (Textversion), `conflict_of`/`conflict_author` (Konfliktkopien), `updated_by`, `deleted` (Grabstein). `level`/`position` sind Altspalten und werden nicht mehr gepflegt; Ebene und Reihenfolge berechnet der Server beim Lesen aus dem Baum. |
|
|
| `doc_ops` | Änderungs-Log je Notiz: `seq` (fortlaufend pro Notiz), `op_id`, `user_id`, `client_id`, `kind`, `block_id`, `payload` (Effekt als JSON). Abspielen aller Einträge ergibt den Stand. |
|
|
| `doc_op_results` | Ergebnis je Client-Operation (`op_id` eindeutig) für idempotente Wiederholungen |
|
|
| `block_tags` | `#Hashtags` je Block (`tag`, `document_id`, `block_id`) |
|
|
| `tag_pages` | Freitext-Notiz je Benutzer und Tag |
|
|
| `doc_pins` | Favoriten je Benutzer |
|
|
| `doc_shares`, `doc_team_shares` | Notiz-Freigaben an Benutzer bzw. Gruppen (`permission`: `read` = Leser, `write` = Bearbeiter) |
|
|
| `workspace_folders` | Explizit angelegte Ordnerpfade |
|
|
| `workspace_shared_folders` | Geteilte Ordner (Entität mit Eigentümer) |
|
|
| `workspace_folder_shares` | Freigaben geteilter Ordner |
|
|
| `workspace_folder_mounts` | Persönlicher Einhängepfad je Empfänger und geteiltem Ordner |
|
|
| `doc_user_folders` | Persönliche Ablage einer geteilten Einzelnotiz je Empfänger |
|
|
|
|
Blocktypen: `h1`, `h2`, `h3`, `paragraph`, `text`, `todo`, `bullet`, `code`, `callout`, `divider`, `table`, `progress`.
|
|
|
|
### Listen
|
|
| Tabelle | Zweck |
|
|
|---|---|
|
|
| `lists` | Geteilte Listen: `title`, `icon`, `list_type` (`shopping`/`checklist`/`vacation`) |
|
|
| `list_shares` | Freigaben: `target_type`, `target_id`, `role` (`editor`/`viewer`) |
|
|
| `list_items` | Einträge: `content`, `category`, `quantity`, `is_completed`, `completed_by(_name)`, `sort_order` |
|
|
|
|
### Gruppen
|
|
| Tabelle | Zweck |
|
|
|---|---|
|
|
| `teams` | `name`, `description`, `org_id` (Standard `default`) |
|
|
| `team_members` | `team_id`, `user_id`, `role` (`admin`/`member`) |
|
|
| `organizations` | Rudimentär; wird vom UI nicht mehr benötigt ([FLAW-24](flaws.md)) |
|
|
|
|
## Beziehungen
|
|
|
|
```
|
|
users 1─n thoughts thoughts n─1 task_lists (task_list_id, lose)
|
|
users 1─n task_lists task_lists 1─n task_list_shares ─(team|user)→ teams/users
|
|
users 1─n documents 1─n blocks 1─n block_tags
|
|
documents n─n users via doc_shares / doc_team_shares (nur Lesen)
|
|
workspace_shared_folders 1─n workspace_folder_shares / workspace_folder_mounts
|
|
users 1─n lists 1─n list_items; lists 1─n list_shares
|
|
teams 1─n team_members n─1 users
|
|
```
|
|
|
|
`thoughts.task_list_id` ist keine Fremdschlüsselbeziehung; beim Löschen einer Liste setzt der Code die Verweise auf `''`.
|
|
|
|
## Migrationen und Aufräumarbeiten beim Start
|
|
|
|
- Spalten/Tabellen/Indizes werden nachgezogen (`ALTER TABLE … ADD COLUMN`, Fehler werden ignoriert).
|
|
- Veraltete Tabellen werden gelöscht: `spaces`, `space_members`, `space_team_shares`, `document_crdt_state`, `document_crdt_updates`, `document_blocks`, `doc_block_tags`.
|
|
- `users.username` wird aus dem E-Mail-Präfix aufgefüllt.
|
|
- Notiz-Synchronisation (`migrateDocSync`): neue Spalten an `blocks` und `documents` (`head_seq`, `title_version`), Tabellen `doc_ops` und `doc_op_results`. Blöcke im alten flachen Format (nur `level`/`position`) werden einmalig in den Baum umgewandelt.
|
|
- Standardwerte in `system_settings` (`INSERT OR IGNORE`): `sharing_mode=teams_only`, `allow_registration=false`, `instance_name=MindSift`, SMTP leer.
|
|
- Die Tabelle `teams` wird einmalig ohne Fremdschlüssel auf `organizations` neu aufgebaut.
|
|
|
|
## Sicherung
|
|
|
|
Die gesamte Instanz besteht aus der Datei `mindsift.db` (plus `-wal`/`-shm` im laufenden Betrieb). Konsistent sichern per `sqlite3 mindsift.db ".backup 'backup.db'"` oder durch Stoppen des Containers und Kopieren des `data/`-Verzeichnisses. Benutzerexport: `GET /api/v1/export` (JSON mit Gedanken, Notizen, Listen, Aufgabenlisten).
|