Files
tessera-ctl/.planning/STATE.md
T
schalli 5c42c558c4
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 56s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m52s
docs(quick-260914-ku1): Kanaele und Versionsstempel abgeschlossen und verifiziert 8/8 — Zusammenfassung, Verifikation, Aktenstand
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-14 16:01:13 +02:00

98 KiB

gsd_state_version, milestone, current_phase, current_phase_name, status, stopped_at, last_updated, last_activity, last_activity_desc, state_head, progress, milestone_name
gsd_state_version milestone current_phase current_phase_name status stopped_at last_updated last_activity last_activity_desc state_head progress milestone_name
1.0 v1.2 17 eigene-ausschreibungs-quellen-je-nutzer verified 2026-09-14: Quick 260914-ku1 abgeschlossen (3 Commits cdb571c/9731501/ea6aa99 gepusht, CI-Lauf 297 success, :beta-Abbilder mit ea6aa99 beta); offen: Fehler-melden-Knopf, danach Erstfreigabe v1.0.0 (Zweig live + Tag), Server-Handgriffe durch den User 2026-09-14T13:53:23.837Z 2026-09-14 Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent ea6aa995b2
total_phases completed_phases total_plans completed_plans
17 15 83 82
Plattform-Berechtigungen

Project State

Project Reference

See: .planning/PROJECT.md (updated 2026-07-17)

Core value: Eine zentrale Plattform, in der beliebige Workflow-Tools als Module lizenziert, aktiviert und genutzt werden koennen -- ohne zwischen verschiedenen Anwendungen wechseln zu muessen. Current focus: Kein laufender Meilenstein — v1.2 ist abgeschlossen, ein neuer wurde noch nicht begonnen.

Current Position

Phase: 17 (eigene-ausschreibungs-quellen-je-nutzer) — VERIFIED / passed Plan: 3 of 3 Status: Phase abgeschlossen und im Browser gegengeprueft — bereit fuer /gsd-ship Last activity: 2026-09-14 - Zwei Auslieferungskanaele + Versionsstempel (260914-ku1, verifiziert 8/8); davor Etappe 3c (260914-eym) und WINDOWS #29 (260914-ebg). Naechster Schritt: Fehler-melden-Knopf, danach Zweig live + Tag v1.0.0

Progress: [██████████] 100%

Performance Metrics

Velocity:

  • Total plans completed: 2
  • Average duration: 12 min
  • Total execution time: 0.38 hours

By Phase:

Phase Plans Total Avg/Plan
01-foundation-portal-shell 2/3 23 min 12 min

Recent Trend:

  • Last 5 plans: 01-01 (16 min), 01-02 (7 min)
  • Trend: improving

Updated after each plan completion | Phase 02-authentication-multi-tenancy P02 | 8min | 3 tasks | 26 files | | Phase 02-authentication-multi-tenancy P03 | 5min | 2 tasks | 20 files | | Phase 05-dashboard-calendar P01 | 14min | 4 tasks | 28 files | | Phase 05-dashboard-calendar P02 | 4min | 4 tasks | 16 files | | Phase 05-dashboard-calendar P03 | 11min | 4 tasks | 14 files | | Phase 05-dashboard-calendar PP04 | 5min | 2 tasks | 11 files | | Phase 06-desktop-client-ci-cd P01 | 5min | 2 tasks | 13 files | | Phase 06 P03 | 3min | 3 tasks | 3 files | | Phase 07-dkv-fleet-module P03 | 5min | 4 tasks | 8 files | | Phase 07-dkv-fleet-module P04 | 7min | 3 tasks | 8 files | | Phase 07-dkv-fleet-module P05 | 8min | 3 tasks | 11 files | | Phase 07-dkv-fleet-module P06 | 5min | 2 tasks | 7 files | | Phase 09 P03 | 17 | 3 tasks | 6 files | | Phase 09-cert-manager-module P04 | 4 | 3 tasks | 5 files | | Phase 09-cert-manager-module P05 | 8 | 3 tasks | 6 files | | Phase 09 P06 | 7 | 3 tasks | 7 files | | Phase 10 P01 | 15min | 3 tasks | 4 files | | Phase 10 P02 | 20min | 3 tasks | 6 files | | Phase 10 P03 | 35min | 3 tasks | 9 files | | Phase 10 P04 | 25min | 3 tasks | 5 files | | Phase 10 P05 | 20min | 3 tasks | 5 files | | Phase 10 P06 | 3min | 2 tasks | 4 files | Per-Plan Metrics:

Plan Duration Tasks Files
Phase 11 P01 8min 3 tasks 11 files
Phase 11 P02 4min 3 tasks 12 files
Phase 11 P03 9min 4 tasks 12 files
Phase 11 P04 12min 2 tasks 5 files
Phase 11 P05 24min 3 tasks 15 files
Phase 11 P06 35min 3 tasks 12 files
Phase 12 P01 35min 3 tasks 7 files
Phase 12 P02 8min 3 tasks 5 files
Phase 12 P03 15min 2 tasks 3 files
Phase 12 P04 12min 3 tasks 10 files
Phase 13 P01 35min 3 tasks 6 files
Phase 13 P02 20min 2 tasks 4 files
Phase 13 P03 45min 3 tasks 5 files
Phase 13 P06 15min 2 tasks 5 files
Phase 13 P04 55min 3 tasks 6 files
Phase 13 P05 40min 2 tasks 4 files
Phase 14 P01 13min 2 tasks 10 files
Phase 14 P02 30min 3 tasks 21 files
Phase 14 P04 25min 2 tasks 6 files
Phase 14 P05 50min 3 tasks 19 files
Phase 15 P01 24min 3 tasks 10 files
Phase 15-modul-berechtigungen-gruppen-user-grants P02 11min 2 tasks 11 files
Phase 15 P05 9min 2 tasks 5 files
Phase 15 P03 32min 3 tasks 8 files
Phase 15 P06 35min 3 tasks 8 files
Phase 15 P07 30min 3 tasks 7 files
Phase 15 P08 30min 2 tasks 12 files
Phase 16 P01 34min 3 tasks 10 files
Phase 16-ad-gruppen-synchronisation P02 5min 3 tasks 5 files
Phase 16-ad-gruppen-synchronisation P03 12min 2 tasks 3 files
Phase 16-ad-gruppen-synchronisation P04 3min 3 tasks 5 files
Phase 16 P05 6min 2 tasks 4 files
Phase 17 P01 76min 3 tasks 12 files
Phase 17 P02 58min 3 tasks 14 files
Phase 17 P03 62min 3 tasks 13 files
Phase quick-260909-ipc P01 55min 3 tasks 8 files
Phase quick-260910-jab P01 70min 3 tasks 16 files
Phase quick-260910-krx P01 26min 3 tasks 7 files
Phase quick-260911-cwh P01 21min 3 tasks 7 files
Phase quick-260911-nke P01 1 Sitzung 3 tasks 27 files
Phase quick-260914-ebg P01 6min 3 tasks 4 files
Phase quick-260914-eym P01 1 Sitzung 3 tasks 29 files

Accumulated Context

Roadmap Evolution

  • Phase 17 added (2026-08-12): Eigene Ausschreibungs-Quellen je Nutzer. TenderEmailConfig (heute tenantId @unique) und TenderRssFeedSource (heute url @unique, plattformweit) wandern auf userId; die Rollenpruefung faellt fuer diese beiden Abschnitte weg, das Abrufintervall der oeffentlichen Quelle bleibt Admin-Sache. Ausschreibungsdaten bleiben plattform-global (D-03 aus Phase 10 unangetastet) — geaendert wird nur, wer Quellen einspeist, nicht wer Treffer sieht. Ausloeser: Backlog 2026-08-11-tender-radar-einstellungen-mischen-rollen.md; die urspruengliche Zustimmung zur gemeinsamen Konfiguration beruhte auf einer missverstaendlichen Erklaerung.
  • Phase 15 added (2026-08-04): Modul-Berechtigungen — Gruppen & User-Grants. Zweistufiger Modulzugriff (Mandanten-Aktivierung + Grants pro Gruppe/User), Gruppen mit optionaler AD-Bindung, default geschlossen, ADMIN/SUPER_ADMIN umgehen Grants, nur Zugriff an/aus. Startet Milestone v1.2 Plattform-Berechtigungen.

Decisions

Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work:

  • [Roadmap v1.1]: 5 phases (10-14) derived from 29 v1.1 requirements, standard granularity — DÖE-only MVP (10: ingestion, 11: filter/UI, 12: notifications) ships first as legally-clean demoable slice, then scraping adapters + cross-source dedup (13), then RSS + email-alert long tail (14)
  • [Roadmap v1.1]: Tender data modeled as platform-global (no tenantId on Tender) — only TenderSavedSearch/TenderMatch are tenant+user-scoped; deviates deliberately from the DKV per-tenant template
  • [Roadmap v1.1]: Scheduler must be poll-once-fan-out-many from Phase 10 onward — explicitly NOT the DKV findFirst() single-tenant pattern (documented pitfall)
  • [Roadmap v1.1]: Notification phase (12) requires an explicit matched-vs-notified state with backfill suppression to avoid first-activation email floods and duplicate digest+instant sends
  • [Roadmap v1.1]: INGEST-07 (vergabe24/aumass hard denylist) enforced in the adapter registry in Phase 13, not just documented
  • [Roadmap v1.1]: inbox/ module extraction (ImapProvider/ExchangeInboxProvider out of dkv/) scoped as a one-time prerequisite refactor inside Phase 14, not done earlier
  • [Roadmap]: 6 phases derived from 44 v1.0 requirements, standard granularity
  • [Roadmap]: Research recommends NestJS + Next.js + PostgreSQL RLS + Keycloak + Tauri stack
  • [01-01]: Used Traefik v2.11 instead of v3.4 due to Docker API version incompatibility on host
  • [01-01]: Traefik placed on frontend-net + backend-net for routing to both web and api services
  • [01-01]: API Dockerfile copies full monorepo node_modules structure for pnpm workspace compatibility
  • [01-02]: OKLCH color space for all design tokens (Tailwind v4 native, perceptually uniform)
  • [01-02]: Dark mode uses oklch(0.17 0.01 260) dark gray-blue for comfortable contrast with yellow primary
  • [01-02]: Cookie-based locale (NEXT_LOCALE) instead of URL routing for portal app
  • [01-02]: CSS custom property --current-sidebar-width for responsive main content margin
  • Phase ?: Route groups (auth)/(portal) for layout separation: auth pages standalone, portal pages wrapped in AppShell
  • Phase ?: Controller-level tenant isolation for ADMIN role as defense-in-depth alongside RLS
  • Phase ?: Remember-me controls cookie maxAge (30d session vs browser-session) not separate token type
  • Phase ?: react-grid-layout v2 uses dragConfig/resizeConfig instead of isDraggable/isResizable
  • [05-02]: SearchProvider defaults as constants merged with user DB rows (no seed migration)
  • [05-02]: useRef with explicit undefined initial value for React 19 strict TypeScript
  • [05-03]: DAVClient class constructor instead of createDAVClient factory (tsdav v2 type compatibility)
  • [05-03]: Dynamic imports for ews-javascript-api and @microsoft/microsoft-graph-client (lazy-load)
  • [05-03]: In-memory Map cache with 5-min TTL for calendar events (Redis not needed at current scale)
  • [05-03]: ews-javascript-api imported as any (no TypeScript definitions available)
  • Phase ?: Optimistic UI for calendar visibility toggle — reverts on API error
  • Phase ?: Auto-run testSource after adding new calendar source for immediate feedback
  • Phase ?: URL constructor for client-side https-only validation (T-05-14)
  • Phase ?: StoreExt trait import required for app.store() in Tauri 2.x
  • Phase ?: frontendDist=../src local page, navigate() for runtime URL override
  • Phase ?: CSP connect-src wildcard for configurable server URL (D-02)
  • Phase ?: Plain docker compose build statt build-push-action (Gitea JWT Pitfall 4)
  • Phase ?: Ephemeral runner mode (GITEA_RUNNER_EPHEMERAL=1) fuer Credential-Revokation pro Job
  • Phase ?: Separate docker-compose.ci.yml fuer opt-in CI-Infrastruktur
  • [07-01]: DKV PDF uses two extraction formats: single-tx (tab-separated) vs multi-tx (columnar) — both handled in DkvParserService
  • [07-01]: Research Pattern 4 regex replaced with empirical dual-format tab/columnar parser after testing against real invoice.pdf
  • [07-01]: CalendarCryptoService exported from CalendarModule for DKV credential encryption reuse
  • [07-02]: Max attachment size 25MB enforced in both ImapProvider and ExchangeInboxProvider before buffering (T-07-05)
  • [07-02]: ExchangeInboxProvider uses WellKnownFolderName.Inbox + FindItems (not FindAppointments — email vs calendar EWS API)
  • [07-02]: export type {} required for type-only re-exports under isolatedModules TypeScript setting
  • [07-03]: SettingsService.getStartupSmtpConfig uses findFirst (tenant-agnostic) for MailModule startup transport
  • [07-03]: MailModule forRootAsync factory priority: DB SmtpConfig → MAIL_* env → TESSERA_SMTP_* env → localhost:1025 fallback
  • [07-03]: DkvMailService injects SettingsService (not PrismaService directly) to reuse decryption logic
  • [07-03]: user-files/ path resolved via path.resolve(__dirname, 4 levels up) from apps/api/dist/dkv/ to monorepo root
  • [07-03]: Export prune sorted by mtime ascending (oldest first), delete all beyond last 10
  • [07-04]: DkvScheduler v1 uses findFirst() — single-tenant; multi-tenant scheduling deferred
  • [07-04]: Circular dep DkvService<->DkvScheduler avoided via controller coordination after PUT config
  • [07-04]: CronJob resolved via require() workaround (pnpm strict isolation: transitive dep)
  • [07-04]: rechnungsnummer from email subject regex /d{2}-d{9}-d{3}/; fallback=email-{uid}
  • [07-05]: refreshKey lift: parent increments on checkNow success; InvoiceHistoryTable reruns useEffect
  • [07-05]: onItemsLoaded callback: InvoiceHistoryTable notifies parent; parent passes items to ExportFileList (avoids second fetch)
  • [07-05]: Password blank on load: configToForm() always sets password=''; hasPassword boolean drives UX hint only
  • [07-05]: CsvImportButton replace: two-step inline confirm (not full modal); accept=".csv" client-side guard
  • Phase ?: splitCerts PEM path reuses parsePemChain; P7B sniffs first bytes
  • Phase ?: splitCerts format crt comparison removed — detectFormat returns pem|der|pfx|p7b only
  • [Phase 10]: Tender ist plattform-global (D-03): kein tenantId, keine RLS/forTenant() — Verhindert versehentliches Ausblenden globaler Daten fuer einen zweiten Mandanten
  • [Phase 10-02]: category: 'procurement' fuer Ausschreibungs-Radar im Marketplace gewaehlt (freies kebab-case, keine Enum-Beschraenkung)
  • [Phase 10-02]: Singleton doe-opendata Poll-Config wird direkt in TendersModule.onModuleInit() upserted, isActive:true per Default (D-04)
  • [Phase 10-02]: Platzhalter-Seite tender-radar/page.tsx nutzt hartkodierten deutschen Text statt next-intl (volle i18n ist CONFIG-03, Phase 14)
  • [Phase 10-03]: Real DÖE fixtures live-captured (pubDay=2026-07-19), not synthetic — 8 notices spanning all D-02 tag classes
  • [Phase 10-03]: sourceNoticeId = OCDS release.id (stable, no version suffix), not the zip entry filename
  • [Phase 10-03]: eForms-DE XML primary for deadlineAt/estimatedValue/procedureType; OCDS primary for ocid/buyerName/title/cpvCodes/region/plz
  • [Phase 10-03]: bundesland left null this plan — NUTS-to-Bundesland mapping deferred to Phase 11 filter UI
  • Phase ?: Poll-once-fan-out-many scheduler: single named cron job, no tenant parameter — deliberately drops DKV's activeTenantId/findFirst per-tenant framing (Pitfall D)
  • Phase ?: SCHEMA-02 change detection implemented via prisma.tender.upsert({ where: { dedupKey } }) — identical notice never duplicates, changed contentHash updates in place
  • Phase ?: D-05 retention as two-phase updateMany/deleteMany with deadlineAt:{lt} filters — null-deadline rows structurally excluded, never auto-expired
  • Phase ?: TendersController talks to PrismaService directly (no intermediate service layer) — source-config upsert and global read are simple enough for this plan's scope
  • Phase ?: Comment wording avoids the literal tenantId token in tenders.controller.ts to prevent false-positive grep-gate failures (same pattern as Plan 10-04)
  • Phase ?: TenderQueryDto.status defaults to active at the controller call site, not baked into the DTO, mirroring DkvController's page/limit default-at-usage pattern
  • Phase ?: Admin-Settings-Formular fuer den DOE-Poll (Intervall 5-1440, Aktiv-Toggle) spiegelt InboxConfigForm ohne Credential-Felder, da die DOE-Quelle keine Auth-Oberflaeche hat
  • [Phase 10]: Keine module-loader-Whitelist noetig fuer settings/page.tsx (verschachtelte Route unter bereits whitelisteter tender-radar-Seite)
  • Phase ?: estimatedValue kommt als String (Prisma Decimal) im JSON-Response — formatValue() im Frontend prüft explizit auf null/NaN statt zu koerzieren
  • Phase ?: openOnly/includeNullValue Default-Semantik lebt im Builder, nicht im DTO
  • Phase ?: deadlineFrom/deadlineTo URL-Param-Namen sind 1:1 identisch zu TenderQueryDto-Feldnamen fuer den Saved-Search-Vertrag aus Plan 11-06
  • Phase ?: bundesland-Filter matcht exakt gegen die befüllte, indexierte Spalte (statt region-startsWith); region bleibt als eigenständiger Präfixfilter erhalten.
  • Phase ?: BUNDESLAND_OPTIONS im Web als kleine Konstante gespiegelt (kein Shared-Package) — web nutzt @tessera/shared nicht, 16-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit.
  • Phase ?: CPV-Divisions-Kurzkatalog (2-stellig, ~45 Einträge) statt EU-Vollkatalog; hasSome-Match gegen precomputed cpvDivisions-Spalte statt Raw-SQL-Präfix-Match
  • Phase ?: cpv-URL-Param als wiederholter Key (?cpv=45&cpv=71) statt Komma-Join; DTO normalisiert Einzelwert per @Transform zu Array
  • Phase ?: TenderDetail fetcht selbstständig via getTender(tenderId) im useEffect (Muster SourceConfigForm); page.tsx bleibt reiner ?tender-Param-Reader
  • Phase ?: ResultsList.tsx modifiziert (nicht im Plan gelistet) — Rule 3: Zeilen-Klick-Handler war notwendig, um den Plan-eigenen key_link/Done-Kriterium zu erfüllen
  • Phase ?: TenderTriage (neu, per-user, kein forTenant/RLS) + favOnly-Sentinel-ID 'none' für garantierten Zero-Match statt versehentlich ungefilterter Liste
  • Phase ?: TenderSavedSearch hat keinen Tender-FK — speichert nur Filterkriterien, unkritisch bei Retention.
  • Phase ?: page/tender-URL-Params bewusst aus dem Suchprofil-Payload ausgeschlossen (Navigations-/View-State, kein Filter-State).
  • Phase ?: Single notifiedAt field (not two per-channel timestamps) as the matched-vs-notified eligibility gate (12-01, D-06)
  • Phase ?: Delta-only matching (no backfill/suppression table) structurally prevents backfill-flood for new saved-search profiles (12-01, D-07)
  • Phase ?: TenderMailService swallows missing-SmtpConfig and send-failure into a single boolean (never throws) so the digest cron gets one clean success signal for stamping notifiedAt
  • Phase ?: TenderDigestScheduler.runDigest(now) takes an injectable clock parameter for testable Monday-only weekly-digest gating
  • Phase ?: Instant-Dispatch filtert die bereits geladenen savedSearches (kein zweiter Query); notifiedAt/channel='instant' nur nach Erfolg gestempelt — identisches Gate wie Digest (D-06)
  • Phase ?: Digest-interval selector inline in settings/page.tsx (already 'use client'); instantAlert toggle uses plain checkbox for chip-based SavedSearchBar UI
  • Phase ?: SCHEMA-03 fingerprint: title+buyer dominant, CPV division, value-bucket, deadline-day, sha256; dedupKey untouched, fingerprint additive
  • Phase ?: One-time TS backfill scripts run via compiled dist/ output (not raw .ts execution) to keep tsc --noEmit clean
  • Phase ?: SourceRegistry.register() throws DeniedPortalError for any portal in DENYLISTED_PORTALS (vergabe24, aumass), enforced at DI-registration time not just documented (INGEST-07)
  • Phase ?: TenderDedupService tier-2 (source:noticeId) match is unconditional (not gated by dedupActive) — idempotent same-source re-poll must work even with a single active source
  • Phase ?: Fingerprint always computed on tender.create regardless of dedupActive, so DÖE-only tenders become fingerprint-matchable the instant a 2nd source activates without further backfill
  • Phase ?: 13-06: portalLabel()-Map bleibt lokal in TenderDetail.tsx (nicht geteilt) — einziger Consumer bisher; Fallback auf sourceUrl-Block bei fehlendem/leerem sources[].
  • Phase ?: 13-04: cheerio (nicht node-html-parser) fuer NetServer-HTML-Parsing gewaehlt — Legitimacy-Gate flaggte node-html-parser als 'too-new' (Fehlmessung: Latest-Version-Datum statt Package-Alter); cheerio bestand das Gate sauber, Human-Approval eingeholt
  • Phase ?: cosinex/DTVP-Selektoren voll befuellt statt needs-JS-deferred (D-01) — Trefferliste ist server-gerendert, live geprueft 2026-07-23
  • Phase ?: cosinex/DTVP liefert echte Notice-Deep-Links (pid=) — besser als NetServer-Fallback (Such-URL); Rule-1-Fix: arrayBuffer()+TextDecoder('iso-8859-1') statt res.text(), da Portal ISO-8859-1 mit rohen Latin-1-Bytes sendet
  • Phase ?: [quick-260723-e7i]: TenderNormalizerService dispatcht per sourceType (normalizeDoe/normalizeBag) mit geteiltem assemble()-Tail — schliesst den Normalizer-Gap aus 13-VERIFICATION.md fuer NetServer/Cosinex-Bag-Records
  • Phase ?: 14-01: DKV inbox providers moved verbatim into shared apps/api/src/inbox/ module; dkv.types.ts re-exports InboxConfig/InboxAttachment/InboxEmail so no DKV consumer import changed (D-01)
  • Phase ?: 14-01: fetchMessages() added as a sibling method (findBodyParts/getItemBodySoap) on both providers without touching fetchPdfAttachments (D-02); DKV not switched to it
  • Phase ?: 14-01: httpntlm loaded via raw require() bypasses vi.mock — tests seed require.cache with a stub before dynamically importing the provider
  • Phase ?: 14-02: fast-xml-parser does not decode numeric HTML entities (Ü) — added explicit decodeNumericEntities() in RssAdapter so service.bund.de titles render correctly
  • Phase ?: 14-02: TenderRssFeedSource save-time hostname/SSRF guard is a SEPARATE enforcement point from the code-level SourceRegistry denylist (RSS feed URLs are runtime admin input, not covered by the DI-boot-time gate)
  • Phase ?: 14-02: TenderSourcePollConfig.pollGranularity ('day'|'tick') added — 'day' sources keep the byte-unchanged lastIngestedDay gate, 'tick' sources (rss) fetch every active scheduler tick (D-15)
  • Phase ?: 14-02: seeded service.bund.de active-by-default RSS feed; zero subreport-elvis rows (no single canonical URL, admin adds relevant municipality feeds)
  • Phase ?: 14-03: D-13 read filter fails CLOSED for an unresolved requesting tenant (no auth context) — only global tenders visible, never a private-tenant leak
  • Phase ?: 14-03: email-alert TenderSourcePollConfig seeded isActive=false (no safe default mailbox, unlike RSS's service.bund.de) — framework-ready-activation-deferred
  • Phase ?: PORTAL_URLS typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so the compiler enforces a URL for every denylisted portal (no re-declared set, no silent gap)
  • Phase ?: CoverageBanner's denylist block is independent of the onlyDoe coverage-note condition — component renders when either block has content, not gated behind the DOE-only check
  • Phase ?: 14-05: tenderRadar i18n namespace added; Bundesland/CPV filter option values stay canonical German for backend compatibility, only labels translated; portal display slugs left untranslated as proper nouns
  • Phase ?: [260728-lih]: DELIBERATE back-compat break — empty groupFilterDns now means 'sync nothing' (was 'import everyone under baseDn'); early-return guard in syncUsersForTenant runs before search/deactivation so an empty selection never mass-deactivates existing LDAP users
  • Phase ?: [260729-d3k]: syncUsersForTenant no-op guard re-keyed from empty groupFilterDns to empty parsed base-DN list — Base-DN(s) are now the sync scope, groupFilterDns is an optional extra restriction (ou= = extra bases, group DN = memberOf constraint)
  • Phase ?: [15-01]: D-01/D-05/D-06/D-02 wie in 15-CONTEXT.md gesperrt umgesetzt (Nutzer-Checkpoint mit 'proceed' bestaetigt)
  • Phase ?: [15-01]: RLS fuer Group/GroupMembership/ModuleGrant aktiviert (T-15-11) statt sie wie Tender* RLS-frei zu lassen
  • Phase ?: [15-02]: isDefault:true läuft in this.prisma.$transaction([updateMany, update]); partieller Unique-Index aus 15-01 bleibt Sicherheitsnetz
  • Phase ?: [15-02]: remove() fängt zusätzlich P2025 ab (NotFoundException statt unbehandeltem 500) — Rule 2, für Concurrency-Anforderung aus must_haves
  • Phase ?: [15-02]: UserService.create ist die einzige Codestelle für D-11/D-12 — LdapService erbt die Regel ohne eigene Kopie (ldap.service.ts unverändert)
  • Phase ?: [15-05]: WIDGET_MODULE_MAP bleibt am Ende dieser Phase bewusst leer (D-22) — kein neuer Widget-Typ, keine Schemaänderung, nur die Filtermechanik
  • Phase ?: [15-05]: vi.hoisted() für die je-Testfall mutierbare WIDGET_MODULE_MAP-Mock-Referenz — vi.mock wird an den Dateianfang gehoben, ein normaler top-level const wäre zur Factory-Ausführungszeit noch nicht initialisiert
  • Phase ?: [15-03]: assertTargetBelongsToTenant als eigenständige Cross-Tenant-Prüfung eines referenzierten Fremdobjekts vor jedem Grant-Insert (T-15-01), kein Vorbild im Bestandscode
  • Phase ?: [15-03]: Kein Import von ModuleRegistryModule in GroupsModule — ModuleGrantsService injiziert ausschließlich PrismaService
  • Phase ?: [15-03]: getCatalogFlags liefert Map nur für aktive Module, Controller mappt fehlenden Eintrag auf beide Flags false
  • Phase ?: [15-06]: Task 2/3-Split von page.tsx haelt jeden Task-Commit fuer sich buildbar (Task 2 ohne Import der erst in Task 3 entstehenden Komponenten)
  • Phase ?: [15-06]: Gruppen-Erstellung mit sofortiger AD-Bindung laeuft zweistufig (POST /groups, dann PATCH ldapDn), weil CreateGroupDto aus 15-02 nur name entgegennimmt
  • Phase ?: [15-06]: matrixCheckboxLabel/directCheckboxLabel (Wave-4-Schluessel) als next-intl-ICU-select mit granted-Parameter modelliert, ein Schluessel bedient freigeben/entziehen
  • Phase ?: [15-07]: aria-label des Grant-Checkboxes beschreibt die vom Klick ausgeloeste Aktion (granted: String(!isGranted)), nicht den aktuellen Haekchen-Zustand
  • Phase ?: [15-07]: UserAccessModal leitet Gruppenmitgliedschafts-Chips ausschliesslich aus der Vereinigung aller viaGroups-Namen von GET /module-grants/users/:userId ab, kein zweiter Endpoint
  • Phase ?: [15-07]: ActivateModuleDialog ruft onSuccess bereits nach dem erfolgreichen activate-Call auf, unabhaengig vom Ausgang des nachfolgenden module-grants-Calls
  • Phase ?: [15-08]: isAdmin-Gate auf /marketplace und /marketplace/[slug] entfernt (D-08: Katalog bleibt Schaufenster fuer jeden authentifizierten Benutzer) — isAdmin gated jetzt nur noch die Aktivieren/Deaktivieren-Aktion (canManage-Prop)
  • Phase ?: [15-08]: MarketplaceCard-Klick delegiert an getrennte onOpenDetail/onLockedClick-Callback-Props statt eigener Router-Logik in der Karte — bleibt praesentational und ueber vi.fn() testbar
  • Phase ?: [quick-260805-fok]: ensureDefaultGroup-Waechter prueft ausschliesslich group.count === 0, nie die fehlende isDefault-Markierung (D-13); Reparatur laeuft als zweiter sequenzieller await-Schritt in AdminSeedService.onApplicationBootstrap statt als eigener Hook in GroupsModule (Ordering-Falle wie in tender-scheduler.service.ts)
  • Phase ?: Checkpoint 1 (16-01 Task 1): approve-both — Group.internalName + ldapObjectGuid + Unique-Index in einer Migration, freigegeben 2026-08-06
  • Phase ?: Migrationsverfahren angepasst: prisma migrate dev verweigert nicht-interaktive Shell — Ersatz via migrate diff + Handdatei + migrate deploy, inkl. Baseline der 24 Altmigrationen per migrate resolve --applied
  • Phase ?: [16-02]: reassignDefaultBeforeDelete() nutzt bewusst nicht findOwned() — eigenes still-false-Muster fuer Batch-Sync-Laeufe (D-06), kein NotFoundException-Abbruch
  • Phase ?: [16-02]: Namenssperre fuer importierte Gruppen (D-03) ist eine Backend-Invariante in GroupsService.update() (BadRequestException), nicht nur ein UI-Disable
  • Phase ?: [16-02]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] — dieser Plan liefert nur den Backend-Teil, das Requirement schliesst erst mit Plan 16-05
  • Phase ?: [16-03]: syncBoundGroupsForTenant() als eigene, in Task 1 noch unverdrahtete Methode gebaut; Task 2 liefert ausschliesslich die Verdrahtung als Schritt 5a vor 5b samt Call-Order-Test — Reihenfolge ist die zentrale Korrektheitsbedingung der Phase
  • Phase ?: [16-03]: A1/A2-Live-Pruefung gegen ViCoTest nicht durchfuehrbar (kein erreichbares AD in dieser Sandbox) — als WINDOWS.md #4 (unrun-verify) festgehalten, negatives Ergebnis ist Stopp-Grund fuer die D-05-Loeschsemantik
  • Phase ?: [16-03]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] — schliesst erst mit Plan 16-05
  • Phase ?: [16-04]: Group.internalName vorgezogen von Task 2 nach Task 1 (Rule 3) - GroupFormModal.tsx kompiliert sonst nicht
  • Phase ?: [16-04]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] - schliesst erst mit Plan 16-05
  • Phase ?: [16-05]: PERM-02 in REQUIREMENTS.md auf [x] gesetzt - alle 5 Phase-16-Erfolgskriterien code-vollstaendig ueber 16-01..16-03; dieser Plan liefert die Sichtbarkeitsschicht (Sync-Bericht D-05/D-06) und die dritte D-04-Anzeigestelle (Freigabe-Matrix)
  • Phase ?: [16-05]: A1/A2-Live-Pruefung gegen echtes AD (WINDOWS.md #4) bleibt trotz PERM-02-Abschluss offen - Korrektheitsannahme unter SC-3/SC-4, kein eigenes Erfolgskriterium; negatives Ergebnis waere Stopp-Grund fuer D-05-Loeschsemantik
  • Phase ?: [17-01]: Checkpoint 1 (gate=blocking) 'weiter' — beide Datenbank-Umbauten der Phase freigegeben, gestuetzt auf gemessene 0 Bestandszeilen (lokal + alpha)
  • Phase ?: [17-01]: TenderEmailConfig.userId @unique ersetzt tenantId @unique (D-01); tenantId bleibt denormalisiert, wird auf create UND update mitgeschrieben
  • Phase ?: [17-01]: email-config-Routen von @Roles(ADMIN,SUPER_ADMIN) auf @UseModule('tender-radar') umgestellt — Postfach ist jetzt Nutzereinstellung (D-01, T-17-06 accept)
  • Phase ?: [17-01]: eigene Seite /modules/tender-radar/my-sources statt Erweiterung von /settings/general/account (D-01 offener Punkt 4)
  • Phase ?: [17-02]: Checkpoint-Freigabe aus 17-01 deckte diese Migration bereits ab, kein zweiter Halt (gemessene 1 Bestandszeile blieb plattformweit)
  • Phase ?: [17-02]: Startbestueckung (tenders.module.ts) vorgezogen aus Task 3 nach Task 1 (Rule 3) - find-then-create statt upsert-on-url, Prismas Compound-Unique-Typ verlangt userId als Pflicht-String
  • Phase ?: [17-02]: seedServiceBundRssFeed() aus TendersModule.onModuleInit extrahiert (tenders.seed.ts), damit die Bestueckungs-Idempotenz echten Produktivcode testet
  • Phase ?: [17-02]: DELETE /rss-feeds/:feedId von @Roles auf @UseModule umgestellt - Besitzpruefung im Dienst ersetzt die Rollenpruefung vollstaendig (T-17-07)
  • Phase ?: [17-03]: RssFeedListForm scope-Prop (personal/platform) bedient beide Seiten; Berechtigung entscheidet der Server (T-17-08)
  • Phase ?: [17-03]: createRssFeed-Antwort traegt kein isPlatformWide (nur GET mappt es) — Komponente leitet es lokal aus dem verwendeten scope ab (Rule 1)
  • Phase ?: [17-03]: Anzeige-Rollenpruefung auf settings/page.tsx ueber useAuthStore (unbekannt/erlaubt/verweigert); verbindliche Pruefung bleibt serverseitig
  • Phase ?: [17-03]: REQUIREMENTS.md SRC-01..05 nachtraeglich ergaenzt — Luecke aus 17-01/17-02, dort schon in SUMMARY-Frontmatter gefuehrt
  • [Phase 17]: [260909-cx0]: Benanntes Volume user-files statt Bind-Mount (uid-1001-Eigentuemerschaft aus dem Image)
  • [Phase 17]: [260909-ipc]: resolveEmailForWrite() bleibt dauerhaft ungebunden (Befund A, T-IPC-04) — email/username sind plattformweit @unique
  • [Phase 17]: [260909-ipc]: getAllActiveConfigs()/onApplicationBootstrap() bleiben dauerhaft ungebunden (Befund B) — Klasse von (ldap-config.service.ts, ldapConfig) korrigiert auf beides
  • [Phase 17]: [260909-ipc]: Standardgruppen-Uebergabe (groups.service.ts) bewusst nicht angefasst — Reihenfolgebedingung fuer Etappe 4
  • [Phase 17]: 260910-krx: Bereich dashboard vollstaendig umgestellt — 12 gebunden, 1 begruendet ungebunden (Modulkatalog); getLayout/saveLayout gemeinsam gebunden; saveLayout uebersetzt PrismaClientUnknownRequestError (nicht P2002) in deutsche Konfliktmeldung; WINDOWS #25 fuer die beweisvernichtende Schleife offen angelegt
  • [Phase 17]: [quick-260911-cwh]: Bereich calendar Etappe 2 gebunden — Cache-Schluessel bleibt ohne Mandantenanteil (User.id ist plattformweit eindeutige UUID, Etappe-3-Entscheidung (1) betrifft nur username/email); keine neue Fehleruebersetzung fuer Besitzpruefungen noetig (Wettlauf-Fall wirft P2025, strukturell unerreichbar); refreshCacheInBackground zaehlt nicht als sechster Hintergrunddienst-Fall
  • [Phase 17]: 260911-nke: forTenant(prisma, tenantId, userId?) — optionaler dritter Parameter statt Schwesterhelfer, IS-NULL-OR-Form in den Regeln der zehn persoenlichen Tabellen, sechs Loch-Pruefungen umgedreht
  • [Phase 17]: [quick-260914-ebg]: Zielrollen-Riegel als eigenstaendige Pruefung nach der Mandantengrenze in UserController.update()/remove() eingezogen (Vorlage AuthService.adminResetPassword, T-FH9-04) — WINDOWS #29 geschlossen
  • [Phase 17]: [quick-260914-eym]: forSystem(prisma) als Schwesterhelfer (eigene Detektor-Erkennungsform, Umkehrung der 3b-Begruendung); system_read_policy FOR SELECT auf fuenf Tabellen, SmtpConfig nicht (Mail-Startpfad entfernt, Transport je Versand nach Mandant); DKV-Planer Auftrag je Mandant (promote); Single-Flight-Riegel bleibt prozessweit -> WINDOWS #37

Pitfalls & Anti-Patterns

Lehren aus abgeschlossenen Sitzungen. Gerettet aus den Checkpoint-Dateien (.continue-here.md), die am 2026-09-07 entfernt wurden.

Muster Beschreibung Schwere Vermeidung
Tautologischer Test Der Test der objectGUID-Suche baute seinen Erwartungswert mit derselben Hilfsfunktion, die der Produktionscode benutzte. Er bestaetigte damit, dass die Funktion zu sich selbst passt — nicht, dass ein Verzeichnis den Filter versteht. Der Fehler ueberlebte Review und 500+ Unit-Tests und haette beim ersten echten Sync alle AD-gebundenen Gruppen geloescht. blocking Tests gegen externe Systeme muessen die Form des Aufrufs pruefen (Buffer statt String, Objekt statt interpoliertem Text), nicht seinen mit Produktionscode erzeugten Inhalt. Siehe ldap.service.spec.ts, die beiden Tests am Ende des syncBoundGroupsForTenant-Blocks.
HTTP 200 als Funktionsbeleg oeffentlichevergabe.de/ui/... ist eine Single-Page-App und antwortet auf JEDE Kennung mit 200 und identischen 1309 Bytes, auch auf NONSENSE123. Ein Statuscode-Test haette "funktioniert" gemeldet. advisory Bei SPA-Zielen den gerenderten Inhalt pruefen (Playwright), nie den Statuscode.
Image-Datum als Aktualitaets-Beleg Das Web-Image trug den 7. August und sah veraltet aus. Tatsaechlich hatten sich die Web-Quellen seither nicht geaendert; Docker-Layer-Caching erzeugt ein bit-identisches Image mit altem Erstellungsdatum. advisory Vor einer Aussage ueber Rueckstand git log -- apps/web pruefen, nicht das Image-Datum.
Server-Compose ist kein Checkout /opt/tessera ist keine Git-Arbeitskopie. Aenderungen an Compose-Dateien im Repository kommen dort nie an; der Deploy holt nur Images. blocking Aenderungen an docker-compose*.yml wirken NICHT auf alpha. Was dort gelten soll, muss zusaetzlich in /opt/tessera/docker-compose.yml eingetragen werden (erlaubt, mit Sicherung). Siehe Backlog 2026-08-11-compose-datei-auf-server-driftet.md.
Stale live container maskiert Fertigstellung Code, Migration und Tests waren gruen, aber der laufende tessera-ctl-api-1 lief auf dem Image von vor der Phase. Niemand hatte je einen echten HTTP-Aufruf gegen die neuen Routen gesehen. Das war kein Code-Defekt, sondern eine unbeobachtete Fertigstellung. advisory Container neu bauen (docker compose up -d --build api web), bevor ein "Tests gruen"-Zustand als nutzersichtbar fertig gilt. Gilt fuer die lokale Entwicklungsmaschine; auf dem Testserver macht der User das selbst.

Infrastruktur-Stand (alpha, Stand 2026-08-11)

Gerettet aus .continue-here.md. Relevant fuer die noch offenen Live-Tests.

  • alpha (192.168.13.12, https://alpha.tessera.ctl.de): API-Image mit allen Fixes; Migrationen 20260811120000_doe_notice_url_backfill und 20260811140000_encrypt_ldap_bind_password angewandt.
  • /opt/tessera/.env: traegt TESSERA_ENCRYPTION_KEY UND den alten CALENDAR_ENCRYPTION_KEY mit gleichem Wert. Sicherung .env.bak.20260811. Die alte Zeile kann weg, sobald kein Rueckfall auf aeltere Images mehr denkbar ist.
  • /opt/tessera/docker-compose.yml: von Hand um die neue Variable ergaenzt, Sicherung docker-compose.yml.bak.20260811. Driftet vom Repository ab.
  • Testdaten auf alpha: 405 Benutzer und die AD-gebundene Gruppe Claude_VT (9 Mitglieder) aus dem Sync vom 2026-08-11. Der User wirft vor dem Go-live ohnehin alles raus.
  • Deploy-Regel: pull/up/restart macht der User. Konfigurationsdateien auf dem Server darf Claude bearbeiten (seit 2026-08-11), vorher sichern.
  • Kein AD-Schreibzugriff (Stand 2026-08-11): Phase-16-UAT 1/3/4 waren deshalb nicht ausfuehrbar und wurden als dokumentierte Annahme geschlossen. Betrifft direkt den offenen Live-Test WINDOWS #4 — dessen Annahme A1 (objectGUID uebersteht Umbenennung) verlangt eine echte Umbenennung im Verzeichnis, also Schreibrechte. Vor dem Live-Test klaeren, ob diese inzwischen vorliegen.

Pending Todos

None yet.

Blockers/Concerns

  • [Roadmap v1.1]: DÖE OpenData API pagination/rate-limit parameters unverified (Swagger UI is JS-rendered) — resolve via a live API call during Phase 10 planning, not assumed from docs.
  • [Roadmap v1.1]: Whether AI-AG NetServer / cosinex VMP search pages require JS rendering is unverified — needs a Phase 13 start-of-phase spike before committing to playwright.
  • Phase 14 Plan 03 (14-03): Task 4 human-verify OPEN — needs a real portal-alert mailbox (incl. Exchange/EWS live path) from the operator before INGEST-05's Exchange path is production-ready. Tasks 1-3 complete and committed (4d6fbb1, 8983231, 1be6b15, 48e1252); API 387/387, web 144/144 green.
  • Phase 15 Plan 06 (15-06): manueller Browser-Durchklick nicht ausgefuehrt — ERLEDIGT 2026-09-07, Gegenprobe WINDOWS.md unrun-verify #1 nachgeholt und bestanden
  • [260909-cx0] WINDOWS #17 (user-files-Volume) bleibt offen: Repository-Fix committet, aber /opt/tessera/docker-compose.yml auf alpha weicht ab und muss vom Nutzer manuell um dieselben zwei Zeilen ergaenzt werden (vorher sichern), danach Container neu erstellen und Ledger schliessen (gsd-tools windows fixed 17)

Quick Tasks Completed

# Description Date Commit Directory
260630-gbh User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen 2026-06-30 merge 260630-gbh-user-settings-passwort-ndern-nur-non-lda
260701-abc Fix i18n: marketplace.accessDenied + calendar form hardcoded EN strings 2026-07-01 e5b76b7 260701-abc-i18n-missing-keys
260707-csw LDAP AD Anbindung: Zugangsdaten aus XWiki vorbefuellen und Import-Filter fuer Benutzer/Gruppen 2026-07-07 a5c500d 260707-csw-ldap-ad-anbindung-zugangsdaten-aus-xwiki
260707-lgh Favoriten-Widget: Icon-Proxy fuer Cross-Origin-Resource-Policy-Seiten (claude.ai) 2026-07-07 f06a2ff 260707-lgh-favoriten-widget-icon-proxy-fuer-cross-o
260708-cuc Fix: FavoriteLink-Tabelle fehlt in Prod-DB, nie als Migration committed (500 auf GET /favorites) 2026-07-08 afef9b2 260708-cuc-fix-favoritelink-tabelle-fehlt-in-prod-d
260708-rev LDAP: CTL-spezifisches AD-Prefill entfernt (Multi-Tenant, "das war nie das Ziel") 2026-07-08 8e8305c (direct)
260708-tst LDAP: Verbindung testen vor dem Speichern einer Config moeglich 2026-07-08 39aa4bf (direct)
260709-abd LDAP: anonymous bind (bindDn/bindPassword optional, Schema nullable + Migration) 2026-07-09 010aceb (direct)
260709-ciu Auth: Benutzernamen ueberall case-insensitive (Login, Seed, LDAP-Sync + Daten-Migration) 2026-07-09 baff7ce (direct)
260709-lda LDAP: ldapts empty-array-Attribut-Bug (E-Mail-Kollision auf Unique-Constraint) 2026-07-09 246dc89 (direct)
260709-sbx LDAP: Suchbox fuer die entdeckten Gruppen/OUs-Liste 2026-07-09 aaa2922 (direct)
260714-lex LDAP: Per-User Exclude/Denylist-Filter (Service-Accounts vom Sync ausschliessen) — live verifiziert: deaktiviert 4 Accounts, 2 echte User aktiv 2026-07-14 9d1323f (direct)
13 Normalizer-Gap Phase 13 schliessen: NetServer/Cosinex-Bag-Dispatch (TenderNormalizerService) 2026-07-23 9881005 —
260728-lih LDAP: Sync strikt selektiv (leere Auswahl = No-Op statt Voll-Import) + Auto-Sync-Default aus (syncIntervalMin 60→0) 2026-07-28 c54e424,57bc7f9,63a07ab 260728-lih-ldap-sync-selektiv-und-auto-sync-default
260729-d3k LDAP: Multi-Base-DN und Base-DN als Sync-Scope (statt leerer Gruppenfilter = No-Op) 2026-07-29 5cbd530,96be7e1 260729-d3k-ldap-multi-base-dn-und-base-dn-als-scope
260805-d0r Benutzer-Detail zeigte Gruppenmitgliedschaften aus Freigaben statt aus Mitgliedschaften — GET /module-grants/users/:userId liefert jetzt { groups, modules }, Chips mit Herkunfts-Badge; im Browser gegengeprüft: Mitgliedschaft bleibt sichtbar, auch wenn die Gruppe kein Modul freigibt 2026-08-05 ecadf69,f8ff74b,8ce3748 260805-d0r-benutzer-detail-zeigt-gruppenmitgliedsch
260811-enc LDAP-Bind-Passwort war das einzige Zugangsdatum im Klartext in der DB. Jetzt AES-256-GCM ueber den bestehenden CalendarCryptoService, Spalte umbenannt zu encryptedBindPassword, Entschluesselung zentral in getConfig()/getAllActiveConfigs(), idempotenter Bootstrap-Backfill fuer Altbestand. Falscher Schluessel wirft, statt still "kein Passwort" zu liefern (sonst wuerde aus einem authentifizierten Bind unbemerkt ein anonymer) 2026-08-11 4f687ea (direct)
260811-j04 DOE-Ausschreibungen verlinkten auf die API (rohes JSON) statt auf die Bekanntmachung — betraf Trefferliste, Detailansicht und Alarm-Mails. Adapter baut die URL jetzt aus der Bekanntmachungsnummer, Migration schreibt 2846 bestehende Zeilen in Tender und TenderSource um. Zielseite im Browser fuer beide Kennungsformen verifiziert (numerisch und UUID) — ein HTTP-Statuscheck taugt dort nicht, die Seite antwortet auf jede Kennung mit 200 2026-08-11 ecf7872 260811-j04-doe-tender-links-point-to-the-api-instea
260811-f9i KRITISCH: objectGUID-Existenzpruefung fand nie etwas — der Filter wurde als \xx-escapter String gebaut, ldapts wandelt das nicht in Rohbytes; beide Suchen (Base-DNs und WR-03-Fallback) teilten ihn, also haette der erste echte Sync JEDE AD-gebundene Gruppe samt Mitgliedschaften und Modulfreigaben geloescht. Jetzt EqualityFilter ueber den rohen Buffer, escapeLdapFilterBuffer() entfernt. Read-only am echten AD gemessen (escapter String 0 Treffer, EqualityFilter 1 korrekter Treffer); Regressionstests gegengeprueft (alter Code = 8 rote Tests) 2026-08-11 d2019dc 260811-f9i-fix-objectguid-existence-sweep-to-use-eq
260805-fok Standardgruppe bei Mandanten-Anlage + Startup-Reparatur — GroupsService.ensureDefaultGroup(tenantId) mit D-13-Waechter (null Gruppen, nicht fehlende Markierung), verdrahtet in TenantService.create und AdminSeedService.ensureDefaultGroupsForAllTenants; schliesst die Migrations-Backfill-Luecke auf frischen Installationen (Testserver: tenants=1 users=4 groups=0) 2026-08-05 9d1254c,0d7d8a5 260805-fok-standardgruppe-bei-mandanten-anlage-und-
21 Verschluesselungsschluessel in den Beispiel-Umgebungsdateien dokumentiert: .env.example hatte gar keinen Eintrag, .env.prod.example nannte noch den alten Namen CALENDAR_ENCRYPTION_KEY. Compose-Teil des Backlog-Punkts war bereits mit 7bda56d erledigt (Vorgabewert raus, :?-Abbruch statt Ersatzwert) 2026-08-11 379606e —
260907-let Verbindungstest fuer das Postfach im Ausschreibungs-Radar nachgeruestet (WINDOWS #16): POST /modules/tender-radar/email-config/test plus Knopf "Verbindung testen" im Formular unter Meine Quellen. Nutzt die vorhandene testConnection() beider Inbox-Provider, Muster vom DKV-Modul. userId ausschliesslich aus dem Auth-Kontext (eigener IDOR-Test mit Koeder-userId), leerer Benutzername oder leeres Passwort faellt auf die gespeicherten verschluesselten Zugangsdaten desselben Nutzers zurueck, keine Zugangsdaten in Logs oder Antwort. Verifiziert: 646/646 API- und 228/228 Web-Tests, beide Typpruefungen sauber, Sprachschluessel-Gate von rot auf gruen. Offen: Browser-Abnahme gegen ein echtes Postfach (Ende-der-Phase, braucht Neubau durch den User) 2026-09-07 c4db3b2 260907-let-verbindungstest-fuer-das-postfach-im-aus
260909-ab3 Zwei Befunde aus der Live-Pruefung behoben. #14: Die Suche in der Freigaben-Matrix filterte beide Achsen mit demselben Begriff und leerte dadurch die jeweils andere — jetzt bleibt die nicht getroffene Achse vollstaendig stehen, die Gruppensuche unter internem UND AD-Namen (#6c) ist per Regressionstest gesichert. #15: AD-Konten mit bereits vergebener Mailadresse werden nun angelegt, nur ohne Adresse (Produktentscheidung des Users vom 2026-09-09; der erste Anspruch behaelt die Adresse), auf BEIDEN Wegen — Sync und Einzelimport. Rohe Prisma-Texte gehen nur noch ins Log, der Bericht zeigt drei verstaendliche deutsche Abschnitte. Sicherheitsfund nebenbei geschlossen (T-Q3-01): der Update-Zweig schrieb die Mailadresse bedingungslos um, ein Verzeichniseintrag haette so die Adresse einer echten Person uebernehmen und deren Passwort-Reset empfangen koennen. User.email ist jetzt optional (Migration geschrieben, laeuft beim naechsten API-Start automatisch mit). Verifiziert 8/8: 651/651 API- und 233/233 Web-Tests, beide Typpruefungen sauber; die Sicherheitspruefung wurde durch Rueckbau falsifiziert (ohne Besitzpruefung schlaegt der Test fehl). Am 2026-09-09 im Browser abgenommen, beide Ledger-Punkte geschlossen (Bericht: 260909-ab3-UAT-2026-09-09.md) 2026-09-09 2167046 260909-ab3-matrix-suche-und-sync-meldungen-reparier
260909-cx0 Hochgeladene Dateien (Avatare, DKV-Exporte) ueberlebten kein --force-recreate des api-Containers (WINDOWS #17) — lagen nur in der fluechtigen Container-Schicht, keine Compose-Datei mountete /app/user-files. Jetzt benanntes Docker-Volume user-files in docker-compose.yml und docker-compose.prod.yml (Eigentuemerschaft uid 1001 aus dem Image, kein Bind-Mount), Betriebshandbuch Kapitel 6/7 entsprechend nachgezogen. Zweiter, unabhaengiger Punkt: CLAUDE.md nannte fuer die Technik-Tabelle noch die 2026-06/07-Empfehlung (Next.js 16.2.x, Prisma 7.8.x, Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged) statt des installierten Stands — jetzt korrigiert auf Next.js 15.5.19, Prisma 6.19.3 etc., nie uebernommene Empfehlungen in eigenem Abschnitt "Recommended But Not Adopted", .planning/research/STACK.md nur mit Hinweiszeile ergaenzt. Keine Abhaengigkeit aktualisiert. WINDOWS #17 bleibt offen — die Aenderung erreicht die laufende Installation auf alpha nicht, /opt/tessera/docker-compose.yml weicht vom Repository ab und muss vom Nutzer selbst ergaenzt werden 2026-09-09 dab72eb,c807049 260909-cx0-dateisicherung-nachruesten-und-versionsa
260909-cx0 Dateisicherung nachgeruestet und Versionsangaben geradegezogen. user-files liegt jetzt in einem benannten Volume (docker-compose.yml und .prod.yml) — vorher lag der Ordner nur in der fluechtigen Container-Schicht, hochgeladene Profilbilder und DKV-Exporte waeren bei jedem --force-recreate weg gewesen. Benanntes Volume statt Bind-Mount, weil das Image /app/user-files an uid 1001 uebereignet; ein frisch angelegtes Host-Verzeichnis gehoert root und haette aus dem Datenverlust einen kaputten Upload gemacht. docker-compose.dev.yml blieb bewusst unveraendert (Compose fuehrt Mount-Listen ueber das Ziel zusammen). CLAUDE.md nennt jetzt die installierten Fassungen statt der urspruenglich empfohlenen (Next.js 15.5.19 statt 16, Prisma 6.19.3 statt 7); neu ist ein Abschnitt 'Recommended But Not Adopted', der sechs nie eingebaute Empfehlungen benennt — darunter Keycloak, Redis und shadcn/ui. Der Block ist generiert, deshalb traegt er einen Herkunftsvermerk und die Recherchedatei eine datierte Hinweiszeile; ihre Zahlen blieben unangetastet. Keine Abhaengigkeit angefasst (per git diff gegengeprueft). WINDOWS #17 am 2026-09-09 geschlossen. Beim Nachtragen auf dem Server kam heraus, dass dort gar nicht docker-compose.yml gilt: die .env setzt COMPOSE_FILE=docker-compose.prod.yml. Die drei Zeilen wurden in dieser Datei ergaenzt (Sicherung docker-compose.prod.yml.bak.20260909-0818). Nach dem Neuerstellen durch den User belegt: Mount tessera_user-files -> /app/user-files vorhanden, Ordner gehoert uid 1001 (das benannte Volume hat die Eigentuemerschaft uebernommen), Schreiben als Dienstnutzer funktioniert, und eine Probedatei lag tatsaechlich unter /var/lib/docker/volumes/tessera_user-files/_data auf dem Host — also ausserhalb des Containers 2026-09-09 c807049 260909-cx0-dateisicherung-nachruesten-und-versionsa
260909-dgj Mandantentrennung auf Datenbankebene vorbereitet (WINDOWS #18). Ausloeser war ein gemessener Befund: die Anwendung verbindet als Rolle mit Superuser- und BYPASSRLS-Recht, daher greifen die sieben vorhandenen Policies gar nicht — ohne gesetzten Mandantenkontext lieferte 'SELECT count(*) FROM Group' zwei statt null Zeilen. Gebaut wurden: Rolle tessera_app ohne Umgehungsrecht (wiederholbare Migration, kein Kennwort im SQL), Trennung von Migrations- und Laufzeitverbindung ueber TESSERA_MIGRATE_DATABASE_URL, ein Pruefwerkzeug mit fuenf transaktionssicheren Nachweisen, Policies fuer die 16 fehlenden Tabellen und eine Betriebsanleitung. Der Schalter bleibt bewusst aus, #18 bleibt offen: im Code stehen 182 Datenbankzugriffe ohne Mandantenkontext gegen 19 mit — darunter zwingend der Anmeldeweg, der die Benutzerzeile liest, bevor der Mandant bekannt ist (er kommt erst aus dieser Zeile). Ein Umschalten wuerde die Anmeldung fuer alle sperren. Dabei fiel #19 an: SearchProvider und TenderRssFeedSource haben ein nullable tenantId; die einfache Policy wuerde die plattformweiten Zeilen nach dem Scharfschalten fuer jeden Mandanten unsichtbar machen. 673/673 Tests gruen 2026-09-09 efaabc9 260909-dgj-mandantentrennung-auf-alle-tabellen-mit-
260909-ipc Mandantentrennung Etappe 2, Bereich ldap: alle 21 klassifizierten Zugriffe in ldap-config.service.ts (9) und ldap.service.ts (12) an forTenant() gebunden. Zuerst gemessen, dann gebaut: rls-scratch-check.mjs um runLdapAreaChecks erweitert (13/13 bestanden), Beleg ist die Zeile ldapconfig-ungebunden-null-zeilen gegen die echte, ausgelieferte Policy — die Umkehr der Fehlerrichtung ist damit gemessen, nicht behauptet. Die geforderte Kritikschrift liegt in docs/mandantentrennung-etappe2-fehlerrichtung.md mit Signaltabelle je Pfad und vier namentlich benannten Stellen, die Leere als Abwesenheit deuten. Sicherheitsluecke nebenbei geschlossen (T-IPC-01): DELETE /ldap/config/mappings/:id nahm nur die Kennung — ein Administrator von Mandant A konnte die Feldzuordnung von B loeschen; der Mandant kommt jetzt aus der Sitzung. Drei Stellen bleiben bewusst ungebunden, jede mit Begruendung im Code: getAllActiveConfigs und die Start-Nachverschluesselung lesen zwingend uebergreifend; resolveEmailForWrite darf nicht gebunden werden, weil email/username plattformweit eindeutig sind — gebunden saehe die Kollisionspruefung keinen fremden Halter, meldete 'frei', und aus einer sauber berichteten Kollision wuerde ein P2002-Abbruch (Produktfrage fuer Etappe 3). Befund, der die Testlage aendert: forTenant war in ldap.service.spec.ts als Identitaet gemockt — die Tests haetten den Umbau in keiner Richtung bemerkt; ersetzt durch zwei unterscheidbare Clients. rls-access-inventory.spec.ts um eine Stand-Spalte und Erkennung gebundener Fundstellen erweitert, dabei zwei bisher unbekannte Paare gefunden (auth.service.ts/passwordResetToken, ldap.service.ts/groupMembership), Klassifikationsdokument auf 61 Paare nachgezogen. Schema, Migrationen, Compose und Umgebungsdateien unberuehrt, Schalter weiter aus. Verifiziert 7/7 (unabhaengig nachgemessen: 719/719 Tests, Typpruefung sauber, 13/13 Live-Pruefungen gegen den echten Container) 2026-09-09 a0c9ef0,9a57fa7,e1586a4 260909-ipc-mandantentrennung-etappe-2-bereich-ldap-
260909-jts Mandantentrennung Etappe 2, Bereich groups — die Berechtigungsschicht. Alle 34 echten Zugriffe in groups.service.ts (21) und module-grants.service.ts (13) gebunden, dazu fuenf Zugriffe, die keine Pruefung dieses Projekts je gesehen hatte: sie laufen innerhalb einer Transaktion ueber den Callback-Parameter, den der Detektor der Inventarpruefung nicht kannte — einer davon ist der Schreibvorgang, der Modulfreigaben vergibt. Das Paar (groups.service.ts, tenantModuleActivation) fehlte im Klassifikationsdokument komplett und ist ergaenzt; der Detektor sieht jetzt auch Transaktionsparameter. Kernbefund — neues Hilfsmittel withTenantTransaction(): die Frage, welche Transaktionsform den Mandantenkontext auf derselben Verbindung traegt, wurde an der lebenden Datenbank gemessen statt angenommen. Form (i) faellt durch (zwei verschiedene pg_backend_pid()), Form (ii) besteht die Einzelmessung, bricht aber unter 40 gleichzeitigen Aufrufen mit P2028 ab, weil jeder Aufruf eine verschachtelte Transaktion aus demselben endlichen Verbindungsvorrat oeffnet; Form (iii) besteht beides. Alle weiteren Bereiche bauen darauf auf. Die Lastprobe war zunaechst nur Fliesstext — eine Zahl, die eine Entscheidung trug, ohne nachvollziehbar zu sein; vom Verifizierer beanstandet und als runConcurrencyProbe nachgereicht (604428a), laeuft seither bei jedem Werkzeuglauf mit: 24 Verletzungen von 40 fuer Form (ii), 0 von 40 fuer Form (iii). Zwei Datenbankregeln greifen kuerzer als gedacht und sind bewusst nur gemessen und festgehalten, nicht repariert: die Regel fuer Gruppenmitgliedschaften prueft nur die Gruppen-, nicht die Benutzerseite (T-JTS-02), die fuer Modulfreigaben nur die Mandantenkennung, nicht die referenzierte Gruppe (T-JTS-03) — dort haengt der Schutz allein an assertTargetBelongsToTenant. Umgekehrter Gefahrenfall geschlossen: ensureDefaultGroup deutet Leere als 'Mandant hat noch keine Gruppe' und baut alles neu auf, eine halb umgestellte Fassung haette eine zweite Standardgruppe samt Freigaben erzeugt — Zaehler und Transaktion sind deshalb gemeinsam gebunden. Beide Testdateien hatten gar keine Attrappe fuer den Helfer, waeren nach der Umstellung also aus dem falschen Grund rot gewesen; ersetzt durch den Zwei-Client-Nachweis, vom Verifizierer durch Rueckbau falsifiziert. Schema, Migrationen, Compose und Umgebungsdateien unberuehrt, Schalter aus. Verifiziert 9/9 (743/743 Tests, Typpruefung sauber, 23/23 Live-Pruefungen) 2026-09-09 fd0b9f7,7f08b27,abb6c8b,604428a 260909-jts-mandantentrennung-etappe-2-bereich-group
260909-laa Mandantentrennung Etappe 2, Bereich tenders — anders geschnitten als die bisherigen: von 23 Paaren werden nur 5 umgestellt (die Nutzer-CRUD-Dienste fuer gespeicherte Suchen, Bearbeitungsstand, Benachrichtigungen, Postfach, RSS), 10 bleiben bewusst ungebunden, weil der Ausschreibungskatalog plattformweit ist (D-03), 2 sind Verteiler, die absichtlich ueber alle Mandanten lesen, und 6 sind Mischfaelle, deren uebergreifende Haelfte zu Etappe 3 gehoert — nur die Je-Treffer-Schleifen wurden gebunden. Die gefaehrlichste Grenze lag in tender-rss-feed.service.ts: dort haben plattformweite RSS-Quellen ein leeres Mandantenfeld; drei der vier Zugriffe duerfen deshalb NICHT binden, sonst waeren diese Quellen nach dem Scharfschalten fuer JEDEN unsichtbar statt nur fuer fremde (WINDOWS #19). Die Datei endet bewusst auf Stand gemischt, und das einzelne bedingte Loeschen blieb eine Anweisung — es aufzuteilen haette das Pruef-/Nutzungsfenster geoeffnet, das der Dateikopf vermeidet. Neue Gegenrichtung, die die Bindung selbst erzeugt: drei upsert-Pfade laufen auf Eindeutigkeitsschluesseln ohne Mandantendimension; ist die Zeile unter dem gebundenen Kontext unsichtbar, wird aus stillem Ueberschreiben ein harter Fehler — jetzt als deutsche Meldung statt als 500. Der Verifizierer fand, dass genau eine der drei fehlte (setTriage), obwohl die Zusammenfassung alle drei behauptete; nachgereicht mit 8cbf4c1 samt zwei Tests, deren Rotwerden durch Rueckbau belegt ist. Befund E, festgehalten statt repariert: alle fuenf Policies dieses Bereichs lesen nur tenantId = current_tenant_id() und haben KEINE Benutzerdimension — zwei Nutzer desselben Mandanten sind auf Datenbankebene fuereinander vollstaendig sichtbar; die Trennung haengt allein am Anwendungscode, der stichprobenartig als korrekt belegt wurde. Produktfrage vor dem zweiten Kunden. Befund K, neue Reihenfolgebedingung fuer Etappe 4: der Mailversand holt SMTP aus dem noch nicht umgestellten Bereich settings — nach dem Scharfschalten ginge fuer NIEMANDEN mehr eine Mail raus; settings muss vor Etappe 4 durch sein. Vierte Zaehlkorrektur des Vorhabens: 62 Rohtreffer sind 61 Modellzugriffe, und von zehn vermeintlichen Controller-Stellen brauchten acht die Durchreichung. Verifiziert 8/9, Luecke behoben (772/772 Tests, Typpruefung sauber, 32/32 Live-Pruefungen) 2026-09-09 3498147,3336a6e,df5c5b7,8cbf4c1 260909-laa-mandantentrennung-etappe-2-bereich-tende
260909-mir Mandantentrennung Etappe 2, Bereich dkv — Tankkarten-Modul. Alle 21 klassifizierten Zugriffe in dkv.service.ts gebunden (am Ende 22, weil der neue Besitzriegel einen Lesezugriff hinzufuegt); genau einer bleibt bewusst ungebunden. Bereits bestehende Fremdzugriffsluecke geschlossen (T-MIR-03): getExportFile(tenantId, filename) nahm die Mandantenkennung entgegen und benutzte sie nie — die Datei kam allein ueber ihren Namen aus dem gemeinsamen user-files/-Verzeichnis, ein Administrator eines beliebigen Mandanten konnte die Tankkarten-Auswertung eines anderen herunterladen. Der Riegel leitet die Zugehoerigkeit jetzt aus DkvInvoiceHistory.exportFilename ab; in der Oberflaeche gegengeprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt, die regulaere Nutzung aendert sich also nicht. Die Luecke ist keine Folge des Umbaus, sie bestand seit jeher. Zerstoerender Fehler in umgekehrter Richtung behoben: saveConfig verschluckte im Zweig, der ein gespeichertes Passwort erhalten soll, Lese- und Entschluesselungsfehler und machte mit leeren Werten weiter — nach dem Scharfschalten haette er ein vorhandenes Passwort durch ein leeres ersetzt und verschluesselt abgelegt, ohne Meldung, nicht rekonstruierbar. Dritte Variante der Testluecke: der Bereich hatte gar keine Testdatei (nicht wie ldap eine, die nichts prueft, nicht wie groups/tenders eine, die abstuerzen wuerde); dkv.service.spec.ts neu angelegt. WINDOWS #21, bewusste Entscheidung: der Planer-Startpfad bleibt ungebunden und wird als benannte Altlast weitergefuehrt — binden ist unmoeglich (onModuleInit hat strukturell keinen Mandanten), Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung. Unsymmetrie zum ldap-Praezedenzfall ausgeschrieben: jener ist heute korrekt und verstummt spaeter, dieser ist HEUTE bereits falsch (bedient einen willkuerlichen Mandanten, bei inaktiver Zeile niemanden) UND verstummt zusaetzlich. Dreifach markiert. Erster Bereich, dessen Kopfzahl beim Hineinsehen NICHT kleiner wurde. Ausfuehrung brach am 2026-09-09 gegen Ende von Aufgabe 3 an einem Sitzungslimit ab — zwei Aufgaben committet, die dritte vollstaendig im Arbeitsbaum; am 2026-09-10 nachgetragen. Aufgefallen durch git status, nicht durch den Bericht. Verifiziert 9/9, zwei Dokumentationsluecken danach behoben (Hintergrunddienst-Abschnitt um den vierten Fall erweitert, Falsifizierungsnachweise nachgetragen). 789/789 Tests, Typpruefung sauber, 41/41 Live-Pruefungen 2026-09-10 761e5e2,222f453,5e8237d 260909-mir-mandantentrennung-etappe-2-bereich-dkv-a
260910-das Mandantentrennung Etappe 2, Bereich user — Benutzerverwaltung, die schwerste Fehlerklasse des Vorhabens (Fremdzugriff hier ist Rechteausweitung ueber Mandantengrenzen, nicht blosse Sichtbarkeit). Alle 17 Zugriffe eingeordnet: Verwaltungswege gebunden, Eindeutigkeits- und Suchwege bewusst ungebunden, jede Entscheidung mit Begruendung am Ort. Startsperre entschaerft — der schwerwiegendste Fund: nach dem Scharfschalten haette eine FRISCHE Installation ihren ersten Administrator nicht anlegen koennen und die Anwendung waere gar nicht erst gestartet. Kette (Glied fuer Glied belegt): die Startpruefung liefert null — nicht weil der Admin fehlt, sondern weil ohne Mandantenkontext keine Zeile sichtbar ist — also wird angelegt, das laeuft in den plattformweit eindeutigen Anmeldenamen, und weil seedAdmin() ungekapselt in onApplicationBootstrap haengt, bricht der Start ab. Betroffen waere jede Installation mit gesetzten Admin-Umgebungswerten gewesen; auf dem bestehenden System nie aufgefallen, weil dort der Admin laengst existiert. Jetzt bindet die Erstanlage an den eine Anweisung zuvor angelegten Mandanten und faengt GENAU den Doppelanlage-Fall ab — jeder andere Fehler bricht den Start weiterhin ab (beide Haelften einzeln nachgewiesen). Riegel repariert, der seit seiner Entstehung wirkungslos war: die Sperre gegen das Loeschen des eigenen Kontos verglich gegen ein Feld, das der Sitzungsnachweis gar nicht traegt (sub; er traegt id, username, role, tenantId) — ein Administrator konnte sein eigenes Konto loeschen. Kein Mandantenproblem, gefunden weil dieser Durchlauf jede Zeile aufschlaegt. Zwei Falschaussagen in eigenen Artefakten berichtigt: der Kopfkommentar von findByUsername behauptete, sie muesse fuer den mandantenuebergreifenden Anmeldeweg ungebunden bleiben — der laeuft seit Etappe 1 ueber die SECURITY-DEFINER-Funktionen, und die Methode hat gemessen NULL Aufrufer; und die Klassifikationszeile der Erstanlage behauptete, es gebe strukturell keinen Mandanten zum Binden, obwohl er eine Anweisung vorher entsteht (Klasse auf beides korrigiert). SUPER_ADMIN-Sicht war nach dem Scharfschalten in JEDER heutigen Form kaputt (ungebunden null Zeilen, gebunden stille Funktionsminderung) — jetzt Schleife ueber alle Mandanten mit gebundenem Rumpf, neues Fundstellenpaar (user.service.ts, tenant). Steuerungsschicht hatte gar keine Tests (7 der 17 Zugriffe plus die gesamte Rollenlogik) — user.controller.spec.ts neu. Plan-Pruefer fand einen Blocker: vier handgepflegte Dokumentstellen benannt, nur zwei abgesichert — also derselbe Fehler, den der Plan verhindern sollte; nachgebessert mit herleitenden statt fest verdrahteten Pruefungen, in vier Einzelmutationen falsifiziert. Verifiziert 10/10 (810/810 Tests, Typpruefung sauber, 53/53 Live-Pruefungen; Selbstloesch-Riegel und Klassenverteilung vom Pruefer eigenhaendig nachgerechnet) 2026-09-10 b848ba6,888f660,3a9391d 260910-das-mandantentrennung-etappe-2-bereich-user-
260910-exd Mandantentrennung Etappe 2, Bereich module-registry — der Berechtigungs-Anfrageweg. module.guard.ts laeuft bei JEDER Modulanfrage und hat null eigene Datenbankzugriffe; seine Richtigkeit ist vollstaendig eine Funktion dessen, was dieser Bereich liefert. Endstand 7 ungebunden / 10 gebunden. Eine Vorgabe des Auftrags war falsch und wurde widerlegt: angeblich wuerde eine Bindung des Modulkatalogs ihn fuer jeden Mandanten unsichtbar machen — gemessen ueber alle 23 ENABLE ROW LEVEL SECURITY-Zeilen steht Module auf keiner, die Tabelle traegt gar keinen Zeilenschutz und kein tenantId. Eine Bindung waere heute WIRKUNGSLOS, nicht katastrophal; katastrophal wird sie erst, wenn Etappe 3 der Tabelle eine Regel gibt. Handlung unveraendert (Katalog bleibt ungebunden), aber Messung und Bedingung sind in Code und Dokument jetzt getrennt — eine richtige Handlung mit falscher Begruendung haelt nur, bis sich jemand auf die Begruendung verlaesst. Die Kernfrage ehrlich beantwortet: es gibt KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Nach dem Scharfschalten saehe ein Unterlauf nicht wie ein Fehler aus, sondern wie 'du hast keine Module' — leere Seitenleiste, leerer Marktplatz, jeder Modulaufruf abgewiesen, fuer den Betroffenen nicht von einem absichtlichen Entzug zu unterscheiden. Dreifach festgehalten: im Text, als Testfall in module.guard.spec.ts (zwei Aufrufe, identische Meldung, gegeneinander gehalten) und als WINDOWS #23 mit konkreter Etappe-4-Vorabpruefung. Erstmals eine Entlastung, die strukturell haelt: die Kette unsichtbare Zeile -> falsches 'frei' -> 23505 kann hier nicht auftreten, weil die Eindeutigkeitsschluessel die Mandantenkennung fuehren — erster von sechs Bereichen, in dem sie abwesend statt umgangen ist; entsprechend wurde KEINE Absicherung eingebaut, die nichts absichert. Wieder zwei Kopfkommentare mit Falschaussagen (isModuleActive 'Used by ModuleGuard', findActiveForTenant als Marktplatz-Lieferant), beide Methoden mit null Aufrufern — berichtigt. module-registry.service.ts hatte trotz 11 der 17 Zugriffe und aller Schreibwege GAR KEINE Testdatei. Reihenfolge-Entlastung: der Dashboard-Filter wird mitgebunden, ohne dass eine dashboard-Datei angefasst wird. Zwei Abweichungen, beide vom eigenen Pruefgatter erzwungen und geprueft: eine Identitaets-Attrappe in tender-scheduler.service.spec.ts (zulaessig — jene Datei prueft Planer-Verhalten, die Bindung ist in module-registry.service.spec.ts mit zwei Klienten belegt) und eine Stand-Spalte, die eine Aufgabe frueher nachgezogen werden musste. Verifiziert 9/9 (833/833 Tests, Typpruefung sauber, 66/66 Live-Pruefungen; Klassenverteilung und Summenzeile vom Pruefer eigenhaendig nachgerechnet). Eine Zahl in der Zusammenfassung (7 statt 6 neue Bindungsnachweise) vom Pruefer nachgezaehlt und berichtigt 2026-09-10 7d45e2f,3df7268,9c0eefe 260910-exd-mandantentrennung-etappe-2-bereich-modul
260910-jab Die drei zu kurz greifenden Datenbankregeln geschlossen — auf ausdrueckliche Anweisung des Users VORGEZOGEN, entgegen der geplanten Reihenfolge (urspruenglich nach Etappe 2, damit jeder Bereich gegen einen stabilen Regelstand misst; der User entschied anders, weil offene Loecher vergessen werden). Erster Durchlauf dieser Serie, der die DATENBANK aendert statt nur Anwendungscode — neue Migration 20260910120000_rls_widen_membership_grant_and_platform_read. T-JTS-02: GroupMembership prueft jetzt BEIDE Seiten (Gruppe UND Benutzer gehoeren zum Mandanten) statt nur die Gruppenseite. T-JTS-03: ModuleGrant prueft zusaetzlich, dass die referenzierte Gruppe bzw. der referenzierte Benutzer zum selben Mandanten gehoert; assertTargetBelongsToTenant bleibt als zweite Verteidigungslinie bestehen. WINDOWS #19: TenderRssFeedSource bekommt VIER nach Befehl getrennte Regeln — Lesen schliesst plattformweite Zeilen ein, Einfuegen/Aendern/Loeschen verlangen weiter einen Mandanten (eine einzige lockere Regel haette jedem Mandanten erlaubt, gemeinsame Quellen zu aendern und zu loeschen, weil USING auch UPDATE und DELETE regelt). Halbe Praemisse von #19 widerlegt: bei SearchProvider gibt es gar keinen Codeweg, der eine mandantenlose Zeile erzeugt — Schreibweg verlangt den Mandanten, Vorgaben sind Konstanten (05-02); als widerlegte Annahme geschlossen, nicht als geloestes Problem, strenge Regel bleibt. DREI Pruefungen schrieben die Loecher als erwartetes Verhalten fest (meine eigene Suche fand nur zwei, der Planer die dritte) — alle drei UMGEDREHT statt geloescht, mit Verweis auf den urspruenglichen Befund: der ausfuehrbare Beleg, dass das Loch existierte, bleibt mit umgekehrtem Vorzeichen erhalten. Die Reparatur erzeugte an einer Stelle selbst den Fehler, gegen den sie antritt: listForUser haette nach der Regelaenderung die plattformweiten, aber nicht die persoenlichen Quellen geliefert — aus einer leeren Liste, die schreit, waere eine kurze geworden, die luegt; deshalb mitgebunden. Messfalle abgefangen: extractPolicySql() las nur die alten Migrationsverzeichnisse und haette nach der neuen Migration still die ABGELOESTE Regel weitergemessen. Werkzeugfalle abgefangen: der uebliche Aufrufweg haette beim Einspielen eine neue Prisma-Hauptversion nachgeladen; stattdessen die im Projekt festgelegte Fassung benutzt. Neuer offener Ledger-Eintrag #24: plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen — in alter wie neuer Regel. Verifiziert 11/11 mit vier ZERSTOERENDEN Gegenproben (jede Regel und die neue Bindung einzeln zurueckgedreht, jedes Mal schlug genau die zustaendige Pruefung fehl, danach byte-identisch wiederhergestellt). 839/839 Tests, Typpruefung sauber, 74/74 Live-Pruefungen; Regeltexte vom Orchestrator in der LAUFENDEN Datenbank gegengelesen 2026-09-10 f4f3115,6b23735,03fb3bf 260910-jab-mandantentrennung-die-drei-zu-kurz-greif
260910-krx Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren: dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen): nach dem Scharfschalten liefert getLayout bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. DashboardLayout.userId ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). Der Verifizierer fand eine Luecke der bekannten Art: die Behauptung, ein gebundener Konfliktschreibvorgang werfe PrismaClientUnknownRequestError (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei groups.service.ts mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. Verifiziert 10/11, Luecke behoben (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) 2026-09-11 6744918,e0ce594,67b5024,6e71206 260910-krx-mandantentrennung-etappe-2-bereich-dashb
260911-cwh Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); calendar.service.spec.ts neu mit 23 Tests. Gespeicherte Zugangsdaten zu fremden Kalender-Servern — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. Der dkv-Passwortverlust-Fall existiert hier NICHT, in beiden Haelften belegt: der Dienst schreibt encryptedPassword nur bei dto.password !== undefined, und calendar-source-form.tsx laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. Zwischenspeicher-Schluessel userId:from:to ohne Mandantenanteil ist sicher: User.id ist @default(uuid()), Kette Schema -> auth.service.ts sub: user.id -> JwtStrategy.validate -> extractContext Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt username/email, nicht id. Eigener Gefahrenfall — halb gebundene Aggregationsschleife: fetchAndCacheEvents liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von Promise.allSettled; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). Frontend macht aus lauten Fehlern stille: calendar-widget.tsx und calendar-settings-panel.tsx fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. Lehre aus dashboard angewandt: 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen schema.prisma (17 = 17 Spalten). Verifiziert 12/12 (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt 2026-09-11 bf5fc4d,77cb124,e0e163e 260911-cwh-mandantentrennung-etappe-2-bereich-calen
260911-e2s Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. 'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1: drei der acht Zugriffe (findAll, findOne, remove im Controller) zaehlen ueber include: { _count: { select: { users } } } in die GESCHUETZTE Tabelle User hinein — Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...), das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus UserService.findAllForPlatformAdmin. Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind — sie sieht nur this.prisma.<Modell>, nicht was ein include: in eine zweite Tabelle hineinrechnet. Alle 19 include:-Stellen und alle _count-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. Die seit Etappe 1 offene Architekturfrage ist entschieden: req.tenantPrisma wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes forTenant() festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei app.module.ts:57, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch req.tenantId (22 Leser in 9 Dateien) und den x-tenant-id-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in rls-access-inventory.spec.ts geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die TenantMiddleware/req.tenantPrisma als lebendig beschrieben. Executor fing einen still fehlgeschlagenen git add (2 von 7 Dateien) selbst an git status und lieferte nach. Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) 2026-09-11 652e762,11f5731,17dca0d,c8de72e 260911-e2s-mandantentrennung-etappe-2-bereich-tenan
260911-fh9 Mandantentrennung Etappe 2, Bereich auth — die drei in Etappe 1 bewusst ausgelassenen Wege (getMe, changePassword, adminResetPassword) gebunden, alle drei brauchten neue Signaturen (nahmen nur userId). Der Anmeldeweg ueber die drei SECURITY-DEFINER-Funktionen NICHT angefasst, per pg_proc belegt (weiterhin genau 9 Spalten, auch nachdem die Wegwerf-Tabelle User 5 fehlende Spalten bekam). Verbleibende 3 'ungebundene' Stellen sind die $queryRaw-Anmeldesuchen, keine Modellzugriffe. Falle, die der Auftrag selbst gestellt hatte: Selbstbedienung darf NICHT an req.tenantId binden — der Guard laesst SUPER_ADMIN diese Kennung per x-tenant-id umschalten (Marktplatz), 'mein Profil' haette ihn sich selbst gegenueber unsichtbar gemacht; gebunden wird an den Mandanten aus dem Sitzungsnachweis (@CurrentUser().tenantId), der Controller enthaelt null Verweise auf req.tenantId/x-tenant-id. Zwei Loecher in adminResetPassword geschlossen, keines davon ein Mandantenproblem: der Weg pruefte weder den Mandanten des Ziels noch dessen Rolle — ein ADMIN konnte das Passwort eines SUPER_ADMIN ueberschreiben. Beides jetzt dicht, SUPER_ADMIN-Pfad ueber UserService.findByIdForPlatformAdmin; AuthModule importiert UserModule, zyklusfrei. Der Schwesterweg PATCH /users/:id hat dieselbe Rollenluecke (T-02-08 prueft nur das ZUWEISEN der Rolle, nicht die bestehende Rolle des Ziels) — ausserhalb der Erlaubnisliste, als WINDOWS #29 festgehalten. Umgekehrte Fehlerrichtung ist hier leise, nicht laut: getMe-Leere wird zu 200 mit leerem Rumpf, header.tsx tut bei if (u) nichts — 'nicht angemeldet' und 'Zeile unsichtbar' sind derselbe Wert (WINDOWS #28); changePassword-Leere liest sich als networkError. Identitaets-Attrappe (ldap-Form) durch asymmetrischen Doppel ersetzt: ungebundener Nachbau ohne Modelle, gebundener ohne $queryRaw — beide Grenzen einzeln falsifizierbar. Verifiziert 8/8 (951/951 Tests, 60 Dateien, Typpruefung sauber, 120/120 Live-Pruefungen; zwei Falsifizierungen vom Pruefer eigenhaendig reproduziert — genau 4 bzw. 2 benannte Tests rot) 2026-09-11 9782bea,92aa8c4,f68beb3 260911-fh9-mandantentrennung-etappe-2-bereich-auth-
260911-gwh Mandantentrennung Etappe 2, Bereiche favorites + settings — LETZTER Durchlauf, Etappe 2 abgeschlossen. 7 favoriteLink-Zugriffe und 3 smtpConfig-Anfragepfade gebunden; genau ein smtpConfig-Zugriff bleibt bewusst offen: der Startpfad, umbenannt in loadAnySmtpConfigForStartupTransport() — SECHSTER Fall der Hintergrunddienst-Falle (findFirst() ohne Mandanten beim Hochfahren in mail.module.ts; heute bedient er einen willkuerlichen Mandanten, nach dem Scharfschalten null), beide Zustaende am Ort, WINDOWS #30. Befund K geschlossen: getDecryptedSmtpConfig(tenantId) bindet — die Reihenfolgebedingung fuer Etappe 4 aus dem tenders-Lauf ist erfuellt und in Kritikschrift (t4)/(d4) und Klassifikation als erfuellt vermerkt. Widget-Besitzriegel in favorites.create() eingebaut, weil GEMESSEN noetig: Pruefung 7 zeigt, dass ein gebundenes Anlegen mit fremder widgetId GELINGT — die Fremdschluessel-Pruefung umgeht den Zeilenschutz; vom Verifizierer live reproduziert und der Riegel durch Rueckbau falsifiziert (genau 4 Tests rot). Beide Bereiche hatten keine Testdatei fuer ihren Dienst; favorites.service.spec.ts (23) und settings.service.spec.ts (20) neu, nodemailer gemockt. Ledger #31/#32 fuer die stille Leere (leere Favoritenleiste = 'nie etwas gespeichert'; fehlende SMTP-Konfiguration = 'nicht eingerichtet', obwohl die Zugangsdaten da sind). Der Planer scheiterte am Sitzungslimit NACH dem Schreiben des Plans, VOR der Rueckmeldung — Plan lag vollstaendig auf der Platte (1226 Zeilen, Struktur gueltig), vom Orchestrator committet, vom Pruefer als Erstleser gegen den Baum gehalten. Verifiziert 9/9 (994/994 Tests, 62 Dateien, Typpruefung sauber, 137/137 Live-Pruefungen; Uebersicht 68/178, Klassenverteilung 33+17+13+2=65 und Migrations-Zaehlung 4+3+16=23 vom Pruefer nachgerechnet) 2026-09-11 88896d3,8f2c13a,b5f22e2,1240932 260911-gwh-mandantentrennung-etappe-2-bereiche-favo
260911-mkj WINDOWS #27 geschlossen — die Bestandsaufnahme sieht jetzt Relationszugriffe. Vierte Erkennungsform in rls-access-inventory.spec.ts: include:/select:/_count: werden ueber schema.prisma (zur Testzeit gelesen) auf das Zielmodell aufgeloest und als (Datei, Modell)-Fundstelle gefuehrt, gebunden oder ungebunden je nach umschliessendem Klienten. Zwei Wachhunde, die LAUT werden statt still: Empfaenger ausserhalb der vier Formen (raw vs. matched) und nicht aufloesbare Konstanten — beide vom Verifizierer live gebrochen und rot gesehen. Gemessen mit Prototyp, Vorhersage exakt getroffen: 7 neue Paare, 3 Stand-Aenderungen, eine Klassenaenderung (ldap-config.service.ts/ldapFieldMapping -> beides/gemischt, weil getAllActiveConfigs() ueber include: { fieldMappings } in die geschuetzte Tabelle reicht — genau die #27-Form, bisher unsichtbar, kein neuer Gefahrenfall). Klassifikation 65 -> 72 Paare (35/21/14/2). Acht gepinnte Proben, darunter die beiden #27-Formen (_count.select.users auf this.prisma.tenant -> user ungebunden; auf gebundenem Klienten -> gebunden). Zwei Dateien standen in KEINER Erkennungsform (tenders.seed.ts mit Client als Funktionsparameter, backfill-tender-source.ts mit eigenem new PrismaClient()) — heute harmlos, als WINDOWS #33 eigenstaendig festgehalten statt still in #27 mitgeschlossen. Planer fing einen Fehler im eigenen Prototyp (Lookahead beim Schema-Parsen, ohne den alle Listenrelationen am Zeilenende verloren gingen). Verifiziert 6/6 (1007/1007 Tests, Typpruefung sauber, nur die Spec unter apps/api/src angefasst). Info vom Pruefer: der Lookahead ist nicht durch einen eigenen Regressionstest gedeckt — der raw/matched-Wachhund ist der eigentliche Schutz 2026-09-11 5ad23d0,388690f 260911-mkj-windows-27-schliessen-relations-blindste
260911-nke Etappe 3b — Benutzerdimension in den Datenbankregeln. Migration 20260911120000_rls_user_dimension_personal_tables: current_user_id() (liest app.current_user, NULLIF fuer den Leerstring), forTenant(prisma, tenantId, userId?) mit optionalem drittem Parameter (kein Schwesterhelfer — der Inventar-Detektor haette ihn nicht gesehen), beide set_config in EINER Anweisung, $transaction behaelt zwei Eintraege. Regeln der ZEHN persoenlichen Tabellen in der Form tenantId = current_tenant_id() AND (current_user_id() IS NULL OR userId = current_user_id()) — ein Aufruf ohne Benutzer (Admin, Hintergrunddienst) sieht weiter den ganzen Mandanten. SearchProvider/TenderRssFeedSource mit vier befehlsgetrennten Regeln (jab-Praezedenz), Mandantenhaelften unveraendert; GroupMembership/ModuleGrant/PasswordResetToken/TenderMatch bewusst ohne Benutzerdimension (Verwaltungs-/Anmelde-/Hintergrundobjekte). 34 Nutzer-CRUD-Aufrufstellen in 8 Diensten reichen den Benutzer durch, Scheduler und Verwaltungswege bleiben zweistellig. SECHS loch-behauptende Pruefungen statt drei — und die Umkehrung war nicht trivial: die alten massen OHNE Benutzer, eine naive Umkehrung waere nach der Migration rot geworden, weil der Aufruf ohne Benutzer per Absicht beide sieht; jede wurde zu ZWEI (alte Messung unter neuem Namen als gewollte Eigenschaft, Umkehrung MIT Benutzer). 13 Extraktionsstellen im Werkzeug auf die neue Migration umgeleitet. Wirkungslos mit ausgeschaltetem Schalter (Rolle tessera hat BYPASSRLS, live bestaetigt) — blockiert das Live-Gehen am Dienstag nicht. Angenommene offene Flanke, festgehalten statt verschwiegen: ein Aufrufer, der den Benutzer vergisst, sieht den ganzen Mandanten (heutiger Stand, keine Verschlechterung) — WINDOWS #34; die dreistelligen Spec-Zusicherungen sind je Datei, nicht je Methode, das Gate 'keine zweistellige Form' ist ein Shell-Check, nicht CI — vom Verifizierer als Bewusstseinspunkt vermerkt. Verifiziert 13/13 (1020/1020 Tests, Typpruefung sauber, 203/203 Live-Pruefungen; NULLIF durch Rueckbau falsifiziert, 33 Pruefungen rot; alle zehn Regeln live in pg_policies gelesen) 2026-09-11 f0b531b,07fc653,b62a905 260911-nke-mandantentrennung-etappe-3b-benutzerdime
260909-eor Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. Kernfund (#20): forTenant() setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen 2026-09-09 da0ac04 260909-eor-anmeldeweg-mandantenfaehig-machen-und-al
260910-jab Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration 20260910120000_rls_widen_membership_grant_and_platform_read: GroupMembership prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), ModuleGrant prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). SearchProvider bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (TenderRssFeedSourceService.listForUser) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur app.current_tenant). Rule-1-Fix: implizites any in tenders.controller.ts nach der Bindung behoben. npx prisma versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet 2026-09-10 f4f3115,6b23735,03fb3bf 260910-jab-mandantentrennung-die-drei-zu-kurz-greif
260914-ebg WINDOWS #29 geschlossen — Zielrollen-Riegel in UserController.update()/remove(). Ein ADMIN kann den SUPER_ADMIN seines Mandanten nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail) oder loeschen; Riegel nach der Mandantengrenze, vor der Rollenzuweisungs-Pruefung (Vorlage AuthService.adminResetPassword, T-FH9-04). Acht neue Spec-Tests (8 -> 16), Baseline 1020 -> 1028 Tests / 62 Dateien, Falsifizierung durch Rueckbau `Tests 2 failed 14 passed (16)(Test 9/13), unabhaengig vom Verifizierer wiederholt. KopfkommentaradminResetPasswordnachgezogen (T-FH9-05 nicht mehr offen). Ledger 16 offen / 1 zurueckgestellt / 19 geschlossen / 36 gesamt: #29 fixed, NEU #35 (Biome-Konfiguration im Bestand nicht lauffaehig,pnpm lint` Leerlauf) und #36 (Admin-Frontend verschluckt 403 still). Verifiziert 6/6, gepusht. 2026-09-14 759ea3b,63f9df0,70d007b
260914-eym Etappe 3c — Systemkontext fuer die Hintergrunddienste. Migration 20260914120000_rls_system_context_read: is_system_context(), fuenf permissive system_read_policy ... FOR SELECT (DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch); forSystem(prisma) in Array-Form mit ausdruecklichem Zuruecksetzen von Mandant/Benutzer, forTenant() setzt app.system_context zurueck (kein Erben, gemessen). Sechs Faelle: DKV-Planer einmal-abfragen-viele-bedienen (Auftrag je Mandant, WINDOWS #21 fixed); Mail-Transport je Versand aus der SmtpConfig des Empfaenger-Mandanten mit unveraenderter Umgebungs-Rueckfallkette, Startpfad und Mailer-Fabrik entfallen (WINDOWS #30 fixed, SmtpConfig ohne Systemregel); ldap getAllActiveConfigs() und Boot-Nachverschluesselung lesen ueber Systemkontext, schreiben je Mandant gebunden; tender-digest Kandidaten und tender-matching Suchprofile ueber Systemkontext, Schleifen gebunden; admin-seed nur dokumentiert (Tenant ohne Regel). Detektor mit fuenfter Erkennungsform forSystem( und exakter Erlaubnisliste (falsifiziert: Fremddatei 1 rot, Zweitaufruf 2 rot). Werkzeug 203 -> 253 (runSystemContextChecks: ungebunden 0 / System beide Mandanten / Schreiben abgewiesen 42501 bzw. count 0 / kein Erben / pg_policies 34, 5x SELECT). Rueckbau (a) 5 rot mit gelungenem Insert, (b) 1 rot, (c1) 253 gruen + (c2) 5 rot, (d) 2/3 rot. Tests 1028 -> 1054 / 62 -> 64 Dateien, tsc 0, 29 Dateien gegen 5e0e408, Schalter AUS (Compose/.env/Schema/Lockfile unveraendert). Klassifikation 61/179/5, 72 Paare, sechs Zeilen system-gebunden; Kritikschrift (y1)-(y5); Auftrag 3c Erledigt. Ledger 15 offen / 1 zurueckgestellt / 21 geschlossen / 37 gesamt; NEU #37 (prozessweiter Single-Flight-Riegel processInbox). Verifiziert 9/9, gepusht. 2026-09-14 3d64567,6e2a641,939c812 260914-eym-mandantentrennung-etappe-3c-systemkontex
260914-ku1 Zwei Auslieferungskanaele und Versionsstempel. main = Beta (Etiketten beta + latest), Tag vX.Y.Z = Live (Etiketten live + vX.Y.Z), Zweig live ohne Tag nur geprueft — Entscheidung in .gitea/scripts/publish-images.sh (--print-plan), CI-Trigger branches: [main, live] + tags: [v*], fetch-depth: 0. Versionsstempel APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME als Build-Args in beide Dockerfiles (web zur Bauzeit als NEXT_PUBLIC_APP_*, api als Laufzeit-ENV; Vorgabe dev). GET /health/version liefert name/version/channel/commit/buildTime, Startlog Tessera API vX (channel) commit. Web: app-version.ts, AppVersionBadge in sidebar.tsx (sidebar-footer.tsx ist seit ba02b25 toter Code). docker-compose.prod.yml: image: ...:${IMAGE_TAG:-beta}. Betriebshandbuch Kapitel 9 (Zwei Kanaele, Freigabe, Hotfix ohne Datenbankaenderung, neuer Live-Server), ci-cd-setup.md auf gemessenen Stand. Falsifiziert: Build mit v9.9.9-test live -> Stempel in dist und Web-Bundle, ohne Args dev. Echter CI-Lauf 297 gruen (5:18 min), Abbilder beta/latest tragen ea6aa99 beta. Tests API 1054 -> 1060 / 64 -> 65 Dateien, Web 233 -> 243 / 38 -> 40, tsc 0, 20 Dateien gegen 6c19451. Offen: Zweig live + Tag v1.0.0 nach dem Fehler-melden-Knopf anlegen; Handgriffe fuer den User (IMAGE_TAG je Server) im SUMMARY. Verifiziert 8/8, gepusht. 2026-09-14 cdb571c,9731501,ea6aa99 260914-ku1-zwei-auslieferungskanaele-beta-auf-main-

Deferred Items

Items acknowledged and carried forward from previous milestone close:

Category Item Status Deferred At
Live-Test AD WINDOWS #4 am 2026-09-07 geschlossen. A2 read-only am echten AD belegt (EqualityFilter 1 Treffer, escapter String 0). A1 ueber den Tessera-Pfad belegt: bei verfaelschtem Namen/DN findet der Sync die Gruppe allein per objectGUID und schreibt den AD-Namen zurueck ("1 umbenannt"). Nicht gemessen, weil dafuer das Verzeichnis geaendert werden muesste: dass AD den objectGUID bei Umbenennung stabil haelt — zugesicherte AD-Eigenschaft, kein Tessera-Code erledigt 2026-09-07
Live-Test AD WINDOWS #6 am 2026-09-07 geschlossen. (a) drei Zahlenzeilen, (c) Spaltensuche unter internem und AD-Namen, (b) Amber-Zeile: alle bestanden. (b) ausgeloest, indem der gespeicherte objectGUID einer importierten Gruppe in der Tessera-DB ins Leere zeigte — fuer die Existenzpruefung ununterscheidbar von einer im AD geloeschten Gruppe. Markierung wanderte vor der Loeschung zurueck (D-06 haelt) erledigt 2026-09-07
Live-Test E-Mail WINDOWS #12 am 2026-09-09 zurueckgestellt (Ledger: waived). Es gibt intern derzeit kein Postfach, in das Ausschreibungs-Alarme hereinkommen — die Voraussetzung existiert nicht; ein frueheres Missverstaendnis hatte eines angenommen. Nachzuholen, sobald ein Alarm-Postfach eingerichtet ist. Teilentlastung: der handgeschriebene NTLM/EWS-Weg wurde am 2026-09-09 beim Test von #16 erstmals gegen den echten Exchange owa.ctl.de gemessen — Anmeldung, FindFolder und Ordneraufloesung funktionieren (echte ServerVersionInfo 15.2 im Protokoll). Offen bleibt allein das Einlesen einer echten Alarm-Mail zurueckgestellt, Voraussetzung fehlt 2026-09-09

Entscheidung des Users vom 2026-09-07 zum AD-Zugriff: An der AD-Struktur wird nichts veraendert — nicht von Claude, nicht vom User. Keine Testgruppen, keine Umbenennungen, keine Loeschungen. Das Dienstkonto svc_tessera hat nur Lesezugriff, und das ist gewollt. Nie Schreibrechte vorschlagen und nie eine Aenderung im Verzeichnis erbitten.

Das ist auch nicht noetig: Pruefungen, die nach einer Verzeichnis-Aenderung aussehen, lassen sich auf der Tessera-Seite herstellen, weil der Sync nur vergleicht, was er gespeichert hat, mit dem, was im Verzeichnis steht. Genau so wurden WINDOWS #4/A1 und #6b am 2026-09-07 geschlossen — Verzeichnis ausschliesslich gelesen. Siehe 16-LIVETEST-2026-09-07.md.

Entscheidung des Users vom 2026-09-14 zur Mandantenfaehigkeit (ERSETZT die Lesart vom 2026-09-07): Der User will "vorerst von der Mandantenfaehigkeit nichts mehr wissen" — das Thema hat ihn viel Zeit gekostet und er ist darueber veraergert. Gebaut und gepusht sind Etappe 1, 2, 3b und 3c; Tessera laeuft als Ein-Firmen-System vollstaendig (alpha, BYPASSRLS, Schalter AUS), und das reicht ihm. Etappe 3a (Anmeldenamen pro Mandant) und Etappe 4 (Scharfschalten) RUHEN auf unbestimmte Zeit — nicht vorschlagen, nicht als "naechsten Schritt" auflisten, nicht in Zusammenfassungen als offen fuehren; die zugehoerigen Ledger-Eintraege (#18, #22, #23, #25, #26, #28, #31, #32, #33, #34, #37) bleiben stehen, werden aber nicht vorgelegt. Der Schalter bleibt AUS. Neue Funktionen werden weiterhin mandantensicher gebaut (forTenant(), wie bisher), aber ohne das Thema zu benennen. Der User erwaegt, die Mandantenfaehigkeit ganz zu streichen und stattdessen je Kunde einen eigenen Docker-Container zu betreiben, in dem er als Betreiber Module mit Lizenzanzahl freigibt — festgehalten in .planning/todos/pending/2026-09-14-lizenzmodell-freigabe-je-server-mit-lizenzanzahl.md. Diese Entscheidung faellt der User, wenn er sie faellen will; wir stossen sie nicht an.

Entscheidung des Users vom 2026-09-07 zur Mandantenfaehigkeit: Tessera wird zunaechst nur intern eingesetzt. Die Mandantentrennung ist damit vorerst zweitrangig — sie bleibt in der Architektur verankert und wird nicht zurueckgebaut, aber ihre Abnahme hat keine Dringlichkeit. Konkret betrifft das den offenen Abnahmeplan 02-05 (Phase 2), dessen Erfolgskriterium 4 die Datentrennung zweier Mandanten im Browser nachweisen soll: bewusst zurueckgestellt, nicht vergessen. Vor einem Einsatz bei externen Kunden ist er nachzuholen.

Entscheidung des Users vom 2026-09-07: Diese drei Punkte werden spaeter am Live-System geprueft, nicht in der Entwicklungsumgebung. Sie bleiben im Ledger ausdruecklich open und sind NICHT auf waived gesetzt — sie sollen nachgeholt werden, nur eben dort, wo ein echtes Verzeichnis und ein echtes Postfach erreichbar sind. Kein Anlass, sie vorher erneut vorzulegen.

Session Continuity

Last session: 2026-09-14T13:53:23.147Z Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; #29 und 3c als /gsd-quick --validate mit voller Kette durchgefuehrt. Stopped at: 2026-09-14: Quick 260914-ku1 abgeschlossen (3 Commits cdb571c/9731501/ea6aa99 gepusht, CI-Lauf 297 success, :beta-Abbilder mit ea6aa99 beta); offen: Fehler-melden-Knopf, danach Erstfreigabe v1.0.0 (Zweig live + Tag), Server-Handgriffe durch den User Resume file: None Last activity: 2026-09-14 - Completed quick task 260914-ku1: Zwei Auslieferungskanaele (beta/live), Versionsstempel, Versionsanzeige, Betriebshandbuch Kapitel 9