Files
tessera-ctl/.planning/phases/06-desktop-client-ci-cd/06-02-PLAN.md
T
2026-06-25 09:25:24 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
06-desktop-client-ci-cd 02 execute 2
06-01
apps/desktop/src-tauri/src/lib.rs
apps/desktop/src-tauri/Cargo.toml
apps/desktop/src-tauri/tauri.conf.json
apps/desktop/src-tauri/capabilities/default.json
apps/desktop/src-tauri/icons/icon.png
apps/desktop/src-tauri/icons/icon.ico
apps/desktop/src-tauri/icons/128x128.png
apps/api/src/health/health.controller.ts
false
DESK-01
DESK-02
truths artifacts key_links
Clicking the window close (X) hides the app to the system tray instead of quitting
The system tray icon shows a context menu with Oeffnen and Beenden; Beenden actually exits the app
Window size and position are saved on close and restored on next launch (first run 1280x800 centered)
An Autostart toggle exists and registers/unregisters the app with the OS, defaulting to disabled
On startup the app fetches the server /version endpoint and shows a native notification when a newer version is available
The app uses a Tessera-branded icon derived from the OKLCH design system
Production bundles build for Linux (AppImage) and Windows (NSIS)
path provides contains
apps/desktop/src-tauri/src/lib.rs Tray, close-to-tray, plugin registration, version-check logic TrayIconBuilder
path provides contains
apps/api/src/health/health.controller.ts GET /health/version endpoint returning app version version
path provides
apps/desktop/src-tauri/icons/icon.png Tessera app + tray icon
from to via pattern
apps/desktop/src-tauri/src/lib.rs main window CloseRequested event window.hide() + api.prevent_close() prevent_close
from to via pattern
apps/desktop/src-tauri/src/lib.rs apps/api GET /health/version HTTP fetch of {server_url}/health/version on startup version
Layer all native desktop behaviors onto the working URL-loading wrapper from Plan 06-01: system tray with close-to-tray (D-03), window state persistence (D-05), autostart toggle (D-04), native notifications + startup version check (D-01, D-08), Tessera-branded icon (D-06), and production bundle targets AppImage + NSIS (D-07). Also add the API `/health/version` endpoint the version check depends on. Code signing is intentionally NOT configured (D-09 — deferred to external-sales stage).

Purpose: Completes DESK-01/DESK-02 — a user can install a real, well-behaved Tessera desktop app that lives in the tray, remembers its window, optionally autostarts, and warns about updates.

Output: A buildable Tauri app producing unsigned AppImage (Linux) and NSIS (Windows) installers with full native integration, plus a /health/version API endpoint.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-desktop-client-ci-cd/06-CONTEXT.md @.planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md @.planning/phases/06-desktop-client-ci-cd/06-01-SUMMARY.md @apps/api/src/health/health.controller.ts @apps/web/src/app/globals.css

<artifacts_this_phase_produces>

  • apps/api GET /health/version → { version, name } (consumed by desktop version check)
  • apps/desktop/src-tauri/src/lib.rs::run() — extended with tray, close-to-tray, 4 plugins, version check
  • apps/desktop/src-tauri/icons/{icon.png,icon.ico,128x128.png} — branded app/tray icons
  • Autostart toggle UI hook (invoked from within the loaded web app via tauri-plugin-autostart API) </artifacts_this_phase_produces>
Task 1: Add GET /health/version endpoint to the API apps/api/src/health/health.controller.ts - apps/api/src/health/health.controller.ts (existing controller, @Public decorator pattern, HealthResponse import) - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Version Check Endpoint code example) - GET /health/version returns HTTP 200 with JSON `{ version: string, name: 'tessera' }` - The endpoint is public (no auth required) — reachable by the desktop shell before login - version resolves from `process.env.npm_package_version` falling back to '0.0.1' Extend the existing `HealthController` with a `@Public() @Get('version')` handler `getVersion()` returning an object `{ version: process.env.npm_package_version ?? '0.0.1', name: 'tessera' }`. Reuse the existing `@Public` decorator import already present in the file. If a `HealthResponse`-style shared type is desired, define a small inline return type or extend `@tessera/shared`; keep it minimal. Mark the route public so the desktop version check (D-08) can reach it pre-auth. cd apps/api && pnpm type-check && grep -q "'version'" src/health/health.controller.ts && grep -q "name: 'tessera'" src/health/health.controller.ts - `GET /health/version` handler exists, is decorated `@Public()`, returns `{ version, name: 'tessera' }` - `pnpm --filter=@tessera/api type-check` passes API exposes a public /health/version returning the app version; type-check green Task 2: Tray, close-to-tray, window-state, autostart, notification, version check in Rust apps/desktop/src-tauri/Cargo.toml, apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/capabilities/default.json - apps/desktop/src-tauri/src/lib.rs (the run() function from 06-01 — extend, do not rewrite the store/navigate logic) - apps/desktop/src-tauri/Cargo.toml (existing deps from 06-01) - apps/desktop/src-tauri/capabilities/default.json (existing permissions from 06-01) - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Pattern 2 Close-to-Tray full Rust example, Pitfall 2 both CloseRequested + ExitRequested, Capabilities Configuration, Version Check Rust example, A6 reqwest assumption) Extend `Cargo.toml` dependencies with `tauri-plugin-notification = "2"`, `tauri-plugin-autostart = "2"`, `tauri-plugin-window-state = "2"`, and `reqwest = { version = "0.12", features = ["json"] }` (per A6; if reqwest proves problematic, fall back to JS-side fetch — note the fallback in SUMMARY). Keep the existing `tauri = { features = ["tray-icon"] }`.
Extend `lib.rs::run()` registering all four plugins on the builder: `tauri_plugin_store` (already present),
`tauri_plugin_notification::init()`, `tauri_plugin_window_state::Builder::default().build()`, and
`tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, None)`. Window-state plugin
auto-handles D-05 save/restore; the 1280x800 centered first-run default already lives in tauri.conf.json.

In `.setup`, build the tray (D-03): a `MenuBuilder` with items `open` ("Oeffnen") and `quit` ("Beenden")
separated by a separator. Create `TrayIconBuilder` with the bundled icon bytes (icon.png from Task 3),
tooltip "Tessera", `show_menu_on_left_click(false)`. `on_menu_event`: "open" → show + set_focus the `main`
window; "quit" → `app.exit(0)`. `on_tray_icon_event`: left-click Up → show + focus `main` window.

Add close-to-tray (D-03, Pitfall 2): `.on_window_event` handling `WindowEvent::CloseRequested` by calling
`window.hide()` then `api.prevent_close()`. ALSO handle the exit path so the process stays alive when the
last window hides — implement the `RunEvent::ExitRequested` guard (call `api.prevent_exit()`) via
`.build(...).run(|_app, event| ...)` so the only real exit path is the tray "Beenden" → `app.exit(0)`.

Add the version check (D-08): after reading server_url in setup, spawn an async task that GETs
`{server_url}/health/version`, parses JSON `version`, compares against the app's own
`env!("CARGO_PKG_VERSION")`; if different/newer, send a native notification via the notification plugin with a
German body (e.g. title "Tessera Update", body "Eine neue Version ist verfuegbar."). Download stays manual.

Extend `capabilities/default.json` permissions to add: `notification:default`,
`notification:allow-is-permission-granted`, `notification:allow-request-permission`, `notification:allow-notify`,
`autostart:allow-enable`, `autostart:allow-disable`, `autostart:allow-is-enabled`, `window-state:default`
(keep existing `core:default`, `store:default`). These grant the loaded web app (D-04 autostart toggle) and
Rust side the needed plugin access.
cd apps/desktop/src-tauri && grep -q "TrayIconBuilder" src/lib.rs && grep -q "prevent_close" src/lib.rs && grep -q "prevent_exit" src/lib.rs && grep -q "health/version" src/lib.rs && grep -q "autostart:allow-enable" capabilities/default.json && grep -q "window-state:default" capabilities/default.json && cargo check - `cargo check` passes with all four plugins registered - lib.rs handles BOTH `prevent_close` (CloseRequested) AND `prevent_exit` (ExitRequested) per Pitfall 2 - lib.rs fetches `{server_url}/health/version` and notifies on version mismatch - capabilities grants autostart + notification + window-state permissions cargo check passes; tray + close-to-tray + window-state + autostart + version-check wired; D-01/03/04/05/08 covered in code Task 3: Generate Tessera-branded icon and configure unsigned AppImage + NSIS bundles apps/desktop/src-tauri/icons/icon.png, apps/desktop/src-tauri/icons/icon.ico, apps/desktop/src-tauri/icons/128x128.png, apps/desktop/src-tauri/tauri.conf.json - apps/desktop/src-tauri/tauri.conf.json (existing bundle config from 06-01 — targets already ["appimage","nsis"]) - apps/web/src/app/globals.css (OKLCH tokens: --primary oklch(0.91 0.19 102) yellow, dark bg oklch(0.17 0.01 260) — icon palette per D-06) - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (bundle targets, NSIS over MSI rationale, Pitfall A5 icon format) Create a Tessera app icon (D-06) derived from the OKLCH design system: a simple square mark using primary yellow `oklch(0.91 0.19 102)` on the dark `oklch(0.17 0.01 260)` background (e.g. a stylized "T" or tessera tile). Author a master 1024x1024 PNG (place a source SVG/PNG under `apps/desktop/src-tauri/icons/` or `scratchpad`), then generate the platform icon set with `pnpm --filter=@tessera/desktop tauri icon `, which produces `icon.png`, `icon.ico`, `128x128.png`, and the platform variants Tauri expects. If `tauri icon` cannot run headless, fall back to producing icon.png (1024), 128x128.png, and icon.ico manually with ImageMagick (`convert`) and note it in SUMMARY.
Confirm `tauri.conf.json` `bundle.icon` references the generated `icons/icon.png` and `icons/icon.ico`, and
`bundle.targets` is `["appimage", "nsis"]` (D-07 AppImage primary; NSIS chosen over MSI per RESEARCH so the
Windows installer can cross-compile from Linux). Ensure the tray icon bytes referenced in lib.rs (Task 2)
point at an existing icon file (e.g. `icons/128x128.png` or `icons/icon.png`).

Per D-09, do NOT configure code signing — internal use ships unsigned by design; signing is deferred until
Tessera is sold externally. Leave all `bundle` signing/certificate fields unset so builds produce unsigned
AppImage and NSIS artifacts.
test -f apps/desktop/src-tauri/icons/icon.png && test -f apps/desktop/src-tauri/icons/icon.ico && grep -q '"appimage"' apps/desktop/src-tauri/tauri.conf.json && grep -q '"nsis"' apps/desktop/src-tauri/tauri.conf.json && file apps/desktop/src-tauri/icons/icon.png | grep -qi png - `icons/icon.png` and `icons/icon.ico` exist and are valid image files - `bundle.targets` includes both `appimage` and `nsis` - Icon palette uses the OKLCH primary/dark colors from the design system - No code-signing/certificate fields configured (D-09 unsigned for internal use) Branded icon set generated; bundle configured for unsigned AppImage + NSIS Task 4: Build the desktop app and verify native behaviors - .planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md (Validation Architecture — desktop smoke tests for D-02/D-03/D-05; Pitfall 2 warning signs) The full native desktop wrapper: tray, close-to-tray, window-state, autostart, notifications, version check, branded icon, AppImage + NSIS bundles. Tauri desktop behavior cannot be unit-tested (RESEARCH Wave 0 Gaps) — it requires manual smoke verification of OS-level integration.
Before this checkpoint the executor runs (and reports output of):
- `pnpm --filter=@tessera/api type-check` and `pnpm --filter=@tessera/web type-check` (regression)
- `cd apps/desktop && pnpm tauri build` — produces the Linux AppImage (Windows NSIS may require a Windows
  runner / cross toolchain; if NSIS cross-build is unavailable on this Linux host, report it and treat the
  Windows target as build-config-verified only, AppImage as the proven artifact for DESK-01)
With the dev web stack running, launch the app (`pnpm --filter=@tessera/desktop tauri dev` or the built AppImage): 1. First run shows the German setup page; enter `http://localhost:3000` → Tessera web app loads (DESK-02) 2. Click the window X → window disappears but a Tessera tray icon remains; the process is still alive (D-03) 3. Right-click tray → "Oeffnen" restores the window; "Beenden" fully exits the process (D-03) 4. Resize/move the window, close to tray and quit, relaunch → window restores prior size/position (D-05) 5. Confirm an Autostart toggle is reachable and defaults to off (D-04) 6. If the server reports a different /health/version, a native update notification appears (D-08) 7. Confirm `apps/desktop/src-tauri/target/release/bundle/appimage/*.AppImage` exists (DESK-01) - Setup → server URL → web app loads (DESK-02) - X hides to tray, process alive; "Beenden" exits (D-03) - Window state restored on relaunch (D-05) - An AppImage artifact is produced (DESK-01) Type "approved" or describe which behavior failed

<threat_model>

Trust Boundaries

Boundary Description
desktop shell → API /health/version Desktop fetches version info from the configured server over HTTP
loaded web app → autostart plugin The web UI (D-04 toggle) invokes OS-level autostart registration via granted capability

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-04 Spoofing /health/version response accept Version string is non-sensitive; worst case is a spurious update notice. No action taken on it (manual download only, D-08)
T-06-05 Elevation autostart capability exposed to WebView accept Internal-only app loading a trusted self-hosted server; autostart enable/disable is the only granted surface, default disabled (D-04)
T-06-06 Denial of Service startup version fetch blocks UI mitigate Run the version check in a spawned async task; never block window display on the HTTP call
T-06-10 Spoofing unsigned installer artifacts (D-09) accept Code signing deferred for internal-only distribution; revisit before external sales. Documented, time-bound risk
T-06-SC Tampering cargo/npm installs mitigate All Tauri plugins [Approved] in RESEARCH Package Legitimacy Audit; reqwest is a mainstream crate, pin to 0.12
</threat_model>
- `pnpm --filter=@tessera/api type-check` and web type-check pass (no regression) - `apps/desktop/src-tauri` `cargo check` passes with all plugins - `pnpm --filter=@tessera/desktop tauri build` produces an AppImage - Manual smoke test (Task 4) confirms tray, close-to-tray, window-state, version notification

<success_criteria>

  • Close-to-tray, tray menu, window-state, autostart, notification + version check all functioning (D-01/03/04/05/08)
  • Branded icon present (D-06); unsigned AppImage + NSIS bundle targets configured (D-07, D-09)
  • API exposes /health/version (supports D-08)
  • DESK-01 (installable Windows/Linux wrapper) and DESK-02 (connects to web backend) satisfied </success_criteria>
Create `.planning/phases/06-desktop-client-ci-cd/06-02-SUMMARY.md` when done.