--- phase: 17-eigene-ausschreibungs-quellen-je-nutzer verified: 2026-08-12T15:10:00Z reverified: 2026-09-07T07:55:00Z status: passed score: 7/7 must-haves verified behavior_unverified: 0 overrides_applied: 0 human_verification: [] human_verification_completed: - test: "17-01 Task 2 human-check — zwei Konten desselben Mandanten, je eigenes Postfach" result: passed completed: 2026-09-07 evidence: "WINDOWS.md #7 fixed; uat-2026-09-07/w7-nutzer2-leer.png; DB: zwei TenderEmailConfig-Zeilen mit je eigenem userId" - test: "17-03 Task 1 human-check — Meine Quellen, drei Abschnitte, eigener Feed, Digest-Intervall" result: passed completed: 2026-09-07 evidence: "WINDOWS.md #8 fixed; uat-2026-09-07/w8-nutzer1-my-sources.png; DB: TenderNotificationPref digestInterval=weekly" - test: "17-03 Task 2 human-check — Rollentrennung /settings, Zahnrad-Navigation" result: passed completed: 2026-09-07 evidence: "WINDOWS.md #9 fixed; uat-2026-09-07/w9-user-settings.png + w9-admin-settings.png" --- # Phase 17: Eigene Ausschreibungs-Quellen je Nutzer — Verification Report **Phase Goal:** Jeder Nutzer speist seine eigenen Ausschreibungs-Quellen ein — eigenes Alert-Postfach und eigene RSS-Feeds statt einer gemeinsamen Konfiguration. Die Einstellungsseite trennt danach sauber nach Zustaendigkeit: Plattform-Administration (Abrufintervall) bleibt Admin-Sache, Quellen und Digest-Intervall gehoeren dem Nutzer. **Verified:** 2026-08-12 — **re-verifiziert im Browser:** 2026-09-07 **Status:** passed **Re-verification:** Yes — die drei offenen Browser-Gegenproben wurden am 2026-09-07 nachgeholt (siehe Nachtrag am Ende) ## Summary Verdict **The phase goal is achieved in the codebase.** All backend and frontend mechanisms described in 17-CONTEXT.md (D-01..D-05), all five ROADMAP requirements (SRC-01..SRC-05), and all seven ROADMAP Success Criteria are implemented, migrated locally, and covered by 362/362 API `src/tenders` tests + 58/58 web tender-radar tests, both re-run fresh in this verification (not taken from SUMMARY claims). The one hard scope fence (D-05: `Tender` stays platform-global) holds — verified directly against the live database schema, not just against source comments. Test quality on inspection is genuinely good: hand-written expectations, evaluating Prisma fakes for ownership/delete-protection, no tautological "expectation built from the same helper as the code under test" pattern found. **Stand 2026-08-12 konnte die Phase nicht auf `passed` gesetzt werden**, weil drei `human-check`-Punkte aus den Plaenen (17-01 Task 2, 17-03 Task 1, 17-03 Task 2) nie in einem echten Browser ausgefuehrt worden waren — offen ausgewiesen in den SUMMARYs und in WINDOWS.md (#7/#8/#9). Die programmatischen Pruefungen liefen damals schon alle gruen; unbewiesen blieb allein der tatsaechliche Durchklick (Formular-Vorbefuellung, Speichern-und-Neuladen, Rollensichtbarkeit im echten Next.js-Router, Navigation ueber das Zahnrad). **Am 2026-09-07 wurden genau diese drei Punkte im echten Browser nachgeholt und alle drei bestanden** — Aufbau, Beobachtungen und Belege stehen im Nachtrag am Ende dieses Berichts. WINDOWS.md #7/#8/#9 stehen auf `fixed`. Damit wechselt die Phase auf `passed`. ## Goal Achievement ### Observable Truths (ROADMAP Success Criteria, SC 1–7) | # | Truth | Status | Evidence | |---|-------|--------|----------| | 1 | Zwei Nutzer desselben Mandanten koennen gleichzeitig je ein eigenes Alert-Postfach anbinden; keiner sieht/ueberschreibt das des anderen | ✓ VERIFIED (mechanism) / see human-check | `TenderEmailConfig.userId @unique` confirmed live in DB (`TenderEmailConfig_userId_key`, no `_tenantId_key`). `getConfigForApi(userId)`/`saveConfig({userId,tenantId})` scoped strictly by userId from `extractTriageContext(req)`, never DTO. Unit + IDOR-shaped controller test pass. Real two-account browser click-through NOT run (WINDOWS.md #7). | | 2 | Nutzer legt eigene RSS-Feeds an, sieht eigene + plattformweite; service.bund.de bleibt fuer alle aktiv | ✓ VERIFIED | `TenderRssFeedSource.userId/tenantId` nullable, `@@unique([userId,url])` confirmed live in DB. `listForUser` returns `OR:[{userId:null},{userId}]`. Live DB query confirms exactly one row: `service.bund.de`, `isActive=true`, `userId`/`tenantId` empty — unchanged since Phase 14. | | 3 | Kein Nutzer kann fremden/plattformweiten Feed loeschen oder plattformweiten Feed anlegen | ✓ VERIFIED | `remove()` is a single conditional `deleteMany` (`id` AND (`userId=caller` OR (`isAdmin` AND `userId=null`))) — no TOCTOU window, ownership comparison lives in the DB condition. `NotFoundException` on no match (never confirms existence). `POST .../rss-feeds` with `scope:'platform'` requires `role===ADMIN|SUPER_ADMIN` inline in the controller, checked against the same `extractTriageContext` source `RolesGuard` reads. DTO carries no `userId`/`tenantId` field — cannot be spoofed. | | 4 | Abruf holt alle Postfaecher/Feeds in einem Durchlauf; Ausfall einer Quelle blockiert die anderen nicht | ✓ VERIFIED | `EmailAlertAdapter.fetchTenders`: `findMany({where:{isActive:true}})` (unwrapped, cross-tenant, deliberate — comment intact), `for` loop with per-row `try/catch`. `RssAdapter.fetchTenders`: same shape, per-feed `try/catch`. Both confirmed unchanged in code and covered by new multi-row/catch-per-row tests (hand-verified, not tautological — see Test Quality section). | | 5 | `/modules/tender-radar/my-sources` zeigt Postfach, eigene Feeds, Benachrichtigungsintervall an einer Stelle | ✓ VERIFIED (mechanism) / see human-check | `my-sources/page.tsx` renders `EmailAlertConfigForm` + `RssFeedListForm scope="personal"` + `DigestIntervalForm` in sequence, gated only by module access (no admin whitelist change needed — nested route). Web tests (`my-sources.test.tsx`, 5 tests) pass. Real browser click-through NOT run (WINDOWS.md #8). | | 6 | `/modules/tender-radar/settings` nur Abrufintervall + plattformweite Feeds; normaler Nutzer sieht keine fehlschlagenden Bedienelemente | ✓ VERIFIED (mechanism) / see human-check | `settings/page.tsx`: three-state display gate (`user===null` → placeholder, `isAdmin` → content, else → hint+link). Mailbox/notification sections physically removed from this file. `settings-roles.test.tsx` (5 tests) confirms USER sees neither section heading, ADMIN/SUPER_ADMIN see both, unresolved role shows neither. Server-side enforcement independently confirmed (`@UseModule` + inline admin check). Real browser click-through NOT run (WINDOWS.md #9). | | 7 | `Tender` bleibt unveraendert ohne Mandantenfeld und ohne RLS (D-05) | ✓ VERIFIED | Live `\d "Tender"` shows no `tenantId` column — only the pre-existing, nullable `ownerTenantId` (Phase 14, D-13, reused by design per plan's own D-06 decision, not a new mechanism). No `CREATE POLICY`/RLS migration touches `Tender`. `grep -r forTenant` across `tenders/` finds only explanatory comments ("must NEVER be wrapped in forTenant()"), zero actual wrapping calls. | **Score:** 7/7 truths verified as implemented mechanisms. 3 of the 7 additionally carry an outstanding real-browser confirmation step (SC 1, 5, 6) — these are not counted as failed (the mechanism is verified), but are surfaced below as required human verification per the routing rules (a ⚠️/human-check item takes precedence over `passed`). ### Requirements Coverage (SRC-01..SRC-05) | Requirement | Description | Status | Evidence | |---|---|---|---| | SRC-01 | Jeder Nutzer bindet eigenes Alert-Postfach an; ein Mandant kann mehrere Postfaecher haben (D-01) | ✓ SATISFIED | Schema/migration/service/controller all confirmed live; `TenderEmailConfig.userId @unique`, `tenantId` plain. | | SRC-02 | RSS-Feeds haben einen Besitzer; Feeds ohne Besitzer bleiben plattformweit, admin-gepflegt (D-02) | ✓ SATISFIED | Schema/migration/service confirmed live; delete-protection + 20-feed cap + SSRF guard on both create paths confirmed by reading `tender-rss-feed.service.ts` directly. | | SRC-03 | Zeitgesteuerter Abruf holt weiterhin alle Quellen in einem Durchlauf; kaputte Quelle blockiert andere nicht | ✓ SATISFIED | Both adapters confirmed unwrapped, per-row `try/catch` intact. | | SRC-04 | Nutzereigene Einstellungen liegen auf einer eigenen, fuer jeden Modulnutzer erreichbaren Seite (D-04) | ✓ SATISFIED (mechanism) | `/my-sources` page confirmed to render all three sections; gear icon confirmed pointed at `/my-sources`. Real navigation click NOT run (WINDOWS.md #8/#9). | | SRC-05 | Verbleibende Administrationseinstellungen sind fuer normale Nutzer nicht sichtbar; Rollenpruefung greift in UI wie API (D-03) | ✓ SATISFIED (mechanism) | Display-gate confirmed in code + tests; server-side `@UseModule`/inline-admin-check confirmed independently. Real browser role-switch NOT run (WINDOWS.md #9). | REQUIREMENTS.md traceability table (`| SRC-01 | Phase 17 | Complete |` … `| SRC-05 | Phase 17 | Complete |`) matches this assessment. No orphaned SRC requirements found. ### Required Artifacts | Artifact | Expected | Status | Details | |---|---|---|---| | `apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql` | Backfill-then-cutover migration | ✓ VERIFIED | Read in full; correct order (add nullable → backfill → delete unowned → drop old unique → set NOT NULL → new unique). Applied locally, `prisma migrate status` clean. | | `apps/api/src/tenders/email-config-migration-sql.spec.ts` | Text-only order proof | ✓ VERIFIED | 6 tests, all assert on real file content/ordering, not vacuous. | | `apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx` | Full 3-section user page | ✓ VERIFIED | Renders `EmailAlertConfigForm` + `RssFeedListForm scope="personal"` + `DigestIntervalForm`, admin-only link to settings. | | `apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql` | Nullable owner-column migration | ✓ VERIFIED | Read in full; no backfill needed (nullable = today's state), correct index swap. Applied locally. | | `apps/api/src/tenders/rss-feed-migration-sql.spec.ts` | Text-only order proof | ✓ VERIFIED | 6 tests present and passing. | | `apps/web/.../my-sources/my-sources.test.tsx` | 3-section coverage test | ✓ VERIFIED | 5 tests, passing. | | `apps/web/.../settings/settings-roles.test.tsx` | Role-gate coverage test | ✓ VERIFIED | 5 tests, passing, checks rendered DOM not internal state. | | `apps/web/.../settings/components/DigestIntervalForm.tsx` | Extracted, behavior-unchanged component | ✓ VERIFIED | Confirmed used identically on both pages. | ### Key Link Verification | From | To | Via | Status | Details | |---|---|---|---|---| | `TenderEmailConfig.userId` | `extractTriageContext(req).userId` | GET/PUT email-config | ✓ WIRED | Confirmed in `tenders.controller.ts` — `userId`/`tenantId` never read from body/query. | | `TenderEmailConfig.tenantId` | `EmailAlertAdapter.extractCandidates(..., cfg.tenantId, ...)` | poll fan-out | ✓ WIRED | `records.push(...this.extractCandidates(messages, cfg.tenantId, fetchedAt))` confirmed in source. | | GET/PUT `/email-config` route position | before `@Get(':id')` | route-order pitfall | ✓ WIRED | Confirmed: all static routes (`source-config`, `rss-feeds`, `email-config`, `coverage`, `denylisted-portals`, `triage`, `saved-searches`) precede `@Get(':id')` at line 582. | | `TenderRssFeedSource.userId` | `extractTriageContext(req).userId` | POST/DELETE rss-feeds | ✓ WIRED | Confirmed — DTO carries no ownership field. | | `TenderRssFeedSource.tenantId` | `RawTenderRecord.ownerTenantId` | `RssAdapter.fetchTenders` | ✓ WIRED | `if (feed.tenantId) { for (const record of feedRecords) record.ownerTenantId = feed.tenantId; }` confirmed in source. | | `TendersModule.onModuleInit()` | service.bund.de seed | idempotent find-then-create | ✓ WIRED | `seedServiceBundRssFeed()` in `tenders.seed.ts` — `findFirst({where:{userId:null,url:...}})` then create — confirmed, matches live DB (exactly 1 row). | | `RssFeedListForm(scope)` | `POST /rss-feeds` with `scope` | dual-purpose component | ✓ WIRED | `createRssFeed(payload, scope)` confirmed passing `scope` into request body; server re-checks admin role independently. | | `useAuthStore().user.role` | admin section visibility | display-only gate | ✓ WIRED | Confirmed in both `settings/page.tsx` and `my-sources/page.tsx`; `user===null` renders neither state (no flash). | | Zahnrad in `tender-radar/page.tsx` | `/modules/tender-radar/my-sources` | universal entry point | ✓ WIRED | `href="/modules/tender-radar/my-sources"` confirmed at line 84. | ### Data-Flow Trace (Level 4) | Artifact | Data Variable | Source | Produces Real Data | Status | |---|---|---|---|---| | `TenderEmailConfig` rows | `userId`, `tenantId` | Live Postgres (172.19.0.2) | Confirmed via `psql \d` and index listing | ✓ FLOWING | | `TenderRssFeedSource` rows | `url`, `label`, `isActive`, `userId`, `tenantId` | Live Postgres | Confirmed: exactly 1 row, service.bund.de, platform-wide, active | ✓ FLOWING | | `Tender` schema | column list | Live Postgres | Confirmed: no `tenantId` column, only pre-existing `ownerTenantId` | ✓ FLOWING (negative check — absence confirmed) | ### Behavioral Spot-Checks | Behavior | Command | Result | Status | |---|---|---|---| | API `src/tenders` full suite (independent re-run, not from SUMMARY) | `pnpm --filter @tessera/api exec vitest run src/tenders` | 28 files, 362 tests passed | ✓ PASS | | API type-check | `pnpm --filter @tessera/api type-check` | clean | ✓ PASS | | Web type-check | `pnpm --filter @tessera/web type-check` | clean | ✓ PASS | | Web tender-radar test suite | `pnpm --filter @tessera/web exec vitest run "src/app/(portal)/modules/tender-radar"` | 10 files, 58 tests passed | ✓ PASS | | Prisma migration status | `npx prisma migrate status` (via container-IP DATABASE_URL) | "Database schema is up to date!" — 29 migrations | ✓ PASS | | `TenderEmailConfig` index list | live `psql` query | `_userId_key` present, `_tenantId_key` absent | ✓ PASS | | `TenderRssFeedSource` index list | live `psql` query | `_userId_url_key` present, old `_url_key` absent | ✓ PASS | | `Tender` schema, no tenant column | live `psql \d "Tender"` | no `tenantId`; `ownerTenantId` unchanged | ✓ PASS | | i18n key parity, `tenderRadar` namespace | node key-diff script | 239/239 keys match de/en | ✓ PASS | | Backlog item moved to completed | `ls .planning/todos/completed/...` | file present, pending copy absent | ✓ PASS | | Git commits exist | `git log --oneline -- apps/api/src/tenders ...` | all 9 task commits found (`05b1d29`, `55ceb24`, `adb72f6`, `9616155`, `7dee116`, `4100bb5`, `150046e`, `809afbc`) | ✓ PASS | ### Probe Execution Not applicable — no `scripts/*/tests/probe-*.sh`-style probes declared or referenced by this phase's plans. ### Test Quality Spot-Check (requested item 7) Inspected `tender-rss-feed.service.spec.ts`, `email-alert.adapter.spec.ts`, `RssFeedListForm.test.tsx`, `settings-roles.test.tsx`, `email-config-migration-sql.spec.ts` in full. - **No tautological "expectation built from the production helper" pattern found.** Every test file that could tempt this (in-memory Prisma fakes, next-intl translation stubs) instead hand-writes expected values and hand-writes the mock translation table separately from the component/message files under test — explicitly called out in comments, e.g. `RssFeedListForm.test.tsx` line 19: "Text is hand-written here, NOT derived from the component/message files under test — a test that builds its own expectation from the same helper the component uses proves nothing." - **No status-code-as-proof pattern found.** Delete-protection tests assert both the thrown exception type/instance AND the row-count side effect (`expect(prisma.__rows.size).toBe(1)` after a rejected delete) — not merely that an HTTP status or exception class was thrown. - **The in-memory Prisma fakes genuinely evaluate `where` clauses** (`matchesWhere` with recursive `OR`/`AND` handling) rather than ignoring the condition and always "succeeding" — the test file itself documents why this matters ("a double that ignored `where` and always 'succeeded' would only fake the protection, not test it"). - One minor, non-blocking observation: `createForUser`'s 20-feed cap check (`count()` then `create()`) is not atomic — a genuine race between two concurrent requests from the same user could both pass the count check before either creates a row, allowing a transient overshoot of the cap by a small margin. This is a soft rate-limit, not a security boundary (ownership/deletion protection IS atomic via the single conditional `deleteMany`), and is not called out as a threat in the plan's own STRIDE table, so it is noted here as an observation, not a gap. ### Anti-Patterns Found None. Grep for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` (and case-insensitive `placeholder`/`not yet implemented`) across all 15 phase-modified backend/frontend files returned only pre-existing, benign uses of the word "placeholder" (documenting the `portals: ['rss']` symbolic constant, and the loading-skeleton UI state) — none indicate unfinished work. ### Human Verification Required — am 2026-09-07 vollstaendig nachgeholt (alle drei bestanden) 1. **17-01 Task 2 human-check (WINDOWS.md #7, open)** **Test:** As a normal USER-role account with module access, open `/modules/tender-radar/my-sources`, fill and save the mailbox form, reload — values persist. Then log in as a second account of the same tenant — the form is empty, not pre-filled with the first user's values. **Expected:** Each user sees and edits only their own mailbox. **Why human:** Requires two live authenticated browser sessions; the underlying `userId`-scoped query and IDOR-safe controller wiring is verified by code/DB inspection and unit tests, but the actual UX round-trip was never clicked through. 2. **17-03 Task 1 human-check (WINDOWS.md #8, open)** **Test:** As a normal user, open `/modules/tender-radar/my-sources` — three sections visible. Add a personal RSS feed — it appears immediately in the editable list; the service.bund.de feed appears below without a remove button. Set the digest interval to "Woechentlich", reload — value persists. **Expected:** All three sections work end-to-end in a real browser. **Why human:** No browser tool was available in the executing session. 3. **17-03 Task 2 human-check (WINDOWS.md #9, open)** **Test:** As a USER-role account, navigate directly to `/modules/tender-radar/settings` — no controls, only the hint + link. As an ADMIN account, same URL — poll interval + platform feeds show, mailbox/notification sections do not. The gear icon on the module page leads to "Meine Quellen" in both cases. **Expected:** Role-based visibility and navigation work identically to the passing component tests, in the real Next.js router. **Why human:** No browser tool was available in the executing session. All three items are honestly recorded as `open`/`unrun-verify` in `.planning/WINDOWS.md` (entries #7, #8, #9) and in each plan's own SUMMARY.md — none of the three SUMMARYs claim these were passed. STATE.md's `stopped_at` field also explicitly flags "Browser-Gegenprobe aller drei Plaene ... stehen als offene Punkte in WINDOWS.md" as outstanding before `/gsd-ship`. This verification confirms that disclosure is accurate and does not overstate what has actually been proven. ### Gaps Summary No implementation gaps found. All must-haves at the mechanism level (schema, migration, service, controller, adapter, frontend wiring, tests, i18n, requirements traceability, backlog closure) are verified directly against the live codebase and a live local database — not inferred from SUMMARY claims. The phase's own scope fence (D-05, `Tender` stays platform-global) is independently confirmed intact. Der einzige offene Punkt war die Gruppe der drei echten Browser-Durchlaeufe — eine Luecke in der Vollstaendigkeit der Pruefung, keine Luecke in der Umsetzung. Sie wurde am 2026-09-07 geschlossen (siehe Nachtrag); es sind keine offenen Punkte mehr verzeichnet. --- *Verified: 2026-08-12* *Verifier: Claude (gsd-verifier)* --- ## Nachtrag: Browser-Gegenprobe vom 2026-09-07 Die drei am 2026-08-12 offen gebliebenen `human-check`-Punkte wurden nachgeholt. Aufbau: frisch gebaute Images aus `main` (`79015dd`), lokaler Docker-Stack, leere Datenbank, echter Chrome ueber Playwright. Testdaten ueber die Oberflaeche angelegt — Modul im Marktplatz aktiviert, Freigabe in der Matrix an die Standardgruppe erteilt, zwei USER-Konten (`nutzer1`, `nutzer2`) im selben Mandanten. ### WINDOWS #7 — eigenes Postfach je Nutzer (17-01 Task 2) — BESTANDEN | Schritt | Beobachtung | |---|---| | `nutzer1` oeffnet `/modules/tender-radar/my-sources` | Postfach-Formular leer, Hinweis „Noch nicht gespeichert" | | Postfach ausfuellen und speichern | „Einstellungen gespeichert.", Hinweis verschwindet | | Seite neu laden | `imap.nutzer1.test`, `alerts@nutzer1.test`, Benutzername `nutzer1-postfach` stehen wieder da | | `nutzer2` oeffnet dieselbe Seite | Formular **leer**, Hinweis „Noch nicht gespeichert" — nicht die Werte von `nutzer1` | | `nutzer2` speichert sein eigenes Postfach | Zweite Zeile entsteht, erste bleibt unveraendert | Datenbank danach: zwei `TenderEmailConfig`-Zeilen, je ein eigener `userId`, beide mit gefuelltem `encryptedInboxCreds`. Beleg: `uat-2026-09-07/w7-nutzer2-leer.png`. ### WINDOWS #8 — Meine Quellen, alle drei Abschnitte (17-03 Task 1) — BESTANDEN | Schritt | Beobachtung | |---|---| | Seitenaufbau | Drei Abschnitte: „Mein Postfach", „Meine Feeds", „Benachrichtigung" | | Eigenen RSS-Feed anlegen | „Mein Testfeed" erscheint **sofort** in der eigenen Liste, mit Entfernen-Knopf | | Plattformweiter Feed | `service-bund` steht darunter unter „Diese Feeds werden von der Administration gepflegt und gelten fuer alle." — **ohne** Entfernen-Knopf | | Digest-Intervall auf „Woechentlich", dann neu laden | Wert steht noch (`TenderNotificationPref.digestInterval = weekly`, nur fuer `nutzer1`) | | Verweis auf die Administrationsseite | Fuer das USER-Konto **nicht** vorhanden | `nutzer2` sieht in „Meine Feeds" nur „Noch keine RSS-Feeds hinterlegt" plus den plattformweiten Feed — der Feed von `nutzer1` taucht bei ihm nicht auf. Beleg: `uat-2026-09-07/w8-nutzer1-my-sources.png`. ### WINDOWS #9 — Rollentrennung der Einstellungsseite (17-03 Task 2) — BESTANDEN | Rolle | `/modules/tender-radar/settings` direkt aufgerufen | |---|---| | USER (`nutzer1`) | Keine Bedienelemente. Nur „Diese Seite verwaltet plattformweite Einstellungen und ist Administratoren vorbehalten." plus Verweis „Meine Quellen →" | | SUPER_ADMIN (`admin`) | Abrufintervall (60 Min.), Aktiv-Schalter, Speichern, plus plattformweite RSS-Feeds **mit** Entfernen-Knopf. Postfach und Benachrichtigung sind auf dieser Seite **nicht** mehr vorhanden | Das Zahnrad auf der Modulseite fuehrt in **beiden** Faellen nach `/modules/tender-radar/my-sources` — als Link angeklickt, nicht nur im Markup gelesen. Belege: `uat-2026-09-07/w9-user-settings.png`, `uat-2026-09-07/w9-admin-settings.png`. ### Zusatzbeleg: Schutz gegen fremde und plattformweite Feeds am laufenden Server Der Verifikationsbericht hatte SC 3 aus dem Quelltext belegt. Die Oberflaeche blendet den Entfernen-Knopf nur aus — das ist kein Beweis, dass der Server ihn auch verweigert. Deshalb zusaetzlich gegen die laufende API gemessen, angemeldet als `nutzer2`: | Aufruf | Antwort | Wirkung | |---|---|---| | `GET /modules/tender-radar/rss-feeds` | 200, genau ein Eintrag: `service-bund`, `isPlatformWide: true` | Der Feed von `nutzer1` ist fuer `nutzer2` unsichtbar | | `DELETE .../rss-feeds/` | 404 | Zeile bleibt bestehen | | `DELETE .../rss-feeds/` | 404 | Zeile bleibt bestehen | | `POST .../rss-feeds` mit `scope: "platform"` | 403 „Nur Administratoren duerfen plattformweite RSS-Feeds anlegen." | Kein Eintrag entsteht | Der Bestand war danach unveraendert: eine plattformweite Zeile, eine Zeile von `nutzer1`. ### Ergebnis Alle sieben Erfolgskriterien sind jetzt nicht nur als Mechanismus, sondern auch im laufenden System belegt. `WINDOWS.md` #7, #8 und #9 stehen auf `fixed`. Der Phasenstatus wechselt von `human_needed` auf `passed`. *Nachtrag verfasst: 2026-09-07*