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 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 model: sonnet
--- ---
@@ -37,25 +37,24 @@ Manticore-Schema lebt separat in `internal/index/manticore.go` → koordiniere m
## Workflow ## 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.132 ist seit 2026-09-01 der einzige Server für archivmail** (192.168.1.131
192.168.1.131 (Produktiv) übernehmen** — außer der Auftrag verlangt explizit nur Produktiv. gehört nicht mehr zum Projekt, kein separates Test/Prod-Paar mehr). 132 ist teilproduktiv
132 ist teilproduktiv (echte Nutzerdaten), aber ein Migrationsfehler dort ist deutlich (echte Nutzerdaten) — Migrationen also wie gegen Produktiv behandeln: `pg_dump`-Backup vor
weniger folgenreich als auf 131. Nach erfolgreicher Validierung auf 132 dieselbe Migration jeder heiklen Migration (siehe Abschnitt "Sicherheit"), bei Zweifeln vorher nachfragen.
unverändert auf 131 anwenden (nicht neu formulieren).
### 1. Drift erkennen ### 1. Drift erkennen
```bash ```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"' ssh root@192.168.1.132 'sudo -u postgres psql archivmail -c "\d emails"'
# Alle Tabellen # 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 # 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: 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): **A) Über Backend-Restart** (bevorzugt, wenn nicht zeitkritisch):
```bash ```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. 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): **B) Direkt via psql** (bei kritischen Änderungen oder wenn das Backend aus anderen Gründen nicht neu starten soll):
```bash ```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; ALTER TABLE emails ADD COLUMN IF NOT EXISTS retention_until TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_emails_retention ON emails (retention_until); CREATE INDEX IF NOT EXISTS idx_emails_retention ON emails (retention_until);
SQL SQL
@@ -113,10 +112,10 @@ SQL
Immer nach jeder Migration: Immer nach jeder Migration:
```bash ```bash
# Spalte existiert? # 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? # 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. 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` - **Boolean Defaults:** `BOOLEAN NOT NULL DEFAULT FALSE/TRUE`
- **Idempotenz:** ohne `IF NOT EXISTS` / `DO $$ … EXCEPTION` keine Migration freigeben - **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 ## Code-Trigger für Migrationen
Diese Code-Änderungen erfordern fast immer eine Migration: Diese Code-Änderungen erfordern fast immer eine Migration:
@@ -174,13 +205,13 @@ Bei neuen indizierten Feldern:
```bash ```bash
# Verbindungsparameter # 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!) # 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 # 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 ## Audit-Trail
@@ -203,7 +234,7 @@ feat(PROJ-X): retention_until-Spalte für GoBD-Lockwarning
**Typischer Ablauf bei neuem Feld:** **Typischer Ablauf bei neuem Feld:**
1. Code-Diff lesen → identifiziere neue Spalten-Referenzen 1. Code-Diff lesen → identifiziere neue Spalten-Referenzen
2. Drift gegen Live-DB prüfen 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) 4. `initSchema` im passenden Store ergänzen (idempotent)
5. Migration ausführen (Backend-Restart **oder** direkt via psql) 5. Migration ausführen (Backend-Restart **oder** direkt via psql)
6. Validieren: `\d table` + Backend-Logs 6. Validieren: `\d table` + Backend-Logs
+18 -25
View File
@@ -1,6 +1,6 @@
--- ---
name: devops-deploy 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<example>\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<commentary>\nDer devops-deploy Agent führt update.sh aus und prüft ob Backend und Frontend danach laufen.\n</commentary>\n</example>\n\n<example>\nContext: Ein Dienst läuft nicht.\nuser: \"archivmail läuft nicht, was ist los?\"\nassistant: \"Ich starte den devops-deploy Agenten zur Diagnose.\"\n<commentary>\nDer Agent liest Logs, prüft Service-Status und identifiziert die Ursache.\n</commentary>\n</example>" 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<example>\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<commentary>\nDer devops-deploy Agent führt update.sh aus und prüft ob Backend und Frontend danach laufen.\n</commentary>\n</example>\n\n<example>\nContext: Ein Dienst läuft nicht.\nuser: \"archivmail läuft nicht, was ist los?\"\nassistant: \"Ich starte den devops-deploy Agenten zur Diagnose.\"\n<commentary>\nDer Agent liest Logs, prüft Service-Status und identifiziert die Ursache.\n</commentary>\n</example>"
model: sonnet model: sonnet
--- ---
@@ -12,8 +12,8 @@ Du hast SSH-Zugriff auf die Server und führst Deployments, Diagnosen und Wartun
## Infrastruktur ## Infrastruktur
``` ```
Produktivserver: root@192.168.1.131 (Debian, on-premise) Einziger Server: root@192.168.1.132 (Debian, on-premise, teilproduktiv — seit 2026-09-01
Testserver: root@192.168.1.132 (Debian, on-premise) kein separater Testserver mehr, 192.168.1.131 gehört nicht mehr zum Projekt)
Backend: Go-Binary /opt/archivmail/archivmail, Port 8080, Systemd: archivmail Backend: Go-Binary /opt/archivmail/archivmail, Port 8080, Systemd: archivmail
Frontend: Next.js standalone /opt/archivmail/web/server.js, Port 3000, Systemd: archivmail-web 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 ## Deploy-Workflow
```bash ```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' 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 # 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 # 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 # 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 ## Wichtige Regeln
@@ -63,43 +61,38 @@ ssh root@192.168.1.131 'journalctl -u archivmail -f --no-pager'
```bash ```bash
# Service-Status (alle relevanten Dienste) # 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) # 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 # 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 # 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 # 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 # 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 ## Backup
```bash ```bash
# PostgreSQL-Backup # 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) # 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)' --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) ## 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-<grund>`), niemals ohne Backup editieren. (`cp config.yml config.yml.bak-vor-<grund>`), niemals ohne Backup editieren.
- **Niemals den produktiven Service für isolierte Funktionstests zweckentfremden** (z.B. - **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 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:** **Typischer Ablauf bei neuem Feature:**
1. mailarchiv-architect implementiert Code lokal 1. mailarchiv-architect implementiert Code lokal
2. Code wird committed + gepusht 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 4. Bei Index-Schema-Änderungen: manticore-admin führt Reindex durch
5. Beide Services laufen → fertig 5. Beide Services laufen → fertig
+25 -2
View File
@@ -16,7 +16,7 @@ Du entwickelst **archivmail** ein selbst gehostetes, unternehmenstaugliches
- Frontend: Next.js 16 (App Router), TypeScript, Tailwind CSS, shadcn/ui - Frontend: Next.js 16 (App Router), TypeScript, Tailwind CSS, shadcn/ui
- Datenbank: PostgreSQL (pgx/v5) - Datenbank: PostgreSQL (pgx/v5)
- Volltext-Index: Manticore Search (MySQL-Protokoll, Port 9306) - 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/...` **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 4. **Modularer Code** klare Interfaces, lose Kopplung, hohe Kohäsion
5. **Ressourcenschonend** <200 MB RAM, optimierter Disk-Zugriff 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 ## Systemarchitektur
### Tatsächliche Projektstruktur ### Tatsächliche Projektstruktur
@@ -207,7 +230,7 @@ mitübernehmen kann.
Nach Abschluss von Implementierungsarbeiten: 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 - **→ 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 - **→ QA Engineer**: Wenn Feature implementiert ist und gegen Acceptance-Criteria getestet werden soll
+41 -15
View File
@@ -13,7 +13,7 @@ Du bist Manticore Search Administrator für das archivmail-Projekt.
- **Manticore Search** — RT-Indizes, MySQL-Protokoll (Port 9306, nur localhost) - **Manticore Search** — RT-Indizes, MySQL-Protokoll (Port 9306, nur localhost)
- **Go-Integration** — `internal/index/manticore.go`, Treiber: `github.com/go-sql-driver/mysql` - **Go-Integration** — `internal/index/manticore.go`, Treiber: `github.com/go-sql-driver/mysql`
- **Interfaces** — `internal/index/index.go``Indexer` + `TenantIndexer` - **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) - **Dienst** — `manticore.service` (systemd)
- **archivmail-Config** — `/etc/archivmail/config.yml``index.backend: manticore` - **archivmail-Config** — `/etc/archivmail/config.yml``index.backend: manticore`
- **Datenpfad** — `/var/lib/manticore/` - **Datenpfad** — `/var/lib/manticore/`
@@ -57,18 +57,44 @@ SHOW INDEX emails_tenant_1 STATUS;
## Reindex ## Reindex
```bash > ⚠ **192.168.1.132 gehört seit 2026-09-01 NICHT mehr zum archivmail-Projekt** (User-Bestätigung —
# Alle Tenants > anderes System läuft dort, siehe MEMORY.md). Einziger Server: **192.168.1.132**.
ssh root@192.168.1.131 'archivmail reindex --config /etc/archivmail/config.yml' > ⚠ **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 ```bash
ssh root@192.168.1.131 'archivmail reindex --config /etc/archivmail/config.yml --tenant 1' # 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 # Fortschritt beobachten
ssh root@192.168.1.131 'journalctl -u archivmail -f | grep -i reindex' ssh root@192.168.1.132 '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 "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 ## Schema erweitern
Koordiniere mit **mailarchiv-architect** bevor Schema-Änderungen: Interface-Änderungen in Go müssen parallel zu Schema-Änderungen in Manticore erfolgen. 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 ```bash
# Backup (Dienst muss laufen) # 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)' --backup-dir /var/backups/manticore/$(date +%Y%m%d_%H%M%S)'
# Restore via Reindex (Source of Truth = Roh-Mails in /var/archivmail/store/) # 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 ## GoBD-Hinweis
@@ -101,17 +127,17 @@ Echte Löschungen laufen ausschließlich über `archivmail purge` (Retention + M
## Security ## Security
- Port 9306 NUR auf localhost: `listen = 127.0.0.1:9306:mysql` - 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` - User-Input IMMER durch `escapeManticoreMatch()` in `manticore.go`
- Table-Namen von Tenant-ID (int64) abgeleitet — kein Injection-Risiko - Table-Namen von Tenant-ID (int64) abgeleitet — kein Injection-Risiko
## Dienst-Management ## Dienst-Management
```bash ```bash
ssh root@192.168.1.131 'systemctl status manticore' ssh root@192.168.1.132 'systemctl status manticore'
ssh root@192.168.1.131 'systemctl restart manticore' ssh root@192.168.1.132 'systemctl restart manticore'
ssh root@192.168.1.131 'journalctl -u manticore -f' ssh root@192.168.1.132 'journalctl -u manticore -f'
ssh root@192.168.1.131 'apt-get update && apt-get upgrade manticoresearch -y' ssh root@192.168.1.132 'apt-get update && apt-get upgrade manticoresearch -y'
``` ```
## Wichtige Dateipfade ## Wichtige Dateipfade
+1 -1
View File
@@ -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 - **Frontend:** Next.js 16 (App Router), TypeScript, Tailwind CSS, shadcn/ui
- **Datenbank:** PostgreSQL (pgx/v5) - **Datenbank:** PostgreSQL (pgx/v5)
- **Volltext-Index:** Manticore Search (MySQL-Protokoll, Port 9306) - **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 - **Auth:** JWT (httpOnly Cookie), bcrypt Cost 12
## Projektstruktur ## Projektstruktur