16 Commits

Author SHA1 Message Date
schalli 5afe2a4bc9 docs(18): Verifikation (human_needed, 8/10) und Bedienprobe als UAT persistiert; Aktenstand
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 1m3s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 4m54s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m5s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 18:12:40 +02:00
schalli 72e488eea4 fix(desktop): Commit-Stempel aus TESSERA_COMMIT (Pipeline) statt nur git rev-parse; build.rs laeuft bei Aenderung neu
Mit dem Cargo-Zwischenspeicher wuerde ein einmal einkompilierter Stempel
sonst veralten und der Beta-Update-Hinweis dauerhaft erscheinen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:43:48 +02:00
schalli b42ba7ede5 docs(18): Protokoll der Review-Korrekturen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:43:04 +02:00
schalli 3accc174b4 docs(18): Review-Befunde behoben
Alle vier Critical/Warning-Befunde aus 18-REVIEW.md sind behoben (CR-01
0d5c80f, WR-01 1b2f803, WR-02 579e24b, WR-03 a8964f1); die beiden Info-Befunde
bleiben laut Auftrag unbearbeitet (dokumentiert als uebersprungen). Status auf
clean gesetzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:42:40 +02:00
schalli 579e24b81a fix(desktop): Beta-Update-Hinweis auch bei gleicher Version aber neuerem Commit anzeigen (WR-02)
desktop-collect.sh vergibt fuer den Beta-Kanal (main) jedem Commit dieselbe
X.Y.Z-Version aus dem letzten Freigabe-Tag (D-07) -- der bisherige Vergleich
nur ueber `version` liess Nutzer zwischen zwei Tags nie eine neuere
Beta-Version sehen, obwohl manifest.json ein neues `commit`-Feld traegt.
build.rs bettet jetzt per `git rev-parse --short=7 HEAD` denselben
Commit-Stempel, den desktop-collect.sh fuer manifest.json schreibt, als
APP_COMMIT zur Kompilierzeit ein; lib.rs vergleicht bei channel == "beta"
zusaetzlich den Commit. Fuer den Live-Kanal bleibt es beim reinen
Versionsvergleich, D-07 (X.Y.Z in tauri.conf.json/Cargo.toml) bleibt
unveraendert. cargo check + cargo clippy -- -D warnings sind sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:40:52 +02:00
schalli 1b2f803c3e fix(desktop): Tauri-CSP auf tatsaechlichen Bedarf von setup.html verengen (WR-01)
Die CSP erlaubte 'unsafe-eval' und Wildcard-Quellen (connect-src/img-src/
font-src/style-src *), obwohl setup.html -- die einzige lokale Seite der App --
kein eval() nutzt, keine externen Schriften/Bilder laedt und ausschliesslich
ueber die Tauri-IPC-Bruecke (window.__TAURI__.core.invoke) mit dem Rust-Teil
spricht. Diese CSP gilt nur fuer vom App-eigenen Protokoll ausgelieferte
Seiten; nach save_server_url() navigiert das Hauptfenster auf die
Server-Adresse, und diese Navigation wird von der CSP der Zielseite selbst
bestimmt, nicht mehr von dieser Konfiguration. Verengt auf
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'
'unsafe-inline'; img-src 'self' data:; connect-src 'self'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:39:51 +02:00
schalli 0d5c80fbbf fix(18): dot-only Manifest-Dateinamen ("."/"..") in getPackage() ablehnen (CR-01)
Die Zeichen-Whitelist /^[A-Za-z0-9._-]+$/ liess Namen wie ".." durch, weil
Punkt und Bindestrich erlaubte Zeichen sind -- path.join(desktopDistDir, '..')
loest aber in den Elternordner auf und unterlaeuft genau die Verteidigung in
der Tiefe (T-18-02), die diese Zeile laut Kommentar herstellen soll. Jetzt
werden "." und ".." explizit abgelehnt UND der aufgeloeste Pfad zusaetzlich
gegen desktopDistDir geprueft (haelt auch kuenftige Varianten ab, falls die
Whitelist anderswo wiederverwendet wird). Neue Testfaelle fuer manifest-Eintraege
namens "..", "." und "../manifest.json".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:39:29 +02:00
schalli a8964f1a23 fix(18): Manifest-Eintraege in getManifest() auf gueltige Form pruefen (WR-03)
Ein kaputter Plattform-Eintrag (z. B. fehlendes name-Feld oder ungueltiger
sha256) fiel bisher erst spaeter unbemerkt durch -- entry.name === undefined
wurde zu "undefined" gecoerct und als Dateiname gesucht. getManifest()
prueft jetzt jeden vorhandenen Plattform-Eintrag (name: string, size: number,
sha256: 64-stelliger Hex-String) und behandelt ein kaputtes Manifest wie ein
fehlendes (404 + Warn-Log), statt die kaputte Form durchzureichen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:38:50 +02:00
schalli 65efdf6ab7 docs(18): Code-Review — 1 kritisch, 3 Warnungen, 2 Hinweise
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:34:15 +02:00
schalli 54f396a288 docs(18-06): STATE/ROADMAP nach Plan 06 aktualisieren 2026-09-16 17:27:35 +02:00
schalli a777814034 docs(18-06): SUMMARY fuer Handbuecher/CHANGELOG/REQUIREMENTS-Plan
Coverage-Block, Manuelle-Abnahme-Abschnitt mit Bedienprobe-Schrittfolge und
den zwei auf den naechsten Freigabe-Tag vertagten Punkten, Self-Check PASSED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:26:58 +02:00
schalli 29219baa60 docs(18-06): REQUIREMENTS.md um DESK-01..05 und Traceability ergaenzen
- Neuer Abschnitt "Phase 18 — Desktop-Client fertigstellen" mit DESK-01..05
  (DESK-01/02 aus Phase 6 fortgefuehrt, DESK-03..05 neu aus 18-CONTEXT.md)
- Traceability-Tabelle um fuenf DESK-Zeilen ergaenzt, Coverage-Satz aktualisiert
- Alle Gesamtlaeufe gruen: API 1086/1086, Web 365/365, beide Typpruefungen
  fehlerfrei, cargo check Finished

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:24:52 +02:00
schalli 8f2069b845 docs(18-06): Betriebshandbuch Kapitel 10, CI/CD-Runbook Job desktop, Entwicklungshandbuch
- Betriebshandbuch: neues Kapitel 10 (Pipeline-Herkunft, Ablageort im Abbild,
  Release-Anhaenge, DESKTOP_DIST_DIR, Fehlerbilder-Tabelle); Kapitel 9 um
  Satz zu Desktop-Paketen im Freigabe-Tag ergaenzt
- CI/CD-Runbook: aus drei werden vier Jobs, Job desktop ausfuehrlich
  beschrieben (Cross-Bau, Cache-Reihenfolge, Cache-vs-upload-artifact-
  Begruendung), Fehlerbehebung um drei Unterabschnitte ergaenzt
  (Job desktop, cache miss, Release-Upload 413)
- Entwicklungshandbuch: apps/desktop ist kein Grundgeruest mehr, neuer
  Abschnitt "Desktop-App lokal bauen", Testabschnitt um vitest src/desktop
  und cargo check/clippy ergaenzt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:22:49 +02:00
schalli 43c7061cb3 docs(18-06): Anwenderhandbuch Kapitel Desktop-App, CHANGELOG-Stichpunkt
- Neues Kapitel "Desktop-App" (9 Unterabschnitte: Was es ist, Herunterladen,
  Installation Windows/Linux, Erster Start, Infobereich/Beenden, Automatischer
  Start, Neue Version, Fehlerbilder), Inhaltsverzeichnis um Punkt 8 ergaenzt
- CHANGELOG-Stichpunkt unter Unveroeffentlicht/Neu im Wortlaut von D-17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:20:20 +02:00
schalli b83d02d6fc docs(18-05): STATE/ROADMAP nach Plan 05 aktualisieren 2026-09-16 17:17:22 +02:00
schalli 1a05290841 docs(18-05): complete Windows-Cross-Bau-Plan mit SUMMARY
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 17:16:25 +02:00
21 changed files with 1349 additions and 40 deletions
+4
View File
@@ -62,6 +62,10 @@ jobs:
desktop: desktop:
name: Desktop-Pakete bauen name: Desktop-Pakete bauen
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
# Commit-Stempel fuer build.rs (WR-02): mit Cargo-Zwischenspeicher wuerde
# `git rev-parse` im Build-Skript sonst nicht neu ausgewertet.
TESSERA_COMMIT: ${{ gitea.sha }}
needs: test needs: test
if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v') if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')
steps: steps:
+19 -1
View File
@@ -81,6 +81,18 @@
- [x] **PERM-06**: Die Migration überführt den Bestand ohne Zugriffsverlust: pro Mandant entsteht eine als Standardgruppe markierte Gruppe mit allen bestehenden Benutzern und Freigaben für alle zum Migrationszeitpunkt aktiven Module. Neue Benutzer — manuell angelegt wie per LDAP importiert — treten der markierten Standardgruppe automatisch bei. - [x] **PERM-06**: Die Migration überführt den Bestand ohne Zugriffsverlust: pro Mandant entsteht eine als Standardgruppe markierte Gruppe mit allen bestehenden Benutzern und Freigaben für alle zum Migrationszeitpunkt aktiven Module. Neue Benutzer — manuell angelegt wie per LDAP importiert — treten der markierten Standardgruppe automatisch bei.
- [x] **PERM-07**: Ein Dashboard-Widget, dessen Modul dem Benutzer nicht freigegeben ist, erscheint nicht auf seinem Dashboard. - [x] **PERM-07**: Ein Dashboard-Widget, dessen Modul dem Benutzer nicht freigegeben ist, erscheint nicht auf seinem Dashboard.
## Phase 18 — Desktop-Client fertigstellen
### DESK — Desktop-Client
**Hinzugefügt 2026-09-16** — DESK-01/02 stammen aus v1.0 (Phase 6) und werden fortgeführt; DESK-03..05 aus `18-CONTEXT.md` abgeleitet.
- [x] **DESK-01**: Tauri-basierter Desktop-Wrapper für Windows und Linux (Phase 6, fortgeführt).
- [x] **DESK-02**: Die Desktop-App verbindet sich mit dem Web-Backend; die Server-Adresse wird beim ersten Start abgefragt (Phase 6, fortgeführt; D-02).
- [ ] **DESK-03**: Der Installer ist in Tessera herunterladbar — Link auf der Anmeldeseite und Seite Einstellungen → Desktop-App, Auslieferung über die Tessera-API ohne Gitea-Zugang (D-01, D-10, D-12).
- [ ] **DESK-04**: Ein Freigabe-Tag baut Windows-Installer und Linux-AppImage in der Pipeline und hängt beide als Dateien an den Gitea-Release (D-04..D-08).
- [ ] **DESK-05**: Der Client trägt die Freigabe-Version, vergleicht sie mit `/desktop/latest` und weist mit Download-Link auf eine neuere Version hin (D-07, D-11, D-13).
## Future Requirements (deferred) ## Future Requirements (deferred)
- [ ] TED API v3 (EU-weite Redundanz) — für DE-only weitgehend redundant zu DÖE. - [ ] TED API v3 (EU-weite Redundanz) — für DE-only weitgehend redundant zu DÖE.
@@ -145,4 +157,10 @@
| PERM-06 | Phase 15 | Pending | | PERM-06 | Phase 15 | Pending |
| PERM-07 | Phase 15 | Complete | | PERM-07 | Phase 15 | Complete |
**Coverage:** 34/34 v1.1 requirements mapped (29 aus Phase 10–14 + SRC-01..05 aus Phase 17, nachträglich am 2026-08-12 ergänzt — die Kategorie fehlte in dieser Datei, obwohl 17-01/17-02 sie bereits in ihren SUMMARY-Frontmattern als erledigt führten) — no orphans. 7/7 v1.2 requirements mapped: PERM-01/03/04/05/06/07 auf Phase 15, PERM-02 auf Phase 16 (umgehängt 2026-08-06). | DESK-01 | Phase 6 / 18 | Complete |
| DESK-02 | Phase 6 / 18 | Complete |
| DESK-03 | Phase 18 | Pending |
| DESK-04 | Phase 18 | Pending |
| DESK-05 | Phase 18 | Pending |
**Coverage:** 34/34 v1.1 requirements mapped (29 aus Phase 10–14 + SRC-01..05 aus Phase 17, nachträglich am 2026-08-12 ergänzt — die Kategorie fehlte in dieser Datei, obwohl 17-01/17-02 sie bereits in ihren SUMMARY-Frontmattern als erledigt führten) — no orphans. 7/7 v1.2 requirements mapped: PERM-01/03/04/05/06/07 auf Phase 15, PERM-02 auf Phase 16 (umgehängt 2026-08-06). 5/5 DESK-Anforderungen auf Phase 18 abgebildet (DESK-01/02 fortgeführt aus Phase 6, DESK-03/04/05 neu in Phase 18) — DESK-03..05 wechseln auf Complete, sobald die Bedienprobe dieser Phase abgeschlossen ist.
+3 -3
View File
@@ -666,7 +666,7 @@ Plans (Wellenstruktur — streng nacheinander, alle drei fassen Schema, Controll
3. Der installierte Client zeigt nach Eingabe der Server-Adresse die Tessera-Anmeldung, laeuft mit Tray/Schliessen-ins-Tray/Autostart wie in Phase 6 und meldet eine neuere Client-Version mit Link zur Download-Seite 3. Der installierte Client zeigt nach Eingabe der Server-Adresse die Tessera-Anmeldung, laeuft mit Tray/Schliessen-ins-Tray/Autostart wie in Phase 6 und meldet eine neuere Client-Version mit Link zur Download-Seite
4. Anwender- und Betriebshandbuch beschreiben Installation, Erststart, Tray-Verhalten, Pipeline, Release-Dateien und Umgebungsvariablen 4. Anwender- und Betriebshandbuch beschreiben Installation, Erststart, Tray-Verhalten, Pipeline, Release-Dateien und Umgebungsvariablen
**Plans:** 4/6 plans executed **Plans:** 6/6 plans executed
Plans: Plans:
**Wave 1** **Wave 1**
@@ -681,8 +681,8 @@ Plans:
**Wave 3** *(blocked on Wave 2 completion)* **Wave 3** *(blocked on Wave 2 completion)*
- [ ] 18-05-PLAN.md — Windows-Cross-Bau (Welle 3): cargo-xwin/NSIS im Job desktop, Einsammeln beider Pakete, Push-Checkpoint mit Iterationsschleife (max. 3 Runden) bis zum gruenen Lauf - [x] 18-05-PLAN.md — Windows-Cross-Bau (Welle 3): cargo-xwin/NSIS im Job desktop, Einsammeln beider Pakete, Push-Checkpoint mit Iterationsschleife (max. 3 Runden) bis zum gruenen Lauf
**Wave 4** *(blocked on Wave 3 completion)* **Wave 4** *(blocked on Wave 3 completion)*
- [ ] 18-06-PLAN.md — Abschluss (Welle 4): Handbuecher (Anwender, Betrieb, Entwicklung, CI/CD-Runbook), CHANGELOG, REQUIREMENTS DESK-01..05, Gesamtlaeufe, Bedienprobe des Nutzers auf Windows - [x] 18-06-PLAN.md — Abschluss (Welle 4): Handbuecher (Anwender, Betrieb, Entwicklung, CI/CD-Runbook), CHANGELOG, REQUIREMENTS DESK-01..05, Gesamtlaeufe, Bedienprobe des Nutzers auf Windows
+12 -8
View File
@@ -4,16 +4,16 @@ milestone: v1.2
current_phase: 18 current_phase: 18
current_phase_name: desktop-client-fertigstellen current_phase_name: desktop-client-fertigstellen
status: verified status: verified
stopped_at: Completed 18-04-PLAN.md stopped_at: Completed 18-06-PLAN.md — Windows-Bedienprobe des Nutzers steht aus
last_updated: "2026-09-16T14:43:25.735Z" last_updated: "2026-09-16T15:27:16.943Z"
last_activity: 2026-09-16 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 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: 289604a28e326f9909963922ca938c6a8fa7d661 state_head: a777814034ee5907cb4d151669447f015841082d
progress: progress:
total_phases: 18 total_phases: 18
completed_phases: 15 completed_phases: 15
total_plans: 89 total_plans: 89
completed_plans: 86 completed_plans: 88
milestone_name: Plattform-Berechtigungen milestone_name: Plattform-Berechtigungen
--- ---
@@ -29,9 +29,9 @@ See: .planning/PROJECT.md (updated 2026-07-17)
## Current Position ## Current Position
Phase: 18 (desktop-client-fertigstellen) — IN PROGRESS Phase: 18 (desktop-client-fertigstellen) — IN PROGRESS
Plan: 4 of 6 (18-02 abgeschlossen) Plan: 6 of 6 (18-02 abgeschlossen)
Status: 18-02 (CI-Job desktop, Cache-Uebergabe an publish, Release-Anhaenge) fertig; 18-03 (Web-Oberflaeche), 18-04 (Client-Updatepruefung), 18-05 (Windows-Cross-Bau + Pipeline-Beweis), 18-06 (Freigabe) stehen aus Status: 18-02 (CI-Job desktop, Cache-Uebergabe an publish, Release-Anhaenge) fertig; 18-03 (Web-Oberflaeche), 18-04 (Client-Updatepruefung), 18-05 (Windows-Cross-Bau + Pipeline-Beweis), 18-06 (Freigabe) stehen aus
Last activity: 2026-09-16 - 18-02: Job desktop (Linux-AppImage, actions/cache) vor publish, Manifest-Pruefung in publish-images.sh, idempotenter Release-Anhang-Upload in publish-release.sh Last activity: 2026-09-16 - Phase 18 Desktop-Client: 6/6 Plaene ausgefuehrt, Code-Review behoben, Verifikation human_needed (Windows-Bedienprobe durch den User, Release-Anhang + Update-Hinweis erst beim naechsten Tag); gepusht, CI-Lauf 367 gruen mit beiden Paketen
Progress: [███░░░░░░░] 33% Progress: [███░░░░░░░] 33%
@@ -127,6 +127,8 @@ Progress: [███░░░░░░░] 33%
| Phase 18 P02 | 8 min | 2 tasks | 3 files | | Phase 18 P02 | 8 min | 2 tasks | 3 files |
| Phase 18-desktop-client-fertigstellen P03 | 20 min | 2 tasks | 12 files | | Phase 18-desktop-client-fertigstellen P03 | 20 min | 2 tasks | 12 files |
| Phase 18 P04 | 9min | 2 tasks | 15 files | | Phase 18 P04 | 9min | 2 tasks | 15 files |
| Phase 18 P05 | 29 min | 3 tasks | 1 files |
| Phase 18 P06 | 21min | 3 tasks | 7 files |
## Accumulated Context ## Accumulated Context
@@ -315,6 +317,8 @@ Recent decisions affecting current work:
- [Phase 18]: 18-02: Cross-Job-Uebergabe per actions/cache (save/restore, Schluessel exakt am gitea.sha) statt upload-/download-artifact, da diese auf der Gitea-Instanz unzuverlaessig sind. — Pitfall 1 aus 18-RESEARCH.md; publish bricht bei Cache-Fehlschlag hart ab (fail-on-cache-miss + explizite Manifest-Pruefung in Workflow und Skript). - [Phase 18]: 18-02: Cross-Job-Uebergabe per actions/cache (save/restore, Schluessel exakt am gitea.sha) statt upload-/download-artifact, da diese auf der Gitea-Instanz unzuverlaessig sind. — Pitfall 1 aus 18-RESEARCH.md; publish bricht bei Cache-Fehlschlag hart ab (fail-on-cache-miss + explizite Manifest-Pruefung in Workflow und Skript).
- [Phase 18]: Desktop-Download-Link/Einstellungsseite lesen GET /desktop/latest memoisiert und blenden sich ohne Pakete aus — Wiederverwendung des app-version.ts-Musters (Modul-Ebene-Promise, still bei Fehler) - [Phase 18]: Desktop-Download-Link/Einstellungsseite lesen GET /desktop/latest memoisiert und blenden sich ohne Pakete aus — Wiederverwendung des app-version.ts-Musters (Modul-Ebene-Promise, still bei Fehler)
- [Phase 18]: 18-04: api_url() als einzige Stelle mit dem /api-proxy-Rewrite-Praefix; Erststart-Seite spricht nur noch ueber window.__TAURI__.core.invoke statt Modul-Import - [Phase 18]: 18-04: api_url() als einzige Stelle mit dem /api-proxy-Rewrite-Praefix; Erststart-Seite spricht nur noch ueber window.__TAURI__.core.invoke statt Modul-Import
- [Phase 18]: Windows-Werkzeuge-Schritt nach dem Cargo-Zwischenspeicher platziert, nicht davor — Damit die cargo-xwin-Installationspruefung (command -v ...) den per actions/cache wiederhergestellten Stand sieht und das Werkzeug bei warmem Cache nicht bei jedem Lauf neu gebaut wird.
- [Phase 18]: 18-06: DESK-03/04/05 bleiben Pending bis Windows-Bedienprobe des Nutzers vorliegt; alle Handbuecher/CHANGELOG/REQUIREMENTS nachgezogen, alle Gesamtlaeufe gruen (API 1086, Web 365, Typpruefungen, cargo check).
### Pitfalls & Anti-Patterns ### Pitfalls & Anti-Patterns
@@ -467,8 +471,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
## Session Continuity ## Session Continuity
Last session: 2026-09-16T14:43:25.471Z Last session: 2026-09-16T15:27:16.618Z
Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; #29 und 3c als /gsd-quick --validate mit voller Kette durchgefuehrt. Resumed: 2026-09-14 — Sitzung ueber /gsd-resume-work fortgesetzt; #29 und 3c als /gsd-quick --validate mit voller Kette durchgefuehrt.
Stopped at: Completed 18-04-PLAN.md Stopped at: Completed 18-06-PLAN.md — Windows-Bedienprobe des Nutzers steht aus
Resume file: None Resume file: None
Last activity: 2026-09-16 - Completed quick task 260916-k2z: Mehrfach-Kreise je Kalender am Tag; Browser-Pruefung + Push ausstehend Last activity: 2026-09-16 - Completed quick task 260916-k2z: Mehrfach-Kreise je Kalender am Tag; Browser-Pruefung + Push ausstehend
@@ -0,0 +1,160 @@
---
phase: 18-desktop-client-fertigstellen
plan: 05
subsystem: infra
tags: [ci, gitea-actions, tauri, cargo-xwin, nsis, windows-cross-build, desktop]
# Dependency graph
requires:
- phase: 18-desktop-client-fertigstellen (18-02)
provides: Job `desktop` mit Linux-AppImage-Bau, `actions/cache`-Uebergabe an `publish`, `desktop-collect.sh`/`publish-release.sh`
- phase: 18-desktop-client-fertigstellen (18-04)
provides: Client mit tauri-plugin-opener/autostart/window-state — die Cargo.toml, die der Cross-Bau kompilieren muss
provides:
- Job `desktop` baut auf dem Linux-Runner zusaetzlich `Tessera-Setup-X.Y.Z.exe` per Cross-Bau (cargo-xwin, NSIS)
- Gruener Pipeline-Lauf auf `main` mit beiden Dateien (`.exe` + `.AppImage`), gruenem `publish`
affects: [18-06]
# Actuals (#2632)
actuals:
tokens: 900
tasks: 3
commits: 2
# Tech tracking
tech-stack:
added: [cargo-xwin 0.23.x (Ubuntu apt: lld/llvm/clang/nsis)]
patterns:
- "Cross-Job-Handoff ueber actions/cache@v4 (Schluessel gitea.sha), nicht upload-artifact — bestaetigt auch fuer den erweiterten Job funktionsfaehig"
- "Cache-Restore-Schritt steht VOR jedem Installationsschritt, der geprueft werden soll (command -v ... || cargo install), sonst greift die Restauration zu spaet und das Werkzeug wird bei jedem Lauf neu gebaut"
key-files:
created: []
modified:
- .gitea/workflows/ci.yml
key-decisions:
- "Windows-Bau laeuft nacheinander im selben Job wie der Linux-Bau (ein Cache, ein Runner) statt in einem eigenen Job — wie in 18-CONTEXT.md (Specific Ideas, Runner-Grenzen 8 Kerne/15 GB) festgelegt."
- "cargo-xwin, NSIS-Plugin-Ablage (~/.local/share/tauri) und die xwin-SDK-Ablage (~/.cache/cargo-xwin) wurden in den bestehenden Cargo-Zwischenspeicher-Schritt aufgenommen statt einen zweiten Cache-Schritt anzulegen."
- "Der Schritt 'Windows-Werkzeuge' (rustup target add + cargo install cargo-xwin) wurde bewusst NACH dem Cargo-Zwischenspeicher-Schritt platziert (nicht wie im Plantext 'nach Rust-Toolchain' woertlich als naechster Schritt), damit die command -v cargo-xwin-Pruefung den wiederhergestellten Cache sieht und das Werkzeug bei warmem Cache nicht jedes Mal neu gebaut wird."
patterns-established:
- "Iterationsschleife (D-16): jede Runde ein eigener Commit mit Praefix 'ci(desktop): Runde N — {Ursache}', Ursache/Fix im SUMMARY dokumentiert."
requirements-completed: [DESK-01, DESK-04, DESK-05]
coverage:
- id: D1
description: "Der CI-Job desktop baut auf dem Linux-Runner den Windows-Installer per Cross-Bau (cargo-xwin, NSIS) und sammelt Tessera-Setup-X.Y.Z.exe neben Tessera-X.Y.Z.AppImage ein"
requirement: "DESK-04"
verification:
- kind: e2e
ref: "Gitea CI/CD Lauf 367 (Commit 742fb5c), Job 'Desktop-Pakete bauen', Schritt 'Pakete einsammeln'"
status: pass
human_judgment: false
- id: D2
description: "Ein Push auf main endet mit einem gruenen Lauf: Job desktop mit beiden Dateien, Job publish mit Abbildern, die die Pakete tragen"
requirement: "DESK-04"
verification:
- kind: e2e
ref: "Gitea CI/CD Lauf 367 (Commit 742fb5c) — alle vier Jobs gruen, Job publish hat Abbilder mit Etiketten beta/latest gepusht"
status: pass
human_judgment: false
- id: D3
description: "Jeder Fehlschlag der Pipeline wird gelesen, der Job angepasst, erneut gepusht — hoechstens drei Runden, jede als normaler Commit"
requirement: "DESK-01"
verification:
- kind: manual_procedural
ref: "Runde 1 (Commit 742fb5c): clippy-Komponente fehlte, ein Commit, danach gruen — 1 von hoechstens 3 Runden verbraucht"
status: pass
human_judgment: false
duration: 29min
completed: 2026-09-16
status: complete
---
# Phase 18 Plan 05: Windows-Cross-Bau im CI-Job desktop Summary
**Der CI-Job `desktop` baut jetzt in einem Lauf auf dem Linux-Runner sowohl das Linux-AppImage als auch — per Cross-Bau mit `cargo-xwin` und NSIS aus dem Ubuntu-Paket — den Windows-Installer; bewiesen durch einen gruenen Pipeline-Lauf mit beiden Dateien und gruenem `publish`.**
## Performance
- **Duration:** 29 min
- **Started:** 2026-09-16T14:46:59Z (erster Commit dieses Plans)
- **Completed:** 2026-09-16T15:15:23Z
- **Tasks:** 3 (Task 2 = Checkpoint, kein eigener Commit; 3 Aufgaben, 2 Commits)
- **Files modified:** 1 (`.gitea/workflows/ci.yml`)
## Accomplishments
- Job `desktop` installiert `lld llvm clang nsis` per apt und ein neues `rustup target x86_64-pc-windows-msvc` + `cargo-xwin` (Schritt "Windows-Werkzeuge", NACH dem Cache-Restore platziert, damit ein warmer Cache das Neu-Bauen von `cargo-xwin` erspart).
- Nach dem bestehenden AppImage-Schritt baut ein neuer Schritt "Windows-Installer bauen (Cross-Bau)" per `pnpm --filter @tessera/desktop exec tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis`.
- "Pakete einsammeln" laeuft jetzt mit `--require linux,windows`; `desktop-collect.sh` kannte den Windows-Zweig (`target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe`) bereits aus 18-01, keine Skript-Aenderung noetig.
- Cargo-Zwischenspeicher um `~/.cargo/bin/cargo-xwin`, `~/.cache/cargo-xwin` (Windows-SDK-Ablage von cargo-xwin) und `~/.local/share/tauri` (NSIS-Plugins) erweitert.
- Iterationsschleife durchlaufen (D-16): 1 Runde — fehlende `clippy`-Komponente im `--profile minimal`-Toolchain behoben, dann gruener Lauf.
- Gruener Pipeline-Lauf auf `main` (Lauf 367, Commit `742fb5c`): alle vier Jobs gruen, Job `desktop` in ~10 min (kalter Cache) mit `Tessera-Setup-1.1.0-beta.742fb5c.exe` (2.775.663 Bytes) und `Tessera-1.1.0-beta.742fb5c.AppImage` (82.479.608 Bytes), Job `publish` hat Abbilder mit Etiketten `beta`/`latest` gepusht.
## Task Commits
Jede Aufgabe wurde atomar committet:
1. **Task 1: Windows-Cross-Bau in den Job desktop einbauen** - `1c4247a` (ci)
2. **Task 2: Push und CI-Lauf beobachten** - Checkpoint, kein eigener Commit (Push durch Orchestrator, kein Repo-Zustand veraendert)
3. **Task 3, Runde 1: clippy-Komponente ergaenzt** - `742fb5c` (ci)
**Plan metadata:** folgt in diesem Commit (`docs(18-05): ...`)
## Files Created/Modified
- `.gitea/workflows/ci.yml` - Windows-Cross-Bau-Schritte im Job `desktop` (apt-Pakete, Werkzeuge, Cache-Pfade, Bauschritt, Einsammeln mit beiden Plattformen), plus die Runde-1-Korrektur (`rustup component add clippy`)
## Decisions Made
- Windows-Bau nacheinander im selben Job wie der Linux-Bau, ein Cache — wie in `18-CONTEXT.md` (Specific Ideas) festgelegt, statt eines zweiten parallelen Jobs, der die Runner-Grenzen (8 Kerne/15 GB) ueberschritten haette.
- Der neue Schritt "Windows-Werkzeuge" wurde entgegen der woertlichen Plan-Reihenfolge ("nach Rust-Toolchain") NACH dem Cargo-Zwischenspeicher-Schritt platziert. Grund: `command -v cargo-xwin >/dev/null 2>&1 || cargo install --locked cargo-xwin` soll den wiederhergestellten Cache sehen koennen — stuende der Schritt vor dem Cache-Restore, waere `cargo-xwin` bei jedem Lauf neu gebaut, selbst wenn der Cache es bereits enthaelt. Das Ziel des Plans ("SDK-Ablage ... soll nur einmal geladen werden") war damit nur durch die Umstellung der Reihenfolge sauber erreichbar; die genannten Kennzeichen des Plans (`cargo-xwin`-Vorkommen, Cache-Pfade, Bauschritt-Reihenfolge AppImage-vor-NSIS) blieben davon unberuehrt und alle automatisierten `<verify>`-Pruefungen bestehen weiterhin.
- Keine `XWIN_CACHE_DIR`-Umgebungsvariable im ersten Anlauf gesetzt (Plan-Vorgabe: das ist eine Ausweichlösung der Iterationsschleife, nicht Teil von Task 1) — nicht noetig, der Lauf war nach der Clippy-Korrektur gruen.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] `cargo clippy` scheiterte an fehlender clippy-Komponente**
- **Found during:** Task 2 (erste CI-Rueckmeldung, "rot")
- **Issue:** Der Schritt "Rust-Toolchain" installiert `stable` mit `--profile minimal`, dieses Profil enthaelt `clippy` nicht. Der bereits bestehende Schritt "Rust pruefen" (aus 18-02, unveraendert von diesem Plan) ruft `cargo clippy` auf und scheiterte mit `'cargo-clippy' is not installed for the toolchain 'stable-x86_64-unknown-linux-gnu'`.
- **Fix:** `"$HOME/.cargo/bin/rustup" component add clippy` direkt nach der Toolchain-Installation im Schritt "Rust-Toolchain" ergaenzt (voller Pfad, weil `$GITHUB_PATH` erst fuer Folgeschritte greift, nicht innerhalb desselben `run:`-Blocks).
- **Files modified:** `.gitea/workflows/ci.yml`
- **Verification:** Lauf 367 (Commit `742fb5c`) — Schritt "Rust pruefen" gruen, gesamter Job `desktop` gruen.
- **Committed in:** `742fb5c` (`ci(desktop): Runde 1 — clippy-Komponente fehlt im Toolchain`)
---
**Total deviations:** 1 auto-fixed (1 blocking, per D-16-Iterationsschleife — dies ist die im Plan vorgesehene Korrekturrunde, keine ungeplante Abweichung im engeren Sinn).
**Impact on plan:** Notwendige Korrektur fuer einen gruenen Lauf; kein Scope-Creep, betraf ausschliesslich den bereits vorhandenen "Rust pruefen"-Schritt aus 18-02.
## Iterationsschleife (D-16)
| Runde | Signatur im Protokoll | Ursache | Aenderung | Commit |
|---|---|---|---|---|
| 1 | `error: 'cargo-clippy' is not installed for the toolchain 'stable-x86_64-unknown-linux-gnu'` (Job `desktop`, Schritt "Rust pruefen", Lauf 366) | `--profile minimal` installiert keine `clippy`-Komponente | `rustup component add clippy` im Schritt "Rust-Toolchain" | `742fb5c` |
Nach Runde 1: Lauf 367 (Commit `742fb5c`) komplett gruen — Iterationsschleife nach 1 von hoechstens 3 Runden beendet.
## Issues Encountered
Keiner ueber die dokumentierte Runde 1 hinaus. Hinweis aus der Rueckmeldung des roten Laufs: nach dem Fehlschlag in Runde-0 (Lauf 366) landete nichts im Cargo-Cache (Post-Schritt wegen `success()=false` uebersprungen) — der naechste Lauf startete entsprechend kalt (~10 min fuer den Job `desktop`). Kein Fehler, nur zur Einordnung der Bau-Dauer.
## User Setup Required
None - keine externe Dienstkonfiguration noetig.
## Next Phase Readiness
- Beide Desktop-Pakete (`.exe`, `.AppImage`) entstehen zuverlaessig in einem CI-Lauf auf `main`; das Beta-Abbild traegt sie ab Lauf 367.
- 18-06 kann den Testserver auf den neuen Stand ziehen und die Pakete dort sichtbar machen (Download-Weg selbst wurde bereits in 18-02 gebaut und per Probelauf geprueft).
- Der Release-Anhang (Gitea-Release-Datei-Upload) ist erst beim naechsten Freigabe-Tag `v*` sichtbar — in diesem Lauf war kein Tag gesetzt, der Release-Schritt lief erwartungsgemaess nicht.
---
*Phase: 18-desktop-client-fertigstellen*
*Completed: 2026-09-16*
## Self-Check: PASSED
- FOUND: .planning/phases/18-desktop-client-fertigstellen/18-05-SUMMARY.md
- FOUND: 1c4247a (Task 1)
- FOUND: 742fb5c (Task 3 Runde 1)
- Gruener CI-Lauf 367 (Commit 742fb5c) durch den Orchestrator bestaetigt, `git status --porcelain` leer, `git log -1` entspricht dem gemeldeten Lauf-Commit.
@@ -0,0 +1,216 @@
---
phase: 18-desktop-client-fertigstellen
plan: 06
subsystem: docs
tags: [documentation, changelog, requirements-traceability, desktop-distribution]
# Dependency graph
requires:
- phase: 18-desktop-client-fertigstellen (18-01..18-05)
provides: "GET /desktop/latest, GET /desktop/download/:platform, Web-Oberflaeche (Anmeldeseite/Einstellungen), Client-Update-Hinweis/Tray/Autostart, CI-Job desktop mit Windows-Cross-Bau (Lauf 367, Commit 742fb5c)"
provides:
- "Anwenderhandbuch Kapitel 'Desktop-App' (9 Unterabschnitte: Was es ist, Herunterladen, Installation Windows/Linux, Erster Start, Infobereich/Beenden, Automatischer Start, Neue Version, Fehlerbilder)"
- "Betriebshandbuch Kapitel 10 (Pipeline-Herkunft, Ablageort im Abbild, Release-Anhaenge, DESKTOP_DIST_DIR, Fehlerbilder)"
- "CI/CD-Runbook: Job desktop dokumentiert (Cross-Bau, Cache-Reihenfolge, Cache-vs-upload-artifact, drei neue Fehlerbehebungs-Unterabschnitte)"
- "Entwicklungshandbuch: apps/desktop nicht mehr als Grundgeruest, Abschnitt 'Desktop-App lokal bauen', Testabschnitt um vitest src/desktop + cargo check/clippy ergaenzt"
- "CHANGELOG-Stichpunkt (D-17), REQUIREMENTS.md Kategorie DESK mit Traceability"
affects: []
actuals:
tokens: 7150
tasks: 3
commits: 3
plan_head_before: b83d02d
tech-stack:
added: []
patterns: []
key-files:
created:
- .planning/phases/18-desktop-client-fertigstellen/18-06-SUMMARY.md
modified:
- docs/anleitung-anwender.md
- docs/anleitung-betrieb.md
- docs/anleitung-entwicklung.md
- docs/ci-cd-setup.md
- CHANGELOG.md
- .planning/REQUIREMENTS.md
key-decisions:
- "DESK-03/04/05 bleiben in REQUIREMENTS.md auf Pending stehen, bis die Bedienprobe des Nutzers (Windows-Installation) tatsaechlich durchgefuehrt wurde — dieser Plan liefert die Dokumentation und alle automatisierten Gesamtlaeufe, kann die Bedienprobe selbst aber nicht ausfuehren (kein Windows-PC in dieser Ausfuehrungsumgebung)."
- "Kapitel 9 (Betriebshandbuch, 'Eine Version freigeben') um einen Verweis-Satz auf Kapitel 10 ergaenzt, statt Kapitel 10 isoliert stehen zu lassen — der Freigabe-Ablauf und der Desktop-Release-Anhang gehoeren fachlich zusammen."
patterns-established: []
requirements-completed: [DESK-01, DESK-02, DESK-03, DESK-04, DESK-05]
coverage:
- id: D1
description: "Anwenderhandbuch: Kapitel 'Desktop-App' mit neun Unterabschnitten (Was es ist, Herunterladen, Installation Windows/Linux inkl. SmartScreen-Anleitung, Erster Start, Infobereich/Beenden, Automatischer Start, Neue Version, Fehlerbilder), Inhaltsverzeichnis-Eintrag, keine firmenspezifische Adresse im Kapitel"
requirement: "DESK-03"
verification:
- kind: other
ref: "DOCS1-OK Pruefkette aus 18-06-PLAN.md <verify> (Kapitel, TOC-Anker, 9 Unterabschnitte, Pflichtbeschriftungen 'Trotzdem ausführen'/'Desktop-App herunterladen (Windows)'/'Mit Windows starten', 0 ctl.de/vicolab-Treffer im Kapitel)"
status: pass
human_judgment: false
- id: D2
description: "CHANGELOG-Stichpunkt im Wortlaut von D-17 unter Unveröffentlicht/Neu, oberhalb der ersten Versionsueberschrift"
requirement: "DESK-03"
verification:
- kind: other
ref: "node-Pruefung aus 18-06-PLAN.md <verify> (Position zwischen '## Unveröffentlicht' und der naechsten Versionsueberschrift)"
status: pass
human_judgment: false
- id: D3
description: "Betriebshandbuch Kapitel 10 (Pipeline-Herkunft, Ablageort /app/desktop-dist im Abbild, Release-Anhaenge, DESKTOP_DIST_DIR-Tabelle, Fehlerbilder-Tabelle) plus Verweissatz in Kapitel 9"
requirement: "DESK-04"
verification:
- kind: other
ref: "BETRIEB-Pruefkette aus 18-06-PLAN.md <verify> (Kapitelueberschrift, TOC-Eintrag '10. [', DESKTOP_DIST_DIR, /app/desktop-dist, Abschnitt Fehlerbilder)"
status: pass
human_judgment: false
- id: D4
description: "CI/CD-Runbook: 'vier' statt 'drei' Jobs, Job desktop ausfuehrlich beschrieben (Cross-Bau-Reihenfolge, Cache-Pfade, Cache-Schluessel desktop-dist-{sha}, Begruendung Cache statt upload-artifact), drei neue Fehlerbehebungs-Unterabschnitte (Job desktop, cache miss, Release-Upload 413) inkl. der realen Fehlerursache aus 18-05 (fehlende clippy-Komponente)"
requirement: "DESK-04"
verification:
- kind: other
ref: "CI-Pruefkette aus 18-06-PLAN.md <verify> (cargo-xwin >=2, desktop-dist-, upload-artifact, 'vier Jobs')"
status: pass
human_judgment: false
- id: D5
description: "Entwicklungshandbuch: apps/desktop nicht mehr als Grundgeruest beschrieben, neuer Abschnitt 'Desktop-App lokal bauen' (Voraussetzungen, desktop-version.sh, lokaler AppImage-Bau, desktop-collect.sh, Hinweis Windows-Installer nur im CI), Testabschnitt um 'vitest run src/desktop' und cargo check/clippy ergaenzt"
requirement: "DESK-03"
verification:
- kind: other
ref: "ENTWICKLUNG-Pruefkette aus 18-06-PLAN.md <verify> (0 Treffer 'Tauri-Grundgerüst', Abschnittsueberschrift, desktop-version.sh, vitest run src/desktop)"
status: pass
human_judgment: false
- id: D6
description: "REQUIREMENTS.md: neuer Abschnitt 'Phase 18 — Desktop-Client fertigstellen' mit DESK-01..05, fuenf Traceability-Zeilen, aktualisierter Coverage-Satz"
requirement: "DESK-01"
verification:
- kind: other
ref: "REQ-OK Pruefkette aus 18-06-PLAN.md <verify> (5 DESK-Eintraege, 5 Traceability-Zeilen)"
status: pass
human_judgment: false
- id: D7
description: "Gesamtlaeufe am Phasenende: API- und Web-Testsuite, beide Typpruefungen, cargo check fuer den Desktop-Client"
requirement: "DESK-05"
verification:
- kind: integration
ref: "pnpm --filter @tessera/api exec vitest run (68 Dateien, 1086 Tests, 0 fehlgeschlagen)"
status: pass
- kind: integration
ref: "pnpm --filter @tessera/web exec vitest run (55 Dateien, 365 Tests, 0 fehlgeschlagen)"
status: pass
- kind: other
ref: "pnpm --filter @tessera/api type-check / pnpm --filter @tessera/web type-check (beide fehlerfrei)"
status: pass
- kind: other
ref: "cargo check (apps/desktop/src-tauri) -> Finished"
status: pass
human_judgment: false
- id: D8
description: "Bedienprobe des Nutzers auf einem Windows-PC: Download, Installation mit SmartScreen-Anleitung, Erststart mit Server-Adresse, Anmeldung im App-Fenster, Infobereich/Tray-Verhalten, Autostart-Umschaltung, Beenden, Neustart mit gemerkter Adresse, Einstellungsseite Desktop-App"
requirement: "DESK-02"
verification: []
human_judgment: true
rationale: "Diese Ausfuehrungsumgebung hat keinen Windows-PC und keine grafische Sitzung — die Bedienprobe (Schritte 1-10 aus dem Plan-<verify>) kann nur der Nutzer selbst auf seinem PC durchfuehren. Dieser Plan liefert Dokumentation und alle automatisierbaren Gesamtlaeufe; die Bedienprobe ist unten unter 'Manuelle Abnahme (ausstehend)' als offener Schritt dokumentiert. DESK-03/04/05 bleiben in REQUIREMENTS.md deshalb bewusst auf Pending, bis das Ergebnis vorliegt."
duration: 21min
completed: 2026-09-16
status: complete
---
# Phase 18 Plan 06: Handbuecher, CHANGELOG, REQUIREMENTS und Gesamtlaeufe zum Abschluss der Desktop-Client-Phase Summary
**Anwender-, Betriebs- und Entwicklungshandbuch sowie das CI/CD-Runbook beschreiben jetzt vollstaendig die fertige Desktop-App (Download, SmartScreen-Installation, Pipeline-Job, Ablageort im Abbild, Release-Anhaenge, lokaler Bau); CHANGELOG und REQUIREMENTS sind nachgezogen; alle automatisierten Gesamtlaeufe (API 1086 Tests, Web 365 Tests, beide Typpruefungen, cargo check) sind gruen — die Windows-Bedienprobe des Nutzers steht noch aus.**
## Performance
- **Duration:** 21 min
- **Started:** 2026-09-16T15:04:00Z (geschaetzt, erster Lesevorgang der Referenzdateien)
- **Completed:** 2026-09-16T15:25:22Z
- **Tasks:** 3
- **Files modified:** 6
## Accomplishments
- `docs/anleitung-anwender.md`: neues Kapitel „Desktop-App" mit neun Unterabschnitten (Was es ist, Herunterladen, Installation unter Windows inkl. SmartScreen-Anleitung „Trotzdem ausführen", Installation unter Linux, Erster Start mit Server-Adresse, Fenster/Infobereich/Beenden, Automatischer Start, Neue Version, Wenn etwas nicht klappt) samt Inhaltsverzeichnis-Eintrag; nur Platzhalteradressen, keine Firmenadresse im Kapitel.
- `CHANGELOG.md`: neuer Stichpunkt „Desktop-App für Windows und Linux: Download auf der Anmeldeseite und unter Einstellungen → Desktop-App" unter „Unveröffentlicht" → „Neu" (D-17, Wortlaut exakt).
- `docs/anleitung-betrieb.md`: neues Kapitel 10 „Desktop-App: Pakete und Release-Dateien" (Pipeline-Herkunft, Ablageort `/app/desktop-dist` im API-Abbild samt `manifest.json`, Release-Anhaenge am Gitea-Release, `DESKTOP_DIST_DIR`-Tabelle, Fehlerbilder-Tabelle mit vier Symptomen); Kapitel 9 um einen Verweissatz ergaenzt.
- `docs/ci-cd-setup.md`: aus „drei" wurden „vier" Jobs, der Job `desktop` ist jetzt ausfuehrlich beschrieben (Systemabhaengigkeiten, Rust-Toolchain inkl. `clippy`-Komponente, Cache-Reihenfolge vor den Windows-Werkzeugen, Cross-Bau-Reihenfolge AppImage-vor-NSIS, Cache-Schluessel `desktop-dist-{sha}`, Begruendung Cache statt `upload-artifact`); drei neue Fehlerbehebungs-Unterabschnitte („Job desktop schlaegt fehl" inkl. der in 18-05 real aufgetretenen fehlenden `clippy`-Komponente, „publish: cache miss", „Release-Upload 413").
- `docs/anleitung-entwicklung.md`: `apps/desktop` wird nicht mehr als Grundgeruest beschrieben, sondern als fertiger Tauri-Client; neuer Abschnitt „Desktop-App lokal bauen" (Voraussetzungen, `desktop-version.sh`, lokaler AppImage-Bau, `desktop-collect.sh`, Hinweis: Windows-Installer nur im CI); Testabschnitt um `pnpm --filter @tessera/api exec vitest run src/desktop` und `cargo check`/`cargo clippy` ergaenzt.
- `.planning/REQUIREMENTS.md`: neuer Abschnitt „Phase 18 — Desktop-Client fertigstellen" mit DESK-01..05 (DESK-01/02 aus Phase 6 fortgefuehrt und als Complete markiert, DESK-03..05 neu und auf Pending, bis die Bedienprobe vorliegt), fuenf Traceability-Zeilen, aktualisierter Coverage-Satz.
- Gesamtlaeufe am Ende der Phase: `pnpm --filter @tessera/api exec vitest run` — 68 Dateien, **1086 Tests, alle gruen**; `pnpm --filter @tessera/web exec vitest run` — 55 Dateien, **365 Tests, alle gruen**; `pnpm --filter @tessera/api type-check` und `pnpm --filter @tessera/web type-check` — beide fehlerfrei; `cargo check` in `apps/desktop/src-tauri` — `Finished`.
## Task Commits
Each task was committed atomically:
1. **Task 1: Anwenderhandbuch — Kapitel "Desktop-App"; CHANGELOG-Stichpunkt** - `43c7061` (docs)
2. **Task 2: Betriebshandbuch Kapitel 10, CI/CD-Runbook, Entwicklungshandbuch** - `8f2069b` (docs)
3. **Task 3: REQUIREMENTS nachziehen, Gesamtlaeufe, Bedienprobe des Nutzers** - `29219ba` (docs)
**Plan metadata:** commit pending (this SUMMARY + STATE.md/ROADMAP.md)
## Files Created/Modified
- `docs/anleitung-anwender.md` - Kapitel „Desktop-App" (9 Unterabschnitte), TOC-Eintrag
- `docs/anleitung-betrieb.md` - Kapitel 10, TOC-Eintrag, Verweissatz in Kapitel 9
- `docs/anleitung-entwicklung.md` - Monorepo-Beschreibung aktualisiert, Abschnitt „Desktop-App lokal bauen", Testabschnitt ergaenzt
- `docs/ci-cd-setup.md` - Job `desktop` beschrieben, drei neue Fehlerbehebungs-Unterabschnitte
- `CHANGELOG.md` - Stichpunkt unter Unveröffentlicht/Neu
- `.planning/REQUIREMENTS.md` - Abschnitt DESK-01..05, Traceability-Zeilen, Coverage-Satz
## Decisions Made
- DESK-03/04/05 bleiben in REQUIREMENTS.md auf Pending, bis die Bedienprobe des Nutzers (Windows-Installation, siehe unten) tatsaechlich stattgefunden hat — dieser Plan konnte nur die Dokumentation und die automatisierten Gesamtlaeufe liefern, nicht die grafische Bedienprobe (kein Windows-PC/keine grafische Sitzung in dieser Ausfuehrungsumgebung).
- In Kapitel 9 des Betriebshandbuchs einen Verweissatz auf das neue Kapitel 10 ergaenzt, statt Kapitel 10 isoliert am Dateiende stehen zu lassen — der Freigabe-Ablauf und die Desktop-Release-Anhaenge gehoeren inhaltlich zusammen.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## Manuelle Abnahme (ausstehend)
Die folgende Bedienprobe konnte in dieser Ausfuehrungsumgebung nicht durchgefuehrt werden (kein Windows-PC, keine grafische Sitzung) und steht noch aus. Voraussetzung: der Testserver laeuft auf dem Beta-Stand mit den Paketen (Lauf 367, Commit `742fb5c` — `Tessera-Setup-1.1.0-beta.742fb5c.exe`, `Tessera-1.1.0-beta.742fb5c.AppImage`); `docker compose pull` und `docker compose up -d --force-recreate` fuehrt der Nutzer dort selbst aus.
1. Anmeldeseite des Testservers im Browser oeffnen: Unter dem Formular steht „Desktop-App herunterladen (Windows)", daneben „Linux-Version", darunter „Version 1.1.0".
2. Auf den Windows-Link klicken: Es laedt `Tessera-Setup-1.1.0-beta.{kennung}.exe` (wenige MB).
3. Datei ausfuehren. Windows zeigt die SmartScreen-Warnung: „Weitere Informationen" → „Trotzdem ausführen". Die Installation laeuft ohne weitere Fragen durch; Tessera startet (sonst ueber das Startmenue).
4. Erststart-Seite: dunkle Karte mit Tessera-Zeichen und gelbem Schriftzug, Feld „Adresse Ihres Tessera-Servers". Adresse des Testservers eintragen (`https://…`), „Verbinden": kurz „Tessera 1.1.0 gefunden – Verbindung wird hergestellt …", dann erscheint die Tessera-Anmeldung **im App-Fenster**.
5. Anmelden. Fenster mit X schliessen: Die App bleibt im Infobereich (Symbol mit Tessera-Zeichen). Linksklick auf das Symbol: Fenster ist wieder da.
6. Rechtsklick auf das Symbol: Menue „Öffnen", „Update herunterladen" (ausgegraut, weil die aktuelle Version installiert ist), „Mit Windows starten" (ohne Haken), „Beenden" — mit Umlauten.
7. „Mit Windows starten" anklicken: Haken erscheint; erneut anklicken: Haken verschwindet.
8. „Beenden": App ist weg (auch aus dem Infobereich).
9. App erneut starten: Sie geht **direkt** zu Tessera (Adresse gemerkt), Fenstergroesse und -position wie beim Beenden.
10. In der App: Einstellungen → Allgemein → „Desktop-App": Seite mit „Aktuelle Version: 1.1.0", „Beta-Ausgabe, Stand {kennung}", zwei gelbe Knoepfe „Für Windows herunterladen" / „Für Linux herunterladen", darunter Dateiname und Groesse (z. B. „… · 101,5 MB" fuer Linux), und vier Saetze Erklaerung.
11. Falls ein Linux-Rechner greifbar ist: AppImage herunterladen, ausfuehrbar machen, starten — Erststart-Seite wie unter 4.
**Nach erfolgreicher Bedienprobe:** DESK-03/04/05 in `.planning/REQUIREMENTS.md` (Requirement-Liste und Traceability-Tabelle) auf Complete setzen.
### Auf den naechsten Freigabe-Tag vertagt (nicht Teil dieser Phase)
Zwei Punkte lassen sich erst beim naechsten Freigabe-Tag pruefen und gehoeren in die Abnahme dieser Version, nicht in diese Phase:
- **(a) Update-Hinweis:** Nach dem Tag `v1.2.0` zeigt der installierte 1.1.0-Client beim Start die Benachrichtigung „Neue Version 1.2.0 verfügbar …", und der Menueeintrag heisst „Version 1.2.0 herunterladen" und oeffnet die Seite Desktop-App im Browser.
- **(b) Release-Anhang:** Der Gitea-Release `v1.2.0` traegt `Tessera-Setup-1.2.0.exe` und `Tessera-1.2.0.AppImage` als Dateien.
## User Setup Required
Keine externe Dienstkonfiguration noetig. Die Bedienprobe oben ist keine Konfigurationsaufgabe, sondern eine manuelle Verifikation durch den Nutzer.
## Next Phase Readiness
- Die Desktop-Client-Phase ist inhaltlich und dokumentarisch abgeschlossen: alle sechs Plaene (18-01 bis 18-06) sind erledigt, alle automatisierten Gesamtlaeufe sind gruen.
- Offen bleibt ausschliesslich die manuelle Bedienprobe des Nutzers auf einem Windows-PC (siehe „Manuelle Abnahme (ausstehend)" oben) sowie die zwei auf den naechsten Freigabe-Tag vertagten Punkte (Update-Hinweis, Release-Anhang).
- Kein technischer Blocker fuer die naechste Phase oder fuer eine Freigabe — die Bedienprobe ist eine reine Abnahmehandlung, keine offene Implementierungsarbeit.
---
*Phase: 18-desktop-client-fertigstellen*
*Completed: 2026-09-16*
## Self-Check: PASSED
All modified/created files verified on disk (`docs/anleitung-anwender.md`, `docs/anleitung-betrieb.md`, `docs/anleitung-entwicklung.md`, `docs/ci-cd-setup.md`, `CHANGELOG.md`, `.planning/REQUIREMENTS.md`, this SUMMARY). All three task commits found in `git log` (`43c7061`, `8f2069b`, `29219ba`). All plan-level `<verify>` items re-run and passing: `DOCS1-OK` (Anwenderhandbuch Kapitel/TOC/9 Unterabschnitte/Pflichtbeschriftungen/0 Firmenadressen, CHANGELOG-Position), `DOCS2-OK` (Betriebshandbuch Kapitel 10/TOC/Variable/Ablageort/Fehlerbilder, CI/CD-Runbook vier Jobs/cargo-xwin/Cache-Schluessel/upload-artifact-Begruendung, Entwicklungshandbuch kein Grundgeruest mehr/neuer Abschnitt/Testzeile), `REQ-OK` (5 DESK-Eintraege, 5 Traceability-Zeilen). `ALL-GREEN` bestaetigt: API 1086/1086, Web 365/365, beide Typpruefungen fehlerfrei, `cargo check` `Finished`. Die Bedienprobe (Windows-PC) ist laut Plan-Checkpoint-Protokoll nicht Teil dieses automatisierten Selbst-Checks — siehe „Manuelle Abnahme (ausstehend)" oben.
@@ -0,0 +1,85 @@
---
phase: 18-desktop-client-fertigstellen
fixed_at: 2026-09-16T17:40:00Z
review_path: .planning/phases/18-desktop-client-fertigstellen/18-REVIEW.md
iteration: 1
findings_in_scope: 4
fixed: 4
skipped: 2
status: all_fixed
verification_env: main checkout (workflow.use_worktrees=false, no isolated worktree used)
---
# Phase 18: Code Review Fix Report
**Fixed at:** 2026-09-16T17:40:00Z
**Source review:** `.planning/phases/18-desktop-client-fertigstellen/18-REVIEW.md`
**Iteration:** 1
**Summary:**
- Findings in scope (Critical + Warning): 4
- Fixed: 4
- Skipped (Info, out of scope by instruction): 2
Verification ran directly in the main checkout at `/home/vicolab/projects/tessera-ctl` — `.planning/config.json` has `workflow.use_worktrees: false`, so no isolated git worktree was created for this run; per the fixer's setup rules this is the documented, safe opt-out path.
## Fixed Issues
### CR-01: Path-traversal defense-in-depth regex accepts dot-only filenames
**Files modified:** `apps/api/src/desktop/desktop.service.ts`, `apps/api/src/desktop/desktop.service.spec.ts`
**Commit:** `0d5c80f`
**Applied fix:** In `getPackage()` step (4), `entry.name === '.'` and `entry.name === '..'` are now rejected explicitly (the character-class regex alone accepted them since `.` and `-` are both allowed characters). Additionally, the resolved absolute path is now checked to still start with the resolved `desktopDistDir` before any filesystem access, as a second, independent layer of defense against future variants of this pattern if the character whitelist is ever reused elsewhere. Added `desktop.service.spec.ts` Test 7a (`entry.name: '..'` → 404), Test 7b (`entry.name: '.'` → 404), and Test 7c (`entry.name: '../manifest.json'` → 404, matching the exact case named in the fix task).
**Verification:** Tier 1 (re-read, clean) + Tier 2 (`vitest run src/desktop`: 13/13 passing; `tsc --noEmit`: clean).
### WR-01: Tauri CSP grants `'unsafe-eval'` and wildcard sources that are never needed
**File modified:** `apps/desktop/src-tauri/tauri.conf.json`
**Commit:** `1b2f803`
**Applied fix:** Tightened `app.security.csp` from `default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; img-src * data:; font-src * data:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval'` to `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'` — exactly the value REVIEW.md suggested. Confirmed by reading `setup.html`: it uses no `eval()`, no remote fonts/images/styles, and talks to the Rust side only via `window.__TAURI__.core.invoke` (IPC bridge, not `fetch()`). Confirmed via Tauri knowledge that `app.security.csp` is injected only into responses served by the app's own asset protocol (the bundled frontend, i.e. `setup.html`) — the `window.navigate()` call in `save_server_url()` that follows loads the user's configured server fresh, governed by that server's own response headers, not by this config, so tightening `connect-src` here does not affect the subsequently loaded remote page.
**Verification:** Tier 1 (re-read, clean) + Tier 2 (`node -e "JSON.parse(...)"`: valid JSON).
### WR-02: Desktop update check ignores commit/channel — beta users between tags never see "update available"
**Files modified:** `apps/desktop/src-tauri/build.rs`, `apps/desktop/src-tauri/src/lib.rs`
**Commit:** `579e24b`
**Applied fix:** `build.rs` now runs `git rev-parse --short=7 HEAD` at compile time and embeds the result as `APP_COMMIT` via `cargo:rustc-env` (falls back to an empty string if `git` is unavailable, e.g. a source tarball without `.git`). This mirrors exactly the format `desktop-collect.sh` already writes into `manifest.json`'s `commit` field. `lib.rs`'s `DesktopLatest` struct now also deserializes `channel` and `commit` (both already present in every `/desktop/latest` response per `DesktopLatestResponse`); the update-available check is now `info.version != app_version || (info.channel == "beta" && info.commit != app_commit)`, so beta clients see the notice for a newer commit on the same tag-derived version, while the live channel keeps the plain version comparison. D-07 (plain `X.Y.Z` in `tauri.conf.json`/`Cargo.toml`, unaffected by this change) is untouched — `desktop-version.sh` was not modified.
**Verification:** Tier 1 (re-read, clean) + Tier 2 (`cargo check`: clean; `cargo clippy -- -D warnings`: clean, forced re-run via `touch src/lib.rs`).
**Note:** This is a logic-level fix to an async comparison with no existing Rust unit tests in this crate to exercise it automatically (only `cargo check`/`clippy`, which verify syntax/lints, not runtime behavior). Per the fixer's verification policy for logic findings, **this one requires human/manual verification** before relying on it — e.g. building a beta package, bumping only the commit (not the tag-derived version), and confirming the tray notice now appears. Flagged in `18-REVIEW.md`.
### WR-03: `getManifest()` shallow-validates `files`; malformed entries fall through to string-coerced lookups
**Files modified:** `apps/api/src/desktop/desktop.service.ts`, `apps/api/src/desktop/desktop.service.spec.ts`
**Commit:** `a8964f1`
**Applied fix:** `getManifest()`'s top-level shape check now also rejects `files` being an array (`Array.isArray(parsed.files)`, since `typeof [] === 'object'` previously slipped through). A new `isValidManifestFileEntry()` helper validates each present platform entry has `name: string`, `size: number`, and `sha256: string` matching a 64-character hex pattern (`/^[a-f0-9]{64}$/i`) — stricter than REVIEW.md's minimum suggestion (which only asked for the three `typeof` checks), matching the fix-task's explicit scope instruction to also validate the sha256 hex format. A malformed entry makes the whole manifest treated as missing (`null` return, same 404 path, plus a `logger.warn`), matching the docstring's stated guarantee that a bad manifest shape means 404. Added Test 9 (manifest with `name` missing on the `linux` entry → `getLatest()` throws `NotFoundException` instead of proceeding to a stringified-`undefined` lookup) and Test 10 (`sha256: 'not-a-hash'` → download 404).
**Verification:** Tier 1 (re-read, clean) + Tier 2 (`vitest run src/desktop`: 13/13 passing; `tsc --noEmit`: clean).
## Skipped Issues
### IN-01: Redundant/duplicated version-format validation in `desktop-collect.sh`
**File:** `.gitea/scripts/desktop-collect.sh:66-77`
**Reason:** Info-severity finding, explicitly out of scope for this fix run per the fix task's scope instruction ("Skip the two Info findings (document as skipped)"). No code change made.
**Original issue:** The `case` glob pattern is immediately followed by a strict `grep -qE` doing the actual validation; the glob branch adds no protection the grep doesn't already provide.
### IN-02: `apps/desktop/src-tauri/capabilities/default.json` grants `opener:allow-open-url` for any http/https URL
**File:** `apps/desktop/src-tauri/capabilities/default.json:17`
**Reason:** Info-severity finding, explicitly out of scope for this fix run. The finding itself also states "no action required unless the opener use expands to cover more than the update link" — not a code change candidate even under broader scope.
**Original issue:** Capability allows opening any http/https URL in the system browser; currently only used for the tray "Update herunterladen" link to the user-configured server, consistent with D-02 (no fixed built-in server) and not expressible more tightly in the static capability file.
## Gate Results
| Gate | Result |
|------|--------|
| `pnpm --filter @tessera/api exec vitest run src/desktop` | 13/13 passed |
| `pnpm --filter @tessera/api type-check` | clean (`tsc --noEmit`, no errors) |
| `cargo check` (`apps/desktop/src-tauri`) | clean |
| `cargo clippy -- -D warnings` (`apps/desktop/src-tauri`) | clean, no warnings |
| `pnpm --filter @tessera/web exec vitest run` | not run — no web files touched by any of the four fixes |
---
_Fixed: 2026-09-16T17:40:00Z_
_Fixer: Claude (gsd-code-fixer)_
_Iteration: 1_
@@ -0,0 +1,178 @@
---
phase: 18-desktop-client-fertigstellen
reviewed: 2026-09-16T00:00:00Z
depth: standard
files_reviewed: 23
files_reviewed_list:
- apps/api/src/desktop/desktop.controller.ts
- apps/api/src/desktop/desktop.service.ts
- apps/api/src/desktop/desktop.module.ts
- apps/api/src/desktop/desktop.service.spec.ts
- apps/api/src/app.module.ts
- apps/api/Dockerfile
- apps/desktop/src-tauri/src/lib.rs
- apps/desktop/src/setup.html
- apps/desktop/src-tauri/capabilities/default.json
- apps/desktop/src-tauri/tauri.conf.json
- apps/desktop/src-tauri/Cargo.toml
- apps/web/src/lib/desktop.ts
- apps/web/src/lib/desktop.test.ts
- apps/web/src/components/desktop/desktop-download-links.tsx
- apps/web/src/components/settings/desktop-app-settings.tsx
- apps/web/src/components/settings/settings-sidebar.tsx
- apps/web/src/app/(auth)/login/page.tsx
- apps/web/src/app/(portal)/settings/general/desktop/page.tsx
- .gitea/workflows/ci.yml
- .gitea/scripts/desktop-collect.sh
- .gitea/scripts/desktop-version.sh
- .gitea/scripts/publish-images.sh
- .gitea/scripts/publish-release.sh
findings:
critical: 1
warning: 3
info: 2
total: 6
status: clean
fixed_at: 2026-09-16T17:40:00Z
fix_report: 18-REVIEW-FIX.md
---
# Phase 18: Code Review Report
**Reviewed:** 2026-09-16T00:00:00Z
**Depth:** standard
**Files Reviewed:** 23
**Status:** clean (all Critical/Warning findings fixed — see `18-REVIEW-FIX.md`)
## Summary
Reviewed the desktop-client-fertigstellen phase: the new `apps/api/src/desktop/` module (public `latest`/`download` routes), the Tauri client's server-address setup flow (`check_server`/`save_server_url`, tray/update UI in `lib.rs`), the web download surfaces (login page link, Settings → Allgemein → Desktop-App), and the CI/CD pipeline that builds, collects, and publishes the desktop packages (`ci.yml`, `desktop-collect.sh`, `desktop-version.sh`, `publish-images.sh`, `publish-release.sh`).
Overall the phase is careful about the things it calls out as security-sensitive: the CI scripts build all JSON with `jq -n`/`--arg` (no manual string concatenation), the Gitea release token is only ever passed to curl via a header file (never on the command line or in a URL), temp files holding the token are created under a `umask 077` directory, and the HTTP-facing platform parameter on `GET /desktop/download/:platform` is whitelisted before any filesystem access (verified against real path-traversal-style HTTP requests in `desktop.service.spec.ts` Test 4). The Tauri capability/CSP surface and version-check flow largely match the locked decisions in `18-CONTEXT.md` (D-08 no network dependency, D-09 no signing).
One genuine gap was found in the second-layer defense against a tampered `manifest.json` (`desktop.service.ts`, D-10/T-18-02): the "defense in depth" filename regex does not reject filenames composed only of dots, so an entry name of `".."` passes the check and `path.join()`s outside `desktop-dist/`. This is not reachable from the public HTTP request today (the manifest is CI-written, not request-controlled), but it is precisely the case the code's own comment says this check exists to block, and it should be fixed to actually do so, especially since the same guard pattern may get reused elsewhere. Three warnings and two info items round out the rest of the findings — none of them break the stated D-10/D-08/D-09 decisions on their own, but they're worth cleaning up.
## Critical Issues
### CR-01: Path-traversal defense-in-depth regex accepts dot-only filenames
**File:** `apps/api/src/desktop/desktop.service.ts:111`
**Issue:** Step (4) is documented as "Verteidigung in der Tiefe (T-18-02): auch ein manipuliertes Manifest darf nicht aus dem Ordner hinausfuehren" — the whole point is that even if `manifest.json`'s `files[platform].name` were corrupted/attacker-influenced, the regex should stop it from resolving outside `desktopDistDir`. The regex used is:
```ts
if (!/^[A-Za-z0-9._-]+$/.test(entry.name)) {
throw new NotFoundException(`No package for platform: ${knownPlatform}`);
}
```
`.` and `-` are both allowed characters, so a name of exactly `".."` (or `"."`, `"..."`, etc.) passes this test — it contains only characters from the allowed class. Verified directly:
```
node -e "console.log(/^[A-Za-z0-9._-]+$/.test('..'))" // true
node -e "console.log(require('path').join('/app/desktop-dist','..'))" // '/app'
```
With `entry.name === '..'`, `path.join(this.desktopDistDir, entry.name)` resolves to the *parent* of `desktop-dist/` (e.g. `/app` in the container image). `fs.existsSync('/app')` is `true` (it's a directory), so the code proceeds to `fs.createReadStream('/app')`, which will emit an `EISDIR` stream error rather than serving a file — not a full data-exfiltration primitive by itself, but it is a real escape of the intended containment boundary, defeats the explicitly-documented guarantee, and produces an unhandled stream-error path (headers already sent) instead of the intended 404. The unit test suite (`desktop.service.spec.ts` Test 7) only exercises `"../x.AppImage"` (rejected because of the `/`), not a bare `".."`/`"."`, so this gap has no test coverage either.
Current exploitability requires `manifest.json` itself to be corrupted or attacker-controlled (today it is written exclusively by `desktop-collect.sh` in CI), so the live attack surface is currently narrow — but the code and the phase's own decision record (D-10, T-18-02) both frame this exact line as the safety net for that scenario, and it doesn't hold.
**Fix:** Don't rely on a character whitelist alone; verify the resolved path is still inside `desktopDistDir`, and/or explicitly reject `.`/`..` segments:
```ts
if (
!/^[A-Za-z0-9._-]+$/.test(entry.name) ||
entry.name === '.' ||
entry.name === '..'
) {
throw new NotFoundException(`No package for platform: ${knownPlatform}`);
}
const filePath = path.join(this.desktopDistDir, entry.name);
const resolvedRoot = path.resolve(this.desktopDistDir) + path.sep;
if (!path.resolve(filePath).startsWith(resolvedRoot)) {
throw new NotFoundException(`No package for platform: ${knownPlatform}`);
}
```
Add a unit test asserting `entry.name: '..'` and `entry.name: '.'` in the manifest both yield 404 (mirroring the existing Test 7 for `"../x.AppImage"`).
**Status:** fixed — commit `0d5c80f`. `.`/`..` are now rejected explicitly and the resolved path is additionally checked against `desktopDistDir`. Added Test 7a/7b/7c (`..`, `.`, `../manifest.json`) to `desktop.service.spec.ts`. See `18-REVIEW-FIX.md`.
## Warnings
### WR-01: Tauri CSP grants `'unsafe-eval'` and wildcard sources that are never needed
**File:** `apps/desktop/src-tauri/tauri.conf.json:24`
**Issue:** `app.security.csp` is:
```json
"default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; img-src * data:; font-src * data:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
```
This CSP applies to the app's own bundled page (`apps/desktop/src/setup.html`) — the only local page the app serves. That page is a static, inline `<style>`/`<script type="module">` document that calls `eval()` nowhere, loads no remote fonts/images/styles, and only talks to the Tauri IPC bridge (`window.__TAURI__.core.invoke`). Granting `'unsafe-eval'` and wildcard `connect-src`/`img-src`/`font-src`/`style-src` removes CSP's protection against script injection (e.g. via a future dependency compromise or a bug that echoes untrusted content into the DOM) for no functional benefit — none of the permissive directives are exercised by the current page.
**Fix:** Tighten to what `setup.html` actually needs, e.g.:
```json
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"
```
`connect-src` doesn't need to allow the user-entered server address here because `check_server`/`save_server_url` go through Rust (`reqwest`, `window.navigate`), not `fetch()` from the page itself. If a concrete need for `'unsafe-eval'` or a wildcard source turns up later, add only that directive with a comment explaining why.
**Status:** fixed — commit `1b2f803`. CSP tightened to exactly the suggested value (`default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'`). Confirmed via Tauri knowledge that this CSP applies only to pages served by the app's own asset protocol (the bundled `setup.html`) — the subsequent `window.navigate()` to the user's server loads a fresh page governed by that server's own headers, not this config. See `18-REVIEW-FIX.md`.
### WR-02: Desktop update check ignores commit/channel — beta users between tags never see "update available"
**File:** `apps/desktop/src-tauri/src/lib.rs:17-20, 199-215`
**Issue:** The update check compares only the numeric semantic version:
```rust
#[derive(serde::Deserialize)]
struct DesktopLatest {
version: String,
}
...
if info.version != app_version {
```
`desktop-version.sh` (D-07) sets both `tauri.conf.json`'s `version` and `Cargo.toml`'s `version` from the *last reachable release tag*, not a fresh per-build number — so on `main` (beta channel), every commit between two tags produces a new build/beta package (`Tessera-X.Y.Z-beta.<sha>.AppImage`) whose `env!("CARGO_PKG_VERSION")` and whose freshly-published `manifest.json.version` are numerically identical (both `X.Y.Z` from the same last tag). A user running an older beta build from three commits ago will never be notified that a newer beta package exists, because the only field compared (`version`) hasn't changed — even though `commit`/`channel` in the JSON response did. This directly undermines D-13's stated purpose ("bei abweichender Version Benachrichtigung 'Neue Version X.Y.Z verfuegbar'") for anyone tracking the beta channel between tags.
**Fix:** Either compare `commit` as well when `channel == "beta"`, or accept this as an intentional scope limit (only tagged releases trigger the notice) and document it explicitly in `docs/anleitung-anwender.md`/`anleitung-betrieb.md` so it isn't mistaken for a bug later. If fixed in code:
```rust
#[derive(serde::Deserialize)]
struct DesktopLatest {
version: String,
channel: String,
commit: String,
}
...
let app_commit = option_env!("APP_COMMIT").unwrap_or("");
let is_newer = info.version != app_version || (info.channel == "beta" && info.commit != app_commit);
```
(requires threading a build-time commit stamp into the desktop binary, which doesn't currently exist — flagging as a design gap either way.)
**Status:** fixed, requires human verification — commit `579e24b`. `build.rs` now embeds `APP_COMMIT` at compile time via `git rev-parse --short=7 HEAD` (same format `desktop-collect.sh` writes to `manifest.json`); `lib.rs` compares `commit` in addition to `version` when `channel == "beta"`, plain version comparison for `live`. D-07 (plain X.Y.Z in `tauri.conf.json`/`Cargo.toml`) untouched. `cargo check` and `cargo clippy -- -D warnings` are clean. No Rust unit tests exist in this crate to exercise the comparison logic automatically (flagged per the fixer's logic-bug verification policy) — recommend a manual check of a beta build before/after a no-version commit to confirm the notice now appears. See `18-REVIEW-FIX.md`.
### WR-03: `getManifest()` shallow-validates `files`; malformed entries fall through to string-coerced lookups
**File:** `apps/api/src/desktop/desktop.service.ts:48, 104-113`
**Issue:** The manifest shape check only verifies `typeof parsed.files === 'object' && parsed.files !== null`, which also accepts an array (`typeof [] === 'object'`). Separately, individual file entries (`manifest.files[platform]`) are never checked for having the required `name`/`size`/`sha256` string/number fields before being used — e.g. if `entry.name` were `undefined` (malformed manifest), `/^[A-Za-z0-9._-]+$/.test(undefined)` coerces to the string `"undefined"`, which matches the regex and proceeds to look for a literal file called `undefined` in `desktop-dist/`. This doesn't currently produce an exploitable outcome (ends in 404), but it's a symptom of `getManifest()` trusting more of the JSON shape than its own docstring claims ("die Grundform nicht stimmt ... 404"), and it means a broken manifest doesn't fail loudly/clearly for whoever is debugging a bad CI run.
**Fix:** Validate each present platform entry has `typeof entry.name === 'string' && typeof entry.size === 'number' && typeof entry.sha256 === 'string'` inside `getManifest()`, logging and returning `null` (same pattern already used for the top-level shape check) if not.
**Status:** fixed — commit `a8964f1`. `getManifest()` now also rejects `files` being an array, and validates each present platform entry (`name`: string, `size`: number, `sha256`: 64-char hex string) via a new `isValidManifestFileEntry()` helper; a malformed entry makes the whole manifest treated as missing (404 + warn log), matching the docstring's stated guarantee. Added Test 9 (missing `name`) and Test 10 (non-hex `sha256`) to `desktop.service.spec.ts`. See `18-REVIEW-FIX.md`.
## Info
### IN-01: Redundant/duplicated version-format validation in `desktop-collect.sh`
**File:** `.gitea/scripts/desktop-collect.sh:66-77`
**Issue:** The `case` pattern (`[0-9]*.[0-9]*.[0-9]*`) is a loose glob check that's immediately followed by a strict `grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'` doing the actual validation — the outer `case` only decides whether to run the `grep`, but the `*)` fallback arm duplicates the exact same error message and exit. The glob branch adds no protection the grep doesn't already provide on its own.
**Fix:** Collapse to a single check:
```sh
if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Version '$VERSION' aus $TAURI_DIR/tauri.conf.json ist nicht rein numerisch (X.Y.Z)." >&2
exit 1
fi
```
**Status:** skipped — Info item, out of scope for this fix run (scope limited to the Critical and Warning findings). No code change made.
### IN-02: `apps/desktop/src-tauri/capabilities/default.json` grants `opener:allow-open-url` for any http/https URL
**File:** `apps/desktop/src-tauri/capabilities/default.json:17`
**Issue:** `{ "identifier": "opener:allow-open-url", "allow": [{ "url": "https://*" }, { "url": "http://*" }] }` lets the Rust side open *any* http/https URL in the system browser — currently only used for the tray "Update herunterladen" item, which opens `{stored server}/settings/general/desktop`. Since the stored server address is arbitrary user input (by design, D-02: no fixed built-in server), this is consistent with the product's multi-tenant intent and isn't a capability-scoping bug per se, but it's broader than strictly necessary (a same-origin-as-configured-server restriction isn't expressible in the static capability file, so this is effectively as tight as it can be made without runtime scoping). Noting for awareness only — no action required unless the opener use expands to cover more than the update link.
**Status:** skipped — Info item, out of scope for this fix run (scope limited to the Critical and Warning findings). No action required per the finding itself.
---
_Reviewed: 2026-09-16T00:00:00Z_
_Reviewer: Claude (gsd-code-reviewer)_
_Depth: standard_
@@ -0,0 +1,51 @@
---
status: testing
phase: 18-desktop-client-fertigstellen
source: [18-VERIFICATION.md]
started: 2026-09-16T18:15:00Z
updated: 2026-09-16T18:15:00Z
---
## Current Test
number: 1
name: Windows-Bedienprobe (Download, Installation, Erststart, Tray, Einstellungsseite)
expected: |
Beta-Server auf dem Stand mit den Paketen aus dem CI-Lauf nach Push von `72e488e` (oder neuer).
1. Anmeldeseite zeigt unter dem Formular "Desktop-App herunterladen (Windows)" mit Versionsangabe.
2. Klick laedt `Tessera-Setup-1.1.0-beta.<commit>.exe` (ca. 2,7 MB).
3. Installation unter Windows: SmartScreen-Hinweis erscheint ("Weitere Informationen" -> "Trotzdem ausfuehren"), danach installiert der Installer ohne weitere Nachfrage.
4. Erststart: Fenster in Tessera-Gestalt fragt nach der Server-Adresse; falsche Adresse -> Fehlermeldung; richtige Adresse -> Tessera-Anmeldung im App-Fenster.
5. Anmeldung funktioniert, Dashboard erscheint im App-Fenster.
6. Fenster schliessen (X) -> App bleibt im Infobereich (Tray). Rechtsklick auf das Symbol: "Oeffnen", "Update herunterladen" (ggf. ausgegraut/fehlend ohne neue Version), "Mit Windows starten" (Haken), "Beenden".
7. "Mit Windows starten" anhaken -> nach Ab-/Anmelden von Windows startet Tessera im Tray.
8. "Beenden" beendet die App vollstaendig.
9. Neustart der App: Server-Adresse ist gemerkt, direkt Anmeldung/Dashboard.
10. Einstellungen -> Allgemein -> Desktop-App zeigt Version, beide Download-Knoepfe mit Dateigroesse und den Erklaerungstext.
11. (optional, Linux) `Tessera-1.1.0-beta.<commit>.AppImage` ausfuehrbar machen und starten -> gleiche Erststart-Seite.
awaiting: user response
## Tests
### 1. Windows-Bedienprobe (Download, Installation, Erststart, Tray, Einstellungsseite)
expected: siehe oben (Schritte 1-11)
result: [pending]
### 2. Release-Anhang am naechsten Freigabe-Tag
expected: Nach dem naechsten Tag `vX.Y.Z` traegt der Gitea-Release `Tessera-Setup-X.Y.Z.exe` und `Tessera-X.Y.Z.AppImage` als Anhaenge (herunterladbar, Groesse > 0). Pruefbar erst bei der naechsten Freigabe (z. B. 1.2.0).
result: [pending]
### 3. Update-Hinweis bei neuerer Client-Version
expected: Ein aelterer installierter Client zeigt nach dem Start die Benachrichtigung "Neue Version X.Y.Z verfuegbar" und im Tray den Eintrag "Update herunterladen", der die Seite Einstellungen -> Desktop-App im Browser oeffnet. Auf der Beta reicht dafuer ein neuerer Commit (gleiche Versionsnummer, anderer Commit-Stempel); auf Live der naechste Freigabe-Tag.
result: [pending]
## Summary
total: 3
passed: 0
issues: 0
pending: 3
skipped: 0
blocked: 0
## Gaps
@@ -0,0 +1,149 @@
---
phase: 18-desktop-client-fertigstellen
verified: 2026-09-16T17:50:00Z
status: human_needed
score: 8/10 must-haves verified
covered_files: [".gitea/scripts/desktop-collect.sh", ".gitea/scripts/desktop-version.sh", ".gitea/scripts/publish-images.sh", ".gitea/scripts/publish-release.sh", ".gitea/workflows/ci.yml", ".planning/REQUIREMENTS.md", ".planning/phases/18-desktop-client-fertigstellen/18-01-PLAN.md", ".planning/phases/18-desktop-client-fertigstellen/18-01-SUMMARY.md", ".planning/phases/18-desktop-client-fertigstellen/18-02-PLAN.md", ".planning/phases/18-desktop-client-fertigstellen/18-02-SUMMARY.md", ".planning/phases/18-desktop-client-fertigstellen/18-03-PLAN.md", ".planning/phases/18-desktop-client-fertigstellen/18-03-SUMMARY.md", ".planning/phases/18-desktop-client-fertigstellen/18-04-PLAN.md", ".planning/phases/18-desktop-client-fertigstellen/18-04-SUMMARY.md", ".planning/phases/18-desktop-client-fertigstellen/18-05-PLAN.md", ".planning/phases/18-desktop-client-fertigstellen/18-05-SUMMARY.md", ".planning/phases/18-desktop-client-fertigstellen/18-06-PLAN.md", ".planning/phases/18-desktop-client-fertigstellen/18-06-SUMMARY.md", ".planning/phases/18-desktop-client-fertigstellen/18-REVIEW-FIX.md", ".planning/phases/18-desktop-client-fertigstellen/18-REVIEW.md", "CHANGELOG.md", "apps/api/src/desktop/desktop.controller.ts", "apps/api/src/desktop/desktop.module.ts", "apps/api/src/desktop/desktop.service.ts", "apps/desktop/src-tauri/build.rs", "apps/desktop/src-tauri/src/lib.rs", "apps/desktop/src/setup.html", "apps/web/src/app/(portal)/settings/general/desktop/page.tsx", "apps/web/src/components/desktop/desktop-download-links.tsx", "apps/web/src/components/settings/desktop-app-settings.tsx", "apps/web/src/lib/desktop.ts", "docs/anleitung-anwender.md", "docs/anleitung-betrieb.md", "docs/anleitung-entwicklung.md", "docs/ci-cd-setup.md"]
covered_digest: "v1:sha256:d489cb5b094aba560b96f3d6ce7537d67fe83a2827906d554c4cccae996e091c"
behavior_unverified: 2
behavior_unverified_items:
- truth: "Ein Freigabe-Tag (v*) baut beide Pakete und haengt sie als Dateien an den Gitea-Release (D-01, D-04..D-08, ROADMAP-SC1 zweite Haelfte)."
test: "Naechsten Freigabe-Tag (z.B. v1.2.0) setzen und pushen; Job desktop + publish beobachten, danach den Gitea-Release des Tags oeffnen."
expected: "Der Release traegt Tessera-Setup-1.2.0.exe und Tessera-1.2.0.AppImage als Anhaenge (Groesse > 0, herunterladbar)."
why_human: "publish-release.sh laeuft laut Workflow-Bedingung nur bei einem echten v*-Tag-Push; Lauf 367 war ein main-Push (kein Tag), der Release-Schritt lief dort erwartungsgemaess nicht. Das Skript selbst ist per --dry-run und Unit-Ebene geprueft (18-02), aber der echte GET/DELETE/POST-Roundtrip gegen die Gitea-Release-API mit einer ~100 MB-Datei ist nur am echten Tag beobachtbar."
- truth: "Der installierte Client zeigt nach Adresseingabe die Tessera-Anmeldung, behaelt Tray/Schliessen-ins-Tray/Autostart aus Phase 6 bei und weist bei einer neueren Client-Version per Benachrichtigung + Tray-Link auf die neue Version hin (ROADMAP-SC3)."
test: "Bedienprobe aus 18-06-SUMMARY.md 'Manuelle Abnahme (ausstehend)', Schritte 1-11: Download vom Testserver, Windows-Installation inkl. SmartScreen, Erststart mit Server-Adresse, Anmeldung im App-Fenster, Tray-Verhalten (Oeffnen/Update/Autostart-Haken/Beenden), Neustart mit gemerkter Adresse, Einstellungsseite."
expected: "Alle 11 Schritte laufen wie in der Bedienprobe beschrieben; nach einem spaeteren Freigabe-Tag zeigt ein aelterer Client zusaetzlich die Update-Benachrichtigung mit Download-Link."
why_human: "Diese Ausfuehrungsumgebung ist kopflos (kein Windows-PC, kein Display/X11/Wayland). Alle unterstuetzenden Schichten sind automatisiert bewiesen (cargo check/clippy sauber, grep-Batterien fuer Tray-Text/Umlaute/Versionspruefungs-Code, WR-02-Fix fuer den Commit-Vergleich verifiziert per Code-Lesen), aber das tatsaechliche Rendering/Verhalten in einer grafischen Sitzung ist nicht pruefbar."
overrides_applied: 0
human_verification:
- test: "Naechster Freigabe-Tag: Release-Anhang pruefen (siehe behavior_unverified_items #1)"
expected: "Beide Dateien am Gitea-Release des Tags vorhanden"
why_human: "publish-release.sh laeuft nur bei Tag-Push; kein Tag in diesem Verifizierungslauf gesetzt"
- test: "Windows-Bedienprobe des Nutzers (siehe behavior_unverified_items #2, 18-06-SUMMARY.md Schritte 1-11)"
expected: "Installer laeuft, Erststart-Seite fuehrt zur Anmeldung, Tray/Autostart/Beenden funktionieren, Einstellungsseite zeigt Version/Knoepfe"
why_human: "Kein Windows-PC/keine grafische Sitzung in dieser Ausfuehrungsumgebung; DESK-03/04/05 bleiben laut REQUIREMENTS.md bewusst auf Pending bis diese Probe erfolgt ist"
- test: "Beta-Update-Hinweis zwischen zwei Tags (WR-02-Fix, commit-basierter Vergleich)"
expected: "Ein aelterer Beta-Client (gleiche X.Y.Z, aelterer Commit) zeigt nach einem neuen Beta-Build die Benachrichtigung"
why_human: "Keine Rust-Unit-Tests in diesem Crate fuer die Vergleichslogik; REVIEW-FIX.md flaggt dies explizit als 'requires human/manual verification'"
---
# Phase 18: Desktop-Client fertigstellen Verification Report
**Phase Goal:** Anwender koennen den Tessera-Desktop-Client als fertigen Windows-Installer (und Linux-AppImage) direkt aus Tessera herunterladen und installieren; die Pipeline baut die Pakete bei jedem Freigabe-Tag und haengt sie an das Gitea-Release; der Client traegt die Freigabe-Version, fragt die Server-Adresse weiterhin beim ersten Start ab und weist bei einer neueren Client-Version mit Download-Link hin.
**Verified:** 2026-09-16T17:50:00Z
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | `GET /desktop/latest` (200 mit Version/Kanal/Dateiliste, 404 ohne Manifest) und `GET /desktop/download/:platform` (attachment-Stream, Whitelist vor Dateisystemzugriff, 400 fuer unbekannte Plattform) sind oeffentlich (`@Public()`) | ✓ VERIFIED | `pnpm --filter @tessera/api exec vitest run src/desktop` — 13/13 gruen (inkl. CR-01/WR-03-Regressionstests); lokaler curl: `GET /desktop/latest` → 200 mit Manifest, `GET /desktop/download/linux` → 200 mit `Content-Disposition: attachment`, `GET /desktop/download/nonsense` → 400 |
| 2 | `desktop-collect.sh`/`desktop-version.sh` schreiben kanonische Dateinamen, `manifest.json` (Version/Kanal/Commit/Groesse/SHA-256) und reine `X.Y.Z`-Versionen aus dem Freigabe-Tag | ✓ VERIFIED | Skripte vorhanden (156/53 Zeilen), von CI-Lauf 367 tatsaechlich benutzt (siehe Truth 4); Code-Review fand keine Beanstandung außer IN-01 (Info, nicht behoben, kein Sicherheitsproblem) |
| 3 | API-Abbild traegt die Pakete unter `/app/desktop-dist/` und liefert sie aus, ohne dass Live-Server Gitea-Zugang brauchen (D-08) | ✓ VERIFIED | `apps/api/Dockerfile` enthaelt `COPY desktop-dist`; lokaler Docker-Stack liefert `/desktop/latest` und `/desktop/download/linux` tatsaechlich aus (curl-Beweis oben; Hinweis: das laufende lokale Abbild ist ein aelterer Baustand, siehe Anmerkung unten) |
| 4 | CI-Job `desktop` baut auf dem Linux-Runner sowohl `Tessera-X.Y.Z.AppImage` als auch `Tessera-Setup-X.Y.Z.exe` (Cross-Bau, cargo-xwin/NSIS) und uebergibt sie per `actions/cache` an `publish`, das ohne Manifest hart abbricht | ✓ VERIFIED | Gitea CI/CD Lauf 367 (Commit `742fb5c`), Job "Desktop-Pakete bauen" gruen, erzeugte `Tessera-Setup-1.1.0-beta.742fb5c.exe` (2.775.663 Bytes) und `Tessera-1.1.0-beta.742fb5c.AppImage` (82.479.608 Bytes); `.gitea/workflows/ci.yml` enthaelt `cargo-xwin`, `--require linux,windows`, `fail-on-cache-miss: true`, `needs: desktop`; `publish-images.sh` bricht ohne `desktop-dist/manifest.json` hart ab (Code-Inspektion, Zeile 63-64) |
| 5 | `publish` haengt Pakete ins API-/Web-Abbild; Job `publish` von Lauf 367 pushte Abbilder mit den Paketen | ✓ VERIFIED | 18-05-SUMMARY.md: Lauf 367 — alle vier Jobs gruen, `publish` pushte Abbilder mit Etiketten `beta`/`latest` |
| 6 | `publish-release.sh` haengt bei Tags jede Manifest-Datei idempotent (GET/DELETE/POST) als Release-Anhang an; Token verlaesst nie die Kommandozeile | ✓ VERIFIED (Code + Trockenlauf) | `.gitea/scripts/publish-release.sh` enthaelt `upload_asset()`, `HDR_AUTH` (nur `Authorization`-Header), kein `GITEA_TOKEN` in einer `curl`-Zeile (grep bestaetigt); `--dry-run --tag v1.1.0` nennt laut 18-02-SUMMARY.md den erwarteten Zielpfad — der echte Upload bei einem Tag ist aber noch nicht gelaufen (siehe Truth 7) |
| 7 | Ein Freigabe-Tag haengt beide Dateien tatsaechlich als Release-Anhang an den Gitea-Release (ROADMAP-SC1, zweite Haelfte) | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code/Skript vorhanden und per Trockenlauf geprueft, aber kein echter `v*`-Tag wurde seit den Phase-18-Aenderungen gepusht (Lauf 367 war ein `main`-Push) — der reale API-Roundtrip ist unbewiesen. Siehe `behavior_unverified_items` |
| 8 | Anmeldeseite zeigt einen unauffaelligen "Desktop-App herunterladen (Windows)"-Link mit Linux-Kurzlink und Version nur wenn `/desktop/latest` antwortet; Einstellungen → Allgemein → Desktop-App zeigt Version, zwei Download-Knoepfe, Dateigroesse, Erklaerung; alle Downloads laufen ueber `API_URL` (ROADMAP-SC2) | ✓ VERIFIED | `apps/web/src/app/(auth)/login/page.tsx` importiert/rendert `DesktopDownloadLinks`; `settings-sidebar.tsx` verlinkt `/settings/general/desktop`; `pnpm --filter @tessera/web exec vitest run src/lib/desktop.test.ts src/components/desktop src/components/settings/desktop-app-settings.test.tsx` — 11/11 gruen; volle Web-Suite 365/365 gruen; `desktopDownloadUrl()` baut Adressen ausschliesslich aus `API_URL` + relativem `url`-Feld |
| 9 | Client fragt die Server-Adresse beim ersten Start ab, prueft sie echt (`check_server`/`/health/version`), speichert sie, navigiert zur Tessera-Anmeldung; Tray behaelt Oeffnen/Schliessen-ins-Tray/Autostart aus Phase 6 und traegt zusaetzlich einen Update-Eintrag mit echten Umlauten; Versionspruefung gegen `/desktop/latest` inkl. Commit-Vergleich fuer Beta (WR-02-Fix) | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code vollstaendig vorhanden und verdrahtet: `check_server`/`save_server_url`-Kommandos, `api_url()`-Helfer, Tray-Eintraege "Öffnen"/"Update herunterladen"/Autostart-Haken/"Beenden" mit korrektem Label je Plattform, `is_newer`-Vergleich inkl. `channel == "beta" && info.commit != app_commit` (WR-02, `build.rs` embeds `APP_COMMIT` via `TESSERA_COMMIT`-Env aus der Pipeline); `cargo check`/`cargo clippy` sauber. Das tatsaechliche Verhalten in einer grafischen Sitzung (echtes Rendering, echter Serverwechsel, echte Benachrichtigung) ist in dieser kopflosen Umgebung nicht beobachtbar — Bedienprobe steht laut 18-06-SUMMARY.md aus. Siehe `behavior_unverified_items` |
| 10 | Anwender-, Betriebs- und Entwicklungshandbuch beschreiben Installation, Erststart, Tray, Pipeline, Release-Dateien und Umgebungsvariablen (ROADMAP-SC4) | ✓ VERIFIED | `docs/anleitung-anwender.md` Kapitel `## Desktop-App` (Zeile 161) mit den geforderten Unterabschnitten; `docs/anleitung-betrieb.md` Kapitel `## 10. Desktop-App: Pakete und Release-Dateien` (Zeile 552) mit `DESKTOP_DIST_DIR`; `docs/anleitung-entwicklung.md` enthaelt `### Desktop-App lokal bauen` und keinen Treffer mehr fuer "Tauri-Grundgerüst"; `docs/ci-cd-setup.md` beschreibt den Job `desktop`; `CHANGELOG.md` traegt den D-17-Stichpunkt |
**Score:** 8/10 truths verified (2 present, behavior-unverified)
### Anmerkung zum lokalen Docker-Stack
Der laufende lokale API-Container liefert `/desktop/latest` mit `"channel":"dev","commit":"ae8fecb"` — das ist ein aelterer, lokal gebauter Stand aus 18-01, nicht der aktuelle Code mit den Review-Fixes (CR-01/WR-01/WR-02/WR-03). Dieser Befund bestaetigt nur, dass die Route/das Streaming-Verhalten funktioniert (Truth 1/3) — er ist **keine** Evidenz dafuer, dass die Review-Fixes in einem laufenden Abbild aktiv sind. Die Review-Fixes selbst sind stattdessen ueber den frisch ausgefuehrten `vitest run src/desktop` (13/13, inkl. der neuen Test 7a/7b/7c und Test 9/10) sowie `cargo check`/`cargo clippy` bewiesen, wie vom Auftraggeber vorgegeben.
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/src/desktop/desktop.service.ts` | Manifest lesen, Plattform-Whitelist, Datei-Stream, Pfad-Traversal-Schutz | ✓ VERIFIED | 176 Zeilen; `PLATFORMS`-Whitelist vor Dateisystemzugriff; CR-01-Fix (`.`/`..`-Ablehnung + `startsWith(resolvedRoot)`) und WR-03-Fix (`isValidManifestFileEntry`) beide im Code vorhanden und getestet |
| `apps/api/src/desktop/desktop.controller.ts` | `GET /desktop/latest`, `GET /desktop/download/:platform`, beide `@Public()` | ✓ VERIFIED | 37 Zeilen, `@Inject(DesktopService)` explizit gesetzt (Vitest/esbuild-Workaround) |
| `.gitea/scripts/desktop-collect.sh` / `desktop-version.sh` | Pakete einsammeln, Version aus Tag | ✓ VERIFIED | 156/53 Zeilen, in CI-Lauf 367 tatsaechlich benutzt |
| `apps/api/Dockerfile` | `COPY desktop-dist` | ✓ VERIFIED | `COPY desktop-dist ./desktop-dist` vor `USER nestjs` (18-01-SUMMARY.md, curl-Beweis bestaetigt Auslieferung) |
| `.gitea/workflows/ci.yml` | Job `desktop`, Windows-Cross-Bau, Cache-Uebergabe an `publish` | ✓ VERIFIED | `cargo-xwin`, `--target x86_64-pc-windows-msvc --bundles nsis`, `--require linux,windows`, `TESSERA_COMMIT`-Env, `fail-on-cache-miss: true`, `needs: desktop` alle vorhanden |
| `.gitea/scripts/publish-release.sh` | Idempotenter Release-Datei-Upload | ✓ VERIFIED (Code) / ⚠️ Realer Upload nicht beobachtet | `upload_asset()`, `HDR_AUTH`, kein Token in `curl`-Zeile |
| `apps/web/src/lib/desktop.ts` + Komponenten | Fetch-Helfer, Login-Link, Einstellungsseite | ✓ VERIFIED | 67/66/122 Zeilen, verdrahtet in `login/page.tsx` und `settings-sidebar.tsx`, 11/11 Tests gruen |
| `apps/desktop/src-tauri/src/lib.rs` + `build.rs` | Versionspruefung, Tray, Erststart-Kommandos, Commit-Stempel | ✓ VERIFIED (Code) / ⚠️ Kein grafischer Beweis | 246/35 Zeilen, `cargo check`/`clippy` sauber, WR-02-Fix verdrahtet |
| `apps/desktop/src-tauri/icons/*` | Echtes Tessera-Icon statt Platzhalter | ✓ VERIFIED | 5 Dateien vorhanden (icon.ico 105.724 Bytes, icon.png 33.721 Bytes — keine 105-Byte-Platzhalter mehr) |
| `docs/anleitung-anwender.md`, `docs/anleitung-betrieb.md`, `docs/anleitung-entwicklung.md`, `docs/ci-cd-setup.md`, `CHANGELOG.md` | Handbuecher + Changelog | ✓ VERIFIED | Alle geforderten Kapitel-Anker gefunden |
| `.planning/REQUIREMENTS.md` | DESK-01..05 mit Traceability | ✓ VERIFIED | 5/5 Eintraege, 5/5 Traceability-Zeilen, DESK-01/02 Complete, DESK-03/04/05 bewusst Pending bis Bedienprobe |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `.gitea/scripts/desktop-collect.sh` | `apps/api/src/desktop/desktop.service.ts` | `manifest.json` | ✓ WIRED | Manifest-Form stimmt mit `DesktopManifest`-Typ und Service-Lesecode ueberein; curl-Beweis bestaetigt reales Ausliefern |
| `apps/api/Dockerfile` | `apps/api/src/desktop/desktop.service.ts` | `COPY desktop-dist` | ✓ WIRED | `desktopDistDir` zeigt auf `/app/desktop-dist`, curl liefert reale Datei |
| `.gitea/workflows/ci.yml (desktop)` | `.gitea/workflows/ci.yml (publish)` | `actions/cache` Schluessel `desktop-dist-${{ gitea.sha }}` | ✓ WIRED | Bestaetigt durch gruenen Lauf 367 (alle vier Jobs gruen) |
| `apps/web/src/lib/desktop.ts` | `apps/api/src/desktop/desktop.controller.ts` | `fetch(`${API_URL}/desktop/latest`)` | ✓ WIRED | `loadDesktopLatest()` ruft `/desktop/latest`; Unit-Tests decken Erfolg/Fehler ab |
| `apps/web/src/components/settings/settings-sidebar.tsx` | `apps/web/src/app/(portal)/settings/general/desktop/page.tsx` | Link `href=/settings/general/desktop` | ✓ WIRED | grep bestaetigt genau 1 Treffer, `aria-current` analog "Konto" |
| `apps/desktop/src-tauri/src/lib.rs (Tray "update")` | `apps/web/.../settings/general/desktop/page.tsx` | `opener().open_url({server}/settings/general/desktop)` | ✓ WIRED | Zeile 153 in `lib.rs` baut exakt diese URL |
| `.gitea/scripts/publish-release.sh` | `desktop-dist/manifest.json` | `jq -r '.files[].name'` | ✓ WIRED (Code) | Upload-Schleife iteriert Manifest-Dateien; realer Netzaufruf am naechsten Tag noch offen |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| API liefert Manifest oeffentlich | `curl -s localhost:3001/desktop/latest` | 200, Manifest mit `files.linux` | ✓ PASS |
| API streamt Datei mit attachment-Header | `curl -sI localhost:3001/desktop/download/linux` | 200, `Content-Disposition: attachment` | ✓ PASS |
| Unbekannte Plattform vor Dateisystemzugriff abgewiesen | `curl -s localhost:3001/desktop/download/nonsense` | 400 "Unknown platform" | ✓ PASS |
| API-Modul-Tests (inkl. Review-Fix-Regressionen) | `pnpm --filter @tessera/api exec vitest run src/desktop` | 13/13 gruen | ✓ PASS |
| Web-Suite komplett | `pnpm --filter @tessera/web exec vitest run` | 365/365 gruen | ✓ PASS |
| Desktop-spezifische Web-Tests | `pnpm --filter @tessera/web exec vitest run src/lib/desktop.test.ts src/components/desktop src/components/settings/desktop-app-settings.test.tsx` | 11/11 gruen | ✓ PASS |
| API Typpruefung | `pnpm --filter @tessera/api exec tsc --noEmit` | fehlerfrei | ✓ PASS |
| Web Typpruefung | `pnpm --filter @tessera/web exec tsc --noEmit` | fehlerfrei | ✓ PASS |
| Rust-Client kompiliert | `cargo check` (apps/desktop/src-tauri) | `Finished` | ✓ PASS |
| Windows-Cross-Bau real in CI | — | Gitea Lauf 367 (bereits vom Auftraggeber gemessen, nicht erneut ausgefuehrt) | ✓ PASS (uebernommene Evidenz) |
### Requirements Coverage
| Requirement | Source Plan(s) | Description | Status | Evidence |
|-------------|-----------------|--------------|--------|----------|
| DESK-01 | 18-01, 18-02, 18-04, 18-05, 18-06 | Tauri-Wrapper Windows+Linux (Phase 6, fortgefuehrt) + Pipeline | ✓ SATISFIED | Code+CI-Lauf 367, REQUIREMENTS.md Complete |
| DESK-02 | 18-04, 18-06 | Server-Adresse beim ersten Start (Phase 6, fortgefuehrt) | ✓ SATISFIED (Code) / Bedienprobe offen | `check_server`/`save_server_url` verdrahtet; REQUIREMENTS.md fuehrt DESK-02 als Complete (aus Phase 6, unveraendert) |
| DESK-03 | 18-01, 18-03, 18-06 | Installer in Tessera herunterladbar | ✓ SATISFIED (Code+Tests) / REQUIREMENTS.md bewusst Pending | Login-Link/Einstellungsseite verdrahtet und getestet; Statuswechsel auf Complete an Bedienprobe geknuepft |
| DESK-04 | 18-02, 18-05, 18-06 | Freigabe-Tag baut beide Pakete, haengt sie an den Release | ⚠️ TEILWEISE | Bau-Haelfte bewiesen (Lauf 367); Release-Anhang-Haelfte nur per Trockenlauf, kein echter Tag in diesem Zyklus |
| DESK-05 | 18-01, 18-04, 18-06 | Client traegt Freigabe-Version, Update-Hinweis mit Link | ✓ SATISFIED (Code) / Bedienprobe offen | Versionspruefung inkl. WR-02-Commit-Vergleich verdrahtet, `cargo check`/`clippy` sauber; grafischer Beweis aussteht |
**Keine verwaisten Requirements.** REQUIREMENTS.md bildet alle 5 DESK-Eintraege korrekt auf Phase 18 ab (DESK-01/02 aus Phase 6 fortgefuehrt); keine zusaetzliche Phase-18-Zuordnung fehlt.
### Anti-Patterns Found
Keine. `grep` auf `TODO|FIXME|XXX|TBD|HACK|PLACEHOLDER|not yet implemented|coming soon` in allen 18 neuen/geaenderten Kerndateien (API-Modul, Skripte, Web-Komponenten, Rust-Client, CI-Workflow) ergab 0 Treffer.
### Code-Review-Status
`18-REVIEW.md`: 1 Critical (CR-01, Pfad-Traversal-Verteidigung), 3 Warnings (WR-01 CSP, WR-02 Beta-Update-Vergleich, WR-03 Manifest-Validierung), 2 Info (beide bewusst uebersprungen, keine Sicherheitswirkung). Alle 4 Critical/Warning-Funde sind laut `18-REVIEW-FIX.md` behoben und per Commit nachgewiesen (`a8964f1`, `0d5c80f`, `1b2f803`, `579e24b`) — durch eigenes Code-Lesen und `vitest`/`cargo`-Laeufe in diesem Verifizierungslauf bestaetigt. Ein fuenfter, in REVIEW-FIX.md nicht dokumentierter Nachfolge-Commit (`72e488e`) behebt einen Cache-bedingten Folgefehler des WR-02-Fixes (Commit-Stempel wuerde mit warmem Cargo-Cache veraltet bleiben) — inhaltlich konsistent und ebenfalls durch Code-Lesen bestaetigt.
### Human Verification Required
1. **Release-Anhang am naechsten Freigabe-Tag**
- **Test:** Naechsten `v*`-Tag setzen/pushen, Job `desktop`+`publish` beobachten, danach den Gitea-Release des Tags oeffnen.
- **Expected:** `Tessera-Setup-X.Y.Z.exe` und `Tessera-X.Y.Z.AppImage` sind als Anhaenge vorhanden und herunterladbar.
- **Why human:** `publish-release.sh` laeuft nur bei einem echten Tag-Push; in diesem Verifizierungszyklus (Lauf 367) war kein Tag gesetzt. Nur per Trockenlauf geprueft.
2. **Windows-Bedienprobe (Erststart, Tray, Anmeldung, Einstellungsseite)**
- **Test:** 18-06-SUMMARY.md "Manuelle Abnahme (ausstehend)", Schritte 1-11.
- **Expected:** Installation mit SmartScreen-Hinweis, Erststart fuehrt zur Tessera-Anmeldung im App-Fenster, Tray (Oeffnen/Update/Autostart-Haken/Beenden) funktioniert, Einstellungsseite zeigt Version/Knoepfe/Groesse.
- **Why human:** Diese Ausfuehrungsumgebung hat keinen Windows-PC und keine grafische Sitzung.
3. **Beta-Update-Hinweis zwischen zwei Commits (WR-02-Fix)**
- **Test:** Zwei Beta-Builds ohne neuen Tag (nur neuer Commit) — aelterer Client soll die Benachrichtigung zeigen.
- **Expected:** Benachrichtigung "Neue Version X.Y.Z verfuegbar" erscheint trotz gleicher `X.Y.Z`-Versionsnummer, weil sich der Commit-Stempel unterscheidet.
- **Why human:** Keine Rust-Unit-Tests fuer diese Vergleichslogik; `18-REVIEW-FIX.md` flaggt dies explizit als manuell zu pruefen.
### Gaps Summary
Keine blockierenden Luecken gefunden. Der gesamte Code-Pfad (API-Modul, CI-Pipeline inkl. Windows-Cross-Bau, Web-Oberflaeche, Client-Versionspruefung/Tray, Handbuecher, REQUIREMENTS-Traceability) ist vorhanden, verdrahtet und — soweit in dieser kopflosen Umgebung moeglich — automatisiert bewiesen (API 13/13 + volle Web-Suite 365/365, beide Typpruefungen sauber, `cargo check`/`clippy` sauber, Gitea-Lauf 367 gruen mit beiden Paketdateien). Zwei Aspekte des Phasenziels sind bewusst nur bis zur Code-/Trockenlauf-Ebene bewiesen und brauchen eine echte Beobachtung: (a) der Release-Datei-Anhang, der nur bei einem echten Freigabe-Tag auslöst, und (b) die grafische Bedienprobe des Windows-Clients selbst. Beides ist von den Autoren der Phase (SUMMARY 18-06, VALIDATION.md "Manual-Only Verifications") bereits explizit als offen dokumentiert und deckt sich mit der vom Auftraggeber vorgegebenen Erwartung ("expected to be human_needed/deferred, not failures"). REQUIREMENTS.md haelt DESK-03/04/05 konsequent auf Pending, bis diese Proben abgeschlossen sind — das ist korrekt und kein Gap.
---
_Verified: 2026-09-16T17:50:00Z_
_Verifier: Claude (gsd-verifier)_
+1 -1
View File
@@ -94,5 +94,5 @@
"label": "Advance to the next step (verify)", "label": "Advance to the next step (verify)",
"reason": "Phase 18 of 18 · ready to verify" "reason": "Phase 18 of 18 · ready to verify"
}, },
"updated_at": "2026-09-16T14:43:23.832Z" "updated_at": "2026-09-16T15:27:01.839Z"
} }
+2 -1
View File
@@ -6,7 +6,8 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
### Neu ### Neu
- Kalender-Widget: Monatsübersicht mit Terminanzahl je Tag, Termine beim Überfahren, darunter „Nächste Termine“ - Desktop-App für Windows und Linux: Download auf der Anmeldeseite und unter Einstellungen → Desktop-App
- Kalender-Widget: Monatsübersicht mit Terminanzahl je Tag, Termine beim Überfahren, darunter „Nächste Termine”
- Kalender-Widget: Einstellungen für Monatsansicht, Anzahl und Zeitraum der Termine - Kalender-Widget: Einstellungen für Monatsansicht, Anzahl und Zeitraum der Termine
- Favoriten-Widget: optionaler Titel (ohne Titel keine Kopfzeile) - Favoriten-Widget: optionaler Titel (ohne Titel keine Kopfzeile)
+51 -1
View File
@@ -161,7 +161,57 @@ describe('DesktopService/DesktopController — HTTP-Durchstich (Phase 18)', () =
expect(res.status).toBe(404); expect(res.status).toBe(404);
}); });
it('Test 8 (bewusst oeffentlich): getLatest und download tragen @Public()', () => { it('Test 7a (dot-only Name im Manifest, CR-01): ".." endet mit 404', async () => {
writeManifest({
linux: { name: '..', size: 123, sha256: 'a'.repeat(64) },
});
const res = await fetch(`${baseUrl}/desktop/download/linux`);
expect(res.status).toBe(404);
});
it('Test 7b (dot-only Name im Manifest, CR-01): "." endet mit 404', async () => {
writeManifest({
linux: { name: '.', size: 123, sha256: 'a'.repeat(64) },
});
const res = await fetch(`${baseUrl}/desktop/download/linux`);
expect(res.status).toBe(404);
});
it('Test 7c (Name mit Traversal-Segment ausserhalb desktopDistDir, CR-01): "../manifest.json" endet mit 404', async () => {
writeManifest({
linux: { name: '../manifest.json', size: 123, sha256: 'a'.repeat(64) },
});
const res = await fetch(`${baseUrl}/desktop/download/linux`);
expect(res.status).toBe(404);
});
it('Test 9 (WR-03, ungueltiger Eintrag im Manifest): name fehlt -- getLatest wirft NotFoundException statt "undefined" als Datei zu suchen', () => {
// Bewusst am `writeManifest()`-Helper vorbei direkt geschrieben -- dessen
// Parametertyp verlangt `name`, hier soll aber genau dessen Fehlen
// geprueft werden (kaputtes Manifest, kein TS-Typfehler im Test).
fs.writeFileSync(
path.join(tempDir, 'manifest.json'),
JSON.stringify({
version: '1.1.0',
channel: 'dev',
commit: 'abc1234',
buildTime: '2026-09-16T00:00:00Z',
files: { linux: { size: 123, sha256: 'a'.repeat(64) } },
}),
);
const service = new DesktopService();
expect(() => service.getLatest()).toThrow(NotFoundException);
});
it('Test 10 (WR-03, ungueltiger Eintrag im Manifest): sha256 ist kein 64-stelliger Hex-String -- 404', async () => {
writeManifest({
linux: { name: PACKAGE_NAME, size: packageSize, sha256: 'not-a-hash' },
});
const res = await fetch(`${baseUrl}/desktop/download/linux`);
expect(res.status).toBe(404);
});
it('Test 11 (bewusst oeffentlich): getLatest und download tragen @Public()', () => {
expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.getLatest)).toBe(true); expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.getLatest)).toBe(true);
expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.download)).toBe(true); expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.download)).toBe(true);
}); });
+55 -4
View File
@@ -15,6 +15,30 @@ import * as path from 'path';
*/ */
const PLATFORMS = ['windows', 'linux'] as const; const PLATFORMS = ['windows', 'linux'] as const;
/** sha256 als Hex-String -- genau 64 Zeichen, 0-9/a-f (Gross-/Kleinschreibung egal). */
const SHA256_HEX_RE = /^[a-f0-9]{64}$/i;
/**
* Grundform eines einzelnen Datei-Eintrags im Manifest (WR-03, Code-Review
* Phase 18): `getManifest()` prueft bislang nur die Kopf-Form, nicht die
* einzelnen Plattform-Eintraege -- ein kaputter/unvollstaendiger Eintrag
* wuerde sonst unbemerkt bis in `getPackage()` durchgereicht (z. B.
* `entry.name === undefined`, das sich zu `"undefined"` coerct und dort
* fehlleitend als Dateiname gesucht wird).
*/
function isValidManifestFileEntry(entry: unknown): entry is DesktopManifestFile {
if (typeof entry !== 'object' || entry === null) {
return false;
}
const candidate = entry as Record<string, unknown>;
return (
typeof candidate.name === 'string' &&
typeof candidate.size === 'number' &&
typeof candidate.sha256 === 'string' &&
SHA256_HEX_RE.test(candidate.sha256)
);
}
@Injectable() @Injectable()
export class DesktopService { export class DesktopService {
private readonly logger = new Logger(DesktopService.name); private readonly logger = new Logger(DesktopService.name);
@@ -45,10 +69,24 @@ export class DesktopService {
try { try {
const raw = fs.readFileSync(manifestPath, 'utf-8'); const raw = fs.readFileSync(manifestPath, 'utf-8');
const parsed = JSON.parse(raw) as DesktopManifest; const parsed = JSON.parse(raw) as DesktopManifest;
if (typeof parsed.version !== 'string' || typeof parsed.files !== 'object' || parsed.files === null) { if (
typeof parsed.version !== 'string' ||
typeof parsed.files !== 'object' ||
parsed.files === null ||
Array.isArray(parsed.files)
) {
this.logger.warn(`manifest.json unter ${manifestPath} hat unerwartete Form`); this.logger.warn(`manifest.json unter ${manifestPath} hat unerwartete Form`);
return null; return null;
} }
for (const platform of PLATFORMS) {
const entry = parsed.files[platform];
if (entry !== undefined && !isValidManifestFileEntry(entry)) {
this.logger.warn(
`manifest.json unter ${manifestPath} hat einen ungueltigen Eintrag fuer Plattform ${platform}`,
);
return null;
}
}
return parsed; return parsed;
} catch (error) { } catch (error) {
this.logger.warn(`manifest.json unter ${manifestPath} konnte nicht gelesen werden: ${error}`); this.logger.warn(`manifest.json unter ${manifestPath} konnte nicht gelesen werden: ${error}`);
@@ -107,13 +145,26 @@ export class DesktopService {
} }
// (4) Verteidigung in der Tiefe (T-18-02): auch ein manipuliertes // (4) Verteidigung in der Tiefe (T-18-02): auch ein manipuliertes
// Manifest darf nicht aus dem Ordner hinausfuehren. // Manifest darf nicht aus dem Ordner hinausfuehren. Die Zeichen-Whitelist
if (!/^[A-Za-z0-9._-]+$/.test(entry.name)) { // allein reicht nicht -- "." und ".." bestehen ausschliesslich aus
// erlaubten Zeichen, meinen im Dateisystem aber "aktueller"/"uebergeordneter
// Ordner". Deshalb zusaetzlich explizit ausschliessen UND den aufgeloesten
// Pfad gegen den Zielordner pruefen (haelt auch kuenftige Varianten dieses
// Musters ab, falls die Zeichen-Whitelist anderswo wiederverwendet wird).
if (
!/^[A-Za-z0-9._-]+$/.test(entry.name) ||
entry.name === '.' ||
entry.name === '..'
) {
throw new NotFoundException(`No package for platform: ${knownPlatform}`);
}
const filePath = path.join(this.desktopDistDir, entry.name);
const resolvedRoot = path.resolve(this.desktopDistDir) + path.sep;
if (!path.resolve(filePath).startsWith(resolvedRoot)) {
throw new NotFoundException(`No package for platform: ${knownPlatform}`); throw new NotFoundException(`No package for platform: ${knownPlatform}`);
} }
// (5) Datei muss existieren. // (5) Datei muss existieren.
const filePath = path.join(this.desktopDistDir, entry.name);
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
throw new NotFoundException(`Package file missing: ${entry.name}`); throw new NotFoundException(`Package file missing: ${entry.name}`);
} }
+32
View File
@@ -1,3 +1,35 @@
fn main() { fn main() {
// Commit-Stempel zur Kompilierzeit einbetten (WR-02, Code-Review Phase 18):
// `desktop-collect.sh` vergibt fuer den Beta-Kanal (main) jedem Commit
// dieselbe X.Y.Z-Version (D-07) -- die Unterscheidung zwischen zwei
// Beta-Bauten steckt nur im `commit`-Feld von manifest.json. Ohne einen
// eigenen Commit-Stempel im Binary kann `lib.rs` diesen Fall nicht
// erkennen. `--short=7` spiegelt exakt das Format, das
// `desktop-collect.sh` fuer manifest.json schreibt. Fehlt `git` (z. B.
// Quell-Tarball ohne .git-Ordner), bleibt der Wert leer -- dann greift
// nur noch der reine Versionsvergleich.
//
// Vorrang hat die Umgebungsvariable TESSERA_COMMIT (die Pipeline setzt sie
// auf den vollen Commit-Hash): der Cargo-Zwischenspeicher wuerde ein
// `git rev-parse` sonst nicht neu auswerten, und ein alter Stempel im
// Programm liesse den Beta-Update-Hinweis dauerhaft erscheinen.
println!("cargo:rerun-if-env-changed=TESSERA_COMMIT");
println!("cargo:rerun-if-changed=../../../.git/HEAD");
let commit = std::env::var("TESSERA_COMMIT")
.ok()
.map(|s| s.trim().chars().take(7).collect::<String>())
.filter(|s| !s.is_empty())
.or_else(|| {
std::process::Command::new("git")
.args(["rev-parse", "--short=7", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())
})
.unwrap_or_default();
println!("cargo:rustc-env=APP_COMMIT={commit}");
tauri_build::build() tauri_build::build()
} }
+11 -1
View File
@@ -17,6 +17,8 @@ struct VersionResponse {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct DesktopLatest { struct DesktopLatest {
version: String, version: String,
channel: String,
commit: String,
} }
/// Baut die Adresse eines API-Pfads aus der gespeicherten Server-Adresse. /// Baut die Adresse eines API-Pfads aus der gespeicherten Server-Adresse.
@@ -193,12 +195,20 @@ pub fn run() {
if let Some(server_url) = url_for_check { if let Some(server_url) = url_for_check {
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
let app_version = env!("CARGO_PKG_VERSION").to_string(); let app_version = env!("CARGO_PKG_VERSION").to_string();
let app_commit = env!("APP_COMMIT").to_string();
let update_item = update.clone(); let update_item = update.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let url = api_url(&server_url, "/desktop/latest"); let url = api_url(&server_url, "/desktop/latest");
if let Ok(resp) = reqwest::get(&url).await { if let Ok(resp) = reqwest::get(&url).await {
if let Ok(info) = resp.json::<DesktopLatest>().await { if let Ok(info) = resp.json::<DesktopLatest>().await {
if info.version != app_version { // Beta-Kanal (main) vergibt jedem Commit dieselbe X.Y.Z-Version
// (D-07, desktop-collect.sh) -- ohne den Commit-Vergleich saehe
// ein Beta-Client zwischen zwei Freigabe-Tags nie einen neueren
// Bau (WR-02, Code-Review Phase 18). Fuer den Live-Kanal bleibt
// es beim reinen Versionsvergleich.
let is_newer = info.version != app_version
|| (info.channel == "beta" && info.commit != app_commit);
if is_newer {
let _ = app_handle let _ = app_handle
.notification() .notification()
.builder() .builder()
+1 -1
View File
@@ -21,7 +21,7 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; img-src * data:; font-src * data:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval'" "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"
} }
}, },
"bundle": { "bundle": {
+56 -3
View File
@@ -16,9 +16,10 @@ Diese Anleitung richtet sich an alle Kolleginnen und Kollegen, die Tessera im Ar
- [Zertifikat-Manager](#zertifikat-manager) - [Zertifikat-Manager](#zertifikat-manager)
- [Domaincheck](#domaincheck) - [Domaincheck](#domaincheck)
7. [Persönliche Einstellungen](#persönliche-einstellungen) 7. [Persönliche Einstellungen](#persönliche-einstellungen)
8. [Einen Fehler melden](#einen-fehler-melden) 8. [Desktop-App](#desktop-app)
9. [Was ist neu](#was-ist-neu) 9. [Einen Fehler melden](#einen-fehler-melden)
10. [Häufige Stolpersteine](#häufige-stolpersteine) 10. [Was ist neu](#was-ist-neu)
11. [Häufige Stolpersteine](#häufige-stolpersteine)
--- ---
@@ -157,6 +158,58 @@ Ein einfaches Werkzeug, um zu prüfen, ob eine Internet-Domain verfügbar ist. G
**Sprache:** Unten in der Seitenleiste finden Sie die Sprachumschaltung zwischen Deutsch und Englisch. **Sprache:** Unten in der Seitenleiste finden Sie die Sprachumschaltung zwischen Deutsch und Englisch.
## Desktop-App
Tessera steht zusätzlich als eigenständige App für Windows und Linux zur Verfügung — für alle, die Tessera lieber in einem eigenen Fenster statt im Browser-Tab nutzen möchten.
### Was die Desktop-App ist
Die Desktop-App öffnet Tessera in einem eigenen Fenster statt in einem Browser-Tab. Nach dem Start legt sie ein Symbol im Infobereich Ihrer Taskleiste ab und bietet dieselben Funktionen wie die gewohnte Tessera-Oberfläche im Browser — es ist derselbe Tessera-Server, nur mit einem eigenen Fenster.
### Herunterladen
Sie finden die Desktop-App an zwei Stellen, beide ohne Zugang zu Gitea:
- Auf der **Anmeldeseite** unterhalb des Formulars: „Desktop-App herunterladen (Windows)" und daneben „Linux-Version".
- Angemeldet unter **Einstellungen → Allgemein → Desktop-App**: dort sehen Sie zusätzlich die aktuelle Version, den Dateinamen und die Dateigröße.
### Installation unter Windows
Führen Sie die heruntergeladene Datei `Tessera-Setup-X.Y.Z.exe` aus. Windows zeigt dabei die Meldung „Der Computer wurde durch Windows geschützt": Klicken Sie auf „Weitere Informationen" und danach auf „Trotzdem ausführen". Der Grund dafür: Die App ist für den internen Gebrauch nicht signiert, das Paket stammt aber unmittelbar aus Ihrem eigenen Tessera-Server. Danach erscheint „Tessera" im Startmenü. Eine neuere Version installieren Sie einfach über die bestehende — die bereits eingetragene Server-Adresse bleibt dabei erhalten.
### Installation unter Linux
Machen Sie die Datei `Tessera-X.Y.Z.AppImage` ausführbar (über die Dateieigenschaften oder mit `chmod +x`) und starten Sie sie anschließend. Eine Installation ist dafür nicht nötig.
### Erster Start: Server-Adresse
Beim ersten Start fragt die App nach der Adresse, unter der Sie Tessera auch im Browser öffnen (zum Beispiel `https://tessera.example.com`). Die App prüft die Adresse und meldet „Tessera X.Y.Z gefunden". Bei einer `http`-Adresse erscheint zusätzlich ein Hinweis — die Verbindung ist trotzdem möglich. Danach folgt die gewohnte Tessera-Anmeldung, direkt im App-Fenster.
### Fenster, Infobereich und Beenden
Schließen Sie das Fenster über das X, legt sich Tessera lediglich in den Infobereich Ihrer Taskleiste, statt sich zu beenden. Ein Linksklick auf das Symbol dort öffnet das Fenster wieder. Ein Rechtsklick zeigt ein Menü mit:
- **Öffnen**
- **Update herunterladen**
- **Mit Windows starten** (unter Linux: **Beim Anmelden starten**) — mit Häkchen
- **Beenden**
Nur „Beenden" beendet die App tatsächlich, auch im Infobereich. Fenstergröße und -position merkt sich die App bis zum nächsten Start.
### Automatischer Start
Setzen oder entfernen Sie das Häkchen bei „Mit Windows starten" (bzw. „Beim Anmelden starten") im Rechtsklick-Menü des Infobereich-Symbols. Ab Werk ist der automatische Start ausgeschaltet.
### Neue Version
Ist eine neuere Version verfügbar, meldet sich die App beim Start mit „Neue Version X.Y.Z verfügbar". Der Menüeintrag „Update herunterladen" heißt dann „Version X.Y.Z herunterladen" und öffnet mit einem Klick die Seite Einstellungen → Desktop-App im Browser — dort laden Sie die neue Version herunter und installieren sie wie oben beschrieben. Ein automatisches Aktualisieren gibt es nicht.
### Wenn etwas nicht klappt
- **„Unter dieser Adresse antwortet kein Tessera-Server"** — prüfen Sie die eingegebene Adresse; gemeint ist die Adresse, unter der Sie Tessera im Browser öffnen, nicht eine interne API-Adresse.
- **Der Download-Link fehlt auf der Anmeldeseite** — der Server trägt derzeit keine Desktop-Pakete. Fragen Sie in diesem Fall Ihren Administrator.
- **Windows zeigt die SmartScreen-Warnung** — das ist normal und erwartet, siehe [Installation unter Windows](#installation-unter-windows).
## Einen Fehler melden ## Einen Fehler melden
Wenn etwas in Tessera nicht so funktioniert, wie Sie es erwarten, müssen Sie niemandem lange erklären, was Sie gesehen haben: Klicken Sie oben rechts in der Kopfleiste auf den Knopf **Fehler melden** (Käfer-Symbol). Tessera nimmt sofort ein Bild der aktuellen Seite auf — genau das, was Sie gerade sehen — und öffnet danach ein kleines Fenster mit einer Vorschau dieses Bildes. Wenn etwas in Tessera nicht so funktioniert, wie Sie es erwarten, müssen Sie niemandem lange erklären, was Sie gesehen haben: Klicken Sie oben rechts in der Kopfleiste auf den Knopf **Fehler melden** (Käfer-Symbol). Tessera nimmt sofort ein Bild der aktuellen Seite auf — genau das, was Sie gerade sehen — und öffnet danach ein kleines Fenster mit einer Vorschau dieses Bildes.
+74 -2
View File
@@ -20,6 +20,7 @@ Betrieb der bereits laufenden Installation, nicht deren automatisierten Build.
7. [Protokolle und Fehlersuche](#7-protokolle-und-fehlersuche) 7. [Protokolle und Fehlersuche](#7-protokolle-und-fehlersuche)
8. [Abgrenzung zur CI/CD-Pipeline](#8-abgrenzung-zur-cicd-pipeline) 8. [Abgrenzung zur CI/CD-Pipeline](#8-abgrenzung-zur-cicd-pipeline)
9. [Zwei Kanäle: Live und Beta](#9-zwei-kanäle-live-und-beta) 9. [Zwei Kanäle: Live und Beta](#9-zwei-kanäle-live-und-beta)
10. [Desktop-App: Pakete und Release-Dateien](#10-desktop-app-pakete-und-release-dateien)
--- ---
@@ -450,9 +451,13 @@ 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 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. `vX.Y.Z` abgelegt. Das dauert etwa vier bis sechs Minuten.
Derselbe Tag baut zusätzlich die Desktop-Pakete (Windows-Installer und
Linux-AppImage) und hängt beide als Dateien an denselben Release an – Details
dazu in [Kapitel 10](#10-desktop-app-pakete-und-release-dateien).
Beim Tag legt die Pipeline zusätzlich einen **Release in Gitea** an: Name 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 „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 finden ihn im Repository unter „Releases”. Fehlt der Abschnitt in der
Änderungsliste, schlägt genau dieser letzte Schritt fehl – die Abbilder sind dann Änderungsliste, schlägt genau dieser letzte Schritt fehl – die Abbilder sind dann
trotzdem gebaut und abgelegt. Der Release wird nachgeholt, sobald der Abschnitt trotzdem gebaut und abgelegt. Der Release wird nachgeholt, sobald der Abschnitt
nachgetragen ist: entweder durch erneutes Auslösen des Tag-Laufs oder lokal per nachgetragen ist: entweder durch erneutes Auslösen des Tag-Laufs oder lokal per
@@ -543,3 +548,70 @@ Kapitel 2 gilt vollständig. Die Abweichungen gegenüber alpha:
dieses Etikett noch nicht – deshalb erst freigeben, dann installieren. Für einen dieses Etikett noch nicht – deshalb erst freigeben, dann installieren. Für einen
Probelauf davor kann vorübergehend `IMAGE_TAG=beta` stehen; danach auf `live` Probelauf davor kann vorübergehend `IMAGE_TAG=beta` stehen; danach auf `live`
umstellen und `pull` + `up -d --force-recreate api web` wiederholen. umstellen und `pull` + `up -d --force-recreate api web` wiederholen.
## 10. Desktop-App: Pakete und Release-Dateien
Seit September 2026 gibt es Tessera zusätzlich als Desktop-App für Windows und
Linux. Dieses Kapitel beschreibt, woher die Pakete kommen, wo sie liegen und
wie Sie Fehlerbilder rund um den Download einordnen. Die Anwendersicht (Download,
Installation, SmartScreen-Hinweis, Bedienung) steht in
`docs/anleitung-anwender.md`, Kapitel „Desktop-App".
### Woher die Pakete kommen
Der CI-Job `desktop` läuft nach `test` und vor `publish` – auf Push nach `main`
und bei jedem Freigabe-Tag `v*`. In diesem einen Job entstehen auf dem
Linux-Runner sowohl das Linux-AppImage als auch der Windows-Installer per
Cross-Bau (`cargo-xwin` + NSIS aus dem Ubuntu-Paket, kein Windows-Rechner in
der Pipeline). Einzelheiten zur Werkzeugkette stehen in
[`docs/ci-cd-setup.md`](./ci-cd-setup.md), Abschnitt 4.
### Wo die Pakete im Abbild liegen
`publish` kopiert die fertigen Pakete in das API-Abbild nach
`/app/desktop-dist/`, zusammen mit einer `manifest.json` (Version, Kanal,
Dateinamen, Größen, Prüfsummen). Auf dem Beta-Kanal tragen die Dateinamen
zusätzlich den Suffix `-beta.{commit}`, zum Beispiel
`Tessera-Setup-1.1.0-beta.742fb5c.exe` und
`Tessera-1.1.0-beta.742fb5c.AppImage`; auf Live steht dort die reine Form
`Tessera-Setup-X.Y.Z.exe` / `Tessera-X.Y.Z.AppImage`.
Kontrolle auf dem Server:
```bash
docker compose exec api ls -l /app/desktop-dist
curl -s https://{ihre-adresse}/api-proxy/desktop/latest
```
`ls -l` zeigt die abgelegten Dateien samt `manifest.json`; die `curl`-Abfrage
liefert dieselben Angaben als JSON (Version, Kanal, je Plattform Dateiname,
Größe, Prüfsumme und relative Download-Adresse) – das ist genau die Antwort,
die auch die Anmeldeseite und die Einstellungsseite auswerten. Antwortet die
Abfrage mit `404`, fehlt entweder das Verzeichnis oder das Manifest; die
Web-Oberfläche blendet den Download-Link dann automatisch aus.
### Release-Dateien in Gitea
Bei einem Freigabe-Tag hängt die Pipeline zusätzlich beide Dateien aus dem
Manifest als Anhänge an den Gitea-Release desselben Tags (Kapitel 9, „Eine
Version freigeben") – idempotent: ein erneuter Lauf ersetzt eine bereits
vorhandene Datei gleichen Namens, statt einen zweiten Anhang anzulegen. Die am
Release hinterlegte Datei ist byteidentisch mit der im Abbild ausgelieferten;
die Prüfsumme (`sha256`) aus `manifest.json` gilt für beide gleichermaßen.
### Umgebungsvariablen
Für die Desktop-Auslieferung ist keine neue Pflichtvariable nötig.
| Variable | Pflicht? | Default | Zweck |
|----------|:---:|---|---|
| `DESKTOP_DIST_DIR` | nein | `/app/desktop-dist` (im Abbild) | Ablageort der Desktop-Pakete und der `manifest.json`, aus dem `GET /desktop/latest` und `GET /desktop/download/:platform` lesen. In der Regel nicht ändern. |
### Fehlerbilder
| Symptom | Wahrscheinliche Ursache | Prüfen / Beheben |
|---|---|---|
| Download-Link fehlt auf der Anmeldeseite bzw. `/api-proxy/desktop/latest` liefert `404` | Das laufende Abbild trägt keine Desktop-Pakete – der Job `publish` hätte ohne Manifest eigentlich abbrechen müssen | Den zugehörigen Pipeline-Lauf prüfen (Job `desktop`/`publish` grün?), danach `docker compose pull` + `up -d --force-recreate api` erneut ausführen. |
| Download bricht bei großen Dateien ab | Größengrenze oder Zeitlimit des vorgeschalteten Proxys (Nginx Proxy Manager) – `client_max_body_size` bzw. Timeout-Einstellungen | Proxy-Konfiguration für die betroffene Adresse prüfen und die Grenze anheben. |
| Client meldet „Unter dieser Adresse antwortet kein Tessera-Server" | Anwender hat die interne API-Adresse statt der Web-Adresse eingetragen, oder `/api-proxy` ist vom Client-Rechner aus nicht erreichbar | Die im Anwenderhandbuch beschriebene Adresse verwenden (dieselbe wie im Browser); Netzwerk-/Firewall-Erreichbarkeit der Web-Adresse prüfen. |
| Windows zeigt die SmartScreen-Warnung | Erwartet – die App ist für den internen Gebrauch nicht signiert (D-09) | Kein Fehler; Anwenderhandbuch, Abschnitt „Installation unter Windows", beschreibt den Ablauf. |
+74 -5
View File
@@ -30,15 +30,23 @@ fest verankert.
apps/ apps/
api/ @tessera/api — NestJS-Backend api/ @tessera/api — NestJS-Backend
web/ @tessera/web — Next.js-Frontend web/ @tessera/web — Next.js-Frontend
desktop/ — — Tauri-Wrapper, früher Stand (nur Cargo-Projekt + eine setup.html) desktop/ @tessera/desktop — Tauri-Desktop-Client (Windows/Linux), fertiges Produkt
packages/ packages/
shared/ @tessera/shared — geteilte Konstanten/Typen, derzeit sehr klein (APP_NAME, HealthResponse) shared/ @tessera/shared — geteilte Konstanten/Typen, inzwischen auch die Manifest-Typen der Desktop-Pakete
module-sdk/ — — TypeScript-Interfaces für den Modul-Vertrag (TesseraModule, ModuleManifest) module-sdk/ — — TypeScript-Interfaces für den Modul-Vertrag (TesseraModule, ModuleManifest)
``` ```
`apps/desktop` besteht bislang nur aus dem Tauri-Grundgerüst (`src-tauri/`) und einer einzelnen `apps/desktop` ist der fertige Desktop-Client (Tauri 2), kein Grundgerüst mehr:
`setup.html` — dort ist noch keine eigentliche Anwendung zu finden. `packages/shared` ist ebenfalls `src-tauri/src/lib.rs` bündelt Tray-Menü, die beiden Erststart-Kommandos
minimal; es enthält aktuell nur eine Konstante und ein Health-Interface, keine DTOs. (`check_server`/`save_server_url`) und die Versionsprüfung gegen den Server;
`src/setup.html` ist die eigenständige Erststart-Seite (kein Bundler, spricht
nur über `window.__TAURI__.core.invoke`). Die fertigen Installationspakete
(Windows-`.exe`, Linux-`.AppImage`) entstehen nicht lokal, sondern im CI-Job
`desktop` (siehe [Desktop-App lokal bauen](#desktop-app-lokal-bauen) für den
lokalen Linux-Bau). `packages/shared` bleibt schlank, trägt inzwischen aber
zusätzlich zu Konstante und Health-Interface auch die Typen für das
Desktop-Paket-Manifest (`DesktopPlatform`, `DesktopManifest`,
`DesktopLatestResponse`).
**Root-Skripte** (`package.json`, laufen über Turborepo durch alle Workspaces): **Root-Skripte** (`package.json`, laufen über Turborepo durch alle Workspaces):
@@ -117,6 +125,46 @@ postgresql://tessera:tessera_dev@<container-ip>:5432/tessera
Das ist relevant, sobald Sie `prisma migrate dev`, `prisma studio` oder ein manuelles `psql` **vom Das ist relevant, sobald Sie `prisma migrate dev`, `prisma studio` oder ein manuelles `psql` **vom
Host aus** statt aus dem `api`-Container heraus ausführen wollen. Host aus** statt aus dem `api`-Container heraus ausführen wollen.
### Desktop-App lokal bauen
Voraussetzungen zusätzlich zu oben:
- Rust (stable) über [rustup](https://rustup.rs/)
- Auf Ubuntu/Debian folgende Systempakete:
```bash
sudo apt-get install -y libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1-dev \
libayatana-appindicator3-dev librsvg2-dev libgtk-3-dev libssl-dev patchelf
```
Version setzen und Linux-Paket bauen:
```bash
sh .gitea/scripts/desktop-version.sh
pnpm --filter @tessera/desktop exec tauri build --bundles appimage
```
`desktop-version.sh` schreibt die Version des letzten Freigabe-Tags in
`tauri.conf.json`/`Cargo.toml` — die im Repository eingecheckten Versionsdateien
sind nur eine Basislinie, nicht die tatsächliche Freigabeversion. Das fertige
Paket liegt danach unter
`apps/desktop/src-tauri/target/release/bundle/appimage/`. Um es wie die
API es ausliefern würde einzusammeln:
```bash
sh .gitea/scripts/desktop-collect.sh --require linux
```
Das schreibt `desktop-dist/` (per `.gitignore` vom Git ausgeschlossen, bis auf
einen Platzhalter) samt `manifest.json`. Starten Sie danach den lokalen
Docker-Stack (`docker compose build api` genügt für die API allein), liefert
`GET /desktop/latest` die dort abgelegten Pakete aus.
**Der Windows-Installer wird nur im CI gebaut** (`cargo-xwin`-Cross-Bau, NSIS
aus dem Ubuntu-Paket `nsis` — siehe `docs/ci-cd-setup.md`, Abschnitt 4). Lokal
genügt für Rust-Änderungen `cargo check`/`cargo clippy` in
`apps/desktop/src-tauri`; einen Windows-Installer lokal zu bauen ist nicht
vorgesehen.
## Architektur im Überblick ## Architektur im Überblick
**Frontend** (`apps/web/src/app`, Next.js App Router): **Frontend** (`apps/web/src/app`, Next.js App Router):
@@ -422,6 +470,27 @@ gegen Services, Controller-Logik und React-Komponenten. Guard-artige Spezifikati
gegen bereits einmal aufgetretene Fehler — wiederkehrende Fallstricke werden in diesem Projekt gegen bereits einmal aufgetretene Fehler — wiederkehrende Fallstricke werden in diesem Projekt
durch einen Test abgesichert, nicht nur durch einen Kommentar. durch einen Test abgesichert, nicht nur durch einen Kommentar.
**Desktop-Modul (`apps/api/src/desktop`):**
```bash
pnpm --filter @tessera/api exec vitest run src/desktop
```
Die Tests laufen als echter HTTP-Durchstich über `NestFactory.create()` +
`app.listen(0)` gegen ein echtes temporäres Verzeichnis (kein `fs`-Mock) —
Manifest lesen, 404 ohne Manifest, Plattform-Whitelist, Pfad-Traversal
abgewiesen.
**Rust (`apps/desktop/src-tauri`):**
```bash
cargo check
cargo clippy
```
Beide laufen auch im CI-Job `desktop` (D-16); ein grüner `cargo clippy` ohne
Warnungen ist Voraussetzung für den Bauschritt.
## Konventionen und Fallstricke ## Konventionen und Fallstricke
**NestJS-Routenreihenfolge:** NestJS matcht Routen in Deklarationsreihenfolge. Eine statische Route **NestJS-Routenreihenfolge:** NestJS matcht Routen in Deklarationsreihenfolge. Eine statische Route
+115 -9
View File
@@ -109,21 +109,71 @@ hardcoden.
Die CI/CD-Pipeline (`.gitea/workflows/ci.yml`) laeuft bei jedem Push auf die Die CI/CD-Pipeline (`.gitea/workflows/ci.yml`) laeuft bei jedem Push auf die
Zweige `main` und `live` sowie bei jedem Tag `v*` (z. B. `v1.0.0`) und besteht Zweige `main` und `live` sowie bei jedem Tag `v*` (z. B. `v1.0.0`) und besteht
aus drei aufeinander aufbauenden Jobs: aus vier aufeinander aufbauenden Jobs:
1. **quality** -- Lint und TypeScript Type-Check (Lint ist derzeit ein Leerlauf, 1. **quality** -- Lint und TypeScript Type-Check (Lint ist derzeit ein Leerlauf,
siehe WINDOWS #35; der Type-Check ist echt) siehe WINDOWS #35; der Type-Check ist echt)
2. **test** -- Vitest Unit- und Integrationstests 2. **test** -- Vitest Unit- und Integrationstests
3. **publish** -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry 3. **desktop** -- Desktop-Pakete fuer Windows und Linux bauen (nur auf `main`
und bei Tags `v*`, siehe unten)
4. **publish** -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry
veroeffentlichen veroeffentlichen
Ablauf: `quality` -> `test` -> `publish` (jeder Job nur bei Erfolg des Ablauf: `quality` -> `test` -> `desktop` -> `publish` (jeder Job nur bei Erfolg
vorherigen). Der Job `publish` besteht aus vier Schritten: `actions/checkout@v4` des vorherigen; `desktop` selbst laeuft nur, wenn die `if`-Bedingung zutrifft
mit `fetch-depth: 0` (volle Historie samt Tags, sonst liefert `git describe` -- auf einem Push nach `live` ohne Tag entfaellt der Job, `publish` startet in
nichts), Login in die Registry (siehe Abschnitt 3), der Aufruf von diesem Fall trotzdem, weil `needs: desktop` bei einem uebersprungenen Job nicht
`.gitea/scripts/publish-images.sh` und der Aufruf von blockiert). Der Job `publish` besteht aus sechs Schritten:
`.gitea/scripts/publish-release.sh` (legt bei Tags `v*` den Gitea-Release aus dem `actions/checkout@v4` mit `fetch-depth: 0` (volle Historie samt Tags, sonst
CHANGELOG-Abschnitt an; auf `main` endet er mit "nichts zu tun"). liefert `git describe` nichts), der Wiederherstellung der Desktop-Pakete aus
dem Zwischenspeicher, einer harten Pruefung des Manifests, 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 und haengt die Desktop-Pakete als
Dateien an; auf `main` endet er mit "nichts zu tun").
### Job `desktop`: Windows- und Linux-Pakete auf dem Linux-Runner
Der Job `desktop` baut auf demselben `ubuntu-latest`-Runner nacheinander (ein
Cache, ein Runner, siehe `18-CONTEXT.md` Specific Ideas) sowohl das
Linux-AppImage als auch -- per Cross-Bau -- den Windows-Installer:
1. **Systemabhaengigkeiten** (`apt-get install`): WebKit/Tauri-Pakete
(`libwebkit2gtk-4.1-dev` usw.) fuer den Linux-Bau, dazu `lld llvm clang
nsis` fuer den Windows-Cross-Bau (der NSIS-Bundler ruft `makensis` aus
genau diesem Paket auf).
2. **Rust-Toolchain** per `rustup` (kein Rust im Runner-Abbild), inklusive
`rustup component add clippy` -- `--profile minimal` installiert `clippy`
sonst nicht mit (siehe Fehlerbehebung unten).
3. **Cargo-Zwischenspeicher** (`actions/cache@v4`, Schluessel ueber den Hash
von `Cargo.lock`): `~/.cargo/registry`, `~/.cargo/git`,
`~/.cargo/bin/cargo-xwin`, `~/.cache/cargo-xwin` (die von `cargo-xwin`
heruntergeladene Windows-SDK-Ablage -- soll nur einmal geladen werden),
`~/.local/share/tauri` (NSIS-Plugins) und `apps/desktop/src-tauri/target`.
4. **Windows-Werkzeuge** -- bewusst NACH dem Cache-Wiederherstellungsschritt:
`rustup target add x86_64-pc-windows-msvc` und
`command -v cargo-xwin || cargo install --locked cargo-xwin`. Stuende
dieser Schritt vor der Cache-Wiederherstellung, wuerde `cargo-xwin` bei
jedem Lauf neu gebaut, selbst wenn der Cache es bereits enthaelt.
5. **Version setzen** (`desktop-version.sh`), **Rust pruefen**
(`cargo check`/`cargo clippy`, D-16), Bau des Linux-AppImage
(`tauri build --bundles appimage`), dann des Windows-Installers per
Cross-Bau (`tauri build --runner cargo-xwin --target
x86_64-pc-windows-msvc --bundles nsis`).
6. **Pakete einsammeln** (`desktop-collect.sh --require linux,windows`) --
schreibt `manifest.json` und schlaegt fehl, wenn eine der beiden Dateien
fehlt.
7. **Uebergabe an `publish`** per `actions/cache/save@v4` mit dem Schluessel
`desktop-dist-${{ gitea.sha }}` (ein neuer Schluessel je Commit, damit
`publish` garantiert die Pakete des gerade gebauten Standes bekommt, nicht
einen aelteren Cache-Treffer).
**Warum `actions/cache` und nicht `upload-artifact`:** Auf dieser
Gitea-Instanz ist `actions/upload-artifact`/`download-artifact` unzuverlaessig
(Erfahrungswert aus 18-02) -- die Uebergabe zwischen `desktop` und `publish`
laeuft deshalb bewusst ueber `actions/cache/save` und
`actions/cache/restore` mit `fail-on-cache-miss: true`, nicht ueber
Artefakt-Uploads.
Das Release-Skript spricht die Gitea-API ueber `GITHUB_API_URL` bzw. Das Release-Skript spricht die Gitea-API ueber `GITHUB_API_URL` bzw.
`GITHUB_SERVER_URL/api/v1` an -- im Job-Container ist das `GITHUB_SERVER_URL/api/v1` an -- im Job-Container ist das
@@ -237,3 +287,59 @@ und einen Branch-Schutz fuer `live` anlegen (T-KU1-04).
2. Pruefen, ob der Tag wirklich gepusht wurde: `git ls-remote --tags origin` 2. Pruefen, ob der Tag wirklich gepusht wurde: `git ls-remote --tags origin`
3. `dev` bedeutet: das Image wurde ohne Build-Args gebaut (lokal statt ueber 3. `dev` bedeutet: das Image wurde ohne Build-Args gebaut (lokal statt ueber
das Skript) -- das ist fuer lokale Builds normal das Skript) -- das ist fuer lokale Builds normal
### Job `desktop` schlaegt fehl
1. **`cargo clippy` meldet `'cargo-clippy' is not installed for the toolchain`**
-- der Schritt "Rust-Toolchain" installiert mit `--profile minimal`, das
`clippy` nicht mitbringt. Behoben durch `rustup component add clippy`
direkt nach der Toolchain-Installation (siehe oben); bei einem aehnlichen
Fehlerbild in Zukunft pruefen, ob diese Zeile noch vorhanden ist.
2. **apt-Paketname unbekannt / `pkg-config` findet eine Bibliothek nicht** --
Paketnamen aendern sich gelegentlich zwischen Ubuntu-Versionen des
Runner-Abbilds; den fehlenden `.pc`/`.so`-Namen aus der Fehlermeldung in
`apt-cache search` nachschlagen und die Paketliste im Schritt
"Systemabhaengigkeiten" ergaenzen.
3. **`openssl-sys` scheitert beim Windows-Cross-Ziel** -- OpenSSL laesst sich
fuer `x86_64-pc-windows-msvc` von Linux aus nicht ohne Weiteres
cross-kompilieren; falls eine neue Abhaengigkeit das ueber `openssl-sys`
statt `rustls-tls` einzieht, das Feature/die Abhaengigkeit auf
`rustls-tls` umstellen (in diesem Job bislang nicht aufgetreten, `reqwest`
ist bereits auf `rustls-tls` konfiguriert).
4. **NSIS-Plugin-Download schlaegt fehl** -- Tauris NSIS-Bundler laedt beim
ersten Bau zusaetzliche Plugins nach `~/.local/share/tauri`; ein
Netzwerkfehler dort bricht den Bauschritt "Windows-Installer bauen
(Cross-Bau)" ab. Lauf erneut anstossen; bleibt der Cache warm, entfaellt
der Download beim naechsten Mal.
5. **Runner-Speicher/-Zeit reicht nicht** -- der Rust-Bau laeuft auf einem
begrenzten Runner (8 Kerne/15 GB, siehe `18-CONTEXT.md`); bei
Speicherdruck `CARGO_BUILD_JOBS` (z. B. auf `4`) als Umgebungsvariable im
Job setzen, um die parallele Uebersetzung zu drosseln.
### `publish`: cache miss
`actions/cache/restore@v4` mit `fail-on-cache-miss: true` bricht den Job
`publish` hart ab, wenn kein Eintrag unter dem Schluessel
`desktop-dist-${{ gitea.sha }}` existiert. Wahrscheinlichste Ursachen:
1. Der Job `desktop` ist fehlgeschlagen oder uebersprungen worden (siehe
`if`-Bedingung oben) -- im Gitea-Actions-Lauf pruefen, ob `desktop`
tatsaechlich gruen war.
2. Der Zwischenspeicher-Server des `act_runner` ist nicht erreichbar oder
nicht aktiviert -- Runner-Konfiguration pruefen (Abschnitt 2,
`[cache] enabled` muss gesetzt sein).
3. Der Commit-SHA im Schluessel weicht zwischen den Jobs ab (sollte bei
`gitea.sha` innerhalb desselben Laufs nicht vorkommen) -- bei Verdacht die
Job-Logs beider Schritte (`Uebergabe an publish` in `desktop`,
`Desktop-Pakete aus dem Zwischenspeicher holen` in `publish`) auf den
verwendeten Schluessel vergleichen.
### Release-Upload 413
Schlaegt der Datei-Upload in `publish-release.sh` mit HTTP 413 (Datei zu
gross) fehl, blockt vermutlich der vorgeschaltete Proxy vor `git.vicolab.de`
den grossen Installer-Upload -- dasselbe bekannte Verhalten wie beim
Image-Push (Abschnitt 3). Abhilfe: `GITEA_API` auf die Host-Adresse
`http://172.18.0.1:3002/api/v1` setzen, damit der Release-Upload denselben
Weg wie der Registry-Push nimmt und den Proxy umgeht -- nur noetig, wenn der
Proxy die Groesse tatsaechlich abweist.