--- 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 `