36 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 | 17 | eigene-ausschreibungs-quellen-je-nutzer | executing | 17-02 abgeschlossen: RSS-Feeds bekommen einen Besitzer (D-02), Migration lokal angewendet (alpha noch nicht ausgerollt), Loeschschutz+Obergrenze+D-06-Tagging getestet. Naechster Schritt: Plan 17-03 (UI-Aufteilung). | 2026-08-12T09:48:46.242Z | 2026-08-12 | Phase 17 execution started |
|
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 17 — eigene-ausschreibungs-quellen-je-nutzer
Current Position
Phase: 17 (eigene-ausschreibungs-quellen-je-nutzer) — EXECUTING Plan: 3 of 3 Status: Ready to execute 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-12 — Phase 17 execution started
Progress: [██████████] 98%
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 |
Accumulated Context
Roadmap Evolution
- Phase 17 added (2026-08-12): Eigene Ausschreibungs-Quellen je Nutzer. TenderEmailConfig (heute
tenantId @unique) und TenderRssFeedSource (heuteurl @unique, plattformweit) wandern aufuserId; 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: Backlog2026-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)
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-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 |
— |
Deferred Items
Items acknowledged and carried forward from previous milestone close:
| Category | Item | Status | Deferred At |
|---|---|---|---|
| (none) |
Session Continuity
Last session: 2026-08-12T09:48:46.207Z
Stopped at: 17-02 abgeschlossen: RSS-Feeds bekommen einen Besitzer (D-02), Migration lokal angewendet (alpha noch nicht ausgerollt), Loeschschutz+Obergrenze+D-06-Tagging getestet. Naechster Schritt: Plan 17-03 (UI-Aufteilung).
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