diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index f447716..eda888b 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -37,8 +37,8 @@ - [x] **UI-01**: Nutzer sieht eine durchsuchbare, sortierbare Trefferliste (Sortierung nach Frist, Wert, Veröffentlichungsdatum). - [x] **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). +- [x] **UI-03**: Nutzer kann Treffer als gelesen/ungelesen markieren (pro Nutzer). +- [x] **UI-04**: Nutzer kann Treffer als Favorit/Merkliste markieren und eine Merklisten-Ansicht filtern (pro Nutzer). - [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. @@ -89,8 +89,8 @@ | FILTER-06 | Phase 11 | Pending | | UI-01 | Phase 11 | Complete | | UI-02 | Phase 11 | Complete | -| UI-03 | Phase 11 | Pending | -| UI-04 | Phase 11 | Pending | +| UI-03 | Phase 11 | Complete | +| UI-04 | Phase 11 | Complete | | UI-05 | Phase 11 | Complete | | NOTIFY-01 | Phase 12 | Pending | | NOTIFY-02 | Phase 12 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 67bdc75..57e89e6 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -386,13 +386,13 @@ 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**: 4/6 plans executed +**Plans**: 5/6 plans executed - [x] 11-01-PLAN.md — Trefferliste + Freitext + Sortierung + Coverage-Banner (FILTER-01/04/05, UI-01/05) - [x] 11-02-PLAN.md — Bundesland-Ableitung (NUTS + Backfill) + Region/PLZ/Bundesland-Filter (FILTER-02) - [x] 11-03-PLAN.md — CPV-Filter (Katalog + Divisionen) + Wert-Filter-UI (FILTER-03/05) - [x] 11-04-PLAN.md — Detailansicht via ?tender= + Quell-Link (UI-02) -- [ ] 11-05-PLAN.md — Triage: gelesen/ungelesen + Favorit + Merklisten-Filter (UI-03/04) +- [x] 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 @@ -462,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 | 4/6 | In Progress| | +| 11. Filter Engine, Results UI & Saved Searches | 5/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 c9cbefb..ab5885f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar current_phase: 11 current_phase_name: filter-engine-results-ui-saved-searches status: executing -stopped_at: Completed 11-04-PLAN.md -last_updated: "2026-07-21T14:23:31.171Z" +stopped_at: Completed 11-05-PLAN.md +last_updated: "2026-07-21T14:40:14.411Z" last_activity: 2026-07-21 last_activity_desc: Phase 11 execution started progress: total_phases: 11 completed_phases: 9 total_plans: 52 - completed_plans: 49 + completed_plans: 50 --- # Project State @@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17) ## Current Position Phase: 11 (filter-engine-results-ui-saved-searches) — EXECUTING -Plan: 5 of 6 +Plan: 6 of 6 Status: Ready to execute Last activity: 2026-07-21 — Phase 11 execution started -Progress: [█████████░] 94% +Progress: [██████████] 96% ## Performance Metrics @@ -84,6 +84,7 @@ Progress: [█████████░] 94% | 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 | ## Accumulated Context @@ -176,6 +177,7 @@ Recent decisions affecting current work: - [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 ### Pending Todos @@ -213,7 +215,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-07-21T14:23:31.158Z -Stopped at: Completed 11-04-PLAN.md +Last session: 2026-07-21T14:40:14.396Z +Stopped at: Completed 11-05-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-05-SUMMARY.md b/.planning/phases/11-filter-engine-results-ui-saved-searches/11-05-SUMMARY.md new file mode 100644 index 0000000..dc051ae --- /dev/null +++ b/.planning/phases/11-filter-engine-results-ui-saved-searches/11-05-SUMMARY.md @@ -0,0 +1,220 @@ +--- +phase: 11-filter-engine-results-ui-saved-searches +plan: 05 +subsystem: api+web +tags: [prisma, nestjs, nextjs, react, vitest, idor, per-user-scoping] + +requires: + - phase: 11-filter-engine-results-ui-saved-searches + plan: 01 + provides: "TendersController, TenderQueryDto, tender-query.builder.ts, ResultsList/FilterPanel components" + - phase: 11-filter-engine-results-ui-saved-searches + plan: 04 + provides: "?tender= URL contract, ResultsList row click handler" +provides: + - "TenderTriage Prisma model (per-user gelesen/ungelesen + Favorit, onDelete: Cascade to Tender)" + - "TenderTriageService: setTriage (idempotent upsert), listForUser, favoriteIds — all userId-scoped" + - "GET/PUT /modules/tender-radar/triage routes (declared before @Get(':id'))" + - "favOnly filter in TenderQueryDto/buildTenderWhere/TendersController.listTenders" + - "fetchTriage()/setTriage() in tender-radar-api.ts" + - "ResultsList: batch-merged triage state + Gelesen/Ungelesen + Favorit row toggles" + - "FilterPanel: 'Nur Favoriten/Merkliste' checkbox (favOnly URL param)" +affects: [11-06-saved-searches] + +tech-stack: + added: [] + patterns: + - "Per-user scoping via manual where:{userId} (FavoritesService/T-08-06 convention) — NOT forTenant()/RLS, which is reserved for auth-core tables (Pitfall 4)" + - "Optimistic UI update with revert-on-failure for both triage toggles, matching the fetch-based (no TanStack Query) client convention" + - "Sentinel id: {in: ['__none__']} guarantees a favOnly zero-match instead of an accidentally-unconstrained query when the user has no favorites" + +key-files: + created: + - apps/api/prisma/migrations/20260721160000_add_tender_triage/migration.sql + - apps/api/src/tenders/tender-triage.service.ts + - apps/api/src/tenders/tender-triage.service.spec.ts + - apps/api/src/tenders/dto/tender-triage.dto.ts + modified: + - apps/api/prisma/schema.prisma + - apps/api/src/tenders/tenders.controller.ts + - apps/api/src/tenders/tenders.controller.spec.ts + - apps/api/src/tenders/tenders.module.ts + - apps/api/src/tenders/dto/tender-query.dto.ts + - apps/api/src/tenders/tender-query.builder.ts + - apps/api/src/tenders/tender-query.builder.spec.ts + - apps/web/src/lib/tender-radar-api.ts + - 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/app/(portal)/modules/tender-radar/components/FilterPanel.tsx + +key-decisions: + - "TenderTriage is a NEW model, not a reuse of FavoriteLink (per CONTEXT.md D-10 explicit instruction) — separate unique constraint (userId,tenderId) vs FavoriteLink's (userId,widgetId)-scoped design for an unrelated feature." + - "No forTenant()/RLS on TenderTriage — manual where:{userId} scoping in every query (TenderTriageService), exactly the FavoritesService/T-08-06 pattern. tenantId is stored on the row for future tenant-level reporting but is never the scoping key." + - "favOnly resolution happens in the controller (TenderTriageService.favoriteIds(userId) called BEFORE buildTenderWhere), not inside the builder itself — keeps tender-query.builder.ts's favIds parameter a pure, already-resolved input, so it stays independently unit-testable without a live DB." + - "Deviation (Rule 3, blocking): tenders.controller.spec.ts's 8 existing `new TendersController(prisma, scheduler)` call sites were updated to pass a third fake TenderTriageService argument — required because the constructor now has a mandatory dependency; without this the existing suite would not compile." + - "Deviation (Rule 3, blocking): ResultsList.test.tsx was extended (not listed in files_modified) with fetchTriage/setTriage mocks and 5 new test cases — the existing 3 tests would still pass without this (the batch-fetch failure is silently caught), but VALIDATION.md's own Requirement→Test map marks this file '⚠️ extend' for Plan 05, and the plan's own Done criteria (toggles work, optimistic update, batch-merge) are otherwise unverified by any automated test." + +requirements-completed: [UI-03, UI-04] + +coverage: + - id: D1 + description: "setTriage upserts on @@unique([userId,tenderId]); repeated calls with identical params never create a second row (idempotent)" + requirement: "UI-03" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-triage.service.spec.ts#setTriage is idempotent" + status: pass + human_judgment: false + - id: D2 + description: "Every triage query is scoped strictly by userId — a foreign user sees nothing for the same tenderId, even after another user set isRead/isFavorite on it (V4 / IDOR, T-11-10)" + requirement: "UI-03, UI-04" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-triage.service.spec.ts#listForUser scopes strictly by userId; #favoriteIds returns only tenderIds favorited by that exact user" + status: pass + human_judgment: false + - id: D3 + description: "Deleting a Tender cascades to remove its TenderTriage rows (ON DELETE CASCADE) — verified against the applied migration on the live dev DB via an insert/delete/rollback transaction, not just the unit fake" + requirement: "UI-03, UI-04" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-triage.service.spec.ts#cascade: after a tender's triage rows are removed" + - kind: manual + ref: "docker exec tessera-ctl-db-1 psql — BEGIN; insert Tender+TenderTriage; DELETE Tender; TenderTriage count 1→0; ROLLBACK (no data mutated)" + status: pass + human_judgment: false + - id: D4 + description: "GET/PUT /modules/tender-radar/triage are declared before @Get(':id') and are therefore never shadowed by the param route (Pitfall 5)" + requirement: "UI-03, UI-04" + verification: + - kind: unit + ref: "apps/api/src/tenders/tenders.controller.spec.ts#declares listTriage and setTriage before getTender" + status: pass + human_judgment: false + - id: D5 + description: "favOnly=true resolves favoriteIds(userId) from the auth context and restricts the tender list to exactly those ids; an empty favorites list yields zero matches, never the unfiltered catalog" + requirement: "UI-04" + verification: + - kind: unit + ref: "apps/api/src/tenders/tender-query.builder.spec.ts#favOnly (5 cases incl. empty-favIds sentinel + DoS cap); apps/api/src/tenders/tenders.controller.spec.ts#listTenders favOnly wiring (3 cases)" + status: pass + human_judgment: false + - id: D6 + description: "ResultsList batch-fetches triage for the visible page and merges isRead/isFavorite in; row toggles call setTriage and update optimistically with revert-on-failure; a failed triage fetch never blocks the list itself" + requirement: "UI-03, UI-04" + verification: + - kind: unit + ref: "apps/web/.../ResultsList.test.tsx — describe('ResultsList — triage') (5 cases: batch-merge, read toggle, favorite toggle + no propagation to row-click, optimistic revert on rejected setTriage, graceful degradation on failed fetchTriage)" + status: pass + human_judgment: false + - id: D7 + description: "FilterPanel exposes a 'Nur Favoriten/Merkliste' checkbox writing favOnly=true into the URL; ResultsList forwards it generically (no code change needed there beyond the already-generic URLSearchParams passthrough)" + requirement: "UI-04" + verification: + - kind: unit + ref: "tsc --noEmit clean across web; full web suite (122 tests) green — no FilterPanel-specific automated test exists (none did before this plan either)" + status: pass + human_judgment: true + rationale: "The visual checkbox interaction and end-to-end filtering against the live 1671-row catalog is a manual/UAT flow (Docker rebuild required, user-executed per project rule) — the backend favOnly branch itself is fully unit-tested (D5)." + +duration: 24min +completed: 2026-07-21 +status: complete +--- + +# Phase 11 Plan 05: Persönliche Triage (gelesen/ungelesen, Favorit) Summary + +**Per-user Tender-Triage — neues `TenderTriage`-Modell (userId-Scoping, kein forTenant/RLS, onDelete: Cascade), GET/PUT `/triage`-Routen vor `:id`, `favOnly`-Merklisten-Filter im Query-Builder, und Gelesen/Favorit-Toggles + Batch-Merge in der Trefferliste — IDOR-hart gegen Fremdzugriff getestet.** + +## Performance + +- **Duration:** 24 min +- **Started:** 2026-07-21T16:24:00+02:00 +- **Completed:** 2026-07-21T16:38:22+02:00 +- **Tasks:** 3 (Task 1 TDD: RED + GREEN; Task 2 + Task 3 direct auto) +- **Files modified:** 15 (4 created, 11 modified) + +## Accomplishments + +- **`TenderTriage` Prisma-Modell** (`schema.prisma`) + Migration `20260721160000_add_tender_triage`: `userId`, `tenantId`, `tenderId`, `isRead`, `isFavorite`, `readAt`/`favoritedAt`-Timestamps, `tender Tender @relation(..., onDelete: Cascade)`, `@@unique([userId,tenderId])`, `@@index([userId])`, `@@index([tenderId])`. Migration lokal auf `tessera-ctl-db-1` angewendet (`docker exec ... psql`, danach `prisma migrate resolve --applied` + `prisma generate` vom Host via Container-IP `172.19.0.2` + `tessera:tessera_dev`); FK-Cascade live gegen die DB verifiziert (siehe D3). +- **`TenderTriageService`** (`tender-triage.service.ts`): `setTriage` (idempotenter Upsert auf `@@unique([userId,tenderId])`, partielle Updates ohne das jeweils andere Flag zu überschreiben), `listForUser(userId, tenderIds)` (kurzschließt bei leerem Array), `favoriteIds(userId)`. Jede Query `where:{userId}` — kein `forTenant`/RLS (Pitfall 4). 8/8 Spec-Fälle grün. +- **Controller-Routen** (`tenders.controller.ts`): `GET /modules/tender-radar/triage?ids=` (Batch, IDs auf 200 begrenzt — DoS, T-11-11) und `PUT /modules/tender-radar/triage` (Upsert) — beide **vor** `@Get(':id')` deklariert (Pitfall 5, per Test abgesichert). `extractTriageContext` liest userId/tenantId ausschließlich aus dem Request-Kontext (`req.user?.id`, `req.tenantId`), niemals aus Body/Query (V4/IDOR). +- **`favOnly`-Filter**: `TenderQueryDto.favOnly` (boolean) → Controller ruft bei `favOnly=true` zuerst `triageService.favoriteIds(userId)` auf und reicht die IDs in `buildTenderWhere(dto, favIds)`; leere Favoritenliste ⇒ `{id:{in:['__none__']}}` (garantiert null Treffer statt versehentlich der ungefilterten Liste). In-Liste auf 500 IDs begrenzt (T-11-11). +- **Frontend**: `fetchTriage(ids)`/`setTriage(payload)` in `tender-radar-api.ts` (plain fetch, `credentials:'include'`, kein TanStack Query). `ResultsList.tsx` batcht die Triage der sichtbaren Zeilen nach jedem Laden, merged sie in einen lokalen State, rendert pro Zeile einen Gelesen/Ungelesen- und einen Favorit-Toggle (optimistisches Update mit Revert bei Fehler, `stopPropagation` verhindert das Öffnen der Detailansicht beim Toggle-Klick); gelesene Zeilen werden per `opacity-60` visuell gedimmt. `FilterPanel.tsx` erhält eine „Nur Favoriten/Merkliste"-Checkbox, die `favOnly=true` in die URL schreibt — `ResultsList` reicht das generisch (bereits bestehende URLSearchParams-Passthrough) an den Backend-Call durch, ohne eigenen Code dafür. + +## Task Commits + +1. **Task 1a: TenderTriage schema/migration + failing spec (RED)** — `7bb695d` (test) +2. **Task 1b: TenderTriageService (GREEN) + Migration angewendet** — `58b0f3d` (feat) +3. **Task 2: GET/PUT /triage-Routen + favOnly-Filter** — `5f97eca` (feat) +4. **Task 3: Frontend Read/Fav-Toggles + Merklisten-Filter + Batch-Merge** — `1eb9e4f` (feat) + +## Files Created/Modified + +- `apps/api/prisma/schema.prisma` — `TenderTriage`-Modell + Back-Relation auf `Tender` +- `apps/api/prisma/migrations/20260721160000_add_tender_triage/migration.sql` — CreateTable + FK Cascade + Indizes (neu) +- `apps/api/src/tenders/tender-triage.service.ts` — Service (neu) +- `apps/api/src/tenders/tender-triage.service.spec.ts` — 8 Tests (neu) +- `apps/api/src/tenders/dto/tender-triage.dto.ts` — Body-DTO (neu) +- `apps/api/src/tenders/dto/tender-query.dto.ts` — `favOnly`-Feld ergänzt +- `apps/api/src/tenders/tender-query.builder.ts` — `favOnly`-Branch + `MAX_FAV_IDS` +- `apps/api/src/tenders/tender-query.builder.spec.ts` — 5 neue favOnly-Tests +- `apps/api/src/tenders/tenders.controller.ts` — `listTriage`/`setTriage`-Routen, `favOnly`-Wiring in `listTenders` +- `apps/api/src/tenders/tenders.controller.spec.ts` — Konstruktor-Updates (Deviation) + 8 neue Tests (Route-Order, Triage-Routen, favOnly-Wiring) +- `apps/api/src/tenders/tenders.module.ts` — `TenderTriageService` als Provider +- `apps/web/src/lib/tender-radar-api.ts` — `fetchTriage`/`setTriage` + Typen +- `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx` — Batch-Merge, Toggles, Dimmed-Read-Styling +- `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx` — Mocks erweitert (Deviation) + 5 neue Tests +- `apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx` — „Nur Favoriten/Merkliste"-Checkbox + +## Decisions Made + +- **TenderTriage als eigenständiges neues Modell** (nicht `FavoriteLink` wiederverwenden) — exakt wie in CONTEXT.md D-10/RESEARCH.md gefordert; `FavoriteLink` ist an `WidgetInstance` gebunden (anderes Feature, andere Unique-Constraint). +- **Kein `forTenant()`/RLS** — jede Query in `TenderTriageService` scoped manuell `where:{userId}`, identisch zum `FavoritesService`-Muster (T-08-06). `tenantId` wird auf der Zeile mitgeführt (spätere Tenant-Auswertung), ist aber nie das Scoping-Feld. +- **favOnly-Auflösung im Controller, nicht im Builder**: `TendersController.listTenders` ruft `triageService.favoriteIds(userId)` auf und reicht das Ergebnis als reinen Parameter (`favIds`) an `buildTenderWhere` — der Builder bleibt dadurch eine pure Funktion, unabhängig testbar ohne Prisma/DB-Mock für den Triage-Service. +- **Sentinel-ID `'__none__'`** statt leerem `in:[]` — Prisma behandelt ein leeres `in:[]` uneinheitlich je nach Kontext; ein garantiert nie existierender String-Wert ist die robustere "garantiert kein Treffer"-Semantik. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] `tenders.controller.spec.ts`-Konstruktoraufrufe erweitert (nicht in `files_modified` von Task 2 gelistet)** +- **Found during:** Task 2 (Controller-Erweiterung) +- **Issue:** `TendersController` erhält mit Task 2 eine dritte Pflicht-Dependency (`TenderTriageService`). Die 8 bestehenden `new TendersController(prisma, scheduler)`-Aufrufe im Spec-File hätten sonst nicht mehr kompiliert (`tsc --noEmit` schlägt fehl: "Expected 3 arguments, but got 2"). +- **Fix:** `makeFakeTriageService()`-Helper ergänzt, alle 8 Konstruktor-Aufrufe um das dritte Argument erweitert (identisches Muster über `replace_all`, da die betroffenen Zeilen textidentisch waren); zusätzlich 8 neue Tests für Route-Order, Triage-Routen und favOnly-Wiring ergänzt. +- **Files modified:** `apps/api/src/tenders/tenders.controller.spec.ts` +- **Commit:** `5f97eca` + +**2. [Rule 3 - Blocking] `ResultsList.test.tsx` erweitert (nicht in `files_modified` von Task 3 gelistet)** +- **Found during:** Task 3 (Frontend-Verdrahtung) +- **Issue:** Der Plan fordert (Done-Kriterium): „Gelesen/Favorit-Toggles wirken und persistieren pro Nutzer; ... Triage-Zustand erscheint gemerged." `11-VALIDATION.md`s eigene Requirement→Test-Map markiert `ResultsList.test.tsx` für Zeile 11-05-03 explizit als „⚠️ extend". Ohne Erweiterung wäre keine dieser Verhaltensweisen automatisiert verifiziert (die bestehenden 3 Tests hätten zwar weiter grün bleiben — der neue `fetchTriage`-Call wird defensiv mit `Array.isArray`-Check abgefangen —, aber Toggle-Klick, Optimistic-Update-Revert und Batch-Merge blieben ungetestet). +- **Fix:** Mocks für `fetchTriage`/`setTriage` ergänzt (analog zu `mockListTenders`); 5 neue Tests: Batch-Merge-Rendering, Read-Toggle-Klick, Favorit-Toggle-Klick (inkl. `stopPropagation`-Nachweis gegen den Zeilen-Klick), Revert bei fehlgeschlagenem `setTriage`, graceful Degradation bei fehlgeschlagenem `fetchTriage`. Zusätzlich `aria-label` auf beiden Toggle-Buttons ergänzt (das ursprüngliche `title`-Attribut allein bestimmt nicht den von Testing Library berechneten Accessible Name, wenn sichtbarer Text vorhanden ist — ohne `aria-label` wären die neuen `getByRole('button', {name: ...})`-Queries fehlgeschlagen). +- **Files modified:** `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx`, `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx` (aria-label-Ergänzung) +- **Commit:** `1eb9e4f` + +## Auth Gates + +None. + +## Issues Encountered + +- Erster GREEN-Testlauf schlug fehl (`prisma._simulateTenderCascadeDelete is not a function`) — Tippfehler im eigenen Test (Methode war auf `prisma.tenderTriage` definiert, Aufruf erfolgte auf `prisma`). Sofort korrigiert (Rule 1, im selben Zug wie die TDD-GREEN-Verifikation, kein separater Deviation-Eintrag da reiner Test-Autor-Fehler vor dem ersten grünen Lauf). + +## User Setup Required + +- **Docker-Rebuild ausstehend** (wie bereits in 11-04 dokumentiert): der laufende Stack (`tessera-ctl-api-1`, `tessera-ctl-web-1`) verwendet noch alte Images. Für eine Live-Verifikation im Browser (Toggle-Klicks, Merklisten-Filter-Checkbox, Cross-User-Isolation mit zwei Testnutzern) ist `docker compose build api web && docker compose up -d` erforderlich (User führt Docker-Rebuild selbst aus — projektinterne Regel, kein Docker-Deploy durch Claude). +- Keine neuen npm-Pakete (RESEARCH Package Legitimacy Audit: keine Neuinstallation für diese Phase). + +## Next Phase Readiness + +- `favOnly`-URL-Param-Konvention (`favOnly=true`) ist etabliert und folgt exakt demselben Muster wie `openOnly`/`includeNullValue` — Plan 11-06 (Saved Searches) kann `favOnly` beim Serialisieren eines Suchprofils einfach mit übernehmen, ohne Sonderbehandlung. +- `TenderTriageService.favoriteIds(userId)` ist als eigenständige, testbare Methode verfügbar — falls ein künftiges Dashboard-Widget "meine Favoriten" unabhängig von der Trefferliste braucht, kann es diese Methode direkt wiederverwenden. +- **Manuelle UAT ausstehend** (siehe „User Setup Required"): alle automatisierten Tests sind grün (149 API-Tests, 122 Web-Tests), `tsc --noEmit` fehlerfrei in beiden Paketen, Migration + Cascade-Verhalten live gegen die Dev-DB verifiziert — die eigentliche Zwei-Nutzer-Cross-Isolation im Browser (D-11) ist ein Multi-User-Flow und daher laut `11-VALIDATION.md` explizit als Manual-Only-Verifikation vorgesehen. + +--- +*Phase: 11-filter-engine-results-ui-saved-searches* +*Completed: 2026-07-21* + +## Self-Check: PASSED + +All 15 created/modified files verified present on disk; all 4 task commits (7bb695d, 58b0f3d, 5f97eca, 1eb9e4f) verified in git log. Migration applied and cascade behavior verified live against tessera-ctl-db-1 (insert/delete/rollback transaction, no data mutated). Full API suite (149 tests, 13 files) and full web suite (122 tests, 22 files) green; `tsc --noEmit` clean in both packages.