docs: Agent-Definitionen bereinigt (192.168.1.131 raus) + Dual-Source-Falle dokumentiert

192.168.1.131 gehört seit 2026-09-01 nicht mehr zum archivmail-Projekt (User-Bestätigung).
Alle Referenzen in CLAUDE.md und Agent-Defs auf den verbleibenden Server 192.168.1.132
korrigiert (Test/Prod-Paar existiert nicht mehr).

Zusätzlich Erkenntnisse aus PROJ-86-Audit (Subagenten db-migrator + mailarchiv-architect)
in die Agent-Defs eingearbeitet: emails.tenant_id vs. email_refs Dual-Source-Falle,
MailDocument-Fan-out-Drift-Muster, emails_global-Mirror-Pflicht, --tenant-Reindex-Lücke.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPFC6Jk2ke1Pq9XcuVGP1R
This commit is contained in:
sysops
2026-09-01 13:56:41 +02:00
co-authored by Claude Sonnet 5
parent c2b92a9a30
commit 25865423f9
5 changed files with 134 additions and 61 deletions
+49 -18
View File
@@ -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<example>\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<commentary>\nNeue Spalten-Referenz im Code → Drift-Check + Migration durch db-migrator.\n</commentary>\n</example>\n\n<example>\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<commentary>\nDrift-Erkennung nach Code-Änderungen ist die Kernaufgabe dieses Agents.\n</commentary>\n</example>\n\n<example>\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<commentary>\nNeue Felder aus Feature-Specs werden vom db-migrator integriert.\n</commentary>\n</example>"
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<example>\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<commentary>\nNeue Spalten-Referenz im Code → Drift-Check + Migration durch db-migrator.\n</commentary>\n</example>\n\n<example>\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<commentary>\nDrift-Erkennung nach Code-Änderungen ist die Kernaufgabe dieses Agents.\n</commentary>\n</example>\n\n<example>\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<commentary>\nNeue Felder aus Feature-Specs werden vom db-migrator integriert.\n</commentary>\n</example>"
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