diff --git a/Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md b/Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md
deleted file mode 100644
index 5e4a0cb..0000000
--- a/Datenbank-Schema-%28KI-%C3%BCberpr%C3%BCfung-n%C3%B6tig%29.md
+++ /dev/null
@@ -1,491 +0,0 @@
-# Datenbank
-
-Die App verwendet **SQLite** (über `better-sqlite3`). Das Schema wird an drei Stellen aufgebaut:
-
-| Datei | Funktion | Aufgabe |
-|---|---|---|
-| `src/db/schema.js` | `initializeSchema(db)` | Legt die Grundtabellen an |
-| `src/db/schema.js` | `runMigrations(db, { dropLegacyChats })` | Ergänzt fehlende Spalten/Tabellen in bestehenden Datenbanken, migriert Altdaten |
-| `src/db/attachments.js` | `initializeAttachments(db)` | Tabellen und Trigger für Bild-Uploads |
-
-**Wichtig:** Das vollständige Schema ergibt sich erst aus allen drei Funktionen zusammen.
-Die Tabelle `chat_message_versions` und die Spalten `chat_messages.sources` und
-`chat_messages.thinking` existieren z. B. nur in `runMigrations`.
-
-## Allgemeine Konventionen
-
-- **`PRAGMA foreign_keys = ON`**: SQLite prüft Fremdschlüssel nur mit dieser Einstellung.
- Sie muss bei jeder Verbindung gesetzt werden.
-- **`PRAGMA journal_mode = WAL`**: Erlaubt gleichzeitiges Lesen während geschrieben wird.
-- **Booleans** werden als `INTEGER` gespeichert (`0` = false, `1` = true),
- z. B. `is_pinned`, `is_shared`.
-- **JSON-Daten** werden als `TEXT` gespeichert (z. B. `retry_variants`, `sources`,
- `custom_fields`, `tags`, `snapshot_json`) und im Code mit `JSON.parse` gelesen.
-- **Zeitstempel** werden mit `DEFAULT CURRENT_TIMESTAMP` in UTC gesetzt.
-- Spaltennamen in der DB sind `snake_case`, im JavaScript-Code `camelCase`
- (siehe z. B. `attachmentMetadata`).
-
-## Übersicht der Bereiche
-
-| Bereich | Tabellen |
-|---|---|
-| Benutzer | `users`, `user_settings` |
-| Chats | `chat_sessions`, `chat_messages`, `chat_message_versions`, `chat_memories`, `persona_chat_state` |
-| Personas | `personas`, `persona_versions`, `prompt_templates` |
-| Marktplatz | `persona_market`, `persona_market_favorites`, `persona_market_ratings`, `persona_market_reports` |
-| Anhänge | `attachments`, `message_attachments`, `attachment_vision_analyses`, `upload_deletions` |
-
-## Beziehungsdiagramm
-
-Legende: **durchgezogene Linie** = echter Fremdschlüssel (von SQLite geprüft),
-**gestrichelte Linie** = nur logische Beziehung (Spalte verweist auf eine andere Tabelle,
-aber ohne `FOREIGN KEY`, wird also nicht von der DB geprüft).
-
-```mermaid
-erDiagram
- users {
- TEXT username PK
- }
- user_settings {
- TEXT username PK, FK
- INTEGER active_persona_id FK
- INTEGER active_user_persona_id FK
- }
- chat_sessions {
- INTEGER id PK
- TEXT username
- INTEGER assistant_persona_id FK
- INTEGER user_persona_id FK
- INTEGER source_chat_id FK
- INTEGER branched_from_message_id FK
- INTEGER active_leaf_message_id
- }
- chat_messages {
- INTEGER id PK
- INTEGER chat_id FK
- INTEGER parent_message_id FK
- INTEGER retry_prompt_message_id
- }
- chat_message_versions {
- INTEGER id PK
- INTEGER message_id FK
- }
- chat_memories {
- INTEGER id PK
- TEXT username FK
- INTEGER chat_id FK
- INTEGER assistant_persona_id FK
- INTEGER user_persona_id FK
- }
- persona_chat_state {
- INTEGER id PK
- TEXT username FK
- INTEGER chat_id FK, UK
- INTEGER assistant_persona_id FK
- INTEGER user_persona_id FK
- }
- personas {
- INTEGER id PK
- TEXT username
- INTEGER source_market_id
- }
- persona_versions {
- INTEGER id PK
- INTEGER persona_id FK
- }
- prompt_templates {
- INTEGER id PK
- TEXT username
- INTEGER persona_id FK
- }
- persona_market {
- INTEGER id PK
- INTEGER persona_id
- TEXT creator_username
- }
- persona_market_favorites {
- INTEGER market_id PK, FK
- TEXT username PK
- }
- persona_market_ratings {
- INTEGER market_id PK, FK
- TEXT username PK
- }
- persona_market_reports {
- INTEGER id PK
- INTEGER market_id FK
- TEXT reporter_username
- }
- attachments {
- TEXT id PK
- TEXT username FK
- TEXT storage_key UK
- }
- message_attachments {
- INTEGER message_id PK, FK
- TEXT attachment_id PK, FK
- }
- attachment_vision_analyses {
- TEXT attachment_id PK, FK
- TEXT vision_model PK
- INTEGER prompt_version PK
- TEXT question PK
- }
- upload_deletions {
- TEXT storage_key PK
- }
-
- users ||--o| user_settings : "hat"
- users ||--o{ chat_memories : "besitzt"
- users ||--o{ persona_chat_state : "besitzt"
- users ||--o{ attachments : "lädt hoch"
- users ||..o{ chat_sessions : "besitzt"
- users ||..o{ personas : "besitzt"
- users ||..o{ persona_market : "veröffentlicht"
- users ||..o{ prompt_templates : "besitzt"
-
- chat_sessions ||--o{ chat_messages : "enthält"
- chat_sessions ||--o{ chat_memories : "hat"
- chat_sessions ||--o| persona_chat_state : "hat"
- chat_sessions |o--o{ chat_sessions : "Quelle für Branch"
- chat_messages |o--o{ chat_sessions : "Abzweigpunkt"
- chat_messages |o--o{ chat_messages : "Parent"
- chat_messages ||--o{ chat_message_versions : "hat Versionen"
- chat_messages ||--o{ message_attachments : "hat"
- attachments ||--o{ message_attachments : "wird verwendet in"
- attachments ||--o{ attachment_vision_analyses : "hat Analysen"
- attachments ||..o| upload_deletions : "Trigger"
-
- personas |o--o{ chat_sessions : "Assistant-/User-Persona"
- personas |o--o{ chat_memories : "bezieht sich auf"
- personas |o--o{ persona_chat_state : "bezieht sich auf"
- personas |o--o{ user_settings : "aktive Persona"
- personas ||--o{ persona_versions : "hat Versionen"
- personas ||--o{ prompt_templates : "hat"
- personas |o..o{ persona_market : "veröffentlicht als"
- persona_market |o..o{ personas : "importiert als"
- persona_market ||--o{ persona_market_favorites : "hat"
- persona_market ||--o{ persona_market_ratings : "hat"
- persona_market ||--o{ persona_market_reports : "hat"
-```
-
-### Lösch-Verhalten (ON DELETE)
-
-| Wird gelöscht | Folge |
-|---|---|
-| `users` | Löscht `user_settings`, `chat_memories`, `persona_chat_state`, `attachments` (CASCADE). **Nicht** automatisch: `chat_sessions`, `personas`, `prompt_templates`, Marktplatz-Einträge (kein FK) |
-| `chat_sessions` | Löscht alle `chat_messages`, `chat_memories`, `persona_chat_state` des Chats |
-| `chat_messages` | Löscht `chat_message_versions` und `message_attachments`. Kind-Nachrichten behalten ihren Platz, `parent_message_id` wird `NULL` |
-| `personas` | Löscht `persona_versions`, `prompt_templates`. In Chats, Memories und Settings wird der Verweis auf `NULL` gesetzt |
-| `persona_market` | Löscht Favoriten, Bewertungen und Meldungen |
-| `attachments` | Löscht `message_attachments`, `attachment_vision_analyses` und trägt die Datei in `upload_deletions` ein (Trigger) |
-
-## Tabellen
-
-### Benutzer
-
-#### `users`
-Registrierte Benutzer.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `username` | TEXT | **PK** |
-| `password` | TEXT | Passwort-Hash |
-| `session_version` | INTEGER | Default `0`. Wird erhöht, um alle Sessions eines Benutzers ungültig zu machen |
-
-#### `user_settings`
-Einstellungen pro Benutzer (1:1 zu `users`).
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `username` | TEXT | **PK**, FK → `users` (CASCADE) |
-| `active_persona_id` | INTEGER | FK → `personas` (SET NULL), aktive Assistant-Persona |
-| `active_user_persona_id` | INTEGER | FK → `personas` (SET NULL), aktive User-Persona |
-
-### Chats
-
-#### `chat_sessions`
-Ein Chat bzw. eine Unterhaltung.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK**, Autoincrement |
-| `username` | TEXT | Besitzer (logisch → `users`, kein FK) |
-| `title` | TEXT | Chat-Titel |
-| `assistant_persona_id` | INTEGER | FK → `personas` (SET NULL) |
-| `user_persona_id` | INTEGER | FK → `personas` (SET NULL) |
-| `folder_name` | TEXT | Ordner in der Seitenleiste |
-| `is_pinned` | INTEGER | Boolean, Default `0` |
-| `archived_at` | DATETIME | `NULL` = nicht archiviert |
-| `scenario_prompt` | TEXT | Szenario-Beschreibung |
-| `scenario_summary` | TEXT | Kurzfassung des Szenarios |
-| `context_summary` | TEXT | Automatische Zusammenfassung des bisherigen Verlaufs |
-| `context_summary_message_id` | INTEGER | Bis zu welcher Nachricht die Zusammenfassung reicht. Default `0` |
-| `active_leaf_message_id` | INTEGER | Letzte Nachricht des aktuell sichtbaren Zweigs (logisch → `chat_messages`) |
-| `source_chat_id` | INTEGER | FK → `chat_sessions` (SET NULL), Ursprungs-Chat bei Branches |
-| `branched_from_message_id` | INTEGER | FK → `chat_messages` (SET NULL), Nachricht, an der abgezweigt wurde |
-| `preferred_model` | TEXT | Bevorzugtes Modell für diesen Chat |
-| `temperature` | REAL | Modell-Parameter |
-| `context_length` | INTEGER | Modell-Parameter |
-| `response_length` | INTEGER | Modell-Parameter |
-| `system_instruction` | TEXT | Zusätzliche System-Anweisung |
-| `created_at`, `updated_at` | DATETIME | |
-
-#### `chat_messages`
-Einzelne Nachrichten. Nachrichten bilden über `parent_message_id` einen **Baum**, damit
-Verzweigungen (z. B. durch Regenerieren oder Bearbeiten) möglich sind.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK**, Autoincrement |
-| `chat_id` | INTEGER | FK → `chat_sessions` (CASCADE) |
-| `role` | TEXT | z. B. `user`, `bot` |
-| `content` | TEXT | Nachrichtentext |
-| `thinking` | TEXT | Reasoning-Ausgabe des Modells (nur via Migration) |
-| `sources` | TEXT | Websuche-Quellen als JSON (nur via Migration) |
-| `model_name` | TEXT | Verwendetes Modell |
-| `retry_variants` | TEXT | Alternative Antworten als JSON |
-| `retry_active_index` | INTEGER | Aktuell angezeigte Variante. Default `0` |
-| `retry_retries_used` | INTEGER | Anzahl genutzter Wiederholungen. Default `0` |
-| `retry_prompt_message_id` | INTEGER | Zugehörige User-Nachricht (logisch → `chat_messages`) |
-| `delivery_status` | TEXT | Default `'complete'` |
-| `error_message` | TEXT | Fehlertext bei fehlgeschlagener Antwort |
-| `parent_message_id` | INTEGER | FK → `chat_messages` (SET NULL), Vorgänger im Baum |
-| `created_at` | DATETIME | |
-
-#### `chat_message_versions`
-Frühere Fassungen einer Nachricht (z. B. nach Bearbeiten oder Regenerieren).
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `message_id` | INTEGER | FK → `chat_messages` (CASCADE), indiziert |
-| `content`, `thinking`, `sources`, `model_name` | TEXT | Stand der Nachricht zu diesem Zeitpunkt |
-| `created_at` | DATETIME | |
-
-#### `chat_memories`
-Fakten, die sich das Modell innerhalb eines Chats merkt.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `username` | TEXT | FK → `users` (CASCADE) |
-| `chat_id` | INTEGER | FK → `chat_sessions` (CASCADE) |
-| `assistant_persona_id`, `user_persona_id` | INTEGER | FK → `personas` (SET NULL) |
-| `fact` | TEXT | Der gemerkte Fakt |
-| `created_at`, `updated_at` | DATETIME | |
-
-#### `persona_chat_state`
-Rollenspiel-Zustand eines Chats (1:1 zu `chat_sessions`, wegen `UNIQUE` auf `chat_id`).
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `username` | TEXT | FK → `users` (CASCADE) |
-| `chat_id` | INTEGER | FK → `chat_sessions` (CASCADE), **UNIQUE** |
-| `assistant_persona_id`, `user_persona_id` | INTEGER | FK → `personas` (SET NULL) |
-| `relationship_notes`, `tone`, `current_location`, `goals`, `unresolved_threads`, `boundaries` | TEXT | Zustand der Unterhaltung |
-| `created_at`, `updated_at` | DATETIME | |
-
-### Personas
-
-#### `personas`
-Charaktere, entweder für den Assistenten oder für den Benutzer selbst.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `username` | TEXT | Besitzer (logisch → `users`, kein FK) |
-| `name` | TEXT | |
-| `pronouns`, `appearance`, `background`, `details` | TEXT | Beschreibung |
-| `custom_fields` | TEXT | Eigene Felder als JSON |
-| `persona_type` | TEXT | `'assistant'` (Default) oder User-Persona |
-| `source_market_id` | INTEGER | Falls aus dem Marktplatz importiert (logisch → `persona_market`) |
-| `source_creator_username` | TEXT | Ursprünglicher Ersteller |
-| `example_dialogues` | TEXT | Beispieldialoge |
-| `created_at`, `updated_at` | DATETIME | |
-
-#### `persona_versions`
-Versionsverlauf einer Persona.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `persona_id` | INTEGER | FK → `personas` (CASCADE) |
-| `username` | TEXT | Wer die Version erstellt hat |
-| `version_number` | INTEGER | Fortlaufende Nummer |
-| `snapshot_json` | TEXT | Vollständiger Stand der Persona als JSON |
-| `created_at` | DATETIME | |
-
-#### `prompt_templates`
-Wiederverwendbare Prompt-Vorlagen.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `username` | TEXT | Besitzer, darf `NULL` sein (z. B. System-Vorlagen) |
-| `persona_id` | INTEGER | FK → `personas` (CASCADE), optional |
-| `name`, `description`, `category` | TEXT | |
-| `prompt_text` | TEXT | Der eigentliche Prompt |
-| `starter_text` | TEXT | Optionaler Einstiegstext |
-| `is_shared` | INTEGER | Boolean, Default `0` |
-| `created_at`, `updated_at` | DATETIME | |
-
-### Marktplatz
-
-#### `persona_market`
-Öffentlich geteilte Personas. Eine Kopie der Persona, damit spätere Änderungen am Original
-den Marktplatz-Eintrag nicht automatisch verändern.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `persona_id` | INTEGER | Original-Persona (logisch → `personas`, kein FK) |
-| `creator_username` | TEXT | Ersteller |
-| `name`, `pronouns`, `appearance`, `background`, `details`, `example_dialogues` | TEXT | Kopie der Persona-Daten |
-| `persona_type` | TEXT | Default `'assistant'` |
-| `tags` | TEXT | Schlagwörter |
-| `usage_count` | INTEGER | Wie oft importiert. Default `0` |
-| `favorite_count` | INTEGER | Zwischengespeicherte Anzahl Favoriten |
-| `rating_total`, `rating_count` | INTEGER | Summe und Anzahl der Bewertungen (Durchschnitt = total / count) |
-| `moderation_status` | TEXT | Default `'approved'` |
-| `moderation_notes` | TEXT | |
-| `soft_deleted_at` | DATETIME | `NULL` = sichtbar. Einträge werden nicht wirklich gelöscht |
-| `created_at`, `updated_at` | DATETIME | |
-
-**UNIQUE** (`persona_id`, `creator_username`): Eine Persona kann pro Ersteller nur einmal veröffentlicht werden.
-
-#### `persona_market_favorites`
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `market_id` | INTEGER | **PK**, FK → `persona_market` (CASCADE) |
-| `username` | TEXT | **PK** |
-| `created_at` | DATETIME | |
-
-Zusammengesetzter PK: Jeder Benutzer kann einen Eintrag nur einmal favorisieren.
-
-#### `persona_market_ratings`
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `market_id` | INTEGER | **PK**, FK → `persona_market` (CASCADE) |
-| `username` | TEXT | **PK** |
-| `rating` | INTEGER | Bewertung |
-| `created_at`, `updated_at` | DATETIME | |
-
-Zusammengesetzter PK: Eine Bewertung pro Benutzer und Eintrag (kann aktualisiert werden).
-
-#### `persona_market_reports`
-Meldungen von Benutzern zur Moderation.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | INTEGER | **PK** |
-| `market_id` | INTEGER | FK → `persona_market` (CASCADE) |
-| `reporter_username` | TEXT | |
-| `reason` | TEXT | |
-| `details` | TEXT | |
-| `status` | TEXT | Default `'open'` |
-| `created_at`, `updated_at` | DATETIME | |
-
-### Anhänge
-
-#### `attachments`
-Hochgeladene Bilder. Die Datei selbst liegt im Upload-Verzeichnis, die DB speichert nur Metadaten.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `id` | TEXT | **PK** |
-| `username` | TEXT | FK → `users` (CASCADE, auch ON UPDATE) |
-| `original_name` | TEXT | Ursprünglicher Dateiname |
-| `storage_key` | TEXT | **UNIQUE**, Dateiname im Speicher |
-| `mime_type` | TEXT | |
-| `size_bytes`, `width`, `height` | INTEGER | |
-| `status` | TEXT | `'temporary'` (Default) oder `'attached'` (per CHECK erzwungen) |
-| `created_at` | TEXT | |
-| `vision_model`, `vision_analysis`, `vision_analyzed_at` | TEXT | Einzelne Bildanalyse (siehe auch `attachment_vision_analyses`) |
-
-#### `message_attachments`
-Verknüpfungstabelle Nachricht ↔ Anhang (n:m).
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `message_id` | INTEGER | **PK**, FK → `chat_messages` (CASCADE) |
-| `attachment_id` | TEXT | **PK**, FK → `attachments` (CASCADE), indiziert |
-| `position` | INTEGER | Reihenfolge der Bilder in der Nachricht |
-
-#### `attachment_vision_analyses`
-Zwischengespeicherte Bildanalysen. Pro Bild, Modell, Prompt-Version und Frage wird
-das Ergebnis nur einmal berechnet.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `attachment_id` | TEXT | **PK**, FK → `attachments` (CASCADE) |
-| `vision_model` | TEXT | **PK** |
-| `prompt_version` | INTEGER | **PK** |
-| `question` | TEXT | **PK** |
-| `analysis` | TEXT | Ergebnis |
-| `analyzed_at` | TEXT | |
-
-#### `upload_deletions`
-Warteschlange für Dateien, die vom Speicher gelöscht werden müssen.
-
-| Spalte | Typ | Hinweis |
-|---|---|---|
-| `storage_key` | TEXT | **PK** |
-
-### Lebenszyklus eines Anhangs
-
-Die Datenbank kann keine Dateien löschen. Deshalb tragen Trigger die zu löschenden
-Dateien in `upload_deletions` ein, und die App räumt sie danach im Dateisystem auf.
-
-```mermaid
-flowchart TD
- A[Bild hochgeladen] --> B["attachments
status = temporary"]
- B -->|Nachricht gesendet| C["status = attached
+ Eintrag in message_attachments"]
- C -->|Nachricht oder Chat gelöscht| D[message_attachments-Zeile gelöscht]
- D -->|"Trigger release_message_attachment
(keine weiteren Verweise)"| E[attachments-Zeile gelöscht]
- B -->|temporärer Upload verworfen| E
- E -->|Trigger queue_attachment_file_delete| F["storage_key in upload_deletions"]
- F --> G[App löscht Datei vom Speicher]
-```
-
-**Trigger:**
-
-- **`release_message_attachment`**: Nach dem Löschen einer Verknüpfung wird der Anhang gelöscht,
- wenn er `attached` ist und von keiner anderen Nachricht mehr verwendet wird.
-- **`queue_attachment_file_delete`**: Nach dem Löschen eines Anhangs wird dessen
- `storage_key` in `upload_deletions` eingetragen.
-
-## Migrationen
-
-`runMigrations` ist so geschrieben, dass sie **beliebig oft** laufen kann:
-
-- `CREATE TABLE IF NOT EXISTS` legt nur fehlende Tabellen an.
-- `ensureColumn` liest die vorhandenen Spalten per `PRAGMA table_info` und führt
- `ALTER TABLE ... ADD COLUMN` nur aus, wenn die Spalte fehlt.
-- `UPDATE ... WHERE ... IS NULL` füllt Default-Werte für alte Zeilen nach.
-
-**Einmalige Datenmigrationen:**
-
-- **Nachrichtenbaum:** Wenn `parent_message_id` neu angelegt wurde, wird jede Nachricht mit
- ihrer Vorgängernachricht im selben Chat verknüpft (`LAG`-Fensterfunktion), und
- `active_leaf_message_id` wird auf die letzte Nachricht gesetzt.
-- **Alte `chats`-Tabelle:** Existiert die frühere Tabelle `chats` und ist `chat_messages`
- leer, werden alle alten Nachrichten pro Benutzer in einen Chat „Imported chat“ übernommen.
- Mit `dropLegacyChats = true` (Config `DROP_LEGACY_CHATS`) wird `chats` danach gelöscht.
-
-## Bekannte Auffälligkeiten
-
-- **Fehlende Fremdschlüssel auf `users`:** `chat_sessions`, `personas`, `prompt_templates`,
- `persona_versions` und die Marktplatz-Tabellen verweisen nur logisch auf `users`.
- Beim Löschen eines Benutzers bleiben diese Daten erhalten, sofern der Code sie nicht
- selbst löscht.
-- **Doppelte Tabellendefinitionen:** Mehrere Tabellen werden sowohl in `initializeSchema`
- als auch in `runMigrations` angelegt. Wegen `IF NOT EXISTS` ist das harmlos, muss aber
- bei Änderungen an beiden Stellen gepflegt werden.
-- **Zwei Orte für Bildanalysen:** `attachments.vision_analysis` und die Tabelle
- `attachment_vision_analyses`. Prüfen, ob die Spalten in `attachments` noch verwendet werden.
-- **Uneinheitliche Zeitstempel-Typen:** Meist `DATETIME`, in den Anhang-Tabellen `TEXT`.
- In SQLite technisch gleichwertig, aber inkonsistent.
-- **Bewertungen ohne Wertebereich:** `persona_market_ratings.rating` hat keinen `CHECK`,
- der gültige Bereich wird nur im Code geprüft.
\ No newline at end of file
diff --git a/Datenbank.md b/Datenbank.md
new file mode 100644
index 0000000..2a9c191
--- /dev/null
+++ b/Datenbank.md
@@ -0,0 +1,239 @@
+# Datenbank
+
+Die App speichert alle Daten in einer **SQLite**-Datenbank und greift über die Bibliothek
+`better-sqlite3` darauf zu.
+
+| Seite | Inhalt |
+|---|---|
+| **Datenbank** (diese Seite) | Überblick, Dateien, Startablauf, Grundlagen, Notfall-Hilfe |
+| [Datenbank-Schema](Datenbank-Schema) | Alle Tabellen, Beziehungen, Lösch-Verhalten, Nachrichtenbaum, Migrationen |
+| [Datenbank-Repositories](Datenbank-Repositories) | Alle Funktionen aus `repositories.js` zum Nachschlagen |
+
+Siehe auch: [TypeScript](TypeScript)
+
+---
+
+## Die Dateien auf einen Blick
+
+| Datei | Sprache | Exportiert | Aufgabe |
+|---|---|---|---|
+| `src/db/index.ts` | TypeScript | `createDatabase` | Öffnet die Datenbank und baut das Schema auf |
+| `src/db/schema.ts` | TypeScript | `initializeSchema`, `runMigrations`, `MigrationOptions` | Grundtabellen anlegen, ältere Datenbanken aktualisieren |
+| `src/db/attachments.ts` | TypeScript | `initializeAttachments`, `attachmentMetadata` | Tabellen und Trigger für Bild-Anhänge, Umwandlung für die API |
+| `src/db/repositories.js` | JavaScript | `createRepositories` | Alle Lese- und Schreibzugriffe der App (ca. 90 Funktionen) |
+
+## Wo die Datenbank in der App steht
+
+Die App ist in Schichten aufgebaut. Jede Schicht spricht nur mit der darunter.
+**SQL steht im laufenden Betrieb ausschließlich in `repositories.js`.**
+
+```mermaid
+flowchart TD
+ Browser["Browser"] --> Routes["routes/
HTTP-Anfragen, Eingabeprüfung"]
+ Routes --> Services["services/
Geschäftslogik"]
+ Services --> Repos["db/repositories.js
SQL-Abfragen"]
+ Repos --> DB[("SQLite
app.db")]
+ Start["App-Start"] -.-> Create["db/index.ts
createDatabase()"]
+ Create -.-> DB
+```
+
+## Startablauf
+
+```mermaid
+flowchart TD
+ A["createDatabase(config)"] --> B{"dbPath ist :memory: ?"}
+ B -- nein --> C["Ordner für die DB-Datei anlegen"]
+ B -- ja --> D
+ C --> D["new Database(dbPath)"]
+ D --> E["initializeSchema(db)
PRAGMAs + 13 Grundtabellen"]
+ E --> F["runMigrations(db, config)
fehlende Spalten, Altdaten"]
+ F --> G["initializeAttachments(db)
Anhang-Tabellen + Trigger"]
+ G --> H["Verbindung zurückgeben"]
+ H --> I["createRepositories(db, config)"]
+```
+
+**Die Reihenfolge ist wichtig:**
+
+- `runMigrations` erwartet, dass die Grundtabellen bereits existieren.
+- `initializeAttachments` verweist per Fremdschlüssel auf `users` und `chat_messages`,
+ diese müssen also vorher angelegt sein.
+
+## Die Dateien im Detail
+
+### `src/db/index.ts`
+
+```ts
+export function createDatabase(config: Config): Database.Database
+```
+
+Einstiegspunkt beim App-Start. Verwendet aus der Config:
+
+| Config-Feld | Verwendung |
+|---|---|
+| `dbPath` | Pfad zur Datenbankdatei. `":memory:"` = Datenbank nur im Arbeitsspeicher (für Tests, weg nach dem Schließen) |
+| `dropLegacyChats` | Wird an `runMigrations` weitergereicht |
+
+Der Ordner wird mit `path.dirname(config.dbPath)` angelegt, funktioniert also auch mit
+einem eigenen `DB_PATH` außerhalb von `data/`.
+
+`runMigrations(db, config)` bekommt die **ganze** Config, liest aber nur `dropLegacyChats`
+heraus. TypeScript erlaubt das, weil `Config` alle Felder von `MigrationOptions` enthält
+(strukturelle Typisierung).
+
+### `src/db/schema.ts`
+
+| Funktion | Aufgabe |
+|---|---|
+| `initializeSchema(db)` | Setzt die PRAGMAs und legt alle Grundtabellen mit `CREATE TABLE IF NOT EXISTS` an. Bestehende Tabellen werden **nicht** verändert |
+| `runMigrations(db, options)` | Ergänzt fehlende Spalten und Tabellen in älteren Datenbanken, übernimmt Altdaten. Details: [Datenbank-Schema → Migrationen](Datenbank-Schema#migrationen) |
+
+Enthaltene Typen:
+
+- `ColumnInfo`: eine Zeile aus `PRAGMA table_info(...)` (intern)
+- `MigrationOptions`: `{ dropLegacyChats?: boolean }` (exportiert)
+
+### `src/db/attachments.ts`
+
+| Funktion | Aufgabe |
+|---|---|
+| `initializeAttachments(db)` | Legt `attachments`, `message_attachments`, `attachment_vision_analyses`, `upload_deletions`, einen Index und zwei Trigger an |
+| `attachmentMetadata(row)` | Wandelt eine DB-Zeile (snake_case) in das API-Format (camelCase) um. Gibt nur öffentliche Felder zurück, **nie** `storage_key` oder `username` |
+
+```ts
+// Eingabe: Zeile aus der Datenbank
+interface AttachmentRow {
+ id: string; original_name: string; mime_type: string;
+ size_bytes: number; width: number; height: number;
+}
+
+// Ausgabe: an das Frontend
+interface AttachmentMetadata {
+ id: string; type: "image"; originalName: string; mimeType: string;
+ sizeBytes: number; width: number; height: number;
+}
+```
+
+`AttachmentRow` enthält nur die Spalten, die `attachmentMetadata` braucht. Eine vollständige
+Zeile aus `SELECT *` passt trotzdem, weil TypeScript zusätzliche Felder erlaubt.
+
+### `src/db/repositories.js`
+
+Die Datenzugriffsschicht (Repository-Pattern). `createRepositories(db, config)` gibt ein
+Objekt mit allen Funktionen zurück, die der Rest der App benutzt, z. B.
+`repositories.listChats(username)`.
+
+Vollständige Referenz: [Datenbank-Repositories](Datenbank-Repositories)
+
+## Konventionen
+
+| Thema | Konvention |
+|---|---|
+| Benutzer-Identität | `username` (TEXT) ist Primärschlüssel und wird in vielen Tabellen als Verweis gespeichert |
+| Booleans | `INTEGER` mit `0` = false, `1` = true (z. B. `is_pinned`, `is_shared`) |
+| JSON | Als `TEXT` gespeichert, im Code mit `JSON.parse` / `JSON.stringify` (z. B. `tags`, `sources`, `retry_variants`) |
+| Zeitstempel | `DEFAULT CURRENT_TIMESTAMP`, in UTC |
+| Namen | Datenbank: `snake_case`. JavaScript/API: meist `camelCase` |
+| Soft Delete | Marktplatz-Einträge werden über `soft_deleted_at` ausgeblendet statt gelöscht |
+
+## PRAGMA-Einstellungen
+
+`PRAGMA` ist ein SQLite-spezifischer Befehl, um Einstellungen der Datenbank selbst zu lesen oder zu setzen.
+
+| PRAGMA | Wo | Bedeutung |
+|---|---|---|
+| `journal_mode = WAL` | `initializeSchema` | Lesen bleibt während Schreibvorgängen möglich. Wird in der Datei gespeichert, es entstehen zusätzlich `app.db-wal` und `app.db-shm` |
+| `foreign_keys = ON` | `initializeSchema` | Ohne diese Einstellung ignoriert SQLite **alle** Fremdschlüssel und `ON DELETE CASCADE`. Gilt nur pro Verbindung |
+| `table_info(tabelle)` | `runMigrations` | Liefert die Spalten einer Tabelle, damit fehlende ergänzt werden können |
+
+## better-sqlite3 in Kürze
+
+`better-sqlite3` arbeitet **synchron**: Es gibt kein `await`, Abfragen blockieren kurz den Event-Loop.
+
+| Methode | Rückgabe | Verwendung |
+|---|---|---|
+| `db.prepare(sql)` | Statement | Abfrage einmal vorbereiten, danach beliebig oft ausführen. `?` sind Platzhalter (Schutz vor SQL-Injection) |
+| `stmt.get(...params)` | Objekt oder `undefined` | Eine Zeile lesen |
+| `stmt.all(...params)` | Array | Alle Zeilen lesen |
+| `stmt.run(...params)` | `{ changes, lastInsertRowid }` | INSERT, UPDATE, DELETE |
+| `db.exec(sql)` | – | Mehrere SQL-Befehle ohne Parameter ausführen (Schema) |
+| `db.transaction(fn)` | Funktion | Alle Befehle in `fn` gelingen gemeinsam oder werden gemeinsam zurückgerollt |
+| `db.pragma(sql)` | abhängig | PRAGMA ausführen |
+
+In TypeScript liefern `.get()` und `.all()` standardmäßig `unknown`. Den Ergebnistyp gibt man per Generic an:
+
+```ts
+db.prepare<[string], { count: number }>("SELECT COUNT(*) AS count FROM personas WHERE username = ?")
+// ^^^^^^^^ ^^^^^^^^^^^^^^^^^
+// Parameter eine Ergebniszeile
+```
+
+Der Typ der Verbindung heißt `Database.Database` (nicht nur `Database`, das ist der Konstruktor).
+
+---
+
+## Notfall-Hilfe
+
+### Wo liegt die Datenbank?
+
+Standard: `/data/app.db`. Änderbar über die Umgebungsvariable `DB_PATH`.
+Im WAL-Modus gehören die Dateien `app.db-wal` und `app.db-shm` dazu.
+
+### Datenbank ansehen
+
+Mit dem SQLite-Kommandozeilenprogramm:
+
+```bash
+sqlite3 data/app.db
+```
+
+```sql
+.tables -- alle Tabellen
+.schema chat_sessions -- Aufbau einer Tabelle
+PRAGMA table_info(personas); -- Spalten einer Tabelle
+PRAGMA foreign_key_check; -- verwaiste Verweise finden
+SELECT * FROM users;
+.quit
+```
+
+Alternativ grafisch: *DB Browser for SQLite*.
+
+### Backup
+
+**Vor jeder Änderung an Schema oder Migrationen ein Backup machen.**
+
+```bash
+sqlite3 data/app.db ".backup data/app-backup.db"
+```
+
+Nicht einfach nur `app.db` kopieren, solange die App läuft: Im WAL-Modus können
+Änderungen noch in `app.db-wal` stehen. `.backup` berücksichtigt das.
+
+### Typische Fehler
+
+| Fehlermeldung | Ursache | Lösung |
+|---|---|---|
+| `SqliteError: no such column: xyz` | Spalte fehlt in einer älteren Datenbank | `ensureColumn` in `runMigrations` ergänzen |
+| `SqliteError: no such table: xyz` | Tabelle wird nirgends angelegt oder Startreihenfolge falsch | `initializeSchema` prüfen, Reihenfolge in `createDatabase` prüfen |
+| `SqliteError: FOREIGN KEY constraint failed` | Verweis auf eine Zeile, die nicht existiert (z. B. Chat-ID eines gelöschten Chats) | IDs prüfen, `PRAGMA foreign_key_check;` ausführen |
+| `SqliteError: UNIQUE constraint failed` | Doppelter Eintrag (z. B. Benutzername existiert schon) | Vorher prüfen oder Fehler abfangen |
+| `SqliteError: database is locked` | Ein anderer Prozess schreibt gerade (z. B. offenes DB-Programm) | Andere Programme schließen, lange Transaktionen vermeiden |
+| `attempt to write a readonly database` / `unable to open database file` | Keine Schreibrechte auf Datei oder Ordner (oft Docker-Volume) | Rechte des `data/`-Ordners prüfen |
+
+### Checkliste: Neue Spalte hinzufügen
+
+1. Spalte im `CREATE TABLE` in `initializeSchema` ergänzen (für neue Datenbanken).
+2. `ensureColumn(...)` in `runMigrations` ergänzen (für bestehende Datenbanken).
+3. SELECT-Abfragen in `repositories.js` ergänzen. Bei `chat_sessions` an **vier** Stellen:
+ `listChatSessions`, `getChatSession`, `getChatSessionByParticipants`, `searchChats`.
+4. Falls die Spalte einen Benutzernamen enthält: `renameUserTransaction` und
+ `deleteUserTransaction` ergänzen.
+5. Wiki-Seite [Datenbank-Schema](Datenbank-Schema) aktualisieren.
+
+### Checkliste: Neue Tabelle hinzufügen
+
+1. `CREATE TABLE IF NOT EXISTS` in `initializeSchema` (bzw. `initializeAttachments` für Anhänge).
+2. Fremdschlüssel mit passendem `ON DELETE` festlegen.
+3. Hat die Tabelle eine `username`-Spalte **ohne** Fremdschlüssel auf `users`:
+ in `renameUserTransaction` und `deleteUserTransaction` eintragen.
+4. Statements und Funktionen in `repositories.js` ergänzen.
+5. Wiki aktualisieren (Tabelle, Diagramm, Lösch-Verhalten).
\ No newline at end of file