diff --git a/.planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md b/.planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md
new file mode 100644
index 0000000..9febb7e
--- /dev/null
+++ b/.planning/phases/06-desktop-client-ci-cd/06-RESEARCH.md
@@ -0,0 +1,757 @@
+# Phase 6: Desktop Client & CI/CD - Research
+
+**Researched:** 2026-06-25
+**Domain:** Tauri 2.x Desktop Wrapper + Gitea Actions CI/CD
+**Confidence:** MEDIUM
+
+## Summary
+
+Phase 6 consists of two independent tracks: (A) a Tauri 2.x desktop wrapper that loads the existing Tessera web frontend via URL, and (B) Gitea-based DevOps tooling with CI/CD pipelines. The desktop app is a thin shell -- no frontend code is bundled, it simply opens a WebView pointing at the configured server URL. All desktop-specific behavior (system tray, notifications, window state, autostart) is handled by official Tauri plugins with minimal custom Rust code.
+
+The Gitea Actions CI/CD track uses GitHub Actions-compatible YAML syntax in `.gitea/workflows/`. An `act_runner` container executes pipeline jobs in Docker. The pipeline runs Lint + TypeCheck, Tests (Vitest), Docker image builds, and auto-deploys via `docker-compose pull && restart` on the same server. This is purely developer tooling -- no Git/Gitea features are exposed in the Tessera application.
+
+A critical prerequisite: **Rust toolchain and WebKitGTK development headers are NOT installed** on the current machine. These must be installed before any Tauri development or builds can proceed. The system has GTK3, librsvg2, and libayatana-appindicator3 (runtime libs), but the **-dev packages for WebKitGTK and the Rust compiler are missing**.
+
+**Primary recommendation:** Install Rust via rustup and WebKitGTK dev headers first, then scaffold `apps/desktop` with `npx tauri init`, configure `frontendDist` to the server URL, and wire up four official Tauri plugins (store, notification, autostart, window-state). For CI/CD, add `act_runner` to docker-compose and create workflow files in `.gitea/workflows/`.
+
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- **D-01:** Wrapper mit System Tray und nativen OS-Benachrichtigungen (z.B. Kalender-Erinnerungen). Braucht Backend-Integration fuer Notification-Events.
+- **D-02:** Server-URL konfigurierbar beim ersten Start. Wird lokal gespeichert. Ein Build fuer alle Umgebungen.
+- **D-03:** Beim Schliessen des Fensters (X-Button) wird in System Tray minimiert. Beenden nur ueber Tray-Kontextmenu.
+- **D-04:** Autostart-Option in Desktop-App-Einstellungen vorhanden, standardmaessig deaktiviert.
+- **D-05:** Fensterposition und -groesse werden beim Schliessen gespeichert, beim naechsten Start wiederhergestellt. Erster Start: 1280x800 zentriert.
+- **D-06:** Eigenes Tessera-App-Icon (basierend auf Design-System, OKLCH Farben).
+- **D-07:** Linux: AppImage als primaeres Format (distro-uebergreifend).
+- **D-08:** Update-Hinweis: App prueft beim Start ob neue Version verfuegbar, zeigt Notification. Download bleibt manuell. Auto-Update kann spaeter nachgeruestet werden.
+- **D-09:** Code Signing kommt spaeter -- erst relevant wenn Tessera an externe Kunden verkauft wird. Fuer interne Nutzung ohne Signierung OK.
+- **D-10:** Gitea ist reines Versionskontroll- und CI/CD-Tooling. Tessera hat keine Git/Gitea-Funktionalitaet in der App.
+- **D-11:** Claude pusht manuell bei Meilensteinen oder grossen Bugfixes nach Gitea. Kein automatischer Sync, kein zeitgesteuerter Push.
+- **D-12:** Gitea Actions CI/CD Pipeline wird bei jedem Push getriggert: Lint + TypeCheck -> Tests (Vitest) -> Docker Images bauen -> Auto-Deploy.
+- **D-13:** Deploy-Ziel ist gleicher Server wie Gitea. Pipeline macht docker-compose pull + restart.
+
+### Claude's Discretion
+- Titelleiste: Nativ vs. Custom -- basierend auf Aufwand und Design-System
+- App-Menueleiste: Kein Menu vs. minimales Menu -- basierend auf Plattform-Konventionen
+- Windows Installer-Format: MSI vs. NSIS -- basierend auf Zielgruppe (intern, spaeter extern)
+- Gitea Actions Workflow-Struktur und Stage-Konfiguration
+- Docker Image Registry: Gitea-intern vs. lokal
+
+### Deferred Ideas (OUT OF SCOPE)
+- Auto-Update mit Tauri Updater
+- Code Signing
+- macOS Support
+- Gitea Webhooks fuer externe Events
+
+
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| DESK-01 | Tauri-basierter Desktop-Wrapper fuer Windows und Linux | Tauri 2.x with `frontendDist` URL loading, AppImage (Linux) and NSIS (Windows) bundle targets |
+| DESK-02 | Desktop-App verbindet sich mit dem Web-Backend (kein eigenstaendiger Server) | `frontendDist` set to configurable external URL, tauri-plugin-store for persisting server URL |
+| INFRA-04 | Automatisierte Gitea-Integration (Commits, Pushes, Merges) | Gitea Actions with act_runner in Docker, workflow in `.gitea/workflows/`, auto-deploy via docker-compose |
+
+
+
+## Architectural Responsibility Map
+
+| Capability | Primary Tier | Secondary Tier | Rationale |
+|------------|-------------|----------------|-----------|
+| WebView shell (URL loading) | Desktop Client (Tauri/Rust) | -- | Tauri owns the native window and WebView; web content comes from the server |
+| System tray + close-to-tray | Desktop Client (Tauri/Rust) | -- | OS-level integration via TrayIconBuilder, window event interception |
+| Native notifications | Desktop Client (Tauri/Rust) | API / Backend | Tauri sends OS notifications; backend must provide notification events (future WebSocket/SSE) |
+| Server URL configuration | Desktop Client (Tauri/Rust) | -- | First-run dialog and persistent storage are purely client-side |
+| Window state persistence | Desktop Client (Tauri/Rust) | -- | tauri-plugin-window-state handles save/restore automatically |
+| Autostart | Desktop Client (Tauri/Rust) | -- | OS-level registration via tauri-plugin-autostart |
+| Version check | Desktop Client (Tauri/Rust) | API / Backend | Desktop fetches version endpoint from API; comparison logic in Rust/JS |
+| CI/CD pipeline | Infrastructure (Gitea/Docker) | -- | Gitea Actions + act_runner, entirely outside the application |
+| Docker image builds | Infrastructure (Gitea/Docker) | -- | Existing Dockerfiles reused by CI pipeline |
+| Auto-deploy | Infrastructure (Gitea/Docker) | -- | docker-compose pull + restart on same server |
+
+## Standard Stack
+
+### Core
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| Tauri | 2.x (2.11+) | Desktop shell | 5MB installer vs Electron 150MB, OS-native WebView, Rust backend. Project-locked decision [CITED: CLAUDE.md] |
+| @tauri-apps/cli | 2.11.3 | Build tooling | Official Tauri CLI for init, dev, build commands [VERIFIED: npm registry + official docs] |
+| @tauri-apps/api | 2.11.1 | JS bridge | Frontend-to-Rust IPC, tray, window management APIs [VERIFIED: npm registry + official docs] |
+| Rust toolchain | stable (1.77.2+) | Compilation | Required by Tauri; installed via rustup [CITED: v2.tauri.app/start/prerequisites/] |
+
+### Tauri Plugins
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| @tauri-apps/plugin-store | 2.4.3 | Persistent KV storage | Stores server URL, app preferences. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/store/] |
+| @tauri-apps/plugin-notification | 2.3.3 | OS notifications | Native desktop notifications for calendar reminders etc. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/notification/] |
+| @tauri-apps/plugin-autostart | 2.5.1 | System startup | Register/unregister app for OS autostart. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/autostart/] |
+| @tauri-apps/plugin-window-state | 2.4.1 | Window persistence | Auto-saves/restores window size, position, maximized state. Official plugin [VERIFIED: npm registry + v2.tauri.app/plugin/window-state/] |
+
+### CI/CD Infrastructure
+
+| Component | Version | Purpose | Why Standard |
+|-----------|---------|---------|--------------|
+| Gitea Actions | (built into Gitea) | CI/CD engine | GitHub Actions-compatible YAML, self-hosted [CITED: docs.gitea.com/usage/actions/act-runner] |
+| act_runner | latest | Workflow executor | Official Gitea runner, executes jobs in Docker containers [CITED: docs.gitea.com/usage/actions/act-runner] |
+| docker/build-push-action | v6 | Image build | Standard GitHub/Gitea action for Docker builds [ASSUMED] |
+| appleboy/ssh-action | v1 | Remote deploy | SSH into deploy target for docker-compose operations [ASSUMED] |
+
+### Alternatives Considered
+
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| NSIS (Windows) | MSI (WiX) | MSI only builds on Windows; NSIS supports cross-compilation from Linux. **Recommendation: NSIS** -- enables building Windows installer from the Linux dev machine [CITED: v2.tauri.app/distribute/windows-installer/] |
+| Native title bar | Custom title bar | Custom requires more effort and platform testing. **Recommendation: Native** -- simpler, consistent with OS, less Rust code |
+| No menu bar | Minimal menu bar | Linux desktop conventions expect a menu. **Recommendation: No menu bar** -- wrapper app has no app-specific actions; tray menu suffices |
+| Gitea internal registry | Local Docker images | Internal registry adds complexity. **Recommendation: Local builds** -- act_runner builds images on same server, no push needed. Use `docker compose build` directly |
+
+**Installation (Desktop app):**
+```bash
+# System prerequisites (Debian/Ubuntu/Trixie)
+sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
+ libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
+
+# Rust toolchain
+curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
+
+# Tauri CLI + plugins (from monorepo root)
+pnpm add -D @tauri-apps/cli --filter=@tessera/desktop
+pnpm add @tauri-apps/api @tauri-apps/plugin-store @tauri-apps/plugin-notification \
+ @tauri-apps/plugin-autostart @tauri-apps/plugin-window-state --filter=@tessera/desktop
+```
+
+## Package Legitimacy Audit
+
+| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
+|---------|----------|-----|-----------|-------------|---------|-------------|
+| @tauri-apps/cli | npm | 3+ yrs (org) | 1.78M/wk | github.com/tauri-apps/tauri | OK* | Approved -- official Tauri project, SUS flag due to recent version publish only |
+| @tauri-apps/api | npm | 3+ yrs (org) | 2.11M/wk | github.com/tauri-apps/tauri | OK* | Approved -- official Tauri project, SUS flag due to recent version publish only |
+| @tauri-apps/plugin-store | npm | 2+ yrs | 236K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
+| @tauri-apps/plugin-notification | npm | 2+ yrs | 293K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
+| @tauri-apps/plugin-autostart | npm | 2+ yrs | 108K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
+| @tauri-apps/plugin-window-state | npm | 2+ yrs | 73K/wk | github.com/tauri-apps/plugins-workspace | OK | Approved |
+
+*`@tauri-apps/cli` and `@tauri-apps/api` were flagged SUS by the legitimacy check due to recent publish dates (2026-06-17/19). These are false positives -- both are official packages from the tauri-apps organization with millions of weekly downloads and verified source repos. The "too-new" signal is triggered by the latest version publish, not the package itself.
+
+**Packages removed due to [SLOP] verdict:** none
+**Packages flagged as suspicious [SUS]:** @tauri-apps/cli and @tauri-apps/api (false positive -- official packages, override to Approved)
+
+## Architecture Patterns
+
+### System Architecture Diagram
+
+```
+Desktop App (Tauri) Server Infrastructure
++---------------------------+ +---------------------------+
+| | | |
+| Rust Backend | HTTP | Next.js (apps/web) |
+| - TrayIconBuilder |--------->| - Server Components |
+| - Window event handler | | - App Router |
+| - Plugin orchestration | | |
+| - Version check logic | +---------------------------+
+| | |
+| WebView (OS native) | | internal
+| - Loads server URL | +---------------------------+
+| - No bundled assets | | NestJS (apps/api) |
+| | | - REST API |
+| Local Storage | | - /health |
+| - tauri-plugin-store | | - /version (new) |
+| - server-url.json | +---------------------------+
+| - preferences.json | |
++---------------------------+ +---------------------------+
+ | PostgreSQL (db) |
+CI/CD Pipeline (Gitea) +---------------------------+
++---------------------------+
+| .gitea/workflows/ci.yml |
+| 1. Lint + TypeCheck |
+| 2. Tests (Vitest) |
+| 3. Docker Build |
+| 4. Deploy (compose pull) |
++---------------------------+
+ |
+ act_runner (Docker)
+ - Executes jobs
+ - Docker socket mount
+```
+
+### Recommended Project Structure
+
+```
+apps/
+├── desktop/ # NEW: Tauri desktop wrapper
+│ ├── package.json # @tessera/desktop
+│ ├── src-tauri/
+│ │ ├── Cargo.toml # Rust dependencies + Tauri plugins
+│ │ ├── tauri.conf.json # Window config, bundle targets, plugins
+│ │ ├── capabilities/
+│ │ │ └── default.json # Plugin permissions
+│ │ ├── icons/ # App icons (PNG, ICO)
+│ │ │ ├── icon.png
+│ │ │ ├── icon.ico
+│ │ │ └── 128x128.png # Tray icon
+│ │ └── src/
+│ │ ├── lib.rs # Plugin setup, tray, window events
+│ │ └── main.rs # Entry point (generated)
+│ └── src/ # Minimal JS for first-run setup page
+│ └── setup.html # Server URL configuration form
+├── web/ # Existing Next.js app
+└── api/ # Existing NestJS API
+
+.gitea/
+└── workflows/
+ └── ci.yml # CI/CD pipeline definition
+```
+
+### Pattern 1: URL-Loading WebView Wrapper
+
+**What:** Tauri app that loads an external web URL instead of bundling frontend assets.
+**When to use:** When the desktop app is a thin shell around an existing web application.
+
+```json
+// Source: v2.tauri.app/reference/config/
+// tauri.conf.json
+{
+ "build": {
+ "frontendDist": "http://localhost:3000",
+ "devUrl": "http://localhost:3000"
+ },
+ "app": {
+ "withGlobalTauri": true,
+ "windows": [
+ {
+ "label": "main",
+ "title": "Tessera",
+ "width": 1280,
+ "height": 800,
+ "center": true,
+ "decorations": true,
+ "resizable": true
+ }
+ ]
+ },
+ "bundle": {
+ "active": true,
+ "targets": ["appimage", "nsis"],
+ "icon": ["icons/icon.png", "icons/icon.ico"]
+ }
+}
+```
+
+**Key insight:** `frontendDist` set to a URL means no assets are embedded. The app must have a configurable URL mechanism -- on first launch, show a setup page (local HTML) where user enters the server URL, then store it via tauri-plugin-store and load the WebView with that URL.
+
+### Pattern 2: Close-to-Tray with System Tray
+
+**What:** Intercept window close, hide to system tray instead of quitting.
+**When to use:** Long-running desktop apps that should stay accessible via tray icon.
+
+```rust
+// Source: v2.tauri.app/learn/system-tray/ + GitHub discussions
+use tauri::{
+ image::Image,
+ menu::{MenuBuilder, MenuItem},
+ tray::TrayIconBuilder,
+ Manager, WindowEvent, RunEvent,
+};
+
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_store::Builder::new().build())
+ .plugin(tauri_plugin_notification::init())
+ .plugin(tauri_plugin_window_state::Builder::default().build())
+ .plugin(tauri_plugin_autostart::init(
+ tauri_plugin_autostart::MacosLauncher::LaunchAgent,
+ None,
+ ))
+ .setup(|app| {
+ // Build tray menu
+ let open = MenuItem::with_id(app, "open", "Oeffnen", true, None::<&str>)?;
+ let quit = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?;
+ let menu = MenuBuilder::new(app)
+ .item(&open)
+ .separator()
+ .item(&quit)
+ .build()?;
+
+ // Create tray icon
+ TrayIconBuilder::new()
+ .icon(Image::from_bytes(include_bytes!("../icons/icon.png"))?)
+ .tooltip("Tessera")
+ .menu(&menu)
+ .show_menu_on_left_click(false)
+ .on_menu_event(|app, event| match event.id.as_ref() {
+ "open" => {
+ if let Some(w) = app.get_webview_window("main") {
+ let _ = w.show();
+ let _ = w.set_focus();
+ }
+ }
+ "quit" => app.exit(0),
+ _ => {}
+ })
+ .on_tray_icon_event(|tray, event| {
+ if let tauri::tray::TrayIconEvent::Click {
+ button: tauri::tray::MouseButton::Left,
+ button_state: tauri::tray::MouseButtonState::Up,
+ ..
+ } = event
+ {
+ let app = tray.app_handle();
+ if let Some(w) = app.get_webview_window("main") {
+ let _ = w.show();
+ let _ = w.set_focus();
+ }
+ }
+ })
+ .build(app)?;
+
+ Ok(())
+ })
+ // Intercept window close -> hide to tray
+ .on_window_event(|window, event| {
+ if let WindowEvent::CloseRequested { api, .. } = event {
+ window.hide().unwrap();
+ api.prevent_close();
+ }
+ })
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
+}
+```
+
+### Pattern 3: Configurable Server URL with First-Run Setup
+
+**What:** On first launch, show a local HTML page for server URL input. Store URL persistently.
+**When to use:** When desktop app connects to a configurable server (D-02).
+
+```rust
+// In setup callback, check if server URL exists
+let store = app.store("config.json")?;
+let server_url = store.get("server_url");
+
+if let Some(url) = server_url {
+ // Load the configured server URL
+ let window = app.get_webview_window("main").unwrap();
+ window.navigate(url.as_str().unwrap().parse().unwrap());
+} else {
+ // Show local setup page (bundled HTML)
+ // User enters URL -> JS calls store.set -> navigates to URL
+}
+```
+
+```javascript
+// Source: v2.tauri.app/plugin/store/
+// setup.html JavaScript
+import { load } from '@tauri-apps/plugin-store';
+
+async function saveAndConnect(url) {
+ const store = await load('config.json', { autoSave: true });
+ await store.set('server_url', url);
+ // Navigate main window to server
+ window.location.href = url;
+}
+```
+
+### Pattern 4: Gitea Actions Multi-Stage Pipeline
+
+**What:** CI/CD pipeline triggered on push with staged quality gates.
+**When to use:** Automated build/test/deploy workflow.
+
+```yaml
+# .gitea/workflows/ci.yml
+# Source: docs.gitea.com/usage/actions/ + botmonster.com guide
+name: Tessera CI/CD
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm lint
+ - run: pnpm type-check
+
+ test:
+ runs-on: ubuntu-latest
+ needs: quality
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm test
+
+ build-deploy:
+ runs-on: ubuntu-latest
+ needs: test
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build and deploy
+ run: |
+ docker compose build web api
+ docker compose up -d web api
+```
+
+### Anti-Patterns to Avoid
+
+- **Bundling frontend assets in Tauri:** Since Tessera desktop is a wrapper, do NOT copy the Next.js build into the Tauri binary. Use `frontendDist` with a URL. This keeps the desktop app tiny and always shows current server content.
+- **Using MSI for Windows builds from Linux:** MSI (WiX) only builds on Windows. Always use NSIS for cross-platform build capability. [CITED: v2.tauri.app/distribute/windows-installer/]
+- **Docker-in-Docker for CI builds:** The act_runner already runs inside Docker. Mount the host Docker socket (`/var/run/docker.sock`) instead of running Docker inside Docker. This avoids complexity and security issues. [CITED: docs.gitea.com/usage/actions/act-runner]
+- **Complex registry setup:** Since deploy target is the same server, skip pushing to a registry. Build images locally with `docker compose build` and restart services.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Window state persistence | Manual size/position save/restore | tauri-plugin-window-state | Handles all edge cases (multi-monitor, maximized state, DPI changes) |
+| Persistent KV storage | Custom file I/O in Rust | tauri-plugin-store | Handles serialization, atomic writes, cross-platform paths |
+| System autostart | Manual registry/plist/desktop-file creation | tauri-plugin-autostart | Cross-platform (Windows registry, Linux .desktop, macOS LaunchAgent) |
+| OS notifications | Custom dbus/Win32 notification code | tauri-plugin-notification | Permission handling, cross-platform API, Tauri-integrated |
+| CI/CD workflow engine | Custom shell scripts | Gitea Actions | YAML-based, GitHub Actions compatible, Docker-native execution |
+| SSH deployment | Manual SSH scripting in CI | appleboy/ssh-action | Handles key auth, connection management, error reporting |
+
+**Key insight:** Every desktop behavior (tray, notifications, autostart, window state) has an official Tauri plugin. The custom Rust code is only ~100 lines of glue connecting these plugins with the close-to-tray behavior and first-run setup flow.
+
+## Common Pitfalls
+
+### Pitfall 1: Missing WebKitGTK Dev Headers
+**What goes wrong:** `cargo build` fails with "pkg-config could not find webkit2gtk-4.1"
+**Why it happens:** Linux runtime libraries (libwebkit2gtk) are installed but the -dev headers for compilation are not.
+**How to avoid:** Install `libwebkit2gtk-4.1-dev` via apt before any Tauri build. The current machine is missing this.
+**Warning signs:** `pkg-config --modversion webkit2gtk-4.1` returns "not found"
+
+### Pitfall 2: Tauri App Exits When Window Hidden
+**What goes wrong:** App quits entirely when user clicks X, even with close-to-tray configured.
+**Why it happens:** Tauri default behavior exits when all windows are destroyed. Hiding a window triggers destroy.
+**How to avoid:** Must handle BOTH `WindowEvent::CloseRequested` (call `api.prevent_close()` + `window.hide()`) AND potentially `RunEvent::ExitRequested` (call `api.prevent_exit()`). Both handlers are needed for reliable close-to-tray. [CITED: github.com/tauri-apps/tauri/discussions/2684]
+**Warning signs:** App process disappears from task manager after clicking X
+
+### Pitfall 3: frontendDist URL vs. Configurable URL Conflict
+**What goes wrong:** `frontendDist` in tauri.conf.json is a static URL, but the app needs a user-configurable server URL.
+**Why it happens:** `frontendDist` is a build-time config, not runtime.
+**How to avoid:** Set `frontendDist` to a local setup page (or use a custom scheme). On app start in Rust, read the stored URL from tauri-plugin-store and call `window.navigate(url)` to redirect to the actual server. The first-run page handles initial URL configuration. [ASSUMED]
+**Warning signs:** All builds hardcoded to one server URL
+
+### Pitfall 4: Gitea Actions docker/build-push-action JWT Parse Error
+**What goes wrong:** `docker/build-push-action@v4` fails because Gitea's ACTIONS_RUNTIME_TOKEN is not a JWT.
+**Why it happens:** Gitea generates a random string token, not a JWT like GitHub. The action tries to parse it as JWT.
+**How to avoid:** Use plain `docker compose build` commands instead of the build-push-action, OR use v6 which may have fixed this. Since we build locally (no registry push), plain commands are simpler anyway. [CITED: docs.gitea.com/usage/actions/comparison]
+**Warning signs:** "JWT parse error" in CI logs
+
+### Pitfall 5: act_runner Docker Socket Security
+**What goes wrong:** Malicious workflow code could access Docker daemon and inspect other containers.
+**Why it happens:** `/var/run/docker.sock` is mounted into the runner container for Docker-in-Docker builds.
+**How to avoid:** Since this is an internal-only CI (Claude pushes manually), the risk is acceptable. For extra safety, use ephemeral runners (`GITEA_RUNNER_EPHEMERAL=1`) which revoke credentials after each job. [CITED: docs.gitea.com/usage/actions/act-runner]
+**Warning signs:** Runner container has persistent access to Docker daemon
+
+### Pitfall 6: pnpm Workspace Tauri Init Confusion
+**What goes wrong:** `tauri init` or `create-tauri-app` runs from wrong directory, creates files in wrong location.
+**Why it happens:** Monorepo structure requires precise directory targeting.
+**How to avoid:** Create `apps/desktop/` manually, cd into it, run `pnpm create tauri-app .` or `npx tauri init`. Ensure package.json has `name: "@tessera/desktop"`. Add to pnpm-workspace.yaml (already covered by `apps/*` glob). [ASSUMED]
+**Warning signs:** src-tauri appears at monorepo root instead of apps/desktop/
+
+## Code Examples
+
+### First-Run Setup Page (Bundled HTML)
+
+```html
+
+
+
+
+
+ Tessera - Setup
+
+
+
+
+
Tessera
+
Server-URL eingeben:
+
+
+
+
+
+
+```
+
+### Cargo.toml Plugin Dependencies
+
+```toml
+# Source: Official Tauri plugin docs
+# apps/desktop/src-tauri/Cargo.toml
+[dependencies]
+tauri = { version = "2", features = ["tray-icon"] }
+tauri-plugin-store = "2"
+tauri-plugin-notification = "2"
+tauri-plugin-autostart = "2"
+tauri-plugin-window-state = "2"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+```
+
+### Capabilities Configuration
+
+```json
+// Source: v2.tauri.app/plugin/autostart/ + notification/ + store/
+// apps/desktop/src-tauri/capabilities/default.json
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Tessera desktop capabilities",
+ "windows": ["main"],
+ "permissions": [
+ "core:default",
+ "store:default",
+ "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"
+ ]
+}
+```
+
+### Version Check Endpoint (API Side)
+
+```typescript
+// Source: Pattern for D-08 update hint
+// apps/api/src/health/health.controller.ts (extend existing)
+@Get('version')
+getVersion() {
+ return {
+ version: process.env.npm_package_version || '0.0.1',
+ name: 'tessera',
+ };
+}
+```
+
+```rust
+// Desktop side: check version on startup
+// In setup callback after loading server URL
+async fn check_version(server_url: &str, current_version: &str) -> Option {
+ let url = format!("{}/version", server_url);
+ if let Ok(resp) = reqwest::get(&url).await {
+ if let Ok(info) = resp.json::().await {
+ let remote = info["version"].as_str().unwrap_or("0.0.0");
+ if remote != current_version {
+ return Some(remote.to_string());
+ }
+ }
+ }
+ None
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Tauri 1.x SystemTray API | Tauri 2.x TrayIconBuilder API | Oct 2024 (Tauri 2.0 stable) | New API: `tauri::tray::TrayIconBuilder`, feature flag renamed `tray-icon` [CITED: v2.tauri.app/blog/tauri-20/] |
+| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | Tauri 2.0 | Notifications moved from core API to plugin [CITED: v2.tauri.app/start/migrate/from-tauri-1/] |
+| WiX MSI only | NSIS + MSI options | Tauri 2.0 | NSIS is now the recommended Windows installer with cross-compilation support [CITED: v2.tauri.app/distribute/windows-installer/] |
+| Gitea without CI | Gitea Actions (since 1.19) | 2023 | Built-in CI/CD, no need for external Jenkins/Drone [CITED: docs.gitea.com/usage/actions/act-runner] |
+
+**Deprecated/outdated:**
+- Tauri 1.x APIs: All `SystemTray`, `CustomMenuItem`, `SystemTrayMenu` APIs replaced
+- `@tauri-apps/api/notification`: Replaced by `@tauri-apps/plugin-notification`
+- `tauri::api::shell`: Removed in Tauri 2, use `tauri-plugin-shell` instead
+
+## Assumptions Log
+
+| # | Claim | Section | Risk if Wrong |
+|---|-------|---------|---------------|
+| A1 | frontendDist can be overridden at runtime via window.navigate() for configurable URL | Architecture Patterns / Pitfall 3 | Setup flow would need different approach (custom protocol or build-time injection) |
+| A2 | appleboy/ssh-action@v1 works with Gitea Actions | Standard Stack (CI/CD) | Would need alternative SSH deployment method (plain script) |
+| A3 | docker/build-push-action@v6 compatibility with Gitea Actions | Pitfall 4 | Use plain docker commands instead (already the recommendation) |
+| A4 | pnpm create tauri-app works inside existing monorepo subdirectory | Pitfall 6 | May need manual scaffolding of src-tauri directory structure |
+| A5 | Tauri tray icon bundle format (.png) works on both Linux and Windows | Code Examples | May need separate icon formats per platform |
+| A6 | reqwest crate available for HTTP requests in Tauri Rust backend | Code Examples (version check) | Could use tauri-plugin-http or JS-side fetch instead |
+
+## Open Questions
+
+1. **Backend Notification Events (D-01)**
+ - What we know: Tauri can send OS notifications via plugin. Desktop app is a WebView wrapper.
+ - What's unclear: How the backend pushes notification events to the desktop app. WebSocket? SSE? Polling? The web app already runs in the WebView, so standard web push patterns apply.
+ - Recommendation: Use the web app's existing event mechanism (if any) or add a simple polling endpoint. Full WebSocket/SSE for notifications can be deferred since the web app itself would also benefit from it.
+
+2. **First-Run Setup Page Architecture**
+ - What we know: Need to show a local page for URL input before loading external URL.
+ - What's unclear: Whether to use a bundled HTML file, a custom Tauri protocol, or handle it entirely in Rust with a simple dialog.
+ - Recommendation: Bundle a minimal `setup.html` that uses tauri-plugin-store. Set `frontendDist` to this page. After URL is saved, navigate to the server URL.
+
+3. **Gitea Instance Access**
+ - What we know: The project has no git remotes configured. Gitea should be on the same server.
+ - What's unclear: Whether Gitea is already running, its URL, and whether Actions are enabled.
+ - Recommendation: Document Gitea setup as a prerequisite. If not running, add to docker-compose or assume existing instance.
+
+## Environment Availability
+
+| Dependency | Required By | Available | Version | Fallback |
+|------------|------------|-----------|---------|----------|
+| Rust toolchain | Tauri compilation | **NO** | -- | Must install via rustup |
+| libwebkit2gtk-4.1-dev | Tauri Linux build | **NO** | -- | Must install via apt |
+| libssl-dev | Tauri dependencies | Check needed | -- | Must install via apt |
+| libxdo-dev | Tauri Linux features | Check needed | -- | Must install via apt |
+| build-essential | Compilation | Check needed | -- | Must install via apt |
+| Node.js | Frontend tooling | YES | v24.16.0 | -- |
+| pnpm | Package manager | YES | 9.15.0 | -- |
+| Docker | Container builds | YES | 29.5.3 | -- |
+| Docker Compose | Service orchestration | YES | v5.1.4 | -- |
+| GTK3 (runtime) | Tauri runtime | YES | 3.24.49 | -- |
+| librsvg2 (runtime) | Icon rendering | YES | 2.60.0 | -- |
+| libayatana-appindicator3 (runtime) | System tray | YES | 0.5.94 | -- |
+| Gitea instance | CI/CD | Unknown | -- | Must verify or set up |
+| act_runner | CI job execution | Unknown | -- | Add to docker-compose |
+
+**Missing dependencies with no fallback:**
+- Rust toolchain (rustc, cargo) -- MUST install before any Tauri work
+- libwebkit2gtk-4.1-dev -- MUST install for Linux compilation
+- Other -dev packages (libssl-dev, libxdo-dev, build-essential) -- MUST install
+
+**Missing dependencies with fallback:**
+- Gitea instance -- if not available, can defer CI/CD track or add to docker-compose
+
+## Validation Architecture
+
+### Test Framework
+
+| Property | Value |
+|----------|-------|
+| Framework | Vitest 3.x |
+| Config file | `apps/web/vitest.config.ts` (existing) |
+| Quick run command | `pnpm --filter=@tessera/web test` |
+| Full suite command | `pnpm test` |
+
+### Phase Requirements to Test Map
+
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| DESK-01 | Tauri app builds for Linux (AppImage) and Windows (NSIS) | manual | `cd apps/desktop && pnpm tauri build` | N/A -- build verification |
+| DESK-02 | Desktop connects to web backend via URL | manual | Verify WebView loads configured URL | N/A -- runtime verification |
+| INFRA-04 | CI pipeline runs on push | integration | Push to Gitea, verify Actions run | N/A -- infrastructure verification |
+| D-02 | Server URL stored persistently | smoke | Launch app, set URL, restart, verify URL retained | N/A -- manual smoke test |
+| D-03 | Close minimizes to tray | smoke | Click X, verify tray icon, verify process alive | N/A -- manual smoke test |
+| D-05 | Window state persisted | smoke | Resize, close, reopen, verify dimensions | N/A -- manual smoke test |
+
+### Sampling Rate
+- **Per task commit:** `pnpm lint && pnpm type-check` (no Tauri-specific unit tests expected)
+- **Per wave merge:** `pnpm test` (full suite)
+- **Phase gate:** Manual smoke test of desktop app + CI pipeline run
+
+### Wave 0 Gaps
+- No automated tests planned for desktop wrapper -- Tauri apps are best validated via manual smoke testing (WebView behavior, tray interaction, OS-level features)
+- CI pipeline validation requires a running Gitea instance
+- Existing web/api tests should continue passing (regression)
+
+## Security Domain
+
+### Applicable ASVS Categories
+
+| ASVS Category | Applies | Standard Control |
+|---------------|---------|-----------------|
+| V2 Authentication | no | Auth handled by web app (Phase 2), not desktop shell |
+| V3 Session Management | no | Sessions managed by web app cookies in WebView |
+| V4 Access Control | no | Access control in NestJS API layer |
+| V5 Input Validation | yes | Server URL input validation (URL format, https preference) |
+| V6 Cryptography | no | No crypto in desktop wrapper |
+
+### Known Threat Patterns for Tauri Desktop
+
+| Pattern | STRIDE | Standard Mitigation |
+|---------|--------|---------------------|
+| Malicious server URL injection | Tampering | Validate URL format, warn on non-HTTPS, store in plugin-store (not plaintext config) |
+| WebView navigation hijacking | Spoofing | Lock WebView to configured server domain via CSP in tauri.conf.json |
+| Docker socket exposure (CI) | Elevation | Ephemeral runners, internal-only CI, no untrusted code |
+| CI secrets leakage | Information Disclosure | Use Gitea secrets store, never log secrets in pipeline output |
+
+## Sources
+
+### Primary (HIGH confidence)
+- [v2.tauri.app/start/prerequisites/](https://v2.tauri.app/start/prerequisites/) - Linux system dependencies
+- [v2.tauri.app/reference/config/](https://v2.tauri.app/reference/config/) - Configuration reference (frontendDist, windows, bundle)
+- [v2.tauri.app/learn/system-tray/](https://v2.tauri.app/learn/system-tray/) - TrayIconBuilder API, menu events
+- [v2.tauri.app/plugin/store/](https://v2.tauri.app/plugin/store/) - Plugin store API
+- [v2.tauri.app/plugin/notification/](https://v2.tauri.app/plugin/notification/) - Notification plugin
+- [v2.tauri.app/plugin/autostart/](https://v2.tauri.app/plugin/autostart/) - Autostart plugin
+- [v2.tauri.app/plugin/window-state/](https://v2.tauri.app/plugin/window-state/) - Window state plugin
+- [v2.tauri.app/distribute/windows-installer/](https://v2.tauri.app/distribute/windows-installer/) - NSIS vs MSI
+- [docs.gitea.com/usage/actions/act-runner](https://docs.gitea.com/usage/actions/act-runner) - Runner setup
+- [docs.gitea.com/usage/actions/comparison](https://docs.gitea.com/usage/actions/comparison) - Gitea vs GitHub Actions
+
+### Secondary (MEDIUM confidence)
+- [npm registry](https://www.npmjs.com/package/@tauri-apps/cli) - Package versions verified
+- [melvinoostendorp.nl/blog/tauri-v2-nextjs-monorepo-guide](https://melvinoostendorp.nl/blog/tauri-v2-nextjs-monorepo-guide) - Monorepo structure reference
+- [botmonster.com/posts/self-hosted-cicd-pipeline-gitea-actions-docker/](https://botmonster.com/posts/self-hosted-cicd-pipeline-gitea-actions-docker/) - Gitea Actions Docker pipeline example
+- [github.com/tauri-apps/tauri/discussions/2684](https://github.com/tauri-apps/tauri/discussions/2684) - Close-to-tray pattern
+
+### Tertiary (LOW confidence)
+- Training knowledge for frontendDist runtime override via window.navigate() (A1)
+- Training knowledge for reqwest crate usage in Tauri (A6)
+
+## Project Constraints (from CLAUDE.md)
+
+- **Docker-based infrastructure:** All components as containers. CI/CD deploys via docker-compose.
+- **Mandantenfaehigkeit:** Multi-tenancy architecture. Desktop wrapper is tenant-agnostic (URL-based).
+- **Claude builds everything:** Architecture must be understandable. Desktop app code should be minimal Rust.
+- **pnpm workspaces:** Desktop app integrates as `apps/desktop` in existing monorepo structure.
+- **Biome for linting:** CI pipeline must run Biome (not ESLint) for lint step.
+- **Vitest for tests:** CI pipeline uses Vitest, matching existing test infrastructure.
+- **TypeScript 5.5+:** Type-check step in CI uses existing turbo task.
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: MEDIUM - All packages verified on npm registry and documented on official Tauri docs. Tauri 2 is stable (released Oct 2024). Package versions confirmed.
+- Architecture: MEDIUM - URL-loading wrapper pattern is documented. Close-to-tray pattern confirmed via multiple community examples. First-run setup flow uses assumed runtime URL override.
+- Pitfalls: HIGH - WebKitGTK dependency confirmed missing via environment probe. JWT parse error documented in Gitea Actions comparison docs.
+
+**Research date:** 2026-06-25
+**Valid until:** 2026-07-25 (stable technology, 30-day validity)