From df16f4682a4db85fa958f581f9d85fb06e0cfefb Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 16 Sep 2026 10:03:06 +0200 Subject: [PATCH] =?UTF-8?q?docs(quick-260916-dcz):=20Plan=20=E2=80=94=20CH?= =?UTF-8?q?ANGELOG.md=20in=20Alltagssprache,=20Seite=20"Was=20ist=20neu"?= =?UTF-8?q?=20mit=20Kanalfilter,=20Gitea-Release=20je=20Freigabe-Tag,=20Ha?= =?UTF-8?q?ndbuecher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei Aufgaben, 19 Dateien ausserhalb .planning. Gemessene Abweichungen vom Auftrag: .dockerignore schliesst Wurzel-*.md aus (Ausnahme !CHANGELOG.md noetig); localhost:3002 ist aus dem CI-Job-Container nicht erreichbar (API-Basis aus GITHUB_SERVER_URL); vier Module statt zwei in den Handbuechern. Bauweg per env.TESSERA_CHANGELOG_MD in next.config.ts (webpack + Turbopack, Server-Bundle-only). COVERAGE.md: Gitea-Release-API (POST/GET-by-tag/PATCH integriert, Rest begruendet aus). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY --- .../260916-dcz-PLAN.md | 315 ++++++++++++++++++ .../COVERAGE.md | 15 + 2 files changed, 330 insertions(+) create mode 100644 .planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-PLAN.md create mode 100644 .planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/COVERAGE.md diff --git a/.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-PLAN.md b/.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-PLAN.md new file mode 100644 index 0000000..0ba665c --- /dev/null +++ b/.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-PLAN.md @@ -0,0 +1,315 @@ +--- +phase: quick-260916-dcz +plan: 01 +type: execute +wave: 1 +depends_on: [] +autonomous: true +requirements: [QUICK-260916-DCZ] + +files_modified: + - CHANGELOG.md + - .dockerignore + - apps/web/Dockerfile + - apps/web/next.config.ts + - apps/web/src/lib/changelog.ts + - apps/web/src/lib/changelog.test.ts + - apps/web/src/app/(portal)/changelog/page.tsx + - apps/web/src/app/(portal)/changelog/changelog-page.test.tsx + - apps/web/src/components/changelog/changelog-view.tsx + - apps/web/src/components/layout/app-version-badge.tsx + - apps/web/src/components/layout/app-version-badge.test.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + - .gitea/scripts/publish-release.sh + - .gitea/workflows/ci.yml + - docs/anleitung-betrieb.md + - docs/anleitung-anwender.md + - docs/anleitung-entwicklung.md + - docs/ci-cd-setup.md + +estimate: + tokens: 140000 + raw_tokens: 140000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "`CHANGELOG.md` liegt im Wurzelverzeichnis, deutsch mit echten Umlauten, Alltagssprache in Sie-Form, Form nach Keep a Changelog: H1, kurzer Vorspann, dann `## Unveröffentlicht` (Untergruppen `### Neu` / `### Geändert` mit den Punkten des Dashboard-Umbaus 260916-bwo UND der Seite „Was ist neu“ dieses Auftrags), darunter `## 1.0.0 – 2026-09-15` mit 6-12 Punkten des Live-Standes aus den Handbuechern (Portal, Dashboard-Widgets, Marktplatz/Freigaben, Ausschreibungs-Radar, DKV-Rechnung, Zertifikat-Manager + Domaincheck, Benutzer/Gruppen/AD, SMTP + Passwort zuruecksetzen, persoenliche Einstellungen, Fehler melden, Versionsanzeige). Keine Dateinamen, keine Commit-Kuerzel, keine unerklaerten Fachbegriffe." + - "Ein angemeldeter Anwender klickt unten in der Seitenleiste auf die Versionszeile (jetzt ein Link mit `aria-label` „Was ist neu“, `href=\"/changelog\"`, Tooltip unveraendert) und sieht unter `/changelog` im Portal-Layout die Seite „Was ist neu“ mit dem Inhalt von `CHANGELOG.md` als gerendertem Markdown (`MDEditor.Markdown` aus dem bereits installierten `@uiw/react-md-editor` 4.1.1 mit `rehype-sanitize`, kein neues Paket). Ohne Anmeldung leitet die bestehende Middleware auf `/login` um (`/changelog` ist keine oeffentliche Route)." + - "Kanalregel als reine Funktion `filterChangelogForChannel(markdown, channel, options?)` in `apps/web/src/lib/changelog.ts`: auf `live` fehlt der Abschnitt „Unveröffentlicht“ vollstaendig; auf `beta` und `dev` bleibt er und traegt die Ueberschrift „Noch nicht freigegeben (Beta)“ (uebersetzt), die Seite zeigt dazu einen Hinweis; ein leerer Abschnitt (ohne Listenpunkt) wird auf allen Kanaelen ausgeblendet; H1 und Vorspann vor der ersten `## `-Ueberschrift entfallen (die Seite hat ihren eigenen Titel). Falsifizierung (a): der live-Test wird rot, sobald die Funktion den Abschnitt nicht mehr entfernt." + - "Der Changelog-Text kommt zur BAUZEIT ins Bundle: `apps/web/next.config.ts` liest `../../CHANGELOG.md` per `fs.readFileSync` und legt den Text als `env.TESSERA_CHANGELOG_MD` ab (Next.js ersetzt `process.env.TESSERA_CHANGELOG_MD` in webpack UND Turbopack ueber denselben Define-Mechanismus — gemessen in `next/dist/lib/static-env.js` `getNextConfigEnv` + `serializeDefineEnv`). Fehlt die Datei, bricht der Build mit klarer deutscher Meldung ab. Im Docker-Bau liegt die Datei im Kontext (`.dockerignore` schliesst `*.md` an der Wurzel aus — gemessen: `COPY README.md` scheitert mit `not found` — daher die Ausnahme `!CHANGELOG.md`) und wird mit EINER COPY-Zeile in die builder-Stufe kopiert. Falsifizierung (c): im lokal gebauten Web-Abbild enthaelt `/app/apps/web/.next/server` den Datums-Marker der 1.0.0-Ueberschrift, `/app/apps/web/.next/static` NICHT (der Text liegt nur im Server-Bundle, nicht in oeffentlich abrufbaren Chunks)." + - "`.gitea/scripts/publish-release.sh` (POSIX sh, `set -eu`, `jq` + `curl` — beides im Runner-Abbild `gitea/runner-images:ubuntu-latest` vorhanden: jq 1.6, und auf dem Host: jq 1.7) entscheidet wie `publish-images.sh` anhand `GITHUB_REF` (`refs/tags/v*`, sonst „nichts zu tun“, Exit 0), akzeptiert `--tag vX.Y.Z` und `--dry-run`, schneidet den Abschnitt `## X.Y.Z` (bis zur naechsten `## `-Ueberschrift, ohne die eigene Ueberschrift, ohne Leerzeilen am Rand) per awk aus `CHANGELOG.md`, baut das JSON ausschliesslich mit `jq --arg`, legt den Release per `POST .../releases` an (`tag_name`, `name` = `Tessera X.Y.Z`, `body` = Abschnitt) und aktualisiert per `PATCH .../releases/{id}`, wenn `GET .../releases/tags/{tag}` bereits 200 liefert (idempotent, Text folgt CHANGELOG.md). Falsifizierung (b): `--dry-run --tag v9.9.9` (kein Abschnitt) endet mit Exit 1 und der Meldung, dass CHANGELOG.md keinen Abschnitt fuer 9.9.9 hat — es entsteht nie ein leerer Release. Das Token kommt nur aus `GITEA_TOKEN` (Umgebung), wird nie ausgegeben und nicht als Kommandozeilenargument uebergeben (Header aus Datei). API-Basis: `GITEA_API`, sonst `GITHUB_API_URL`, sonst `GITHUB_SERVER_URL/api/v1`, sonst `http://localhost:3002/api/v1` — im CI-Job-Container ist `localhost:3002` NICHT erreichbar (gemessen: 000), `https://git.vicolab.de` schon (200)." + - "`.gitea/workflows/ci.yml`: der Job `publish` hat nach dem Abbild-Schritt einen Schritt, der `sh .gitea/scripts/publish-release.sh` mit `GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}` in `env` aufruft (kein Echo, kein Argument). Das Token traegt gemessen `write:repository` (Scopes der Nutzer-Tokens `cc-full`/`Claude-Code`, per Basic-Auth gelesen) — das deckt Releases ab. Rueckwirkend existiert nach dem echten lokalen Lauf `--tag v1.0.0` (Token aus der Push-URL, nie ausgeben) der Release `Tessera 1.0.0` zum bestehenden Tag `v1.0.0` (Commit e509860; vorher gemessen: null Releases im Repo); ein zweiter Lauf geht den PATCH-Weg (Exit 0, weiterhin genau ein Release)." + - "Handbuecher: `docs/anleitung-betrieb.md` Kapitel 9 „Eine Version freigeben“ nennt VOR dem Tag den Schritt „CHANGELOG.md: Unveröffentlicht in X.Y.Z – Datum umbenennen, neues leeres Unveröffentlicht anlegen, auf main pushen“, den automatischen Gitea-Release durch die Pipeline (und was passiert, wenn der Abschnitt fehlt) und die Seite „Was ist neu“ als vierten Weg unter „Woran Sie erkennen, welche Version läuft“; der Absatz „Erstfreigabe v1.0.0“ steht in der Vergangenheit (erfolgt: Tag 2026-09-14, live seit 2026-09-15). `docs/anleitung-anwender.md` hat einen Abschnitt „Was ist neu“ (Klick auf die Version unten links; auf Live nur Freigegebenes) samt Inhaltsverzeichnis-Eintrag. `docs/anleitung-entwicklung.md` traegt unter „Konventionen und Fallstricke“ die Regel „jede Änderung sofort in CHANGELOG.md unter Unveröffentlicht“. `docs/ci-cd-setup.md` (ASCII-Umschrift wie im Bestand) nennt den vierten Schritt des Jobs `publish`, die Token-Berechtigung `repository: write` und den Release je Tag." + - "Baseline am Ende: Web `Test Files 48 passed (48)` / `Tests 301 passed (301)` (Planungszeit 46/286 plus 10 in `changelog.test.ts`, 3 in `changelog-page.test.tsx`, 2 in `app-version-badge.test.tsx`), API unveraendert `67 passed (67)` / `1078 passed (1078)`, `tsc --noEmit` in web, api und shared Exit 0, `pnpm install --frozen-lockfile` Exit 0 (keine neuen Pakete), `git diff --stat 7a6f42e -- . ':!.planning'` nennt genau `19 files changed`; `.env*`, Compose-Dateien, Prisma-Schema, `pnpm-lock.yaml`, `package.json` beider Apps, `umlaut-dictionary.ts` unangetastet. Nach `git push` endet der CI-Lauf zum gepushten Commit mit `conclusion == success` (der Release-Schritt meldet auf `main` „nichts zu tun“)." + artifacts: + - "CHANGELOG.md — H1 `# Änderungen an Tessera`, Vorspann (2-3 Saetze), `## Unveröffentlicht` mit `### Neu` und `### Geändert`, `## 1.0.0 – 2026-09-15` mit `### Neu`" + - ".dockerignore — Zeile `!CHANGELOG.md` direkt nach `*.md`" + - "apps/web/Dockerfile — in der builder-Stufe genau eine neue Zeile `COPY CHANGELOG.md ./` (zwischen `COPY tsconfig.base.json ./` und `ENV NEXT_PUBLIC_API_URL`), Kommentar mit Verweis auf next.config.ts" + - "apps/web/next.config.ts — `readChangelog()` (node:fs/node:path, `path.resolve(__dirname, '../../CHANGELOG.md')`, klare Fehlermeldung) und `env: { TESSERA_CHANGELOG_MD: readChangelog() }`" + - "apps/web/src/lib/changelog.ts — `export type ChangelogChannel = AppChannel`; `export interface FilteredChangelog { markdown: string; hasUnreleased: boolean }`; `export function filterChangelogForChannel(markdown: string, channel: AppChannel, options?: { unreleasedHeading?: string }): FilteredChangelog`; `export const UNRELEASED_HEADING = 'Unveröffentlicht'`; `export const changelogMarkdown: string = process.env.TESSERA_CHANGELOG_MD ?? ''` (voller Literalname)" + - "apps/web/src/lib/changelog.test.ts — NEU, 10 Tests laut " + - "apps/web/src/app/(portal)/changelog/page.tsx — async Server-Komponente (kein 'use client'), `getTranslations('changelog')`, Kanal aus `appVersion.channel`, rendert `

`, Intro, optionalen Hinweis (`data-testid=\"changelog-unreleased-hint\"`), `` oder Leer-Text (`data-testid=\"changelog-empty\"`)" + - "apps/web/src/app/(portal)/changelog/changelog-page.test.tsx — NEU, 3 Tests laut " + - "apps/web/src/components/changelog/changelog-view.tsx — 'use client', `ChangelogView({ markdown })` mit `MDEditor.Markdown` (`source`, `rehypePlugins={[[rehypeSanitize]]}`, `wrapperElement={{ 'data-color-mode': mode }}` aus `useTheme().resolvedTheme` nach Mount, `data-testid=\"changelog-markdown\"` am Wrapper)" + - "apps/web/src/components/layout/app-version-badge.tsx — `span` wird `Link` (next/link) auf `/changelog` mit `aria-label={t('whatsNew')}`, `title` wie bisher, `data-testid=\"app-version\"`, Klassen wie bisher plus `hover:text-foreground hover:underline`" + - "apps/web/src/components/layout/app-version-badge.test.tsx — `next/link`-Mock (Props durchreichen) und 2 neue Tests (href, aria-label)" + - "apps/web/src/messages/de.json + en.json — `sidebar.whatsNew` und neuer Namensraum `changelog` mit `title`, `intro`, `unreleasedHeading`, `unreleasedHint`, `empty`" + - ".gitea/scripts/publish-release.sh — NEU, ausfuehrbar (chmod +x), Kopfkommentar wie publish-images.sh" + - ".gitea/workflows/ci.yml — vierter Schritt im Job `publish`" + - "docs/anleitung-betrieb.md, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md, docs/ci-cd-setup.md — Abschnitte wie in den truths" + key_links: + - "Bauzeit-Einbettung: `env` in next.config.ts wird von `getNextConfigEnv` zu `process.env.TESSERA_CHANGELOG_MD` fuer client/edge/nodejs-Varianten definiert und per `serializeDefineEnv` JSON-stringifiziert (mehrzeilige Werte sicher). Deshalb (1) muss `changelog.ts` den vollen Literalnamen verwenden (kein Destructuring, kein `process.env[name]` — dieselbe Regel wie in app-version.ts), (2) darf `changelog.ts` NUR von der Server-Seite (`page.tsx`) importiert werden, damit der Text nicht in `.next/static` landet, (3) haengt die Docker-Falsifizierung an der COPY-Zeile PLUS der `.dockerignore`-Ausnahme — eine ohne die andere laesst den Build mit `not found` bzw. mit der eigenen Fehlermeldung scheitern." + - "Option (b) `?raw`/`asset/source` scheidet aus: `apps/web` startet mit `next dev --turbopack`, der `webpack`-Block in next.config.ts greift dort nicht, und eine Turbopack-`rules`-Regel braeuchte einen Loader (`raw-loader`) — verbotenes neues Paket. Option (a) Laufzeit-`fs.readFileSync` in einer Server-Komponente scheidet aus, weil jede Seite dynamisch ist (`i18n/request.ts` liest Cookies) und die Datei dann per `outputFileTracingIncludes` ausserhalb des Projektordners in die runner-Stufe muesste — zwei Mechanismen statt einem. Option (c) generierte Datei braeuchte Skript-Ketten vor build/dev/tsc/vitest." + - "Seite -> Abzeichen: `sidebar.test.tsx` mockt `@/components/layout/app-version-badge` komplett — der Link-Umbau erfordert dort keine Aenderung; `app-version-badge.test.tsx` rendert die echte Komponente und braucht deshalb einen `next/link`-Mock wie in sidebar.test.tsx (Props inkl. `aria-label`, `title`, `data-testid` durchreichen)." + - "Auth: `apps/web/src/middleware.ts` schuetzt jede Route ausser `/login`, `/reset-password`, `/_next/*`, `/api*` — `/changelog` ist damit ohne weiteres Zutun nur angemeldet erreichbar. Statische Chunks unter `/_next/static` sind NICHT geschuetzt — deshalb Server-Bundle-only (siehe oben)." + - "Release-Skript -> Gitea: Aus dem Job-Container ist Gitea nur ueber `https://git.vicolab.de` erreichbar (per-Job-Netz `GITEA-ACTIONS-TASK-…-network`, Runner-Instanz-URL = git.vicolab.de); `docker login localhost:3002` funktioniert heute nur, weil der Docker-DAEMON (Host) die Registry anspricht, nicht der Job-Container. Das Skript darf also im CI nie auf localhost:3002 zurueckfallen — es loest `GITHUB_API_URL`/`GITHUB_SERVER_URL` auf und gibt die gewaehlte API-Basis (ohne Token) als erste Zeile aus; der CI-Lauf auf `main` beweist die Aufloesung im Log, der Release-Weg selbst wird lokal gegen localhost:3002 mit `--tag v1.0.0` bewiesen." + - "Abschnitt-Schnitt: awk-Muster `^## X\\.Y\\.Z( |$)` bis zur naechsten `^## `; zur Planungszeit auf einem Muster-Changelog bewiesen (1.0.0 und 0.9.0 korrekt, `1.0` und `2.0.0` leer -> Exit 1). `## Unveröffentlicht` kann nie getroffen werden, weil der Tag `^v[0-9]+\\.[0-9]+\\.[0-9]+$` erfuellen muss." + - "Umlaut-Waechter: `umlaut-guard.spec.ts` prueft jede neue de.json-Zeichenkette gegen `SUSPECT_RE=/(ae|oe|ue|ss)/i` und die Allowlist. Die vorgegebenen Texte enthalten kein solches Token ausser bereits gelisteten Woertern (`aktuelle`) — daher bleibt `umlaut-dictionary.ts` unangetastet (Gate). Woerter wie „Neuerungen“ (ue), „Fassung“ (ss), „dass“ (ss) sind zu vermeiden." +--- + + +Aenderungsliste fuer Anwender und Betrieb: (1) `CHANGELOG.md` im Wurzelverzeichnis in Alltagssprache (rueckwirkend 1.0.0, dazu „Unveröffentlicht“ mit dem Dashboard-Umbau und dieser Seite); (2) Seite „Was ist neu“ unter `/changelog`, erreichbar per Klick auf die Versionszeile in der Seitenleiste, mit Kanalfilter (Live sieht nur Freigegebenes) — Text zur Bauzeit ins Bundle, kein neues Paket; (3) Gitea-Release je Freigabe-Tag durch die Pipeline (Skript mit `--dry-run`, idempotent, rueckwirkend `v1.0.0`); (4) Handbuecher (Betrieb Kapitel 9, Anwender, Entwicklung, CI-Setup). + +Purpose: Auf dem Live-Server soll jederzeit einsehbar sein, was sich geaendert hat — fuer Anwender in der Oberflaeche, fuer den Betrieb im Gitea-Release, fuer die Entwicklung als Pflichtschritt je Aenderung. +Output: 19 Dateien (13 Code/Tests/Bau, 2 CI, 4 Handbuecher), drei Commits mit Scope `quick-260916-dcz` (feat / ci / docs), gepusht, CI-Lauf beobachtet, Release `v1.0.0` in Gitea vorhanden. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@CLAUDE.md +@.planning/quick/260916-bwo-dashboard-feineres-raster-spalten-und-ze/260916-bwo-SUMMARY.md +@.planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-PLAN.md +@apps/web/src/components/layout/app-version-badge.tsx +@apps/web/src/components/layout/app-version-badge.test.tsx +@apps/web/src/lib/app-version.ts +@apps/web/src/components/layout/sidebar.test.tsx +@apps/web/src/components/modules/module-access-gate.tsx +@apps/web/src/components/modules/module-access-gate.test.tsx +@apps/web/src/components/dashboard/widgets/note-widget.tsx +@apps/web/src/components/dashboard/widgets/note-widget.test.tsx +@apps/web/src/middleware.ts +@apps/web/next.config.ts +@apps/web/Dockerfile +@.dockerignore +@.gitea/workflows/ci.yml +@.gitea/scripts/publish-images.sh +@apps/web/src/messages/umlaut-guard.spec.ts +@docs/anleitung-betrieb.md +@docs/anleitung-anwender.md +@docs/anleitung-entwicklung.md +@docs/ci-cd-setup.md + + + +Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/main). Abweichungen vom Auftragstext sind mit „DELTA“ markiert. + +- Baseline: Web `46 files / 286 tests`, API `67 / 1078`, `tsc --noEmit` web/api/shared Exit 0 (frisch nachgemessen, stimmt mit dem Auftrag ueberein). +- Tag `v1.0.0` ist ein annotierter Tag (Objekt 4d36942) auf Commit `e509860`, Tagger-Datum 2026-09-14; live seit 2026-09-15 laut STATE.md. Changelog-Datum bleibt wie beauftragt `2026-09-15` (Tag der Inbetriebnahme). +- Gitea 1.26.2; `GET /repos/schalli/tessera-ctl/releases` -> leere Liste (kein Release vorhanden). Token aus der Push-URL (Form `user:token@localhost:3002`) antwortet auf `/api/v1/user` mit 200; die Nutzer-Tokens `cc-full` und `Claude-Code` tragen u. a. `write:repository` und `write:package` — `secrets.REGISTRY_TOKEN` ist eines davon (docker login mit `-u schalli`), reicht also fuer Releases. Repo-Rechte: admin/push/pull true, `has_releases: true`. +- DELTA (wichtig): `.dockerignore` enthaelt `*.md` — gemessen mit `COPY README.md` in einem Test-Dockerfile: `"/README.md": not found`. Eine COPY-Zeile allein reicht NICHT; `.dockerignore` braucht die Ausnahme `!CHANGELOG.md` (zweite Bau-Datei, im N enthalten). +- DELTA: `localhost:3002` ist aus einem Container mit dem Runner-Abbild NICHT erreichbar (curl -> 000), `https://git.vicolab.de/api/v1/version` schon (200). Job-Container laufen in einem per-Job-Netz (`GITEA-ACTIONS-TASK-…-network`), Runner-Instanz-URL `https://git.vicolab.de`. Das Release-Skript darf im CI nicht auf localhost:3002 zeigen. +- Werkzeuge: Runner-Abbild `gitea/runner-images:ubuntu-latest` hat `jq` 1.6, `curl`, `python3`, `git`; Host hat `jq` 1.7, `curl`, `python3`. Entscheidung: `jq`. +- `@uiw/react-md-editor` 4.1.1 exportiert `MDEditor.Markdown` (statisch am Default-Export, Typ `MarkdownPreviewProps`: `source`, `rehypePlugins`, `wrapperElement` mit `data-color-mode: 'light'|'dark'`, `className`, `style`). `@uiw/react-markdown-preview` ist NUR transitiv vorhanden (pnpm-Isolation) — nicht direkt importieren. Das Notiz-Widget nutzt `MDEditor` mit `preview='preview'` und `previewOptions.rehypePlugins=[[rehypeSanitize]]`; sein Test mockt `@uiw/react-md-editor` als Modul mit `default` + `commands`. +- Next 15.5.19: `next.config.ts` wird per SWC nach CommonJS transpiliert und mit Dateiname `/next.config.compiled.js` geladen -> `__dirname` = `apps/web`. `env`-Werte laufen ueber `getNextConfigEnv` (Schluessel `process.env.`, verboten nur `NODE_*`, `__*`, `NEXT_RUNTIME`) und `serializeDefineEnv` (JSON.stringify) fuer webpack und Turbopack gleichermassen. `node:fs`, `node:path`, `__dirname` typechecken in `apps/web` (Scratch-Datei, tsc Exit 0). +- Jede Seite ist dynamisch (`i18n/request.ts` -> `cookies()`), deshalb keine Prerender-Abkuerzung; `middleware.ts` schuetzt alle Routen ausser `/login`, `/reset-password`, `/_next/*`, `/api*`. +- Bestehende Server-Komponente mit `getTranslations` + Test-Muster (Funktion awaiten, Ergebnis rendern, `next-intl/server` gemockt): `module-access-gate.tsx` / `.test.tsx`. Bestehende Server-Seiten ohne 'use client': `(portal)/settings/page.tsx`, `(portal)/modules/[category]/[moduleSlug]/page.tsx`. +- `sidebar.test.tsx` mockt das Abzeichen als Modul — der Link-Umbau beruehrt sidebar.tsx/-test nicht. `app-version-badge.test.tsx` mockt heute `next-intl` und `@/lib/app-version`, aber nicht `next/link`. +- `umlaut-guard.spec.ts`: Allowlist enthaelt u. a. `aktuelle`, `neue`, `muss`, `Adresse`, `Passwort` — NICHT `Neuerungen`, `Fassung`, `dass`. Texte unten sind so gewaehlt, dass `umlaut-dictionary.ts` unangetastet bleibt. +- DELTA: Die Handbuecher nennen VIER Module (Ausschreibungs-Radar, DKV-Rechnung, Zertifikat-Manager, Domaincheck), nicht zwei; „DKV Fleet“ heisst fuer Anwender „DKV-Rechnung“. Das Anwenderhandbuch beschreibt in „Aufbau der Oberfläche“ heute keine Versionszeile. +- Commits seit `v1.0.0`: nur Doku-Commits und 260916-bwo (5 Commits) — „Unveröffentlicht“ = bwo-Punkte + diese Seite. +- Lokaler `docker build` des Web-Abbilds: Kontext enthaelt `apps/desktop/src-tauri/target` (3,8 GB, nicht in .dockerignore) — BuildKit ueberfuehrt inkrementell; ku1 hat lokal 99-129 s je Bau gemessen. Einplanen: 2-4 Minuten. +- Planer-Beitraege: `schema-gate` — keine Prisma-/Schema-Datei im Umfang, kein Push-Task. `api-coverage` — Gitea-Release-API ist eine externe API: Matrix in `COVERAGE.md` neben diesem Plan (POST/GET-by-tag/PATCH INTEGRATE; list/latest/delete/assets/draft OPT-OUT mit Grund). `assumption-delta scan` -> `{"skipped":true,"reason":"phase_unresolved"}` (Quick-Task ohne ROADMAP-Abschnitt); inhaltlich keine Singular->Plural-Verschiebung (Kanaele existieren seit ku1) -> `no-change`. `estimate-calibration`: factor 1, sample_count 0, confidence low. + + + + + + Task 1: CHANGELOG.md, Bauzeit-Einbettung, Kanalfilter mit Tests, Seite „Was ist neu“, Abzeichen-Link, i18n + CHANGELOG.md, .dockerignore, apps/web/Dockerfile, apps/web/next.config.ts, apps/web/src/lib/changelog.ts, apps/web/src/lib/changelog.test.ts, apps/web/src/app/(portal)/changelog/page.tsx, apps/web/src/app/(portal)/changelog/changelog-page.test.tsx, apps/web/src/components/changelog/changelog-view.tsx, apps/web/src/components/layout/app-version-badge.tsx, apps/web/src/components/layout/app-version-badge.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json + + - apps/web/src/lib/app-version.ts (Literalname-Regel fuer process.env, `AppChannel`) + - apps/web/src/lib/app-version.test.ts (Muster vi.stubEnv + vi.resetModules + dynamischer Import) + - apps/web/src/components/layout/app-version-badge.tsx + .test.tsx + - apps/web/src/components/layout/sidebar.test.tsx (Zeilen 1-20: `next/link`-Mock) + - apps/web/src/components/modules/module-access-gate.tsx + .test.tsx (Server-Komponente mit getTranslations, Testmuster) + - apps/web/src/components/dashboard/widgets/note-widget.tsx (MDEditor + rehypeSanitize) und note-widget.test.tsx (Modul-Mock von @uiw/react-md-editor) + - apps/web/src/components/layout/app-shell.tsx (mounted-Guard) und apps/web/src/app/(portal)/change-password/page.tsx (Seitenrahmen `mx-auto max-w-… py-8 px-4`, `

`) + - apps/web/next.config.ts, apps/web/Dockerfile, .dockerignore + - apps/web/src/messages/umlaut-guard.spec.ts (Regeln), de.json/en.json Namensraum `sidebar` + - docs/anleitung-anwender.md (Abschnitte Aufbau der Oberflaeche, Dashboard, Marktplatz, Module, Persoenliche Einstellungen, Fehler melden) und docs/anleitung-administration.md (Kapitel 1-6) — Quelle der 1.0.0-Punkte, nichts raten + - .planning/quick/260916-bwo-dashboard-feineres-raster-spalten-und-ze/260916-bwo-SUMMARY.md Abschnitt „Fuer den Changelog“ (5 Punkte) + + + changelog.test.ts (10 Tests, Vorlage: Muster-Changelog als Konstante mit H1, Vorspann, `## Unveröffentlicht` (`### Geändert` mit zwei Punkten), `## 1.0.0 – 2026-09-15` (`### Neu` mit zwei Punkten), `## 0.9.0 – 2026-09-01`): + - Test 1 (live): Ergebnis enthaelt weder `## Unveröffentlicht` noch die Punkte darunter, aber `## 1.0.0 – 2026-09-15`, `## 0.9.0 – 2026-09-01` und deren Punkte in dieser Reihenfolge; `hasUnreleased === false`. (Falsifizierung a: wird rot, sobald die Funktion den Abschnitt nicht entfernt.) + - Test 2 (beta): Ergebnis enthaelt `## Noch nicht freigegeben (Beta)` statt `## Unveröffentlicht`, die Punkte darunter bleiben, `hasUnreleased === true`. + - Test 3 (dev): wie Test 2. + - Test 4 (eigenes Label): `options.unreleasedHeading = 'Not yet released (beta)'` -> Ueberschrift `## Not yet released (beta)`. + - Test 5 (leerer Abschnitt, beta): Unveröffentlicht nur mit `### Neu` ohne Listenpunkt -> Abschnitt fehlt im Ergebnis, `hasUnreleased === false`, 1.0.0 bleibt. + - Test 6 (kein Abschnitt): Markdown ohne Unveröffentlicht -> `hasUnreleased === false`, Versionsabschnitte unveraendert. + - Test 7 (Hierarchie): H1-Zeile und Vorspann fehlen; das Ergebnis beginnt (nach trim) mit `## `; `### `-Ueberschriften bleiben. + - Test 8 (CRLF): Eingabe mit `\r\n` -> live entfernt den Abschnitt ebenfalls, Ergebnis enthaelt kein `\r`. + - Test 9 (Einbettung): `vi.stubEnv('TESSERA_CHANGELOG_MD', '# T\n\n## 1.0.0 – 2026-01-01\n\n- x')` + `vi.resetModules()` + dynamischer Import -> `changelogMarkdown` ist exakt dieser Wert. + - Test 10 (ohne Variable): `vi.stubEnv('TESSERA_CHANGELOG_MD', '')` bzw. unset -> `changelogMarkdown === ''`. + changelog-page.test.tsx (3 Tests; Mocks: `next-intl/server` getTranslations mit Handtabelle fuer `title`, `intro`, `unreleasedHeading`, `unreleasedHint`, `empty`; `@/lib/app-version` mit veraenderbarem `appVersion.channel`; `@/lib/changelog` per `vi.doMock` mit `importOriginal` und eigenem `changelogMarkdown`; `@uiw/react-md-editor` als Modul mit `default: { Markdown: ({ source }) =>
{source}
}`; `next-themes` `useTheme: () => ({ resolvedTheme: 'light' })`; Seite wie module-access-gate.test awaiten und rendern): + - Test 1 (live): H1 „Was ist neu“ sichtbar, `data-testid="changelog-unreleased-hint"` fehlt, `md`-Text enthaelt `1.0.0`, aber weder `Unveröffentlicht` noch `Noch nicht freigegeben`. + - Test 2 (beta): Hinweis vorhanden (Text aus Tabelle), `md`-Text enthaelt `Noch nicht freigegeben (Beta)`. + - Test 3 (leer): `changelogMarkdown = ''` -> `data-testid="changelog-empty"` mit Leer-Text sichtbar, kein `md`-Element. + app-version-badge.test.tsx (+2, plus `next/link`-Mock, der `href`, `className`, `title`, `aria-label`, `data-testid` durchreicht; `whatsNew: 'Was ist neu'` in der next-intl-Tabelle): + - Test 5: `screen.getByTestId('app-version')` hat `href="/changelog"` (Tag `a`). + - Test 6: `aria-label` ist `Was ist neu`; Tests 1-4 bleiben gruen (Text, title mit/ohne API, dev ohne title). +
+ + Schritt A — RED: die drei Testdateien gemaess `` anlegen bzw. ergaenzen; `pnpm -C apps/web exec vitest run src/lib/changelog.test.ts "src/app/(portal)/changelog" src/components/layout/app-version-badge.test.tsx` muss rot sein (Modul fehlt / kein Link), Ausgabe fuer das SUMMARY notieren. + + Schritt B — `CHANGELOG.md` (Wurzel, UTF-8, echte Umlaute, Sie-Form, Alltagssprache; keine Dateinamen, keine Commit-Kuerzel, keine unerklaerten Fachbegriffe). Aufbau: `# Änderungen an Tessera`; Vorspann (2-3 Saetze: was die Liste ist, neueste Version oben, „Unveröffentlicht“ = nur in der Beta enthalten); `## Unveröffentlicht` mit `### Neu` (Punkt 1: die Seite „Was ist neu“ — ein Klick auf die Versionsnummer unten in der Seitenleiste zeigt diese Liste; auf Live nur Freigegebenes. Punkt 2: die Uhr-Schriftgroesse in Punkt aus bwo-Punkt 3) und `### Geändert` (bwo-Punkte 1, 2, 4, 5 sprachlich geglaettet, echte Umlaute); dann `## 1.0.0 – 2026-09-15` (Gedankenstrich U+2013, keine eckigen Klammern) mit `### Neu` und 11 Punkten, JEDER aus den Handbuechern belegt: (1) Portal mit Kopfleiste und Seitenleiste — Dashboard, Marktplatz, freigegebene Module nach Kategorien mit Suchfeld, Seitenleiste ein-/ausklappbar, hell/dunkel/System, Deutsch/Englisch; (2) persoenliches Dashboard mit frei anordenbaren Kacheln: Uhr, Suchleiste, Kalender, Notizen, Taschenrechner, Favoriten, Link, Stoppuhr; Bearbeitungsmodus (hinzufuegen, verschieben, Groesse ziehen), Einstellungen je Kachel unter Einstellungen > Dashboard (Kalenderquellen, Suchanbieter, Links); (3) Marktplatz mit Status Aktiviert/Verfuegbar, Suche, Filter, Detailseite; Aktivierung durch Administratoren, Freigabe je Gruppe oder Benutzer (Freigaben-Matrix); (4) Ausschreibungs-Radar: Trefferliste oeffentlicher Ausschreibungen, Filter (Frist, Postleitzahl, Bundesland, Branche, Wert), Suchprofile mit Sofort-Alarm per E-Mail, Sammel-Mail taeglich/woechentlich, Merken/Gelesen, eigene Postfaecher und RSS-Feeds als Quellen; (5) DKV-Rechnung: automatische Verarbeitung von DKV-Tankkarten-Rechnungen aus einem Postfach, Fahrzeug-Stammdaten mit CSV-Import, Verarbeitungshistorie, Exportdateien; (6) Zertifikat-Manager (analysieren, aufteilen, zusammenfuehren, konvertieren) und Domaincheck (Verfuegbarkeit von Internet-Domains); (7) Benutzer-, Gruppen- und Rechteverwaltung: Rollen Benutzer/Admin/Super-Admin, lokale und verzeichnisgefuehrte Konten, Gruppen mit Standardgruppe, Active-Directory-Anbindung mit Import von Gruppen und Einzelbenutzern, Ausschlussliste, automatische Synchronisation; (8) E-Mail-Versand (SMTP) mit Testnachricht, Passwort vergessen/zuruecksetzen per E-Mail, erzwungene Passwortaenderung bei neuen Konten; (9) persoenliche Einstellungen: Profilbild, Akzentfarbe, Passwort aendern fuer lokale Konten; (10) Knopf „Fehler melden“ mit Bildschirmfoto, Beschreibung und technischen Angaben per E-Mail an den Administrator; (11) Versionsanzeige unten in der Seitenleiste (Version und Kanal Live/Beta). Umlaute in dieser Aufzaehlung sind ASCII-Umschrift des Plans — in der Datei echte Umlaute. + + Schritt C — Bauweg. `.dockerignore`: direkt unter `*.md` die Zeile `!CHANGELOG.md` mit Kommentarzeile (Grund: Wurzel-Markdown ist ausgeschlossen, diese eine Datei braucht der Web-Bau). `apps/web/Dockerfile`: in der builder-Stufe nach `COPY tsconfig.base.json ./` genau eine Zeile `COPY CHANGELOG.md ./` plus Kommentar (quick-260916-dcz: next.config.ts liest die Datei zur Bauzeit; Ausnahme in .dockerignore). `apps/web/next.config.ts`: `import { readFileSync } from 'node:fs'`, `import path from 'node:path'`; Funktion `readChangelog(): string`, die `path.resolve(__dirname, '../../CHANGELOG.md')` liest und bei Fehler eine Error mit deutscher Meldung wirft (Dateipfad nennen, Hinweis auf .dockerignore-Ausnahme und COPY-Zeile); in `nextConfig` den Schluessel `env: { TESSERA_CHANGELOG_MD: readChangelog() }` ergaenzen; Kopfkommentar (deutsch, ASCII, 3-5 Zeilen): Bauzeit-Einbettung, gilt fuer webpack und Turbopack, Server-Bundle-only durch Importdisziplin (nur page.tsx importiert lib/changelog). + + Schritt D — `apps/web/src/lib/changelog.ts` (Kopfkommentar deutsch ASCII): `UNRELEASED_HEADING = 'Unveröffentlicht'`; `changelogMarkdown = process.env.TESSERA_CHANGELOG_MD ?? ''` mit vollem Literalnamen und Kommentar zur Literalname-Regel; `filterChangelogForChannel(markdown, channel, options?)`: Zeilenenden auf `\n` normalisieren; Zeilen ab der ersten `## `-Ueberschrift behalten (H1 und Vorspann verwerfen); Abschnitte an `^## `-Grenzen bilden; den Abschnitt mit Ueberschrift `^## Unveröffentlicht\s*$` suchen; „leer“ = kein Listenpunkt `^\s*[-*] ` im Abschnitt; leer ODER channel === 'live' -> Abschnitt verwerfen, `hasUnreleased=false`; sonst Ueberschriftszeile durch `## ${options?.unreleasedHeading ?? 'Noch nicht freigegeben (Beta)'}` ersetzen, `hasUnreleased=true`; Rueckgabe `{ markdown: abschnitte.join('\n').trim() + '\n', hasUnreleased }`. Keine Abhaengigkeit von React/Next im Modul (rein testbar). + + Schritt E — Seite. `apps/web/src/components/changelog/changelog-view.tsx`: 'use client'; `import MDEditor from '@uiw/react-md-editor'`, `import rehypeSanitize from 'rehype-sanitize'`, `useTheme` aus `next-themes`; `mounted`-Guard wie AppShell; `mode: 'light'|'dark'` = `resolvedTheme === 'dark' ? 'dark' : 'light'` (vor Mount 'light'); Wrapper `
` mit ``. `apps/web/src/app/(portal)/changelog/page.tsx`: async Server-Komponente ohne 'use client' (Vorbild module-access-gate.tsx); `const t = await getTranslations('changelog')`; `const { markdown, hasUnreleased } = filterChangelogForChannel(changelogMarkdown, appVersion.channel, { unreleasedHeading: t('unreleasedHeading') })`; Rahmen `
`, `

{t('title')}

`, `

{t('intro')}

`; wenn `hasUnreleased`: Hinweisbox (gelbes Muster aus change-password/page.tsx) mit `data-testid="changelog-unreleased-hint"` und `t('unreleasedHint')`; wenn `markdown.trim()` leer: `

{t('empty')}

`, sonst ``. Kopfkommentar: Kanalregel (Live ohne Unveröffentlicht), Quelle Bauzeit-Variable, Auth durch Middleware. + + Schritt F — Abzeichen. `app-version-badge.tsx`: `import Link from 'next/link'`; das `span` wird ``; Inhalt und Tooltip-Logik unveraendert; Kopfkommentar um den Satz ergaenzen, dass die Zeile seit quick-260916-dcz zur Seite „Was ist neu“ fuehrt. + + Schritt G — i18n (beide Dateien, Schluesselparitaet). de.json: `sidebar.whatsNew` = „Was ist neu“; neuer Top-Level-Namensraum `changelog` (nach `bugReport` einordnen): `title` „Was ist neu“, `intro` „Alle Änderungen an Tessera, sortiert nach Version – die aktuelle Version steht oben.“, `unreleasedHeading` „Noch nicht freigegeben (Beta)“, `unreleasedHint` „Die Punkte unter „Noch nicht freigegeben“ sind in dieser Beta bereits enthalten, aber noch nicht als Version freigegeben.“, `empty` „Noch keine Einträge vorhanden.“ en.json: `sidebar.whatsNew` „What's new“; `changelog`: `title` „What's new“, `intro` „All changes to Tessera, sorted by version – the current version is at the top.“, `unreleasedHeading` „Not yet released (beta)“, `unreleasedHint` „The items under “Not yet released” are already part of this beta but have not been released as a version yet.“, `empty` „No entries yet.“ (Sollte der ICU-Parser am Apostroph in „What's“ anstossen, „What is new“ verwenden.) Diese Texte brauchen keinen neuen Eintrag in `umlaut-dictionary.ts` — die Datei bleibt unangetastet. + + Schritt H — GREEN: Zielsuite gruen, dann die gesamte Web-Suite (46+2 Dateien) und `tsc`. Danach lokal `pnpm -C apps/web exec next build` einmal laufen lassen (webpack-Build, ca. 1-2 Minuten) und pruefen, dass `apps/web/.next/server` den Datums-Marker enthaelt und `apps/web/.next/static` nicht (Vorstufe zur Docker-Falsifizierung in Task 2). Commit `feat(quick-260916-dcz): CHANGELOG.md, Seite "Was ist neu" mit Kanalfilter, Versionszeile als Link, Bauzeit-Einbettung`. + + + cd /home/vicolab/projects/tessera-ctl && pnpm -C apps/web exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)" ; test -f CHANGELOG.md; echo CL_EXISTS=$? ; grep -c "^## Unveröffentlicht$" CHANGELOG.md ; grep -c "^## 1.0.0 – 2026-09-15$" CHANGELOG.md ; grep -c "^### " CHANGELOG.md ; awk '/^## 1\.0\.0/{f=1;next} /^## /{if(f)exit} f' CHANGELOG.md | grep -c "^- " ; grep -c "^!CHANGELOG.md$" .dockerignore ; grep -c "^COPY CHANGELOG.md ./$" apps/web/Dockerfile ; grep -c "TESSERA_CHANGELOG_MD" apps/web/next.config.ts ; grep -c "process.env.TESSERA_CHANGELOG_MD" apps/web/src/lib/changelog.ts ; grep -c "export function filterChangelogForChannel" apps/web/src/lib/changelog.ts ; grep -rl "@/lib/changelog'" apps/web/src --include=*.tsx --include=*.ts | grep -v test | grep -vc "changelog/page.tsx" ; grep -c "MDEditor.Markdown" apps/web/src/components/changelog/changelog-view.tsx ; grep -c "rehypeSanitize" apps/web/src/components/changelog/changelog-view.tsx ; head -1 "apps/web/src/app/(portal)/changelog/page.tsx" | grep -c "use client" ; grep -c 'href="/changelog"' apps/web/src/components/layout/app-version-badge.tsx ; grep -c "aria-label={t('whatsNew')}" apps/web/src/components/layout/app-version-badge.tsx ; grep -c '"whatsNew"' apps/web/src/messages/de.json ; grep -c '"unreleasedHint"' apps/web/src/messages/en.json ; D2=$(git diff --stat 7a6f42e -- apps/web/src/messages/umlaut-dictionary.ts apps/web/package.json pnpm-lock.yaml); echo D2_EXIT=$? ; test -z "$D2"; echo UNTOUCHED=$? ; pnpm -C apps/web exec tsc --noEmit >/dev/null 2>&1; echo TSC_web=$? + Die Vitest-Zeilen weichen von `48 passed (48)` / `301 passed (301)` ab; CL_EXISTS ist nicht 0; einer der greps auf CHANGELOG.md liefert nicht genau 1 (Unveröffentlicht, 1.0.0-Ueberschrift) bzw. weniger als 3 (`### `) bzw. weniger als 6 oder mehr als 12 (Punkte unter 1.0.0); `.dockerignore`- oder `Dockerfile`-grep ist nicht 1; ein Code-grep, der >= 1 sein muss, liefert 0; der Import-Zaehler von `@/lib/changelog` ausserhalb von page.tsx ist nicht 0 (Text wuerde ins Client-Bundle wandern); page.tsx beginnt mit `use client` (Zaehler 1 statt 0); UNTOUCHED ist 1; TSC_web ist nicht 0. + + Web-Suite 48/301 gruen, tsc 0; CHANGELOG.md mit Unveröffentlicht (bwo + diese Seite) und 1.0.0 (6-12 Punkte, aus den Handbuechern) vorhanden; Bauweg (dockerignore-Ausnahme, COPY, env in next.config.ts) steht; `/changelog` rendert als Server-Seite den kanalgefilterten Changelog ueber `MDEditor.Markdown` + rehype-sanitize; Versionszeile ist ein Link mit aria-label; de/en-Schluessel vollstaendig, Umlaut-Woerterbuch unangetastet; ein Commit `feat(quick-260916-dcz)`. + + + + Task 2: Release-Skript mit --dry-run, CI-Schritt, Docker-Falsifizierung, rueckwirkender Release v1.0.0 + .gitea/scripts/publish-release.sh, .gitea/workflows/ci.yml + + - .gitea/scripts/publish-images.sh (Kopfkommentar-Stil, Entscheidung anhand GITHUB_REF, `--print-plan`, `set -eu`, kein Secret) + - .gitea/workflows/ci.yml (Job `publish`, `${{ secrets.REGISTRY_TOKEN }}` nur im Login-Schritt) + - apps/web/Dockerfile (runner-Stufe: `/app/apps/web/.next/server` aus standalone, `/app/apps/web/.next/static` separat kopiert) + - .planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-PLAN.md (Task 2: lokaler Docker-Beweis, Task 3: Token aus pushurl, nie ausgeben) + - COVERAGE.md neben diesem Plan (welche Release-Endpunkte genutzt werden) + + Gitea antwortet lokal: `curl -s --max-time 5 http://localhost:3002/api/v1/version` liefert `{"version":"1.26.2"}`; `command -v jq` und `command -v docker` liefern Pfade; `git remote get-url --push origin` enthaelt `localhost:3002` (Token-Quelle fuer den echten Lauf — nie ausgeben). + + Schritt A — `.gitea/scripts/publish-release.sh` (POSIX sh, `chmod +x`, `set -eu`, Kopfkommentar deutsch ASCII im Stil von publish-images.sh: Zweck, Entscheidung anhand GITHUB_REF, Aufrufformen, Umgebungsvariablen, dass das Token nie ausgegeben wird). Verhalten: + 1. Argumente in einer `while`-Schleife: `--dry-run` (Schalter), `--tag ` (Wert), sonst Fehler mit Hilfetext Exit 2. + 2. Tag: aus `--tag`, sonst aus `GITHUB_REF` (`refs/tags/v*` -> Rest), sonst Meldung „Kein Freigabe-Tag (nur refs/tags/v*): nichts zu tun.“ und Exit 0. Tag muss `^v[0-9]+\.[0-9]+\.[0-9]+$` erfuellen (grep -E), sonst Exit 1 mit Meldung. `VERSION=${TAG#v}`. + 3. API-Basis: `GITEA_API`, sonst `GITHUB_API_URL`, sonst `${GITHUB_SERVER_URL}/api/v1`, sonst `http://localhost:3002/api/v1`; Repo: `GITEA_REPO`, sonst `GITHUB_REPOSITORY`, sonst `schalli/tessera-ctl`. Erste Ausgabezeile: `Gitea-API: Repo: Tag: ` (ohne Token). + 4. `CHANGELOG="${CHANGELOG_FILE:-CHANGELOG.md}"`; fehlt die Datei: Exit 1 mit Meldung. Abschnitt schneiden (zur Planungszeit bewiesenes Muster): `awk -v ver="$VERSION" 'BEGIN{esc=ver; gsub(/\./,"\\.",esc); pat="^## " esc "( |$)"} $0 ~ pat {f=1; next} /^## / {if(f) exit} f {print}'`, danach fuehrende/abschliessende Leerzeilen entfernen (zweiter awk, der die letzte nicht-leere Zeile merkt, plus `sed '1{/^$/d}'`). Ist das Ergebnis leer: nach stderr „CHANGELOG.md hat keinen Abschnitt fuer Version (erwartet eine Zeile '## – '). Kein Release ohne Text.“ und Exit 1 (Falsifizierung b). + 5. JSON ausschliesslich per `jq -n --arg tag "$TAG" --arg name "Tessera $VERSION" --arg body "$BODY" '{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}'` (kein manuelles Quoting). Fehlt `jq`: Exit 1 mit Meldung. + 6. `--dry-run`: JSON und die Zielpfade (`POST /repos//releases` bzw. `PATCH …/releases/`) ausgeben, Exit 0, kein Netzaufruf, kein Token noetig. + 7. Echter Lauf: `GITEA_TOKEN` muss gesetzt sein (sonst Exit 1 mit Meldung, Wert nie ausgeben). Header in eine temporaere Datei (`umask 077`, `mktemp`, `trap` zum Loeschen) schreiben und mit `curl -sS --header @"$HDR"` verwenden — das Token erscheint so weder in Argumenten noch in der Prozessliste. `GET /repos//releases/tags/` mit `-o "$RESP" -w '%{http_code}'`: 200 -> `ID=$(jq -r .id "$RESP")`, `PATCH …/releases/$ID` mit `{name, body}` (jq wie oben ohne tag_name), Erwartung 200 -> Ausgabe „Release aktualisiert (id )“; 404 -> `POST …/releases`, Erwartung 201 -> „Release angelegt (id …)“; jeder andere Code -> Code und Antwort-Body (der Body enthaelt nie das Token) nach stderr, Exit 1. `Content-Type: application/json`, `--data @"$JSONFILE"` (JSON in Datei, nicht als Argument). + 8. Das Skript kennt kein `set -x` und kein Echo einer Variablen mit dem Token. + + + Schritt B — `.gitea/workflows/ci.yml`: im Job `publish` nach dem Schritt „Versionsstempel berechnen, Abbilder bauen und veroeffentlichen“ einen Schritt `- name: Gitea-Release zum Freigabe-Tag anlegen (nur bei Tags v*)` mit `env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}` und `run: sh .gitea/scripts/publish-release.sh`. Kopfkommentar der Datei um eine Zeile ergaenzen (Tag v* -> zusaetzlich Gitea-Release aus CHANGELOG.md). Keine weitere Aenderung. `node -e "require('js-yaml')"` steht nicht sicher zur Verfuegung — YAML-Pruefung ueber `python3 -c 'import yaml'` nur, wenn PyYAML vorhanden ist, sonst genuegt die strukturelle grep-Pruefung unten. + + Schritt C — Lokale Beweise (Ausgaben ins SUMMARY): + 1. `sh .gitea/scripts/publish-release.sh --dry-run --tag v1.0.0` -> Exit 0, JSON mit `"name": "Tessera 1.0.0"` und Body, der mit `### Neu` beginnt. + 2. `sh .gitea/scripts/publish-release.sh --dry-run --tag v9.9.9` -> Exit 1, Meldung nennt 9.9.9 (Falsifizierung b). + 3. `GITHUB_REF=refs/heads/main sh .gitea/scripts/publish-release.sh --dry-run` -> Exit 0, „nichts zu tun“. + 4. `GITHUB_REF=refs/tags/v1.0.0 GITHUB_SERVER_URL=https://git.vicolab.de sh .gitea/scripts/publish-release.sh --dry-run` -> erste Zeile nennt `https://git.vicolab.de/api/v1`. + 5. Docker-Falsifizierung (c): `docker build -t tessera-web-dcz-test --build-arg APP_VERSION=v9.9.9-test --build-arg APP_CHANNEL=live -f apps/web/Dockerfile .` (2-4 Minuten; als Hintergrundbefehl starten, falls die Vordergrundzeit knapp ist). Danach `docker run --rm --entrypoint sh tessera-web-dcz-test -c 'grep -rl "2026-09-15" /app/apps/web/.next/server | wc -l; grep -rl "2026-09-15" /app/apps/web/.next/static | wc -l'` -> erste Zahl >= 1, zweite genau 0. + + Zusaetzlich Gegenprobe der COPY/dockerignore-Kopplung: `git stash`-frei pruefen, indem ein zweiter Bau mit `--build-arg` NICHT noetig ist — stattdessen im SUMMARY die gemessene Kontext-Meldung aus `docker build` (Zeile mit `COPY CHANGELOG.md`) zitieren. Test-Abbild danach `docker rmi tessera-web-dcz-test`. + 6. Echter Lauf fuer v1.0.0 (Token NIE ausgeben, nur in Variablen): `PUSHURL=$(git remote get-url --push origin); TOK=${PUSHURL#*://}; TOK=${TOK#*:}; TOK=${TOK%%@*}`; dann `GITEA_API=http://localhost:3002/api/v1 GITEA_TOKEN="$TOK" sh .gitea/scripts/publish-release.sh --tag v1.0.0` -> Exit 0, Zeile „Release v1.0.0 angelegt (id N)“. Pruefung: `curl -s -H "Authorization: token $TOK" -o rel.json http://localhost:3002/api/v1/repos/schalli/tessera-ctl/releases/tags/v1.0.0; jq -c '{id, tag_name, name, draft, prerelease}' rel.json; jq -r .body rel.json` (Datei im Scratchpad) -> `tag_name v1.0.0`, `name "Tessera 1.0.0"`, `draft false`, Body beginnt mit `### Neu` und ist laenger als 100 Zeichen. Zweiter Lauf desselben Skriptaufrufs -> Exit 0, Zeile „aktualisiert (id N)“ mit derselben id; `…/releases` in eine Datei holen und `jq length` darauf -> 1 (idempotent, kein Duplikat). + Commit `ci(quick-260916-dcz): Gitea-Release je Freigabe-Tag aus CHANGELOG.md (publish-release.sh, idempotent, --dry-run), Schritt in ci.yml`. + + + cd /home/vicolab/projects/tessera-ctl && test -x .gitea/scripts/publish-release.sh; echo EXEC=$? ; head -1 .gitea/scripts/publish-release.sh | grep -c '^#!/bin/sh$' ; grep -c "^set -eu$" .gitea/scripts/publish-release.sh ; grep -c "set -x" .gitea/scripts/publish-release.sh ; grep -cE 'echo[^\n]*GITEA_TOKEN' .gitea/scripts/publish-release.sh ; grep -c 'jq -n --arg' .gitea/scripts/publish-release.sh ; grep -c 'header @' .gitea/scripts/publish-release.sh ; grep -c 'releases/tags/' .gitea/scripts/publish-release.sh ; grep -c 'PATCH' .gitea/scripts/publish-release.sh ; sh .gitea/scripts/publish-release.sh --dry-run --tag v1.0.0 >/dev/null 2>&1; echo DRY_OK=$? ; sh .gitea/scripts/publish-release.sh --dry-run --tag v9.9.9 >/dev/null 2>/tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-dry-err.txt; echo DRY_MISSING=$? ; grep -c "9.9.9" /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-dry-err.txt ; GITHUB_REF=refs/heads/main sh .gitea/scripts/publish-release.sh --dry-run | grep -c "nichts zu tun" ; sh .gitea/scripts/publish-release.sh --dry-run --tag v1.0.0 | grep -c '"name": "Tessera 1.0.0"' ; grep -c "publish-release.sh" .gitea/workflows/ci.yml ; grep -c "GITEA_TOKEN: \${{ secrets.REGISTRY_TOKEN }}" .gitea/workflows/ci.yml ; grep -c "secrets.REGISTRY_TOKEN" .gitea/workflows/ci.yml ; PUSHURL=$(git remote get-url --push origin); echo PU_EXIT=$? ; TOK=${PUSHURL#*://}; TOK=${TOK#*:}; TOK=${TOK%%@*}; curl -s -H "Authorization: token $TOK" -o /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rel.json http://localhost:3002/api/v1/repos/schalli/tessera-ctl/releases/tags/v1.0.0; echo REL_EXIT=$? ; jq -r '.tag_name, .name, .draft' /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rel.json ; jq -r '.body' /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rel.json > /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rel-body.txt; wc -c < /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rel-body.txt ; curl -s -H "Authorization: token $TOK" -o /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rels.json http://localhost:3002/api/v1/repos/schalli/tessera-ctl/releases; jq length /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/d06a4a73-407a-4ea6-b686-d1f72c209902/scratchpad/dcz-rels.json + EXEC ist nicht 0; PU_EXIT oder REL_EXIT ist nicht 0; Shebang- oder `set -eu`-Zaehler ist nicht 1; der `set -x`-Zaehler oder der Echo-Token-Zaehler ist nicht 0; jq-/header-/tags-/PATCH-greps liefern 0; DRY_OK ist nicht 0; DRY_MISSING ist 0 (ein Tag ohne Abschnitt darf nicht durchgehen) oder die stderr-Datei nennt 9.9.9 nicht; „nichts zu tun“ fehlt fuer refs/heads/main; der Name-grep im dry-run-JSON ist 0; ci.yml nennt das Skript nicht genau einmal, den env-Eintrag nicht genau einmal oder REGISTRY_TOKEN nicht genau zweimal (Login + Release); die drei Release-Zeilen lauten nicht `v1.0.0`, `Tessera 1.0.0`, `false`; die Body-Laenge (wc -c) ist nicht groesser als 100; die Release-Anzahl ist nicht 1. + + Skript vorhanden und POSIX-sauber; dry-run fuer v1.0.0 liefert JSON, fuer v9.9.9 Exit 1 mit klarer Meldung; main-Ref ist ein No-Op; ci.yml ruft das Skript nach dem Abbild-Schritt mit dem Token aus `env` auf; das Web-Abbild traegt den Changelog nur im Server-Bundle (Docker-Beweis im SUMMARY mit beiden Zahlen); Release `Tessera 1.0.0` existiert in Gitea zum Tag v1.0.0, ein Wiederholungslauf aktualisiert statt dupliziert; ein Commit `ci(quick-260916-dcz)`. + + + + Task 3: Handbuecher (Betrieb Kapitel 9, Anwender, Entwicklung, CI-Setup), Push und Beobachtung des CI-Laufs + docs/anleitung-betrieb.md, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md, docs/ci-cd-setup.md + + - docs/anleitung-betrieb.md Kapitel 9 (Zeilen 357-530: „Eine Version freigeben“, „Woran Sie erkennen, welche Version läuft“, Absatz „Erstfreigabe v1.0.0“) — Ton: Alltagssprache, echte Umlaute, Sie-Form + - docs/anleitung-anwender.md (Inhaltsverzeichnis Zeilen 6-22, „Aufbau der Oberfläche“ Zeilen 38-55, Abschnittsfolge vor „Häufige Stolpersteine“) + - docs/anleitung-entwicklung.md „Konventionen und Fallstricke“ (ab Zeile 425) — Ton: technisch, echte Umlaute + - docs/ci-cd-setup.md Abschnitte 3 (Secrets-Tabelle) und 4 (Pipeline-Ueberblick, Etiketten-Tabelle) — ASCII-Umschrift wie im Bestand + - .planning/quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/260914-ku1-PLAN.md Task 3 Schritt 4 (CI-Beobachtung ueber die Gitea-API) + + Gitea antwortet lokal (`curl -s --max-time 5 http://localhost:3002/api/v1/version` -> `{"version":"1.26.2"}`) und `docker ps --format '{{.Names}}' | grep -c '^gitea-runner$'` liefert `1` (sonst Push trotzdem, Beobachtung als offenen Punkt ins SUMMARY). + + 1. `docs/anleitung-betrieb.md`, Kapitel 9: (a) Unterabschnitt „Eine Version freigeben“ — vor dem Befehlsblock einen nummerierten Vorschritt einfuegen: in `CHANGELOG.md` den Abschnitt „Unveröffentlicht“ in „X.Y.Z – JJJJ-MM-TT“ umbenennen, darueber ein neues leeres „Unveröffentlicht“ anlegen, auf `main` committen und pushen — erst dann `live` zusammenfuehren und taggen; (b) nach dem Absatz zur Pipeline-Dauer einen Absatz: die Pipeline legt beim Tag zusaetzlich einen Release in Gitea an (Name „Tessera X.Y.Z“, Text = der Abschnitt dieser Version aus CHANGELOG.md, zu finden unter Releases im Repository); fehlt der Abschnitt, schlaegt genau dieser letzte Schritt fehl — die Abbilder sind dann trotzdem gebaut, der Release wird nach dem Nachtragen des Abschnitts durch erneutes Ausloesen des Tags-Laufs oder lokal per Skript (`.gitea/scripts/publish-release.sh --tag vX.Y.Z`) nachgeholt; (c) Absatz „Erstfreigabe v1.0.0“ in die Vergangenheit setzen: erfolgt am 2026-09-14 (Tag), live seit 2026-09-15; der Release „Tessera 1.0.0“ wurde nachtraeglich angelegt; (d) unter „Woran Sie erkennen, welche Version läuft“ einen vierten Punkt: Klick auf die Versionszeile unten in der Seitenleiste oeffnet „Was ist neu“ — auf Live nur freigegebene Versionen, auf Beta zusaetzlich „Noch nicht freigegeben (Beta)“. Echte Umlaute, Sie-Form. + 2. `docs/anleitung-anwender.md`: (a) in „Aufbau der Oberfläche“, Absatz Seitenleiste, einen Satz ergaenzen: ganz unten steht die Versionsnummer von Tessera; ein Klick darauf oeffnet „Was ist neu“; (b) neuen Abschnitt `## Was ist neu` VOR „Häufige Stolpersteine“ (4-6 Saetze: was die Seite zeigt, Gruppen Neu/Geändert/Behoben, neueste Version oben, Hinweis „Noch nicht freigegeben (Beta)“ nur auf der Beta, auf Live nur Freigegebenes); (c) Inhaltsverzeichnis-Eintrag an passender Stelle. Echte Umlaute, Sie-Form. + 3. `docs/anleitung-entwicklung.md`, „Konventionen und Fallstricke“: neuen Fettabsatz „**Änderungsliste (`CHANGELOG.md`):**“ — jede Aenderung sofort unter „Unveröffentlicht“ eintragen (Alltagssprache fuer Anwender, Sie-Form, echte Umlaute, Gruppen Neu/Geändert/Behoben, keine Dateinamen/Commit-Kuerzel); Freigabe = Abschnitt umbenennen + neues leeres Unveröffentlicht; die Seite „Was ist neu“ (`apps/web/src/app/(portal)/changelog/page.tsx`) liest den Text zur Bauzeit aus `env.TESSERA_CHANGELOG_MD` in `next.config.ts` (deshalb `COPY CHANGELOG.md` im Web-Dockerfile und die Ausnahme `!CHANGELOG.md` in `.dockerignore`; nur `page.tsx` darf `@/lib/changelog` importieren, damit der Text nicht in oeffentliche Client-Chunks gelangt); Kanalregel in `filterChangelogForChannel` mit Tests; `.gitea/scripts/publish-release.sh` schneidet beim Tag den Abschnitt fuer den Gitea-Release — ohne Abschnitt bricht der CI-Schritt ab. Echte Umlaute. + 4. `docs/ci-cd-setup.md` (ASCII-Umschrift wie im Bestand): Secrets-Tabelle — `REGISTRY_TOKEN` braucht zusaetzlich Schreibrecht auf das Repository (`repository: write`) fuer Releases, wird im Release-Schritt ueber `env` an das Skript gereicht, nie als Argument; Pipeline-Ueberblick — Job `publish` besteht aus vier Schritten (Checkout, Login, publish-images.sh, publish-release.sh); Etiketten-Tabelle — Zeile Tag `vX.Y.Z` ergaenzen um „+ Gitea-Release `Tessera X.Y.Z` mit dem CHANGELOG-Abschnitt“; kurzer Hinweis, dass das Skript die API ueber `GITHUB_API_URL`/`GITHUB_SERVER_URL` (im Job-Container `https://git.vicolab.de`) anspricht und `localhost:3002` dort nicht erreichbar ist. + 5. Commit `docs(quick-260916-dcz): Betriebshandbuch Kapitel 9 (Changelog-Schritt, Gitea-Release, Seite Was ist neu), Anwender-, Entwicklungs- und CI-Handbuch`. Dann `git push` (Push-URL zeigt auf localhost:3002; schlichtes `git push` genuegt). + 6. Beobachtung des CI-Laufs (Token NIE ausgeben): `PUSHED=$(git rev-parse HEAD); PUSHURL=$(git remote get-url --push origin); TOK=${PUSHURL#*://}; TOK=${TOK#*:}; TOK=${TOK%%@*}`; 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 (als Hintergrundbefehl, falls `sleep` im Vordergrund blockiert ist), Eintrag mit `head_sha == PUSHED`, auf `status == completed` warten; Erwartung `conclusion == success`. Zusaetzlich versuchen, das Job-Log des Jobs `publish` zu lesen (`…/actions/runs//jobs`, dann `…/actions/jobs//logs`, falls die Gitea-Version antwortet) und die Zeilen `Gitea-API: https://git.vicolab.de/api/v1 …` und „nichts zu tun“ des Release-Schritts zitieren; antwortet der Log-Endpunkt nicht, im SUMMARY vermerken (der Erfolg des Laufs beweist den Exit 0 des Schritts). Lauf-ID, Dauer (`started_at`/`completed_at`), conclusion ins SUMMARY. Ist `conclusion` nicht `success`: Ursache aus dem Job-Log benennen, Korrektur als eigener `fix(quick-260916-dcz)`-Commit, erneut pushen und beobachten. + 7. Wird das SUMMARY erst nach dem Push committet, den Push danach wiederholen (ein weiterer CI-Lauf ist erwartet). + + + cd /home/vicolab/projects/tessera-ctl && grep -c "CHANGELOG.md" docs/anleitung-betrieb.md ; grep -c "Was ist neu" docs/anleitung-betrieb.md ; grep -c "Release" docs/anleitung-betrieb.md ; grep -c "^## Was ist neu" docs/anleitung-anwender.md ; grep -c "Was ist neu" docs/anleitung-anwender.md ; grep -c "Änderungsliste" docs/anleitung-entwicklung.md ; grep -c "TESSERA_CHANGELOG_MD" docs/anleitung-entwicklung.md ; grep -c "publish-release.sh" docs/ci-cd-setup.md ; grep -c "repository: write" docs/ci-cd-setup.md ; grep -c '[äöüÄÖÜß]' docs/ci-cd-setup.md ; pnpm -C apps/web exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)" ; pnpm -C apps/api exec vitest run 2>&1 | grep -E "^\s+(Test Files|Tests)" ; for p in packages/shared apps/api apps/web; do pnpm -C $p exec tsc --noEmit >/dev/null 2>&1; echo "TSC_$p=$?"; done ; pnpm install --frozen-lockfile >/dev/null 2>&1; echo FROZEN=$? ; D=$(git diff --stat 7a6f42e -- . ':!.planning'); echo GIT_EXIT=$? ; tail -n1 <<< "$D" ; U=$(git diff --stat 7a6f42e -- '.env*' docker-compose.yml docker-compose.prod.yml docker-compose.dev.yml docker-compose.ci.yml pnpm-lock.yaml apps/web/package.json apps/api/package.json apps/api/prisma apps/api/Dockerfile apps/web/src/messages/umlaut-dictionary.ts); echo U_EXIT=$? ; test -z "$U"; echo U_EMPTY=$? ; S=$(git status -sb); head -n1 <<< "$S" + Ein grep auf die Handbuecher liefert 0 (betrieb: CHANGELOG.md >= 2, Was ist neu >= 1, Release >= 2; anwender: `## Was ist neu` genau 1, Was ist neu >= 2; entwicklung: Änderungsliste >= 1, TESSERA_CHANGELOG_MD >= 1; ci-cd-setup: publish-release.sh >= 2, repository: write >= 1) oder ci-cd-setup.md enthaelt echte Umlaute (Zaehler nicht 0); Web weicht von 48/301 oder API von 67/1078 ab; ein TSC_*, FROZEN oder GIT_EXIT ist nicht 0; die Summenzeile nennt nicht genau `19 files changed`; U_EMPTY ist 1 (eine unantastbare Datei wurde geaendert); die Statuszeile zeigt `[ahead` oder `[behind`. + + Vier Handbuecher auf dem gemessenen Stand (Kapitel 9 mit Changelog-Vorschritt, Release-Hinweis, Erstfreigabe in der Vergangenheit, vierter Erkennungsweg; Anwender-Abschnitt „Was ist neu“ mit TOC; Entwicklungsregel; CI-Setup ASCII); Suiten Web 48/301, API 67/1078; tsc 0 dreimal; frozen-lockfile 0; genau 19 Dateien ausserhalb `.planning`; Push erfolgt, CI-Lauf zum HEAD `success` (Lauf-ID, Dauer und Release-Schritt-Zeilen im SUMMARY); Commit `docs(quick-260916-dcz)`. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Repository-Datei -> Web-Bundle -> Browser des Anwenders | `CHANGELOG.md` (vertrauenswuerdige, versionierte Quelle) wird zu HTML gerendert; jeder Commit-Autor kann Markdown einbringen | +| Internet -> `/changelog` und `/_next/static/*` | Seite nur mit gueltigem Session-Cookie (Middleware); statische Chunks sind ohne Anmeldung abrufbar | +| Gitea-Repository -> CI-Job-Container -> Gitea-API | Beim Tag-Push haelt der Job das Repo-Token als Secret und ruft die Release-API ueber `https://git.vicolab.de` auf | +| CHANGELOG-Text -> JSON-Body -> Gitea-Release | Freitext (Anfuehrungszeichen, Backslashes, Zeilenumbrueche) wird in JSON und dann in Gitea-Markdown ueberfuehrt | +| Docker-Bau-Kontext -> Abbild | Eine zusaetzliche Wurzeldatei gelangt in den Kontext und in die builder-Stufe | + +## STRIDE Threat Register (ASVS Level 1, Blocking-Schwelle `high`) + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-DCZ-01 | Tampering (XSS ueber Markdown) | `changelog-view.tsx` (`MDEditor.Markdown`) | medium | mitigate | `rehypePlugins={[[rehypeSanitize]]}` wie im Notiz-Widget (Gate: grep `rehypeSanitize` in changelog-view.tsx >= 1); Quelle ist eine versionierte Repo-Datei, keine Nutzereingabe; kein `dangerouslySetInnerHTML` | +| T-DCZ-02 | Information Disclosure | Changelog-Text in `/_next/static` (ohne Auth abrufbar) | low | mitigate | Text nur im Server-Bundle: `@/lib/changelog` wird ausschliesslich von `page.tsx` (Server-Komponente) importiert (Gate Task 1: Import-Zaehler ausserhalb page.tsx = 0; Docker-Gate Task 2: `.next/static` enthaelt den Marker nicht) | +| T-DCZ-03 | Elevation / Zugriff | `/changelog` ohne Anmeldung | medium | mitigate | Bestehende `middleware.ts` (alle Routen ausser `/login`, `/reset-password`) — keine neue oeffentliche Route; Seitentest rendert unabhaengig davon, der Schutz liegt eine Schicht darueber und wird nicht geschwaecht (publicRoutes unveraendert, Gate: `git diff` auf middleware.ts leer, da nicht in files_modified) | +| T-DCZ-04 | Information Disclosure (Secret im Log) | `publish-release.sh`, `ci.yml` | high | mitigate | Token nur ueber `env: GITEA_TOKEN` (Gitea maskiert Secrets im Log), im Skript kein Shell-Tracing, kein Echo des Tokens, Header aus Datei (`--header @file`, nicht in argv/Prozessliste), JSON aus Datei; lokal: Token nur in Variablen, nie ausgegeben (Gates Task 2: `set -x`-Zaehler 0, Echo-Token-Zaehler 0, `header @` >= 1) | +| T-DCZ-05 | Tampering (JSON-/Body-Injection) | Release-Body aus CHANGELOG.md | medium | mitigate | JSON ausschliesslich per `jq -n --arg` (korrektes Escaping), keine String-Konkatenation; `--dry-run` zeigt das JSON vorab (Gate: `jq -n --arg` >= 1) | +| T-DCZ-06 | Denial of Service / Fehlbetrieb | Leerer oder falscher Release | low | mitigate | Tag-Regex `^v[0-9]+\.[0-9]+\.[0-9]+$`, Abschnittspflicht (Exit 1 ohne Text), idempotenter PATCH statt Duplikat (Gates Task 2: DRY_MISSING != 0, Release-Anzahl 1) | +| T-DCZ-07 | Spoofing (falsche API-Basis) | Skript im CI-Job-Container | medium | mitigate | API-Basis aus `GITHUB_API_URL`/`GITHUB_SERVER_URL` (https://git.vicolab.de, TLS), Fallback localhost nur lokal; erste Ausgabezeile nennt die Basis; Token geht nur an diese Basis | +| T-DCZ-08 | Repudiation | Welcher Text zu welcher Version gehoert | low | accept | Release-Text stammt aus der versionierten Datei am getaggten Commit; Aenderungen sind ueber Git nachvollziehbar | +| T-DCZ-09 | Information Disclosure (Bau-Kontext) | `.dockerignore`-Ausnahme | low | mitigate | Ausnahme gilt genau fuer `!CHANGELOG.md` (Gate: exakte Zeile), `*.md` bleibt ausgeschlossen, keine `.env`-Aenderung | +| T-DCZ-SC | Tampering (Supply Chain) | npm-Installs | high | mitigate | Keine neuen Pakete: `MDEditor.Markdown` aus dem installierten `@uiw/react-md-editor` 4.1.1, `rehype-sanitize` vorhanden; Gate `pnpm install --frozen-lockfile` Exit 0 und `pnpm-lock.yaml`/`package.json` unangetastet — daher kein Legitimitaets-Checkpoint noetig | + + + +- `pnpm -C apps/web exec vitest run` -> `Test Files 48 passed (48)` / `Tests 301 passed (301)`; `pnpm -C apps/api exec vitest run` -> `67 passed (67)` / `1078 passed (1078)`. +- `tsc --noEmit` in packages/shared, apps/api, apps/web -> Exit 0; `pnpm install --frozen-lockfile` -> Exit 0. +- `git diff --stat 7a6f42e -- . ':!.planning'` -> genau `19 files changed`; `.env*`, Compose, Prisma, Lockfile, package.json, umlaut-dictionary.ts unangetastet. +- Falsifizierung (a): Test 1 in `changelog.test.ts` (live) wird rot, wenn `filterChangelogForChannel` den Abschnitt nicht mehr entfernt (Probe im SUMMARY: Funktion kurzzeitig auf Durchreichen gesetzt -> genau dieser Test rot, danach zurueck). +- Falsifizierung (b): `sh .gitea/scripts/publish-release.sh --dry-run --tag v9.9.9` -> Exit 1, Meldung nennt 9.9.9. +- Falsifizierung (c): lokal gebautes Web-Abbild: `grep -rl "2026-09-15" /app/apps/web/.next/server | wc -l` >= 1 und dasselbe fuer `.next/static` = 0. +- Gitea: `GET /repos/schalli/tessera-ctl/releases/tags/v1.0.0` -> `Tessera 1.0.0`, `draft false`, Body > 100 Zeichen; Release-Anzahl 1. +- CI-Lauf zum gepushten HEAD: `conclusion == success`. +- Browser-Gegenprobe (Playwright MCP, nur wenn ein Dev-Stack laeuft; sonst als offenen Punkt vermerken): Klick auf die Versionszeile fuehrt zu `/changelog`, Seite zeigt „Was ist neu“ mit Hinweis „Noch nicht freigegeben (Beta)“ (lokal Kanal dev). + + + +- CHANGELOG.md mit `Unveröffentlicht` (Dashboard-Umbau + Seite „Was ist neu“) und `1.0.0 – 2026-09-15` (6-12 Punkte aus den Handbuechern), Alltagssprache, echte Umlaute, Sie-Form. +- Seite `/changelog` (nur angemeldet, Portal-Layout) rendert den Changelog ueber `MDEditor.Markdown` + rehype-sanitize; Live sieht kein `Unveröffentlicht`, Beta/dev sehen „Noch nicht freigegeben (Beta)“ mit Hinweis; Versionszeile ist ein Link mit aria-label „Was ist neu“; de/en vollstaendig. +- Bauweg: `env.TESSERA_CHANGELOG_MD` in next.config.ts, `COPY CHANGELOG.md ./` im Web-Dockerfile, `!CHANGELOG.md` in .dockerignore; Docker-Beweis Server-Bundle ja / statische Chunks nein. +- `publish-release.sh` (dry-run, idempotent, Exit 1 ohne Abschnitt) + ci.yml-Schritt; Release `Tessera 1.0.0` in Gitea. +- Handbuecher aktualisiert; alle Zahlen-Gates erfuellt; gepusht; CI gruen. + + + +Nach Abschluss `.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-SUMMARY.md` anlegen (Deutsch, ASCII): Messwerte aller Gates (Vitest-Zeilen, tsc, 19 files changed), RED-Ausgabe aus Task 1, Docker-Beweis (beide Zahlen, Baudauer), dry-run-Ausgaben, Release-Antwort (id, name, body_len) und PATCH-Wiederholung, CI-Lauf (ID, Dauer, conclusion, Release-Schritt-Zeilen oder Grund, warum das Log nicht lesbar war), Abschnitt „Fuer den Changelog“ entfaellt (die Punkte stehen bereits in CHANGELOG.md unter Unveröffentlicht), offene Punkte (z. B. Browser-Gegenprobe, CI-Beweis des Release-Wegs erst beim naechsten Tag). + diff --git a/.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/COVERAGE.md b/.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/COVERAGE.md new file mode 100644 index 0000000..69a382a --- /dev/null +++ b/.planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/COVERAGE.md @@ -0,0 +1,15 @@ +# API Coverage — Gitea REST API v1, Bereich Releases (quick-260916-dcz) + +> Full coverage by default. Opt-outs are explicit, reasoned decisions. +> Gemessen 2026-09-16: Gitea 1.26.2 auf localhost:3002 (intern) bzw. https://git.vicolab.de (aus dem CI-Job-Container erreichbar, localhost:3002 dort NICHT). Token aus der Push-URL und `secrets.REGISTRY_TOKEN` tragen `write:repository` (deckt Releases ab). + +| capability | decision | reason | +|---|---|---| +| `GET /repos/{owner}/{repo}/releases/tags/{tag}` (Release zum Tag lesen) | INTEGRATE | Idempotenz-Pruefung vor dem Anlegen | +| `POST /repos/{owner}/{repo}/releases` (Release anlegen) | INTEGRATE | Kernfall beim Tag-Push | +| `PATCH /repos/{owner}/{repo}/releases/{id}` (Release aktualisieren) | INTEGRATE | Wiederholter Lauf zum selben Tag haelt Name/Text mit CHANGELOG.md synchron | +| `GET /repos/{owner}/{repo}/releases` (alle Releases listen) | OPT-OUT | nicht noetig — die Abfrage je Tag genuegt | +| `GET /repos/{owner}/{repo}/releases/latest` | OPT-OUT | nicht noetig — die Oberflaeche zeigt den Changelog aus dem Bundle, nicht aus Gitea | +| `DELETE /repos/{owner}/{repo}/releases/{id}` | OPT-OUT | ausdruecklich ausserhalb des Auftrags — Releases werden nie automatisch geloescht | +| Release-Anhaenge (`.../releases/{id}/assets`) | OPT-OUT | nicht noetig — Auslieferung laeuft ueber die Container-Registry, nicht ueber Anhaenge | +| Draft/Prerelease-Markierungen | OPT-OUT | nicht noetig — jeder Tag `v*` ist eine Freigabe; `draft:false`, `prerelease:false` fest |