--- phase: quick-260914-ku1 plan: 01 subsystem: infra tags: [ci, gitea-actions, docker, build-args, next-public-env, nestjs, health, versionsstempel, compose, handbuch] status: complete requires: - phase: quick-260914-ku1 planning (1cd4212) provides: Plan mit Erlaubnisliste, gemessenen Bezugszahlen und Threat-Register provides: - "GET /health/version liefert { name, version, channel, commit, buildTime } aus APP_* (Vorgaben dev/dev), Startzeile `Tessera API () `" - "VersionResponse in packages/shared; apps/web/src/lib/app-version.ts als importierbare Quelle (appVersion + loadApiVersion) fuer Abzeichen und kommenden Fehler-melden-Knopf" - "AppVersionBadge unten in der Seitenleiste (Desktop und mobile Schublade, nicht eingeklappt) mit Tooltip Commit/API-Version" - "Beide Dockerfiles nehmen APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME als Build-Args; Web bettet NEXT_PUBLIC_APP_* zur Bauzeit ein" - ".gitea/scripts/publish-images.sh entscheidet Kanal/Etiketten aus GITHUB_REF (main -> beta+latest, v* -> live+vX.Y.Z, sonst nichts), --print-plan ohne Docker" - "ci.yml loest auf main, live und Tags v* aus; publish mit fetch-depth 0 und Skriptaufruf" - "docker-compose.prod.yml mit ${IMAGE_TAG:-beta} fuer web und api" - "Betriebshandbuch Kapitel 9 (Kanaele, IMAGE_TAG je Server, Freigabe, Hotfix ohne Datenbankaenderung, Versionskontrolle, neuer Live-Server); ci-cd-setup auf gemessenen Stand" affects: [fehler-melden-knopf, erstfreigabe-v1.0.0, live-server-einrichtung, deploy] actuals: tokens: 41545 tasks: 3 commits: 3 plan_head_before: 1cd4212df08cb0910e6c5c02abf6926534951935 tech-stack: added: [] patterns: - "Versionsstempel-Kette: CI-Skript -> --build-arg -> globales ARG + ARG-Wiederholung je Stufe -> ENV (runner) bzw. NEXT_PUBLIC_* vor pnpm build (web-builder)" - "Kanalentscheidung im POSIX-Skript statt in Workflow-if-Ausdruecken, lokal per --print-plan pruefbar" - "process.env.NEXT_PUBLIC_* nur mit vollem Literalnamen lesen (Bauzeit-Einbettung durch Next.js)" - "||-Vorgaben fuer Compose-Leerstring-Semantik (leer == ungesetzt)" key-files: created: - apps/api/src/health/app-version.ts - apps/api/src/health/health.controller.spec.ts - apps/web/src/lib/app-version.ts - apps/web/src/lib/app-version.test.ts - apps/web/src/components/layout/app-version-badge.tsx - apps/web/src/components/layout/app-version-badge.test.tsx - .gitea/scripts/publish-images.sh modified: - packages/shared/src/index.ts - apps/api/src/health/health.controller.ts - apps/api/src/main.ts - apps/web/src/components/layout/sidebar.tsx - apps/web/src/components/layout/sidebar.test.tsx - apps/web/src/messages/de.json - apps/web/src/messages/en.json - apps/web/Dockerfile - apps/api/Dockerfile - .gitea/workflows/ci.yml - docker-compose.prod.yml - docs/anleitung-betrieb.md - docs/ci-cd-setup.md key-decisions: - "Kanal ist die Primaerdarstellung (IMAGE_TAG, APP_CHANNEL); `latest` nur noch Alias von `beta`, damit alpha ohne Handgriff weiterlaeuft" - "Zweig `live` ohne Tag wird geprueft, aber nicht veroeffentlicht — nur ein Tag darf das live-Etikett belegen (T-KU1-07)" - "package.json-Versionen bleiben 0.0.1; die Wahrheit der Version ist der Git-Tag (git describe)" - "GET /health/version bleibt @Public (T-KU1-03), per Spec-Test gepinnt" - "apps/web spiegelt den Antworttyp lokal (ApiVersionInfo) statt @tessera/shared zu importieren — Lockfile und Docker-deps-Stufe bleiben unangetastet" - "Commits direkt auf main (Projektkonvention branching_strategy: none; das Kanalmodell setzt main = Beta voraus)" patterns-established: - "ARG-Sichtbarkeit in Multi-Stage-Dockerfiles: globales ARG mit Vorgabe + ARG NAME (ohne Wert) in jeder nutzenden Stufe" - "Memoisiertes Modul-Promise fuer einmalige API-Abfragen je Seitenladung, still bei Fehler" requirements-completed: [QUICK-260914-KU1] coverage: - id: D1 description: "GET /health/version aus APP_* mit Vorgaben, Compose-Leerstring-Semantik, Startzeile, @Public gepinnt" requirement: QUICK-260914-KU1 verification: - kind: unit ref: "apps/api/src/health/health.controller.spec.ts (6 Tests)" status: pass - kind: integration ref: "docker run tessera-ku1-api:args node -e formatAppVersionLine() -> Tessera API v9.9.9-test (live) abc1234" status: pass - id: D2 description: "Web-Quelle app-version.ts (appVersion, loadApiVersion memoisiert/still) und AppVersionBadge in der Seitenleiste" requirement: QUICK-260914-KU1 verification: - kind: unit ref: "apps/web/src/lib/app-version.test.ts (5), app-version-badge.test.tsx (4), sidebar.test.tsx#renders the version badge below the navigation" status: pass - kind: integration ref: "grep -rl v9.9.9-test /app/apps/web/.next/static | wc -l -> 1 (Bauzeit-Einbettung)" status: pass - id: D3 description: "CI-Trigger je Kanal, publish-images.sh, Build-Args in beiden Dockerfiles, IMAGE_TAG in Compose" requirement: QUICK-260914-KU1 verification: - kind: automated_ui ref: "js-yaml-Strukturpruefung, --print-plan in drei Lagen, docker compose config --images mit/ohne IMAGE_TAG" status: pass - kind: e2e ref: "Gitea-Actions-Lauf 297 (success) und CI-gebaute :beta-Abbilder mit APP_VERSION=ea6aa99 APP_CHANNEL=beta" status: pass - id: D4 description: "Betriebshandbuch Kapitel 9 und ci-cd-setup auf gemessenen Stand" requirement: QUICK-260914-KU1 verification: - kind: other ref: "grep-Gates (Kapitelueberschrift 1, IMAGE_TAG 11, Hotfix-Regel 1, veraltete Aussagen 0, Skriptname 4, Umlaute in ci-cd-setup 0)" status: pass metrics: duration: "22 min (13:28Z bis 13:50Z, davon ca. 7,5 min lokale Docker-Bauten und 5,3 min CI-Lauf)" completed: "2026-09-14" --- # Quick 260914-ku1 Plan 01: Zwei Auslieferungskanaele (Beta auf main, Live per Tag) mit Versionsstempel durch alle Schichten — Summary Versionsstempel `APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME` fliesst vom CI-Skript ueber Build-Args in beide Abbilder, aus `GET /health/version` und dem Startlog der API und als `v · ` unten in der Seitenleiste; `main` veroeffentlicht `beta`+`latest`, ein Tag `v*` veroeffentlicht `live`+`vX.Y.Z`; `docker-compose.prod.yml` waehlt den Kanal ueber `${IMAGE_TAG:-beta}`; das Betriebshandbuch erklaert Kanaele, Freigabe, Hotfix (ohne Datenbankaenderung) und den neuen Live-Server. Der echte CI-Weg ist einmal durchlaufen: Lauf 297 hat `:beta`-Abbilder mit `ea6aa99 beta` gebaut. ## Ausgangslage und Bezugspunkt Alle Gates gegen `6c19451` (Code unangetastet seit Planung; HEAD bei Start `1cd4212`, Arbeitsbaum sauber, `main == origin/main`). Baseline vor jeder Aenderung (erneut gemessen, identisch mit der Planung): | Suite | Befehl | Ergebnis | |---|---|---| | API | `pnpm -C apps/api exec vitest run` | `Test Files 64 passed (64)` / `Tests 1054 passed (1054)`, Exit 0 | | Web | `pnpm -C apps/web exec vitest run` | `Test Files 38 passed (38)` / `Tests 233 passed (233)`, Exit 0 | ## Task 1 — Versionsstempel durch alle Schichten (TDD) ### RED-Lauf (Schritt A, vor jedem Produktionscode) `pnpm -C apps/api exec vitest run src/health/health.controller.spec.ts` (Exit 1): ``` Error: Cannot find module './app-version' imported from '.../apps/api/src/health/health.controller.spec.ts' Test Files 1 failed (1) Tests no 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` (Exit 1): ``` ❯ src/components/layout/app-version-badge.test.tsx (0 test) Failed to resolve import "./app-version-badge" ❯ src/lib/app-version.test.ts (0 test) Failed to resolve import "./app-version" ❯ src/components/layout/sidebar.test.tsx (6 tests | 1 failed) × renders the version badge below the navigation Unable to find an element by: [data-testid="app-version-badge"] Test Files 3 failed (3) Tests 1 failed | 5 passed (6) ``` ### GREEN und Gate (Schritt B/C) Verify-Block von Task 1, gemessen: | Pruefung | Erwartet | Gemessen | |---|---|---| | API-Spec `health.controller.spec.ts` | `Tests 6 passed (6)` | `Tests 6 passed (6)` | | Web drei Specs | `Test Files 3 passed (3)` / `Tests 15 passed (15)` | `Test Files 3 passed (3)` / `Tests 15 passed (15)` | | Plan-Checker-Zusatz: obige drei + `umlaut-guard.spec.ts` + `tenderRadar-parity.spec.ts` in EINEM Aufruf | gruen | `Test Files 5 passed (5)` / `Tests 21 passed (21)` | | `grep -c npm_package_version health.controller.ts` | 0 | 0 | | `grep -c "formatAppVersionLine()" main.ts` | 1 | 1 | | `grep -c process.env.NEXT_PUBLIC_APP_VERSION app-version.ts` | 1 | 1 | | `grep -c "" sidebar.tsx` | 1 | 1 | | Node-Zeile Uebersetzungen | `Beta Live Entwicklung \| Development` | `Beta Live Entwicklung \| Development` | | `tsc --noEmit` shared / api / web | 0 / 0 / 0 | 0 / 0 / 0 | | API-Suite voll | 65 / 1060 | `Test Files 65 passed (65)` / `Tests 1060 passed (1060)` | | Web-Suite voll | 40 / 243 | `Test Files 40 passed (40)` / `Tests 243 passed (243)` | Commit `cdb571c` — `git show --stat HEAD` zeigt 13 Dateien (471+/8-). ## Task 2 — Build-Args, Veroeffentlichungs-Skript, CI-Trigger, IMAGE_TAG ### Gates ohne Docker (Schritt F), gemessen ``` branches=["main","live"] tags=["v*"] jobs=quality,test,publish fetchDepth=0 script=true needs=test main_plan=4 tag_plan=4 live_plan=0 stamp=1 compose_beta=2 compose_live=2 latest-in-compose=0 ARG APP_VERSION=dev: api 1 / web 1; NEXT_PUBLIC_APP_VERSION=$APP_VERSION: 1; ENV APP_VERSION=$APP_VERSION: web 1 / api 1; EXEC=0 git describe --tags --always -> cdb571c (kein Tag vorhanden, Bau vor dem ersten Tag scheitert nicht) ``` `--print-plan` in den drei Lagen (woertlich): ``` GITHUB_REF=refs/heads/main: Tessera cdb571c (beta) cdb571c 2026-09-14T13:33:40Z -> Etiketten: beta latest push localhost:3002/schalli/tessera-ctl/web:beta push localhost:3002/schalli/tessera-ctl/web:latest push localhost:3002/schalli/tessera-ctl/api:beta push localhost:3002/schalli/tessera-ctl/api:latest GITHUB_REF=refs/tags/v1.2.3: Tessera cdb571c (live) cdb571c 2026-09-14T13:33:40Z -> Etiketten: live v1.2.3 push localhost:3002/schalli/tessera-ctl/web:live push localhost:3002/schalli/tessera-ctl/web:v1.2.3 push localhost:3002/schalli/tessera-ctl/api:live push localhost:3002/schalli/tessera-ctl/api:v1.2.3 GITHUB_REF=refs/heads/live: Kein Veroeffentlichungs-Anlass fuer 'refs/heads/live' (nur main und Tags v*): nichts zu tun. ``` `docker compose -f docker-compose.prod.yml config --images` ohne Variable: `web:beta`, `api:beta`, `postgres:16-alpine`; mit `IMAGE_TAG=live`: `api:live`, `postgres:16-alpine`, `web:live`. ### Falsifizierung Bauzeit-Einbettung (Schritt E) Baudauer (warmer deps-Cache): api:args 111 s, web:args 129 s, api:noargs 113 s, web:noargs 99 s — zusammen 7 min 32 s, alle Exit 0. | # | Befehl (Kurzform) | Erwartet | Gemessen | |---|---|---|---| | 1 | `api:args` node `APP_VERSION APP_CHANNEL APP_COMMIT APP_BUILD_TIME` | `v9.9.9-test live abc1234 2026-09-14T00:00:00Z` | `v9.9.9-test live abc1234 2026-09-14T00:00:00Z` | | 2 | `api:args` node `require("/app/apps/api/dist/health/app-version").formatAppVersionLine()` | `Tessera API v9.9.9-test (live) abc1234` | `Tessera API v9.9.9-test (live) abc1234` | | 3 | `web:args` sh `grep -rl v9.9.9-test /app/apps/web/.next/static \| wc -l` | >= 1 | `1` | | 4 | `web:args` node `APP_VERSION` | `v9.9.9-test` | `v9.9.9-test` | | 5 | `api:noargs` node `APP_VERSION APP_CHANNEL` / `web:noargs` node `APP_VERSION` | `dev dev` / `dev` | `dev dev` / `dev` | | 6 | `web:noargs` Bundle-Grep auf `v9.9.9-test` | `0` | `0` | | 6b | `api:noargs` dist-Zeile (Zusatz) | `Tessera API dev (dev)` | `Tessera API dev (dev)` | Aufgeraeumt: `docker rmi` der vier `tessera-ku1-*`-Etiketten und `tessera-web-plancheck:baseline` (alle „Untagged", `docker images` zeigt keine mehr). Commit `9731501` — 5 Dateien (115+/13-). ## Task 3 — Handbuch, ci-cd-setup, Push, CI-Beobachtung Precondition: `curl localhost:3002/api/v1/version` -> `{"version":"1.26.2"}`; `docker ps | grep -c ^gitea-runner$` -> 1. Gates, gemessen: | Pruefung | Erwartet | Gemessen | |---|---|---| | `grep -c "^## 9. Zwei Kanäle: Live und Beta"` | 1 | 1 | | `grep -c IMAGE_TAG anleitung-betrieb.md` | >= 6 | 11 | | `grep -c "Keine Datenbankänderung als Hotfix"` | 1 | 1 | | `grep -c "Kein Registry-Push\|build-deploy" ci-cd-setup.md` | 0 | 0 | | `grep -c publish-images.sh ci-cd-setup.md` | >= 2 | 4 | | `grep -c '[äöüÄÖÜß]' ci-cd-setup.md` | 0 | 0 | | `git diff --stat 6c19451 -- . ':!.planning'` | `GIT_EXIT=0`, `20 files changed` | `GIT_EXIT=0`, `20 files changed, 851 insertions(+), 49 deletions(-)` | | Unangetastet-Stichprobe (prisma, biome.json, .env*, package.json, lockfile) | `U_EXIT=0`, `U_EMPTY=0` | `U_EXIT=0`, `U_EMPTY=0` | | `git status -sb` nach Push | kein `[ahead` | `## main...origin/main` | Commit `ea6aa99` — 2 Dateien (265+/28-). `git push` -> `6c19451..ea6aa99 main -> main`. ### CI-Lauf nach dem Push | Feld | Wert | |---|---| | Lauf-ID | 297 (event `push`, ref `main`, `head_sha` = `ea6aa99…`) | | status / conclusion | `completed` / `success` | | started_at / completed_at | 2026-09-14T15:44:23+02:00 / 2026-09-14T15:49:41+02:00 — **5 min 18 s** (Planung: 4-6 min mit Build-Args) | | `api:beta` node `APP_VERSION APP_CHANNEL APP_COMMIT APP_BUILD_TIME` | `ea6aa99 beta ea6aa99 2026-09-14T13:46:12Z` (`git rev-parse --short ea6aa99` = `ea6aa99`) | | `web:beta` node `APP_VERSION APP_CHANNEL` | `ea6aa99 beta` | | `web:beta` Bundle-Grep auf `ea6aa99` | `1` (Bauzeit-Einbettung ueber den echten CI-Weg) | | `docker image inspect Created` web:beta / web:latest | beide `2026-09-14T15:47:54.520283239+02:00` (ein Bau, zwei Etiketten, nach dem Push) | | `docker image inspect Created` api:beta / api:latest | beide `2026-09-14T15:48:40.191081238+02:00` | | Registry (Gitea-API `/packages/schalli?type=container`) | `tessera-ctl/web` `beta` 15:47:59, `latest` 15:48:00; `tessera-ctl/api` `beta` und `latest` 15:49:34 | Kein Fehlversuch, ein einziger Push, ein einziger Lauf. ## Abschluss-Verifikation (nach Task 3) - API `Test Files 65 passed (65)` / `Tests 1060 passed (1060)`, Exit 0 - Web `Test Files 40 passed (40)` / `Tests 243 passed (243)`, Exit 0 - `tsc --noEmit`: `TSC_packages/shared=0`, `TSC_apps/api=0`, `TSC_apps/web=0` - `git fetch -q && git status -sb | head -1` -> `## main...origin/main` - `git ls-remote --heads origin live | wc -l` -> 0, `git tag | wc -l` -> 0 (Zweig `live` und Tag `v1.0.0` wie geplant NICHT angelegt) `git status --porcelain` (vor dem Schreiben dieses SUMMARY): leer. `git log --oneline 1cd4212..HEAD`: ``` ea6aa99 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 9731501 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 cdb571c feat(quick-260914-ku1): Versionsstempel — GET /health/version aus APP_*, VersionResponse, app-version.ts und Abzeichen v · in der Seitenleiste ``` `commits: 3` gemessen aus `git rev-list --count 1cd4212..HEAD`; `actuals.tokens` = 166183 Zeichen ueber die 20 geaenderten Dateien / 4 = 41545 (der reine Diff waere 49515 Zeichen = 12378). ## Deviations from Plan ### Auto-fixed Issues None — plan executed exactly as written. Alle Zahlen des Plans (65/1060, 40/243, 20 Dateien, 13/5/2 Dateien je Commit, Grep-Werte) wurden exakt getroffen; keine Erwartung musste angepasst werden. ### Prozess-Abweichung (dokumentiert, keine Code-Abweichung) **Commits auf `main`.** Die Executor-Vorschrift verlangt eigentlich einen Nicht-Standard-Zweig. Dieses Projekt arbeitet per `branching_strategy: none` seit jeher direkt auf `main`, der Auftrag verlangt ausdruecklich `git push` auf `main`, und das Kanalmodell dieses Plans definiert `main` = Beta — ein Seitenzweig haette den Plan nicht erfuellen koennen (die Pipeline haette nichts gebaut). Kein `git update-ref`, kein Force-Push, kein Eingriff in `.planning/config.json`. ## Nebenbefunde - **`apps/web/src/components/layout/sidebar-footer.tsx` ist seit `ba02b25` (2026-06-26) toter Code**: wird nirgends gerendert, einziger Treffer ausserhalb der Datei ist der Mock in `sidebar.test.tsx`. Unangetastet gelassen (nicht in der Erlaubnisliste); Kandidat fuer einen Aufraeum-Quick-Task. - **`.env.prod.example` bewusst nicht angefasst** (Regel: keine `.env*`-Dateien). Die Zeile `IMAGE_TAG` steht nur im Handbuch (Kapitel 3 Tabelle, Kapitel 9 mit ausdruecklichem Hinweis, dass die Vorlage sie noch nicht enthaelt). - Der Web-Bau laeuft in der CI jetzt bei jedem Lauf durch die builder-Stufe (NEXT_PUBLIC_APP_COMMIT aendert sich je Commit) — gemessen 5 min 18 s statt unter 2 min zuvor; im ci-cd-setup vermerkt. - Handbuch Kapitel 6/7 nennen weiterhin `/opt/tessera/docker-compose.yml` fuer die Volume-Reparatur; Kapitel 9 nennt die tatsaechlich benutzte Datei `/opt/tessera/docker-compose.prod.yml` (`.env` setzt `COMPOSE_FILE`). Die aelteren Stellen wurden nicht umgeschrieben (nicht Teil des Plans). ## Threat Flags Keine neue Angriffsflaeche ausserhalb des Threat-Registers des Plans: `GET /health/version` war bereits @Public (T-KU1-03, akzeptiert und per Spec-Test 6 gepinnt); das Skript kennt kein Secret; das Push-Token wurde in Task 3 nur in einer Shell-Variablen benutzt (Ausgabe der Push-URL im Log mit `***` maskiert). ## Known Stubs Keine. Alle neuen Werte sind an echte Quellen gebunden (Umgebung, `/health/version`); ohne Build-Args greifen die bewussten Vorgaben `dev`. ## Was bewusst offen bleibt - **Zweig `live` und Tag `v1.0.0` sind NICHT angelegt** — Rezept steht im Handbuch Kapitel 9 („Erstfreigabe v1.0.0"); erfolgt nach dem Fehler-melden-Knopf (eigener Quick-Task, der `apps/web/src/lib/app-version.ts` importiert). - **Server nicht angefasst**: alpha (`/opt/tessera/.env` und `docker-compose.prod.yml`) und der neue Live-Server werden vom User eingerichtet, siehe „Handgriffe" unten. Bis dahin zieht alpha weiter `:latest` = dasselbe Beta-Abbild. - `.env.prod.example` ohne `IMAGE_TAG`-Zeile (Regel `.env*`); ein spaeterer Quick-Task darf sie ergaenzen. - `latest` bleibt als Alias von `beta`, bis alpha auf `IMAGE_TAG=beta` umgestellt ist; danach kann das Etikett aus dem Skript entfallen. - Tag-Schutz `v*` und Branch-Schutz `live` in Gitea (T-KU1-04) — heute nicht noetig (nur `schalli` hat Schreibrecht), im ci-cd-setup als Empfehlung fuer den Fall weiterer Konten. - Human-Check (end-of-phase, nicht blockierend): lokal `docker compose up -d --build web api` und unten in der Seitenleiste `dev · Entwicklung` sehen, Tooltip `API dev (dev)`; eingeklappt verschwindet die Zeile. ## Handgriffe fuer den User Aus dem Handbuch Kapitel 9 („Die eine Zeile je Server" und „Den neuen Live-Server einrichten"): **Auf alpha (Beta), einmalig:** 1. In `/opt/tessera/.env` die Zeile `IMAGE_TAG=beta` eintragen. 2. Sicherung der Compose-Datei: ```bash cd /opt/tessera cp docker-compose.prod.yml docker-compose.prod.yml.bak.$(date +%Y%m%d) ``` 3. In `/opt/tessera/docker-compose.prod.yml` die zwei `image:`-Zeilen aendern (nur das Ende der Zeile): ```yaml image: git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta} image: git.vicolab.de/schalli/tessera-ctl/api:${IMAGE_TAG:-beta} ``` 4. Dann wie in Kapitel 4: ```bash docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d --force-recreate api web ``` 5. Kontrolle: unten in der Seitenleiste steht ` · Beta`; `curl -s http://localhost:3001/health/version` zeigt `"channel":"beta"`. **Auf dem neuen Live-Server (tessera.ctl.de):** Kapitel 2 des Handbuchs vollstaendig, mit diesen Abweichungen: - `IMAGE_TAG=live` in der `.env` (Pflicht — ohne die Zeile zieht der Server die Beta). - Eigene, neu erzeugte Geheimnisse (`JWT_SECRET`, `TESSERA_ENCRYPTION_KEY`, `DB_PASSWORD`, Admin-Passwort); nichts von alpha uebernehmen. - Eigene, leere Datenbank; die alpha-Datenbank wird NICHT kopiert (falls doch gewuenscht: Kapitel 6 UND derselbe `TESSERA_ENCRYPTION_KEY`). - `APP_URL=https://tessera.ctl.de`. - Der erste `pull` holt `:live` — dieses Etikett gibt es erst nach der Erstfreigabe v1.0.0. Also erst freigeben (Claude: `git checkout -b live main`, Tag `v1.0.0`, Push), dann installieren; oder fuer einen Probelauf voruebergehend `IMAGE_TAG=beta`, danach auf `live` umstellen und `pull` + `up -d --force-recreate api web` wiederholen. **Bei jeder Freigabe danach** (User sagt „Version X freigeben", Claude pusht Zweig und Tag), auf dem Live-Server: ```bash docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d --force-recreate api web ``` ## Self-Check: PASSED - Dateien vorhanden: alle 7 neu erstellten Dateien (`app-version.ts` api/web, beide Specs/Tests, Badge + Test, `publish-images.sh`) — FOUND. - Commits vorhanden: `cdb571c`, `9731501`, `ea6aa99` — FOUND (`git log --oneline 1cd4212..HEAD`), alle auf `origin/main`.