27 KiB
phase, plan, type, wave, depends_on, files_modified, files_deleted, autonomous, requirements, user_setup, estimate, must_haves
| phase | plan | type | wave | depends_on | files_modified | files_deleted | autonomous | requirements | user_setup | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 18-desktop-client-fertigstellen | 04 | execute | 2 |
|
|
|
true |
|
|
|
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:
setup.htmlimportiert 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 ueberwindow.__TAURI__.core.invokemit zwei Rust-Kommandos (withGlobalTauriist bereits aktiv).- 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/versionlief 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.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.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<String, String>):
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).
<acceptance_criteria>
- 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.
</acceptance_criteria>
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
<fails_when>cargo check oder cargo clippy endet nicht mit einer Finished-Zeile (Kompilier- oder Clippy-Fehler) — RUST-OK fehlt.</fails_when>
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
<fails_when>Eines der Kennzeichen (Kommandos, Rewrite-Praefix, Umlaut-Label, Autostart-Haken, Update-Link, Capability, Abhaengigkeit) fehlt — WIRING-OK fehlt.</fails_when>
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.
` im gleichen Stil wie die Warnung, gruenliche Farbe) und `await invoke('save_server_url', { url: normalizedUrl })`; schlaegt das Speichern fehl: "Die Adresse konnte nicht gespeichert werden: …". Enter-Taste und Eingabe-Reset bleiben.
setup.html — Markup und Gestalt (Sie-Form, Tessera-Farben). Ueber der
Ueberschrift das Tessera-Zeichen als inline-SVG (Inhalt von
apps/web/src/app/icon.svg, Breite 56px), <h1> "Tessera" in #ffed00,
Untertitel "Desktop-App einrichten", Label "Adresse Ihres Tessera-Servers",
darunter ein Hilfstext <p class="hint"> "Das ist die Adresse, unter der
Sie Tessera auch im Browser öffnen." Das value-Attribut des Eingabefelds
entfernen (keine vorbelegte Adresse — ein Paket fuer alle Umgebungen, D-02),
Platzhalter https://tessera.example.com bleibt. Knopf "Verbinden" in
Markengelb mit dunkler Schrift (#1a1a1a), Karte dunkel
(oklch(0.22 0.01 260)), Rahmenfarbe in Olive (#9c9440) fuer Fokus.
Alle Texte mit echten Umlauten (<meta charset="UTF-8"> steht bereits).
<title> "Tessera – Desktop-App einrichten".
CSP (tauri.conf.json). In app.security.csp bei script-src den
Fremdhost-Eintrag entfernen, sodass dort nur noch 'self' 'unsafe-inline' 'unsafe-eval'
steht (die Seite laedt nichts mehr von aussen; T-18-11). connect-src *
bleibt (D-02).
Icons. Aus apps/web/src/app/icon.svg erzeugen (im Verzeichnis
apps/desktop/src-tauri/icons): magick -background none -density 512 ../../../web/src/app/icon.svg -resize 512x512 icon.png;
daraus 128x128.png (128), 128x128@2x.png (256), 32x32.png (32) per
-resize; icon.ico mit magick icon.png -define icon:auto-resize=256,128,64,48,32,16 icon.ico.
In tauri.conf.json bundle.icon auf
["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png", "icons/icon.ico"]
setzen. Das versehentlich verschachtelte, versionierte Verzeichnis
apps/desktop/src-tauri/apps/ mit git rm -r entfernen (Rest aus Phase 6).
Lokaler Beweis (D-16). rm -rf apps/desktop/src-tauri/target/release/bundle,
dann pnpm --filter @tessera/desktop exec tauri build --bundles appimage
(Version bleibt 1.1.0 aus der Basislinie), danach
sh .gitea/scripts/desktop-collect.sh --require linux. Im SUMMARY die
Baudauer und die Groesse des AppImage festhalten. Optional, wenn eine
grafische Sitzung vorhanden ist: das AppImage starten, Erststart-Seite
ansehen, http://localhost:3000 eingeben, Anmeldung sehen; Ergebnis im
SUMMARY notieren (kein Pflichtschritt — die Windows-Probe macht der Nutzer
am Phasenende).
<acceptance_criteria>
- grep -c "window.__TAURI__.core" apps/desktop/src/setup.html ergibt 1; grep -c "invoke('check_server'" apps/desktop/src/setup.html ergibt 1; grep -c "invoke('save_server_url'" apps/desktop/src/setup.html ergibt 1.
- grep -c '^\s*import ' apps/desktop/src/setup.html ergibt 0 (kein Modul-Import mehr).
- grep -c 'unpkg.com' apps/desktop/src-tauri/tauri.conf.json ergibt 0.
- grep -c 'value="http://localhost:3000"' apps/desktop/src/setup.html ergibt 0; grep -c 'Adresse Ihres Tessera-Servers' apps/desktop/src/setup.html ergibt mindestens 1.
- magick identify -format '%wx%h\n' apps/desktop/src-tauri/icons/icon.png ergibt 512x512; magick identify apps/desktop/src-tauri/icons/icon.ico | wc -l ergibt 6.
- test ! -e apps/desktop/src-tauri/apps endet mit 0 (verschachteltes Verzeichnis per git rm -r entfernt).
- jq -r '.bundle.icon | length' apps/desktop/src-tauri/tauri.conf.json ergibt 5.
- desktop-dist/manifest.json traegt Tessera-1.1.0.AppImage aus dem frischen Bau (Zeitstempel des AppImage neuer als der von Task 1 geaenderten lib.rs).
</acceptance_criteria>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
Erststart-Seite -> Rust-Kommandos (invoke) |
Die vom Anwender eingegebene Adresse wird an check_server/save_server_url uebergeben. |
Client -> Server (/api-proxy/health/version, /api-proxy/desktop/latest) |
Ausgehende HTTP-Aufrufe an die gespeicherte Adresse. |
Tray -> Systembrowser (opener) |
Der Client oeffnet eine Adresse ausserhalb der App. |
| WebView -> entfernte Web-App | Nach dem Erststart laeuft die Tessera-Web-App im WebView (unveraendert seit Phase 6). |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-18-10 | Spoofing / Tampering | check_server/save_server_url (beliebige Adresse) |
low | mitigate | Nur http/https, Adresse wird geparst und normalisiert; der Aufruf geht nur an die vom Anwender selbst eingegebene Adresse, ausschliesslich auf dessen Rechner (kein Server-seitiges SSRF). Zeitlimit 8 s. |
| T-18-11 | Tampering | CSP der lokalen Seite (Fremdhost in script-src) |
low | mitigate | Fremdhost entfernt; die Seite laedt keinen externen Code mehr. |
| T-18-12 | Elevation of Privilege | Tray update (Opener) |
low | mitigate | Adresse wird aus der gespeicherten server_url gebaut, nie aus Serverdaten; Capability auf http://*/https://* beschraenkt (kein file:/Schema-Missbrauch). |
| T-18-13 | Spoofing | Update-Hinweis aus /desktop/latest (falsche Version vorgetaeuscht) |
low | accept | Der Hinweis fuehrt nur auf die Tessera-Seite; kein Auto-Update, kein Download ohne Nutzeraktion (D-03). |
| T-18-14 | Information Disclosure | Unsignierte Binaries / SmartScreen | low | accept | Keine Code-Signierung in dieser Phase (D-09); Erklaerung im Anwenderhandbuch (18-06). |
| T-18-SC | Tampering | cargo add tauri-plugin-opener@2 |
low | mitigate | Legitimitaetspruefung in RESEARCH: OK (tauri-apps/plugins-workspace, 374k Downloads/Woche); Lockfile committet; kein [ASSUMED]/[SUS]-Paket, daher keine Sperr-Freigabe noetig. |
| </threat_model> |
<success_criteria>
- Erststart-Seite prueft die Adresse wirklich, speichert sie und fuehrt zur Anmeldung; Sie-Form, Tessera-Gestalt, kein Fremdcode.
- Tray: Öffnen · Update herunterladen · Autostart-Haken · Beenden, mit Umlauten; Update-Eintrag oeffnet die Download-Seite im Browser.
- Versionspruefung gegen
/api-proxy/desktop/latestmit Benachrichtigung. - Echtes App-Icon;
cargo check/clippyund lokaler AppImage-Bau gruen. </success_criteria>