4 Commits

Author SHA1 Message Date
schalli c5f4adeeed docs(quick-260916-dcz): Betriebshandbuch Kapitel 9 (Changelog-Schritt, Gitea-Release, Seite Was ist neu), Anwender-, Entwicklungs- und CI-Handbuch
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 1m0s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m51s
- Betrieb Kapitel 9: Vorschritt CHANGELOG.md vor dem Tag, automatischer Gitea-Release samt Verhalten bei fehlendem Abschnitt, Erstfreigabe v1.0.0 in der Vergangenheit, vierter Erkennungsweg "Was ist neu"
- Anwender: Satz zur Versionszeile in "Aufbau der Oberflaeche", neuer Abschnitt "Was ist neu" vor den Stolpersteinen, Inhaltsverzeichnis; Abschnitt "Dashboard" (dyv) unangetastet
- Entwicklung: Regel "Aenderungsliste" unter Konventionen und Fallstricke (Bauzeit-Einbettung, Importdisziplin, Kanalregel, Release-Skript)
- CI-Setup (ASCII): REGISTRY_TOKEN mit repository: write, vier Schritte im Job publish, Release je Tag, API-Basis im Job-Container

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-16 11:25:10 +02:00
schalli 6940bd05a1 ci(quick-260916-dcz): Gitea-Release je Freigabe-Tag aus CHANGELOG.md (publish-release.sh, idempotent, --dry-run), Schritt in ci.yml
- publish-release.sh: POSIX sh, Entscheidung anhand GITHUB_REF wie publish-images.sh, --tag/--dry-run, Abschnitt per awk, JSON nur per jq --arg, Token nur aus GITEA_TOKEN ueber Header-Datei, GET/tags -> PATCH oder POST, Exit 1 ohne Abschnitt
- ci.yml: vierter Schritt im Job publish mit GITEA_TOKEN aus secrets.REGISTRY_TOKEN ueber env
- Bewiesen: dry-run v1.0.0 (Body 2227 Zeichen), v9.9.9 Exit 1, main nichts zu tun, API-Basis aus GITHUB_SERVER_URL; Docker-Abbild traegt den Changelog nur im Server-Bundle (1/0); Release Tessera 1.0.0 (id 1) angelegt, zweiter Lauf PATCH, genau ein Release

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-16 11:24:20 +02:00
schalli ba06db98c5 feat(quick-260916-dcz): CHANGELOG.md, Seite "Was ist neu" mit Kanalfilter, Versionszeile als Link, Bauzeit-Einbettung
- CHANGELOG.md (Wurzel): Unveroeffentlicht (bwo + dyv + diese Seite, 8 Punkte) und 1.0.0 – 2026-09-15 (11 Punkte aus den Handbuechern), Alltagssprache, echte Umlaute
- next.config.ts liest CHANGELOG.md zur Bauzeit nach env.TESSERA_CHANGELOG_MD; .dockerignore-Ausnahme !CHANGELOG.md und COPY-Zeile im Web-Dockerfile
- lib/changelog.ts: filterChangelogForChannel (live ohne Unveroeffentlicht, beta/dev mit "Noch nicht freigegeben (Beta)", leerer Abschnitt ausgeblendet), 10 Tests
- Server-Seite /changelog (MDEditor.Markdown + rehype-sanitize, Hinweis auf Beta, Leer-Text), 3 Tests; Versionszeile ist Link mit aria-label, 2 Tests
- de/en: sidebar.whatsNew und Namensraum changelog; Web-Suite 49/309, tsc 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-16 11:20:22 +02:00
schalli 0db21627f5 docs(quick-260916-dcz): Plan revidiert (Runde 1) — Bezugspunkt 963fa36, Nachbesserung 260916-dyv in Unveroeffentlicht
Bezugs-Commit aller git-diff-Gates 7a6f42e -> 963fa36; Baseline Web 46/286 -> 47/294
(frisch gemessen), Zielzahlen 48/301 -> 49/309; Unveroeffentlicht = bwo + dyv + Seite
"Was ist neu" als eine Liste (2 Neu + 6 Geaendert) mit eigenem Zaehl-Gate; Hinweis,
dass der dyv-Stand des Dashboard-Abschnitts im Anwenderhandbuch unangetastet bleibt.
Von den 19 Plan-Dateien hat dyv nur de.json/en.json (dragHint) und
anleitung-anwender.md (Abschnitt Dashboard) beruehrt — Einfuegepunkte bleiben gueltig.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-16 11:07:09 +02:00
20 changed files with 866 additions and 36 deletions
+3
View File
@@ -5,4 +5,7 @@ dist
.git
.env
*.md
# quick-260916-dcz: Wurzel-Markdown bleibt draussen, nur diese eine Datei braucht der Web-Bau
# (apps/web/next.config.ts liest sie zur Bauzeit, COPY im Web-Dockerfile).
!CHANGELOG.md
coverage
+155
View File
@@ -0,0 +1,155 @@
#!/bin/sh
# publish-release.sh -- Gitea-Release je Freigabe-Tag aus CHANGELOG.md anlegen
# (quick-260916-dcz).
#
# Entscheidung wie publish-images.sh allein anhand GITHUB_REF:
# refs/tags/vX.Y.Z -> Abschnitt "## X.Y.Z" aus CHANGELOG.md schneiden und als
# Release "Tessera X.Y.Z" anlegen (bzw. aktualisieren, wenn
# der Release zum Tag schon existiert -- idempotent)
# alles andere -> nichts zu tun (Exit 0)
#
# Aufrufformen:
# sh .gitea/scripts/publish-release.sh # im CI, Tag aus GITHUB_REF
# sh .gitea/scripts/publish-release.sh --tag v1.0.0 # lokal, expliziter Tag
# sh .gitea/scripts/publish-release.sh --dry-run --tag v1.0.0 # nur JSON und Ziel zeigen
#
# Umgebung:
# GITEA_TOKEN Zugriffstoken (Pflicht im echten Lauf; im CI aus secrets.REGISTRY_TOKEN
# ueber `env`). Wird nie ausgegeben und nie als Argument uebergeben --
# der Authorization-Header kommt aus einer temporaeren Datei.
# GITEA_API API-Basis; sonst GITHUB_API_URL, sonst GITHUB_SERVER_URL/api/v1,
# sonst http://localhost:3002/api/v1 (nur lokal erreichbar).
# GITEA_REPO owner/repo; sonst GITHUB_REPOSITORY, sonst schalli/tessera-ctl.
# CHANGELOG_FILE Pfad zur Aenderungsliste; Vorgabe CHANGELOG.md.
#
# Fehlt der Abschnitt fuer die Version, endet das Skript mit Exit 1 -- es entsteht
# nie ein leerer Release. JSON wird ausschliesslich mit jq gebaut.
set -eu
usage() {
echo "Aufruf: publish-release.sh [--dry-run] [--tag vX.Y.Z]" >&2
}
DRY_RUN=0
TAG=""
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--tag)
[ $# -ge 2 ] || { usage; exit 2; }
TAG="$2"
shift
;;
*) usage; exit 2 ;;
esac
shift
done
if [ -z "$TAG" ]; then
REF="${GITHUB_REF:-}"
case "$REF" in
refs/tags/v*) TAG="${REF#refs/tags/}" ;;
*)
echo "Kein Freigabe-Tag (nur refs/tags/v*): nichts zu tun."
exit 0
;;
esac
fi
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Ungueltiger Tag '$TAG' (erwartet vX.Y.Z)." >&2
exit 1
fi
VERSION="${TAG#v}"
API="${GITEA_API:-${GITHUB_API_URL:-${GITHUB_SERVER_URL:+${GITHUB_SERVER_URL}/api/v1}}}"
API="${API:-http://localhost:3002/api/v1}"
API="${API%/}"
REPO="${GITEA_REPO:-${GITHUB_REPOSITORY:-schalli/tessera-ctl}}"
echo "Gitea-API: $API Repo: $REPO Tag: $TAG"
CHANGELOG="${CHANGELOG_FILE:-CHANGELOG.md}"
if [ ! -f "$CHANGELOG" ]; then
echo "$CHANGELOG nicht gefunden." >&2
exit 1
fi
command -v jq >/dev/null 2>&1 || { echo "jq fehlt." >&2; exit 1; }
# Abschnitt "## X.Y.Z" bis zur naechsten "## "-Ueberschrift, ohne die eigene
# Ueberschrift; danach fuehrende und abschliessende Leerzeilen entfernen.
BODY=$(awk -v ver="$VERSION" '
BEGIN { esc = ver; gsub(/\./, "\\.", esc); pat = "^## " esc "( |$)" }
$0 ~ pat { f = 1; next }
/^## / { if (f) exit }
f { print }
' "$CHANGELOG" | awk '
{ line[NR] = $0; if ($0 !~ /^[[:space:]]*$/) last = NR }
END { for (i = 1; i <= last; i++) print line[i] }
' | sed '1{/^$/d}')
if [ -z "$BODY" ]; then
echo "$CHANGELOG hat keinen Abschnitt fuer Version $VERSION (erwartet eine Zeile '## $VERSION – <Datum>'). Kein Release ohne Text." >&2
exit 1
fi
NAME="Tessera $VERSION"
CREATE_JSON=$(jq -n --arg tag "$TAG" --arg name "$NAME" --arg body "$BODY" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
UPDATE_JSON=$(jq -n --arg name "$NAME" --arg body "$BODY" '{name: $name, body: $body}')
RELEASES_URL="$API/repos/$REPO/releases"
TAG_URL="$API/repos/$REPO/releases/tags/$TAG"
if [ "$DRY_RUN" -eq 1 ]; then
echo "Probelauf (kein Netzaufruf):"
echo " POST $RELEASES_URL"
echo " PATCH $RELEASES_URL/<id> (falls GET $TAG_URL bereits 200 liefert)"
printf '%s\n' "$CREATE_JSON"
exit 0
fi
if [ -z "${GITEA_TOKEN:-}" ]; then
echo "Kein Zugriffstoken in der Umgebung gesetzt (siehe Kopfkommentar)." >&2
exit 1
fi
umask 077
TMPDIR_REL=$(mktemp -d)
trap 'rm -rf "$TMPDIR_REL"' EXIT INT TERM
HDR="$TMPDIR_REL/headers"
RESP="$TMPDIR_REL/response.json"
JSONFILE="$TMPDIR_REL/payload.json"
printf 'Authorization: token %s\nContent-Type: application/json\n' "$GITEA_TOKEN" > "$HDR"
CODE=$(curl -sS --header @"$HDR" -o "$RESP" -w '%{http_code}' "$TAG_URL")
case "$CODE" in
200)
ID=$(jq -r .id "$RESP")
printf '%s' "$UPDATE_JSON" > "$JSONFILE"
CODE=$(curl -sS --header @"$HDR" -X PATCH --data @"$JSONFILE" -o "$RESP" -w '%{http_code}' "$RELEASES_URL/$ID")
if [ "$CODE" = "200" ]; then
echo "Release $TAG aktualisiert (id $ID)"
else
echo "PATCH $RELEASES_URL/$ID antwortete mit $CODE:" >&2
cat "$RESP" >&2
exit 1
fi
;;
404)
printf '%s' "$CREATE_JSON" > "$JSONFILE"
CODE=$(curl -sS --header @"$HDR" -X POST --data @"$JSONFILE" -o "$RESP" -w '%{http_code}' "$RELEASES_URL")
if [ "$CODE" = "201" ]; then
echo "Release $TAG angelegt (id $(jq -r .id "$RESP"))"
else
echo "POST $RELEASES_URL antwortete mit $CODE:" >&2
cat "$RESP" >&2
exit 1
fi
;;
*)
echo "GET $TAG_URL antwortete mit $CODE:" >&2
cat "$RESP" >&2
exit 1
;;
esac
+6
View File
@@ -1,6 +1,7 @@
# Kanalmodell (quick-260914-ku1): main -> Kanal beta (Etiketten beta + latest);
# Tag v* -> Kanal live (Etiketten live + vX.Y.Z); Zweig live ohne Tag wird nur geprueft.
# Die Entscheidung trifft .gitea/scripts/publish-images.sh anhand GITHUB_REF.
# Tag v* (quick-260916-dcz): zusaetzlich Gitea-Release aus dem CHANGELOG.md-Abschnitt (publish-release.sh).
name: Tessera CI/CD
on:
@@ -66,3 +67,8 @@ jobs:
- name: Versionsstempel berechnen, Abbilder bauen und veroeffentlichen
run: sh .gitea/scripts/publish-images.sh
- name: Gitea-Release zum Freigabe-Tag anlegen (nur bei Tags v*)
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: sh .gitea/scripts/publish-release.sh
@@ -36,16 +36,16 @@ estimate:
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."
- "`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, der Nachbesserung 260916-dyv und der Seite „Was ist neu“ dieses Auftrags — zu EINER stimmigen Liste zusammengefuehrt, 8 Punkte), 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“)."
- "Baseline am Ende: Web `Test Files 49 passed (49)` / `Tests 309 passed (309)` (Planungszeit nach Revision 47/294 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 963fa36 -- . ':!.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`"
- "CHANGELOG.md — H1 `# Änderungen an Tessera`, Vorspann (2-3 Saetze), `## Unveröffentlicht` mit `### Neu` (2 Punkte) und `### Geändert` (6 Punkte, bwo + dyv zusammengefuehrt), `## 1.0.0 – 2026-09-15` mit `### Neu` (11 Punkte)"
- ".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() }`"
@@ -71,6 +71,8 @@ must_haves:
---
<objective>
Revidiert (Runde 1): Bezugspunkt `963fa36`, Baseline Web 47/294, Nachbesserung 260916-dyv im Abschnitt „Unveröffentlicht“ aufgenommen.
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.
@@ -86,6 +88,7 @@ Output: 19 Dateien (13 Code/Tests/Bau, 2 CI, 4 Handbuecher), drei Commits mit Sc
@.planning/STATE.md
@CLAUDE.md
@.planning/quick/260916-bwo-dashboard-feineres-raster-spalten-und-ze/260916-bwo-SUMMARY.md
@.planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-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
@@ -109,9 +112,10 @@ Output: 19 Dateien (13 Code/Tests/Bau, 2 CI, 4 Handbuecher), drei Commits mit Sc
</context>
<planning_measurements>
Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/main). Abweichungen vom Auftragstext sind mit „DELTA“ markiert.
**Revidiert (Runde 1, 2026-09-16):** Bezugspunkt ist jetzt HEAD `963fa36` (nach der Dashboard-Nachbesserung 260916-dyv: dc992c9, dbbd54f, cf97b5b, 8792819 + Akten 963fa36; Arbeitsbaum sauber, main == origin/main). Urspruenglich am 2026-09-16 an `7a6f42e` gemessen; alles unten gilt unveraendert, sofern nicht als Revision markiert. Abweichungen vom Auftragstext sind mit „DELTA“ markiert.
- Revision: dyv hat von den 19 Plan-Dateien nur `de.json`/`en.json` (je eine Zeile `dragHint` im Dashboard-Namensraum) und `docs/anleitung-anwender.md` (Abschnitt „Dashboard“, Zeilen 57-84: Stift-Schalter unten rechts, ganze Kachel ziehbar, Mindestgroessen) beruehrt — die Einfuegepunkte dieses Plans (`sidebar`/`changelog`-Namensraum; „Aufbau der Oberfläche“ Zeile 38-55, neuer Abschnitt vor „Häufige Stolpersteine“ Zeile 172, Inhaltsverzeichnis Zeilen 6-22) liegen ausserhalb und bleiben gueltig. `app-version-badge.tsx`, `sidebar.tsx`, `next.config.ts`, Dockerfile, `.dockerignore`, `.gitea/*`, die drei anderen Handbuecher: unveraendert (git diff 7a6f42e..963fa36 leer).
- Baseline: Web `46 files / 286 tests`, API `67 / 1078`, `tsc --noEmit` web/api/shared Exit 0 (frisch nachgemessen, stimmt mit dem Auftrag ueberein).
- Baseline (Revision Runde 1, gemessen an 963fa36): Web `47 files / 294 tests` (260916-dyv brachte `page.test.tsx` mit 8 Tests), API `67 / 1078`, `tsc --noEmit` web Exit 0 (api/shared an 7a6f42e gemessen, von dyv nicht beruehrt).
- 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).
@@ -145,7 +149,7 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
- 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)
- .planning/quick/260916-bwo-dashboard-feineres-raster-spalten-und-ze/260916-bwo-SUMMARY.md Abschnitt „Fuer den Changelog“ (5 Punkte) und .planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-SUMMARY.md Abschnitt „Fuer den Changelog“ (5 Punkte) — Vorlage der zusammengefuehrten Liste steht in Schritt B
</read_first>
<behavior>
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`):
@@ -170,7 +174,7 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
<action>
Schritt A — RED: die drei Testdateien gemaess `<behavior>` 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 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` (Revision Runde 1: bwo- und dyv-Punkte zu EINER Liste zusammengefuehrt, 8 Punkte, sprachlich geglaettet, echte Umlaute) mit `### Neu` — (N1) Seite „Was ist neu“: ein Klick auf die Versionsnummer unten in der Seitenleiste zeigt diese Liste; auf Live nur Freigegebenes, auf der Beta zusaetzlich „Noch nicht freigegeben“; (N2) Uhr: unter Einstellungen > Dashboard laesst sich die Schriftgroesse der Uhrzeit fest in Punkt (8 bis 200) vorgeben, leer gelassen passt sie sich weiter automatisch an — und `### Geändert` — (G1) Raster + Mindestgroessen: das Dashboard-Raster ist doppelt so fein, Widgets lassen sich in kleineren Schritten verschieben und in der Groesse ziehen; jedes Widget hat jetzt genau die Mindestgroesse, bei der es gerade noch bedienbar ist (kleiner geht es nicht, groesser jederzeit), auch bereits platzierte; gespeicherte Anordnungen werden beim ersten Aufruf automatisch uebernommen und verrutschen nicht; (G2) Verschieben: im Bearbeitungsmodus laesst sich jede Kachel an einer beliebigen Stelle anfassen (ausser an Eingabefeldern, Knoepfen und Links), ein grauer Griff am oberen Rand zeigt das an; Kacheln ueberlappen sich beim Ablegen nicht mehr — ueber einer belegten Stelle springt die Kachel an ihren Ausgangspunkt zurueck; (G3) der Bearbeiten-Schalter des Dashboards sitzt jetzt unten rechts, die Widgets beginnen direkt unter der Kopfzeile; (G4) Uhrzeit, Stoppuhr und Rechner wachsen und schrumpfen mit ihrer Kachel — eine grosse Uhr-Kachel zeigt eine grosse Uhrzeit; die Stoppuhr hat kompaktere Knoepfe und passt so auch in kleine Kacheln; (G5) die Raender sind ueberall enger: aeusserer Seitenrahmen auf allen Seiten, Abstand zwischen den Widgets und Innenabstaende der Widgets halbiert; (G6) das Anwenderhandbuch beschreibt das feine Raster, die mitwachsende Uhrzeit, die Schriftgroessen-Einstellung, den neuen Schalter, das Ziehen und die Mindestgroessen; 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).
@@ -182,13 +186,13 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
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`.
Schritt H — GREEN: Zielsuite gruen, dann die gesamte Web-Suite (47+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`.
</action>
<verify>
<automated>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=$?</automated>
<fails_when>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.</fails_when>
<automated>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 "^- " ; awk '/^## Unveröffentlicht$/{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 963fa36 -- 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=$?</automated>
<fails_when>Die Vitest-Zeilen weichen von `49 passed (49)` / `309 passed (309)` 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) bzw. weniger als 7 oder mehr als 9 (Punkte unter Unveröffentlicht — Soll 8); `.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.</fails_when>
</verify>
<done>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)`.</done>
<done>Web-Suite 49/309 gruen, tsc 0; CHANGELOG.md mit Unveröffentlicht (bwo + dyv + diese Seite, 8 Punkte) 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)`.</done>
</task>
<task type="auto">
@@ -239,7 +243,7 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
<files>docs/anleitung-betrieb.md, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md, docs/ci-cd-setup.md</files>
<read_first>
- 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-anwender.md (Inhaltsverzeichnis Zeilen 6-22, „Aufbau der Oberfläche“ Zeilen 38-55, Abschnittsfolge vor „Häufige Stolpersteine“ Zeile 172; Abschnitt „Dashboard“ Zeilen 57-84 ist Stand dyv und bleibt unangetastet)
- 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)
@@ -247,7 +251,7 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
<precondition>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).</precondition>
<action>
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.
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) den von 260916-dyv geaenderten Abschnitt „Dashboard“ (Stift-Schalter unten rechts, ganze Kachel ziehbar, Mindestgroessen) NICHT anfassen; 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).
@@ -255,10 +259,10 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
7. Wird das SUMMARY erst nach dem Push committet, den Push danach wiederholen (ein weiterer CI-Lauf ist erwartet).
</action>
<verify>
<automated>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"</automated>
<fails_when>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`.</fails_when>
<automated>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 963fa36 -- . ':!.planning'); echo GIT_EXIT=$? ; tail -n1 <<< "$D" ; U=$(git diff --stat 963fa36 -- '.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"</automated>
<fails_when>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 49/309 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`.</fails_when>
</verify>
<done>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)`.</done>
<done>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 49/309, 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)`.</done>
</task>
</tasks>
@@ -291,9 +295,9 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
</threat_model>
<verification>
- `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)`.
- `pnpm -C apps/web exec vitest run` -> `Test Files 49 passed (49)` / `Tests 309 passed (309)`; `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.
- `git diff --stat 963fa36 -- . ':!.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.
@@ -311,5 +315,5 @@ Gemessen am 2026-09-16 an HEAD `7a6f42e` (Arbeitsbaum sauber, main == origin/mai
</success_criteria>
<output>
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).
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 von bwo, dyv und diesem Auftrag stehen bereits in CHANGELOG.md unter Unveröffentlicht), offene Punkte (z. B. Browser-Gegenprobe, CI-Beweis des Release-Wegs erst beim naechsten Tag).
</output>
+35
View File
@@ -0,0 +1,35 @@
# Änderungen an Tessera
Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an Tessera geändert hat. Die neueste Version steht oben. Der Abschnitt „Unveröffentlicht“ nennt Änderungen, die bereits in der Beta enthalten, aber noch nicht als Version freigegeben sind.
## Unveröffentlicht
### Neu
- Seite „Was ist neu“: Ein Klick auf die Versionsnummer unten in der Seitenleiste zeigt diese Liste. Auf dem Live-System sehen Sie nur freigegebene Versionen, auf der Beta zusätzlich den Abschnitt „Noch nicht freigegeben“.
- Uhr: Unter Einstellungen > Dashboard lässt sich die Schriftgröße der Uhrzeit fest in Punkt (8 bis 200) vorgeben. Lassen Sie das Feld leer, passt sich die Schrift weiterhin automatisch an.
### Geändert
- Dashboard-Raster und Mindestgrößen: Das Raster ist doppelt so fein, Widgets lassen sich in kleineren Schritten verschieben und in der Größe ziehen. Jedes Widget hat jetzt genau die Mindestgröße, bei der es gerade noch bedienbar ist – kleiner geht es nicht, größer jederzeit; das gilt auch für bereits platzierte Widgets. Gespeicherte Anordnungen werden beim ersten Aufruf automatisch übernommen und verrutschen nicht.
- Verschieben: Im Bearbeitungsmodus lässt sich jede Kachel an einer beliebigen Stelle anfassen (außer an Eingabefeldern, Knöpfen und Links); ein grauer Griff am oberen Rand zeigt das an. Kacheln überlappen sich beim Ablegen nicht mehr – über einer belegten Stelle springt die Kachel an ihren Ausgangspunkt zurück.
- Der Bearbeiten-Schalter des Dashboards sitzt jetzt unten rechts; die Widgets beginnen direkt unter der Kopfzeile.
- Uhrzeit, Stoppuhr und Rechner wachsen und schrumpfen mit ihrer Kachel – eine große Uhr-Kachel zeigt eine große Uhrzeit. Die Stoppuhr hat kompaktere Knöpfe und passt so auch in kleine Kacheln.
- Die Ränder sind überall enger: Der äußere Seitenrahmen auf allen Seiten, der Abstand zwischen den Widgets und die Innenabstände der Widgets sind halbiert.
- Das Anwenderhandbuch beschreibt das feine Raster, die mitwachsende Uhrzeit, die Schriftgrößen-Einstellung, den neuen Schalter, das Ziehen und die Mindestgrößen.
## 1.0.0 – 2026-09-15
### Neu
- Portal mit Kopfleiste und Seitenleiste: Dashboard, Marktplatz und die für Sie freigegebenen Module nach Kategorien mit Suchfeld. Die Seitenleiste lässt sich ein- und ausklappen, das Erscheinungsbild wechselt zwischen Hell, Dunkel und System, die Sprache zwischen Deutsch und Englisch.
- Persönliches Dashboard mit frei anordenbaren Kacheln: Uhr, Suchleiste, Kalender, Notizen, Taschenrechner, Favoriten, Link und Stoppuhr. Im Bearbeitungsmodus fügen Sie Kacheln hinzu, verschieben sie und ziehen ihre Größe; die Einstellungen je Kachel (Kalenderquellen, Suchanbieter, Links) finden Sie unter Einstellungen > Dashboard.
- Marktplatz mit den Zuständen „Aktiviert“ und „Verfügbar“, Suche, Filtern und einer Detailseite je Modul. Administratoren aktivieren ein Modul für das Unternehmen und geben es je Gruppe oder Benutzer frei (Freigaben-Matrix).
- Ausschreibungs-Radar: Trefferliste öffentlicher Ausschreibungen mit Filtern nach Frist, Postleitzahl, Bundesland, Branche und Auftragswert; Suchprofile mit Sofort-Alarm per E-Mail; Sammel-Mail täglich oder wöchentlich; Merken und Gelesen-Markierung; eigene Postfächer und RSS-Feeds als Quellen.
- DKV-Rechnung: Automatische Verarbeitung von DKV-Tankkarten-Rechnungen aus einem Postfach, Fahrzeug-Stammdaten mit CSV-Import, Verarbeitungshistorie und Exportdateien zum Herunterladen.
- Zertifikat-Manager (Zertifikate analysieren, aufteilen, zusammenführen und konvertieren) und Domaincheck (Verfügbarkeit von Internet-Domains prüfen).
- Benutzer-, Gruppen- und Rechteverwaltung: Rollen Benutzer, Admin und Super-Admin; lokale und verzeichnisgeführte Konten; Gruppen mit Standardgruppe; Anbindung an das Active Directory mit Import von Gruppen und Einzelbenutzern, Ausschlussliste und automatischer Synchronisation.
- E-Mail-Versand (SMTP) mit Testnachricht; „Passwort vergessen“ und Zurücksetzen per E-Mail; erzwungene Passwortänderung bei neuen Konten.
- Persönliche Einstellungen: Profilbild, Akzentfarbe und Passwort ändern für lokale Konten.
- Knopf „Fehler melden“ in der Kopfleiste: Bildschirmfoto, Beschreibung und technische Angaben gehen per E-Mail an den Administrator.
- Versionsanzeige unten in der Seitenleiste mit Versionsnummer und Kanal (Live oder Beta).
+3
View File
@@ -22,6 +22,9 @@ COPY --from=deps /app/ ./
COPY apps/web/ ./apps/web/
COPY packages/shared/ ./packages/shared/
COPY tsconfig.base.json ./
# quick-260916-dcz: next.config.ts liest CHANGELOG.md zur Bauzeit (Seite "Was ist neu");
# .dockerignore schliesst *.md aus und macht fuer genau diese Datei eine Ausnahme.
COPY CHANGELOG.md ./
ENV NEXT_PUBLIC_API_URL=/api-proxy
# Muss VOR dem Build stehen: Next.js bettet NEXT_PUBLIC_* zur Bauzeit ins Bundle ein.
# So spaet wie moeglich, damit die COPY-Schichten darueber im Cache bleiben.
+26
View File
@@ -1,10 +1,36 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
// Bauzeit-Einbettung der Aenderungsliste (quick-260916-dcz): CHANGELOG.md aus
// dem Wurzelverzeichnis wird hier gelesen und als `env.TESSERA_CHANGELOG_MD`
// abgelegt. Next.js ersetzt `process.env.TESSERA_CHANGELOG_MD` fuer webpack UND
// Turbopack ueber denselben Define-Mechanismus. Der Text bleibt nur im
// Server-Bundle, weil ausschliesslich die Server-Seite (changelog/page.tsx)
// `@/lib/changelog` importiert — Importdisziplin, kein Client-Chunk.
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
function readChangelog(): string {
const file = path.resolve(__dirname, '../../CHANGELOG.md');
try {
return readFileSync(file, 'utf8');
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`CHANGELOG.md konnte nicht gelesen werden (${file}): ${reason}. ` +
'Die Datei muss im Wurzelverzeichnis liegen; im Docker-Bau braucht sie die Ausnahme ' +
'`!CHANGELOG.md` in .dockerignore und die Zeile `COPY CHANGELOG.md ./` in apps/web/Dockerfile.',
);
}
}
const nextConfig: NextConfig = {
output: 'standalone' as const,
env: {
TESSERA_CHANGELOG_MD: readChangelog(),
},
async rewrites() {
const apiUrl = process.env.API_INTERNAL_URL || 'http://api:3001';
return [
@@ -0,0 +1,120 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* changelog-page.test — Seite "Was ist neu" unter /changelog (quick-260916-dcz).
*
* Die Seite ist eine async Server-Komponente (Vorbild module-access-gate.test:
* Funktion awaiten, Ergebnis rendern). `next-intl/server` bekommt eine
* Handtabelle, `@/lib/app-version` einen veraenderbaren Kanal, `@/lib/changelog`
* wird je Test per `vi.doMock` mit `importOriginal` (echte Filterfunktion,
* eigener `changelogMarkdown`) bereitgestellt, `@uiw/react-md-editor` als
* Modul mit `default.Markdown`, das die Quelle roh in ein <pre> schreibt.
*/
vi.mock('next-intl/server', () => ({
getTranslations: async () => (key: string) => {
const translations: Record<string, string> = {
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.',
};
return translations[key] ?? key;
},
}));
let mockChannel: 'beta' | 'live' | 'dev' = 'live';
vi.mock('@/lib/app-version', () => ({
get appVersion() {
return { version: 'v1.2.3', channel: mockChannel, commit: 'abc1234' };
},
}));
vi.mock('@uiw/react-md-editor', () => ({
default: {
Markdown: ({ source }: { source?: string }) => <pre data-testid="md">{source}</pre>,
},
}));
vi.mock('next-themes', () => ({
useTheme: () => ({ resolvedTheme: 'light' }),
}));
const SAMPLE = [
'# Änderungen an Tessera',
'',
'Vorspann.',
'',
'## Unveröffentlicht',
'',
'### Neu',
'',
'- Punkt U1',
'',
'## 1.0.0 – 2026-09-15',
'',
'### Neu',
'',
'- Punkt A',
'',
].join('\n');
function mockChangelog(markdown: string) {
vi.doMock('@/lib/changelog', async (importOriginal) => {
const original = await importOriginal<typeof import('@/lib/changelog')>();
return { ...original, changelogMarkdown: markdown };
});
}
async function renderPage() {
const { default: ChangelogPage } = await import('./page');
const element = await ChangelogPage();
render(element);
}
afterEach(() => {
cleanup();
vi.resetModules();
vi.doUnmock('@/lib/changelog');
mockChannel = 'live';
});
describe('ChangelogPage /changelog (quick-260916-dcz)', () => {
it('Test 1 (live): Titel sichtbar, kein Hinweis, Markdown ohne Unveröffentlicht', async () => {
mockChannel = 'live';
mockChangelog(SAMPLE);
await renderPage();
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Was ist neu');
expect(screen.queryByTestId('changelog-unreleased-hint')).not.toBeInTheDocument();
const md = screen.getByTestId('md').textContent ?? '';
expect(md).toContain('1.0.0');
expect(md).not.toContain('Unveröffentlicht');
expect(md).not.toContain('Noch nicht freigegeben');
});
it('Test 2 (beta): Hinweis vorhanden, Markdown traegt "Noch nicht freigegeben (Beta)"', async () => {
mockChannel = 'beta';
mockChangelog(SAMPLE);
await renderPage();
expect(screen.getByTestId('changelog-unreleased-hint')).toHaveTextContent(
'Die Punkte unter „Noch nicht freigegeben“ sind in dieser Beta bereits enthalten, aber noch nicht als Version freigegeben.',
);
const md = screen.getByTestId('md').textContent ?? '';
expect(md).toContain('Noch nicht freigegeben (Beta)');
expect(md).toContain('- Punkt U1');
});
it('Test 3 (leer): ohne Text erscheint der Leer-Hinweis und kein Markdown-Element', async () => {
mockChannel = 'beta';
mockChangelog('');
await renderPage();
expect(screen.getByTestId('changelog-empty')).toHaveTextContent('Noch keine Einträge vorhanden.');
expect(screen.queryByTestId('md')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,47 @@
import { getTranslations } from 'next-intl/server';
import { ChangelogView } from '@/components/changelog/changelog-view';
import { appVersion } from '@/lib/app-version';
import { changelogMarkdown, filterChangelogForChannel } from '@/lib/changelog';
/**
* Seite "Was ist neu" unter /changelog (quick-260916-dcz).
*
* Async Server-Komponente ohne 'use client' (Vorbild module-access-gate.tsx):
* der Text stammt aus der Bauzeit-Variable `TESSERA_CHANGELOG_MD`
* (next.config.ts liest CHANGELOG.md) und bleibt so im Server-Bundle. Die
* Kanalregel liegt in `filterChangelogForChannel`: auf Live fehlt der
* Abschnitt "Unveröffentlicht", auf Beta/Entwicklung erscheint er als "Noch
* nicht freigegeben (Beta)" samt Hinweis. Zugriff nur angemeldet — die
* bestehende middleware.ts schuetzt jede Route ausser /login und
* /reset-password; /changelog ist keine oeffentliche Route.
*/
export default async function ChangelogPage() {
const t = await getTranslations('changelog');
const { markdown, hasUnreleased } = filterChangelogForChannel(changelogMarkdown, appVersion.channel, {
unreleasedHeading: t('unreleasedHeading'),
});
return (
<div className="mx-auto max-w-3xl py-8 px-4">
<h1 className="text-2xl font-bold text-foreground mb-2">{t('title')}</h1>
<p className="text-sm text-muted-foreground mb-6">{t('intro')}</p>
{hasUnreleased && (
<div
data-testid="changelog-unreleased-hint"
className="rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 px-4 py-3 text-sm text-yellow-800 dark:text-yellow-200 mb-6"
>
{t('unreleasedHint')}
</div>
)}
{markdown.trim() === '' ? (
<p data-testid="changelog-empty" className="text-sm text-muted-foreground">
{t('empty')}
</p>
) : (
<ChangelogView markdown={markdown} />
)}
</div>
);
}
@@ -0,0 +1,39 @@
'use client';
import { useEffect, useState } from 'react';
import MDEditor from '@uiw/react-md-editor';
import rehypeSanitize from 'rehype-sanitize';
import { useTheme } from 'next-themes';
/**
* Gerenderte Aenderungsliste fuer die Seite "Was ist neu" (quick-260916-dcz).
*
* Nutzt `MDEditor.Markdown` aus dem bereits installierten `@uiw/react-md-editor`
* (wie die Vorschau des Notiz-Widgets) mit `rehype-sanitize` — kein neues
* Paket, kein dangerouslySetInnerHTML. Der Farbmodus folgt `next-themes`
* erst nach dem Mount (vorher 'light'), damit Server- und Client-Markup
* uebereinstimmen (mounted-Guard wie in AppShell). Der Markdown-Text kommt
* als Prop von der Server-Seite; dieses Modul importiert `@/lib/changelog`
* bewusst NICHT, damit der Text nicht in Client-Chunks landet.
*/
export function ChangelogView({ markdown }: { markdown: string }) {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const mode: 'light' | 'dark' = mounted && resolvedTheme === 'dark' ? 'dark' : 'light';
return (
<div data-testid="changelog-markdown" className="rounded-md border border-border bg-card p-4">
<MDEditor.Markdown
source={markdown}
rehypePlugins={[[rehypeSanitize]]}
wrapperElement={{ 'data-color-mode': mode }}
style={{ background: 'transparent' }}
/>
</div>
);
}
@@ -4,9 +4,33 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* AppVersionBadge.test — die Versionszeile unten in der Seitenleiste
* (quick-260914-ku1). Vorlage: sidebar.test.tsx (next-intl-Mock, cleanup,
* dynamischer Import nach dem Setzen der Mocks).
* dynamischer Import nach dem Setzen der Mocks). Seit quick-260916-dcz ist die
* Zeile ein Link zur Seite "Was ist neu" — deshalb der `next/link`-Mock, der
* href, className, title, aria-label und data-testid durchreicht (Tests 5/6).
*/
vi.mock('next/link', () => ({
default: ({
children,
href,
className,
title,
'aria-label': ariaLabel,
'data-testid': testId,
}: {
children: React.ReactNode;
href: string;
className?: string;
title?: string;
'aria-label'?: string;
'data-testid'?: string;
}) => (
<a href={href} className={className} title={title} aria-label={ariaLabel} data-testid={testId}>
{children}
</a>
),
}));
vi.mock('next-intl', () => ({
useTranslations: (ns: string) => (key: string) => {
const map: Record<string, Record<string, string>> = {
@@ -14,6 +38,7 @@ vi.mock('next-intl', () => ({
'channel.beta': 'Beta',
'channel.live': 'Live',
'channel.dev': 'Entwicklung',
whatsNew: 'Was ist neu',
},
};
return map[ns]?.[key] ?? key;
@@ -95,4 +120,22 @@ describe('AppVersionBadge (quick-260914-ku1)', () => {
expect(el).toHaveTextContent('dev · Entwicklung');
expect(el.hasAttribute('title')).toBe(false);
});
it('Test 5 (Link): die Versionszeile ist ein Link auf /changelog', async () => {
mockLoad.mockResolvedValue(null);
const AppVersionBadge = await importBadge();
render(<AppVersionBadge />);
const el = screen.getByTestId('app-version');
expect(el.tagName).toBe('A');
expect(el).toHaveAttribute('href', '/changelog');
});
it('Test 6 (aria-label): der Link traegt den Namen "Was ist neu"', async () => {
mockLoad.mockResolvedValue(null);
const AppVersionBadge = await importBadge();
render(<AppVersionBadge />);
expect(screen.getByTestId('app-version')).toHaveAttribute('aria-label', 'Was ist neu');
});
});
@@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { type ApiVersionInfo, appVersion, loadApiVersion } from '@/lib/app-version';
@@ -9,6 +10,8 @@ import { type ApiVersionInfo, appVersion, loadApiVersion } from '@/lib/app-versi
* `v1.0.0 · Beta` oder lokal `dev · Entwicklung` (quick-260914-ku1).
* Der Tooltip nennt den Web-Commit und, sobald geladen, die API-Version
* samt Kanal. "Commit" und "API" sind in beiden Sprachen gleich.
* Seit quick-260916-dcz ist die Zeile ein Link zur Seite "Was ist neu"
* (/changelog); der Tooltip bleibt, der Linkname steht im aria-label.
*/
export function AppVersionBadge() {
const t = useTranslations('sidebar');
@@ -30,8 +33,14 @@ export function AppVersionBadge() {
const title = parts.length > 0 ? parts.join(' · ') : undefined;
return (
<span data-testid="app-version" className="block truncate text-xs text-muted-foreground" title={title}>
<Link
href="/changelog"
data-testid="app-version"
aria-label={t('whatsNew')}
title={title}
className="block truncate text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
>
{appVersion.version} · {t(`channel.${appVersion.channel}`)}
</span>
</Link>
);
}
+190
View File
@@ -0,0 +1,190 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { filterChangelogForChannel } from './changelog';
/**
* changelog.test — Kanalregel fuer die Seite "Was ist neu" (quick-260916-dcz).
*
* `filterChangelogForChannel` ist eine reine Funktion ueber dem Markdown-Text
* von CHANGELOG.md: auf `live` verschwindet der Abschnitt "Unveröffentlicht",
* auf `beta`/`dev` bleibt er mit der Ueberschrift "Noch nicht freigegeben
* (Beta)"; H1 und Vorspann entfallen immer. Test 1 ist die Falsifizierung (a)
* des Plans: er wird rot, sobald die Funktion den Abschnitt nicht entfernt.
* Tests 9/10 pruefen die Bauzeit-Einbettung ueber `TESSERA_CHANGELOG_MD`
* (Muster: app-version.test.ts — stubEnv + resetModules + dynamischer Import).
*/
const SAMPLE = [
'# Änderungen an Tessera',
'',
'Vorspann: die neueste Version steht oben.',
'',
'## Unveröffentlicht',
'',
'### Geändert',
'',
'- Punkt U1 (noch nicht freigegeben)',
'- Punkt U2 (noch nicht freigegeben)',
'',
'## 1.0.0 – 2026-09-15',
'',
'### Neu',
'',
'- Punkt A der Version 1.0.0',
'- Punkt B der Version 1.0.0',
'',
'## 0.9.0 – 2026-09-01',
'',
'### Neu',
'',
'- Punkt C der Version 0.9.0',
'',
].join('\n');
afterEach(() => {
vi.unstubAllEnvs();
});
describe('filterChangelogForChannel (quick-260916-dcz)', () => {
it('Test 1 (live): entfernt den Abschnitt Unveröffentlicht samt Punkten, behaelt die Versionen in Reihenfolge', () => {
const result = filterChangelogForChannel(SAMPLE, 'live');
expect(result.hasUnreleased).toBe(false);
expect(result.markdown).not.toContain('## Unveröffentlicht');
expect(result.markdown).not.toContain('Punkt U1');
expect(result.markdown).not.toContain('Punkt U2');
expect(result.markdown).toContain('## 1.0.0 – 2026-09-15');
expect(result.markdown).toContain('## 0.9.0 – 2026-09-01');
expect(result.markdown).toContain('- Punkt A der Version 1.0.0');
expect(result.markdown).toContain('- Punkt C der Version 0.9.0');
expect(result.markdown.indexOf('## 1.0.0')).toBeLessThan(result.markdown.indexOf('## 0.9.0'));
});
it('Test 2 (beta): ersetzt die Ueberschrift durch "Noch nicht freigegeben (Beta)", Punkte bleiben', () => {
const result = filterChangelogForChannel(SAMPLE, 'beta');
expect(result.hasUnreleased).toBe(true);
expect(result.markdown).toContain('## Noch nicht freigegeben (Beta)');
expect(result.markdown).not.toContain('## Unveröffentlicht');
expect(result.markdown).toContain('- Punkt U1');
expect(result.markdown).toContain('- Punkt U2');
expect(result.markdown).toContain('## 1.0.0 – 2026-09-15');
});
it('Test 3 (dev): verhaelt sich wie beta', () => {
const result = filterChangelogForChannel(SAMPLE, 'dev');
expect(result.hasUnreleased).toBe(true);
expect(result.markdown).toContain('## Noch nicht freigegeben (Beta)');
expect(result.markdown).not.toContain('## Unveröffentlicht');
expect(result.markdown).toContain('- Punkt U1');
});
it('Test 4 (eigenes Label): options.unreleasedHeading bestimmt die Ueberschrift', () => {
const result = filterChangelogForChannel(SAMPLE, 'beta', {
unreleasedHeading: 'Not yet released (beta)',
});
expect(result.markdown).toContain('## Not yet released (beta)');
expect(result.markdown).not.toContain('## Noch nicht freigegeben (Beta)');
});
it('Test 5 (leerer Abschnitt, beta): Unveröffentlicht ohne Listenpunkt wird ausgeblendet', () => {
const empty = [
'# Titel',
'',
'## Unveröffentlicht',
'',
'### Neu',
'',
'## 1.0.0 – 2026-09-15',
'',
'- Punkt A der Version 1.0.0',
'',
].join('\n');
const result = filterChangelogForChannel(empty, 'beta');
expect(result.hasUnreleased).toBe(false);
expect(result.markdown).not.toContain('Unveröffentlicht');
expect(result.markdown).not.toContain('Noch nicht freigegeben');
expect(result.markdown).toContain('## 1.0.0 – 2026-09-15');
expect(result.markdown).toContain('- Punkt A der Version 1.0.0');
});
it('Test 6 (kein Abschnitt): ohne Unveröffentlicht bleiben die Versionsabschnitte unveraendert', () => {
const noUnreleased = [
'# Titel',
'',
'## 1.0.0 – 2026-09-15',
'',
'### Neu',
'',
'- Punkt A der Version 1.0.0',
'',
'## 0.9.0 – 2026-09-01',
'',
'- Punkt C der Version 0.9.0',
'',
].join('\n');
const expected = [
'## 1.0.0 – 2026-09-15',
'',
'### Neu',
'',
'- Punkt A der Version 1.0.0',
'',
'## 0.9.0 – 2026-09-01',
'',
'- Punkt C der Version 0.9.0',
'',
].join('\n');
const beta = filterChangelogForChannel(noUnreleased, 'beta');
const live = filterChangelogForChannel(noUnreleased, 'live');
expect(beta.hasUnreleased).toBe(false);
expect(live.hasUnreleased).toBe(false);
expect(beta.markdown).toBe(expected);
expect(live.markdown).toBe(expected);
});
it('Test 7 (Hierarchie): H1 und Vorspann fehlen, Ergebnis beginnt mit "## ", "### " bleibt', () => {
const result = filterChangelogForChannel(SAMPLE, 'beta');
expect(result.markdown).not.toContain('# Änderungen an Tessera');
expect(result.markdown).not.toContain('Vorspann:');
expect(result.markdown.trim().startsWith('## ')).toBe(true);
expect(result.markdown).toContain('### Geändert');
expect(result.markdown).toContain('### Neu');
});
it('Test 8 (CRLF): Windows-Zeilenenden werden normalisiert, live entfernt den Abschnitt ebenfalls', () => {
const crlf = SAMPLE.replace(/\n/g, '\r\n');
const result = filterChangelogForChannel(crlf, 'live');
expect(result.hasUnreleased).toBe(false);
expect(result.markdown).not.toContain('\r');
expect(result.markdown).not.toContain('Unveröffentlicht');
expect(result.markdown).toContain('## 1.0.0 – 2026-09-15');
});
it('Test 9 (Einbettung): changelogMarkdown ist exakt der Wert von TESSERA_CHANGELOG_MD', async () => {
const text = '# T\n\n## 1.0.0 – 2026-01-01\n\n- x';
vi.stubEnv('TESSERA_CHANGELOG_MD', text);
vi.resetModules();
const mod = await import('./changelog');
expect(mod.changelogMarkdown).toBe(text);
});
it('Test 10 (ohne Variable): changelogMarkdown ist ein leerer String', async () => {
vi.stubEnv('TESSERA_CHANGELOG_MD', '');
vi.resetModules();
const empty = await import('./changelog');
expect(empty.changelogMarkdown).toBe('');
vi.stubEnv('TESSERA_CHANGELOG_MD', undefined);
vi.resetModules();
const unset = await import('./changelog');
expect(unset.changelogMarkdown).toBe('');
});
});
+84
View File
@@ -0,0 +1,84 @@
import type { AppChannel } from './app-version';
/**
* Aenderungsliste fuer die Seite "Was ist neu" (quick-260916-dcz).
*
* Quelle ist CHANGELOG.md im Wurzelverzeichnis; `apps/web/next.config.ts` liest
* die Datei zur Bauzeit und legt den Text als `env.TESSERA_CHANGELOG_MD` ab.
* Dieses Modul darf NUR von der Server-Seite (`app/(portal)/changelog/page.tsx`)
* importiert werden — sonst landet der Text in oeffentlich abrufbaren
* Client-Chunks unter /_next/static.
*
* Kanalregel (`filterChangelogForChannel`): auf `live` fehlt der Abschnitt
* "Unveröffentlicht" vollstaendig; auf `beta` und `dev` bleibt er und traegt
* die Ueberschrift "Noch nicht freigegeben (Beta)" (uebersetzbar). Ein leerer
* Abschnitt (ohne Listenpunkt) wird auf allen Kanaelen ausgeblendet. H1 und
* Vorspann vor der ersten `## `-Ueberschrift entfallen — die Seite hat ihren
* eigenen Titel. Reine Funktion ohne React/Next, deshalb direkt testbar.
*/
export type ChangelogChannel = AppChannel;
export interface FilteredChangelog {
markdown: string;
hasUnreleased: boolean;
}
export const UNRELEASED_HEADING = 'Unveröffentlicht';
const DEFAULT_UNRELEASED_LABEL = 'Noch nicht freigegeben (Beta)';
/**
* Wie in app-version.ts: Next.js ersetzt `process.env.<NAME>` zur Bauzeit nur,
* wenn der Ausdruck woertlich mit vollem Namen im Code steht — kein
* Destructuring, kein `process.env[name]`.
*/
export const changelogMarkdown: string = process.env.TESSERA_CHANGELOG_MD ?? '';
const SECTION_RE = /^## /;
const UNRELEASED_RE = new RegExp(`^## ${UNRELEASED_HEADING}\\s*$`);
const LIST_ITEM_RE = /^\s*[-*] /;
function splitSections(lines: string[]): string[][] {
const sections: string[][] = [];
let current: string[] | null = null;
for (const line of lines) {
if (SECTION_RE.test(line)) {
current = [line];
sections.push(current);
} else if (current) {
current.push(line);
}
// Zeilen vor der ersten `## `-Ueberschrift (H1, Vorspann) werden verworfen.
}
return sections;
}
export function filterChangelogForChannel(
markdown: string,
channel: AppChannel,
options?: { unreleasedHeading?: string },
): FilteredChangelog {
const normalized = markdown.replace(/\r\n?/g, '\n');
const sections = splitSections(normalized.split('\n'));
let hasUnreleased = false;
const kept: string[][] = [];
for (const section of sections) {
if (!UNRELEASED_RE.test(section[0])) {
kept.push(section);
continue;
}
const isEmpty = !section.slice(1).some((line) => LIST_ITEM_RE.test(line));
if (isEmpty || channel === 'live') {
continue;
}
hasUnreleased = true;
const label = options?.unreleasedHeading ?? DEFAULT_UNRELEASED_LABEL;
kept.push([`## ${label}`, ...section.slice(1)]);
}
const joined = kept.map((section) => section.join('\n')).join('\n').trim();
return { markdown: joined ? `${joined}\n` : '', hasUnreleased };
}
+9 -1
View File
@@ -109,7 +109,8 @@
"beta": "Beta",
"live": "Live",
"dev": "Entwicklung"
}
},
"whatsNew": "Was ist neu"
},
"dashboard": {
"title": "Dashboard",
@@ -512,6 +513,13 @@
"errorSendFailed": "Die E-Mail konnte nicht gesendet werden. Bitte versuchen Sie es später erneut oder wenden Sie sich an Ihren Administrator.",
"errorGeneric": "Die Meldung konnte nicht gesendet werden."
},
"changelog": {
"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."
},
"locale": {
"de": "Deutsch",
"en": "English"
+9 -1
View File
@@ -109,7 +109,8 @@
"beta": "Beta",
"live": "Live",
"dev": "Development"
}
},
"whatsNew": "What's new"
},
"dashboard": {
"title": "Dashboard",
@@ -512,6 +513,13 @@
"errorSendFailed": "The e-mail could not be sent. Please try again later or contact your administrator.",
"errorGeneric": "The report could not be sent."
},
"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."
},
"locale": {
"de": "Deutsch",
"en": "English"
+9 -2
View File
@@ -17,7 +17,8 @@ Diese Anleitung richtet sich an alle Kolleginnen und Kollegen, die Tessera im Ar
- [Domaincheck](#domaincheck)
7. [Persönliche Einstellungen](#persönliche-einstellungen)
8. [Einen Fehler melden](#einen-fehler-melden)
9. [Häufige Stolpersteine](#häufige-stolpersteine)
9. [Was ist neu](#was-ist-neu)
10. [Häufige Stolpersteine](#häufige-stolpersteine)
---
@@ -52,7 +53,7 @@ Links steht das Tessera-Logo, in der Mitte der aktuelle Seitentitel. Rechts find
**Seitenleiste (links)**
Ganz oben stehen zwei feste Einträge: **Dashboard** (Ihre Startseite) und **Marktplatz**. Darunter folgt ein Suchfeld „Module suchen…", mit dem Sie die Modulliste filtern können, und darunter die Liste der für Sie freigegebenen Module, gruppiert nach **Kategorien**. Ein Klick auf eine Kategorie klappt sie auf und zeigt die einzelnen Module darin. Sind für Sie noch keine Module aktiv, steht dort „Keine Module".
Unten in der Seitenleiste finden Sie die Sprachumschaltung (Deutsch/English) sowie Ihren Namen mit Rolle. Über den Pfeil-Button am unteren Rand können Sie die Seitenleiste ein- und wieder ausklappen — im eingeklappten Zustand bleiben nur die Symbole sichtbar, das spart Platz auf kleineren Bildschirmen.
Unten in der Seitenleiste finden Sie die Sprachumschaltung (Deutsch/English) sowie Ihren Namen mit Rolle. Über den Pfeil-Button am unteren Rand können Sie die Seitenleiste ein- und wieder ausklappen — im eingeklappten Zustand bleiben nur die Symbole sichtbar, das spart Platz auf kleineren Bildschirmen. Ganz unten steht die Versionsnummer von Tessera; ein Klick darauf öffnet die Seite [Was ist neu](#was-ist-neu).
## Dashboard
@@ -169,6 +170,12 @@ Mit **Senden** gehen folgende Angaben als E-Mail an Ihren Administrator: das Bil
Nach dem Senden erscheint „Vielen Dank, die Meldung wurde gesendet." Falls das nicht klappt, sagt Ihnen Tessera, warum: Entweder ist noch kein Postfach für Fehlermeldungen eingerichtet (dann sprechen Sie Ihren Administrator an), oder Sie haben in kurzer Zeit zu viele Meldungen geschickt (höchstens fünf in zehn Minuten), oder die E-Mail konnte gerade nicht gesendet werden (dann versuchen Sie es später noch einmal). Mit **Abbrechen** oder der Escape-Taste schließen Sie das Fenster, ohne etwas zu senden.
## Was ist neu
Ein Klick auf die Versionsnummer ganz unten in der Seitenleiste öffnet die Seite **Was ist neu**. Sie zeigt die Änderungsliste von Tessera: Für jede Version steht dort in einfachen Worten, was neu hinzugekommen ist, was sich geändert hat und was behoben wurde — gegliedert in die Gruppen **Neu**, **Geändert** und **Behoben**. Die neueste Version steht oben.
Auf dem Live-System sehen Sie nur freigegebene Versionen. Auf der Beta erscheint zusätzlich der Abschnitt **Noch nicht freigegeben (Beta)** mit einem gelben Hinweis: Diese Punkte sind in der Beta bereits enthalten, aber noch nicht als Version freigegeben.
## Häufige Stolpersteine
- **Die Anmeldung schlägt fehl, obwohl Passwort und E-Mail stimmen.** Prüfen Sie, ob Sie im Feld „Benutzername" tatsächlich Ihren Benutzernamen eingegeben haben — nicht Ihre E-Mail-Adresse. Das ist mit Abstand der häufigste Grund für eine scheinbar kaputte Anmeldung.
+25 -5
View File
@@ -432,6 +432,11 @@ Zeilen einmal ergänzt.
Das Freigeben erledigt Claude; Sie sagen nur „Version X freigeben“. Zur Einordnung,
was dabei passiert:
1. **Änderungsliste abschließen:** In `CHANGELOG.md` wird der Abschnitt
„Unveröffentlicht“ in „X.Y.Z – JJJJ-MM-TT“ umbenannt, darüber ein neues, leeres
„Unveröffentlicht“ angelegt, und das Ganze auf `main` committet und gepusht.
Erst dann wird zusammengeführt und getaggt:
```bash
git checkout live
git merge --ff-only main
@@ -445,6 +450,14 @@ weigert, ist eine frühere Korrektur (siehe Hotfix, Schritt 5) noch nicht zurüc
Zweig `live` wird nur geprüft, der Tag `vX.Y.Z` wird gebaut und als `live` und
`vX.Y.Z` abgelegt. Das dauert etwa vier bis sechs Minuten.
Beim Tag legt die Pipeline zusätzlich einen **Release in Gitea** an: Name
„Tessera X.Y.Z“, Text ist der Abschnitt dieser Version aus `CHANGELOG.md`. Sie
finden ihn im Repository unter „Releases“. Fehlt der Abschnitt in der
Änderungsliste, schlägt genau dieser letzte Schritt fehl – die Abbilder sind dann
trotzdem gebaut und abgelegt. Der Release wird nachgeholt, sobald der Abschnitt
nachgetragen ist: entweder durch erneutes Auslösen des Tag-Laufs oder lokal per
Skript (`.gitea/scripts/publish-release.sh --tag vX.Y.Z`).
Danach spielen Sie die Version auf dem Live-Server ein – Kapitel 4 gilt unverändert:
```bash
@@ -452,10 +465,12 @@ docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --force-recreate api web
```
**Erstfreigabe v1.0.0:** Den Zweig `live` gibt es noch nicht. Er entsteht beim
ersten Mal aus `main` (`git checkout -b live main`), bekommt den Tag `v1.0.0` und
wird zusammen mit dem Tag gepusht. Das erfolgt, sobald der Knopf „Fehler melden“
eingebaut ist – nicht in diesem Durchlauf.
**Erstfreigabe v1.0.0:** Die erste Freigabe ist erfolgt. Der Zweig `live`
entstand am 2026-09-14 aus `main` (`git checkout -b live main`), bekam den Tag
`v1.0.0` und wurde zusammen mit dem Tag gepusht; seit 2026-09-15 läuft diese
Version auf dem Live-Server. Der Release „Tessera 1.0.0“ in Gitea wurde
nachträglich mit dem Skript angelegt, weil die Änderungsliste erst danach
eingeführt wurde.
### Einen Fehler auf Live beheben (Hotfix)
@@ -483,7 +498,7 @@ eine Datenbankänderung, wird sie als reguläre Version über `main` freigegeben
### Woran Sie erkennen, welche Version läuft
Drei Wege, vom einfachsten zum genauesten:
Vier Wege, vom einfachsten zum genauesten:
1. **In der Oberfläche:** Unten in der Seitenleiste steht `v1.0.0 · Live` bzw.
`v1.0.0-12-gabc1234 · Beta`. Wenn Sie die Maus darüber halten, erscheinen die
@@ -505,6 +520,11 @@ Drei Wege, vom einfachsten zum genauesten:
```
Zeigt die Startzeile `Tessera API v1.0.0 (live) abc1234` (siehe Kapitel 7).
4. **Was sich geändert hat:** Ein Klick auf die Versionszeile unten in der
Seitenleiste öffnet die Seite „Was ist neu“ mit der Änderungsliste. Auf Live
sehen Sie nur freigegebene Versionen; auf der Beta steht zusätzlich der
Abschnitt „Noch nicht freigegeben (Beta)“ mit dem, was seit der letzten
Freigabe dazugekommen ist.
### Den neuen Live-Server einrichten
+15
View File
@@ -437,6 +437,21 @@ dokumentiert das an jeder betroffenen Stelle explizit im Kommentar (`source-conf
jedem neuen `@Get(':id')`/`@Put(':id')`/`@Delete(':id')` in einem Controller mit weiteren statischen
GET-Routen: statische Routen zuerst deklarieren.
**Änderungsliste (`CHANGELOG.md`):** Jede Änderung, die Anwender oder Betrieb bemerken, wird sofort
im selben Auftrag in `CHANGELOG.md` unter „Unveröffentlicht“ eingetragen — in Alltagssprache für
Anwender, Sie-Form, echte Umlaute, gegliedert in „Neu“, „Geändert“ und „Behoben“; keine Dateinamen,
keine Commit-Kürzel, keine unerklärten Fachbegriffe. Bei der Freigabe wird der Abschnitt in
„X.Y.Z – JJJJ-MM-TT“ umbenannt und darüber ein neues leeres „Unveröffentlicht“ angelegt (siehe
Betriebshandbuch Kapitel 9). Die Seite „Was ist neu“ (`apps/web/src/app/(portal)/changelog/page.tsx`)
liest den Text zur Bauzeit aus `env.TESSERA_CHANGELOG_MD`, das `apps/web/next.config.ts` aus der
Datei befüllt — deshalb steht `COPY CHANGELOG.md ./` im Web-Dockerfile und `!CHANGELOG.md` als
Ausnahme in `.dockerignore`. Nur `page.tsx` darf `@/lib/changelog` importieren, damit der Text im
Server-Bundle bleibt und nicht in öffentlich abrufbare Client-Chunks gelangt. Die Kanalregel (Live
ohne „Unveröffentlicht“, Beta/Entwicklung mit „Noch nicht freigegeben (Beta)“) liegt in
`filterChangelogForChannel` (`apps/web/src/lib/changelog.ts`) mit Tests. Beim Tag `vX.Y.Z`
schneidet `.gitea/scripts/publish-release.sh` den Abschnitt der Version heraus und legt daraus den
Gitea-Release an — fehlt der Abschnitt, bricht dieser CI-Schritt mit Exit 1 ab.
**i18n — Schlüsselparität zwischen de.json und en.json:** Jeder benutzersichtbare Text gehört in
beide Sprachdateien, `apps/web/src/messages/de.json` und `apps/web/src/messages/en.json`. Ein
strukturelle Wächter-Test, `apps/web/src/messages/tenderRadar-parity.spec.ts`, prüft für den
+13 -5
View File
@@ -85,7 +85,7 @@ fuer die Pipeline konfiguriert. Benoetigt wird genau eines:
| Secret | Beschreibung |
|--------|--------------|
| `REGISTRY_TOKEN` | Gitea-Zugangstoken (Access Token) mit Schreibrecht auf Pakete (`package: write`). Wird im Job `publish` fuer `docker login localhost:3002 --password-stdin` verwendet. |
| `REGISTRY_TOKEN` | Gitea-Zugangstoken (Access Token) mit Schreibrecht auf Pakete (`package: write`) und zusaetzlich auf das Repository (`repository: write`, fuer Releases). Wird im Job `publish` fuer `docker login localhost:3002 --password-stdin` verwendet und im Release-Schritt ueber `env` als `GITEA_TOKEN` an `.gitea/scripts/publish-release.sh` gereicht -- nie als Argument. |
Das Token erscheint nie im Log: es wird per `--password-stdin` uebergeben und
Gitea maskiert Secret-Werte in der Job-Ausgabe. Das Veroeffentlichungs-Skript
@@ -118,10 +118,18 @@ aus drei aufeinander aufbauenden Jobs:
veroeffentlichen
Ablauf: `quality` -> `test` -> `publish` (jeder Job nur bei Erfolg des
vorherigen). Der Job `publish` besteht aus drei Schritten: `actions/checkout@v4`
vorherigen). Der Job `publish` besteht aus vier Schritten: `actions/checkout@v4`
mit `fetch-depth: 0` (volle Historie samt Tags, sonst liefert `git describe`
nichts), Login in die Registry (siehe Abschnitt 3) und der Aufruf von
`.gitea/scripts/publish-images.sh`.
nichts), Login in die Registry (siehe Abschnitt 3), der Aufruf von
`.gitea/scripts/publish-images.sh` und der Aufruf von
`.gitea/scripts/publish-release.sh` (legt bei Tags `v*` den Gitea-Release aus dem
CHANGELOG-Abschnitt an; auf `main` endet er mit "nichts zu tun").
Das Release-Skript spricht die Gitea-API ueber `GITHUB_API_URL` bzw.
`GITHUB_SERVER_URL/api/v1` an -- im Job-Container ist das
`https://git.vicolab.de`; `localhost:3002` ist von dort NICHT erreichbar (nur der
Docker-Daemon des Hosts erreicht die Registry so). Lokal laesst sich das Skript
mit `--dry-run --tag vX.Y.Z` pruefen, ohne Netzaufruf und ohne Token.
### Zwei Kanaele: Etiketten je Anlass
@@ -131,7 +139,7 @@ Das Skript `.gitea/scripts/publish-images.sh` entscheidet allein anhand
| Anlass | Kanal (`APP_CHANNEL`) | Etiketten in der Registry |
|--------|----------------------|---------------------------|
| Push auf `main` | `beta` | `beta` und `latest` (`latest` ist nur ein Alias fuer `beta` und entfaellt spaeter) |
| Tag `vX.Y.Z` | `live` | `live` und `vX.Y.Z` |
| Tag `vX.Y.Z` | `live` | `live` und `vX.Y.Z` + Gitea-Release `Tessera X.Y.Z` mit dem CHANGELOG-Abschnitt |
| Push auf `live` ohne Tag | -- | keine; der Lauf prueft nur (`quality`, `test`), das Skript endet mit "nichts zu tun" |
Das Kanalmodell fuer den Betrieb (welcher Server welches Etikett zieht, Freigabe,