--- phase: quick-260925-bow plan: 01 quick_id: 260925-bow status: complete subsystem: web + api (Benutzer, Änderungsliste) tags: [release-notice, changelog, user, prisma, a11y, rls] requires: - CHANGELOG.md als Bauzeit-Text (quick-260916-dcz) - APP_VERSION als Laufzeit-ENV der API (quick-260914-ku1) provides: - "Spalte User.lastSeenReleaseVersion (Migration 20260925120000_user_last_seen_release)" - "parseReleaseVersion / compareReleaseVersions / ReleaseNoticeResponse in @tessera/shared" - "getRunningRelease() als einzige Quelle der laufenden Version" - "GET /users/me/release-notice, POST /users/me/release-seen" - "selectReleaseNotice (Web), Server-Aktionen, ReleaseNoticeDialog, ReleaseNoticeHost in AppShell" affects: - apps/web/src/components/layout/app-shell.tsx - apps/api/src/user/user.service.ts (create) - apps/api/src/user/admin-seed.service.ts tech-stack: added: [] patterns: - "Server-Aktion in eigener 'use server'-Datei als einziger Web-Importeur der Änderungsliste neben page.tsx" - "Fenster per React.lazy nur bei vorhandener Nachricht geladen" key-files: created: - apps/api/prisma/migrations/20260925120000_user_last_seen_release/migration.sql - apps/api/src/health/release-version.spec.ts - apps/api/src/user/dto/release-seen.dto.ts - apps/web/src/lib/release-notes.ts - apps/web/src/lib/release-notes.test.ts - apps/web/src/lib/release-notice-actions.ts - apps/web/src/lib/release-notice-actions.test.ts - apps/web/src/components/release-notice/release-notice-dialog.tsx - apps/web/src/components/release-notice/release-notice-dialog.test.tsx - apps/web/src/components/release-notice/release-notice-host.tsx - apps/web/src/components/release-notice/release-notice-host.test.tsx modified: - packages/shared/src/index.ts - apps/api/prisma/schema.prisma - apps/api/src/health/app-version.ts - apps/api/src/user/user.controller.ts - apps/api/src/user/user.controller.spec.ts - apps/api/src/user/user.service.ts - apps/api/src/user/user.service.spec.ts - apps/api/src/user/admin-seed.service.ts - apps/api/src/user/admin-seed.service.spec.ts - apps/web/src/components/changelog/changelog-view.tsx - apps/web/src/components/layout/app-shell.tsx - apps/web/src/messages/de.json - apps/web/src/messages/en.json - apps/web/src/messages/umlaut-dictionary.ts - apps/web/src/lib/changelog.ts - apps/web/next.config.ts - docs/mandantentrennung-zugriffsklassifikation.md - docs/anleitung-anwender.md - docs/anleitung-entwicklung.md - CHANGELOG.md decisions: - "Einzige Quelle der laufenden Version ist APP_VERSION der API (getRunningRelease()); das Web nimmt currentRelease aus GET /users/me/release-notice" - "Fehlt der Abschnitt der laufenden Version in der Änderungsliste des Web-Abbilds ganz, gibt es kein Fenster; eine laufende Version nur mit „Entfernt“ zählt ebenso als ohne Abschnitt" - "Scrollbereich des Fensters ist ein benannter section ohne tabIndex (keine neue Biome-Warnung, keine Biome-Ausnahme)" metrics: duration: 16min completed: 2026-09-25 actuals: tokens: 30400 tasks: 3 commits: 9 plan_head_before: 9225ed1bf980aa688e50b55f23162927bf95b40c --- # Quick 260925-bow: „Was ist neu“-Fenster beim ersten Anmelden nach einem Versionswechsel Summary Nach einem Versionswechsel zeigt Tessera jedem Benutzer beim ersten Laden des Portals einmal ein Fenster „Neu in Version X.Y.Z“ mit Neu / Verbessert / Behoben aus CHANGELOG.md (höchstens drei Versionen, neueste zuerst). Der gesehene Stand liegt pro Benutzer in der neuen Spalte `User.lastSeenReleaseVersion` und wird erst beim Schließen über `POST /users/me/release-seen` gemerkt, gebunden an Benutzer und Mandanten. ## Was gebaut wurde **Aufgabe 1 (Tracer, DB → API → Server-Aktion → Fenster)** - `packages/shared`: `parseReleaseVersion` (optionales `v`, drei Zifferngruppen zu je 1 bis 6 Ziffern, optional Describe-Anhang, verankert, ≤ 64 Zeichen, kanonisch ohne führende Nullen), `compareReleaseVersions` (numerisch, wirft bei Unparsebarem), `ReleaseNoticeResponse`. Warnkommentar über `WIDGET_TYPES` ergänzt. - `getRunningRelease()` in `app-version.ts` mit Begründung, warum die API die Quelle ist. - Spalte + Migration (`ALTER TABLE "User" ADD COLUMN "lastSeenReleaseVersion" TEXT;`), kein Standardwert, kein Backfill. **Lokal auf die DB angewendet** (`prisma migrate deploy` über 172.19.0.2); `tessera_app` hat Rechte auf Tabellenebene und damit auch auf die neue Spalte. - `GET me/release-notice` und `POST me/release-seen` in `user.controller.ts` vor der Kennungs-Route, ohne `@Roles`, beide über `forTenant()` + `where: { id: currentUser.id }`. `ReleaseSeenDto` mit `@IsString`, `@MaxLength(32)`, `@Matches` auf kanonisches X.Y.Z; im Rumpf zusätzlich Format-, dev- und „nicht über laufend“-Prüfung; nie absenken, unparsebaren Altwert überschreiben. - Web: `release-notes.ts` (reine Auswahl, kein Import der Änderungsliste), `release-notice-actions.ts` (`'use server'`), `ReleaseNoticeDialog` (role=dialog, aria-modal, aria-labelledby, Anfangsfokus Überschrift, Escape, Fokusfalle mit dynamischer Elementliste, Fokus-Rückgabe, keine Animation, nur Tailwind-Tokens), `ReleaseNoticeHost` (Ref-Sperre, nicht auf `/change-password`, `React.lazy`, merkt erst beim Schließen), eingebunden nur in `AppShell`. `ChangelogView` mit `variant="plain"`. Texte `releaseNotice` in de.json/en.json. **Aufgabe 2 (Anlagewege)** - `UserService.create()` (Admin-Anlage und beide LDAP-Wege) und `AdminSeedService` setzen `lastSeenReleaseVersion: getRunningRelease()`; kein neuer Parameter. grep-Nachweis: kein eigener `user.create` in `apps/api/src/ldap`. - RLS-Buchführung nachgemessen mit der Gate-Schleife: **user 8/17/0** (vorher 8/14/0, +3 gebunden in `user.controller.ts`), **Summe 61/216/6** (vorher 61/213/6). Übersichts-, Summen- und Fundstellenzeile mit Vermerk **quick-260925-bow** fortgeschrieben. `rls-access-inventory.spec.ts` grün (Paar `user.controller.ts | user | gebunden` unverändert). **Aufgabe 3 (Doku und volle Prüfungen)** - CHANGELOG „Unveröffentlicht → Neu“, Abschnitt „Was ist neu“ im Anwenderhandbuch, Entwicklerdoku (Importregel erweitert, Versionsquelle, Endpunkte, Spalte, Folge für die Freigabe, Ausprobieren mit `APP_VERSION`), Kopfkommentare `changelog.ts` und `next.config.ts`. ## Gemessene Ergebnisse | Prüfung | Ergebnis | |---|---| | API-Suite (voll) | 85 Dateien, **1435 Tests grün** | | Web-Suite (voll) | 95 Dateien, **924 Tests grün** | | `pnpm turbo run type-check lint` | 9/9 Tasks erfolgreich | | Biome-Warnungen | **Web 53** (Grenze 53), **API 82** (Grenze 82) | | Stil-Gate (Versal/Sperrschrift, `·`, `→`, rohes HTML) | leer | | i18n-Gate `releaseNotice` de/en | vollständig | | RLS user / Summe | 8/17/0 / 61/216/6 | | `next build` | erfolgreich | | Chunk-Nachweis | Satz „hochgeladene Bilder liegen jetzt im Dateibereich des Servers“: 2 Dateien unter `.next/server` (`changelog/page.js` und der Server-Chunk der Aktion), **0 unter `.next/static`**; ebenso der neue CHANGELOG-Eintrag (2 / 0). `next-env.d.ts` unverändert. | Abgleich mit der echten CHANGELOG.md: gemerkt `1.0.0`, laufend `1.4.0` ergibt 1.4.0 (new/changed/fixed), 1.3.1 (changed/fixed), 1.3.0 (new/changed/fixed) und `omittedCount` 2, also genau das, was die Browserprüfung des Orchestrators erwartet; gemerkt `null` ergibt nur 1.4.0; gemerkt `1.4.0` ergibt kein Fenster. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] StrictMode-Doppeleffekt verwarf das Abrufergebnis im Host** - **Found during:** Aufgabe 1 (Host-Test „StrictMode-Doppeleffekt führt nicht zu zwei Abrufen“) - **Issue:** Ein Abbruch-Flag im Aufräumen des Effekts wurde beim simulierten Unmount gesetzt, der zweite Effektlauf fragte wegen der Ref-Sperre nicht erneut, das einzige Ergebnis ging verloren, kein Fenster. - **Fix:** Abbruch-Flag entfernt (setState nach Unmount ist in React 19 folgenlos), Kommentar dazu. - **Commit:** 187fb76 **2. [Rule 3 - Blocking] Umlaut-Wächter kannte „Verbessert“ nicht** - **Found during:** Aufgabe 3 (volle Web-Suite) - **Issue:** `umlaut-guard.spec.ts` meldete das korrekt geschriebene Wort „Verbessert“ (Gruppe `releaseNotice.section.changed`) als unbekanntes „ss“-Wort. - **Fix:** „Verbessert“ in `UMLAUT_ALLOWLIST` (`umlaut-dictionary.ts`) mit Vermerk aufgenommen. - **Commit:** 2aeb3e8 **3. [Rule 3 - Blocking] Scrollbereich ohne `tabIndex={0}`** - **Issue:** Der geplante `div tabIndex={0} aria-label` erzeugte zwei neue Biome-Warnungen (`noNoninteractiveTabindex`, `useAriaPropsSupportedByRole`) und hätte die Grenze Web ≤ 53 gerissen; Biome-Ausnahmen in neuen Dateien sind laut Plan verboten. - **Fix:** Benannter `
` ohne tabIndex. Heutige Chromium- und WebKit-Versionen (Browser und Desktop-App) machen einen Scroll-Container ohne fokussierbaren Inhalt selbst per Tastatur erreichbar; enthält er Links, scrollt der Tab-Fokus mit. Innere Versionsblöcke sind `div` statt `section`, damit keine verschachtelten Landmarken entstehen. - **Commit:** 187fb76 **4. [Kleinigkeit] Überschriften-Hierarchie** - Bei einer Version sind die Gruppen `h3` (wie geplant); bei mehreren Versionen trägt die Version `h3` und die Gruppen `h4` (statt einer Versionszeile ohne Überschriftenrolle). Test entsprechend. **5. [Hinweis] `prisma validate` braucht DATABASE_URL** - Das Aufgabe-1-Gate ruft `prisma validate` ohne Umgebung auf; hier scheitert das nur an der fehlenden `DATABASE_URL` (P1012). Mit gesetzter URL (echte Container-IP bzw. Platzhalter) ist das Schema gültig. `prisma format` wurde bewusst NICHT verwendet, weil es die ganze Schemadatei umformatiert hätte; die neue Zeile ist von Hand eingetragen. **6. [Hinweis] Bestehende Format-Befunde nicht angefasst** - `biome check` meldet in `app-shell.tsx` und `changelog-view.tsx` Format-/Importreihenfolge-Befunde, die schon vorher bestanden; das Lint-Skript ist `biome lint`, die Dateien wurden deshalb nicht umformatiert. Neue Dateien sind mit `biome check --write` formatiert, ohne Ausnahmen, `any` nur in Test-Attrappen wie im Bestand. ## Hinweise für den Orchestrator (Browserprüfung, D-11) - Die Migration ist auf der lokalen DB bereits angewendet; der laufende API-Container (altes Abbild) stört sich an der zusätzlichen nullbaren Spalte nicht. Für die Prüfung muss wie im Plan beschrieben mit `--build-arg APP_VERSION=1.4.0-1-g0000000` neu gebaut werden (der Describe-Anhang braucht mindestens 4 Hex-Zeichen nach `g`, `g0000000` passt). - Auf `/change-password` wird nicht abgefragt; die nächste Seite danach fragt einmal. ## Threat Flags Keine neuen Angriffsflächen außerhalb des Bedrohungsmodells: die zwei Endpunkte (T-BOW-01/02/05/06), die Migration (T-BOW-07), die Server-Aktion/Chunks (T-BOW-03) und das Markdown-Rendering über `ChangelogView` mit `rehype-sanitize` (T-BOW-04) sind dort erfasst und umgesetzt. ## Commits | Hash | Nachricht | |---|---| | b35edd5 | test(260925-bow): Versionsvergleich und Was-ist-neu-Endpunkte (rot) | | 59db32a | feat(260925-bow): gesehene Version pro Benutzer merken - Spalte, Versionsfunktionen, API | | cf7784e | test(260925-bow): Auswahl, Server-Aktionen, Fenster und Host des Was-ist-neu-Fensters | | 187fb76 | feat(260925-bow): Was-ist-neu-Fenster im Portal-Rahmen | | 25c8db7 | test(260925-bow): Anlagewege tragen die laufende Version ein (rot) | | 5ae9aaa | feat(260925-bow): neue Benutzer bekommen die laufende Version eingetragen | | 4fa5aaf | docs(260925-bow): RLS-Buchfuehrung nachgemessen (user 8/17/0, Summe 61/216/6) | | 2aeb3e8 | fix(260925-bow): Umlaut-Waechter kennt das korrekte Wort Verbessert | | b3b7b5d | docs(260925-bow): Was-ist-neu-Fenster in CHANGELOG, Anwender- und Entwicklerdoku | Nicht gepusht, kein Tag, keine Freigabe. `.planning/**` nicht committet. ## Self-Check: PASSED Alle 11 neuen Dateien vorhanden, alle 9 Commits im Verlauf (`git rev-list --count 9225ed1..HEAD` = 9).