Files
tessera-ctl/.planning/STATE.md
T
schalli 637866b9f2
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 54s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
docs: close the DOE notice-link backlog item
Records the user's choice between the two candidate link targets (the notice
page on oeffentlichevergabe.de, not the awarding portal's own page), what was
checked before touching the adapter, and the trap in verifying it: the target
is a single-page app that answers 200 with an identical shell for any id, so
only rendered content proves the link works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:45:44 +02:00

33 KiB

gsd_state_version, milestone, milestone_name, current_phase, current_phase_name, status, stopped_at, last_updated, last_activity, last_activity_desc, progress
gsd_state_version milestone milestone_name current_phase current_phase_name status stopped_at last_updated last_activity last_activity_desc progress
1.0 v1.1 Ausschreibungs-Radar 16 AD-Gruppen-Synchronisation complete Phase 16 abgeschlossen — UAT durchgefuehrt, kritischer Sweep-Fehler gefunden und behoben (260811-f9i), Rest-Annahme A1 bewusst als offen akzeptiert 2026-08-11T09:25:00.000Z 2026-08-11 DOE-Bekanntmachungslinks repariert (260811-j04)
total_phases completed_phases total_plans completed_plans
16 15 80 78

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: Phase 16 — AD-Gruppen-Synchronisation

Current Position

Phase: 16 (AD-Gruppen-Synchronisation) — COMPLETE Plan: 5 of 5 Status: Abgeschlossen. UAT 2/5/6 bestanden, 7 teilweise, 1/3/4/8 bewusst uebersprungen (kein AD-Schreibzugriff, Entscheidung des Users 2026-08-11). UAT-Test 2 hat einen kritischen Fehler in der Loescherkennung aufgedeckt, der beim ersten echten Sync alle AD-gebundenen Gruppen entfernt haette — behoben in 260811-f9i. Offene Annahme A1 (objectGUID uebersteht Umbenennung) steht dokumentiert in 16-VERIFICATION.md. Last activity: 2026-08-11 — UAT abgeschlossen, Sweep-Fix ausgeliefert

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

Accumulated Context

Roadmap Evolution

  • 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

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 aus dem Plan-Verification-Block nicht ausgefuehrt (kein Browser-Tool in dieser Session) — vor /gsd-ship nachholen, siehe WINDOWS.md unrun-verify #1

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-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-

Deferred Items

Items acknowledged and carried forward from previous milestone close:

Category Item Status Deferred At
(none)

Session Continuity

Last session: 2026-08-11T09:25:00.000Z Stopped at: Phase 16 abgeschlossen — UAT durchgefuehrt, objectGUID-Sweep repariert und gepusht Resume file: None Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created