Files
schalli acfffa3097
Tessera CI/CD / Lint & Type Check (push) Successful in 51s
Tessera CI/CD / Tests (push) Successful in 1m20s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m13s
docs(quick-260925-bow): Was-ist-neu-Fenster
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 09:01:48 +02:00

50 KiB
Raw Permalink Blame History

phase, plan, quick_id, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
phase plan quick_id type wave depends_on autonomous requirements files_modified estimate must_haves
quick-260925-bow 01 260925-bow execute 1
true
QUICK-260925-bow
packages/shared/src/index.ts
apps/api/prisma/schema.prisma
apps/api/prisma/migrations/20260925120000_user_last_seen_release/migration.sql (neu)
apps/api/src/health/app-version.ts
apps/api/src/health/release-version.spec.ts (neu)
apps/api/src/user/dto/release-seen.dto.ts (neu)
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/lib/release-notes.ts (neu)
apps/web/src/lib/release-notes.test.ts (neu)
apps/web/src/lib/release-notice-actions.ts (neu)
apps/web/src/lib/release-notice-actions.test.ts (neu)
apps/web/src/components/release-notice/release-notice-dialog.tsx (neu)
apps/web/src/components/release-notice/release-notice-dialog.test.tsx (neu)
apps/web/src/components/release-notice/release-notice-host.tsx (neu)
apps/web/src/components/release-notice/release-notice-host.test.tsx (neu)
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/lib/changelog.ts (nur Kopfkommentar)
apps/web/next.config.ts (nur Kommentar)
docs/mandantentrennung-zugriffsklassifikation.md
docs/anleitung-anwender.md
docs/anleitung-entwicklung.md
CHANGELOG.md
tokens raw_tokens tasks confidence
90000 90000 3 low
truths artifacts key_links
Wer sich nach einem Versionswechsel zum ersten Mal im Portal anmeldet (laufende freigegebene Version liegt über der zuletzt gesehenen), sieht einmalig ein Fenster „Neu in Version X.Y.Z“ mit den Gruppen „Neu“, „Verbessert“ (aus Geändert) und „Behoben“ aus CHANGELOG.md, neueste Version zuerst (D-02, D-03, D-07)
„Verstanden“, das Schließen-Kreuz, Escape, ein Klick auf den abgedunkelten Hintergrund oder auf „Alle Änderungen ansehen“ schließen das Fenster und merken die Version dauerhaft pro Benutzer in der Datenbank – im Browser und in der Desktop-App erscheint es danach nicht mehr; gemerkt wird erst beim Schließen, nie beim Öffnen (D-01, D-05)
Wer mehrere Versionen verpasst hat, sieht höchstens die drei neuesten; bei mehr steht ein Satz mit der Zahl der weiteren Versionen im Fenster; unten steht immer der Link „Alle Änderungen ansehen“ zu /changelog (D-03)
Bestandsbenutzer ohne gemerkten Stand sehen nur die laufende Version; Benutzer, die ein Administrator anlegt, die der LDAP-Abgleich anlegt, und der Erst-Administrator bekommen bei der Anlage die laufende Version eingetragen und sehen kein Fenster bis zur nächsten Version (D-04)
Ohne gültige freigegebene Versionsnummer (lokaler Stand „dev“, bloßer Commit-Stempel) erscheint nie ein Fenster; auf der Anmeldeseite und auf /change-password erscheint es auch nicht; der Erststart-Dialog der Desktop-App und die Bildschirmfoto-Funktion von „Fehler melden“ bleiben unberührt (D-02, D-08)
Der Server nimmt als „gesehen“ nur eine wohlgeformte Version X.Y.Z an, die nicht über der laufenden liegt, senkt einen gemerkten Stand nie ab und ändert ausschließlich die Zeile des angemeldeten Benutzers in dessen Mandanten (D-05)
Der Text der Änderungsliste bleibt im Server-Bundle; in den öffentlich abrufbaren Client-Chunks unter /_next/static steht er nicht (Bestandsregel aus quick-260916-dcz)
path provides contains
packages/shared/src/index.ts parseReleaseVersion, compareReleaseVersions, ReleaseNoticeResponse — eine Implementierung für API und Web (D-02, D-06) export function parseReleaseVersion
path provides contains
apps/api/prisma/migrations/20260925120000_user_last_seen_release/migration.sql nullbare Spalte User.lastSeenReleaseVersion (D-01) lastSeenReleaseVersion
path provides contains
apps/api/src/health/app-version.ts getRunningRelease() — einzige Quelle der laufenden Version (API-APP_VERSION) export function getRunningRelease
path provides contains
apps/api/src/user/user.controller.ts GET /users/me/release-notice, POST /users/me/release-seen me/release-seen
path provides contains
apps/web/src/lib/release-notes.ts selectReleaseNotice — reine Auswahl der Versionsabschnitte (Bereich, Deckel 3, null, unparsbar) export function selectReleaseNotice
path provides contains
apps/web/src/lib/release-notice-actions.ts Server-Aktionen fetchReleaseNotice / markReleaseSeenAction ('use server') 'use server'
path provides contains
apps/web/src/components/release-notice/release-notice-dialog.tsx barrierefreies Fenster (role=dialog, aria-modal, Fokusfalle, Escape) aria-modal
path provides contains
apps/web/src/components/release-notice/release-notice-host.tsx lädt die Nachricht einmal je Seitenladung im Portal-Rahmen und merkt beim Schließen markReleaseSeenAction
from to via pattern
apps/web/src/components/layout/app-shell.tsx apps/web/src/components/release-notice/release-notice-host.tsx <ReleaseNoticeHost /> im Portal-Rahmen (nur (portal)-Layout, nie /login) ReleaseNoticeHost
from to via pattern
apps/web/src/lib/release-notice-actions.ts GET /users/me/release-notice fetch mit Session-Cookie über API_INTERNAL_URL, danach selectReleaseNotice(changelogMarkdown, …) users/me/release-notice
from to via pattern
apps/web/src/components/release-notice/release-notice-host.tsx POST /users/me/release-seen markReleaseSeenAction(notice.currentRelease) im onClose markReleaseSeenAction
from to via pattern
apps/api/src/user/user.service.ts + admin-seed.service.ts apps/api/src/health/app-version.ts lastSeenReleaseVersion: getRunningRelease() bei jeder Benutzeranlage getRunningRelease

Quick 260925-bow — „Was ist neu“-Fenster beim ersten Anmelden nach einem Versionswechsel

Nach einem Versionswechsel zeigt Tessera jedem Benutzer beim ersten Laden des Portals einmal ein Fenster mit den für ihn wichtigen Änderungen (Neu / Verbessert / Behoben) aus CHANGELOG.md. Der gesehene Stand wird pro Benutzer in der Datenbank gemerkt, damit das Fenster im Browser und in der Desktop-App genau einmal erscheint.

Purpose: Anwender erfahren ohne Suchen, was sich geändert hat und welche Fehler behoben sind (Nutzerwunsch vom 25.09.). Output: neue Spalte + Migration, zwei API-Endpunkte, reine Versions- und Auswahlfunktionen mit Tests, Server-Aktionen, barrierefreies Fenster im Portal-Rahmen, Doku, CHANGELOG-Eintrag.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/STATE.md @./CLAUDE.md

Verbindliche Entscheidungen des Orchestrators (hier nummeriert, in den Aufgaben zitiert)

  • D-01 Pro Benutzer in der DB: neue nullbare Spalte User.lastSeenReleaseVersion String? (Prisma-Migration). Gilt damit für Browser und Desktop-App.
  • D-02 Nur freigegebene Versionen zählen: die laufende Version kommt aus APP_VERSION (führendes v und den Describe-Anhang -N-g<sha> entfernen, z. B. v10.2.3-5-gabc1234 → 10.2.3). Nicht parsebar (dev, bloßer Commit-SHA) → nie ein Fenster.
  • D-03 Einmal nach der Anmeldung im Portal-Rahmen (nicht auf der Anmeldeseite), wenn laufende Version > gemerkte. Inhalt: jede freigegebene Version aus CHANGELOG mit gemerkt < Version ≤ laufend, neueste zuerst, nur die Abschnitte Neu / Geändert / Behoben (andere Abschnitte und leere weglassen). Höchstens 3 Versionen; bei mehr ein Satz plus Link „Alle Änderungen ansehen“ zu /changelog. Unten immer ein Link zu /changelog.
  • D-04 lastSeen === null (Bestandsbenutzer beim ersten Ausrollen): nur der Abschnitt der laufenden Version. Neu angelegte Benutzer bekommen bei der Anlage die laufende Version eingetragen (alle Anlagewege). Die API braucht dafür die Version selbst — eine einzige Quelle wählen und dokumentieren.
  • D-05 Schließen („Verstanden“, Escape, Klick auf den Hintergrund) merkt über einen kleinen angemeldeten Endpunkt (POST /users/me/release-seen mit der Version); der Server prüft Wohlgeformtheit und „nicht größer als die laufende Version“. Erst beim Schließen merken, nicht beim Öffnen.
  • D-06 Versionsvergleich: numerischer semver-Vergleich, reine Funktion, mit Unit-Tests.
  • D-07 Barrierefreies Fenster (role="dialog", aria-modal, Fokusfalle, Anfangsfokus auf Überschrift oder Schließen-Knopf, Escape schließt, reduzierte Bewegung). Vorbilder: widget-catalog-modal.tsx, bug-report-dialog.tsx. Tailwind-4-Tokens, Dunkelmodus. Titel „Neu in Version 1.4.0“; Gruppen „Neu“, „Verbessert“ (für Geändert), „Behoben“; Einträge mit demselben Renderer wie die Seite /changelog. Sie-Form, Schlüssel in de.json + en.json.
  • D-08 Nicht auf der Anmeldeseite; darf den Erststart-Dialog der Desktop-App nicht blockieren; darf die Bildschirmfoto-Funktion von „Fehler melden“ nicht stören (offen sein ist in Ordnung).
  • D-09 Tests: Parser/Auswahl (Bereich, Deckel 3, null, unparsebar), semver-Vergleich, Fenster rendern/schließen merkt, nicht gezeigt wenn aktuell, Endpunkt-Validierung + Mandanten-/Benutzerbindung, Anlagewege setzen das Feld. Volle API- und Web-Suiten, turbo type-check lint, Biome-Warnungen Web ≤ 53, API ≤ 82 (Stand vorher gemessen: 53 / 82), RLS-Bestandsaufnahme-Spec + docs/mandantentrennung-zugriffsklassifikation.md nachziehen.
  • D-10 CHANGELOG „Unveröffentlicht → Neu“-Eintrag; Erwähnung im Anwenderhandbuch.
  • D-11 Browserprüfung macht der Orchestrator (siehe <verification>), nicht der Executor.

Einzige Quelle der laufenden Version (Entscheidung zu D-04, von Claude getroffen)

Die API-Umgebungsvariable APP_VERSION, gelesen über getRunningRelease() in apps/api/src/health/app-version.ts. Begründung: zwei der drei Anlagewege laufen ohne jede Web-Anfrage (LDAP-Abgleich per Zeitplan, Erst-Administrator beim API-Start) und können die Version nur aus der API kennen; die Prüfung „nicht größer als laufend“ in POST /users/me/release-seen ebenso. Das Web wertet für diese Funktion seine eigene NEXT_PUBLIC_APP_VERSION NICHT aus, sondern nimmt currentRelease aus der Antwort von GET /users/me/release-notice. Beide Abbilder bekommen im CI denselben APP_VERSION-Wert (.gitea/scripts/publish-images.sh, eine Schleife für web und api), deshalb passen Änderungsliste (im Web-Abbild) und Version (aus der API) im Betrieb zusammen. Fehlt der Abschnitt der laufenden Version in der Änderungsliste des Web-Abbilds, entsteht einfach kein Fenster (und nichts wird gemerkt). Parse- und Vergleichsfunktion stehen EINMAL in packages/shared/src/index.ts und werden von API und Web importiert.

Bestand, den der Executor kennen muss (vom Planer gelesen)

  • apps/web/src/lib/changelog.ts: changelogMarkdown (Bauzeit-Text aus TESSERA_CHANGELOG_MD), filterChangelogForChannel. Dieses Modul darf nur Server-Code importieren — sonst landet der Text in öffentlichen Client-Chunks. Eine 'use server'-Datei ist Server-Code (Client-Komponenten bekommen nur eine Aktions-Referenz).
  • apps/web/src/components/changelog/changelog-view.tsx: ChangelogView rendert Markdown mit MDEditor.Markdown + rehype-sanitize, Farbmodus nach Mount. Wird wiederverwendet (D-07).
  • apps/web/src/components/layout/app-shell.tsx: Portal-Rahmen (nur im (portal)-Layout; /login liegt in (auth) ohne AppShell). Anmeldung navigiert per window.location.href → AppShell wird frisch gemountet.
  • apps/web/src/lib/auth-actions.ts + auth-actions.test.ts: Muster für Server-Aktionen (Cookie session → Cookie: session=… an API_URL = process.env.API_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001') und deren Tests (next/headers gemockt, fetch per vi.stubGlobal).
  • apps/web/src/middleware.ts leitet bei mustChangePassword auf /change-password (liegt IM Portal-Rahmen).
  • apps/api/src/health/app-version.ts: getAppVersion() liest process.env.APP_VERSION || 'dev' zur Laufzeit (Tests: vi.stubEnv, Vorbild health.controller.spec.ts).
  • packages/shared/src/index.ts: rohes TypeScript ohne Bauschritt, die API lädt es im Betrieb über das Type-Stripping von Node 24 → nur löschbare Syntax (keine enum, kein namespace, keine Parameter-Eigenschaften), keine relativen Importe in neue Dateien (CJS-require findet keine .ts-Endung) — deshalb alles direkt in index.ts. Das Web importiert bereits Laufzeitwerte daraus (widget-registry.tsx).
  • apps/api/src/user/user.controller.ts: @Controller('users') + @UseGuards(RolesGuard); Selbstbedienungswege ohne @Roles (Vorbild PATCH me/accent-color: forTenant(this.prisma, currentUser.tenantId) → tenantPrisma.user.update({ where: { id: currentUser.id } … })). Globale ValidationPipe({ whitelist: true, transform: true }) → Body braucht eine DTO-Klasse mit class-validator-Dekoratoren, sonst werden Felder entfernt. Route-Reihenfolge: neue statische me/…-Routen VOR @Get(':id') einfügen (Projektregel gegen 404-Shadowing).
  • apps/api/src/user/user.controller.spec.ts: Zwei-Klienten-Attrappe (forTenant gemockt → prisma.__makeBoundClient(tenantId), scopedFindUnique/scopedUpdate filtern nach Mandant, boundCallLog).
  • Benutzer-Anlagewege (per grep user\.create vollständig ermittelt): UserService.create() in apps/api/src/user/user.service.ts (einziger Erzeugungspunkt für Admin-Anlage POST /users UND beide LDAP-Wege LdapService.upsertMappedUser/importUsersByDn) und AdminSeedService in apps/api/src/user/admin-seed.service.ts (Erst-Administrator). apps/api/scripts/rls-scratch-check.mjs legt nur Wegwerf-Testbenutzer in einer Prüf-DB an — kein Produktweg, bleibt unverändert.
  • Migrationen laufen beim API-Start (apps/api/scripts/migrate-and-start.sh). Letzte vorhandene: 20260924120000_dashboard_image_drop_data. Die Anmelde-Funktionen auth_lookup_* liefern eine feste Spaltenliste (RETURNS TABLE) — eine neue Spalte berührt sie nicht.
  • RLS-Buchführung: apps/api/src/prisma/rls-access-inventory.spec.ts prüft Paare (Datei, Modell); das Paar user.controller.ts | user | gebunden existiert. Die Übersichtszeile | user | 8 | 14 | 0 | und die Summenzeile | **Summe** | **61** | **213** | **6** | in docs/mandantentrennung-zugriffsklassifikation.md werden mit der Gate-Schleife nachgerechnet (siehe Aufgabe 2).
  • Stilregeln neuer UI-Dateien (aus quick-260924-i8v übernommen, Gate in Aufgabe 3): keine Versal- oder Sperrschrift-Klassen, keine Mittelpunkt- oder Pfeilzeichen in Texten, kein rohes HTML-Einfügen.
  • Commits je Aufgabe mit feat(260925-bow) / test(260925-bow) / docs(260925-bow); .planning/** committet der Executor nicht. Nicht pushen, kein Tag, keine Freigabe.
Aufgabe 1 (Tracer): Bestandsbenutzer mit altem Stand sieht das Fenster, Schließen merkt die Version — DB → API → Server-Aktion → Fenster im Portal packages/shared/src/index.ts, apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260925120000_user_last_seen_release/migration.sql, apps/api/src/health/app-version.ts, apps/api/src/health/release-version.spec.ts, apps/api/src/user/dto/release-seen.dto.ts, apps/api/src/user/user.controller.ts, apps/api/src/user/user.controller.spec.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, 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/lib/changelog.ts, apps/web/src/lib/changelog.test.ts (Abschnittszerlegung, CRLF-Normalisierung, Testform) - apps/web/src/components/changelog/changelog-view.tsx - apps/web/src/components/dashboard/widget-catalog-modal.tsx (Hintergrund als echte Schaltfläche, role=dialog, Escape) und apps/web/src/components/bug-report/bug-report-dialog.tsx (Knopfklassen `bg-primary … text-primary-foreground`) - apps/web/src/lib/auth-actions.ts (fetchSessionState) und apps/web/src/lib/auth-actions.test.ts - apps/web/src/components/layout/app-shell.tsx, apps/web/src/components/layout/header.tsx (Server-Aktion aus useEffect, Ref-Sperre gegen StrictMode-Doppeleffekt) - packages/shared/src/index.ts (Warnkommentar über WIDGET_TYPES), apps/api/src/health/app-version.ts, apps/api/src/health/health.controller.spec.ts - apps/api/src/user/user.controller.ts (Selbstbedienungswege ab `me/avatar`), apps/api/src/user/user.controller.spec.ts, apps/api/src/user/dto/create-user.dto.ts - apps/api/prisma/schema.prisma (model User), apps/api/prisma/migrations/20260702000000_add_user_accent_color/migration.sql (Form einer Spaltenergänzung) - Versionen (shared, getestet in apps/api/src/health/release-version.spec.ts): parseReleaseVersion liefert für `v10.2.3` → `10.2.3`, `10.2.3` → `10.2.3`, `v10.2.3-5-gabc1234` → `10.2.3`, `10.2.3-12-g0123456789abcdef` → `10.2.3`, führende Nullen `010.02.3` → `10.2.3`; `null` für `dev`, leer, bloßen SHA `abc1234`, `10.2`, `10.2.3-rc.1`, `10.2.3-dirty`, Leerzeichen am Rand, Eingaben über 64 Zeichen. compareReleaseVersions: `1.10.0` > `1.9.0` (numerisch, nicht lexikografisch), `2.0.0` > `1.99.99`, gleich → 0, `v10.2.3` gegen `10.2.3` → 0; wirft bei nicht parsebarer Eingabe. getRunningRelease mit `vi.stubEnv('APP_VERSION', 'v10.2.3-5-gabc1234')` → `10.2.3`, mit `dev` oder ungesetzt → null - API GET /users/me/release-notice: liefert `{ currentRelease, lastSeenReleaseVersion }` des angemeldeten Benutzers, gelesen über den an `currentUser.tenantId` gebundenen Klienten (boundCallLog), fremder Benutzer desselben Ids in anderem Mandanten ist unsichtbar → NotFoundException - API POST /users/me/release-seen: `10.2.3` bei laufend `10.2.3` → gespeichert, Antwort nennt den gespeicherten Stand; Formate `v10.2.3`, `10.2`, `abc`, `10.2.3-5-gabc1234`, Nicht-String → BadRequestException; Version über der laufenden → BadRequestException; laufend nicht parsebar (`dev`) → BadRequestException; gemerkt `10.2.3`, gesendet `10.1.0` → bleibt `10.2.3` (nie absenken); gemerkter Wert unparsebar → wird überschrieben; zwei Benutzer in zwei Mandanten: nur die Zeile des Anfragenden ändert sich - Web selectReleaseNotice (release-notes.test.ts, eigene Markdown-Fixtures): current null oder unparsebar → null; lastSeen null → nur der Abschnitt der laufenden Version; lastSeen ≥ current → null; lastSeen `1.3.0`, current `1.4.0` → Versionen 1.4.0 und 1.3.1 (neueste zuerst), omittedCount 0; lastSeen `1.0.0` bei fünf Versionen darüber → die drei neuesten, omittedCount 2; lastSeen unparsebar → wie null; „Unveröffentlicht“ mit Punkten wird nie ausgewählt; Versionen über current (Web neuer als API) werden ausgelassen; laufende Version ohne Abschnitt in der Liste → null; nur Neu/Geändert/Behoben in fester Reihenfolge new, changed, fixed unabhängig von der Dateireihenfolge; „Entfernt“ und leere Abschnitte fehlen; eine Version nur mit „Entfernt“ fällt ganz weg; CRLF wird normalisiert - Web Server-Aktionen: ohne Cookie → null und kein fetch; API 200 → Ergebnis von selectReleaseNotice auf dem (gemockten) changelogMarkdown; API nicht-ok oder Netzfehler → null; markReleaseSeenAction schickt POST mit `Content-Type: application/json`, Cookie und `{ version }`, liefert `{ success: false }` bei nicht-ok - Fenster: Titel „Neu in Version 1.4.0“ (Schlüssel), role=dialog mit aria-modal und aria-labelledby auf die Überschrift, Anfangsfokus auf der Überschrift; Gruppenüberschriften aus den Schlüsseln new/changed/fixed; Versionsunterüberschriften nur bei mehr als einer Version; Satz über weitere Versionen nur bei omittedCount > 0; Link zu /changelog immer vorhanden; „Verstanden“, Kreuz, Escape, Hintergrund und Link rufen onClose genau einmal; Tab vom letzten fokussierbaren Element springt zum ersten, Umschalt+Tab vom ersten zum letzten - Host: ohne Nachricht rendert er nichts; mit Nachricht erscheint das Fenster, markReleaseSeenAction wird beim Öffnen NICHT aufgerufen; nach Schließen verschwindet das Fenster und markReleaseSeenAction wurde genau einmal mit currentRelease aufgerufen; auf `/change-password` wird fetchReleaseNotice nicht aufgerufen, nach dem Wechsel auf `/` genau einmal; StrictMode-Doppeleffekt führt nicht zu zwei Abrufen Umsetzung in dieser Reihenfolge, jeweils Test zuerst (rot), dann Code (grün):
  1. Versionen (D-02, D-06) in packages/shared/src/index.ts direkt (keine neue Datei, nur löschbare Syntax; den Warnkommentar über WIDGET_TYPES um den Hinweis ergänzen, dass diese Funktionen der zweite Laufzeit-Import der API sind): parseReleaseVersion(raw: string): string | null mit dem Muster optionales v, drei Zifferngruppen zu je 1–6 Ziffern, optional genau der Describe-Anhang -<Zahl>-g<4–40 Hex-Zeichen>, ganze Zeichenkette verankert, kein Trimmen, Länge über 64 ergibt null; Rückgabe kanonisch Number(a).Number(b).Number(c). compareReleaseVersions(a: string, b: string): number parst beide, wirft Error bei null, vergleicht die drei Zahlen der Reihe nach und liefert -1/0/1. Dazu export interface ReleaseNoticeResponse { currentRelease: string | null; lastSeenReleaseVersion: string | null }. Tests in apps/api/src/health/release-version.spec.ts (API-Suite, weil packages/shared keinen eigenen Testlauf hat; Vorbild widget-module-map.spec.ts).

  2. Laufende Version der API (einzige Quelle, siehe Kontext): in apps/api/src/health/app-version.ts export function getRunningRelease(): string | null = parseReleaseVersion(getAppVersion().version), Laufzeit-Import aus @tessera/shared, Kopfkommentar ergänzen (warum die API die Quelle ist). Tests im selben Spec.

  3. Spalte (D-01): lastSeenReleaseVersion String? im model User (neben accentColor), Migration apps/api/prisma/migrations/20260925120000_user_last_seen_release/migration.sql mit Kopfkommentar (quick-260925-bow, wozu die Spalte dient, null = Bestandsbenutzer) und genau ALTER TABLE "User" ADD COLUMN "lastSeenReleaseVersion" TEXT;. Danach pnpm --filter @tessera/api exec prisma generate. Kein Standardwert, kein Backfill (D-04: null ist gewollt).

  4. Endpunkte (D-05) in apps/api/src/user/user.controller.ts, beide VOR @Get(':id'), ohne @Roles (jeder angemeldete Benutzer), beide über forTenant(this.prisma, currentUser.tenantId) mit where: { id: currentUser.id } — kein Kennungsparameter aus der Anfrage. GET me/release-notice: findUnique mit select: { lastSeenReleaseVersion: true }, fehlt die Zeile, dann NotFoundException, sonst ReleaseNoticeResponse mit currentRelease: getRunningRelease(). POST me/release-seen mit @HttpCode(HttpStatus.OK) und neuer DTO apps/api/src/user/dto/release-seen.dto.ts (ReleaseSeenDto, version mit @IsString(), @MaxLength(32), @Matches auf die kanonische Form X.Y.Z): im Rumpf zusätzlich prüfen (Unit-Tests rufen die Methode ohne Pipe): typeof version === 'string' und parseReleaseVersion(version) === version, sonst BadRequestException; ist getRunningRelease() null, dann BadRequestException (keine freigegebene Version); ist compareReleaseVersions(version, running) > 0, dann BadRequestException. Dann gemerkten Stand lesen (findUnique, fehlt er, dann NotFoundException); nur wenn der gemerkte Wert null oder unparsebar ist oder die neue Version größer ist, update mit data: { lastSeenReleaseVersion: version }; Antwort { success: true, lastSeenReleaseVersion: <gespeicherter Stand> }. JSDoc je Methode mit Bezug auf quick-260925-bow und die Mandantenbindung. Tests im bestehenden user.controller.spec.ts mit der vorhandenen Zwei-Klienten-Attrappe, APP_VERSION per vi.stubEnv (in afterEach vi.unstubAllEnvs()).

  5. Auswahl (D-03, D-04) in neuer reiner Datei apps/web/src/lib/release-notes.ts (importiert NICHT das Changelog-Modul und keine React-/Next-Module, damit Typen daraus auch in Client-Komponenten sicher sind): Typen ReleaseSectionKind = 'new' | 'changed' | 'fixed', ReleaseNotesSection { kind; markdown } (nur die Zeilen unter der Gruppenüberschrift, Leerzeilen am Rand entfernt), ReleaseNotesVersion { version; sections }, ReleaseNotice { currentRelease; versions; omittedCount }, Konstante RELEASE_NOTICE_MAX_VERSIONS = 3. parseChangelogReleases(markdown): CRLF normalisieren, an ## -Überschriften zerlegen, Version = erstes Wort der Überschrift durch parseReleaseVersion (aus @tessera/shared) — „Unveröffentlicht“ und alles Unparsebare fällt weg; darin ### -Gruppen genau „Neu“ als new, „Geändert“ als changed, „Behoben“ als fixed, andere Gruppen verwerfen, Gruppe ohne Listenpunkt (^\s*[-*] ) verwerfen, Ausgabe in fester Reihenfolge new/changed/fixed, Versionen ohne Gruppe verwerfen. selectReleaseNotice(markdown, currentRelease, lastSeen): Regeln wie im behavior-Block; Sortierung absteigend per compareReleaseVersions (nicht der Dateireihenfolge vertrauen); ergibt sich keine Version, Rückgabe null.

  6. Server-Aktionen in neuer Datei apps/web/src/lib/release-notice-actions.ts mit 'use server' (eigene Datei, damit auth-actions.ts die Änderungsliste nicht importiert): fetchReleaseNotice(): Promise<ReleaseNotice | null> liest das Cookie session wie fetchSessionState, ruft GET ${API_URL}/users/me/release-notice mit cache: 'no-store', prüft die Antwortform (beide Felder string oder null, sonst null) und gibt selectReleaseNotice(changelogMarkdown, body.currentRelease, body.lastSeenReleaseVersion) zurück; jeder Fehler still mit Rückgabe null. markReleaseSeenAction(version: string): Promise<{ success: boolean }> schickt POST ${API_URL}/users/me/release-seen. changelogMarkdown kommt aus @/lib/changelog — erlaubt, weil Server-Code. Tests nach Vorbild auth-actions.test.ts, @/lib/changelog per vi.mock mit eigenem Markdown.

  7. Renderer wiederverwenden (D-07): ChangelogView bekommt eine optionale Eigenschaft variant?: 'card' | 'plain' (Vorgabe card, Seite /changelog unverändert); plain lässt Rahmen, Hintergrund und Innenabstand weg, data-testid bleibt. Die bestehenden Tests der Seite müssen unverändert grün bleiben.

  8. Fenster (D-03, D-07, D-08) apps/web/src/components/release-notice/release-notice-dialog.tsx ('use client'), Eigenschaften { notice: ReleaseNotice; onClose: () => void }. Aufbau nach widget-catalog-modal.tsx: äußerer fixed inset-0 z-50-Container, Hintergrund als echte Schaltfläche mit aria-label (Schlüssel releaseNotice.close) und bg-black/50, Dialog role="dialog", aria-modal="true", aria-labelledby auf die Überschrift (useId), bg-card border border-border rounded-lg shadow-xl, w-full max-w-lg mx-4 max-h-[85vh] flex flex-col. Kopf: h2 „Neu in Version {version}“ mit tabIndex={-1} und Anfangsfokus per Ref beim Mount, daneben Schließen-Kreuz (SVG aus dem Katalog-Fenster, aria-label common.close). Mitte: Einleitungssatz, dann scrollbarer Bereich (overflow-y-auto, tabIndex={0}, aria-label) mit je Version (Unterüberschrift „Version {version}“ nur bei mehr als einer Version) je Gruppe eine h3 (text-sm font-semibold text-foreground) und <ChangelogView markdown={section.markdown} variant="plain" />. Fuß: bei omittedCount > 0 der Satz moreVersions (ICU-Plural), links next/link „Alle Änderungen ansehen“ auf /changelog (Klick ruft onClose, Navigation läuft normal weiter), rechts Hauptknopf „Verstanden“ mit den Knopfklassen aus bug-report-dialog.tsx. Tastatur: keydown-Listener auf document — Escape ruft onClose; Tab/Umschalt+Tab zyklisch innerhalb der aktuell fokussierbaren Elemente des Dialogs (a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), dynamisch abgefragt, weil die Markdown-Ausgabe Links enthalten kann); liegt der Fokus auf der Überschrift, springt Tab auf das erste Element. Beim Unmount den Fokus auf das zuvor aktive Element zurückgeben. Keine Einblendanimation; falls doch ein Übergang nötig ist, nur mit motion-safe:-Präfix. Nur Tailwind-Tokens (bg-card, text-foreground, text-muted-foreground, border-border, bg-primary), damit Dunkelmodus automatisch stimmt. Stilregeln aus dem Kontext beachten.

  9. Host apps/web/src/components/release-notice/release-notice-host.tsx ('use client'): Zustand notice, Ref-Sperre „schon abgefragt“; useEffect auf usePathname(): ist die Sperre gesetzt oder beginnt der Pfad mit /change-password, nichts tun; sonst Sperre setzen und fetchReleaseNotice() aufrufen, Ergebnis in den Zustand (Fehler still). Das Fenster per React.lazy + Suspense fallback={null} laden, damit der Markdown-Renderer nur geladen wird, wenn wirklich eine Nachricht da ist. onClose: zuerst setNotice(null) (Fenster sofort weg), dann void markReleaseSeenAction(notice.currentRelease) — schlägt das Merken fehl, erscheint das Fenster beim nächsten Laden erneut (gewollt, nicht stumm verloren). In app-shell.tsx <ReleaseNoticeHost /> nach </main> einfügen (nur dort; Anmeldeseite hat keine AppShell, der Erststart-Dialog der Desktop-App ist die lokale apps/desktop/src/setup.html vor dem Portal und wird nicht berührt; die Bildschirmfoto-Funktion rastert document.body und nimmt ein offenes Fenster einfach mit).

  10. Texte (D-07) neuer Namensraum releaseNotice in de.json und en.json mit identischem Schlüsselsatz: title („Neu in Version {version}“ / „New in version {version}“), intro („Tessera wurde aktualisiert. Das hat sich für Sie geändert:“ / „Tessera has been updated. Here is what changed for you:“), versionHeading („Version {version}“), section.new („Neu“ / „New“), section.changed („Verbessert“ / „Improved“), section.fixed („Behoben“ / „Fixed“), moreVersions (DE: „Dazu kommen Änderungen aus {count, plural, one {# älteren Version} other {# älteren Versionen}}.“, EN: „There are also changes from {count, plural, one {# earlier version} other {# earlier versions}}.“), showAll („Alle Änderungen ansehen“ / „View all changes“), confirm („Verstanden“ / „Got it“), close („Fenster schließen“ / „Close window“), contentLabel („Änderungen“ / „Changes“). Echte Umlaute, Sie-Form.

Commit(s): feat(260925-bow): … und test(260925-bow): … (TDD-Reihenfolge darf in einzelnen Commits sichtbar sein). cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec prisma generate >/dev/null && pnpm --filter @tessera/api exec prisma validate && node -e "const s=require('./packages/shared/src/index.ts'); if (s.parseReleaseVersion('v10.2.3-5-gabc1234')!=='10.2.3' || s.parseReleaseVersion('dev')!==null || s.compareReleaseVersions('1.10.0','1.9.0')<=0) process.exit(1)" && pnpm --filter @tessera/api exec vitest run release-version user.controller && pnpm --filter @tessera/web exec vitest run release-notes release-notice changelog && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && test -z "$(grep -lE '^import .lib/changelog' apps/web/src/lib/release-notes.ts apps/web/src/components/release-notice/.tsx)" && grep -q "^'use server'" apps/web/src/lib/release-notice-actions.ts && test "$(grep -rl '<ReleaseNoticeHost' apps/web/src --include=*.tsx | grep -v '.test.tsx$')" = "apps/web/src/components/layout/app-shell.tsx" && awk '/me/release-notice|me/release-seen/{n=NR} /@Get(.:id.)/{if(!g)g=NR} END{exit !(n && g && n<g)}' apps/api/src/user/user.controller.ts Mit gesetzter APP_VERSION liefert die API die laufende Version und den gemerkten Stand, nimmt nur gültige, nicht zu hohe Versionen als gesehen an und bindet beides an den angemeldeten Benutzer im eigenen Mandanten; das Web wählt die richtigen Abschnitte (Bereich, Deckel 3, null, unparsebar), zeigt im Portal-Rahmen ein barrierefreies Fenster und merkt erst beim Schließen. Gezielte API- und Web-Tests grün, beide type-checks grün, Node lädt die gemeinsamen Funktionen ohne Bauschritt.

Aufgabe 2: Neue Benutzer sehen kein Verlaufsfenster — alle Anlagewege tragen die laufende Version ein; RLS-Buchführung nachziehen 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, docs/mandantentrennung-zugriffsklassifikation.md - apps/api/src/user/user.service.ts (`create()` samt Kopfkommentar „EINZIGER Erzeugungspunkt“), apps/api/src/user/user.service.spec.ts (describe „create — Standardgruppen-Mitgliedschaft“) - apps/api/src/user/admin-seed.service.ts (Erstanlage), apps/api/src/user/admin-seed.service.spec.ts - docs/mandantentrennung-zugriffsklassifikation.md: Abschnitt „Übersicht je Bereich“ (Zeile `| user |` und `| **Summe** |`), Fundstellenzeile `apps/api/src/user/user.controller.ts | user` - UserService.create mit `APP_VERSION=v10.2.3-3-gabc1234` → die an `tenantPrisma.user.create` übergebenen Daten enthalten `lastSeenReleaseVersion: '10.2.3'`; mit `APP_VERSION=dev` bzw. ungesetzt → `lastSeenReleaseVersion: null` - Der Wert lässt sich über die Parameter von create() nicht von außen setzen (kein neues Feld in der Signatur) - AdminSeedService legt den Erst-Administrator mit `lastSeenReleaseVersion` der laufenden Version an (`10.2.3` bzw. null bei `dev`) - LDAP-Anlage: beide LDAP-Wege gehen über UserService.create (grep-Nachweis, kein eigener `user.create` in apps/api/src/ldap) Per D-04: In `UserService.create()` in den `data` von `tenantPrisma.user.create` das Feld `lastSeenReleaseVersion: getRunningRelease()` ergänzen (Import aus `../health/app-version`), NICHT als Parameter der Methode — der Wert ist eine Eigenschaft des Servers, nicht des Aufrufers. Kopfkommentar von `create()` um einen Absatz ergänzen: neue Benutzer (Admin-Anlage, beide LDAP-Wege) bekommen die laufende freigegebene Version eingetragen, damit sie kein „Was ist neu“-Fenster mit Altlasten sehen (quick-260925-bow); `null` auf Ständen ohne freigegebene Version. Dasselbe in `AdminSeedService` bei der Erstanlage des Administrators (dort ebenfalls kurzer Kommentar). Tests zuerst: in `user.service.spec.ts` und `admin-seed.service.spec.ts` je ein Fall mit `vi.stubEnv('APP_VERSION', 'v10.2.3-3-gabc1234')` und ein Fall mit `dev`, `vi.unstubAllEnvs()` im `afterEach`.

RLS-Buchführung (D-09): Aufgabe 1 hat in user.controller.ts neue gebundene Rohtreffer tenantPrisma.user. hinzugefügt (erwartet +3: ein findUnique im GET, findUnique + update im POST). Das Paar (Datei, Modell) bleibt gebunden, die Spec braucht keine neue Zeile. Mit der Gate-Schleife nachrechnen (je Bereichsverzeichnis grep -ro auf this.prisma.<Modell>, tenantPrisma.<Modell>., systemPrisma.<Modell>., ohne spec-Dateien; Summe über alle Bereiche) und eintragen: Zeile | user | … | mit den gemessenen Werten und einem vorangestellten Vermerk im etablierten Stil (quick-260925-bow: +N gebunden in user.controller.ts, „Was ist neu“-Fenster, GET me/release-notice und POST me/release-seen, nachgemessen mit der Gate-Schleife; danach „Vorher:“ und der bisherige Text); Summenzeile: Werte ersetzen, den Vermerk quick-260925-bow: an den Anfang der Hinweisspalte stellen und den bisherigen Text mit „Vorher:“ anhängen (die Gates erwarten den Vermerk jeweils direkt nach den Zahlen); in der Fundstellenzeile apps/api/src/user/user.controller.ts | user die Aufzählung der Selbstbedienungszugriffe um die beiden neuen Wege ergänzen (weiterhin forTenant(), where: { id: currentUser.id }). Werte messen, nicht aus diesem Plan abschreiben.

Commit(s): feat(260925-bow): …, test(260925-bow): …, docs(260925-bow): RLS-Buchfuehrung …. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run user.service admin-seed rls-access-inventory && test -z "$(grep -rnE '(tenantPrisma|this.prisma|tx).user.create' apps/api/src/ldap --include=.ts | grep -v '.spec.ts')" && grep -q 'getRunningRelease' apps/api/src/user/user.service.ts && grep -q 'getRunningRelease' apps/api/src/user/admin-seed.service.ts && K=docs/mandantentrennung-zugriffsklassifikation.md && U=$(grep -ro "this.prisma.[a-zA-Z]" apps/api/src/user | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma.[a-zA-Z]." apps/api/src/user | grep -v spec | wc -l | tr -d ' ') && S=$(grep -ro "systemPrisma.[a-zA-Z]." apps/api/src/user | grep -v spec | wc -l | tr -d ' ') && { grep -qE "^| user | ${U} | ${B} | ${S} | **quick-260925-bow" "$K" || { echo "ZEILE user nennt nicht ${U}/${B}/${S} mit Vermerk"; exit 1; }; } && TU=0 && TB=0 && TS=0 && for d in apps/api/src//; do u=$(grep -ro "this.prisma.[a-zA-Z]" "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); b=$(grep -ro "tenantPrisma.[a-zA-Z]." "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); s=$(grep -ro "systemPrisma.[a-zA-Z]." "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); TU=$((TU+u)); TB=$((TB+b)); TS=$((TS+s)); done && echo "ABGELEITET ${TU}/${TB}/${TS}" && { grep -qE "^| **Summe** | **${TU}** | **${TB}** | **${TS}** | **quick-260925-bow" "$K" || { echo "SUMMENZEILE nennt nicht ${TU}/${TB}/${TS} mit Vermerk"; exit 1; }; } && grep -E '^| apps/api/src/user/user.controller.ts | user |' "$K" | grep -q 'release' Jeder Anlageweg (Admin-Anlage, LDAP-Abgleich und -Import über UserService.create, Erst-Administrator) trägt die laufende freigegebene Version ein, auf dev-Ständen null; Tests dafür grün; RLS-Bestandsaufnahme-Spec grün; Übersichts-, Summen- und Fundstellenzeile nachgemessen und mit Vermerk fortgeschrieben.

Aufgabe 3: Doku, CHANGELOG, Kommentare zur Importregel; volle Suiten, Lint, Biome-Grenzen und Nachweis, dass die Änderungsliste nicht in Client-Chunks landet CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md, apps/web/src/lib/changelog.ts, apps/web/next.config.ts - CHANGELOG.md (Kopf bis „## 1.4.0“; „## Unveröffentlicht“ ist derzeit leer) - docs/anleitung-anwender.md, Abschnitt „## Was ist neu“ - docs/anleitung-entwicklung.md, Absatz „**Änderungsliste (`CHANGELOG.md`):**“ (enthält die Regel, welche Datei das Changelog-Modul importieren darf) - Kopfkommentar von apps/web/src/lib/changelog.ts und Kommentarblock oben in apps/web/next.config.ts Per D-10: Unter `## Unveröffentlicht` in CHANGELOG.md eine Gruppe `### Neu` mit einem Punkt in Alltagssprache, Sie-Form, echte Umlaute, ohne Dateinamen/Fachbegriffe, sinngemäß: Nach einem Versionswechsel zeigt Tessera bei Ihrer ersten Anmeldung ein Fenster mit den wichtigsten Änderungen der neuen Version – neue Funktionen, Verbesserungen und behobene Fehler; haben Sie mehrere Versionen verpasst, erscheinen die drei neuesten; „Verstanden“ schließt das Fenster, es erscheint erst mit der nächsten Version wieder, im Browser wie in der Desktop-App; die vollständige Liste bleibt unter „Was ist neu“. Das Wort „Versionswechsel“ muss im Punkt vorkommen (Gate).

Anwenderhandbuch docs/anleitung-anwender.md, Abschnitt „Was ist neu“: neuen Absatz (Sie-Form) — das Fenster nach einem Versionswechsel, was es zeigt (Neu / Verbessert / Behoben, höchstens drei Versionen, Link „Alle Änderungen ansehen“), wie es sich schließt, dass es pro Benutzer nur einmal je Version erscheint (auch in der Desktop-App), dass neu angelegte Konten es erst mit der nächsten Version sehen, und dass Beta-Punkte unter „Noch nicht freigegeben“ darin nicht vorkommen. Das Wort „Versionswechsel“ muss im Abschnitt vorkommen (Gate).

Entwicklerdoku docs/anleitung-entwicklung.md, im Absatz zur Änderungsliste: die Importregel erweitern (Server-Code darf das Changelog-Modul importieren: page.tsx UND die 'use server'-Datei release-notice-actions.ts; Client-Komponenten nie, auch nicht release-notes.ts), danach ein kurzer Absatz zum Fenster: einzige Quelle der laufenden Version ist APP_VERSION der API (getRunningRelease()), Begründung (LDAP-Abgleich und Erst-Administrator laufen ohne Web), Endpunkte GET /users/me/release-notice und POST /users/me/release-seen (Validierung, nie absenken), Spalte User.lastSeenReleaseVersion (null = Bestandsbenutzer, zeigt nur die laufende Version), gemeinsame Funktionen parseReleaseVersion/compareReleaseVersions in packages/shared, Folge für die Freigabe: erst ein Tag vX.Y.Z (bzw. dessen Describe-Stand auf Beta) löst das Fenster aus, Punkte unter „Unveröffentlicht“ nie; lokal mit dev erscheint nie ein Fenster (zum Ausprobieren APP_VERSION als Build-Arg setzen). Die Kopfkommentare in apps/web/src/lib/changelog.ts und apps/web/next.config.ts an dieselbe erweiterte Importregel anpassen (nur Kommentar, kein Code).

Dann die vollen Prüfungen (D-09). Der Web-Build für den Chunk-Nachweis dauert einige Minuten (Befehl mit großzügiger Zeitgrenze ausführen); Build-Ausgaben (apps/web/.next, ggf. geändertes apps/web/next-env.d.ts) nicht committen — next-env.d.ts bei Änderung per git checkout -- zurücksetzen. Scheitert next build lokal aus Gründen außerhalb dieser Änderung, das in der SUMMARY mit der Fehlermeldung festhalten und den Importnachweis aus Aufgabe 1 als Ersatz benennen — nicht stillschweigend überspringen. Biome-Formatierung neuer Dateien mit biome check --write angleichen; keine neuen any, Nicht-null-Behauptungen oder Biome-Ausnahmen in neuen Dateien.

Commit: docs(260925-bow): …. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run && pnpm --filter @tessera/web exec vitest run && pnpm turbo run type-check lint && W=$(pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -oE '^Found [0-9]+ warning' | grep -oE '[0-9]+' || true) && A=$(pnpm --filter @tessera/api exec biome lint . 2>&1 | grep -oE '^Found [0-9]+ warning' | grep -oE '[0-9]+' || true) && echo "Biome web=${W:-0} api=${A:-0}" && test "${W:-0}" -le 53 && test "${A:-0}" -le 82 && test -z "$(grep -vE '^\s*(//|*|/*|{/*)' apps/web/src/components/release-notice/release-notice-dialog.tsx apps/web/src/components/release-notice/release-notice-host.tsx | grep -nE 'uppercase|tracking-widest|·|→|dangerouslySetInnerHTML')" && node -e 'for (const f of ["de","en"]) { const m = require("./apps/web/src/messages/" + f + ".json").releaseNotice; if (!m) throw new Error(f + ": releaseNotice fehlt"); for (const k of ["title","intro","versionHeading","moreVersions","showAll","confirm","close","contentLabel"]) if (typeof m[k] !== "string" || !m[k]) throw new Error(f + ": fehlt releaseNotice." + k); for (const k of ["new","changed","fixed"]) if (!m.section || !m.section[k]) throw new Error(f + ": fehlt releaseNotice.section." + k); if (/·|→/.test(JSON.stringify(m))) throw new Error(f + ": verbotenes Zeichen"); }' && awk '/^## Unveröffentlicht/{f=1; next} /^## /{f=0} f' CHANGELOG.md | grep -q '^### Neu' && awk '/^## Unveröffentlicht/{f=1; next} /^## /{f=0} f' CHANGELOG.md | grep -q 'Versionswechsel' && awk '/^## Was ist neu/{f=1; next} /^## /{f=0} f' docs/anleitung-anwender.md | grep -q 'Versionswechsel' && grep -q 'release-notice-actions' docs/anleitung-entwicklung.md && grep -q 'getRunningRelease' docs/anleitung-entwicklung.md && LOG=$(mktemp) && { pnpm --filter @tessera/web build >"$LOG" 2>&1 || { tail -40 "$LOG"; exit 1; }; } && P='hochgeladene Bilder liegen jetzt im Dateibereich des Servers' && test -n "$(grep -rl "$P" apps/web/.next/server)" && test -z "$(grep -rl "$P" apps/web/.next/static)" && { git checkout -- apps/web/next-env.d.ts 2>/dev/null || true; } && git diff --quiet -- apps/web/next-env.d.ts CHANGELOG, Anwender- und Entwicklerdoku beschreiben das Fenster; Kommentare zur Importregel stimmen; volle API- und Web-Suite grün, turbo type-check lint grün, Biome-Warnungen Web ≤ 53 und API ≤ 82, Stil- und i18n-Gate leer bzw. vollständig; nach next build steht der Änderungslistentext im Server-Bundle, aber nicht unter .next/static.

<threat_model>

Trust Boundaries

Boundary Description
Browser/Desktop-App → Web-Server-Aktion Client ruft fetchReleaseNotice/markReleaseSeenAction; Eingabe version ist unvertrauenswürdig
Web → API (/users/me/release-*) Session-Cookie authentifiziert; Body { version } unvertrauenswürdig
API → PostgreSQL (User-Zeile) Zeilenschutz je Mandant über forTenant()
Änderungsliste (Bauzeit-Text) → Client-Bundles Text darf nicht in öffentlich abrufbare /_next/static-Chunks

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-BOW-01 Tampering POST /users/me/release-seen low mitigate DTO (@IsString, @MaxLength(32), @Matches kanonisch) plus Rumpfprüfung parseReleaseVersion(v) === v, ≤ getRunningRelease(), null-laufend → 400; nie absenken. Wirkung beschränkt auf das eigene Fenster
T-BOW-02 Elevation of Privilege GET/POST /users/me/release-* medium mitigate Kein Kennungsparameter; where: { id: currentUser.id } über forTenant(this.prisma, currentUser.tenantId); Test mit zwei Benutzern in zwei Mandanten (Aufgabe 1)
T-BOW-03 Information Disclosure release-notice-actions.ts / Client-Chunks low mitigate Changelog-Modul nur aus 'use server'-Datei und page.tsx; release-notes.ts und Fenster-Dateien ohne diesen Import (Gate Aufgabe 1); Build-Nachweis .next/static enthält den Text nicht (Gate Aufgabe 3)
T-BOW-04 Tampering (XSS) ReleaseNoticeDialog low mitigate Markdown ausschließlich über ChangelogView (MDEditor.Markdown + rehype-sanitize), kein rohes HTML-Einfügen (Stil-Gate Aufgabe 3); Quelle ist die versionierte CHANGELOG.md
T-BOW-05 Denial of Service Versionsparser low mitigate Eingabelänge ≤ 64 im Parser, @MaxLength(32) in der DTO, verankerter Ausdruck ohne verschachtelte Wiederholungen
T-BOW-06 Information Disclosure GET /users/me/release-notice low accept Nennt nur die laufende Version, die GET /health/version ohnehin öffentlich liefert (T-KU1-03), und den eigenen gemerkten Stand
T-BOW-07 Tampering Migration 20260925120000_user_last_seen_release low accept Reines ADD COLUMN nullbar ohne Standardwert, kein Datenumbau; auth_lookup_* liefern feste Spaltenlisten und bleiben unberührt
</threat_model>
Executor (automatisiert, siehe Aufgaben): gezielte Tests je Schicht (Aufgabe 1), Anlagewege + RLS-Buchführung (Aufgabe 2), volle API- und Web-Suite, `pnpm turbo run type-check lint`, Biome-Warnungen Web ≤ 53 / API ≤ 82, Stil- und i18n-Gate, Chunk-Nachweis per `next build` (Aufgabe 3).

Orchestrator (D-11, Browserprüfung mit Playwright am lokalen Stack, NICHT Aufgabe des Executors):

  1. Lokal mit einer freigegebenen Versionsnummer bauen, damit überhaupt ein Fenster entstehen kann (lokal steht sonst dev): docker compose build --build-arg APP_VERSION=1.4.0-1-g0000000 api web && docker compose up -d --force-recreate api web — die Migration läuft beim API-Start.
  2. docker exec tessera-ctl-db-1 psql -U tessera -d tessera -c "UPDATE \"User\" SET \"lastSeenReleaseVersion\"='1.0.0' WHERE username='admin'" → Anmelden: Fenster „Neu in Version 1.4.0“ mit 1.4.0, 1.3.1, 1.3.0, Satz „Dazu kommen Änderungen aus 2 älteren Versionen.“, Link zu /changelog; Überschrift hat den Fokus; Tab bleibt im Fenster; Dunkelmodus prüfen.
  3. „Verstanden“ → DB zeigt 1.4.0; Neuladen → kein Fenster.
  4. lastSeenReleaseVersion = NULL → nur Abschnitt 1.4.0, keine Versionsunterüberschrift. = '1.4.0' → kein Fenster. Escape und Hintergrundklick schließen ebenfalls und merken.
  5. Anmeldeseite zeigt nie ein Fenster; „Fehler melden“ mit offenem Fenster funktioniert (Bildschirmfoto enthält das Fenster).

<success_criteria>

  • Bestandsbenutzer mit älterem Stand sehen nach einem Versionswechsel genau einmal das Fenster mit Neu / Verbessert / Behoben der verpassten Versionen (höchstens drei, neueste zuerst, Hinweis auf weitere), Schließen merkt dauerhaft pro Benutzer.
  • Neu angelegte Benutzer (Admin, LDAP, Erst-Administrator) und dev-Stände sehen kein Fenster.
  • Server validiert und bindet an Benutzer und Mandant; RLS-Buchführung nachgemessen.
  • Alle Suiten, type-check, lint grün; Biome Web ≤ 53, API ≤ 82; Änderungsliste nicht in Client-Chunks.
  • CHANGELOG „Unveröffentlicht → Neu“, Anwender- und Entwicklerdoku ergänzt. </success_criteria>
Create `.planning/quick/260925-bow-was-ist-neu-fenster-beim-ersten-anmelden/260925-bow-SUMMARY.md` when done (gemessene Biome-Zahlen, gemessene RLS-Zählwerte user/Summe, Ergebnis des Chunk-Nachweises, Testzahlen API/Web).