--- phase: 17-eigene-ausschreibungs-quellen-je-nutzer plan: 01 type: execute wave: 1 depends_on: [] files_modified: - apps/api/prisma/schema.prisma - apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql - apps/api/src/tenders/tender-email-config.service.ts - apps/api/src/tenders/tenders.controller.ts - apps/api/src/tenders/adapters/email-alert.adapter.ts - apps/api/src/tenders/tender-email-config.service.spec.ts - apps/api/src/tenders/adapters/email-alert.adapter.spec.ts - apps/api/src/tenders/email-config-migration-sql.spec.ts - apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx autonomous: false requirements: [SRC-01, SRC-03] user_setup: [] estimate: tokens: 60000 raw_tokens: 60000 tasks: 3 confidence: low must_haves: truths: - "Zwei verschiedene Nutzer desselben Mandanten koennen gleichzeitig je ein eigenes Alert-Postfach hinterlegen (D-01)." - "Beim Abruf werden beide Postfaecher in einem Durchlauf geleert; faellt eines aus, laufen die anderen weiter (D-01, offener Punkt 3)." - "Was aus dem Postfach eines Nutzers hereinkommt, bleibt weiterhin auf dessen Mandanten begrenzt — die in Phase 14 gebaute Herkunftsmarkierung bleibt unveraendert (D-05)." - "Eine bereits vorhandene Postfach-Zeile geht bei der Umstellung nicht verloren, sondern bekommt einen Besitzer (offener Punkt 1)." - "Die Seite `/modules/tender-radar/my-sources` ist fuer jeden Nutzer mit Modulzugang erreichbar und zeigt sein eigenes Postfach." artifacts: - apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql - apps/api/src/tenders/email-config-migration-sql.spec.ts - apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx key_links: - "TenderEmailConfig.userId <-> extractTriageContext(req).userId — Besitz kommt ausschliesslich aus dem angemeldeten Konto, nie aus dem Request-Body (IDOR)." - "TenderEmailConfig.tenantId <-> EmailAlertAdapter.extractCandidates(..., cfg.tenantId, ...) — die Herkunftsmarkierung der eingelesenen Ausschreibungen haengt weiter an diesem Feld." - "GET/PUT /modules/tender-radar/email-config <-> Reihenfolge vor `@Get(':id')` — statische Route darf nicht von der Parameter-Route verdeckt werden." --- Das Alert-Postfach wechselt vom Mandanten zum einzelnen Nutzer. Heute gibt es genau ein Postfach pro Mandant (`TenderEmailConfig.tenantId @unique`) — ein zweiter Kollege mit eigenem Portal-Konto kann seine Ausschreibungs-Alarme schlicht nicht anbinden. Nach diesem Plan hinterlegt jeder Nutzer sein eigenes Postfach, und der Abruf holt alle hinterlegten Postfaecher in einem Durchlauf ab. Dieser Plan ist die duenne, aber vollstaendige Bahn durch alle Schichten: Datenbank -> Migration -> Dienst -> Endpunkt -> eine eigene, fuer jeden Modulnutzer erreichbare Seite. Erst wenn diese eine Bahn nachweislich laeuft, bauen die Plaene 17-02 (RSS-Feeds) und 17-03 (Oberflaeche aufteilen) daneben weiter. Purpose: D-01 aus 17-CONTEXT.md umsetzen und damit die eigentliche Ursache des Backlog-Punkts beheben — nicht nur die Anzeige, sondern den Zuschnitt. Output: Geaenderte Tabelle samt Umzug der Bestandsdaten, nutzerbezogener Dienst und Endpunkt, neue Seite "Meine Quellen" mit dem Postfach-Formular. **Harte Grenze (D-05):** Ausschreibungsdaten bleiben plattform-global. `Tender` bekommt kein Mandantenfeld, keine RLS, keinen Mandanten-Wrapper. Geaendert wird ausschliesslich, wer Quellen einspeist — nicht, wer Treffer sieht. Innerhalb eines Mandanten sieht weiterhin jeder alles. **Entscheidung zum offenen Punkt 1 (Besitzer der Bestandszeile):** Eine vorhandene Postfach-Zeile wird dem aeltesten aktiven ADMIN bzw. SUPER_ADMIN ihres Mandanten zugeordnet, nicht geloescht. Begruendung: Die Zeile wurde seinerzeit von genau dieser Rolle angelegt, sie enthaelt verschluesselte Zugangsdaten, und ein Loeschen wuerde den laufenden Abruf ohne Vorwarnung stilllegen. Nur falls ein Mandant ueberhaupt keinen aktiven Administrator hat, wird die Zeile entfernt — dann gaebe es niemanden, der sie je wieder bearbeiten koennte, waehrend das Postfach im Hintergrund weiter abgefragt wuerde. **Entscheidung zum offenen Punkt 4 (wohin mit den nutzereigenen Abschnitten):** Es entsteht eine eigene nutzerseitige Modulseite `/modules/tender-radar/my-sources` ("Meine Quellen"), getrennt von der Administrationsseite. Begruendung: Die Alternative, alles nach `/settings/general` zu verschieben, sammelt dort mit jedem neuen Modul modulfremde Einstellungen an — genau der Einwand aus dem Backlog-Punkt. Modulsachen bleiben beim Modul; DKV-Fleet und kuenftige Module koennen dieselbe Bauform uebernehmen. Der Adresspfad bleibt englisch (`my-sources`), wie jeder andere Pfad im Projekt auch; die Beschriftung ist deutsch. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-CONTEXT.md @apps/api/prisma/schema.prisma @apps/api/src/tenders/tender-email-config.service.ts @apps/api/src/tenders/adapters/email-alert.adapter.ts @apps/api/prisma/migrations/20260811140000_encrypt_ldap_bind_password/migration.sql @apps/api/src/tenders/doe-url-migration-sql.spec.ts Task 1: Freigabe fuer die beiden Datenbank-Umbauten dieser Phase Darf ich die Tabellenstruktur fuer Postfaecher und RSS-Feeds jetzt umbauen? Diese Phase aendert zwei Tabellen dauerhaft. Beides laesst sich nur mit einer weiteren Datenbank-Aenderung zurueckdrehen, nicht durch einfaches Rueckgaengigmachen der Dateien: 1. **Postfaecher** (dieser Plan): Das Postfach haengt danach am Nutzer statt am Mandanten. Eine bereits eingerichtete Postfach-Zeile bekommt dabei den aeltesten aktiven Administrator ihres Mandanten als Besitzer. Findet sich dort kein Administrator, wird die Zeile entfernt — sie waere sonst fuer niemanden mehr bearbeitbar, wuerde aber weiter abgefragt. 2. **RSS-Feeds** (Plan 17-02): Die Feeds bekommen ein Besitzer-Feld. Alle heute vorhandenen Feeds bleiben unveraendert plattformweit fuer alle aktiv — dort geht nichts verloren. Auf dem Testserver liegen echte Daten. Der Umbau selbst laeuft hier lokal; auf dem Testserver wird er erst wirksam, wenn Sie dort selbst neu ausrollen. Vor diesem Ausrollen ist eine Datenbank-Sicherung sinnvoll. Bevor ich anfange, zaehle ich nach, wie viele Postfach-Zeilen es auf dem Testserver ueberhaupt gibt, und nenne Ihnen die Zahl — nur lesend, ich fasse dort nichts an. Antworten Sie mit "weiter" oder "stopp". Task 2: Ein eigenes Postfach je Nutzer — durchgehend von der Datenbank bis zur Seite Die lokale Entwicklungsdatenbank ist ohne Host-Port erreichbar: Container-IP des `db`-Containers ermitteln (`docker inspect`) und `DATABASE_URL` mit Zugang `tessera:tessera_dev` darauf zeigen lassen — sonst laesst sich die Migration nicht anwenden (Projektwissen "Lokale DB-Migrationen"). Die Migration schreibt Bestandsdaten um und entfernt eine Eindeutigkeitsregel; ein Rueckweg braucht eine zweite Migration. Freigabe erfolgt in Task 1. apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql, apps/api/src/tenders/tender-email-config.service.ts, apps/api/src/tenders/tenders.controller.ts, apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx apps/api/src/tenders/tender-email-config.service.ts, apps/api/src/tenders/tenders.controller.ts (Zeilen 90-115 und 270-305), apps/api/prisma/migrations/20260723113917_tender_email_config_owner_tenant_id/migration.sql, apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx - Ein Nutzer speichert sein Postfach; danach liest derselbe Nutzer genau diese Werte zurueck. - Ein zweiter Nutzer desselben Mandanten speichert ein anderes Postfach; beide Zeilen existieren nebeneinander, keine ueberschreibt die andere. - Nutzer B liest nie die Werte von Nutzer A — gelesen wird ausschliesslich nach dem angemeldeten Konto. - Das Passwort verlaesst den Server nie: die Antwort enthaelt weiterhin nur `hasPassword: boolean`. - Wird nur der Benutzername geaendert und das Passwortfeld leer gelassen, bleibt das gespeicherte Passwort erhalten (bestehende Semantik, unveraendert). **Vorabpruefung (nur lesend, Testserver).** Ueber SSH auf 192.168.13.12 mit `psql` zaehlen, wie viele Zeilen in `TenderEmailConfig` stehen und zu welchem Mandanten sie gehoeren. Ergebnis im SUMMARY festhalten. Nichts aendern, kein `docker compose`-Kommando dort ausfuehren — Ausrollen ist Sache des Nutzers. **Schema (`apps/api/prisma/schema.prisma`, Modell `TenderEmailConfig`).** Feld `userId String @unique` ergaenzen. `tenantId` von `@unique` auf ein gewoehnliches Feld zurueckstufen (bleibt erhalten, denormalisiert, fuer die SMTP-Aufloesung und die Herkunftsmarkierung — gleiche Rolle wie in `TenderMatch`, D-01). Bestehenden `@@index([tenantId])` behalten, `@@index([userId])` ergaenzen. Den Kopfkommentar des Modells um zwei Saetze erweitern: Besitz liegt beim Nutzer (D-01, Phase 17), `tenantId` bleibt denormalisiert. **Migration.** Verzeichnis `apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/` anlegen, `migration.sql` von Hand schreiben (Projektentscheidung "Migrationsverfahren angepasst": `prisma migrate dev` verweigert die nicht-interaktive Shell). Erwarteter Ablauf, in dieser Reihenfolge: 1. Spalte `userId` als `TEXT` ohne Pflicht ergaenzen. 2. Bestandszeilen befuellen: je Zeile den aeltesten aktiven Nutzer mit Rolle `ADMIN` oder `SUPER_ADMIN` desselben Mandanten ermitteln (sortiert nach `createdAt`, bei Gleichstand nach `id`, `LIMIT 1`) und dessen `id` eintragen. 3. Zeilen, die danach immer noch keinen Besitzer haben, entfernen (kein Administrator im Mandanten vorhanden — siehe Begruendung im Objective). 4. Eindeutigkeitsregel auf `tenantId` entfernen (`TenderEmailConfig_tenantId_key`), gewoehnlichen Index `..._tenantId_idx` stehen lassen. 5. Spalte `userId` auf Pflicht setzen, eindeutigen Index `TenderEmailConfig_userId_key` und Index `TenderEmailConfig_userId_idx` anlegen. Ueber die Anweisungen einen deutschen Kommentarkopf setzen, der erklaert, warum umgestellt wird und warum die Bestandszeile dem aeltesten Administrator zufaellt — gleiche Form wie `20260811140000_encrypt_ldap_bind_password/migration.sql`. Anwenden mit `prisma migrate deploy` gegen die lokale Datenbank, danach `prisma generate`. **Dienst (`tender-email-config.service.ts`).** `getConfigForApi` nimmt statt `tenantId` nun `userId` und sucht darueber. `saveConfig` nimmt ein Objekt `{ userId, tenantId }` als ersten Parameter; im `upsert` ist `userId` der Schluessel, `tenantId` wird sowohl beim Anlegen als auch beim Aktualisieren mitgeschrieben (der Mandant eines Nutzers kann sich aendern, das denormalisierte Feld muss mitziehen). `EMAIL_CONFIG_SAFE_SELECT` um `userId` ergaenzen; der verschluesselte Zugangsdaten-Block bleibt wie bisher aus jeder Antwort ausgeschlossen. Die Semantik "Passwort leer heisst gespeichertes Passwort behalten" bleibt unveraendert; nur die Suchbedingungen wechseln von `tenantId` auf `userId`. Klassenkommentar entsprechend nachziehen. **Endpunkt (`tenders.controller.ts`).** `GET`/`PUT /email-config`: die Rollenpruefung `@Roles(ADMIN, SUPER_ADMIN)` entfaellt und wird durch `@UseModule('tender-radar')` ersetzt — das Postfach ist ab jetzt eine Nutzereinstellung, der Zugang haengt an der Modulfreigabe aus Phase 15. `userId` und `tenantId` kommen weiterhin ausschliesslich aus `extractTriageContext(req)`, nie aus dem Rumpf der Anfrage. **Die Position beider Handler im Datei-Aufbau nicht verschieben**: sie stehen vor der Parameter-Route `@Get(':id')`, sonst verdeckt diese sie und die Endpunkte antworten mit 404 (dokumentierte Falle, Unit-Tests fangen sie nicht). Die vorhandenen Kommentare zu dieser Reihenfolge stehen lassen und den Hinweis auf die Mandantenbindung im Kommentar auf die Nutzerbindung umschreiben. **Seite (`apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx`).** Neue Client-Seite, gleiche Bauform wie die vorhandene Einstellungsseite: Ueberschrift aus dem i18n-Namensraum `tenderRadar` (neuer Schluessel `mySources.title`, deutsch "Meine Quellen", englisch "My sources"; beide Sprachdateien `apps/web/src/messages/de.json` und `en.json` ergaenzen), darunter ein Abschnitt "Mein Postfach" mit dem **unveraenderten** bestehenden `EmailAlertConfigForm` (nur importieren, nicht kopieren, nicht umbauen — es spricht dieselben Endpunkte an). Ein kurzer Hinweistext unter der Ueberschrift benennt ehrlich, was D-05 bedeutet: die hier eingespeisten Ausschreibungen erscheinen in der Trefferliste aller Kollegen desselben Mandanten. Keine Aenderung am Modul-Verzeichnis noetig — verschachtelte Route unterhalb der bereits freigeschalteten Modulseite, gleiche Begruendung wie bei der Einstellungsseite. cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma validate && npx prisma migrate status cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api type-check && pnpm --filter @tessera/web type-check cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma db execute --stdin <<'SQL' SELECT indexname FROM pg_indexes WHERE tablename = 'TenderEmailConfig' ORDER BY indexname; SQL Im Browser als normaler Nutzer (Rolle USER mit Modulfreigabe) `/modules/tender-radar/my-sources` oeffnen: die Seite laedt, das Postfach-Formular ist bedienbar, Speichern quittiert erfolgreich und nach einem Neuladen stehen die Werte wieder da. Danach mit einem zweiten Konto desselben Mandanten anmelden: dort steht ein leeres Formular, nicht die Werte des ersten Nutzers. `prisma migrate status` meldet alle Migrationen angewandt. Die Indexliste zeigt `TenderEmailConfig_userId_key` und enthaelt `TenderEmailConfig_tenantId_key` nicht mehr. Beide Typpruefungen laufen fehlerfrei. Zwei Konten desselben Mandanten haben je eine eigene Postfach-Zeile. Task 3: Der Abruf holt alle Postfaecher — und beweist es mit Tests apps/api/src/tenders/adapters/email-alert.adapter.ts, apps/api/src/tenders/adapters/email-alert.adapter.spec.ts, apps/api/src/tenders/tender-email-config.service.spec.ts, apps/api/src/tenders/email-config-migration-sql.spec.ts apps/api/src/tenders/adapters/email-alert.adapter.spec.ts, apps/api/src/tenders/tender-email-config.service.spec.ts, apps/api/src/tenders/doe-url-migration-sql.spec.ts - Zwei aktive Postfaecher **desselben** Mandanten werden in einem einzigen Abruf beide geleert. Das ist genau die Faehigkeit, die es vorher nicht gab — der Test faellt ohne Task 2 durch. - Faellt eines von zwei Postfaechern mit einem Fehler aus, liefert der Abruf trotzdem die Datensaetze des anderen und protokolliert eine Warnung. - Die eingelesenen Datensaetze tragen weiterhin die Herkunftsmarkierung aus dem Mandantenfeld der jeweiligen Postfach-Zeile (unveraenderte Sichtbarkeit gegenueber anderen Mandanten, D-05). - Die Sammelabfrage liest weiterhin alle aktiven Postfaecher ueber Mandantengrenzen hinweg in einem Zug — der Plattform-Zeitplan ist keine Anfrage eines einzelnen Mandanten. - Die Migrationsdatei ordnet Bestandszeilen einem Administrator zu, bevor sie unbesetzte Zeilen entfernt. **Anpassung im Abruf (`email-alert.adapter.ts`).** Inhaltlich aendert sich nichts an der Mechanik: die Sammelabfrage bleibt exakt wie sie ist (alle aktiven Zeilen in einem Zug), und die Herkunftsmarkierung der Datensaetze kommt weiterhin aus dem Mandantenfeld der jeweiligen Zeile — dieses Feld existiert nach Task 2 unveraendert. Zu aendern sind nur zwei Dinge: (a) die Warnmeldung im Fehlerfall benennt jetzt das Postfach ueber die Zeilen-`id` und die Besitzer-Kennung statt des Mandanten, weil ein Mandant ab sofort mehrere Postfaecher haben kann und die alte Meldung sonst mehrdeutig waere — Zugangsdaten duerfen dabei nach wie vor nirgends im Protokoll landen; (b) der Klassenkommentar erklaert, dass die Auffaecherung ab Phase 17 pro Postfach und nicht mehr pro Mandant laeuft. Die ausdrueckliche Warnung im Kommentar, dass diese Sammelabfrage niemals in einen Mandanten-Wrapper gehoert, bleibt Wort fuer Wort stehen. Die Fehlerbehandlung je Postfach (ein kaputtes Postfach blockiert die anderen nicht) bleibt unangetastet. **Tests `email-alert.adapter.spec.ts`.** Bestehende Faelle auf die neue Datenform nachziehen (Zeilen tragen jetzt zusaetzlich `userId`) und drei neue Faelle ergaenzen: 1. Zwei aktive Zeilen mit **gleichem** Mandanten, aber verschiedenen Besitzern und verschiedenen Postfachdaten: der Postfach-Anbieter wird zweimal aufgerufen, mit den jeweils eigenen Zugangsdaten, und das Ergebnis enthaelt die Kandidaten aus beiden Postfaechern. 2. Von zwei Zeilen wirft die erste beim Abholen einen Fehler: das Ergebnis enthaelt trotzdem die Datensaetze der zweiten, und eine Warnung wurde protokolliert. 3. Die Herkunftsmarkierung der erzeugten Datensaetze entspricht dem Mandantenfeld der jeweiligen Zeile — bei zwei Zeilen unterschiedlicher Mandanten also zwei unterschiedliche Werte. Wichtig: Die Erwartungswerte in diesen Tests von Hand hinschreiben. Sie duerfen **nicht** ueber dieselbe Hilfsfunktion erzeugt werden, die auch der Produktivcode benutzt — ein Test, der seine Erwartung mit dem Pruefling selbst baut, beweist nichts (dokumentierte Anti-Pattern im Projekt). **Tests `tender-email-config.service.spec.ts`.** Auf den neuen Schluessel umstellen: gelesen und geschrieben wird nach Besitzer. Zwei Faelle ergaenzen: Speichern legt beim Anlegen sowohl Besitzer als auch Mandant an; ein zweiter Nutzer im selben Mandanten erzeugt eine zweite Zeile statt die erste zu ueberschreiben. Der bestehende Fall, dass der verschluesselte Zugangsdatenblock nie in der Antwort auftaucht, bleibt erhalten und muss weiterhin durchlaufen. **Neuer Test `email-config-migration-sql.spec.ts`.** Nach dem Vorbild von `doe-url-migration-sql.spec.ts` die von Hand geschriebene Migrationsdatei ohne Datenbank pruefen, rein als Text: die Zuordnung zum Administrator kommt in der Datei vor dem Entfernen unbesetzter Zeilen; die Auswahl beschraenkt sich auf aktive Nutzer mit Administratorrolle desselben Mandanten und nimmt genau einen; die alte Eindeutigkeitsregel auf dem Mandantenfeld wird entfernt und eine neue auf dem Besitzerfeld angelegt; die Pflichtsetzung der neuen Spalte steht nach der Befuellung. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/tenders/adapters/email-alert.adapter.spec.ts src/tenders/tender-email-config.service.spec.ts src/tenders/email-config-migration-sql.spec.ts cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/tenders Alle Tests im Bereich `src/tenders` laufen gruen. Der Fall "zwei Postfaecher desselben Mandanten werden beide abgeholt" existiert und besteht. Der Fall "ein kaputtes Postfach blockiert die anderen nicht" existiert und besteht. Die Migrationsdatei wird textuell geprueft. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser -> API (`/modules/tender-radar/email-config`) | Ab sofort erreicht jeder Nutzer mit Modulfreigabe diese Endpunkte, nicht mehr nur Administratoren. Besitzangaben aus der Anfrage sind grundsaetzlich nicht vertrauenswuerdig. | | API -> Datenbank (`TenderEmailConfig`) | Traegt verschluesselte Postfach-Zugangsdaten. | | Plattform-Zeitplan -> fremdes Postfach | Ausgehende Verbindung an eine vom Nutzer angegebene Adresse. | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-17-01 | Elevation of Privilege | `GET/PUT /email-config` | high | mitigate | Besitzer und Mandant kommen ausschliesslich aus `extractTriageContext(req)`; die DTO traegt kein Besitzer- und kein Mandantenfeld. Test: ein zweiter Nutzer liest nie die Zeile des ersten. | | T-17-02 | Information Disclosure | Antwort von `GET /email-config` | high | mitigate | `EMAIL_CONFIG_SAFE_SELECT` schliesst den verschluesselten Zugangsdatenblock weiterhin aus; die Antwort traegt nur `hasPassword`. Bestehender Test bleibt aktiv. | | T-17-03 | Information Disclosure | Protokollausgabe im Abruf | medium | mitigate | Die neue Warnmeldung nennt nur Zeilen-`id` und Besitzer-Kennung, nie Benutzername, Passwort oder Postfachadresse. | | T-17-04 | Denial of Service | Abruf ueber mehrere Postfaecher | medium | mitigate | Fehlerbehandlung je Postfach bleibt erhalten — ein haengendes Postfach blockiert die uebrigen nicht. Test deckt den Fall ab. | | T-17-05 | Tampering | Hand geschriebene Migration | high | mitigate | Reihenfolge (befuellen vor loeschen, Pflicht nach Befuellung) wird textuell getestet; Anwendung zuerst nur lokal, Ausrollen auf den Testserver bleibt Nutzeraktion. | | T-17-06 | Elevation of Privilege | Wegfall der Rollenpruefung an den Postfach-Endpunkten | medium | accept | Beabsichtigt (D-01): das Postfach ist eine Nutzereinstellung. Der Zugang bleibt ueber die Modulfreigabe aus Phase 15 gedeckelt. | | T-17-SC | Tampering | Paketinstallationen | low | accept | Diese Phase installiert kein neues Paket — keine Lieferketten-Pruefung noetig. | 1. `pnpm --filter @tessera/api exec vitest run src/tenders` — vollstaendig gruen. 2. `pnpm --filter @tessera/api type-check` und `pnpm --filter @tessera/web type-check` — fehlerfrei. 3. `npx prisma migrate status` im API-Verzeichnis — keine offene Migration. 4. Indexliste von `TenderEmailConfig` enthaelt einen eindeutigen Index auf dem Besitzerfeld und keinen mehr auf dem Mandantenfeld. 5. Browser-Gegenprobe mit zwei Konten desselben Mandanten (siehe `human-check` in Task 2). - Zwei Nutzer desselben Mandanten haben je eine eigene Postfach-Zeile. - Ein Nutzer sieht ueber die API ausschliesslich sein eigenes Postfach. - Der Abruf holt in einem Durchlauf alle aktiven Postfaecher; ein Ausfall blockiert die anderen nicht. - Eine vorhandene Postfach-Zeile hat nach der Migration einen Besitzer. - `/modules/tender-radar/my-sources` ist fuer jeden Nutzer mit Modulfreigabe erreichbar und zeigt dessen Postfach. - `Tender` bleibt unveraendert ohne Mandantenfeld und ohne RLS (D-05). Create `.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-01-SUMMARY.md` when done. Im SUMMARY festhalten: Anzahl der auf dem Testserver vorgefundenen Postfach-Zeilen und wem sie zugeordnet wurden.