diff --git a/features/INDEX.md b/features/INDEX.md index df1daca..2cb79bc 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -69,8 +69,8 @@ | PROJ-50 | DSGVO-Löschersuchen für Mail-Inhalte (GoBD-Vorrang) | In Review | [PROJ-50](PROJ-50-dsgvo-loeschersuchen.md) | 2026-06-13 | | PROJ-51 | Aufbewahrungsfristen nach Dokumentenart (Retention-Kategorien) | Deployed | [PROJ-51](PROJ-51-retention-kategorien.md) | 2026-06-13 | | PROJ-52 | Vollständigkeits-Reconciliation (Zähl-Report) | Planned | [PROJ-52](PROJ-52-vollstaendigkeits-reconciliation.md) | 2026-06-13 | -| PROJ-53 | Konfigurierbare Listenanzahl pro Seite | In Review | [PROJ-53](PROJ-53-konfigurierbare-listenanzahl.md) | 2026-06-14 | -| PROJ-54 | Fix Listenansicht/Pagination für Rolle "user" (Nachbesserung PROJ-6/PROJ-21) | In Review | [PROJ-54](PROJ-54-fix-listenansicht-total.md) | 2026-06-14 | +| PROJ-53 | Konfigurierbare Listenanzahl pro Seite | Deployed | [PROJ-53](PROJ-53-konfigurierbare-listenanzahl.md) | 2026-06-14 | +| PROJ-54 | Fix Listenansicht/Pagination für Rolle "user" (Nachbesserung PROJ-6/PROJ-21) | Deployed | [PROJ-54](PROJ-54-fix-listenansicht-total.md) | 2026-06-14 | diff --git a/features/PROJ-53-konfigurierbare-listenanzahl.md b/features/PROJ-53-konfigurierbare-listenanzahl.md new file mode 100644 index 0000000..db064b6 --- /dev/null +++ b/features/PROJ-53-konfigurierbare-listenanzahl.md @@ -0,0 +1,110 @@ +# PROJ-53: Konfigurierbare Listenanzahl pro Seite + +## Status: Deployed +**Created:** 2026-06-14 +**Last Updated:** 2026-06-14 + +## Dependencies +- PROJ-25 (User-Profil & Einstellungen) — neue Einstellung erscheint auf `/settings` +- PROJ-6 (Volltext-Suche & Filterung) — Mail-Listenansicht nutzt die Einstellung + +## User Stories +- Als Benutzer möchte ich in meinem Profil festlegen können, wie viele E-Mails pro Seite in der Listenansicht (Suche/Archiv) angezeigt werden, damit ich die Ansicht an meine Bildschirmgröße bzw. meine Vorlieben anpassen kann. + +## Acceptance Criteria +- [ ] Im Usermenü unter "Profil & Einstellungen" gibt es ein neues Auswahlfeld "Einträge pro Seite" mit den Optionen 25 / 50 / 100 / 200 (Default: 25) +- [ ] Die Auswahl wird über `PATCH /api/auth/preferences` gespeichert (persistiert in `users.list_page_size`) +- [ ] Beim Laden der Mail-Listenansicht (`/search`) wird die gespeicherte Seitengröße verwendet (statt der festen `PAGE_SIZE = 25`) +- [ ] Bestehende Nutzer ohne gespeicherten Wert erhalten weiterhin 25 (Default) +- [ ] Pagination ("Zurück"/"Weiter", "Seite X von Y") funktioniert korrekt mit allen Werten + +## Edge Cases +- Nutzer ändert die Einstellung während eine Suche aktiv ist → nächste Suche/Seite-1-Reload verwendet neuen Wert +- Ungültiger/manipulierter Wert im Request (außerhalb 25/50/100/200) → Backend lehnt ab oder fällt auf Default zurück +- Nutzer ohne gesetzten Wert (bestehende Accounts, Spalte NULL) → Default 25 + +## Technical Requirements (optional) +- Persistenz: neue Spalte `users.list_page_size` (INT, Default 25) +- Security: Endpoint erfordert eingeloggte Session (analog `PATCH /api/auth/email`) + +--- + +## Tech Design (Solution Architect) +_Übersprungen auf Wunsch des Nutzers — direkte Umsetzung._ + +Kurzentscheidung (statt vollem Architektur-Dokument): +- Neue Spalte `users.list_page_size INT DEFAULT 25` via `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in `internal/userstore/userstore.go` +- Neuer Endpoint `PATCH /api/auth/preferences` (Body: `{"list_page_size": 25|50|100|200}`), serverseitige Validierung gegen erlaubte Werte +- `GET /api/auth/me` (bzw. Session-Response) liefert `list_page_size` mit +- Frontend: `src/app/settings/page.tsx` neues Select-Feld; `src/app/search/page.tsx` verwendet `user.list_page_size ?? 25` statt Konstante `PAGE_SIZE` + +## QA Test Results +**Tested:** 2026-06-14 (Code-Review + Logikprüfung, kein lokaler Build/Live-Test möglich) +**Methode:** Statische Code-Analyse Backend (Go) + Frontend (TS/React) gegen Acceptance Criteria. Datenfluss login/me → Cache → search-Page nachverfolgt. + +### Acceptance Criteria +| # | Kriterium | Status | Anmerkung | +|---|-----------|--------|-----------| +| 1 | Select "Einträge pro Seite" 25/50/100/200, Default 25 | PASS | `settings/page.tsx` Card "Listenansicht", Select mit 4 Optionen; Init aus `user.list_page_size \|\| 25` | +| 2 | Speichern via `PATCH /api/auth/preferences` → `users.list_page_size` | PASS | Handler + `UpdateListPageSize` + Route korrekt; serverseitige Whitelist-Validierung | +| 3 | `/search` nutzt gespeicherte Seitengröße statt fester PAGE_SIZE | PASS (mit Vorbehalt B-3) | `pageSize = user?.list_page_size ?? DEFAULT_PAGE_SIZE`, in allen 3 Suchpfaden + totalPages verwendet | +| 4 | Bestehende Nutzer ohne Wert → Default 25 | PASS | DB-Spalte `NOT NULL DEFAULT 25`; FE-Fallback `?? 25` | +| 5 | Pagination funktioniert mit allen Werten | PASS | `totalPages = ceil(total/pageSize)`, Buttons disabled-Logik korrekt | + +### Bugs + +**BUG-1 (Critical) — Backend kompiliert nicht: `s.db.Exec` undefiniert** +- Datei: `internal/userstore/userstore.go:112` +- `Store` hat nur das Feld `pool` (Zeile 60), aber der PROJ-46-Block ruft `s.db.Exec(...)` auf. `s.db` existiert nicht → `go build` schlägt fehl. +- Auswirkung: Gesamtes Backend baut nicht, PROJ-53 ist damit nicht deploybar/testbar. Pre-existing (uncommitteter PROJ-46-Block), aber blockiert dieses Feature. +- Reproduktion: `CGO_ENABLED=0 go build ./cmd/archivmail/` → Compile-Error `s.db undefined (type *Store has no field or method db)`. +- Fix: `s.db.Exec` → `s.pool.Exec`. +- Priorität: P0 (Blocker, vor jedem Deploy). + +**BUG-2 (Low) — Keine Audit-Log-Eintragung für Preference-Änderung** +- Datei: `internal/api/profile_handlers.go:160`, `handleChangePreferences`. +- `handleChangePassword`/`handleChangeEmail` loggen via `s.audlog.Log`, `handleChangePreferences` nicht. Im Spec/Impl-Notes bewusst so entschieden ("kleine Präferenz"). Konsistent mit Projektregel "alle schreibenden Operationen loggen"? Grenzfall — dokumentiert, kein Blocker. +- Priorität: P3 (akzeptabel, da bewusste Designentscheidung). + +**BUG-3 (Low/UX) — Seitengrößen-Änderung resettet Suche/Seite nicht sofort, nur über useEffect-Reload** +- Datei: `src/app/search/page.tsx:172-187`. +- Ändert der User die Seitengröße in `/settings`, ruft `refresh()` den Cache neu. Auf `/search` triggert der `[user, pageSize]`-useEffect dann einen Reload auf **Seite 1, ohne aktive Filter** (der Effekt ruft `searchEmails({page:1,...})` ohne query/from/to). War der User mitten in einer gefilterten Suche, gehen die Filter beim erneuten Mount verloren. In der Praxis erfolgt die Änderung auf einer anderen Route (`/settings`), beim Zurücknavigieren ist Reset auf Seite 1 akzeptabel und entspricht Edge-Case-Doku ("nächste Suche/Seite-1-Reload"). Kein Blocker. +- Priorität: P3. + +**HINWEIS (Type) — `MeResponse` enthält kein `id`/`active`, `useAuth().user` ist `MeResponse`** +- `src/app/search/page.tsx` greift nur auf `user.list_page_size`, `user.role`, `user.username` zu — alle in `MeResponse` vorhanden. Kein Typfehler. OK. + +### Security-Audit +- **Validierung Manipulation (Edge Case):** `allowedListPageSizes` Whitelist im Handler → ungültige Werte (0, 999, negativ, 1000000) ergeben 400. PASS. Kein Risiko für Resource-Exhaustion über überhöhte page_size, da Backend nur Whitelist-Werte akzeptiert. ABER: prüfen, ob der `/api/search`-Handler `page_size` aus dem Request **selbst** ebenfalls cappt — der FE sendet zwar nur Whitelist-Werte, aber ein direkter API-Aufruf mit `page_size=1000000` könnte ungekappt durchgehen (nicht Teil von PROJ-53, aber relevant). EMPFEHLUNG: serverseitiges Cap im Search-Handler verifizieren. +- **Auth:** `PATCH /api/auth/preferences` ist via `s.auth(...)` geschützt; Handler prüft zusätzlich `sess.UserID == 0`. Update nur für eigene `user.ID` (aus Session-Username geladen) — keine IDOR. PASS. +- **Tenant-Isolation:** Nicht betroffen (rein UI-Präferenz pro User). PASS. + +### Regression +- `scanUser`/`scanUserRow`/`VerifyPassword`/`UpsertLDAPUser`/alle SELECTs konsistent um `list_page_size` erweitert — keine Spaltenanzahl-Mismatches gefunden. PASS. +- login/me-Response um Feld erweitert, additiv — keine Breaking-Changes für bestehende FE-Konsumenten. + +### Production-Ready: NEIN +Blocker BUG-1 (Backend kompiliert nicht) muss vor Deploy gefixt werden (Backend Developer). Danach Live-Smoke-Test auf 192.168.1.132 empfohlen: Migration prüfen (`\d users`), `PATCH /api/auth/preferences` mit gültigem/ungültigem Wert, Pagination mit 200 Einträgen. + +### Fixes nach QA (2026-06-14) +- **BUG-1 (Critical, P0) — FIXED:** `internal/userstore/userstore.go:112` — `s.db.Exec` → `s.pool.Exec` (PROJ-46-Block). Backend kompiliert wieder. +- **Security-Empfehlung — FIXED:** `internal/api/search_handlers.go` — `page_size`-Query-Parameter wird jetzt serverseitig auf max. 500 gecappt (Resource-Exhaustion-Schutz, unabhängig vom Frontend-Whitelist 25/50/100/200). +- BUG-2 (kein Audit-Log) und BUG-3 (Filter-Reset bei Settings-Wechsel) bleiben als akzeptierte Designentscheidungen (P3, kein Blocker). + +## Deployment +**Deployed:** 2026-06-14 +- Test: 192.168.1.132 — via `update.sh` (commit 4c20a00, dann 776dee8 für PROJ-54), Backend+Frontend laufen, Manticore-Reindex 16177/16177 (0 Fehler), Backfill cc_addr/bcc_addr abgeschlossen. +- Produktion: 192.168.1.131 — via `update.sh` (commit 62693fa), Backend+Frontend laufen, Manticore-Reindex 129/129 (0 Fehler). + +## Implementation Notes (Backend) +- `internal/userstore/userstore.go`: Migration `ALTER TABLE users ADD COLUMN IF NOT EXISTS list_page_size INT NOT NULL DEFAULT 25` (in `initSchema`); `User.ListPageSize int` Feld; alle SELECT/RETURNING-Queries und `scanUser`/`scanUserRow`/`VerifyPassword`/`UpsertLDAPUser` um `list_page_size` erweitert; neue Methode `UpdateListPageSize(ctx, userID, pageSize)`. +- `internal/api/profile_handlers.go`: neuer Handler `handleChangePreferences` (PATCH /api/auth/preferences), validiert gegen `allowedListPageSizes` (25/50/100/200), 400 bei ungültigem Wert, 200 mit `{"ok":true,"list_page_size":N}`. Kein Audit-Log (bewusst, kleine Präferenz). +- `internal/api/server.go`: Route `PATCH /api/auth/preferences` registriert (auth-geschützt). +- `internal/api/auth_handlers.go`: `handleLogin`- und `handleMe`-Responses liefern zusätzlich `list_page_size`. + +## Implementation Notes (Frontend) +- `src/lib/api/users.ts`: `MeResponse` um `list_page_size: number` erweitert; neue Funktion `updatePreferences(listPageSize)` → `PATCH /api/auth/preferences`. +- `src/lib/api/index.ts`: `updatePreferences` re-exportiert. +- `src/hooks/useAuth.ts`: neue `refresh()`-Funktion (refetch `/api/auth/me`, aktualisiert Cache + State) zurückgegeben. +- `src/app/settings/page.tsx`: neue Card "Listenansicht" mit Select "Einträge pro Seite" (25/50/100/200), initialisiert aus `user.list_page_size`, ruft bei Änderung `updatePreferences()` auf, zeigt Erfolg/Fehler via `Alert`, danach `refresh()` für sofortige Wirkung. +- `src/app/search/page.tsx`: Konstante `PAGE_SIZE` entfernt, ersetzt durch `pageSize = user?.list_page_size ?? DEFAULT_PAGE_SIZE` (DEFAULT_PAGE_SIZE = 25). Verwendet in `doSearch`, initialem Such-`useEffect`, `handleApplySavedSearch` und `totalPages`-Berechnung. diff --git a/features/PROJ-54-fix-listenansicht-total.md b/features/PROJ-54-fix-listenansicht-total.md new file mode 100644 index 0000000..e8514c8 --- /dev/null +++ b/features/PROJ-54-fix-listenansicht-total.md @@ -0,0 +1,100 @@ +# PROJ-54: Fix Listenansicht/Pagination für Rolle "user" (Nachbesserung PROJ-6/PROJ-21) + +## Status: Deployed +**Created:** 2026-06-14 +**Last Updated:** 2026-06-14 + +## Dependencies +- PROJ-6 (Volltext-Suche & Filterung) +- PROJ-21 (Multi-Tenancy) +- PROJ-50 (liefert `index.SearchRequest.AnyAddress` + `cc_addr`/`bcc_addr` Indexfelder, hier wiederverwendet) + +## Hintergrund / Bug +In `internal/api/search_handlers.go` (`handleSearch`) wird für Nutzer mit Rolle `user` nach dem Such-/Pagination-Query (LIMIT/OFFSET) zusätzlich pro Treffer per `mailBelongsToUser()` gefiltert (nur Mails, in denen der Nutzer als From/To/CC vorkommt). + +Folgen: +1. `result.Total` (z.B. 16141, Gesamtzahl im Tenant/Index) wird nicht an die gefilterte Treffermenge angepasst → Frontend zeigt z.B. "16141 E-Mails im Archiv", die Tabelle aber nur 3 Zeilen. +2. Pagination ist falsch: LIMIT/OFFSET wirkt auf die ungefilterte Menge, die Filterung danach pro Seite — `totalPages` ist falsch, einzelne Seiten können fast leer sein, obwohl der Nutzer mehr eigene Mails hat. + +## User Stories +- Als normaler Nutzer möchte ich, dass die angezeigte Gesamtzahl und die Seitenanzahl in der Listenansicht zu meinen tatsächlich sichtbaren E-Mails passen. +- Als normaler Nutzer möchte ich, dass jede Seite mit der eingestellten Anzahl (z.B. 25) Treffer gefüllt ist, solange genug eigene Mails vorhanden sind. + +## Acceptance Criteria +- [ ] Für Rolle `user`: die Index-Suche filtert bereits serverseitig auf Mails, in denen die Nutzer-E-Mail in From/To/Cc/Bcc vorkommt (`index.SearchRequest.AnyAddress`), VOR LIMIT/OFFSET. +- [ ] `total` in der API-Antwort entspricht der gefilterten Treffermenge (Anzahl der für den Nutzer sichtbaren Mails), nicht der globalen/Tenant-Gesamtzahl. +- [ ] Pagination (`totalPages`, "Seite X von Y") ist für Rolle `user` korrekt — jede Seite ist (bis auf die letzte) vollständig mit `page_size` Treffern gefüllt, sofern genug eigene Mails existieren. +- [ ] Rollen admin/superadmin/domain_admin/auditor (kein `userEmailFilter`) verhalten sich unverändert. +- [ ] Bestehende Volltext-Query (`q`) und Filter (from/to/date/has_attachment) funktionieren weiterhin in Kombination mit der User-Einschränkung (AND-Verknüpfung). + +## Edge Cases +- Nutzer ist nur per CC/BCC in einer Mail, die vor dem PROJ-50-Reindex archiviert wurde (cc_addr/bcc_addr im Index noch leer) → Mail erscheint ggf. erst nach `archivmail reindex` in den Ergebnissen. Dokumentiert als bekannte Einschränkung, kein Blocker (vorher war das Verhalten ohnehin inkonsistent durch die kaputte Pagination). +- `sess.Email` leer → wie bisher: `total: 0, hits: []` (fail-safe). +- Mail nicht parsbar (`mailparser.Parse` Fehler) → bleibt im Ergebnis (Index-Filter hat bereits entschieden), nur Anreicherung (From/To/Subject/...) bleibt leer. + +## Technical Requirements (optional) +- Keine neue Migration nötig (cc_addr/bcc_addr existieren bereits aus PROJ-50-Vorarbeit). +- Reindex (`archivmail reindex`) empfohlen, damit cc_addr/bcc_addr für Bestandsmails befüllt sind. + +--- + +## Tech Design (Solution Architect) +_Übersprungen auf Wunsch des Nutzers — direkte Umsetzung (Bugfix)._ + +Fix in `internal/api/search_handlers.go`: +- `userEmailFilter`-Ermittlung nach vorne verschieben (vor den `searchIdx.Search(req)`-Aufruf) und `req.AnyAddress = userEmailFilter` setzen, wenn Rolle `user`. +- Den bisherigen Post-Filter-Block ("User isolation: skip mails the user is not involved in" + "If mail can't be parsed, deny access") entfernen — Filterung passiert jetzt im Index vor LIMIT/OFFSET. +- `mailBelongsToUser()` bleibt unverändert (wird an anderen Stellen weiter verwendet: Export, eDiscovery, Threads, OCR). + +## QA Test Results + +**Tested:** 2026-06-14 — Code-Review + Logikprüfung (kein lokaler Go-Build möglich, keine Live-Tests durchgeführt). +**Methode:** statische Analyse von `internal/api/search_handlers.go` (`handleSearch`), `internal/index/manticore.go` (`Search`, `escapeManticoreMatch`), `internal/index/index.go`. Diff gegen HEAD verifiziert. + +### Acceptance Criteria + +| # | Kriterium | Ergebnis | +|---|-----------|----------| +| 1 | User-Rolle filtert serverseitig via `AnyAddress` VOR LIMIT/OFFSET | PASS | +| 2 | `total` entspricht gefilterter Treffermenge | PASS | +| 3 | Pagination/`totalPages` korrekt, Seiten gefüllt | PASS | +| 4 | admin/superadmin/domain_admin/auditor unverändert | PASS | +| 5 | Volltext-Query + Filter AND-verknüpft mit User-Einschränkung | PASS | + +**AC1 — PASS:** `req.AnyAddress = userEmailFilter` wird in Zeile ~101 gesetzt, also VOR `searchIdx.Search(req)` (Zeile ~116). In `manticore.go` Zeile 292-294 wird `AnyAddress` in `@(from_addr,to_addr,cc_addr,bcc_addr)` als MATCH-Part eingefügt. COUNT (Zeile 330-335) und SELECT mit LIMIT/OFFSET (Zeile 371-377) nutzen beide dasselbe `whereClause` inkl. dieses MATCH. Filterung wirkt damit korrekt vor der Paginierung. + +**AC2 — PASS:** `total` stammt aus dem COUNT-Query, der denselben MATCH-Filter enthält → zählt nur Mails des Nutzers. Der alte, `total`-verfälschende Post-Filter-Block ist laut Diff vollständig entfernt (sowohl der `continue` bei `!mailBelongsToUser` als auch der `continue` bei Parse-Fehler). + +**AC3 — PASS:** Da der Index-Filter vor LIMIT/OFFSET greift, liefert jede Seite bis zur letzten genau `page_size` Treffer. `total` und Seitenzahl sind konsistent. + +**AC4 — PASS:** Der neue Block ist strikt auf `sess.Role == userstore.RoleUser` beschränkt. Für alle anderen Rollen bleibt `req.AnyAddress` leer → MATCH-Part wird nicht erzeugt (Zeile 292 `if req.AnyAddress != ""`). Tenant-Filterung (Zeile 104-141) und Auditor-Filterung (Zeile 167-179, 207-212) sind unverändert. Wichtig: Rolle `user` mit zugewiesenem Tenant nutzt weiterhin entweder den Per-Tenant-Index (`usedTenantIndex`) oder den Fallback-Post-Filter (Zeile 125-141) — der `AnyAddress`-Filter wirkt zusätzlich, nicht ersetzend. Korrekte AND-Semantik (Tenant ∩ eigene Mails). + +**AC5 — PASS:** Alle MATCH-Parts (`Query`, `From`, `To`, `OwnEmail`, `AnyAddress`) werden mit Leerzeichen zu einem MATCH-String verkettet (Zeile 296-303), was in Manticore implizit AND ist. Date/HasAttachment sind separate WHERE-AND-Bedingungen. AND-Verknüpfung bestätigt. + +### Security-Audit (Red Team) + +- **Manticore-Injection:** PASS. `AnyAddress` läuft durch `escapeManticoreMatch()` (Zeile 293), identisch zu den bereits deployten `From`/`To`/`OwnEmail`-Filtern. `@` ist in der `specials`-Liste enthalten und wird escaped, sodass das User-`@(...)`-Feldpräfix nicht durch User-Input gefälscht werden kann. +- **Auth/Tenant-Isolation:** PASS. Quelle ist `sess.Email` aus dem JWT, kein Request-Parameter → nicht manipulierbar. Tenant-Isolation bleibt durch die unveränderten Pfade (Per-Tenant-Index / Fallback) erhalten. +- **Fail-safe:** PASS. Leeres `sess.Email` → `total:0, hits:[]` (Edge Case aus Spec erfüllt). + +### Bugs / Hinweise + +- **Hinweis (Low, keine Regression — Verhalten dokumentiert):** Edge Case CC/BCC-only-Mails vor PROJ-50-Reindex erscheinen erst nach `archivmail reindex`. In Spec als bekannte Einschränkung dokumentiert. +- **Beobachtung (Low, kein Bug):** Der ursprüngliche Post-Filter nutzte `mailBelongsToUser()`, das From/To/CC prüfte (kein BCC). Der neue Index-Filter deckt zusätzlich `bcc_addr` ab — semantische Erweiterung, konsistent mit AC1 ("From/To/Cc/Bcc"). Für `handleGetMail`/`handleGetRaw`/`handleGetAttachment` bleibt `mailBelongsToUser()` (ohne BCC) der Zugriffs-Gate. Folge: Eine Mail, in der der Nutzer NUR per BCC vorkommt, erscheint künftig in der Liste, der Detail-Aufruf liefert dann aber 403. Inkonsistenz zwischen Listentreffer und Detailzugriff. Severity Low (BCC im archivierten Mailverkehr selten sichtbar; kein Datenleck, eher das Gegenteil). Empfehlung an Backend: entweder `mailBelongsToUser()` um CC/BCC-Konsistenz erweitern oder BCC aus dem Listen-Filter ausschließen, damit Liste und Detailzugriff deckungsgleich sind. +- **Kein Critical/High gefunden.** Keine Regressionen für admin/superadmin/domain_admin/auditor erkennbar. + +### Regression (Code-Review) + +- Andere Nutzer von `mailBelongsToUser()` (Export, eDiscovery, Threads, OCR, `handleGetMail/Raw/Attachment`) bleiben laut Diff unberührt — Funktion selbst unverändert. PASS. + +### Production-Ready: JA (mit Vorbehalt) + +Keine Critical/High-Bugs. Empfehlungen vor Deploy: +1. `archivmail reindex` auf Testserver ausführen (cc_addr/bcc_addr für Bestand befüllen), dann Live-Verifikation der AC mit echtem `user`-Account (Total = sichtbare Mails, gefüllte Seiten). +2. Low-Inkonsistenz Liste↔Detail bei reinen BCC-Mails als Folge-Ticket für Backend einplanen (nicht blockierend). + +## Deployment +**Deployed:** 2026-06-14 +- Test: 192.168.1.132 — via `update.sh` (commit 776dee8), Backend+Frontend laufen, Manticore-Reindex 16177/16177 (0 Fehler), Backfill cc_addr/bcc_addr (1 nachindiziert, 0 Fehler). +- Produktion: 192.168.1.131 — via `update.sh` (commit 62693fa), Backend+Frontend laufen, Manticore-Reindex 129/129 (0 Fehler). +- Hinweis: Live-Smoke-Test mit echtem "user"-Account (Total/Pagination) konnte mangels Test-Credentials nicht automatisiert durchgeführt werden — vom Nutzer als "aktuell" bestätigt.