docs: rewrite documentation and README from current codebase, add flaw analysis
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
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:
1 parent
4e9c4ffa8c
commit
bc13235da6
14 files changed
+733
-1618
No files matched your search
+68
-358
@@ -1,373 +1,83 @@
|
||||
# 🗄️ Datenbank-Design & SQLite Schema
|
||||
# Datenbank
|
||||
|
||||
MindSift verwendet als persistente Datenspeicherung eine lokale SQLite-Datenbank (`/data/mindsift.db`). Dieses Dokument beschreibt das relationale Datenmodell, Tabellenstrukturen, Indizes, Fremdschlüsselbeziehungen und das Migrationskonzept.
|
||||
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)).
|
||||
|
||||
## 📊 1. Entity-Relationship-Diagramm (ERD)
|
||||
## Tabellen
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
users ||--o{ thoughts : "erstellt"
|
||||
users ||--o{ documents : "besitzt"
|
||||
users ||--o{ lists : "besitzt"
|
||||
users ||--o{ task_lists : "besitzt"
|
||||
users ||--o{ api_keys : "besitzt"
|
||||
users ||--o{ team_members : "ist Mitglied in"
|
||||
### 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_*` |
|
||||
|
||||
teams ||--o{ team_members : "besteht aus"
|
||||
teams ||--o{ task_list_shares : "erhält Zugriff"
|
||||
teams ||--o{ list_shares : "erhält Zugriff"
|
||||
teams ||--o{ doc_team_shares : "erhält Zugriff"
|
||||
teams ||--o{ workspace_folder_shares : "erhält Zugriff"
|
||||
### 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`) |
|
||||
|
||||
task_lists ||--o{ thoughts : "gruppiert"
|
||||
task_lists ||--o{ task_list_shares : "wird geteilt mit"
|
||||
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>'`.
|
||||
|
||||
lists ||--o{ list_items : "enthält"
|
||||
lists ||--o{ list_shares : "wird geteilt mit"
|
||||
### 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: `document_id`, `type`, `content`, `level`, `position` (REAL), `checked`, `collapsed`, `lang` |
|
||||
| `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` effektiv immer `read`) |
|
||||
| `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 |
|
||||
|
||||
documents ||--o{ blocks : "besteht aus"
|
||||
documents ||--o{ block_tags : "indiziert"
|
||||
blocks ||--o{ block_tags : "verknüpft"
|
||||
documents ||--o{ doc_shares : "geteilt mit Benutzern"
|
||||
documents ||--o{ doc_team_shares : "geteilt mit Teams"
|
||||
documents ||--o{ doc_user_folders : "eigene Platzierung"
|
||||
documents ||--o{ doc_pins : "Favorit von"
|
||||
Blocktypen: `h1`, `h2`, `h3`, `paragraph`, `text`, `todo`, `bullet`, `code`, `callout`, `divider`, `table`, `progress`.
|
||||
|
||||
workspace_shared_folders ||--o{ workspace_folder_shares : "wird geteilt mit"
|
||||
workspace_shared_folders ||--o{ workspace_folder_mounts : "eingehängt bei"
|
||||
workspace_shared_folders ||--o{ documents : "enthält"
|
||||
### 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 `''`.
|
||||
|
||||
## 📑 2. Detaillierte Tabellen-Spezifikation
|
||||
## Migrationen und Aufräumarbeiten beim Start
|
||||
|
||||
### 2.1 Benutzerverwaltung, System & Sicherheit
|
||||
- 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.
|
||||
- Alle Dokument-, Team- und Ordnerfreigaben werden auf `read` gesetzt (bei **jedem** Start, siehe [FLAW-17](flaws.md)).
|
||||
- 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.
|
||||
|
||||
#### `users`
|
||||
Verwaltet Konten, Rollen, Multi-Channel Alarmziele und Benutzerpräferenzen.
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY, -- Präfix "usr_..."
|
||||
email TEXT UNIQUE NOT NULL, -- E-Mail-Adresse
|
||||
username TEXT NOT NULL DEFAULT '', -- Eindeutiger Login-Name
|
||||
password_hash TEXT NOT NULL, -- bcrypt Hash
|
||||
display_name TEXT NOT NULL, -- Anzeigename
|
||||
avatar_url TEXT NOT NULL DEFAULT '', -- Profilbild URL oder leer
|
||||
role TEXT NOT NULL DEFAULT 'member', -- 'admin' | 'member'
|
||||
is_active INTEGER NOT NULL DEFAULT 1, -- 1 = aktiv, 0 = gesperrt
|
||||
token_version INTEGER NOT NULL DEFAULT 1, -- Erhöht bei Passwortwechsel/Rechteentzug zur Token-Invalidierung
|
||||
alarm_channels TEXT NOT NULL DEFAULT 'discord', -- Kommagetrennt: 'email', 'discord', 'webhook'
|
||||
alarm_email TEXT NOT NULL DEFAULT '', -- Ziel-E-Mail für Alarme
|
||||
discord_webhook_url TEXT NOT NULL DEFAULT '', -- Discord Webhook URL
|
||||
custom_webhook_url TEXT NOT NULL DEFAULT '', -- HTTP POST JSON Webhook URL
|
||||
notify_before_minutes INTEGER NOT NULL DEFAULT 30, -- Vorwarnzeit in Minuten
|
||||
created_at TEXT NOT NULL, -- ISO 8601 String
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
```
|
||||
## Sicherung
|
||||
|
||||
#### `system_settings`
|
||||
Globale Instanz-Konfiguration (Key-Value-Tabelle).
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
key TEXT PRIMARY KEY, -- 'sharing_mode', 'allow_registration', 'instance_name', 'smtp_...'
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
#### `user_invites`
|
||||
Einladungscodes für kontrollierte Benutzer-Registrierungen.
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS user_invites (
|
||||
id TEXT PRIMARY KEY, -- Präfix "inv_..."
|
||||
code TEXT UNIQUE NOT NULL, -- Format "MS-XXXXXXXX"
|
||||
email TEXT DEFAULT '', -- Optional: an bestimmte E-Mail gebunden
|
||||
role TEXT NOT NULL DEFAULT 'member', -- Rolle des neuen Kontos ('member', 'admin')
|
||||
created_by TEXT NOT NULL, -- Ersteller User-ID
|
||||
used_by TEXT DEFAULT '', -- Eingelöste User-ID
|
||||
expires_at TEXT NOT NULL, -- Gültig bis (ISO 8601)
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_invites_code ON user_invites(code);
|
||||
```
|
||||
|
||||
#### `api_keys`
|
||||
Persönliche API-Schlüssel für externe Skripte, Smarthome und Ingestion.
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY, -- Präfix "key_..."
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL, -- Bezeichnung (z.B. "Home Assistant")
|
||||
key_prefix TEXT NOT NULL, -- Erste 12 Zeichen im Klartext für Anzeige
|
||||
key_hash TEXT UNIQUE NOT NULL, -- SHA-256 Hash des kompletten Schlüssels
|
||||
scopes TEXT NOT NULL DEFAULT 'ingest,read',
|
||||
last_used_at TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
#### `revoked_tokens`
|
||||
Sperrliste für explizit abgemeldete JWTs (Blacklist bis zum regulären Token-Ablaufdatum).
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS revoked_tokens (
|
||||
token_hash TEXT PRIMARY KEY, -- SHA-256 Hash des Tokens
|
||||
user_id TEXT NOT NULL, -- Benutzer-ID
|
||||
expires_at TEXT NOT NULL, -- ISO 8601 Ablaufdatum
|
||||
revoked_at TEXT NOT NULL -- ISO 8601 Sperrzeitpunkt
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_revoked_tokens_expires ON revoked_tokens(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_revoked_tokens_user ON revoked_tokens(user_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Gruppen & Organisations-Teams
|
||||
|
||||
#### `organizations` & `teams` & `team_members`
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY, -- Präfix "org_..."
|
||||
name TEXT NOT NULL,
|
||||
owner_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id TEXT PRIMARY KEY, -- Präfix "tm_..."
|
||||
org_id TEXT NOT NULL DEFAULT 'default',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
team_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'member', -- 'admin' | 'member'
|
||||
joined_at TEXT NOT NULL,
|
||||
PRIMARY KEY (team_id, user_id),
|
||||
FOREIGN KEY(team_id) REFERENCES teams(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Gedanken, Triage & Aufgaben
|
||||
|
||||
In MindSift nutzen Quick Dumps (Triage) und Aufgaben dieselbe Basistabelle **`thoughts`**. Ein Quick Dump ist ein Gedanke mit `status = 'inbox'`, der durch Triage in `status = 'kept'` oder `status = 'converted'` übergeht.
|
||||
|
||||
#### `thoughts`
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS thoughts (
|
||||
id TEXT PRIMARY KEY, -- Präfix "th_..."
|
||||
user_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL, -- Textinhalt inkl. Hashtags
|
||||
tags TEXT NOT NULL DEFAULT '[]', -- JSON Array: ["smarthome", "keller"]
|
||||
priority TEXT NOT NULL DEFAULT 'normal', -- 'low' | 'normal' | 'high' | 'urgent'
|
||||
source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'voice' | 'webhook'
|
||||
status TEXT NOT NULL DEFAULT 'inbox', -- 'inbox' | 'kept' | 'discarded' | 'converted'
|
||||
is_completed INTEGER NOT NULL DEFAULT 0, -- 0 = offen, 1 = erledigt
|
||||
converted_type TEXT DEFAULT '', -- 'task' | 'document' | 'list_item'
|
||||
converted_ref_id TEXT DEFAULT '', -- Verknüpfte ID (z.B. doc_id oder list_item_id)
|
||||
due_date TEXT DEFAULT '', -- Fälligkeitsdatum "YYYY-MM-DD"
|
||||
task_list_id TEXT NOT NULL DEFAULT '', -- Zugeordnete Aufgabenliste ("" = Standard)
|
||||
kanban_status TEXT NOT NULL DEFAULT 'todo',-- 'todo' | 'in_progress' | 'done' | column.id
|
||||
notes TEXT NOT NULL DEFAULT '', -- Zusätzliche Notizen / Beschreibung
|
||||
subtasks TEXT NOT NULL DEFAULT '[]', -- JSON Array von SubTask: [{id, title, isCompleted}]
|
||||
alarm_sent INTEGER NOT NULL DEFAULT 0, -- 1 = Alarm wurde bereits versendet
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, -- Manuelle Reorder-Sortierung
|
||||
created_at TEXT NOT NULL,
|
||||
triaged_at TEXT DEFAULT '',
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_thoughts_user_status ON thoughts(user_id, status, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_thoughts_list_sort ON thoughts(task_list_id, sort_order ASC, created_at DESC);
|
||||
```
|
||||
|
||||
#### `task_lists` & `task_list_shares`
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS task_lists (
|
||||
id TEXT PRIMARY KEY, -- Präfix "tl_..."
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '📋',
|
||||
color TEXT NOT NULL DEFAULT '#3b82f6',
|
||||
team_id TEXT NOT NULL DEFAULT '', -- Optional: an Team gebunden
|
||||
columns TEXT NOT NULL DEFAULT '[]', -- JSON Array von Kanban-Spalten
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_list_shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_list_id TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')),
|
||||
target_id TEXT NOT NULL, -- Team-ID oder User-ID
|
||||
created_at TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
UNIQUE(task_list_id, target_type, target_id),
|
||||
FOREIGN KEY(task_list_id) REFERENCES task_lists(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Workspace: Dokumente, Blöcke & Geteilte Ordner
|
||||
|
||||
#### `documents`, `blocks` & `block_tags` (Database-First Outliner Schema)
|
||||
Dokumente werden als Kopfdatensatz in `documents` gespeichert, während der Inhalt als feingranulare Blöcke in `blocks` verwaltet wird. Hashtags (#tags) werden per Regex analysiert und in `block_tags` abgelegt. MindSift speichert alle Notizen zu 100% in SQLite (keine physischen `.md`-Dateien auf Festplatte).
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id TEXT PRIMARY KEY, -- Präfix "doc_..."
|
||||
user_id TEXT NOT NULL DEFAULT '', -- Eigentümer (usr_...)
|
||||
space_id TEXT DEFAULT '',
|
||||
title TEXT NOT NULL DEFAULT '', -- Dokumenttitel
|
||||
folder_id TEXT NOT NULL DEFAULT '', -- Ordner-Zuordnung
|
||||
markdown_cache TEXT NOT NULL DEFAULT '', -- Kompiliertes Standard-Markdown (automatischer Cache)
|
||||
icon TEXT NOT NULL DEFAULT '📄', -- Emoji-Icon
|
||||
content TEXT NOT NULL DEFAULT '', -- Plaintext / Text-Repräsentation
|
||||
blocks_json TEXT NOT NULL DEFAULT '[]', -- Legacy/Zusatz-Cache
|
||||
tags TEXT NOT NULL DEFAULT '[]', -- JSON Array von Tags
|
||||
folder_path TEXT NOT NULL DEFAULT '', -- Relativer Ordnerpfad des Eigentümers
|
||||
is_pinned INTEGER NOT NULL DEFAULT 0, -- Globaler Pin
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_user ON documents(user_id, is_archived, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
id TEXT PRIMARY KEY, -- Block-ID (z.B. "blk_..." oder "docId-b0")
|
||||
document_id TEXT NOT NULL, -- Zugehöriges Dokument (doc_...)
|
||||
content TEXT NOT NULL DEFAULT '', -- Textinhalt des Blocks
|
||||
level INT NOT NULL DEFAULT 0, -- Hierarchieebene (0 = Root, 1 = Kind...)
|
||||
position REAL NOT NULL DEFAULT 0, -- Fließkommazahl für konfliktfreies Einfügen/Sortieren
|
||||
type TEXT NOT NULL DEFAULT 'bullet', -- 'bullet', 'todo', 'h1', 'h2', 'h3', 'code', 'quote', 'callout', 'divider'
|
||||
checked INTEGER NOT NULL DEFAULT 0, -- Für Checklisten (0 = offen, 1 = abgehakt)
|
||||
collapsed INTEGER NOT NULL DEFAULT 0, -- Für eingeklappte Unterbäume (1 = zugeklappt)
|
||||
lang TEXT NOT NULL DEFAULT '', -- Programmiersprache für Codeblöcke
|
||||
FOREIGN KEY(document_id) REFERENCES documents(id) ON DELETE CASCADE
|
||||
);
|
||||
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`)
|
||||
Ermöglicht das Teilen kompletter Ordnerstrukturen. Verschiebt ein Empfänger den geteilten Ordner in seiner Hierarchie, wird **nur sein Mount-Pfad** aktualisiert – die Quellstruktur beim Eigentümer und anderen Empfängern bleibt unberührt!
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS workspace_shared_folders (
|
||||
id TEXT PRIMARY KEY, -- Präfix "wsf_..."
|
||||
owner_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_folder_shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
folder_id TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')),
|
||||
target_id TEXT NOT NULL,
|
||||
permission TEXT NOT NULL CHECK(permission IN ('read', 'write')),
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(folder_id, target_type, target_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_folder_mounts (
|
||||
user_id TEXT NOT NULL,
|
||||
folder_id TEXT NOT NULL,
|
||||
mount_path TEXT NOT NULL, -- Eigener lokaler Pfad des Benutzers (z.B. "Arbeit/Projekte/Team-Docs")
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY(user_id, folder_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS doc_user_folders (
|
||||
user_id TEXT NOT NULL,
|
||||
doc_id TEXT NOT NULL,
|
||||
folder_path TEXT NOT NULL DEFAULT '', -- Individuelle Ordnerplatzierung für geteilte Einzelnotizen
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY(user_id, doc_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Geteilte Listen & Listen-Einträge
|
||||
|
||||
#### `lists`, `list_items` & `list_shares`
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS lists (
|
||||
id TEXT PRIMARY KEY, -- Präfix "lst_..."
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🛒',
|
||||
list_type TEXT NOT NULL DEFAULT 'shopping',-- 'shopping', 'packing', 'inventory', 'todo'
|
||||
updated_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_lists_user ON lists(user_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
list_id TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL CHECK(target_type IN ('team', 'user')),
|
||||
target_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'editor' CHECK(role IN ('editor', 'viewer')),
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(list_id, target_type, target_id),
|
||||
FOREIGN KEY(list_id) REFERENCES lists(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS list_items (
|
||||
id TEXT PRIMARY KEY, -- Präfix "itm_..."
|
||||
list_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL, -- Bezeichnung des Eintrags
|
||||
category TEXT NOT NULL DEFAULT '', -- Regal/Kategorie (z.B. "Kühlregal")
|
||||
quantity TEXT NOT NULL DEFAULT '', -- Menge (z.B. "2x", "500g")
|
||||
is_completed INTEGER NOT NULL DEFAULT 0, -- 1 = abgehakt
|
||||
completed_by TEXT NOT NULL DEFAULT '', -- User-ID des Abhakers
|
||||
completed_by_name TEXT NOT NULL DEFAULT '',-- Name des Abhakers für Live-Badge
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
completed_at TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY(list_id) REFERENCES lists(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_list_items_sync ON list_items(list_id, is_completed, sort_order ASC);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 3. Migrationsstrategie
|
||||
|
||||
MindSift setzt auf **idempotente automatische In-Process-Migrationen** beim Serverstart ([`internal/storage/sqlite.go`](../internal/storage/sqlite.go)):
|
||||
1. **Schema-Bootstrap:** Ausführung des Basis-Schemas via `CREATE TABLE IF NOT EXISTS`.
|
||||
2. **Column & Index Migration Array:** Ein vordefiniertes Array idempotenter DDL-Befehle (z.B. `ALTER TABLE thoughts ADD COLUMN ...`, `CREATE TABLE IF NOT EXISTS doc_user_folders ...`).
|
||||
3. **Fehlerbehandlung:** Wenn Spalten bereits existieren (z.B. `"duplicate column name"`), wird der Fehler ignoriert und der Start fortgesetzt.
|
||||
4. **Vorteil:** Keine manuellen Migrations-Skripte nötig; Container-Updates können jederzeit ohne Downtime oder Vorbereitung über bestehende Datenbanken ausgerollt werden.
|
||||
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).
|
||||
Reference in new issue
Block a user