From 51501aa7ee9c9b49744bcaf9a783f9eb88cd7ce0 Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 23 Jul 2026 15:55:23 +0200 Subject: [PATCH] docs(260723-lvg): complete "Jetzt abrufen" quick task summary Co-Authored-By: Claude Opus 4.8 --- .../260723-lvg-SUMMARY.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 .planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-SUMMARY.md diff --git a/.planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-SUMMARY.md b/.planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-SUMMARY.md new file mode 100644 index 0000000..40c890d --- /dev/null +++ b/.planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-SUMMARY.md @@ -0,0 +1,140 @@ +--- +task: quick-260723-lvg +title: "Jetzt abrufen"-Button für Ausschreibungs-Radar (manueller Poll-Trigger) +status: complete +one-liner: Admin-gated POST /modules/tender-radar/poll-now delegating to TenderIngestionService.pollDueSources(), plus a header PollNowButton (DKV spinner/disabled pattern) that refetches ResultsList on success. +key-files: + created: + - apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.tsx + - apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.test.tsx + modified: + - apps/api/src/tenders/tenders.controller.ts + - apps/api/src/tenders/tenders.controller.spec.ts + - apps/web/src/lib/tender-radar-api.ts + - apps/web/src/app/(portal)/modules/tender-radar/page.tsx + - apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json +metrics: + duration: "~35 min" + completed: 2026-07-23 +--- + +# Quick 260723-lvg: "Jetzt abrufen"-Button für Ausschreibungs-Radar Summary + +Added a manual "Jetzt abrufen" poll trigger to the Ausschreibungs-Radar module, +analogous to DKV's "Jetzt prüfen"/check-now. Backend: one admin-gated +`POST /modules/tender-radar/poll-now` endpoint that runs +`TenderIngestionService.pollDueSources()` immediately. Frontend: a +`PollNowButton` in the tender-radar page header next to the settings gear, +using the DKV spinner/disabled button pattern, refetching the result list on +success via a `refreshKey` bump, with i18n error handling on failure. + +## Tasks Completed + +### Task 1: Backend admin-gated POST /poll-now endpoint + controller spec +- Injected `TenderIngestionService` as the LAST constructor param on + `TendersController` (declared optional `?:` in TypeScript so all 7-arg + existing spec call sites still type-check; NestJS DI always resolves it + in production since it's already a provider in `tenders.module.ts`). +- Added `pollNow()` handler: `@Post('poll-now')`, `@Roles(Role.ADMIN, + Role.SUPER_ADMIN)`, declared immediately after `getSourceConfig()` — well + before `@Get(':id')` (route-order Pitfall 5). Body: awaits + `pollDueSources()`, returns `{ ok: true }`. No try/catch — the service + never throws (catch-and-log per tick + per source). +- Added `makeFakeIngestionService()` helper + a new describe block asserting + delegation, return shape, and `@Roles` metadata via + `Reflect.getMetadata(ROLES_KEY, TendersController.prototype.pollNow)`. +- Added a route-declaration-order regression test (`pollNow` index < + `getTender` index) to the existing "route declaration order" describe + block. +- Commit: `7502f97` + +### Task 2: Frontend PollNowButton, pollNow client, refetch wiring + i18n +- `pollNow()` client in `tender-radar-api.ts` — `POST + /modules/tender-radar/poll-now`, `credentials: 'include'`, throws on + non-2xx, mirrors DKV's `checkNow` client shape. +- New `PollNowButton.tsx` (self-contained, `'use client'`): copies the DKV + `SpinnerIcon`, renders the primary button with the DKV opacity/disabled + states, shows a dismissible destructive-text error line on failure. Props: + `{ onPolled?: () => void }`. +- `page.tsx`: added `refreshKey` state, wrapped the settings gear `` + and `` in a right-side flex cluster inside + the existing header row, passed `refreshKey` into ``. +- `ResultsList.tsx`: accepts an optional `refreshKey` prop + (`{ refreshKey = 0 }: { refreshKey?: number } = {}`), added to the `load` + `useCallback` dependency array alongside `paramsKey`. +- `de.json`/`en.json`: added `tenderRadar.page.pollNow`, `pollNowTitle`, + `polling`, `pollError`, `pollErrorDismiss` to both locales (parity spec + green). +- New `PollNowButton.test.tsx`: render/label test, click→pollNow→onPolled + test, pending-spinner/disabled test (deferred promise), rejection→error + copy→no-onPolled test. +- Commit: `bdaf154` + +## Semantic Honesty + +No `force` parameter was added to `pollDueSources()` — out of scope per the +plan. The button labels honestly communicate "fällige Quellen jetzt abrufen" +(fetch DUE sources now): `pollNowTitle` / EN `Fetch due sources now`. A +`'day'`-granularity source (e.g. `doe-opendata`) that already ingested +today's batch is a structural no-op on this click, by design. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking issue] Made `tenderIngestionService` constructor param optional** +- **Found during:** Task 1, `npx tsc --noEmit` after the vitest run passed +- **Issue:** The plan's approach of appending `TenderIngestionService` as a + required (non-optional) 8th constructor param compiles fine at the type + level for new call sites, but breaks TypeScript compilation for every + pre-existing 7-arg `new TendersController(...)` call in + `tenders.controller.spec.ts` (36 call sites) — vitest itself doesn't type-check + so the test run was green, but `tsc --noEmit` caught it (`TS2554: Expected + 8 arguments, but got 7`). +- **Fix:** Declared the param optional (`tenderIngestionService?: + TenderIngestionService`) and used a non-null assertion (`this + .tenderIngestionService!.pollDueSources()`) inside `pollNow()` — safe + because NestJS DI always resolves and injects this provider in production + (already registered in `tenders.module.ts`); only unit tests that never + call `pollNow()` omit it. +- **Files modified:** `apps/api/src/tenders/tenders.controller.ts` +- **Commit:** `7502f97` + +No other deviations — plan executed as written otherwise. + +## Test Results + +- API: `cd apps/api && npx vitest run` → 393/393 passed (was 390/390; +3 new: + delegation, roles metadata, route-order guard). +- API: `cd apps/api && npx tsc --noEmit -p tsconfig.json` → clean. +- Web: `cd apps/web && npx vitest run` → 155/155 passed (was 151/151; +4 new + PollNowButton tests). +- Web: `cd apps/web && npx tsc --noEmit` → clean. +- Targeted verify command (`PollNowButton.test.tsx` + `ResultsList.test.tsx` + + `tenderRadar-parity.spec.ts`): 15/15 passed. + +## Known Stubs + +None. + +## Threat Flags + +None beyond the plan's own threat register (T-lvg-01/02/03), which is fully +addressed: `@Roles(ADMIN, SUPER_ADMIN)` gates the new route, no per-body +privilege input, generic i18n error copy on 403. + +## Self-Check: PASSED + +- `apps/api/src/tenders/tenders.controller.ts` — FOUND +- `apps/api/src/tenders/tenders.controller.spec.ts` — FOUND +- `apps/web/src/lib/tender-radar-api.ts` — FOUND +- `apps/web/src/app/(portal)/modules/tender-radar/page.tsx` — FOUND +- `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx` — FOUND +- `apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.tsx` — FOUND +- `apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.test.tsx` — FOUND +- `apps/web/src/messages/de.json` — FOUND +- `apps/web/src/messages/en.json` — FOUND +- commit `7502f97` — FOUND +- commit `bdaf154` — FOUND