---
phase: 18-desktop-client-fertigstellen
plan: 04
type: execute
wave: 2
depends_on: ["18-01"]
files_modified:
- apps/desktop/src-tauri/src/lib.rs
- apps/desktop/src-tauri/Cargo.toml
- apps/desktop/src-tauri/Cargo.lock
- apps/desktop/src-tauri/capabilities/default.json
- apps/desktop/src-tauri/tauri.conf.json
- apps/desktop/src/setup.html
- 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/icons/128x128@2x.png
- apps/desktop/src-tauri/icons/32x32.png
files_deleted:
- apps/desktop/src-tauri/apps/desktop/src-tauri/icons/128x128.png
- apps/desktop/src-tauri/apps/desktop/src-tauri/icons/32x32.png
- apps/desktop/src-tauri/apps/desktop/src-tauri/icons/icon.ico
- apps/desktop/src-tauri/apps/desktop/src-tauri/icons/icon.png
autonomous: true
requirements: [DESK-01, DESK-02, DESK-05]
user_setup: []
estimate:
tokens: 90000
raw_tokens: 90000
tasks: 2
confidence: low
must_haves:
truths:
- "Der Client vergleicht beim Start seine Version mit `{server}/api-proxy/desktop/latest`; weicht sie ab, zeigt er die Benachrichtigung 'Neue Version X.Y.Z verfügbar' und schaltet den Tray-Eintrag 'Update herunterladen' frei, der `{server}/settings/general/desktop` im Systembrowser öffnet (D-11, D-13)."
- "Die Erststart-Seite fragt die Server-Adresse ab, prueft sie ueber `/api-proxy/health/version` (Rust-Kommando, kein CORS), speichert sie und laedt die Tessera-Anmeldung; Texte in Sie-Form, Tessera-Farben und -Logo (D-02, D-13)."
- "Das Tray-Menue traegt 'Öffnen', 'Update herunterladen', den Haken 'Mit Windows starten' (auf Linux 'Beim Anmelden starten') und 'Beenden' — mit echten Umlauten; Schliessen-ins-Tray, Fensterzustand und Autostart-Plugin bleiben wie in Phase 6 (D-13, D-14)."
- "Der Client traegt das Tessera-Zeichen als App- und Fenster-Icon (kein flaches gelbes Quadrat), und `cargo check`, `cargo clippy` sowie ein lokaler AppImage-Bau laufen durch (D-16)."
artifacts:
- path: "apps/desktop/src-tauri/src/lib.rs"
provides: "Kommandos check_server und save_server_url, Versionspruefung gegen /desktop/latest, Tray-Eintraege update und autostart, Opener"
contains: "check_server"
- path: "apps/desktop/src-tauri/capabilities/default.json"
provides: "opener:allow-open-url mit http/https-Scope"
contains: "opener:allow-open-url"
- path: "apps/desktop/src/setup.html"
provides: "Erststart-Seite ohne Bundler-Import, ueber window.__TAURI__.core.invoke"
contains: "__TAURI__"
- path: "apps/desktop/src-tauri/icons/icon.ico"
provides: "Mehrgroessen-ICO (16 bis 256) aus dem Tessera-Zeichen"
key_links:
- from: "apps/desktop/src-tauri/src/lib.rs"
to: "apps/api/src/desktop/desktop.controller.ts"
via: "GET {server}/api-proxy/desktop/latest — Feld version"
pattern: "api-proxy/desktop/latest"
- from: "apps/desktop/src/setup.html"
to: "apps/desktop/src-tauri/src/lib.rs"
via: "window.__TAURI__.core.invoke('check_server' | 'save_server_url')"
pattern: "invoke\\('check_server'"
- from: "apps/desktop/src-tauri/src/lib.rs (Tray 'update')"
to: "apps/web/src/app/(portal)/settings/general/desktop/page.tsx"
via: "opener().open_url(`{server}/settings/general/desktop`)"
pattern: "settings/general/desktop"
---
Der Tauri-Client aus Phase 6 wird zum fertigen Produkt: Versionspruefung
gegen `/desktop/latest` mit Update-Hinweis und Download-Link im Tray,
Autostart-Haken im Tray, Umlaute in allen Tray-Texten, eine Erststart-Seite
in Sie-Form mit Tessera-Gestalt, die die Adresse wirklich prueft, und ein
echtes App-Icon. Der Windows-Bau wird in 18-05 in der Pipeline bewiesen; hier
werden `cargo check`, `cargo clippy` und ein lokaler AppImage-Bau als Beweis
vor dem Push verlangt (D-16).
Purpose: D-11, D-13 und D-14 aus 18-CONTEXT.md sowie Erfolgskriterium 3.
Output: Geaenderte `lib.rs`, neues Plugin `tauri-plugin-opener`, erweiterte
Capabilities, ueberarbeitete `setup.html`, Icon-Satz, lokal gebautes AppImage.
**Zwei Befunde aus der Planung, die dieser Plan behebt:**
1. `setup.html` importiert das Store-Plugin als nacktes ES-Modul; ohne
Bundler und ohne Importmap scheitert dieser Import im gebauten Client mit
"Failed to resolve module specifier", der Knopf "Verbinden" tut dann
nichts. Die Seite spricht kuenftig ausschliesslich ueber
`window.__TAURI__.core.invoke` mit zwei Rust-Kommandos (`withGlobalTauri`
ist bereits aktiv).
2. Die API ist vom Client nur ueber den Web-Ursprung erreichbar
(Next.js-Rewrite `/api-proxy/*`, siehe 18-01). Die bisherige Pruefung
gegen `{server}/health/version` lief im Betrieb ins Leere; alle Aufrufe
gehen jetzt ueber `{server}/api-proxy/...`.
**Discretion (Icon-Pruefung, Tray-Reihenfolge):** Die heutigen Icons sind
flache gelbe Quadrate (32x32, 105 Bytes). Sie werden aus dem Web-Zeichen
`apps/web/src/app/icon.svg` neu erzeugt. Tray-Reihenfolge: Öffnen ·
Update herunterladen · — · Autostart-Haken · — · Beenden.
## Artifacts this phase produces
Dieser Plan: `apps/desktop/src-tauri/src/lib.rs` (Funktionen `api_url`,
`check_server`, `save_server_url`, Struktur `DesktopLatest`, Tray-IDs
`open`/`update`/`autostart`/`quit`), `Cargo.toml` (+`tauri-plugin-opener`),
`Cargo.lock`, `capabilities/default.json` (`opener:allow-open-url`),
`tauri.conf.json` (Icon-Liste, CSP ohne Fremdhost), `apps/desktop/src/setup.html`,
`icons/icon.png` (512), `icons/128x128.png`, `icons/128x128@2x.png`,
`icons/32x32.png`, `icons/icon.ico` (16-256). Entfernt: das versehentlich
verschachtelte Verzeichnis `apps/desktop/src-tauri/apps/`. Gesamtliste der
Phase: siehe 18-01-PLAN.md.
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/18-desktop-client-fertigstellen/18-CONTEXT.md
@.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md
@.planning/phases/06-desktop-client-ci-cd/06-02-SUMMARY.md
@apps/desktop/src-tauri/src/lib.rs
@apps/desktop/src-tauri/Cargo.toml
@apps/desktop/src-tauri/capabilities/default.json
@apps/desktop/src-tauri/tauri.conf.json
@apps/desktop/src/setup.html
@apps/web/src/app/icon.svg
Task 1: lib.rs — Kommandos fuer die Erststart-Seite, Versionspruefung gegen /desktop/latest, Tray mit Update und Autostart
Rust/Cargo 1.96 mit Clippy ist installiert (`cargo clippy --version` antwortet), `cargo check` in `apps/desktop/src-tauri` ist am Stand von 18-01 gruen, und die Basislinie `1.1.0` aus 18-01 Task 2 ist eingecheckt.
Plugin-Einbindung und Tray-Aufbau sind lokal in einer Datei; ein Rueckbau ist ein Commit.
apps/desktop/src-tauri/src/lib.rs,
apps/desktop/src-tauri/Cargo.toml,
apps/desktop/src-tauri/Cargo.lock,
apps/desktop/src-tauri/capabilities/default.json
apps/desktop/src-tauri/src/lib.rs (gesamt, 121 Zeilen),
apps/desktop/src-tauri/Cargo.toml,
apps/desktop/src-tauri/capabilities/default.json,
.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Code Examples 4 und 5, "Package Legitimacy Audit"),
.planning/phases/18-desktop-client-fertigstellen/18-PATTERNS.md (Abschnitte lib.rs, capabilities, Cargo.toml)
- `check_server(url)` (async Tauri-Kommando) prueft Schema http/https, ruft `{url}/api-proxy/health/version` mit 8 Sekunden Zeitlimit ab und liefert `Ok(version)`; jeder Fehler liefert `Err({deutsche Meldung in Sie-Form})`.
- `save_server_url(url)` normalisiert die Adresse, schreibt `server_url` in `config.json` des Store-Plugins, speichert den Store und navigiert das Fenster `main` auf die Adresse.
- Beim Start mit gespeicherter Adresse laeuft die Versionspruefung gegen `{server}/api-proxy/desktop/latest`; bei `version != CARGO_PKG_VERSION` erscheint die Benachrichtigung (Titel "Tessera-Update", Text "Neue Version X.Y.Z verfügbar – Download über das Symbol im Infobereich."), und der Tray-Eintrag `update` wird aktiviert und in "Version X.Y.Z herunterladen" umbenannt.
- Tray-Eintrag `update` oeffnet `{server}/settings/general/desktop` im Systembrowser ueber `tauri-plugin-opener`.
- Tray-Haken `autostart` spiegelt beim Start `autolaunch().is_enabled()`; ein Klick schaltet um und setzt den Haken auf den neuen Zustand.
- Tray-Texte: "Öffnen", "Update herunterladen", "Mit Windows starten" (unter `cfg!(target_os = "windows")`, sonst "Beim Anmelden starten"), "Beenden".
**Abhaengigkeit.** Im Verzeichnis `apps/desktop/src-tauri`
`cargo add tauri-plugin-opener@2` ausfuehren (Legitimitaetspruefung in
RESEARCH: `OK`, offizielles Plugin aus `tauri-apps/plugins-workspace`;
gleiche unpinnte Major-Schreibweise wie die anderen `tauri-plugin-*`-Zeilen).
`Cargo.lock` wird dabei aktualisiert und mit committet.
**Capabilities (`capabilities/default.json`).** An das `permissions`-Array
das Objekt `{ "identifier": "opener:allow-open-url", "allow": [ { "url": "https://*" }, { "url": "http://*" } ] }`
anhaengen (`http://*` wegen D-02: interne Server ohne TLS sind erlaubt,
gleiche Begruendung wie die HTTP-Warnung der Erststart-Seite). Die
Autostart-Rechte sind bereits vorhanden.
**`lib.rs` — Imports und Plugins.** Zusaetzlich `use tauri_plugin_opener::OpenerExt;`,
`use tauri_plugin_autostart::ManagerExt;` (neben `MacosLauncher`),
`tauri::menu::CheckMenuItemBuilder`, `tauri::AppHandle`, `std::time::Duration`.
Plugin `.plugin(tauri_plugin_opener::init())` registrieren und
`.invoke_handler(tauri::generate_handler![check_server, save_server_url])`
vor `.setup(...)` einhaengen.
**Hilfsfunktion `fn api_url(server: &str, path: &str) -> String`**: liefert
`format!("{}/api-proxy{}", server.trim_end_matches('/'), path)` — die einzige
Stelle, an der der Rewrite-Praefix steht; Kommentar erklaert, warum
(Next.js-Rewrite, API nicht unter dem Web-Hostnamen).
**Kommando `check_server`** (`#[tauri::command] async fn check_server(url: String) -> Result`):
`tauri::Url::parse` (Fehler: "Diese Adresse ist ungültig."), Schema
`http`/`https` (sonst "Es sind nur Adressen mit http oder https erlaubt."),
`reqwest::Client::builder().timeout(Duration::from_secs(8)).build()`,
GET `api_url(&url, "/health/version")`; Netzfehler -> "Unter dieser Adresse
antwortet kein Tessera-Server."; Nicht-2xx -> "Der Server antwortete mit
Status {code}."; JSON in die bestehende Struktur `VersionResponse` (Feld
`version`) -> `Ok(version)`. Meldungen sind Sie-Form-tauglich (keine
Anrede), Umlaute als UTF-8.
**Kommando `save_server_url`** (`#[tauri::command] fn save_server_url(app: AppHandle, url: String) -> Result<(), String>`):
Adresse parsen und als `String` normalisieren (`Url::as_str`), Store
`config.json` ueber `app.store(...)`, `store.set("server_url", serde_json::json!(normalized))`,
`store.save()` (Fehler als `String`), danach `get_webview_window("main")`
und `navigate(parsed_url)`.
**Versionspruefung umbauen.** Struktur `DesktopLatest { version: String }`
(`serde::Deserialize`). Im bestehenden `async_runtime::spawn`-Block die
Adresse durch `api_url(&server_url, "/desktop/latest")` ersetzen, Antwort
als `DesktopLatest` lesen; bei Abweichung Benachrichtigung mit Titel
"Tessera-Update" und Text "Neue Version {v} verfügbar – Download über das
Symbol im Infobereich." **und** am geklonten Handle des Tray-Eintrags
`update` `set_text(format!("Version {v} herunterladen"))` und
`set_enabled(true)` aufrufen (Rueckgaben mit `let _ =` ignorieren, wie
bisher).
**Tray-Menue.** Eintraege in dieser Reihenfolge: `open` "Öffnen";
`update` "Update herunterladen" mit `.enabled(false)` beim Bau (wird erst
nach der Pruefung freigeschaltet); Trenner; `autostart` als
`CheckMenuItemBuilder::with_id("autostart", label)` mit `.checked(app.autolaunch().is_enabled().unwrap_or(false))`,
Label `if cfg!(target_os = "windows") { "Mit Windows starten" } else { "Beim Anmelden starten" }`;
Trenner; `quit` "Beenden". Fuer `on_menu_event` vorher
`let server_for_menu = url_for_check.clone();` und
`let autostart_for_menu = autostart.clone();` anlegen (die Handles sind
`Clone + Send + Sync`). Neue `match`-Arme: `"update"` -> wenn eine Adresse
gespeichert ist, `app.opener().open_url(format!("{}/settings/general/desktop", server.trim_end_matches('/')), None::<&str>)`;
`"autostart"` -> `let mgr = app.autolaunch(); let on = mgr.is_enabled().unwrap_or(false);`
dann `mgr.disable()` bzw. `mgr.enable()`, bei Erfolg `set_checked(!on)`,
bei Fehler `set_checked(on)` (Haken bleibt bei der Wahrheit). Die
bestehenden Arme `open`/`quit`, `on_tray_icon_event`, `on_window_event`
(Schliessen-ins-Tray) und `RunEvent::ExitRequested` bleiben unveraendert
(D-14). Bestehender Kommentar "Tray menu" um zwei Saetze zu den neuen
Eintraegen ergaenzen.
Nach dem Umbau `cargo check` und `cargo clippy` ausfuehren; Clippy-Warnungen
in den **geaenderten** Zeilen beheben (bestehende Warnungen andernorts nur
beheben, wenn trivial).
- `grep -c '^tauri-plugin-opener = "2"' apps/desktop/src-tauri/Cargo.toml` ergibt 1; `grep -c 'name = "tauri-plugin-opener"' apps/desktop/src-tauri/Cargo.lock` ergibt 1.
- `grep -c '"opener:allow-open-url"' apps/desktop/src-tauri/capabilities/default.json` ergibt 1.
- `grep -v '^\s*//' apps/desktop/src-tauri/src/lib.rs | grep -c 'fn check_server'` ergibt 1; ebenso `fn save_server_url` 1, `fn api_url` 1, `api-proxy` mindestens 1, `tauri_plugin_opener::init()` 1, `generate_handler!\[check_server, save_server_url\]` 1.
- `grep -c '"Öffnen"' apps/desktop/src-tauri/src/lib.rs` ergibt 1; `grep -c '"Mit Windows starten"' apps/desktop/src-tauri/src/lib.rs` ergibt 1; `grep -c 'CheckMenuItemBuilder::with_id("autostart"' apps/desktop/src-tauri/src/lib.rs` ergibt 1; `grep -c 'settings/general/desktop' apps/desktop/src-tauri/src/lib.rs` ergibt 1.
- `grep -c '/desktop/latest' apps/desktop/src-tauri/src/lib.rs` ergibt 1.
- `cargo check` und `cargo clippy` in `apps/desktop/src-tauri` enden mit Exit 0.
cd /home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri && cargo check 2>&1 | tail -1 | grep -q Finished && cargo clippy 2>&1 | tail -1 | grep -q Finished && echo RUST-OK
`cargo check` oder `cargo clippy` endet nicht mit einer `Finished`-Zeile (Kompilier- oder Clippy-Fehler) — `RUST-OK` fehlt.
cd /home/vicolab/projects/tessera-ctl && grep -v '^\s*//' apps/desktop/src-tauri/src/lib.rs | grep -q 'fn check_server' && grep -q 'fn save_server_url' apps/desktop/src-tauri/src/lib.rs && grep -q 'api-proxy' apps/desktop/src-tauri/src/lib.rs && grep -q '"Öffnen"' apps/desktop/src-tauri/src/lib.rs && grep -q 'CheckMenuItemBuilder::with_id("autostart"' apps/desktop/src-tauri/src/lib.rs && grep -q 'settings/general/desktop' apps/desktop/src-tauri/src/lib.rs && grep -q '"opener:allow-open-url"' apps/desktop/src-tauri/capabilities/default.json && grep -q '^tauri-plugin-opener = "2"' apps/desktop/src-tauri/Cargo.toml && echo WIRING-OK
Eines der Kennzeichen (Kommandos, Rewrite-Praefix, Umlaut-Label, Autostart-Haken, Update-Link, Capability, Abhaengigkeit) fehlt — `WIRING-OK` fehlt.
`lib.rs` kompiliert mit Opener-Plugin, beiden Kommandos, Versionspruefung
gegen `/api-proxy/desktop/latest`, Tray mit Update-Eintrag und
Autostart-Haken und Umlaut-Texten; Capabilities und Cargo-Dateien sind
nachgezogen; `cargo check`/`cargo clippy` gruen.
Task 2: Erststart-Seite in Tessera-Gestalt und Sie-Form, echtes App-Icon, lokaler AppImage-Beweis
ImageMagick 7 (`magick`) ist auf dem Entwicklungsrechner vorhanden (am 2026-09-16 geprueft: 7.1.1, mit SVG- und ICO-Unterstuetzung); die Tauri-Linux-Abhaengigkeiten aus 18-01 Task 1 sind installiert.
apps/desktop/src/setup.html,
apps/desktop/src-tauri/tauri.conf.json,
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/icons/128x128@2x.png,
apps/desktop/src-tauri/icons/32x32.png
apps/desktop/src/setup.html (gesamt, 254 Zeilen),
apps/desktop/src-tauri/tauri.conf.json,
apps/web/src/app/icon.svg (Tessera-Zeichen, 72x72),
apps/web/src/components/brand/brand.ts (BRAND_YELLOW #ffed00, BRAND_OLIVE #9c9440, BRAND_PLATE #1a1a1a),
.gitea/scripts/desktop-collect.sh (aus 18-01)
- Die Seite laedt keinen Modulcode von aussen und importiert kein npm-Paket; sie nutzt `window.__TAURI__.core.invoke`.
- Klick auf "Verbinden" (oder Enter): Adresse pruefen (leer, ungueltig, falsches Schema -> Fehlertext in Sie-Form), dann `invoke('check_server', { url })`; bei Fehler erscheint die Meldung des Kommandos, der Knopf ist wieder bedienbar; bei Erfolg erscheint kurz "Tessera {version} gefunden – Verbindung wird hergestellt …" und `invoke('save_server_url', { url })` fuehrt zur Tessera-Anmeldung.
- Bei http ausserhalb von localhost bleibt die Warnung (Sie-Form) sichtbar, die Verbindung ist erlaubt (D-02).
- Die Seite zeigt das Tessera-Zeichen (inline-SVG aus `icon.svg`) und den Schriftzug "Tessera" in Markengelb auf dunklem Grund; keine vorbelegte Server-Adresse, nur der Platzhalter `https://tessera.example.com`.
- Das App-Icon ist das Tessera-Zeichen in 512x512 (PNG) und als ICO mit den Groessen 16, 32, 48, 64, 128, 256.
- `pnpm --filter @tessera/desktop exec tauri build --bundles appimage` laeuft lokal durch und `desktop-collect.sh` sammelt `Tessera-1.1.0.AppImage` ein.
**`setup.html` — Skript.** Den `