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

24 KiB
Raw Blame History

phase, verified, reverified, status, score, behavior_unverified, overrides_applied, human_verification, human_verification_completed
phase verified reverified status score behavior_unverified overrides_applied human_verification human_verification_completed
17-eigene-ausschreibungs-quellen-je-nutzer 2026-08-12T15:10:00Z 2026-09-07T07:55:00Z passed 7/7 must-haves verified 0 0
test result completed evidence
17-01 Task 2 human-check — zwei Konten desselben Mandanten, je eigenes Postfach passed 2026-09-07 WINDOWS.md #7 fixed; uat-2026-09-07/w7-nutzer2-leer.png; DB: zwei TenderEmailConfig-Zeilen mit je eigenem userId
test result completed evidence
17-03 Task 1 human-check — Meine Quellen, drei Abschnitte, eigener Feed, Digest-Intervall passed 2026-09-07 WINDOWS.md #8 fixed; uat-2026-09-07/w8-nutzer1-my-sources.png; DB: TenderNotificationPref digestInterval=weekly
test result completed evidence
17-03 Task 2 human-check — Rollentrennung /settings, Zahnrad-Navigation passed 2026-09-07 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
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.
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