Beide Features wurden auf 192.168.1.131 deployt (Commit 4c92587), inkl. der
zugehörigen QA-Bugfixes (Dry-Run-Adressmatching PROJ-43, days-Clamp PROJ-52).
Deployment-Abschnitte in den Feature-Specs und INDEX.md aktualisiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
16 KiB
PROJ-52: Vollständigkeits-Reconciliation (Zähl-Report Mailserver vs. Archiv)
Status: Deployed
Created: 2026-06-13 Last Updated: 2026-07-04
Hintergrund
Der GoBD/DSGVO-Compliance-Check (docs/GOBD_DSGVO_CHECKLIST.md, Punkt 1) bewertet
"Vollständigkeit" nur als "Teilweise erfüllt": SMTP-BCC-Journaling (PROJ-4) und IMAP/POP3-Import
(PROJ-3/8/14/45) sind robust (z.B. 452-Retry bei Storage-Fehlern), aber es gibt keinen
zentralen Mechanismus, der zeigt, ob tatsächlich ALLE erwarteten E-Mails archiviert wurden
(VOI-Grundsatz 2: "kein Dokument darf auf dem Weg ins Archiv oder im Archiv selbst verloren
gehen"). Diese Spec ergänzt einen täglichen Zähl-Report pro Quelle.
Dependencies
- Requires: PROJ-4 (SMTP-Import), PROJ-3/PROJ-14 (IMAP/POP3-Import), PROJ-45 (IMAP Per-Folder UID-Tracking)
- Requires: PROJ-17 (Admin Dashboard) – Anzeige des Reports
- Requires: PROJ-11/PROJ-48 (Audit-Log) – Auffälligkeiten werden protokolliert
User Stories
- Als Admin möchte ich täglich sehen, wie viele E-Mails pro Quelle (SMTP-Journal, IMAP-Konto, POP3-Konto) archiviert wurden, damit ich Ausreißer (plötzlich 0 Mails) erkenne.
- Als Admin möchte ich für IMAP/POP3-Quellen einen Soll/Ist-Vergleich sehen: Anzahl Mails im Quell-Postfach (laut letztem Sync) vs. Anzahl archivierter Mails für diese Quelle.
- Als Auditor möchte ich nachvollziehen können, ob es Tage mit auffälligen Abweichungen gab (z.B. SMTP-Dienst war down).
- Als Admin möchte ich bei einer signifikanten Abweichung (z.B. >50% Rückgang ggü. Durchschnitt der letzten 7 Tage) eine Warnung im Dashboard sehen.
Acceptance Criteria
- Täglicher Job (Cron, analog PROJ-8-Scheduler) berechnet pro Tag und Quelle
(
source_type:smtp,imap:<account_id>,pop3:<account_id>,import) die Anzahl neu archivierter E-Mails (received_at/imported_atam jeweiligen Tag) - Für IMAP-Konten (PROJ-45 UID-Tracking): zusätzlicher Soll/Ist-Vergleich – Anzahl Mails im
Quell-Ordner laut letztem
UIDVALIDITY/UID-Stand vs. Anzahl im Archiv für diesen Ordner - Ergebnisse werden in Tabelle
reconciliation_reports(date, tenant_id, source_type, source_id, expected_count, archived_count, delta) persistiert - Admin-Dashboard (PROJ-17) zeigt eine neue Kachel/Tabelle "Vollständigkeits-Check" mit den letzten 7 Tagen pro Quelle
- Abweichung > konfigurierbarem Schwellenwert (Default: 50% unter 7-Tage-Durchschnitt,
reconciliation.alert_threshold_pctinconfig.yml) → Warn-Badge im Dashboard + Audit-Log-Eintrag (event_type: reconciliation_anomaly) - Report ist als CSV exportierbar (analog Audit-Log-Export aus PROJ-11)
- Tage ohne Aktivität (0 Mails) werden explizit als
0ausgewiesen, nicht als fehlender Datensatz (damit Lücken im Cron-Lauf selbst erkennbar sind)
Edge Cases
- Quelle wurde erst kürzlich angelegt (kein 7-Tage-Durchschnitt vorhanden) → kein Alert, Anzeige "Noch nicht genug Daten"
- SMTP-Journal hat naturgemäß starke Schwankungen (Wochenende vs. Wochentag) → Schwellenwert ist konfigurierbar, Doku weist auf mögliche False-Positives an Wochenenden/Feiertagen hin
- IMAP-Quell-Postfach wurde vom Nutzer geleert (Mails dort gelöscht, aber bereits archiviert) →
expected_countsinkt,archived_countbleibt hoch →deltanegativ in "gute" Richtung, kein Alert (nur Rückgang vonarchived_countselbst ist relevant) - Reconciliation-Job selbst schlägt fehl (z.B. DB-Timeout) → Fehler wird geloggt, vorheriger Tag bleibt ohne Report-Eintrag, Dashboard zeigt "Daten fehlen für " statt falscher Nullwerte
- Multi-Tenant: Reports sind pro Tenant; Tenant-Admins sehen nur eigene Quellen, Super-Admin sieht alle
Technical Requirements
- Neue Tabelle
reconciliation_reports(siehe AC), Index auf(tenant_id, date, source_type) - Cron-Job-Registrierung analog bestehendem IMAP-Sync-Scheduler (PROJ-8)
- Wiederverwendung von
internal/imap-Funktionen zur Ermittlung der Quell-Postfach-Anzahl (sofern bereits durch UID-Tracking verfügbar, kein zusätzlicher IMAP-Login nötig wenn vermeidbar)
Implementation Notes (Backend, 2026-07-03)
Neues Package internal/reconciliation/
reconciliation.go:Store(eigener pgxpool),initSchema(), Tabellereconciliation_reports (id, date, tenant_id, source_type, source_id, expected_count, archived_count, delta, created_at). Upsert-Key ist ein COALESCE-Ausdrucksindex(date, COALESCE(tenant_id,-1), source_type, COALESCE(source_id,-1)), weiltenant_id/source_idNULL-fähig sind und Postgres NULLs in einem normalen UNIQUE-Index als verschieden behandelt (sonst Doppelzeilen für smtp/import/tenant-lose Buckets). Zusätzlicher Lookup-Index(tenant_id, date, source_type)laut AC.compute.go:ComputeForDate(ctx, day, thresholdPct)— Read-Phase (archived pro Bucket für den Tag, known-buckets ausemailsDISTINCT, IMAP-Snapshot), dann Upsert in einer Transaktion. Bei Query-Fehler wird der Tag NICHT geschrieben (kein falscher 0-Eintrag; Dashboard zeigt "fehlt"). 0-Mail-Tage werden für jeden bekannten Bucket explizit als 0 persistiert. Alert: 7-Tage-Durchschnitt (trailing_average, NULL-safeIS NOT DISTINCT FROM); Alert nur bei ≥7 Vortages-Datensätzen;archived < avg*(1-pct/100)→ Audit-Eintragreconciliation_anomaly(event viaaudit.EventReconciliationAnomaly).query.go:DashboardData()(letzte N Tage pro Quelle, tenant-gescoped, Alert-Flag +enough_data) undExportRows()(CSV).
Source-Tracking (nötige Ergänzung — emails hatte keine Herkunftsspalte)
emails bekam via storage.initSchema() zwei Spalten source_type TEXT,
source_id BIGINT + Index (received_at, source_type, source_id, tenant_id).
Neue Methode storage.Store.TagSource(ctx, id, sourceType, sourceID) schreibt
nur solange source_type IS NULL (first-write-wins → dedupte Mehrfach-Mails
werden nicht doppelt gezählt). Verdrahtet in allen Ingestion-Pfaden:
internal/smtpd/smtpd.go→smtp, nilinternal/imap/importer.go+internal/imap/scheduler.go→imap, account_id (accountID durchfetchBatch/fetchSyncBatch/storeAndIndexdurchgereicht)internal/pop3/importer.go→pop3, account_idcmd/archivmail/cmd_import.go+internal/api/upload.go→import, nil
IMAP Soll/Ist (Abweichung von der Spec — dokumentiert)
Das PROJ-45 UID-Tracking (imap_folder_state) speichert nur last_uid, KEINE
Nachrichtenzahl. Daher wird expected_count für IMAP-Quellen als Proxy aus
SUM(last_uid) je Konto gebildet (kein zusätzlicher IMAP-Login) und
delta = kumulativ_archiviert(Konto) − expected. archived_count bleibt
konsistent für ALLE Quellen die pro-Tag neu archivierte Zahl (steuert das
Alerting). Postfach-Leerung → expected sinkt, delta positiv → kein Alert
(Alert nur bei Rückgang von archived_count), wie in Edge Cases gefordert.
Cron statt Dauer-Goroutine
Analog PROJ-58 als CLI-Subcommand archivmail reconcile (cmd_reconcile.go),
Default = Vortag; --date, --days N (Backfill, älteste→neueste Reihenfolge
für konsistente Trailing-Average-Historie). Registriert in main.go. Der Daemon
(serve) verdrahtet den Store nur lesend für die API (SetReconciliation).
Config
config.ReconciliationConfig.AlertThresholdPct *int (reconciliation.alert_threshold_pct),
Default 50 via ResolvedThresholdPct(). Beispiele in config.test.yml und
config/config.docker.yml.example.
API-Endpoints (tenant-gescoped, authAdmin = domain_admin+)
GET /api/admin/reconciliation?days=7(max 90){ "days": 7, "threshold_pct": 50, "sources": [ { "source_type": "smtp", "source_id": null, "source_key": "smtp", "tenant_id": null, "points": [ {"date":"2026-06-27","archived_count":42,"expected_count":null,"delta":null,"missing":false}, {"date":"2026-06-28","archived_count":null,"expected_count":null,"delta":null,"missing":true} ], "avg_7d": 40.5, "enough_data": true, "alert": false } ] }source_key:smtp|import|imap:<id>|pop3:<id>.missing:true= kein Report-Datensatz (Cron nicht gelaufen), ≠archived_count:0.enough_data:false→ UI zeigt "Noch nicht genug Daten".alert:true→ Warn-Badge.GET /api/admin/reconciliation/export.csv?days=30(max 366) — CSVdate,tenant_id,source,expected_count,archived_count,delta, Audit-Eintragexport. Tenant-Scope: domain_admin nur eigener Tenant, superadmin alle.
Tenant-Isolation
reconTenantScope() filtert wie handleMailTimeseries: Session mit tenant_id
→ nur eigener Tenant, superadmin (nil) → alle. Kein {id}-Pfadparameter, daher
kein IDOR-Vektor; Filter erfolgt in der SQL-WHERE.
Manticore
Keine Index-Änderung nötig (reine PostgreSQL-Aggregation).
Offene Punkte / Handoff
- Cron-Eintrag für
archivmail reconcilemuss ininstall.sh/update.shergänzt werden (devops-deploy). - Frontend: Dashboard-Kachel + TS-Typen in
src/lib/api/. - Kein lokaler
go buildmöglich — Build/QA separat auf Testserver.
Implementation Notes (Frontend, 2026-07-03)
Neue API-Schicht src/lib/api/reconciliation.ts
- Typen
ReconciliationPoint,ReconciliationSource,ReconciliationResponse(1:1 zur Backend-Response;archived_count/expected_count/deltasindnumber | null,missing: boolean). getReconciliation(days = 7)→GET /api/admin/reconciliation?days=7über den bestehendenrequest<T>-Wrapper (core.ts, credentials/401-Handling inklusive).exportReconciliationCSV(days = 30)→ direkterfetchauf/api/admin/reconciliation/export.csv(Blob-Download, Content-Disposition- Dateiname-Parsing, analogdownloadMailAttachment/exportDSGVORequestPDF).- Re-Exports in
src/lib/api/index.tsergänzt.
Neue Kachel src/components/admin/tabs/ReconciliationCard.tsx
- Self-fetching Client-Komponente (
useEffectbeim Mount), Loading-/Error-/ Empty-States implementiert. - Tabelle (shadcn
Table): eine Zeile pro Quelle, Label hübsch formatiert ("SMTP-Journal", "Datei-Import", "IMAP-Konto #3", "POP3-Konto #X"). - Spalten = letzte 7 Tage; Zelle zeigt
archived_count, bei IMAP zusätzlich "Soll (delta)".missing:true→ "—" (klar unterschieden von "0", mit Tooltip "Cron nicht gelaufen").enough_data:false→ Zeile zeigt "Noch nicht genug Daten" (colSpan).alert:true→ rotes Badge "Auffällig", sonst "OK". Zusätzliche Spalten "Ø 7 Tage" und "Status". - CSV-Export-Button (30 Tage) und Aktualisieren-Button im Kachel-Header.
- Horizontal scrollbar (
overflow-x-auto) für mobile Breiten.
Einbindung / Rollen-Sichtbarkeit
- Gerendert innerhalb
DashboardTab(vor "Benutzerübersicht"). Der gesamte Admin-Bereich (src/app/admin/page.tsx) ist bereits viauseAuth("domain_admin", "/admin/login")auf domain_admin+ beschränkt → keine zusätzliche Client-Gate nötig, normale User erreichen den Tab nicht. Backend bleibt maßgebliche Sicherheitsgrenze (tenant-gescoped,authAdmin); superadmin sieht alle Quellen, domain_admin nur eigene.
Verifikation
npx tsc --noEmitfehlerfrei. Kein direktesfetch()in Komponenten außer dem zentralen Blob-Download-Helper in der API-Schicht.
Geänderte/neue Dateien
- neu:
src/lib/api/reconciliation.ts - neu:
src/components/admin/tabs/ReconciliationCard.tsx - geändert:
src/lib/api/index.ts(Re-Exports) - geändert:
src/components/admin/tabs/DashboardTab.tsx(Kachel eingebunden)
Tech Design (Solution Architect)
To be added by /architecture
QA Test Results
Getestet: 2026-07-04 auf Testserver 192.168.1.132 (Binary v0.9.1, deployt 2026-07-03).
Test-Accounts: qa-superadmin (superadmin), qa-da-t1 (domain_admin Tenant 1),
qa-da-t3 (domain_admin Tenant 3) — dedizierte QA-User, Passwörter für den Test gesetzt
(siehe Testdaten-Hinweis unten). Gesamtergebnis: BESTANDEN (alle testbaren AC grün,
1 Minor-Beobachtung).
Ergebnis je Acceptance Criterion
- Täglicher Job pro Tag/Quelle — BESTANDEN.
archivmail reconcile --date 2026-07-03läuft, berechnet pro Bucket (smtp,imap:<id>,import) die Tages-Zahl. Ausgabe:reconcile: complete days=1 threshold_pct=50 anomalies_total=1. - IMAP Soll/Ist — BESTANDEN (mit dokumentierter Spec-Abweichung).
expected_countist der SUM(last_uid)-Proxy pro Konto (kein zusätzl. IMAP-Login), z.B. imap:1 exp=2288, imap:4 exp=116371.deltawird negativ ausgewiesen. Verhalten entspricht Implementation Note "IMAP Soll/Ist (Abweichung von der Spec)". - Persistenz
reconciliation_reports— BESTANDEN. Tabelle enthält nach Lauf 12 Zeilen für 2026-07-03 mit allen Spalten (date, tenant_id, source_type, source_id, expected_count, archived_count, delta). NULL-fähige tenant_id/source_id-Buckets korrekt getrennt. - Admin-Dashboard-Kachel — Frontend-Komponente vorhanden (
ReconciliationCard.tsx); API liefert die vom UI erwartete Struktur (siehe API-Tests). UI visuell nicht separat durchgeklickt, aber Datenkontrakt verifiziert. - Abweichung > Schwellenwert → Alert + Audit — BESTANDEN. Reconcile erkannte Anomalie:
source=import date=2026-07-03 archived=0 avg_7d=14.1 threshold=50%. Audit-Log enthältevent_type=reconciliation_anomaly(2 Einträge). Schwelle 50% aus Config greift. - CSV-Export — BESTANDEN.
GET /api/admin/reconciliation/export.csv?days=7liefert Headerdate,tenant_id,source,expected_count,archived_count,delta+ Zeilen. Audit-Eintragevent_type=exporterzeugt. - 0-Mail-Tage explizit als 0 — BESTANDEN. Tage ohne Aktivität stehen als
archived_count:0(nichtmissing), fehlende Cron-Läufe als"missing":true(z.B. 2026-07-04 noch nicht gelaufen). Unterscheidung 0 vs. fehlend sauber.
Sicherheit / Tenant-Isolation
- Auth-Bypass: Ohne Cookie liefern
/api/admin/reconciliationund/export.csvbeide401 {"error":"missing authorization"}. BESTANDEN. - Tenant-Scoping: domain_admin Tenant 1 sieht nur Tenant-1-Quellen (imap:1, imap:2, import,
smtp — alle tenant_id 1); domain_admin Tenant 3 sieht nur Tenant-3-Quellen (imap:4, imap:5,
import) und KEINE Tenant-1/2-Daten. superadmin sieht alle inkl. tenant_id=null-Buckets.
Cross-Tenant-Leck: keins. BESTANDEN. Kein
{id}-Pfadparameter → kein IDOR-Vektor.
Minor-Beobachtung (kein Blocker)
- Severity: Low.
days-Parameter-Clamping asymmetrisch:days=90gibt 90 zurück (Max ok), aberdays=91/days=999fällt still auf Default 7 zurück statt auf das Maximum 90 zu clampen. Erwartbarer wäre Clamp auf 90. Reine UX-Feinheit, kein Sicherheits-/Datenproblem. Repro:GET /api/admin/reconciliation?days=91→{"days":7,...}.
Testdaten-Hygiene
- Passwörter der 3 dedizierten
qa-*-Accounts wurden für den Test auf einen bekannten Wert gesetzt (bcrypt cost 12, direkt inusers.password_hash). Keine produktiven/echten Admin-Accounts angefasst. Kein Server auf 131 berührt. Temp-Datei/tmp/qa.sqlentfernt. - Der manuelle
reconcile-Lauf schrieb reguläre Report-/Audit-Zeilen für 2026-07-03 (echte Produktivdaten des Testservers) — beabsichtigt, keine Testverschmutzung.
Deployment
Deployt: 2026-07-04 auf Produktivserver 192.168.1.131 via update.sh (Commit 4c92587,
zusammen mit PROJ-43 im selben Deploy). Enthält den QA-Fix für das Minor-Finding
"days-Clamping" (internal/api/reconciliation_handlers.go: days>90 wird jetzt auf 90
geclampt statt still auf den Default 7 zurückzufallen).
- Backend ✓ läuft (
archivmail, Health/api/health→{"status":"ok","version":"0.9.1"}) - Frontend ✓ läuft (
archivmail-web) - Smoke-Test:
GET /api/admin/reconciliationohne Auth →401(Endpoint erreichbar, Auth-Gate aktiv). Rollen-/Funktionstest ist QA-Scope, nicht Teil dieses Deploy-Checks. - Cron-Eintrag für
archivmail reconcilebereits vorhanden in/etc/cron.d/archivmail(Zeile "Vollständigkeits-Reconciliation (PROJ-52)", nachts 04:10 Uhr, eingespielt viaupdate.shSchritt "Spiele Cron-Jobs ein..."). Kein offener Handoff-Punkt mehr — der ursprünglich genannte Cron-Handoff wurde bereits in einem früheren Commit (e91206c chore(PROJ-52): Cron-Job für Reconciliation in /etc/cron.d/archivmail ergänzen) erledigt und beim heutigen Deploy verifiziert (Datei auf 131 geprüft, Zeile vorhanden).