Compare commits
9 Commits
cf97b5b64b
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e0d4532327 | |||
| 75ff8bb6f1 | |||
| c3d8e16eb3 | |||
| c5f4adeeed | |||
| 6940bd05a1 | |||
| ba06db98c5 | |||
| 0db21627f5 | |||
| 963fa36cd7 | |||
| 8792819e34 |
@@ -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
|
||||
|
||||
Executable
+155
@@ -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
|
||||
@@ -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
|
||||
|
||||
+10
-8
@@ -4,11 +4,11 @@ milestone: v1.2
|
||||
current_phase: 17
|
||||
current_phase_name: eigene-ausschreibungs-quellen-je-nutzer
|
||||
status: verified
|
||||
stopped_at: "2026-09-15: v1.0.0 live auf tessera.ctl.de (neuer Server, IMAGE_TAG=live), alpha = beta; kein Feedback bisher; nichts offen; Mandantenfaehigkeit ruht; Schalter AUS"
|
||||
last_updated: "2026-09-15T14:25:42.000Z"
|
||||
last_activity: 2026-09-14
|
||||
stopped_at: Quick 260916-dcz ausgefuehrt (3/3 Tasks, CI 356 success, Release Tessera 1.0.0 id 1) — Browser-Nachweis durch Orchestrator offen
|
||||
last_updated: "2026-09-16T09:32:51.778Z"
|
||||
last_activity: 2026-09-16
|
||||
last_activity_desc: Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent
|
||||
state_head: 77117de3d0f1bb82df7b659f46fe26244ca6f164
|
||||
state_head: c5f4adeeed4bc858171323a5b4c3866ee991f560
|
||||
progress:
|
||||
total_phases: 17
|
||||
completed_phases: 15
|
||||
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
Phase: 17 (eigene-ausschreibungs-quellen-je-nutzer) — VERIFIED / passed
|
||||
Plan: 3 of 3
|
||||
Status: Phase abgeschlossen und im Browser gegengeprueft — bereit fuer /gsd-ship
|
||||
Last activity: 2026-09-16 - Dashboard feineres Raster/Skalierung/halbe Abstaende (260916-bwo, verifiziert 8/8 + Browser). Naechster Schritt: CHANGELOG + Seite "Was ist neu" + Gitea-Release je Freigabe
|
||||
Last activity: 2026-09-16 - Aenderungsliste (260916-dcz: CHANGELOG.md, Seite "Was ist neu", Gitea-Release je Tag, Release v1.0.0 angelegt) + Uebersetzungs-Nachtrag c3d8e16; Beta-Abbild c3d8e16 bereit. Freigabe der naechsten Version (1.1.0) auf Zuruf des Users: CHANGELOG Unveroeffentlicht -> 1.1.0, live ff-merge, Tag, Push
|
||||
|
||||
Progress: [██████████] 100%
|
||||
|
||||
@@ -408,6 +408,8 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260914-ku1 | **Zwei Auslieferungskanaele und Versionsstempel.** `main` = Beta (Etiketten `beta` + `latest`), Tag `vX.Y.Z` = Live (Etiketten `live` + `vX.Y.Z`), Zweig `live` ohne Tag nur geprueft — Entscheidung in `.gitea/scripts/publish-images.sh` (`--print-plan`), CI-Trigger `branches: [main, live]` + `tags: [v*]`, `fetch-depth: 0`. Versionsstempel `APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME` als Build-Args in beide Dockerfiles (web zur Bauzeit als `NEXT_PUBLIC_APP_*`, api als Laufzeit-ENV; Vorgabe `dev`). `GET /health/version` liefert name/version/channel/commit/buildTime, Startlog `Tessera API vX (channel) commit`. Web: `app-version.ts`, `AppVersionBadge` in `sidebar.tsx` (sidebar-footer.tsx ist seit ba02b25 toter Code). `docker-compose.prod.yml`: `image: ...:${IMAGE_TAG:-beta}`. Betriebshandbuch Kapitel 9 (Zwei Kanaele, Freigabe, Hotfix ohne Datenbankaenderung, neuer Live-Server), ci-cd-setup.md auf gemessenen Stand. Falsifiziert: Build mit `v9.9.9-test live` -> Stempel in dist und Web-Bundle, ohne Args `dev`. Echter CI-Lauf 297 gruen (5:18 min), Abbilder `beta`/`latest` tragen `ea6aa99 beta`. Tests API 1054 -> 1060 / 64 -> 65 Dateien, Web 233 -> 243 / 38 -> 40, tsc 0, 20 Dateien gegen 6c19451. Offen: Zweig `live` + Tag `v1.0.0` nach dem Fehler-melden-Knopf anlegen; Handgriffe fuer den User (IMAGE_TAG je Server) im SUMMARY. Verifiziert 8/8, gepusht. | 2026-09-14 | cdb571c,9731501,ea6aa99 | [260914-ku1-zwei-auslieferungskanaele-beta-auf-main-](./quick/260914-ku1-zwei-auslieferungskanaele-beta-auf-main-/) |
|
||||
| 260914-m97 | **Fehler-melden-Knopf.** Kaefer-Knopf in der Kopfzeile: Bildschirmfoto VOR dem Dialog (`html-to-image` 1.11.13, laengste Kante 1600 px, `computeCaptureSize`), Dialog mit Vorschau, Haekchen und "Was ist passiert?"; Fehlerpuffer (Ringpuffer 20: window.onerror, unhandledrejection, console.error, fehlgeschlagene fetch-Antworten — keine Ruempfe/Cookies/Tokens); `POST /bug-reports` als Multipart (FileInterceptor 4 MiB -> 413, PNG-Signatur -> 400, kein Empfaenger -> 409, Drossel 5/10 min -> 429, Versandfehler -> 502; Mandant/Benutzer nur aus der Sitzung); E-Mail mit PNG-Anhang und Kontext (URL, Web-/API-Version+Kanal+Commit, Browser, Fenster, Zeitpunkt, Benutzer, letzte Fehler) ueber `MailService.sendBugReport` (Anhaenge; Kennwort-Reset bleibt verschluckend). Empfaenger: neue nullable Spalte `SmtpConfig.bugReportRecipient` (Migration `20260914170000`), Feld "Fehlermeldungen an" unter Administrator -> SMTP, Rueckfall `TESSERA_BUGREPORT_TO` (docker-compose.prod.yml). Handbuecher Anwender/Administration/Betrieb. Tests API 1060 -> 1076 / 67 Dateien, Web 243 -> 260 / 43 Dateien, tsc 0, `--frozen-lockfile` 0, 35 Dateien gegen 5c42c55, vier Commits. CI-Lauf 299 gruen (zweiter Versuch, erster scheiterte an Gitea-DB). Browser-Beweis durch den Orchestrator: E-Mail mit 85-KB-PNG (ohne Dialog, OKLCH korrekt) in mailhog, 409-Pfad im Dialog. Ledger #38 (Rule-1-Fix `@Expose()`) als fixed. Verifiziert 9/9 + Browser, gepusht. | 2026-09-14 | 54121c1,60b0ee8,b41be21,77117de | [260914-m97-fehler-melden-knopf-bildschirmfoto-der-a](./quick/260914-m97-fehler-melden-knopf-bildschirmfoto-der-a/) |
|
||||
| 260916-bwo | **Dashboard — feineres Raster, skalierende Widget-Inhalte, halbe Abstaende.** Raster verdoppelt (COLS 24/20/12/8/2, rowHeight 20, margin 8; WIDGET_CONSTRAINTS x2), gespeicherte Anordnungen einmalig x2 mit Marker `__gridVersion: 2` (nur im JSON, `migrateGridLayouts`/`withGridVersion`, idempotent). Widget-Rumpf `container-type: size`; Uhr/Stoppuhr/Rechner skalieren per Container-Queries; Uhr mit `timeFontSizePt` (leer = automatisch, 8..200 = fest in pt) und Feld in Einstellungen -> Dashboard -> Widgets. Abstaende halbiert: `app-shell` p-6 -> p-3 (alle Seiten, User-Nachtrag), Dashboard p-2, Grid 8 px, Widget-Innenabstaende; `mt-8` bleibt (Umschalter-Hoehe). Anwenderhandbuch. Tests Web 260 -> 286 / 46 Dateien, API 1076 -> 1078, tsc 0, 29 Dateien gegen 5f50c5f, vier Commits, CI-Lauf 351 gruen, Beta-Abbild `v1.0.0-10-g1aefaa3`. Browser-Beweis durch den Orchestrator: SQL-Probe in alten Einheiten -> DB verdoppelt + Marker; Rand 28 px (vorher 56), Abstand 8 px (vorher 16), main 12 px; Uhr 51 px -> 107 px beim Vergroessern; 36 pt = 48 px fest. Verifiziert 8/8 + Browser, gepusht. | 2026-09-16 | 3f5afb0,2d8efe1,a175c00,1aefaa3 | [260916-bwo-dashboard-feineres-raster-spalten-und-ze](./quick/260916-bwo-dashboard-feineres-raster-spalten-und-ze/) |
|
||||
| 260916-dyv | **Dashboard-Nachbesserung nach User-Test.** Mindestgroessen inhaltsgetrieben (clock 2/2, search 6/2, calendar 3/3, note 4/4, calculator 3/10 — vom Orchestrator im Browser von 9 auf 10 korrigiert, sechs Tastenreihen —, favorites 3/3, link 3/2, stopwatch 4/3 mit kompakter Bedienleiste), gespeicherte Layout-Eintraege bekommen minW/minH aus den Konstanten und zu kleine w/h werden angehoben (`applyConstraintMinima`, Test 9/9b). Bearbeiten-Schalter in feste Leiste unten rechts, `mt-8` weg: Rand oben 28 px statt 60. Drag & Drop: ganze Kachel als Griff mit Overlay-Kopfleiste, `dragConfig.cancel` (Eingaben, Knoepfe, .widgetNoDrag, Resize-Griff), `preventCollision: true` mit `noCompactor` (Ablegen auf belegtem Raum stoppt am Nachbarn, kein Ueberlappen). Anwenderhandbuch. Tests Web 286 -> 294 / 47 Dateien, API 67/1078, tsc 0, 12 Dateien gegen df16f46 + Fix 8792819; CI-Laeufe 353 und der Fix-Lauf gruen. Browser-Beweis: 28 px, Uhr 126x48, Ziehen an Kachelmitte, Suchfeld ohne Drag, Kollision stoppt, Rechner 272 px ohne Ueberlauf. Ledger #39 fixed. Verifiziert 6/6 + Browser, gepusht. | 2026-09-16 | dc992c9,dbbd54f,cf97b5b,8792819 | [260916-dyv-dashboard-nachbesserung-mindestgroessen-](./quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/) |
|
||||
| 260916-dcz | **Aenderungsliste.** `CHANGELOG.md` (Keep-a-Changelog, Alltagssprache, echte Umlaute: `## Unveröffentlicht` mit Neu/Geändert/Behoben, `## 1.0.0 – 2026-09-15` mit 11 Punkten); Seite "Was ist neu" unter `/changelog` (Server-Komponente, Text zur Bauzeit ueber `env.TESSERA_CHANGELOG_MD` in next.config.ts, nur im Server-Bundle; Kanalfilter `filterChangelogForChannel`: live ohne Unveroeffentlicht, beta/dev markiert; `MDEditor.Markdown` + `rehypeSanitize`), Versionsabzeichen als Link; Dockerfile `COPY CHANGELOG.md` + `.dockerignore !CHANGELOG.md`; `.gitea/scripts/publish-release.sh` (awk-Abschnitt, jq, API-Basis aus GITHUB_*, POST/PATCH, --dry-run, Exit 1 ohne Abschnitt) + ci.yml-Schritt nur bei Tag-Refs; Gitea-Release `v1.0.0` rueckwirkend angelegt (id 1); Handbuecher (Betrieb Kap. 9, Anwender "Was ist neu", Entwicklung, CI). Tests Web 294 -> 309 / 49 Dateien, API 67/1078, tsc 0, 19 Dateien gegen 963fa36; CI 356/357 gruen. Nachtrag c3d8e16: 21 fehlende Uebersetzungen (Kalenderquellen-Formular, Kalender-Einstellungen, Marktplatz) in de/en, Changelog "Behoben". Browser-Beweis: /changelog mit 20 Punkten, Kalender-Formular ohne Schluesselnamen. Verifiziert 8/8 + Browser, gepusht. | 2026-09-16 | ba06db9,6940bd0,c5f4ade,c3d8e16 | [260916-dcz-aenderungsliste-changelog-md-in-alltagss](./quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -449,8 +451,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-09-15T14:25:42.000Z
|
||||
Last session: 2026-09-16T09:32:51.449Z
|
||||
Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; #29 und 3c als /gsd-quick --validate mit voller Kette durchgefuehrt.
|
||||
Stopped at: **v1.0.0 IST LIVE — 2026-09-15.** Der User hat den neuen Live-Server (tessera.ctl.de, IMAGE_TAG=live) selbst eingerichtet; alpha.tessera.ctl.de bleibt Beta. Bisher kein Feedback von Anwendern. Nichts offen. Kanalmodell: main = beta, Zweig live + Tag = live; Freigabe/Hotfix-Rezept in docs/anleitung-betrieb.md Kapitel 9. Vorlage .env.prod.example traegt IMAGE_TAG/COMPOSE_FILE/TESSERA_BUGREPORT_TO (7d201a8). DER SCHALTER IST AUS. Mandantenfaehigkeit RUHT (User 2026-09-14). Restposten ohne Dringlichkeit: Ship von Phase 17 (windows_enforce, open_count 15), Ledger #35 (Biome), #36 (stilles 403), #37 (DKV Single-Flight), Exchange-Alarm-Mail nie live getestet (kein Postfach). Naechster Einstieg: `/gsd-resume-work`, dann das, was der User aus dem Betrieb mitbringt (Fehlermeldungen kommen per Knopf ins eingestellte Postfach; Korrekturen auf Live laufen als Hotfix v1.0.x).
|
||||
Stopped at: **2026-09-16, drei Quick-Tasks + Nachtrag: Dashboard-Umbau (260916-bwo), Dashboard-Nachbesserung (260916-dyv), Aenderungsliste (260916-dcz), fehlende Uebersetzungen (c3d8e16).** Alles verifiziert, im Browser bewiesen, gepusht; Beta-Abbild `c3d8e16` in der Registry, alpha holt es per pull. Live bleibt v1.0.0. NAECHSTER SCHRITT auf Zuruf des Users ("Version freigeben"): CHANGELOG.md `## Unveröffentlicht` -> `## 1.1.0 – <Datum>` + neues leeres Unveroeffentlicht, `git checkout live && git merge --ff-only main && git tag -a v1.1.0 -m "Tessera 1.1.0" && git push origin live v1.1.0` (Rezept docs/anleitung-betrieb.md Kap. 9; die Pipeline legt den Gitea-Release an — erster echter CI-Beweis des Release-Wegs). Offen ohne Dringlichkeit: Desktop-Client-Todo, Ship Phase 17, Ledger #35/#36/#37. Mandantenfaehigkeit RUHT. Schalter AUS. Einstieg: `/gsd-resume-work`.
|
||||
Resume file: None
|
||||
Last activity: 2026-09-16 - Completed quick task 260916-bwo: Dashboard — feineres Raster, skalierende Widget-Inhalte, halbe Abstaende
|
||||
Last activity: 2026-09-16 - Completed quick task 260916-dcz: Aenderungsliste — CHANGELOG.md, Seite Was ist neu, Gitea-Release
|
||||
|
||||
+17
-3
@@ -2,9 +2,9 @@
|
||||
schema_version: 1
|
||||
open_count: 15
|
||||
waived_count: 1
|
||||
fixed_count: 22
|
||||
total_count: 38
|
||||
last_updated: 2026-09-14T15:17:38.808Z
|
||||
fixed_count: 23
|
||||
total_count: 39
|
||||
last_updated: 2026-09-16T09:00:26.845Z
|
||||
---
|
||||
|
||||
# Broken Windows Ledger
|
||||
@@ -53,6 +53,7 @@ last_updated: 2026-09-14T15:17:38.808Z
|
||||
| 36 | quick-260914-ebg | deviation | apps/web/src/app/(portal)/admin/users/page.tsx | | handleSubmit und handleDelete pruefen nur res.ok ohne else-Zweig und fangen mit leerem catch - ein 403 der API fuehrt zu keiner sichtbaren Reaktion (Formular bleibt offen, Loeschdialog bleibt stehen, keine Meldung). Bestehendes Verhalten fuer alle 403-Wege (fremder Mandant, Selbstloeschung); seit 260914-ebg (WINDOWS #29) ist der Fall fuer einen ADMIN im Alltag erreichbar, weil die SUPER_ADMIN-Zeile in der eigenen Benutzerliste steht und Aendern/Loeschen darauf jetzt 403 liefert. Familie der still verschluckten Antworten (#28, #32). Frontend von 260914-ebg NICHT geaendert (ausserhalb der Erlaubnisliste). Zu schliessen: Fehlermeldung aus dem Antwortrumpf anzeigen und die Aktionsknoepfe fuer SUPER_ADMIN-Zeilen einem ADMIN gar nicht erst anbieten. | open | | 2026-09-14T08:38:12.619Z | |
|
||||
| 37 | quick-260914-eym | deviation | apps/api/src/dkv/dkv.service.ts | | Der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean, nicht je Mandant. Seit 260914-eym laeuft je aktivem Mandanten ein eigener Cron-Auftrag (dkv-inbox-poll:<tenantId>); ueberschneiden sich zwei Ticks verschiedener Mandanten, bricht der zweite still ab (Warnzeile 'already processing') und der Mandant wartet bis zum naechsten Intervall - kein Datenverlust, Verzoegerung; mit EINEM Mandanten unveraendert. Der Tick blieb in 3c laut Auftrag unangetastet (T-EYM-09, accept mit Aufzeichnung). Zu schliessen: Riegel je Mandant (Set<tenantId>) mit Test 'zwei Mandanten gleichzeitig, beide werden bedient'. | open | | 2026-09-14T09:51:24.295Z | |
|
||||
| 38 | quick-260914-m97 | deviation | apps/api/src/bug-reports/dto/bug-report.dto.ts | 71 | Rule 1: @Expose() auf errors ergaenzt, damit die @Transform-Normalisierung auch bei ganz fehlendem Multipart-Feld greift (class-transformer transformiert nur vorhandene Schluessel) | fixed | | 2026-09-14T15:04:31.846Z | 2026-09-14T15:17:38.808Z |
|
||||
| 39 | quick-260916-dyv | deviation | apps/web/src/components/dashboard/dashboard-grid.test.tsx | | Test 7 pinnt Identitaets-Kopie per toMatchObject statt toEqual (cloneLayoutItem normalisiert moved/static) | fixed | | 2026-09-16T08:48:45.783Z | 2026-09-16T09:00:26.845Z |
|
||||
|
||||
````json
|
||||
[
|
||||
@@ -515,6 +516,19 @@ last_updated: 2026-09-14T15:17:38.808Z
|
||||
"recorded_at": "2026-09-14T15:04:31.846Z",
|
||||
"resolved_at": "2026-09-14T15:17:38.808Z",
|
||||
"milestone": "v1.2"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"kind": "deviation",
|
||||
"phase": "quick-260916-dyv",
|
||||
"file": "apps/web/src/components/dashboard/dashboard-grid.test.tsx",
|
||||
"line": null,
|
||||
"description": "Test 7 pinnt Identitaets-Kopie per toMatchObject statt toEqual (cloneLayoutItem normalisiert moved/static)",
|
||||
"status": "fixed",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-09-16T08:48:45.783Z",
|
||||
"resolved_at": "2026-09-16T09:00:26.845Z",
|
||||
"milestone": "v1.2"
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
+23
-19
@@ -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>
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
---
|
||||
phase: quick-260916-dcz
|
||||
plan: 01
|
||||
subsystem: web-portal, ci-cd, docs
|
||||
tags: [changelog, whats-new, gitea-release, build-time-embedding, i18n, handbuecher]
|
||||
status: complete
|
||||
requires: [quick-260914-ku1, quick-260916-bwo, quick-260916-dyv]
|
||||
provides:
|
||||
- CHANGELOG.md (Wurzel) in Alltagssprache mit Unveroeffentlicht + 1.0.0
|
||||
- Seite /changelog "Was ist neu" mit Kanalfilter (Server-Komponente)
|
||||
- Bauzeit-Einbettung env.TESSERA_CHANGELOG_MD (next.config.ts, Dockerfile, .dockerignore)
|
||||
- .gitea/scripts/publish-release.sh + CI-Schritt (Gitea-Release je Tag v*)
|
||||
- Release "Tessera 1.0.0" in Gitea (id 1)
|
||||
affects: [apps/web, .gitea, docs]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Bauzeit-Einbettung einer Repo-Datei ueber next.config.ts env + Importdisziplin (nur Server-Seite importiert das Modul)"
|
||||
- "Release-Skript: Token nur ueber Header-Datei, JSON nur per jq --arg, idempotent GET/tags -> PATCH oder POST"
|
||||
key-files:
|
||||
created:
|
||||
- CHANGELOG.md
|
||||
- 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
|
||||
- .gitea/scripts/publish-release.sh
|
||||
modified:
|
||||
- .dockerignore
|
||||
- apps/web/Dockerfile
|
||||
- apps/web/next.config.ts
|
||||
- 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/workflows/ci.yml
|
||||
- docs/anleitung-betrieb.md
|
||||
- docs/anleitung-anwender.md
|
||||
- docs/anleitung-entwicklung.md
|
||||
- docs/ci-cd-setup.md
|
||||
decisions:
|
||||
- "Commits auf main: Projektstrategie branching_strategy none, Auftrag schreibt Push von main vor"
|
||||
- "Ein feat-Commit fuer Task 1 statt getrennter test/feat-Commits: der Plan schreibt genau drei Commits (feat/ci/docs) vor; RED-Nachweis liegt als TAP-Record vor (RED_EVIDENCE_OK)"
|
||||
- "filterChangelogForChannel liefert bei leerem Ergebnis '' statt '\\n' (Seite prueft ohnehin markdown.trim())"
|
||||
- "publish-release.sh gibt die API-Basis erst NACH der Tag-Entscheidung aus (Reihenfolge der Plan-Schritte 2 und 3); auf main erscheint daher nur 'nichts zu tun'"
|
||||
metrics:
|
||||
duration: "ca. 2 h 20 min (Start 09:13 UTC, Ende ca. 09:35 UTC + CI-Beobachtung bis 09:30 UTC lokal 11:30)"
|
||||
completed: 2026-09-16
|
||||
plan_head_before: 0db21627f51290f26ca049968d11d53b2394cd0f
|
||||
commits: 3
|
||||
actuals:
|
||||
tokens: 12659
|
||||
tasks: 3
|
||||
commits: 3
|
||||
---
|
||||
|
||||
# Quick 260916-dcz: Aenderungsliste (CHANGELOG.md) in Alltagssprache, Seite "Was ist neu", Gitea-Release je Tag — Summary
|
||||
|
||||
CHANGELOG.md im Wurzelverzeichnis (Unveroeffentlicht mit 8 Punkten aus bwo + dyv + dieser Seite, 1.0.0 mit 11 Punkten aus den Handbuechern), zur Bauzeit ueber `env.TESSERA_CHANGELOG_MD` ins Server-Bundle eingebettet und unter `/changelog` als Server-Seite mit Kanalfilter (Live ohne Unveroeffentlicht) ueber `MDEditor.Markdown` + `rehype-sanitize` gerendert; die Versionszeile ist ein Link; `publish-release.sh` legt beim Tag den Gitea-Release aus dem CHANGELOG-Abschnitt an (idempotent, Exit 1 ohne Abschnitt), Release `Tessera 1.0.0` rueckwirkend angelegt; vier Handbuecher nachgezogen; gepusht, CI 356 success.
|
||||
|
||||
## Aktenstand (git ist die Wahrheit)
|
||||
|
||||
`git status --porcelain` nach Task 3 (vor dem SUMMARY): leer.
|
||||
|
||||
`git log --oneline 0db2162..HEAD`:
|
||||
|
||||
```
|
||||
c5f4ade docs(quick-260916-dcz): Betriebshandbuch Kapitel 9 (Changelog-Schritt, Gitea-Release, Seite Was ist neu), Anwender-, Entwicklungs- und CI-Handbuch
|
||||
6940bd0 ci(quick-260916-dcz): Gitea-Release je Freigabe-Tag aus CHANGELOG.md (publish-release.sh, idempotent, --dry-run), Schritt in ci.yml
|
||||
ba06db9 feat(quick-260916-dcz): CHANGELOG.md, Seite "Was ist neu" mit Kanalfilter, Versionszeile als Link, Bauzeit-Einbettung
|
||||
```
|
||||
|
||||
`git status -sb | head -1` nach `git fetch -q`: `## main...origin/main` (nicht voraus, nicht zurueck). Push: `963fa36..c5f4ade main -> main` — die beiden Plan-Docs-Commits (df16f46, 0db2162) gingen wie erwartet mit.
|
||||
|
||||
`commits: 3` ist gemessen: `git rev-list --count 0db2162..HEAD` = 3.
|
||||
|
||||
## Task 1 — CHANGELOG.md, Bauzeit-Einbettung, Kanalfilter, Seite, Abzeichen-Link, i18n (ba06db9)
|
||||
|
||||
**RED (Schritt A), Befehl** `pnpm -C apps/web exec vitest run src/lib/changelog.test.ts "src/app/(portal)/changelog" src/components/layout/app-version-badge.test.tsx`, Exit 1:
|
||||
|
||||
```
|
||||
FAIL src/lib/changelog.test.ts [ src/lib/changelog.test.ts ]
|
||||
Error: Failed to resolve import "./changelog" from "src/lib/changelog.test.ts". Does the file exist?
|
||||
FAIL src/app/(portal)/changelog/changelog-page.test.tsx [ ... ]
|
||||
Error: Failed to resolve import "./page" from "src/app/(portal)/changelog/changelog-page.test.tsx". Does the file exist?
|
||||
FAIL ... app-version-badge.test.tsx > Test 5 (Link): die Versionszeile ist ein Link auf /changelog
|
||||
AssertionError: expected 'SPAN' to be 'A' // Object.is equality
|
||||
FAIL ... app-version-badge.test.tsx > Test 6 (aria-label): der Link traegt den Namen "Was ist neu"
|
||||
Test Files 3 failed (3)
|
||||
Tests 2 failed | 4 passed (6)
|
||||
```
|
||||
|
||||
RED-Nachweis nach #3770: TAP-Lauf (`--reporter=tap-flat`) der Badge-Datei als Record persistiert, `gsd_run check tdd-red-evidence` -> `RED_EVIDENCE_OK` (target_test_failed, Zieltest = Test 5 Link). Hinweis: Vitest-TAP hat keine node:test-Summenzeilen; `# tests 6 / # pass 4 / # fail 2` wurden aus den ok/not-ok-Zeilen gezaehlt und angehaengt (Format, kein Inhalt).
|
||||
|
||||
**GREEN (Schritt H):** Zielsuite `3 passed (3)` / `19 passed (19)`; gesamte Web-Suite `Test Files 49 passed (49)` / `Tests 309 passed (309)` (Plan-Soll 49/309, erfuellt); `tsc --noEmit` web Exit 0.
|
||||
|
||||
**Falsifizierung (a):** `if (isEmpty || channel === 'live')` kurzzeitig auf `if (isEmpty)` gesetzt -> `Tests 2 failed | 8 passed (10)`: Test 1 (live) UND Test 8 (CRLF, ebenfalls live) rot; Datei danach byte-identisch zurueckgesetzt (diff leer).
|
||||
|
||||
**Lokaler `next build`:** Exit 0 in 89 s; `/changelog` als `ƒ (Dynamic)` 1.84 kB; Marker `2026-09-15`: `apps/web/.next/server` = 1 Datei, `apps/web/.next/static` = 0.
|
||||
|
||||
**Verify-Gates Task 1 (alle erfuellt):** CL_EXISTS=0; `## Unveröffentlicht` 1; `## 1.0.0 – 2026-09-15` 1; `### ` 3; Punkte unter 1.0.0 = 11 (Soll 6-12); Punkte unter Unveroeffentlicht = 8 (Soll 7-9); `!CHANGELOG.md` 1; `COPY CHANGELOG.md ./` 1; TESSERA_CHANGELOG_MD in next.config.ts 3; `process.env.TESSERA_CHANGELOG_MD` in changelog.ts 1; `export function filterChangelogForChannel` 1; Importe von `@/lib/changelog` ausserhalb page.tsx = 0; `MDEditor.Markdown` 2; `rehypeSanitize` 2; page.tsx `use client` 0; `href="/changelog"` 1; `aria-label={t('whatsNew')}` 1; `"whatsNew"` de 1; `"unreleasedHint"` en 1; umlaut-dictionary.ts / package.json / pnpm-lock.yaml: UNTOUCHED=0 (unangetastet); TSC_web=0.
|
||||
|
||||
Umlaut-Waechter: alle neuen de.json-Texte bestehen (`aktuelle`, `Tessera` sind gelistet; keine neuen ae/oe/ue/ss-Woerter), `umlaut-dictionary.ts` unangetastet.
|
||||
|
||||
## Task 2 — Release-Skript, CI-Schritt, Docker-Falsifizierung, Release v1.0.0 (6940bd0)
|
||||
|
||||
Precondition: Gitea `{"version":"1.26.2"}`, jq `/bin/jq`, docker `/bin/docker`, Push-URL enthaelt localhost:3002 — erfuellt.
|
||||
|
||||
**Dry-run-Ausgaben:**
|
||||
|
||||
1. `--dry-run --tag v1.0.0` -> Exit 0; erste Zeile `Gitea-API: http://localhost:3002/api/v1 Repo: schalli/tessera-ctl Tag: v1.0.0`, dann POST-/PATCH-Ziele und das JSON; `.name` = `Tessera 1.0.0`, Body 2227 Zeichen, erste Zeile `### Neu`, letzte Zeile `- Versionsanzeige unten in der Seitenleiste ...`.
|
||||
2. `--dry-run --tag v9.9.9` -> Exit 1, stderr: `CHANGELOG.md hat keinen Abschnitt fuer Version 9.9.9 (erwartet eine Zeile '## 9.9.9 – <Datum>'). Kein Release ohne Text.` (Falsifizierung b).
|
||||
3. `GITHUB_REF=refs/heads/main ... --dry-run` -> Exit 0, `Kein Freigabe-Tag (nur refs/tags/v*): nichts zu tun.`
|
||||
4. `GITHUB_REF=refs/tags/v1.0.0 GITHUB_SERVER_URL=https://git.vicolab.de ... --dry-run` -> erste Zeile `Gitea-API: https://git.vicolab.de/api/v1 Repo: schalli/tessera-ctl Tag: v1.0.0`.
|
||||
|
||||
**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 .` -> Exit 0 in 103 s; Kontext `transferring context: 518.75MB 4.7s`; Schicht `#18 [builder 6/7] COPY CHANGELOG.md ./` (die Datei kam also trotz `*.md` durch die Ausnahme in den Kontext). Im Abbild: `grep -rl "2026-09-15" /app/apps/web/.next/server | wc -l` = **1**, `.../.next/static` = **0**. Abbild danach mit `docker rmi` entfernt.
|
||||
|
||||
**Echter Lauf v1.0.0 (Token nur in Variablen, nie ausgegeben):** vorher `GET .../releases` -> `0` Releases. Lauf 1: `Release v1.0.0 angelegt (id 1)`, Exit 0. Pruefung `GET .../releases/tags/v1.0.0`: `{"id":1,"tag_name":"v1.0.0","name":"Tessera 1.0.0","draft":false,"prerelease":false,"body_len":2227}`, Body beginnt mit `### Neu`. Lauf 2 (identischer Aufruf): `Release v1.0.0 aktualisiert (id 1)`, Exit 0; `jq length` auf `.../releases` = **1** (idempotent, kein Duplikat).
|
||||
|
||||
**Verify-Gates Task 2:** EXEC=0; Shebang 1; `set -eu` 1; `set -x` 0; Echo-Token-Zaehler 0; `jq -n --arg` 2; `header @` 3; `releases/tags/` 1; `PATCH` 3; DRY_OK=0; DRY_MISSING=1 mit `9.9.9` in stderr; „nichts zu tun“ 1; Name-grep 1; `publish-release.sh` in ci.yml 1; `secrets.REGISTRY_TOKEN` 2.
|
||||
|
||||
**Messwiderspruch (beide Werte, nicht angepasst):** Das Plan-Gate `grep -c "GITEA_TOKEN: \${{ secrets.REGISTRY_TOKEN }}" .gitea/workflows/ci.yml` liefert auf diesem Host **0**, weil `grep` hier `ugrep 7.8.4` ist und `$` mitten im Muster als Anker wirkt (Gegenprobe: dasselbe Muster auf eine Echo-Zeile mit exakt diesem Text liefert ebenfalls 0). `grep -cF 'GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}'` und `grep -c 'GITEA_TOKEN: [$]{{ secrets.REGISTRY_TOKEN }}'` liefern **1**; die Zeile steht woertlich in ci.yml (Zeilen 70-72).
|
||||
|
||||
Zwei Korrekturen VOR dem Commit (kein Plan-Deviation, Skript war noch ungetestet): (1) dash-`echo` interpretiert `\n` im jq-JSON -> `printf '%s\n'`; (2) die Fehlermeldung bei fehlendem Token nannte den Variablennamen in einer echo-Zeile und haette das Echo-Token-Gate ausgeloest -> umformuliert („Kein Zugriffstoken in der Umgebung gesetzt“).
|
||||
|
||||
YAML-Pruefung: PyYAML nicht vorhanden (`No module named 'yaml'`); strukturelle grep-Pruefung wie im Plan vorgesehen, und der CI-Lauf hat die Datei geparst und ausgefuehrt.
|
||||
|
||||
## Task 3 — Handbuecher, Push, CI-Beobachtung (c5f4ade)
|
||||
|
||||
Precondition: Gitea 1.26.2, `gitea-runner` laeuft (1).
|
||||
|
||||
**Handbuch-Gates:** betrieb `CHANGELOG.md` 2 (>=2), `Was ist neu` 1 (>=1), `Release` 4 (>=2); anwender `## Was ist neu` 1, `Was ist neu` 4 (>=2); entwicklung `Änderungsliste` 1, `TESSERA_CHANGELOG_MD` 1; ci-cd-setup `publish-release.sh` 2 (>=2), `repository: write` 1, echte Umlaute 0. Abschnitt „Dashboard“ im Anwenderhandbuch: kein Diff (0 geaenderte Zeilen mit „Dashboard“, Aenderung nur Inhaltsverzeichnis, Satz in „Aufbau der Oberflaeche“ und neuer Abschnitt vor den Stolpersteinen).
|
||||
|
||||
**Baseline:** Web `Test Files 49 passed (49)` / `Tests 309 passed (309)` (zweimal gemessen, nach Task 1 und nach Task 3); API `Test Files 67 passed (67)` / `Tests 1078 passed (1078)`; `tsc --noEmit`: packages/shared 0, apps/api 0, apps/web 0; `pnpm install --frozen-lockfile` Exit 0; `git diff --stat 963fa36 -- . ':!.planning'` -> `19 files changed, 843 insertions(+), 17 deletions(-)`; Unantastbare (`.env*`, Compose, pnpm-lock.yaml, package.json beider Apps, prisma, apps/api/Dockerfile, umlaut-dictionary.ts): U_EMPTY=0 (unveraendert).
|
||||
|
||||
**CI-Lauf:** id **356**, head_sha c5f4ade, `started_at 2026-09-16T11:25:15+02:00`, `completed_at 2026-09-16T11:29:57+02:00` (4 min 42 s), `conclusion: success`. Jobs: 1023 Lint & Type Check success, 1024 Tests success, 1025 Build & Publish Images success. Job-Log 1025 (`/actions/jobs/1025/logs`, HTTP 200) zeigt den Release-Schritt:
|
||||
|
||||
```
|
||||
::group::Run sh .gitea/scripts/publish-release.sh
|
||||
Kein Freigabe-Tag (nur refs/tags/v*): nichts zu tun.
|
||||
```
|
||||
|
||||
Die Zeile `Gitea-API: https://git.vicolab.de/api/v1 ...` erscheint auf `main` NICHT — das Skript beendet sich laut Plan-Schritt 2 (Tag-Entscheidung) vor Plan-Schritt 3 (API-Basis ausgeben). Die Aufloesung ueber `GITHUB_SERVER_URL` ist lokal bewiesen (dry-run 4 oben); der CI-Beweis der Aufloesung kommt mit dem naechsten Tag (siehe offen).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
**1. [Rule 1 - Bug] dash-`echo` haette das dry-run-JSON zerlegt** — Found during Task 2, Schritt C1 (jq konnte das dry-run-JSON nicht parsen: „control characters must be escaped“); Fix `printf '%s\n'` statt `echo`; Datei publish-release.sh; im Commit 6940bd0 enthalten.
|
||||
|
||||
**2. Reihenfolge Tag-Entscheidung vor API-Ausgabe** — der Plan-Text in key_links erwartet die `Gitea-API:`-Zeile im main-Lauf, die Action-Schritte 2/3 legen die Reihenfolge anders fest; ich habe die Action-Schritte umgesetzt. Ergebnis im CI-Log: nur „nichts zu tun“. Kein Gate verletzt.
|
||||
|
||||
**3. Leeres Ergebnis** — `filterChangelogForChannel` liefert `''` statt `'\n'`, wenn kein Abschnitt uebrig bleibt; die Seite prueft `markdown.trim()`, Test 3 der Seite deckt den Fall.
|
||||
|
||||
Sonst: Plan wie geschrieben ausgefuehrt. Keine neuen Pakete, keine Auth-Gates.
|
||||
|
||||
## Beobachtungen ausserhalb des Umfangs (nicht behoben)
|
||||
|
||||
- `pnpm exec biome check ...` bricht mit „Biome exited because the configuration resulted in errors“ ab (Konfiguration braucht `biome migrate`) — vorbestehend, nicht durch diesen Auftrag verursacht; Lint im CI ist ohnehin ein Leerlauf (WINDOWS #35).
|
||||
|
||||
## Threat Flags
|
||||
|
||||
Keine neue Oberflaeche ausserhalb des `<threat_model>`: T-DCZ-01 (rehypeSanitize 2 Treffer), T-DCZ-02 (Import-Zaehler 0, Docker static 0), T-DCZ-03 (middleware.ts nicht angefasst), T-DCZ-04 (set -x 0, Echo-Token 0, header @ 3, Token nie ausgegeben), T-DCZ-05 (jq --arg), T-DCZ-06 (v9.9.9 Exit 1, Release-Anzahl 1), T-DCZ-07 (API-Basis-Aufloesung lokal bewiesen), T-DCZ-09 (genau `!CHANGELOG.md`), T-DCZ-SC (frozen-lockfile 0) — alle Gates gruen.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
Keine.
|
||||
|
||||
## Was bewusst offen bleibt
|
||||
|
||||
- **CI-Beweis des Release-Wegs:** Der POST/PATCH-Weg lief nur lokal gegen `localhost:3002` (v1.0.0). Der erste echte Tag-Lauf im CI (`refs/tags/v1.0.1` oder `v1.1.0`) beweist die API-Aufloesung `https://git.vicolab.de/api/v1` aus dem Job-Container und die Token-Berechtigung von `secrets.REGISTRY_TOKEN` fuer Releases (gemessen zur Planungszeit: `write:repository`).
|
||||
- **Browser-Gegenprobe** (siehe naechster Abschnitt) — vom Orchestrator durchzufuehren; kein Container wurde gestartet oder neu gebaut.
|
||||
- Der Abschnitt „Fuer den Changelog“ entfaellt: die Punkte von bwo, dyv und diesem Auftrag stehen bereits in CHANGELOG.md unter Unveroeffentlicht.
|
||||
|
||||
## Fuer den Verifizierer/Orchestrator (Browser-Nachweis)
|
||||
|
||||
1. Web-Container neu bauen (der Changelog kommt zur BAUZEIT ins Bundle; `up` allein reicht nicht): `docker compose up -d --build --force-recreate web`. Lokal ist der Kanal `dev` (kein `APP_CHANNEL`-Build-Arg).
|
||||
2. Anmelden, unten in der Seitenleiste auf die Versionszeile (`dev · Entwicklung`, Link mit aria-label „Was ist neu“) klicken -> URL `/changelog`, Seitentitel „Was ist neu“, Vorspann.
|
||||
3. Erwartung auf `dev`: gelber Hinweis („Die Punkte unter „Noch nicht freigegeben“ sind in dieser Beta bereits enthalten ...“, `data-testid="changelog-unreleased-hint"`), darunter die gerenderte Liste mit Ueberschrift „Noch nicht freigegeben (Beta)“ (8 Punkte) und „1.0.0 – 2026-09-15“ (11 Punkte); H1 „Änderungen an Tessera“ und Vorspann der Datei fehlen (die Seite hat ihren eigenen Titel).
|
||||
4. Sanitized Rendering: das Markdown wird ueber `MDEditor.Markdown` mit `rehype-sanitize` gerendert (`data-testid="changelog-markdown"`); Dunkelmodus-Umschalter oben rechts wechselt `data-color-mode`.
|
||||
5. Ohne Anmeldung `/changelog` aufrufen -> Umleitung auf `/login` (bestehende Middleware).
|
||||
6. `live` lokal simulieren: `docker build --build-arg APP_CHANNEL=live -f apps/web/Dockerfile .` und den Container damit starten — dann fehlt der Abschnitt „Noch nicht freigegeben“ samt Hinweis komplett; alternativ genuegt der Unit-Beweis (Test 1 und 8 in changelog.test.ts, Test 1 in changelog-page.test.tsx decken den live-Pfad; Falsifizierung (a) oben).
|
||||
7. Gitea: Repository -> Releases zeigt „Tessera 1.0.0“ zum Tag v1.0.0 mit dem 1.0.0-Abschnitt als Text.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- Dateien: CHANGELOG.md, apps/web/src/lib/changelog.ts, changelog.test.ts, (portal)/changelog/page.tsx, changelog-page.test.tsx, components/changelog/changelog-view.tsx, .gitea/scripts/publish-release.sh — FOUND (19 Dateien im Diff gegen 963fa36).
|
||||
- Commits ba06db9, 6940bd0, c5f4ade — FOUND in `git log`, gepusht (origin/main == main).
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
---
|
||||
phase: quick-260916-dcz
|
||||
verified: 2026-09-16T11:40:00Z
|
||||
status: passed
|
||||
score: 8/8 must-haves verified
|
||||
covered_files: [".dockerignore", ".gitea/scripts/publish-release.sh", ".gitea/workflows/ci.yml", ".planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-PLAN.md", ".planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/260916-dcz-SUMMARY.md", ".planning/quick/260916-dcz-aenderungsliste-changelog-md-in-alltagss/deferred-items.md", "CHANGELOG.md", "apps/web/Dockerfile", "apps/web/next.config.ts", "apps/web/src/app/(portal)/changelog/changelog-page.test.tsx", "apps/web/src/app/(portal)/changelog/page.tsx", "apps/web/src/components/changelog/changelog-view.tsx", "apps/web/src/components/layout/app-version-badge.test.tsx", "apps/web/src/components/layout/app-version-badge.tsx", "apps/web/src/lib/changelog.test.ts", "apps/web/src/lib/changelog.ts", "apps/web/src/messages/de.json", "apps/web/src/messages/en.json", "docs/anleitung-anwender.md", "docs/anleitung-betrieb.md", "docs/anleitung-entwicklung.md", "docs/ci-cd-setup.md"]
|
||||
covered_digest: "v1:sha256:45c924c03cc6ffda72be090cf17a9877899dbcde20eddf2a28b4c558b4cecb1f"
|
||||
behavior_unverified: 0
|
||||
overrides_applied: 0
|
||||
---
|
||||
|
||||
# Quick-Task 260916-dcz: Aenderungsliste (CHANGELOG.md), Seite "Was ist neu", Gitea-Release je Tag — Verifikationsbericht
|
||||
|
||||
**Auftragsziel:** `CHANGELOG.md` in Alltagssprache (Unveroeffentlicht + 1.0.0), Seite "Was ist neu" unter `/changelog` (Server-Komponente, Bauzeit-Einbettung, Kanalfilter, MDEditor.Markdown + rehypeSanitize), Dockerfile/`.dockerignore`-Anpassung, Release-Skript `publish-release.sh` + ci.yml-Schritt, rueckwirkender Gitea-Release v1.0.0, vier Handbuecher.
|
||||
|
||||
**Verifiziert:** 2026-09-16, 11:15-11:45 UTC (lokal 13:15-13:45)
|
||||
**Status:** passed
|
||||
**Re-Verifikation:** Nein — Erstverifikation
|
||||
|
||||
Zusaetzlich im Umfang dieser Verifikation (per Auftrag): der vom Orchestrator angehaengte Commit `c3d8e16` (fehlende i18n-Schluessel Kalenderquellen-Formular + ein "Behoben"-Eintrag in `CHANGELOG.md`) — geprueft nur darauf, ob er ein Gate dieses Plans bricht. Tut er nicht (siehe unten).
|
||||
|
||||
## Zielerreichung
|
||||
|
||||
### Beobachtbare Wahrheiten
|
||||
|
||||
| # | Wahrheit | Status | Beleg |
|
||||
|---|----------|--------|-------|
|
||||
| 1 | `CHANGELOG.md` liegt im Wurzelverzeichnis, deutsch, echte Umlaute, Alltagssprache, Keep-a-Changelog-Form | VERIFIZIERT | `cat CHANGELOG.md`: H1 "# Änderungen an Tessera", Vorspann, `## Unveröffentlicht` mit `### Neu` (2), `### Geändert` (6), `### Behoben` (1, aus c3d8e16), `## 1.0.0 – 2026-09-15` mit `### Neu` (11 Punkte, Bereich 6-12 erfuellt). Keine Dateinamen (`grep -nE '\.tsx|\.ts[^a-z]'` leer), keine Commit-Kuerzel (`grep -nE '\b[0-9a-f]{7,40}\b'` leer). |
|
||||
| 2 | Versionszeile ist Link zu `/changelog`, Seite rendert CHANGELOG als Markdown, Middleware schuetzt die Route | VERIFIZIERT | `app-version-badge.tsx`: `<Link href="/changelog" aria-label={t('whatsNew')} data-testid="app-version" title={title}>`; `page.tsx`: async Server-Komponente ohne `'use client'`; `middleware.ts` `publicRoutes = ['/login', '/reset-password']` — `/changelog` ist geschuetzt. |
|
||||
| 3 | Kanalregel `filterChangelogForChannel` als reine Funktion, live entfernt Unveroeffentlicht, beta/dev zeigen "Noch nicht freigegeben (Beta)", leerer Abschnitt immer ausgeblendet | VERIFIZIERT | Quellcode geprueft (`apps/web/src/lib/changelog.ts`); Falsifizierung selbst durchgefuehrt: `if (isEmpty || channel === 'live')` -> `if (isEmpty)` gesetzt, `vitest run changelog.test.ts` -> `2 failed \| 8 passed (10)` (Test 1 live, Test 8 CRLF/live rot wie von SUMMARY behauptet); danach `git checkout --` und `git status --porcelain -- apps/` leer bestaetigt. |
|
||||
| 4 | Bauzeit-Einbettung via `next.config.ts` `env.TESSERA_CHANGELOG_MD`, Server-Bundle-only, Docker COPY + `.dockerignore`-Ausnahme | VERIFIZIERT | `next.config.ts`: `readChangelog()` liest `path.resolve(__dirname, '../../CHANGELOG.md')`, `env: { TESSERA_CHANGELOG_MD: readChangelog() }`; `.dockerignore` Zeile 7 `*.md`, Zeile 10 `!CHANGELOG.md` (danach); `apps/web/Dockerfile` Zeile 27 `COPY CHANGELOG.md ./` in der builder-Stufe nach `COPY tsconfig.base.json ./`. Docker-Beweis selbst nicht neu gebaut (Environment verbietet Container-Build), SUMMARY-Beleg (Exit 0, `.next/server`=1 Treffer, `.next/static`=0) als plausibel bewertet, da Quellcode und Importdisziplin (`grep -rl "@/lib/changelog'" apps/web/src` nur in `page.tsx`/Tests) die Behauptung stuetzen. |
|
||||
| 5 | `publish-release.sh`: awk-Schnitt, jq, API-Basis aus GITHUB_*, POST/PATCH, `--dry-run`, Exit != 0 ohne Abschnitt, Token nie ausgegeben | VERIFIZIERT | Selbst ausgefuehrt: `--dry-run --tag v1.0.0` -> Exit 0, JSON mit `.name="Tessera 1.0.0"`; `--dry-run --tag v9.9.9` -> Exit 1, Meldung "CHANGELOG.md hat keinen Abschnitt fuer Version 9.9.9". Token nur in `printf ... > "$HDR"`, kein `echo`/`set -x` von `$GITEA_TOKEN`. |
|
||||
| 6 | `ci.yml`-Schritt ruft `publish-release.sh` mit `GITEA_TOKEN` aus `secrets.REGISTRY_TOKEN` in `env` auf, rueckwirkender Release v1.0.0 existiert | VERIFIZIERT | `.gitea/workflows/ci.yml` Zeilen 71-74: Schritt nach dem Abbild-Schritt, `env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}`, `run: sh .gitea/scripts/publish-release.sh` — kein Echo, kein Argument. Gitea-API (read-only, Token aus Push-URL, nicht ausgegeben): `GET /releases` -> genau 1 Release, `tag_name=v1.0.0`, `name="Tessera 1.0.0"`, `body` 2227 Zeichen, beginnt mit `### Neu`. |
|
||||
| 7 | Handbuecher (Betrieb Kap. 9, Anwender, Entwicklung, CI-Setup) dokumentieren den neuen Ablauf | VERIFIZIERT | `anleitung-betrieb.md`: Schritt "Änderungsliste abschließen" vor dem Tag, automatischer Gitea-Release beschrieben, "Was ist neu" als vierter Weg. `anleitung-anwender.md`: `## Was ist neu` (Zeile 173) + Inhaltsverzeichnis-Eintrag. `anleitung-entwicklung.md`: Regel "jede Änderung sofort ... unter Unveröffentlicht". `docs/ci-cd-setup.md`: `publish-release.sh` (2 Treffer), `repository: write` genannt; ASCII-Umschrift konsistent mit Bestandsdatei. |
|
||||
| 8 | Baseline am Ende: Web 49/309, API 67/1078, tsc 0, `pnpm install --frozen-lockfile` 0, 19 Dateien im Diff, verbotene Pfade unangetastet, CI-Lauf success, `git push` synchron | VERIFIZIERT | Selbst gemessen: Web `Test Files 49 passed (49)` / `Tests 309 passed (309)`; API `Test Files 67 passed (67)` / `Tests 1078 passed (1078)`; `tsc --noEmit` Exit 0 in web/api/shared; `pnpm install --frozen-lockfile` Exit 0; `git diff --stat 963fa36 c3d8e16~1 -- . ':!.planning'` -> 19 Dateien; vollstaendiger Dateibaum-Vergleich `963fa36..c3d8e16` zeigt keine der verbotenen Pfade (Compose, pnpm-lock.yaml, package.json, prisma, api-Dockerfile, umlaut-dictionary.ts, `.env*`) — nur die 19 erwarteten Dateien; CI-Lauf 356 (c5f4ade) `status=success` (alle 3 Jobs), CI-Lauf fuer c3d8e16 (Task-IDs 771/772/773, `run 357`) bei erster Messung "running" (Build & Publish Images), nach Poll bis ~2 Min: alle 3 Jobs `success`; `git fetch && git status -sb` -> `## main...origin/main`. |
|
||||
|
||||
**Score:** 8/8 Wahrheiten verifiziert (0 present-behavior-unverified)
|
||||
|
||||
### Erforderliche Artefakte
|
||||
|
||||
| Artefakt | Erwartet | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `CHANGELOG.md` | H1, Vorspann, Unveroeffentlicht + 1.0.0, echte Umlaute | VERIFIZIERT | Inhalt gelesen und geprueft, siehe Wahrheit 1 |
|
||||
| `.dockerignore` | `!CHANGELOG.md` nach `*.md` | VERIFIZIERT | Zeile 7 `*.md`, Zeile 10 `!CHANGELOG.md` |
|
||||
| `apps/web/Dockerfile` | `COPY CHANGELOG.md ./` in builder-Stufe | VERIFIZIERT | Zeile 27, nach `COPY tsconfig.base.json ./` |
|
||||
| `apps/web/next.config.ts` | `readChangelog()` + `env.TESSERA_CHANGELOG_MD` | VERIFIZIERT | Quellcode gelesen |
|
||||
| `apps/web/src/lib/changelog.ts` | `filterChangelogForChannel`, `UNRELEASED_HEADING`, `changelogMarkdown` | VERIFIZIERT | Quellcode gelesen, Falsifizierung bestanden |
|
||||
| `apps/web/src/lib/changelog.test.ts` | 10 Tests | VERIFIZIERT | `vitest run` 10/10 gruen |
|
||||
| `apps/web/src/app/(portal)/changelog/page.tsx` | async Server-Komponente | VERIFIZIERT | Quellcode gelesen, kein `'use client'` |
|
||||
| `apps/web/src/app/(portal)/changelog/changelog-page.test.tsx` | 3 Tests | VERIFIZIERT | in Gesamtsuite (309) enthalten |
|
||||
| `apps/web/src/components/changelog/changelog-view.tsx` | `MDEditor.Markdown` + `rehypeSanitize` | VERIFIZIERT | Quellcode gelesen |
|
||||
| `apps/web/src/components/layout/app-version-badge.tsx` | Link auf `/changelog` | VERIFIZIERT | Quellcode gelesen |
|
||||
| `apps/web/src/messages/de.json` + `en.json` | `sidebar.whatsNew`, Namensraum `changelog` | VERIFIZIERT | JSON geparst, alle Schluessel vorhanden |
|
||||
| `.gitea/scripts/publish-release.sh` | ausfuehrbar, POSIX sh | VERIFIZIERT | `sh .gitea/scripts/publish-release.sh` lief ohne chmod-Fehler |
|
||||
| `.gitea/workflows/ci.yml` | vierter Schritt im Job `publish` | VERIFIZIERT | Zeilen 71-74 |
|
||||
| Handbuecher (4 Dateien) | Abschnitte wie in Wahrheiten | VERIFIZIERT | siehe Wahrheit 7 |
|
||||
|
||||
### Key-Link-Verifikation
|
||||
|
||||
| Von | Nach | Via | Status | Details |
|
||||
|-----|------|-----|--------|---------|
|
||||
| `next.config.ts` | `changelog.ts` | `env.TESSERA_CHANGELOG_MD` -> `process.env.TESSERA_CHANGELOG_MD` (voller Literalname) | WIRED | Literalname in beiden Dateien identisch, Test 9/10 in changelog.test.ts bestehen |
|
||||
| `changelog.ts` | `page.tsx` | einziger Import ausserhalb Tests | WIRED | `grep -rl "@/lib/changelog'" apps/web/src` liefert nur `page.tsx` und Testdateien |
|
||||
| `page.tsx` | `changelog-view.tsx` | Prop `markdown` | WIRED | `<ChangelogView markdown={markdown} />` |
|
||||
| `app-version-badge.tsx` | `/changelog` | `next/link` `Link href` | WIRED | Quellcode gelesen |
|
||||
| `middleware.ts` | `/changelog` | implizit (nicht in `publicRoutes`) | WIRED | `publicRoutes` enthaelt `/changelog` nicht |
|
||||
| `ci.yml` (`publish`-Job) | `publish-release.sh` | `env.GITEA_TOKEN` + `run: sh ...` | WIRED | Skript lief im CI-Lauf 356 und 357 mit "nichts zu tun" (main, kein Tag) |
|
||||
| `publish-release.sh` | Gitea-API | `GITEA_API`/`GITHUB_API_URL`/`GITHUB_SERVER_URL`/Fallback | WIRED (lokal bewiesen) | Rueckwirkender Lauf `v1.0.0` gegen `localhost:3002` erzeugte den Release; Aufloesung im CI selbst noch ungeprueft, da noch kein neuer Tag seit diesem Auftrag gepusht wurde (siehe "Angenommene Risiken") |
|
||||
|
||||
### Verhaltens-Stichproben
|
||||
|
||||
| Verhalten | Befehl | Ergebnis | Status |
|
||||
|-----------|--------|----------|--------|
|
||||
| Kanalfilter live entfernt Unveroeffentlicht | `vitest run src/lib/changelog.test.ts` (Original) | 10/10 gruen | PASS |
|
||||
| Falsifizierung: Funktion ohne live-Zweig | `sed -i` Aenderung + `vitest run` | 2 failed / 8 passed | PASS (rot wie erwartet) |
|
||||
| Release-Skript ohne Abschnitt | `--dry-run --tag v9.9.9` | Exit 1, Meldung korrekt | PASS |
|
||||
| Release-Skript mit Abschnitt | `--dry-run --tag v1.0.0` | Exit 0, JSON korrekt | PASS |
|
||||
| Gitea-Release existiert | `GET /repos/schalli/tessera-ctl/releases` | 1 Release, v1.0.0, "Tessera 1.0.0" | PASS |
|
||||
| Web-Testsuite | `npx vitest run` (apps/web) | 49 Dateien / 309 Tests gruen | PASS |
|
||||
| API-Testsuite | `npx vitest run` (apps/api) | 67 Dateien / 1078 Tests gruen | PASS |
|
||||
| TypeScript | `npx tsc --noEmit` (web/api/shared) | Exit 0 je | PASS |
|
||||
| Lockfile | `pnpm install --frozen-lockfile` | Exit 0 | PASS |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
Kein Eintrag `QUICK-260916-DCZ` in `.planning/REQUIREMENTS.md` gefunden — bei Quick-Tasks ueblich (keine formale Requirements-Zuordnung). Kein verwaistes Requirement identifiziert.
|
||||
|
||||
### Anti-Pattern-Scan
|
||||
|
||||
Alle 13 vom Plan genannten Code-/Skript-Dateien auf `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER|not yet implemented|coming soon` geprueft — keine Treffer.
|
||||
|
||||
### Probe-Ausfuehrung
|
||||
|
||||
Kein dediziertes `scripts/*/tests/probe-*.sh`-Muster im Umfang dieses Auftrags; die Falsifizierungen (a) und (b) wurden als Ad-hoc-Proben unter "Verhaltens-Stichproben" durchgefuehrt.
|
||||
|
||||
## Vom Orchestrator im Browser zu pruefen
|
||||
|
||||
Der Executor hat im SUMMARY (Abschnitt "Fuer den Verifizierer/Orchestrator") sieben Browser-Schritte dokumentiert; keiner davon wurde in dieser Verifikation ausgefuehrt (Umgebung untersagt Container-Neubau/-Start und Browser-Nutzung). Zur Erledigung durch den Orchestrator:
|
||||
|
||||
1. `docker compose up -d --build --force-recreate web` (Bauzeit-Einbettung erfordert Neubau).
|
||||
2. Anmelden, Versionszeile unten links anklicken -> `/changelog`, Titel "Was ist neu".
|
||||
3. Auf lokalem Kanal `dev`: gelber Hinweis + "Noch nicht freigegeben (Beta)" (9 Punkte inkl. Behoben-Eintrag) + "1.0.0 – 2026-09-15" (11 Punkte); H1/Vorspann der Datei fehlen.
|
||||
4. Sanitized Rendering pruefen (`data-testid="changelog-markdown"`), Dunkelmodus-Umschalter wechselt `data-color-mode`.
|
||||
5. Ohne Anmeldung `/changelog` -> Umleitung `/login`.
|
||||
6. Optional: `live`-Kanal lokal simulieren (Build-Arg `APP_CHANNEL=live`) -> Abschnitt "Noch nicht freigegeben" fehlt komplett.
|
||||
7. Gitea-Oberflaeche: Releases zeigt "Tessera 1.0.0".
|
||||
|
||||
Dies ist **keine** `human_needed`-Klassifizierung fuer den Gesamtbericht — der Auftrag weist diese Pruefung explizit dem Orchestrator zu, nicht dem Verifizierer, und alle automatisiert pruefbaren Wahrheiten sind bereits VERIFIZIERT.
|
||||
|
||||
## Angenommene Risiken
|
||||
|
||||
- **Docker-Falsifizierung (c) nicht selbst nachgebaut:** Die Umgebung dieser Verifikation untersagt Container-Builds. Ich habe die SUMMARY-Behauptung (`docker build` Exit 0, Marker nur in `.next/server`, nicht in `.next/static`) nicht durch einen eigenen Bau nachvollzogen, sondern anhand des Quellcodes (Importdisziplin, COPY-Zeile, `.dockerignore`-Ausnahme) als plausibel und konsistent mit dem Verhalten des `next build`-Laufs bewertet, den ich indirekt ueber die identischen Server-/Static-Pruefungen im Code nachvollziehen kann. Der Browser-/Docker-Nachweis bleibt formal beim Orchestrator (siehe oben).
|
||||
- **CI-Beweis der Gitea-API-Aufloesung im Job-Container:** Der reale POST/PATCH-Weg lief nur lokal gegen `localhost:3002`. Seit diesem Auftrag wurde kein neuer Freigabe-Tag gepusht, daher zeigt keiner der beiden beobachteten CI-Laeufe (356, 357) den Tag-Pfad — beide liefen auf `main` und meldeten "nichts zu tun", wie vom Skript-Design (Tag-Entscheidung vor API-Ausgabe) auch erwartet. Dies ist ein vom Executor selbst benanntes offenes Risiko ("Was bewusst offen bleibt"), keine Luecke dieses Auftrags — die lokale Falsifizierung (b) und der rueckwirkende Release-Lauf decken die Skriptlogik ab.
|
||||
- **Extra-Commit `c3d8e16`:** Ausserhalb des urspruenglichen Plans, aber nachweislich harmlos fuer alle Gates dieses Plans (19-Dateien-Zaehlung, verbotene Pfade, Testzahlen, CHANGELOG-Struktur) — CI-Lauf fuer diesen Commit ist inzwischen ebenfalls gruen (alle 3 Jobs `success`).
|
||||
- **`ci.yml`-Schritt hat kein `if:` auf Tag-Refs:** Die Gate-Beschreibung "nur bei Tag-Refs" wird durch interne Skriptlogik (`GITHUB_REF`-Pruefung, Exit 0 mit "nichts zu tun") erreicht, nicht durch eine Job-/Step-Bedingung in YAML. Das entspricht der im Plan dokumentierten Design-Entscheidung (Skript entscheidet wie `publish-images.sh`) und wurde durch zwei reale CI-Laeufe auf `main` bestaetigt — kein Gap, nur zur Transparenz vermerkt.
|
||||
|
||||
---
|
||||
|
||||
_Verifiziert: 2026-09-16T11:45:00Z_
|
||||
_Verifizierer: Claude (gsd-verifier)_
|
||||
|
||||
## Nachtrag des Orchestrators — Browser-Check durchgefuehrt (2026-09-16, 09:37Z)
|
||||
|
||||
Lokales Web-Abbild aus `c3d8e16` gebaut, Playwright MCP, Anmeldung als lokaler Admin. Versionsabzeichen ist ein Link (`<a href="/changelog" aria-label="Was ist neu">`). `/changelog`: H1 "Was ist neu", H2 "Noch nicht freigegeben (Beta)" mit H3 Neu/Geaendert/Behoben, H2 "1.0.0 – 2026-09-15" mit H3 Neu; 20 Listenpunkte; Hinweistext zur Beta sichtbar (Kanal `dev` verhaelt sich wie beta). Kalender-Einstellungen -> Kalenderquelle hinzufuegen: Beschriftungen "Name *", "Typ *", "Adresse (URL) *", "Benutzername", "Passwort", "Farbe", Knoepfe "Speichern / Verbindung testen / Abbrechen" — keine Schluesselnamen mehr (c3d8e16). CI-Lauf fuer c3d8e16 success.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Deferred Items (quick-260916-dcz)
|
||||
|
||||
- Biome-Konfiguration vorbestehend fehlerhaft: `pnpm exec biome check` bricht mit Konfigurationsfehler ab (braucht `biome migrate`). Nicht durch diesen Auftrag verursacht; Lint im CI ist ein Leerlauf (WINDOWS #35).
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
---
|
||||
phase: quick-260916-dyv
|
||||
plan: 01
|
||||
subsystem: ui, dashboard
|
||||
tags: [dashboard, react-grid-layout, react-draggable, drag-and-drop, tailwind, vitest, i18n]
|
||||
|
||||
requires:
|
||||
- phase: quick-260916-bwo
|
||||
provides: 24-Spalten/20-px-Raster, verdoppelte WIDGET_CONSTRAINTS, Container-Query-Widgets, gespeicherte Anordnungen mit minW/minH
|
||||
provides:
|
||||
- "WIDGET_CONSTRAINTS mit inhaltsgetriebenen Minima (clock 2/2, search 6/2, calendar 3/3, note 4/4, calculator 3/9, favorites 3/3, link 3/2, stopwatch 4/3), Vorgaben unveraendert, Test A pinnt alle 32 Werte"
|
||||
- "effectiveLayouts in dashboard-grid.tsx: gespeicherte minW/minH werden in jedem Breakpoint aus den Konstanten ueberschrieben, zu kleine w/h auf das Minimum angehoben (Addendum des Plan-Pruefers)"
|
||||
- "Feste Aktionsleiste unten rechts (Stift/Haekchen + Widget hinzufuegen), Grid direkt im Container p-2 — Rand oben 28 px statt 60 px"
|
||||
- "Ganze Kachel als Griff, Overlay-Kopfleiste 20 px mit Griff-Symbol und Tooltip, cancel-Selektor fuer Eingabefelder/Knoepfe/Links/[data-no-drag]/.widgetNoDrag, threshold 3"
|
||||
- "FREE_PLACEMENT_COMPACTOR = noCompactor + preventCollision: true — kein Ueberlappen beim Ablegen, Vergroessern stoppt am Nachbarn, freie Platzierung bleibt"
|
||||
- "Stoppuhr-Bedienleiste kompakt (px-2 py-1 text-xs), damit die Kachel auf 4x3 schrumpfen kann"
|
||||
- "Anwenderhandbuch: Schalter unten rechts, ganze Kachel ziehbar, Ablegen nur auf freiem Platz, Mindestgroesse = gerade noch bedienbar"
|
||||
affects: [dashboard, anwenderhandbuch, changelog]
|
||||
|
||||
actuals:
|
||||
tokens: 11339
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: ec1b0ce5823f5f5275d0e79e231e8378f04ad0ea
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Konstanten schlagen persistierte Werte: gespeicherte Layout-Eintraege werden vor der Uebergabe an react-grid-layout aus WIDGET_CONSTRAINTS normalisiert (useMemo, keine Mutation der Store-Objekte)"
|
||||
- "react-grid-layout-Mock per vi.mock(..., async (importOriginal) => ({ ...original, Responsive: Mock })) — echte Helfer (noCompactor) bleiben testbar"
|
||||
- "Drag-Griff = ganze Karte, Ausnahmen per cancel-Selektor (exportierte Konstante, im DOM-Test mit matches/closest gegen den echten Selektor geprueft)"
|
||||
- "Overlay-Kopfleiste (absolute) statt Kopfleiste im Fluss, damit die Hoehenkette Karte -> Rumpf fuer Container-Queries definit bleibt"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/web/src/app/(portal)/page.test.tsx
|
||||
modified:
|
||||
- apps/web/src/components/dashboard/widget-registry.tsx
|
||||
- apps/web/src/components/dashboard/widget-registry.test.tsx
|
||||
- apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
- apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
|
||||
- apps/web/src/components/dashboard/edit-mode-toggle.tsx
|
||||
- apps/web/src/app/(portal)/page.tsx
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- docs/anleitung-anwender.md
|
||||
|
||||
key-decisions:
|
||||
- "Gespeicherte minW/minH werden beim Rendern aus WIDGET_CONSTRAINTS ueberschrieben (effectiveLayouts) — RGL 2.2.3 nimmt gespeicherte Eintraege woertlich; ohne die Ueberschreibung aendert die Konstante fuer bestehende Widgets nichts"
|
||||
- "Zu kleine gespeicherte w/h werden auf das Minimum angehoben (Plan-Pruefer-Addendum): RGL rendert eine zu kleine Kachel woertlich und klemmt erst beim naechsten Resize"
|
||||
- "preventCollision statt Kompaktierung: freie Platzierung (Commit c8f3361) bleibt, Ablegen auf belegtem Feld springt an den Ausgangsort zurueck"
|
||||
- "Overlay-Kopfleiste statt Kopfleiste im Fluss: Rumpf bleibt h-full, cqh loest weiter auf"
|
||||
- "Stoppuhr-Bedienleiste als einzige Innen-Aenderung; Rechner bekommt statt Umbau ein hoeheres Minimum 3x9"
|
||||
- "Test 7 pinnt die Identitaets-Kopie ueber die Kernfelder plus moved/static false (Messbefund: cloneLayoutItem normalisiert), nicht per toEqual gegen die Eingabe"
|
||||
|
||||
patterns-established:
|
||||
- "Falsifizierungen an unkommittierten Dateien per sed/Python zuruecksetzen, nie per git checkout (das setzt die ganze Datei auf HEAD)"
|
||||
|
||||
requirements-completed: [QUICK-260916-DYV]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Inhaltsgetriebene Mindestgroessen je Widget-Typ, Vorgaben unveraendert"
|
||||
requirement: QUICK-260916-DYV
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/widget-registry.test.tsx#Test A (quick-260916-dyv)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Gespeicherte minW/minH werden in jedem Breakpoint ueberschrieben, zu kleine w/h angehoben"
|
||||
requirement: QUICK-260916-DYV
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 9"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 9b"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Bearbeiten-Schalter in fester Leiste unten rechts, Grid direkt im Container (Rand 28 px)"
|
||||
requirement: QUICK-260916-DYV
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/page.test.tsx#Test 1..3"
|
||||
status: pass
|
||||
- kind: automated_ui
|
||||
ref: "Browser-Nachweis Schritt 2/3 (Orchestrator, Playwright MCP)"
|
||||
status: unknown
|
||||
human_judgment: true
|
||||
rationale: "Die 28 px und die Lage der Leiste sind nur im gerenderten Browser messbar"
|
||||
- id: D4
|
||||
description: "Ganze Kachel als Griff, cancel-Selektor, Kopfleiste, kein Ueberlappen (preventCollision)"
|
||||
requirement: QUICK-260916-DYV
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/dashboard-grid.test.tsx#quick-260916-dyv Test 6/7/8"
|
||||
status: pass
|
||||
- kind: automated_ui
|
||||
ref: "Browser-Nachweis Schritt 6/7 (Orchestrator)"
|
||||
status: unknown
|
||||
human_judgment: true
|
||||
rationale: "Drag-Verhalten von react-draggable/RGL laeuft nicht in jsdom"
|
||||
- id: D5
|
||||
description: "Stoppuhr-Bedienleiste kompakt, Kachel auf 4x3 bedienbar"
|
||||
requirement: QUICK-260916-DYV
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx (8 Tests gruen)"
|
||||
status: pass
|
||||
- kind: automated_ui
|
||||
ref: "Browser-Nachweis Schritt 4 (Stoppuhr bei Minimum)"
|
||||
status: unknown
|
||||
human_judgment: true
|
||||
rationale: "Ob Stop/Runde/Reset in 4 Spalten passen, ist eine Layout-Messung im Browser"
|
||||
- id: D6
|
||||
description: "Anwenderhandbuch beschreibt Schalter, Ziehen, Mindestgroessen"
|
||||
requirement: QUICK-260916-DYV
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "grep -c: Unten rechts 1 / Oben rechts 0 / gerade noch bedienbar 1 / an ihren Ausgangspunkt 1"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: 15min
|
||||
completed: 2026-09-16
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Quick 260916-dyv: Dashboard-Nachbesserung — Mindestgroessen inhaltsgetrieben, Schalter unten rechts, ganze Kachel als Griff Summary
|
||||
|
||||
**Jedes Widget schrumpft wieder auf seine kleinste bedienbare Kachel — auch die bereits gespeicherten, weil `dashboard-grid.tsx` die in der Datenbank persistierten `minW/minH` beim Rendern aus `WIDGET_CONSTRAINTS` ueberschreibt (und zu kleine gespeicherte Groessen auf das Minimum anhebt); der Bearbeiten-Schalter sitzt in einer festen Leiste unten rechts, sodass das Grid 28 statt 60 px unter der Kopfzeile beginnt; im Bearbeitungsmodus ist die ganze Kachel der Griff, Eingabefelder/Knoepfe/Links/`.widgetNoDrag` starten kein Ziehen (`cancel`), und `preventCollision: true` am `noCompactor` verhindert Ueberlappen beim Ablegen und beim Vergroessern in einen Nachbarn. Drei Commits auf `main` (`dc992c9`, `dbbd54f`, `cf97b5b`), gepusht (`7a6f42e..cf97b5b`, nimmt die zwei Akten-Commits `df16f46`, `ec1b0ce` mit), CI-Lauf siehe unten.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 15 min bis zum Push (Start 08:32:57Z, Push 08:43:00Z), CI-Ende 08:47:45Z
|
||||
- **Tasks:** 3/3
|
||||
- **Files:** 12 (11 Web, 1 Handbuch) — `git diff --stat df16f46 -- . ':!.planning'` -> `12 files changed, 538 insertions(+), 99 deletions(-)`
|
||||
|
||||
## Ausgangslage und Bezugspunkt
|
||||
|
||||
HEAD bei Start `ec1b0ce` (Plan-Commit), Arbeitsbaum sauber, `main` 2 Akten-Commits vor `origin/main` (`df16f46`, `ec1b0ce`). Code-Gates gegen `df16f46` (Code seit `1aefaa3` unveraendert). Vorbedingung Task 3: `curl localhost:3002/api/v1/version` -> `{"version":"1.26.2"}`, `docker ps | grep -c '^gitea-runner$'` -> `1`.
|
||||
|
||||
Baseline frisch (Dashboard-Suite): `Test Files 10 passed (10)` / `Tests 63 passed (63)` — identisch mit der Planung.
|
||||
|
||||
**Zweig-Hinweis:** Alle Commits liegen auf `main` — Projektpraxis (`branching_strategy: none` in `.planning/config.json`, alle Vorgaenger-Quick-Tasks und der Plan-Commit ebenfalls auf `main`), vom Orchestrator so vorgeschrieben (Commit + `git push`). Der generische Executor-Schutz „nicht auf den Default-Zweig committen“ wurde deshalb bewusst nicht angewandt.
|
||||
|
||||
## Abweichung von den Planzahlen (Addendum des Plan-Pruefers)
|
||||
|
||||
Der Plan-Pruefer hat eine Test-9-Variante verlangt (gespeicherte Groesse unter dem neuen Minimum wird angehoben). Umgesetzt als **Test 9b** in `dashboard-grid.test.tsx`. Dadurch verschieben sich alle Zaehlungen um **+1**:
|
||||
|
||||
| Gate | Plan | Gemessen | Befehl |
|
||||
|---|---|---|---|
|
||||
| Task 1 Dashboard-Suite | 10 / 64 | `Test Files 10 passed (10)` / `Tests 65 passed (65)` | `pnpm -C apps/web exec vitest run src/components/dashboard` |
|
||||
| `dashboard-grid.test.tsx` nach Task 1 | 6 | `Tests 7 passed (7)` | `pnpm -C apps/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx` |
|
||||
| Task 2 Teil-Suite | 13 / 76 | `Test Files 13 passed (13)` / `Tests 77 passed (77)` | `pnpm -C apps/web exec vitest run src/components/dashboard "src/app/(portal)/page.test.tsx" src/messages` |
|
||||
| `dashboard-grid.test.tsx` nach Task 2 | 9 | `Tests 10 passed (10)` | wie oben, einzelne Datei |
|
||||
| Web gesamt | 47 / 293 | `Test Files 47 passed (47)` / `Tests 294 passed (294)` | `pnpm -C apps/web exec vitest run` |
|
||||
|
||||
Unveraendert wie geplant: `widget-registry.test.tsx` 11, `stopwatch-widget.test.tsx` 8, `page.test.tsx` 3, `src/messages` 6, API `Test Files 67 passed (67)` / `Tests 1078 passed (1078)`.
|
||||
|
||||
**Was react-grid-layout mit einem zu kleinen Eintrag sonst taete (gemessen, 2.2.3):** `synchronizeLayoutWithChildren` (`chunk-WGL5FSZH.mjs:559-562`) klont den gespeicherten Eintrag woertlich — `h: 8` bleibt `h: 8`, auch wenn `minH` (aus der Ueberschreibung) 9 ist. Geklemmt wird nur beim Vergroessern/Verkleinern: `minMaxSize.constrainSize` (`chunk-KDANGDDL.mjs:26-31`, `clamp(h, item.minH ?? 1, ...)`) und die Pixel-`minConstraints` an `Resizable` (`chunk-WGL5FSZH.mjs:472-475`). `correctBounds` (`chunk-76RTO6EO.mjs:257-277`) prueft nur Spaltenueberlauf, nicht Minima. Folge ohne Anhebung: ein gespeicherter Rechner mit `h: 8` bliebe bis zum ersten Anfassen des Groessen-Griffs unten abgeschnitten (unterste Tastenreihe fehlt) und spraenge dann auf 9. Mit der Anhebung in `applyConstraintMinima` (`w: Math.max(entry.w, minW)`, `h: Math.max(entry.h, minH)`) ist er sofort vollstaendig; beim naechsten Speichern landen 9 Zeilen in der DB.
|
||||
|
||||
## Task 1 — Mindestgroessen, Ueberschreibung, Stoppuhr (`dc992c9`, 5 Dateien)
|
||||
|
||||
**RED** `pnpm -C apps/web exec vitest run src/components/dashboard/widget-registry.test.tsx src/components/dashboard/dashboard-grid.test.tsx`:
|
||||
|
||||
```
|
||||
× Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert
|
||||
× quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die Vorgaben und die inhaltsgetriebenen Minima als data-grid
|
||||
× quick-260916-dyv Test 9: gespeicherte minW/minH werden in JEDEM Breakpoint aus WIDGET_CONSTRAINTS ueberschrieben, x/y/w/h bleiben, unbekannte Typen und Schluessel unveraendert
|
||||
× quick-260916-dyv Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben (Rechner h 8 -> 9, w bleibt wenn >= minW)
|
||||
Tests 4 failed | 14 passed (18)
|
||||
```
|
||||
|
||||
(Plan erwartete `3 failed`; +1 durch Test 9b.)
|
||||
|
||||
**GREEN** — `WIDGET_CONSTRAINTS` auf die Tabelle, `applyConstraintMinima` + `useMemo` (`effectiveLayouts`, vor dem Leerzustand wegen Hook-Reihenfolge) in `dashboard-grid.tsx`, `layouts={effectiveLayouts}` und `data-grid`-Fallback aus `effectiveLayouts.lg`, Stoppuhr-Knoepfe `px-2 py-1 text-xs` und Zeile `gap-1 py-1`.
|
||||
|
||||
Gates: Dashboard-Suite `10 passed (10)` / `65 passed (65)`; Greps `minW: 3, minH: 9` 1, `minW: 2, minH: 2, defaultW: 4, defaultH: 4` 1, `defaultW: 12, defaultH: 4` 1; `effectiveLayouts` 3; Stoppuhr `px-2 py-1 text-xs` 4, `px-4 py-1.5 text-sm` 0; `TSC_web=0`.
|
||||
|
||||
**Falsifizierung (a)** — `clock.minW/minH` in `widget-registry.tsx` auf den bwo-Wert 4/4: `Tests 3 failed | 15 passed (18)`: „Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert“, „quick-260916-bwo Test 5: …“, „quick-260916-dyv Test 9: …“. Zurueckgesetzt, gruen. (Lehre: `git checkout -- <Datei>` an der noch unkommittierten Datei hat die gesamte Task-1-Aenderung zurueckgesetzt — erneut angewandt, Gates erneut gruen; bei (b)/(c) deshalb per `sed`/Python zurueckgestellt.)
|
||||
|
||||
## Task 2 — Leiste unten rechts, Griff, cancel, preventCollision (`dbbd54f`, 8 Dateien)
|
||||
|
||||
**RED** `pnpm -C apps/web exec vitest run src/components/dashboard/dashboard-grid.test.tsx "src/app/(portal)/page.test.tsx"`:
|
||||
|
||||
```
|
||||
× Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2
|
||||
× Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben dem Haekchen in derselben Leiste, Grid im Bearbeitungsmodus
|
||||
× quick-260916-dyv Test 6: dragConfig-Pin — handle Karte, cancel fuer Interaktives, threshold 3; resizeConfig folgt dem Bearbeitungsmodus
|
||||
× quick-260916-dyv Test 7: Compactor-Pin — echter noCompactor plus preventCollision: true, compact ist Identitaets-Kopie (freie Platzierung)
|
||||
× quick-260916-dyv Test 8: cancel/handle-Semantik im DOM — Karte ist Griff, Loesch-Knopf/Eingaben/widgetNoDrag passen auf cancel, Kopfleiste als Overlay nur im Bearbeitungsmodus
|
||||
Tests 5 failed | 8 passed (13)
|
||||
```
|
||||
|
||||
page-Test 3 (Umschalten) war wie vorhergesagt bereits gruen. `importOriginal` fuer `react-grid-layout` laeuft in jsdom — kein Rueckfall auf den Objekt-Mock noetig.
|
||||
|
||||
**Messbefund Test 7:** Der echte `noCompactor.compact` ist `cloneLayout` -> `cloneLayoutItem` (`chunk-76RTO6EO.mjs:204-223`): kopiert `i/x/y/w/h` unveraendert, normalisiert aber `moved`/`static` zu `false` und fuegt `minW/maxW/minH/maxH/...` als `undefined` hinzu. Das im Plan vorgesehene `toEqual` gegen die Eingabe scheitert deshalb an `moved: false, static: false` (erster GREEN-Lauf: `1 failed | 76 passed (77)`). Test 7 pinnt nun `toMatchObject({ i, x, y, w, h, moved: false, static: false })`, neue Referenzen fuer Array und Element, und dass die Eingabe unveraendert bleibt — inhaltlich dieselbe Aussage (Identitaet als Kopie, keine Verschiebung).
|
||||
|
||||
**GREEN** — `WIDGET_DRAG_HANDLE_SELECTOR`, `WIDGET_DRAG_CANCEL_SELECTOR`, `FREE_PLACEMENT_COMPACTOR: Compactor = { ...noCompactor, preventCollision: true }`, `dragConfig` mit `handle/cancel/threshold: 3`; `widget-wrapper.tsx` neu (Karte im Bearbeitungsmodus `widget-drag-handle cursor-grab active:cursor-grabbing border-primary/40`, Overlay-Kopfleiste `absolute inset-x-0 top-0 z-10 flex h-5 ... bg-muted/70` mit Sechs-Punkte-SVG, Tooltip `dragHint`, `data-testid="widget-drag-head"`, Loesch-Knopf `h-5 w-5` mit `data-no-drag=""`; Rumpf `@container-size h-full` als reines Literal); `edit-mode-toggle.tsx` `shadow-lg`, inaktiv `border border-border bg-card`; `page.tsx` feste Leiste `fixed bottom-6 right-6 z-20 flex items-center gap-2` (erst „Widget hinzufuegen“, dann Umschalter), Grid direkt im Container; `dragHint` in de/en direkt nach `deleteTooltip`.
|
||||
|
||||
Gates: Teil-Suite `13 passed (13)` / `77 passed (77)` (dashboard-grid 10, page 3, messages 6); Greps `preventCollision: true` 2 (Definition + Kommentar), `WIDGET_DRAG_CANCEL_SELECTOR` 2, `threshold: 3` 2 (Wert + Kommentar), `mt-8` 0, `top-2` 0, Leiste 1, `h-[6px]` 0, `data-no-drag` 3, `cursor-grab` 2, `"dragHint"` je 1, `W_EXIT=0`, `W_EMPTY=0`, `TSC_web=0`.
|
||||
|
||||
**Falsifizierungen (b) und (c)** (Rueckstellung per `sed`/Python, danach `Tests 13 passed (13)` und nur die Arbeitsdateien im Diff):
|
||||
|
||||
| Falsifizierung | Eingriff | Rot (Testname woertlich) |
|
||||
|---|---|---|
|
||||
| (b1) | Zeile `cancel: WIDGET_DRAG_CANCEL_SELECTOR,` aus `dragConfig` entfernt | `Tests 1 failed | 9 passed (10)`: „quick-260916-dyv Test 6: dragConfig-Pin — handle Karte, cancel fuer Interaktives, threshold 3; resizeConfig folgt dem Bearbeitungsmodus“ |
|
||||
| (b2) | `FREE_PLACEMENT_COMPACTOR = { ...noCompactor }` (ohne `preventCollision`) | `Tests 1 failed | 9 passed (10)`: „quick-260916-dyv Test 7: Compactor-Pin — echter noCompactor plus preventCollision: true, compact ist Identitaets-Kopie (freie Platzierung)“ |
|
||||
| (c) | `<div className="mt-8">` um `DashboardGrid` wieder eingefuegt | `Tests 1 failed | 2 passed (3)`: „Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2“ |
|
||||
|
||||
## Task 3 — Handbuch, Abschluss-Gates, Push, CI (`cf97b5b`, 1 Datei)
|
||||
|
||||
Handbuch-Greps: `Unten rechts auf dem Dashboard` 1, `Oben rechts auf dem Dashboard` 0, `gerade noch bedienbar` 1, `an ihren Ausgangspunkt` 1.
|
||||
|
||||
Abschluss-Gates (alle nach dem Handbuch-Edit, vor dem Commit):
|
||||
|
||||
| Gate | Ergebnis |
|
||||
|---|---|
|
||||
| Web `pnpm -C apps/web exec vitest run` | `Test Files 47 passed (47)` / `Tests 294 passed (294)` (Plan 293, +1 Test 9b) |
|
||||
| API `pnpm -C apps/api exec vitest run` | `Test Files 67 passed (67)` / `Tests 1078 passed (1078)` |
|
||||
| `tsc --noEmit` shared / api / web | `TSC_packages/shared=0`, `TSC_apps/api=0`, `TSC_apps/web=0` |
|
||||
| `pnpm install --frozen-lockfile` | `FROZEN=0` |
|
||||
| `git diff --stat df16f46 -- . ':!.planning'` | `GIT_EXIT=0`, `12 files changed, 538 insertions(+), 99 deletions(-)` |
|
||||
| Unantastbar-Stichprobe (`.env*`, Compose, Lockfile, package.json, prisma, api/src/dashboard, globals.css, umlaut-dictionary.ts, dashboard-store.ts, grid-layout-migration.ts) | `U_EXIT=0`, `U_EMPTY=0` (leer) |
|
||||
| `git status -sb` vor Push / nach `git fetch` | `## main...origin/main [voraus 5]` / `## main...origin/main` |
|
||||
|
||||
Push: `7a6f42e..cf97b5b main -> main` (fuenf Commits: `df16f46`, `ec1b0ce` Akten; `dc992c9`, `dbbd54f`, `cf97b5b` Code/Handbuch).
|
||||
|
||||
### CI-Lauf nach dem Push
|
||||
|
||||
| Feld | Versuch 1 (einziger) |
|
||||
|---|---|
|
||||
| Lauf-ID | 353 (event `push`, ref `main`, `head_sha` `cf97b5b64b48ac08cb410966e98cf532f600681d`) |
|
||||
| status / conclusion | `completed` / **`success`** |
|
||||
| started_at / completed_at | 10:43:05 / 10:47:45 (+02:00) — **4 min 40 s** (15 Polls a 20 s; beim ersten Poll 7 s nach dem Push bereits `in_progress`, keine Warteschlange) |
|
||||
| Jobs | Lint & Type Check `success` (08:43:05-08:43:53Z), Tests `success` (08:43:55-08:44:51Z), Build & Publish Images `success` (08:44:53-08:47:45Z) |
|
||||
| `api:beta` node `APP_VERSION APP_CHANNEL APP_COMMIT` | `v1.0.0-16-gcf97b5b beta cf97b5b` — `APP_COMMIT` = `git rev-parse --short HEAD` = `cf97b5b`; `APP_VERSION` im `git describe`-Format (wie in 260916-bwo gemessen) |
|
||||
| `docker image inspect Created` api:beta | `2026-09-16T10:46:42+02:00` (per `docker pull` geholt, kein Container gebaut oder gestartet) |
|
||||
|
||||
Beobachtung per Hintergrund-Skript (`watch-ci.sh` im Scratchpad, Token nur in einer Shell-Variablen, nie ausgegeben), Gitea-API `GET /repos/schalli/tessera-ctl/actions/runs?limit=5`, Eintrag mit `head_sha == PUSHED`. Keine Infrastruktur- oder Code-Fehler, kein erneuter Lauf noetig.
|
||||
|
||||
## Git-Stand vor dem SUMMARY (Wahrheit)
|
||||
|
||||
`git status --porcelain` (leer):
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
`git log --oneline ec1b0ce..HEAD`:
|
||||
|
||||
```
|
||||
cf97b5b docs(quick-260916-dyv): Anwenderhandbuch — Bearbeiten-Schalter unten rechts, ganze Kachel ziehbar, Mindestgroessen
|
||||
dbbd54f feat(quick-260916-dyv): Bearbeiten-Schalter unten rechts (Rand oben 28 px), ganze Kachel als Griff mit Kopfleiste, cancel-Selektor, kein Ueberlappen beim Ablegen
|
||||
dc992c9 fix(quick-260916-dyv): Mindestgroessen inhaltsgetrieben (kleinste bedienbare Kachel je Typ), gespeicherte Minima ueberschrieben, Stoppuhr-Bedienleiste kompakt
|
||||
```
|
||||
|
||||
`commits: 3` gemessen: `git rev-list --count ec1b0ce5823f5f5275d0e79e231e8378f04ad0ea..HEAD` -> `3`. `actuals.tokens` 11339 = Zeichen des Diffs `git diff df16f46 -- . ':!.planning'` (45356) / 4; zum Vergleich Zeichen der 12 geaenderten Dateien gesamt 170986 / 4 = 42746. Plan-Schaetzung 90000 (confidence low) — deutlich zu hoch fuer den Diff.
|
||||
|
||||
## Gemessene Befunde und Entscheidungen
|
||||
|
||||
- **Zweiter Grund fuer „nicht klein genug“ (der im Auftrag nicht genannte):** Gespeicherte `minW/minH` schlagen `data-grid`. `synchronizeLayoutWithChildren` liest `data-grid` nur fuer Kinder ohne Eintrag; alle bestehenden Widgets haben einen Eintrag mit den in 260916-bwo verdoppelten Minima. Ohne `effectiveLayouts` aendert die Konstantentabelle fuer den User NICHTS Sichtbares — Test 9 ist der entscheidende Test, nicht Test A.
|
||||
- **Kollision und `preventCollision`:** `noCompactor = { type: null, allowOverlap: false, compact: cloneLayout }`, `preventCollision` fehlt (`false`). Beim Ziehen auf ein belegtes Feld (`moveElement` -> `moveElementAwayFromCollision`, `chunk-76RTO6EO.mjs:279-328`, Zweig `collisionNorth && compactType === null`): das GEZOGENE springt auf die Zeile des getroffenen (`collidesWith.y = itemToMove.y`), das getroffene rutscht um seine EIGENE Hoehe nach unten (`itemToMove.y += itemToMove.h`), keine Kaskade, keine Nachpruefung; `compact` ist Identitaet — Ueberlappungen bleiben. Beim Vergroessern mit dem `se`-Griff ist `shouldMoveItem` false, `moveElement` wird nicht gerufen: die Ueberlappung entsteht stumm. Mit `preventCollision: true`: `if (hasCollisions && preventCollision) { l.x = oldX; l.y = oldY; l.moved = false; return layout; }` — Platzhalter bleibt am Ursprung; Vergroessern prueft `getAllCollisions` am neuen Mass und behaelt das alte. Entscheidung: `preventCollision` statt `verticalCompactor`, weil die freie Platzierung (Commit `c8f3361`, „prevent auto-compaction on drag“) gewollt ist — Kompaktierung wuerde Luecken automatisch schliessen. `preventCollision` lebt am Compactor-Objekt (`compactor.preventCollision ?? false`, `chunk-WGL5FSZH.mjs:666`), daher ein eigenes Objekt.
|
||||
- **Griff/cancel:** react-draggable 4.7.0 prueft `cancel` NACH `handle`, beides vom Ereignisziel aufwaerts bis zum RGL-Element (`Draggable.js:417`, `matchesSelectorAndParentsTo`). Die Karte ist Kind des RGL-Elements, also passt jedes Ziel in der Karte auf den Griff; `cancel` gewinnt fuer Eingabefelder/Knoepfe/Links/`[contenteditable]`/`[data-no-drag]`/`.widgetNoDrag`. RGL haengt `.react-resizable-handle` selbst voran (`chunk-WGL5FSZH.mjs:526`). Die 12 bislang toten `widgetNoDrag`-Stellen (Favoriten 5, Link 7) sind damit wirksam. Test 8 fuehrt `matches`/`closest` mit dem echten Selektor in jsdom aus (T-DYV-02: ungueltiger Selektor wuerfe dort).
|
||||
- **Overlay-Kopfleiste statt Kopfleiste im Fluss:** Der Rumpf bleibt `h-full`, die Hoehenkette Karte -> Rumpf bleibt definit, `cqh` loest weiter auf (Grund fuer `h-full` in 260916-bwo). Eine Kopfleiste im Fluss haette den Rumpf im Bearbeitungsmodus um 20 px gekuerzt und den Rechner bei Mindestgroesse beschnitten. Preis: die obersten 20 px des Widget-Inhalts liegen im Bearbeitungsmodus unter der halbtransparenten Leiste (`bg-muted/70`) — im Ansichtsmodus gibt es keine Leiste.
|
||||
- **Stoppuhr-Bedienleiste als einzige Innen-Aenderung:** Bei Mindestbreite 4 Spalten (lg 191 px, Rumpf 179 px) ueberlief die laufende Stoppuhr (Stop + Runde + Reset mit `px-4 text-sm` ca. 222 px) — Reset abgeschnitten; kompakt (`px-2 text-xs`) ca. 151 px passt, Zeile 32 statt 48 px. Ohne diese Aenderung waere das inhaltsgetriebene Minimum 6x4 — BREITER als heute, das Gegenteil des Auftrags. Der Rechner wird NICHT umgebaut: er bekommt das hoehere Minimum 3x9 (heute 4x8), weil bei 8 Zeilen (216 px) die unterste Tastenreihe (+/-, 0, Komma, =) abgeschnitten ist (Anzeige 40 + Speicherzeile 28 + 5 Reihen 28 + Abstaende = 240 px > 216 px).
|
||||
- **Stift unten rechts:** Im Ansichtsmodus kann der 36x36-px-Stift (bei `bottom-6 right-6`) im schlimmsten Fall die untere rechte Ecke eines Widgets verdecken, das bis an den rechten Rand und bis unter den Viewport-Rand reicht — hingenommen (der bisherige „Widget hinzufuegen“-Knopf lag bereits dort, nur im Bearbeitungsmodus).
|
||||
- **`fixed` und `z-20`:** `fixed` bezieht sich auf das Viewport (kein transformierter Vorfahr — der bisherige Knopf funktionierte an derselben Stelle); `z-20` liegt ueber RGL-Elementen (gezogenes Element `z-index: 3`).
|
||||
- **Mock per `importOriginal`:** laeuft in jsdom; `noCompactor` ist echt, `Responsive` bleibt Mock.
|
||||
|
||||
## Mindestgroessen-Tabelle (gerechnet; Browser-Bestaetigung durch den Orchestrator)
|
||||
|
||||
Spaltenbreite lg bei 1200 px = 41,67 px (Kachelbreite(w) = 49,67w - 8), beim Orchestrator-Viewport von 260916-bwo 60 px; Kachelhoehe(h) = 28h - 8 px.
|
||||
|
||||
| Typ | bwo-Minimum | neu | px bei Spalte 41,67 / 60 | Rechnung | Bewusste Grenze |
|
||||
|---|---|---|---|---|---|
|
||||
| clock | 4/4 | **2/2** | 91x48 / 128x48 | Zeit `min(20cqw, 50cqh)` ca. 17-20 px, 8 Zeichen tabular ca. 80 px in 83 px | Datum (falls an) 10 px darunter, eng |
|
||||
| search | 6/4 | **6/2** | 290x48 / 400x48 | Auswahl 120 + Eingabe >= 80 + Knopf 40 + Abstaende 28 = 268 <= 278 Rumpf; Eingabe `h-8` 32 px in 48 px | 3 Spalten (141 px) waeren unbrauchbar — Breite bleibt 6 |
|
||||
| calendar | 6/6 | **3/3** | 141x76 / 196x76 | eine Terminzeile 48 px + `p-1.5` = 60 <= 76; Leertext 3 Zeilen text-sm 60 px | knapp; mehr Termine brauchen mehr Hoehe |
|
||||
| note | 4/6 | **4/4** | 191x104 / 264x104 | Kopfzeile ca. 36 + Vorschau >= 2 Zeilen (40) = 76 <= 104; Editiermodus Werkzeugleiste ca. 29 + 2 Zeilen | — |
|
||||
| calculator | 4/8 | **3/9** | 141x244 / 196x244 | Breite 4 Tasten x >= 30 + 3x4 + `p-1` 8 = 140 <= 141; Hoehe 40 + 4 + 28 + 4 + 5x28 + 4x4 = 156 + `p-1` 8 = 240 <= 244 | Minimum HOEHER als 4x8, weil bei 8 Zeilen die unterste Tastenreihe abgeschnitten ist; gespeicherte 4x8-Rechner werden auf h 9 angehoben |
|
||||
| favorites | 4/6 | **3/3** | 141x76 / 196x76 | Umschaltzeile (nur Bearbeitungsmodus) ca. 110 <= 133; drei Listenzeilen (20 + 4) in 68 px; Kachelraster `grid-cols-3` ca. 40-px-Kacheln | — |
|
||||
| link | 4/4 | **3/2** | 141x48 / 196x48 | Listenzeile Symbol 20 + Titel in 40 px Rumpf; Umschaltzeile <= 133 | Kachelansicht (Symbol 32 + Text 16 + Abstand) braucht 3 Zeilen — der User zieht eine Zeile hoeher |
|
||||
| stopwatch | 4/4 | **4/3** | 191x76 / 264x76 | NUR mit kompakter Leiste: laufend Stop+Runde+Reset ca. 151 <= 179; Zeile 32 px; Anzeige `min(16cqw, 35cqh)` = 26,6 px im Rest von 32 px | Rundenliste (`max-h-32`) braucht mehr Hoehe — Nebenfunktion |
|
||||
|
||||
Vorgaben (`defaultW/defaultH`) unveraendert: clock 4/4, search 12/4, calendar 8/12, note 6/8, calculator 6/10, favorites 6/10, link 4/4, stopwatch 6/6 — neue Widgets erscheinen wie gewohnt; alle Minima <= Vorgabe (`it.each` prueft es).
|
||||
|
||||
## Durchsicht Dashboard (gerechnet; Browser-Bestaetigung durch den Orchestrator)
|
||||
|
||||
| Widget | Standardgroesse | Mindestgroesse | Befund |
|
||||
|---|---|---|---|
|
||||
| Uhr | 4x4, Zeit skaliert (bwo) | 2x2: Zeit ca. 17-20 px lesbar | gut |
|
||||
| Suche | 12x4 | 6x2: Auswahl, Eingabe >= 80 px, Knopf | gut |
|
||||
| Kalender | 8x12 | 3x3: Leertext oder eine Terminzeile | auffaellig: knapp, aber bedienbar |
|
||||
| Notizen | 6x8 | 4x4: Titel + 2 Zeilen | gut |
|
||||
| Rechner | 6x10, Tasten skalieren (bwo) | 3x9: alle 5 Tastenreihen inkl. „=“ | gut; Todo: Innenaufbau kompakter, damit < 9 Zeilen reichen |
|
||||
| Favoriten | 6x10 | 3x3: Umschaltzeile + Listenzeilen | gut |
|
||||
| Link | 4x4 | 3x2: eine Listenzeile | auffaellig: Kachelansicht braucht 3 Zeilen |
|
||||
| Stoppuhr | 6x6 | 4x3: Anzeige 26,6 px, Stop/Runde/Reset sichtbar | gut; Todo: Rundenliste bei Minimum |
|
||||
|
||||
**Bleibt als Todo (nicht Teil dieses Auftrags):**
|
||||
- Rechner-Innenaufbau kompakter (Speicherzeile/Anzeige), damit weniger als 9 Zeilen reichen.
|
||||
- Rundenliste / laufende Stoppuhr bei Minimum 4x3: die Liste (`max-h-32`) hat dort keinen Platz.
|
||||
- Hart englische Texte im Einstellungsformular (aus 260916-bwo: „Timezone“, „Saving...“, „Title“, englischer Kalender-Hinweis).
|
||||
- Link-Kachelansicht bei 2 Zeilen (braucht 3).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Addendum des Plan-Pruefers] Zu kleine gespeicherte w/h werden angehoben**
|
||||
- **Found during:** Task 1 (vom Pruefer vorgegeben)
|
||||
- **Issue:** Ein gespeicherter Rechner mit `h: 8` bliebe trotz `minH: 9` unten abgeschnitten, bis er einmal angefasst wird (RGL klemmt nur beim Resize).
|
||||
- **Fix:** `applyConstraintMinima` setzt `w: Math.max(w, minW)`, `h: Math.max(h, minH)` im selben Durchlauf; Test 9b.
|
||||
- **Files modified:** `dashboard-grid.tsx`, `dashboard-grid.test.tsx`
|
||||
- **Commit:** `dc992c9`
|
||||
|
||||
**2. [Rule 1 - Messbefund] Test 7 nicht per `toEqual` gegen die Eingabe**
|
||||
- **Found during:** Task 2 GREEN
|
||||
- **Issue:** Der echte `cloneLayoutItem` normalisiert `moved`/`static` zu `false`; `toEqual` scheiterte.
|
||||
- **Fix:** `toMatchObject` auf Kernfelder + `moved: false, static: false`, Referenzen ungleich, Eingabe unveraendert.
|
||||
- **Files modified:** `dashboard-grid.test.tsx`
|
||||
- **Commit:** `dbbd54f`
|
||||
|
||||
**3. [Prozess] Falsifizierung (a) hat per `git checkout` die unkommittierte Registry-Aenderung mit zurueckgesetzt**
|
||||
- **Fix:** Aenderung erneut angewandt, alle Task-1-Gates erneut gemessen (identisch), dann committet. (b)/(c) per `sed`/Python zurueckgestellt.
|
||||
|
||||
Sonst: Plan exakt wie geschrieben ausgefuehrt. Keine neuen Pakete, kein Schema, keine `.env*`/Compose/Lockfile-Aenderung, `umlaut-dictionary.ts` unangetastet.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
Keine neue Angriffsflaeche ausserhalb des `<threat_model>`: keine API-Aenderung, keine Benutzerdaten in Selektoren oder Styles, `effectiveLayouts` ersetzt manipulierte Minima (T-DYV-01, Test 9 mit unbekanntem Typ ohne Absturz).
|
||||
|
||||
## Known Stubs
|
||||
|
||||
Keine.
|
||||
|
||||
## Was bewusst offen bleibt
|
||||
|
||||
- Der Browser-Nachweis (8 Schritte, unten) — Sache des Orchestrators/Verifizierers; das lokale Web-Abbild stammt aus `1aefaa3` und muss VORHER neu gebaut werden (`docker compose up -d --build web`).
|
||||
- Die vier Todos aus der Durchsicht (Rechner-Innenaufbau, Rundenliste, englische Formulartexte, Link-Kachelansicht).
|
||||
- Die 20 px hohe Overlay-Kopfleiste verdeckt im Bearbeitungsmodus den obersten Streifen des Widget-Inhalts halbtransparent — bewusst (Hoehenkette), im Browser beurteilen.
|
||||
- Der Stift kann im Ansichtsmodus 36x36 px eines Eck-Widgets verdecken.
|
||||
- Mandantenfaehigkeit/alpha/live: nichts geaendert; gespeicherte Anordnungen dort tragen die alten Minima, bis der Benutzer einmal speichert — sichtbar ist das nicht, weil `effectiveLayouts` sie beim Rendern ohnehin ersetzt.
|
||||
|
||||
## Fuer den Verifizierer/Orchestrator (Browser-Nachweis)
|
||||
|
||||
Vorher: `docker compose up -d --build web` (die API braucht keinen Neubau). Playwright MCP gegen `http://localhost:3000`, Bounding-Boxen per `boundingBox()`, nie per `fetch` aus der Seite. Anmelden `admin` / `admin123`.
|
||||
|
||||
1. **Aufbau:** „Dashboard bearbeiten“ (Stift UNTEN RECHTS), nacheinander Uhr, Suchleiste, Rechner, Stoppuhr, Notizen, Kalender, Favoriten, Link hinzufuegen (jedes erscheint unter dem vorigen), Bearbeitungsmodus beenden (Haekchen unten rechts), Seite neu laden.
|
||||
2. **Rand oben:** `main.app-shell-main` oben vs. erstes `[data-widget-id]` oben -> 28 px (+/-1; vorher 60). Linker Rand 28 px, Luecke zwischen Nachbarn 8 px (unveraendert).
|
||||
3. **Feste Leiste:** Bounding-Box des Stifts (aria-label „Dashboard bearbeiten“): Abstand zum rechten und unteren Viewport-Rand je 24 px, 36x36 px, Karten-Hintergrund + Rahmen + Schatten; im Bearbeitungsmodus „Widget hinzufuegen“ links neben dem Haekchen in derselben Leiste; kein Element mehr oben rechts im Dashboard-Container.
|
||||
4. **Mindestgroessen:** je Widget den Groessen-Griff unten rechts weit nach oben links ziehen; Bounding-Box mit der Tabelle vergleichen (Spaltenbreite `(Containerbreite - 200)/24` px, Zeile 20 px + 8 px): Uhr 2x2, Suche 6x2, Kalender 3x3, Notizen 4x4, Rechner 3x9, Favoriten 3x3, Link 3x2, Stoppuhr 4x3. Screenshot je Widget. Bedienbarkeit: Uhrzeit lesbar; Suchfeld >= 80 px, Knopf sichtbar; Rechner ALLE fuenf Tastenreihen inkl. „=“; Stoppuhr starten -> Stop, Runde, Reset sichtbar und klickbar; Notiz Titel + 2 Zeilen; Kalender Leertext/Terminzeile; Favoriten Umschaltzeile + Listenzeilen; Link eine Listenzeile. Faellt ein Widget durch: Kachelgroesse und Grund notieren (dann Konstante + Test A anpassen — kleiner Korrekturlauf).
|
||||
5. **Persistierte Minima:** nach einem Speichern (Haekchen) `docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT layouts->''lg'' FROM "DashboardLayout";'` -> Eintraege tragen die neuen `minW/minH` (und der Rechner `h >= 9`). **Gegenprobe A (jsonb_set):** beim Uhr-Eintrag `minW: 8, minH: 8` setzen, Seite neu laden, Uhr auf 2x2 verkleinern -> geht.
|
||||
**Gegenprobe B (kompletter Probe-Datensatz in NEUEN Raster-Einheiten mit verdoppelten Minima und einem Rechner mit h 8)** — setzt voraus, dass fuer `admin` noch keine `DashboardLayout`-Zeile existiert (sonst vorher `DELETE FROM "DashboardLayout" WHERE "userId" = (SELECT id FROM "User" WHERE username = 'admin');`) und dass zwei `WidgetInstance`-Zeilen mit den Ids `probe-clock` und `probe-calc` angelegt werden; `__gridVersion: 2` ist Pflicht, sonst verdoppelt `migrateGridLayouts` beim Laden erneut:
|
||||
|
||||
```sql
|
||||
INSERT INTO "WidgetInstance" (id, "userId", "tenantId", "widgetType", config, "createdAt", "updatedAt")
|
||||
SELECT 'probe-clock', u.id, u."tenantId", 'clock', '{}'::jsonb, now(), now() FROM "User" u WHERE u.username = 'admin';
|
||||
INSERT INTO "WidgetInstance" (id, "userId", "tenantId", "widgetType", config, "createdAt", "updatedAt")
|
||||
SELECT 'probe-calc', u.id, u."tenantId", 'calculator', '{}'::jsonb, now(), now() FROM "User" u WHERE u.username = 'admin';
|
||||
INSERT INTO "DashboardLayout" (id, "userId", "tenantId", layouts, "createdAt", "updatedAt")
|
||||
SELECT gen_random_uuid()::text, u.id, u."tenantId",
|
||||
'{"__gridVersion": 2,
|
||||
"lg": [{"i":"probe-clock","x":0,"y":0,"w":4,"h":4,"minW":8,"minH":8},
|
||||
{"i":"probe-calc","x":4,"y":0,"w":6,"h":8,"minW":4,"minH":8}],
|
||||
"md": [{"i":"probe-clock","x":0,"y":0,"w":4,"h":4,"minW":8,"minH":8},
|
||||
{"i":"probe-calc","x":4,"y":0,"w":6,"h":8,"minW":4,"minH":8}],
|
||||
"sm": [], "xs": [], "xxs": []}'::jsonb,
|
||||
now(), now()
|
||||
FROM "User" u WHERE u.username = 'admin';
|
||||
```
|
||||
|
||||
Erwartung nach Neuladen: die Uhr (gespeichert `minW/minH 8`, also groesser als ihre eigene Kachel 4x4) laesst sich auf 2x2 verkleinern; der Rechner wird SOFORT mit 9 Zeilen gerendert (Bounding-Box-Hoehe 244 px statt 216 px), alle fuenf Tastenreihen sichtbar, ohne dass er angefasst wurde. Nach einem Speichern zeigt die SQL-Abfrage `minW 2/minH 2` bei der Uhr und `h 9, minW 3, minH 9` beim Rechner. Danach aufraeumen: `DELETE FROM "DashboardLayout" WHERE "userId" = (SELECT id FROM "User" WHERE username = 'admin'); DELETE FROM "WidgetInstance" WHERE id IN ('probe-clock','probe-calc');`
|
||||
6. **Ziehen:** im Bearbeitungsmodus die Uhr an ihrer MITTE (nicht an der Kopfleiste) 200 px nach rechts ziehen -> sie rastet versetzt ein; Kopfleiste 20 px hoch mit sechs Griff-Punkten und Tooltip „Ziehen Sie die Kachel, um sie zu verschieben“; `mousedown` im Suchfeld + 100 px Bewegung -> Text markiert, Suchleiste bewegt sich NICHT; Klick auf das Loesch-Symbol rechts in der Kopfleiste -> Widget verschwindet, kein Ziehen; Groessen-Griff funktioniert weiter.
|
||||
7. **Ablegen auf belegter Stelle:** die Uhr ueber die Suchleiste ziehen und loslassen -> die Uhr steht wieder am Ausgangsort, die Suchleiste ist nicht verschoben, nichts ueberlappt; die Suchleiste in Richtung Uhr vergroessern -> die Groesse stoppt vor der Uhr. Beobachtetes Verhalten woertlich notieren.
|
||||
8. **Durchsicht:** jedes Widget bei Standard- und Mindestgroesse (gut / auffaellig / Todo) in die VERIFICATION uebernehmen.
|
||||
|
||||
Nach der Probe: Probe-Datensaetze entfernen, Playwright-Artefakte entfernen.
|
||||
|
||||
## Fuer den Changelog
|
||||
|
||||
- Widgets lassen sich wieder deutlich kleiner ziehen — jedes Widget hat jetzt genau die Mindestgroesse, bei der es gerade noch bedienbar ist; das gilt auch fuer bereits platzierte Widgets.
|
||||
- Der Bearbeiten-Schalter des Dashboards sitzt jetzt unten rechts; dadurch beginnen die Widgets direkt unter der Kopfzeile.
|
||||
- Verschieben ist einfacher: im Bearbeitungsmodus laesst sich jedes Widget an einer beliebigen Stelle anfassen (ausser an Eingabefeldern und Knoepfen), ein grauer Griff am oberen Rand zeigt das an. Widgets ueberlappen sich beim Ablegen nicht mehr — ueber einer belegten Stelle springt das Widget an seinen Ausgangspunkt zurueck.
|
||||
- Die Stoppuhr hat kompaktere Knoepfe und passt so auch in kleine Kacheln.
|
||||
- Das Anwenderhandbuch beschreibt den neuen Schalter, das Ziehen und die Mindestgroessen.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Alle 12 Dateien vorhanden (10 Code/Handbuch-Pfade einzeln geprueft, de.json/en.json eingeschlossen), Commits `dc992c9`, `dbbd54f`, `cf97b5b` in `git log --all`; nach `git fetch`: `## main...origin/main` (nicht voraus, nicht zurueck); `git status --porcelain` zeigt nur das ungetrackte SUMMARY (Akten-Commit durch den Orchestrator).
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
---
|
||||
phase: quick-260916-dyv
|
||||
verified: 2026-09-16T10:53:00Z
|
||||
status: passed
|
||||
score: 6/6 code-Wahrheiten verifiziert (Browser-Nachweis steht aus, Sache des Orchestrators)
|
||||
covered_files:
|
||||
- .planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-PLAN.md
|
||||
- .planning/quick/260916-dyv-dashboard-nachbesserung-mindestgroessen-/260916-dyv-SUMMARY.md
|
||||
- apps/web/src/components/dashboard/widget-registry.tsx
|
||||
- apps/web/src/components/dashboard/widget-registry.test.tsx
|
||||
- apps/web/src/components/dashboard/dashboard-grid.tsx
|
||||
- apps/web/src/components/dashboard/dashboard-grid.test.tsx
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
- apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
|
||||
- apps/web/src/components/dashboard/edit-mode-toggle.tsx
|
||||
- apps/web/src/app/(portal)/page.tsx
|
||||
- apps/web/src/app/(portal)/page.test.tsx
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- docs/anleitung-anwender.md
|
||||
human_verification:
|
||||
- test: "Browser-Nachweis Schritt 1-8 (siehe SUMMARY.md, Abschnitt 'Fuer den Verifizierer/Orchestrator'): Aufbau aller acht Widgets, Rand oben 28 px, feste Leiste unten rechts, Mindestgroessen je Widget-Typ (inkl. Bedienbarkeit), persistierte Minima nach dem Speichern plus Gegenproben A/B (jsonb_set / kompletter Probe-Datensatz mit verdoppelten Minima und Rechner h=8), Ziehen an der Mitte, kein Drag aus dem Suchfeld, Ablegen auf belegter Stelle (kein Ueberlappen, Rueckfall an den Ausgangsort), Vergroessern stoppt am Nachbarn, abschliessende Durchsicht je Widget"
|
||||
expected: "Alle acht Punkte wie im SUMMARY/PLAN beschrieben: 28 px Rand, Leiste unten rechts mit Stift/Haekchen + Widget hinzufuegen, jedes Widget schrumpft auf die Tabellenwerte und bleibt bedienbar, gespeicherte Minima werden nach dem Speichern durch die neuen Werte ersetzt (Gegenprobe A: manipulierte minW/minH in der DB werden von den Konstanten geschlagen; Gegenprobe B: ein zu kleiner gespeicherter Rechner (h=8) wird SOFORT mit 9 Zeilen gerendert, keine Ueberlappung beim Ablegen, Vergroessern stoppt am Nachbarn)"
|
||||
why_human: "Layout-Geometrie (Pixelmasse, Bounding-Boxen), tatsaechliches Drag-Verhalten von react-draggable/react-grid-layout und visuelle Bedienbarkeit lassen sich nicht aus jsdom/Unit-Tests ableiten — laut PLAN.md ausdruecklich als 'Browser-Nachweis durch den Orchestrator (Playwright MCP)' vorgesehen, human_verify_mode: end-of-phase"
|
||||
---
|
||||
|
||||
# Quick-Task 260916-dyv Verifikation
|
||||
|
||||
**Auftrag:** Dashboard-Nachbesserung — inhaltsgetriebene Mindestgroessen (mit Ueberschreibung gespeicherter Werte und Anhebung zu kleiner Werte), Bearbeiten-Schalter unten rechts (Rand 28 px), ganze Kachel als Griff mit cancel-Selektor und `preventCollision`, Anwenderhandbuch, gepusht, CI gruen.
|
||||
|
||||
**Verifiziert:** 2026-09-16, 10:41-10:53 UTC
|
||||
**Status:** human_needed (alle Code-Wahrheiten VERIFIZIERT; der Browser-Nachweis ist explizit dem Orchestrator zugewiesen, siehe unten — dafuer allein wird NICHT `human_needed` vergeben, aber Steps 3/4/6/7 im SUMMARY sind noch offen und muessen protokolliert werden)
|
||||
|
||||
## 1. Git-Stand
|
||||
|
||||
| Pruefung | Befehl | Ergebnis |
|
||||
|---|---|---|
|
||||
| Commit-Reihenfolge | `git log --oneline ec1b0ce..HEAD` | `cf97b5b`, `dbbd54f`, `dc992c9` — exakt wie erwartet |
|
||||
| Dateiumfang | `git diff --stat df16f46 -- . ':!.planning'` | `12 files changed, 538 insertions(+), 99 deletions(-)` — genau 12 |
|
||||
| Unantastbare Pfade | `git diff --stat df16f46 -- '.env*' docker-compose*.yml pnpm-lock.yaml apps/web/package.json apps/api/package.json apps/api/prisma apps/api/src/dashboard apps/web/src/app/globals.css apps/web/src/messages/umlaut-dictionary.ts apps/web/src/lib/stores/dashboard-store.ts apps/web/src/lib/grid-layout-migration.ts` | leer, Exit 0 — nichts angefasst |
|
||||
|
||||
## 2. Testsuiten und Typprüfung
|
||||
|
||||
| Suite | Erwartung | Gemessen |
|
||||
|---|---|---|
|
||||
| Web `pnpm -C apps/web exec vitest run` | 47 Dateien / 294 Tests | `Test Files 47 passed (47)` / `Tests 294 passed (294)` |
|
||||
| API `pnpm -C apps/api exec vitest run` | 67 Dateien / 1078 Tests | `Test Files 67 passed (67)` / `Tests 1078 passed (1078)` |
|
||||
| `tsc --noEmit` (shared/api/web) | dreimal 0 | `TSC_shared=0`, `TSC_api=0`, `TSC_web=0` |
|
||||
| `pnpm install --frozen-lockfile` | 0 | `FROZEN=0` |
|
||||
|
||||
Alle Zahlen decken sich mit der +1-Korrektur im SUMMARY (Test 9b, Plan-Pruefer-Addendum) — 293 (Plan) + 1 = 294 stimmt.
|
||||
|
||||
## 3. Code-Lektüre (Substanz und Verdrahtung)
|
||||
|
||||
### `widget-registry.tsx`
|
||||
`WIDGET_CONSTRAINTS` traegt die Tabelle woertlich: clock 2/2/4/4, search 6/2/12/4, calendar 3/3/8/12, note 4/4/6/8, calculator 3/9/6/10, favorites 3/3/6/10, link 3/2/4/4, stopwatch 4/3/6/6 — deckt sich mit PLAN und SUMMARY. Kommentar deutsch/ASCII, erklaert die Herleitung. `defaultW/defaultH` unveraendert gegenueber `git diff df16f46` (nur die vier `min*`-Spalten geaendert).
|
||||
|
||||
### `widget-registry.test.tsx`
|
||||
Test A pinnt `WIDGET_CONSTRAINTS` per `toEqual` exakt gegen das Objekt oben (Zeile 56-79). Der `it.each`-Test (min <= default) bleibt bestehen.
|
||||
|
||||
### `dashboard-grid.tsx`
|
||||
`applyConstraintMinima` (Zeile 86-111): fuer jeden Breakpoint-Key, fuer jeden Eintrag mit bekanntem Typ wird `minW/minH` aus der Konstanten gesetzt UND `w`/`h` per `Math.max` auf das Minimum angehoben, sonst der Eintrag unveraendert kopiert (keine Mutation der Eingabe — per Kopie `{ ...entry }`/neues Objekt). `effectiveLayouts = useMemo(() => applyConstraintMinima(layouts, widgets), [layouts, widgets])` — greift fuer JEDEN Breakpoint, nicht nur `lg`. Wird an `Responsive` als `layouts={effectiveLayouts}` durchgereicht UND im `data-grid`-Fallback verwendet (`effectiveLayouts.lg?.find(...)`). `dragConfig` traegt `handle: WIDGET_DRAG_HANDLE_SELECTOR` (`.widget-drag-handle`), `cancel: WIDGET_DRAG_CANCEL_SELECTOR` (`'input, textarea, select, button, a, [contenteditable], [data-no-drag], .widgetNoDrag'`), `threshold: 3`. `compactor={FREE_PLACEMENT_COMPACTOR}` = `{ ...noCompactor, preventCollision: true }`. Alles wortgleich mit PLAN/SUMMARY.
|
||||
|
||||
### `dashboard-grid.test.tsx`
|
||||
Tests 6-9b sind inhaltlich meaningful (nicht nur Zaehlung):
|
||||
- Test 6: `toEqual` exakter Vergleich von `dragConfig`/`resizeConfig` in beiden Moden.
|
||||
- Test 7: echter `noCompactor` via `importOriginal`, `compact()` als Funktionsaufruf mit echtem Rueckgabewert gepruefte, Referenzungleichheit UND Eingabe-Unveraenderlichkeit — nachvollziehbar begruendete Abweichung von `toEqual` zu `toMatchObject` (siehe unten).
|
||||
- Test 8: echte DOM-Matches (`matches`/`closest`) gegen den exportierten Selektor, inklusive Eingabefeld und `.widgetNoDrag`-Element, die zur Laufzeit angehaengt werden — keine Attrappen.
|
||||
- Test 9: zwei Breakpoints (`lg`, `md`), ein unbekannter Typ bleibt woertlich erhalten (kein Absturz), `Object.keys` der Layouts unveraendert.
|
||||
- Test 9b: Rechner `h:8 -> 9` (unter dem neuen Minimum, angehoben), Suche `w:4 -> 6` (unter `minW`), Eingabeobjekt bleibt bei Pruefung nach dem Rendern unveraendert (`layouts.lg[0].h` immer noch `8`).
|
||||
|
||||
### `widgets/stopwatch-widget.tsx`
|
||||
Diff zeigt die vier Knopf-Klassen `px-4 py-1.5 text-sm` -> `px-2 py-1 text-xs`, Zeile `gap-2 py-2` -> `gap-1 py-1`. Handler/aria-labels unveraendert (nicht Teil des Diffs).
|
||||
|
||||
### `widgets/widget-wrapper.tsx`
|
||||
Karte traegt im Bearbeitungsmodus `widget-drag-handle cursor-grab active:cursor-grabbing border-primary/40` als Klassenliteral; im Ansichtsmodus `border-primary/20` (unveraendert). Kopfleiste `absolute inset-x-0 top-0 z-10 flex h-5 ...` — echtes Overlay, NICHT im Fluss. Rumpf bleibt `@container-size h-full` als reines Literal (keine wirkungslose `pt-0`-Bedingung mehr). Loesch-Knopf traegt `data-no-drag=""` und ist innerhalb der Kopfleiste.
|
||||
|
||||
### `apps/web/src/app/(portal)/page.tsx` + `page.test.tsx`
|
||||
Kein `mt-8`, kein absolut positionierter Block oben rechts mehr. Grid direkt im Container `relative p-2`. Feste Leiste `fixed bottom-6 right-6 z-20 flex items-center gap-2`, DOM-Reihenfolge: „Widget hinzufuegen“ (nur im Bearbeitungsmodus) VOR `EditModeToggle` — Test 2 prueft das per `compareDocumentPosition`. `page.test.tsx` hat 3 meaningful Tests (nicht nur Existenzpruefung): Leisten-Klassen, fehlende Elemente (`mt-8`/`top-2`/Widget-hinzufuegen im Ansichtsmodus), Elternschaft der Knoepfe, Klick-Handler-Aufruf.
|
||||
|
||||
### i18n
|
||||
`dragHint` in `de.json` (182: „Ziehen Sie die Kachel, um sie zu verschieben“) und `en.json` (182: „Drag the tile to move it“) vorhanden, direkt nach `deleteTooltip` (Zeilennummer identisch in beiden Dateien — Paritaet). `src/messages`-Suite (`umlaut-guard.spec.ts` eingeschlossen) gruen: `Test Files 2 passed (2)` / `Tests 6 passed (6)`.
|
||||
|
||||
### `docs/anleitung-anwender.md`
|
||||
Enthaelt „Unten rechts auf dem Dashboard“ (1x), „Oben rechts auf dem Dashboard“ (0x), „gerade noch bedienbar“ (1x), „an ihren Ausgangspunkt“ (1x) — exakt wie im Plan gefordert. Echte Umlaute im Text bestaetigt beim Lesen.
|
||||
|
||||
## 4. Falsifizierung (eigenstaendig durchgefuehrt)
|
||||
|
||||
Arbeitsbaum vor Eingriff sauber (`git status --porcelain -- apps/` leer). `applyConstraintMinima` in `dashboard-grid.tsx` per Python-Skript so geaendert, dass `w`/`h` NICHT mehr per `Math.max` angehoben werden (nur `entry.w`/`entry.h` durchgereicht). Ergebnis:
|
||||
|
||||
```
|
||||
Test Files 1 failed (1)
|
||||
Tests 1 failed | 9 passed (10)
|
||||
- Expected { h: 9, ... }
|
||||
+ Received { h: 8, ... }
|
||||
❯ ... Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben
|
||||
```
|
||||
|
||||
Test 9b wird rot, alle anderen bleiben gruen — bestaetigt, dass die Anhebung tatsaechlich von diesem Codepfad getragen wird und der Test sie wirklich prueft. Danach `git checkout -- apps/web/src/components/dashboard/dashboard-grid.tsx`; `git status --porcelain -- apps/` wieder leer; `dashboard-grid.test.tsx` erneut `10 passed (10)`. Arbeitsbaum unveraendert gegenueber dem Ausgangszustand.
|
||||
|
||||
## 5. CI und Registry
|
||||
|
||||
| Pruefung | Ergebnis |
|
||||
|---|---|
|
||||
| Gitea Actions Run 353 (`head_sha cf97b5b64b48ac08cb410966e98cf532f600681d`) | `status: completed`, `conclusion: success` |
|
||||
| `docker run --rm --entrypoint node localhost:3002/schalli/tessera-ctl/api:beta -e '...APP_VERSION, APP_COMMIT'` | `v1.0.0-16-gcf97b5b cf97b5b` — `APP_COMMIT` entspricht dem gepushten Commit, das `beta`-Abbild ist NICHT veraltet |
|
||||
| `git fetch -q && git status -sb` | `## main...origin/main` — kein `[ahead`, kein `[behind` |
|
||||
|
||||
Token wurde in eine Shell-Variable extrahiert und nirgends ausgegeben.
|
||||
|
||||
## 6. WINDOWS.md-Eintrag (Bewertung, nicht editiert)
|
||||
|
||||
Neuer Eintrag #39 (`quick-260916-dyv`, Status `open`): „Test 7 pinnt Identitaets-Kopie per `toMatchObject` statt `toEqual` (`cloneLayoutItem` normalisiert `moved`/`static`)“.
|
||||
|
||||
**Beurteilung:** Kein verschleierter Mangel, sondern eine korrekt dokumentierte, unausweichliche Anpassung. Gemessen (siehe Abschnitt 3, Test 7): der ECHTE `noCompactor.compact` (nicht der Mock) normalisiert beim Kopieren `moved`/`static` auf `false` und fuegt `undefined`-Felder fuer `minW/maxW/...` hinzu. Ein `toEqual` gegen die reine Eingabe (wie im PLAN woertlich vorgesehen) haette daher fast IMMER fehlgeschlagen — das ist ein Bibliotheksverhalten, keine Fehlfunktion des eigenen Codes. Der jetzige Test prueft inhaltlich dieselbe Aussage (Kernfelder identisch, neue Referenzen, `moved: false`/`static: false`, Eingabe unveraendert) — Identitaets-Kopie ohne Verschiebung, exakt die Intention aus dem Truth-Text „preventCollision lebt am Compactor-Objekt … Test importiert den echten noCompactor“. Der offene WINDOWS-Eintrag ist daher eine ehrliche, aber niedrigpriore Buchfuehrungsnotiz (Plan sagte `toEqual`, Code liefert `toMatchObject`) — kein Blocker fuer dieses Vorhaben. Empfehlung: als „waived“/„fixed“ mit Verweis auf diese Verifikation schliessen, sobald der Ledger-Verantwortliche zustimmt; nicht Teil dieses Verifikationsumfangs, daher nicht editiert.
|
||||
|
||||
## 7. Was noch fehlt: Vom Orchestrator im Browser zu pruefen
|
||||
|
||||
Laut PLAN.md (`<verification>`, Human-Check end-of-phase) und SUMMARY.md (Abschnitt „Fuer den Verifizierer/Orchestrator“) ist der komplette Browser-Nachweis explizit NICHT Teil des automatisierten Verifizierungsumfangs. Dieser Verifizierer hat KEINE Container gestartet/gestoppt, KEINEN Browser bedient und NICHT in die lokale Datenbank geschrieben (Auftrag). Vor dem Test: `docker compose up -d --build web` (das lokale Web-Abbild stammt noch aus `1aefaa3`).
|
||||
|
||||
1. **Aufbau:** Anmelden (`admin`/`admin123`), „Dashboard bearbeiten“ (Stift unten rechts), alle acht Widget-Typen nacheinander hinzufuegen, Bearbeitungsmodus beenden, Seite neu laden.
|
||||
2. **Rand oben:** `main.app-shell-main` oben vs. erstes `[data-widget-id]` oben -> 28 px (+/-1; vorher 60). Linker Rand 28 px, Luecke 8 px.
|
||||
3. **Feste Leiste:** Bounding-Box des Stifts: 24 px zu rechtem/unterem Viewport-Rand, 36x36 px, Karten-Hintergrund+Rahmen+Schatten; im Bearbeitungsmodus „Widget hinzufuegen“ links neben dem Haekchen in derselben Leiste; kein Element mehr oben rechts im Dashboard-Container.
|
||||
4. **Mindestgroessen je Widget:** Groessen-Griff bis zum Anschlag ziehen, Bounding-Box gegen die Tabelle vergleichen (Uhr 2x2, Suche 6x2, Kalender 3x3, Notizen 4x4, Rechner 3x9, Favoriten 3x3, Link 3x2, Stoppuhr 4x3), Bedienbarkeit je Typ pruefen (Rechner: alle fuenf Tastenreihen inkl. „=“; Stoppuhr: Start -> Stop/Runde/Reset alle sichtbar und klickbar). Screenshot je Widget bei Minimum.
|
||||
5. **Persistierte Minima + Gegenproben (SQL, in der lokalen Test-DB):**
|
||||
- Nach Speichern: `docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c 'SELECT layouts->''lg'' FROM "DashboardLayout";'` -> neue `minW/minH`.
|
||||
- Gegenprobe A: Uhr-Eintrag per `jsonb_set` auf `minW: 8, minH: 8` setzen, neu laden, auf 2x2 verkleinerbar -> Konstanten schlagen die DB.
|
||||
- Gegenprobe B (SQL aus dem SUMMARY, Abschnitt „Fuer den Verifizierer/Orchestrator“, Punkt 5): kompletter Probe-Datensatz mit `probe-clock`/`probe-calc`, `__gridVersion: 2`, verdoppelten Minima und Rechner `h: 8` einspielen; Erwartung: Uhr sofort auf 2x2 verkleinerbar, Rechner SOFORT mit 9 Zeilen gerendert (244 px Hoehe) ohne Anfassen; nach Speichern zeigt SQL `minW 2/minH 2` (Uhr) und `h 9, minW 3, minH 9` (Rechner). Danach Probe-Datensaetze wieder loeschen (`DELETE FROM ...` wie im SUMMARY vorgegeben).
|
||||
6. **Ziehen:** Uhr an der Mitte (nicht Kopfleiste) 200 px ziehen -> Versatz; Kopfleiste 20 px mit sechs Punkten und Tooltip „Ziehen Sie die Kachel, um sie zu verschieben“; `mousedown` im Suchfeld + Bewegung -> Text markiert, KEIN Drag; Klick auf Loesch-Symbol -> Widget verschwindet, kein Ziehen ausgeloest; Groessen-Griff funktioniert weiter.
|
||||
7. **Ablegen auf belegter Stelle:** Uhr ueber Suchleiste ziehen und loslassen -> Uhr springt an Ausgangsort zurueck, Suchleiste unveraendert, keine Ueberlappung; Suchleiste Richtung Uhr vergroessern -> stoppt vor der Uhr. Beobachtung woertlich notieren.
|
||||
8. **Durchsicht:** jedes Widget bei Standard- und Mindestgroesse (gut/auffaellig/Todo) in die abschliessende VERIFICATION uebernehmen; danach Probe-Datensaetze/Playwright-Artefakte entfernen.
|
||||
|
||||
Faellt ein Widget bei Schritt 4 durch, ist laut PLAN ein kleiner Korrekturlauf (Konstante + Test A anpassen) vorgesehen, keine Neuplanung.
|
||||
|
||||
## Angenommene Risiken
|
||||
|
||||
- Der 20 px hohe Overlay-Kopfstreifen verdeckt im Bearbeitungsmodus die obersten 20 px des Widget-Inhalts halbtransparent — bewusste Entscheidung (Hoehenkette fuer Container-Queries bleibt definit), im Browser-Nachweis (Schritt 2/4) mitzupruefen, ob das bei den kleinsten Kacheln (z. B. Uhr 2x2 = 48 px hoch) stoerend wirkt.
|
||||
- Der Stift kann im Ansichtsmodus im schlimmsten Fall 36x36 px der Ecke eines am Rand liegenden Widgets verdecken — bewusst hingenommen (der bisherige „Widget hinzufuegen“-Knopf lag bereits an derselben Stelle, nur im Bearbeitungsmodus sichtbar).
|
||||
- Der Rechner braucht jetzt 3x9 (hoeher als die alten 4x8) statt eines Innenumbaus — bewusste, im SUMMARY begruendete Entscheidung; als Todo vermerkt (Innenaufbau kompakter machen), nicht Teil dieses Auftrags.
|
||||
- Die Link-Kachelansicht braucht laut Rechnung 3 Zeilen bei einer Mindesthoehe von 2 — im SUMMARY als bewusste Grenze benannt („der User zieht eine Zeile hoeher“); im Browser-Nachweis (Schritt 4/8) zu bestaetigen.
|
||||
- Der WINDOWS.md-Eintrag #39 (Test 7, `toMatchObject` statt `toEqual`) bleibt offen zur Buchfuehrung, ist aber inhaltlich durch einen Messbefund am echten `noCompactor` begruendet (Abschnitt 6) — kein Hinweis auf einen tatsaechlichen Fehler im produktiven Code.
|
||||
- Alle acht Schritte des Browser-Nachweises (Abschnitt 7) sind ungeprueft — ausdruecklich dem Orchestrator zugewiesen (PLAN.md, `human_verify_mode: end-of-phase`), nicht Teil des automatisierten Umfangs dieses Verifizierers.
|
||||
|
||||
## Nachtrag des Orchestrators — Browser-Check durchgefuehrt (2026-09-16, 08:55-09:00Z)
|
||||
|
||||
Umgebung: lokale Container (web aus `cf97b5b`, danach aus `8792819`), Playwright MCP, Anmeldung als lokaler Admin; SQL-Probe aus dem SUMMARY (Anordnung mit `__gridVersion: 2`, verdoppelten Minima `minW/minH 8` an der Uhr, Rechner `h: 8`).
|
||||
|
||||
| Schritt | Beobachtung |
|
||||
|---|---|
|
||||
| Rand oben | Kopfzeilen-Unterrand 60, erstes Widget top 88 -> **28 px** (vorher 60) |
|
||||
| Bearbeiten-Knopf | unten rechts, 24 px vom Rand; im Bearbeitungsmodus Leiste mit "Widget hinzufuegen" + Haekchen |
|
||||
| Gespeicherte Minima | Uhr mit gespeichertem minW/minH 8 liess sich auf **126x48 px** (2x2) verkleinern, Uhrzeit 23 px; Rechner mit h 8 wurde beim Laden auf das Minimum angehoben |
|
||||
| Ganze Kachel als Griff | Ziehen an der Kachelmitte verschiebt die Uhr um 280 px |
|
||||
| cancel-Selektor | mousedown im Suchfeld + Bewegung: Such-Widget bleibt exakt an Ort und Stelle |
|
||||
| Kollision | Rechner auf das belegte Such-Widget gezogen: stoppt unmittelbar davor (left 537 -> 671, rechte Kante 1066 < 1074), keine Ueberlappung, Such-Widget unveraendert |
|
||||
| **Befund Rechner** | bei minH 9 (244 px) Inhalt 268 px, unterste Tastenreihe (0 / , / =) um 25 px abgeschnitten — der Planer hatte fuenf statt sechs Tastenreihen gezaehlt. **Behoben in `8792819`** (minH 10, Test 9b und Kommentare nachgezogen, Web 47/294 gruen, tsc 0): Kachel 272 px, Ueberlauf 0, "=" innerhalb. CI-Lauf fuer 8792819 `success` |
|
||||
|
||||
Nach der Probe: Probe-Zeilen entfernt (0/0), Playwright-Artefakte entfernt. Ledger #39 (Test-7-Anpassung, in-scope) als fixed.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Ä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
|
||||
|
||||
## 1.1.0 – 2026-09-16
|
||||
|
||||
### 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.
|
||||
|
||||
### Behoben
|
||||
|
||||
- Kalender-Einstellungen: Die Felder des Formulars für Kalenderquellen (Name, Typ, Adresse, Benutzername, Passwort, Domäne, Farbe) und die Knöpfe zeigten technische Schlüsselnamen statt Beschriftungen; ebenso die Rückmeldungen beim Speichern und ein Hinweis im Marktplatz. Alle Texte sind jetzt auf Deutsch und Englisch hinterlegt.
|
||||
|
||||
## 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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -239,7 +239,7 @@ describe('DashboardGrid', () => {
|
||||
expect(passed.sm).toEqual([]);
|
||||
});
|
||||
|
||||
it('quick-260916-dyv Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben (Rechner h 8 -> 9, w bleibt wenn >= minW)', async () => {
|
||||
it('quick-260916-dyv Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben (Rechner h 8 -> 10, w bleibt wenn >= minW)', async () => {
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
const layouts = {
|
||||
@@ -268,10 +268,10 @@ describe('DashboardGrid', () => {
|
||||
);
|
||||
|
||||
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
|
||||
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 9, minW: 3, minH: 9 });
|
||||
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 3, minH: 10 });
|
||||
expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 6, h: 4, minW: 6, minH: 2 });
|
||||
// Jeder Breakpoint: md-Rechner 3x7 -> 3x9 (w schon = minW, h angehoben).
|
||||
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 3, h: 9, minW: 3, minH: 9 });
|
||||
// Jeder Breakpoint: md-Rechner 3x7 -> 3x10 (w schon = minW, h angehoben).
|
||||
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 3, h: 10, minW: 3, minH: 10 });
|
||||
// Eingabe unveraendert (keine Mutation des Store-Objekts).
|
||||
expect(layouts.lg[0].h).toBe(8);
|
||||
expect(layouts.lg[0].minH).toBe(8);
|
||||
|
||||
@@ -76,7 +76,7 @@ interface DashboardGridProps {
|
||||
* die einzige Quelle (T-DYV-01: manipulierte Minima wirken nicht).
|
||||
*
|
||||
* Liegt die gespeicherte Groesse UNTER dem neuen Minimum (z. B. Rechner mit
|
||||
* h 8, neues minH 9), wird w/h auf das Minimum angehoben: RGL selbst klemmt
|
||||
* h 8, neues minH 10), wird w/h auf das Minimum angehoben: RGL selbst klemmt
|
||||
* w/h nur beim Vergroessern/Verkleinern (`minMaxSize.constrainSize`,
|
||||
* chunk-KDANGDDL.mjs:26-31; `minConstraints` an `Resizable`,
|
||||
* chunk-WGL5FSZH.mjs:472-475), rendert die zu kleine Kachel aber woertlich —
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => {
|
||||
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
|
||||
calendar: { minW: 3, minH: 3, defaultW: 8, defaultH: 12 },
|
||||
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
|
||||
calculator: { minW: 3, minH: 9, defaultW: 6, defaultH: 10 },
|
||||
calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 },
|
||||
favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 },
|
||||
link: { minW: 3, minH: 2, defaultW: 4, defaultH: 4 },
|
||||
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
|
||||
|
||||
@@ -35,7 +35,7 @@ export const WIDGET_CONSTRAINTS: Record<
|
||||
// quick-260916-dyv: minW/minH = kleinste noch bedienbare Kachel je Typ im
|
||||
// 24-Spalten/20-px-Raster, aus dem Innenaufbau gerechnet (Suche: Auswahl 120 +
|
||||
// Eingabe + Knopf; Rechner: Anzeige 40 + Speicherzeile 28 + 5 Tastenreihen 28 =
|
||||
// 240 px -> 9 Zeilen; Stoppuhr: kompakte Bedienleiste). defaultW/defaultH =
|
||||
// 268 px -> 10 Zeilen; Stoppuhr: kompakte Bedienleiste). defaultW/defaultH =
|
||||
// altes 12-Spalten-Mass x2, unveraendert. Gespeicherte minW/minH werden in
|
||||
// dashboard-grid.tsx aus dieser Tabelle ueberschrieben.
|
||||
// Existing widgets
|
||||
@@ -44,7 +44,10 @@ export const WIDGET_CONSTRAINTS: Record<
|
||||
calendar: { minW: 3, minH: 3, defaultW: 8, defaultH: 12 },
|
||||
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
|
||||
// Phase 8 new widgets (D-01)
|
||||
calculator: { minW: 3, minH: 9, defaultW: 6, defaultH: 10 },
|
||||
// minH 10, nicht 9 (260916-dyv, Browser-Messung des Orchestrators): der Rechner hat
|
||||
// SECHS Tastenreihen (%/CE/C/Loeschen, 1/x/x2/Wurzel/Division, 7-9, 4-6, 1-3, 0/Komma/=),
|
||||
// Inhalt 268 px; bei 9 Zeilen (244 px) fehlte die unterste Reihe um 25 px.
|
||||
calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 },
|
||||
favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 },
|
||||
link: { minW: 3, minH: 2, defaultW: 4, defaultH: 4 },
|
||||
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -109,7 +109,8 @@
|
||||
"beta": "Beta",
|
||||
"live": "Live",
|
||||
"dev": "Entwicklung"
|
||||
}
|
||||
},
|
||||
"whatsNew": "Was ist neu"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -210,7 +211,27 @@
|
||||
"connectionSuccess": "Verbindung erfolgreich",
|
||||
"connectionError": "Verbindung fehlgeschlagen. Bitte überprüfen Sie die URL und Zugangsdaten.",
|
||||
"addSource": "Quelle hinzufügen",
|
||||
"sourceEmpty": "Keine Kalenderquellen eingerichtet. Fügen Sie eine Quelle hinzu, um Termine anzuzeigen."
|
||||
"sourceEmpty": "Keine Kalenderquellen eingerichtet. Fügen Sie eine Quelle hinzu, um Termine anzuzeigen.",
|
||||
"formFieldName": "Name",
|
||||
"formFieldType": "Typ",
|
||||
"formFieldExchangeMode": "Exchange-Anbindung",
|
||||
"formFieldDomain": "Domäne",
|
||||
"formFieldDomainHint": "Optional – nur nötig, wenn Ihr Exchange-Server die Anmeldung als DOMÄNE\\Benutzer erwartet.",
|
||||
"formFieldUrl": "Adresse (URL)",
|
||||
"formFieldUsername": "Benutzername",
|
||||
"formFieldPassword": "Passwort",
|
||||
"formFieldColor": "Farbe",
|
||||
"formSave": "Speichern",
|
||||
"formSaving": "Wird gespeichert …",
|
||||
"formTestConnection": "Verbindung testen",
|
||||
"formTesting": "Verbindung wird geprüft …",
|
||||
"formTestSuccess": "Verbindung erfolgreich.",
|
||||
"formTestFailed": "Verbindung fehlgeschlagen. Bitte prüfen Sie Adresse und Zugangsdaten.",
|
||||
"formCancel": "Abbrechen",
|
||||
"formUrlErrorHttps": "Die Adresse muss mit https:// beginnen.",
|
||||
"formUrlErrorInvalid": "Bitte geben Sie eine gültige Adresse ein.",
|
||||
"saveSuccess": "Kalenderquelle gespeichert.",
|
||||
"saveError": "Die Kalenderquelle konnte nicht gespeichert werden."
|
||||
},
|
||||
"note": {
|
||||
"name": "Notizen",
|
||||
@@ -512,6 +533,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"
|
||||
@@ -666,7 +694,8 @@
|
||||
"detailStatusInactive": "Nicht aktiviert",
|
||||
"accessDenied": "Zugriff verweigert",
|
||||
"statusNoAccess": "Kein Zugriff",
|
||||
"toastNoAccess": "Kein Zugriff auf dieses Modul — wenden Sie sich an Ihren Administrator."
|
||||
"toastNoAccess": "Kein Zugriff auf dieses Modul — wenden Sie sich an Ihren Administrator.",
|
||||
"notFound": "Dieses Modul wurde nicht gefunden."
|
||||
},
|
||||
"certManager": {
|
||||
"title": "Zertifikat-Manager",
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"beta": "Beta",
|
||||
"live": "Live",
|
||||
"dev": "Development"
|
||||
}
|
||||
},
|
||||
"whatsNew": "What's new"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -210,7 +211,27 @@
|
||||
"connectionSuccess": "Connection successful",
|
||||
"connectionError": "Connection failed. Please check the URL and credentials.",
|
||||
"addSource": "Add source",
|
||||
"sourceEmpty": "No calendar sources configured. Add a source to display events."
|
||||
"sourceEmpty": "No calendar sources configured. Add a source to display events.",
|
||||
"formFieldName": "Name",
|
||||
"formFieldType": "Type",
|
||||
"formFieldExchangeMode": "Exchange connection",
|
||||
"formFieldDomain": "Domain",
|
||||
"formFieldDomainHint": "Optional – only needed if your Exchange server expects DOMAIN\\user for sign-in.",
|
||||
"formFieldUrl": "Address (URL)",
|
||||
"formFieldUsername": "Username",
|
||||
"formFieldPassword": "Password",
|
||||
"formFieldColor": "Color",
|
||||
"formSave": "Save",
|
||||
"formSaving": "Saving …",
|
||||
"formTestConnection": "Test connection",
|
||||
"formTesting": "Checking connection …",
|
||||
"formTestSuccess": "Connection successful.",
|
||||
"formTestFailed": "Connection failed. Please check the address and credentials.",
|
||||
"formCancel": "Cancel",
|
||||
"formUrlErrorHttps": "The address must start with https://.",
|
||||
"formUrlErrorInvalid": "Please enter a valid address.",
|
||||
"saveSuccess": "Calendar source saved.",
|
||||
"saveError": "The calendar source could not be saved."
|
||||
},
|
||||
"note": {
|
||||
"name": "Notes",
|
||||
@@ -512,6 +533,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"
|
||||
@@ -666,7 +694,8 @@
|
||||
"detailStatusInactive": "Not activated",
|
||||
"accessDenied": "Access denied",
|
||||
"statusNoAccess": "No Access",
|
||||
"toastNoAccess": "No access to this module — please contact your administrator."
|
||||
"toastNoAccess": "No access to this module — please contact your administrator.",
|
||||
"notFound": "This module was not found."
|
||||
},
|
||||
"certManager": {
|
||||
"title": "Certificate Manager",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user