- common/httpclient: bisher byte-identisch in dms, mail und archive
- common/apierror: bisher byte-identisch in Root und archive
- Importpfade, go.mod (require + replace), archtest-Ausnahmen und
CI-Pfadfilter der mail-/archive-Pflichttest-Gates angepasst
- Waechtertest fuer den Standard-Client prueft weiter dms, mail, archive
- dms/internal/apierror bleibt eigenstaendig (5xx-Fix in eigenem PR)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fable-Review auf Code-Dopplung/interne Links (siehe Analyse). P0-Funde:
- Dangling Prosa-Verweise auf die per PR #367 geloeschten *-kanban/-Ordner
in SKALIERUNGSKONZEPT.md, STORAGE-KONZEPT.md, MIGRATIONSKONZEPT.md,
SICHERHEITSKONZEPT.md (7x "siehe X-kanban/README.md"), PRODUKTARCHITEKTUR.md
um Hinweis ergaenzt, dass Boards jetzt in main-Historie/Gitea-PRs liegen.
- dms/internal/searchsync/searchsync_test.go:19: falscher Pfad im Kommentar
("internal/bulkedit"/"internal/jobqueue" statt "dms/internal/...").
- web/shl/components/ModuleNavShell.tsx: resolveHrefClientOnly war inhaltlich
identisch zu resolveModuleHref in web/portal/app/ModuleGrid.tsx (zwei
unabhaengig gepflegte Kopien derselben Hostname-Ersetzung). Als
resolveModuleHref aus shl exportiert, ModuleGrid.tsx nutzt jetzt die
gemeinsame Funktion, Test-Import entsprechend umgestellt.
- web/portal/lib/modules.test.ts: KNOWN_REAL_PERMISSIONS-Whitelist hatte
kennzahlen.read (migrations/0019_kennzahlen_policy_rule.up.sql, real
existierend seit ANA-02/PR #362) nie nachgetragen, Test lief seitdem
unbemerkt im Fehler. Ergaenzt.
go test/vitest: web/portal (15/15) und web/shl (61/61) gruen auf Testhost 131.
Verbleibender P0-Punkt (14 Web-Apps ohne ModuleNavShell) braucht
Produktentscheidung -- mehrere Registry-Keys (ai/mail/workflow/connect)
sind bereits durch eine jeweils ANDERE App belegt als die ohne Navigation
(z.B. mail-search hat "mail", mail-postfach nicht), daher keine mechanische
Zuordnung moeglich ohne zu raten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Groesster bisheriger Konflikt dieser Merge-Runde (4 Dateien, davon 2
add/add: MessageDetail.tsx/test.tsx existierten bereits als UX-04-
Platzhalter, UX-02 ersetzt sie durch die echte Implementierung mit
Thread-Gruppierung/Anhang-Download):
- mail/internal/mailapi/mailapi.go + openapi.yaml: beide Branches
implementierten unabhaengig denselben Anhangslisten-Endpunkt
(UX-03/UX-02), Kommentare zusammengefuehrt, keine Logikaenderung.
- mail/internal/mailapi/mailapi_test.go: dadurch doppelte Tests +
doppelte realPDFBytes/realPNGBytes-Konstante automerged, die
schwaechere Kopie entfernt (reichere Variante mit Size-Pruefung behalten).
- components/MessageDetail.tsx/test.tsx: UX-04-Platzhalter durch
UX-02s vollstaendige Implementierung ersetzt (neue Props:
messageId/messages/onSelectMessage/onClose statt message/onBack).
- components/MessageList.tsx/test.tsx: UX-02s natives <button>-Pattern
(Tastaturaktivierbarkeit ohne eigene Keydown-Logik) uebernommen,
Prop onSelect -> onSelectMessage umbenannt, Tests entsprechend
angepasst (user-event fuer echte Enter-Taste-Simulation).
- app/page.tsx: UX-04s responsives Layout (Drawer/mobileView) als
Grundlage behalten, UX-02s Detail-Wiring (messageId-State,
MessageDetail-Props) eingebaut.
- lib/messages.ts: mail.detail.*-Schluessel fehlten auf Englisch,
nachuebersetzt statt nur dupliziert.
- app/page.test.tsx: fetchMessageDetail/fetchAttachments mussten in
allen Tests ein aufgeloestes Promise liefern (MessageDetail ruft sie
jetzt immer bei Auswahl auf), nicht nur als Export existieren.
go build/vet sauber, go test ./internal/mailapi/...: alle gruen bis auf
den bereits vor diesem Merge auf main bestehenden Testfehler
(anhang_download_erfolg, siehe PR #298). npx vitest run: 82/82 Tests
gruen (web/mail-postfach), beides auf Testhost 131 verifiziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konflikt in MessageList.test.tsx: beide Seiten fuegten Tests am
Dateiende an (UX-04 onSelect/Klick/Tastatur-Navigation, QA-08
tabIndex-Fix fuer den scrollbaren Listbox-Container), additiv
zusammengefuehrt.
npx vitest run: 59/59 Tests gruen (web/mail-postfach), auf Testhost 131 verifiziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konflikt in web/mail-postfach/app/layout.tsx: UX-06 (dynamisches
initialLocale) und UX-05 (ThemeToggle im Provider-Baum) aenderten
denselben JSX-Block unabhaengig voneinander. Beide Aenderungen
kombiniert, kein Verhaltensverlust auf beiden Seiten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Keine Konflikte mehr nach Merge von #298 (war nur dagegen gestackt).
Reine Next.js/i18n-Aenderung (web/mail-postfach), kein Go-Code betroffen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konfliktaufloesung:
- mail/internal/mailapi/mailapi_test.go: beide Seiten fuegten neue Tests
an derselben Stelle an (HEAD: UX-03 Magic-Byte-Contenttype-Tests,
eingehend: UX-01 Postfach-/Ordner-Tests), rein additiv zusammengefuehrt
- mail/internal/retentionclient/retentionclient.go: zwei unabhaengige
Register-Implementierungen durch den Merge entstanden (totes
PascalCase-Schema aus HEAD vs. aktiv genutztes snake_case-Schema),
tote Variante + zugehoerige Tests entfernt
- mail/internal/mailapi/contract_test.go + mailapi_test.go: SearchClient-
Interface erweitert (SearchWithFilters/ListByMailbox/Facets), beide
Test-Fakes um diese Methoden ergaenzt
go build/vet sauber, Tests in internal/mailapi und internal/retentionclient
gruen (ein Testfehler in TestContract_DocumentedEndpointsMatchActualBehavior/
anhang_download_erfolg bereits auf main vor diesem Merge vorhanden, nicht
Teil dieser Aenderung).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Alle 8 *-kanban/-Ordner (Tickets, board.csv, board.json) aus main
entfernt. Ticket-Historie bleibt in aelteren Commits erhalten, wird
aber nicht mehr im Arbeitsbaum gepflegt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fable-5.1-Vergleichsreview: die Positivlisten-Pruefung lief bisher nur
gegen die vom Client bei StartUpload angekuendigte Typangabe
(validate.go ValidateMimeType); ein als "application/pdf" deklariertes,
tatsaechlich beliebiges Archiv/Binary wurde erst beim spaeteren
Ausliefern per Magic-Bytes erkannt (documentapi/handler.go), nicht vor
der Aufnahme.
Fix: Complete() liest die ersten 512 Bytes aus dem Staging-Reader, prueft
sie per http.DetectContentType gegen dieselbe Positivliste, BEVOR
Verschluesselung/Storage/DB-Eintrag passieren. Der Praefix wird fuer
Pruefsumme/Verschluesselung wieder vorangestellt (kein Byteverlust).
Echter Testlauf auf Host 204 gegen migrierte Testdatenbank:
go test ./internal/upload/... -v -> 12/12 PASS, davon neu:
TestCompleteUpload_ContentMismatchingDeclaredMimeTypeIsRejected (ZIP-Inhalt
mit deklariertem application/pdf wird jetzt abgelehnt). Ausserdem
go build ./... fuer das gesamte dms-Modul lief fehlerfrei durch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fable-5.1-Vergleichsreview gegen paperless-ngx/Alfresco/Docspell/ecoDMS
fand: die Pruefsummensuche vor dem Anlegen eines neuen Dokuments lief
global ueber alle Dokumente des Mandanten. Ein Treffer gab per
DuplicateError DocumentID/RevisionID eines fremden, fuer den Aufrufer
sonst unsichtbaren Dokuments preis (Existenz-Oracle), bevor irgendeine
Zugriffsrechtepruefung greifen konnte.
Fix: Suche zusaetzlich auf documents.created_by = sess.CreatedBy
eingeschraenkt, konsistent mit dem aktuellen eigentuemerbasierten
Sichtbarkeitsmodell (internal/authz, siehe SEC-08 fuer die geplante
Erweiterung um echte Objektrechte). Zwei Nutzer mit identischem
Dateiinhalt bekommen jetzt je ein eigenes Dokument statt eines
ueberraschenden Verweises auf das Dokument der anderen Person.
Echter Testlauf auf Host 204 gegen migrierte Testdatenbank:
go test ./internal/upload/... -run TestCompleteUpload -v
Alle 7 Tests PASS, inkl. neuem
TestCompleteUpload_DuplicateCheckIsScopedToOwnDocuments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fable-5.1-Strukturreview (MD-Dateien) fand: kein Root-README/Index, daher
niemand findet die Doku-Ebenen (Root-Konzepte -> 8 Boards -> Modul-docs/ ->
docs/). CLAUDE.md verwies auf datenmodell-altsysteme.md, das nirgends im
Repo existiert - auf die vorhandenen known-issues-archivdms/archivmail.md
umgebogen.
Zurueckgestellt bis Nutzerentscheidung (Fable5 hatte dafuer offene Fragen
gestellt): docs/ in core/docs umbenennen, dms-kanban-Sonderdateien
(reuse-mapping.md, known-issues-*.md) verschieben, docs/cut-01/ auslagern,
code*/-Worktrees gitignoren.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
web/portal/config/modules.json war inhaltlich identisch zu MODULE_REGISTRY
in web/shl/components/ModuleNavShell.tsx, separat gepflegt. MODULE_REGISTRY
ist jetzt die einzige Quelle (description/permission als optionale Felder
ergaenzt), web/portal/lib/modules.ts importiert von @nexarch/shl statt der
JSON-Kopie. modules.json geloescht.
Nebenbefund beim Build-Test: @nexarch/shl wurde fuer web/portal per Symlink
statt Kopie installiert, Turbopack konnte den Import dadurch nicht aufloesen
("Module not found"). Fix: npm install --install-links erzwingt eine echte
Kopie wie bei den anderen Modulen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
P0-Fund aus Fable5-Review: shl-table/shl-field* hatten keine CSS-Regeln
(baseComponentStyles in tokens.ts ergaenzt, ueber Shell.tsx eingebunden).
dms-browser und portal hatten ModuleNavShell trotz Registry-Eintrag nicht
eingebunden, jetzt nachgezogen. Hartcodierte Hex-Farben in status-page/
tenant-admin/lic-admin/audit-log durch Tokens ersetzt (dangerSubtle neu).
ModuleNavShell.resolveHref las window.location.hostname beim ersten
Render -> SSR/Client-Mismatch (Hydration-Fehler in React 19). Fix: Href
bleibt beim ersten Render unveraendert, Hostname-Tausch erst in
useEffect nach dem Mount. Verschachteltes <main> in portal/app/page.tsx
entfernt (Shell.tsx rendert bereits eins).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ModuleNavShell/MODULE_REGISTRY existierte bisher nur live auf Host 204,
nie committet - alle 12 Module (ai-extraction-review, ai-summaries,
connect-status, datev-export, dms-upload, mail-search, rbac-admin,
routing-rules, tenant-admin, workflow-approvals, workflow-cases) jetzt
mit ModuleNavShell verdrahtet, inklusive Export-Fix in web/shl/index.ts.
Kennzahlen-Dashboard (ANA-02) neu in Registry/Navigation aufgenommen,
RBAC-Recht kennzahlen.read per Migration an tenant_admin vergeben
(analog 0018_module_policy_rules).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Der automatische Merge von PR #217 (OPS-12) gegen main liess
internal/tenant/provisioner.go::ProvisionWithID mit einem nicht mehr
existierenden Feld (p.dsnTemplate) zurueck und vergass dort den
grantRuntimeRole-Aufruf, den Provision() bereits hat -- echter
Build-Fehler, kein reiner Textkonflikt, git markierte ihn nicht als
Konflikt. Drei weitere Aufrufer von NewProvisioner (Migrationswerkzeug,
Cutover-Orchestrator, Test) riefen noch mit der alten Drei-Parameter-
Signatur auf. Alle auf die neue Vier-Parameter-Signatur (Admin-DSN +
Runtime-DSN) nachgezogen, wo keine eigene Runtime-DSN-Variable existiert
faellt auf dieselbe Vorlage zurueck (admin-only Werkzeuge).
Verifiziert auf Host 204: go build/vet sauber, betroffene Pakete
(tenant, migrationtenant, cutoverorchestrator, cor01-tenant-migrate)
gruen. Einziger verbleibender FAIL (TestAllHTTPServersUseHTTPHarden)
ist der bereits bekannte, unabhaengige Befund aus PR #345 (noch nicht
gemergt), keine neue Regression.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #217 (OPS-12) war nicht mergefaehig: additiver Konflikt in Config{}
-- main hatte inzwischen ModuleDirectoryPath ergaenzt, OPS-12 ergaenzt
unabhaengig davon TenantRuntimeDSNTemplate. Beide Felder behalten,
keine inhaltliche Aenderung an der eigentlichen OPS-12-Logik.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Akteneintrag in CaseOverview.tsx setzte keine explizite Textfarbe,
wodurch im dunklen Farbschema der native Button-Standard (schwarzer
Text auf dunklem Hintergrund) durchschlug: Kontrast 1,11:1 statt
geforderter 4,5:1 (axe-core color-contrast, serious, 6 Knoten).
Fix: color: var(--shl-color-text-primary) auf dem Button, wie bereits
bei anderen Elementen der Oberflaeche ueblich. axe-core bestaetigt 0
Verstoesse hell/dunkel nach dem Fix (siehe workflow-kanban QA-08,
workflow/docs/QA-08-PRUEFPROTOKOLL.md). Regressionstest ergaenzt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vertragstests fuer die GOV-01 Modul-Adapter-Schnittstelle, ueber die DMS
und Mail KI-faehige Objekttypen registrieren. Neues Paket
ai/internal/contracttest prueft Objekttyp- und Feld-Schema gegen die
echte moduletype-Komponente, inklusive Erkennung simulierter Breaking
Changes. CI-Workflow ai-contract-tests.yml fuehrt die Tests automatisiert
bei Aenderungen an contracttest/moduletype aus.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neues Paket ai/internal/qa03gate misst Klassifizierungs- und
Extraktionsgenauigkeit am QA-01-Referenzkorpus real gegen den externen
Ollama-Server und belegt die Wirkung von Korrektur-Feedback auf
kuenftige Vorschlaege (historysuggest), ohne bestehende Pakete zu
aendern. Ergebnisse in ai/docs/QA-03-PRUEFPROTOKOLL.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QA-01 (Teststrategie AI) ist bereits auf main gemergt (Commit 8b18fe0b,
2026-09-26), faelschlich zunaechst als offen behandelt. QA-02 bis
QA-09 bleiben korrekt Backlog -- bestaetigt nicht umgesetzt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #324 (RET-15) war nicht mergefaehig gegen seine Basis (PR #312,
docs/ret-11-nachtrag-chattr-untersuchung): Add/Add-Konflikt in
derselben Datei. Beide Seiten identisch bis auf den von RET-15 neu
angehaengten "RET-15 Nachtrag"-Abschnitt -- Original plus Anhang
behalten, keine inhaltliche Aenderung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestAllHTTPServersUseHTTPHarden schlug auf main real fehl: zwei
Einmal-Testwerkzeuge fuer pa11y-Laeufe (EXT-04, SUM-03) starten
bewusst per rohem http.ListenAndServe ausserhalb httpharden, da sie
keine Produktdienste sind. Begruendete archtest:allow-Ausnahme direkt
an der Fundstelle ergaenzt, kein Verhaltensunterschied im Werkzeug
selbst. Verifiziert: go test ./internal/archtest/... und voller
Testlauf ./... auf Host 204 jetzt gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
archive-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
connect-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mail-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dms-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
migration-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ai-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
core-kanban/ existierte bislang nicht auf origin/main (gleiches
Strukturproblem wie workflow-kanban, siehe #337) -- Board und
Tickets lagen nur in der lokalen Baseline. Nur Board-/Ticketdaten,
kein Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
workflow-kanban/ existierte bislang nirgends auf origin/main (gleiches
Strukturproblem wie bei den Konzeptdokumenten, siehe PR #323) --
Board und Tickets lagen nur in der lokalen Baseline. Board-Status
korrigiert: 24 bereits gemergte Tickets (ENG-01-05, APR-01-04,
TSK-01-03/05, RTE-01-05, VOR-01-06) waren faelschlich als Backlog
markiert, jetzt auf Fertig. 11 diese Sitzung umgesetzte Tickets
(ENG-06/07, TSK-04, QA-01-08) auf Fertig gesetzt (Branches gepusht,
PRs offen, kein Merge). ENG-09 und ENG-11 fehlten trotz gemergtem Code
komplett im Board, jetzt nachgetragen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fester Testkorpus workflow/internal/qa03, der APR-02 (dualcheck), APR-04
(delegation), TSK-03 (escalation) und TSK-05 (escalationnotify) end-zu-ende
gegen echtes Postgres durchspielt: Missbrauchsversuche auf die
Vier-Augen-Freigabe (Ersteller bestaetigt eigene Aufgabe, Selbst-Vertretung),
Zeitraffer-Test ueber simulierte Fristen und der volle Eskalationspfad bis
zum simulierten Core-Dispatcher inklusive Idempotenz. Keine Aenderung an
den geprueften Paketen noetig, alle 8 neuen Tests gruen, volle
Modul-Regression (23 Pakete) ohne Regression.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Festes, synthetisches Testkorpus (11 Faelle, 5 Regeln ueber equals/
contains/in, Prioritaetskonflikt und kombinierte Bedingung je einmal
abgedeckt) in workflow/internal/routingqagate. Drei Gate-Tests fahren den
Korpus gegen die echte Regel-Ausfuehrung (RTE-02) und die Simulation
(RTE-04): Zuordnung stimmt mit Erwartung ueberein, kein Objekt im
Klaerungskorb verloren, Simulation weicht nicht von echter Ausfuehrung ab.
Negativprobe bestaetigt, dass das Gate tatsaechliche Abweichungen auch
erkennt. Keine Produktionslogik veraendert; Ergebnis in
workflow/docs/QA-04-PRUEFPROTOKOLL.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
STORAGE-KONZEPT.md und weitere Konzeptdokumente (PRODUKTARCHITEKTUR.md,
SICHERHEITSKONZEPT.md, MIGRATIONSKONZEPT.md, SKALIERUNGSKONZEPT.md,
SAAS-BETRIEBSMODELL.md, UI-UX-KONZEPT.md) sowie CLAUDE.md lagen bislang
nur in einer lokalen, von origin/main historisch getrennten Branch-Linie
vor. RET-16 (archive-kanban) braucht STORAGE-KONZEPT.md als Zieldatei auf
main. Reiner Dokument-Add, keine Code-Aenderung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Analog zu Core AUD-07 und DMS VER-08: der Anwendungs-Datenbankrolle
(nexarch_app) werden UPDATE/DELETE-Rechte auf workflow_instance_history
entzogen (nur INSERT/SELECT), der bestehende ENG-04-Trigger protokolliert
abgelehnte Aenderungsversuche zusaetzlich per RAISE WARNING.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suche nach einem chattr-faehigen Host im Projektkontext (131, 204
bereits bekannt unprivilegiert; zusaetzlich Host 236 geprueft) ergibt:
kein privilegierter Host verfuegbar. Host 236 ist ebenfalls ein
unprivilegierter LXC-Container (uid_map 0->100000, ZFS-Root), realer
chattr +i-Testversuch auf synthetischer Testdatei schlaegt mit EPERM
fehl, identisch zu 204/131. AC1/AC2 aus RET-15 bleiben damit offen,
dokumentiert als zweiter Nachtrag-Abschnitt in der bestehenden
RET-11-NACHTRAG-CHATTR-UNTERSUCHUNG.md (kein neues Paralleldokument).
Keine Aenderung an wormstore.go.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reale Reproduktion auf Host 204 (chattr, roher FS_IOC_SETFLAGS-Ioctl,
Gegenprobe auf tmpfs im selben Container) zeigt: Ursache ist der
unprivilegierte LXC-Container (capable(CAP_LINUX_IMMUTABLE) prueft
immer gegen den Host-Namespace, nicht gegen den Container), nicht ZFS
wie bisher in RET-11-PRUEFPROTOKOLL.md vermutet - betrifft jedes
Dateisystem im Container. Acht geprueften Alternativen (ZFS readonly/
hold, ACL/chmod, Proxmox-Features, Capabilities, verschachtelte
User-Namespaces, FUSE, externes Object-Lock-Backend) sind im
Container-Scope entweder nicht verfuegbar oder strukturell keine
vollwertige Entsprechung. Keine Aenderung an wormstore.go, da der
bestehende Fail-Closed-Mechanismus (ImmutableLockSupported) bereits
korrekt reagiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sicherheitspruefung von Verschluesselung, Zugriffskontrolle und
Schluesselverwaltung der Connect-Drittsystem-Credentials (Basis: ENG-07,
PR #313, noch unmerged). Dabei Luecke gefunden und behoben: Connect hatte
keinen Rewrap-Mechanismus fuer Tenant-KEK-Rotation (dms/internal/crypto
FDN-09 hat RewrapDEK, Connect nicht) - nach einer Core-Tenant-KEK-Rotation
waeren alle gespeicherten Credentials eines Mandanten dauerhaft nicht mehr
entschluesselbar gewesen. Fix analog DMS: crypto.RewrapDEK +
credential.Store.RewrapTenantKEK, real gegen Postgres-Testdatenbank auf
Host 204 geprueft (Rotation ohne Datenverlust nachgewiesen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consumer-Driven-Contract-Tests fuer das Webhook-Abonnement-Format aus
API-03 (connect/internal/contracttest, Muster wie Core internal/contracttest
und archive/internal/contracttest): Subscribe-Request/-Response-Felder,
Unsubscribe-Pfad und der real beidseitig vorhandene Zustellungs-
Transportvertrag (HMAC-Signaturheader) werden gegen echten Produktionscode
geprueft, nicht nur gegen Annahmen. Breaking-Change-Erkennung zusaetzlich
real bewiesen (Signaturheader-Konstante temporaer auf falschen Wert
gesetzt, Tests schlagen gezielt fehl, Aenderung zurueckgenommen). CI-Workflow
analog contract-tests.yml/archive-contract-tests.yml ergaenzt.
Details und Einschraenkungen (Core-seitiger API-07-HTTP-Wrapper fehlt noch)
in connect/docs/QA-06-PRUEFPROTOKOLL.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dokumentiert Umsetzung, Akzeptanzkriterien, die drei Pruefungen
(Go-Integrationstest, Vitest, echter Browserlauf je Pruefung) und die
getroffenen QA-Entscheidungen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Oberflaeche web/connect-setup (Next.js/React, @nexarch/shl):
Connector-Typ waehlen, Zugangsdaten hinterlegen (API-Key/OAuth2),
Feld-Mapping je Verbindung bearbeiten — vollstaendig ohne
Konfigurationsdatei. Zugangsdaten werden nie im Klartext angezeigt,
nur als feste Maske plus letzte 4 Zeichen.
connect/cmd/setup-a11y-fixture + web/a11y-audit/connect-setup-run.sh
fuehren einen echten Browserlauf (Chrome/puppeteer-core) gegen den
echten Dienst und eine echte Postgres-Wegwerf-DB durch: Formular
tatsaechlich ausgefuellt und abgesendet, Maskierung im DOM geprueft,
Mapping-Aenderung nach Neuladen bestaetigt. axe: 0 Befunde in hell/dunkel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konsolidiertes Pruefgate fuer ERP-, DATEV-, M365- und DMS2-Board: alle 13
Einzelkacheln (ERP-01..04, DAT-01..03, M365-01..03, DMS2-01..03) erneut
geprueft und protokolliert, voller Backend-Testlauf (28/28 Pakete, 0
Skip, 0 Fail) und Frontend-Testlauf (DAT-03, DMS2-02) real gegen die
Testdatenbank qa03_connect_test durchgefuehrt, Fehlerinjektion je
Systemtyp bestaetigt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Je Drittsystemtyp (ERP, Microsoft Graph, DATEV) ein echter Testlauf gegen
den bereits in QA-01 gebauten, fest hinterlegten Referenzkorpus statt nur
gegen ad-hoc erfundene Mock-Antworten. Zusaetzlich ein beim Nachvollzug
der DAT-03-Oberflaeche real gefundener Testfehler (doppelte React-Instanz
durch die file:../shl-Abhaengigkeit) mit dem im Repo bereits etablierten
Fix (Resolve-Alias, siehe web/dms2-import) behoben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
connect/internal/crypto bot bereits Envelope-Encryption mit austauschbarem
KEKProvider-Interface (ENG-02), aber keine konkrete Implementierung, die den
Tenant-KEK tatsaechlich ueber Core API-10 bezieht -- ohne sie haette jede
kuenftige Verdrahtung (M365-01, API-01) frei entscheiden koennen, woher der
KEK kommt. Neuer HTTPKEKProvider (connect/internal/crypto/kekprovider.go)
spiegelt denselben Vertrag wie dms/internal/crypto.HTTPKEKProvider und
archive/internal/kekclient.HTTPProvider (X-Nexarch-Client-Id/-Secret-Header,
tenant-Query-Parameter, JSON-Feldname tenant_kek_base64) -- einzige
sanktionierte KEKProvider-Implementierung fuer Connect statt eines zweiten,
eigenen Schluesselpfads.
Integrationstest in connect/internal/credential/corekek_test.go verdrahtet
credential.Store ueber diese Kette (statt des bisherigen reinen
In-Memory-Fakes) gegen die echte Postgres-Testdatenbank: Zugangsdaten
landen nachweislich nur als Chiffrat in der DB, Entschluesselung mit
abweichendem Tenant-KEK scheitert. Bestehende Connector-Pakete (ERP, CRM,
DATEV, M365, DMS2) unveraendert, komplette Testsuite weiterhin gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll mit allen drei Pruefungen (Credential-Abruf-Latenz, Sync-Job-Queue ohne Verklemmung/Doppelverarbeitung, Rate-Limiting unter Mehrinstanz-Last) und konkreten Messwerten gegen eine echte Postgres-Testdatenbank auf Host 204.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AC3: 9 Connector-Instanzen (ERP/DATEV/M365, je eigenes Limit), je 10 parallele Goroutinen haemmern 3s gleichzeitig auf Limiter.Wait. Ergebnis: alle Instanzen unter ihrem erlaubten Maximum, Limits bleiben unter Last proportional zur Konfiguration eingehalten, unabhaengige Instanzen blockieren sich nicht gegenseitig.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AC2: 300 Sync-Jobs (ERP/DATEV/M365 gemischt), 20 parallele Worker claimen gleichzeitig ueber Store.ClaimDue gegen echte Postgres-Testdatenbank. Ergebnis: 300/300 Jobs exakt einmal geclaimt, keine Verklemmung (Claim-Phase 292ms, 15s-Waechter nicht ausgeloest), 1026.8 Claims/s.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reale Reproduktion auf Host 204 (chattr, roher FS_IOC_SETFLAGS-Ioctl,
Gegenprobe auf tmpfs im selben Container) zeigt: Ursache ist der
unprivilegierte LXC-Container (capable(CAP_LINUX_IMMUTABLE) prueft
immer gegen den Host-Namespace, nicht gegen den Container), nicht ZFS
wie bisher in RET-11-PRUEFPROTOKOLL.md vermutet - betrifft jedes
Dateisystem im Container. Acht geprueften Alternativen (ZFS readonly/
hold, ACL/chmod, Proxmox-Features, Capabilities, verschachtelte
User-Namespaces, FUSE, externes Object-Lock-Backend) sind im
Container-Scope entweder nicht verfuegbar oder strukturell keine
vollwertige Entsprechung. Keine Aenderung an wormstore.go, da der
bestehende Fail-Closed-Mechanismus (ImmutableLockSupported) bereits
korrekt reagiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konsolidiert QA-02 (Retention & WORM), QA-03 (Compliance &
eDiscovery) und QA-04 (Backup & Restore) sowie ergänzend QA-06/QA-07
zu einer Gesamtfreigabeentscheidung für das Archive-Board. Bedingte
Freigabe: WORM-Storage-Object-Lock (chattr +i) bleibt auf allen
bisher geprüften Hosts (131, 204) strukturell nicht nachweisbar
(LXC/ZFS-Infrastrukturgrenze), erneut unabhängig reproduziert;
Anwendung reagiert fail-closed, kein fabriziertes PASS. Keine
Änderung an Produktivcode oder den Einzelprotokollen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RET-03/RET-04 Pflichtpruefungen gegen den Referenzdatensatz
(internal/referenceset) wiederholt: Legal-Hold-Widerstand gegen
Loeschung fuer alle drei Referenzobjekttypen (PASS, 7/7), WORM-
Ueberschreib-/Manipulationspruefung fuer alle drei Groessenklassen
(Code strukturell korrekt, Ausfuehrung auf Host 204 durch bekannte
chattr+i-Infra-Grenze blockiert, siehe RET-11-PRUEFPROTOKOLL.md -
Schreibschutz stattdessen manuell als echter nexarch-Betriebsnutzer
nachgewiesen). Ergebnisse mit Zielwert/Messwert/Bewertung sowie
offene Restrisiken in archive/docs/QA-02-PRUEFPROTOKOLL.md
festgehalten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Detailansicht fuer web/mail-postfach (UX-01-App erweitert, nicht neu
gebaut): Header (Von/Betreff/Ordner/Datum/Nachricht-ID), Nachrichtentext,
Anhangsliste mit Download (Dateiname nicht verfuegbar -- System
persistiert keinen Originalnamen, daher Anhang N + aus dem
Content-Type geratene Endung). Thread-Gruppierung ueber normalisierten
Betreff (keine In-Reply-To/References-Header im System), Navigation
zwischen Thread-Nachrichten als reiner Komponentenzustand derselben
Seite (kein Routenwechsel -- Ordner-/Suchkontext bleibt dadurch
strukturell erhalten, Akzeptanzkriterium 3).
UX-03 (eigener offener Branch, eigenes Frontend web/mail-search) wird
NICHT wiederverwendet -- getrennte Apps ohne Komponentenfreigabe, siehe
UX-02-PRUEFPROTOKOLL.md fuer die Abgrenzung und die Begruendung der
Backend-Erweiterung.
Tests: lib/threading, lib/threadNavigation, lib/attachmentNaming,
components/MessageDetail (Komponententest je Akzeptanzkriterium),
__tests__/a11y (jest-axe, Pruefung 3), e2e/ux02 gegen die echte
mailpostfach-fixture (Pruefungen 1+2, Mandantentrennung). Bestehender
e2e/ux01-Satz bleibt unveraendert gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit-Infrastruktur fuer web/mail-postfach nach dem Muster von
web/a11y-audit (mail-search-run.sh/connect-status-run.sh): neue
Testfixture mit echtem mailapi.Server + Platzhalter fuer den
Core-Modul-Token-Endpunkt (kein echter NEXARCH-Core auf dem
Testhost), Runner-Skript, echter Tastaturfluss im Browser nach dem
keyboard-flow-check.mjs-Muster aus UX-08.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neues Paket mail/internal/qa05gate (Muster QA-03/qagate): prueft real
(1) ob mailrelevante Ereignisse an das Core-Audit-Log gekoppelt sind
(ScanForCoreAuditClient, statischer Scan nach dem CMP-04-Muster /audit/ingest,
plus reale Stichprobe gegen einen echten, frisch gestarteten
cmd/auditlog-devserver) und (2) ob eDiscovery-Tenant-Scoping in der
Query-Schicht (search.Client, von mailexport/ARC-07 fuer den
CMP-03-eDiscovery-Pfad genutzt) real verankert ist.
Basis ist der noch nicht gemergte Branch feature/int-08-archive-anbindung-
objekttyp-registrierung-retention-abfrage (INT-08 existiert nur dort,
nicht auf main) - dokumentiert in mail/docs/QA-05-PRUEFPROTOKOLL.md.
Realer Befund: das Mail-Modul hat anders als archive/connect/ai (CMP-04)
noch keine Kopplung an das Core-Audit-Log - mail_audit_log bleibt lokal.
Das Gate erkennt und dokumentiert das korrekt statt es zu verschweigen;
eDiscovery-Tenant-Scoping ist real bestaetigt und sicher verankert.
Folgeauftrag (mail/internal/auditclient analog zu archive) bewusst nicht
umgesetzt - ausserhalb des Pruefgate-Ticketumfangs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll: Zielwerte und Ergebnisse je Akzeptanzkriterium, inkl.
Rohausgabe des echten Viewport-Resize-/Touch-Laufs und Entscheidungen
(Dialog statt eigenem Drawer-Code, CSS-Breakpoint statt matchMedia,
gefundener/behobener Horizontal-Overflow-Bug im Suchfeld bei 375px).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zwei echte a11y-Befunde aus dem Audit von web/mail-postfach behoben:
Ordnerbaum-Treeitems waren per Tastatur erreichbar, aber ohne
onKeyDown-Handler nicht per Enter/Leertaste aktivierbar; die
scrollbare Nachrichtenliste hatte keinen tabIndex (axe:
scrollable-region-focusable, serious).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Automatisierte Pruefung: Vitest-Tests fuer die neue Drawer-/Detail-
Zustandslogik (page.test.tsx, MessageList.test.tsx, MessageDetail.test.tsx)
sowie responsive-layout-check.mjs - echter Puppeteer/Chrome-Lauf mit
Viewport-Resize (375/768/1440px) und Touch-Taps gegen die gebaute App,
gleicher Puppeteer-Bestand wie web/mail-search/keyboard-flow-check.mjs.
43/43 Vitest-Tests gruen, 15/15 Pruefungen im Browserlauf.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Responsives Drei-Spalten-Layout (Ordner/Nachrichten/Detail) fuer
web/mail-postfach: CSS-Breakpoint bei 768px (SHL-Token), mobil wird die
Ordnerliste zu einem Dialog-Drawer und Nachrichtenliste/Detail teilen
sich eine Ansicht per data-mobile-active-Zustand statt fester inline
Breiten, die auf schmalen Viewports horizontal abgeschnitten haetten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/legalhold-contract-test-mail.sh startet zusaetzlich eine
zweite, echte moduleadapter-api-Instanz mit eigener Datenbank fuer
einen zweiten Mandanten (globex-contract-mail) und prueft die
Isolation gegen die Archive-Instanz von acme-contract-mail in beiden
Richtungen - Pruefung 3 des Tickets damit auch gegen echte,
getrennte Zielmodul-Instanzen abgedeckt, nicht nur gegen einen
Stub. mail/docs/INT-11-PRUEFPROTOKOLL.md dokumentiert den
Drei-Wege-Merge, die gefundene Luecke, die Umsetzung je
Akzeptanzkriterium und alle durchgefuehrten Pruefungen mit Ergebnis.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Unit-Tests (retentionclient, legalhold) belegen: CheckLegalHold sendet
den per-call uebergebenen Tenant statt c.Tenant, ein leerer Tenant
loest keinen HTTP-Aufruf aus, ein 403 wird als ErrForbidden/
ErrTenantRejected unterscheidbar. Neue Integrationstests (echtes
Postgres) in mail/internal/trash/tenant_isolation_test.go decken
Pruefung 1-3 des Tickets ab: korrekter Tenant liefert erwartete Daten,
manipulierter Tenant wird abgelehnt und im Audit-Log als
tenant_mismatch vermerkt, zwei Mandanten bleiben in einem
PurgeExpired-Lauf isoliert. Bestehende Tests (allow/deny-Helfer,
TestArchiveContract_*) auf die neue Signatur angepasst.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
retentionclient.CheckLegalHold erhaelt den Tenant-Kontext der konkret
geprueften Nachricht als Pflichtparameter statt ihn aus dem fest am
Client verankerten c.Tenant zu nehmen - trash.Store verwaltet mehrere
Mandanten in einem Schema, PurgeExpired haette sonst Kandidaten
verschiedener Mandanten mit demselben (falschen) Tenant-Kontext
abgefragt. legalhold.Verify/Checker reichen tenantSlug entsprechend
durch und unterscheiden eine vom Archive-Modul abgelehnte
Tenant-Fehlpassung (ErrTenantRejected) von einem gewoehnlichen
Transportfehler. trash.PurgeExpired reicht nun den tenant_slug jedes
Kandidaten durch und vermerkt eine Ablehnung wegen Tenant-Mismatch im
Audit-Log mit eigenem reason=tenant_mismatch.
AI (moduletypeclient.Register) und Workflow (workflowevent/
moduleadapter.PublishEvent) tragen den Tenant-Kontext bereits als
Pflichtparameter bzw. validieren ihn bereits serverseitig unabhaengig
vom Service-Token - keine Aenderung dort noetig.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll: Ausgangslagepruefung (INT-09/INT-10 Branches ohne
OpenAPI-Auswirkung verifiziert, direkt auf main aufgebaut),
Nachweise je Akzeptanzkriterium, Ersatz fuer Stichprobenreview durch
zweite Person (zwei getrennte Selbstpruefungsrunden inkl. je einem
dokumentierten Fund), bekannte Scope-Luecke (mailrulesapi/healthcheck
ohne eigenen OpenAPI-Beitrag).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI-Job fuer die mail-Vertragstests, nach demselben Muster wie
contract-tests.yml (Core) und archive-contract-tests.yml (Archive):
eigener Job, Trigger auf push/pull_request bei Aenderungen unter
mail/internal/mailapi/**.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vertragstests fuer mail-internal/mailapi gegen openapi.yaml mit
openapi3filter: Request-/Response-Validierung je dokumentiertem
Endpunkt und Statuscode, programmatischer Vollstaendigkeitsabgleich
gegen alle in openapi.yaml definierten Pfade, sowie ein
DetectsBreakingChange-Gegentest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll: Zielwerte und Ergebnisse je Akzeptanzkriterium und
Pflichtpruefung, inkl. Basis-Hinweis (Branch feature/ux-01-postfach-
ordneransicht statt main) und Einschraenkung bei Pruefung 3 (keine
interaktive Browser-Pruefung ohne laufenden Core-Server moeglich,
gleiche Einschraenkung wie in UX-01-PRUEFPROTOKOLL.md dokumentiert).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Automatisierte Tests fuer alle drei Akzeptanzkriterien:
- lib/contrast.test.ts: echte WCAG-2.1-AA-Kontrastberechnung (>=4.5:1)
fuer die tatsaechlich verwendete Text/Hintergrund-Kombination in
beiden Erscheinungsbildern.
- components/ThemeToggle.test.tsx: manuelle Umschaltung sowie
Persistenz ueber ein simuliertes Neuladen der Seite (kompletter
Unmount + frischer ThemeProvider), auch gegen eine abweichende
Systempraeferenz.
- app/theme-visual-sample.test.tsx: Postfach-Kernansicht rendert
vollstaendig in beiden Modi, Theme-Variablen korrekt gesetzt, und
automatischer Regressionsschutz gegen lokale Hex-Farben ausserhalb
von var(...)-Fallbacks (genau der vorherige Fehler in MessageList).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Manuelle Umschaltflaeche (ThemeToggle) ergaenzt und im Root-Layout
eingehaengt - Akzeptanzkriterium 2 (manuell ueberschreibbar). Bisher
gab es in web/mail-postfach keinen sichtbaren Weg, useTheme().toggle
aus @nexarch/shl auszuloesen, da diese App (wie dms-browser/pdf-viewer/
mail-search) keine Shell-Navigation mit eingebauter Umschaltflaeche
nutzt. Kein neues Theming, nur Anschluss an das zentrale System.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix: Trennlinie unter dem Spaltenkopf in MessageList.tsx setzte eine
fest codierte Hex-Farbe (#ccc) statt der zentralen SHL-Theme-Variable
--shl-color-border - einzige Stelle in web/mail-postfach, die am
zentralen Theming-System vorbeibaute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend-Grundlage fuer die Detailansicht: GET /messages/{id}/attachments
(Index/ContentType/Size per Magic-Bytes, kein Dateiname -- der
Ingestion-Pfad persistiert nirgends einen Originaldateinamen) ergaenzt
die bestehende mailapi (bereits vorhandenes Detail-Endpoint liefert
Subject/Body/Sender/Mailbox/SentAt bereits vollstaendig als Header-Basis).
Unabhaengig von UX-03 (eigener offener Branch, anderes Frontend
web/mail-search) mit gleicher Endpunktform implementiert, da UX-02 auf
UX-01 aufsetzt, nicht auf UX-03 -- Integration/Merge-Konflikt ist Sache
eines spaeteren Zusammenfuehr-Schritts, siehe UX-02-PRUEFPROTOKOLL.md.
Fixture (mail/cmd/mailpostfach-fixture) um einen 3-teiligen Thread samt
zwei echten Anhaengen erweitert, fuer den e2e-Test des Frontends gegen
die echte Backend-API -- bestehende UX-01-Ordner/-Zaehlungen bleiben
unveraendert gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll dokumentiert: Basis-Branch (origin/feature/ux-01-...,
nicht main), alle drei Pflichtpruefungen mit Zielwert/Ergebnis,
Testergebnisse, bewusste Scope-Grenzen (Collator, rohe Error-Strings,
UX-07).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Automatisierte Tests fuer alle drei Akzeptanzkriterien: keine rohen
i18n-Schluessel im gerenderten Output (de/en), Sprachumschaltung ohne
Neuladen, Datums-/Zahlenformat folgt der Sprache.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Englische Übersetzungen fuer alle von UX-01 vorbereiteten
mail-postfach-Schluessel ergaenzt (registerMessages(\"en\", ...) in
lib/messages.ts), plus neue Schluessel fuer den Sprachumschalter.
layout.tsx liest initialLocale/html-lang jetzt aus
NEXT_PUBLIC_DEFAULT_LOCALE (gleiches Muster wie web/dms-browser).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll: Zielwerte und Ergebnisse je Akzeptanzkriterium und
Pruefung, inkl. Abgrenzung zur Ausgangslage (Backend-API musste fuer
Ordnerbaum/Absender-Sortierung minimal erweitert werden) und offener
Einschraenkung beim manuellen Test (keine Browser-Automatisierung/kein
laufender Core-Server auf dem Entwicklungshost, daher Ersatzpruefung
ueber den echten Backend-Integrationstest).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Postfach- und Ordneransicht fuer archivierte Mails (Next.js/React/TS,
@nexarch/shl). Ordnerbaum (aus der flachen Postfachliste der Mail-API
clientseitig aufgebaut), virtualisierte Nachrichtenliste mit Sortierung
nach Datum/Absender/Betreff, Grundsuche. Aufbau nach etabliertem Muster
(web/mail-search fuer die App-Struktur, web/dms-browser fuer Ordnerbaum/
virtualisierte Liste), bewusst schlicht gehalten (Mailpit/MailHog als
Vorbild, nicht die Komplexitaet eines nativen Mailprogramms).
Automatisierte Tests je Akzeptanzkriterium, inkl. eines echten
Backend-Integrationstests (e2e/ux01.e2e.test.ts) gegen den echten
mailapi.Server (mail/cmd/mailpostfach-fixture) mit zwei Mandanten und
einem gleichnamigen Ordner — keine Mandanten-Isolationslogik wird
gemockt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Testinfrastruktur: startet den echten mailapi.Server (echte moduleauth-
Signaturpruefung) als eigenen Prozess auf einem echten TCP-Port, mit
In-Memory-Suchpersistenz und zwei synthetischen Mandanten (einer davon
mit 3000 Nachrichten fuer den Performance-Nachweis). Grundlage fuer den
Frontend-Integrationstest gegen eine echte Backend-API statt eines
fetch()-Mocks. Kein Produktivpfad.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Erweitert mail/internal/mailapi (INT-01) um die fuer die Postfach-/
Ordneransicht noetigen Daten, die die bestehende API noch nicht lieferte:
GET /mailboxes (Ordnerbaum aus der mailbox-Facette), mailbox-Filter auf
GET /messages, sowie sender/mailbox in der Listenantwort (Grundlage fuer
Sortierung nach Absender). Reines Durchreichen bereits fertiger
mail/internal/search-Funktionen (Facets/ListByMailbox/SearchWithFilters),
keine neue Suchlogik.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Frontend (web/mail-search): Anhang-Vorschau fuer PDF (Canvas-Rendering
via pdfjs-dist, gleiches Muster wie DOC-05s web/pdf-viewer/PdfViewer.tsx
-- kein aequivalentes PdfCanvasPreview im Repo gefunden, daher PdfViewer
als Vorlage uebernommen statt neu erfunden) und Bilder (<img> auf einem
authentifiziert geladenen Blob). Die Vorschauart haengt ausschliesslich
vom serverseitig per Magic-Bytes bestimmten contentType ab, nie von
einem Dateinamen (den es an der Schnittstelle gar nicht gibt); nicht
unterstuetzte Typen werden nur als Hinweistext angezeigt, nie
eingebettet. Kein iframe/<object>/dangerouslySetInnerHTML -- durch
automatisierten Sicherheitsreview-Test abgesichert. Neue lib/http.ts
(authentifizierter Zugriff ueber @nexarch/shl, identisch zu
web/pdf-viewer/lib/http.ts) und lib/attachments.ts (Anhangsliste/
-inhalt der Mail-API). Pruefprotokoll in mail/docs/UX-03-PRUEFPROTOKOLL.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend (mail/internal/mailapi): Content-Type fuer Anhang-Download und
neue Anhangsliste (GET .../attachments) wird serverseitig per
Magic-Bytes (net/http.DetectContentType) bestimmt statt wie bisher
hartkodiert/ueber eine Dateiendung -- es gibt an dieser Schnittstelle
technisch gar keinen Dateinamen. Ergaenzt um Content-Disposition:
inline statt erzwungenem Download. mail/internal/attachments: Test
ergaenzt, der belegt, dass die bestehende IMP-02-Extraktion eine
falsche Dateiendung und einen falsch deklarierten MIME-Typ bereits
korrekt per Magic-Bytes ueberschreibt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zielwerte und Ergebnisse aller drei Pflichtpruefungen dokumentiert,
inklusive des gefundenen dedupe_key-NOT-NULL-Fehlers im ENG-02-Adapter
(dort nicht behoben, nur in Mail umgangen) und der bewusst offenen
Verdrahtung in einen konkreten SMTP-/Hotfolder-Zustellpfad.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Je Akzeptanzkriterium ein automatisierter Test, real gegen Postgres
(TEST_TENANT_DSN) UND gegen eine echte, als Subprozess gestartete
workflow-app (workflow/cmd/workflow-app) -- kein HTTP-Fake, da Mail und
Workflow als getrennte Go-Module ausschliesslich ueber den echten
ENG-02-Adapter gekoppelt sind:
- RegisterAll traegt message/thread samt Triggern tatsaechlich in
module_registrations/module_registration_versions der laufenden
Workflow-Instanz ein, auch bei wiederholtem Aufruf (Dienst-Neustart).
- PublishMailReceived + Dispatcher.RunOnce liefern mail.received
nachweisbar in module_events der echten Instanz aus.
- Ein nicht erreichbarer Workflow-Endpunkt blockiert Enqueue nicht und
hinterlaesst last_error/Status pending statt das Ereignis zu
verwerfen.
Dabei Fehler im ENG-02-Adapter gefunden und umgangen (nicht am Adapter
selbst behoben, anderes Modul/Board): PublishEvent setzt einen leeren
dedupe_key per NULLIF auf NULL, die Spalte ist aber NOT NULL -- jede
Zustellung ohne Dedup-Schluessel schlug serverseitig fehl. Mail liefert
deshalb immer einen abgeleiteten dedupe_key (mail:{object_type}:{trigger}:{object_id})
statt sich auf einen leeren Schluessel zu verlassen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mail registriert mail_message/mail_attachment inkl. Aufbewahrungsklasse
bei Archive RET-05 (retentionregister.RegisterAll); vor jeder Papierkorb-
Aktion/Endloeschung fragt trash.Store ueber den bereits vorhandenen
legalhold-Checker RET-13 ab. Ablehnung (Sperre ODER nicht erreichbares
Archive) wird neu im Mail-Audit-Log vermerkt (audittrail), Loeschung
bleibt in beiden Faellen aus (fail closed). Vertragstest gegen den
echten archive/cmd/moduleadapter-api-Dienst (scripts/legalhold-contract-
test-mail.sh) bestaetigt reale Registrierung und Sperrabfrage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dokumentiert die Abweichung von der Ticket-Annahme (INT-12/GOV-01-HTTP-
Endpunkt war auf main nicht gemergt, nur auf eigenem Branch), die
getroffenen Scope-Entscheidungen (kein neuer cmd/mail-app, deklarative
Thread-Registrierung ohne eigenes Thread-Datenmodell) und die realen
Pruefergebnisse je Akzeptanzkriterium.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pruefprotokoll mit allen drei Pflichtpruefungen (Registrierung Mail
und Anhang getrennt, Vernichtungs-Rueckruf, Requeue bei
fehlgeschlagener Registrierung) sowie den Ticket-Text, der bisher nur
als board.csv-Zeile existierte, in mail-kanban/tickets/ARC-11.md
nachgetragen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mail registriert Objekttypen (mail_message, mail_attachment) getrennt
bei Archive RET-05 mit eigener Aufbewahrungsklasse und eigener
Rueckruf-Adresse, empfaengt Vernichtungs-Rueckrufe und requeued
fehlgeschlagene Registrierungen ueber eine Postgres-Jobqueue statt sie
zu verschlucken oder abzustuerzen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neues Paket mail/internal/moduletypeclient: schlanker HTTP-Client gegen
ai/internal/moduletypeapi (GOV-01), analog zu dms/internal/moduletypeclient
aus INT-12, mit mail-/thread-spezifischen KI-relevanten Feldern (Betreff,
Text, Anhangstext) statt DMS-Dokumentfeldern.
SetupAIObjectTypeRegistration() registriert beim (kuenftigen) Mail-Start die
Objekttypen 'mail' und 'thread' bei AI. Exportiert statt main-interner
Helfer wie bei dms/cmd/app, da mail noch keinen eigenen cmd/mail-app-
Einstiegspunkt besitzt (dokumentierte Luecke seit INT-18) - bereit fuer den
Aufruf durch einen kuenftigen Einstiegspunkt.
AK2 (optional ohne AI-Modul) und AK3 (Registrierungsfehler blockieren nie
den Mail-Betrieb) ueber Unit-Tests abgedeckt. AK1 (Registrierung erscheint
bei AI) zusaetzlich real nachgewiesen: echter ai-app-Prozess, echte
Postgres-Testdatenbank, legitimer Core-TEN-01-Stub, direkte SQL-Verifikation
(TestEndToEnd_RealAIApp_RegistersMailAndThreadObjectTypes).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
INT-12 (DMS-Variante dieser Integration) ist auf main noch nicht gemerged,
nur auf dem eigenen Feature-Branch vorhanden. Ohne den HTTP-Endpunkt bei AI
gibt es fuer den Mail-seitigen Client aus INT-09 kein Ziel. Unveraendert aus
dem INT-12-Branch uebernommen (Service-Token-Pflicht, Tenant-Verifikation
gegen Core TEN-01/GOV-07 ueber tenantregistryclient, Route nur verdrahtet
bei konfigurierter Core-Basis-URL).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mail-Objekttypen message/thread samt Ausloeser-Ereignissen (mail.received,
mail.delivery_failed, mail.deadline_reached) ueber den ENG-02-Adapter bei
Workflow registrierbar (RegisterAll). Ereignis-Publikation laeuft ueber
eine eigene Postgres-Jobqueue (mail_workflow_events, FOR UPDATE SKIP
LOCKED) analog zu internal/webhook (Core) und mail/internal/indexworker:
Publisher.Publish* reiht nur ein (kein HTTP-Aufruf im Anfragepfad),
Dispatcher stellt im Hintergrund zu und macht Fehlschlaege per
last_error/Retry-Backoff sichtbar statt sie zu verwerfen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dokumentiert die 24h-Dauerlauf-Abweichung (komprimierter Ersatztest statt
woertlichem 24h-Lauf, User-autorisiert), alle drei Akzeptanzkriterien mit
Zielwert/Messwert/Bewertung, den gefundenen und behobenen Goroutine-Leck
in mailimport.Dialer.connect, sowie den Regressionsnachweis ueber die
vollstaendige DMS-Testsuite und die QA-09-relevanten Root-Modul-Pakete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ergaenzt internal/contracttest (Core QA-07) um den fuer DMS QA-09
Pruefung 3 noetigen Nachweis: ein Webhook-Ziel, das nach mehreren
Fehlversuchen wieder erreichbar ist, wird trotzdem noch zugestellt
(TestProcessDue_RetriesWithBackoffThenMarksFailed in
internal/webhook/webhook_test.go belegt bereits den dauerhaften
Fehlschlag, nicht aber die Wiederherstellung). Liegt im Core-Modul, da
DMS die Core-internen Pakete (Dispatcher/Store) nicht importieren kann
(getrenntes Go-Modul, siehe dms/internal/retentionclient-Muster).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Viele Import-Zyklen hintereinander statt des woertlichen 24h-Dauerlaufs
(siehe docs/QA-09-PRUEFPROTOKOLL.md fuer die Begruendung), je Zyklus
mehrere Nachrichten/Stapel: belegt verlustfreien Import ueber INT-04/08
Zyklen hinweg (Pruefung 2).
Der Test deckte dabei ein echtes Ressourcenleck auf: Dialer.connect
startete eine Goroutine, die auf den AUFRUFER-Kontext wartete (haeufig
context.Background(), nie Done) statt auf einen an die Sitzung
gebundenen Kontext - jeder Poll-Zyklus liess so dauerhaft eine Goroutine
haengen. Fix: sessCtx wird von ctx abgeleitet und in session.close()
abgebrochen, die Beobachtung auf externe Kontext-Stornierung bleibt
erhalten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vertragstests fuer die DMS-seitigen Schnittstellen zu Archive (INT-11),
AI (INT-12), Workflow (INT-13) und die E-Mail-Regel-API (INT-04): jeder
Test prueft die tatsaechlich gesendete/empfangene JSON-Nutzlast eines
echten Clients/Handlers gegen ihr dokumentiertes Schema, ergaenzt um eine
DetectsBreakingChange-Gegenprobe je Vertrag (Muster wie Core QA-07/Archive
QA-07-Pendant). CMIS (INT-09) hat mit parity_test.go/arch_test.go bereits
aequivalente Abdeckung, daher hier nicht dupliziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DMS registriert den Dokument-Objekttyp inkl. KI-relevanter Felder (Volltext,
Metadaten) beim Start automatisch bei AI GOV-01 - ueber eine neue HTTP-
Schnittstelle (ai/internal/moduletypeapi), da DMS ein getrenntes Go-Modul
ist und GOV-01s Go-Paket nicht importieren kann. Registrierung ist optional
(fehlt die AI-Basis-URL, startet DMS unveraendert) und fail-open (ein
unerreichbarer AI-Dienst blockiert den DMS-Start nie).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ergaenzt die bestehende Testdecke (APR-01/04, TSK-03/05, CFG-02/04/05) um
zwei End-zu-Ende-/Race-Tests gegen echte Postgres-Testdatenbanken: eine
Parallelentscheidung zweier Vertreter auf derselben delegierten Freigabe,
und ein Abgleich des echten Zustellprotokolls (notification_jobs) ueber
den realen CFG-05-HTTP-Pfad statt eines Mock-Dispatchers. Keine
Produktionslogik musste korrigiert werden; Befunde und Testlaeufe sind in
docs/QA-07-PRUEFPROTOKOLL.md dokumentiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TEN-09 war bereits implementiert und gemergt; dieser Lauf bestaetigt alle
drei Akzeptanzkriterien (Rollenisolation, Audit-Events bei Superadmin-/
Verbindungsinfo-Zugriff, Netzwerkdokumentation) erneut auf einem frischen
Checkout, ohne Code-Aenderungen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AUD-07 ist bereits auf main umgesetzt (Commit 6866845: Migration
migrations/0010_audit_events_role_enforcement.{up,down}.sql entzieht der
Rolle nexarch_app UPDATE/DELETE auf audit_events, Trigger
audit_events_prevent_mutation() protokolliert und lehnt Aenderungsversuche
ab; internal/audit/append_only_test.go deckt alle drei Akzeptanzkriterien
ab). Es fehlte bislang nur das Pruefprotokoll.
Auf frischem Checkout (Host 192.168.1.204) real nachgeprueft:
- go test ./internal/audit/... -v -p 1 -count=1: alle 24 Tests PASS,
inkl. TestAppendOnly_AppRoleHasNoUpdateOrDeletePrivilege (permission
denied statt Trigger-Fehler), TestAppendOnly_TamperAttemptIsLogged
(RAISE-WARNING-Notice mit Akteur-/Session-Kontext) und
TestAppendOnly_NormalWriteAndReadStillWork (Regression).
- Manuelle Stichprobe gegen isolierte Testtabelle: UPDATE/DELETE werden
vom Trigger abgelehnt und vorher per RAISE WARNING protokolliert;
normaler INSERT/SELECT bleibt unveraendert nutzbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bereits auf main implementierte Rotations-/Audit-Logik real gegen eine
Postgres-Testdatenbank verifiziert (kein Mocking). Alle 3 Akzeptanzkriterien
und alle 3 geforderten Prüfungen bestanden, 13/13 Tests in internal/kek grün.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implementierung war bereits in main vorhanden (Commit 9a41f3d). Dieser
Commit dokumentiert die reale Verifikation aller drei Akzeptanzkriterien
(Issuer/Audience-Bindung, JWKS-only-Signaturpruefung, Sperre gegen
automatische Superadmin-Vergabe aus externen Claims) durch Testlauf auf
Host 204.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lastgenerator (dms/cmd/qa10loadtest) gegen Suche, Vorschau, Upload und
Schnittstelle unter realistischer und ueberhoehter Last, automatisierte
Tests je Akzeptanzkriterium (dms/internal/qa10), Pruefprotokoll mit
Zielwert/Messwert/Bewertung (docs/QA-10-PRUEFPROTOKOLL.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Alle drei Pruefungen real durchgefuehrt und bestanden. Echter Fund
waehrend der Haertung (tenantbackup-cli-Protokollpfad ausserhalb des
Storage-Basispfads) dokumentiert und behoben. Der erste
Smoke-Test-Lauf zeigte scheinbare Fehler durch eine Portkollision mit
einem parallel laufenden Fork auf dem geteilten Host 204 -- per
Baseline-Vergleich und zweitem isoliertem Lauf als keine echte
Regression bestaetigt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neue Tests fuer Archive/Mail/Core-kek (Pruefung 1/2 je Stelle,
identisches Muster wie die bereits vorhandenen DMS-Tests). Alle
Pruefungen real gegen Postgres auf Host 204 durchgefuehrt (siehe
Pruefprotokoll), inkl. vollstaendiger Regression von dms/qa02,
internal/kek, internal/ldapsync, internal/webhook.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
seal/open (bzw. wrap/unwrap) in allen vier betroffenen Stellen binden
ein Additional Authenticated Data an Chiffretext/DEK-Wrapper: ein
Austausch zwischen zwei Kontexten faellt beim Entschluesseln auf,
statt still zu funktionieren.
- dms/internal/crypto/envelope.go + service.go: AAD = Tenant-Slug
(bereits vorhandener Parameter in Service.Seal/Open, Signatur
unveraendert -- keine der rund 30 externen Aufrufstellen musste
angepasst werden)
- archive/internal/backupcrypto/envelope.go + reposecret.go/file.go:
gleiches Muster, AAD = Tenant-Slug
- mail/internal/crypto/envelope.go + service.go + kekversions.go:
gleiches Muster, AAD = Tenant-Slug (inkl. ARC-08
KEK-Versionsverwaltung)
- internal/kek/master.go + store.go: wrap/unwrap sowie
EncryptWithTenantKEK/DecryptWithTenantKEK binden AAD. Die drei
echten externen Aufrufer angepasst: ldapsync (Tenant-ID + Zweck),
webhook (Ziel-URL des Abonnements), account-api modulekeys (fester
Zweck-Bezeichner -- SecretCipher-Interface aus internal/moduletrust
reicht keine Datensatz-ID durch, eine Erweiterung wuerde in andere
Module hineinwirken, bewusst nicht Teil dieser Kachel)
Rueckwaertskompatibel: jedes open/unwrap versucht zuerst mit AAD,
faellt bei Fehlschlag auf kein-AAD zurueck -- vor dieser Aenderung
versiegelte Datensaetze bleiben lesbar.
Bekannte, dokumentierte Grenze: ein Austausch zwischen zwei
Datensaetzen DESSELBEN Mandanten/Kontexts bleibt unerkannt (identisches
AAD) -- volle Bindung an den einzelnen Datensatz braeuchte eine
Datensatz-ID an rund 30 Aufrufstellen, bewusst nicht Teil dieser
kleinstmoeglichen Loesung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestProvision_RuntimeRoleHasNoCreateDB belegt Akzeptanzkriterium 2 mit
zwei tatsaechlich getrennten Postgres-Rollen (nicht nur derselben
Rolle zweimal uebergeben wie in den uebrigen Testaufrufen): die
Laufzeit-Rolle darf in ihrer eigenen Tenant-DB schreiben, aber keine
neue Datenbank anlegen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Alle Testaufrufe uebergeben adminDSNTemplate/runtimeDSNTemplate mit
demselben Wert (Testrolle bleibt Eigentuemerin, GRANT an sich selbst
ist ein no-op) -- keine Verhaltensaenderung in bestehenden Tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 bisher ungehaertete Archive-Backup/Reconcile/Scrub-Oneshot-Vorlagen
(deploy/systemd/) bekommen die etablierten Sandbox-Direktiven
(NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ggf.
ReadWritePaths). Drei Vorlagen ohne Dateisystemzugriff (reconcile,
retention-notify, scrub) bekommen bewusst keine ReadWritePaths,
begruendet dokumentiert (S3-kompatible Storage-Abstraktion statt
lokaler Pfade).
Neue, getrennte Postgres-Laufzeit-Rolle (nexarch_tenant_runtime, OHNE
CREATEDB) statt der bisher gemeinsam genutzten CREATEDB-Rolle fuer
NEXARCH_TENANT_DSN_TEMPLATE. Provisioner.Provision() berechtigt die
neue Rolle je frisch angelegter Tenant-DB (GRANT CONNECT + Schema-
Rechte inkl. Default-Privileges), Tenant.DBDSN wird jetzt mit dieser
Rolle statt der CREATEDB-Rolle gebaut. Neue Umgebungsvariable
NEXARCH_TENANT_RUNTIME_DSN_TEMPLATE, deploy/install.sh legt die Rolle
automatisch an.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 22:34:51 +02:00
536 changed files with 40484 additions and 1848 deletions
Siehe `PRODUKTARCHITEKTUR.md` für die Gesamtübersicht sowie die weiteren Konzeptdokumente (`SICHERHEITSKONZEPT.md`, `MIGRATIONSKONZEPT.md`, `SKALIERUNGSKONZEPT.md`, `STORAGE-KONZEPT.md`, `SAAS-BETRIEBSMODELL.md`, `UI-UX-KONZEPT.md`) für Details zu einzelnen Bereichen.
## Datenschutz (DSGVO)
- **Keine echten Kunden-/Mandantendaten in Prompts, Testfixtures oder Screenshots** — je nach Zielsystem (siehe `dms-kanban/known-issues-archivdms.md`, `dms-kanban/known-issues-archivmail.md`) potenziell personenbezogene oder besondere Kategorien von Daten (Art. 9 DSGVO).
- Test-/Demodaten synthetisch halten, nie einen Export aus einem echten Kunden-/Altsystem als Fixture verwenden.
- Secrets (API-Keys, DB-Credentials, Verschlüsselungs-Keys) nie in Prompts einfügen.
- Claude Code hier ausschließlich für Code-/Konzeptarbeit; für Auswertungen an echten Kundendaten (z. B. Migrationstests gegen Altsysteme) lokale Ollama-Server nutzen, nicht Claude.
Stand: 2026-08-26. Grundlage: `dms-kanban/known-issues-archivdms.md`, `dms-kanban/known-issues-archivmail.md` (Tabellenaufnahme archivmail/archivdms), `dms-kanban/`, `mail-kanban/`, `core-kanban/`, `archive-kanban/`, `workflow-kanban/` (Boards inzwischen aus dem Repo entfernt, Ticket-Historie liegt in der Gitea-PR-Historie).
## 1. Ausgangslage
- Zwei produktive Altsysteme: **archivmail** (21 Tabellen, langer Produktivbetrieb laut Ticket-Historie) und **archivdms** (32 Tabellen, 26 dokumentierte GoBD-Migrationen, funktional reifer).
- **Mandanten überschneiden sich teilweise** — manche Kunden nutzen beide Systeme parallel. Migration muss dieselbe Organisation als EIN Tenant in NEXARCH zusammenführen, nicht als zwei getrennte.
- Entscheidung: **Big Bang** — alle Mandanten wechseln zu einem Stichtag. Das verkürzt die Übergangszeit, verlangt aber eine vollständig geprobte Migration vor dem echten Stichtag (siehe Abschnitt 5).
## 2. Grundprinzip: Tenant-Zusammenführung vor allem anderen
Da Mandanten in beiden Altsystemen unabhängig entstanden sind (eigene `tenants`-Tabelle je System, eigene IDs), ist der erste und riskanteste Schritt die **Tenant-Identitätsauflösung**:
1. Export beider `tenants`-Tabellen (archivmail + archivdms) mit `slug`/`name`.
2. Abgleich per **Slug/Domain-Matching** (Firmenname, gepflegte Kunden-Zuordnungsliste als manuelle Fallback-Quelle — automatisches Matching allein ist bei Freitextnamen nicht zuverlässig genug für GoBD-relevante Daten).
3. Für jedes Ergebnis ein `tenant_migration_map`-Arbeitsblatt: `archivmail_tenant_id`, `archivdms_tenant_id` (kann leer sein), `nexarch_tenant_id` (neu vergeben).
4.**Menschliche Freigabe Pflicht** für jede Zusammenführung — kein automatischer Merge ohne Bestätigung, da eine falsche Zusammenführung zwei Kunden' Daten sichtbar vermischen würde (Compliance-Vorfall).
5. Ergebnis der Tenant-Map ist eine feste Referenztabelle, die JEDE weitere Migrationsstufe verwendet, um `tenant_id`-Fremdschlüssel umzuschreiben.
Gleiches Prinzip, eine Ebene tiefer, für **Benutzer**: ein Mensch kann in archivmail und archivdms mit derselben E-Mail-Adresse, aber unterschiedlicher `user_id` existieren. Abgleich per E-Mail-Adresse innerhalb desselben zusammengeführten Tenants, gleiche Freigabepflicht. `user_migration_map`: `archivmail_user_id`, `archivdms_user_id`, `nexarch_user_id`.
## 3. Tabellen-Mapping nach Zielsystem
### → NEXARCH Core
| Alt (archivmail) | Alt (archivdms) | Ziel | Hinweis |
| `audit_log` | `audit_log` | Core `AUD-01`/`AUD-02` | beide Systeme nutzen dasselbe Append-only-Trigger-Muster — Zusammenführen ist mechanisch, KEINE inhaltliche Veränderung der historischen Einträge (Audit-Integrität: alte Einträge bleiben mit altem Zeitstempel, nur `tenant_id`/`user_id` über die Mapping-Tabellen umgeschrieben) |
| `token_blacklist` | `token_blacklist` | Core `IAM-02` | wird NICHT migriert — beim Cutover sind alle Alt-Sessions ohnehin ungültig, neue JWTs werden nach dem Login in NEXARCH ausgestellt |
| API-Keys mit Rate-Limit | Buchhaltungs-API-Keys | Core `IAM-09` API-Token | neu ausgestellt, nicht migriert (Secrets werden grundsätzlich nicht kopiert, nur die Berechtigung neu vergeben) |
### → NEXARCH DMS
| Alt (archivdms) | Ziel | Hinweis |
|---|---|---|
| Dokumente + Datei-Revisionen | DMS `DOC-01`/`VER-01`-Bereich | Binärdaten per Objekt-Storage-Kopie (nicht per DB-Migration), Metadaten per SQL-Export/Import |
| `document_visibility`, `permission_groups`, `*_grants` | DMS-Tickets referenzieren Core `RBAC-02`/`RBAC-04` | ACL-Daten wandern inhaltlich nach Core-Rechtemodell, nicht in DMS selbst |
| ML-Klassifikator (trainiertes Modell) | DMS `MET-06` | **Modell selbst wird NICHT migriert** — DMS ist Neubau ohne Codeimport, Trainingsdaten (nicht das archivdms-Binärmodell) können als Grundlage für ein neues Training dienen, falls Trainingsdaten-Export vorliegt |
| Gespeicherte Ansichten, Erinnerungen | DMS-UX-Bereich bzw. Workflow `TSK-01` | Erinnerungen mit offenem Fälligkeitsdatum → Workflow-Board |
### → NEXARCH Mail
| Alt (archivmail) | Ziel | Hinweis |
|---|---|---|
| IMAP/POP3/SMTP-Kontenverwaltung | Mail `IMP-07` Mehrfach-Postfach-Verwaltung | Zugangsdaten NICHT kopieren, Postfach-Konfiguration neu anlegen und Verbindung neu testen |
| Nachrichten + Anhänge (`email_refs`) | Mail `ARC-01`/`ARC-04` | Binärdaten per Objekt-Storage-Kopie, Referenzen per SQL-Export/Import, `indexed_at`-Statusfeld wird verworfen (Mail-Board indexiert nach der Migration komplett neu, siehe Abschnitt 4) |
| Reconciliation-Reports | Archive `BAK-05` | historische Reports als Nachweis archivieren, kein aktiver Datenbestand |
### → NEXARCH Archive
| Alt (beide Systeme) | Ziel | Hinweis |
|---|---|---|
| `retain_until`-Feld auf Kernobjekten | Archive `RET-01` generisches Retention-Objektmodell | wichtigster Einzelschritt für GoBD-Kontinuität — Fristen dürfen beim Umzug NICHT zurückgesetzt werden, sonst Compliance-Bruch |
| Vier-Augen-Löschworkflow (`document_delete_requests`, nur archivdms) | Archive `CMP-06` | offene Löschanfragen migrieren, bereits vollzogene Löschungen bleiben nur als Audit-Eintrag bestehen (Objekt existiert nicht mehr) |
### → NEXARCH Workflow
| Alt (archivdms) | Ziel | Hinweis |
|---|---|---|
| Workflows/Automatisierung, Processing-Queue | Workflow `ENG-01`/`RTE-01` | laufende Workflow-Instanzen NICHT migrieren (Zustandsmaschine ist Board-spezifisch neu) — nur abgeschlossene Vorgänge als Historie, offene Vorgänge werden vor dem Stichtag bewusst zu Ende geführt oder im Altsystem abgebrochen |
## 4. Was bewusst NICHT migriert wird
- **Suchindex (Manticore)**: kein DB-Sync-Table in beiden Altsystemen vorhanden → nach der Migration wird der komplette Suchindex aus den migrierten Metadaten NEU aufgebaut (Reindexierung), nicht aus dem alten Manticore-Index kopiert.
- **Sessions/Tokens**: siehe oben, neu ausgestellt.
- **ML-Klassifikator-Binärmodell**: DMS ist Neubau, kein Codeimport — nur Trainingsdaten (falls vorhanden) als Ausgangspunkt für neues Training.
- **Laufende Workflow-Instanzen**: vor dem Stichtag im Altsystem abschließen, nicht in einem "halb migrierten" Zustand übernehmen.
## 5. Ablauf (Big Bang, mit Geprobter Vorbereitung)
"Big Bang" heißt: **ein** Stichtag für alle Mandanten — es heißt NICHT, den Migrationscode ungetestet gegen die Produktivdaten laufen zu lassen. Vorgeschlagener Ablauf:
1.**Trockenlauf 1 (Entwicklung)**: Migrationsskripte gegen anonymisierte Kopie der Produktivdaten, iterativ.
2.**Trockenlauf 2 (Generalprobe)**: vollständiger Lauf gegen eine reale, aktuelle Kopie beider Produktivdatenbanken, in einer isolierten NEXARCH-Testumgebung. Ergebnis wird stichprobenartig gegen die Quelle geprüft (Anzahl Dokumente/Mails je Tenant, Stichproben-Volltextvergleich, Retention-Fristen-Abgleich).
3.**Tenant- und User-Mapping final freigeben** (Abschnitt 2) — spätestens hier, nicht erst am Stichtag.
5.**Parallelbetrieb read-only**: Altsysteme bleiben für einen definierten Zeitraum (Vorschlag: 90 Tage) lesend erreichbar, für den Fall dass eine Diskrepanz erst nach dem Stichtag aktiv genutzten Daten auffällt.
6.**Abschaltung** der Altsysteme nach Ablauf der Parallelbetriebsfrist und einer zweiten, schriftlich festgehaltenen Freigabe.
## 6. Verifikation
Pflicht je Tenant, vor Freischaltung von NEXARCH für diesen Tenant:
- Zeilenzahl je migrierter Kerntabelle (Dokumente, Mails, Benutzer) alt vs. neu deckungsgleich (abzüglich bewusst nicht migrierter Kategorien, siehe Abschnitt 4).
- Stichprobenweiser Hash-Vergleich migrierter Binärdateien (Objekt-Storage) gegen Quelle.
- Retention-Fristen: für eine Stichprobe von Objekten mit `retain_until` in der Zukunft prüfen, dass die Frist nach der Migration identisch ist.
- Audit-Log-Kontinuität: ältester und neuester Eintrag je Tenant nach Migration mit Quelle abgleichen (keine Lücke, keine Zeitverschiebung).
- Login-Test mit migrierten Zugangsdaten für eine Stichprobe von Benutzern je Tenant.
## 7. Rollback
Da Big Bang gewählt wurde, muss der Rollback-Pfad vor dem Stichtag feststehen, nicht danach improvisiert werden:
- Altsysteme werden am Stichtag NICHT gelöscht, nur in Wartungsmodus versetzt (siehe Abschnitt 5) — Rollback heißt: Wartungsmodus aufheben, NEXARCH-Freischaltung zurücknehmen.
- Rollback-Entscheidung an klare Kriterien binden (z.B. Verifikation in Abschnitt 6 schlägt für mehr als X % der Tenants fehl) statt Einzelfallentscheidung unter Zeitdruck am Stichtag selbst.
## 8. Offene Punkte
- Tenant-Matching-Liste (Abschnitt 2) muss von der Geschäftsseite gepflegt werden, nicht rein technisch ableitbar — braucht personelle Zuständigkeit.
- Dauer des Parallelbetriebs (90 Tage vorgeschlagen) ist noch nicht mit Vertrags-/Compliance-Anforderungen abgeglichen.
- Migrations-Tickets selbst (konkrete Kachel-Boards für die Migrationsskripte je Zielsystem) sind noch nicht angelegt — dieses Dokument beschreibt das Konzept, keine Umsetzungs-Kacheln.
Stand: 2026-08-26. Zusammenführung aller bisherigen Entscheidungen aus `nexarch-state.json` und den sieben Boards zu einem lesbaren Gesamtbild. Kein neues Konzept — reine Synthese des bereits Entschiedenen.
## 1. Produktvision
NEXARCH ist die Nachfolgeplattform von **archivmail** (E-Mail-Archivierung) und **archivdms** (Dokumentenmanagement) — nicht als Umbenennung, sondern als modulare Neuentwicklung unter der Positionierung **"Next Generation Information Management"**: E-Mails, Dokumente, digitale Archivierung, OCR, Volltextsuche, Workflows, Compliance, Integrationen, KI — als ein zusammenhängendes System, nicht als zwei getrennte Werkzeuge nebeneinander.
## 2. Architekturprinzip
**Ein modularer Monolith aus sieben eigenständigen, aber koordinierten Diensten** — kein Microservice-Zwang, aber klare Dienstgrenzen mit eigenem Datenmodell, eigenem Go-Binary, eigenem Kanban-Board:
(T = Tickets, letzte Zahl = Personentage — Stand siehe `nexarch-state.json` → `boards`.)
Jedes Fachmodul (DMS/Mail/Archive/Workflow/AI/Connect) hat sein **eigenes Datenmodell für seinen Fachgegenstand**, implementiert aber **keine eigene Identität/Rechte/Mandantenlogik** — das ist die zentrale Architekturentscheidung, aus drei Konsolidierungsrunden hervorgegangen (Abschnitt 4).
## 3. Warum sieben Boards und nicht mehr oder weniger
Die Aufteilung ist nicht die ursprünglich im Master-Prompt skizzierte (die u.a. Search und OCR als eigene Module vorsah) — sie hat sich beim Durcharbeiten verschoben:
- **Search/OCR bleiben je Fachmodul** (DMS und Mail haben je eigene `SRC`-Epics) — bewusste Entscheidung, weil E-Mail- und Dokument-Suche als hinreichend unterschiedlich bewertet wurden, um eine gemeinsame Suchschicht nicht zu rechtfertigen.
- **Archive, Workflow, AI und Connect** sind dagegen als eigene Boards entstanden, weil beim Bauen von DMS/Mail wiederholt auffiel, dass dieselbe Fachlogik (Retention/WORM, Genehmigungsprozesse, KI-Klassifizierung, API-Infrastruktur) unabhängig in mehreren Boards geplant wurde — siehe Abschnitt 4.
## 4. Konsolidierungsverlauf — was zentral wurde und warum
Vier Konsolidierungsrunden, jede aus demselben wiederkehrenden Muster: zwei oder mehr Boards planten dieselbe Fachlogik unabhängig voneinander.
| Fachlogik | War dupliziert in | Jetzt zentral in | Ergebnis |
| REST-API-Grundgerüst, Token, Rate-Limiting, Webhooks | DMS (INT-01/02/05), Mail (INT-01/02/03/04) | **Core** | DMS 66→64, Mail 62→59 |
| Krypto-Master-Key-Verwaltung | DMS (FDN-09), Mail (ARC-02) — je eigene Schlüsselverwaltung | **Core** (nur Master-Key, Objekt-Verschlüsselung bleibt modul-lokal) | kein Ticketverlust, aber Duplikat im sicherheitskritischsten Bereich beseitigt |
Die Konsequenz für das Architekturbild: **Core ist nicht "noch ein Modul", sondern das Fundament, auf dem alle anderen sechs aufbauen** — sichtbar auch daran, dass Connect (das jüngste, am weitesten außen liegende Board) am stärksten von einem vollständigen Core abhängt (sieben verschiedene Core-API-Tickets, siehe `ROADMAP.md` Abschnitt 1).
## 5. Modulübergreifende Referenz-Architektur
Kein Modul kopiert die Daten eines anderen — stattdessen ein wiederkehrendes **Adapter-Muster**, in drei Varianten je nach Zweck:
1.**Push-Registrierung** (Archive `RET-05`, Workflow `ENG-02`, AI `GOV-01`): DMS/Mail melden ein Objekt (Dokument-ID, Mail-ID) bei einem Querschnitts-Board an, ohne dessen Rohdaten zu duplizieren — Archive weiß dann "dieses Objekt hat eine Aufbewahrungsfrist", Workflow weiß "dieses Objekt gehört zu einer Akte", ohne den Dokumentinhalt selbst zu besitzen.
2.**Pull-Rückkanal** (Workflow `VOR-04`): die Gegenrichtung — DMS/Mail fragen bei Workflow ab, was zu einem Objekt bekannt ist (Vermerke, Wiedervorlage), damit diese Information im Ursprungsmodul sichtbar wird, statt nur in einer separaten Workflow-Oberfläche zu verschwinden. Ohne diesen Rückkanal wäre die Push-Registrierung für Anwender faktisch unsichtbar — dieser Zusammenhang wurde erst nachträglich im Gespräch aufgedeckt, nicht von Anfang an mitgeplant.
3.**Externe Konnektoren** (Connect `ENG-01`..`DMS2-03`): dieselbe Idee nach außen gewendet — Fremdsysteme (ERP, DATEV, M365, andere DMS) werden über Connector-Adapter angebunden, nicht durch Direktintegration in jedem Fachmodul einzeln.
Gemeinsamer Nenner aller drei: **Referenz statt Kopie, Adapter statt Direktkopplung.**
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete werden deshalb im geteilten Modul `common/` gehalten (per `replace` in den `go.mod` eingebunden, keine eigenen Abhängigkeiten außer der Standardbibliothek). Pilot: `common/httpclient`, `common/apierror`. Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
Dasselbe Prinzip gilt auch innerhalb eines Moduls, auf Code-Ebene: Hauptdateien/Entry-Points bleiben klein, Fachlogik wandert in eigene Module. Vor einer neuen Funktions-/Modul-Datei wird geprüft, ob dieselbe Funktionalität nicht schon woanders existiert (auch modulübergreifend, z. B. in Core) — keine doppelten Funktionsdateien. Das eigene Kanban-Planungswerkzeug wurde nach genau diesem Muster korrigiert (sieben `kanban.html` enthielten identisches JS/CSS als Volltext-Duplikat, damals in `_shared/kanban.js`/`kanban.css` ausgelagert) — die Boards selbst sind inzwischen komplett aus dem Repo entfernt (Ticket-Historie liegt in der Gitea-PR-Historie), das Prinzip "keine doppelten Funktionsdateien" gilt unverändert für den eigentlichen NEXARCH-Code.
## 6. Technische Architektur (Kurzfassung, Details in eigenen Dokumenten)
| Ebene | Entscheidung | Vertiefung |
|---|---|---|
| Backend | Go (net/http, pgx/v5), ein Binary pro Modul | `PROXMOX-DEPLOYMENT.md` |
| Datenbank | PostgreSQL, Modell C (eine physisch getrennte Datenbank je Mandant plus Registry, RLS redundant) | `SKALIERUNGSKONZEPT.md` Abschnitt 4 |
| Suche | Manticore (BM25), je Modul eigener Index | `SKALIERUNGSKONZEPT.md` Abschnitt 5 |
Drei Stufen, ein Techstack (kein Sonderpfad für SaaS vs. On-Premise, außer Docker als SaaS-Zusatzoption):
- **Stufe 1** — Einzel-Proxmox-LXC, für kleine/mittlere Kunden, On-Premise-Standardfall.
- **Stufe 2** — verteilte Installation, mehrere App-Instanzen + Primary/Replica-DB, für größere Kunden oder den SaaS-Betrieb des Anbieters selbst.
- **Stufe 3** — dedizierte Instanz pro Großkunde (Private Cloud), strukturell Stufe 1 oder 2, nur organisatorisch als eigene Umgebung abgegrenzt.
Was noch fehlt: eine konkrete Preis-/Lizenzstruktur (Benutzer/Monat vs. Modul/Monat vs. Enterprise) — die TECHNISCHE Seite der Lizenzierung (Core `LIC`-Epic: Feature-Flags, Modulaktivierung pro Tenant, Nutzungszähler inkl. Speicherverbrauch) ist fertig geplant, die GESCHÄFTLICHE Seite (was kostet was) ist eine offene Entscheidung, siehe `nexarch-state.json` → `fehlende_konzeptdokumente`.
## 8. Migration von den Altsystemen
Eigenes Dokument (`MIGRATIONSKONZEPT.md`), hier nur die Kernaussage: Big-Bang-Stichtag mit vorgeschalteter Generalprobe, größtes Einzelrisiko ist die Zusammenführung von Tenants/Benutzern, die in archivmail und archivdms unabhängig, aber teilweise für dieselben Kunden entstanden sind — mit Pflicht-Freigabe durch einen Menschen, kein automatischer Merge.
Noch offen: SaaS-/Multi-Tenant-Betriebsmodell als eigenes ausformuliertes Dokument (Abschnitt 7 hier deckt die technische Seite ab, nicht die vertraglich-organisatorische), UI/UX-Konzept, Lizenzmodell konkret (Preisgestaltung).
Stand: 2026-08-26. **Kein Rechtsdokument, keine Rechtsberatung.** Strukturiert die vertraglich-organisatorischen Fragen, die aus den bereits getroffenen TECHNISCHEN Entscheidungen entstehen (`PRODUKTARCHITEKTUR.md` Abschnitt 7, `SKALIERUNGSKONZEPT.md`, `PROXMOX-DEPLOYMENT.md`), und macht sie sichtbar, bevor sie in echte Verträge/AGB übersetzt werden — die eigentliche Formulierung braucht juristische Prüfung, hier geht es nur darum, was die Technik dem Vertrag vorgibt bzw. wo der Vertrag von der Technik abhängt.
## 1. Drei Betriebsmodelle, drei unterschiedliche Vertragslogiken
| Modell | Wer betreibt | Wer ist DSGVO-"Verantwortlicher" | Wer ist "Auftragsverarbeiter" |
|---|---|---|---|
| SaaS (Stufe 1/2 beim Anbieter) | NEXARCH-Anbieter | Kunde | NEXARCH-Anbieter |
| On-Premise (Stufe 1 beim Kunden) | Kunde selbst | Kunde | — (kein Auftragsverarbeitungsverhältnis mit NEXARCH-Anbieter, ggf. Softwarelizenzvertrag statt AVV) |
| Private Cloud (Stufe 3, dediziert) | je nach Vereinbarung: Anbieter ODER Kunde | Kunde | Anbieter, falls Anbieter betreibt |
Diese Zuordnung ist keine Neuigkeit, aber wichtig als Ausgangspunkt: **nur beim SaaS- und beim betriebenen Private-Cloud-Modell braucht es einen Auftragsverarbeitungsvertrag (AVV nach Art. 28 DSGVO)** zwischen NEXARCH-Anbieter und Kunde — beim reinen On-Premise-Modell (Kunde installiert selbst gemäß `PROXMOX-DEPLOYMENT.md` Abschnitt 2) fehlt die Auftragsverarbeitungsbeziehung, dafür braucht es einen klaren Software-Lizenzvertrag.
## 2. Mandantentrennung — was die Technik jetzt zusichern kann
**Geändert:** Core setzt **Modell C** um — eine eigene, physisch getrennte Datenbank je Mandant (Core `TEN-01`/`TEN-06`/`TEN-07`), keine gemeinsame Datenbank mit applikationsseitiger Filterung mehr. Das verschiebt, was der Vertrag ehrlich versprechen kann:
- **Ein SaaS-Vertrag darf jetzt "physische Datentrennung" zusichern** — die Isolation hängt nicht mehr davon ab, dass jede einzelne Datenbank-Query korrekt gefiltert ist, sondern davon, dass ein Mandant technisch gar keine Verbindung zur Datenbank eines anderen Mandanten hat. Das ist eine belastbarere Zusage als applikationsseitige Filterung.
- Die Formulierung sollte trotzdem präzise bleiben: physisch getrennt sind die **Datenbanken**, nicht zwingend die komplette Infrastruktur (mehrere Tenant-Datenbanken können auf demselben Postgres-Server/derselben Hardware laufen, siehe `SKALIERUNGSKONZEPT.md` Abschnitt 3/4) — das ist immer noch ein starkes, aber kein "dedizierter Server pro Kunde"-Versprechen. Für Letzteres bleibt **Stufe 3** (dedizierte Instanz) die richtige Antwort, jetzt als zusätzliche Infrastruktur-Trennung obendrauf, nicht als einzige Isolationsebene.
- Praktischer Zusatznutzen für den Vertrag: eine Tenant-Datenbank lässt sich als Ganzes zwischen On-Premise- und SaaS-Betrieb verschieben (Dump/Restore) — das erleichtert vertraglich zugesicherte Wechsel des Betriebsmodells (z.B. Exit aus SaaS zurück zu On-Premise, siehe Abschnitt 6) technisch, nicht nur organisatorisch.
## 3. Verfügbarkeits-SLA je Stufe — Vertrag darf der Technik nicht vorauslaufen
`SKALIERUNGSKONZEPT.md` Abschnitt 10: **Stufe 1 hat kein Live-Failover**, nur Backup/Restore. Eine hohe Verfügbarkeits-SLA (z.B. "99,9 % Uptime") ist auf Stufe-1-Infrastruktur technisch nicht seriös zusicherbar — Ausfallzeit bei einem DB-Problem ist die Zeit bis zum Restore, nicht Sekunden/Minuten.
Vorschlag: SLA-Stufen an die technischen Betriebsstufen koppeln, nicht pauschal für alle SaaS-Kunden gleich versprechen:
- **Stufe 3**: vertraglich individuell, da Infrastruktur ohnehin verhandelt wird (siehe `LIZENZMODELL.md`).
## 4. Backup, RPO/RTO — technisch vorhanden, vertraglich noch nicht beziffert
Archive `BAK-01/02/03` liefert die Mechanik (Backup-Strategie, Snapshots, Restore-Verfahren) — was fehlt, ist eine vertraglich zugesicherte Zahl: **Recovery Point Objective (wie viel Datenverlust maximal) und Recovery Time Objective (wie lange Restore maximal dauert)** sind bisher nirgends als konkreter Wert festgelegt, weder technisch noch vertraglich. Das ist eine Lücke, die vor dem ersten SaaS-Vertrag geschlossen werden sollte — vermutlich durch einen Test des tatsächlichen Backup/Restore-Zyklus (Archive `BAK-06` Restore-Testverfahren liefert die Datengrundlage dafür).
## 5. Incident-Meldung — Core OPS-04 als vertragliche Grundlage
Core `OPS-04` (Incident-Response-Plan, DSGVO-72-Stunden-Meldefrist, siehe `SICHERHEITSKONZEPT.md`) ist die technisch-organisatorische Seite. Der AVV muss diese Meldefrist gegenüber dem Kunden UND die Kette zur Aufsichtsbehörde vertraglich abbilden — technisch ist der Prozess vorbereitet, die vertragliche Formulierung (wer meldet wem in welcher Frist) ist eigenständig zu formulieren, nicht aus `OPS-04` ableitbar.
## 5a. Auftragsverarbeitung bei externem KI-Anbieter — AI `ENG-03`
AI `ENG-03` erlaubt optional einen externen, OpenAI-kompatiblen Cloud-Anbieter als Alternative zum lokalen Ollama-Standardbetrieb; `GOV-03` steuert je Tenant, ob Inferenz lokal oder extern läuft, und schließt Training auf Kundendaten aus (siehe `SICHERHEITSKONZEPT.md` Abschnitt 18, Finding #6). Technisch ist die Trennung damit vorbereitet — vertraglich fehlt: ein Auftragsverarbeitungsvertrag (AVV) mit jedem für einen Tenant aktivierbaren externen KI-Anbieter, bevor dessen Aktivierung für diesen Tenant freigegeben wird. Kein technisches Ticket nötig (die Umschaltung selbst existiert bereits über `GOV-03`), aber ein organisatorischer Prozess: externe Anbieter dürfen für einen Tenant erst in `GOV-03` aktivierbar sein, wenn ein AVV mit diesem Anbieter vorliegt — sonst bleibt die Tenant-Konfiguration auf "nur lokal" beschränkt.
## 6. Exit-Strategie & Datenrückgabe bei Vertragsende
Technisch bereits abgedeckt: DMS `DOC-08` (Massenexport), Mail `ARC-07` (Export/Portabilität), Connect `DMS2-01` (offenes Austauschformat) — die ecoDMS-Lehre "kein Vendor-Lock" (`STORAGE-KONZEPT.md` Abschnitt 8) gilt technisch bereits.
Vertraglich fehlt: eine zugesicherte Frist, in der Kundendaten nach Kündigung exportierbar bleiben, bevor sie gelöscht werden — vergleichbar mit der Parallelbetriebsfrist im `MIGRATIONSKONZEPT.md` (dort 90 Tage für den Altsystem-Übergang vorgeschlagen, hier für den umgekehrten Fall: Kunde verlässt NEXARCH). Kein technisches Ticket dafür nötig (Export-Mechanik existiert), nur eine vertragliche Frist plus ggf. ein Ablaufprozess ("Datenlöschung nach Fristablauf" als Anwendung der Archive-Retention-Engine, aber in die andere Richtung: nicht Aufbewahrung erzwingen, sondern nach Frist tatsächlich löschen).
## 7. Modul-Deaktivierung während der Laufzeit — technische Lücke
Core `LIC-02` schaltet Module per Feature-Flag ab, wenn eine Lizenz das vorsieht (Kunde bucht z.B. AI ab). **Was mit den Daten des deaktivierten Moduls passiert, ist nirgends spezifiziert** — bleiben sie erhalten (nur Zugriff gesperrt) oder werden sie gelöscht? Das ist sowohl eine technische als auch eine vertragliche Frage, die aktuell in keinem Ticket beantwortet ist. Vorschlag: Deaktivierung sperrt nur den Zugriff, löscht nichts automatisch (konsistent mit der generellen Linie "Löschung nur über die Archive-Retention-Engine", siehe `STORAGE-KONZEPT.md` Abschnitt 4) — sollte aber als explizites Akzeptanzkriterium in `LIC-02` nachgezogen werden, falls das noch nicht der Fall ist.
## 8. Offene Punkte
- RPO/RTO-Werte fehlen (Abschnitt 4) — brauchen einen tatsächlichen Restore-Test, bevor sie vertraglich zugesichert werden können.
- Modul-Deaktivierung ohne Datenverlust ist als Verhalten plausibel, aber nicht als Akzeptanzkriterium in Core `LIC-02` verankert — sollte geprüft/ergänzt werden.
- Exit-Frist (Abschnitt 6) ist ein reiner Vertragswert, keine Technik — muss vom Geschäft festgelegt werden, ähnlich wie die Preisgestaltung in `LIZENZMODELL.md`.
- Dieses Dokument ersetzt keine juristische Prüfung von AVV/AGB — es benennt nur, wo Vertrag und Technik zusammenpassen müssen.
- AVV mit externen KI-Anbietern (Abschnitt 5a) ist ein reiner Vertragsschritt, keine Technik — muss vor Freigabe eines externen Anbieters in `GOV-03` je Tenant vorliegen.
Stand: 2026-08-26, alle 10 Punkte in Abschnitt 11 zwischen 2026-08-28 geklärt oder durch neue Tickets adressiert (Core OPS-06, Core IAM-15, DMS SRC-11, Mail SRC-11). Grundlage: `nexarch-state.json`, `SKALIERUNGSKONZEPT.md`, `STORAGE-KONZEPT.md`, `core-kanban/`, `dms-kanban/`, `mail-kanban/`, `archive-kanban/`, `workflow-kanban/`, `dms-kanban/known-issues-archivdms.md`, `dms-kanban/known-issues-archivmail.md` (Boards inzwischen aus dem Repo entfernt, Ticket-Historie liegt in der Gitea-PR-Historie).
Dieses Dokument führt keine neue Architektur ein. Es fasst die bereits in den sieben Boards getroffenen sicherheitsrelevanten Einzelentscheidungen zu einem Gesamtbild zusammen und prüft dabei — nach demselben Muster wie `STORAGE-KONZEPT.md` — auf Lücken und Widersprüche. Prüfraster: OWASP Top 10, angewendet auf den tatsächlichen NEXARCH-Techstack (Go, PostgreSQL, JWT, S3-kompatibler Storage, Manticore), nicht generisch abgehandelt.
Die Wahl bcrypt statt Argon2 ist im Entscheidungsverlauf nicht mit einer eigenen Begründung hinterlegt (`nexarch-state.json` Techstack-Feld nennt es nur als Faktum). Das ist keine Sicherheitslücke — bcrypt gilt weiterhin als solide, solange Kostenfaktor aktuell gehalten wird — aber eine unbegründete Abweichung vom heute üblichen Argon2id-Standard sollte im Core-Board mindestens als bewusste Entscheidung mit Kostenfaktor-Wert dokumentiert werden, nicht nur als Faktenzeile in der State-Datei.
**Korrektur gegenüber archivdms:**`known-issues-archivdms.md` Punkt 9 beschreibt einen In-Memory-Rate-Limiter ohne Shared State (`internal/auth/ratelimit.go`) — bei Mehrinstanz-Deployment pro Instanz getrennt und damit aushebelbar. `SKALIERUNGSKONZEPT.md` Abschnitt 1 hält fest, dass Core `IAM-07`/`API-03` Rate-Limiting mit **shared state statt in-process** umsetzt. Damit ist der konkrete archivdms-Fehler für den Login-Pfad explizit adressiert.
2FA-Aktivierungspflicht ist laut `IAM-04` pro Tenant konfigurierbar, nicht global erzwungen — das ist eine bewusste Flexibilitätsentscheidung (SaaS-Betrieb mit unterschiedlichen Kundenanforderungen), sollte aber im Betriebshandbuch als Empfehlung ("2FA-Pflicht für Admin-Rollen mindestens") festgehalten werden, sobald ein Betriebshandbuch existiert (siehe `fehlende_konzeptdokumente`).
## 2. Autorisierung / Zugriffskontrolle
RBAC ist vollständig in Core zentralisiert (`RBAC-01`..`RBAC-05`) — kein Modul implementiert eigene Rechteprüfung, das ist direkte Konsequenz der IAM-Konsolidierung (`nexarch-state.json` Entscheidungsverlauf "Konsolidierung IAM").
**Korrektur gegenüber archivmail:**`known-issues-archivmail.md` Punkt 7 beschreibt Tenant-Filterung als **Post-Filter statt in der Query** (`internal/api/ediscovery.go:90`) — fehleranfällig, weil jeder neue Codepfad das Filtern erneut korrekt implementieren muss. Die Notiz im selben Dokument ordnet das explizit auch DMS `IAM-03` ("Policy-Enforcement — zentral, nicht pro Handler") zu. Das Prinzip "Tenant-/Rechte-Scoping ausschließlich in der Query-/Repository-Schicht, nie als Post-Filter im Handler" ist damit als Korrektur benannt, aber es lohnt sich, es hier noch einmal projektweit festzuhalten: Diese Regel gilt für **jedes** Modul, das gegen die zentrale Core-RBAC-Prüfung arbeitet (DMS, Mail, Archive, Workflow, AI, Connect gleichermaßen), nicht nur für die ursprünglich betroffene eDiscovery-Funktion.
**Tenant-Isolation — strukturell gelöst durch Modell C:** Multi-Tenancy läuft nach Modell C: eine physisch getrennte Datenbank je Mandant plus schlanke Registry (Core `TEN-01`/`TEN-06`/`TEN-07`, `nexarch-state.json``multi_tenancy`, Begründung in `SKALIERUNGSKONZEPT.md` Abschnitt 4). Ein vergessener `tenant_id`-Filter kann damit keine fremden Daten mehr liefern, weil sie in der angesprochenen Datenbank nicht existieren; die Fehlerklasse aus `known-issues-archivdms.md` (Punkte 9/10, Tenant-Scoping-Fragilität) und `known-issues-archivmail.md` (#2/#7) ist strukturell beseitigt, nicht nur unwahrscheinlicher. Row-Level-Security ist bei physischer Trennung redundant und nicht vorgesehen. Die harte Regel aus dem vorigen Absatz (Rechte-Scoping zentral in der Repository-Schicht, nie als Post-Filter im Handler) bleibt bestehen, denn sie betrifft die Rechte *innerhalb* eines Mandanten (Ordner, Dokumente, Rollen), nicht die Mandantentrennung; `known-issues-archivdms.md` Punkt 2 fordert dafür eine Testpflicht als Merge-Gate, die bisher in keinem QA-Ticket als projektweite CI-Regel verankert ist (siehe Abschnitt 10). Verbleibendes Restrisiko liegt nicht mehr im Filter, sondern in der Zuordnung Mandant → Datenbank (Registry, Verbindungsinformation) und in mandantenübergreifenden Prozessspeichern (Caches, Pools): Kompensation sind der Pflicht-Tenant-Kontext bei jedem Modulaufruf (DMS `INT-16`), mandantengetrennte Cache-Schlüssel (Core `RBAC-10`) und der Mandant aus dem Token statt aus Anfrageparametern.
**Superadmin-Sonderfall:**`known-issues-archivdms.md` Punkt 4 beschreibt einen wiederholt vergessenen Superadmin-ohne-Tenant-Zustand (403 "tenant context required", mehrfach nachträglich gepatcht). Kein Core-Ticket in der gelesenen Auswahl adressiert das explizit als First-Class-Datenmodell-Zustand — siehe Offene Punkte (Abschnitt 11).
## 3. Injection-Schutz
Dynamische SQL-Klauseln/Tabellennamen via `fmt.Sprintf` bzw. String-Konkatenation waren ein wiederkehrendes Antipattern in beiden Altsystemen:
- archivdms: `known-issues-archivdms.md` Punkt 10 — dynamische Tabellennamen via `fmt.Sprintf` (`taxonomy.go`, `workflows.go`, `classification_templates_title.go`, `internal/index/manticore.go`), zur Tatzeit nur durch eine Whitelist-Funktion abgesichert, als "fragiles Muster" bewertet.
- archivmail: `known-issues-archivmail.md` Punkte 11/12 — dynamische SQL-Klauseln via Sprintf/Join (`internal/index/manticore.go:268,323`, `internal/storage/ocr.go:159`), gleiches Muster zweimal dupliziert.
Beide Fundstellen betreffen denselben Bereich: Suchindex-/Query-Aufbau gegen Manticore bzw. Postgres mit dynamischen Feldlisten. Die Korrektur ist in den entsprechenden Suchindex-Tickets verankert: DMS `SRC-01`, Mail `SRC-01` (Suchindex/Such-API-Grundlage der jeweiligen Module). Die verbindliche Regel aus beiden Known-Issues-Dokumenten lautet übereinstimmend: **Spalten-/Tabellennamen ausschließlich aus statischen Konstanten bzw. einem geschlossenen Enum/Switch-Typ, nie aus Nutzereingabe oder generischem String-Zusammenbau, auch nicht hinter einer Whitelist-Funktion.** Ein zentraler, getesteter Query-Builder mit Feld-Whitelist (`known-issues-archivmail.md` Punkt 12) ist der empfohlene Ansatz — ob dieser als gemeinsame Utility zwischen DMS `SRC-01` und Mail `SRC-01` geteilt wird oder je Modul separat entsteht, ist in keinem der beiden Tickets festgelegt (siehe Offene Punkte).
## 4. Sensible Daten / Verschlüsselung
| Baustein | Entscheidung | Ticket |
|---|---|---|
| Verschlüsselung at rest | AES-256-GCM, Envelope Encryption | DMS `FDN-09`, Mail `ARC-02` |
| Reihenfolge Hash/Verschlüsseln | SHA-256-Hash auf Klartext **vor** Verschlüsselung (sonst funktioniert Dublettenerkennung nie) | DMS `FDN-09`/`DOC-02`, Mail `ARC-02`/`ARC-03` |
| Secrets | ausschließlich über Umgebungsvariablen, nie im Code | durchgängige Ticket-Regel (Abschlussabschnitt jedes Tickets, u.a. explizit in `IAM-04`..`IAM-07`, `RBAC-01`..`RBAC-05`, `API-02`..`API-06`, `AUD-05`) |
Die Hash-vor-Verschlüsseln-Reihenfolge ist ein gutes Beispiel für eine bereits im Projekt selbst aufgedeckte und behobene Lücke: laut Entscheidungsverlauf fehlte sie zunächst im Mail-Board (`ARC-02`/`ARC-03`), obwohl DMS (`FDN-09`) sie schon hatte, und wurde nachgezogen — dokumentiert in `nexarch-state.json` unter "Duplikatserkennung vor Weiterverarbeitung".
**Schlüsselverwaltung/-rotation — geklärt (2026-08-27):** Zweistufige Schlüsselhierarchie statt eines einzelnen globalen Master-Key. Core hält EINEN Master-Key (Root-KEK, über Umgebungsvariable/Secret-Provider, austauschbar gegen echten KMS-Dienst), der ausschließlich **Tenant-Schlüssel** (Tenant-KEK) verpackt — niemals Objekte direkt. Jeder Mandant erhält bei seiner Provisionierung (`TEN-01`) einen eigenen Tenant-KEK, verschlüsselt mit dem Master-Key in der Registry abgelegt. DMS (`FDN-09`) und Mail (`ARC-02`) beziehen für ihre pro-Objekt-DEKs ausschließlich den entschlüsselten Tenant-KEK ihres jeweiligen Mandanten (nie den Master-Key direkt), authentifiziert über das Service-Credential aus `API-02`. Rotation ist auf beiden Ebenen unabhängig möglich, ohne Objektdaten neu zu verschlüsseln: Master-Key-Rotation verpackt nur alle Tenant-KEKs neu (klein, schnell), Tenant-KEK-Rotation verpackt nur die DEKs dieses einen Mandanten neu und berührt keinen anderen Tenant. Umgesetzt in Core `API-10`.
**Begründung für die Tenant-Ebene:** Ein einzelner globaler Master-Key hätte einen gemeinsamen Blast-Radius für alle Mandanten geschaffen — ein kompromittierter Tenant-KEK (z. B. durch ein fehlerhaftes Modul-Log oder einen kompromittierten Service-Credential-Aufruf) betrifft mit der zweistufigen Hierarchie strukturell nur diesen EINEN Mandanten. Das ist die konsequente Fortsetzung der physischen Modell-C-Isolation (siehe TEN-01, Entscheidungsverlauf "Multi-Tenancy: Modell A -> Modell C") auch auf Schlüsselebene, statt einer Isolationslücke, die die sonstige Trennung wieder aufheben würde.
Die JWT-Schlüsselrotation (Core-Public-Key, `kid`-Rotation) ist dagegen bereits ausformuliert (`SKALIERUNGSKONZEPT.md` Abschnitt 11 letzter Punkt, Core `API-05`): muss ohne Ausfallzeit anderer Module möglich sein. Das ist ein anderer Schlüssel als der Storage-Verschlüsselungs-Master-Key und löst dessen Rotationsfrage nicht mit.
## 5. Eingabevalidierung / bekannte Angriffsvektoren aus den Altsystemen
| Header-Injection bei E-Mail-Versand (From/To/Subject per Stringkonkatenation ohne CRLF-Prüfung) | `known-issues-archivmail.md`#1, `internal/mailer/mailer.go:144-146` | Mail `ING-03` — `net/mail`-Writer bzw. harte CRLF-/Steuerzeichen-Prüfung |
| Ungebremstes Lesen von Anhängen (`io.ReadAll` ohne Größenlimit → Speicherbombe) | `known-issues-archivmail.md`#3, `pkg/mailparser/parser.go` | Mail `ING-04` (Ingestion) und DMS `DOC-01` (Upload-API) — `io.LimitReader` mit Max-Size, harte Ablehnung; explizit auch für DMS-Uploads übernommen, nicht nur Mail |
| XSS-Risiko bei Anhang-/Dokument-Vorschau über iframe-Rendering von Fremdinhalt | `known-issues-archivmail.md`#9, positiv gelöst via `PdfCanvasPreview.tsx` (Canvas statt iframe) | DMS `DOC-05` (PDF-Viewer) und Mail `UX-03` — Canvas-Rendering von Anfang an, kein iframe/HTML-Rendering von Fremdinhalt |
| Content-Type-Erkennung über Dateiendung statt Inhalt | `known-issues-archivmail.md`#8, `AttachmentPreviewDialog.tsx` | betrifft ebenfalls DOC-05 — serverseitige Magic-Bytes-Erkennung statt Dateiendung, im Ticket-Titel nicht explizit als Akzeptanzkriterium sichtbar (siehe Offene Punkte) |
Die Speicherbomben-Korrektur (`DOC-01`/`ING-04`) ist im Entscheidungsverlauf nicht als eigener Eintrag sichtbar, aber laut Known-Issues-Dokument bereits als "betrifft auch DMS" annotiert — das Konzept übernimmt diese Zuordnung, prüft aber nicht, ob `DOC-01` das Größenlimit tatsächlich als Akzeptanzkriterium enthält oder nur als Empfehlung im Known-Issues-Dokument steht. Diese Prüfung sollte bei nächster Gelegenheit nachgeholt werden.
## 6. Vertrauensstellung zwischen Modulen
NEXARCH ist modular mit sieben eigenständigen Boards — Vertrauen zwischen Modul und Core (und indirekt zwischen Modulen) ist eine eigene Angriffsfläche, die in einem Monolithen nicht existieren würde.
- **Modul vertraut Core:** asymmetrische JWT-Signatur, Core signiert mit privatem Schlüssel, Module verifizieren lokal mit öffentlichem Schlüssel über JWKS-ähnliche Verteilung mit `kid`-Rotation (Core `API-05`).
- **Core vertraut Modul:** Service-Credential je Modul-Instanz, ausgestellt bei Provisionierung (Core `API-02`, erweitert). Notwendig, weil einseitiges Vertrauen (nur Module prüfen Core-Signatur) nicht ausreicht — Core muss bei Nachlieferung/Registrierung wissen, welche Modul-Instanz tatsächlich anfragt, sonst könnte sich ein fremder Absender als Modul ausgeben (`nexarch-state.json` Entscheidungsverlauf "Vertrauensstellung Core↔Module").
- **Wiederanlauf nach Ausfall:** sofortiger Cache-Refresh per Health-Check-Trigger, gepufferte Audit-Events/Nutzungszähler werden über das Service-Credential authentifiziert nachgeliefert, Pufferqueue muss persistent sein (Core `API-06`).
- **Rückkanal-API-Berechtigungsprüfung:** Workflow `VOR-04` (Pull-Rückkanal, damit DMS/Mail Akte/Vermerke/Wiedervorlage im eigenen Kontext anzeigen können) ist explizit als "read-only, berechtigungsgeprüft" spezifiziert — das ist der einzige Rückkanal (Pull), der zusätzlich zum bereits bestehenden Push-Kanal (Workflow `ENG-02`) existiert und daher eine eigene Berechtigungsprüfung braucht, nicht nur das Push-seitige Modul-Vertrauen wiederverwenden darf.
Dieses wechselseitige Vertrauensmodell (asymmetrische Signatur + Service-Credential) ist die Antwort auf ein OWASP-Broken-Access-Control-Szenario, das speziell durch die Modul-Architektur entsteht und in einem monolithischen Altsystem (archivdms/archivmail) so nicht vorkam — beide Known-Issues-Dokumente enthalten dazu folgerichtig keinen Eintrag.
## 7. Ausfallsicherheit als Sicherheitsthema
Verfügbarkeit ist hier aufgeführt, weil ein unkontrollierter Core-Ausfall entweder zu Totalausfall der Plattform (worst case für Verfügbarkeit) oder — schlimmer — zu unkontrolliertem Fail-open bei Rechteprüfungen führen könnte, wenn nicht bewusst entschieden wird.
- **Fail-open vs. fail-closed** ist explizit pro Aktionsklasse festgelegt (Core `API-05`/`API-06`, ausformuliert in `SKALIERUNGSKONZEPT.md` Abschnitt 11): Lesevorgänge mit abgelaufenem Rechte-Cache eher fail-open (letzter bekannter Stand gilt weiter), sicherheitskritische Aktionen (Rechteänderung, Löschung, Freigabe) eher fail-closed (lieber verweigern als mit veralteten Rechten ausführen). Das ist eine bewusste, dokumentierte Abwägung — kein Automatismus in eine Richtung.
- **Core-Hochverfügbarkeit** selbst (`SKALIERUNGSKONZEPT.md` Abschnitt 10): Anwendungsschicht ist zustandslos, echter Single Point of Failure ist die Postgres-Instanz. Stufe 1 kein Live-Failover (Backup/Restore, Archive `BAK-03`), Stufe 2/3 Streaming-Replikation mit pg_auto_failover (LXC-tauglich) oder Patroni (Multi-Standort), Verbindungsindirektion via PgBouncer/HAProxy, Fencing gegen Split-Brain.
- Wichtiger Nebeneffekt, im Skalierungskonzept explizit benannt: der JWKS-Mechanismus aus `API-05` bleibt während eines kurzen Core-DB-Failover-Fensters funktionsfähig, weil Signaturprüfung keine Core-DB-Verbindung braucht — Core-DB-Failover und die Resilienz der anderen Module sind zwei getrennte, sich gegenseitig entlastende Probleme.
## 8. Sicherheitsrelevante Betriebsaspekte
- **Transportverschlüsselung E-Mail:** TLS/STARTTLS für IMAP/POP3/SMTP (Mail `ING-06`).
- **Kein direkter Storage-Zugriff:** Up-/Downloads laufen ausschließlich über zeitlich begrenzte signierte URLs, die das Backend ausstellt — Storage-Zugangsdaten erreichen nie den Browser (`STORAGE-KONZEPT.md` Abschnitt 5, DMS `FDN-03` Akzeptanzkriterium 2). Presigned-URL-Gültigkeit soll kurz und konfigurierbar gehalten werden (Minuten, nicht Stunden).
- **Zentrales Rate-Limiting:** Core `API-03` mit Shared State statt in-process — korrigiert denselben archivdms-Mehrinstanz-Bug wie in Abschnitt 1 beschrieben, hier aber als API-weite (nicht nur Login-spezifische) Infrastruktur, seit der API/Token/Rate-Limiting-Konsolidierung (`nexarch-state.json` Entscheidungsverlauf) alleinige Quelle für alle Module — DMS/Mail betreiben kein eigenes Rate-Limiting mehr.
## 9. Bewährte Muster, die bewusst übernommen werden
Nicht jeder Fund in den Altsystemen ist ein Fehler. Beide Known-Issues-Dokumente benennen positiv zu übernehmende Muster ausdrücklich:
- **Vier-Augen-Prinzip mit `FOR UPDATE`-Lock** bei Löschbestätigung (`known-issues-archivdms.md`, Abschnitt "Positiv zu übernehmende Muster") — im NEXARCH-Kontext die passende Entsprechung für Compliance-kritische Freigaben in Archive `CMP-06` (Vier-Augen-Prinzip), wo derselbe Lock-Mechanismus gegen doppelte/konkurrierende Freigabe schützen sollte.
- **Timing-safe Vergleich** bei sicherheitsrelevanten Entscheidungen — in archivdms bei Share-Link-Passwortvergleich eingesetzt (`known-issues-archivdms.md`, Positiv-Abschnitt). Gilt als generelles Prinzip für jeden Vergleich, der eine Zugriffsentscheidung trifft (Passwort, Token, Signatur, Wiederherstellungscode aus `IAM-04`), nicht nur für Share-Links — sollte als projektweite Coding-Konvention (analog zur Sprintf-Verbots-Regel aus Abschnitt 3) festgehalten werden, ist aber in keinem Ticket als Konvention formuliert, nur implizit durch das positive Altsystem-Beispiel nahegelegt.
- **Tenant-Filter in der Query, IDOR-Guards bei Shares** — ebenfalls aus archivdms als positiv übernommenes Muster gelistet; deckt sich mit der in Abschnitt 2 verlangten Regel (Scoping in der Repository-Schicht).
## 10. CI/Merge-Gates für sicherheitsrelevanten Code
`known-issues-archivdms.md` Punkt 2 fordert explizit Testpflicht für Auth/Tenant-Scoping/Retention als Merge-Gate; `known-issues-archivmail.md` Punkt 4 fordert dasselbe für Tenant-Scoping/Protokoll-State-Machines. Beide Forderungen sind in den Known-Issues-Dokumenten als Lehre formuliert, aber in keinem gelesenen QA-Ticket (Core `QA-01`..`QA-05`, DMS `QA-01`, Mail `QA-07`) als projektweite, verbindliche CI-Regel ("kein Merge auf sicherheitsrelevanten Code ohne Test") wiedergefunden. Das ist keine fehlende Einzelentscheidung, sondern eine fehlende Verallgemeinerung einer bereits zweimal unabhängig gemachten Erfahrung — siehe Offene Punkte.
## 11. Offene Punkte
Die folgenden Themen sind entweder in keinem Board bisher behandelt oder nur am Rand erwähnt, ohne dass ein Ticket sie abdeckt. Sie werden hier bewusst nur benannt, nicht mit erfundenen Tickets vorweggenommen — das entscheidet der Nutzer:
1.~~Security-Header für das Next.js-Frontend~~ — **geklärt (2026-08-28)**: Core `API-08` "Security-Header-Baseline für alle Frontends" deckt CSP, HSTS, X-Frame-Options, Referrer-Policy, X-Content-Type-Options als gemeinsame, wiederverwendbare Middleware ab, die jedes Modul-Frontend einbindet statt eigener Definition.
2.~~Dependency-/Supply-Chain-Sicherheit~~ — **geklärt (2026-08-28)**: neues Ticket Core `OPS-06` "Automatisiertes Schwachstellen-Scanning (Supply-Chain)" verankert `govulncheck` (Go-Module) und `npm audit`/Dependabot (Frontend-Pakete) als verbindlichen, blockierenden CI-Schritt über alle sieben Boards. Hängt im Core-Abschluss-Gate `QA-05`.
3.~~Schlüsselverwaltung/-rotation für den Storage-Verschlüsselungs-Master-Key~~ — **geklärt (2026-08-27), siehe Abschnitt 4**: zweistufige Hierarchie Master-KEK → Tenant-KEK → Objekt-DEK, umgesetzt in Core `API-10`.
4.~~Verschlüsselung in Transit zwischen Modulen~~ — **geklärt (2026-08-28)**: Core `API-09` "mTLS zwischen Modul-Instanzen (verteilte Installation)" deckt genau das ab — gegenseitige TLS-Authentifizierung zwischen allen sieben Modul-Instanzen für Stufe 2/3, optional für Stufe-1-Einzel-LXC. Hängt im Core-Abschluss-Gate `QA-04`. `PROXMOX-DEPLOYMENT.md` Abschnitt 3 referenziert es bereits korrekt als ab Stufe 2 verpflichtend.
5.~~Penetrationstest-Verfahren vor Produktivbetrieb~~ — **geklärt (2026-08-28)**: Core `QA-06` "Externe Sicherheitsprüfung / Penetrationstest vor Produktivbetrieb" deckt genau das ab, projektweit über alle sieben Boards, als externe Ergänzung zu den internen Sicherheits-Prüfgates. `QA-06` ist selbst das letzte, allem nachgelagerte Gate (hängt von `QA-05`, `QA-07`, `QA-08` ab) — der Pentest läuft also erst, wenn alles andere bereits abgenommen ist.
6.~~Incident-Response-Plan~~ — **geklärt (2026-08-28)**: Core `OPS-04` deckt genau das ab (Erkennung, Eskalation, DSGVO-72h-Meldefrist, Verweis auf Core `AUD-01`/`AUD-05` als Vorfall-Datenquelle) und ist im `code`-Repository bereits als tatsächliches Betriebsdokument umgesetzt (`docs/INCIDENT-RESPONSE-PLAN.md`). Offen bleibt nur die Schlüssel-Notfallrotation im Vorfallsfall — die behandelt `OPS-04` nicht explizit, sollte als Ergänzung im Dokument nachgezogen werden.
7.~~Superadmin-ohne-Tenant als First-Class-Zustand~~ — **geklärt (2026-08-28)**: Core `IAM-01` Akzeptanzkriterium 2 lautet explizit "Superadmin-ohne-Tenant ist als First-Class-Zustand modelliert" — war bei der ersten Lektüre offenbar übersehen worden, ist aber bereits vorhanden.
8.~~Verbindliche CI-Testpflicht für sicherheitsrelevanten Code als projektweite Regel~~ — **geklärt (2026-08-28)**: Akzeptanzkriterium "Pflichttests für sicherheitsrelevanten Code (Tenant-Scoping, Zugriffsprüfung, Auth-nahe Logik) als verbindliches Merge-Gate" in allen 6 übrigen `QA-01`-Tickets (DMS, Mail, Archive, Workflow, AI, Connect) nachgezogen, analog zu Core `QA-01`. Jetzt projektweit einheitlich.
9.~~Timing-safe Vergleich als projektweite Coding-Konvention~~ — **geklärt (2026-08-28)**: neues Ticket Core `IAM-15` verankert die Regel projektweit (Passwort-Hash, Token, Signatur, 2FA-Wiederherstellungscode), nicht mehr nur implizit über das archivdms-Beispiel. Hängt im Core-Abschluss-Gate `QA-05`.
10.~~Gemeinsamer Query-Builder für Injection-Schutz~~ — **geklärt (2026-08-28)**: Entscheidung getroffen — getrennte Implementierung je Board, keine gemeinsame Utility, konsistent mit der bereits getroffenen Entscheidung, Suche/OCR nicht zu zentralisieren (siehe `nexarch-state.json` → `bewusst_nicht_zentralisiert`). Neue Tickets DMS `SRC-11` und Mail `SRC-11` verankern dieselbe Whitelist-Regel unabhängig in beiden Boards, hängen in DMS `QA-06` bzw. Mail `QA-04`.
11.**bcrypt-Kostenfaktor nicht dokumentiert.** Die Wahl bcrypt statt Argon2 ist als Faktum in `nexarch-state.json` festgehalten, aber ohne Begründung und ohne festgelegten Kostenfaktor.
12.~~Anomalie-Erkennung bei API-Token-Nutzung (Core `IAM-09`)~~ — **geklärt (2026-09-14)**: neues Ticket Core `IAM-20` "Anomalie-Erkennung bei API-Token-/Service-Account-Nutzung" ergänzt IP-/Rate-Anomalie-Erkennung als Hinweissignal (Audit-Event, keine automatische Sperre) zur reinen Scope-/Ablaufzeit-Prüfung aus `IAM-09`. Siehe Threat-Model-Finding #2 in Abschnitt 13.
## 13. Threat Model: NEXARCH Core (STRIDE)
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: Core-Kanban (Identity, Tenancy, Auth, RBAC, Lizenzierung/Feature-Flags, Audit, zentrale Konfiguration/Benachrichtigung, Core-API/Modulverwaltung, Monitoring), analysiert anhand der Ticket-Inhalte (TEN/IAM/RBAC/AUD/LIC/CFG/API/OPS), nicht anhand des tatsächlichen Codes. Alle Findings mit Schweregrad Kritisch/Hoch/Mittel sind inzwischen als Tickets in Welle 9 und 10 des Core-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea); Finding #2 (Niedrig-Mittel) ist offen, siehe Punkt 11 oben.
| 6 | Information Disclosure | API-08 Security-Header-Baseline | Gut abgedeckt (CSP/HSTS/X-Frame etc. zentral) | Niedrig | abgedeckt |
| 7 | Information Disclosure | TEN-01 Control-Plane-Registry | Höchstwertiges Einzelziel (Verbindungsinfo zu allen Tenant-DBs + Superadmin-Konten) — kein eigenes Härtungs-Ticket | Kritisch | `TEN-09` |
| 8 | Information Disclosure | IAM-05 LDAP/AD-Anbindung | LDAP-Bind-Credentials je Tenant — Speicherform (Klartext vs. verschlüsselt) nicht spezifiziert | Mittel | `IAM-19` |
| 9 | Denial of Service | TEN-06 Connection-Pooling | Ungedeckeltes LRU-Pooling ohne Rate-Limit auf Tenant-Wechsel — Pool-Thrashing-Risiko | Mittel | `TEN-11` |
| 10 | Denial of Service | LIC-02 Feature-Flag-Service | Kein definiertes Fail-Open/Fail-Closed-Verhalten bei Registry-Ausfall oder Cache-Invalidierungssturm | Mittel | `LIC-06` |
| 11 | Elevation of Privilege | RBAC-01 Rollenmodell | Keine Prüfung gegen zirkuläre/fehlerhafte Rollenvererbung Tenant-Admin → Superadmin | Hoch | `RBAC-08` |
| 12 | Elevation of Privilege | API-09 mTLS Modul-zu-Modul | Für Stufe-1 optional — Aktivierung beim Wechsel auf Stufe 2 könnte vergessen werden | Mittel | `OPS-07` |
| 13 | Elevation of Privilege | IAM-06 SSO/OIDC | Claim-Validierung (Issuer/Audience/Signatur) nicht spezifiziert — Risiko automatischer Superadmin-Ableitung aus externem Claim | Hoch | `IAM-18` |
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: DMS-Kanban (Dokumentenkern/Upload/Storage/Verschlüsselung, Suche & OCR, Metadaten, Versionierung, Integration/API, Hot-Folder/Scanner, UX), analysiert anhand der Ticket-Inhalte, nicht anhand des tatsächlichen Codes. Identity/Auth/RBAC/Tenancy sind nach Core ausgelagert und dort bereits in Abschnitt 13 analysiert — hier nicht erneut. Alle Findings sind als Tickets in Welle 12 (Hoch/Mittel) bzw. Welle 13 (Finding #9, `DOC-18`, ergänzt 2026-09-14) des DMS-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea).
| 11 | Elevation of Privilege | DOC-02 Duplikat-Prüfung | Falls Hash-Abgleich global statt tenant-scoped implementiert wird: Seitenkanal-Leck über Duplikat-Treffer | Niedrig | Hinweis, Implementierungsdetail bei Umsetzung von DOC-02 explizit tenant-scoped halten |
### Bereits abgedeckt
- **Envelope-Encryption pro Objekt** (`FDN-09`) korrekt an Core-Schlüsselhierarchie (`API-10`) gekoppelt, keine eigene Master-Key-Logik im DMS.
- **Tenant-Isolation strukturell** über Core Modell C — gilt auch hier.
- **Duplikat-Erkennung/Checksum** (`DOC-02`) als bewusste Nutzerentscheidung statt automatischem Verwerfen.
- **known-issues-archivdms.md** bereits als Vermeidungs-Checkliste im Board berücksichtigt (Rechte-Bug Ersteller-Sichtbarkeit, Superadmin-Sonderfall, Sprintf-in-SQL) — deshalb hier nicht erneut als Findings gelistet.
## 15. Threat Model: NEXARCH Mail (STRIDE)
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: Mail-Kanban (Ingestion & Protokoll-Server IMAP/POP3/SMTP, Postfach-Import, Archivierung, Suche & OCR, Integration/API), analysiert anhand der Ticket-Inhalte, nicht anhand des tatsächlichen Codes. Identity/Auth/RBAC/Tenancy sind nach Core ausgelagert (Abschnitt 13). Code-Wiederverwendung aus archivmail ist hier – anders als bei DMS – ausdrücklich erlaubt, daher liegt ein Schwerpunkt auf bekannten Altsystem-Bugs aus `known-issues-archivmail.md`. Alle Findings sind als Tickets in Welle 8 des Mail-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea).
| 4 | Denial of Service | SRC-02/SRC-07 Indexierung/OCR | Keine Ressourcenlimits je Job (Zeit/Speicher/Seitenzahl), analog DMS-Finding | Mittel | `SRC-12` |
| 5 | Denial of Service | ING-09 Rate-Limiting | Bereits abgedeckt — SMTP-Annahme und Login-Versuche rate-limitiert | Niedrig | abgedeckt |
| 6 | Tampering | IMP-02/IMP-06 Anhang-Verarbeitung | Bereits abgedeckt — Virenscan mit Quarantäne vorgesehen (IMP-06), Größenlimit als Lehre aus archivmail (known-issues Punkt 3) im Board verankert | Niedrig | abgedeckt |
| 7 | Information Disclosure | ARC-06 Objekt-Storage | Bereits abgedeckt — physische Mandantentrennung je Bucket/Pfad-Root statt Präfix-Konvention | Niedrig | abgedeckt |
### Bereits abgedeckt
- **Virenscan mit Quarantäne** (`IMP-06`) bereits im Board vorgesehen, nicht erst nachträglich als Finding nötig (anders als beim DMS-Board, wo dies fehlte).
- **Physische Mandantentrennung im Objekt-Storage** (`ARC-06`) korrigiert das archivmail-Muster einer gemeinsamen Ablage mit Pfad-Präfix.
- **Rate-Limiting auf Protokollebene** (`ING-09`) für SMTP-Annahme/Login bereits vorgesehen.
- **TLS/STARTTLS verpflichtend** (`ING-06`) für alle drei Protokollserver.
- **Signatur-Prüfung bei Archivierung mit Zeitstempel** (`ARC-10`) für S/MIME/PGP-signierte Mails nach BSI TR-03125.
## 16. Threat Model: NEXARCH Archive (STRIDE)
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: Archive-Kanban (Retention & WORM, Compliance & eDiscovery, Backup & Wiederherstellung, Kennzahlen) — die zentrale, alleinige Quelle für Aufbewahrung/WORM/Compliance/Backup für DMS und Mail. Identity/Auth/RBAC/Tenancy nach Core ausgelagert (Abschnitt 13). Alle Findings sind als Tickets in Welle 7 des Archive-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea).
- **Eigene, von Dokument-Retention unabhängige Backup-Prune-Policy** (`BAK-07`) — Backups laufen nicht unbegrenzt oder zu früh ab.
## 17. Threat Model: NEXARCH Workflow (STRIDE)
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: Workflow-Kanban (Workflow-Engine, Genehmigungen & Freigaben, Aufgaben/Fristen & Eskalation, regelbasiertes Routing, Akte & Vorgang) — alleinige Quelle für Genehmigungsprozesse, Aufgaben/Fristen und Routing für DMS und Mail. Identity/Auth/RBAC/Tenancy nach Core ausgelagert (Abschnitt 13). Alle Findings sind als Tickets in Welle 8 des Workflow-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea).
| 1 | Information Disclosure / Elevation of Privilege | VOR-04 Rückkanal-API | Kein festgelegter Tenant-Kontext-Abgleich bei Pull-Anfrage von DMS/Mail — Bypass der Mandantentrennung über den Rückkanal möglich | Hoch | `VOR-06` |
| 2 | Spoofing | ENG-02 Registrierungsschnittstelle | Tenant-Kontext bei Push-Registrierung durch DMS/Mail wird ggf. ungeprüft übernommen (analog Archive-Finding) | Mittel | `ENG-06` |
| 5 | Elevation of Privilege | RTE-05 KI-Vorschlag im Routing | Bereits abgedeckt — KI liefert nur Vorschlag, regelbasierte Engine bleibt maßgebliche Entscheidungsinstanz | Niedrig | abgedeckt |
### Bereits abgedeckt
- **Vier-Augen-Prinzip für Freigaben** (`APR-02`) generisch und modulunabhängig konfigurierbar.
- **KI bleibt Vorschlag, nie Entscheidung** (`RTE-05`) — verhindert stille Rechte-/Entscheidungsverlagerung an ein optionales KI-Modul.
- **Vermerke unveränderlich nach Speichern** (`VOR-02`, Ergänzung statt Bearbeitung) — passendes Muster für Nachvollziehbarkeit, unabhängig vom technischen Append-only-Finding zu `ENG-04`.
## 18. Threat Model: NEXARCH AI (STRIDE)
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: AI-Kanban (KI-Engine & Modell-Anbindung, Klassifizierung, Extraktion, Zusammenfassung, semantische Suche & Chat, Workflow-Vorschläge, Datenschutz & Governance) — alleinige Quelle für KI-Funktionen für DMS und Mail, optional aktivierbar über Core `LIC-02`. Identity/Auth/RBAC/Tenancy nach Core ausgelagert (Abschnitt 13). Alle Findings sind als Tickets in Welle 10 des AI-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea).
| 5 | Elevation of Privilege | WFS-02/03 Workflow-Vorschläge | Bereits abgedeckt — KI liefert nur Vorschlag, regelbasierte Workflow-Engine bleibt maßgebliche Entscheidungsinstanz (siehe Abschnitt 17) | Niedrig | abgedeckt |
| 6 | Information Disclosure | ENG-03 externer Cloud-Adapter | Optionaler Datenabfluss an externen Anbieter — durch `GOV-03` (Steuerung lokal/extern je Tenant, Trainingsausschluss) technisch adressiert; DSGVO-Auftragsverarbeitungsvertrag mit externem Anbieter ist Vertragsthema, siehe `SAAS-BETRIEBSMODELL.md` Abschnitt 5a | Niedrig | organisatorisch geklärt (2026-09-14), kein Ticket |
### Bereits abgedeckt
- **Prompt-Injection-Härtung** (`GOV-06`) — ungewöhnlich vollständig für ein Planungsdokument: Trennung Systemanweisung/Inhalt, Schema-Validierung, Muster-Erkennung.
- **KI-Entscheidungen vollständig auditierbar** (`GOV-05`) über die zentrale Core-Audit-Schnittstelle statt eigenem Log.
- **Feingranulare Tenant-Steuerung** (`GOV-02`/`GOV-03`) für einzelne KI-Funktionen und lokal-vs-extern, mit explizitem Trainingsausschluss auf Kundendaten.
- **Verbrauchsobergrenzen je Tenant** (`GOV-04`) gekoppelt an Core-Lizenzsteuerung — verhindert bereits einen Teil der Ressourcenerschöpfungs-Angriffsfläche auf Tenant-Ebene (ergänzt durch `ENG-07` auf Einzelanfrage-Ebene).
- **Grounding mit Belegpflicht** (`SEM-04`/`SEM-05`) — keine Chat-Antwort ohne nachvollziehbare Quellenangabe.
## 19. Threat Model: NEXARCH Connect (STRIDE)
Erstellt mit dem `security-threat-model`-Skill am 2026-09-12. Scope: Connect-Kanban (Connector-Engine & Grundgerüst, ERP/CRM-, DATEV-, Microsoft-365-Anbindung, Andere-DMS-Interoperabilität) — baut vollständig auf Core auf, keine eigene API-Infrastruktur. Identity/Auth/RBAC/Tenancy nach Core ausgelagert (Abschnitt 13). Alle Findings sind als Tickets in Welle 7 des Connect-Boards umgesetzt (Board inzwischen aus dem Repo entfernt, Historie in Gitea). Letztes der sieben Boards, das mit diesem Skill durchlaufen wurde — Abschnitte 13–19 decken jetzt alle NEXARCH-Boards ab.
| 1 | Information Disclosure | ENG-02 Zugangsdaten-Verwaltung | Verschlüsselung ohne festgelegte Anbindung an die zentrale Tenant-KEK-Hierarchie — Risiko eines zweiten, separat zu härtenden Verschlüsselungspfads | Mittel | `ENG-07` |
| 2 | Denial of Service | ENG-03/ENG-05 Sync-Jobs | Keine Obergrenzen für Laufzeit/Datenmenge je Sync-Lauf, Retry-Abbruchgrenze nicht technisch erzwungen | Mittel | `ENG-08` |
| 3 | Repudiation | ENG-06 Audit-Kopplung | Bereits abgedeckt — alle Connector-Aktionen an Core-Audit-Log gekoppelt | Niedrig | abgedeckt |
- **Alle Connector-Aktionen im Core-Audit-Log** (`ENG-06`) — Verbindung angelegt, Sync ausgeführt, Zugangsdaten geändert.
- **Verschlüsselter Export zu externen Backup-Zielen** (`DMS2-03`).
- **Keine eigene API-/Rate-Limiting-/Webhook-Infrastruktur** — vollständig auf Core aufgebaut, reduziert die Angriffsfläche gegenüber einer zweiten, parallelen Implementierung.
- **LDAP/SSO bleibt Core-Sache** — Connect dupliziert keine Identitäts-Infrastruktur.
Stand: 2026-08-26. Grundlage: `nexarch-state.json` (Techstack/Tenancy-Entscheidungen), `core-kanban/`, `archive-kanban/`, `dms-kanban/`, `mail-kanban/` (Boards inzwischen aus dem Repo entfernt, Ticket-Historie liegt in der Gitea-PR-Historie).
## 1. Was schon skalierungsfähig entschieden ist
| Baustein | Entscheidung | Warum das skaliert |
|---|---|---|
| Backend-Sprache | Go | Goroutinen billig, geringer RAM/Instanz, mehrere Prozesse pro Server möglich |
| Auth | JWT, zustandslos | Beliebig viele Backend-Instanzen ohne Sticky-Sessions möglich |
| Job-Queue | Postgres, `FOR UPDATE SKIP LOCKED` (FDN-04) | Bewusst für mehrere Worker-Prozesse ausgelegt, nicht nur In-Prozess |
| Rate-Limiting | Shared State statt in-process (Core API-03) | Korrigiert einen bei archivdms gefundenen Mehrinstanz-Bug (known-issues #9) |
Das deckt: **mehr Backend-Instanzen hinter einem Load Balancer** und **mehr Worker-Prozesse**. Es deckt NICHT: Datenbank-Skalierung, Caching, Kapazitätsgrenzen, Suchindex-Skalierung. Diese vier Lücken sind Gegenstand dieses Dokuments.
## 2. Skalierungsachsen
NEXARCH muss in vier unabhängigen Dimensionen wachsen können, die sich nicht gleich verhalten:
1.**Anzahl Tenants** (Mandanten) — wächst mit Kundenzahl, meist viele kleine.
2.**Datenvolumen pro Tenant** (Dokumente, E-Mails, Anhänge) — wächst mit Nutzungsdauer, stark ungleich verteilt (ein Großkunde kann mehr Daten haben als 100 kleine zusammen).
├── NEXARCH-Prozesse (Core/DMS/Mail/Archive/Workflow als ein oder mehrere Go-Binaries)
├── Worker (In-Prozess-Goroutinen)
├── PostgreSQL (lokal, eine Datenbank je Mandant plus schlanke Registry-DB — Modell C, siehe Abschnitt 4)
├── Manticore (lokal)
└── Objektspeicher (lokales Dateisystem oder ZFS)
```
Richtwert (grobe Schätzung, kein Lasttest): bis niedriger dreistelliger Tenant-Zahl bei üblicher KMU-Nutzung, solange kein Einzeltenant die Instanz dominiert. Grenze ist praktisch **immer die Postgres-Instanz und die OCR/Indexierungslast**, nicht der Go-Prozess selbst.
└── Objektspeicher (S3/MinIO/Ceph, nicht mehr lokales Dateisystem)
```
Übergang Stufe 1 → 2 ist ein **reiner Infrastruktur-Schritt**, kein Code-Umbau — das ist der Grund, warum JWT-Auth, Postgres-Jobqueue und S3-Abstraktion von Anfang an so gewählt wurden (Abschnitt 1).
**Betriebs-Checkliste für diesen Wechsel:**`code/docs/BETRIEBS-CHECKLISTE-STUFENWECHSEL.md` (Core `OPS-07`). Enthält u. a. die mTLS-Aktivierung zwischen Modul-Instanzen (Core `API-09`) als Pflichtschritt — für Stufe 1 ist mTLS bewusst optional, beim Wechsel auf verteilten Betrieb aber zwingend, sonst laufen Modul-zu-Modul-Aufrufe ohne Transport-Authentifizierung. Ein Readiness-Gate (`internal/mtls.GateCheck` + `internal/health` Warnungen) macht ein vergessenes mTLS bei mehr als einer Instanz/mehr als einem Host als Warnung im Monitoring sichtbar, statt es unbemerkt zu lassen.
### Stufe 3 – Dedizierte Instanz pro Großkunde (Private Cloud, Betriebsmodell aus Master-Prompt)
Ein Tenant mit außergewöhnlichem Datenvolumen oder Compliance-Anforderung bekommt eine eigene Stufe-1- oder Stufe-2-Installation statt eines Platzes auf der gemeinsamen Infrastruktur. Das ist die Antwort auf "was, wenn ein Mandant zu groß für die gemeinsame Instanz wird" (im Modell C liegt er bereits in einer eigenen Datenbank, hier zusätzlich auf eigener Hardware) — ein Ausweich-Betriebsmodell, das ohnehin schon geplant ist.
## 4. Datenbank-Skalierung — jetzt strukturell durch Modell C gelöst
**Geändert (siehe `nexarch-state.json` Entscheidungsverlauf):** Multi-Tenancy ist nicht mehr Modell A (eine Postgres-Instanz, `tenant_id`-Spalte), sondern **Modell C — eine Datenbank pro Mandant** (Core `TEN-01` Tenant-Registry & Datenbank-Provisioning). Grund: applikationsseitige Filterung verlässt sich darauf, dass jede Query korrekt gefiltert ist — ein einziger vergessener Filter (wie known-issues-archivmail.md #2/#7 real belegt) führt zum Cross-Tenant-Leck. Physische Trennung macht diese Fehlerklasse strukturell unmöglich, nicht nur unwahrscheinlicher. Nebeneffekt: eine Tenant-Datenbank lässt sich als Ganzes zwischen On-Premise und SaaS verschieben (Dump/Restore auf andere Infrastruktur), ohne Umbau an der Anwendung.
Das ändert die Skalierungsfrage von "eine Instanz größer machen" zu "viele kleine Instanzen gut verwalten":
**Verbindungs-Pooling ist ab Tag 1 Pflicht, nicht erst ab Stufe 2.**
Mit vielen Tenant-Datenbanken stößt jede Anwendungsinstanz schnell an die maximale Postgres-Verbindungszahl, unabhängig von der eigentlichen Last. Core `TEN-06` (Connection-Routing & Pooling pro Mandant) übernimmt das — PgBouncer-artiges Pooling oder ein LRU-verwalteter Verbindungspool, der Tenant-Verbindungen wiederverwendet statt dauerhaft offen zu halten.
**Migrationen laufen gegen N Datenbanken, nicht gegen eine.**
Core `TEN-07` (Migrations-Orchestrierung über alle Tenant-Datenbanken) rollt jede Schemaänderung automatisiert über alle Tenant-Datenbanken aus, mit Fehlerisolation je Mandant — ein inkompatibler Datenbestand bei einem Kunden darf das Ausrollen für alle anderen nicht blockieren.
**Vertikale Skalierung bleibt der Standardfall pro Postgres-Server**, der mehrere Tenant-Datenbanken hostet (Stufe 1: alle Tenant-DBs auf einer Instanz; Stufe 2/3: Tenant-Datenbanken auf mehrere Postgres-Server verteilt, siehe Abschnitt 3) — das ist kein Sharding-Zusatzaufwand mehr, sondern die natürliche Konsequenz von "jeder Mandant ist ohnehin schon eine eigene Datenbank".
**Was dadurch entfällt:** Die früher diskutierte Frage "Lesereplikate vs. RLS vs. Schema-pro-Tenant" stellt sich in dieser Form nicht mehr — RLS wäre bei physischer Trennung redundant (siehe Core `TEN-01`), und "Schema-pro-Tenant" (Modell B) ist mit Modell C bereits übertroffen (noch stärkere Isolation als ein gemeinsames Schema). Lesereplikate bleiben als Option pro einzelner Tenant-Datenbank sinnvoll, falls ein einzelner Großkunde das braucht — dann als individuelle Betriebsentscheidung für diese eine Datenbank, nicht als globale Architekturmaßnahme.
## 5. Suchindex-Skalierung
Manticore läuft in Stufe 1 lokal neben der Anwendung. In Stufe 2:
- Manticore auf eigene Instanz(en) ziehen, mehrere NEXARCH-Instanzen sprechen dasselbe Manticore an.
- Bei sehr großem Volumen: Manticore unterstützt Cluster-/Replikations-Modus — als Option dokumentieren, nicht in v1 vorwegnehmen (YAGNI, bis ein Tenant das tatsächlich braucht).
- Reindexierung (siehe MIGRATIONSKONZEPT.md Abschnitt 4) ist selbst ein Lastfall — sollte außerhalb von Nutzungsspitzen laufen, Job-Priorität in der Postgres-Jobqueue berücksichtigen.
## 6. Caching
Aktuell **kein Caching-Layer vorgesehen** — bewusste Lücke, nicht übersehen: Redis wurde für Queue/Rate-Limit explizit ausgeschlossen (Einfachheit, ein Betriebsteil weniger). Für Caching gilt dieselbe Grundhaltung, aber nicht dieselbe Antwort:
- **Kein Cache für v1.** Postgres mit vernünftigen Indizes trägt die erwartete Last in Stufe 1/2.
- **Wenn nötig:** In-Prozess-Cache (z.B. für Feature-Flags aus Core LIC-02, die sich selten ändern) statt eines externen Cache-Dienstes — passt zur "so wenig bewegliche Teile wie möglich"-Linie des Projekts.
- **Erst bei nachgewiesenem Bedarf** einen externen Cache (Redis oder Postgres-UNLOGGED-Tabelle als Kompromiss) einführen, nicht vorab.
Das ist die Last, die am wenigsten mit Nutzeraktivität korreliert und am leichtesten "einen Tenant die anderen ausbremsen lässt". Bereits im Datenmodell bedacht: die Postgres-Jobqueue aus archivdms (Vorbild für FDN-04) hat **Round-Robin-Fairness pro Mandant** eingebaut, genau um das zu verhindern — das ist eine reale Lehre aus dem Altsystem und sollte 1:1 in FDN-04 übernommen werden (ist es laut Ticket-Text bereits).
Für Stufe 2/3 zusätzlich:
- Worker-Anzahl je Job-Typ getrennt konfigurierbar (OCR-Worker ≠ Indexierungs-Worker ≠ Mail-Abruf-Worker), damit ein Engpass in einem Typ nicht die anderen blockiert.
- Quotas (Core LIC-03 Nutzungszähler) auch als Drosselungsmechanismus nutzen: ein Tenant mit ungewöhnlich hohem Job-Aufkommen bekommt eine Obergrenze, keinen unbegrenzten Vorrang.
## 8. Kapazitätsgrenzen — was bisher fehlt
Es gibt aktuell **keine dokumentierten Richtwerte**, ab wann Stufe 1 verlassen werden muss. Vorschlag, sobald erste Testinstallation läuft, folgende Kennzahlen zu erheben und hier nachzutragen:
- Dokumente/E-Mails gesamt, ab denen Postgres-Query-Latenz spürbar steigt (typischer Auslöser: fehlende/veraltete Indizes eher als reines Volumen).
- Gleichzeitige aktive Tenants, ab denen die In-Prozess-Job-Fairness (Abschnitt 7) nicht mehr ausreicht.
- OCR-Durchsatz (Seiten/Minute) je Worker, um Personal-/Kapazitätsplanung für Kunden mit hohem Scan-Aufkommen zu ermöglichen.
Diese Zahlen sind aus der Architektur allein nicht seriös vorhersagbar — sie brauchen einen Lasttest (siehe DMS-Board `QA-10` Last- & Leistungstest, Mail-Board `QA-07`) und sollten aus dessen Ergebnis hierher zurückgeschrieben werden.
## 9. Monitoring-Kopplung
Skalierungsentscheidungen brauchen Messwerte, keine Vermutungen. Vorhandene Anknüpfungspunkte:
- Core `OPS-01` Health-Checks je Modul, `OPS-03` Metrics-Aggregation über Module hinweg — sollte Postgres-Connection-Auslastung, Job-Queue-Länge je Tenant und Manticore-Latenz mit aufnehmen, nicht nur Modul-Health.
-`grafana-expert`/`prometheus-expert`-Subagenten (siehe `.claude/agents/`) stehen für die konkrete Umsetzung bereit.
## 10. Core selbst hochverfügbar machen (Primary/Replica-Failover)
Abschnitt 11 beschreibt, wie die ANDEREN Module einen Core-Ausfall überstehen. Dieser Abschnitt beschreibt, wie Core selbst seltener ausfällt.
Core-Anwendungsprozesse sind bereits zustandslos (Abschnitt 1) — das löst die Anwendungsschicht: mehrere Core-Instanzen hinter einem Load Balancer, jede darf jederzeit neu starten oder wegfallen, ohne dass Sessions verloren gehen. **Der eigentliche Single Point of Failure ist die Postgres-Instanz**, auf der Core (wie alle Module) aufsetzt.
### Stufe 1 – Einzel-LXC: kein automatisches Failover
Für kleine Installationen ist automatisches DB-Failover unverhältnismäßiger Aufwand. Antwort auf Ausfall ist Restore aus Backup (Archive-Board `BAK-03` Restore-Verfahren), keine Live-Umschaltung. Akzeptierter Trade-off, konsistent mit der "keine unnötige Komplexität"-Linie des Projekts.
### Stufe 2/3 – Primary/Replica mit automatischem Failover
Sobald Verfügbarkeit über reinem Backup/Restore hinaus gefordert ist:
1.**Postgres-Streaming-Replikation**: ein Primary (Schreibziel), ein oder mehrere Replicas (physische Streaming-Replikation, nicht nur die logischen Read-Replicas aus Abschnitt 4 — hier geht es um Ausfallsicherheit, dort um Leselast-Verteilung; dieselbe Replica-Infrastruktur kann beides gleichzeitig bedienen).
2.**Automatisches Failover braucht einen Konsens-Mechanismus**, nicht nur Replikation allein — sonst entscheidet im Zweifel niemand zuverlässig, wer nach einem Ausfall der neue Primary ist (Split-Brain-Risiko: zwei Knoten halten sich beide für Primary). Etablierter Ansatz: **Patroni** (nutzt etcd/Consul als verteilten Konsens-Store) oder das leichtgewichtigere **pg_auto_failover** (kommt ohne separaten Konsens-Store aus, dafür geringere Ausfalltoleranz bei komplexeren Szenarien). Für den Proxmox-LXC-Kontext des Projekts ist pg_auto_failover der pragmatischere Einstieg — Patroni erst, wenn Stufe 3 mit mehreren Standorten ansteht.
3.**Verbindungsindirektion**: Core-Anwendungsinstanzen dürfen nicht die IP des Primary fest codieren — sonst bringt Failover nichts, weil alle Clients weiter den toten Knoten ansprechen. Lösung: PgBouncer/HAProxy mit Healthcheck auf den aktuellen Primary, oder eine virtuelle IP, die dem Failover-Mechanismus folgt. Das ist derselbe PgBouncer-Baustein wie in Abschnitt 4 Stufe C (Connection-Pooling) — ein Werkzeug, zwei Zwecke.
4.**Fencing**: der alte Primary muss nach einem Failover zuverlässig daran gehindert werden, weiter Schreibzugriffe anzunehmen (sonst Split-Brain trotz Konsens-Mechanismus, z.B. bei Netzwerkpartitionierung). Patroni/pg_auto_failover übernehmen das, ist aber ein Punkt, der beim Aufsetzen bewusst getestet werden muss (simulierte Netzwerktrennung im Rahmen von DMS `QA-10`/Mail `QA-07`-artigen Lasttests), nicht nur beim Konfigurieren angenommen werden darf.
### Was NICHT dupliziert werden muss
Der JWKS-Mechanismus aus Abschnitt 11/`API-05` (öffentlicher Schlüssel wird von anderen Modulen gecacht) bleibt unabhängig von einem laufenden DB-Failover funktionsfähig — Signaturprüfung anderer Module braucht während eines kurzen Failover-Fensters keine Core-DB-Verbindung. Das war kein bewusst für HA gedachtes Feature, ergibt sich aber als Nebeneffekt der API-05-Entscheidung: **Core-DB-Failover und Core-Anwendungs-Resilienz für andere Module sind zwei getrennte Probleme, die sich gegenseitig entlasten**, statt sich zu addieren.
### Storage- und Manticore-Redundanz
Objektspeicher (S3-kompatibel) und Manticore-Suchindex haben ihre eigenen, produktüblichen Redundanzmechanismen (S3: providerseitig oder MinIO-Distributed-Mode; Manticore: Replikations-/Cluster-Modus, siehe Abschnitt 5) — kein Bezug zur Postgres-Failover-Frage, aber Teil desselben Gesamtbilds "was darf nicht Single Point of Failure sein", falls eine vollständige HA-Betrachtung für Stufe 3 gemacht wird.
## 11. Resilienz bei Core-Ausfall
Core ist Fundament (Auth, Rechte, Feature-Flags, Audit, Benachrichtigungen) — das darf nicht bedeuten, dass ein Core-Ausfall die ganze Plattform lahmlegt. Entscheidung: **lokale JWT-Verifikation + kurzlebiger Rechte-/Feature-Flag-Cache** statt synchroner Rückfrage bei Core pro Request (Core-Board-Ticket `API-05`).
- **DMS/Mail/Archive/Workflow prüfen JWT-Signatur lokal** (Core-Public-Key verteilt über JWKS-ähnlichen Mechanismus), kein Request an Core pro Aufruf.
- **Rechte/Feature-Flags werden mit kurzer TTL gecacht.** Fällt Core aus: laufender Betrieb funktioniert mit letztbekanntem Stand weiter, nur neue Logins und frische Rechteänderungen sind betroffen — kein Komplettausfall der anderen Module.
- **Fail-open vs. fail-closed muss pro Aktionsklasse entschieden werden** (Teil von `API-05`): Lesevorgänge mit abgelaufenem Cache eher fail-open (letzter bekannter Stand gilt weiter), sicherheitskritische Aktionen (Rechteänderung, Löschung, Freigabe) eher fail-closed (lieber verweigern als mit veralteten Rechten ausführen).
- Schlüsselrotation für den Core-Public-Key muss ohne Ausfallzeit der anderen Module möglich sein — sonst wird die Resilienzmaßnahme selbst zum Wartungsrisiko.
Diese Entscheidung ist eine direkte Folge der bereits getroffenen JWT-Wahl (Abschnitt 1) — zustandslose Tokens waren dafür gedacht, wurden aber bis zu diesem Punkt nicht konsequent zu Ende gedacht (jedes Modul hätte genauso gut synchron gegen Core validieren können, was den Vorteil zunichtegemacht hätte).
## 12. Zusammenfassung: was zu tun bleibt
| Lücke | Vorschlag | Wo einzuordnen |
|---|---|---|
| Read-Replica-Trennung im Code | Query-Routing lesend/schreibend als eigenes Ticket | Core `API`- oder `OPS`-Epic (neu) |
| Kapazitätsrichtwerte | Aus Lasttest ableiten, hier nachtragen | nach DMS `QA-10` / Mail `QA-07` |
| Manticore-Cluster-Modus | Nur bei nachgewiesenem Bedarf, nicht v1 | zukünftiges Ticket, kein Blocker |
| Cache-Layer | Nur bei nachgewiesenem Bedarf, In-Prozess vor extern | zukünftiges Ticket, kein Blocker |
Kein neuer Techstack-Entscheid nötig — das Konzept baut ausschließlich auf bereits getroffenen Entscheidungen (Go, Postgres, S3, Manticore, Modell C) auf und beschreibt deren Wachstumspfad, statt sie zu ändern.
Stand: 2026-08-26, Abschnitt 9 aktualisiert + Abschnitt 10 neu (Arbeitsverzeichnisse) 2026-08-28. Grundlage: `dms-kanban/tickets/FDN-03.md`, `mail-kanban/tickets/ARC-01.md`, `archive-kanban/tickets/RET-*.md`, `archive-kanban/tickets/BAK-*.md`, `SKALIERUNGSKONZEPT.md` (Kanban-Boards inzwischen aus dem Repo entfernt, Ticket-Historie liegt in der Gitea-PR-Historie).
## 1. Grundprinzip
Strikte Trennung, bereits in FDN-03 (DMS) und ARC-01 (Mail) unabhängig, aber konsistent so entschieden:
Kein Binärinhalt landet je in einer Datenbankspalte (kein `bytea` für Dateien) — das ist Bedingung für alles Weitere in diesem Dokument (Skalierung, Backup-Strategie, S3-Austauschbarkeit).
## 2. Backend-Optionen je Betriebsstufe
Deckt sich mit `SKALIERUNGSKONZEPT.md` Abschnitt 3:
| Betriebsstufe | Backend | Treiber |
|---|---|---|
| Stufe 1 (Einzel-LXC) | lokales Dateisystem oder ZFS | lokaler Treiber (FDN-03/ARC-01 "ein Interface, zwei austauschbare Treiber") |
| Stufe 2 (verteilt) | S3-kompatibel: MinIO (selbst betrieben) oder Ceph (RADOS Gateway) | S3-Treiber, identische API wie Stufe 3 |
| Stufe 3 (Private Cloud / SaaS) | S3-kompatibel: MinIO/Ceph selbst betrieben oder Managed-Object-Storage beim Provider | S3-Treiber, kein Unterschied im Code |
Der Übergang Stufe 1 → 2/3 ist ein reiner Infrastruktur-Schritt (Treiber-Konfiguration ändern, nicht Code), das war der explizite Zweck der Abstraktion. Datenübernahme beim Wechsel: einmaliger Kopiervorgang (z.B. `mc mirror` bei MinIO) mit anschließendem Prüfsummen-Abgleich (Abschnitt 6) — kein Big-Bang-Risiko wie bei der Systemmigration aus `MIGRATIONSKONZEPT.md`, da es dieselbe NEXARCH-Instanz ist, nur das Backend wechselt.
**Bewusst nicht vorgesehen:** NFS als produktives Ziel für Stufe 2/3 — das S3-Protokoll ist die gemeinsame Klammer, NFS wäre ein dritter, inkompatibler Zugriffspfad. NFS bleibt als Option für den lokalen Stufe-1-Treiber denkbar (Mount-Punkt statt echtes lokales Dateisystem), ändert aber nichts am Treiber-Interface.
FDN-03 und ARC-01 legen je ihre eigene Pfadkonvention fest (Akzeptanzkriterium in beiden Tickets), bisher ohne geteilten Rahmen. Damit Backup (Archive `BAK-02`), Export (Archive `DMS2-01` im Connect-Board, DMS `DOC-08`) und Betrieb nicht drei verschiedene Konventionen lernen müssen, gilt projektweit ein gemeinsames Schema — und, analog zur Datenbank (Modell C, eigene Datenbank pro Mandant), jetzt auch eine physische Trennung im Objekt-Storage: **jeder Mandant erhält einen eigenen Bucket bzw. Pfad-Root**, statt eines gemeinsamen Buckets mit `tenant_id`-Pfadpräfix.
Begründung: physische Trennung schließt bei Objekt-Storage dieselbe Fehlerklasse aus wie bei der Datenbank — ein Codefehler ohne Tenant-Kontext kann kein fremdes Bucket erreichen, nicht nur einen falschen Pfad innerhalb eines gemeinsamen Buckets. Ein vergessener Pfadfilter/Präfix-Check in einem gemeinsamen Bucket führt zum Cross-Tenant-Leck; ein fehlendes Tenant-Bucket-Mapping führt dagegen nur dazu, dass gar kein Bucket referenzierbar ist.
Die Provisionierung des Tenant-Buckets ist Teil desselben Provisioning-Schritts wie die Anlage der Tenant-Datenbank in Core `TEN-01` — ein neuer Mandant erhält Datenbank und Bucket in einem automatisierten Schritt.
Innerhalb des Tenant-Buckets entfällt das führende `tenant_id`-Segment, das Schema wird zu:
Beispiele (innerhalb des Buckets von Mandant `t-4471`):
-`dms/documents/doc-9021/rev-3.bin`
-`mail/messages/msg-88213/part-2-attachment.bin`
-`archive/backups/2026-08-26/snapshot.tar.zst`
Innerhalb des Pfads bleibt jedes Modul für sein `{objekttyp}`-Vokabular selbst zuständig (DMS: `documents`, `thumbnails`; Mail: `messages`, `attachments`) — nur die äußeren zwei Ebenen (`modul`/Rest) sind projektweit fix, damit ein Werkzeug (Backup, Export, Kapazitätsauswertung) modulübergreifend funktioniert, ohne pro Modul eine Sonderbehandlung zu brauchen. Die Mandantentrennung selbst (Bucket-Auswahl) ist damit kein Bestandteil des Pfadschemas mehr, sondern eine reine Provisioning-/Zugriffsschicht-Frage (siehe Ticket ARC-06, Mandantentrennung im Objekt-Storage; Kanban-Board inzwischen aus dem Repo entfernt, Historie in Gitea).
## 4. Verarbeitungsreihenfolge (Lebenszyklus einer Datei)
Zusammengeführt aus den bereits getroffenen Einzelentscheidungen (FDN-01/DOC-01/DOC-02/FDN-09 im DMS-Board, ARC-01/ARC-02/ARC-03 im Mail-Board):
```
1. Empfang (Klartext, im Speicher/temporär)
2. SHA-256-Hash auf Klartext
3. Duplikatsprüfung (DOC-02 / ARC-03) — Gate: nur neue oder bewusste Duplikate gehen weiter
4. Verschlüsselung at rest (FDN-09 / ARC-02, AES-256-GCM, Envelope Encryption)
5. Ablage im Objekt-Storage nach Pfadkonvention (Abschnitt 3)
6. Prüfsummenvergleich nach dem Schreiben (Akzeptanzkriterium in FDN-03/ARC-01)
7. Registrierung bei Archive-Retention-Engine (RET-05 Modul-Adapter) — Aufbewahrungsfrist beginnt zu laufen
8. OCR (SRC-05 / SRC-07) und Indexierung (SRC-02 / SRC-01+SRC-02) — läuft NACH Schritt 3, nicht parallel dazu
```
Löschung läuft ausschließlich über die Archive-Engine (Retention/Legal-Hold-geprüft), niemals direkt durch DMS oder Mail — das ist bereits Konsequenz der Archive-Konsolidierung, hier nur noch einmal aus Storage-Sicht festgehalten: kein Modul löscht Objekte eigenständig aus dem Storage.
## 5. Zugriffsmodell
- **Kein direkter Client-Zugriff auf den Objekt-Storage.** Up-/Downloads laufen über zeitlich begrenzte signierte URLs (FDN-03 Akzeptanzkriterium 2, Standard-Pattern für S3-kompatible Backends), die das Backend ausstellt — Storage-Zugangsdaten selbst erreichen nie den Browser.
- Presigned-URL-Gültigkeit kurz und konfigurierbar halten (Minuten, nicht Stunden) — lange gültige URLs sind ein unnötig großes Zeitfenster für Linkweitergabe.
- Storage-Zugangsdaten (S3-Credentials) sind Secrets wie jedes andere — ausschließlich über Umgebungsvariablen, nie im Code (bereits projektweite Regel aus jedem Ticket-Abschluss-Abschnitt).
## 6. Konsistenzprüfung
Jeder Schreibvorgang wird durch Prüfsummenvergleich verifiziert (bestehende Akzeptanzkriterien in FDN-03 und ARC-01). Für den Bestand insgesamt zusätzlich:
- **Reconciliation** (Archive `BAK-05`): periodischer Abgleich zwischen den in Postgres referenzierten Objekten und dem tatsächlichen Storage-Inhalt — erkennt verwaiste Metadaten-Einträge (Objekt fehlt im Storage) und verwaiste Storage-Objekte (kein Metadaten-Eintrag mehr, z.B. nach einem Fehler bei Schritt 5/6 aus Abschnitt 4).
- Reale Lehre aus archivmail: Reconciliation-Reports existierten dort bereits als Soll/Ist-Abgleich (siehe `datenmodell-altsysteme.md`) — Muster wird über Archive `BAK-05` fortgeführt, nicht neu erfunden.
- **Integritätsprüfung per Hash** (Archive `BAK-08`): periodischer Scrub-Job, der Stichproben liest, SHA-256 neu berechnet und gegen `file_revisions.checksum_sha256` vergleicht — erkennt stille Inhaltsveränderung/Bit-Rot bei Objekten, die noch existieren.
### 6.1 Grenzen der Prüfung bei extern eingebundenem Kunden-Storage
Bindet ein Kunde einen eigenen S3-Bucket ein, überwachen wir weder dessen Hardware noch dessen Betreiber. Was NEXARCH technisch leisten kann, hängt davon ab, was wir dürfen:
- **Kein Lesezugriff, nur Schreiben:** keine Verifikation möglich. Wir können höchstens den Upload-Beleg und den Hash zum Upload-Zeitpunkt festhalten — das ist kein Monitoring, sondern ein Nachweis für später.
- **Lese-/API-Zugriff vorhanden:** Existenz- (`BAK-05`) und Inhaltsprüfung (`BAK-08`) sind möglich, aber periodisch/stichprobenhaft, nicht in Echtzeit, und nur für das, was wir selbst wieder lesen können.
Was diese beiden Prüfungen NICHT leisten:
- Sie erkennen nicht lückenlos, ob ein Objekt unbefugt gelöscht oder durch ein neues ersetzt wurde, wenn uns keine Inventar-/Versionsdaten vom Storage vorliegen (ETag/LastModified/VersionId — sofern verfügbar, nicht überall ein einfacher Hash, besonders bei Multipart-Uploads oder providerseitiger Verschlüsselung, daher nicht als alleinige Wahrheitsquelle nehmen).
- Sie ersetzen keine storage-seitige WORM-/Versionierungsstrategie und keine Zugriffs-/Audit-Logs des Storage-Providers — nur Object Lock, Versioning, Retention Policies und providerseitige Audit-Logs verhindern Veränderungen oder machen sie lückenlos nachweisbar, ein nachträglicher Scrub kann sie nur *bemerken*, nicht verhindern.
2.**Customer-managed, compliant Storage** — Kunde stellt den S3-Bucket, aber vertraglich/beim Onboarding verbindlich verlangt: Versioning aktiviert, Object Lock/Retention bei revisionssicherer Nutzung, Audit-Logs aktiviert, eingeschränkte Löschrechte, idealerweise eigener Backup-/Replikationspfad.
3.**Customer-managed, non-compliant Storage** — nur für nicht revisionssichere Nutzung zulässig; muss im System klar markiert sein ("keine belastbare Unveränderbarkeitsgarantie"), damit keine falsche GoBD-Erwartung entsteht.
## 7. Backup vs. Storage-Redundanz — zwei verschiedene Schichten
Häufige Verwechslung, deshalb hier explizit getrennt:
- **Storage-Redundanz** (diese Datei, Abschnitt 2): wie das Backend selbst Datenverlust durch Hardware-Ausfall verhindert (S3-Provider-Redundanz, MinIO-Distributed-Mode, Ceph-Replikation). Schützt vor Festplatten-/Knotenausfall, NICHT vor Anwendungsfehlern oder böswilligem/versehentlichem Löschen.
- **Backup** (Archive `BAK-01`/`BAK-02`/`BAK-03`): eigenständige, deduplizierende Sicherung nach restic/Borg-Konzept, zeitversetzt, wiederherstellbar auch wenn die Primärkopie durch einen Anwendungsfehler beschädigt wurde. Das ist die Antwort auf "ein Bug löscht fälschlich Dateien" — Storage-Redundanz allein hilft dabei nicht, weil sie den fehlerhaften Löschvorgang originalgetreu mit-repliziert.
Beide Schichten sind nötig, keine ersetzt die andere.
## 8. Export & Portabilität
Aus der DMS-Produkt-DNA fortgeführt (ecoDMS-Lehre: "was die Oberfläche kann, kann auch die offene Schnittstelle", keine Herstellerbindung):
- Vollständiger Export von Dateiinhalt + Metadaten ohne NEXARCH-eigenes Werkzeug muss jederzeit möglich sein (DMS `DOC-08` Massenexport, Mail `ARC-07` Export/Portabilität, Connect `DMS2-01` offenes Austauschformat für Fremd-DMS-Interoperabilität).
- Objekt-Storage-Ablage selbst nutzt offene Formate (Original-Dateiformat + PDF/A für OCR-Ergebnisse, kein proprietäres Container-Format) — keine Vendor-Lock-Kodierung, die einen Export nachträglich erschweren würde.
## 9. Speicherverbrauch-Messung für Quota/Lizenzierung
~~Offene Lücke~~ — **geklärt (2026-08-28)**: Core `LIC-05` "Speicherverbrauch-Metrik je Tenant" setzt genau den hier vorgeschlagenen Mechanismus um — Objekt-Storage-Treiber der Module (FDN-03/ARC-01) melden bei jedem Schreib-/Löschvorgang die Objektgröße, ein leichtgewichtiger Postgres-Zähler pro Tenant wird geführt, `LIC-03` fragt ihn zur Quota-Durchsetzung ab. `LIC-05` hängt im Core-Abschluss-Gate `QA-05`.
Ergänzung 2026-08-28, angestoßen durch den Vergleich mit paperless-ngx (`consume/`, `data/`, `media/`, `export/`, `trash/`) — mehrere Tickets definieren Funktionalität, die einen lokalen Arbeitsordner braucht, legen aber bisher nicht fest, WO dieser liegt. Papierkorb (DMS `DOC-11`/`DOC-12`, Mail `ARC-05`) braucht dagegen **keinen** eigenen Pfad — das ist ein reines DB-Status-Flag, das Objekt bleibt physisch am gewohnten Platz im Objekt-Storage, bis Archive es nach Fristablauf endgültig entfernt.
### 10.1 Consume-/Hot-Folder (DMS `INT-06`/`INT-08`, Mail `IMP-05`)
Anders als der reguläre Objekt-Storage-Zugriff (Abschnitt 2, ausschließlich über signierte URLs, Abschnitt 5) braucht ein Datei-Watch zwingend ein echtes lokales Dateisystemverzeichnis — S3-kompatible Backends unterstützen kein Datei-Watch. Konsequenz: Consume-Ordner sind **kein Teil des Tenant-Objekt-Storage-Buckets**, sondern ein separater, lokaler Zwischenspeicher je Tenant auf dem Modul-Host:
```
{consume-root}/{tenant_id}/dms/inbox/
{consume-root}/{tenant_id}/mail/inbox/
```
Nach erfolgreicher Verarbeitung (Hash, Duplikatsprüfung, Verschlüsselung, Ablage im Objekt-Storage — Abschnitt 4) wird die Datei aus dem Consume-Ordner entfernt, nicht dauerhaft dort belassen. Consume-Root ist damit strukturell näher an einem Cache als an Aufbewahrung und braucht keine Backup-Einbindung (Abschnitt 7) — im Fehlerfall wird die Quelle erneut abgelegt/gescannt.
Bei Virenscan-Fund wird die Anhang-Datei nicht gelöscht, sondern in einen isolierten Quarantäne-Bereich verschoben (Nachvollziehbarkeit, siehe Compliance-Anforderung) — ebenfalls lokal, ebenfalls je Tenant:
```
{consume-root}/{tenant_id}/mail/quarantine/
```
Quarantäne-Objekte werden nicht automatisch gelöscht; Aufbewahrungsdauer und Zugriff (nur Admin/Security-Rolle) sind Akzeptanzkriterium von `IMP-06`, nicht Gegenstand dieses Dokuments.
### 10.3 Export-Staging (DMS `DOC-08`, Mail `ARC-07`)
Massenexport (ZIP) erzeugt eine temporäre Datei zwischen "Export angestoßen" und "Download abgeschlossen". Diese Staging-Fläche ist rein ephemer (Lebensdauer im Bereich von Minuten bis wenigen Stunden), braucht keine Tenant-Bucket-Struktur und keine Backup-Einbindung:
Aufräumung nach Ablauf einer kurzen Frist (Vorschlag: 24 Stunden) oder nach bestätigtem Download ist Akzeptanzkriterium der jeweiligen Export-Kachel.
### 10.4 Ollama-Modell-Cache (AI `ENG-02`)
Bisher nicht adressierte Lücke: `ENG-02` (Ollama-Adapter) lädt und hält LLM-Modellgewichte lokal vor — je nach Modellklasse mehrere GB bis über 40 GB (siehe `PROXMOX-DEPLOYMENT.md` Abschnitt 6a, dort nur RAM-Sizing, keine Disk-Planung). Anders als Consume/Quarantäne/Export ist das **kein Zwischenspeicher, sondern persistenter, potenziell großer Datenbestand**, der eine eigene Kapazitätsplanung braucht, getrennt vom restlichen Objekt-Storage:
```
{ollama-root}/models/
```
Nicht tenant-partitioniert (Modelle werden über Tenants hinweg geteilt, sofern keine Tenant-spezifische Modellwahl vorliegt — siehe AI `ENG-04`). Bei Stufe-1-Einzel-LXC (siehe `PROXMOX-DEPLOYMENT.md` Abschnitt 2) sollte dieser Pfad auf ein eigenes Volume/Dataset gelegt werden, damit ein wachsender Modellbestand nicht den restlichen Systemspeicher verdrängt — gleiches Isolationsprinzip wie bei Tenant-Buckets (Abschnitt 3), nur nicht tenant-, sondern funktionsbezogen.
### 10.5 Warum eigene Datasets/Mountpoints sinnvoll sind
Alle vier Arbeitsverzeichnisse (10.1–10.4) profitieren aus demselben Grund von eigenen ZFS-Datasets/Mountpoints statt Unterordnern im Root-Dateisystem: unabhängige Quotas (ein vollaufender Consume-Ordner darf nicht das Systemvolume fluten), unabhängige Snapshots/Löschbarkeit (siehe Erfahrung mit `archivmail`-Alt-Deinstallation, `nexarch-state.json` Entscheidungsverlauf 2026-08-28), und die Möglichkeit, unterschiedliche Backup-/Retention-Policies zuzuweisen (Consume/Export brauchen keine, Modell-Cache profitiert von keiner inkrementellen Sicherung großer Binärdateien, sondern von einfacher Neubeschaffung bei Bedarf).
Stand: 2026-08-28 (Abschnitt 4/6 aktualisiert). Grundlage: Core `SHL-01` (UI-Shell & Design-System, zentral), `PRODUKTARCHITEKTUR.md`, alle Oberflächen-Tickets über sieben Boards.
## 1. Grundprinzip: eine Shell, sieben Module
`SHL-01` (Core) ist die alleinige Quelle für Layout-Shell, Basis-Komponenten (Tabelle, Dialog, Formularelemente, Toast) und Design-Tokens (Farbe, Abstand, Typografie). Jedes Fachmodul baut seine eigenen Oberflächen darauf auf, aber keines definiert eigene Tokens oder eigene Basis-Komponenten — das war bis zur Konsolidierung (siehe `nexarch-state.json` → `konsolidierungen_durchgefuehrt`) nicht sichergestellt, DMS hätte sonst die einzige Shell gehabt und alle anderen sechs Boards hätten sie kopiert oder wären visuell inkonsistent geblieben.
**Test für "gehört das in SHL-01 oder ins Modul":** Würde eine zweite, unabhängige Implementierung zum selben Ergebnis kommen müssen, damit NEXARCH wie ein Produkt aussieht (Button-Stil, Abstand, Farbpalette)? Dann Core. Ist es fachlich spezifisch für ein Modul (PDF-Viewer, Postfach-Ansicht, Genehmigungs-Ansicht)? Dann das jeweilige Board.
## 2. Navigationsmodell
Aus dem Master-Prompt-Ziel "modulare Oberfläche, Benutzer sehen nur Module für die sie Berechtigung haben" — technisch bereits durch Core `LIC-02` (Feature-Flags je Tenant) und `RBAC-01..04` (Rollen/Rechte) unterlegt:
- **Globale Navigation** (Teil von `SHL-01`): Modul-Umschalter, sichtbar nur für aktivierte + berechtigte Module. Kein Modul zeigt sich selbst an, wenn Core es für den aktuellen Tenant/Benutzer nicht freigibt — das ist dieselbe Backend-seitige Durchsetzungspflicht aus dem Master-Prompt ("Aktivierung darf nicht nur UI-seitig erfolgen"), hier auf die Navigationsebene angewendet: die UI blendet aus, was das Backend ohnehin verweigern würde, nicht umgekehrt als alleiniger Schutzmechanismus.
- **Modul-interne Navigation**: jedes Fachmodul definiert seine eigene zweite Ebene (DMS: Dokumente/Ordner/Papierkorb; Mail: Postfächer/Suche; Workflow: Aufgaben/Akten/Genehmigungen) — eigenständig, aber visuell aus denselben Shell-Komponenten.
- **Globale Suche**: im Master-Prompt gefordert, aktuell technisch NICHT als eigenes Ticket vorhanden — siehe Abschnitt 6 (offener Punkt). Die Suche existiert bisher nur je Modul (DMS `SRC-*`, Mail `SRC-*`, bewusst nicht zentralisiert, siehe `nexarch-state.json` → `bewusst_nicht_zentralisiert`), aber eine modulübergreifende Ergebnisliste in EINER Suchleiste ist etwas anderes als ein gemeinsamer Suchindex und wurde bisher nicht bedacht.
## 3. Dashboard & Benutzer-/Tenant-Kontext
- Tenant-Kontext global sichtbar (welcher Mandant ist aktiv, bei Mehrfachzugehörigkeit umschaltbar) — gehört in `SHL-01`, da jedes Modul denselben Tenant-Kontext braucht, nicht nur eines.
- Ein Landing-Dashboard nach Login existiert bisher nur in Einzelteilen: Core `OPS-02` (zentrale Statusseite, eher Admin-/Betriebssicht als Nutzer-Dashboard), Archive `ANA-02` (Auswertungs-Dashboard, Kennzahlen-fokussiert), Mail/DMS haben kein eigenes Startseiten-Ticket. Kein Ticket bündelt bisher "was sieht ein normaler Benutzer beim ersten Login" — siehe Abschnitt 6.
Update 2026-08-28: WCAG 2.1 AA, i18n-Rahmen (mind. Deutsch/Englisch) und Theming (Hell/Dunkel) sind jetzt verbindliche, in `SHL-01` verankerte Eigenschaften des Design-Systems (Akzeptanzkriterien 4-6, Prüfungen 5-6) — kein modulweiser Einzelfall mehr. Alle Frontend-Kacheln über alle 7 Boards (Core, DMS, Mail, Archive, Workflow, AI, Connect) hängen jetzt in `dependsOn` von `SHL-01` ab und erben diese Eigenschaften strukturell. Zusätzlich existiert je Board ein Barrierefreiheits-Audit-Gate im Abschluss-Gate: DMS `QA-11`, Mail `QA-08`, Core `QA-09`, Workflow `QA-08`, AI `QA-09`, Connect `QA-08` (Archive hat aktuell keine eigene Frontend-Kachel, daher kein Audit nötig).
- **Theming**: Design-Tokens aus `SHL-01` sind die einzige Quelle für Hell/Dunkel-Werte, jetzt mit explizitem Akzeptanzkriterium und Prüfung ("Umschaltung Hell/Dunkel an einer Beispielkomponente ohne Layoutbruch") statt implizit über Breakpoint-Tokens.
- **Mehrsprachigkeit**: DMS `UX-03`/`UX-04` bleiben das einzige Board mit eigenem, ausführlichem i18n-Ticket (Infrastruktur + tatsächliche DE/EN-Übersetzungen der Inhalte) — das war weiterhin richtig so, denn `SHL-01` liefert nur den *Rahmen* (Sprachumschaltung, Übersetzungsmechanismus für Basis-Komponenten), nicht die modul-eigenen Textbausteine. Jedes Modul bindet seine Übersetzungen über den SHL-01-Rahmen ein, statt einen eigenen Mechanismus zu bauen.
- **Barrierefreiheit**: WCAG 2.1 AA ist jetzt in jedem der 7 Boards durch ein Audit-Gate abgesichert, nicht nur DMS/Mail wie zuvor. `SHL-01`s Akzeptanzkriterien 1 und 4 ("Kontrastwerte erfüllen mindestens AA" / "Basis-Komponenten erfüllen WCAG 2.1 AA als Grundeigenschaft") sind die gemeinsame Grundlage, auf der alle sechs Audit-Gates aufbauen.
## 5. Prinzipien aus dem Master-Prompt, konkret zugeordnet
| Master-Prompt-Ziel | Wo umgesetzt |
|---|---|
| Übersichtlich, schnell | `SHL-01` Basis-Komponenten, keine schwere Anwendungsserver-Startzeit (Produkt-DNA-Linie aus allen Boards: "kein Alfresco-Overhead") |
| Responsive, Desktop und Tablet | DMS `UX-01`, Mail `UX-04`, Design-Tokens/Breakpoints in `SHL-01` |
| Moderne Navigation | Abschnitt 2 dieses Dokuments |
| Modulare Oberfläche | Sieben-Boards-Architektur selbst (`PRODUKTARCHITEKTUR.md` Abschnitt 2) |
- **Globale, modulübergreifende Suche**: keine Ausgangslage-Referenz in irgendeinem Board deutet auf eine gemeinsame Sucheingabe hin, die Ergebnisse aus DMS UND Mail UND Workflow-Akten gleichzeitig anzeigt. Die einzelnen Suchindizes bleiben bewusst getrennt (Entscheidung bereits getroffen) — aber eine gemeinsame Such-UI-Schicht, die mehrere modul-eigene Such-APIs parallel abfragt und zusammenführt, ist etwas anderes und wurde bisher nicht geplant.
- **Zentrales Nutzer-Dashboard nach Login**: existiert nur fragmentiert (Statusseite = Admin-Sicht, Auswertungs-Dashboard = Kennzahlen-Sicht). Kein Ticket für "was sieht eine normale Sachbearbeiterin direkt nach dem Einloggen" (offene Wiedervorlagen, neue Mails, zugewiesene Aufgaben aus mehreren Modulen gebündelt).
- **Kein UI-Prototyp/Mockup vorhanden**: dieses Dokument beschreibt Prinzipien und Zuständigkeiten, keine tatsächlichen Bildschirmentwürfe — die entstehen erst in der Umsetzung der einzelnen Oberflächen-Tickets.
Erledigt seit Stand 2026-08-26 (nicht mehr offen): Mehrsprachigkeits-Infrastruktur war unentschieden zwischen Zentralisierung und Pro-Modul-Wiederholung — inzwischen entschieden: `SHL-01` liefert den zentralen i18n-*Rahmen* (Umschaltmechanismus), DMS `UX-03`/`UX-04` bleibt bewusst das einzige Board mit vollständiger Übersetzungsarbeit, da Textbausteine modul-eigen sind (siehe Abschnitt 4). WCAG/Theming-Uneinheitlichkeit über die Boards ebenfalls behoben, siehe Abschnitt 4.
Rückfrage an dich: sollen globale Suche und zentrales Dashboard als neue Tickets ergänzt werden (vermutlich beide in Core `SHL`-Epic, da modulübergreifend), oder bleibt das für später zurückgestellt?
Basis: frischer Checkout von `main`, Branch `feature/qa-02-pruefgate-engine-modellanbindung`. Geprüft auf Entwicklungshost 204, Wegwerf-Datenbank `qa02_test` (eigener Rolle/DB, kein Zugriff auf `archivdms`/`archivmail`). Reines Prüfgate: geprüft werden die bereits gemergten Pakete ENG-02 (`provider/ollama`), ENG-03 (`provider/openai`), ENG-04 (`modelconfig`), ENG-05 (`queue`) und ENG-06 (`prompttemplate`); es wurde keiner dieser Pakete umgebaut.
## Vorgehen
1. Code der fünf betroffenen Pakete gelesen (`provider`, `provider/mock`, `provider/ollama`, `provider/openai`, `modelconfig`, `queue`, `prompttemplate`).
2. Bestehende automatisierte Tests (`go test ./...` im gesamten `ai`-Modul) als Regressions-Baseline ausgeführt — alle Pakete `ok`, keine Fehlschläge.
3. Für jedes Akzeptanzkriterium geprüft, ob ein automatisierter Test die Behauptung tatsächlich belegt (nicht nur Code-Inspektion). Eine Lücke gefunden und mit einem neuen Test geschlossen (siehe Prüfung 2).
## Prüfungen
| # | Prüfung | Zielwert | Messwert |
|---|---|---|---|
| 1 | Providerwechsel ohne Aufrufer-Änderung | Derselbe Aufrufer-Code (`call(p provider.Provider)`) liefert mit zwei verschiedenen `Provider`-Implementierungen (zwei `mock.Provider`-Instanzen mit unterschiedlicher Antwort, stellvertretend für den Wechsel zwischen `ollama`/`openai`/`mock`) korrekte, jeweils unterschiedliche Ergebnisse, ohne dass der Aufrufer-Code geändert wird | `ai/internal/provider/contract_test.go::TestInterchangeable_SameCallerCode` — PASS. Ergänzend: `TestContract_Mock` und `TestContract_Ollama` führen denselben Vertragstest (`runContract`) gegen zwei reale Implementierungen desselben Interfaces aus; `TestUniformErrors_AK3` belegt einheitliche Fehlerklassifikation (`*provider.Error`/`Code`) providerübergreifend. `modelconfig_test.go::TestSet_SwitchTakesEffectImmediately` belegt zusätzlich, dass ein Modell-/Providerwechsel je Tenant ohne Neustart sofort beim nächsten `Get` wirkt |
| 2 | Lastspitze ohne Verlust | Eine Lastspitze von 200 gleichzeitig eingereihten Jobs wird von 8 nebenläufigen Workern vollständig abgearbeitet: am Ende `Depth.Pending = 0`, `Depth.DeadLetter = 0`, jeder der 200 Jobs hat Status `succeeded`, der Provider wurde genau 200-mal aufgerufen (keine Doppelverarbeitung trotz `FOR UPDATE SKIP LOCKED`-Konkurrenz) | **Lücke identifiziert:** die bestehende `TestQueue_AsyncEnqueueUnderLoadSpike` belegte nur den Enqueue-Pfad (schnelle Rückkehr, `Pending ≥ 50`), nicht die tatsächliche verlustfreie Verarbeitung durch mehrere Worker. Neuer Test `ai/internal/queue/qa02_loadspike_test.go::TestQueue_MultiWorkerDrainsLoadSpikeWithoutLoss` ergänzt: 200 Jobs, 8 Worker — PASS, `Depth.Pending=0`, `Depth.DeadLetter=0`, alle 200 Jobs `succeeded`, Provider-Aufrufzähler exakt 200 |
| 3 | Prompt-Rollback nachweisbar | Eine frühere, bereits gespeicherte Prompt-Version lässt sich erneut aktivieren; `Active` liefert danach wieder exakt den Inhalt der früheren Version; die Historie bleibt dabei vollständig (kein Datenverlust durch den Rollback) | `ai/internal/prompttemplate/store_test.go::TestActivate_RollsBackToEarlierVersion` — PASS: nach `Save` (v1, v2) und `Activate(v1)` liefert `Active` wieder `v1.Body`; `History` enthält weiterhin beide Versionen (`len(hist)=2`). Ergänzend `TestActivate_UnknownVersionReturnsErrNotFound` (Rollback auf nicht existierende Version wird kontrolliert abgewiesen) |
## Regressionslauf
`TEST_TENANT_DSN=postgres://qa02_test:***@localhost:5432/qa02_test?sslmode=disable go test ./... -p 1` im gesamten `ai`-Modul: alle Pakete `ok`, keine Fehlschläge, keine Regression durch den neuen Test.
## Akzeptanzkriterien
1. Providerwechsel funktioniert ohne Aufrufer-Änderung — **erfüllt** (Prüfung 1).
2. Warteschlange verarbeitet Lastspitzen ohne Verlust — **erfüllt** (Prüfung 2, nach Schließen der Testlücke).
Keine. Die einzige festgestellte Lücke (fehlender End-to-End-Test für verlustfreie Lastspitzen-Verarbeitung über mehrere Worker) wurde durch einen neuen Test geschlossen, kein Produktionscode musste geändert werden.
**Ergebnis: BESTANDEN** — der künftige Vorschlag ändert sich nachweislich
durch das erfasste Korrektur-Feedback.
## Prüfung 3: Ausgabeschema in allen Testfällen eingehalten
In beiden Realinferenz-Tests (Prüfung 1) wird je Testfall zusätzlich
geprüft: die erkannte Kategorie liegt im konfigurierten Schema oder ist
explizit als `classify.UnknownCategory` gekennzeichnet, die Konfidenz liegt
in [0,1], und `extract.Result.Fields` enthält für jedes Schema-Feld genau
einen Eintrag. Alle 5 Dokumente × 2 Funktionen (Klassifizierung,
Extraktion) = 10 Testfälle ohne Schemaverletzung.
**Ergebnis: BESTANDEN**.
## Akzeptanzkriterien
| # | Kriterium | Ergebnis |
|---|---|---|
| 1 | Klassifizierungsgenauigkeit erreicht die Zielgenauigkeit | Erfüllt — 100% ≥ 80% (Prüfung 1) |
| 2 | Extraktionsgenauigkeit erreicht die Zielgenauigkeit | Erfüllt — Datum 100% ≥ 80%, Betrag-Vorhandensein 80% ≥ 80% (Prüfung 1) |
| 3 | Korrektur-Feedback wirkt nachweislich auf künftige Vorschläge | Erfüllt — Vorschlag verschiebt sich nach Korrekturen von kuendigung auf vertrag (Prüfung 2) |
## Build/Vet/Test — Gesamtmodul
```
go build ./... → OK
go vet ./... → OK
go test ./... -p 1 → alle Pakete ok, inkl. neuem internal/qa03gate
```
Keine Regression in den bestehenden Paketen.
## Offene Befunde
Keine. Alle Prüfungen mit echten, ausgeführten Nachweisen (realer
Modellaufruf gegen `office.sysops.de:11435`, reale Korrektur-/
Vorschlagslogik) bestanden. `TestKlassifizierungsgenauigkeit_...` und
`TestExtraktionsgenauigkeit_...` werden bei fehlendem
`TEST_AI_OLLAMA_BASE_URL`/`TEST_AI_OLLAMA_MODEL` übersprungen statt zu
simulieren (konsistent mit dem bestehenden Muster in
`ai/internal/extract/extract_test.go` und `ai/internal/doctype/real_test.go`).
## Ergebnis
QA-03 erfüllt alle Akzeptanzkriterien mit echten, ausgeführten Nachweisen.
- **Testkorpus**: die 5 Dokumente aus `ai/internal/refcorpus/testdata` (QA-01, synthetisch,
`labels.json` mit `synthetic: true`) — je ein Dokument = ein Abschnitt.
- **Testfragen**: 5 feste, beantwortbare Fragen (je eine pro Dokumentart) plus 1 bewusst
korpusfremde Frage ("Wie ist das Wetter morgen in Timbuktu?") für den Unbelegt-Fall.
- **Einbettung**: deterministisches TF-IDF über das Korpusvokabular (kein Netzwerk, kein
Ollama/OpenAI-Aufruf — reproduzierbar). Reine Termfrequenz ohne IDF-Gewichtung wurde
zunächst probiert und verworfen (Precision@1 nur 80%, weil fachlich entscheidende, seltene
Wörter wie "Beratungsleistung" gegen häufige Wörter wie "Betrag"/"Rechnung" untergingen,
die in mehreren Dokumenten der Testakte auftreten, z. B. zitiert die Mahnung die Rechnung).
- **Generierung**: fester, deterministischer Antworttext mit Zitat-Marker — die Citations
werden in `docchat.Ask` unabhängig vom Modelltext aus den Suchtreffern gebaut, daher ist
kein echtes Sprachmodell nötig, um AK1–AK3 zu prüfen.
## Akzeptanzkriterien
| # | Kriterium | Ergebnis |
|---|---|---|
| 1 | Suchqualität an Testkorpus dokumentiert | Erfüllt — Precision@1 = 5/5 = 100% über alle 5 Testfragen (Prüfung 1) |
| 2 | Chat-Antworten sind durchgängig mit Quelle belegt | Erfüllt — alle 5 beantwortbaren Testfragen liefern ≥1 Citation, die über `ResolveCitation` auf einen tatsächlich indizierten Abschnitt des erwarteten Dokuments auflöst (Prüfung 2) |
| 3 | Unbelegte Antwort wird zuverlässig gekennzeichnet | Erfüllt für den strukturell erzwungenen Fall (keine freigegebenen Referenzen → `NoEvidenceAnswer`, keine Citations). **Offener Befund** für den Fall einer fachlich unbelegten Frage *mit* vollem Zugriff — siehe unten |
welche Produktionscode-Pakete die Governance-Bausteine tatsächlich aufrufen.
## Akzeptanzkriterien
| # | Kriterium | Ergebnis |
|---|---|---|
| 1 | Opt-out einer Funktion wird in der Praxis respektiert | **Teilweise erfüllt, mit offenem Befund.** Der Mechanismus selbst (`tenantfeature.Store.IsEnabled`) funktioniert korrekt und sperrt eine deaktivierte Funktion zuverlässig, sobald ein Aufrufer ihn befragt (`TestAK1_OptOut_BlocksSimulatedHandlerCall`). **Kein** produktiver HTTP-Handler (`summaryapi`, `semanticsearchapi`, `extractapi`, `extractconfirmapi`, `docchatapi`, `routingsuggestapi`) befragt ihn aktuell — siehe Befund unten. Ein Tenant-Opt-out wirkt damit heute an keiner einzigen echten Schnittstelle. |
| 2 | Datenresidenz-Konfiguration wird eingehalten | **Erfüllt**, end-to-end über die reale `GuardedProvider`-Kette nachgewiesen (`TestAK2_Residency_EndToEndThroughGuardedProvider`): ohne Freigabe blockiert, nach `Approve` durchlässig, nach `Revoke` wieder blockiert, fremder Mandant unabhängig blockiert. Ergänzender Befund: `GuardedProvider` wird in keinem `cmd/`-Einstiegspunkt instanziiert (nur `ollama`, kein `openai`, ist produktiv verdrahtet) — die Prüfung bestätigt den *Mechanismus*, nicht einen produktiven externen Datenfluss, da es aktuell keinen gibt. |
| 3 | Jede geprüfte Entscheidung ist im Audit nachvollziehbar | **Teilweise erfüllt, mit offenem Befund.** Für den exemplarischen Entscheidungspfad `classify` ist die Kette (`decisionaudit.Recorder` → `auditclient` → Core-Audit) end-to-end korrekt und fail-closed (`TestAK3_DecisionAudit_SampleTraceableEndToEnd`). Alle anderen produktiven Entscheidungsfunktionen (`extract`, `summarize`, `docchat`, `semanticsearch`, `routing-suggest`) melden aktuell **keine** Entscheidung an das Audit-Log — siehe Befund unten. |
## Prüfungen vor der Abnahme
| # | Prüfung | Zielwert | Messwert |
|---|---|---|---|
| 1 | Opt-out-Fall im Test bestätigt | Deaktivierte Funktion wird von einem den Stand abfragenden Aufrufer nicht ausgeführt | **Bestanden** (Mechanismus): `TestAK1_OptOut_BlocksSimulatedHandlerCall`, real gegen Postgres (`qa05_ai_test`), PASS. **Nicht erreicht** (Praxis): 0 von 6 produktiven API-Handler-Paketen importieren `ai/internal/tenantfeature` (statische Prüfung, siehe Befund 1). |
| 2 | Datenresidenz-Fall lokal und extern geprüft | Lokal = Standard ohne Freigabe blockiert extern; nach Freigabe durchlässig; nach Rücknahme wieder blockiert | **Bestanden**: `TestAK2_Residency_EndToEndThroughGuardedProvider`, real gegen Postgres, PASS — alle vier Teilfälle (ohne Freigabe / nach Approve / nach Revoke / fremder Mandant) korrekt. |
| 3 | Audit-Nachvollziehbarkeit an Stichprobe bestätigt | Eine Beispiel-Entscheidung erzeugt genau einen Audit-Eintrag mit Modell-/Prompt-Version-Verweis | **Bestanden** (Stichprobe `classify`): `TestAK3_DecisionAudit_SampleTraceableEndToEnd`, PASS, Eintrag mit `action=ai.decision.classify`, `target`, `model`, `prompt_version` korrekt. **Nicht erreicht** (Vollabdeckung): nur `internal/classify` ruft `decisionaudit`/`auditclient` auf, 5 weitere produktive Entscheidungsfunktionen nicht (siehe Befund 2). |
Datum: 2026-10-17. Bündelt die sechs Einzelprüfgates (QA-02, QA-03, QA-04, QA-05, QA-07, QA-08) sowie den Barrierefreiheits-Audit (QA-09) zu einer Freigabeentscheidung für das ai-kanban-Board. Quelle je Gate: Prüfprotokoll des jeweiligen Branches (`ai/docs/<TICKET>-PRUEFPROTOKOLL.md`), gelesen per `git show <branch>:<pfad>` ohne Merge, analog zum etablierten Muster aus connect-kanban/workflow-kanban QA-05/06.
## Geprüfte Branches (Stand dieser Abnahme, kein Branch gemerged)
| QA-05 (Datenschutz & Governance) | Opt-out respektiert, Datenresidenz eingehalten, Audit-Nachvollziehbarkeit | AK2 (Datenresidenz) voll erfüllt. **AK1 und AK3 nur teilweise erfüllt**: Opt-out-Mechanismus funktioniert im Test, aber kein produktiver HTTP-Handler fragt ihn ab (Lücke bereits von GOV-02 als außerhalb seines Scopes vermerkt). Audit-Nachvollziehbarkeit nur für `classify` bestätigt, nicht für extract/summarize/docchat/semanticsearch/routing-suggest (bereits von GOV-05 so dokumentiert). | **Teilweise bestanden** |
| QA-07 (Vertragstests GOV-01) | Vertragstests decken Schnittstelle ab, Breaking Change wird erkannt, CI-Integration | Live-Breaking-Change real herbeigeführt (`json:"name"`→`json:"field_name"`), Test schlug real fehl, zurückgesetzt. CI-Workflow vorhanden und pfadgefiltert. | **Bestanden** |
| QA-08 (Last & Leistung) | Chat-Antwortzeit unter Last, Queue-Verhalten ohne unkontrolliertes Wachstum, definiertes Überlastverhalten | Chat p95=4,01s (Zielwert begründet auf 25s Sicherheitsmarge angehoben, geteilte Infrastruktur). Queue-Durchsatz 371/s, 0 Fehler. **Echte Lücke gefunden und geschlossen**: Überlastverhalten war vorher undefiniert (unbegrenztes Wachstum), jetzt `MaxPendingJobs`-Limit mit definiertem `ErrQueueFull`. | **Bestanden** |
| QA-09 (Barrierefreiheit) | Keine kritischen Verstöße, Bildschirmleser-Ersatznachweis, Restbefunde mit Frist | 0 Verstöße über alle 3 Oberflächen (axe hell+dunkel, Reflow, Textabstände), 15/15 Tastaturabläufe bestanden. Bildschirmleser-Lücke (kein NVDA/Orca) ist bekannte projektweite Einschränkung, nur referenziert. | **Bestanden** |
## Akzeptanzkriterium 2: Gesamtprotokoll ist an einem Ort abgelegt
Dieses Dokument (`ai/docs/QA-06-PRUEFPROTOKOLL.md`) referenziert alle sieben Einzelprotokolle mit Branch/Commit, Zielwert, Messwert und Bewertung. **Erfüllt.**
## Akzeptanzkriterium 3: Offene Punkte sind mit Verantwortlichkeit vermerkt
1.**QA-04 / semanticsearch-Mindestschwelle**: `semanticsearch.Service.Search` liefert bei fachlich unbelegten Fragen trotzdem Top-K-Treffer mit Ähnlichkeit nahe 0, `docchat.Service.Ask` erzeugt daraus eine scheinbar belegte Antwort. Empfehlung: Folgeticket für eine Mindest-Ähnlichkeitsschwelle in `semanticsearch`/`docchat`. Verantwortlich: AI-Modul-Team, vor produktivem Rollout von SEM-04 zu klären.
2.**QA-05 / Opt-out ohne Wirkung an echten Endpunkten**: `tenantfeature.Store.IsEnabled` funktioniert, aber kein produktiver Handler (`summaryapi`, `semanticsearchapi`, `extractapi`, `extractconfirmapi`, `docchatapi`, `routingsuggestapi`) fragt ihn ab. War bereits bei GOV-02 als "nicht Bestandteil dieser Kachel" ausgewiesen — hier erneut als vor Produktivbetrieb zu schließende Lücke bestätigt. Verantwortlich: Folgeticket zur Verdrahtung in allen Handlern.
3.**QA-05 / Audit-Nachvollziehbarkeit unvollständig**: Nur `classify` meldet Entscheidungen ans Audit-Log; `extract`, `summarize`, `docchat`, `semanticsearch`, `routing-suggest` nicht (bereits von GOV-05 dokumentiert). Verantwortlich: Folgeticket zur Audit-Verdrahtung in den übrigen Funktionen.
4.**QA-05 / GuardedProvider nicht produktiv verdrahtet**: Die Datenresidenz-Durchsetzung (`openai.GuardedProvider`) ist im Test bestätigt, aber in keinem `cmd/`-Diensteinstiegspunkt produktiv instanziiert (nur `ollama`, kein `openai`-Pfad aktiv). Verantwortlich: bei Aktivierung eines externen Providers zu verdrahten.
5.**QA-08 / Hohe Parallelität nur simuliert**: Echte 10x-Parallelität wurde bewusst nicht gegen den geteilten Ollama-Host gefahren (Ressourcenschutz), sondern mit Stub + realer Baseline-Latenz simuliert. Eine Aussage über das Verhalten des Modell-Hosts selbst unter echter hoher Last ist damit nicht getroffen.
6.**QA-08 / Kein HTTP-Statuscode-Mapping für Überlast**: `ErrQueueFull` ist bisher nur ein Go-API-Fehlerwert, kein HTTP-Statuscode-Mapping (folgt mit dem noch fehlenden `cmd/ai-app`-Diensteinstiegspunkt, ENG-08).
7.**QA-09 / Bildschirmleser-Durchlauf**: Keine echte NVDA/VoiceOver/Orca-Prüfung möglich (projektweite, bereits unter UX-06 nachverfolgte Werkzeuglücke), Accessibility-Tree-Dumps als Ersatz abgelegt.
8.**Keiner der sieben Gate-Branches ist gemerged.** Diese Abnahme bewertet den Stand der offenen Branches, nicht den Stand von `main`.
## Freigabeentscheidung
**Bedingte Freigabe.** Sechs von sieben Gates sind vollständig bestanden (QA-02, QA-03, QA-04 mit dokumentiertem Befund, QA-07, QA-08, QA-09). QA-05 ist nur teilweise erfüllt: zwei von drei Akzeptanzkriterien (Opt-out-Wirkung an echten Endpunkten, vollständige Audit-Nachvollziehbarkeit) sind strukturell noch nicht an den produktiven Pfaden verdrahtet — das ist keine neue Lücke dieser Prüfung, sondern eine bereits von GOV-02/GOV-05 selbst dokumentierte Einschränkung, hier bestätigt statt übersehen.
Produktive Inbetriebnahme kann erfolgen, sobald:
- die Opt-out- und Audit-Verdrahtung aus QA-05 (Punkte 2+3) in den produktiven Handlern nachgezogen ist, und
- alle sieben Branches plus `ENG-08` (`cmd/ai-app`, selbst noch nicht umgesetzt, für produktiven Start nötig) in sinnvoller Reihenfolge gemerged sind.
Die übrigen offenen Punkte (1, 4, 5, 6, 7) sind nicht freigabeblockierend, aber mit Verantwortlichkeit nachzuverfolgen.
Datum: 2026-10-17. Host: Entwicklungshost 192.168.1.204, Wegwerf-Cluster (Port 5433, eigene Rolle/DB `aiqa07test`/`aiqa07testdb`). Frischer Klon von `main`.
## Umsetzung
Neues Paket `ai/internal/contracttest`: Vertragstests für die Modul-Adapter-Schnittstelle GOV-01 (`ai/internal/moduletype`), über die DMS und Mail KI-fähige Objekttypen registrieren. Gleiches Muster wie Archive QA-06 (`archive/internal/contracttest`): `RequireJSONFields` dekodiert eine echte JSON-Antwort und meldet fehlende Pflichtschlüssel.
Zwei Verträge geprüft, jeweils gegen die TATSÄCHLICHE Antwort der echten Komponente, nicht gegen eine nachgebaute Attrappe:
- **Vertrag 1 – registrierter Objekttyp** (`moduletype.ObjectType`): echte `Registry.Register`/`Get`-Rückgabe (gegen Wegwerf-Postgres) JSON-kodiert, Pflichtfelder `TenantSlug`, `ModuleName`, `ObjectType`, `Fields` geprüft. Läuft nur mit `TEST_TENANT_DSN` (sonst Skip, wie bei Archive QA-06).
- **Vertrag 2 – Feld-Schema** (`moduletype.Field`): echte JSON-Kodierung eines `Field`-Werts geprüft auf `name`, `kind`. Läuft immer, ohne Datenbank.
Zu jedem Vertrag ein Test, der einen simulierten Breaking Change (entferntes bzw. umbenanntes Pflichtfeld) gegen `RequireJSONFields` prüft und dessen Fehlschlagen erwartet (AK2).
CI-Integration: `.gitea/workflows/ai-contract-tests.yml`, ausgelöst bei Änderungen unter `ai/internal/contracttest/**` oder `ai/internal/moduletype/**`, führt `go test ./internal/contracttest/... -v` automatisiert aus (AK3).
## Akzeptanzkriterien
| # | Kriterium | Ergebnis |
|---|---|---|
| 1 | Vertragstests decken die Modul-Adapter-Schnittstelle GOV-01 ab | **Bestanden.**`TestContract_GOV01_RegisteredObjectTypeSchema` (Objekttyp-Rückgabe) und `TestContract_GOV01_FieldSchema` (Feld-Schema) prüfen beide die echte `moduletype`-Komponente. |
| 2 | Ein absichtlich brechender Schema-Wechsel lässt die Vertragstests fehlschlagen | **Bestanden.** Siehe Prüfung 2 unten: Umbenennung von `json:"name"` auf `json:"field_name"` in `moduletype.Field` live im Code herbeigeführt, `TestContract_GOV01_FieldSchema` schlägt fehl mit `field-schema fehlen pflichtfelder: [name]`, danach zurückgesetzt. |
| 3 | Vertragstests laufen automatisiert in der CI, nicht nur manuell | **Bestanden.**`.gitea/workflows/ai-contract-tests.yml` führt die Tests bei Push/PR auf die betroffenen Pfade automatisch aus. |
## Prüfungen vor der Abnahme
| # | Prüfung | Zielwert | Messwert | Ergebnis |
|---|---|---|---|---|
| 1 | Testlauf gegen aktuellen Stand | 0 Fehlschläge | `go test ./internal/contracttest/... -v`: 4/4 Tests PASS | **Bestanden** |
| 2 | Simulierte Breaking Change wird erkannt | Testlauf nach Rename schlägt fehl, nach Rückbau wieder grün | Rename `json:"name"` → `json:"field_name"` in `moduletype.go`: `TestContract_GOV01_FieldSchema` FAIL (`fehlen pflichtfelder: [name]`); nach `git checkout -- internal/moduletype/moduletype.go`: 4/4 Tests wieder PASS | **Bestanden** |
| 3 | CI-Integration nachgewiesen | Workflow-Datei mit passenden `paths`-Filtern vorhanden | `.gitea/workflows/ai-contract-tests.yml` vorhanden, Trigger auf `ai/internal/contracttest/**` und `ai/internal/moduletype/**`, Schritt `go test ./internal/contracttest/... -v` | **Bestanden** |
## Testergebnis
4/4 neue Tests grün (`go test ./internal/contracttest/... -v`), `go build ./...` und `go vet ./...` im AI-Modul sauber. Volle Modul-Regression (`go test ./...`) ohne Fehler, keine bestehenden Tests betroffen.
## Hinweis zur Datenbankabdeckung
Wie bei Archive QA-06 läuft Vertrag 1 (Objekttyp-Rückgabe) nur mit gesetztem `TEST_TENANT_DSN` gegen eine echte Postgres-Instanz; in der CI ohne diese Variable wird dieser Test übersprungen (`t.Skip`), nicht als Fehler gewertet. Vertrag 2 (Feld-Schema) sowie beide Breaking-Change-Tests laufen immer, auch ohne Datenbank — das ist der in der CI tatsächlich scharfe Teil der Vertragstests. Kein offener Befund, aber bewusste Einschränkung, identisch zum etablierten Muster.
Geprüft auf Entwicklungshost 204, frischer Klon, Go-Build/Test. Queue-Tests gegen echtes Postgres (Wegwerf-DB `qa08_ai_test`), Chat-Tests mit einem realen Lauf gegen den geteilten Ollama-Host `office.sysops.de:11435` (`gpt-oss:20b`) sowie einem Stub-Lauf für höhere Parallelität (Begründung siehe unten).
## Umsetzung
-`ai/internal/loadtest`: Lasttest-Muster übernommen aus `mail/internal/loadtest` (Go-Module teilen keine `internal/`-Pakete, daher eigenständige Kopie) — reale nebenläufige Aufrufe über die Go-Standardbibliothek statt k6/vegeta (nicht installierbar, Projektregel).
-`ai/internal/limits`: neues Feld `MaxPendingJobs` (Default 500, über `AI_INFERENCE_MAX_PENDING_JOBS` konfigurierbar) — definiert die Überlast-Grenze der Queue.
-`ai/internal/queue`: `Enqueue` prüft vor dem Anlegen eines Jobs `Depth.Pending` gegen `limits.MaxPendingJobs` und liefert `ErrQueueFull`, statt die Warteschlange unbegrenzt wachsen zu lassen (AK3).
-`ai/internal/queue/queue_loadtest_test.go`, `ai/internal/docchat/docchat_qa08_loadtest_test.go`: neue Lasttests (siehe Prüfungen).
## Prüfungen
| # | Prüfung | Zielwert | Messwert | Ergebnis |
|---|---|---|---|---|
| 1a | Parallele Chat-Anfragen, real gegen `gpt-oss:20b`, Concurrency 3, 6 Sessions | p50 ≤ 10s, p95 ≤ 25s (angepasster Lastwert, s. Entscheidungen) | p50 = 3,65s, p95 = 4,01s, p99 = 4,01s, max = 4,50s, Durchsatz 0,71/s, 0 Fehler | **Bestanden**, deutlich unter Zielwert |
| 1b | Eigener Anfrage-Pfad (Retrieval+Prompt+Zitate) bei 10-facher Parallelität (Stub mit realistischer Baseline-Latenz 9,2s aus SEM-04), Concurrency 30, 90 Sessions | Zusatzverzögerung durch eigenen Code ≤ 2s über der simulierten Modell-Latenz | p50 = 9,21s, p95 = 9,21s, max = 9,21s → Aufschlag ≈ 9ms | **Bestanden** |
| 2 | Queue-Länge unter paralleler Last (4 Worker, 20 gleichzeitige Enqueue-Sessions, 120 Jobs) | Queue wird abgearbeitet, kein unkontrolliertes Wachstum (`Pending` kehrt auf 0 zurück) | Enqueue: p50=53ms, p95=55ms, max=55ms, Durchsatz 371/s, 0 Fehler. `Pending=0, Processing=0, DeadLetter=0` nach Ablauf | **Bestanden** |
| 3 | Überlast-Szenario: `MaxPendingJobs=5`, 6. Enqueue-Versuch | Definiertes Verhalten (kontrollierte Ablehnung, kein Blockieren/Absturz), Erholung nach Abarbeitung | 6. Versuch liefert `ErrQueueFull`, `Pending` bleibt bei 5 (kein Job angelegt). Nach einem `ProcessOne` ist wieder Platz, Enqueue gelingt | **Bestanden** |
## Akzeptanzkriterien
1. Definierte Ziel-Antwortzeit für Chat-Anfragen unter paralleler Last eingehalten — erfüllt (Prüfung 1a/1b).
2. Inferenz-Queue verarbeitet mehrere gleichzeitige Anfragen ohne unkontrolliertes Anwachsen der Warteschlange — erfüllt (Prüfung 2).
3. Verhalten bei Überlast (Queue voll) ist definiert — erfüllt (Prüfung 3, `ErrQueueFull`).
## Entscheidungen (analog workflow-kanban QA-07)
- **p95-Zielwert für Chat unter Last auf 25s angehoben** (statt dem SEM-04-Einzelwert von 10s): `office.sysops.de:11435` ist ein geteilter Ollama-Host für mehrere Projekte/Sessions gleichzeitig; ein einzelner Modellprozess verarbeitet Anfragen nicht beliebig parallel. Diesen Umstand architektonisch aufzulösen (z. B. mehrere Modellinstanzen) ist nicht Scope dieses Prüfgates. Der tatsächlich gemessene Wert (p95 = 4,01s) liegt weit unter beiden Zielwerten — die Anhebung ist eine Sicherheitsmarge für künftige Läufe unter echter Fremdlast auf dem geteilten Host, keine Reaktion auf einen tatsächlichen Fehlschlag in diesem Lauf.
- **Höhere Parallelität (30 statt 3) nur mit Stub-Provider getestet**: 30 echte gleichzeitige Anfragen gegen den geteilten Ollama-Host wären ein Lasttest gegen fremde, von anderen Projekten mitgenutzte Infrastruktur, nicht gegen den eigenen Code — und würde Host-204-Regel "Last vorher prüfen, moderat halten" verletzen. Die simulierte Latenz ist die real gemessene SEM-04-Baseline (9,2s), kein erfundener Wert; geprüft wird damit ausschließlich, ob der *eigene* Anfrage-Pfad bei zehnfacher Last selbst zum Flaschenhals wird (ist er nicht: Aufschlag ≈ 9ms).
- **`MaxPendingJobs`-Grenzwert 500 (Default)**: willkürlich gewählte, aber großzügige Schwelle, da kein bestehender Zielwert für maximale Warteschlangentiefe im Ticket vorgegeben war; über `AI_INFERENCE_MAX_PENDING_JOBS` pro Umgebung anpassbar. Die Rückmeldung bei Erreichen (`ErrQueueFull`) ist bewusst ein regulärer Fehlerwert, keine Panik/kein stiller Datenverlust, und der Aufrufer-Code (zukünftiger HTTP-Adapter) kann ihn auf einen definierten Statuscode abbilden (noch kein HTTP-Endpunkt für die Queue vorhanden, siehe ENG-05-Protokoll).
## Gesamtlauf auf 204
-`go build ./...`, `go vet ./...`: sauber.
-`gofmt -l .`: nur vorbestehende, nicht von diesem Ticket berührte Dateien gemeldet (Formatierungsabweichungen außerhalb des QA-08-Scopes, nicht angefasst).
-`go test ./...` (mit `TEST_TENANT_DSN` gegen Wegwerf-DB `qa08_ai_test`): alle Pakete `ok`, keine Regression.
- Lasttests einzeln mit `-v` verifiziert (siehe Prüfungstabelle), Host-Last vor dem Lauf per `uptime` geprüft, Nebenläufigkeit moderat gehalten (max. 30 Goroutinen, keine CPU-intensive Arbeit — Chat-Last wartet nur auf Netzwerk/simulierte Latenz, Queue-Last ist reine Postgres-Interaktion).
## Offene Befunde
- Kein HTTP-Endpunkt für die Queue vorhanden (bereits in ENG-05 als Folgeticket vermerkt) — `ErrQueueFull` ist bislang nur auf Go-API-Ebene definiert, eine Zuordnung zu einem HTTP-Statuscode (z. B. 429) folgt mit dem Modul-Adapter.
- Reale Parallel-Last für Chat wurde bewusst nur mit geringer Nebenläufigkeit (3) gegen den produktiv geteilten Ollama-Host gefahren; eine Aussage über das Verhalten bei echter hoher Nebenläufigkeit *des Modell-Hosts selbst* (nicht des eigenen Codes) ist damit nicht getroffen und wäre Scope eines eigenen Kapazitätstickets für `office.sysops.de`.
## Aufräumen nach diesem Lauf
Wegwerf-Datenbank/Rolle `qa08_ai_test` auf Host 204 wurde für diesen Lauf angelegt und kann nach Abnahme des Tickets entfernt werden (`DROP DATABASE qa08_ai_test; DROP ROLE qa08_ai_test;`).
| AK3: Restbefunde mit Frist | **erfüllt**– keine echten Verstöße gefunden, daher nur die bekannte projektweite Methodik-Lücke terminiert (Abschnitt 5) |
| Tastaturabläufe ohne Maus (alle drei Oberflächen) | 15 von 15 Prüfungen bestanden, 0 Mausereignisse |
| Reflow (320 CSS-px) | 3 von 3 Ansichten, kein horizontaler Seitenscroll |
| Textabstände (WCAG 1.4.12) | 3 von 3 Ansichten, kein abgeschnittener Text |
## 1. Automatisierte axe-Prüfung (Prüfung 1, AK1)
Lauf gegen production-Builds (`next build && next start`) aller drei Apps, headless Chrome, hell und dunkel:
| Ansicht | Hell | Dunkel |
|---|---|---|
| `ai-extraction-review` (Port 4120) | axe: keine | axe: keine |
| `ai-summaries` (Port 4121) | axe: keine | axe: keine |
| `ai-search-chat` (Port 4122) | axe: keine | axe: keine |
`report.json` (axe, voller Accessibility-Tree ohne `interestingOnly`-Filter) enthält über alle 6 Läufe 0 Befunde (`violations: []`). **Keine kritischen oder schweren Verstöße**– AK1 erfüllt, keine Fixes in dieser Kachel erforderlich.
## 2. Reflow und Textabstände (ergänzende Prüfungen)
- **Reflow bei 320 CSS-px** (entspricht 400 % Zoom bei 1280 px): alle 3 Ansichten ohne horizontalen Seitenscroll, axe: keine Verstöße (`report-reflow.json`).
- **Textabstände** (Zeilenhöhe 1,5, Buchstaben 0,12 em, Wörter 0,16 em, Absätze 2 em, WCAG 1.4.12): alle 3 Ansichten ohne abgeschnittenen Text, axe: keine Verstöße (`report-textspacing.json`).
## 3. Vollständige Tastaturabläufe ohne Maus (Prüfung 2)
`qa09-keyboard-flows.js` führt pro Oberfläche die zentralen Nutzerabläufe ausschließlich per Tastatur aus und dokumentiert jeden Schritt sowie einen Accessibility-Tree-Dump des relevanten Zustands:
| Ablauf | Ergebnis |
|---|---|
| EXT-04: axe beim Laden | keine kritischen/schweren Verstöße |
| EXT-04: Extraktionsfeld per Tastatur korrigierbar | bestanden |
| EXT-04: Bestätigen nur mit Tastatur ausgelöst | bestanden |
| EXT-04: Abbrechen nur mit Tastatur ausgelöst, keine Übernahme | bestanden |
| SUM-03: axe beim Laden | keine kritischen/schweren Verstöße |
| SUM-03: Link zum Originaldokument per Tastatur erreichbar | bestanden |
| SUM-03: Link zum Original-Thread per Tastatur erreichbar | bestanden |
| SEM-06: axe Suche (Startzustand) | keine kritischen/schweren Verstöße |
| SEM-06: Suche nur mit Tastatur ausgeführt, Treffer angezeigt | bestanden |
| SEM-06: Zitat per Tastatur zum Originalabschnitt aufgelöst | bestanden |
**15 von 15 Prüfungen bestanden**, 0 Mausereignisse. Keine Tastaturfallen, kein unsichtbarer Fokus.
Die vollständigen Accessibility-Tree-Dumps der geprüften Zustände liegen unter `ai/docs/QA-09-tree/`:
`4120_.txt`, `4121_.txt`, `4122_.txt` (Grundzustand der drei Apps), `ext04-geladen.txt`, `ext04-bestaetigt.txt`, `sum03-geladen.txt`, `sem06-suche-leer.txt`, `sem06-suche-treffer.txt`, `sem06-chat-leer.txt`, `sem06-chat-antwort.txt`, `sem06-chat-zitat-aufgeloest.txt`.
## 4. Bildschirmleser-Ersatznachweis (AK2)
Es gilt die **projektweit bekannte Einschränkung**: Auf dem Testhost (192.168.1.204) und der Entwicklungsmaschine steht kein echter Bildschirmleser (NVDA, VoiceOver, Orca) zur Verfügung. Diese Lücke ist bereits an anderer Stelle dokumentiert und terminiert, u. a. in `dms/docs/UX-06-PRUEFPROTOKOLL.md` (Befund „Bildschirmleser-Durchlauf NVDA/VoiceOver offen"), und wird hier nicht erneut neu aufgemacht, sondern nur referenziert.
Als Ersatznachweis dienen die Accessibility-Tree-Dumps aus Abschnitt 3 (`ai/docs/QA-09-tree/*.txt`): sie zeigen für jeden geprüften Zustand die tatsächliche Rollen-/Name-/Wert-Struktur, wie sie ein Bildschirmleser vorfände (Rollen, zugängliche Namen, Fokusreihenfolge, Live-Region-Inhalte bei Status-/Ergebnisänderungen in `ai-search-chat`). Ergänzend wurde der Quellcode der betroffenen Formular- und Status-Elemente (Bestätigen/Abbrechen in EXT-04, Links in SUM-03, Such-/Chat-Ergebnisanzeige in SEM-06) manuell durchgesehen; keine Hinweise auf fehlende zugängliche Namen oder fehlende Status-Ankündigung.
AK2 gilt damit als **erfüllt im Rahmen der verfügbaren Prüfmittel**, mit der bekannten offenen Lücke „echter AT-Durchlauf" (Abschnitt 5).
## 5. Restbefunde und Fristen (AK3)
Es wurden **keine echten WCAG-2.1-AA-Verstöße** gefunden (0 axe-Befunde über alle Läufe, 15/15 Tastaturprüfungen bestanden). Es bleibt der bereits bekannte, nicht ticketspezifische Methodik-Restbefund:
- **Echter Bildschirmleser-Durchlauf (NVDA/VoiceOver/Orca) steht aus** – wie in `dms/docs/UX-06-PRUEFPROTOKOLL.md` beschrieben, bietet weder Testhost noch Entwicklungsmaschine eine grafische Oberfläche mit installiertem Screenreader. Ersatzweise wurden Accessibility-Tree-Dumps und Quellcode-Durchsicht genutzt (Abschnitt 4). **Frist:** nachzuholen, sobald ein Arbeitsplatz mit echtem Bildschirmleser für das AI-Board verfügbar ist, spätestens vor der finalen Gesamtabnahme des AI-Boards. Schweregrad: niedrig – automatisierte axe-Prüfung und Tastatur-/Rollen-/Namen-Prüfung decken die wesentlichen WCAG-2.1-AA-Kriterien bereits ab.
Keine blockierenden Befunde. Keine Fixes an den geprüften Oberflächen waren erforderlich.
## 6. Build/Testergebnis
```
go build (ai/cmd/searchchatapi-a11y-fixture, extractapi-a11y-fixture, summaryapi-a11y-fixture) -> ok
3x next build (ai-extraction-review, ai-summaries, ai-search-chat) -> clean
**Bestanden.** Keine kritischen oder schweren Barrierefreiheits-Verstöße (AK1), Bildschirmleser-Ersatznachweis über Accessibility-Tree-Dumps und Quellcode-Durchsicht erbracht (AK2), kein neuer Restbefund außer der bereits bekannten, projektweiten Screenreader-Lücke, die mit Frist terminiert bleibt (AK3).
t.Logf("QA-08 Warteschlangenstand nach Abarbeitung: pending=%d processing=%d dead_letter=%d",depth.Pending,depth.Processing,depth.DeadLetter)
ifresult.Errors>0{
t.Fatalf("%d von %d Enqueue-Aufrufen fehlgeschlagen, erwartet 0 ohne Ueberlast-Grenze",result.Errors,result.Sessions)
}
ifdepth.Pending!=0{
t.Fatalf("depth.Pending = %d nach Ablauf der Wartezeit, erwartet 0 (Warteschlange sollte abgearbeitet sein, kein unkontrolliertes Wachstum)",depth.Pending)
}
}
// QA-08 Pruefung 3 (AK3): bei erreichter Ueberlast-Grenze ist das Verhalten
// definiert — Enqueue lehnt kontrolliert mit ErrQueueFull ab, statt
`mail_message`/`handelsrechtlich_6j`, `audit_event`/`sofort_loeschbar`), setzt je Objekt eine
Aufbewahrungssperre, prüft, dass `ReleaseExpired` das gesperrte, fällige Referenzobjekt NICHT
freigibt, und dass `Destroy` mit `ErrOnLegalHold` scheitert.
-`archive/internal/wormstore/wormstore_test.go`: neuer Test
`TestPruefgate02_OverwriteAndTamperDetectionForReferenceSetPayloads`– schreibt je
Größenklasse (`SizeSmall`/`SizeMedium`/`SizeLarge`) die deterministischen
`referenceset.WormPayload`-Nutzdaten, vergleicht die Prüfsumme mit
`referenceset.WormPayloadChecksum`, prüft den Überschreibversuch über die Go-API
(`ErrAlreadyExists`) und den direkten Schreibzugriff am Dateisystem.
- Kein Produktivcode in `deletionworkflow`/`wormstore` geändert – nur Testdateien, RET-03/RET-04
bleiben unverändert (Pflichttestgate aus `TESTSTRATEGIE-ARCHIVE.md` Abschnitt 3 bleibt erfüllt).
## Prüfungen vor der Abnahme
| # | Prüfung | Zielwert | Messwert | Bewertung |
|---|---|---|---|---|
| 1 | Löschversuch an gesperrtem Objekt schlägt fehl (RET-03, Referenzdatensatz) | `Destroy` liefert `ErrOnLegalHold` für jedes der drei Referenzobjekte, Status bleibt unverändert, `ReleaseExpired` gibt das gesperrte Objekt trotz Fälligkeit nicht frei | `go test ./internal/deletionworkflow/... -v -count=1` gegen echte `TEST_TENANT_DSN`-DB, 2026-10-16: **7/7 PASS**, davon `TestDestroy_ObjectWithActiveHoldResistsDeletion` (Alt-Fixture) und `TestPruefgate02_LegalHoldResistsDeletionForReferenceSetObjects` (Referenzdatensatz, alle 3 Objekttypen) beide grün; zusätzlich `TestDestructionLog_IsItselfLogged`/`TestDestructionLog_IsImmutable`-Familie unverändert grün | **bestanden** |
| 2 | Überschreibversuch am WORM-Speicher schlägt fehl (RET-04, Referenzdatensatz) | Zweiter `Put` auf denselben Schlüssel liefert `ErrAlreadyExists`; direkter Schreibzugriff am Dateisystem scheitert; Prüfsumme der gelesenen Daten entspricht `referenceset.WormPayloadChecksum` | **Teilweise bestanden, Teilergebnis offen**– siehe Restrisiko 1. Automatisierter Store-Testlauf (`TEST_WORMSTORE_LOCK_DIR` auf ZFS-Wurzel des Testhosts) übersprang 7 Tests (inkl. des neuen `TestPruefgate02_OverwriteAndTamperDetectionForReferenceSetPayloads`) real mit der Meldung "chattr +i wird ... nicht durchgesetzt" – reproduziert exakt den in `RET-11-PRUEFPROTOKOLL.md` für Host 131 dokumentierten Befund, jetzt auch für Host 204 bestätigt. Als Ersatznachweis für die ursprüngliche RET-04-Garantie (chmod 0400, ohne chattr) wurde **manuell** als echter Betriebsnutzer geprüft: Verzeichnis `nexarch:nexarch`-Eigentümer (realistische Produktionslage), Datei von `nexarch` geschrieben und anschließend `chmod 0400` gesetzt; `sudo -u nexarch` Schreibversuch auf die Datei → **„Permission denied", Exit 1**, Inhalt unverändert. Reflection-Test `TestStore_OnlyDestroyCanRemoveObjects` (kein Lösch-Pfad außer `Destroy`) lief real und **PASS** | **teilweise bestanden** (Schreibschutz real nachgewiesen, Storage-Object-Lock-Ebene auf diesem Host nicht prüfbar) |
| 3 | Offene Restrisiken sind schriftlich benannt | Vollständige, ehrliche Liste | 3 Punkte identifiziert und dokumentiert (unten) | **bestanden** |
## Build/Test-Ergebnis (192.168.1.204)
```
cd archive && go build ./... -> clean
cd archive && go vet ./... -> clean
golangci-lint -> auf Host 204 nicht installiert (anders als 131 in frueheren
Protokollen; keine Installation vorgenommen, siehe Vorgabe
"keine Toolchain-Installationen auf 204"); go vet als
verfuegbare statische Pruefstufe genutzt
go test ./internal/deletionworkflow/... -v -count=1
`TestExport_ChecksumsMatchAfterUnzip` — alle PASS, keine Regression.
## Prüfung 2 (Akzeptanzkriterium 2 / Pflichtprüfung 2): Vier-Augen-Freigabe mit zwei unabhängigen Testrollen erneut nachgewiesen
Neuer Test `TestConfirmAndExecute_QA03_TwoIndependentTestRoles`
(`archive/internal/deletionapproval/deletionapproval_test.go`) gegen die echte
PostgreSQL-Testdatenbank (`TEST_TENANT_DSN`), mit zwei ausdrücklich benannten, unabhängigen
Testrollen (`testrolle_sachbearbeiterin_a@qa03.example` fordert an,
`testrolle_pruefer_b@qa03.example` bestätigt):
| Prüfgröße | Zielwert | Messwert | Bewertung |
|---|---|---|---|
| Freigabe durch unabhängige zweite Testrolle (B) auf Anforderung von Testrolle A | Löschung wird ausgeführt, Objektstatus `deleted` | Löschung real ausgeführt, Status `deleted` | bestanden |
| Freigabeversuch durch dieselbe Testrolle (A bestätigt eigene Anforderung), ohne zweite Bestätigung | `ErrSamePerson`, Objektstatus bleibt `expired` (keine Löschung) | `ErrSamePerson`, Status real unverändert `expired` | bestanden |
und über die echte Volltextsuche auffindbar gemacht — keine Fixture-Doubles.
- **Mail**: echter `mail/internal/mailapi.Server`, mit echtem `mail/internal/search.Client`
(Manticore-Tabelle `mail_documents`, eigener Mandant `cmp08-ediscovery-mail`) und echtem
`mail/internal/storage.Service`; 4 synthetische Nachrichten über `search.Client.Index`
eingetragen. Mail hat noch keinen eigenen `cmd/app`-Produktivserver (siehe
`mail/internal/mailapi`-Paketkommentar) — Modul-Token-Verifikation läuft über die ECHTE
Verifikationslogik `mail/internal/moduleauth.Verifier`, mit einem lokal erzeugten
Test-Schlüsselpaar statt eines laufenden Core-Kontodienstes (`NewTestVerifierAndIssuer`,
dieselbe Konvention wie `mail/internal/mailapi/e2e_test.go`) — siehe Restrisiko unten.
- Beide Instanzen liefen als Wegwerf-Prüfwerkzeuge (`dms/cmd/cmp08-ediscovery-fixture`,
`mail/cmd/cmp08-ediscovery-fixture`), nach dem Testlauf wieder entfernt — nicht Teil dieses
Commits, nur die Produktivkomponente (`ediscoveryhttp`) und der Nachweistest bleiben.
- Alle Testdaten synthetisch (Suchbegriff `CmpAchtEdiscoveryFund`, Absendername
`pruefperson@cmp08.example.test`), keine echten Kunden-/Mandantendaten.
### Prüfung 1 (Akzeptanzkriterium 1): Export gegen echte, über RET-05 registrierte Instanzen
Vor dem eigentlichen Such-/Exportlauf registriert der Test beide Instanzen real über das
bestehende RET-05-Interface (`archive/internal/moduleadapter.Register`) gegen die echte
Postgres-Testdatenbank (`TEST_TENANT_DSN`): `module_name=dms/object_type=dms_document` mit
Rückruf-Adresse der echten laufenden DMS-Instanz, `module_name=mail/object_type=mail_message`
mit Rückruf-Adresse der echten laufenden Mail-Instanz.
| Prüfgröße | Zielwert | Messwert | Bewertung |
|---|---|---|---|
| Registrierungen in `module_registrations` nach Register() | 2 (DMS + Mail) | 2 | bestanden |
| Export erfolgt gegen die registrierten, laufenden Instanzen (nicht Referenzdatensatz) | ja | ja — `ediscoveryhttp.DMSAdapter`/`MailAdapter` rufen die echten Endpunkte auf | bestanden |
### Prüfung 2 (Akzeptanzkriterium 2): Treffer aus beiden Modulen, prüfsummengesichert
Test `TestEndToEnd_CMP08_RealDMSAndMailInstances`
(`archive/internal/ediscoveryhttp/e2e_test.go`): `ediscovery.Search()` über beide echten
Adapter, `ediscovery.Export()` baut das ZIP, der Test entpackt es erneut und vergleicht die
SHA-256-Prüfsumme jeder Datei gegen den `manifest.json`-Eintrag.
| Prüfgröße | Zielwert | Messwert | Bewertung |
|---|---|---|---|
| Treffer mit `module_type=dms_document` im Suchergebnis | ≥ 1 | 4 | bestanden |
| Treffer mit `module_type=mail_message` im Suchergebnis | ≥ 1 | 4 | bestanden |
`archive/docs/QA-04-ABNAHMEPROTOKOLL.md`, `archive/docs/QA-06-PRUEFPROTOKOLL.md` und
`archive/docs/RET-11-PRUEFPROTOKOLL.md` wurden direkt aus `main` gelesen.
## 1. Zusammenfassung der drei fachlichen Prüfgates (Akzeptanzkriterium 1/2)
| Gate | Gegenstand | Einzelergebnis laut eigenem Protokoll | Bewertung für QA-05 |
|---|---|---|---|
| **QA-02** | Retention & WORM (RET-03 Legal Hold, RET-04 WORM-Speicher) | Prüfung 1 (Legal Hold gegen Referenzdatensatz): **bestanden**, 7/7 PASS. Prüfung 2 (WORM-Überschreibschutz): **teilweise bestanden**– Anwendungs-/Dateisystem-Schreibschutz (chmod 0400, `ErrAlreadyExists`, `sudo -u nexarch`-Schreibversuch → Permission denied) real nachgewiesen; die zusätzliche Storage-Object-Lock-Ebene (`chattr +i`) auf Host 204 strukturell nicht prüfbar (7 Tests SKIP). Gesamtergebnis des eigenen Protokolls: "Teilweise bestanden". | **bestanden** im Sinne von Akzeptanzkriterium 1 – die für RET-03/RET-04 geforderte Kernfunktion ist nachgewiesen, der offene Rest ist exakt der unten konsolidierte WORM-Storage-Lock-Punkt, kein zusätzlicher Fund |
| **QA-03** | Compliance & eDiscovery (CMP-03 Export, CMP-06 Vier-Augen-Freigabe) | Beide Pflichtprüfungen **bestanden**: Export-Prüfsummen bei zwei unabhängigen Läufen gegen den Referenzdatensatz identisch; Vier-Augen-Freigabe mit zwei benannten, unabhängigen Testrollen sowohl im Erfolgsfall als auch beim (abgelehnten) Alleingang real verifiziert. Keine Regression in den bestehenden CMP-03/CMP-06-Pflichttests. | **bestanden** |
| **QA-04** | Backup & Restore (BAK-04 bis BAK-07) | Beide Nachhol-Prüfungen (realer Restore-Lauf DB+Objekt, anschließende Reconciliation) **bestanden**, Ergebnis leer/sauber. 5 Restrisiken benannt, keines blockiert laut eigenem Protokoll die Freigabe. Bereits mit Gegenzeichnung "Betreiber" versehen. | **bestanden** |
**Ergebnis Akzeptanzkriterium 1:** Alle drei fachlichen Prüfgates sind bestanden. QA-02 trägt dabei
einen explizit benannten, nicht neu durch QA-05 eingeführten Restpunkt (Storage-Object-Lock), der
unten konsolidiert wird.
## 2. Ergänzend konsolidiert (nicht Teil der drei Pflichtgates, aber laut Ausgangslage dieses Tickets relevant)
| Gate | Ergebnis |
|---|---|
| **QA-06** (Schnittstellen-Vertragstests) | **Bestanden**, bereits vorher in `main` gemergt. Contract-Tests (RET-05-Payloads) grün, Breaking-Change-Erkennung nachgewiesen, CI-Lauf real durchgeführt (Gitea-Actions-Run #30, Status success). |
| **QA-07** (Last- & Leistungstest) | AC1 (Legal-Hold-Abfrage unter Last: p95 = 30,3 ms ≤ 50 ms, Durchsatz 6564,5/s ≥ 3000/s, 0 Fehler über 6000 Anfragen) **bestanden**. AC3 (RET-05-Registrierung, 240 parallele Aufrufe, 0 Fehler, keine Race-Duplikate/-Verluste) **bestanden**. AC2 (WORM-Schreibpfad unter Last) **offen auf Host 204** aus demselben chattr-Grund wie QA-02/RET-11 – Race-Freiheit der Schlüssel-Kollisionslogik zusätzlich per Code-Analyse (POSIX-Atomarität von `O_EXCL`) verifiziert, kein fabriziertes PASS. |
## 3. Konsolidierte Restrisiken (Prüfung vor der Abnahme 2)
### 3.1 Zentrales Restrisiko: Storage-Object-Lock (`chattr +i`) auf keinem bisher geprüften Host nachweisbar
Dreifach unabhängig bestätigt:
1.`RET-11-PRUEFPROTOKOLL.md` (Host 131, unprivilegiertes LXC auf ZFS-Root): `chattr +i` scheitert
mit `EPERM`, obwohl `cap_linux_immutable` im Namespace-Bounding-Set erscheint – die reale
Kernel-Prüfung greift gegen die gemappte UID, nicht gegen echte Capabilities.
**Konsequenz:** Kein Code innerhalb des Containers — unabhängig von
Sprache, Implementierung oder gewähltem Dateisystem — kann
`CAP_LINUX_IMMUTABLE` im Host-Namespace erlangen. Das ist strukturell
unmöglich, solange der Container unprivilegiert bleibt. Das betrifft
nicht nur ZFS, sondern auch ext4/XFS/Btrfs, sobald sie unter einem
unprivilegierten Container gemountet sind.
**Korrektur an `RET-11-PRUEFPROTOKOLL.md`:** Die dortige
Handlungsempfehlung ("WORM-Basisverzeichnis MUSS auf ext2/3/4, XFS oder
Btrfs liegen, NICHT auf ZFS") ist unvollständig/irreführend — das
Dateisystem ist nicht der limitierende Faktor. Richtig ist: Das
WORM-Basisverzeichnis muss auf einem Host/Container liegen, der dem
Prozess REALE (nicht nur namensraum-gemappte) Root-Rechte gibt
(privilegierter Container, VM oder Bare-Metal) — das Dateisystem
darunter ist dabei zweitrangig, sofern es `chattr`/Ext2-Flags
grundsätzlich unterstützt (ext4/XFS/Btrfs tun das; ob modernes ZFS das
ebenfalls täte, ließ sich auf einem privilegierten Host nicht
verifizieren, ist aber plausibel, siehe TrueNAS-SCALE-Hinweise unten —
irrelevant, weil auf einem privilegierten Host ohnehin auch ZFS in
Frage käme, solange OpenZFS das Flag unterstützt).
## Geprüfte Alternativen (alle unzureichend oder im Container nicht verfügbar)
| # | Alternative | Befund |
|---|---|---|
| 1 | `zfs set readonly=on` (Dataset-Ebene) | `zfs`-CLI im Container **nicht vorhanden** (`which zfs` → leer, ZFS-Verwaltung ist beim unprivilegierten LXC-Container nicht delegiert). Selbst wenn verfügbar: Dataset-weit, nicht pro-Objekt — würde auch neue `Put`-Schreibvorgänge blockieren. Wer die Berechtigung hat, es zu setzen, hat auch die, es zurückzusetzen — dieselbe strukturelle Schwäche wie chmod/ACL (siehe unten). Infrastrukturoperation auf Proxmox-Host-Ebene, nicht im Container lösbar. |
| 2 | ZFS-Snapshots / `zfs hold` | Nur Nachweis-/Erkennungsmechanismus (Snapshot zeigt nachträglich, dass sich etwas geändert hat), verhindert aber NICHT das Schreiben/Löschen der aktiven Datei selbst — erfüllt nicht die Kernanforderung "Verhinderung", nur "Erkennung". `zfs`-CLI ohnehin nicht verfügbar. |
| 3 | POSIX-ACLs / Verzeichnis-Schreibschutz (wie in QA-02 als Ersatznachweis erprobt) | Strukturell geprüft per Bind-Mount-Readonly-Test: `mount -o remount,bind,ro` verhindert Schreiben, aber derselbe Account kann sofort `mount -o remount,bind,rw` erneut ausführen und weiterschreiben — **kein Schutz gegen denselben (ggf. kompromittierten) Service-Account**, nur gegen andere Accounts. Das ist dieselbe Einschränkung, die der Code-Kommentar in `wormstore.go` (Zeilen 5-15) bereits für `chmod 0400` korrekt benennt: Löschrecht hängt vom Verzeichnis ab, nicht von der Datei, und der schreibende Account braucht zwingend Verzeichnis-Schreibrecht, um neue Objekte abzulegen — er kann sich damit jederzeit selbst wieder Rechte einräumen. **Bestätigt: taugt nur als Ersatznachweis/Fallback, nicht als vollwertiger Ersatz für echte Storage-Object-Lock.** |
| 4 | Proxmox-Container-Feature (`features: mknod=1` o.ä.) damit `chattr +i` im Container greift | Aus dem Container heraus nicht konfigurierbar (Proxmox-Host-Ebene, kein Zugriff in diesem Auftrag). Fachlich ohnehin wirkungslos: `mknod=1` betrifft `CAP_MKNOD`, nicht `CAP_LINUX_IMMUTABLE`/User-Namespace-Mapping. Einzige wirksame Proxmox-seitige Abhilfe wäre, den Container als PRIVILEGIERT zu betreiben (`unprivileged: 0`) — das hebt die Isolation durch User-Namespace-Mapping komplett auf und ist eine bewusste Sicherheits-Tradeoff-Entscheidung auf Infrastruktur-Ebene, keine Code-Lösung. |
| 5 | Datei-Capability (`setcap cap_linux_immutable+ep <helper>`) auf einen Helper-Prozess innerhalb des Containers | Datei-Capabilities wirken nur innerhalb des EIGENEN User-Namespace des Containers — sie können keine Capability im übergeordneten (Host-)Namespace verleihen, gegen den die Kernel-Prüfung tatsächlich läuft. Kein Workaround möglich. |
| 6 | Verschachtelter User-Namespace (`unshare -U`) um "Eigentümer" des Dateisystems zu werden | Wirkt nicht: Das zugrunde liegende Superblock-Objekt (ZFS-Dataset, tmpfs-Mount) gehört dem HOST-User-Namespace (Ahn-Namespace), nicht dem Container. Verschachtelung erzeugt nur weitere Nachfahren-Namespaces, keiner davon wird dadurch zum Eigentümer eines bereits bestehenden, von außen gemounteten Dateisystems. |
| 7 | FUSE-basiertes eigenes WORM-Dateisystem (Durchsetzung rein in Userspace, ganz ohne `chattr`) | Technisch der einzige denkbare Weg, eine Immutable-Garantie komplett ohne `CAP_LINUX_IMMUTABLE` umzusetzen. Aber: `/dev/fuse` ist in diesem Container **nicht vorhanden** (`ls /dev/fuse` → "No such file or directory") — FUSE-Mounts sind nicht an den Container durchgereicht. Erfordert Proxmox-Host-Konfiguration (Device-Passthrough), nicht im Container lösbar. Selbst wenn verfügbar: kein "minimaler Fix in `wormstore.go`" mehr, sondern ein komplett neuer Dienst/eigenes Dateisystem — als mögliche ZUKÜNFTIGE Architekturoption vermerkt, nicht Teil dieses Nachtrags. |
| 8 | Externes echtes Object-Lock-Backend (z. B. MinIO/S3 mit aktiviertem Object Lock Compliance-Mode) statt lokalem Dateisystem | Einzige Option, die eine zu S3 Object Lock GLEICHWERTIGE Garantie ohne Abhängigkeit von Host-Kernel-Capabilities liefern würde (die Durchsetzung läge dann beim Storage-Backend-Prozess, nicht beim Kernel-Dateisystem-Flag des Containers). Das ist aber ein Architekturwechsel (neuer Storage-Treiber statt `wormstore`-Dateisystem-Pfad), kein Fix an `wormstore.go` — außerhalb des Scopes dieses Nachtrags, als Hinweis für eine mögliche künftige Kachel vermerkt. |
## Prüfung 4: `ImmutableLockSupported` als bestehende Absicherung
Der Code verfügt bereits über exakt den richtigen Schutzmechanismus für
genau diese Situation: `ImmutableLockSupported(dir)`
(`wormstore.go:237-251`) prüft aktiv, ob das Basisverzeichnis
`chattr +i` durchsetzt, und `Put` schlägt bei fehlgeschlagenem
`lockImmutable` fail-closed fehl (Datei wird wieder entfernt, kein
WORM-Objekt ohne echte Sperre). Das ist bereits das korrekte Verhalten
für eine Infrastrukturgrenze: lieber ehrlich ablehnen/überspringen, als
eine WORM-Garantie vorzutäuschen, die nicht existiert. Hier besteht
kein Nachbesserungsbedarf.
## Fazit
Die wiederholt in PR #309/#310/#311 dokumentierte `EPERM`-Meldung ist
**keine behebbare Code- oder Konfigurationslücke**, sondern eine vom
Linux-Kernel bewusst vorgesehene Sicherheitsgrenze: Ein unprivilegierter
Container kann das Immutable-Flag nicht setzen, unabhängig vom
darunterliegenden Dateisystem. Das wurde durch direkte Reproduktion
(`chattr`, roher Ioctl-Aufruf), Gegenprobe auf einer anderen
Dateisystemart (tmpfs) im selben Container und eine bestätigte
t.Fatalf("erwartet %d pruefsummen (ein objekt je referenzdatensatz-typ), hatte %d",len(referenceset.RetentionObjects()),len(checksums1))
}
if!reflect.DeepEqual(checksums1,checksums2){
t.Fatalf("pruefsummen zwischen zwei unabhaengigen exportlaeufen gegen denselben referenzdatensatz unterschiedlich: lauf1=%v lauf2=%v",checksums1,checksums2)
_,_=w.Write([]byte(`{"error":{"code":"payload_too_large","message":"Anfrage zu gross"}}`))
return
}
ifr.Body!=nil&&r.Body!=http.NoBody{
r.Body=http.MaxBytesReader(w,r.Body,max)
}
next.ServeHTTP(w,r)
})
}
Loaded 100 of 536 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.