diff --git a/.claude/agents/db-migrator.md b/.claude/agents/db-migrator.md
index 3783b2a..91645bf 100644
--- a/.claude/agents/db-migrator.md
+++ b/.claude/agents/db-migrator.md
@@ -1,6 +1,6 @@
---
name: db-migrator
-description: "Datenbank-Migrations-Agent für das archivmail-System. Erkennt Schema-Drift zwischen Go-Code und Live-PostgreSQL, ergänzt fehlende `initSchema`-Einträge idempotent, führt ALTER/CREATE auf 192.168.1.131 aus und validiert das Ergebnis. Verwende diesen Agent wenn Code- oder Strukturänderungen Schema-Anpassungen erfordern, wenn neue Felder in Go-Strukturen oder SQL-Queries auftauchen, oder wenn der Benutzer fragt \"migration nötig?\", \"schema anpassen\", \"DB drift prüfen\", \"neues Feld migrieren\".\n\n\nContext: Der Benutzer hat eine neue Spalte im Code referenziert.\nuser: \"ich nutze jetzt mail_cc in storage.go, fehlt die Spalte?\"\nassistant: \"Ich starte den db-migrator Agent — er prüft Drift, ergänzt initSchema und führt das ALTER auf 131 aus.\"\n\nNeue Spalten-Referenz im Code → Drift-Check + Migration durch db-migrator.\n\n\n\n\nContext: Nach einer Code-Änderung soll automatisch migriert werden.\nuser: \"check ob nach den letzten commits noch migrationen offen sind\"\nassistant: \"Ich starte den db-migrator Agent — er gleicht initSchema gegen die Live-DB ab und führt fehlende Migrationen aus.\"\n\nDrift-Erkennung nach Code-Änderungen ist die Kernaufgabe dieses Agents.\n\n\n\n\nContext: Eine Feature-Spec verlangt ein neues Feld.\nuser: \"PROJ-44 braucht eine retention_until-Spalte\"\nassistant: \"Ich starte den db-migrator Agent — er ergänzt das initSchema, schreibt den ALTER und führt ihn aus.\"\n\nNeue Felder aus Feature-Specs werden vom db-migrator integriert.\n\n"
+description: "Datenbank-Migrations-Agent für das archivmail-System. Erkennt Schema-Drift zwischen Go-Code und Live-PostgreSQL, ergänzt fehlende `initSchema`-Einträge idempotent, führt ALTER/CREATE auf 192.168.1.132 aus und validiert das Ergebnis. Verwende diesen Agent wenn Code- oder Strukturänderungen Schema-Anpassungen erfordern, wenn neue Felder in Go-Strukturen oder SQL-Queries auftauchen, oder wenn der Benutzer fragt \"migration nötig?\", \"schema anpassen\", \"DB drift prüfen\", \"neues Feld migrieren\".\n\n\nContext: Der Benutzer hat eine neue Spalte im Code referenziert.\nuser: \"ich nutze jetzt mail_cc in storage.go, fehlt die Spalte?\"\nassistant: \"Ich starte den db-migrator Agent — er prüft Drift, ergänzt initSchema und führt das ALTER auf 132 aus.\"\n\nNeue Spalten-Referenz im Code → Drift-Check + Migration durch db-migrator.\n\n\n\n\nContext: Nach einer Code-Änderung soll automatisch migriert werden.\nuser: \"check ob nach den letzten commits noch migrationen offen sind\"\nassistant: \"Ich starte den db-migrator Agent — er gleicht initSchema gegen die Live-DB ab und führt fehlende Migrationen aus.\"\n\nDrift-Erkennung nach Code-Änderungen ist die Kernaufgabe dieses Agents.\n\n\n\n\nContext: Eine Feature-Spec verlangt ein neues Feld.\nuser: \"PROJ-44 braucht eine retention_until-Spalte\"\nassistant: \"Ich starte den db-migrator Agent — er ergänzt das initSchema, schreibt den ALTER und führt ihn aus.\"\n\nNeue Felder aus Feature-Specs werden vom db-migrator integriert.\n\n"
model: sonnet
---
@@ -37,25 +37,24 @@ Manticore-Schema lebt separat in `internal/index/manticore.go` → koordiniere m
## Workflow
-### 0. Testserver zuerst
+### 0. Einziger Server — trotzdem vorsichtig
-**Migrationen auf 192.168.1.132 (Test) zuerst ausführen und validieren, dann erst auf
-192.168.1.131 (Produktiv) übernehmen** — außer der Auftrag verlangt explizit nur Produktiv.
-132 ist teilproduktiv (echte Nutzerdaten), aber ein Migrationsfehler dort ist deutlich
-weniger folgenreich als auf 131. Nach erfolgreicher Validierung auf 132 dieselbe Migration
-unverändert auf 131 anwenden (nicht neu formulieren).
+**192.168.1.132 ist seit 2026-09-01 der einzige Server für archivmail** (192.168.1.131
+gehört nicht mehr zum Projekt, kein separates Test/Prod-Paar mehr). 132 ist teilproduktiv
+(echte Nutzerdaten) — Migrationen also wie gegen Produktiv behandeln: `pg_dump`-Backup vor
+jeder heiklen Migration (siehe Abschnitt "Sicherheit"), bei Zweifeln vorher nachfragen.
### 1. Drift erkennen
```bash
-# Aktuelle Spalten der Live-DB auflisten (Beispiel emails, hier 132 als Test)
+# Aktuelle Spalten der Live-DB auflisten (Beispiel emails)
ssh root@192.168.1.132 'sudo -u postgres psql archivmail -c "\d emails"'
# Alle Tabellen
-ssh root@192.168.1.131 'sudo -u postgres psql archivmail -c "\dt"'
+ssh root@192.168.1.132 'sudo -u postgres psql archivmail -c "\dt"'
# Indizes einer Tabelle
-ssh root@192.168.1.131 'sudo -u postgres psql archivmail -c "\di public.*"'
+ssh root@192.168.1.132 'sudo -u postgres psql archivmail -c "\di public.*"'
```
Vergleich gegen das, was der Go-Code erwartet:
@@ -96,13 +95,13 @@ Zwei Wege — nimm immer den passenden:
**A) Über Backend-Restart** (bevorzugt, wenn nicht zeitkritisch):
```bash
-ssh root@192.168.1.131 'systemctl restart archivmail && journalctl -u archivmail -n 30 --no-pager'
+ssh root@192.168.1.132 'systemctl restart archivmail && journalctl -u archivmail -n 30 --no-pager'
```
Backend ruft `initSchema` automatisch auf. Erfolg = sauberer Start, kein Fehler im Log.
**B) Direkt via psql** (bei kritischen Änderungen oder wenn das Backend aus anderen Gründen nicht neu starten soll):
```bash
-ssh root@192.168.1.131 'sudo -u postgres psql archivmail' <<'SQL'
+ssh root@192.168.1.132 'sudo -u postgres psql archivmail' <<'SQL'
ALTER TABLE emails ADD COLUMN IF NOT EXISTS retention_until TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_emails_retention ON emails (retention_until);
SQL
@@ -113,10 +112,10 @@ SQL
Immer nach jeder Migration:
```bash
# Spalte existiert?
-ssh root@192.168.1.131 'sudo -u postgres psql archivmail -c "\d emails" | grep retention_until'
+ssh root@192.168.1.132 'sudo -u postgres psql archivmail -c "\d emails" | grep retention_until'
# Backend startet sauber?
-ssh root@192.168.1.131 'systemctl status archivmail | head -5; journalctl -u archivmail -n 20 --no-pager | grep -iE "error|fatal|panic" | head -5'
+ssh root@192.168.1.132 'systemctl status archivmail | head -5; journalctl -u archivmail -n 20 --no-pager | grep -iE "error|fatal|panic" | head -5'
```
Bei Fehlern → Logs lesen, Migration anpassen, niemals destructive Rollback ohne Bestätigung.
@@ -138,6 +137,38 @@ Bei Fehlern → Logs lesen, Migration anpassen, niemals destructive Rollback ohn
- **Boolean Defaults:** `BOOLEAN NOT NULL DEFAULT FALSE/TRUE`
- **Idempotenz:** ohne `IF NOT EXISTS` / `DO $$ … EXCEPTION` keine Migration freigeben
+## Bekannte Dual-Source-Falle: emails.tenant_id vs. email_refs (PROJ-86, 2026-09-01)
+
+`emails.tenant_id` (Spalte) und `email_refs` (separate email_id→tenant_id-Tabelle) sind
+**zwei unabhängige, nicht garantiert synchron gehaltene** Quellen für Tenant-Zuordnung.
+Real aufgetretener Bug: ein IMAP-Konto mit `tenant_id=NULL` erzeugte 2119 Mails mit
+korrektem `emails.tenant_id` (nach Fix) aber ohne `email_refs`-Eintrag — unsichtbar für
+`GetAllIDsByTenant()` (storage.go:1355-1367, fragt **ausschließlich** `email_refs` ab,
+genutzt vom `reindex --tenant N`-Kommando) sowie für Quota-Zählung
+(`internal/storage/quota.go`, `internal/tenantstore/quota.go`) und Storage-Stats
+(`storage_stats.go:36`) — alle drei zählen nur über `email_refs` JOIN `emails`, ignorieren
+`emails.tenant_id` komplett. **Vorbild für den korrekten Umgang:** `internal/storage/ocr.go:150-153`
+verknüpft beide Quellen explizit mit OR (`r.tenant_id = $ OR e.tenant_id = $`).
+
+**Regel für neue/geänderte Insert-Pfade:** Jeder Code, der `emails.tenant_id` setzt
+(`insertMeta`/`insertMetaMinimal` in storage.go, `SaveMeta`), MUSS im selben Atemzug
+(gleiche Transaktion) einen `email_refs`-Eintrag anlegen — nicht als optionaler
+Zusatzschritt in einer anderen Funktion. Bei jeder DB-Audit-Anfrage in diesem Projekt
+proaktiv prüfen: `SELECT COUNT(*) FROM emails e WHERE e.tenant_id IS NOT NULL AND NOT
+EXISTS (SELECT 1 FROM email_refs r WHERE r.email_id = e.id)` — sollte immer 0 sein.
+Mittelfristige Lösung (noch nicht umgesetzt): DB-Trigger oder zentrale Helper-Funktion,
+die `email_refs` automatisch synchron hält, statt es an jedem Call-Site manuell zu pflegen.
+
+Ähnliche "zwei Felder/Tabellen ohne erzwungene Konsistenz"-Risiken im Schema (Audit
+2026-09-01, noch nicht gefixt, nur beobachtet):
+- `indexed_at` (storage.go:1060) markiert Manticore-Indexierung ohne Rückkanal bei
+ fehlgeschlagenem Manticore-Write danach — Reihenfolge in `tenant_worker.go` prüfen.
+- `storage_objects` (Checksum/Compression-Metadaten) wird an 3 Stellen befüllt
+ (storage.go:501, recompress.go:148, attachments.go:57), kein zentraler Owner.
+- `email_attachments` (M:N) hat kein `ON DELETE CASCADE`, nur manuelles
+ `DELETE FROM email_attachments` (storage.go:770) — neuer Lösch-Pfad ohne das vergisst
+ leicht verwaiste Referenzen.
+
## Code-Trigger für Migrationen
Diese Code-Änderungen erfordern fast immer eine Migration:
@@ -174,13 +205,13 @@ Bei neuen indizierten Feldern:
```bash
# Verbindungsparameter
-ssh root@192.168.1.131 'cat /etc/archivmail/config.yml | grep -A 6 "^database:"'
+ssh root@192.168.1.132 'cat /etc/archivmail/config.yml | grep -A 6 "^database:"'
# pg_dump VOR riskanter Migration (immer!)
-ssh root@192.168.1.131 'pg_dump -U postgres archivmail > /tmp/archivmail_pre_migration_$(date +%Y%m%d_%H%M%S).sql'
+ssh root@192.168.1.132 'pg_dump -U postgres archivmail > /tmp/archivmail_pre_migration_$(date +%Y%m%d_%H%M%S).sql'
# Live-Größe der relevanten Tabelle prüfen — Migrationen auf großen Tabellen brauchen Sonderbehandlung
-ssh root@192.168.1.131 'sudo -u postgres psql archivmail -c "SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;"'
+ssh root@192.168.1.132 'sudo -u postgres psql archivmail -c "SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;"'
```
## Audit-Trail
@@ -203,7 +234,7 @@ feat(PROJ-X): retention_until-Spalte für GoBD-Lockwarning
**Typischer Ablauf bei neuem Feld:**
1. Code-Diff lesen → identifiziere neue Spalten-Referenzen
2. Drift gegen Live-DB prüfen
-3. `pg_dump`-Backup auf 131
+3. `pg_dump`-Backup auf 132
4. `initSchema` im passenden Store ergänzen (idempotent)
5. Migration ausführen (Backend-Restart **oder** direkt via psql)
6. Validieren: `\d table` + Backend-Logs
diff --git a/.claude/agents/devops-deploy.md b/.claude/agents/devops-deploy.md
index 706f7b8..fe62408 100644
--- a/.claude/agents/devops-deploy.md
+++ b/.claude/agents/devops-deploy.md
@@ -1,6 +1,6 @@
---
name: devops-deploy
-description: "Server-Management, Deployment, Systemd-Dienste, nginx, Logs und Monitoring für das archivmail On-Premise-System auf root@192.168.1.131. Verwende diesen Subagent für Deployments, Service-Neustarts, Log-Analyse, nginx-Konfiguration, Systemd-Units, Backup, oder wenn der Benutzer fragt \"deploy\", \"server neu starten\", \"logs anschauen\", \"dienst läuft nicht\".\n\n\nContext: Der Benutzer möchte nach Code-Änderungen deployen.\nuser: \"deploy auf 131\"\nassistant: \"Ich starte den devops-deploy Agenten für das Deployment auf 192.168.1.131.\"\n\nDer devops-deploy Agent führt update.sh aus und prüft ob Backend und Frontend danach laufen.\n\n\n\n\nContext: Ein Dienst läuft nicht.\nuser: \"archivmail läuft nicht, was ist los?\"\nassistant: \"Ich starte den devops-deploy Agenten zur Diagnose.\"\n\nDer Agent liest Logs, prüft Service-Status und identifiziert die Ursache.\n\n"
+description: "Server-Management, Deployment, Systemd-Dienste, nginx, Logs und Monitoring für das archivmail On-Premise-System auf root@192.168.1.132. Verwende diesen Subagent für Deployments, Service-Neustarts, Log-Analyse, nginx-Konfiguration, Systemd-Units, Backup, oder wenn der Benutzer fragt \"deploy\", \"server neu starten\", \"logs anschauen\", \"dienst läuft nicht\".\n\n\nContext: Der Benutzer möchte nach Code-Änderungen deployen.\nuser: \"deploy auf 131\"\nassistant: \"Ich starte den devops-deploy Agenten für das Deployment auf 192.168.1.132.\"\n\nDer devops-deploy Agent führt update.sh aus und prüft ob Backend und Frontend danach laufen.\n\n\n\n\nContext: Ein Dienst läuft nicht.\nuser: \"archivmail läuft nicht, was ist los?\"\nassistant: \"Ich starte den devops-deploy Agenten zur Diagnose.\"\n\nDer Agent liest Logs, prüft Service-Status und identifiziert die Ursache.\n\n"
model: sonnet
---
@@ -12,8 +12,8 @@ Du hast SSH-Zugriff auf die Server und führst Deployments, Diagnosen und Wartun
## Infrastruktur
```
-Produktivserver: root@192.168.1.131 (Debian, on-premise)
-Testserver: root@192.168.1.132 (Debian, on-premise)
+Einziger Server: root@192.168.1.132 (Debian, on-premise, teilproduktiv — seit 2026-09-01
+ kein separater Testserver mehr, 192.168.1.131 gehört nicht mehr zum Projekt)
Backend: Go-Binary /opt/archivmail/archivmail, Port 8080, Systemd: archivmail
Frontend: Next.js standalone /opt/archivmail/web/server.js, Port 3000, Systemd: archivmail-web
@@ -28,20 +28,18 @@ Config: /etc/archivmail/config.yml, /etc/archivmail/keyfile
## Deploy-Workflow
```bash
-# Test-Deploy auf 132 zuerst (immer bevorzugen — 132 ist teilproduktiv, aber Fehler dort sind billiger als auf 131)
+# Deploy (einziger Server, seit 2026-09-01 kein separates Test/Prod-Paar mehr — trotzdem
+# vorsichtig, da teilproduktiv mit echten Nutzerdaten)
ssh root@192.168.1.132 'bash /opt/archivmail/update.sh'
-# Nach erfolgreicher Prüfung auf 132: Deploy auf Produktiv
-ssh root@192.168.1.131 'bash /opt/archivmail/update.sh'
-
# Nur Backend neu starten
-ssh root@192.168.1.131 'systemctl restart archivmail'
+ssh root@192.168.1.132 'systemctl restart archivmail'
# Nur Frontend neu starten
-ssh root@192.168.1.131 'systemctl restart archivmail-web'
+ssh root@192.168.1.132 'systemctl restart archivmail-web'
# Logs live
-ssh root@192.168.1.131 'journalctl -u archivmail -f --no-pager'
+ssh root@192.168.1.132 'journalctl -u archivmail -f --no-pager'
```
## Wichtige Regeln
@@ -63,43 +61,38 @@ ssh root@192.168.1.131 'journalctl -u archivmail -f --no-pager'
```bash
# Service-Status (alle relevanten Dienste)
-ssh root@192.168.1.131 'systemctl status archivmail archivmail-web manticore nginx postgresql'
+ssh root@192.168.1.132 'systemctl status archivmail archivmail-web manticore nginx postgresql'
# Fehler-Logs (letzte 10 Minuten)
-ssh root@192.168.1.131 'journalctl -u archivmail --since "10 minutes ago" --no-pager'
+ssh root@192.168.1.132 'journalctl -u archivmail --since "10 minutes ago" --no-pager'
# Port-Check
-ssh root@192.168.1.131 'ss -tlnp | grep -E "8080|3000|80|443|5432|2525|9306"'
+ssh root@192.168.1.132 'ss -tlnp | grep -E "8080|3000|80|443|5432|2525|9306"'
# Disk-Space
-ssh root@192.168.1.131 'df -h /var/archivmail /var/lib/manticore /opt/archivmail'
+ssh root@192.168.1.132 'df -h /var/archivmail /var/lib/manticore /opt/archivmail'
# nginx-Status + Syntax-Check
-ssh root@192.168.1.131 'systemctl status nginx && nginx -t'
+ssh root@192.168.1.132 'systemctl status nginx && nginx -t'
# PostgreSQL-Verbindung prüfen
-ssh root@192.168.1.131 'psql -U postgres -c "SELECT COUNT(*) FROM emails;" archivmail'
+ssh root@192.168.1.132 'psql -U postgres -c "SELECT COUNT(*) FROM emails;" archivmail'
```
## Backup
```bash
# PostgreSQL-Backup
-ssh root@192.168.1.131 'pg_dump -U postgres archivmail > /tmp/archivmail_$(date +%Y%m%d).sql'
+ssh root@192.168.1.132 'pg_dump -U postgres archivmail > /tmp/archivmail_$(date +%Y%m%d).sql'
# Manticore-Index-Backup (Dienst muss laufen)
-ssh root@192.168.1.131 'manticore_backup --config /etc/manticoresearch/manticore.conf \
+ssh root@192.168.1.132 'manticore_backup --config /etc/manticoresearch/manticore.conf \
--backup-dir /var/backups/manticore/$(date +%Y%m%d_%H%M%S)'
```
-## Testserver (192.168.1.132)
-
-Für Tests auf dem Testserver dieselben Befehle mit `root@192.168.1.132` verwenden.
-Nach erfolgreichen Tests auf 132 immer auch auf 131 deployen.
-
## Test-Hygiene (kritisch — wiederholt Quelle von Folgefehlern)
-- **Vor jeder Config-Änderung auf 131/132:** Backup mit Zeitstempel/Beschreibung anlegen
+- **Vor jeder Config-Änderung auf 132:** Backup mit Zeitstempel/Beschreibung anlegen
(`cp config.yml config.yml.bak-vor-`), niemals ohne Backup editieren.
- **Niemals den produktiven Service für isolierte Funktionstests zweckentfremden** (z.B.
Admin-Passwort-Hash überschreiben, um sich einzuloggen). Wenn ein Login zum Testen nötig
@@ -146,6 +139,6 @@ einen entsprechenden Copy-Schritt?
**Typischer Ablauf bei neuem Feature:**
1. mailarchiv-architect implementiert Code lokal
2. Code wird committed + gepusht
-3. devops-deploy führt `update.sh` auf 131 aus
+3. devops-deploy führt `update.sh` auf 132 aus
4. Bei Index-Schema-Änderungen: manticore-admin führt Reindex durch
5. Beide Services laufen → fertig
diff --git a/.claude/agents/mailarchiv-architect.md b/.claude/agents/mailarchiv-architect.md
index f803196..f657ec9 100644
--- a/.claude/agents/mailarchiv-architect.md
+++ b/.claude/agents/mailarchiv-architect.md
@@ -16,7 +16,7 @@ Du entwickelst **archivmail** – ein selbst gehostetes, unternehmenstaugliches
- Frontend: Next.js 16 (App Router), TypeScript, Tailwind CSS, shadcn/ui
- Datenbank: PostgreSQL (pgx/v5)
- Volltext-Index: Manticore Search (MySQL-Protokoll, Port 9306)
-- Deployment: Debian on-premise (192.168.1.131), Systemd
+- Deployment: Debian on-premise (192.168.1.132), Systemd
**Go-Modul: `archivmail`** — Imports sind immer `archivmail/internal/...`, NIEMALS `github.com/archivmail/...`
@@ -32,6 +32,29 @@ Gedächtnis annehmen (die Datei wird laufend fortgeschrieben).
4. **Modularer Code** – klare Interfaces, lose Kopplung, hohe Kohäsion
5. **Ressourcenschonend** – <200 MB RAM, optimierter Disk-Zugriff
+## Bekanntes Strukturproblem: MailDocument-Fan-out-Drift (PROJ-86, 2026-09-01)
+
+10 Stellen bauen `index.MailDocument{}` manuell (internal/pop3/importer.go:167,
+internal/imap/importer.go:289, internal/api/upload.go:216, cmd/archivmail/cmd_index_pending.go:133,
+cmd_import.go:270, cmd_reindex.go:119, cmd_fix_subjects.go:248, main.go:609+751,
+cmd/archivmail-import/main.go:124) plus separate Delete-Pfade (cmd_purge.go,
+dsgvo_handlers.go). Grund: Ingest-Vielfalt (IMAP/POP3/SMTP/Upload/CLI-Import/Reindex/
+Backfill/Purge) × wachsende Fan-out-Pflichten (Global-Mirror-Index, Datums-Fallback,
+email_refs-Sync) — jede neue Pflicht multipliziert sich mit der Zahl der Ingest-Pfade.
+Zwei reale Produktionsbugs binnen kurzer Zeit entstanden genau so (emails_global bekam
+nie Tenant-Mails gespiegelt; date_ts=0 bei fehlendem Date-Header).
+
+**Empfehlung bei künftigen Änderungen an MailDocument/Indexierung:** Nicht an allen 10
+Stellen einzeln nachziehen (Copy-Paste-Konsistenz = Antipattern). Stattdessen zwei
+schmale zentrale Funktionen einführen (noch nicht umgesetzt, ~0,5-1 PT):
+1. `index.NewMailDocument(...)` — reiner Konstruktor, setzt alle Pflichtfelder inkl.
+ `EffectiveDate`-Fallback korrekt, ersetzt die 10 manuellen Struct-Literale.
+2. `index.IndexWithMirror(ctx, tenantIdx, doc)` / `DeleteWithMirror(...)` — schreibt
+ immer sowohl `emails_tenant_N` als auch `emails_global`, ersetzt die 5
+ Doppel-Write-Stellen.
+`email_refs`-Sync bewusst NICHT mit hineinpacken — das ist ein eigenständiges
+Datenmodell-Problem (siehe db-migrator.md), keine Index-Fabrik-Aufgabe.
+
## Systemarchitektur
### Tatsächliche Projektstruktur
@@ -207,7 +230,7 @@ mitübernehmen kann.
Nach Abschluss von Implementierungsarbeiten:
-- **→ devops-deploy**: Wenn Code bereit zum Testen/Deployen ist — Agent führt `update.sh` zuerst auf 192.168.1.132 (Test) aus, nach erfolgreicher Prüfung erst auf 192.168.1.131 (Produktiv)
+- **→ devops-deploy**: Wenn Code bereit zum Testen/Deployen ist — Agent führt `update.sh` auf 192.168.1.132 (einziger Server, seit 2026-09-01 kein separater Testserver mehr) aus
- **→ manticore-admin**: Wenn der Manticore-Index-Schema geändert wurde (neue Felder, neue Tabellen) — Agent führt `ALTER TABLE` + `reindex` durch
- **→ QA Engineer**: Wenn Feature implementiert ist und gegen Acceptance-Criteria getestet werden soll
diff --git a/.claude/agents/manticore-admin.md b/.claude/agents/manticore-admin.md
index c90e4d4..73d9c66 100644
--- a/.claude/agents/manticore-admin.md
+++ b/.claude/agents/manticore-admin.md
@@ -13,7 +13,7 @@ Du bist Manticore Search Administrator für das archivmail-Projekt.
- **Manticore Search** — RT-Indizes, MySQL-Protokoll (Port 9306, nur localhost)
- **Go-Integration** — `internal/index/manticore.go`, Treiber: `github.com/go-sql-driver/mysql`
- **Interfaces** — `internal/index/index.go` → `Indexer` + `TenantIndexer`
-- **Server** — root@192.168.1.131 (Produktiv), root@192.168.1.132 (Test)
+- **Server** — root@192.168.1.132 (einziger Server, teilproduktiv)
- **Dienst** — `manticore.service` (systemd)
- **archivmail-Config** — `/etc/archivmail/config.yml` → `index.backend: manticore`
- **Datenpfad** — `/var/lib/manticore/`
@@ -57,18 +57,44 @@ SHOW INDEX emails_tenant_1 STATUS;
## Reindex
-```bash
-# Alle Tenants
-ssh root@192.168.1.131 'archivmail reindex --config /etc/archivmail/config.yml'
+> ⚠ **192.168.1.132 gehört seit 2026-09-01 NICHT mehr zum archivmail-Projekt** (User-Bestätigung —
+> anderes System läuft dort, siehe MEMORY.md). Einziger Server: **192.168.1.132**.
+> ⚠ **Kein `mysql`-Client auf dem Host installiert** — Diagnose/Reindex-Fortschritt nur über die
+> HTTP-API auf `127.0.0.1:9308` (`?mode=raw` für volles SQL-Set), siehe Abschnitt "Verbindung auf Server".
-# Einzelner Tenant
-ssh root@192.168.1.131 'archivmail reindex --config /etc/archivmail/config.yml --tenant 1'
+```bash
+# Alle Tenants (liest emails.tenant_id direkt — vollständig, siehe Warnung unten)
+ssh root@192.168.1.132 'archivmail reindex --config /etc/archivmail/config.yml'
+
+# Einzelner Tenant — ⚠ NICHT äquivalent zum Voll-Reindex, siehe Warnung
+ssh root@192.168.1.132 'archivmail reindex --config /etc/archivmail/config.yml --tenant 1'
# Fortschritt beobachten
-ssh root@192.168.1.131 'journalctl -u archivmail -f | grep -i reindex'
-ssh root@192.168.1.131 'watch -n 5 "mysql -h 127.0.0.1 -P 9306 -u manticore -e \"SELECT COUNT(*) FROM emails_tenant_1;\" 2>/dev/null"'
+ssh root@192.168.1.132 'journalctl -u archivmail -f | grep -i reindex'
+ssh root@192.168.1.132 "watch -n 5 \"curl -s 'http://127.0.0.1:9308/sql?mode=raw' --data-urlencode 'query=SELECT COUNT(*) FROM emails_tenant_1'\""
```
+> ⚠ **`--tenant N` kann Mails unter den Tisch fallen lassen (PROJ-86, 2026-09-01):** Der
+> `--tenant`-Modus liest Mail-IDs über `GetAllIDsByTenant()` → fragt **ausschließlich** die
+> `email_refs`-Tabelle ab, nicht `emails.tenant_id`. Wenn `email_refs` für eine Mail fehlt
+> (Dual-Source-Bug, siehe db-migrator.md), wird sie beim tenant-scoped Reindex übersprungen —
+> obwohl `archivmail reindex` OHNE `--tenant`-Flag (liest `emails.tenant_id` direkt über
+> `GetTenantForMail` je ID) sie korrekt gefunden und indexiert hätte. **Bei Zweifel an der
+> Vollständigkeit eines `--tenant`-Reindex: Vergleiche `SELECT COUNT(*) FROM emails WHERE
+> tenant_id=N` (Postgres) gegen `SELECT COUNT(*) FROM emails_tenant_N` (Manticore) — bei
+> Abweichung lieber den vollen `reindex` ohne `--tenant` fahren.**
+
+## Bekannte Lücke: emails_global bekommt Tenant-Mails nicht automatisch gespiegelt
+
+Bis PROJ-86 (2026-09-01) schrieb jeder Indexier-Pfad eine Mail NUR in ihre
+`emails_tenant_N`-Tabelle, nie zusätzlich nach `emails_global` — die Superadmin-Suche
+(liest bei `tenantID==nil` genau diesen globalen Index) sah dadurch nur einen Bruchteil
+aller Mails. Seit dem Fix spiegeln 5 Stellen im Code (cmd_reindex.go, tenant_worker.go,
+imap/importer.go, cmd_purge.go, dsgvo_handlers.go) explizit auch nach `emails_global`.
+**Bei jeder neuen Indexier-Stelle im Code (9. Ingest-Pfad kommt bestimmt) prüfen, ob der
+Global-Mirror mitgezogen wurde** — Diagnose: `SELECT COUNT(*) FROM emails_global` sollte
+≈ `SELECT COUNT(*) FROM emails` (Postgres-Gesamtzahl) sein, nicht nur der no-Tenant-Anteil.
+
## Schema erweitern
Koordiniere mit **mailarchiv-architect** bevor Schema-Änderungen: Interface-Änderungen in Go müssen parallel zu Schema-Änderungen in Manticore erfolgen.
@@ -82,11 +108,11 @@ Koordiniere mit **mailarchiv-architect** bevor Schema-Änderungen: Interface-Än
```bash
# Backup (Dienst muss laufen)
-ssh root@192.168.1.131 'manticore_backup --config /etc/manticoresearch/manticore.conf \
+ssh root@192.168.1.132 'manticore_backup --config /etc/manticoresearch/manticore.conf \
--backup-dir /var/backups/manticore/$(date +%Y%m%d_%H%M%S)'
# Restore via Reindex (Source of Truth = Roh-Mails in /var/archivmail/store/)
-ssh root@192.168.1.131 'archivmail reindex --config /etc/archivmail/config.yml'
+ssh root@192.168.1.132 'archivmail reindex --config /etc/archivmail/config.yml'
```
## GoBD-Hinweis
@@ -101,17 +127,17 @@ Echte Löschungen laufen ausschließlich über `archivmail purge` (Retention + M
## Security
- Port 9306 NUR auf localhost: `listen = 127.0.0.1:9306:mysql`
-- Check: `ssh root@192.168.1.131 'ss -tlnp | grep 9306'`
+- Check: `ssh root@192.168.1.132 'ss -tlnp | grep 9306'`
- User-Input IMMER durch `escapeManticoreMatch()` in `manticore.go`
- Table-Namen von Tenant-ID (int64) abgeleitet — kein Injection-Risiko
## Dienst-Management
```bash
-ssh root@192.168.1.131 'systemctl status manticore'
-ssh root@192.168.1.131 'systemctl restart manticore'
-ssh root@192.168.1.131 'journalctl -u manticore -f'
-ssh root@192.168.1.131 'apt-get update && apt-get upgrade manticoresearch -y'
+ssh root@192.168.1.132 'systemctl status manticore'
+ssh root@192.168.1.132 'systemctl restart manticore'
+ssh root@192.168.1.132 'journalctl -u manticore -f'
+ssh root@192.168.1.132 'apt-get update && apt-get upgrade manticoresearch -y'
```
## Wichtige Dateipfade
diff --git a/CLAUDE.md b/CLAUDE.md
index 1224091..8b16d91 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,7 +8,7 @@ Selbst gehostetes Mail-Archiv-System. Go-Backend + Next.js-Frontend + PostgreSQL
- **Frontend:** Next.js 16 (App Router), TypeScript, Tailwind CSS, shadcn/ui
- **Datenbank:** PostgreSQL (pgx/v5)
- **Volltext-Index:** Manticore Search (MySQL-Protokoll, Port 9306)
-- **Deployment:** On-Premise, Systemd — Produktiv: 192.168.1.131 | Test: 192.168.1.132
+- **Deployment:** On-Premise, Systemd — einziger Server: 192.168.1.132 (teilproduktiv, seit 2026-09-01 kein separater Testserver mehr)
- **Auth:** JWT (httpOnly Cookie), bcrypt Cost 12
## Projektstruktur