diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 355b344..69d93f5 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -410,7 +410,7 @@ Plans: 3. Activating a new search profile with many pre-existing historical matches does not flood the user with individual emails -- backfill is suppressed/batched on first activation, and a match already notified once (digest or instant) is never notified again for the same tender+profile pair (explicit matched-vs-notified state) 4. Notification emails are sent through the tenant's own SMTP configuration (reusing the DKV mail pattern), not a shared/global system mailer -**Plans**: 3/4 plans executed +**Plans**: 4/4 plans executed **UI hint**: yes **Wave 1** @@ -424,7 +424,7 @@ Plans: **Wave 3** *(blocked on Wave 2)* - [x] 12-03-PLAN.md — Sofort-Alerts am Poll-Tick + Ein-Mail-Garantie über beide Kanäle (NOTIFY-02/03) -- [ ] 12-04-PLAN.md — Web-UI: Digest-Intervall + Sofort-Alert-Toggle, Pref-Backend (NOTIFY-01/02) +- [x] 12-04-PLAN.md — Web-UI: Digest-Intervall + Sofort-Alert-Toggle, Pref-Backend (NOTIFY-01/02) ### Phase 13: Scraping Adapters & Cross-Source Deduplication @@ -476,6 +476,6 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 | 9. Cert Manager Module | 6/6 | Complete | 2026-07-02 | | 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 | | 11. Filter Engine, Results UI & Saved Searches | 6/6 | In Progress| | -| 12. Tender Notifications | 3/4 | In Progress| | +| 12. Tender Notifications | 4/4 | In Progress| | | 13. Scraping Adapters & Cross-Source Deduplication | 0/TBD | Not started | - | | 14. RSS, Email-Alert Ingestion & Module Rollout | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index f734d0e..b955e8b 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.1 milestone_name: Ausschreibungs-Radar current_phase: 12 current_phase_name: tender-notifications -status: executing -stopped_at: Completed 12-03-PLAN.md -last_updated: "2026-07-22T07:23:12.233Z" +status: verifying +stopped_at: Completed 12-04-PLAN.md +last_updated: "2026-07-22T07:32:37.037Z" last_activity: 2026-07-22 last_activity_desc: Phase 12 execution started progress: total_phases: 12 - completed_phases: 10 + completed_phases: 11 total_plans: 56 - completed_plans: 54 + completed_plans: 55 --- # Project State @@ -29,10 +29,10 @@ See: .planning/PROJECT.md (updated 2026-07-17) Phase: 12 (tender-notifications) — EXECUTING Plan: 4 of 4 -Status: Ready to execute +Status: Phase complete — ready for verification Last activity: 2026-07-22 — Phase 12 execution started -Progress: [██████████] 96% +Progress: [██████████] 98% ## Performance Metrics @@ -89,6 +89,7 @@ Progress: [██████████] 96% | 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 | ## Accumulated Context @@ -189,6 +190,7 @@ Recent decisions affecting current work: - [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 ### Pending Todos @@ -226,7 +228,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-07-22T07:23:12.215Z -Stopped at: Completed 12-03-PLAN.md +Last session: 2026-07-22T07:32:37.021Z +Stopped at: Completed 12-04-PLAN.md 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 diff --git a/.planning/phases/12-tender-notifications/12-04-SUMMARY.md b/.planning/phases/12-tender-notifications/12-04-SUMMARY.md new file mode 100644 index 0000000..fe718d2 --- /dev/null +++ b/.planning/phases/12-tender-notifications/12-04-SUMMARY.md @@ -0,0 +1,189 @@ +--- +phase: 12-tender-notifications +plan: 04 +subsystem: api+ui +tags: [nestjs, prisma, class-validator, nextjs, tender-radar, notifications] + +requires: + - phase: 12-tender-notifications (12-01/12-02/12-03) + provides: "TenderNotificationPref + instantAlert schema fields, digest scheduler (daily default due-check), instant-dispatch mail path" +provides: + - "GET/PUT /modules/tender-radar/notification-pref (per-user digestInterval CRUD, IDOR-safe)" + - "instantAlert passthrough on saved-search create/update DTOs + service" + - "Web UI: digest-interval selector (settings page) + Sofort-Alert toggle per profile (SavedSearchBar)" +affects: [14-tender-i18n-and-admin-sources] + +tech-stack: + added: [] + patterns: + - "Per-user pref CRUD via upsert on @@unique(userId) — same scoping convention as TenderSavedSearchService/TenderTriageService, no forTenant()/RLS" + - "Route declared before @Get(':id') to avoid the NestJS route-order shadowing pitfall" + +key-files: + created: + - apps/api/src/tenders/dto/notification-pref.dto.ts + - apps/api/src/tenders/tender-notification-pref.service.ts + - apps/api/src/tenders/tender-notification-pref.service.spec.ts + modified: + - apps/api/src/tenders/dto/saved-search.dto.ts + - apps/api/src/tenders/tender-saved-search.service.ts + - apps/api/src/tenders/tender-saved-search.service.spec.ts + - apps/api/src/tenders/tenders.controller.ts + - apps/api/src/tenders/tenders.controller.spec.ts + - apps/api/src/tenders/tenders.module.ts + - apps/web/src/lib/tender-radar-api.ts + - "apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx" + - "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.tsx" + - "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx" + +key-decisions: + - "Digest-interval selector kept inline in settings/page.tsx (already a 'use client' shell) rather than split into a separate component — no standalone unit-test coverage was called for in this plan for that piece" + - "instantAlert toggle uses a plain HTML checkbox with aria-label 'Sofort-Alert für {name}', not a shadcn switch, matching the existing minimal chip-based SavedSearchBar UI" + +patterns-established: + - "Notification-pref GET/PUT routes declared immediately after the saved-searches DELETE route and before @Get(':id') — same static-route-before-param-route convention as source-config/coverage/triage/saved-searches" + +requirements-completed: [NOTIFY-01, NOTIFY-02] + +coverage: + - id: D1 + description: "Per-user digest interval (Täglich/Wöchentlich/Aus) is readable/writable via GET/PUT /modules/tender-radar/notification-pref, defaults to 'daily', IDOR-safe" + requirement: "NOTIFY-01" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-notification-pref.service.spec.ts#TenderNotificationPrefService (4 tests)" + status: pass + - kind: unit + ref: "apps/api/src/tenders/tenders.controller.spec.ts#GET/PUT notification-pref (NOTIFY-01, per-user, T-12-14)" + status: pass + human_judgment: false + - id: D2 + description: "Digest-interval selector in the tender-radar settings page loads current value on mount and persists changes" + requirement: "NOTIFY-01" + verification: [] + human_judgment: true + rationale: "UI persistence-across-reload behavior is the end-of-phase UAT item (settings page reload check) — not covered by an automated component test in this plan" + - id: D3 + description: "Per-profile Sofort-Alert toggle (instantAlert) persists via the existing saved-search create/update path, default off" + requirement: "NOTIFY-02" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-saved-search.service.spec.ts#instantAlert passthrough (3 tests)" + status: pass + - kind: unit + ref: "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx#Sofort-Alert toggle (2 tests)" + status: pass + human_judgment: false + - id: D4 + description: "All notification-pref and saved-search routes derive userId/tenantId exclusively from extractTriageContext(req), never body/query (IDOR)" + requirement: "NOTIFY-01" + verification: + - kind: unit + ref: "apps/api/src/tenders/tenders.controller.spec.ts#derives userId from req.user ... never from a query param (V4/IDOR)" + status: pass + human_judgment: false + +duration: 12min +completed: 2026-07-22 +status: complete +--- + +# Phase 12 Plan 04: Digest-Intervall + Sofort-Alert Bedienoberfläche Summary + +**Per-user digest-interval CRUD (GET/PUT notification-pref, default 'daily') plus instantAlert passthrough on saved-search create/update — both wired into a settings-page selector and a per-profile SavedSearchBar toggle, closing the loop from 12-01/02/03's pipeline to a user-controllable UI.** + +## Performance + +- **Duration:** ~12 min +- **Completed:** 2026-07-22 +- **Tasks:** 3/3 completed +- **Files modified:** 10 (3 created, 7 modified) + +## Accomplishments + +- `TenderNotificationPrefService` (per-user digestInterval CRUD, default `'daily'` when no row exists, upsert on `@@unique(userId)`) + `UpdateNotificationPrefDto` (`@IsIn(['daily','weekly','off'])`) +- `GET`/`PUT /modules/tender-radar/notification-pref`, declared before `@Get(':id')` (route-order pitfall avoided, regression test added) +- `instantAlert` now flows through `Create/UpdateSavedSearchDto` and `TenderSavedSearchService.create/update` (optional, `@IsBoolean`, falls back to the Prisma column default `false` when omitted) +- `tender-radar-api.ts`: `fetchNotificationPref`/`saveNotificationPref`, and `SavedSearch`/`CreateSavedSearchPayload`/`UpdateSavedSearchPayload` extended with `instantAlert` +- Settings page: "Benachrichtigungen" section with a Täglich/Wöchentlich/Aus select, loads via `fetchNotificationPref` on mount, saves via `saveNotificationPref` on change +- `SavedSearchBar`: per-profile Sofort-Alert checkbox (`aria-label="Sofort-Alert für {name}"`), reflects `profile.instantAlert`, calls `updateSavedSearch(id, { instantAlert })` + reloads on toggle + +## Task Commits + +1. **Task 1: Backend — Pref-Service + Routen + instantAlert-Durchreichung** - `9e7ba53` (feat, TDD RED→GREEN) +2. **Task 2: Frontend API-Client — Pref-Funktionen + instantAlert in SavedSearch** - `73a7e49` (feat) +3. **Task 3: Frontend UI — Digest-Intervall-Auswahl + Sofort-Alert-Toggle** - `d60080e` (feat) + +_Note: no separate metadata commit yet — this SUMMARY/STATE/ROADMAP commit follows._ + +## Files Created/Modified + +- `apps/api/src/tenders/dto/notification-pref.dto.ts` - `UpdateNotificationPrefDto` (`@IsIn`) +- `apps/api/src/tenders/tender-notification-pref.service.ts` - per-user digestInterval CRUD, default `'daily'`, upsert on `userId` +- `apps/api/src/tenders/tender-notification-pref.service.spec.ts` - RED-first TDD spec (4 tests) +- `apps/api/src/tenders/dto/saved-search.dto.ts` - `instantAlert?: boolean` on both Create/Update DTOs +- `apps/api/src/tenders/tender-saved-search.service.ts` - passes `instantAlert` through create/update when supplied +- `apps/api/src/tenders/tender-saved-search.service.spec.ts` - 3 new instantAlert passthrough tests +- `apps/api/src/tenders/tenders.controller.ts` - `getNotificationPref`/`setNotificationPref` routes + constructor DI +- `apps/api/src/tenders/tenders.controller.spec.ts` - fake pref service added to all 20 controller instantiations + new route-order/IDOR tests +- `apps/api/src/tenders/tenders.module.ts` - registers `TenderNotificationPrefService`, doc comment updated +- `apps/web/src/lib/tender-radar-api.ts` - `NotificationPref` type + `fetchNotificationPref`/`saveNotificationPref`; `instantAlert` added to `SavedSearch`/payloads +- `apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx` - Benachrichtigungen section (digest-interval select) +- `apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.tsx` - per-profile Sofort-Alert checkbox +- `apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx` - checkbox-state + toggle-calls-updateSavedSearch tests + +## Decisions Made + +- Kept the digest-interval selector inline in `settings/page.tsx` rather than extracting a separate `NotificationPrefForm.tsx` component — the page is already a `'use client'` shell (unlike `SourceConfigForm`'s admin-only split), and this plan's file list/test scope didn't call for standalone unit coverage of that piece. +- Used a plain checkbox (not a switch) for the Sofort-Alert toggle to match the existing minimal chip-based `SavedSearchBar` styling. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Updated `tenders.controller.spec.ts` fakes for the new constructor parameter** +- **Found during:** Task 1 (adding `TenderNotificationPrefService` to `TendersController`'s constructor) +- **Issue:** Adding a 5th required constructor parameter broke all 20 existing `new TendersController(...)` call sites in the spec file (missing-argument TS errors) +- **Fix:** Added a `makeFakeNotificationPrefService()` helper and appended it as the 5th argument to every existing constructor call; also added two new tests (route-declaration-order guard + GET/PUT IDOR-scoping proof) for the new routes +- **Files modified:** `apps/api/src/tenders/tenders.controller.spec.ts` +- **Verification:** `npx tsc --noEmit` clean; full API test suite (19 files / 209 tests) green +- **Committed in:** `9e7ba53` (Task 1 commit) + +--- + +**Total deviations:** 1 auto-fixed (blocking — test compilation) +**Impact on plan:** Necessary to keep the existing test suite compiling and passing after the planned constructor change. No scope creep — this is the direct, unavoidable consequence of the plan's own "Injiziere TenderNotificationPrefService in den Controller-Konstruktor" instruction. + +## Issues Encountered + +None. + +## IDOR Mitigation (T-12-14/15/16) + +- `UpdateNotificationPrefDto` and both saved-search DTOs carry no `userId`/`tenantId` field — both routes derive them exclusively from `extractTriageContext(req)` (cookie-backed auth context), never from body/query. +- `TenderNotificationPrefService` scopes every read/write by `userId` via the `@@unique(userId)` constraint — a foreign `userId` can never read or overwrite another user's pref (proven in `tender-notification-pref.service.spec.ts`: "getForUser() is scoped strictly by userId"). +- `digestInterval` is validated server-side to exactly `'daily'|'weekly'|'off'` via `@IsIn` (T-12-15) before it reaches the service/scheduler due-check logic. +- `instantAlert` validated via `@IsBoolean`; ownership of the saved-search profile is still enforced in `TenderSavedSearchService.update()` (pre-existing NotFound-collapse pattern, T-11-14) — unchanged by this plan. +- No new package installs; no new trust boundary beyond the two already-modeled routes (matches `12-04-PLAN.md`'s threat register — no `## Threat Flags` needed). + +## Known Stubs + +None. + +## User Setup Required + +None - no external service configuration required. Both `TenderNotificationPref.digestInterval` and `TenderSavedSearch.instantAlert` columns already existed in the local dev DB (seeded in Plan 12-01) — no migration needed for this plan. + +## Next Phase Readiness + +- NOTIFY-01/02 are now fully wired end-to-end: user sets digest interval / Sofort-Alert in the web UI → the 12-01/02/03 matching/digest/instant pipeline reads and honors those settings. +- Remaining end-of-phase UAT (per `12-04-PLAN.md` ``): manually confirm in the running app that (a) setting the interval to "Wöchentlich" and reloading the settings page keeps the value, and (b) toggling a profile's Sofort-Alert and reloading keeps it on. Requires a docker rebuild/restart of the web+api containers to pick up these changes (per project convention, the user performs the rebuild, not this agent). +- Phase 12 is otherwise code-complete pending that UAT + docker rebuild (consistent with the phase-level pause note in `eb10bd3`). + +--- +*Phase: 12-tender-notifications* +*Completed: 2026-07-22* + +## Self-Check: PASSED + +All 12 listed files/paths verified present on disk; all 3 task commit hashes (`9e7ba53`, `73a7e49`, `d60080e`) verified present in git log.