MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s
349 lines
15 KiB
Markdown
349 lines
15 KiB
Markdown
# 🗄️ Datenbank-Design & SQLite Schema
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 📊 1. Entity-Relationship-Diagramm (ERD)
|
||
|
||
```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"
|
||
|
||
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"
|
||
|
||
task_lists ||--o{ thoughts : "gruppiert"
|
||
task_lists ||--o{ task_list_shares : "wird geteilt mit"
|
||
|
||
lists ||--o{ list_items : "enthält"
|
||
lists ||--o{ list_shares : "wird geteilt mit"
|
||
|
||
documents ||--o{ document_blocks : "besteht aus"
|
||
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"
|
||
|
||
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"
|
||
```
|
||
|
||
---
|
||
|
||
## 📑 2. Detaillierte Tabellen-Spezifikation
|
||
|
||
### 2.1 Benutzerverwaltung, System & Sicherheit
|
||
|
||
#### `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
|
||
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);
|
||
```
|
||
|
||
#### `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
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 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` & `document_blocks` (Database-First Outliner)
|
||
Dokumente werden als Kopfdatensatz in `documents` gespeichert, während der Inhalt als feingranulare Blöcke in `document_blocks` verwaltet wird. Zusätzlich spiegelt MindSift die Daten synchron als physische `.md`-Datei auf Festplatte.
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS documents (
|
||
id TEXT PRIMARY KEY, -- Präfix "doc_..."
|
||
user_id TEXT NOT NULL, -- Eigentümer
|
||
title TEXT NOT NULL, -- Dokumenttitel
|
||
icon TEXT NOT NULL DEFAULT '📄', -- Emoji-Icon
|
||
content TEXT NOT NULL DEFAULT '', -- Markdown-Volltext (Spiegelung)
|
||
blocks_json TEXT NOT NULL DEFAULT '[]', -- Legacy JSON-Cache
|
||
tags TEXT NOT NULL DEFAULT '[]', -- JSON Array von Tags
|
||
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_archived INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL,
|
||
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_shared_folder ON documents(shared_folder_id);
|
||
|
||
CREATE TABLE IF NOT EXISTS document_blocks (
|
||
id TEXT PRIMARY KEY, -- Präfix "blk_..."
|
||
doc_id TEXT NOT NULL,
|
||
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
|
||
level INTEGER NOT NULL DEFAULT 0, -- Hierarchieebene (0 = Root, 1 = Kind...)
|
||
checked INTEGER NOT NULL DEFAULT 0, -- Für Checklisten (0 = offen, 1 = abgehakt)
|
||
collapsed INTEGER NOT NULL DEFAULT 0, -- Für eingeklappte Unterbäume
|
||
lang TEXT NOT NULL DEFAULT '', -- Programmiersprache für Codeblöcke
|
||
updated_at TEXT NOT NULL,
|
||
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);
|
||
```
|
||
|
||
#### 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.
|