diff --git a/.planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-PLAN.md b/.planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-PLAN.md
new file mode 100644
index 0000000..197d038
--- /dev/null
+++ b/.planning/quick/260723-lvg-jetzt-abrufen-button-fuer-ausschreibungs/260723-lvg-PLAN.md
@@ -0,0 +1,237 @@
+---
+phase: quick-260723-lvg
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_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/PollNowButton.tsx
+ - apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.test.tsx
+ - apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx
+ - apps/web/src/messages/de.json
+ - apps/web/src/messages/en.json
+autonomous: true
+requirements: [quick-260723-lvg]
+
+must_haves:
+ truths:
+ - "An admin clicking 'Jetzt abrufen' triggers an immediate TenderIngestionService.pollDueSources() run on the server"
+ - "The button shows a spinner and is disabled while the poll is in flight (DKV check-now pattern)"
+ - "After a successful poll the tender result list refetches without the user changing any filter"
+ - "A failed poll (e.g. 403 for a non-admin) surfaces a clear i18n error message, list stays intact"
+ - "New tenderRadar.page.* keys exist in BOTH de.json and en.json (parity spec green)"
+ artifacts:
+ - "POST /modules/tender-radar/poll-now route on TendersController, @Roles(ADMIN, SUPER_ADMIN), declared before @Get(':id')"
+ - "pollNow() client in tender-radar-api.ts"
+ - "PollNowButton.tsx self-contained button component + PollNowButton.test.tsx"
+ - "refreshKey wiring: page.tsx passes it, ResultsList.tsx refetches on it"
+ key_links:
+ - "PollNowButton.onPolled -> page.tsx setRefreshKey -> ResultsList refetch"
+ - "pollNow() client -> POST /modules/tender-radar/poll-now -> tenderIngestionService.pollDueSources()"
+---
+
+
+Add a manual "Jetzt abrufen" poll trigger to the Ausschreibungs-Radar, analogous
+to DKV's "Jetzt prüfen"/check-now. Backend: one admin-gated endpoint that runs
+`TenderIngestionService.pollDueSources()` immediately. Frontend: a button in the
+tender-radar page header next to the existing gear, DKV spinner/disabled pattern,
+result-list refetch on success, DE/EN i18n.
+
+Purpose: Give admins an on-demand way to pull due sources without waiting for the
+scheduler tick — the standard operator affordance already present in DKV.
+
+Output: One POST route (+ controller spec), one API client function, one
+PollNowButton component (+ test), a refreshKey wiring in page.tsx/ResultsList,
+and paired de/en i18n keys.
+
+## Semantic honesty (READ FIRST — do NOT introduce a force flag)
+
+`pollDueSources()` does NOT force a re-download. It honors the existing cursor:
+- `'day'`-granularity sources (DÖE `doe-opendata`) fetch only not-yet-ingested
+ days via `nextDayToFetch` — on a fresh DB that is "now", but if today was
+ already ingested this tick is a No-Op for that source.
+- `'tick'`-granularity sources (`rss`, `email-alert`) fetch on every active tick.
+
+The button is therefore "fällige Quellen jetzt abrufen" (fetch DUE sources now),
+NOT "alles neu herunterladen". Label copy and the button `title` must reflect
+this. Adding a `force` parameter to `pollDueSources()` is explicitly OUT OF SCOPE.
+
+
+
+@$HOME/.claude/gsd-core/workflows/execute-plan.md
+@$HOME/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/STATE.md
+@CLAUDE.md
+
+# Backend reference — DKV check-now analog + tenders controller conventions
+@apps/api/src/dkv/dkv.controller.ts
+@apps/api/src/tenders/tenders.controller.ts
+@apps/api/src/tenders/tenders.controller.spec.ts
+@apps/api/src/auth/decorators/roles.decorator.ts
+
+# Frontend reference — DKV button/spinner pattern + tender-radar page/list/api/i18n
+@apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx
+@apps/web/src/lib/dkv-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/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx
+@apps/web/src/lib/tender-radar-api.ts
+@apps/web/src/messages/tenderRadar-parity.spec.ts
+
+
+
+
+
+ Task 1: Add admin-gated POST /poll-now endpoint + controller spec
+ apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.controller.spec.ts
+
+ - `pollNow()` calls `this.tenderIngestionService.pollDueSources()` exactly once and resolves to `{ ok: true }`.
+ - `pollNow` is declared BEFORE `getTender` in the class body (Object.getOwnPropertyNames order), mirroring the Pitfall-5 route-order convention used for every other static route.
+ - `pollNow` carries `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` metadata — assert via `Reflect.getMetadata(ROLES_KEY, TendersController.prototype.pollNow)` (import `ROLES_KEY` from `../auth/decorators/roles.decorator`), expect it to contain both roles.
+ - All existing controller instantiations in the spec (7 positional constructor args) keep passing unchanged.
+
+
+ In `tenders.controller.ts`: inject `TenderIngestionService` by APPENDING it as the
+ LAST constructor parameter (`private readonly tenderIngestionService: TenderIngestionService`)
+ — appending preserves every existing `new TendersController(...7 args...)` call site in the
+ spec (they pass undefined for the new slot and never touch pollNow). Import
+ `TenderIngestionService` from `./tender-ingestion.service`. `TenderIngestionService` is
+ already a provider in `tenders.module.ts`, so no module change is needed.
+
+ Add a new handler `pollNow()` in a clearly-commented "Admin manual poll trigger" section
+ placed immediately AFTER `getSourceConfig` (line ~190) and well before `@Get(':id')`
+ (`getTender`). Decorate with `@Post('poll-now')` and `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`.
+ Because the platform-wide upstream fetch is a DoS/rate lever, gate to admins only (T-lvg-01).
+ Body: `await this.tenderIngestionService.pollDueSources(); return { ok: true };`. Do NOT
+ add a `force` argument — `pollDueSources()` takes none and honors the day-cursor by design.
+ In the handler doc comment, state that this fetches DUE sources now (not a forced
+ re-download) and note it is declared before `@Get(':id')` per the route-order pitfall.
+ `pollDueSources()` never throws (catch-and-log per tick + per source), so no try/catch here.
+
+ In `tenders.controller.spec.ts`: add a `makeFakeIngestionService()` helper returning
+ `{ pollDueSources: vi.fn(async () => undefined) }`. Add a new describe block
+ "TendersController — POST /poll-now (admin manual trigger)" with tests:
+ (1) `pollNow()` calls the fake `pollDueSources` once and returns `{ ok: true }` — construct
+ the controller with the 7 existing fakes PLUS the ingestion fake as the 8th arg;
+ (2) roles metadata via `Reflect.getMetadata(ROLES_KEY, TendersController.prototype.pollNow)`
+ contains `Role.ADMIN` and `Role.SUPER_ADMIN` (import `Role` from `@prisma/client`, `ROLES_KEY`
+ from the roles decorator). Add one test to the existing "route declaration order" describe
+ asserting `pollNow` index < `getTender` index. Leave all existing tests untouched.
+
+
+ cd apps/api && pnpm vitest run src/tenders/tenders.controller.spec.ts
+
+ Controller spec passes: pollNow delegates to pollDueSources, returns {ok:true}, is roles-gated, declared before getTender; all pre-existing tenders.controller tests still green.
+
+
+
+ Task 2: Frontend "Jetzt abrufen" button, pollNow client, refetch wiring + i18n
+ apps/web/src/lib/tender-radar-api.ts, apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.tsx, apps/web/src/app/(portal)/modules/tender-radar/components/PollNowButton.test.tsx, 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
+
+ - PollNowButton renders a button labelled `t('page.pollNow')`; clicking it calls the mocked `pollNow` client once.
+ - While the promise is pending the button is disabled and shows the spinner + `t('page.polling')` copy.
+ - On success it calls the `onPolled` prop callback (drives the list refetch) and shows no error.
+ - On rejection it renders an error message `t('page.pollError')` and does NOT call `onPolled`.
+ - ResultsList refetches when its `refreshKey` prop changes (incremented on poll success).
+ - de.json and en.json define the identical tenderRadar key set (parity spec green).
+
+
+ `tender-radar-api.ts`: add `pollNow()` — `POST ${API_URL}/modules/tender-radar/poll-now`,
+ `credentials: 'include'`, no body; `if (!res.ok) throw new Error('Failed to trigger tender poll');`
+ `return res.json();` Type the return as `Promise<{ ok: boolean }>`. Mirror the DKV
+ `checkNow` client shape.
+
+ `components/PollNowButton.tsx` (NEW, `'use client'`): self-contained unit so it can be tested
+ in isolation (same convention as CoverageBanner/ResultsList/SavedSearchBar tests). Copy the
+ `SpinnerIcon` SVG from the DKV page (20×20, `animate-spin`, `stroke="currentColor"`). Props:
+ `{ onPolled?: () => void }`. Local state: `isPolling` (bool), `pollError` (string|null). Uses
+ `useTranslations('tenderRadar')`. Root is `<div className="flex flex-col items-end gap-1">`
+ so it drops into the header's right cluster and grows an error line downward. Render a primary
+ button mirroring the DKV "Jetzt prüfen" button styles (`rounded bg-primary px-4 py-2 text-sm
+ font-medium text-primary-foreground ... flex items-center`, `disabled={isPolling}`, opacity/cursor
+ when polling). Button `title={t('page.pollNowTitle')}` (honest "fällige Quellen jetzt abrufen").
+ While `isPolling`: `<SpinnerIcon />{t('page.polling')}`; else `t('page.pollNow')`.
+ `handlePoll`: set `isPolling` true + clear error, `await pollNow()`, on success call
+ `onPolled?.()`, catch → `setPollError(t('page.pollError'))`, finally `setIsPolling(false)`.
+ When `pollError` is set, render a small right-aligned `text-xs text-destructive` line with the
+ message and a dismiss "×" button (`aria-label={t('page.pollErrorDismiss')}`) that clears it.
+
+ `page.tsx`: import `useState` from react and `PollNowButton`. Add
+ `const [refreshKey, setRefreshKey] = useState(0);` in `TenderRadarContent`. Wrap the existing
+ gear `<Link>` and the new `<PollNowButton onPolled={() => setRefreshKey((k) => k + 1)} />`
+ together in a right-side `<div className="flex items-center gap-2">` inside the existing
+ header `flex items-start justify-between` row (button sits NEXT TO the gear). Change the list
+ render to `<ResultsList refreshKey={refreshKey} />`.
+
+ `ResultsList.tsx`: accept an optional prop — change the signature to
+ `export function ResultsList({ refreshKey = 0 }: { refreshKey?: number } = {})`. Add
+ `refreshKey` to the `load` `useCallback` dependency array (alongside `paramsKey`, keeping the
+ existing eslint-disable line) so an incremented `refreshKey` re-runs `load()` and refetches the
+ list without any URL/filter change. This is the same parent-increments-refreshKey pattern DKV
+ uses for InvoiceHistoryTable (STATE decision 07-05).
+
+ `de.json` + `en.json`: add under `tenderRadar.page` (both locales — parity is enforced by
+ tenderRadar-parity.spec.ts, missing-in-either fails): `pollNow`, `pollNowTitle`, `polling`,
+ `pollError`, `pollErrorDismiss`. Suggested DE: "Jetzt abrufen" / "Fällige Quellen jetzt
+ abrufen" / "Wird abgerufen…" / "Der Abruf konnte nicht ausgelöst werden. Möglicherweise fehlen
+ Ihnen die nötigen Rechte." / "Schließen". EN mirrors: "Fetch now" / "Fetch due sources now" /
+ "Fetching…" / "The fetch could not be triggered. You may not have the required permissions." /
+ "Dismiss".
+
+ `PollNowButton.test.tsx` (NEW): mirror ResultsList.test.tsx setup — `vi.mock('@/lib/tender-radar-api', ...)`
+ exposing a `mockPollNow`; `vi.mock('next-intl', ...)` with a flat key→copy lookup for the
+ `page.*` keys used. Tests: (1) renders the pollNow label; (2) click calls `pollNow` once and, on
+ resolve, calls the `onPolled` prop (use `waitFor`); (3) while pending the button is disabled and
+ shows the polling copy (resolve a deferred promise to observe the transition); (4) on reject it
+ shows the pollError copy and never calls `onPolled`. Use `@testing-library/react`
+ render/fireEvent/waitFor/cleanup, `afterEach` reset, matching the existing test file style.
+
+
+ cd apps/web && pnpm vitest run src/app/\(portal\)/modules/tender-radar/components/PollNowButton.test.tsx src/app/\(portal\)/modules/tender-radar/components/ResultsList.test.tsx src/messages/tenderRadar-parity.spec.ts
+
+ PollNowButton test green (render, click→pollNow, spinner/disabled, error→no onPolled); ResultsList test still green with the new optional prop; tenderRadar de/en parity spec green.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| browser → API (POST /poll-now) | authenticated client triggers a platform-wide upstream fetch |
+| API → external tender sources | pollDueSources fans out to DÖE/RSS/etc. upstreams |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-lvg-01 | Denial of Service | POST /modules/tender-radar/poll-now | medium | mitigate | `@Roles(ADMIN, SUPER_ADMIN)` gates the trigger — non-admins get 403; upstream fetch is not user-open. pollDueSources honors the day-cursor so repeated clicks are largely No-Op for `day` sources (idempotent). |
+| T-lvg-02 | Elevation of Privilege | poll-now handler | low | mitigate | Global JwtAuthGuard + RolesGuard enforce the `@Roles` decorator; no per-body privilege input. Spec asserts roles metadata is present. |
+| T-lvg-03 | Information Disclosure | 403 error surfaced in UI | low | accept | Generic i18n error copy; no backend internals leaked. Button visible to all, action denied server-side. |
+
+
+
+- API: `cd apps/api && pnpm vitest run src/tenders/tenders.controller.spec.ts` green.
+- Web: `cd apps/web && pnpm vitest run` for the three targeted specs green.
+- No `force` argument added to `pollDueSources()` anywhere (semantic honesty).
+- New i18n keys present in BOTH de.json and en.json.
+
+
+
+- POST /modules/tender-radar/poll-now exists, admin-gated, declared before @Get(':id'), delegates to pollDueSources(), returns {ok:true}.
+- Header button next to the gear triggers the poll with DKV spinner/disabled UX; success refetches the list; failure shows an i18n error.
+- All targeted API + web specs green; parity spec green.
+- Two atomic commits (Task 1 backend, Task 2 frontend). No push, no docker restart.
+
+
+
diff --git a/.planning/v1.1-MILESTONE-AUDIT.md b/.planning/v1.1-MILESTONE-AUDIT.md
new file mode 100644
index 0000000..02403f9
--- /dev/null
+++ b/.planning/v1.1-MILESTONE-AUDIT.md
@@ -0,0 +1,41 @@
+# v1.1 Ausschreibungs-Radar — Milestone Audit
+
+**Milestone:** v1.1 (Phasen 10–14)
+**Audited:** 2026-07-24
+**Verdict:** GAPS — 1 neuer Blocker, 1 Warnung, 2 akzeptierte Deferrals
+
+## Phasen-Verifikationen (aggregiert)
+
+| Phase | Status | Score |
+|-------|--------|-------|
+| 10 Foundation & DÖE | passed | 5/5 |
+| 11 Filter/UI/Saved Searches | passed | 5/5 |
+| 12 Notifications | passed | 4/4 |
+| 13 Scraping-Adapter + Dedup | gaps_found | 3/4 (Truth 3 Dedup dormant, Option C) |
+| 14 RSS/E-Mail/Rollout | human_needed | 4/5 (EWS-Live-Test offen) |
+
+Alle 24 Requirements sind in REQUIREMENTS.md als `[x]` markiert. Der Integrations-Checker bestätigt: die Ingestion-Pipeline aller 5 Quellen ist verdrahtet (Registry → pollDueSources → Normalizer → Tender), Filter/UI/Notifications/Denylist/i18n sind end-to-end verbunden. ABER:
+
+## BLOCKER — NetServer/cosinex-Adapter haben keinen Aktivierungspfad
+
+**Betroffene REQ-IDs: INGEST-02, INGEST-03, CONFIG-02 (teilweise)**
+
+- `ai-netserver` und `cosinex-dtvp` `TenderSourcePollConfig`-Zeilen werden mit `isActive: false` geseedet, Kommentar „activation deferred to Phase 14 UI".
+- Aber Phase 14 hat diese UI nie geliefert: `SourceConfigDto` (`dto/source-config.dto.ts`) hat KEIN `sourceType`-Feld; `GET/PUT /modules/tender-radar/source-config` (`tenders.controller.ts:43,190-215`) ist auf `DOE_SOURCE_TYPE = 'doe-opendata'` hartverdrahtet. `SourceConfigForm.tsx` spricht denselben Single-Source-Endpoint an.
+- Es existiert KEINE Route/Service-Methode/UI, die `isActive` für `ai-netserver`/`cosinex-dtvp` umschalten kann.
+- Netto: Die in Phase 13 gebauten, getesteten, registrierten Adapter (INGEST-02/03) sind produktiv unerreichbar — sie bleiben für immer `isActive:false` (außer manuellem DB-Write). Widerspricht Phase-13-Kriterien 1/2 und CONFIG-02 („Admin verwaltet Quellen-Poll-Konfiguration").
+
+**Fix (klein):** `SourceConfigDto`/Endpoint auf beliebigen `sourceType` generalisieren (nicht nur DÖE) + `SourceConfigForm` alle pollbaren Quellen auflisten und togglebar machen. Denylist-Gate bleibt (vergabe24/aumass werden ohnehin nicht registriert).
+
+## WARNUNG — Scheduler-Cron hängt allein am DÖE-Config
+
+`TenderSchedulerService.onApplicationBootstrap()` (`tender-scheduler.service.ts:73-89`) registriert den einen globalen Cron NUR, wenn `doe-opendata` aktiv ist; die admin `source-config` PUT-Route ruft `stopJob()`, wenn DÖE deaktiviert wird. Da dieser eine Cron via `pollDueSources()` ALLE aktiven Quellen fächert, würde ein Deaktivieren von DÖE still auch RSS + E-Mail-Ingestion abschalten. Architektur-Schuld (Phase-13/14-Fan-out auf Phase-10-Single-Source-Scheduler gesetzt, ohne die Bootstrap/Stop-Bedingung auf „irgendeine aktive Quelle" umzustellen). Nicht user-facing im Normalfall (DÖE ist active-by-default, kein Toggle für die anderen exponiert) → WARNUNG, nicht Blocker.
+
+## Akzeptierte Deferrals (keine neuen Findings)
+
+1. **SCHEMA-03 Cross-Source-Fingerprint-Dedup dormant** — `dedupActive = activePortalCount >= 2`; Fingerprint-CPV-Asymmetrie (Option C, 13-VERIFICATION.md). Hinweis: durch den Blocker oben verschärft — mit inaktiven NetServer/cosinex sind praktisch nur DÖE+RSS aktiv, echte Cross-Source-Overlaps bleiben unwahrscheinlich.
+2. **14-03 EWS/Exchange-Live-Test** — braucht echtes Postfach, vom User bewusst vertagt.
+
+## Empfehlung
+
+Den Blocker (Quellen-Aktivierungs-UI) vor dem formalen v1.1-Abschluss schließen — er ist klein und macht INGEST-02/03 + CONFIG-02 erst wirklich wahr. Die Scheduler-Warnung optional gleich mitnehmen. EWS-Test + Dedup-dormant bleiben legitime „braucht-Live-Daten"-Deferrals nach v1.2.