docs(quick-260909-cx0): Plan fuer Dateisicherung und Versionskorrektur

Zwei unabhaengige Aufgaben: user-files als benanntes Volume in beide
Repository-Compose-Dateien plus Betriebshandbuch Kapitel 6/7 (WINDOWS #17),
und die Technik-Tabelle in CLAUDE.md auf den installierten Stand bringen.
Beide Abnahmetore vor der Arbeit als rot gemessen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
2026-09-09 09:31:51 +02:00
parent 3501eb4dd1
commit 7b49747c72
@@ -0,0 +1,435 @@
---
quick_id: 260909-cx0
slug: dateisicherung-nachruesten-und-versionsa
date: 2026-09-09
status: planned
relates_to: 06-desktop-client-ci-cd, 07-dkv-fleet-module
windows_ref: 17
severity: medium
phase: quick-260909-cx0
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- docker-compose.yml
- docker-compose.prod.yml
- docs/anleitung-betrieb.md
- CLAUDE.md
- .planning/research/STACK.md
autonomous: true
requirements: [WINDOWS-17]
estimate:
tokens: 45000
raw_tokens: 45000
tasks: 2
confidence: low
must_haves:
truths:
- "Ein Neuerstellen der Container (`up -d --force-recreate api`) auf Basis der Compose-Dateien aus dem Repository laesst hochgeladene Profilbilder und DKV-Exporte bestehen — sie liegen dann ausserhalb der beschreibbaren Container-Schicht."
- "Die gerenderte Konfiguration von `docker-compose.yml`, von `docker-compose.prod.yml` und der Kombination Basis + `docker-compose.dev.yml` traegt fuer den Dienst `api` je genau einen Mount mit dem Ziel `/app/user-files`."
- "Der Speicherort ist ein benanntes Volume, kein Host-Verzeichnis — der API-Prozess laeuft als uid 1001 und darf ohne Zutun des Betreibers hineinschreiben."
- "Das Betriebshandbuch (Kapitel 6) beschreibt den Speicher als dauerhaft, nennt die Mount-Zeile woertlich zum Uebernehmen in die abweichende Serverdatei und behauptet nicht mehr, `pgdata` sei der einzige dauerhafte Datenspeicher."
- "Das Betriebshandbuch sagt ausdruecklich, dass die Aenderung im Repository die laufende Installation NICHT erreicht und der Betreiber sie in `/opt/tessera/docker-compose.yml` selbst eintragen muss."
- "CLAUDE.md nennt fuer jede aufgefuehrte Technik die Fassung, die `pnpm install` tatsaechlich aufloest beziehungsweise die in den Compose-/Dockerfiles steht."
- "Technik, die empfohlen, aber nie eingebaut wurde, steht in CLAUDE.md nicht mehr als Bestandteil des Systems, sondern sichtbar als nicht uebernommene Empfehlung mit Angabe dessen, was stattdessen laeuft."
- "CLAUDE.md und `docs/anleitung-entwicklung.md` widersprechen einander in keiner Versionsangabe mehr."
- "Keine Abhaengigkeit wurde aktualisiert: `package.json` (Wurzel und je App) und `pnpm-lock.yaml` sind gegenueber dem Ausgangsstand unveraendert."
artifacts:
- docker-compose.yml
- docker-compose.prod.yml
- docs/anleitung-betrieb.md
- CLAUDE.md
- .planning/research/STACK.md
key_links:
- "`apps/api/Dockerfile:23-26` (Verzeichnis `/app/user-files` gehoert uid 1001 `nestjs`) plus `:36` (`USER nestjs`) <-> Wahl der Mount-Art: ein leeres benanntes Volume uebernimmt diese Eigentuemerschaft beim ersten Anlegen, ein frisch von Docker erzeugtes Host-Verzeichnis gehoert root. Deshalb benanntes Volume."
- "`apps/api/src/user/user.controller.ts:40` und `apps/api/src/dkv/dkv-export.service.ts:59` loesen beide vier Ebenen ueber `dist/` hinaus auf und landen im Container auf `/app/user-files` — exakt dem Mount-Ziel. Ein anderes Ziel wuerde am Schreibort vorbei mounten und die Luecke offen lassen."
- "Repository-Compose <-> `/opt/tessera/docker-compose.yml`: es gibt keine Verbindung. Der Deploy holt nur Images; die Aenderung wirkt erst, wenn der Betreiber sie dort selbst eintraegt."
- "CLAUDE.md-Block `GSD:stack-start source:research/STACK.md` <-> `.planning/research/STACK.md`: der Block ist eine woertliche Kopie der Empfehlung von 2026-06/07. Ohne Vermerk auf beiden Seiten holt eine kuenftige Regeneration die falschen Zahlen zurueck."
---
<objective>
Zwei kleine, voneinander vollstaendig unabhaengige Punkte. Getrennte Aufgaben, getrennte Commits.
**Punkt A (WINDOWS #17) — hochgeladene Dateien ueberleben kein Neuerstellen der Container.**
Der Code legt Profilbilder und DKV-Exporte unter `user-files/` ab, aber keine Compose-Datei
im Repository mountet dieses Verzeichnis. Es existiert damit nur in der beschreibbaren
Container-Schicht und ist bei jedem `up -d --force-recreate` weg. Bisher ist kein Schaden
entstanden — es hat noch kein Nutzer ein Profilbild hinterlegt —, aber der erste Upload
faellt in dieselbe Grube.
**Punkt B — CLAUDE.md nennt Fassungen, die nicht installiert sind.**
Die Technik-Tabelle ist eine woertliche Kopie der Stack-Empfehlung von 2026-06/07 und
beschreibt einen Wunschzustand: Next.js 16.2.x statt der installierten 15.5.19, Prisma 7.8.x
statt 6.19.3, dazu Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky und
lint-staged, von denen nichts im Projekt existiert. Das neue Entwicklungshandbuch
(`docs/anleitung-entwicklung.md`) dokumentiert bewusst den echten Stand — beide Dokumente
widersprechen sich damit schriftlich.
Purpose: Nutzerdaten ueberstehen einen Redeploy, und wer neu dazustoesst, liest in CLAUDE.md,
was wirklich installiert ist, statt was einmal empfohlen wurde.
Output: Zwei getrennte, jeweils fuer sich lauffaehige Commits — A (Datenhaltung + Handbuch),
B (Dokumentationskorrektur).
## Gemessener Ausgangszustand (am Arbeitsbaum geprueft, 2026-09-09)
### Punkt A
| Beleg | Fundstelle |
|-------|------------|
| Profilbilder werden nach `user-files/avatars/` geschrieben | `apps/api/src/user/user.controller.ts:40` (`path.resolve(__dirname, '..','..','..','..','user-files','avatars')`), Ablage `:249-266` |
| DKV-Exporte werden nach `user-files/` geschrieben | `apps/api/src/dkv/dkv-export.service.ts:59` (gleiche Aufloesung), Schreiben `:135` |
| Im Image ist der Zielpfad `/app/user-files`, angelegt und uebereignet an uid 1001 | `apps/api/Dockerfile:21` (`WORKDIR /app`), `:23-26` (`mkdir -p /app/user-files` + `chown nestjs:nestjs`), `:36` (`USER nestjs`) |
| `docker-compose.yml` kennt nur ein Volume, `pgdata`; der Dienst `api` hat gar keinen `volumes`-Block | `docker-compose.yml:20-65` (api) und `:92-93` (Top-Level-Volumes) |
| `docker-compose.prod.yml` ebenso | `docker-compose.prod.yml:22-61` (api) und `:89-90` |
| `docker-compose.dev.yml` mountet nur Quellcode und Prisma-Schema | `docker-compose.dev.yml:7-10` |
| Gerenderte Konfiguration traegt fuer `api` keinen Mount auf `/app/user-files` — in allen drei Kombinationen | selbst gemessen mit `docker compose config --format json` fuer Basis, Prod und Basis+Dev: dreimal `FEHLT` |
| Das Betriebshandbuch beschreibt die Luecke bereits und raet zu manuellem Herauskopieren | `docs/anleitung-betrieb.md:278-290` (Kapitel 6) und `:316` (Fehlertabelle Kapitel 7) |
### Punkt B
Verglichen wurden die Tabellenwerte in `CLAUDE.md:26-97` gegen `package.json` (Wurzel und je App)
und die in `pnpm-lock.yaml` aufgeloesten Fassungen (`importers:`-Abschnitt), dazu die
Compose- und Dockerfiles.
| CLAUDE.md sagt | Tatsaechlich installiert (2026-09-09) |
|---|---|
| Next.js 16.2.x (`:39`) | **15.5.19** (Vorgabe `^15.3.0`) |
| Prisma 7.8.x (`:56`) | **6.19.3** — `prisma` und `@prisma/client`, Vorgabe `^6.0.0`; auch `apps/api/Dockerfile:33` nennt 6.19.3 |
| Keycloak 26.6.x als Identitaetsanbieter (`:64`), `nest-keycloak-connect` (`:65`) | **nicht vorhanden** — kein Keycloak-Dienst in irgendeiner Compose-Datei, kein Paket im Lockfile. Angemeldet wird ueber `@nestjs/jwt` 11.0.2, `passport` 0.7.0, `argon2` 0.44.0, Verzeichnisanbindung ueber `ldapts` 8.1.8 |
| Redis 7.x als Cache/Sitzungsspeicher (`:58`) | **nicht vorhanden** — kein Dienst, kein Paket |
| TanStack Query 5.101.x (`:48`) | **nicht installiert** |
| shadcn/ui CLI v4 (`:43`) | **nicht benutzt** — keine `components.json`, kein `components/ui`-Verzeichnis |
| Playwright 1.x als E2E-Test (`:88`) | **keine Projektabhaengigkeit** — Browserpruefungen laufen ueber das Playwright-MCP-Werkzeug, im Repository existiert keine E2E-Suite und keine `playwright.config.*` |
| Husky 9.x (`:96`), lint-staged 15.x (`:97`) | **nicht installiert**, kein `.husky`-Verzeichnis |
| Vitest 3.x (`:87`) | gemischt: `apps/api` 3.2.6, `apps/web` **4.1.9** |
| Docker 27.x / Compose 2.x (`:78-79`) | Wirtseigenschaft, vom Repository nicht gesetzt; auf dieser Maschine gemessen: Docker 29.8.0, Compose v5.5.1 |
| Tauri 2.x (`:72`) | 2.11.1 / CLI 2.11.3 — stimmt; `apps/desktop` ist laut `docs/anleitung-entwicklung.md:38-40` bisher nur Geruest |
| pnpm 9.x, Turborepo 2.9.x, React 19.x, TypeScript 5.5+, Tailwind 4.3.x, next-themes 0.4.x, next-intl 4.13.x, react-grid-layout 2.2.x, Zustand 5.0.x, NestJS 11.x, Express 5.x, PostgreSQL 16.x, Biome 2.x, Testing Library | stimmen. Genau: pnpm 9.15.0, Turborepo 2.9.18, React/React-DOM 19.2.7, TypeScript 5.9.3, Tailwind 4.3.1, next-themes 0.4.6, next-intl 4.13.0, react-grid-layout 2.2.3, Zustand 5.0.14, NestJS 11.1.27, Express 5.2.1 (mittelbar ueber `@nestjs/platform-express`), `postgres:16-alpine`, Biome 2.5.0, `@testing-library/react` 16.3.2 |
| (fehlt in der Tabelle) | Node 24 — beide produktiven Dockerfiles ziehen `node:24-alpine` |
## Zur Verteilung auf den Server (bitte woertlich weitergeben, NICHT ausfuehren)
`/opt/tessera/docker-compose.yml` ist **keine** Arbeitskopie dieses Repositorys. Die Datei
wurde dort von Hand bearbeitet und weicht ab (Sicherung `docker-compose.yml.bak.20260811`).
Der Deploy holt ausschliesslich Images. Eine Aenderung an den Compose-Dateien im Repository
erreicht die laufende Installation deshalb **nie**.
Damit die Reparatur auf alpha wirkt, muss der Nutzer dieselben zwei Zeilen selbst in
`/opt/tessera/docker-compose.yml` eintragen und die Container einmal neu erstellen. Dieser
Plan sieht dafuer **keine** Handlung vor: kein SSH, kein `docker`-Aufruf gegen irgendeinen
Server, keine Datei ausserhalb dieses Arbeitsbaums. Das Betriebshandbuch bekommt die
Anleitung dafuer schriftlich, damit sie nicht in einer Sitzung verloren geht.
Solange das nicht geschehen ist, bleibt der Ledger-Eintrag WINDOWS #17 **offen** — die
Luecke besteht auf der laufenden Installation weiter. Der Executor schliesst ihn nicht;
das Schliessen ist die Entscheidung des Nutzers, nachdem er die Serverdatei ergaenzt hat
(`gsd-tools windows fixed 17`).
## Zur Wahl der Speicherart (Begruendung, wie gefordert)
Gewaehlt: **benanntes Volume** `user-files`, gemountet auf `/app/user-files`.
Ausschlaggebend ist die Eigentuemerschaft. Das Image legt `/app/user-files` an und uebereignet
es uid 1001 (`apps/api/Dockerfile:23-26`), der Prozess laeuft als dieser Nutzer (`:36`). Ein
leeres benanntes Volume uebernimmt beim ersten Mounten Inhalt und Eigentuemerschaft des
Image-Verzeichnisses — Schreiben funktioniert sofort. Ein Host-Verzeichnis, das Docker beim
Start neu anlegt, gehoert dagegen root; der API-Prozess koennte nicht hineinschreiben, und aus
einem Datenverlust wuerde ein kaputter Upload. Ein Bind-Mount waere also nur mit einer
zusaetzlichen Handlung des Betreibers (Verzeichnis anlegen und auf 1001 uebereignen) korrekt —
genau die Art stiller Voraussetzung, die auf dem Server erfahrungsgemaess untergeht.
Zur Sicherung passt das: das Betriebshandbuch nennt in Kapitel 6 bereits
`docker compose cp api:/app/user-files ./user-files-backup`, und dieser Befehl funktioniert
mit einem benannten Volume unveraendert weiter — nur ist er kuenftig eine Sicherung und keine
Rettung mehr. Zusaetzlich passt die Wahl zum bereits vorhandenen `pgdata`, das dieselbe Form hat.
Das Handbuch muss dennoch angefasst werden, weil Kapitel 6 heute das Gegenteil behauptet.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@CLAUDE.md
Dateien, die beim Umsetzen gelesen werden muessen:
@docker-compose.yml
@docker-compose.prod.yml
@docs/anleitung-betrieb.md
Belege, die nicht veraendert werden (nur zum Nachschlagen):
- `apps/api/Dockerfile` — Zeilen 21-26 und 36 begruenden die Wahl des benannten Volumes.
- `apps/api/src/user/user.controller.ts:40` und `apps/api/src/dkv/dkv-export.service.ts:59`
— beide Schreibpfade, beide landen im Container auf `/app/user-files`.
- `docs/anleitung-entwicklung.md` — nennt bereits den echten Stand (pnpm 9.15.0, `node:24-alpine`,
Biome, Vitest in beiden Apps). Aufgabe B muss dazu passen, nicht davon abweichen.
- `pnpm-lock.yaml`, Abschnitt `importers:` — die verbindliche Quelle fuer die aufgeloesten
Fassungen. Nicht die `package.json`-Vorgaben (`^15.3.0`) als Fassung ausgeben.
Verbindliche Konventionen aus dem Bestand:
- Die Handbuecher unter `docs/` sind deutsch mit echten Umlauten (`für`, `über`). Neue Saetze
dort in derselben Schreibweise. Der Umlaut-Test in `apps/web/src/messages` betrifft nur die
Oberflaechentexte, nicht diese Dokumente.
- Der Technik-Block in CLAUDE.md ist englisch. Er bleibt englisch — dies ist eine
Zahlenkorrektur, keine Uebersetzung.
</context>
<tasks>
<task type="auto">
<name>Task 1: user-files dauerhaft speichern und das Betriebshandbuch nachziehen (WINDOWS #17)</name>
<files>docker-compose.yml, docker-compose.prod.yml, docs/anleitung-betrieb.md</files>
<precondition>Die Docker-CLI ist lokal aufrufbar (`docker compose version` antwortet). Die Pruefung rendert ausschliesslich Konfiguration und startet, baut und stoppt nichts.</precondition>
<reversibility rating="costly">Die Speicherart laesst sich spaeter aendern, aber nicht folgenlos: ein Wechsel auf ein Host-Verzeichnis erfordert einmaliges Umkopieren des Volume-Inhalts und eine Uebereignung an uid 1001.</reversibility>
<action>
Beide Compose-Dateien im Repository bekommen denselben Zusatz. Nichts anderes aendern —
keine Umgebungsvariablen, keine Ports, keine Healthchecks.
In `docker-compose.yml`: beim Dienst `api` einen Block `volumes:` einfuegen (Einrueckung wie
bei `networks:` desselben Dienstes) mit dem einzigen Eintrag `- user-files:/app/user-files`.
Sinnvolle Stelle ist direkt vor `healthcheck:` (heute Zeile 60). Anschliessend im
Top-Level-Block `volumes:` (heute Zeile 92-93) neben `pgdata:` einen zweiten Eintrag
`user-files:` ergaenzen — ohne Wert, das ist ein benanntes Volume mit Vorgaben.
In `docker-compose.prod.yml`: identisch, Dienst `api` (Block vor `healthcheck:`, heute
Zeile 56) und Top-Level-Block `volumes:` (heute Zeile 89-90).
`docker-compose.dev.yml` bleibt unveraendert. Grund, gemessen: Compose fuehrt die
Mount-Listen ueber das Ziel zusammen, die Kombination Basis + Dev traegt den neuen Mount
also automatisch mit — nachgewiesen an der gerenderten Konfiguration. Ein zweiter Eintrag
dort waere Doppelpflege.
Danach `docs/anleitung-betrieb.md`, Kapitel 6, Abschnitt "Was sonst noch an Zustand
existiert" (heute Zeile 269-290). Drei Dinge:
(1) Der erste Aufzaehlungspunkt (`PostgreSQL-Daten`, Zeile 271-277) behauptet, `pgdata` sei
der einzige dauerhafte Datenspeicher. Das stimmt nach dieser Aenderung nicht mehr — Satz so
umformulieren, dass es zwei benannte Volumes gibt. Den vorhandenen Schreibfehler
"daürhafte" dabei mitkorrigieren.
(2) Der Aufzaehlungspunkt `Hochgeladene Dateien` (Zeile 278-290) wird ersetzt. Er muss
kuenftig sagen: die Dateien liegen im benannten Volume `user-files`, gemountet auf
`/app/user-files` im Dienst `api`, eingetragen in `docker-compose.yml` und
`docker-compose.prod.yml`; sie ueberstehen ein `--force-recreate`; gesichert werden sie
weiterhin mit `docker compose cp api:/app/user-files ./user-files-backup`, alternativ ueber
eine Sicherung des Volumes; das Volume traegt im `docker volume ls` den Projektnamen als
Praefix (`<projekt>_user-files`). Die Verweise auf `apps/api/src/user/user.controller.ts`
und `apps/api/src/dkv/dkv-export.service.ts` als Schreibstellen bleiben erhalten. Zeigen Sie
dabei einen kurzen YAML-Ausschnitt mit genau den zwei neuen Zeilen — die Dienst-Zeile in der
Form `user-files:/app/user-files` und den Top-Level-Eintrag —, damit ein Betreiber sie
kopieren kann. Diese Zeichenkette ist Teil der Abnahmepruefung.
(3) Ein deutlich abgesetzter Hinweis im selben Abschnitt: `/opt/tessera/docker-compose.yml`
auf dem Server ist keine Arbeitskopie des Repositorys, wurde von Hand bearbeitet und wird
von einem Deploy nicht angefasst. Wer die Reparatur dort haben will, traegt dieselben zwei
Zeilen selbst ein (vorher sichern) und erstellt die Container einmal neu. Bis dahin gilt
fuer die laufende Installation weiterhin der alte, verlustbehaftete Zustand; pruefbar mit
`docker inspect tessera-api-1` und einem Blick auf `Mounts`.
Schliesslich Kapitel 7, Fehlertabelle, Zeile 316 ("Avatare/DKV-Exporte nach einem Deploy
verschwunden"): die Ursachenspalte trifft nach dieser Aenderung nur noch auf eine
Installation zu, deren Compose-Datei den Mount nicht hat. Zeile entsprechend umschreiben
und als Abhilfe den Eintrag der zwei Zeilen samt Verweis auf Kapitel 6 nennen, nicht mehr
das vorherige "langfristig ergaenzen".
Ausdruecklich nicht Teil dieser Aufgabe: irgendein Aufruf gegen alpha oder einen anderen
Server, `docker compose up`, `pull`, `restart` oder das Anlegen von Verzeichnissen auf
einem Host.
</action>
<verify>
<automated>node -e "const {execFileSync}=require('child_process');const fs=require('fs');const env={...process.env,TESSERA_ENCRYPTION_KEY:'x',DATABASE_URL:'x',JWT_SECRET:'x',DB_PASSWORD:'x',TESSERA_ADMIN_EMAIL:'x',TESSERA_ADMIN_PASSWORD:'x'};const r=a=>JSON.parse(execFileSync('docker',['compose',...a,'config','--format','json'],{env}));const c=(l,cfg)=>{const m=(cfg.services.api.volumes||[]).filter(v=>v.target==='/app/user-files');console.log(l,m.length===1?'OK '+m[0].type+':'+m[0].source:'FEHLT');return m.length===1;};const doc=fs.readFileSync('docs/anleitung-betrieb.md','utf8').includes('user-files:/app/user-files');console.log('Betriebshandbuch nennt die Mount-Zeile',doc?'OK':'FEHLT');const res=[c('basis',r(['-f','docker-compose.yml'])),c('prod',r(['-f','docker-compose.prod.yml'])),c('basis+dev',r(['-f','docker-compose.yml','-f','docker-compose.dev.yml'])),doc];process.exit(res.every(Boolean)?0:1)"</automated>
<note>Am 2026-09-09 gegen den unveraenderten Arbeitsbaum ausgefuehrt: alle vier Pruefungen melden FEHLT, Rueckgabewert 1. Gegen eine Kopie mit dem hier beschriebenen Zusatz melden die drei Compose-Pruefungen `OK volume:user-files`, Rueckgabewert 0. Das Tor ist also echt rot und wird durch genau diese Aenderung gruen.</note>
</verify>
<done>
Alle drei gerenderten Konfigurationen (Basis, Prod, Basis+Dev) tragen fuer den Dienst `api`
genau einen Mount vom Typ `volume` mit Quelle `user-files` auf `/app/user-files`; beide
Compose-Dateien fuehren `user-files` als benanntes Top-Level-Volume neben `pgdata`;
`docker-compose.dev.yml` ist unveraendert; Kapitel 6 des Betriebshandbuchs beschreibt den
Speicher als dauerhaft, nennt die Mount-Zeile woertlich, nennt den Sicherungsbefehl und
weist auf die abweichende Serverdatei hin; die Fehlerzeile in Kapitel 7 passt dazu; kein
Aufruf gegen einen Server wurde ausgefuehrt; WINDOWS #17 bleibt offen.
</done>
</task>
<task type="auto">
<name>Task 2: Versionsangaben in CLAUDE.md auf den installierten Stand bringen</name>
<files>CLAUDE.md, .planning/research/STACK.md</files>
<reversibility rating="reversible">Reine Dokumentationsaenderung, jederzeit zuruecknehmbar.</reversibility>
<action>
Nur Text. Keine Abhaengigkeit wird aktualisiert, kein `pnpm add`, kein `pnpm update`,
`package.json` und `pnpm-lock.yaml` bleiben unangetastet.
Zuerst die Zahlen selbst nachschlagen und nicht aus diesem Plan uebernehmen: `pnpm-lock.yaml`,
Abschnitt `importers:`, liefert je Arbeitsbereich Vorgabe und aufgeloeste Fassung. Verbindlich
ist die aufgeloeste Fassung. Fuer Dienste ausserhalb von npm gelten die Compose- und
Dockerfiles (`postgres:16-alpine`, `node:24-alpine`). Die Tabelle im `objective` dieses Plans
ist der am 2026-09-09 gemessene Stand und dient als Gegenprobe — weicht Ihr Befund ab,
zaehlt Ihr Befund, und die Abweichung gehoert in die Zusammenfassung.
Dann den Block zwischen `<!-- GSD:stack-start ... -->` und `<!-- GSD:stack-end -->`
(CLAUDE.md Zeile 22-169) ueberarbeiten. Der Block bleibt englisch.
(a) Direkt unter die Ueberschrift `## Technology Stack` einen kurzen Herkunftsvermerk setzen:
dass die Tabellen den installierten Stand zeigen, am 2026-09-09 gegen `package.json`,
`pnpm-lock.yaml` und die Compose-/Dockerfiles geprueft; dass die urspruengliche Empfehlung
von 2026-06/07 in `.planning/research/STACK.md` liegt; und dass eine Regeneration dieses
Blocks aus jener Datei die Zahlen wieder verfaelschen wuerde.
(b) In den Technik-Tabellen jede Versionsangabe auf die tatsaechlich aufgeloeste Fassung
setzen. Die Spaltenueberschrift so benennen, dass klar ist, dass dort der Ist-Stand steht.
Ergaenzen Sie eine Zeile fuer Node (`node:24-alpine`, aus beiden produktiven Dockerfiles),
weil das die verbindliche Laufzeit ist und bisher fehlt. Vitest bekommt beide Fassungen mit
Angabe der App, weil sie sich unterscheiden. Bei Docker und Docker Compose gehoert dazu,
dass es Wirtseigenschaften sind, die das Repository nicht festlegt — mit der auf der
Entwicklungsmaschine gemessenen Fassung und Datum. Bei den Authentifizierungs-Zeilen tritt
an die Stelle des nie eingebauten Identitaetsanbieters, was wirklich laeuft: `@nestjs/jwt`,
`passport` mit `@nestjs/passport`, `argon2` fuer Passwoerter und `ldapts` fuer die
Verzeichnisanbindung; `@nestjs/passport` traegt heute ausserdem eine falsche Zweckangabe
(angeblich Schluessel-Authentifizierung zwischen Modulen) — richtig ist der Einsatz fuer die
Anmelde- und JWT-Strategien.
(c) Alles, was empfohlen, aber nie uebernommen wurde, verschwindet aus den Ist-Tabellen und
erscheint stattdessen in einem neuen, deutlich benannten Abschnitt im selben Block, direkt
unter den Tabellen: die Empfehlung, was stattdessen im Einsatz ist, und woher die Empfehlung
stammt. Betroffen sind der Identitaetsanbieter samt zugehoerigem NestJS-Paket, der
Cache-Dienst, die Server-State-Bibliothek, die Komponentenbibliothek, der E2E-Testlaeufer und
die beiden Git-Hook-Werkzeuge; ebenso die beiden Faelle, in denen zwar dieselbe Technik, aber
ein aelterer Hauptstand installiert ist (Next.js und Prisma) — dort mit dem klaren Vermerk,
dass die neuere Fassung empfohlen, aber nicht uebernommen wurde, und ohne jede Aussage
darueber, ob eine Aktualisierung geplant sei.
Dieser Abschnitt MUSS eine Aufzaehlung sein, keine Tabelle. Die Abnahmepruefung verwirft
jede Zeile, die eine dieser Techniken als erste Tabellenzelle fuehrt — das ist genau die
Form, die "ist eingebaut" behauptet.
(d) Die drei Anschluss-Abschnitte im selben Block angleichen, damit sie der korrigierten
Tabelle nicht widersprechen:
- `## Alternatives Considered` (Zeile 99-117): einen Einleitungssatz voranstellen, dass die
Tabelle die Entscheidungslage von 2026-06 festhaelt und ihre Spalte "Recommended" keine
Aussage ueber den heutigen Stand ist. Die Tabelle selbst bleibt inhaltlich stehen.
- `## Version Pinning Strategy` (Zeile 147-152): die Beispielangaben auf die installierten
Haupt-Fassungen bringen; die Zeile zum Identitaetsanbieter-Image ist gegenstandslos und
gehoert in die Aufzaehlung aus (c) statt in eine Regel.
- `## Sources` (Zeile 154-167): als Quellen der damaligen Empfehlung kennzeichnen, nicht als
Belege des Ist-Stands. Die Links bleiben.
- `## Multi-Tenancy Strategy` (Zeile 119-125) bleibt: Prisma Client Extensions sind
tatsaechlich im Einsatz (`apps/api/src/prisma/prisma-tenant.extension.ts`). Diesen Beleg
dort ergaenzen.
(e) Zuletzt `.planning/research/STACK.md`: unter die Ueberschrift `# v1.0 Base Stack
(reference — unchanged)` (Zeile 97) eine einzelne, abgesetzte Hinweiszeile setzen, dass es
sich um die Empfehlung vom Juni/Juli 2026 handelt, dass sie teilweise nicht uebernommen
wurde und dass der installierte Stand in CLAUDE.md steht. Nichts loeschen, keine Zahl in
diesem Dokument aendern — es ist ein datiertes Rechercheergebnis und bleibt als solches
lesbar.
Zum Schluss `docs/anleitung-entwicklung.md` gegenlesen (nicht aendern) und in der
Zusammenfassung bestaetigen, dass keine Versionsangabe der beiden Dokumente einander mehr
widerspricht.
</action>
<verify>
<automated>node -e "const t=require('fs').readFileSync('CLAUDE.md','utf8').split('\n');const has=re=>t.some(l=>re.test(l));const bad=t.filter(l=>/^\| (Keycloak|Redis|TanStack Query|Husky|lint-staged|Playwright|nest-keycloak-connect|shadcn\/ui) \|/.test(l));const res=[['Next-Zeile nennt 15.5.19',has(/^\| Next\.js \| 15\.5\.19/)],['Prisma-Zeile nennt 6.19.3',has(/^\| Prisma \| 6\.19\.3/)],['keine nicht installierte Technik als Ist-Zeile',bad.length===0]];for(const e of res)console.log(e[1]?'OK ':'ROT ',e[0]);if(bad.length)console.log('Treffer:\n'+bad.map(l=>l.slice(0,50)).join('\n'));process.exit(res.every(e=>e[1])?0:1)"</automated>
<automated>git diff --exit-code -- package.json apps/api/package.json apps/web/package.json apps/desktop/package.json packages/shared/package.json packages/module-sdk/package.json pnpm-lock.yaml</automated>
<note>Erste Pruefung am 2026-09-09 gegen den unveraenderten Arbeitsbaum ausgefuehrt: alle drei Punkte ROT, acht Trefferzeilen, Rueckgabewert 1; gegen eine korrigierte Kopie Rueckgabewert 0. Die zweite Pruefung ist heute gruen und bleibt es nur, solange keine Abhaengigkeit angefasst wird — sie ist die Absicherung gegen ein versehentliches Aktualisieren.</note>
</verify>
<done>
Jede Versionsangabe im Technik-Block von CLAUDE.md entspricht der in `pnpm-lock.yaml`
aufgeloesten Fassung beziehungsweise den Compose-/Dockerfiles; eine Node-Zeile ist ergaenzt;
Vitest ist mit beiden Fassungen je App gefuehrt; Docker/Compose sind als Wirtseigenschaft
mit Messdatum gekennzeichnet; die Authentifizierungs-Zeilen beschreiben die eingebaute
eigene Anmeldung statt eines Identitaetsanbieters; alle nie uebernommenen Empfehlungen
stehen sichtbar in einer Aufzaehlung statt in den Ist-Tabellen; Herkunftsvermerk gesetzt;
`Alternatives Considered`, `Version Pinning Strategy` und `Sources` widersprechen der
Tabelle nicht mehr; `.planning/research/STACK.md` traegt eine Hinweiszeile und ist sonst
unveraendert; `package.json` und `pnpm-lock.yaml` sind unveraendert.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| beschreibbare Container-Schicht -> dauerhafter Speicher | Von Nutzern hochgeladene Inhalte (Profilbilder, DKV-Exporte) verlassen die fluechtige Schicht und ueberdauern den Container |
| Host-Dateisystem <-> Container (verworfene Bind-Mount-Variante) | Ein Host-Verzeichnis waere ein zweiter Zugriffsweg auf Nutzerdaten, vorbei an der API |
| Dokument -> Leser/Agent | CLAUDE.md praegt Annahmen darueber, welche Schutzmechanismen angeblich vorhanden sind |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-cx0-01 | Information Disclosure | benanntes Volume `user-files` (docker-compose.yml, docker-compose.prod.yml) | low | accept | Es entsteht keine neue Zugriffsflaeche: die Dateien werden weiterhin ausschliesslich ueber authentifizierte Endpunkte ausgeliefert (`GET /users/me/avatar` streamt aus dem in der Datenbank hinterlegten Pfad; DKV-Downloads pruefen den Dateinamen gegen ein `DKV_*.xlsx`-Muster, `apps/api/src/dkv/dkv.controller.ts:134`). Es wird kein statisches Verzeichnis veroeffentlicht. Neu ist allein die Lebensdauer |
| T-cx0-02 | Tampering | verworfene Bind-Mount-Variante | medium | mitigate | Benanntes Volume statt Host-Pfad: kein Verzeichnis des Wirts wird in den Container gereicht, kein repo-relativer Pfad legt Nutzerinhalte in die Arbeitskopie. Zusaetzlich bleibt die Eigentuemerschaft uid 1001 aus `apps/api/Dockerfile:23-26` erhalten, statt root-eigene Rechte einzufuehren |
| T-cx0-03 | Denial of Service | Plattenbedarf des Volumes | low | accept | Wachstum ist bereits im Code begrenzt: Profilbilder sind auf 2 MB gedeckelt (`apps/api/src/user/user.controller.ts:232`) und je Nutzer bleibt genau eine Datei (aeltere Endungen werden geloescht, `:255-261`); DKV-Exporte werden auf zehn Dateien beschnitten (`MAX_EXPORT_FILES = 10`, `apps/api/src/dkv/dkv-export.service.ts:11`). Das Volume selbst hat keine Groessengrenze — als Betriebshinweis in Kapitel 6 aufgenommen, keine Codeaenderung |
| T-cx0-04 | Spoofing | CLAUDE.md, Abschnitt Authentifizierung | medium | mitigate | Das Dokument nennt heute einen Identitaetsanbieter, der nicht existiert. Wer das glaubt, nimmt Schutzfunktionen an (Sitzungsverwaltung, Sperren, Verzeichnisfoederation), die in Wahrheit selbst gebaut sind. Task 2 ersetzt die Angabe durch die tatsaechliche Kette aus eigenem JWT, `argon2` und `ldapts` |
| T-cx0-05 | Repudiation | WINDOWS #17 im Ledger | low | mitigate | Der Eintrag wird NICHT geschlossen. Die Reparatur im Repository erreicht die laufende Installation nicht; ein Schliessen wuerde einen Zustand behaupten, der auf alpha nicht vorliegt. Die Zusammenfassung haelt fest, was noch aussteht und wer es tut |
| T-cx0-SC | Tampering | npm/pnpm-Installationen | n/a | accept | Dieser Plan installiert kein Paket und aendert keine Abhaengigkeit. Eine Pruefung der Paketherkunft ist deshalb nicht erforderlich; die zweite automatisierte Pruefung in Task 2 (`git diff --exit-code` auf alle `package.json` und `pnpm-lock.yaml`) erzwingt genau das |
</threat_model>
<verification>
Automatisiert (beide Aufgaben, aus der Wurzel des Arbeitsbaums):
1. Das Mount-Tor aus Task 1 — drei gerenderte Konfigurationen plus die Mount-Zeile im
Betriebshandbuch. War vor der Arbeit vierfach rot.
2. Das Versions-Tor aus Task 2 — zwei Stichproben auf die korrigierten Zahlen plus die
Ausschlusspruefung fuer nie eingebaute Technik. War vor der Arbeit dreifach rot.
3. `git diff --exit-code` auf alle `package.json` und `pnpm-lock.yaml` — belegt, dass keine
Abhaengigkeit angefasst wurde.
4. `git status --short` — nur die fuenf im Plan genannten Dateien duerfen geaendert sein.
<human-check>
Nachzuholen durch den Nutzer, nicht durch den Executor — beides braucht einen Neubau der
Container, den der Nutzer selbst ausfuehrt:
(a) **Lokaler Beweis, dass die Dateien ueberleben.** Container mit den geaenderten
Compose-Dateien neu erstellen, im Portal unter den eigenen Einstellungen ein Profilbild
hochladen, danach `docker compose up -d --force-recreate api`, Seite neu laden: das Bild ist
noch da. Gegenprobe frueher: genau hier ging es verloren.
(b) **Uebernahme auf alpha.** Dieselben zwei Zeilen in `/opt/tessera/docker-compose.yml`
eintragen (Datei vorher sichern), Container neu erstellen, danach `docker inspect
tessera-api-1` pruefen — unter `Mounts` muss `/app/user-files` erscheinen. Erst wenn das
erledigt ist, darf WINDOWS #17 geschlossen werden (`gsd-tools windows fixed 17`).
(c) **Durchsicht der korrigierten Technik-Tabelle** durch den Nutzer, falls gewuenscht —
inhaltlich pruefbar ohne Fachkenntnis: es darf nichts drinstehen, was es im Projekt nicht gibt.
</human-check>
</verification>
<success_criteria>
1. Beide automatisierten Tore gruen, beide waren vorher nachweislich rot.
2. Genau zwei Commits, in dieser Reihenfolge und jeweils fuer sich lauffaehig:
Commit 1 (Task 1) — `fix(compose): user-files dauerhaft speichern und Betriebshandbuch nachziehen`;
Commit 2 (Task 2) — `docs(claude): Versionsangaben auf den installierten Stand bringen`.
3. Keine Abhaengigkeit aktualisiert, kein Server angefasst, keine Datei ausserhalb des
Arbeitsbaums beruehrt.
4. WINDOWS #17 bleibt offen; die Zusammenfassung nennt den verbliebenen Schritt des Nutzers
woertlich und in Alltagssprache.
5. Die Zusammenfassung nennt die gewaehlte Speicherart samt Begruendung und alle beim
Nachschlagen gefundenen Abweichungen von der Versionstabelle in diesem Plan.
</success_criteria>
<output>
Create `.planning/quick/260909-cx0-dateisicherung-nachruesten-und-versionsa/260909-cx0-SUMMARY.md` when done.
Festhalten: die tatsaechlich eingetragene Mount-Zeile im Wortlaut (zum Kopieren fuer den
Server), das Ergebnis beider Tore vor und nach der Arbeit, jede Abweichung zwischen der
Versionstabelle dieses Plans und dem selbst nachgeschlagenen Stand, sowie der offene Rest:
Serverdatei ergaenzen und danach den Ledger-Eintrag schliessen.
</output>