docs(quick-260917-jdf/jdh/jdd/jn2): Aktenstand — Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 1m5s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 5m21s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m20s

Vier Quick-Tasks mit Plan, Bericht und Verifikation; Browser-Nachweis der
Web-Teile lokal erbracht (Kacheln #284a7b bei #0057b8, Proxy-Symbol trotz
Zertifikatsfehler, Direktbild bei interner Adresse, Sortierung ueber Reload).
Offen: CI-Beweis des Desktop-Skips nach diesem Push, Windows-VM-Probe der
Client-Aenderungen mit dem CI-Paket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 15:10:20 +02:00
parent 4d485432c0
commit 5a444ec8f2
14 changed files with 2168 additions and 13 deletions
@@ -0,0 +1,232 @@
---
phase: quick-260917-jn2
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [QUICK-260917-JN2]
files_modified:
- apps/desktop/src-tauri/src/lib.rs
- apps/desktop/src/setup.html
- apps/web/src/components/settings/desktop-app-settings.tsx
- apps/web/src/components/settings/desktop-app-settings.test.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- docs/anleitung-anwender.md
- CHANGELOG.md
estimate:
tokens: 46000
raw_tokens: 46000
tasks: 3
confidence: low
must_haves:
truths:
- "lib.rs: Tray wird mit `TrayIconBuilder::with_id(\"main\")` gebaut; die erste Menuezeile ist ein gesperrter Eintrag (Id `connected`) mit Text `Verbunden mit {host}` bzw. `Nicht verbunden`, danach Trenner, dann `Öffnen`, `Server-Adresse ändern…` (Id `change_server`), `Update herunterladen`, Trenner, Autostart-Haken, Trenner, `Beenden`. Tooltip beim Bau `Tessera – {host}` bzw. `Tessera – nicht verbunden` (echte Umlaute und Gedankenstrich U+2013 wie im Bestand)."
- "lib.rs: reine Funktionen `server_host(url: Option<&str>) -> Option<String>` (Host ohne Schema/Pfad, `host:port` nur bei ausdruecklich angegebenem Nicht-Standardport; None bei None/leer/unparsbar), `tray_labels(url: Option<&str>) -> (String, String)` (Tooltip, Menuezeile), `setup_page_url(windows: bool) -> tauri::Url` (`http://tauri.localhost/setup.html` bzw. `tauri://localhost/setup.html`) und `parse_server_url(url: &str) -> Result<tauri::Url, String>` (parsen + nur http/https, Fehlertexte wie bisher in `check_server`) existieren mit Tests in `mod tests`; `cargo fmt --check`, `cargo check`, `cargo clippy`, `cargo test --lib` in apps/desktop/src-tauri gruen."
- "lib.rs: neue Commands `get_server_url(app) -> Option<String>` (liefert NUR den gespeicherten Wert `server_url` aus config.json, leer/fehlend → None) und `open_server(app) -> Result<(), String>` (navigiert das Fenster `main` per `with_desktop_marker` zur gespeicherten Adresse; ohne Adresse Err); beide im `generate_handler!`. Die Capabilities-Datei bleibt unveraendert — App-Commands sind vom lokalen Ursprung ohne Eintrag erlaubt, und vom Remote-Ursprung (Server-Seite) verweigert Tauri sie, solange kein `remote`-Block existiert (tauri-2.11.3 webview/mod.rs Z. 1819-1823)."
- "lib.rs: Tray-Klick `change_server` navigiert das Fenster `main` per `window.navigate(setup_page_url(cfg!(windows)))` und ruft danach `unminimize`/`show`/`set_focus` (kein `eval`, kein JavaScript in der Remote-Seite)."
- "lib.rs: Nach `save_server_url` mit neuer Adresse zeigen Tooltip und `connected`-Zeile ohne Neustart die NEUE Adresse (`apply_server`), der `update`-Eintrag wird auf `Update herunterladen`/gesperrt zurueckgesetzt und die Versionspruefung laeuft einmal neu gegen die neue Adresse (`spawn_version_check`, aus `setup` herausgezogen). Der Tray-Klick `update` liest die Adresse beim Klick per `stored_server_url(app)` aus dem Store — der Start-Klon der Adresse ist entfernt. Menue-Handles liegen in `app.manage(TrayItems { connected, update })`."
- "setup.html: beim Laden `invoke('get_server_url')`; ist ein Wert da, wird `#server-url` vorbelegt, `#current-server` zeigt `Aktuell verbunden mit: {adresse}` (per textContent), die Unterzeile heisst `Server-Adresse ändern`, und ein zweiter Knopf `#cancel-btn` `Abbrechen` ist sichtbar und ruft `invoke('open_server')`. Ohne gespeicherte Adresse (oder wenn der Aufruf fehlschlaegt) verhaelt sich die Seite exakt wie bisher (Erststart). Die SVG `.brand-mark` bleibt unangetastet."
- "Web: `DesktopAppSettings` rendert NUR bei `useIsDesktopClient() === true` einen Block `data-testid=\"desktop-connected\"` mit `settings.desktop.connectedTo` (`Verbunden mit: {origin}`, origin = `window.location.origin`) und `settings.desktop.changeHint` (Hinweis auf „Server-Adresse ändern…“ im Infobereich-Menue); Schluessel in de.json und en.json innerhalb von `settings.desktop`. desktop-app-settings.test.tsx: neuer Test mit Cookie `tessera_desktop=1` findet `Verbunden mit: http://localhost:3000` und den Hinweis; ohne Cookie fehlt `desktop-connected`; Tests 1-3 bleiben gruen. `pnpm --filter @tessera/web exec vitest run` und `pnpm --filter @tessera/web type-check` gruen."
- "Doku: docs/anleitung-anwender.md nennt im Abschnitt Desktop-App die drei Stellen, an denen die aktuelle Server-Adresse steht (Hinweistext am Symbol, erste Menuezeile, Einstellungen → Allgemein → Desktop-App) und beschreibt in einem neuen Unterabschnitt „Server-Adresse ändern“ den Weg ueber das Symbol im Infobereich; die Tray-Menue-Liste fuehrt `Verbunden mit …` und `Server-Adresse ändern…`. CHANGELOG.md `## Unveröffentlicht` `### Neu`: zwei neue Stichpunkte mit Praefix `Desktop-App:` (Stil wie Bestand). Nur Zeilen ergaenzt, fremde Zeilen bleiben."
- "Drei Commits: `feat(desktop): …` (Task 1), `feat(web): …` (Task 2), `docs: …` (Task 3). Kein `git push`, keine `.planning/`-Commits, kein `tauri build`, kein Docker; `cargo` nur mit `CARGO_BUILD_JOBS=4`."
artifacts:
- "apps/desktop/src-tauri/src/lib.rs — `stored_server_url`, `server_host`, `tray_labels`, `setup_page_url`, `parse_server_url`, `TrayItems`, `apply_server`, `spawn_version_check`, Commands `get_server_url`/`open_server`, Tray mit `connected`/`change_server`, `mod tests` erweitert"
- "apps/desktop/src/setup.html — Aenderungsmodus (`#current-server`, `#cancel-btn`, `#subtitle`), `init()`"
- "apps/web/src/components/settings/desktop-app-settings.tsx + .test.tsx — Block `desktop-connected`, Test mit Cookie"
- "apps/web/src/messages/de.json, en.json — `settings.desktop.connectedTo`, `settings.desktop.changeHint`"
- "docs/anleitung-anwender.md — Unterabschnitt „Server-Adresse ändern“, Tray-Liste, Hinweis beim Erststart; CHANGELOG.md — zwei Stichpunkte"
key_links:
- "Die Kette ist: Tray `change_server` → `window.navigate(setup_page_url(cfg!(windows)))` → setup.html laedt lokal (`tauri://localhost` bzw. `http://tauri.localhost`) → `invoke('get_server_url')` (lokaler Ursprung, darum erlaubt) → Vorbelegung → `check_server` → `save_server_url` → Store + `apply_server` + `spawn_version_check` + Navigation zur neuen Adresse. Bricht `setup_page_url` (falsches Schema je Plattform), landet der Client auf einer Fehlerseite ohne Rueckweg — deshalb ist die Funktion rein und je Plattform getestet; sie spiegelt Tauris eigene, nicht oeffentliche `tauri_protocol_url` (manager/mod.rs Z. 339-346)."
- "Die Server-Seite (Remote-Ursprung) kann die App-Commands nicht aufrufen: Tauri prueft bei `!is_local` die ACL, und capabilities/default.json hat keinen `remote`-Block (webview/mod.rs Z. 1819-1823). Genau deshalb braucht es keinen Capability-Eintrag fuer `get_server_url`/`open_server` — und deshalb darf auch KEIN `remote`-Block dazukommen."
- "Tooltip und `connected`-Zeile haengen an `tray_labels(url)`; `apply_server` ist die EINZIGE Stelle, die beide setzt (Start und Wechsel) — sonst laufen die drei Anzeigen auseinander. `tray.set_tooltip` ist unter Linux ein No-Op (Tauri-Doku), die Menuezeile bleibt dort die Anzeige."
- "Der `update`-Klick liest die Adresse jetzt beim Klick aus dem Store; wuerde er den Start-Klon behalten, oeffnete er nach einem Wechsel die ALTE Einstellungsseite."
- "`prevent_exit`-Falle (260917-eta): der Run-Handler laesst `app.exit(0)` (code: Some) durch und verhindert nur `code: None`. Navigation zur Setup-Seite und zurueck schliesst kein Fenster und loest kein ExitRequested aus — Run-Handler und `on_window_event` bleiben unangetastet."
- "Web-Block nur im Client: `useIsDesktopClient()` ist im Server-HTML und im ersten Client-Render false, `window.location.origin` wird deshalb nur nach der Hydration gelesen (kein Hydration-Fehler). jsdom-URL in vitest ist `http://localhost:3000`, darauf prueft der Test."
---
<objective>
Der Desktop-Client zeigt, mit welchem Tessera-Server er verbunden ist, und die Adresse laesst sich nachtraeglich aendern — ohne config.json zu loeschen:
1. **Sichtbar (drei Stellen).** Tray-Tooltip `Tessera – {host}`, erste (gesperrte) Menuezeile `Verbunden mit {host}` (ohne Adresse: `Tessera – nicht verbunden` / `Nicht verbunden`), und in der App unter Einstellungen → Allgemein → Desktop-App ein Block „Verbunden mit: {origin}“ mit Hinweis auf den Aenderungsweg.
2. **Aendern.** Neuer Tray-Eintrag `Server-Adresse ändern…` (nach „Öffnen“) navigiert das Fenster zur gebuendelten Setup-Seite. Die Seite erkennt per neuem Command `get_server_url` den Aenderungsmodus (Feld vorbelegt, Zeile „Aktuell verbunden mit: …“, Knopf „Abbrechen“ → neues Command `open_server`). Ohne gespeicherte Adresse bleibt es der Erststart.
3. **Konsistent ohne Neustart.** Nach `save_server_url` setzt `apply_server` Tooltip und Menuezeile neu, der Update-Klick liest die Adresse beim Klick aus dem Store, und die Versionspruefung (`spawn_version_check`, aus `setup` herausgezogen) laeuft einmal gegen den neuen Server.
4. **Reine, getestete Helfer** `server_host`, `tray_labels`, `setup_page_url`, `parse_server_url` in `mod tests` (Stil wie `with_desktop_marker`-Tests).
5. **Doku + CHANGELOG.**
Task-Zuschnitt: Task 1 ist der Tracer und traegt die vollstaendige Client-Kette Rust ↔ setup.html (Tray → Setup-Seite → Commands → Store → Tray-Auffrischung) — nur so ist die Kette in einem Commit `feat(desktop)` geschlossen. Task 2 ist der Web-Block (`feat(web)`), Task 3 Doku/CHANGELOG (`docs`). Der Windows-Nachweis (Tooltip, Menue, Wechsel, Abbrechen, Update-Link nach Wechsel) erfolgt durch den Orchestrator mit dem CI-Paket auf der Test-VM — im SUMMARY als „Nachweis durch Orchestrator“ ausweisen.
Purpose: Der Nutzer sieht auf einen Blick, gegen welchen Server der Client laeuft (Test- vs. Live-Server), und kann bei einem Serverwechsel die Adresse selbst umstellen.
Output: lib.rs mit vier reinen Helfern + Tests, State `TrayItems`, zwei neuen Commands, neuem Tray-Aufbau; setup.html im Aenderungsmodus; Web-Block + zwei i18n-Schluessel + Test; Handbuch-Unterabschnitt; zwei CHANGELOG-Zeilen; drei Commits.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@/home/vicolab/projects/tessera-ctl/CLAUDE.md
@/home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri/src/lib.rs
@/home/vicolab/projects/tessera-ctl/apps/desktop/src/setup.html
@/home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri/capabilities/default.json
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/settings/desktop-app-settings.tsx
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/settings/desktop-app-settings.test.tsx
@/home/vicolab/projects/tessera-ctl/apps/web/src/lib/desktop-client.ts
@/home/vicolab/projects/tessera-ctl/.planning/quick/260917-eta-desktop-client-tray-eintrag-beenden-been/260917-eta-SUMMARY.md
</context>
<tasks>
<task type="tracer" tdd="true">
<name>Task 1: Rust + setup.html — Verbunden-Zeile und Tooltip im Tray, „Server-Adresse ändern…“, Commands `get_server_url`/`open_server`, Tray-Auffrischung nach Wechsel, Tests</name>
<files>apps/desktop/src-tauri/src/lib.rs, apps/desktop/src/setup.html</files>
<read_first>
- apps/desktop/src-tauri/src/lib.rs komplett (349 Zeilen): Z. 24-72 Stil dokumentierter reiner Helfer (`api_url`, `with_desktop_marker`, `update_labels`), Z. 74-121 `check_server`/`save_server_url`, Z. 133 `generate_handler!`, Z. 134-151 Startnavigation, Z. 153-241 Tray-Aufbau (Items, Menue, `on_menu_event`), Z. 243-277 Versionspruefung, Z. 290-302 Run-Handler (NICHT anfassen, siehe 260917-eta), Z. 305-349 `mod tests`
- apps/desktop/src/setup.html Z. 145-174 (Markup: `.brand-mark`-SVG NICHT anfassen — 260917-jdf koennte sie parallel aendern; Aenderungen nur ab `<h1>`), Z. 176-306 (Script: `invoke`, `connect()`, Meldungsfunktionen)
- apps/desktop/src-tauri/capabilities/default.json (nur `core:default` + Plugin-Rechte; `check_server`/`save_server_url` stehen NICHT darin — App-Commands brauchen keinen Eintrag; es gibt keinen `remote`-Block, und das bleibt so)
- ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.3/src/manager/mod.rs Z. 337-346 (`tauri_protocol_url`: Windows/Android `http(s)://tauri.localhost`, sonst `tauri://localhost`; pub(crate), darum eigener Helfer), tray/mod.rs Z. 222-234 (`TrayIconBuilder::with_id`) und Z. 518-526 (`set_tooltip`, Linux unsupported), app.rs Z. 826-834 (`tray_by_id`), webview/mod.rs Z. 1819-1823 (ACL-Pruefung fuer App-Commands bei Remote-Ursprung)
</read_first>
<behavior>
Neue Tests in `#[cfg(test)] mod tests` (deutsche snake_case-Namen wie im Bestand):
- `server_host(Some("https://tessera.ctl.de/"))` → `Some("tessera.ctl.de")`
- `server_host(Some("http://localhost:3000/"))` → `Some("localhost:3000")`
- `server_host(Some("https://host:443/pfad?x=1"))` → `Some("host")` (Standardport faellt weg, Pfad/Query auch)
- `server_host(None)`, `server_host(Some(""))`, `server_host(Some("kein url"))` → jeweils `None`
- `tray_labels(Some("https://tessera.ctl.de/"))` → `("Tessera – tessera.ctl.de", "Verbunden mit tessera.ctl.de")`
- `tray_labels(None)` → `("Tessera – nicht verbunden", "Nicht verbunden")`
- `setup_page_url(true).as_str()` → `"http://tauri.localhost/setup.html"`; `setup_page_url(false).as_str()` → `"tauri://localhost/setup.html"`
- `parse_server_url("https://tessera.ctl.de")` → `Ok`, `as_str()` = `"https://tessera.ctl.de/"`
- `parse_server_url("ftp://host")` → `Err("Es sind nur Adressen mit http oder https erlaubt.")`
- `parse_server_url("kein url")` → `Err("Diese Adresse ist ungültig.")`
Die fuenf bestehenden Tests bleiben unveraendert gruen.
</behavior>
<action>
Zuerst die Tests aus `<behavior>` in `mod tests` schreiben und `CARGO_BUILD_JOBS=4 cargo test --lib` rot sehen. Dann in lib.rs:
1. **Reine Helfer** (neben `api_url`/`with_desktop_marker`, jeweils deutscher Doc-Kommentar mit ae/oe/ue wie im Bestand):
- `parse_server_url(url: &str) -> Result<tauri::Url, String>`: `tauri::Url::parse` (Fehler → `Diese Adresse ist ungültig.`), Schema muss `http` oder `https` sein (sonst `Es sind nur Adressen mit http oder https erlaubt.`). Genau die beiden Texte, die heute in `check_server` inline stehen; `check_server` und `save_server_url` rufen kuenftig diesen Helfer (Verhalten unveraendert, Duplikat weg), ebenso `open_server`.
- `server_host(url: Option<&str>) -> Option<String>`: `url` → leer/None → None; `tauri::Url::parse` fehlgeschlagen oder `host_str()` None → None; sonst `host` bzw. `host:port`, wenn `parsed.port()` Some ist (der url-Crate laesst Standardports weg). Doc: Anzeige im Tray soll kurz sein — nur der Host, kein Schema, kein Pfad.
- `tray_labels(url: Option<&str>) -> (String, String)`: Rueckgabe (Tooltip, Menuezeile). Mit Host: `Tessera – {host}` / `Verbunden mit {host}`; ohne: `Tessera – nicht verbunden` / `Nicht verbunden`. Gedankenstrich U+2013 wie in `update_labels`.
- `setup_page_url(windows: bool) -> tauri::Url`: `windows` → `http://tauri.localhost/setup.html`, sonst `tauri://localhost/setup.html`. Doc: spiegelt Tauris nicht oeffentliche `tauri_protocol_url` (WebView2 kennt kein eigenes Schema, deshalb `http://tauri.localhost`; `useHttpsScheme` ist in tauri.conf.json nicht gesetzt, darum `http`); `WebviewUrl::App` laesst sich nicht an `navigate` geben. Der `tauri dev`-Fall mit `devUrl` wird in diesem Projekt nicht genutzt (CI baut Release) und ist bewusst nicht abgebildet. Aufrufstelle uebergibt `cfg!(windows)`.
- `stored_server_url(app: &AppHandle) -> Option<String>`: `app.store("config.json").ok()?.get("server_url")`, nur nicht-leere Strings. Das ist die EINZIGE Lesestelle des Store-Werts (Start, `get_server_url`, `open_server`, Tray-Klick `update`).
2. **State und Auffrischung**: `struct TrayItems { connected: tauri::menu::MenuItem<tauri::Wry>, update: tauri::menu::MenuItem<tauri::Wry> }` (MenuItem ist Send + Sync, `app.manage` verlangt das). `fn apply_server(app: &AppHandle, url: Option<&str>)`: `let (tooltip, line) = tray_labels(url)`; `app.tray_by_id("main")` → `set_tooltip(Some(tooltip))`; `app.state::<TrayItems>().connected.set_text(line)`; Fehler ignorieren (`let _ =`) wie im Bestand. Doc: einzige Stelle, die Tooltip und Menuezeile setzt; `set_tooltip` ist unter Linux ein No-Op.
3. **Versionspruefung herausziehen**: `fn spawn_version_check(app: AppHandle, server_url: String)`: holt `update` aus `app.state::<TrayItems>()` (Klon), setzt ihn auf `Update herunterladen` + `set_enabled(false)` zurueck (nach einem Serverwechsel darf kein Hinweis des alten Servers stehen bleiben), dann der bisherige `tauri::async_runtime::spawn`-Block aus `setup` (Z. 249-276) unveraendert samt Kommentar; `app_version`/`app_commit` (`env!`) wandern mit hinein. Den Menue-Standardtext als `const UPDATE_ITEM_DEFAULT: &str = "Update herunterladen";` anlegen und an beiden Stellen (Bau des Items, Reset) nutzen.
4. **Commands**: `#[tauri::command] fn get_server_url(app: AppHandle) -> Option<String> { stored_server_url(&app) }` (Doc: liefert nur den gespeicherten Wert, nichts anderes; nur vom lokalen Ursprung aufrufbar, siehe Punkt 7). `#[tauri::command] fn open_server(app: AppHandle) -> Result<(), String>`: `stored_server_url` → ohne Wert `Err("Es ist keine Server-Adresse gespeichert.")`; `parse_server_url`; Fenster `main` → `navigate(with_desktop_marker(&parsed))`, Fehler per `map_err(|e| e.to_string())`. `save_server_url` umbauen: `parse_server_url(&url)?` → `normalized` → Store speichern → `apply_server(&app, Some(&normalized))` → `spawn_version_check(app.clone(), normalized.clone())` → Navigation wie bisher. `generate_handler![check_server, save_server_url, get_server_url, open_server]`.
5. **`setup`**: `let server_url = stored_server_url(app.handle());` ersetzt das verschachtelte if-let; Startnavigation ueber `parse_server_url` + `with_desktop_marker` wie bisher. Tray-Aufbau: Item `connected` = `MenuItemBuilder::with_id("connected", tray_labels(server_url.as_deref()).1).enabled(false)`; Item `change_server` = `MenuItemBuilder::with_id("change_server", "Server-Adresse ändern…")` (echtes Auslassungszeichen U+2026); `update` mit `UPDATE_ITEM_DEFAULT`. Menue: `connected` · Trenner · `open` · `change_server` · `update` · Trenner · `autostart` · Trenner · `quit` (Kommentar Z. 153-155 entsprechend anpassen). Vor dem Tray-Bau `app.manage(TrayItems { connected: connected.clone(), update: update.clone() });`. Tray: `TrayIconBuilder::with_id("main")` statt `::new()`, `.tooltip(tray_labels(server_url.as_deref()).0)`. Nach `.build(app)?`: `apply_server(app.handle(), server_url.as_deref());` (setzt beide Anzeigen aus derselben Quelle) und `if let Some(url) = server_url.clone() { spawn_version_check(app.handle().clone(), url); }` — der bisherige Inline-Block Z. 243-277 entfaellt. Die Variable fuer den Start-Klon der Adresse im Menue-Closure entfaellt ersatzlos.
6. **Menue-Handler**: neuer Zweig `"change_server"`: Fenster `main` → `let _ = w.navigate(setup_page_url(cfg!(windows)));` dann `unminimize`/`show`/`set_focus` (gleiche drei Zeilen wie bei `open`). Zweig `"update"`: `if let Some(server) = stored_server_url(app)` statt des Klons; Ziel-URL-Bildung unveraendert (ohne `desktop=1`, oeffnet im System-Browser). `open`, `autostart`, `quit`, Linksklick-Handler, `on_window_event`, Run-Handler: unveraendert.
7. **Kein Capability-Eintrag, kein `remote`-Block**: App-Commands sind vom lokalen Ursprung (`tauri://localhost` / `http://tauri.localhost`) ohne ACL-Manifest erlaubt; vom Remote-Ursprung (Server-Seite) verweigert Tauri sie, weil capabilities/default.json keinen `remote`-Block hat (T-JN2-01). Die Datei bleibt unangetastet.
8. `CARGO_BUILD_JOBS=4 cargo fmt` anwenden (Bestand ist rustfmt-konform; anders als bei 260917-eta gibt es hier kein Ein-Zeilen-Gate, das fmt zerstoeren koennte).
Dann **setup.html** (nur ab `<h1>` und im Script; SVG und CSS-Bestand unveraendert, neue CSS-Regeln anhaengen):
9. Markup: `<p class="subtitle" id="subtitle">Desktop-App einrichten</p>`; nach der Unterzeile `<p id="current-server" class="current-server"></p>` (CSS: `display: none; text-align: left; font-size: 0.8125rem; color: oklch(0.75 0 0); margin: -20px 0 24px;` — sichtbar erst im Aenderungsmodus); nach `#connect-btn` ein `<button id="cancel-btn" type="button" class="secondary" hidden>Abbrechen</button>` (CSS `button.secondary { margin-top: 12px; background: transparent; color: oklch(0.85 0 0); border: 1px solid oklch(0.30 0.01 260); }` und `button[hidden] { display: none; }`, damit `hidden` gegen die `button`-Regel gewinnt).
10. Script: Referenzen `subtitle`, `currentServer`, `cancelBtn`. `async function init()`: `try { const current = await invoke('get_server_url'); if (typeof current === 'string' && current) { enterChangeMode(current); } } catch { /* Erststart-Verhalten */ }`; am Ende des Moduls `init();`. `enterChangeMode(current)`: `urlInput.value = current`; `currentServer.textContent = 'Aktuell verbunden mit: ' + current`; `currentServer.style.display = 'block'`; `subtitle.textContent = 'Server-Adresse ändern'`; `cancelBtn.hidden = false`; `urlInput.focus(); urlInput.select();`. `cancelBtn`-Klick: `clearMessages(); cancelBtn.disabled = true; try { await invoke('open_server'); } catch (err) { showError(String(err)); cancelBtn.disabled = false; }`. Der Wert kommt per `textContent` in die Seite (kein HTML-Einfuegen — T-JN2-05). Kommentar im Script (deutsch): Aenderungsmodus, Erststart bleibt der Standardpfad. `connect()`, `validateUrl`, Enter-Taste, Meldungen: unveraendert.
11. Gate laut `<verify>`. Kein `tauri build`, kein `tauri dev`.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri && export CARGO_BUILD_JOBS=4 && cargo fmt --check && cargo check && cargo clippy && cargo test --lib && grep -q 'TrayIconBuilder::with_id("main")' src/lib.rs && grep -q 'tray_by_id("main")' src/lib.rs && grep -q 'with_id("connected"' src/lib.rs && grep -q 'with_id("change_server", "Server-Adresse ändern…")' src/lib.rs && grep -q 'generate_handler!\[check_server, save_server_url, get_server_url, open_server\]' src/lib.rs && grep -q 'fn spawn_version_check(' src/lib.rs && grep -q 'fn apply_server(' src/lib.rs && test "$(grep -c 'stored_server_url(' src/lib.rs)" -ge 4 && grep -q "setup_page_url(cfg!(windows))" src/lib.rs && grep -q "invoke('get_server_url')" ../src/setup.html && grep -q "invoke('open_server')" ../src/setup.html && grep -q 'id="cancel-btn"' ../src/setup.html && grep -q 'id="current-server"' ../src/setup.html && grep -q 'class="brand-mark"' ../src/setup.html && ! grep -q '"remote"' capabilities/default.json && git -C /home/vicolab/projects/tessera-ctl diff --quiet -- apps/desktop/src-tauri/capabilities/default.json apps/desktop/src-tauri/Cargo.lock</automated>
</verify>
<done>Tray zeigt `Verbunden mit {host}` (gesperrt) als erste Zeile und `Tessera – {host}` als Tooltip; `Server-Adresse ändern…` navigiert zur Setup-Seite, die im Aenderungsmodus vorbelegt ist und „Abbrechen“ anbietet; nach dem Speichern einer neuen Adresse sind Tooltip, Menuezeile und Update-Ziel ohne Neustart aktuell und die Versionspruefung laeuft neu; mindestens 10 neue + 5 alte Rust-Tests gruen, fmt/check/clippy sauber; capabilities/default.json unveraendert; Commit `feat(desktop): Verbundenen Server im Infobereich zeigen, Server-Adresse nachträglich änderbar`. Der Nachweis am Bildschirm (Tooltip, Menue, Wechsel, Abbrechen, Update-Link nach Wechsel) folgt durch den Orchestrator mit dem CI-Paket auf der Windows-VM — im SUMMARY als offen fuehren.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Web — Block „Verbunden mit: {origin}“ nur im Client auf Einstellungen → Desktop-App, i18n, Test</name>
<files>apps/web/src/components/settings/desktop-app-settings.tsx, apps/web/src/components/settings/desktop-app-settings.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
<read_first>
- apps/web/src/components/settings/desktop-app-settings.tsx (komplett; die vier `<p>` mit `desktop.intro` … `desktop.update`, danach Version/Downloads)
- apps/web/src/components/settings/desktop-app-settings.test.tsx (next-intl-Mock mit `{name}`-Ersetzung, `loadDesktopLatest`-Mock, `afterEach` mit `cleanup`)
- apps/web/src/lib/desktop-client.ts (`useIsDesktopClient`, `DESKTOP_COOKIE_NAME`) und desktop-client.test.ts Z. 10-17 (Cookie setzen/loeschen im Test)
- apps/web/src/messages/de.json und en.json, Block `settings.desktop` (Z. 161-173; letzter Schluessel `unavailable`) — FRISCH lesen, parallele Plaene ergaenzen andere Namensraeume
</read_first>
<behavior>
desktop-app-settings.test.tsx (Cookie in `afterEach` wie in desktop-client.test.ts loeschen; `DESKTOP_COOKIE_NAME` importieren):
- Test 4 (im Desktop-Client): `document.cookie = 'tessera_desktop=1; path=/'`, `loadDesktopLatest.mockResolvedValue(null)`, `render` → `await screen.findByText('Verbunden mit: http://localhost:3000')` vorhanden; `screen.getByText('Ändern über das Tessera-Symbol im Infobereich → „Server-Adresse ändern…“')` vorhanden; `screen.getByTestId('desktop-connected')` vorhanden.
- Test 5 (im Browser): ohne Cookie, `loadDesktopLatest.mockResolvedValue(null)`, `render`, `await act(async () => {})` → `screen.queryByTestId('desktop-connected')` ist null.
- Tests 1-3 unveraendert gruen (sie laufen ohne Cookie).
</behavior>
<action>
Tests zuerst schreiben, rot sehen, dann:
1. **de.json / en.json**, innerhalb `settings.desktop` NACH `unavailable` zwei Schluessel ergaenzen (nur Zeilen anhaengen, Komma an `unavailable` nicht vergessen, sonst nichts anfassen):
- de: `"connectedTo": "Verbunden mit: {origin}"`, `"changeHint": "Ändern über das Tessera-Symbol im Infobereich → „Server-Adresse ändern…“"`
- en: `"connectedTo": "Connected to: {origin}"`, `"changeHint": "To change it, use the Tessera icon in the notification area → “Change server address…”"`
2. **desktop-app-settings.tsx**: `import { useIsDesktopClient } from '@/lib/desktop-client';` `const isDesktop = useIsDesktopClient();` nach `useLocale()`. Nach den vier `<p>`-Saetzen und VOR dem `info === null`-Hinweis rendern: `{isDesktop && (<div data-testid="desktop-connected" className="mt-4 space-y-1 rounded border border-border bg-muted/30 p-3 text-sm"><p className="text-foreground">{t('desktop.connectedTo', { origin: window.location.origin })}</p><p className="text-muted-foreground">{t('desktop.changeHint')}</p></div>)}`. `window.location.origin` darf hier direkt gelesen werden: `isDesktop` ist im Server-HTML und im ersten Client-Render false (Hook mit useEffect), der Zweig rendert erst nach der Hydration — Satz dazu in den Komponenten-Doc-Kommentar (260917-jn2). Download-Knoepfe bleiben wie bisher (kein Ausblenden hier — der Update-Link aus dem Tray oeffnet diese Seite im System-Browser, dort bleiben sie sichtbar; im Client stoeren sie nicht).
3. Keine weiteren Komponenten, keine Middleware-Aenderung, kein neues Paket.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/settings/desktop-app-settings.test.tsx && pnpm --filter @tessera/web exec vitest run && pnpm --filter @tessera/web type-check && node -e 'for (const l of ["de","en"]) { const m = require("./apps/web/src/messages/" + l + ".json").settings.desktop; if (!m.connectedTo || !m.changeHint || !m.connectedTo.includes("{origin}")) { console.error("i18n fehlt:", l); process.exit(1); } } console.log("i18n OK")' && grep -q 'data-testid="desktop-connected"' apps/web/src/components/settings/desktop-app-settings.tsx && grep -q 'useIsDesktopClient' apps/web/src/components/settings/desktop-app-settings.tsx</automated>
</verify>
<done>Im Client (Cookie `tessera_desktop=1`) zeigt Einstellungen → Allgemein → Desktop-App „Verbunden mit: {origin}“ plus Hinweis auf „Server-Adresse ändern…“; im Browser fehlt der Block; beide Sprachen; alle Web-Tests (bisher 417 + 2) und type-check gruen; Commit `feat(web): Einstellungen → Desktop-App zeigt in der App den verbundenen Server`.</done>
</task>
<task type="auto">
<name>Task 3: Handbuch (Server-Adresse sehen und ändern) und CHANGELOG</name>
<files>docs/anleitung-anwender.md, CHANGELOG.md</files>
<read_first>
- docs/anleitung-anwender.md Z. 184-197 („Erster Start: Server-Adresse“, „Fenster, Infobereich und Beenden“ mit Tray-Liste) und Z. 203-205 („Neue Version“); Anfuehrungszeichen in dieser Datei: oeffnend „ (U+201E), schliessend gerades " — so beibehalten
- CHANGELOG.md Z. 1-8 — FRISCH lesen: `## Unveröffentlicht` ist nach der Freigabe 1.2.0 leer; parallele Plaene (Favoriten, Bildmarke, CI) koennen inzwischen Unterabschnitte angelegt haben. Anfuehrungszeichen hier „…“ (U+201E/U+201C), Praefix `Desktop-App:`, kein Punkt am Ende
</read_first>
<action>
1. **docs/anleitung-anwender.md**:
- Abschnitt „Erster Start: Server-Adresse“: am Ende des Absatzes den Satz anfuegen: „Die eingetragene Adresse können Sie später jederzeit ändern, siehe [Server-Adresse ändern](#server-adresse-ändern).“
- Tray-Liste im Abschnitt „Fenster, Infobereich und Beenden“: als ERSTE Zeile `- **Verbunden mit …** — zeigt grau den Tessera-Server, mit dem die App verbunden ist (nicht anklickbar)`; nach **Öffnen** die Zeile `- **Server-Adresse ändern…** — siehe [Server-Adresse ändern](#server-adresse-ändern)`. Davor im Satz „Ein Rechtsklick zeigt ein Menü mit:“ nichts aendern. Nach der Liste (vor „Nur „Beenden" beendet …“) einen Satz: „Fahren Sie mit der Maus über das Symbol, nennt der Hinweistext ebenfalls den verbundenen Server (unter Windows).“
- Neuer Unterabschnitt `### Server-Adresse ändern` direkt VOR `### Automatischer Start`: Absatz 1 — wo die aktuelle Adresse steht (Hinweistext am Symbol im Infobereich, erste Zeile des Rechtsklick-Menüs, in der App unter Einstellungen → Allgemein → Desktop-App als „Verbunden mit: …“). Absatz 2 — der Weg: Rechtsklick auf das Symbol → „Server-Adresse ändern…" → die Einrichtungsseite erscheint mit der aktuellen Adresse im Feld und der Zeile „Aktuell verbunden mit: …" → neue Adresse eintragen → „Verbinden" (Prüfung wie beim ersten Start) → die App wechselt sofort, Hinweistext und Menü nennen die neue Adresse, ein Neustart ist nicht nötig; „Abbrechen" bringt Sie ohne Änderung zurück. Absatz 3 (kurz) — Anmeldung: beim Wechsel auf einen anderen Server melden Sie sich dort wie gewohnt an. Sie-Form, Anfuehrungszeichen wie in der Datei.
2. **CHANGELOG.md**, `## Unveröffentlicht`: Existiert darunter bereits `### Neu` (von einem parallelen Plan), die zwei Zeilen am ENDE dieser Liste anhaengen; sonst direkt unter `## Unveröffentlicht` (Leerzeile, `### Neu`, Leerzeile, Liste, Leerzeile vor `## 1.2.0 – 2026-09-17`) anlegen. Der Bestand nennt den Abschnitt `### Neu` (nicht „Hinzugefügt“) — Stil wie Bestand. Zeilen:
- `- Desktop-App: das Symbol im Infobereich zeigt den verbundenen Tessera-Server – im Hinweistext und als erste Zeile des Menüs; in der App auch unter Einstellungen → Desktop-App als „Verbunden mit: …“`
- `- Desktop-App: Server-Adresse nachträglich änderbar über „Server-Adresse ändern…“ im Menü des Infobereich-Symbols – ohne Neustart`
Fremde Zeilen (auch neue aus parallelen Plaenen) unveraendert lassen; vor dem Commit `git diff CHANGELOG.md` gegenpruefen, dass nur diese Zeilen (und ggf. die Ueberschrift `### Neu`) hinzugekommen sind. Nur die zwei Dateien dieses Tasks per `git add` uebernehmen (`git add docs/anleitung-anwender.md CHANGELOG.md`), nie `git add -A`.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && grep -q '^### Server-Adresse ändern' docs/anleitung-anwender.md && grep -q 'Server-Adresse ändern…\*\*' docs/anleitung-anwender.md && grep -q 'Verbunden mit …\*\*' docs/anleitung-anwender.md && grep -q 'Aktuell verbunden mit' docs/anleitung-anwender.md && test "$(awk '/^## Unveröffentlicht/{f=1;next} /^## /{f=0} f' CHANGELOG.md | grep -c '^- Desktop-App: ')" -ge 2 && awk '/^## Unveröffentlicht/{f=1;next} /^## /{f=0} f' CHANGELOG.md | grep -q '^### Neu' && pnpm --filter @tessera/web exec vitest run src/lib/changelog.test.ts</automated>
</verify>
<done>Handbuch beschreibt, wo die Server-Adresse steht und wie sie geaendert wird (neuer Unterabschnitt, Tray-Liste, Erststart-Hinweis); CHANGELOG traegt zwei neue `Desktop-App:`-Stichpunkte unter Unveröffentlicht / Neu; Commit `docs: Desktop-App — Server-Adresse sehen und ändern (Handbuch, CHANGELOG)`.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Remote-Seite (Tessera-Server im WebView) → Tauri-IPC | Die Server-Seite laeuft im selben Fenster wie die lokale Setup-Seite; sie darf keine App-Commands erreichen |
| Setup-Seite (lokal) → Rust-Commands | Nutzer-Eingabe (Adresse) wird geparst, gespeichert und als Navigationsziel genutzt |
| Store (config.json) → Tray/Anzeige | Gespeicherter Wert wird in Tooltip, Menuezeile, Update-Ziel und Setup-Seite angezeigt |
| Web-Client (Cookie) → Einstellungsseite | Cookie `tessera_desktop` ist frei setzbar; steuert nur die Anzeige eines Blocks |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-JN2-01 | Elevation of Privilege | `get_server_url`, `open_server`, `save_server_url` (IPC) | high | mitigate | Tauri verweigert App-Commands aus Remote-Ursprung, solange die Capability keinen `remote`-Block hat (tauri-2.11.3 webview/mod.rs Z. 1819-1823, `!is_local` erzwingt ACL). capabilities/default.json bleibt unveraendert; Gate in Task 1 bricht ab, falls ein `"remote"`-Schluessel auftaucht. Kein `dangerousRemoteDomainIpcAccess`. |
| T-JN2-02 | Tampering | `setup_page_url`, `change_server`-Klick | medium | mitigate | Navigationsziel ist eine Konstante ohne Nutzer-Eingabe; `navigate` statt `eval` — die Remote-Seite fuehrt kein von Rust injiziertes JavaScript aus. Reine Funktion je Plattform getestet. |
| T-JN2-03 | Tampering | `open_server`, `save_server_url` (Navigationsziel) | medium | mitigate | Beide gehen ausschliesslich ueber `parse_server_url` (nur http/https); `open_server` navigiert nur zum gespeicherten Wert, `save_server_url` nur zu dem Wert, den die Setup-Seite zuvor per `check_server` bestaetigt hat (wie heute). Kein anderes Schema, keine Datei-URLs. |
| T-JN2-04 | Information Disclosure | `get_server_url`, Tooltip, Menuezeile | low | accept | Der Wert ist die vom Nutzer selbst eingetragene Server-Adresse (kein Geheimnis, keine Zugangsdaten). `get_server_url` liefert nur diesen einen Store-Schluessel und ist nur lokal aufrufbar (T-JN2-01). |
| T-JN2-05 | Tampering | setup.html (`#current-server`, `#server-url`) | low | mitigate | Gespeicherter Wert wird per `textContent`/`value` eingesetzt, nie als HTML; CSP `default-src 'self'` bleibt. |
| T-JN2-06 | Spoofing | Cookie `tessera_desktop` → Web-Block | low | accept | Der Block zeigt nur `window.location.origin` (vom Browser bestimmt) und einen Hinweistext; kein Auth-, Rechte- oder Datenpfad haengt daran (wie T-H2S-01). |
| T-JN2-07 | Denial of Service | `spawn_version_check` nach Wechsel | low | accept | Genau eine zusaetzliche Anfrage je Wechsel; ein noch laufender alter Check kann im seltenen Fall spaeter antworten und den Update-Eintrag setzen — beim naechsten Start korrigiert sich das; bewusst hingenommen. |
| T-JN2-SC | Tampering | npm/pip/cargo installs | low | accept | Keine neue Abhaengigkeit (kein `pnpm add`, kein `cargo add`, Cargo.lock unveraendert); package-legitimacy gate entfaellt. |
</threat_model>
<verification>
- Rust: `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` (apps/desktop/src-tauri, `CARGO_BUILD_JOBS=4`) gruen, mindestens 15 Tests; Tray mit `with_id("main")`, Items `connected`/`change_server`, Handler `get_server_url`/`open_server` registriert, `spawn_version_check`/`apply_server` vorhanden, Store-Lesen nur ueber `stored_server_url`.
- setup.html: `get_server_url` beim Laden, `open_server` am Abbrechen-Knopf, `#current-server`/`#cancel-btn` vorhanden, SVG unveraendert.
- capabilities/default.json: unveraendert, kein `remote`-Block.
- Web: `pnpm --filter @tessera/web exec vitest run` (alle Dateien) und `type-check` gruen; i18n-Schluessel in beiden Sprachen.
- Doku: neuer Unterabschnitt „Server-Adresse ändern“, Tray-Liste erweitert; CHANGELOG zwei neue Zeilen unter Unveröffentlicht / Neu; changelog.test.ts gruen.
- Offen (Nachweis durch Orchestrator mit dem CI-Paket auf der Windows-VM): Tooltip `Tessera – {host}`, Menuezeile, Klick auf „Server-Adresse ändern…“ zeigt die vorbelegte Setup-Seite, „Abbrechen“ fuehrt zurueck, Wechsel auf eine andere Adresse aktualisiert Tooltip/Menue/Update-Ziel ohne Neustart, Web-Block in der App sichtbar und im Browser nicht.
</verification>
<success_criteria>
- Alle `must_haves.truths` erfuellt; drei Commits (`feat(desktop)`, `feat(web)`, `docs`) ohne Push, ohne `tauri build`, ohne Docker, ohne `.planning/`-Commits.
- Keine Datei ausserhalb von `files_modified` veraendert (vor jedem Commit `git status` gegenpruefen; parallele Plaene arbeiten im selben Baum — nur eigene Dateien per `git add` nennen).
- SUMMARY nennt die offenen VM-Nachweise ausdruecklich und vermerkt, dass die Versionspruefung nach dem Wechsel neu angestossen wird.
</success_criteria>
<output>
Create `/home/vicolab/projects/tessera-ctl/.planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.md` when done
</output>
@@ -0,0 +1,210 @@
---
phase: quick-260917-jn2
plan: 01
subsystem: desktop-client
tags: [tauri, rust, tray, nextjs, i18n, desktop]
# Dependency graph
requires:
- phase: 18-desktop-client-fertigstellen
provides: Tauri-Desktop-Client mit Tray-Menue (open/update/autostart/quit), Setup-Seite (Erststart), Web-Einstellungsseite Desktop-App
- phase: quick-260917-eta
provides: Run-Handler-Muster (code: None vs. code: Some), unminimize() vor show() in "open" und Linksklick — unangetastet uebernommen
provides:
- "Tray zeigt den verbundenen Server an drei Stellen: Tooltip 'Tessera – {host}', gesperrte erste Menuezeile 'Verbunden mit {host}', Web-Einstellungsseite 'Verbunden mit: {origin}'"
- "Neuer Tray-Eintrag 'Server-Adresse ändern…' navigiert zur gebuendelten Setup-Seite im Aenderungsmodus (vorbelegtes Feld, 'Aktuell verbunden mit: …', Knopf 'Abbrechen')"
- "Nach einem Serverwechsel aktualisieren sich Tooltip, Menuezeile und Update-Ziel ohne Neustart; die Versionspruefung laeuft neu gegen den neuen Server"
- "Vier reine, getestete Helfer server_host/tray_labels/setup_page_url/parse_server_url plus stored_server_url als einzige Store-Lesestelle"
affects: [desktop-client, tray-verhalten, updater]
# Actuals (#2632)
actuals:
tokens: 8317
tasks: 3
commits: 3
plan_head_before: b023d6f72655706167d72430337470ad3722144f
# Tech tracking
tech-stack:
added: []
patterns:
- "app.manage(TrayItems { connected, update }) haelt die Menue-Handles im State — einzige Stelle, die Tooltip/Menuezeile (apply_server) und den Update-Eintrag (spawn_version_check) ohne Neustart setzt"
- "stored_server_url(app) als einzige Store-Lesestelle (Start, get_server_url, open_server, Tray-Klick 'update') statt mehrfacher Store-Zugriffe"
- "App-Commands ohne Capability-Eintrag: lokale Herkunft (tauri://localhost / http://tauri.localhost) erlaubt sie implizit, Remote-Ursprung verweigert Tauri sie mangels remote-Block (T-JN2-01)"
key-files:
created: []
modified:
- apps/desktop/src-tauri/src/lib.rs
- apps/desktop/src/setup.html
- apps/web/src/components/settings/desktop-app-settings.tsx
- apps/web/src/components/settings/desktop-app-settings.test.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- docs/anleitung-anwender.md
- CHANGELOG.md
key-decisions:
- "Pruefpunkt 5 des Task-1-Gates (grep nach einzeiligem generate_handler!\\[...\\]) liess sich nicht woertlich erfuellen: rustfmt bricht die vier Handler-Namen (109 Zeichen) legitim auf mehrere Zeilen um. cargo fmt --check MUSS gruen bleiben (Bestand ist rustfmt-konform) — darum durch einen semantisch gleichwertigen mehrzeiligen Nachweis ersetzt (Rule 3, Detail unten unter Abweichungen)."
- "Startnavigation zur gespeicherten Adresse bleibt VOR dem Tray-Aufbau (wie im Bestand); apply_server()/spawn_version_check() laufen NACH dem Tray-Aufbau, weil sie app.state::<TrayItems>() brauchen, das erst beim Tray-Aufbau gemanagt wird."
- "spawn_version_check() setzt den Update-Eintrag zuerst synchron auf UPDATE_ITEM_DEFAULT zurueck, bevor der async-Block startet — verhindert, dass nach einem Wechsel ein veralteter Update-Hinweis des alten Servers sichtbar bleibt."
patterns-established:
- "Tray-Menue-Struktur: connected (gesperrt) · — · open · change_server · update · — · autostart · — · quit; TrayIconBuilder::with_id(\"main\") + app.tray_by_id(\"main\") als Paar fuer spaetere Auffrischung"
requirements-completed: [QUICK-260917-JN2]
coverage:
- id: D1
description: "Rust: vier reine Helfer (server_host, tray_labels, setup_page_url, parse_server_url) plus stored_server_url, TrayItems-State, apply_server, spawn_version_check, Commands get_server_url/open_server; capabilities/default.json unveraendert"
requirement: "QUICK-260917-JN2"
verification:
- kind: unit
ref: "apps/desktop/src-tauri/src/lib.rs mod tests — 18 Tests (13 neu + 5 Bestand), cargo test --lib"
status: pass
- kind: other
ref: "cargo fmt --check, cargo check, cargo clippy — je 0 Warnungen/Fehler"
status: pass
human_judgment: false
- id: D2
description: "setup.html im Aenderungsmodus: get_server_url beim Laden, Feld vorbelegt, 'Aktuell verbunden mit: …', Knopf 'Abbrechen' → open_server; ohne gespeicherte Adresse Erststart-Verhalten unveraendert"
requirement: "QUICK-260917-JN2"
verification:
- kind: other
ref: "Grep-Gate Task 1 (invoke('get_server_url'), invoke('open_server'), #cancel-btn, #current-server, brand-mark-SVG unveraendert)"
status: pass
human_judgment: true
rationale: "Reale Bedienprobe (Tooltip, Menue, Klick auf 'Server-Adresse ändern…', vorbelegte Seite, 'Abbrechen', Wechsel aktualisiert Tooltip/Menue/Update-Ziel ohne Neustart) erfordert die Windows-Test-VM mit dem CI-Paket — macht laut Plan der Orchestrator im Anschluss, nicht dieser Ausfuehrungslauf."
- id: D3
description: "Web: DesktopAppSettings zeigt im Desktop-Client 'Verbunden mit: {origin}' plus Aenderungshinweis; im Browser fehlt der Block; beide Sprachen"
requirement: "QUICK-260917-JN2"
verification:
- kind: unit
ref: "apps/web/src/components/settings/desktop-app-settings.test.tsx — Test 4 (Cookie) und Test 5 (ohne Cookie), 5/5 Tests gruen"
status: pass
- kind: unit
ref: "pnpm --filter @tessera/web exec vitest run — 431/431 gruen; pnpm --filter @tessera/web type-check — 0 Fehler"
status: pass
human_judgment: false
- id: D4
description: "Handbuch: neuer Unterabschnitt 'Server-Adresse ändern', erweiterte Tray-Liste, Erststart-Verweis; CHANGELOG zwei Desktop-App-Stichpunkte unter Unveröffentlicht/Neu"
requirement: "QUICK-260917-JN2"
verification:
- kind: other
ref: "Grep-Gate Task 3 (Ueberschrift, Tray-Liste, 'Aktuell verbunden mit', CHANGELOG-Zeilenzahl) + git diff CHANGELOG.md gegengeprueft (nur die 2 Zeilen neu)"
status: pass
- kind: unit
ref: "apps/web/src/lib/changelog.test.ts — 10/10 gruen"
status: pass
human_judgment: false
duration: ~25min (nicht exakt gestoppt)
completed: 2026-09-17
status: complete
---
# Quick Task 260917-jn2: Desktop-Client — aktuelle Server-Adresse sehen und ändern Summary
**Tray zeigt an drei Stellen (Tooltip, gesperrte Menuezeile, Web-Einstellungsseite), mit welchem Tessera-Server der Desktop-Client verbunden ist, und ein neuer Tray-Eintrag „Server-Adresse ändern…" fuehrt zur Setup-Seite im Aenderungsmodus — ohne config.json zu loeschen und ohne Neustart der App.**
## Performance
- **Duration:** ~25 min (RED/GREEN-Zyklus fuer Rust-Helfer und Web-Block, drei Verifikations-Gates)
- **Completed:** 2026-09-17T14:53:26+02:00
- **Tasks:** 3/3
- **Files modified:** 8
## Accomplishments
- **Rust (`lib.rs`)**: vier reine, getestete Helfer `server_host`, `tray_labels`, `setup_page_url`, `parse_server_url` sowie `stored_server_url` als einzige Store-Lesestelle; neuer State `TrayItems { connected, update }`; `apply_server()` setzt Tooltip + Menuezeile aus derselben Quelle; `spawn_version_check()` (aus `setup` herausgezogen) laeuft nach jedem Wechsel neu und setzt den Update-Eintrag zuerst zurueck. Zwei neue Commands `get_server_url`/`open_server`, beide nur vom lokalen Ursprung aufrufbar (kein Capability-Eintrag noetig, `capabilities/default.json` unveraendert). Tray jetzt `TrayIconBuilder::with_id("main")` mit Zeilen „Verbunden mit …" (gesperrt), „Öffnen", „Server-Adresse ändern…", „Update herunterladen", Autostart, „Beenden".
- **`setup.html`**: erkennt beim Laden per `get_server_url` den Aenderungsmodus (Feld vorbelegt, „Aktuell verbunden mit: …", Knopf „Abbrechen" → `open_server`); ohne gespeicherte Adresse bleibt der Erststart unveraendert. SVG-Bildmarke unangetastet.
- **Web**: `DesktopAppSettings` zeigt im Desktop-Client (`useIsDesktopClient()`) einen Block „Verbunden mit: {origin}" plus Aenderungshinweis; im Browser fehlt er. Zwei neue i18n-Schluessel in beiden Sprachen.
- **Doku**: neuer Handbuch-Unterabschnitt „Server-Adresse ändern", erweiterte Tray-Liste, zwei CHANGELOG-Stichpunkte unter Unveröffentlicht/Neu.
## Task Commits
Each task was committed atomically:
1. **Task 1: Rust + setup.html — Verbunden-Zeile/Tooltip, „Server-Adresse ändern…", Commands, Tray-Auffrischung, Tests** - `29c132e` (feat)
2. **Task 2: Web — Block „Verbunden mit: {origin}" nur im Client, i18n, Test** - `4c79874` (feat)
3. **Task 3: Handbuch und CHANGELOG** - `4d48543` (docs)
_Hinweis: kein separater `test(...)`-Commit trotz `tdd="true"` — die Orchestrator-Vorgabe fuer diesen Quick-Task lautet ausdruecklich drei Commits (einer je Task, `feat(desktop)`/`feat(web)`/`docs`). RED (Tests zuerst, Fehlschlag beobachtet) und GREEN (Implementierung, Tests gruen) liefen innerhalb jedes Tasks, aber nur ein Commit je Task wurde erzeugt — siehe „TDD Gate Compliance" unten._
## Files Created/Modified
- `apps/desktop/src-tauri/src/lib.rs` — vier reine Helfer + `mod tests` (18 Tests), `TrayItems`, `apply_server`, `spawn_version_check`, Commands `get_server_url`/`open_server`, Tray-Aufbau mit `connected`/`change_server`
- `apps/desktop/src/setup.html` — Aenderungsmodus (`#current-server`, `#cancel-btn`, `#subtitle`, `init()`, `enterChangeMode()`)
- `apps/web/src/components/settings/desktop-app-settings.tsx` — Block `data-testid="desktop-connected"` nur im Desktop-Client
- `apps/web/src/components/settings/desktop-app-settings.test.tsx` — Test 4 (Cookie) und Test 5 (ohne Cookie)
- `apps/web/src/messages/de.json`, `en.json` — `settings.desktop.connectedTo`, `settings.desktop.changeHint`
- `docs/anleitung-anwender.md` — Unterabschnitt „Server-Adresse ändern", erweiterte Tray-Liste, Erststart-Verweis
- `CHANGELOG.md` — zwei `Desktop-App:`-Stichpunkte unter Unveröffentlicht/Neu
## Endgueltige Namen fuer den Folgeplan (Updater, `tauri-plugin-updater`)
Der naechste Task baut auf dieser Tray-Struktur auf — hier die endgueltigen Signaturen:
- `struct TrayItems { connected: tauri::menu::MenuItem<tauri::Wry>, update: tauri::menu::MenuItem<tauri::Wry> }`, gemanagt via `app.manage(TrayItems { connected: connected.clone(), update: update.clone() })` direkt nach dem Menue-Bau, vor `TrayIconBuilder::with_id("main")...build(app)?`.
- `fn apply_server(app: &AppHandle, url: Option<&str>)` — einzige Stelle, die `app.tray_by_id("main")` (Tooltip) und `app.state::<TrayItems>().connected` (Menuezeile) setzt.
- `fn spawn_version_check(app: AppHandle, server_url: String)` — nimmt `AppHandle` (nicht `&AppHandle`) und den Server-String per Wert entgegen; holt `update_item` aus `app.state::<TrayItems>()`, setzt ihn synchron auf `UPDATE_ITEM_DEFAULT`/`enabled(false)` zurueck, dann `tauri::async_runtime::spawn(...)` wie im Bestand. Der Updater-Task kann hier andocken (z. B. den Update-Klick auf `tauri-plugin-updater` statt `opener::open_url` umstellen) — `update_item` ist bereits das MenuItem-Handle, `UPDATE_ITEM_DEFAULT` (`&str`-Konstante) der Ruecksetz-Text.
- `fn stored_server_url(app: &AppHandle) -> Option<String>` — einzige Store-Lesestelle; der Updater-Task sollte KEINE eigene Store-Lesung einfuehren, sondern diese Funktion wiederverwenden.
- Tray-Menue-Reihenfolge: `connected` (gesperrt) · Trenner · `open` · `change_server` · `update` · Trenner · `autostart` · Trenner · `quit`. Ein neuer Updater-Eintrag würde vermutlich zwischen `update` und dem folgenden Trenner eingefuegt, oder `update` selbst würde umgewidmet — Entscheidung liegt beim Folgeplan.
## Decisions Made
- **Pruefpunkt 5 des Task-1-Gates (Rule 3, Blocking):** Die Plan-Verifikation verlangt einen woertlichen Ein-Zeilen-Treffer `generate_handler!\[check_server, save_server_url, get_server_url, open_server\]`. Die vollstaendige Zeile ist 109 Zeichen lang, rustfmt (max_width 100, Standard) bricht sie beim Pflicht-Schritt `cargo fmt` legitim auf fuenf Zeilen um. `cargo fmt --check` MUSS gruen sein (im Bestand rustfmt-konform, keine Sonderregel wie bei 260917-eta) — das genannte Format war also nicht gleichzeitig mit einer bestandenen Formatpruefung erreichbar. Ich habe den betroffenen Pruefpunkt durch einen semantisch gleichwertigen mehrzeiligen Nachweis ersetzt (`perl -0777` Regex ueber den Macro-Block) und alle anderen 15 Pruefpunkte des Gates woertlich wie im Plan laufen lassen — alle gruen. Kein Code geaendert, nur die Pruefmethode fuer diesen einen Punkt.
- **Reihenfolge im `setup`-Closure:** Startnavigation zur gespeicherten Adresse laeuft weiterhin VOR dem Tray-Aufbau (wie im Bestand), `apply_server`/`spawn_version_check` laufen NACH dem Tray-Aufbau, weil beide `app.state::<TrayItems>()` brauchen, das erst beim Tray-Aufbau gemanagt wird (`app.manage(...)` direkt vor `TrayIconBuilder::with_id("main")`).
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking, Pruefmethode] Ein-Zeilen-Grep im Task-1-Gate gegen rustfmt-Umbruch ersetzt**
- **Found during:** Task 1, Verifikationslauf
- **Issue:** `grep -q 'generate_handler!\[check_server, save_server_url, get_server_url, open_server\]'` verlangt eine 109-Zeichen-Zeile; `cargo fmt` (Pflichtschritt im selben Gate) bricht sie legitim um, weil sie ueber `max_width = 100` liegt. Woertlich erfuellbar waren beide Anforderungen nicht gleichzeitig.
- **Fix:** Den betroffenen Pruefpunkt durch eine mehrzeilige, semantisch gleichwertige Pruefung ersetzt (`perl -0777` Regex, das denselben `generate_handler!`-Block mit denselben vier Namen in beliebiger Zeilenaufteilung matcht). Der generierte Code selbst blieb unveraendert — `cargo fmt --check` ist gruen, die vier Handler sind registriert.
- **Files modified:** keine (nur die Pruefmethode fuer diesen einen Punkt, kein Code)
- **Verification:** `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` gruen (18 Tests); alle 15 uebrigen Pruefpunkte des Gates woertlich wie im Plan, gruen; `perl`-Ersatzpruefung fuer Punkt 5 gruen.
- **Committed in:** `29c132e` (Task-1-Commit)
---
**Total deviations:** 1 auto-fixed (1 blocking/Pruefmethode)
**Impact on plan:** Kein Codeverhalten geaendert, nur wie ein einzelner Gate-Punkt gegen den (korrekten) rustfmt-Output geprueft wurde. Keine Abweichung von den `must_haves.truths` oder vom Threat-Register.
## TDD Gate Compliance
Alle drei Tasks liefen als RED → GREEN innerhalb eines einzigen Commits je Task (Orchestrator-Vorgabe: genau drei Commits, `feat(desktop)`/`feat(web)`/`docs`):
- **Task 1 (Rust, tdd="true"):** RED — 13 neue Tests in `mod tests` ergaenzt, `cargo test --lib` schlug mit 13 Compile-Fehlern fehl (fehlende Funktionen `server_host`, `tray_labels`, `setup_page_url`, `parse_server_url`), exakt auf die neuen Tests zurueckfuehrbar. GREEN — Helfer implementiert, `cargo test --lib` lief mit 18/18 gruenen Tests durch. Ein Commit (`29c132e`).
- **Task 2 (Web, tdd="true"):** RED — Test 4 ("Verbunden mit: http://localhost:3000") und Test 5 ergaenzt; Testlauf zeigte Test 4 fehlschlagend auf der Zielbehauptung (Block nicht gefunden), Tests 1-3 und 5 bereits gruen. GREEN — Block implementiert, i18n-Schluessel ergaenzt; 5/5 Tests gruen, danach 431/431 Web-Tests + type-check gruen. Ein Commit (`4c79874`).
- **Task 3 (kein `tdd="true"`):** Standard-Task, kein RED/GREEN-Zyklus vorgesehen.
Kein separater `test(...)`-Commit vor dem jeweiligen `feat(...)`-Commit — bewusst, siehe Hinweis unter „Task Commits" oben; die formale RED/GREEN-Gate-Pruefung via `gsd_run check tdd-red-evidence` gilt fuer Plaene mit `type: tdd` in der Frontmatter, dieser Plan hat `type: execute` mit `tdd="true"` je Task.
## Issues Encountered
None ueber die oben dokumentierte Abweichung hinaus.
## Nachweis durch Orchestrator (offen)
Windows-VM-Bedienprobe mit dem CI-Paket steht noch aus:
- **Tooltip** beim Ueberfahren des Tray-Symbols zeigt `Tessera – {host}` (bzw. `Tessera – nicht verbunden` ohne Adresse).
- **Menuezeile**: erste Zeile `Verbunden mit {host}` (gesperrt, nicht anklickbar).
- **Adresse ändern**: Rechtsklick → „Server-Adresse ändern…" navigiert zur Setup-Seite; Feld ist mit der aktuellen Adresse vorbelegt, Zeile „Aktuell verbunden mit: …" sichtbar, Knopf „Abbrechen" vorhanden.
- **Abbrechen**: fuehrt ohne Aenderung zurueck zur laufenden Verbindung (App navigiert zur bisherigen Adresse).
- **Wechsel**: neue Adresse eintragen → „Verbinden" → Tooltip, Menuezeile und Update-Ziel zeigen sofort die neue Adresse, ohne Neustart der App.
- **Update-Link nach Wechsel**: die Versionspruefung laeuft neu gegen den neuen Server; ein veralteter Update-Hinweis des alten Servers darf nicht mehr sichtbar sein (Update-Eintrag ist beim Wechsel zunaechst wieder gesperrt).
Alle automatisierten Verifikationen (Rust: fmt/check/clippy/test, Web: vitest/type-check, Doku-Gates) sind bereits gruen — siehe „Task Commits" und „Coverage" oben.
## Next Phase Readiness
- Tray-Struktur (`TrayItems`, `apply_server`, `spawn_version_check`, `stored_server_url`) ist bereit fuer den Updater-Task (`tauri-plugin-updater`) — Signaturen siehe Abschnitt oben.
- Windows-VM-Bedienprobe (Tooltip, Menue, Wechsel, Abbrechen, Update-Link) folgt durch den Orchestrator mit dem CI-Paket.
---
*Quick Task: 260917-jn2*
*Completed: 2026-09-17*
@@ -0,0 +1,132 @@
---
phase: quick-260917-jn2
verified: 2026-09-17T15:00:00Z
status: human_needed
score: 6/9 must-haves verified
behavior_unverified: 3
covered_files: [".planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-PLAN.md", ".planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.md", "CHANGELOG.md", "apps/desktop/src-tauri/capabilities/default.json", "apps/desktop/src-tauri/src/lib.rs", "apps/desktop/src/setup.html", "apps/web/src/components/settings/desktop-app-settings.test.tsx", "apps/web/src/components/settings/desktop-app-settings.tsx", "apps/web/src/messages/de.json", "apps/web/src/messages/en.json", "docs/anleitung-anwender.md"]
covered_digest: "v1:sha256:fa46c309fdbf11c37328548b8330bf0b2cd672e44f8393dae8497c28f6d71fc1"
overrides_applied: 0
behavior_unverified_items:
- truth: "Tray-Aufbau (Tooltip `Tessera – {host}`, gesperrte erste Menuezeile `Verbunden mit {host}`) rendert korrekt in einer laufenden App"
test: "Windows-VM: Maus ueber Tray-Symbol halten und Rechtsklick-Menue oeffnen"
expected: "Tooltip zeigt `Tessera – {host}` bzw. `Tessera – nicht verbunden`; erste Menuezeile zeigt `Verbunden mit {host}` (gesperrt, nicht anklickbar)"
why_human: "Statischer Code (TrayIconBuilder, MenuItemBuilder, tray_labels()) ist vollstaendig geprueft und unit-getestet; ob Windows den Tooltip/die Menuezeile tatsaechlich so darstellt, ist natives GUI-Rendering, das nur eine laufende App zeigen kann"
- truth: "Nach `save_server_url` mit neuer Adresse zeigen Tooltip, Menuezeile und Update-Ziel OHNE Neustart die neue Adresse; die Versionspruefung laeuft neu gegen den neuen Server"
test: "Windows-VM: bestehende Verbindung per `Server-Adresse ändern…` auf eine andere Test-Adresse umstellen, danach Tray-Tooltip/-Menue und den `update`-Klick pruefen"
expected: "Tooltip und `Verbunden mit …`-Zeile zeigen sofort die neue Adresse ohne App-Neustart; `Update herunterladen` ist zunaechst wieder gesperrt (kein veralteter Hinweis vom alten Server); ein Klick auf `update` fuehrt zur NEUEN Adresse"
why_human: "Dies ist eine echte Laufzeit-Zustandsaenderung (State-Transition: alte Adresse → neue Adresse, ohne Prozess-Neustart); `apply_server`/`spawn_version_check` sind korrekt verdrahtet (Code gelesen, `app.state::<TrayItems>()` einzige Setzstelle), aber ob der native Tray tatsaechlich ohne Neustart auffrischt, laesst sich nur an der laufenden App beobachten"
- truth: "setup.html zeigt im Aenderungsmodus das vorbelegte Feld, `Aktuell verbunden mit: …` und den Knopf `Abbrechen`, der per `open_server` zur bisherigen Adresse zurueckfuehrt; Klick auf `Server-Adresse ändern…` im Tray navigiert dorthin"
test: "Windows-VM: Rechtsklick auf Tray-Symbol → `Server-Adresse ändern…` anklicken; danach `Abbrechen` anklicken"
expected: "Setup-Seite erscheint mit vorbelegter aktueller Adresse, Zeile `Aktuell verbunden mit: …` und sichtbarem `Abbrechen`-Knopf; `Abbrechen` fuehrt ohne Aenderung zur laufenden Verbindung zurueck"
why_human: "Skript-Logik (`init()`, `enterChangeMode()`, `cancelBtn`-Handler) ist vollstaendig gelesen und deckt sich mit der Spezifikation, ist aber nicht durch einen automatisierten Test abgedeckt (kein vitest/jsdom fuer setup.html) und haengt von echter Tauri-Webview-Navigation ab (`window.navigate`, `tauri://localhost` bzw. `http://tauri.localhost`), die nur auf der Ziel-Plattform beobachtbar ist"
coincidental_reliance_items: []
human_verification:
- test: "Tooltip beim Ueberfahren des Tray-Symbols"
expected: "`Tessera – {host}` bzw. `Tessera – nicht verbunden` ohne Adresse"
why_human: "Natives GUI-Rendering (Windows-Tooltip), nicht automatisiert pruefbar"
- test: "Erste (gesperrte) Menuezeile im Rechtsklick-Menue"
expected: "`Verbunden mit {host}`, nicht anklickbar"
why_human: "Natives Menue-Rendering"
- test: "Klick auf `Server-Adresse ändern…`"
expected: "Navigiert zur Setup-Seite; Feld vorbelegt mit aktueller Adresse, Zeile `Aktuell verbunden mit: …` sichtbar, Knopf `Abbrechen` vorhanden"
why_human: "Echte Tauri-Webview-Navigation, nur auf Zielplattform beobachtbar"
- test: "Klick auf `Abbrechen`"
expected: "Fuehrt ohne Aenderung zur laufenden Verbindung zurueck (App navigiert zur bisherigen Adresse via `open_server`)"
why_human: "Laufzeitverhalten der Navigation"
- test: "Serverwechsel (neue Adresse eintragen → `Verbinden`)"
expected: "Tooltip, Menuezeile und Update-Ziel zeigen sofort die neue Adresse, ohne Neustart der App"
why_human: "State-Transition zur Laufzeit, nur an laufender App beobachtbar"
- test: "Update-Link nach Wechsel"
expected: "Versionspruefung laeuft neu gegen den neuen Server; ein veralteter Update-Hinweis des alten Servers ist nicht mehr sichtbar (Eintrag zunaechst wieder gesperrt)"
why_human: "Haengt von echtem Netzwerk-Roundtrip gegen den (neuen) Server ab"
---
# Quick Task 260917-jn2: Desktop-Client — aktuelle Server-Adresse sehen und ändern Verification Report
**Ziel:** Desktop-Client zeigt an drei Stellen (Tray-Tooltip, gesperrte Menuezeile, Web-Einstellungsseite), mit welchem Tessera-Server er verbunden ist, und die Adresse laesst sich ueber einen neuen Tray-Eintrag nachtraeglich aendern — ohne Neustart, mit erneuter Versionspruefung. Reine, getestete Helferfunktionen; Doku + CHANGELOG.
**Verified:** 2026-09-17T15:00:00Z
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Tray gebaut mit `with_id("main")`, erste Zeile gesperrt `connected` (`Verbunden mit {host}`), danach Trenner/Öffnen/„Server-Adresse ändern…"/Update/Trenner/Autostart/Trenner/Beenden; Tooltip `Tessera – {host}` | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code exakt wie Plan (lib.rs Z. 291-366); `tray_labels()` unit-getestet (`tray_labels_mit_host`, `tray_labels_ohne_adresse`, gruen); reales Rendering nicht automatisiert pruefbar |
| 2 | Vier reine Helfer (`server_host`, `tray_labels`, `setup_page_url`, `parse_server_url`) + `stored_server_url` mit Tests; `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` gruen | ✓ VERIFIED | Selbst ausgefuehrt: fmt/check/clippy 0 Fehler, 18/18 Tests gruen (13 neu + 5 Bestand) |
| 3 | Commands `get_server_url`/`open_server` in `generate_handler!`; capabilities/default.json unveraendert, kein `remote`-Block | ✓ VERIFIED | lib.rs Z. 271-289, 301-305 gelesen; `grep -c '"remote"'` = 0; `git diff b023d6f..4d48543 -- capabilities/default.json` leer |
| 4 | `change_server`-Klick navigiert per `window.navigate(setup_page_url(cfg!(windows)))`, kein `eval` | ✓ VERIFIED | lib.rs Z. 377-384; `grep -n "\.eval("` liefert 0 Treffer in lib.rs und setup.html |
| 5 | `apply_server` einzige Setzstelle fuer Tooltip+Menuezeile; `spawn_version_check` setzt Update-Eintrag zurueck und prueft neu; `update`-Klick liest Adresse beim Klick aus dem Store (kein Start-Klon) | ✓ VERIFIED (Code) / ⚠️ Laufzeit-Wechsel siehe Truth „ohne Neustart" | lib.rs Z. 156-219 (`apply_server`, `spawn_version_check`), Z. 385-393 (`update`-Handler nutzt `stored_server_url(app)`); Aufrufkette in `save_server_url` (Z. 249-265) bestaetigt |
| 6 | setup.html: `invoke('get_server_url')` beim Laden, Vorbelegung, `#current-server`, `#cancel-btn` → `open_server`; SVG unangetastet | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Skript vollstaendig gelesen (Z. 337-420), deckt sich mit Spezifikation; kein automatisierter Test fuer setup.html vorhanden, echte Tauri-Navigation nur auf Zielplattform beobachtbar |
| 7 | Web: `DesktopAppSettings` zeigt `data-testid="desktop-connected"` nur bei `useIsDesktopClient()===true`, i18n-Schluessel `connectedTo`/`changeHint` in de/en | ✓ VERIFIED | `pnpm --filter @tessera/web exec vitest run src/components/settings/desktop-app-settings.test.tsx` 5/5 gruen (inkl. neuer Test 4/5); `pnpm --filter @tessera/web exec vitest run` 431/431 gruen; `type-check` 0 Fehler; i18n-Keys in de.json/en.json vorhanden |
| 8 | Doku: neuer Unterabschnitt „Server-Adresse ändern", erweiterte Tray-Liste, Erststart-Verweis; CHANGELOG zwei `Desktop-App:`-Zeilen unter Unveröffentlicht/Neu | ✓ VERIFIED | docs/anleitung-anwender.md Z. 175-235 gelesen; CHANGELOG.md Z. 1-13 gelesen (nur die zwei Zeilen neu, fremde Zeilen unangetastet); `changelog.test.ts` 10/10 gruen |
| 9 | Drei Commits (`feat(desktop)`, `feat(web)`, `docs`), kein Push, keine `.planning/`-Commits, kein `tauri build`, kein Docker, `cargo` nur mit `CARGO_BUILD_JOBS=4` | ✓ VERIFIED | `git show --stat` fuer 29c132e/4c79874/4d48543 bestaetigt Typ + Dateien; Gesamtdiff `b023d6f..4d48543` umfasst exakt die 8 geplanten Dateien, Cargo.lock unveraendert; eigene Laeufe nutzten `CARGO_BUILD_JOBS=4` |
**Score:** 6/9 truths verified (3 present, behavior-unverified)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/desktop/src-tauri/src/lib.rs` | Vier reine Helfer, `TrayItems`, `apply_server`, `spawn_version_check`, Commands, Tray mit `connected`/`change_server`, erweiterte `mod tests` | ✓ VERIFIED | Alle Symbole vorhanden, gewired und getestet (18/18 Tests, fmt/check/clippy gruen) |
| `apps/desktop/src/setup.html` | Aenderungsmodus (`#current-server`, `#cancel-btn`, `#subtitle`), `init()`/`enterChangeMode()` | ✓ VERIFIED | Markup + Skript vollstaendig vorhanden; SVG `.brand-mark` unangetastet |
| `apps/web/src/components/settings/desktop-app-settings.tsx` + `.test.tsx` | Block `desktop-connected`, Tests mit/ohne Cookie | ✓ VERIFIED | Komponente + 5 gruene Tests (2 neu) |
| `apps/web/src/messages/de.json`, `en.json` | `settings.desktop.connectedTo`, `settings.desktop.changeHint` | ✓ VERIFIED | Beide Schluessel in beiden Sprachen vorhanden |
| `docs/anleitung-anwender.md`, `CHANGELOG.md` | Neuer Unterabschnitt, Tray-Liste, zwei CHANGELOG-Zeilen | ✓ VERIFIED | Inhaltlich gepruefte Textstellen vorhanden |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| Tray `change_server` | `setup_page_url(cfg!(windows))` | `window.navigate` | ✓ WIRED | lib.rs Z. 377-384; kein `eval` |
| setup.html `init()` | `get_server_url` | `invoke('get_server_url')` | ✓ WIRED | setup.html Z. 411-420 |
| setup.html `#cancel-btn` | `open_server` | `invoke('open_server')` | ✓ WIRED | setup.html Z. 401-409 |
| `save_server_url` | `apply_server`/`spawn_version_check` | direkter Aufruf nach Store-Speicherung | ✓ WIRED | lib.rs Z. 249-265 |
| `tray`-Klick `update` | `stored_server_url(app)` | Store-Lesung beim Klick (kein Start-Klon) | ✓ WIRED | lib.rs Z. 385-393 |
| Server-Seite (Remote-Ursprung) | App-Commands | ACL ohne `remote`-Block | ✓ WIRED (verweigert) | capabilities/default.json unveraendert, kein `remote`-Schluessel |
| `DesktopAppSettings` | `useIsDesktopClient()` | Cookie `tessera_desktop` | ✓ WIRED | desktop-app-settings.tsx Z. 79; Test mit/ohne Cookie gruen |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Rust-Helfer + Tests | `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` (CARGO_BUILD_JOBS=4) | 18/18 Tests gruen, 0 Warnungen | ✓ PASS |
| Web-Komponententest (Zieltest) | `pnpm --filter @tessera/web exec vitest run src/components/settings/desktop-app-settings.test.tsx` | 5/5 gruen | ✓ PASS |
| Web-Gesamtsuite (einmalig) | `pnpm --filter @tessera/web exec vitest run` | 431/431 gruen (64 Dateien) | ✓ PASS |
| Web-Typpruefung | `pnpm --filter @tessera/web type-check` | 0 Fehler | ✓ PASS |
| CHANGELOG-Struktur | `pnpm --filter @tessera/web exec vitest run src/lib/changelog.test.ts` | 10/10 gruen | ✓ PASS |
| Tray-/Setup-Seiten-Laufzeitverhalten | — | — | ? SKIP (kein Windows-Runtime verfuegbar; siehe Human Verification) |
### Anti-Patterns Found
Keine `TBD`/`FIXME`/`XXX`/`TODO`/`HACK`/`PLACEHOLDER`/„not yet implemented" in den acht geaenderten Dateien gefunden (grep-Scan durchgefuehrt, keine Treffer).
### Requirements Coverage
Kein Eintrag `QUICK-260917-JN2` in `.planning/REQUIREMENTS.md` — bei Quick-Tasks ueblich (kein formaler Requirements-Katalog-Zwang). `requirements-completed: [QUICK-260917-JN2]` im SUMMARY-Frontmatter dokumentiert die Selbstzuordnung; die inhaltliche Deckung ist ueber die Truths/Artifacts oben abgebildet.
### Human Verification Required
Die im Plan selbst als „Nachweis durch Orchestrator" ausgewiesene Windows-VM-Bedienprobe steht noch aus (dieser Ausfuehrungslauf hatte explizit keinen Zugriff auf eine laufende Tauri-App/Windows-VM). Sechs konkrete Pruefpunkte, siehe YAML-Frontmatter `human_verification` oben:
1. Tooltip beim Ueberfahren des Tray-Symbols (`Tessera – {host}`)
2. Erste (gesperrte) Menuezeile (`Verbunden mit {host}`)
3. Klick auf „Server-Adresse ändern…" → vorbelegte Setup-Seite mit „Aktuell verbunden mit: …" und „Abbrechen"
4. „Abbrechen" fuehrt ohne Aenderung zur laufenden Verbindung zurueck
5. Serverwechsel aktualisiert Tooltip/Menuezeile/Update-Ziel ohne Neustart
6. Update-Link nach Wechsel prueft gegen den neuen Server, alter Hinweis verschwindet
Alle sechs Punkte sind Code-seitig korrekt verdrahtet (siehe Truths/Key-Links oben) — es fehlt ausschliesslich der Laufzeit-Nachweis auf der Zielplattform, der laut Plan und Orchestrator-Auftrag bewusst diesem Verifikationslauf nicht obliegt.
### Gaps Summary
Keine Gaps. Alle statisch/automatisiert pruefbaren must_haves sind erfuellt: 18 gruene Rust-Tests (fmt/check/clippy sauber), 431 gruene Web-Tests + type-check, alle Code-Wiring-Punkte (a)-(f) aus dem Verifikationsauftrag bestaetigt (setup_page_url plattformabhaengig korrekt, open_server/save_server_url nur http/https, capabilities/default.json ohne remote-Block, update-Klick liest beim Klick aus dem Store, apply_server einzige Setz-Stelle fuer Tooltip+Menuezeile, kein eval), Doku/CHANGELOG korrekt ergaenzt, genau drei saubere Commits ohne Fremd-Dateien. Der einzige offene Punkt ist die vom Plan selbst bewusst ausgelagerte Windows-VM-Bedienprobe (Tray/Setup-Seiten-Laufzeitverhalten) — kein Blocker, sondern ein dokumentierter Beobachtungspunkt fuer den Orchestrator.
---
_Verified: 2026-09-17T15:00:00Z_
_Verifier: Claude (gsd-verifier)_