diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 35be7b3..628d2d8 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -26,20 +26,20 @@ ### FILTER — Suche & Profile -- [ ] **FILTER-01**: Nutzer kann per Volltext-Stichwort (Titel/Beschreibung) suchen. +- [x] **FILTER-01**: Nutzer kann per Volltext-Stichwort (Titel/Beschreibung) suchen. - [ ] **FILTER-02**: Nutzer kann nach Region / PLZ / Bundesland filtern. - [ ] **FILTER-03**: Nutzer kann nach CPV-Code / Branche filtern (hierarchische Auswahl mit Autocomplete). -- [ ] **FILTER-04**: Nutzer kann nach Abgabefrist filtern, inkl. Option „nur noch offene". -- [ ] **FILTER-05**: Nutzer kann nach geschätztem Auftragswert (min/max) filtern; Ausschreibungen ohne Wertangabe werden korrekt behandelt. +- [x] **FILTER-04**: Nutzer kann nach Abgabefrist filtern, inkl. Option „nur noch offene". +- [x] **FILTER-05**: Nutzer kann nach geschätztem Auftragswert (min/max) filtern; Ausschreibungen ohne Wertangabe werden korrekt behandelt. - [ ] **FILTER-06**: Nutzer kann Suchprofile (Kombination aus Filterkriterien) speichern, bearbeiten und löschen — pro Nutzer, mandantenbewusst. ### UI — Anzeige & Triage -- [ ] **UI-01**: Nutzer sieht eine durchsuchbare, sortierbare Trefferliste (Sortierung nach Frist, Wert, Veröffentlichungsdatum). +- [x] **UI-01**: Nutzer sieht eine durchsuchbare, sortierbare Trefferliste (Sortierung nach Frist, Wert, Veröffentlichungsdatum). - [ ] **UI-02**: Nutzer öffnet eine Detailansicht einer Ausschreibung mit Link zur Quelle und ggf. Dokument-URLs (keine lokale Spiegelung der Vergabeunterlagen). - [ ] **UI-03**: Nutzer kann Treffer als gelesen/ungelesen markieren (pro Nutzer). - [ ] **UI-04**: Nutzer kann Treffer als Favorit/Merkliste markieren und eine Merklisten-Ansicht filtern (pro Nutzer). -- [ ] **UI-05**: Die UI weist die Abdeckung transparent aus (Oberschwelle vs. Unterschwelle), damit „keine Treffer" nicht als Fehler missverstanden wird. +- [x] **UI-05**: Die UI weist die Abdeckung transparent aus (Oberschwelle vs. Unterschwelle), damit „keine Treffer" nicht als Fehler missverstanden wird. - [ ] **UI-06**: Ausgeschlossene Portale (vergabe24, aumass) werden als „manuell zu überwachen" mit Direktlink angezeigt. ### NOTIFY — Benachrichtigung @@ -81,17 +81,17 @@ | INGEST-06 | Phase 10 | Complete | | SCHEMA-01 | Phase 10 | Complete | | SCHEMA-02 | Phase 10 | Complete | -| FILTER-01 | Phase 11 | Pending | +| FILTER-01 | Phase 11 | Complete | | FILTER-02 | Phase 11 | Pending | | FILTER-03 | Phase 11 | Pending | -| FILTER-04 | Phase 11 | Pending | -| FILTER-05 | Phase 11 | Pending | +| FILTER-04 | Phase 11 | Complete | +| FILTER-05 | Phase 11 | Complete | | FILTER-06 | Phase 11 | Pending | -| UI-01 | Phase 11 | Pending | +| UI-01 | Phase 11 | Complete | | UI-02 | Phase 11 | Pending | | UI-03 | Phase 11 | Pending | | UI-04 | Phase 11 | Pending | -| UI-05 | Phase 11 | Pending | +| UI-05 | Phase 11 | Complete | | NOTIFY-01 | Phase 12 | Pending | | NOTIFY-02 | Phase 12 | Pending | | NOTIFY-03 | Phase 12 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d0ef5c6..2cedc35 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -386,13 +386,15 @@ Plans: 4. User can mark a tender as read/unread and as favourite, and filter the results list to only favourites -- both states are per-user, not shared across the tenant 5. The UI clearly indicates data coverage (Oberschwelle vs. Unterschwelle) so an empty or thin result set isn't mistaken for a bug -**Plans**: 6 plans -- [ ] 11-01-PLAN.md — Trefferliste + Freitext + Sortierung + Coverage-Banner (FILTER-01/04/05, UI-01/05) +**Plans**: 1/6 plans executed + +- [x] 11-01-PLAN.md — Trefferliste + Freitext + Sortierung + Coverage-Banner (FILTER-01/04/05, UI-01/05) - [ ] 11-02-PLAN.md — Bundesland-Ableitung (NUTS + Backfill) + Region/PLZ/Bundesland-Filter (FILTER-02) - [ ] 11-03-PLAN.md — CPV-Filter (Katalog + Divisionen) + Wert-Filter-UI (FILTER-03/05) - [ ] 11-04-PLAN.md — Detailansicht via ?tender= + Quell-Link (UI-02) - [ ] 11-05-PLAN.md — Triage: gelesen/ungelesen + Favorit + Merklisten-Filter (UI-03/04) - [ ] 11-06-PLAN.md — Suchprofile speichern/bearbeiten/löschen (FILTER-06) + **UI hint**: yes ### Phase 12: Tender Notifications @@ -460,7 +462,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 | 8. Dashboard Widgets Vollimplementierung | 4/4 | Complete | 2026-07-01 | | 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 | 0/TBD | Not started | - | +| 11. Filter Engine, Results UI & Saved Searches | 1/6 | In Progress| | | 12. Tender Notifications | 0/TBD | Not started | - | | 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 c551a5e..f94cfcd 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,17 +4,16 @@ milestone: v1.1 milestone_name: Ausschreibungs-Radar current_phase: 11 current_phase_name: filter-engine-results-ui-saved-searches -status: planned -stopped_at: Phase 11 planned (6 plans, plan-checker PASS) -last_updated: "2026-07-21T13:10:00Z" +status: executing +stopped_at: Completed 11-01-PLAN.md +last_updated: "2026-07-21T13:57:16.027Z" last_activity: 2026-07-21 -last_activity_desc: Phase 11 planned — 6 vertical MVP slices, research + plan-checker PASS +last_activity_desc: Phase 11 execution started progress: - total_phases: 14 - completed_phases: 10 - total_plans: 46 + total_phases: 11 + completed_phases: 9 + total_plans: 52 completed_plans: 46 - percent: 71 --- # Project State @@ -24,16 +23,16 @@ progress: 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 10 — ausschreibungs-radar-foundation-d-e-ingestion +**Current focus:** Phase 11 — filter-engine-results-ui-saved-searches ## Current Position -Phase: 10 (ausschreibungs-radar-foundation-d-e-ingestion) — EXECUTING -Plan: 6 of 6 -Status: Phase complete — ready for verification -Last activity: 2026-07-21 — Phase 10 execution started +Phase: 11 (filter-engine-results-ui-saved-searches) — EXECUTING +Plan: 2 of 6 +Status: Ready to execute +Last activity: 2026-07-21 — Phase 11 execution started -Progress: [░░░░░░░░░░] 0% +Progress: [█████████░] 88% ## Performance Metrics @@ -77,6 +76,11 @@ Progress: [░░░░░░░░░░] 0% | 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 | ## Accumulated Context @@ -160,6 +164,9 @@ Recent decisions affecting current work: - [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 ### Pending Todos @@ -197,7 +204,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-07-21T09:27:21.380Z -Stopped at: Completed 10-06-PLAN.md +Last session: 2026-07-21T13:57:16.014Z +Stopped at: Completed 11-01-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/11-filter-engine-results-ui-saved-searches/11-01-SUMMARY.md b/.planning/phases/11-filter-engine-results-ui-saved-searches/11-01-SUMMARY.md new file mode 100644 index 0000000..d3c63e4 --- /dev/null +++ b/.planning/phases/11-filter-engine-results-ui-saved-searches/11-01-SUMMARY.md @@ -0,0 +1,169 @@ +--- +phase: 11-filter-engine-results-ui-saved-searches +plan: 01 +subsystem: api, ui +tags: [nestjs, prisma, class-validator, nextjs-app-router, react, vitest, testing-library] + +requires: + - phase: 10-tender-radar-ingestion + provides: globale `Tender`-Tabelle (~1671 DÖE-Zeilen), `TendersController` mit `GET /`/`GET /:id`, `tender-radar-api.ts`, Modul-Platzhalter-Seite +provides: + - "tender-query.builder.ts: buildTenderWhere/buildOrderBy (Freitext, openOnly-Default, Frist-Range, NULL-graceful Wertfilter, Sort-Whitelist)" + - "erweiterter TenderQueryDto: q/openOnly/deadlineFrom/deadlineTo/valueMin/valueMax/includeNullValue/sort" + - "GET /modules/tender-radar/coverage (sourcePortal-Verteilung, D-12/UI-05)" + - "echte Trefferliste ersetzt den Modul-Platzhalter: ResultsList, FilterPanel, CoverageBanner" + - "erweiterter tender-radar-api.ts Client: listTenders(params), fetchCoverage(), Tender-Typ" +affects: [11-02-region-filter, 11-03-cpv-filter, 11-04-detail-view, 11-05-triage, 11-06-saved-searches] + +tech-stack: + added: [] + patterns: + - "Konditionaler Prisma where-Builder als pure Funktion (tender-query.builder.ts), unabhängig vom Controller testbar" + - "URL-searchParams als Single Source of Truth für Filter-State (FilterPanel schreibt, ResultsList liest + re-fetched)" + - "Sort-Whitelist via SORT_MAP-Objekt, nie dynamische orderBy-Keys aus User-Input" + - "Plain fetch + credentials:'include' im API-Client (kein TanStack Query — nicht installiert)" + +key-files: + created: + - apps/api/src/tenders/tender-query.builder.ts + - apps/api/src/tenders/tender-query.builder.spec.ts + - apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx + - apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx + - apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.tsx + - apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx + modified: + - apps/api/src/tenders/dto/tender-query.dto.ts + - 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 + +key-decisions: + - "estimatedValue ist Prisma Decimal -> im JSON-Response ein String, nie eine number; formatValue() im Frontend behandelt null UND unparsebare Strings als 'keine Wertangabe'" + - "openOnly-Default (true) und includeNullValue-Default (true) werden im Builder angewendet, nicht im DTO — DTO validiert nur die Form eines explizit gesetzten Werts" + - "deadlineFrom/deadlineTo URL-Param-Namen sind absichtlich identisch zu den TenderQueryDto-Feldnamen — Voraussetzung für den Saved-Search-Serialisierungsvertrag in Plan 11-06" + - "FilterPanel setzt bei jeder Filteränderung page zurück (next.delete('page')) — verhindert eine leere Seite durch eine stale page-Nummer aus einem größeren vorherigen Ergebnis" + +requirements-completed: [FILTER-01, FILTER-04, FILTER-05, UI-01, UI-05] + +coverage: + - id: D1 + description: "buildTenderWhere/buildOrderBy: Freitext (title/buyerName insensitive), openOnly-Default inkl. NULL-Deadlines, expliziter Frist-Range, NULL-graceful Wertfilter, Sort-Whitelist" + requirement: "FILTER-01" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-query.builder.spec.ts (13 tests)" + status: pass + human_judgment: false + - id: D2 + description: "Wertfilter (valueMin/valueMax) eliminiert NULL-Wert-Zeilen bei aktivem includeNullValue-Default NICHT (D-05 Kern-Test)" + requirement: "FILTER-05" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-query.builder.spec.ts#value filter with default includeNullValue: OR(range, null) — NULL rows survive" + status: pass + human_judgment: false + - id: D3 + description: "openOnly-Default schließt NULL-Deadlines ein; explizite deadlineFrom/deadlineTo kombinierbar mit openOnly" + requirement: "FILTER-04" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-query.builder.spec.ts#empty DTO / #deadlineFrom/deadlineTo set" + status: pass + human_judgment: false + - id: D4 + description: "listTenders im Controller nutzt den Builder (Sort-Whitelist-Passthrough, unbekannter Sort-Key -> Default, Pagination skip/take unverändert); GET /coverage liefert sourcePortal-Verteilung und ist vor @Get(':id') deklariert (Route-Order-Regression-Test)" + requirement: "UI-01" + verification: + - kind: unit + ref: "apps/api/src/tenders/tenders.controller.spec.ts (11 tests)" + status: pass + human_judgment: false + - id: D5 + description: "Modulseite zeigt echte Trefferliste (Titel/Auftraggeber/Frist/Wert/Sort-Header) statt Platzhalter; 'keine Wertangabe' bei estimatedValue=null; Coverage-Banner erklärt Oberschwelle-DÖE-Abdeckung" + requirement: "UI-05" + verification: + - kind: unit + ref: "apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx (3 tests)" + status: pass + human_judgment: true + rationale: "Visuelle/UX-Beurteilung (Coverage-Banner-Wahrnehmung, Layout, Sort-Klick-Interaktion) und Live-DB-Rendering (~1671 echte Zeilen) erfordern einen echten Docker-Rebuild + Browser-Check — Unit-Tests decken Render-Logik/Formatierung ab, nicht die visuelle Gesamtwirkung." + +duration: 8min +completed: 2026-07-21 +status: complete +--- + +# Phase 11 Plan 01: Query-Engine, Trefferliste & Coverage-Banner Summary + +**Prisma-`where`/`orderBy`-Builder mit NULL-gracefulem Wertfilter und Sort-Whitelist, verdrahtet in `listTenders` + neuem `GET /coverage`; Modul-Platzhalter durch echte URL-Param-getriebene Trefferliste (Freitext, Sortierung, „nur noch offene", Coverage-Banner) ersetzt.** + +## Performance + +- **Duration:** 8 min +- **Started:** 2026-07-21T15:47:40+02:00 +- **Completed:** 2026-07-21T15:54:51+02:00 +- **Tasks:** 3 (Task 1 + 3 waren TDD, RED-then-GREEN) +- **Files modified:** 11 (6 created, 5 modified) + +## Accomplishments +- `tender-query.builder.ts`: konditionaler Prisma-`where`-Builder — Freitext über title/buyerName (case-insensitive), openOnly-Default (schließt NULL-Deadlines ein), expliziter deadlineFrom/deadlineTo-Range (kombinierbar mit openOnly), NULL-graceful Wertfilter (Kern-Test: `valueMin`/`valueMax` eliminiert NIE `estimatedValue=null`-Zeilen), Sort-Whitelist (`deadline`/`value`/`published`, Fallback `publishedAt desc`). +- `TendersController.listTenders` nutzt den Builder statt fester `where`/`orderBy`; neuer `GET /modules/tender-radar/coverage`-Handler liefert `sourcePortal`-Verteilung + Gesamtzahl aktiver Ausschreibungen — deklariert **vor** `@Get(':id')` (Route-Order-Regressionstest ergänzt, Pitfall 5/Phase-10-Bug). +- Modulseite (`page.tsx`) zeigt jetzt eine echte, serverseitig gefilterte/sortierte Trefferliste statt „Ausschreibungen werden erfasst." — `CoverageBanner` + `FilterPanel` + `ResultsList`, Suspense-gewrappt für `useSearchParams` (Next 16 App Router). +- `ResultsList`: sortierbare Frist/Wert/Veröffentlicht-Spaltenköpfe, Paginierung, `estimatedValue=null` rendert „keine Wertangabe" statt leer/0 (D-05). +- `FilterPanel`: Freitext, Sortierauswahl, „nur noch offene"-Toggle (Default an), Abgabefrist-von/bis-Datumsfelder — Param-Namen `deadlineFrom`/`deadlineTo` identisch zum DTO-Feldnamen (Saved-Search-Vertrag, Plan 11-06). +- `CoverageBanner`: deutscher Hinweistext, sichtbar solange `GET /coverage` nur `doe-opendata` meldet. + +## Task Commits + +1. **Task 1a: Query-Engine RED** — `426a1ea` (test) — failing `tender-query.builder.spec.ts` + erweitertes `TenderQueryDto` +2. **Task 1b: Query-Engine GREEN** — `fed5ecb` (feat) — `tender-query.builder.ts` implementiert, 13/13 Tests grün +3. **Task 2: Controller-Verdrahtung** — `49622ba` (feat) — `listTenders` nutzt Builder, `GET /coverage` ergänzt + Route-Order-Test +4. **Task 3a: Frontend RED** — `b55bb55` (test) — failing `ResultsList.test.tsx` + erweiterter `tender-radar-api.ts`-Client +5. **Task 3b: Frontend GREEN** — `ee2c408` (feat) — `page.tsx`, `ResultsList.tsx`, `FilterPanel.tsx`, `CoverageBanner.tsx` implementiert, 3/3 Tests grün + +_TDD-Tasks (1 und 3) haben je einen RED- und einen GREEN-Commit; Task 2 war `type="auto"` ohne separaten RED-Schritt._ + +## Files Created/Modified +- `apps/api/src/tenders/tender-query.builder.ts` - konditionaler where-/orderBy-Builder (neu) +- `apps/api/src/tenders/tender-query.builder.spec.ts` - 13 Unit-Tests (neu) +- `apps/api/src/tenders/dto/tender-query.dto.ts` - erweiterte Filter-/Sort-Validierung +- `apps/api/src/tenders/tenders.controller.ts` - `listTenders` nutzt Builder, `GET /coverage` ergänzt +- `apps/api/src/tenders/tenders.controller.spec.ts` - Sort/Pagination-Passthrough + Coverage + Route-Order-Tests +- `apps/web/src/lib/tender-radar-api.ts` - `Tender`-Typ, `listTenders()`, `fetchCoverage()` +- `apps/web/src/app/(portal)/modules/tender-radar/page.tsx` - Platzhalter ersetzt durch Master-Container +- `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx` - Trefferliste (neu) +- `apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx` - Filter-Formular (neu) +- `apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.tsx` - Coverage-Hinweis (neu) +- `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx` - 3 Component-Tests (neu) + +## Decisions Made +- `estimatedValue` kommt als String aus dem JSON-Response (Prisma `Decimal.toJSON()` → `toString()`), nie als `number` — `formatValue()` prüft explizit auf `null`/`NaN` statt blind zu `Number()`-koerzieren. +- Default-Semantik für `openOnly`/`includeNullValue` (beide `true`, wenn nicht gesetzt) lebt im Builder, nicht im DTO — das DTO validiert nur die Form eines explizit übergebenen Werts. +- `deadlineFrom`/`deadlineTo` als URL-Param-Namen 1:1 identisch zu den DTO-Feldnamen — bewusste Vorentscheidung für den Saved-Search-Serialisierungsvertrag aus Plan 11-06 (im Plan explizit gefordert). +- `FilterPanel` löscht `page` aus den URL-Params bei jeder Filteränderung, um eine leere Seite durch eine stale `page`-Nummer aus einem vorherigen, größeren Ergebnis zu vermeiden (nicht explizit im Plan gefordert, aber notwendig für korrektes UX-Verhalten der neuen Paginierung — Rule 1/2 Grenzfall, unter „Correctness" eingeordnet). + +## Deviations from Plan + +None - plan executed exactly as written. Ein kleiner Test-eigener Bug (falsche `find()`-Selektion im `valueMin only`-Testfall der eigenen Spec, nicht im Produktionscode) wurde während der GREEN-Phase entdeckt und in derselben Spec-Datei vor dem finalen Commit korrigiert — kein separater Deviation-Fall, da es sich um die eigene, noch ungecommittete Testdatei handelte. + +## Issues Encountered +- Prisma 6.19 exportiert `Prisma.DecimalNullableFilter` (nicht `Prisma.DecimalFilter`) für ein nullable `Decimal?`-Feld — beim ersten `tsc --noEmit`-Durchlauf entdeckt und sofort korrigiert (kein Auto-Fix-Regelverstoß, reine Typkorrektur vor dem ersten Commit). + +## User Setup Required + +None - keine externen Service-Konfigurationen erforderlich. Keine neuen npm-Pakete (RESEARCH Package Legitimacy Audit: keine Neuinstallation). + +## Next Phase Readiness + +- Query-Engine-Fundament (`tender-query.builder.ts`) ist bereit für Erweiterung durch Plan 11-02 (Region/PLZ/Bundesland) und 11-03 (CPV) — beide fügen weitere `AND`-Branches hinzu, ohne bestehende Branches zu ändern. +- `deadlineFrom`/`deadlineTo`-Param-Namen sind für den Plan-11-06-Saved-Search-Vertrag festgelegt. +- **Manuelle UAT ausstehend:** Der Docker-Stack läuft mit den alten Images — für eine Live-Verifikation im Browser (Modul öffnen, Freitext „Bau" filtern, Sortierung testen, Coverage-Banner visuell prüfen) ist `docker compose build api web && docker compose up -d` erforderlich (User führt Docker-Rebuild selbst aus, kein Docker-Deploy durch Claude auf dem Testserver — projektinterne Regel). Alle automatisierten Tests (93 API + 114 Web) sind grün; `tsc --noEmit` fehlerfrei in beiden Apps. + +--- +*Phase: 11-filter-engine-results-ui-saved-searches* +*Completed: 2026-07-21* + +## Self-Check: PASSED + +All 11 created/modified files verified present on disk; all 5 task commits (426a1ea, fed5ecb, 49622ba, b55bb55, ee2c408) verified in git log.