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:
co-authored by
Claude Sonnet 5
parent
c2b92a9a30
commit
25865423f9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user