docs(06): research phase domain - Tauri 2.x desktop wrapper + Gitea Actions CI/CD
This commit is contained in:
@@ -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>
|
||||
|
||||
## 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
|
||||
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
|
||||
## 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 |
|
||||
|
||||
</phase_requirements>
|
||||
|
||||
## 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
|
||||
<!-- Source: Pattern derived from tauri-plugin-store docs -->
|
||||
<!-- apps/desktop/src/setup.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tessera - Setup</title>
|
||||
<style>
|
||||
body { font-family: system-ui; display: flex; justify-content: center;
|
||||
align-items: center; min-height: 100vh; margin: 0;
|
||||
background: oklch(0.17 0.01 260); color: white; }
|
||||
.setup { text-align: center; max-width: 400px; }
|
||||
input { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #555;
|
||||
background: #222; color: white; font-size: 16px; margin: 16px 0; }
|
||||
button { padding: 12px 32px; border-radius: 8px; border: none;
|
||||
background: oklch(0.85 0.17 85); color: black; cursor: pointer;
|
||||
font-size: 16px; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="setup">
|
||||
<h1>Tessera</h1>
|
||||
<p>Server-URL eingeben:</p>
|
||||
<input id="url" type="url" placeholder="https://tessera.example.com"
|
||||
value="http://localhost:3000" />
|
||||
<button onclick="connect()">Verbinden</button>
|
||||
</div>
|
||||
<script type="module">
|
||||
import { load } from '@tauri-apps/plugin-store';
|
||||
|
||||
window.connect = async () => {
|
||||
const url = document.getElementById('url').value.trim();
|
||||
if (!url) return;
|
||||
const store = await load('config.json', { autoSave: true });
|
||||
await store.set('server_url', url);
|
||||
window.location.href = url;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 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<String> {
|
||||
let url = format!("{}/version", server_url);
|
||||
if let Ok(resp) = reqwest::get(&url).await {
|
||||
if let Ok(info) = resp.json::<serde_json::Value>().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)
|
||||
Reference in New Issue
Block a user