Files
tessera-ctl/.planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-SUMMARY.md
T
schalli 5c42c558c4
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 56s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m52s
docs(quick-260914-ku1): Kanaele und Versionsstempel abgeschlossen und verifiziert 8/8 — Zusammenfassung, Verifikation, Aktenstand
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-14 16:01:13 +02:00

362 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <version> (<channel>) <commit>`"
- "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<Version> · <Kanal>` 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 "<AppVersionBadge />" 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<Version> · <Kanal> 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 `<Kurzkennung> · 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`.