--- phase: 18-desktop-client-fertigstellen plan: 01 subsystem: infra tags: [nestjs, tauri, gitea-actions, streamable-file, desktop-distribution] # Dependency graph requires: - phase: 06-desktop-client-ci-cd provides: Tauri-Grundgeruest (apps/desktop, AppImage+NSIS-Bundle-Ziele, Tray, Setup-Seite) provides: - .gitea/scripts/desktop-collect.sh (Pakete einsammeln, manifest.json schreiben) - .gitea/scripts/desktop-version.sh (Version aus dem Freigabe-Tag schreiben) - apps/api/src/desktop/ (GET /desktop/latest, GET /desktop/download/:platform, beide @Public()) - packages/shared DesktopPlatform/DesktopManifest(File)/DesktopLatest(Response) Typen - apps/api/Dockerfile mit COPY desktop-dist - Basislinie 1.1.0 in tauri.conf.json/Cargo.toml/Cargo.lock/package.json affects: [18-02-ci-pipeline-release-assets, 18-03-web-oberflaeche, 18-04-client-updateprüfung] actuals: tokens: 6718 tasks: 2 commits: 2 plan_head_before: 0e4eb9b tech-stack: added: [] patterns: - "NestJS StreamableFile fuer grosse Downloads statt res.send(buffer) (Installer-Groessenordnung)" - "Manifest-getriebene Dateiauswahl: Dateiname kommt ausschliesslich aus manifest.json, nie aus dem Request-Pfad (Whitelist vor Dateisystemzugriff)" - "HTTP-Durchstich-Spec ueber NestFactory.create() + app.listen(0) statt fs-Mocks fuer datei-lesende Module" key-files: created: - .gitea/scripts/desktop-collect.sh - .gitea/scripts/desktop-version.sh - apps/api/src/desktop/desktop.module.ts - apps/api/src/desktop/desktop.controller.ts - apps/api/src/desktop/desktop.service.ts - apps/api/src/desktop/desktop.service.spec.ts - desktop-dist/.gitkeep modified: - .gitignore - apps/api/Dockerfile - apps/api/src/app.module.ts - packages/shared/src/index.ts - apps/desktop/package.json - apps/desktop/src-tauri/Cargo.toml - apps/desktop/src-tauri/Cargo.lock - apps/desktop/src-tauri/tauri.conf.json key-decisions: - "DesktopController braucht @Inject(DesktopService) explizit auf dem Konstruktor-Parameter — Vitest transpiliert ueber esbuild, das emitDecoratorMetadata nicht abbildet; ohne den expliziten Token bleibt desktopService bei einem echten NestFactory-Bau (der HTTP-Durchstich-Test) undefined, obwohl derselbe Code unter tsc (nest build) korrekt aufgeloest wuerde." - "Lokaler Stack am Ende beider Tasks zweimal neu gebaut (einmal je Task) statt nur einmal am Schluss, damit jede Verify-Stufe gegen den tatsaechlich damals gueltigen desktop-dist-Inhalt prueft und der Stack in einem konsistenten 1.1.0-Endzustand stehen bleibt." patterns-established: - "PLATFORMS-Konstante (geschlossener Wertevorrat) vor jedem Dateisystemzugriff pruefen, danach erst das Manifest lesen — Reihenfolge ist die Sicherheitseigenschaft (T-18-01)." requirements-completed: [DESK-01, DESK-03, DESK-05] coverage: - id: D1 description: "GET /desktop/latest liefert Version/Kanal/Dateiliste aus manifest.json (200) oder 404 ohne Manifest" requirement: "DESK-03" verification: - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 1 (latest, Manifest vorhanden)" status: pass - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 2 (getLatest ohne Manifest)" status: pass - kind: other ref: "curl -sf http://localhost:3001/desktop/latest (lokaler Docker-Stack, neu gebautes Abbild)" status: pass human_judgment: false - id: D2 description: "GET /desktop/download/:platform streamt die Datei mit attachment-Header, Whitelist vor Dateisystemzugriff, Traversal/unbekannte Plattform enden mit 400, fehlende Pakete/Namen mit 404" requirement: "DESK-03" verification: - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 3 (download/linux)" status: pass - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 4 (Plattform-Whitelist + Traversal ueber HTTP)" status: pass - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 5 (Whitelist vor Dateisystem)" status: pass - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 6 (Manifest nur mit windows)" status: pass - kind: integration ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 7 (manipulierter Name im Manifest)" status: pass - kind: other ref: "curl -sI http://localhost:3001/desktop/download/linux (lokaler Docker-Stack)" status: pass human_judgment: false - id: D3 description: "Beide Routen tragen @Public() (kein Anmelde-Zwang)" requirement: "DESK-03" verification: - kind: unit ref: "apps/api/src/desktop/desktop.service.spec.ts#Test 8 (bewusst oeffentlich)" status: pass human_judgment: false - id: D4 description: "desktop-collect.sh sammelt das Tauri-AppImage ein, benennt es kanonisch um und schreibt manifest.json mit korrekter Groesse/SHA-256" requirement: "DESK-01" verification: - kind: other ref: "sh .gitea/scripts/desktop-collect.sh --require linux + sha256sum-Vergleich gegen manifest.json (zweimal ausgefuehrt: 0.0.1 und 1.1.0)" status: pass human_judgment: false - id: D5 description: "desktop-version.sh schreibt die reine X.Y.Z-Version des letzten Freigabe-Tags in tauri.conf.json/Cargo.toml, verweigert Vorab-/Metadatenformen" requirement: "DESK-05" verification: - kind: other ref: "sh .gitea/scripts/desktop-version.sh --print + Negativproben (v1.2.3-beta abgelehnt, v2.0.0 akzeptiert-aber-ungeschrieben)" status: pass human_judgment: false - id: D6 description: "Basislinie 1.1.0 in allen vier Client-Dateien eingecheckt, cargo check bleibt gruen" requirement: "DESK-05" verification: - kind: other ref: "cargo check (apps/desktop/src-tauri) -> Finished" status: pass human_judgment: false duration: 13min completed: 2026-09-16 status: complete --- # Phase 18 Plan 01: Desktop-Paket-Durchstich (Skript -> Abbild -> API) Summary **Linux-AppImage aus dem Tauri-Bau wird per neuem `.gitea/scripts/desktop-collect.sh` unter kanonischem Namen samt `manifest.json` eingesammelt, vom neu gebauten API-Abbild (`apps/api/src/desktop/`) ohne Anmeldung ausgeliefert (`GET /desktop/latest`, `GET /desktop/download/linux`), und die Client-Version stammt ab sofort aus dem Freigabe-Tag (`desktop-version.sh`, Basislinie 1.1.0).** ## Performance - **Duration:** 13 min - **Started:** 2026-09-16T13:58:00Z (geschaetzt) - **Completed:** 2026-09-16T14:11:25Z - **Tasks:** 2 - **Files modified:** 15 ## Accomplishments - Neues API-Modul `apps/api/src/desktop/` mit `GET /desktop/latest` (200 mit Version/Kanal/Dateiliste, 404 ohne Manifest) und `GET /desktop/download/:platform` (Stream mit `Content-Disposition: attachment`, Plattform-Whitelist vor jedem Dateisystemzugriff, Traversal/unbekannte Plattform -> 400, fehlende Pakete/manipulierte Namen -> 404) — 8 gruene Spec-Tests via echtem HTTP-Durchstich (`NestFactory.create` + `app.listen(0)`, kein `fs`-Mock). - `.gitea/scripts/desktop-collect.sh` sammelt das gebaute AppImage ein, benennt es kanonisch (`Tessera-{Version}{Suffix}.AppImage`) und schreibt `manifest.json` (Version, Kanal, Commit, Groesse, SHA-256) — Kanalmodell identisch zu `publish-images.sh` (main=beta, Tag=live, sonst dev); Windows-Zweig bereits angelegt, aber in diesem Plan noch nicht gefordert (kommt in 18-05). - `.gitea/scripts/desktop-version.sh` schreibt die reine `X.Y.Z`-Version des letzten Freigabe-Tags in `tauri.conf.json`/`Cargo.toml`, verweigert jede Vorab-/Metadatenform (Pitfall 2 — NSIS-Ressourcen sind rein numerisch); Basislinie `1.1.0` (aktueller Tag `v1.1.0`) in allen vier Client-Dateien eingecheckt, `cargo check` bleibt gruen. - Lokaler Durchstich zweimal bewiesen: einmal mit dem Phase-6-AppImage (Version 0.0.1) fuer Task 1, einmal mit einem frisch gebauten AppImage (Version 1.1.0, Task 2) — beide Male liefert das neu gebaute API-Abbild die Datei ueber `/desktop/download/linux` und `/api-proxy/desktop/latest` (Web-Container) korrekt aus. Der lokale Stack steht am Ende auf der finalen 1.1.0-Baseline. ## Task Commits Each task was committed atomically: 1. **Task 1: Ein Linux-Paket aus dem Bau bis zum Download aus der API — eine Strecke** - `ae8fecb` (feat) 2. **Task 2: Die Version kommt aus dem Freigabe-Tag — Skript und Basislinie 1.1.0** - `614289a` (feat) **Plan metadata:** commit pending (this SUMMARY + STATE.md/ROADMAP.md/REQUIREMENTS.md) ## Files Created/Modified - `.gitea/scripts/desktop-collect.sh` - Pakete einsammeln, umbenennen, `manifest.json` schreiben (Kanalmodell, `--require linux[,windows]`) - `.gitea/scripts/desktop-version.sh` - Version aus dem letzten Freigabe-Tag in `tauri.conf.json`/`Cargo.toml` schreiben, `--print`-Option - `.gitignore` - `desktop-dist/*` ignoriert, `!desktop-dist/.gitkeep` als versionierter Platzhalter - `apps/api/Dockerfile` - `COPY desktop-dist ./desktop-dist` vor `USER nestjs` - `apps/api/src/app.module.ts` - `DesktopModule` registriert (hinter `HealthModule`) - `apps/api/src/desktop/desktop.module.ts` - Modul-Verdrahtung (Vorbild `health.module.ts`) - `apps/api/src/desktop/desktop.controller.ts` - `GET /desktop/latest`, `GET /desktop/download/:platform`, beide `@Public()`, `@Inject(DesktopService)` explizit - `apps/api/src/desktop/desktop.service.ts` - Manifest lesen, `PLATFORMS`-Whitelist, Datei-Stream, 6-stufige Sicherheitspruefung in `getPackage()` - `apps/api/src/desktop/desktop.service.spec.ts` - HTTP-Durchstich-Spec (8 Tests, echtes Temp-Verzeichnis, unabhaengig berechneter SHA-256) - `packages/shared/src/index.ts` - `DesktopPlatform`, `DesktopManifestFile`, `DesktopManifest`, `DesktopLatestFile`, `DesktopLatestResponse` - `desktop-dist/.gitkeep` - Platzhalter, damit `docker build` auch ohne CI-Pakete funktioniert - `apps/desktop/package.json`, `apps/desktop/src-tauri/tauri.conf.json`, `apps/desktop/src-tauri/Cargo.toml`, `apps/desktop/src-tauri/Cargo.lock` - Basislinie `1.1.0` ## Decisions Made - `@Inject(DesktopService)` explizit auf dem Controller-Konstruktor gesetzt, weil Vitest ueber esbuild transpiliert (kein `emitDecoratorMetadata`) — ohne den expliziten Token bleibt die Abhaengigkeit im echten `NestFactory.create()`-Durchstich `undefined`, obwohl `nest build` (tsc) denselben Code ohne `@Inject()` korrekt aufloest. Kein Verhaltensunterschied im Produktionsbau, nur eine Testinfrastruktur-Notwendigkeit fuer den in RESEARCH/PATTERNS vorgeschlagenen echten HTTP-Durchstich ohne `fs`-Mocks. - Lokaler Docker-Stack (API-Container) wurde zweimal neu gebaut — einmal je Task — statt nur am Ende, damit jede der drei automatisierten ``-Stufen tatsaechlich gegen den zu diesem Zeitpunkt gueltigen `desktop-dist`-Inhalt prueft, und der Stack am Ende in einem konsistenten 1.1.0-Zustand stehen bleibt (nicht mit der Task-1-Zwischenversion 0.0.1). - Testobjekt fuer Task 1: das bereits vorhandene Phase-6-AppImage (`Tessera_0.0.1_amd64.AppImage`, 106.461.688 Bytes, SHA-256 `ea5e1ef5...`) wurde direkt verwendet, wie im Plan als zulaessige Abkuerzung vorgesehen ("Liegt ... noch das AppImage aus Phase 6, reicht es fuer diesen Durchstich"). Fuer Task 2 war ein frischer Bau mit der neuen Version 1.1.0 zwingend (Basislinie-Nachweis). ## AppImage-Baudaten (Auftrag des Output-Abschnitts) - **Task 1 (Testobjekt Phase-6-AppImage, kein frischer Bau):** `Tessera_0.0.1_amd64.AppImage`, 106.461.688 Bytes, SHA-256 `ea5e1ef56c282009ab8c20adbf84dbdb8b3fc29e777884817d50c7ad44bfb0ec` (Build-Datum 25. Juni, aus einer fruaheren Sitzung — nicht in dieser Sitzung neu gebaut). - **Task 2 (frischer Bau mit Basislinie 1.1.0):** `Tessera_1.1.0_amd64.AppImage`, 106.928.632 Bytes, SHA-256 `da38fd89ced60c91e4a32929f43dfdc1435efdc8b668cddb2f47c9348010fcb4`. `pnpm --filter @tessera/desktop exec tauri build --bundles appimage` lief bei warmem `target/`-Verzeichnis (nach Entfernen des alten `bundle/`-Ordners) — Rust-Kompilierung 40,99 s laut `cargo`-Ausgabe, Gesamtlauf (inkl. Bundling) rund 2,5 Minuten Wanduhrzeit (14:06:56Z Start bis 14:09:39Z Manifest-Buildzeit). ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] `@Inject(DesktopService)` noetig fuer den HTTP-Durchstich-Test unter Vitest** - **Found during:** Task 1 (erster Testlauf von `desktop.service.spec.ts`) - **Issue:** Alle 5 HTTP-abhaengigen Tests scheiterten mit 500 ("Cannot read properties of undefined (reading 'getLatest')"). Ursache: Vitest transpiliert `.ts`-Dateien ueber esbuild, das `emitDecoratorMetadata` (TypeScript-Compiler-Feature) nicht abbildet — NestJS' automatische Konstruktor-Injection stuetzt sich normalerweise auf die von `tsc` erzeugten `design:paramtypes`-Metadaten, die unter esbuild fehlen. Ein echter `NestFactory.create()`-Bau (wie ihn RESEARCH/PATTERNS fuer den fs-mock-freien Test vorschlagen) konnte `DesktopService` deshalb nicht automatisch in `DesktopController` injizieren. - **Fix:** Expliziten Injection-Token per `@Inject(DesktopService)` auf dem Konstruktor-Parameter ergaenzt — das macht die Abhaengigkeit unabhaengig von `design:paramtypes` explizit und funktioniert sowohl unter Vitest/esbuild als auch im echten `nest build` (tsc) unveraendert. - **Files modified:** `apps/api/src/desktop/desktop.controller.ts` - **Verification:** Alle 8 Spec-Tests gruen nach der Aenderung (`pnpm --filter @tessera/api exec vitest run src/desktop`). - **Committed in:** `ae8fecb` (Task 1 commit) --- **Total deviations:** 1 auto-fixed (1 blocking) **Impact on plan:** Notwendig, um den vom Plan geforderten fs-mock-freien HTTP-Durchstich-Test ueberhaupt lauffaehig zu machen. Keine Verhaltensaenderung im Produktionscode, keine Ausweitung des Umfangs. ## Issues Encountered None. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - Die duenne Strecke Skript -> Abbild -> API ist bewiesen; 18-02 (CI-Pipeline, `desktop`-Job, `publish-release.sh`-Erweiterung) kann direkt auf `desktop-collect.sh`/`desktop-version.sh` und dem API-Modul aufbauen. - `packages/shared`-Typen (`DesktopLatestResponse` etc.) stehen fuer 18-03 (Web-Oberflaeche) und 18-04 (Client-Versionspruefung) bereit. - Kein Blocker. Der Windows-Cross-Bau (cargo-xwin, NSIS) ist NICHT Teil dieses Plans — `desktop-collect.sh` hat den Windows-Zweig bereits vorbereitet (ungetestet), 18-05 baut ihn aus und beweist ihn in der Pipeline. --- *Phase: 18-desktop-client-fertigstellen* *Completed: 2026-09-16* ## Self-Check: PASSED All created files verified on disk (`.gitea/scripts/desktop-collect.sh`, `.gitea/scripts/desktop-version.sh`, `apps/api/src/desktop/{desktop.module.ts,desktop.controller.ts,desktop.service.ts,desktop.service.spec.ts}`, `desktop-dist/.gitkeep`). All three task/plan commits found in `git log` (`ae8fecb`, `614289a`, plus this SUMMARY's own commit). All plan-level `` items re-run and passing: `pnpm --filter @tessera/api exec vitest run src/desktop` (8/8 green), `pnpm --filter @tessera/api type-check` (clean), `desktop-dist/manifest.json` at `1.1.0`/`Tessera-1.1.0.AppImage` with matching SHA-256, local Docker stack serving `/desktop/latest` and `/desktop/download/linux` (also via `/api-proxy/`), both new scripts pass `sh -n`, `desktop-version.sh` rejects a pre-release tag.