docs(18-01): Plan 01 abgeschlossen — Durchstich Skript -> Abbild -> API bewiesen
SUMMARY: 8 gruene Spec-Tests, Typpruefung fehlerfrei, lokaler Stack liefert /desktop/latest und /desktop/download/linux auf Basislinie 1.1.0 aus. Ein Deviation-Eintrag (Rule 3): @Inject(DesktopService) explizit noetig, da Vitest ueber esbuild ohne emitDecoratorMetadata transpiliert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
---
|
||||
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 `<verify>`-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*
|
||||
Reference in New Issue
Block a user