Files
tessera-ctl/.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-VERIFICATION.md
T
schalli 72d0ba7e48 test(17): Browser-Gegenproben #7/#8/#9 nachgeholt — Phase 17 auf passed
Die drei human-check-Punkte aus Phase 17 waren am 2026-08-12 offen geblieben,
weil in der Ausfuehrungssitzung kein Browser-Tool verfuegbar war. Sie wurden
jetzt gegen frisch gebaute Images aus main (79015dd) mit echtem Chrome und
leerer Datenbank durchgeklickt.

#7 (17-01 Task 2): nutzer1 speichert sein Postfach, die Werte ueberleben den
Reload; nutzer2 sieht ein leeres Formular statt der Werte von nutzer1. Danach
zwei TenderEmailConfig-Zeilen mit je eigenem userId.

#8 (17-03 Task 1): Meine Quellen zeigt alle drei Abschnitte, ein eigener
RSS-Feed erscheint sofort mit Entfernen-Knopf, der plattformweite service-bund
steht darunter ohne, und das Digest-Intervall "Woechentlich" ueberlebt den
Reload.

#9 (17-03 Task 2): USER sieht auf /settings keine Bedienelemente, nur Hinweis
und Verweis; SUPER_ADMIN sieht Abrufintervall und plattformweite Feeds, aber
kein Postfach und keine Benachrichtigung mehr. Das Zahnrad fuehrt in beiden
Faellen nach Meine Quellen.

Zusaetzlich am laufenden Server gemessen, weil eine ausgeblendete Schaltflaeche
kein Beweis fuer eine serverseitige Sperre ist: als nutzer2 liefert GET
/rss-feeds nur den plattformweiten Feed, DELETE auf den plattformweiten wie auf
den fremden Feed antwortet 404 ohne die Zeile anzufassen, und POST mit
scope=platform wird mit 403 abgewiesen.

WINDOWS.md #7/#8/#9 auf fixed (open_count 9 -> 6), Verifikationsbericht mit
Nachtrag und Screenshots auf passed, ROADMAP auf Complete, STATE nachgezogen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
2026-09-07 09:53:02 +02:00

238 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/<plattformweiter Feed>` | 404 | Zeile bleibt bestehen |
| `DELETE .../rss-feeds/<Feed von nutzer1>` | 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*