Files
archivmail/features/PROJ-52-vollstaendigkeits-reconciliation.md
T
sysopsandClaude Sonnet 5 be93614c9f feat(PROJ-52): Vollständigkeits-Reconciliation (Zähl-Report Mailserver vs. Archiv)
Täglicher Cron-Job (archivmail reconcile) berechnet pro Tenant/Quelle
(SMTP-Journal, IMAP-Konto, POP3-Konto, Datei-Import) archivierte Mail-Zahlen,
für IMAP zusätzlich einen Soll/Ist-Vergleich via UID-Tracking. Abweichungen
über Schwellenwert erzeugen Audit-Log-Warnung. Neue Admin-Dashboard-Kachel
"Vollständigkeits-Check" (letzte 7 Tage, Warn-Badge, CSV-Export).

Schließt die "teilweise erfüllt"-Lücke bei Vollständigkeit im
GoBD/DSGVO-Compliance-Check (VOI-Grundsatz 2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 22:48:42 +02:00

225 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROJ-52: Vollständigkeits-Reconciliation (Zähl-Report Mailserver vs. Archiv)
## Status: In Review
**Created:** 2026-06-13
**Last Updated:** 2026-07-03
## 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_at` am 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_pct` in `config.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 `0` ausgewiesen, 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_count` sinkt, `archived_count` bleibt hoch → `delta` negativ in "gute" Richtung,
kein Alert (nur Rückgang von `archived_count` selbst 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 <Datum>" 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)
---
<!-- Sections below are added by subsequent skills -->
## Implementation Notes (Backend, 2026-07-03)
### Neues Package `internal/reconciliation/`
- `reconciliation.go`: `Store` (eigener pgxpool), `initSchema()`, Tabelle
`reconciliation_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))`, weil `tenant_id`/`source_id` NULL-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 aus `emails` DISTINCT, 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-safe `IS NOT DISTINCT FROM`);
Alert nur bei ≥7 Vortages-Datensätzen; `archived < avg*(1-pct/100)`
Audit-Eintrag `reconciliation_anomaly` (event via `audit.EventReconciliationAnomaly`).
- `query.go`: `DashboardData()` (letzte N Tage pro Quelle, tenant-gescoped,
Alert-Flag + `enough_data`) und `ExportRows()` (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`, nil
- `internal/imap/importer.go` + `internal/imap/scheduler.go``imap`,
account_id (accountID durch `fetchBatch`/`fetchSyncBatch`/`storeAndIndex`
durchgereicht)
- `internal/pop3/importer.go``pop3`, account_id
- `cmd/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)
```json
{
"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) — CSV
`date,tenant_id,source,expected_count,archived_count,delta`, Audit-Eintrag
`export`. 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 reconcile` muss in `install.sh`/`update.sh`
ergänzt werden (devops-deploy).
- Frontend: Dashboard-Kachel + TS-Typen in `src/lib/api/`.
- Kein lokaler `go build` mö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`/`delta` sind
`number | null`, `missing: boolean`).
- `getReconciliation(days = 7)` → `GET /api/admin/reconciliation?days=7` über den
bestehenden `request<T>`-Wrapper (`core.ts`, credentials/401-Handling inklusive).
- `exportReconciliationCSV(days = 30)` → direkter `fetch` auf
`/api/admin/reconciliation/export.csv` (Blob-Download, Content-Disposition-
Dateiname-Parsing, analog `downloadMailAttachment`/`exportDSGVORequestPDF`).
- Re-Exports in `src/lib/api/index.ts` ergänzt.
### Neue Kachel `src/components/admin/tabs/ReconciliationCard.tsx`
- Self-fetching Client-Komponente (`useEffect` beim 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 <expected> (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 via
`useAuth("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 --noEmit` fehlerfrei. Kein direktes `fetch()` 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
_To be added by /qa_
## Deployment
_To be added by /deploy_