44154a4697
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
17 KiB
Markdown
285 lines
17 KiB
Markdown
---
|
|
phase: 06-desktop-client-ci-cd
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on:
|
|
- 06-01
|
|
files_modified:
|
|
- 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
|
|
autonomous: false
|
|
requirements:
|
|
- DESK-01
|
|
- DESK-02
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "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)"
|
|
artifacts:
|
|
- path: "apps/desktop/src-tauri/src/lib.rs"
|
|
provides: "Tray, close-to-tray, plugin registration, version-check logic"
|
|
contains: "TrayIconBuilder"
|
|
- path: "apps/api/src/health/health.controller.ts"
|
|
provides: "GET /health/version endpoint returning app version"
|
|
contains: "version"
|
|
- path: "apps/desktop/src-tauri/icons/icon.png"
|
|
provides: "Tessera app + tray icon"
|
|
key_links:
|
|
- from: "apps/desktop/src-tauri/src/lib.rs"
|
|
to: "main window CloseRequested event"
|
|
via: "window.hide() + api.prevent_close()"
|
|
pattern: "prevent_close"
|
|
- from: "apps/desktop/src-tauri/src/lib.rs"
|
|
to: "apps/api GET /health/version"
|
|
via: "HTTP fetch of {server_url}/health/version on startup"
|
|
pattern: "version"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<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>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Add GET /health/version endpoint to the API</name>
|
|
<files>apps/api/src/health/health.controller.ts</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<behavior>
|
|
- 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'
|
|
</behavior>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/api && pnpm type-check && grep -q "'version'" src/health/health.controller.ts && grep -q "name: 'tessera'" src/health/health.controller.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `GET /health/version` handler exists, is decorated `@Public()`, returns `{ version, name: 'tessera' }`
|
|
- `pnpm --filter=@tessera/api type-check` passes
|
|
</acceptance_criteria>
|
|
<done>API exposes a public /health/version returning the app version; type-check green</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="false">
|
|
<name>Task 2: Tray, close-to-tray, window-state, autostart, notification, version check in Rust</name>
|
|
<files>apps/desktop/src-tauri/Cargo.toml, apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/capabilities/default.json</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `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
|
|
</acceptance_criteria>
|
|
<done>cargo check passes; tray + close-to-tray + window-state + autostart + version-check wired; D-01/03/04/05/08 covered in code</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="false">
|
|
<name>Task 3: Generate Tessera-branded icon and configure unsigned AppImage + NSIS bundles</name>
|
|
<files>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</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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 <master.png>`,
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `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)
|
|
</acceptance_criteria>
|
|
<done>Branded icon set generated; bundle configured for unsigned AppImage + NSIS</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<name>Task 4: Build the desktop app and verify native behaviors</name>
|
|
<read_first>
|
|
- .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)
|
|
</read_first>
|
|
<what-built>
|
|
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)
|
|
</what-built>
|
|
<how-to-verify>
|
|
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)
|
|
</how-to-verify>
|
|
<acceptance_criteria>
|
|
- 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)
|
|
</acceptance_criteria>
|
|
<resume-signal>Type "approved" or describe which behavior failed</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
Create `.planning/phases/06-desktop-client-ci-cd/06-02-SUMMARY.md` when done.
|
|
</output>
|