1cd4212df0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
324 lines
63 KiB
Markdown
324 lines
63 KiB
Markdown
---
|
|
phase: quick-260914-ku1
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
autonomous: true
|
|
requirements: [QUICK-260914-KU1]
|
|
|
|
files_modified:
|
|
- packages/shared/src/index.ts
|
|
- apps/api/src/health/app-version.ts
|
|
- apps/api/src/health/health.controller.ts
|
|
- apps/api/src/health/health.controller.spec.ts
|
|
- apps/api/src/main.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
|
|
- 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
|
|
- .gitea/scripts/publish-images.sh
|
|
- docker-compose.prod.yml
|
|
- docs/anleitung-betrieb.md
|
|
- docs/ci-cd-setup.md
|
|
|
|
estimate:
|
|
tokens: 110000
|
|
raw_tokens: 110000
|
|
tasks: 3
|
|
confidence: low
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Ein angemeldeter Anwender sieht unten in der Seitenleiste (ausgeklappt, Desktop und mobile Schublade) eine kleine Zeile `<Version> · <Kanal>` (z. B. `v1.0.0 · Beta`, lokal `dev · Entwicklung`); der Tooltip (`title`) nennt den Web-Commit und — sobald `GET /health/version` geantwortet hat — die API-Version samt Kanal. Ein Fehler beim Laden der API-Version ist still (Zeile bleibt, Tooltip ohne API-Teil). Eingeklappt: nichts (konsistent mit dem heutigen `!isCollapsed`-Muster der Seitenleiste)."
|
|
- "`GET /health/version` liefert `{ name: 'tessera', version, channel, commit, buildTime }` aus `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILD_TIME` mit Vorgaben `dev`/`dev`/``/`` — leere Zeichenketten (Compose reicht unbelegte Variablen leer weiter) zaehlen wie ungesetzt. Beim Start der API steht eine Protokollzeile `Tessera API <version> (<channel>) <commit>`. Der Endpunkt bleibt bewusst @Public (T-KU1-03), durch Spec-Test gepinnt."
|
|
- "Ein lokaler `docker build` beider Dockerfiles MIT `--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` liefert Abbilder, in denen (a) `node -e 'console.log(process.env.APP_VERSION)'` `v9.9.9-test` ausgibt, (b) im Web-Abbild mindestens eine Datei unter `/app/apps/web/.next/static` die Zeichenkette `v9.9.9-test` enthaelt (Bauzeit-Einbettung durch Next.js bewiesen) und (c) `formatAppVersionLine()` aus dem kompilierten API-`dist` `Tessera API v9.9.9-test (live) abc1234` liefert. OHNE Build-Args baut alles weiter und liefert `dev`."
|
|
- "`.gitea/workflows/ci.yml` (per js-yaml geparst) loest auf `push` fuer `branches: [main, live]` UND `tags: ['v*']` aus; `quality` und `test` laufen fuer alle; `publish` checkt mit `fetch-depth: 0` aus und ruft `.gitea/scripts/publish-images.sh`. Das Skript entscheidet allein anhand `GITHUB_REF`: `refs/heads/main` -> Kanal `beta`, Etiketten `beta` und `latest`; `refs/tags/v*` -> Kanal `live`, Etiketten `live` und `vX.Y.Z`; jeder andere Ref (auch der Zweig `live` ohne Tag) -> `nichts zu tun`, Exit 0, kein Push. `--print-plan` zeigt das ohne Docker-Aufruf. Versionsstempel: `git describe --tags --always` (heute ohne Tags: kurzer SHA), `git rev-parse --short HEAD`, `date -u` ISO."
|
|
- "`docker-compose.prod.yml` bleibt EINE Datei; beide Abbild-Zeilen tragen `${IMAGE_TAG:-beta}`; `docker compose -f docker-compose.prod.yml config --images` rendert ohne Variable zweimal `:beta`, mit `IMAGE_TAG=live` zweimal `:live`. Registry-Host `git.vicolab.de` und alles andere in der Datei unangetastet."
|
|
- "Nach `git push` laeuft die Pipeline auf dem lokalen Gitea (localhost:3002, Runner `gitea-runner` mit Host-Docker-Socket) sichtbar durch: der Lauf zum gepushten Commit endet `completed`/`success`, und die vom Runner auf DIESEM Host gebauten Abbilder `localhost:3002/schalli/tessera-ctl/{api,web}:beta` tragen `APP_VERSION` = kurzer SHA des gepushten Commits und `APP_CHANNEL=beta` — der Versionsstempel ist damit einmal ueber den echten CI-Weg bewiesen, nicht nur lokal."
|
|
- "`docs/anleitung-betrieb.md` hat einen neuen Abschnitt `## 9. Zwei Kanäle: Live und Beta` in Alltagssprache mit echten Umlauten (Ton der Datei), der erklaert: was ein Kanal ist; welche Adresse welches Etikett holt (`latest` = Beta, bleibt vorerst); die eine `.env`-Zeile je Server (`IMAGE_TAG=beta` auf alpha, `IMAGE_TAG=live` auf dem neuen Server) und die zwei `image:`-Zeilen in der Server-Compose-Datei; Freigabe einer Version (Schritte, die Claude ausfuehrt, und `pull` + `up -d --force-recreate api web` durch den User); Hotfix-Ablauf inkl. der Regel 'keine Datenbankänderung als Hotfix' mit Begruendung; wie man die Version in der Oberflaeche, per `curl` und im Log erkennt; Einrichtung des neuen Live-Servers (Verweis auf Abschnitt 2 plus Abweichungen: `IMAGE_TAG=live`, eigene Secrets, eigene Datenbank, KEINE Kopie der alpha-Datenbank ohne ausdruecklichen Wunsch); Rezept fuer die Erstfreigabe v1.0.0. `docs/ci-cd-setup.md` behauptet nicht mehr, es gebe keinen Registry-Push oder einen `build-deploy`-Job, sondern beschreibt Trigger, Etiketten, Build-Args und die laengere Laufzeit."
|
|
- "Baseline am Ende: API `Test Files 65 passed (65)` / `Tests 1060 passed (1060)` (Planungszeit 64/1054 plus 6 neue), Web `Test Files 40 passed (40)` / `Tests 243 passed (243)` (Planungszeit 38/233 plus 5 + 4 + 1 neue), `tsc --noEmit` in api, web und shared Exit 0; `git diff --stat 6c19451 -- . ':!.planning'` nennt genau `20 files changed`; DATABASE_URL, `.env`-Dateien, `schema.prisma`, Migrationen, `biome.json`, `package.json`-Versionen unangetastet."
|
|
artifacts:
|
|
- "packages/shared/src/index.ts — `export interface VersionResponse { name: string; version: string; channel: string; commit: string; buildTime: string }` neben `HealthResponse`"
|
|
- "apps/api/src/health/app-version.ts — `getAppVersion(): VersionResponse` (liest `process.env.APP_*` mit `||`-Vorgaben) und `formatAppVersionLine(v?: VersionResponse): string`"
|
|
- "apps/api/src/health/health.controller.ts — `getVersion(): VersionResponse` delegiert an `getAppVersion()`, weiterhin `@Public()`; kein Zugriff mehr auf die npm-Paketversion"
|
|
- "apps/api/src/health/health.controller.spec.ts — NEU (es gab keinen Spec), 6 Tests"
|
|
- "apps/api/src/main.ts — `console.log(formatAppVersionLine())` direkt nach der bestehenden Port-Zeile"
|
|
- "apps/web/src/lib/app-version.ts — `appVersion: { version, channel: 'beta'|'live'|'dev', commit }` aus `process.env.NEXT_PUBLIC_APP_VERSION` / `_CHANNEL` / `_COMMIT` (jeweils voller Literalname) und `loadApiVersion(): Promise<ApiVersionInfo | null>` (memoisiert, `credentials: 'include'`, still bei Fehler) — die importierbare Quelle fuer den kommenden Fehler-melden-Knopf"
|
|
- "apps/web/src/lib/app-version.test.ts — NEU, 5 Tests (vi.stubEnv + vi.resetModules + dynamischer Import)"
|
|
- "apps/web/src/components/layout/app-version-badge.tsx — `AppVersionBadge`, `data-testid=\"app-version\"`, Text `${version} · ${t('channel.'+channel)}`, `title` aus Commit und API-Version"
|
|
- "apps/web/src/components/layout/app-version-badge.test.tsx — NEU, 4 Tests"
|
|
- "apps/web/src/components/layout/sidebar.tsx — Abzeichen-Block unter dem Einklapp-Block, nur `!isCollapsed`, ohne `hidden md:block` (damit auch die mobile Schublade ihn zeigt)"
|
|
- "apps/web/src/components/layout/sidebar.test.tsx — Mock fuer `@/components/layout/app-version-badge` (wie der bestehende SidebarFooter-Mock) und ein sechster Test"
|
|
- "apps/web/src/messages/de.json + en.json — `sidebar.channel.{beta,live,dev}` = Beta/Live/Entwicklung bzw. Beta/Live/Development"
|
|
- "apps/web/Dockerfile + apps/api/Dockerfile — globale `ARG APP_VERSION=dev`, `ARG APP_CHANNEL=dev`, `ARG APP_COMMIT=`, `ARG APP_BUILD_TIME=` vor dem ersten FROM; im builder (nur web) `ARG`-Wiederholung + `ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION NEXT_PUBLIC_APP_CHANNEL=$APP_CHANNEL NEXT_PUBLIC_APP_COMMIT=$APP_COMMIT` unmittelbar VOR `RUN pnpm --filter=@tessera/web build`; im runner (beide) `ARG`-Wiederholung + `ENV APP_VERSION=$APP_VERSION APP_CHANNEL=$APP_CHANNEL APP_COMMIT=$APP_COMMIT APP_BUILD_TIME=$APP_BUILD_TIME`"
|
|
- ".gitea/scripts/publish-images.sh — POSIX sh, `set -eu`, Kanal-/Etiketten-Entscheidung, `--print-plan`, Bau mit vier `--build-arg`, `docker tag` + `docker push` je Etikett; gibt nie ein Secret aus"
|
|
- ".gitea/workflows/ci.yml — Trigger erweitert, `publish` mit `fetch-depth: 0` und Skriptaufruf; Login-Schritt unveraendert"
|
|
- "docker-compose.prod.yml — `image: git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta}` und `.../api:${IMAGE_TAG:-beta}`"
|
|
- "docs/anleitung-betrieb.md — Abschnitt 9 neu, Inhaltsverzeichnis, Tabelle in Abschnitt 1, `IMAGE_TAG`-Zeile in Abschnitt 3, Etiketten in Abschnitt 4, Log-Zeile in Abschnitt 7"
|
|
- "docs/ci-cd-setup.md — Abschnitte 3 (Secrets: REGISTRY_TOKEN) und 4 (Pipeline-Ueberblick) auf den gemessenen Stand; ASCII-Umschrift wie im Bestand"
|
|
key_links:
|
|
- "Bauzeit-Einbettung: `ENV NEXT_PUBLIC_APP_*` steht im builder VOR `pnpm build`, und `app-version.ts` liest jede Variable mit vollem Literalnamen — nur dann ersetzt Next.js den Ausdruck im Browser-Bundle (heute nachweisbar: 28 Dateien unter `.next/static` enthalten das eingebettete `/api-proxy`). Deshalb muss Task 1 (Code) VOR Task 2 (Build-Beweis) liegen: ohne die Quelle gibt es nichts einzubetten, der Grep auf `v9.9.9-test` waere sinnlos."
|
|
- "`.dockerignore` schliesst `.git` aus — `git describe` kann NICHT im Dockerfile laufen; der Stempel kommt ausschliesslich per `--build-arg` aus der CI (Skript), lokal greift die Vorgabe `dev`."
|
|
- "ARG-Sichtbarkeit: ein `ARG` vor dem ersten FROM liefert nur die Vorgabe; jede Stufe, die den Wert nutzt, wiederholt `ARG NAME` (ohne Wert) — zur Planungszeit mit einem Zweistufen-Testbau bestaetigt (`builder sees: v9.9.9-test / live`, `runner: v9.9.9-test live`)."
|
|
- "Runner und Gitea laufen auf DIESEM Rechner (`gitea`, `gitea-runner` mit Host-Docker-Socket, `localhost:3002` antwortet): der CI-Lauf ist nach dem Push ueber `GET /api/v1/repos/schalli/tessera-ctl/actions/runs` (Token aus `git config --get remote.origin.pushurl`, nie ausgeben) beobachtbar, und die CI-Abbilder erscheinen in `docker images` des Hosts."
|
|
- "Der Web-Container ruft `${NEXT_PUBLIC_API_URL}/health/version` = `/api-proxy/health/version`; `next.config.ts` schreibt `/api-proxy/:path*` auf `API_INTERNAL_URL` um — derselbe Weg wie `/modules/active` in `sidebar.tsx`."
|
|
- "`sidebar.test.tsx` stubbt `fetch` global mit dem Modul-Array; ohne Mock des Abzeichens bekaeme `loadApiVersion()` dieses Array — deshalb Modul-Mock wie beim `SidebarFooter`."
|
|
---
|
|
|
|
<objective>
|
|
Zwei Auslieferungskanaele fuer Tessera: `main` = Beta (alpha.tessera.ctl.de), Zweig `live` + Tag `vX.Y.Z` = Live (tessera.ctl.de, neuer Server ab 2026-09-15). Dieser Plan liefert (1) den Versionsstempel durch alle Schichten — CI berechnet `APP_VERSION`/`APP_CHANNEL`/`APP_COMMIT`/`APP_BUILD_TIME`, beide Dockerfiles nehmen sie als Build-Args, die API antwortet auf `GET /health/version` und protokolliert beim Start, die Web-Oberflaeche zeigt `v1.0.0 · Beta` unten in der Seitenleiste; (2) die Pipeline-Trigger und Etiketten je Kanal mit einem lokal pruefbaren Veroeffentlichungs-Skript; (3) `IMAGE_TAG` in der Compose-Datei; (4) das Betriebshandbuch fuer einen Nicht-Programmierer: Kanaele, Freigabe, Hotfix (ohne Datenbankaenderung), Versionskontrolle, Einrichtung des neuen Live-Servers.
|
|
|
|
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).
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/STATE.md
|
|
@.gitea/workflows/ci.yml
|
|
@apps/web/Dockerfile
|
|
@apps/api/Dockerfile
|
|
@docker-compose.prod.yml
|
|
@apps/api/src/health/health.controller.ts
|
|
@apps/api/src/main.ts
|
|
@packages/shared/src/index.ts
|
|
@apps/web/src/components/layout/sidebar.tsx
|
|
@apps/web/src/components/layout/sidebar.test.tsx
|
|
@apps/web/src/messages/umlaut-guard.spec.ts
|
|
@docs/anleitung-betrieb.md
|
|
@docs/ci-cd-setup.md
|
|
|
|
<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-Suite `pnpm -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 --noEmit` in `apps/api`, `apps/web`, `packages/shared` je Exit 0.
|
|
- **`SidebarFooter` (`sidebar-footer.tsx`) wird seit `ba02b25` (2026-06-26, „restructure sidebar and admin navigation") NIRGENDS gerendert** — einziger Treffer ausserhalb der Datei ist der Mock in `sidebar.test.tsx`. Die Benutzerinfo lebt im Header-Dropdown (`header.tsx` 134-154), die Seitenleiste endet mit dem Einklapp-Block (`sidebar.tsx` 187-199, `hidden md:block border-t border-sidebar-border p-2`; `!isCollapsed` blendet dort und in der Navigation jeden Text aus). Eine Versionszeile in `sidebar-footer.tsx` waere unsichtbar. Deshalb: eigene Komponente `AppVersionBadge`, gerendert in `sidebar.tsx` im `sidebarContent` (Zeile 91 `<div className="flex h-full flex-col bg-sidebar">`, Ende bei 201) NACH dem Einklapp-Block; `sidebarContent` wird auch in der mobilen Schublade (Zeile 245) gerendert. `sidebar-footer.tsx` bleibt unangetastet (toter Code, im SUMMARY als Nebenbefund nennen, nicht loeschen).
|
|
- **Ohne Quellcode, der `process.env.NEXT_PUBLIC_APP_VERSION` liest, bettet Next.js nichts ein** — der Grep auf `v9.9.9-test` im Web-Abbild funktioniert erst, wenn `app-version.ts` existiert. 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:baseline` existiert (Cache-Waerme), darf am Ende mit `docker rmi` weg.
|
|
- ARG-Semantik bestaetigt (Zweistufen-Testbau im Scratchpad): globale `ARG X=dev` vor dem ersten FROM + `ARG X` in jeder nutzenden Stufe -> ohne Args `dev`, mit `--build-arg` der Wert in builder UND runner.
|
|
- `.dockerignore`: `node_modules .next dist .turbo .git .env *.md coverage` — `.git` fehlt im Kontext, `git describe` im Dockerfile unmoeglich.
|
|
- `git tag` liefert keine Zeile (0 Tags); `git describe --tags --always` -> `6c19451` (kurzer SHA, Gate „Bau vor dem ersten Tag scheitert nicht" erfuellt). Nur Zweig `main` lokal und remote. Gitea 1.26.2 auf localhost:3002; `branch_protections` leer; 0 Kollaborateure (nur schalli). Push-URL `localhost:3002`, Fetch-URL `git.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 in `docker images` des Hosts (`localhost:3002/schalli/tessera-ctl/api:latest` wurde vom letzten Lauf 296 zu HEAD `6c19451` gebaut; 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 per `GET http://localhost:3002/api/v1/repos/schalli/tessera-ctl/actions/runs?limit=3` mit `Authorization: token <Token aus pushurl>` lesbar (Felder `head_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/null` laeuft lokal mit Exit 0 (die lokale `.env` liefert den Pflichtwert `TESSERA_ENCRYPTION_KEY`) und rendert heute `web:latest` / `api:latest`. `IMAGE_TAG` kommt in `.env.example` und `.env.prod.example` nicht vor.
|
|
- Server alpha (nur gelesen, `ls`/`grep` per SSH): `/opt/tessera/.env` traegt `COMPOSE_FILE` (Zeile 32) und `APP_URL`; `/opt/tessera/docker-compose.prod.yml` (93 Zeilen) ist die Serverdatei mit `web:latest`/`api:latest` (Zeilen 3 und 23) und weicht vom Repo (94 Zeilen) genau um die fehlende Zeile `TESSERA_MIGRATE_DATABASE_URL` ab. Der aeltere Hinweis in Handbuch Abschnitt 6/7 auf `/opt/tessera/docker-compose.yml` ist 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.0` liegt in `node_modules/.pnpm`, ist aber vom Repo-Root nicht per `require('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: parst `ci.yml`, `on.push.branches = ["main"]`, `tags = undefined`, Jobs `quality,test,publish`). Der Schluessel `on` wird als String geparst (YAML-1.2-Schema).
|
|
- `apps/web` haengt NICHT von `@tessera/shared` ab (nur `apps/api`); ein neuer Import wuerde `package.json` + `pnpm-lock.yaml` aendern. Deshalb spiegelt `app-version.ts` den Antworttyp lokal (`ApiVersionInfo`), `VersionResponse` bleibt in `packages/shared` die API-Wahrheit.
|
|
- Workspace-Paketversionen stehen NICHT in `pnpm-lock.yaml` (alle 16 Treffer auf `0.0.1` sind Fremdpakete) — ein Bump auf 1.0.0 waere lockfile-neutral, wuerde aber die deps-Stufe beider Dockerfiles (COPY `package.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_KEY` aus `../auth/decorators/public.decorator`); Metadaten-Pruefung per `Reflect.getMetadata` wie in `tenant.controller.spec.ts` 300-308.
|
|
- Web-Muster: `const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'` + `fetch(..., { credentials: 'include' })` (`sidebar.tsx` 12, 43-47; `favorites-api.ts`). `vi.resetModules()` + dynamischer Import: `module-access-gate.test.tsx` 37. Uebersetzungs-Mock: `sidebar.test.tsx` 16-41 (`useTranslations(ns)` -> `map[ns][key] ?? key`, verschachtelte Schluessel als `'categories.label'`).
|
|
- Uebersetzungs-Waechter: `umlaut-guard.spec.ts` prueft de.json auf `ae/oe/ue/ss`-Token ausserhalb `UMLAUT_ALLOWLIST` (Fehlermeldung nennt den Fix) und de/en-Schluesselgleichheit; `tenderRadar-parity.spec.ts` nur den Namensraum `tenderRadar`. „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 trotzdem `tdd="true"`, die Tests sind vorab formulierbar), `security_enforcement=true`, ASVS 1, Blocking-Schwelle `high`, `human_verify_mode=end-of-phase`.
|
|
- Biome ist im Bestand nicht lauffaehig (WINDOWS #35, `pnpm lint` = Leerlauf) — kein Biome-Gate in diesem Plan; `biome.json` unangetastet.
|
|
</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>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Versionsstempel durch alle Schichten — API /health/version, geteilter Typ, Web-Quelle app-version.ts, Abzeichen in der Seitenleiste (Tests zuerst)</name>
|
|
<files>packages/shared/src/index.ts, apps/api/src/health/app-version.ts, apps/api/src/health/health.controller.ts, apps/api/src/health/health.controller.spec.ts, apps/api/src/main.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, 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</files>
|
|
<behavior>
|
|
API — `apps/api/src/health/health.controller.spec.ts` (NEU; Stil wie `tenant.controller.spec.ts`: `import 'reflect-metadata'`, deutsche Testnamen „Test N (...)", Kopfkommentar mit Bezug quick-260914-ku1). `vi.stubEnv` fuer `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILD_TIME`; `afterEach(() => vi.unstubAllEnvs())`. Controller direkt instanziieren (`new HealthController()`, keine Abhaengigkeiten).
|
|
- Test 1 (`check()`): liefert `status: 'ok'` und einen `timestamp`, den `Date.parse` versteht — Regressions-Pin des unveraenderten Healthchecks.
|
|
- Test 2 (Vorgaben): ohne die vier Variablen liefert `getVersion()` genau `{ name: 'tessera', version: 'dev', channel: 'dev', commit: '', buildTime: '' }`.
|
|
- Test 3 (durchgereicht): `APP_VERSION=v1.2.3`, `APP_CHANNEL=live`, `APP_COMMIT=abc1234`, `APP_BUILD_TIME=2026-09-14T12:00:00Z` -> alle vier Felder woertlich, `name` weiterhin `'tessera'`.
|
|
- Test 4 (Compose-Semantik): alle vier Variablen auf `''` gestubbt -> Ergebnis wie Test 2 (leer zaehlt wie ungesetzt; Begruendung: Compose reicht unbelegte Variablen als Leerstring weiter, siehe `migrate-and-start.sh`).
|
|
- Test 5 (`formatAppVersionLine`): mit den Werten aus Test 3 -> `'Tessera API v1.2.3 (live) abc1234'`; ohne Commit (Vorgaben) -> `'Tessera API dev (dev)'` — kein Leerzeichen am Ende.
|
|
- Test 6 (bewusst oeffentlich, T-KU1-03): `Reflect.getMetadata(IS_PUBLIC_KEY, HealthController.prototype.getVersion)` ist `true` und ebenso fuer `check`.
|
|
Web — `apps/web/src/lib/app-version.test.ts` (NEU; jeder Test `vi.resetModules()` und `const mod = await import('./app-version')`, weil die Konstante beim Laden des Moduls gelesen und die API-Antwort memoisiert wird; `vi.unstubAllEnvs()` und `vi.unstubAllGlobals()` im `afterEach`):
|
|
- Test 1 (Vorgaben): ohne `NEXT_PUBLIC_APP_*` -> `mod.appVersion` gleich `{ version: 'dev', channel: 'dev', commit: '' }`.
|
|
- Test 2 (Umgebung): `NEXT_PUBLIC_APP_VERSION=v1.2.3`, `_CHANNEL=beta`, `_COMMIT=abc1234` -> woertlich durchgereicht.
|
|
- Test 3 (Normalisierung): `NEXT_PUBLIC_APP_CHANNEL=gamma` -> `channel === 'dev'` (nur `beta` und `live` sind bekannte Kanaele; ein fremder Wert darf keinen fehlenden Uebersetzungsschluessel erzeugen).
|
|
- Test 4 (Laden, memoisiert): `vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ name: 'tessera', version: 'v1.2.3', channel: 'beta', commit: 'abc1234', buildTime: 'x' }) })))`; zwei Aufrufe `mod.loadApiVersion()` liefern beide das Objekt, `fetch` wurde genau EINMAL aufgerufen, die URL endet auf `/health/version`, die Optionen enthalten `credentials: 'include'`.
|
|
- Test 5 (still bei Fehler): `fetch` lehnt ab (`Promise.reject(new Error('netz'))`) -> `await mod.loadApiVersion()` ist `null`, nichts wird geworfen; zweiter Fall im selben Test mit `ok: false` -> ebenfalls `null`.
|
|
Web — `apps/web/src/components/layout/app-version-badge.test.tsx` (NEU; Vorlage `sidebar.test.tsx`: `cleanup`/`vi.restoreAllMocks` im `afterEach`, next-intl-Mock mit `sidebar: { 'channel.beta': 'Beta', 'channel.live': 'Live', 'channel.dev': 'Entwicklung' }`; Modul-Mock `vi.mock('@/lib/app-version', () => ({ appVersion: mockAppVersion, loadApiVersion: mockLoad }))` mit veraenderbaren Variablen; Komponente per dynamischem Import nach dem Setzen der Mocks):
|
|
- Test 1 (Zeile): `appVersion = { version: 'v1.2.3', channel: 'beta', commit: 'abc1234' }`, `loadApiVersion` liefert `null` -> `screen.getByTestId('app-version')` hat den Text `v1.2.3 · Beta`.
|
|
- Test 2 (Tooltip mit API): `loadApiVersion` liefert `{ name: 'tessera', version: 'v1.2.3', channel: 'beta', commit: 'abc1234', buildTime: '' }` -> `waitFor`: das `title`-Attribut enthaelt `Commit abc1234` UND `API v1.2.3 (beta)`.
|
|
- Test 3 (Tooltip ohne API): `loadApiVersion` liefert `null` -> `title` enthaelt `Commit abc1234` und NICHT `API`.
|
|
- Test 4 (Kanal dev, kein Commit): `appVersion = { version: 'dev', channel: 'dev', commit: '' }`, `loadApiVersion` -> `null` -> Text `dev · Entwicklung`, und das Element hat KEIN `title`-Attribut (nichts zu zeigen).
|
|
Web — `sidebar.test.tsx`: Modul-Mock `vi.mock('@/components/layout/app-version-badge', () => ({ AppVersionBadge: () => <div data-testid="app-version-badge" /> }))` neben dem bestehenden SidebarFooter-Mock; neuer sechster Test „renders the version badge below the navigation" -> nach `waitFor` auf `Dashboard` ist `screen.getByTestId('app-version-badge')` im Dokument (Store-Mock hat `isCollapsed: false`).
|
|
</behavior>
|
|
<action>
|
|
Schritt A — RED: die drei neuen Testdateien und den sechsten Sidebar-Test aus `<behavior>` anlegen, BEVOR Produktionscode entsteht. `pnpm -C apps/api exec vitest run src/health/health.controller.spec.ts` und `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` muessen rot sein (Modul nicht gefunden bzw. Erwartungen verfehlt) — die Ausgabezeilen ins SUMMARY.
|
|
|
|
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).
|
|
</action>
|
|
<verify>
|
|
<automated>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 "<AppVersionBadge />" 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</automated>
|
|
</verify>
|
|
<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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Build-Args in beide Dockerfiles, Veroeffentlichungs-Skript und CI-Trigger je Kanal, IMAGE_TAG in der Compose-Datei — Falsifizierung durch lokale Baeue</name>
|
|
<files>apps/web/Dockerfile, apps/api/Dockerfile, .gitea/scripts/publish-images.sh, .gitea/workflows/ci.yml, docker-compose.prod.yml</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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=$?</automated>
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Betriebshandbuch „Zwei Kanäle: Live und Beta", ci-cd-setup auf gemessenen Stand, Push und Beobachtung des echten CI-Laufs</name>
|
|
<files>docs/anleitung-betrieb.md, docs/ci-cd-setup.md</files>
|
|
<precondition>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).</precondition>
|
|
<action>
|
|
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/<kurzer-name>` 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).
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<done>
|
|
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 `<sha> beta` der vom Runner gebauten Abbilder (oder, falls Gitea/Runner nicht erreichbar waren, den Grund und den offenen Punkt).
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
Nach Task 3, alles aus `/home/vicolab/projects/tessera-ctl`:
|
|
- `pnpm -C apps/api exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)"` -> `Test Files 65 passed (65)` / `Tests 1060 passed (1060)`
|
|
- `pnpm -C apps/web exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)"` -> `Test Files 40 passed (40)` / `Tests 243 passed (243)`
|
|
- `for p in packages/shared apps/api apps/web; do pnpm -C $p exec tsc --noEmit; echo "TSC_$p=$?"; done` -> dreimal `=0`
|
|
- `D=$(git diff --stat 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)'` -> `<kurzer SHA des gepushten Commits> 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.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Versionsstempel fliesst CI -> Build-Args -> Abbilder -> `GET /health/version` / Startlog -> Seitenleiste; lokal ohne Args bleibt alles `dev`; die Bauzeit-Einbettung im Browser-Bundle ist mit `v9.9.9-test` bewiesen; der echte CI-Lauf nach dem Push hat `:beta`-Abbilder mit dem SHA des gepushten Commits gebaut.
|
|
- Pipeline: `main` -> `beta` + `latest`; Tag `v*` -> `live` + `vX.Y.Z`; `live` ohne Tag prueft nur; `fetch-depth: 0`; Entscheidung im Skript, lokal per `--print-plan` gepinnt.
|
|
- `docker-compose.prod.yml` mit `${IMAGE_TAG:-beta}`, beide Werte per `config --images` bewiesen; 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.md` ohne die veralteten Aussagen.
|
|
- API 1060/65, Web 243/40, `tsc` dreimal 0, genau 20 Dateien ausserhalb `.planning`, drei Commits mit Scope `quick-260914-ku1`, gepusht; `live`-Zweig und `v1.0.0` NICHT angelegt.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-SUMMARY.md` when done
|
|
</output>
|