Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
63 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260914-ku1 | 01 | execute | 1 | true |
|
|
|
|
Purpose: Morgen geht Live. Der User muss einen Fehler auf Live beheben koennen, ohne Beta-Neuerungen mitzunehmen, die Korrektur danach kontrolliert in die Beta uebernehmen und jederzeit sehen, welche Fassung ein Anwender benutzt. Der Fehler-melden-Knopf (eigener Folge-Quick-Task) bekommt mit apps/web/src/lib/app-version.ts seine Quelle.
Output: 20 Dateien (13 Code/Tests, 5 Build/CI/Compose, 2 Handbuecher), drei Commits mit Scope quick-260914-ku1, gepusht, CI-Lauf beobachtet und die CI-gebauten :beta-Abbilder auf ihren Stempel geprueft. Zweig live und Tag v1.0.0 werden NICHT in diesem Plan angelegt (Rezept steht im Handbuch; der Orchestrator macht das nach dem Fehler-melden-Task).
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
<planning_measurements>
Zur Planungszeit (2026-09-14, HEAD 6c19451, Arbeitsbaum sauber, main == origin/main) gemessen — die Ausfuehrung misst erneut; diese Zahlen sind der Bezugspunkt der Gates:
- API-Suite
pnpm -C apps/api exec vitest run->Test Files 64 passed (64),Tests 1054 passed (1054). Web-Suitepnpm -C apps/web exec vitest run->Test Files 38 passed (38),Tests 233 passed (233)(die Baseline im Auftrag „64/1054" war nur die API).tsc --noEmitinapps/api,apps/web,packages/sharedje Exit 0. SidebarFooter(sidebar-footer.tsx) wird seitba02b25(2026-06-26, „restructure sidebar and admin navigation") NIRGENDS gerendert — einziger Treffer ausserhalb der Datei ist der Mock insidebar.test.tsx. Die Benutzerinfo lebt im Header-Dropdown (header.tsx134-154), die Seitenleiste endet mit dem Einklapp-Block (sidebar.tsx187-199,hidden md:block border-t border-sidebar-border p-2;!isCollapsedblendet dort und in der Navigation jeden Text aus). Eine Versionszeile insidebar-footer.tsxwaere unsichtbar. Deshalb: eigene KomponenteAppVersionBadge, gerendert insidebar.tsximsidebarContent(Zeile 91<div className="flex h-full flex-col bg-sidebar">, Ende bei 201) NACH dem Einklapp-Block;sidebarContentwird auch in der mobilen Schublade (Zeile 245) gerendert.sidebar-footer.tsxbleibt unangetastet (toter Code, im SUMMARY als Nebenbefund nennen, nicht loeschen).- Ohne Quellcode, der
process.env.NEXT_PUBLIC_APP_VERSIONliest, bettet Next.js nichts ein — der Grep aufv9.9.9-testim Web-Abbild funktioniert erst, wennapp-version.tsexistiert. Reihenfolge deshalb: Task 1 Code, Task 2 Build/CI. Nachweis des Mechanismus heute:docker run --rm --entrypoint sh <web-image> -c 'grep -rl "/api-proxy" /app/apps/web/.next/static | wc -l'-> 28. - Docker 29.8.0 / Compose v5.5.1 lokal. Ein Web-Bau mit warmem Cache (deps-Stufe getroffen, builder neu) dauerte 129 s; der API-Bau liegt in derselben Groessenordnung. Vier lokale Baeue (Task 2) sind also 6-12 Minuten — erwartete Dauer, kein Haenger. Lokales Zwischenabbild
tessera-web-plancheck:baselineexistiert (Cache-Waerme), darf am Ende mitdocker rmiweg. - ARG-Semantik bestaetigt (Zweistufen-Testbau im Scratchpad): globale
ARG X=devvor dem ersten FROM +ARG Xin jeder nutzenden Stufe -> ohne Argsdev, mit--build-argder Wert in builder UND runner. .dockerignore:node_modules .next dist .turbo .git .env *.md coverage—.gitfehlt im Kontext,git describeim Dockerfile unmoeglich.git tagliefert keine Zeile (0 Tags);git describe --tags --always->6c19451(kurzer SHA, Gate „Bau vor dem ersten Tag scheitert nicht" erfuellt). Nur Zweigmainlokal und remote. Gitea 1.26.2 auf localhost:3002;branch_protectionsleer; 0 Kollaborateure (nur schalli). Push-URLlocalhost:3002, Fetch-URLgit.vicolab.de(Memory).- Gitea UND
gitea-runner(act_runner v0.6.1, Labels ubuntu-latest, Host-Docker-Socket) laufen auf DIESEM Rechner. Folge: die CI-Baeue landen indocker imagesdes Hosts (localhost:3002/schalli/tessera-ctl/api:latestwurde vom letzten Lauf 296 zu HEAD6c19451gebaut; Lauf gestartet 13:51:28, beendet 13:53:15 — unter 2 Minuten, weil Docker-Layer-Cache; mit Build-Args wird der builder jedes Mal neu laufen, also kuenftig ~4-6 Minuten). Der Lauf ist perGET http://localhost:3002/api/v1/repos/schalli/tessera-ctl/actions/runs?limit=3mitAuthorization: token <Token aus pushurl>lesbar (Felderhead_sha,status,conclusion); anonym 401. Der Auftrag nahm an, der Lauf sei nicht beobachtbar — er ist es. docker compose -f docker-compose.prod.yml config --images 2>/dev/nulllaeuft lokal mit Exit 0 (die lokale.envliefert den PflichtwertTESSERA_ENCRYPTION_KEY) und rendert heuteweb:latest/api:latest.IMAGE_TAGkommt in.env.exampleund.env.prod.examplenicht vor.- Server alpha (nur gelesen,
ls/grepper SSH):/opt/tessera/.envtraegtCOMPOSE_FILE(Zeile 32) undAPP_URL;/opt/tessera/docker-compose.prod.yml(93 Zeilen) ist die Serverdatei mitweb:latest/api:latest(Zeilen 3 und 23) und weicht vom Repo (94 Zeilen) genau um die fehlende ZeileTESSERA_MIGRATE_DATABASE_URLab. Der aeltere Hinweis in Handbuch Abschnitt 6/7 auf/opt/tessera/docker-compose.ymlist damit ueberholt — im neuen Abschnitt 9 die tatsaechliche Datei nennen. Laufende Container:tessera-web-1,tessera-api-1,tessera-db-1. - YAML-Parser: PyYAML NICHT installiert;
js-yaml@4.2.0liegt innode_modules/.pnpm, ist aber vom Repo-Root nicht perrequire('js-yaml')aufloesbar — nur ueber den vollen Pfad/home/vicolab/projects/tessera-ctl/node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml(geprueft: parstci.yml,on.push.branches = ["main"],tags = undefined, Jobsquality,test,publish). Der Schluesselonwird als String geparst (YAML-1.2-Schema). apps/webhaengt NICHT von@tessera/sharedab (nurapps/api); ein neuer Import wuerdepackage.json+pnpm-lock.yamlaendern. Deshalb spiegeltapp-version.tsden Antworttyp lokal (ApiVersionInfo),VersionResponsebleibt inpackages/shareddie API-Wahrheit.- Workspace-Paketversionen stehen NICHT in
pnpm-lock.yaml(alle 16 Treffer auf0.0.1sind Fremdpakete) — ein Bump auf 1.0.0 waere lockfile-neutral, wuerde aber die deps-Stufe beider Dockerfiles (COPYpackage.json) invalidieren. Entscheidung:package.json-Versionen bleiben 0.0.1, die Wahrheit ist der Tag; im Handbuch so benannt. health.controller.ts: kein Spec vorhanden (Wave-0-Scaffold in Task 1).getVersion()liest heute die npm-Paketversion, die im Container nie gesetzt ist (Vorgabe'0.0.1'); kein Konsument im Web (grep -rn "health/version" apps/nur der Controller selbst).@Public()=SetMetadata('isPublic', true)(IS_PUBLIC_KEYaus../auth/decorators/public.decorator); Metadaten-Pruefung perReflect.getMetadatawie intenant.controller.spec.ts300-308.- Web-Muster:
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'+fetch(..., { credentials: 'include' })(sidebar.tsx12, 43-47;favorites-api.ts).vi.resetModules()+ dynamischer Import:module-access-gate.test.tsx37. Uebersetzungs-Mock:sidebar.test.tsx16-41 (useTranslations(ns)->map[ns][key] ?? key, verschachtelte Schluessel als'categories.label'). - Uebersetzungs-Waechter:
umlaut-guard.spec.tsprueft de.json aufae/oe/ue/ss-Token ausserhalbUMLAUT_ALLOWLIST(Fehlermeldung nennt den Fix) und de/en-Schluesselgleichheit;tenderRadar-parity.spec.tsnur den NamensraumtenderRadar. „Beta", „Live", „Entwicklung" enthalten keines der Token. docs/anleitung-betrieb.md: 346 Zeilen, 73 Zeilen mit echten Umlauten, viermalßneben „grösseren" — echte Umlaute beibehalten. Anker: Inhaltsverzeichnis 12-21 (Eintrag 8 in Zeile 21), Tabelle Abschnitt 1 Zeilen 32-33 (:latest), Konfigurationstabelle bis Zeile 161 (API_INTERNAL_URL), Abschnitt 4 Zeilen 194/207/210 (:latest), Abschnitt 7 Zeile 320 (Tessera API running on port 3001), Abschnitt 8 ab Zeile 334 (Ende der Datei 346).docs/ci-cd-setup.md: 169 Zeilen, 0 Umlaute (ASCII-Umschrift beibehalten); Anker: Zeilen 81-93 (Secrets: „keine Secrets", „kein Registry-Push"), 95-117 (Pipeline-Ueberblick:build-deploy, „Kein Registry-Push", D-13), 143-147 (Workflow-Dateien, D-11).- Detektoren:
api-coverage->{"detected":false};assumption-delta scan quick-260914-ku1->{"skipped":true,"reason":"phase_unresolved"}(Quick-Task ohne ROADMAP-Abschnitt; inhaltlich IST es eine Einzahl-zu-Mehrzahl-Aenderung — ein Etikett wird zu zwei Kanaelen; Entscheidung siehe<assumption_delta_decision>);schema-gate-> keine Schemadatei in der Erlaubnisliste. Konfiguration:tdd_mode=false(Task 1 traegt trotzdemtdd="true", die Tests sind vorab formulierbar),security_enforcement=true, ASVS 1, Blocking-Schwellehigh,human_verify_mode=end-of-phase. - Biome ist im Bestand nicht lauffaehig (WINDOWS #35,
pnpm lint= Leerlauf) — kein Biome-Gate in diesem Plan;biome.jsonunangetastet. </planning_measurements>
<assumption_delta_decision>
Noun, das jetzt primaer ist: der Auslieferungskanal (beta | live), nicht das Registry-Etikett. Entscheidung: promote — IMAGE_TAG in der Compose-Datei und APP_CHANNEL im Stempel sind die Primaerdarstellung; latest wird zum Alias von beta herabgestuft (bleibt nur, damit der bestehende alpha-Server ohne Handgriff weiterlaeuft, und darf spaeter entfallen — Handbuch sagt das). Kein add-alongside: es gibt keine Stelle mehr, die latest als eigene Wahrheit fuehrt.
</assumption_delta_decision>
Schritt B — GREEN, API:
1. `packages/shared/src/index.ts`: nach `HealthResponse` das Interface `VersionResponse` mit `name`, `version`, `channel`, `commit`, `buildTime` (alle `string`) exportieren; kurzer Kommentar, dass `channel` in der Praxis `beta` | `live` | `dev` ist und die Wahrheit der Version der Git-Tag ist (quick-260914-ku1).
2. `apps/api/src/health/app-version.ts` (NEU): `getAppVersion(): VersionResponse` liest `process.env.APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILD_TIME` jeweils mit `||` (NICHT `??`) und den Vorgaben `'dev'`, `'dev'`, `''`, `''`, `name: 'tessera'`; `formatAppVersionLine(v = getAppVersion()): string` baut `Tessera API ${version} (${channel})` und haengt ` ${commit}` nur an, wenn `commit` nicht leer ist. Kopfkommentar (deutsch, ASCII): woher die Werte kommen (Build-Args -> `ENV` in der Runner-Stufe beider Dockerfiles, gesetzt vom CI-Skript `.gitea/scripts/publish-images.sh`), warum `||` (Compose-Leerstring-Semantik) und dass `main.ts` die Zeile beim Start protokolliert.
3. `health.controller.ts`: `getVersion(): VersionResponse` gibt `getAppVersion()` zurueck; Import `VersionResponse` als `import type` neben `HealthResponse`; `@Public()` und `@Get('version')` bleiben. Der bisherige Zugriff auf die npm-Paketversion entfaellt vollstaendig (das Gate greppt darauf, Erwartung 0). Kommentar ueber `getVersion` (drei Zeilen): bewusst oeffentlich — Betreiber-Kontrolle per `curl` auf dem Server ohne Anmeldung, keine Systemkomponenten-Versionen, Repo privat (T-KU1-03).
<!-- planner-discipline-allow: npm_package_version -->
4. `main.ts`: `import { formatAppVersionLine } from './health/app-version';` und direkt nach `console.log('Tessera API running on port 3001');` die Zeile `console.log(formatAppVersionLine());` (gleicher Stil wie die bestehende Zeile; kein Nest-Logger, damit die Zeile im Serverlog neben der Port-Zeile steht — Handbuch Abschnitt 7 nennt beide).
Schritt C — GREEN, Web:
5. `apps/web/src/lib/app-version.ts` (NEU): `export type AppChannel = 'beta' | 'live' | 'dev'`; `export interface AppVersionInfo { version: string; channel: AppChannel; commit: string }`; `export interface ApiVersionInfo { name: string; version: string; channel: string; commit: string; buildTime: string }` (Kommentar: Spiegel von `VersionResponse` aus `packages/shared`, weil `apps/web` nicht von `@tessera/shared` abhaengt und ein neuer Import Lockfile und Docker-deps-Stufe aendern wuerde). `normalizeChannel(raw)` -> `'beta'`/`'live'` durchreichen, alles andere `'dev'`. `export const appVersion: AppVersionInfo` mit `process.env.NEXT_PUBLIC_APP_VERSION || 'dev'`, `normalizeChannel(process.env.NEXT_PUBLIC_APP_CHANNEL)`, `process.env.NEXT_PUBLIC_APP_COMMIT || ''` — JEDER Zugriff mit vollem Literalnamen, kein Destructuring, kein `process.env[name]` (Kopfkommentar erklaert: Next.js ersetzt nur den woertlichen Ausdruck zur Bauzeit, sonst ist der Wert im Browser leer). `const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'` wie in `sidebar.tsx`. `export function loadApiVersion(): Promise<ApiVersionInfo | null>` memoisiert ein Modul-Promise: `fetch(`${API_URL}/health/version`, { credentials: 'include' })` -> bei `res.ok` das JSON, sonst `null`; `.catch(() => null)`. Kopfkommentar nennt den Zweck: Quelle fuer das Abzeichen UND den kommenden Fehler-melden-Knopf.
6. `apps/web/src/components/layout/app-version-badge.tsx` (NEU, `'use client'`): `useTranslations('sidebar')`, `useState<ApiVersionInfo | null>(null)`, `useEffect` ruft `loadApiVersion().then(setApi)` einmal. Rendert ein `<span data-testid="app-version" className="block truncate text-xs text-muted-foreground" title={title}>` mit Text `{appVersion.version} · {t(`channel.${appVersion.channel}`)}`. `title`: Teile `Commit ${appVersion.commit}` (nur wenn Commit nicht leer) und `API ${api.version} (${api.channel})` (nur wenn geladen), mit ` · ` verbunden; keine Teile -> `title={undefined}` (kein Attribut). „Commit" und „API" sind in beiden Sprachen gleich, deshalb keine Schluessel dafuer.
7. `sidebar.tsx`: Import `AppVersionBadge` aus `@/components/layout/app-version-badge`; im `sidebarContent` NACH dem Einklapp-Block (gemessen 187-199) und vor dem schliessenden `</div>` (201) einen Block `{!isCollapsed && (<div className="border-t border-sidebar-border px-4 py-2"><AppVersionBadge /></div>)}` — bewusst OHNE `hidden md:block`, damit die mobile Schublade (rendert `sidebarContent`, Zeile 245) die Zeile ebenfalls zeigt; `!isCollapsed` haelt das heutige Muster (eingeklappt: kein Text).
8. `de.json`/`en.json`: im Namensraum `sidebar` ein Objekt `channel` mit `beta: "Beta"`, `live: "Live"`, `dev: "Entwicklung"` bzw. `dev: "Development"` — in BEIDEN Dateien an derselben Stelle (hinter `modules`), sonst faellt der Schluesselgleichheits-Test.
9. `sidebar.test.tsx`: Modul-Mock und sechster Test aus `<behavior>`.
Dann alle betroffenen Specs gruen: API-Spec `Tests 6 passed (6)`; Web `app-version.test.ts` 5, `app-version-badge.test.tsx` 4, `sidebar.test.tsx` 6. Volle Suiten: API `Test Files 65 passed (65)` / `Tests 1060 passed (1060)`, Web `Test Files 40 passed (40)` / `Tests 243 passed (243)`; `tsc --noEmit` in api, web, shared Exit 0. Weicht eine Zahl ab, ist das ein Befund fuer das SUMMARY — erst die Ursache benennen, dann korrigieren.
Commit: `feat(quick-260914-ku1): Versionsstempel — GET /health/version aus APP_*, VersionResponse, app-version.ts und Abzeichen v<Version> · <Kanal> in der Seitenleiste` mit genau den 13 Dateien dieser Aufgabe (`git show --stat HEAD` zeigt 13).
cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/api exec vitest run src/health/health.controller.spec.ts 2>&1 | grep -E "^\s+Tests" ; pnpm -C apps/web exec vitest run src/lib/app-version.test.ts src/components/layout/app-version-badge.test.tsx src/components/layout/sidebar.test.tsx 2>&1 | grep -E "^\s+(Test Files|Tests)" ; grep -c "npm_package_version" apps/api/src/health/health.controller.ts ; grep -c "formatAppVersionLine()" apps/api/src/main.ts ; grep -c "process.env.NEXT_PUBLIC_APP_VERSION" apps/web/src/lib/app-version.ts ; grep -c "" apps/web/src/components/layout/sidebar.tsx ; node -e "const d=require('./apps/web/src/messages/de.json'),e=require('./apps/web/src/messages/en.json');console.log(d.sidebar.channel.beta,d.sidebar.channel.live,d.sidebar.channel.dev,'|',e.sidebar.channel.dev)" ; for p in packages/shared apps/api apps/web; do pnpm -C $p exec tsc --noEmit; echo "TSC_$p=$?"; done
API-Spec-Zeile `Tests 6 passed (6)`; Web-Ausgabe `Test Files 3 passed (3)` und `Tests 15 passed (15)`; Greps liefern `0` (npm-Paketversion), `1` (main.ts), `1` (Literalzugriff), `1` (Sidebar); die Node-Zeile lautet `Beta Live Entwicklung | Development`; alle drei `TSC_...=0`. Der RED-Lauf aus Schritt A steht mit seinen Ausgabezeilen im SUMMARY. Commit existiert mit genau 13 Dateien.
Task 2: Build-Args in beide Dockerfiles, Veroeffentlichungs-Skript und CI-Trigger je Kanal, IMAGE_TAG in der Compose-Datei — Falsifizierung durch lokale Baeue
apps/web/Dockerfile, apps/api/Dockerfile, .gitea/scripts/publish-images.sh, .gitea/workflows/ci.yml, docker-compose.prod.yml
Schritt A — Dockerfiles (beide): vor dem ersten `FROM` vier globale Args `ARG APP_VERSION=dev`, `ARG APP_CHANNEL=dev`, `ARG APP_COMMIT=`, `ARG APP_BUILD_TIME=` mit einem zweizeiligen Kommentar (ASCII): Werte kommen aus `.gitea/scripts/publish-images.sh`; lokal greifen die Vorgaben; jede nutzende Stufe wiederholt `ARG NAME`, weil ein globales ARG nur die Vorgabe liefert.
- `apps/web/Dockerfile`, Stufe `builder`: NACH den vier `COPY`-Zeilen und der bestehenden Zeile `ENV NEXT_PUBLIC_API_URL=/api-proxy`, unmittelbar VOR `RUN pnpm --filter=@tessera/web build`: `ARG APP_VERSION`, `ARG APP_CHANNEL`, `ARG APP_COMMIT`, dann `ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION NEXT_PUBLIC_APP_CHANNEL=$APP_CHANNEL NEXT_PUBLIC_APP_COMMIT=$APP_COMMIT` (Kommentar: muss VOR dem Build stehen, Next.js bettet zur Bauzeit ein; so spaet wie moeglich, damit die COPY-Schichten im Cache bleiben). Stufe `runner`: nach `ENV NODE_ENV=production` die vier `ARG`-Wiederholungen und `ENV APP_VERSION=$APP_VERSION APP_CHANNEL=$APP_CHANNEL APP_COMMIT=$APP_COMMIT APP_BUILD_TIME=$APP_BUILD_TIME` (Laufzeit-Umgebung, schadet nicht, gleiche Form wie die API).
- `apps/api/Dockerfile`, Stufe `runner`: nach `ENV NODE_ENV=production` dieselben vier `ARG`-Wiederholungen und dieselbe `ENV`-Zeile. Die builder-Stufe der API braucht nichts (Nest liest zur Laufzeit).
Sonst nichts an den Dockerfiles aendern (Nutzer, Ports, CMD, Prisma-Kopierpfade bleiben).
Schritt B — `.gitea/scripts/publish-images.sh` (NEU, POSIX `sh`, `set -eu`, ausfuehrbar `chmod +x`, Kopfkommentar ASCII mit Bezug quick-260914-ku1 und dem Kanalmodell):
- `REGISTRY="${REGISTRY:-localhost:3002/schalli/tessera-ctl}"`, `REF="${GITHUB_REF:-}"`.
- `case "$REF" in refs/tags/v*) APP_CHANNEL=live; TAGS="live ${REF#refs/tags/}" ;; refs/heads/main) APP_CHANNEL=beta; TAGS="beta latest" ;; *) echo "Kein Veroeffentlichungs-Anlass fuer '$REF' (nur main und Tags v*): nichts zu tun."; exit 0 ;; esac` — der Zweig `live` OHNE Tag wird damit gepreuft, aber nicht veroeffentlicht (Begruendung im Kommentar: auf `live` ist jeder auslieferbare Stand ein Tag; ein ungetaggter Merge darf das `live`-Etikett nicht ueberschreiben, sonst waere der Tag nicht mehr die Wahrheit).
- `APP_VERSION="$(git describe --tags --always)"`, `APP_COMMIT="$(git rev-parse --short HEAD)"`, `APP_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"`; eine Ausgabezeile `Tessera $APP_VERSION ($APP_CHANNEL) $APP_COMMIT $APP_BUILD_TIME -> Etiketten: $TAGS`.
- `if [ "${1:-}" = "--print-plan" ]`: fuer `IMG in web api` und `TAG in $TAGS` je eine Zeile `push $REGISTRY/$IMG:$TAG` ausgeben, `exit 0` — kein Docker-Aufruf (fuer lokale Gates und die CI-Fehlersuche).
- Sonst fuer `IMG in web api`: `docker build -t "$REGISTRY/$IMG:$APP_CHANNEL" --build-arg APP_VERSION="$APP_VERSION" --build-arg APP_CHANNEL="$APP_CHANNEL" --build-arg APP_COMMIT="$APP_COMMIT" --build-arg APP_BUILD_TIME="$APP_BUILD_TIME" -f "apps/$IMG/Dockerfile" .`; dann fuer jedes `TAG in $TAGS`: `docker tag "$REGISTRY/$IMG:$APP_CHANNEL" "$REGISTRY/$IMG:$TAG"` und `docker push "$REGISTRY/$IMG:$TAG"`.
- Das Skript gibt niemals ein Secret aus (es kennt keins; der Login bleibt im Workflow).
Schritt C — `.gitea/workflows/ci.yml`: `on.push.branches: [main, live]` und `on.push.tags: ['v*']`. `quality` und `test` unveraendert. `publish`: `actions/checkout@v4` bekommt `with: fetch-depth: 0` (Kommentar: ohne volle Historie und Tags liefert `git describe` nichts — Pflicht fuer den Stempel); der Login-Schritt bleibt byte-identisch; die drei bisherigen Build-/Push-Schritte werden durch EINEN Schritt `Versionsstempel berechnen, Abbilder bauen und veroeffentlichen` mit `run: sh .gitea/scripts/publish-images.sh` ersetzt. Kein `if:` auf Job-Ebene — die Entscheidung liegt im Skript, damit sie lokal mit `--print-plan` pruefbar ist und nicht vom Ausdrucks-Auswerter des Runners abhaengt. Kommentar oben in der Datei (ASCII): Kanalmodell in drei Zeilen.
Schritt D — `docker-compose.prod.yml`: nur die beiden `image:`-Zeilen auf `git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta}` bzw. `.../api:${IMAGE_TAG:-beta}` mit einem Kommentar darueber (Ton der Datei, englisch wie die Nachbarkommentare oder deutsch — an den vorhandenen Kommentaren orientieren): `IMAGE_TAG` in `.env` = `beta` oder `live`, Vorgabe `beta`. Registry-Host, Ports, Umgebung, Healthchecks unangetastet. Danach darf kein `:latest` mehr in der Datei stehen (Gate).
<!-- planner-discipline-allow: :latest -->
Schritt E — Falsifizierung (erwartete Dauer 6-12 Minuten fuer vier Baeue, KEIN Haenger; Layer-Cache der deps-Stufe ist warm):
1. `docker build -t tessera-ku1-api:args --build-arg APP_VERSION=v9.9.9-test --build-arg APP_CHANNEL=live --build-arg APP_COMMIT=abc1234 --build-arg APP_BUILD_TIME=2026-09-14T00:00:00Z -f apps/api/Dockerfile .` und dasselbe fuer web (`tessera-ku1-web:args`, `-f apps/web/Dockerfile`).
2. `docker build -t tessera-ku1-api:noargs -f apps/api/Dockerfile .` und `tessera-ku1-web:noargs` — ohne Args.
3. Beweise (Ausgaben ins SUMMARY): `docker run --rm --entrypoint node tessera-ku1-api:args -e 'console.log(process.env.APP_VERSION, process.env.APP_CHANNEL, process.env.APP_COMMIT, process.env.APP_BUILD_TIME)'` -> `v9.9.9-test live abc1234 2026-09-14T00:00:00Z`; `docker run --rm --entrypoint node tessera-ku1-api:args -e 'console.log(require("/app/apps/api/dist/health/app-version").formatAppVersionLine())'` -> `Tessera API v9.9.9-test (live) abc1234` (kompilierter Code liest die Laufzeit-Umgebung); `docker run --rm --entrypoint sh tessera-ku1-web:args -c 'grep -rl "v9.9.9-test" /app/apps/web/.next/static | wc -l'` -> mindestens `1` (Bauzeit-Einbettung im Browser-Bundle); `docker run --rm --entrypoint node tessera-ku1-web:args -e 'console.log(process.env.APP_VERSION)'` -> `v9.9.9-test`; beide `:noargs`-Abbilder -> `dev` bei derselben Node-Zeile, und im Web-Bundle ohne Args ist `v9.9.9-test` NICHT enthalten (`wc -l` -> `0`).
4. Aufraeumen: `docker rmi tessera-ku1-api:args tessera-ku1-web:args tessera-ku1-api:noargs tessera-ku1-web:noargs tessera-web-plancheck:baseline` (die Etiketten; Layer bleiben im Cache).
Schritt F — Gates ohne Docker: js-yaml-Struktur (siehe verify), `--print-plan` in drei Lagen, `docker compose config --images` mit und ohne `IMAGE_TAG`, `git describe --tags --always` liefert einen 7-stelligen Hex-SHA (kein Tag vorhanden, Bau vor dem ersten Tag scheitert nicht).
Commit: `ci(quick-260914-ku1): zwei Kanaele — main -> beta+latest, Tag v* -> live+vX.Y.Z, Versionsstempel als Build-Args in beide Dockerfiles, IMAGE_TAG in docker-compose.prod.yml` mit genau den 5 Dateien dieser Aufgabe.
cd /home/vicolab/projects/tessera-ctl && node -e "const y=require('/home/vicolab/projects/tessera-ctl/node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml');const d=y.load(require('fs').readFileSync('.gitea/workflows/ci.yml','utf8'));const p=d.jobs.publish.steps;const co=p.find(s=>(s.uses||'').startsWith('actions/checkout'));console.log('branches='+JSON.stringify(d.on.push.branches),'tags='+JSON.stringify(d.on.push.tags),'jobs='+Object.keys(d.jobs).join(','),'fetchDepth='+(co&&co.with&&co.with['fetch-depth']),'script='+p.some(s=>(s.run||'').includes('publish-images.sh')),'needs='+d.jobs.publish.needs)" ; GITHUB_REF=refs/heads/main sh .gitea/scripts/publish-images.sh --print-plan | grep -c "^push .*:\(beta\|latest\)$" ; GITHUB_REF=refs/tags/v1.2.3 sh .gitea/scripts/publish-images.sh --print-plan | grep -c "^push .*:\(live\|v1.2.3\)$" ; GITHUB_REF=refs/heads/live sh .gitea/scripts/publish-images.sh --print-plan | grep -c "^push " ; GITHUB_REF=refs/heads/main sh .gitea/scripts/publish-images.sh --print-plan | grep -cE "^Tessera [0-9a-f]{7} \(beta\) [0-9a-f]{7} [0-9]{4}-" ; docker compose -f docker-compose.prod.yml config --images 2>/dev/null | grep -c ':beta$' ; IMAGE_TAG=live docker compose -f docker-compose.prod.yml config --images 2>/dev/null | grep -c ':live$' ; grep -c ':latest' docker-compose.prod.yml ; grep -c '^ARG APP_VERSION=dev' apps/web/Dockerfile apps/api/Dockerfile ; grep -c 'NEXT_PUBLIC_APP_VERSION=\$APP_VERSION' apps/web/Dockerfile ; grep -c 'ENV APP_VERSION=\$APP_VERSION' apps/web/Dockerfile apps/api/Dockerfile ; test -x .gitea/scripts/publish-images.sh; echo EXEC=$?
Node-Zeile lautet `branches=["main","live"] tags=["v*"] jobs=quality,test,publish fetchDepth=0 script=true needs=test`; die vier `--print-plan`-Greps liefern `4`, `4`, `0`, `1`; Compose-Greps `2` und `2`; `:latest`-Grep `0`; `ARG`-Grep je Datei `1`; `NEXT_PUBLIC_APP_VERSION`-Grep `1`; `ENV APP_VERSION`-Grep je Datei `1`; `EXEC=0`.
Das SUMMARY traegt unter „Falsifizierung Bauzeit-Einbettung" die sechs Docker-Ausgaben aus Schritt E (`v9.9.9-test live abc1234 ...`, die `Tessera API`-Zeile, die Trefferzahl im Web-Bundle >= 1, `v9.9.9-test` Web-Laufzeit, `dev`/`dev` ohne Args, `0` Treffer ohne Args) und die gemessene Baudauer. Commit existiert mit genau 5 Dateien.
Task 3: Betriebshandbuch „Zwei Kanäle: Live und Beta", ci-cd-setup auf gemessenen Stand, Push und Beobachtung des echten CI-Laufs
docs/anleitung-betrieb.md, docs/ci-cd-setup.md
Gitea antwortet lokal: `curl -s --max-time 5 http://localhost:3002/api/v1/version` liefert `{"version":"1.26.2"}`, und `docker ps --format '{{.Names}}' | grep -c '^gitea-runner$'` liefert `1` (sonst ist der CI-Lauf nicht beobachtbar — dann Push trotzdem, Beobachtung als offenen Punkt ins SUMMARY).
Schritt A — `docs/anleitung-betrieb.md` (echte Umlaute, Alltagssprache, Sie-Form wie im Bestand, ohne Fachjargon, jeder Befehl als Codeblock):
1. Inhaltsverzeichnis (Zeilen 12-21): Eintrag `9. [Zwei Kanäle: Live und Beta](#9-zwei-kanäle-live-und-beta)` hinter Eintrag 8.
2. Abschnitt 1, Tabelle (Zeilen 32-33): `web:latest`/`api:latest` durch `web:${IMAGE_TAG}` bzw. `api:${IMAGE_TAG}` ersetzen und in Klammern „(`beta` oder `live`, siehe Kapitel 9)".
3. Abschnitt 3, Konfigurationstabelle: neue Zeile nach `API_INTERNAL_URL` (Zeile 161): `IMAGE_TAG` | empfohlen | `beta` | Welcher Kanal auf diesem Server läuft: `beta` (alle Neuerungen, alpha) oder `live` (nur freigegebene Versionen, tessera.ctl.de). Siehe Kapitel 9.
4. Abschnitt 4 (Zeilen 194, 207, 210): `:latest` im Text durch „demselben Etikett (`beta` bzw. `live`)" und in den zwei `docker image inspect`-Befehlen durch `:beta` (mit Hinweis „auf dem Live-Server `:live`") ersetzen.
5. Abschnitt 7 (Zeile 320): nach `Tessera API running on port 3001` die neue Zeile `Tessera API v1.0.0 (live) abc1234` als zweite Erwartung nennen (Version, Kanal, Kurzkennung des Standes).
6. Neuer Abschnitt `## 9. Zwei Kanäle: Live und Beta` NACH Abschnitt 8 (Dateiende), mit diesen Unterabschnitten (`###`), jeder in drei bis acht Sätzen plus Befehle:
- „Was ein Kanal ist": Beta = alles Neue, sofort nach jeder Änderung (Adresse alpha.tessera.ctl.de, Etikett `beta`; `latest` ist nur ein zweiter Name für `beta`, bleibt vorerst und kann später wegfallen). Live = nur freigegebene Versionen mit Nummer (tessera.ctl.de, Etikett `live` und zusätzlich `v1.0.0`, `v1.0.1`, …). Die Versionsnummer kommt aus der Freigabe (Git-Tag), nicht aus einer Datei im Code; zwischen zwei Freigaben zeigt die Beta „v1.0.0-12-abc1234" (12 Änderungen nach 1.0.0), vor der allerersten Freigabe nur eine Kurzkennung.
- „Die eine Zeile je Server": `IMAGE_TAG=beta` in `/opt/tessera/.env` auf alpha, `IMAGE_TAG=live` auf dem neuen Server; ohne die Zeile nimmt die Compose-Datei `beta`. Dazu, weil `/opt/tessera` keine Arbeitskopie ist (Kapitel 3, Drift): die zwei `image:`-Zeilen in `/opt/tessera/docker-compose.prod.yml` (das ist die auf alpha benutzte Datei; `.env` setzt `COMPOSE_FILE`) von Hand auf die Form `git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta}` und `.../api:${IMAGE_TAG:-beta}` bringen (vorher `cp docker-compose.prod.yml docker-compose.prod.yml.bak.$(date +%Y%m%d)`), danach `pull` und `up -d --force-recreate api web`. Hinweis: die Vorlage `.env.prod.example` enthält die Zeile noch nicht — beim Anlegen einer neuen `.env` von Hand ergänzen.
- „Eine Version freigeben" (was Claude tut; der User sagt nur „Version X freigeben"): `git checkout live`, `git merge --ff-only main` (geht das nicht, ist eine Korrektur noch nicht zurück in `main` — erst Hotfix-Schritt 5 nachholen), `git tag -a vX.Y.Z -m "Tessera X.Y.Z"`, `git push origin live vX.Y.Z`; die Pipeline baut den Stand mit dem Tag und legt `live` + `vX.Y.Z` ab (zwei Läufe: Zweig prüft nur, Tag veröffentlicht). Danach der User auf dem Live-Server: `docker compose -f docker-compose.prod.yml pull` und `docker compose -f docker-compose.prod.yml up -d --force-recreate api web` (Kapitel 4 gilt unverändert). Erstfreigabe v1.0.0 als eigener kleiner Absatz: `git checkout -b live main`, Tag `v1.0.0`, Push — erfolgt nach dem Fehler-melden-Knopf, nicht in diesem Durchlauf.
- „Einen Fehler auf Live beheben (Hotfix)": 1. `git checkout live && git pull`; 2. Korrekturzweig `hotfix/` von `live`; 3. Korrektur + Tests; 4. nach `live` mergen, Tag `vX.Y.(Z+1)`, `git push origin live vX.Y.(Z+1)`, User spielt auf Live ein; 5. Korrektur in die Beta: `git checkout main && git merge live` — vorher prüfen, ob sie dort zusammenpasst (Konflikte, Tests), dann `git push` — die Beta bekommt sie mit dem nächsten Lauf. Regel in eigener Fettschrift: **Keine Datenbankänderung als Hotfix.** Begründung in Alltagssprache: Datenbankänderungen (Migrationen) werden nach ihrem Zeitstempel im Namen sortiert und in dieser Reihenfolge ausgeführt; die Beta hat womöglich schon neuere Änderungen eingespielt; eine Hotfix-Änderung mit noch späterem Stempel landet beim Zusammenführen hinter Änderungen, die sie eigentlich nicht kennt — das ist der eine Fall, der beim Übernehmen in die Beta still kaputtgehen kann. Braucht eine Korrektur eine Datenbankänderung, wird sie als reguläre Version über `main` freigegeben.
- „Woran Sie erkennen, welche Version läuft": (a) unten in der Seitenleiste steht `v1.0.0 · Live` bzw. `· Beta`; Maus darüber zeigt die Kurzkennung und die Version des Servers — weichen Oberfläche und Server ab, wurde nur einer der beiden Container neu erstellt (Kapitel 4, `--force-recreate api web`); (b) auf dem Server `curl -s http://localhost:3001/health/version` (Antwortfelder `version`, `channel`, `commit`, `buildTime`); (c) `docker compose -f docker-compose.prod.yml logs api | grep "Tessera API"`.
- „Den neuen Live-Server einrichten": Kapitel 2 gilt vollständig; Abweichungen: `IMAGE_TAG=live` in der `.env`; eigene, neu erzeugte Geheimnisse (`JWT_SECRET`, `TESSERA_ENCRYPTION_KEY`, `DB_PASSWORD`, Admin-Passwort) — nichts von alpha übernehmen; eigene, leere Datenbank (die API legt den ersten Admin an) — die alpha-Datenbank wird NICHT kopiert, es sei denn, das wird ausdrücklich gewünscht (dann Kapitel 6 Wiederherstellung UND derselbe `TESSERA_ENCRYPTION_KEY`, sonst sind gespeicherte Zugangsdaten unbrauchbar); `APP_URL=https://tessera.ctl.de`; erster `pull` holt `:live` — vor der Erstfreigabe v1.0.0 gibt es dieses Etikett noch nicht, deshalb erst freigeben, dann installieren (oder für den Probelauf `IMAGE_TAG=beta`, danach umstellen).
7. Abschnitt 8 bleibt; nur der Verweis „Kapitel 4" um „und 9" ergänzen, falls dort vom Einspielen die Rede ist (Zeile 344-345).
Schritt B — `docs/ci-cd-setup.md` (ASCII-Umschrift wie im Bestand, keine Umlaute):
1. Abschnitt 3 „Gitea Secrets" (Zeilen 81-93): die Aussage, es wuerden keine Secrets gebraucht und es gebe keinen Registry-Push, durch den Stand ersetzen: Secret `REGISTRY_TOKEN` (Gitea-Zugangstoken mit Paket-Schreibrecht), verwendet im Login `docker login localhost:3002 --password-stdin` (Token nie im Log; Gitea maskiert Secrets); Push geht ueber `localhost:3002`, weil der Nginx Proxy Manager vor `git.vicolab.de` grosse Blobs blockt — das Pullen auf den Servern laeuft ueber `git.vicolab.de`.
2. Abschnitt 4 „Pipeline-Ueberblick" (Zeilen 95-117): Trigger `push` auf `main` und `live` sowie Tags `v*`; Jobs `quality` (Lint ist derzeit ein Leerlauf, WINDOWS #35, Type-Check echt) -> `test` -> `publish`; `publish` = `actions/checkout@v4` mit `fetch-depth: 0` (Tags fuer `git describe`), Login, `.gitea/scripts/publish-images.sh`. Etiketten-Tabelle: `main` -> `beta` + `latest` (Alias, entfaellt spaeter); Tag `vX.Y.Z` -> `live` + `vX.Y.Z`; Zweig `live` ohne Tag -> nur pruefen. Stempel: `APP_VERSION` (`git describe --tags --always`), `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILD_TIME` als `--build-arg` in beide Dockerfiles; Web bettet `NEXT_PUBLIC_APP_*` zur Bauzeit ein, deshalb baut der Web-`builder` jetzt bei jedem Lauf neu (Laufzeit eher 4-6 statt 2 Minuten). Lokale Probe: `GITHUB_REF=refs/tags/v1.2.3 sh .gitea/scripts/publish-images.sh --print-plan`. Der Unterabschnitt „Kein Registry-Push" und das Wort `build-deploy` verschwinden; D-13 als ueberholt kennzeichnen.
<!-- planner-discipline-allow: Kein Registry-Push, build-deploy -->
3. Abschnitt 5 „Workflow-Dateien" (143-147): einen Satz ergaenzen — ein Tag-Push ist der Freigabe-Hebel fuer Live; heute hat nur das Konto `schalli` Schreibrecht (0 Kollaborateure, keine Branch-Regeln); kommen weitere Konten dazu, in Gitea eine Tag-Schutzregel fuer `v*` und Branch-Schutz fuer `live` anlegen (T-KU1-04).
4. Abschnitt 6 „Fehlerbehebung": Punkt „Stempel zeigt `dev` oder nur eine Kurzkennung statt des Tags" -> `fetch-depth: 0` im Checkout pruefen und ob der Tag gepusht wurde (`git ls-remote --tags origin`).
Schritt C — Gates, Commit, Push, Beobachtung:
1. `grep -c "^## 9. Zwei Kanäle: Live und Beta" docs/anleitung-betrieb.md` -> 1; `grep -c "IMAGE_TAG" docs/anleitung-betrieb.md` -> mindestens 6; `grep -c "Keine Datenbankänderung als Hotfix" docs/anleitung-betrieb.md` -> 1; `grep -c "Kein Registry-Push\|build-deploy" docs/ci-cd-setup.md` -> 0; `grep -c "publish-images.sh" docs/ci-cd-setup.md` -> mindestens 2; `grep -c '[äöüÄÖÜß]' docs/ci-cd-setup.md` -> 0 (ASCII-Konvention der Datei gehalten).
2. `D=$(git diff --stat 6c19451 -- . ':!.planning'); echo GIT_EXIT=$?; tail -n1 <<< "$D"` -> `GIT_EXIT=0` und `20 files changed`; Stichprobe unangetastet: `git diff --stat 6c19451 -- apps/api/prisma biome.json '.env*' apps/web/package.json apps/api/package.json pnpm-lock.yaml` -> leer.
3. Commit: `docs(quick-260914-ku1): Betriebshandbuch — Zwei Kanäle Live und Beta, Freigabe, Hotfix ohne Datenbankänderung, neuer Live-Server; ci-cd-setup auf gemessenen Stand` (nur die 2 Dateien). Danach `git push` (schlichter Aufruf, die Push-URL zeigt auf localhost:3002); `S=$(git status -sb); echo GIT_EXIT=$?; head -n1 <<< "$S"` -> `GIT_EXIT=0`, kein `[ahead`.
4. Beobachtung des echten CI-Laufs (Token NIE ausgeben — nur in der Variablen verwenden): `PUSHED=$(git rev-parse HEAD); PUSHURL=$(git config --get remote.origin.pushurl); TOK=$(printf '%s' "$PUSHURL" | sed -E 's#.*schalli:([^@]+)@.*#\1#')`; dann bis zu 12 Minuten alle 20 s `curl -s -H "Authorization: token $TOK" "http://localhost:3002/api/v1/repos/schalli/tessera-ctl/actions/runs?limit=5"` abfragen, den Eintrag mit `head_sha == PUSHED` nehmen und auf `status == completed` warten (als Hintergrundbefehl starten, falls `sleep` im Vordergrund blockiert ist). Erwartung `conclusion == success`. Danach auf dem Host: `docker run --rm --entrypoint node localhost:3002/schalli/tessera-ctl/api:beta -e 'console.log(process.env.APP_VERSION, process.env.APP_CHANNEL)'` -> `<kurzer SHA von PUSHED> beta` (Vergleich mit `git rev-parse --short $PUSHED`), und `docker image inspect -f '{{.Created}}' localhost:3002/schalli/tessera-ctl/web:beta localhost:3002/schalli/tessera-ctl/web:latest` zeigt zwei gleiche Zeitstempel NACH dem Push (beide Etiketten aus demselben Bau). Ergebnis (Lauf-ID, Dauer `started_at`/`completed_at`, Stempel-Ausgabe) ins SUMMARY. Ist `conclusion` nicht `success`: Job-Log ueber `.../actions/runs/<id>/jobs` lesen, Ursache im SUMMARY benennen, Korrektur als eigener `fix(quick-260914-ku1)`-Commit, erneut pushen und beobachten — die haeufigste Ursache waere ein Runner-Umgebungsdetail (Git im Job-Container, `GITHUB_REF` nicht gesetzt); das Skript-Design mit `--print-plan` grenzt das ein.
5. Wird das SUMMARY erst nach dem Push committet, den Push danach wiederholen (ein weiterer CI-Lauf ist erwartet und in Ordnung).
cd /home/vicolab/projects/tessera-ctl && grep -c "^## 9. Zwei Kanäle: Live und Beta" docs/anleitung-betrieb.md ; grep -c "IMAGE_TAG" docs/anleitung-betrieb.md ; grep -c "Keine Datenbankänderung als Hotfix" docs/anleitung-betrieb.md ; grep -c "Kein Registry-Push\|build-deploy" docs/ci-cd-setup.md ; grep -c "publish-images.sh" docs/ci-cd-setup.md ; grep -c '[äöüÄÖÜß]' docs/ci-cd-setup.md ; D=$(git diff --stat 6c19451 -- . ':!.planning'); echo GIT_EXIT=$? ; tail -n1 <<< "$D" ; U=$(git diff --stat 6c19451 -- apps/api/prisma biome.json '.env*' apps/web/package.json apps/api/package.json pnpm-lock.yaml); echo U_EXIT=$? ; test -z "$U"; echo U_EMPTY=$? ; S=$(git status -sb); head -n1 <<< "$S"
Greps liefern `1`, `>= 6`, `1`, `0`, `>= 2`, `0`; `GIT_EXIT=0` und die Summenzeile nennt `20 files changed`; die Unangetastet-Stichprobe liefert `U_EXIT=0` und `U_EMPTY=0`; die Status-Zeile enthaelt kein `[ahead`. Das SUMMARY traegt unter „CI-Lauf nach dem Push" Lauf-ID, `conclusion`, Dauer und die Stempel-Zeile ` beta` der vom Runner gebauten Abbilder (oder, falls Gitea/Runner nicht erreichbar waren, den Grund und den offenen Punkt).
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Gitea-Repository -> CI-Runner -> Registry | Ein Push auf main oder ein Tag v* loest Bau und Veroeffentlichung aus; der Runner haelt das Registry-Token als Secret und den Host-Docker-Socket |
| Registry -> Server (alpha, Live) | docker compose pull holt das Etikett aus IMAGE_TAG; welches Etikett, entscheidet eine Zeile in .env auf dem Server |
Internet -> GET /health/version (@Public) |
Unauthentifizierter Aufrufer erfaehrt Name, Version, Kanal, Commit-Kurzkennung, Bauzeit |
| Angemeldeter Nutzer -> Seitenleiste | Sieht Version, Kanal, Web-Commit und API-Version im Tooltip |
| Entwicklungsablauf (Hotfix) -> Datenbank der Beta | Ein Merge live -> main bringt Aenderungen in eine Umgebung mit moeglicherweise neueren Migrationen |
STRIDE Threat Register (ASVS Level 1, Blocking-Schwelle high)
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-KU1-01 | Information Disclosure | REGISTRY_TOKEN im CI-Log (ci.yml Login-Schritt, neues Skript) |
medium | mitigate | Login bleibt --password-stdin aus ${{ secrets.REGISTRY_TOKEN }} (Gitea maskiert Secrets im Log); das Skript kennt das Token nicht und gibt nur Versions-/Etikettenzeilen aus; Task 3 Schritt C4 verwendet das Push-Token nur in einer Shell-Variablen, nie in einer Ausgabe |
| T-KU1-02 | Information Disclosure | Web-Commit-Kurzkennung im Tooltip fuer angemeldete Nutzer (app-version-badge.tsx) |
low | accept | Repository ist privat (Gitea, Fetch nur mit Konto); ein 7-stelliger SHA ist ohne Repo-Zugang nicht verwertbar; die Beta-Versionszeichenkette v1.0.0-12-gabc1234 enthaelt ihn ohnehin; Nutzen fuer Fehlermeldungen ueberwiegt |
| T-KU1-03 | Information Disclosure | GET /health/version bleibt @Public() mit allen Feldern (health.controller.ts) |
low | accept | Bewusste Entscheidung, durch Spec-Test 6 gepinnt: Hauptzweck ist die Betreiber-Kontrolle per curl auf dem Server ohne Anmeldung (Handbuch Abschnitt 9); es werden keine Versionen von Systemkomponenten (Node, Nest, Postgres) preisgegeben — ASVS V14.3.3 zielt auf solche; Repo privat, Tessera laeuft hinter dem Nginx Proxy Manager fuer interne Nutzer. Wird Tessera spaeter extern verkauft, commit/buildTime hinter die Anmeldung ziehen (eine Zeile: @Public() entfernen, Abzeichen ruft ohnehin mit Cookie) |
| T-KU1-04 | Elevation of Privilege | Tag-Push v* als Freigabe-Hebel fuer Live (ci.yml, Skript) |
medium | accept | Gemessen: 0 Kollaborateure, nur schalli hat Schreibrecht, Claude ist einziger Committer (D-11); kein Fremdcode. Empfehlung in ci-cd-setup.md Abschnitt 5: bei weiteren Konten Tag-Schutz v* und Branch-Schutz live in Gitea. Gitea-Konfiguration liegt ausserhalb der Erlaubnisliste |
| T-KU1-05 | Tampering | Falscher Kanal auf einem Server (IMAGE_TAG fehlt/falsch, Live zieht Beta) |
medium | mitigate | Vorgabe beta ist fuer den bestehenden alpha-Server der richtige und fuer den Live-Server der auffaellige Fall (Seitenleiste zeigt · Beta, /health/version channel: beta); Handbuch nennt die Zeile je Server und die drei Kontrollwege; docker compose config-Gate beweist die Aufloesung beider Werte |
| T-KU1-06 | Tampering | Hotfix mit Migration bricht beim Merge live -> main die Beta-Datenbank (Prisma sortiert nach Zeitstempel) |
medium | mitigate | Regel „Keine Datenbankaenderung als Hotfix" im Handbuch mit Begruendung in Alltagssprache; Korrekturen mit Migration gehen nur als regulaere Version ueber main; Prisma-Schema und Migrationen in diesem Plan unangetastet |
| T-KU1-07 | Denial of Service | Ungetaggter Push auf live ueberschreibt das live-Etikett mit einem ungepruefte Stand |
medium | mitigate | Skript veroeffentlicht NUR fuer refs/heads/main und refs/tags/v*; jeder andere Ref endet mit „nichts zu tun" — durch --print-plan-Gate (Task 2, dritter Grep = 0) gepinnt |
| T-KU1-08 | Repudiation | Welcher Stand laeuft, ist ohne Stempel nicht nachvollziehbar (heute immer 0.0.1) |
low | mitigate | Genau der Gegenstand des Plans: Stempel im Abbild, in der Antwort, im Startlog und in der Oberflaeche; CI-Lauf nach dem Push beweist den echten Weg (Task 3 Schritt C4) |
| T-KU1-SC | Tampering | npm/pip/cargo installs | low | accept | Dieser Plan installiert KEIN Paket (keine neue Abhaengigkeit, pnpm-lock.yaml unangetastet — Gate in Task 3); Paketlegitimitaets-Gate nicht ausgeloest |
| </threat_model> |
6c19451 -- . ':!.planning'); echo GIT_EXIT=$?; tail -n1 <<< "$D"` -> `GIT_EXIT=0` und `20 files changed`
- `U=$(git diff --stat 6c19451 -- apps/api/prisma biome.json '.env*' apps/web/package.json apps/api/package.json pnpm-lock.yaml); echo U_EXIT=$?; test -z "$U"; echo U_EMPTY=$?` -> `U_EXIT=0` und `U_EMPTY=0`
- `GITHUB_REF=refs/heads/live sh .gitea/scripts/publish-images.sh --print-plan | grep -c "^push "` -> `0`; `GITHUB_REF=refs/tags/v1.2.3 sh .gitea/scripts/publish-images.sh --print-plan | grep -c "^push "` -> `4`
- `IMAGE_TAG=live docker compose -f docker-compose.prod.yml config --images 2>/dev/null | grep -c ':live$'` -> `2`
- `docker run --rm --entrypoint node localhost:3002/schalli/tessera-ctl/api:beta -e 'console.log(process.env.APP_VERSION, process.env.APP_CHANNEL)'` -> ` beta` (nach abgeschlossenem CI-Lauf)
- SUMMARY enthaelt: RED-Laeufe (Task 1), „Falsifizierung Bauzeit-Einbettung" mit sechs Docker-Ausgaben und Baudauer (Task 2), „CI-Lauf nach dem Push" mit Lauf-ID/Dauer/Stempel (Task 3), Nebenbefund „`sidebar-footer.tsx` ist seit ba02b25 toter Code" und den Hinweis, dass `.env.prod.example` bewusst nicht angefasst wurde (Regel `.env`-Dateien) — die `IMAGE_TAG`-Zeile steht nur im Handbuch.
- Human-Check (end-of-phase, nicht blockierend): lokal `docker compose up -d --build web api` und im Browser unten in der Seitenleiste `dev · Entwicklung` sehen, Tooltip `API dev (dev)`; eingeklappt verschwindet die Zeile.
<success_criteria>
- Versionsstempel fliesst CI -> Build-Args -> Abbilder ->
GET /health/version/ Startlog -> Seitenleiste; lokal ohne Args bleibt allesdev; die Bauzeit-Einbettung im Browser-Bundle ist mitv9.9.9-testbewiesen; der echte CI-Lauf nach dem Push hat:beta-Abbilder mit dem SHA des gepushten Commits gebaut. - Pipeline:
main->beta+latest; Tagv*->live+vX.Y.Z;liveohne Tag prueft nur;fetch-depth: 0; Entscheidung im Skript, lokal per--print-plangepinnt. docker-compose.prod.ymlmit${IMAGE_TAG:-beta}, beide Werte perconfig --imagesbewiesen; Registry-Host unangetastet.- Handbuch Abschnitt 9 in Alltagssprache mit echten Umlauten (Kanal,
.env-Zeile, Freigabe, Hotfix ohne Datenbankaenderung, Versionskontrolle, neuer Live-Server, Erstfreigabe-Rezept);ci-cd-setup.mdohne die veralteten Aussagen. - API 1060/65, Web 243/40,
tscdreimal 0, genau 20 Dateien ausserhalb.planning, drei Commits mit Scopequick-260914-ku1, gepusht;live-Zweig undv1.0.0NICHT angelegt. </success_criteria>