diff --git a/.planning/STATE.md b/.planning/STATE.md index 81b0faf..637cc65 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -434,6 +434,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests. | 260917-e15 | **Desktop-Client-Icon: T statt "1".** Die fuenf Icon-Dateien in `apps/desktop/src-tauri/icons/` waren mit ImageMagicks internem MSVG-Renderer erzeugt, der `transform="rotate(12 51 21)"` nicht rendert -> gedrehte gelbe Kachel fehlte, App-Symbol sah aus wie eine "1". Satz mit `tauri icon` (resvg) aus `apps/web/src/app/icon.svg` neu erzeugt (nur die fuenf Dateien aus `bundle.icon`, kein icns/android/ios), Pixel-Gate an der Kachelmitte `FFED00FF`, icon.ico 16/24/32/48/64/256. Changelog. Nebenbefund: Erststart-Seite (Inline-SVG im WebView) war nie betroffen. | 2026-09-17 | 16564f4,6bb92dc | [260917-e15-desktop-client-icon-fehlende-gedrehte-ge](./quick/260917-e15-desktop-client-icon-fehlende-gedrehte-ge/) | | 260917-eta | **Desktop-Client: Tray „Beenden" beendete die App nicht; „Öffnen"/Linksklick holten minimiertes Fenster nicht zurueck.** Auf der Windows-Test-VM reproduziert (tasklist: `tessera-desktop.exe` lief nach „Beenden" weiter; Autostart-Haken im selben Menue funktionierte -> Klick kam an). Ursache: `app.run`-Handler rief bei jedem `RunEvent::ExitRequested` `api.prevent_exit()` — auch fuer `app.exit(0)` aus dem Tray. Fix: Muster `ExitRequested { code: None, api, .. }` (Tauri 2.11.3: `code` None = Nutzer-Interaktion, Some = programmatisch). Dazu `w.unminimize()` vor `show()` in „open" und im Linksklick-Handler (nach Win+D bewirkte „Öffnen" nichts). cargo check/clippy 0 Warnungen, rustfmt (9cb9d2e). Changelog 2 Stichpunkte. | 2026-09-17 | 68a69c6,9ba7456,9cb9d2e | [260917-eta-desktop-client-tray-eintrag-beenden-been](./quick/260917-eta-desktop-client-tray-eintrag-beenden-been/) | | 260917-gsh | **Akzentfarbe als Hex-Code eingebbar; Bildmarke uebernimmt die Akzentfarbe.** `normalizeHexColor()` in `lib/color.ts` (optionales `#`, 3-stellige Kurzform, Kleinschreibung; 9 Tests), Textfeld neben dem Farbwaehler mit Zwei-Wege-Sync, `aria-invalid` + Fehlertext + gesperrtes Speichern bei ungueltigem Wert (7 Komponententests), i18n `settings.account.accentColorHex/-Invalid`. Gedrehte Kachel in `LogoMark` per Inline-Style `fill: var(--primary, #ffed00)` — folgt `applyAccentColor`, Anmeldeseite bleibt gelb. Tests Web 365 -> 381 / 57 Dateien, tsc 0. | 2026-09-17 | 795c6a4,1601d97,db478e0 | [260917-gsh-akzentfarbe-in-einstellungen-konto-zusae](./quick/260917-gsh-akzentfarbe-in-einstellungen-konto-zusae/) | +| 260917-gyd | **Web-Robustheit: Ruecksprung nach Anmeldung, Sitzungswaechter, Widgets-Seite uebersetzt.** Middleware leitet auf `/login?next=` (Helfer `lib/safe-next.ts`: nur relative Pfade, kein `//`, kein `\\`, kein `/login`; Tests), Anmeldeseite springt nach Erfolg dorthin. Neue Server Action `fetchSessionState()` (authenticated/unauthenticated/unavailable): bei 401/403 oder 200 ohne Benutzer wird das Sitzungscookie geloescht und der Header leitet auf `/login?next=…` — 5xx/Netzwerkfehler bleiben still (kein Redirect bei API-Ausfall). Befund vom Testserver-DB-Reset: `/auth/me` liefert bei geloeschtem Benutzer 200 mit leerem Body. `settings/dashboard`: `common.loading` + `settings.widgets.empty` statt englischer Hartkodierung. Tests Web gruen, tsc 0. | 2026-09-17 | 4b279ea,474d170,2868ffe | [260917-gyd-web-nach-anmeldung-zurueck-zur-ursprueng](./quick/260917-gyd-web-nach-anmeldung-zurueck-zur-ursprueng/) | ## Deferred Items diff --git a/.planning/quick/260917-gyd-web-nach-anmeldung-zurueck-zur-ursprueng/260917-gyd-SUMMARY.md b/.planning/quick/260917-gyd-web-nach-anmeldung-zurueck-zur-ursprueng/260917-gyd-SUMMARY.md new file mode 100644 index 0000000..3dde4bc --- /dev/null +++ b/.planning/quick/260917-gyd-web-nach-anmeldung-zurueck-zur-ursprueng/260917-gyd-SUMMARY.md @@ -0,0 +1,185 @@ +--- +phase: quick-260917-gyd +plan: 01 +subsystem: auth +tags: [next.js, middleware, jwt, i18n, vitest] + +requires: [] +provides: + - "safe-next.ts: buildNextParam()/sanitizeNextPath() als reine, getestete Funktionen fuer den Ruecksprung-Parameter (Open-Redirect-Schutz)" + - "fetchSessionState() in auth-actions.ts: klassifiziert die Sitzung (authenticated/unauthenticated/unavailable) und loescht das Cookie nur bei nachweislich toter Sitzung" + - "Header-Waechter erkennt tote Sitzung und leitet auf /login?next=… um, bleibt bei API-Ausfall still" + - "Einstellungen → Widgets vollstaendig uebersetzt (i18n statt hartkodiertem Englisch)" +affects: [web-auth, web-settings] + +actuals: + tokens: 6602 + tasks: 3 + commits: 3 +plan_head_before: 4778824c73caafbf859c71c5c5a1c40401732296 + +tech-stack: + added: [] + patterns: + - "safe-next.ts: reine Funktionen ohne Node-/DOM-APIs, damit ein Modul sowohl von der Edge-Middleware als auch vom Client importiert werden kann" + - "fetchSessionState() klassifiziert API-Antworten in drei Zustaende (authenticated/unauthenticated/unavailable) statt eines binaeren Erfolg/Misserfolg, um Abmelde-Schleifen bei API-Ausfaellen zu vermeiden" + +key-files: + created: + - apps/web/src/lib/safe-next.ts + - apps/web/src/lib/safe-next.test.ts + - apps/web/src/lib/auth-actions.test.ts + - apps/web/src/components/layout/header.test.tsx + modified: + - apps/web/src/middleware.ts + - apps/web/src/app/(auth)/login/page.tsx + - apps/web/src/lib/auth-actions.ts + - apps/web/src/components/layout/header.tsx + - apps/web/src/app/(portal)/settings/dashboard/page.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + - CHANGELOG.md + +key-decisions: + - "fetchCurrentUser() bleibt unveraendert (Signatur und Verhalten) — change-password/page.tsx, account-settings-form.tsx und deren Test-Mock haengen davon ab; fetchSessionState() ist additiv daneben entstanden, mit akzeptierter kleiner Code-Duplikation der fetch-Zeilen" + - "Cookie-Loeschung bei toter Sitzung passiert ausschliesslich in der Server Action fetchSessionState() (cookieStore.delete() ist nur dort erlaubt), der Header liest nur den klassifizierten Zustand und navigiert" + - "login/page.tsx liest next erst beim Absenden aus window.location.search statt per useSearchParams(), weil der Hook in Next 15 eine Suspense-Grenze braucht und sonst next build fuer die statisch vorgerenderte Login-Seite abbricht" + +patterns-established: + - "Sitzungswaechter-Muster: dreiwertige Klassifikation (authenticated/unauthenticated/unavailable) statt AuthUser | null, damit ein API-Ausfall nicht als tote Sitzung fehlinterpretiert wird" + +requirements-completed: [QUICK-260917-GYD] + +coverage: + - id: D1 + description: "Middleware haengt next-Parameter (Pfad + Query ohne _rsc) an beide Login-Umleitungen an; nicht fuer / und /login…" + requirement: QUICK-260917-GYD + verification: + - kind: unit + ref: "apps/web/src/lib/safe-next.test.ts (9 Faelle)" + status: pass + human_judgment: false + - id: D2 + description: "Anmeldeseite springt nach Erfolg auf den bereinigten next-Wert; unsichere Werte fallen auf / zurueck" + requirement: QUICK-260917-GYD + verification: + - kind: unit + ref: "apps/web/src/lib/safe-next.test.ts (sanitizeNextPath-Faelle)" + status: pass + human_judgment: false + - id: D3 + description: "fetchSessionState() loescht das Cookie nur bei 401/403/leerer 200-Antwort, meldet 5xx/Netzwerkfehler als unavailable ohne Cookie-Loeschung" + requirement: QUICK-260917-GYD + verification: + - kind: unit + ref: "apps/web/src/lib/auth-actions.test.ts (8 Faelle)" + status: pass + human_judgment: false + - id: D4 + description: "Header leitet bei toter Sitzung per Vollnavigation auf /login?next=… um und bleibt bei API-Ausfall still (Avatar zeigt weiterhin ?)" + requirement: QUICK-260917-GYD + verification: + - kind: unit + ref: "apps/web/src/components/layout/header.test.tsx (4 Faelle)" + status: pass + human_judgment: false + - id: D5 + description: "Einstellungen → Widgets zeigt Lade- und Leerhinweis ueber i18n (de/en) statt hartkodiertem Englisch" + requirement: QUICK-260917-GYD + verification: + - kind: other + ref: "grep-Gates auf settings/dashboard/page.tsx + de.json/en.json settings.widgets.empty (Task-3-verify)" + status: pass + human_judgment: false + - id: D6 + description: "Browser-Nachweis: ohne Sitzung /settings/general/desktop aufrufen → Login-URL traegt next, nach Anmeldung landet man auf der Desktop-Seite; Sitzungscookie mit nicht mehr existierender Benutzer-ID → Umleitung zur Anmeldeseite statt ?-Avatar" + verification: [] + human_judgment: true + rationale: "Erfordert einen echten Browser-Lauf gegen die laufende API/DB (Orchestrator-Aufgabe, nicht Teil dieses Ausfuehrungsplans laut -Sektion des Plans)" + +duration: 7min +completed: 2026-09-17 +status: complete +--- + +# Quick Task 260917-gyd: Ruecksprung nach Anmeldung, Sitzungswaechter bei toter API-Sitzung, Widgets-Seite uebersetzt Summary + +**`next`-Parameter fuer den Ruecksprung nach Anmeldung (safe-next.ts, Open-Redirect-getestet), `fetchSessionState()` unterscheidet tote Sitzung von API-Ausfall und leitet den Header ab, Einstellungen → Widgets durchgaengig deutsch.** + +## Performance + +- **Duration:** 7 min +- **Started:** 2026-09-17T10:24:00Z +- **Completed:** 2026-09-17T10:30:49Z +- **Tasks:** 3 +- **Files modified:** 12 + +## Accomplishments +- Middleware und Anmeldeseite tragen jetzt einen getesteten `next`-Ruecksprung durch — der Desktop-App-Link „Update herunterladen" landet nach der Anmeldung wieder auf der urspruenglich angeforderten Seite +- `fetchSessionState()` erkennt eine tote Sitzung (401/403/leere 200-Antwort) zuverlaessig und trennt sie von einem stillen API-Ausfall (5xx/Netzwerkfehler) — der Header meldet sich bei toter Sitzung ab statt ein halb angemeldetes „?"-Avatar-Portal zu zeigen +- Einstellungen → Widgets zeigt Lade- und Leerhinweis vollstaendig ueber i18n + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Ruecksprung — `next`-Parameter in Middleware und Anmeldeseite** - `4b279ea` (feat) +2. **Task 2: Sitzungswaechter — `fetchSessionState()` unterscheidet tote Sitzung von API-Ausfall** - `474d170` (feat) +3. **Task 3: Widgets-Seite uebersetzen, CHANGELOG ergaenzen, Gesamtlauf** - `2868ffe` (feat) + +_Alle drei Tasks folgten TDD (RED zuerst bei Task 1 und 2, Task 3 ohne `tdd="true"`)._ + +## Files Created/Modified +- `apps/web/src/lib/safe-next.ts` - `buildNextParam()`/`sanitizeNextPath()`, reine Funktionen, Open-Redirect-Schutz +- `apps/web/src/lib/safe-next.test.ts` - 9 Testfaelle beider Funktionen +- `apps/web/src/middleware.ts` - beide Login-Umleitungen ueber gemeinsamen `redirectToLogin()`-Helfer mit `next` +- `apps/web/src/app/(auth)/login/page.tsx` - springt nach Erfolg auf `sanitizeNextPath(next)` +- `apps/web/src/lib/auth-actions.ts` - `fetchSessionState()` (neu, additiv), `fetchCurrentUser()` unveraendert +- `apps/web/src/lib/auth-actions.test.ts` - 8 Klassifikations-Testfaelle +- `apps/web/src/components/layout/header.tsx` - Waechter im useEffect ersetzt `fetchCurrentUser` durch `fetchSessionState` +- `apps/web/src/components/layout/header.test.tsx` - 4 Testfaelle (authenticated/unauthenticated auf Unterseite/auf `/`/unavailable) +- `apps/web/src/app/(portal)/settings/dashboard/page.tsx` - `tCommon('loading')` und `t('widgets.empty')` statt Festtext +- `apps/web/src/messages/de.json`, `en.json` - `settings.widgets.empty` (additiv) +- `CHANGELOG.md` - drei Stichpunkte unter „Behoben" + +## Decisions Made +- `fetchCurrentUser()` unangetastet gelassen (Signatur- und Verhaltensgarantie fuer change-password/page.tsx und account-settings-form.tsx), `fetchSessionState()` daneben additiv mit kleiner Code-Duplikation der fetch-Aufrufzeilen +- Cookie-Loeschung ausschliesslich in der Server Action, nicht im Client-Header +- `next`-Wert wird in login/page.tsx erst beim Absenden aus `window.location.search` gelesen (kein `useSearchParams()`, wegen fehlender Suspense-Grenze bei der statisch vorgerenderten Anmeldeseite) + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered +- `bug-report-button.test.tsx` (nicht Teil dieses Plans) schlug einmal im Gesamtlauf flakey fehl (Checkbox-Timing) und lief beim naechsten Lauf sowie isoliert gruen — ausserhalb des Scopes dieses Plans, nicht auto-gefixt, kein Deviation-Eintrag noetig. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness +- Browser-Nachweis (ohne Sitzung `/settings/general/desktop` aufrufen, tote Sitzung simulieren, Widgets-Seite pruefen) steht laut Plan beim Orchestrator aus, nicht Teil dieser Ausfuehrung. +- Keine Blocker fuer Folgearbeiten; `fetchCurrentUser()` bleibt fuer bestehende Aufrufer nutzbar. + +--- +*Phase: quick-260917-gyd* +*Completed: 2026-09-17* + +## Self-Check: PASSED + +- FOUND: apps/web/src/lib/safe-next.ts +- FOUND: apps/web/src/lib/safe-next.test.ts +- FOUND: apps/web/src/middleware.ts +- FOUND: apps/web/src/app/(auth)/login/page.tsx +- FOUND: apps/web/src/lib/auth-actions.ts +- FOUND: apps/web/src/lib/auth-actions.test.ts +- FOUND: apps/web/src/components/layout/header.tsx +- FOUND: apps/web/src/components/layout/header.test.tsx +- FOUND: apps/web/src/app/(portal)/settings/dashboard/page.tsx +- FOUND: apps/web/src/messages/de.json +- FOUND: apps/web/src/messages/en.json +- FOUND: CHANGELOG.md +- FOUND commit: 4b279ea +- FOUND commit: 474d170 +- FOUND commit: 2868ffe diff --git a/.planning/quick/260917-h2s-desktop-client-web-erkennt-den-client-do/260917-h2s-PLAN.md b/.planning/quick/260917-h2s-desktop-client-web-erkennt-den-client-do/260917-h2s-PLAN.md new file mode 100644 index 0000000..8b23b54 --- /dev/null +++ b/.planning/quick/260917-h2s-desktop-client-web-erkennt-den-client-do/260917-h2s-PLAN.md @@ -0,0 +1,240 @@ +--- +phase: quick-260917-h2s +plan: 01 +type: execute +wave: 1 +depends_on: [] +autonomous: true +requirements: [QUICK-260917-H2S] + +files_modified: + - apps/desktop/src-tauri/src/lib.rs + - apps/desktop/src-tauri/tauri.conf.json + - apps/desktop/src-tauri/icons/nsis-header.bmp + - apps/desktop/src-tauri/icons/nsis-sidebar.bmp + - apps/web/src/middleware.ts + - apps/web/src/middleware.test.ts + - apps/web/src/lib/desktop-client.ts + - apps/web/src/lib/desktop-client.test.ts + - apps/web/src/components/desktop/desktop-download-links.tsx + - apps/web/src/components/desktop/desktop-download-links.test.tsx + - apps/web/src/components/desktop/desktop-context-menu-guard.tsx + - apps/web/src/components/desktop/desktop-context-menu-guard.test.tsx + - apps/web/src/app/layout.tsx + - CHANGELOG.md + - docs/anleitung-anwender.md + - docs/anleitung-entwicklung.md + +estimate: + tokens: 48000 + raw_tokens: 48000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "lib.rs: eine reine Funktion `with_desktop_marker(url: &tauri::Url) -> tauri::Url` haengt `desktop=1` als Query-Paar an einen Klon an (via `query_pairs_mut().append_pair`); BEIDE Navigationen zur Server-Adresse (`save_server_url` und die Startnavigation im `setup`) uebergeben `with_desktop_marker(&parsed)` an `window.navigate`. Der Wert `server_url` im Store bleibt ohne Parameter (weiterhin `parsed.as_str()`)." + - "lib.rs: eine reine Funktion `update_labels(version_changed: bool, version: &str, commit: &str) -> (String, String)` liefert (Menuetext, Benachrichtigungstext): bei `version_changed` `Version {v} herunterladen` / `Neue Version {v} verfügbar – Download über das Symbol im Infobereich.`; sonst `Neuen Beta-Stand herunterladen` / `Neuer Beta-Stand {commit} verfügbar – Download über das Symbol im Infobereich.` (echte Umlaute wie im Bestand). Die Versionspruefung im `setup` nutzt genau diese Funktion; `is_newer` bleibt wie bisher (Versionsvergleich ODER Beta-Commit-Vergleich)." + - "lib.rs traegt ein `#[cfg(test)] mod tests` mit Tests fuer `with_desktop_marker` (Adresse ohne Pfad, Adresse mit vorhandenem Query, Original unveraendert) und `update_labels` (beide Zweige); `cargo fmt --check`, `cargo check`, `cargo clippy` und `cargo test --lib` in apps/desktop/src-tauri sind gruen." + - "middleware.ts: Helfer `withDesktopCookie(req: NextRequest, res: NextResponse): NextResponse` setzt bei `req.nextUrl.searchParams.get('desktop') === '1'` das Cookie `tessera_desktop=1` (path `/`, maxAge 31536000, sameSite `lax`, httpOnly `false`, secure nur wenn `req.nextUrl.protocol === 'https:'`) auf die uebergebene Antwort und gibt sie zurueck; ohne Parameter Rueckgabe unveraendert. JEDE `return`-Anweisung der `middleware`-Funktion (Fruehausstieg oeffentliche Routen, Statics, Redirect ohne Session, Redirect Passwortwechsel, `NextResponse.next()` nach gueltigem JWT, Redirect bei ungueltigem JWT — und alle, die Plan 260917-gyd zwischenzeitlich ergaenzt hat) laeuft durch `withDesktopCookie(req, …)`." + - "middleware.test.ts (`// @vitest-environment node`) belegt: `/login?desktop=1` → Set-Cookie mit `tessera_desktop=1`, `Path=/`, `Max-Age=31536000`, `SameSite=lax`, ohne `Secure`, ohne `HttpOnly`; `/login` ohne Parameter → kein Set-Cookie; `/dashboard?desktop=1` ohne Session → Status 307, `location` enthaelt `/login`, Set-Cookie vorhanden; `https://…/login?desktop=1` → `Secure` gesetzt; `/dashboard?desktop=1` mit gueltigem HS256-JWT (jose `SignJWT`, `vi.stubEnv('JWT_SECRET', …)`) → Set-Cookie vorhanden und Header `x-middleware-next` = `1`." + - "apps/web/src/lib/desktop-client.ts exportiert `DESKTOP_COOKIE_NAME = 'tessera_desktop'`, `isDesktopClient(): boolean` (liest `document.cookie`, `typeof document === 'undefined'` → false, true genau wenn ein Eintrag `tessera_desktop=1` existiert) und den Hook `useIsDesktopClient(): boolean` (useState(false) + useEffect, damit SSR und erster Client-Render uebereinstimmen). desktop-client.test.ts prueft: ohne Cookie false, mit `tessera_desktop=1` true, mit `tessera_desktop=0` false, `document` undefiniert (vi.stubGlobal) → false, Hook via `renderHook` liefert nach dem Effekt true." + - "DesktopDownloadLinks rendert im Desktop-Client nichts: `useIsDesktopClient()` → `null`, und der Ladeeffekt bricht bei `isDesktopClient()` vor dem Aufruf von `loadDesktopLatest` ab. Neuer Test 4 (Cookie gesetzt, `loadDesktopLatest` haette beide Pakete geliefert): `loadDesktopLatest` wird NICHT aufgerufen, `container.firstChild` ist null. Cookie wird in `afterEach` geloescht; Tests 1-3 bleiben unveraendert gruen." + - "DesktopContextMenuGuard (`'use client'`, rendert `null`): bei `useIsDesktopClient()` true registriert ein Effekt einen `contextmenu`-Listener auf `document`, der `preventDefault()` ruft — AUSSER das Ziel liegt in `input`, `textarea`, `select` oder einem contenteditable-Bereich (`closest('input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"], [contenteditable=\"plaintext-only\"]')` oder `isContentEditable === true`). Aufraeumfunktion entfernt den Listener. Im RootLayout (`apps/web/src/app/layout.tsx`) eingebunden. Test (jsdom, MouseEvent `contextmenu` bubbles+cancelable): mit Cookie auf `div` → `dispatchEvent` false, auf `input`/`textarea`/Kind eines `[contenteditable=\"true\"]` → true; ohne Cookie auf `div` → true; nach `unmount()` auf `div` → true." + - "tauri.conf.json enthaelt `bundle.windows.nsis` mit exakt: `languages: [\"German\"]`, `displayLanguageSelector: false`, `installerIcon: \"icons/icon.ico\"`, `headerImage: \"icons/nsis-header.bmp\"`, `sidebarImage: \"icons/nsis-sidebar.bmp\"`, `installMode: \"currentUser\"`; alle Schluessel sind in `NsisConfig.properties` des lokalen CLI-Schemas (apps/desktop/node_modules/@tauri-apps/cli/config.schema.json) enthalten; die beiden BMP-Dateien liegen in apps/desktop/src-tauri/icons/ (BMP3, 150x57 bzw. 164x314); `cargo check` in src-tauri bleibt gruen (tauri-codegen parst die Konfiguration mit `deny_unknown_fields`)." + - "CHANGELOG.md `## Unveröffentlicht`: zwei Stichpunkte unter `### Geändert` (Beta-Hinweis nennt den Stand; Installer auf Deutsch mit Tessera-Grafik und -Symbol) und zwei unter `### Behoben` (Download-Links in der App ausgeblendet; Browser-Kontextmenue in der App ausgeblendet, in Eingabefeldern erhalten), Stil wie Bestand (Praefix `Desktop-App:`, typografische Anfuehrungszeichen, kein Punkt am Ende). Nur zusaetzliche Zeilen — vorhandene Zeilen (auch neue aus 260917-gyd) bleiben stehen." + - "`pnpm --filter @tessera/web exec vitest run` und `pnpm --filter @tessera/web type-check` enden gruen. Kein Docker-Build, kein `tauri build`, kein `git push`, keine Dateien ausserhalb von files_modified + .planning/." + - "Drei Commits: `feat(desktop): …` (Task 1), `feat(web): …` (Task 2), `feat(desktop): …`/`docs: …` (Task 3)." + artifacts: + - "apps/desktop/src-tauri/src/lib.rs — `with_desktop_marker`, `update_labels`, `mod tests`" + - "apps/desktop/src-tauri/tauri.conf.json — Block `bundle.windows.nsis`" + - "apps/desktop/src-tauri/icons/nsis-header.bmp, nsis-sidebar.bmp — neu (aus dem Scratchpad kopiert)" + - "apps/web/src/middleware.ts — `withDesktopCookie`" + - "apps/web/src/middleware.test.ts — neu (oder erweitert, falls 260917-gyd sie angelegt hat)" + - "apps/web/src/lib/desktop-client.ts + .test.ts — neu" + - "apps/web/src/components/desktop/desktop-download-links.tsx + .test.tsx — Client-Waechter, Test 4" + - "apps/web/src/components/desktop/desktop-context-menu-guard.tsx + .test.tsx — neu" + - "apps/web/src/app/layout.tsx — Guard eingebunden" + - "CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md — Stichpunkte/Saetze" + key_links: + - "Die Kette Client → Web ist: Rust haengt `desktop=1` an die ERSTE Navigation → Middleware setzt das Cookie auf die Antwort dieser Anfrage (auch wenn sie ein 307 nach /login ist — WebView2 uebernimmt Set-Cookie auf Redirects) → alle Folgeseiten sehen `tessera_desktop=1` in `document.cookie`. Faellt eines der drei Glieder aus, greift nichts; deshalb setzt die Middleware das Cookie auf JEDER Rueckgabe, nicht nur auf `next()`." + - "Der Tray-Eintrag „Update herunterladen“ oeffnet die Einstellungsseite im SYSTEM-Browser (opener), nicht im Client — dort muessen die Download-Links sichtbar bleiben. Deshalb bekommt diese URL KEIN `desktop=1`, und der Browser des Nutzers bekommt das Cookie nie." + - "`isDesktopClient()` liest das Cookie synchron; wuerde die Komponente es beim ersten Render nutzen, unterschieden sich Server-HTML (kein document) und Client-HTML → Hydration-Fehler. Darum der Hook mit useEffect; der Ladeeffekt darf `isDesktopClient()` dagegen direkt aufrufen (Effekte laufen nur im Client)." + - "Middleware-Datei wird VOR diesem Plan durch 260917-gyd geaendert (`next`-Rueckkehrparameter). Die Helferfunktion ist additiv: sie umschliesst Rueckgabewerte, aendert keine Redirect-Ziele. Der Test prueft `location` nur auf `enthaelt /login`, nicht auf exakte Gleichheit." + - "jsdom implementiert `HTMLElement.isContentEditable` nicht — die Ausnahme fuer contenteditable MUSS ueber `closest('[contenteditable…]')` laufen, sonst ist sie im Test unsichtbar und im Browser trotzdem aktiv (oder umgekehrt)." + - "tauri-codegen (`tauri::generate_context!()` in `run()`) parst tauri.conf.json beim `cargo check` mit `deny_unknown_fields`; ein Tippfehler im nsis-Block faellt lokal auf, obwohl kein Installer gebaut wird. Ob die BMPs korrekt eingebunden sind, zeigt erst der CI-Bau — Nachweis durch den Orchestrator auf der Windows-VM." +--- + + +Vier Befunde aus der Bedienprobe des Desktop-Clients auf der Windows-VM schliessen: + +1. **Client-Erkennung.** Der Rust-Client haengt bei beiden Navigationen zur Server-Adresse `desktop=1` an; die Next.js-Middleware setzt daraufhin das Cookie `tessera_desktop=1` (ein Jahr, lax, nicht httpOnly). Web-Helfer `isDesktopClient()` + Hook `useIsDesktopClient()`. +2. **Download-Links** auf der Anmeldeseite erscheinen im Client nicht mehr (und der Client fragt `/desktop/latest` gar nicht erst an). +3. **Kontextmenue.** Ein kleiner Client-Waechter im RootLayout unterdrueckt das WebView2-Browser-Kontextmenue, laesst es in Eingabefeldern (input/textarea/select/contenteditable) aber zu. +4. **Beta-Label.** Gleiche Versionsnummer, anderer Commit → Tray „Neuen Beta-Stand herunterladen“ und Benachrichtigung „Neuer Beta-Stand {commit} verfügbar …“ statt der verwirrenden gleichen Version. +5. **Installer.** `bundle.windows.nsis` in tauri.conf.json: Deutsch ohne Sprachauswahl, Tessera-Symbol, Kopf- und Seitenbild (BMPs fertig im Scratchpad), currentUser. + +Die Reihenfolge der Tasks folgt der Vorgabe des Orchestrators (Rust → Web → Installer/Doku). Die einzige lokal Ende-zu-Ende pruefbare Kette (Anfrage mit `desktop=1` → Cookie auf der Antwort → Hook → Komponente rendert nichts) liegt komplett in Task 2 — deshalb traegt Task 2 die Tracer-Rolle; Task 1 liefert den Einstiegspunkt (Rust) mit Unit-Test. Der Beweis ueber die WebView2-Grenze (Cookie im echten Client, deutscher Installer, Grafik, Symbol) erfolgt durch den Orchestrator nach dem CI-Bau auf der Windows-VM. + +Purpose: Der Client soll sich wie eine App anfuehlen (keine Browser-Reste, keine sinnlosen Download-Angebote), der Beta-Update-Hinweis soll verstaendlich sein, und der Installer soll zur Marke und zur Sprache der Anwender passen. +Output: lib.rs mit zwei reinen Helfern + Tests; Middleware-Cookie + Test; desktop-client.ts + Test; angepasste DesktopDownloadLinks + Test; DesktopContextMenuGuard + Test; layout.tsx; nsis-Block + zwei BMPs; CHANGELOG + zwei Handbuchsaetze; drei Commits. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@/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-tauri/tauri.conf.json +@/home/vicolab/projects/tessera-ctl/apps/web/src/middleware.ts +@/home/vicolab/projects/tessera-ctl/apps/web/src/components/desktop/desktop-download-links.tsx +@/home/vicolab/projects/tessera-ctl/apps/web/src/components/desktop/desktop-download-links.test.tsx +@/home/vicolab/projects/tessera-ctl/apps/web/src/app/layout.tsx + + + + + + Task 1: Rust — `desktop=1` an beide Navigationen, Beta-Label per `update_labels`, Unit-Tests + apps/desktop/src-tauri/src/lib.rs + + - apps/desktop/src-tauri/src/lib.rs Z. 24-31 (Stil eines dokumentierten reinen Helfers: `api_url`), Z. 66-80 (`save_server_url`: `parsed` → `normalized` in den Store, danach Navigation), Z. 96-107 (Startnavigation im `setup`), Z. 197-231 (Versionspruefung: `is_newer`, Benachrichtigung, `update_item.set_text`) + - apps/desktop/src-tauri/build.rs Z. 1-16 (warum `APP_COMMIT` existiert — Beta-Kanal vergibt jedem Commit dieselbe Version) + + + `#[cfg(test)] mod tests` in lib.rs: + - `with_desktop_marker` auf `https://tessera.example.com` → `https://tessera.example.com/?desktop=1` + - `with_desktop_marker` auf `https://host/app?x=1` → `https://host/app?x=1&desktop=1` + - Das uebergebene Original hat nach dem Aufruf weiterhin `query() == None` (Klon, kein In-Place) + - `update_labels(true, "1.2.0", "abc1234")` → `("Version 1.2.0 herunterladen", "Neue Version 1.2.0 verfügbar – Download über das Symbol im Infobereich.")` + - `update_labels(false, "1.1.0", "abc1234")` → `("Neuen Beta-Stand herunterladen", "Neuer Beta-Stand abc1234 verfügbar – Download über das Symbol im Infobereich.")` + + + Zuerst die Tests aus `` als `mod tests` ans Dateiende schreiben und `cargo test --lib` rot sehen (Funktionen existieren noch nicht). Dann: + + 1. Neben `api_url` eine reine Funktion `with_desktop_marker(url: &tauri::Url) -> tauri::Url` anlegen: Klon des Urls, `query_pairs_mut().append_pair("desktop", "1")`, Klon zurueckgeben. Deutscher Doc-Kommentar mit ae/oe/ue (Stil wie bei `api_url`): Warum der Parameter nur an die Navigation geht und nie in den Store (`server_url` bleibt die reine Adresse; die Middleware setzt daraus das Cookie `tessera_desktop`, siehe apps/web/src/middleware.ts), und dass die Tray-URL „Update herunterladen“ ihn bewusst NICHT bekommt (oeffnet im System-Browser, dort muessen die Download-Links sichtbar bleiben). + 2. In `save_server_url` und in der Startnavigation des `setup` das Argument beider `window.navigate`-Aufrufe auf `with_desktop_marker(&parsed)` aendern. `normalized` (Store-Wert) bleibt `parsed.as_str()` — vor dem Anhaengen gebildet, also ohne Parameter. + 3. Reine Funktion `update_labels(version_changed: bool, version: &str, commit: &str) -> (String, String)` (Rueckgabe: Menuetext, Benachrichtigungstext) mit den beiden Zweigen aus ``; echte Umlaute und Gedankenstrich exakt wie der bestehende Benachrichtigungstext. Doc-Kommentar: Beta-Kanal vergibt jedem Commit dieselbe X.Y.Z (D-07), darum nennt der zweite Zweig den Commit-Stempel statt der Version. + 4. In der Versionspruefung: `let version_changed = info.version != app_version;` und `is_newer` daraus plus dem bestehenden Beta-Commit-Vergleich bilden (Logik unveraendert). Innerhalb von `if is_newer` ein `let (menu_text, body) = update_labels(version_changed, &info.version, &info.commit);` und beide bisherigen `format!`-Aufrufe (Benachrichtigungs-`body` und `update_item.set_text`) durch `body` bzw. `menu_text` ersetzen. Der Kommentarblock ueber `is_newer` bleibt, ein Satz ergaenzt, dass die Texte aus `update_labels` kommen. + 5. `cargo fmt` anwenden (Bestand ist rustfmt-konform). + + Nicht anfassen: Tray-Menue, Autostart, Fensterverhalten, `check_server`, build.rs. + + + cd /home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri && cargo fmt --check && cargo check && cargo clippy && cargo test --lib && test "$(grep -c 'with_desktop_marker(&parsed)' src/lib.rs)" = "2" && grep -q 'update_labels(version_changed' src/lib.rs + + Beide Navigationen tragen `desktop=1`, der Store-Wert nicht; Beta-Bau mit gleicher Version zeigt „Neuen Beta-Stand herunterladen“ / „Neuer Beta-Stand {commit} verfügbar …“, Versionswechsel weiterhin „Version {v} herunterladen“; fmt/check/clippy/test gruen; Commit `feat(desktop): Client meldet sich per desktop=1, Beta-Hinweis nennt den Stand`. + + + + Task 2: Web — Middleware-Cookie, `desktop-client.ts`, DesktopDownloadLinks im Client aus, Kontextmenue-Waechter, Tests + apps/web/src/middleware.ts, apps/web/src/middleware.test.ts, apps/web/src/lib/desktop-client.ts, apps/web/src/lib/desktop-client.test.ts, apps/web/src/components/desktop/desktop-download-links.tsx, apps/web/src/components/desktop/desktop-download-links.test.tsx, apps/web/src/components/desktop/desktop-context-menu-guard.tsx, apps/web/src/components/desktop/desktop-context-menu-guard.test.tsx, apps/web/src/app/layout.tsx + + - apps/web/src/middleware.ts — FRISCH lesen (Plan 260917-gyd hat sie vor diesem Plan geaendert: `next`-Rueckkehrparameter). Alle `return`-Anweisungen der `middleware`-Funktion zaehlen, wie sie JETZT sind. + - `ls apps/web/src/middleware.test.ts` — existiert sie bereits (aus 260917-gyd), wird sie um einen eigenen `describe`-Block erweitert statt neu angelegt. + - apps/web/src/components/desktop/desktop-download-links.test.tsx Z. 1-45 (next-intl-Mock, `vi.hoisted`-Mock fuer `@/lib/desktop`, `afterEach` mit cleanup) + - apps/web/src/lib/desktop.test.ts Z. 1-15 (Kopfkommentar-Stil fuer lib-Tests) + - apps/web/src/app/layout.tsx (Server Component; Client-Komponente darf importiert und gerendert werden) + + + middleware.test.ts (`// @vitest-environment node` als erste Zeile; `NextRequest` aus `next/server`, `SignJWT` aus `jose`; `vi.stubEnv('JWT_SECRET', 'test-secret')` in `beforeEach`, `vi.unstubAllEnvs()` in `afterEach`): + - Test 1: `new NextRequest('http://localhost:3000/login?desktop=1')` → `res.headers.get('set-cookie')` enthaelt `tessera_desktop=1`, `Path=/`, `Max-Age=31536000`, `SameSite=lax`; enthaelt NICHT `Secure`, NICHT `HttpOnly`. + - Test 2: `http://localhost:3000/login` ohne Parameter → `set-cookie` ist null. + - Test 3: `http://localhost:3000/dashboard?desktop=1` ohne Session-Cookie → `res.status` 307, `res.headers.get('location')` enthaelt `/login`, `set-cookie` enthaelt `tessera_desktop=1`. + - Test 4: `https://tessera.example.com/login?desktop=1` → `set-cookie` enthaelt `Secure`. + - Test 5: gueltiges JWT (`new SignJWT({ sub: 'u1' }).setProtectedHeader({ alg: 'HS256' }).setIssuedAt().setExpirationTime('5m').sign(new TextEncoder().encode('test-secret'))`) als Header `cookie: session=` auf `http://localhost:3000/dashboard?desktop=1` → `set-cookie` enthaelt `tessera_desktop=1` und `res.headers.get('x-middleware-next')` ist `'1'`. + desktop-client.test.ts (jsdom; Cookie in `afterEach` per `document.cookie = 'tessera_desktop=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'` loeschen): + - ohne Cookie → `isDesktopClient()` false + - `document.cookie = 'tessera_desktop=1; path=/'` → true + - `tessera_desktop=0` → false + - `vi.stubGlobal('document', undefined)` → false (danach `vi.unstubAllGlobals()`) + - Hook: Cookie gesetzt, `renderHook(() => useIsDesktopClient())` → `await waitFor(() => expect(result.current).toBe(true))`; ohne Cookie bleibt `result.current` false + desktop-download-links.test.tsx, neuer Test 4 („im Desktop-Client“): Cookie gesetzt, `loadDesktopLatest.mockResolvedValue({... files: { windows, linux } })`, `render`, `await act(async () => {})` → `expect(loadDesktopLatest).not.toHaveBeenCalled()`, `expect(container.firstChild).toBeNull()`. `afterEach` loescht das Cookie (Tests 1-3 laufen ohne Cookie weiter). + desktop-context-menu-guard.test.tsx (jsdom; Helfer `fire(el) = el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))` — Rueckgabe false bedeutet preventDefault): + - mit Cookie, `render()`, `await act(async () => {})`: `fire(div)` → false; `fire(input)` → true; `fire(textarea)` → true; `fire(select)` → true; `fire(span in div[contenteditable="true"])` → true + - ohne Cookie: `fire(div)` → true + - mit Cookie, nach `unmount()`: `fire(div)` → true + + + Tests aus `` zuerst schreiben, rot sehen, dann implementieren: + + 1. **middleware.ts** — Konstante `DESKTOP_COOKIE = 'tessera_desktop'` und Helfer `withDesktopCookie(req: NextRequest, res: NextResponse): NextResponse` oberhalb von `middleware` anlegen: wenn `req.nextUrl.searchParams.get('desktop') === '1'`, `res.cookies.set(DESKTOP_COOKIE, '1', { path: '/', maxAge: 60 * 60 * 24 * 365, sameSite: 'lax', httpOnly: false, secure: req.nextUrl.protocol === 'https:' })`; immer `res` zurueckgeben. Kurzer Kommentar: Der Desktop-Client (apps/desktop/src-tauri/src/lib.rs, `with_desktop_marker`) haengt den Parameter nur an seine erste Navigation; das Cookie muss deshalb auf JEDER Antwort landen, auch auf dem Fruehausstieg fuer oeffentliche Routen und auf Redirects — sonst geht die Kennung beim 307 nach /login verloren. `httpOnly: false` ist Absicht (wird von `isDesktopClient()` in apps/web/src/lib/desktop-client.ts gelesen); der Wert ist kein Geheimnis. Danach JEDE `return`-Anweisung innerhalb von `middleware` (einschliesslich solcher, die 260917-gyd ergaenzt hat) in `withDesktopCookie(req, …)` einhuellen; beim Zweig mit `response.cookies.delete('session')` die bestehende Variable durchreichen. Keine Redirect-Ziele, keine Reihenfolge aendern, kein React-Import in der Middleware. Die Middleware-Datei bleibt ansonsten unberuehrt. + 2. **apps/web/src/lib/desktop-client.ts** — exportiert `DESKTOP_COOKIE_NAME = 'tessera_desktop'`, `isDesktopClient()` (bei `typeof document === 'undefined'` false; sonst `document.cookie.split(';').some((c) => c.trim() === `${DESKTOP_COOKIE_NAME}=1`)`) und `useIsDesktopClient()` (`useState(false)`, `useEffect(() => { setIsDesktop(isDesktopClient()); }, [])`, Rueckgabe des Zustands). Kopfkommentar wie in `desktop.ts`: Gegenstueck zur Middleware; Hook statt Direktaufruf beim Render, weil Server-HTML und erster Client-Render sonst auseinanderlaufen (Hydration). + 3. **desktop-download-links.tsx** — `const isDesktop = useIsDesktopClient();` nach `useState`; im Ladeeffekt als erste Zeile `if (isDesktopClient()) return;` (kein Request aus dem Client); nach allen Hooks `if (isDesktop) return null;` vor der bestehenden `!info`-Pruefung. Doc-Kommentar der Komponente um einen Satz ergaenzen (im Desktop-Client entfaellt der Block, Kennung ueber Cookie). + 4. **desktop-context-menu-guard.tsx** — `'use client'`; `export function DesktopContextMenuGuard()`: `const isDesktop = useIsDesktopClient();` `useEffect` mit Abhaengigkeit `[isDesktop]`: wenn false, nichts; sonst Handler `(event: MouseEvent) => { const target = event.target; if (!(target instanceof Element)) return; if (target.closest(EDITABLE_SELECTOR) || (target as HTMLElement).isContentEditable === true) return; event.preventDefault(); }` mit `EDITABLE_SELECTOR = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]'`; `document.addEventListener('contextmenu', handler)`; Aufraeumfunktion entfernt ihn. Rueckgabe `null`. Kommentar: WebView2 zeigt sonst Zurueck/Aktualisieren/Speichern unter/Drucken; in Eingabefeldern bleibt Kopieren/Einfuegen erreichbar; jsdom kennt `isContentEditable` nicht, daher zusaetzlich der Selektor. + 5. **layout.tsx** — `import { DesktopContextMenuGuard } from '@/components/desktop/desktop-context-menu-guard';` und `` unmittelbar vor `{children}` innerhalb von `NextIntlClientProvider` rendern. + 6. Keine neuen i18n-Schluessel (nichts wird angezeigt). de.json/en.json nicht anfassen. + + + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/middleware.test.ts src/lib/desktop-client.test.ts src/components/desktop && pnpm --filter @tessera/web exec vitest run && pnpm --filter @tessera/web type-check && grep -q "withDesktopCookie(req" apps/web/src/middleware.ts && grep -q "DesktopContextMenuGuard" apps/web/src/app/layout.tsx + + Anfrage mit `?desktop=1` bekommt auf jeder Antwortart das Cookie; `isDesktopClient()`/`useIsDesktopClient()` lesen es; DesktopDownloadLinks rendert im Client nichts und laedt nichts; Rechtsklick ausserhalb von Eingabefeldern ist im Client unterdrueckt; alle Web-Tests (bisher 381 + neue) und type-check gruen; Commit `feat(web): Desktop-Client per Cookie erkennen — Download-Links und Browser-Kontextmenü in der App aus`. + + + + Task 3: NSIS-Installer auf Deutsch mit Tessera-Grafik, CHANGELOG, Handbuchsaetze + apps/desktop/src-tauri/tauri.conf.json, apps/desktop/src-tauri/icons/nsis-header.bmp, apps/desktop/src-tauri/icons/nsis-sidebar.bmp, CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md + + - apps/desktop/src-tauri/tauri.conf.json (Block `bundle`, es gibt noch keinen `windows`-Schluessel) + - CHANGELOG.md Z. 1-35 (`## Unveröffentlicht`, Stil der Stichpunkte mit Praefix `Desktop-App:`) — FRISCH lesen, 260917-gyd hat Zeilen ergaenzt + - docs/anleitung-anwender.md Z. 176-197 („Installation unter Windows“, Tray-Menue-Liste) + - docs/anleitung-entwicklung.md Z. 162-166 (Absatz „Der Windows-Installer wird nur im CI gebaut“) + + + 1. Die zwei fertigen Bilder kopieren: `cp /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/36238f40-3162-4b4a-9c11-56905a933eef/scratchpad/nsis-img/nsis-header.bmp /tmp/claude-1000/-home-vicolab-projects-tessera-ctl/36238f40-3162-4b4a-9c11-56905a933eef/scratchpad/nsis-img/nsis-sidebar.bmp apps/desktop/src-tauri/icons/` (nur die beiden .bmp, nicht die -preview.png). Sollte der Scratchpad-Ordner fehlen, STOPP und an den Orchestrator melden — nicht selbst neue Bilder erzeugen. + 2. In tauri.conf.json unter `bundle` (nach `icon`) den Block `"windows": { "nsis": { "languages": ["German"], "displayLanguageSelector": false, "installerIcon": "icons/icon.ico", "headerImage": "icons/nsis-header.bmp", "sidebarImage": "icons/nsis-sidebar.bmp", "installMode": "currentUser" } }` ergaenzen (Pfade relativ zu src-tauri wie die `icon`-Liste; 2-Leerzeichen-Einrueckung wie im Bestand). Keine weiteren Schluessel (kein `template`, kein `installerHooks`). + 3. CHANGELOG.md, `## Unveröffentlicht`, jeweils als NEUE Zeilen am Ende der Liste: unter `### Geändert` „Desktop-App: Hinweis auf einen neuen Beta-Stand nennt den Stand (Commit-Kürzel) statt der unveränderten Versionsnummer“ und „Desktop-App: Windows-Installer auf Deutsch mit Tessera-Grafik und -Symbol“; unter `### Behoben` „Desktop-App: Download-Links auf der Anmeldeseite werden in der App nicht mehr angeboten“ und „Desktop-App: Rechtsklick zeigte das Browser-Kontextmenü (Zurück, Aktualisieren, Drucken …) – in der App ausgeblendet, in Eingabefeldern bleibt es erhalten“. Bestehende Zeilen (auch neue aus 260917-gyd) bleiben unveraendert. + 4. docs/anleitung-anwender.md: Im Absatz „Installation unter Windows“ (Z. 178) nach dem Satz zu „Trotzdem ausführen“ ergaenzen: „Der Installationsassistent führt auf Deutsch durch die Installation; sie erfolgt für den angemeldeten Benutzer und benötigt keine Administratorrechte.“ In der Tray-Menue-Liste (Z. 193) hinter **Update herunterladen** ergaenzen: „ — wird aktiv, sobald eine neue Version vorliegt (auf dem Beta-Kanal: „Neuen Beta-Stand herunterladen“)“. Sie-Form, typografische Anfuehrungszeichen wie im Bestand. + 5. docs/anleitung-entwicklung.md: Im Absatz Z. 162-166 einen Satz anfuegen: Sprache, Symbol und Bilder des Installers stehen in `apps/desktop/src-tauri/tauri.conf.json` unter `bundle.windows.nsis` (Deutsch ohne Sprachauswahl, `icons/nsis-header.bmp` 150×57 und `icons/nsis-sidebar.bmp` 164×314 als 24-Bit-BMP); Aenderungen daran lassen sich nur ueber den CI-Bau auf einem Windows-Rechner pruefen, lokal validiert `cargo check` lediglich die Schluessel. + 6. Gate laut ``: Schema-Pruefung per node gegen `NsisConfig.properties` und `NSISInstallerMode` des lokalen CLI-Schemas, Existenz und Format der BMPs, `cargo check` (tauri-codegen parst die Konfiguration mit `deny_unknown_fields`). Kein `tauri build`, kein Docker. + + + cd /home/vicolab/projects/tessera-ctl && node -e 'const fs=require("fs"),p=require("path");const s=require("./apps/desktop/node_modules/@tauri-apps/cli/config.schema.json");const c=JSON.parse(fs.readFileSync("apps/desktop/src-tauri/tauri.conf.json","utf8"));const n=c.bundle.windows.nsis;const allowed=Object.keys(s.definitions.NsisConfig.properties);const bad=Object.keys(n).filter(k=>!allowed.includes(k));if(bad.length){console.error("unbekannte Schluessel:",bad);process.exit(1)}const modes=s.definitions.NSISInstallerMode.oneOf.map(o=>o.enum[0]);if(!modes.includes(n.installMode)){console.error("installMode ungueltig");process.exit(1)}if(JSON.stringify(n.languages)!==JSON.stringify(["German"])||n.displayLanguageSelector!==false){console.error("languages/displayLanguageSelector");process.exit(1)}for(const k of["installerIcon","headerImage","sidebarImage"]){const f=p.join("apps/desktop/src-tauri",n[k]);if(!fs.existsSync(f)){console.error("fehlt:",f);process.exit(1)}}console.log("nsis-Block OK")' && magick identify -format '%f %m %wx%h\n' apps/desktop/src-tauri/icons/nsis-header.bmp apps/desktop/src-tauri/icons/nsis-sidebar.bmp | grep -q 'nsis-header.bmp BMP3 150x57' && magick identify -format '%f %m %wx%h\n' apps/desktop/src-tauri/icons/nsis-sidebar.bmp | grep -q 'BMP3 164x314' && (cd apps/desktop/src-tauri && cargo check) && test "$(grep -c '^- Desktop-App: ' CHANGELOG.md)" -ge 7 && grep -q 'bundle.windows.nsis' docs/anleitung-entwicklung.md && grep -q 'Installationsassistent' docs/anleitung-anwender.md + + nsis-Block mit den sechs Schluesseln steht in tauri.conf.json und besteht Schema- und codegen-Pruefung; beide BMPs liegen in icons/; CHANGELOG traegt vier neue Stichpunkte; beide Handbuecher nennen den deutschen Installer bzw. den Ort der Konfiguration; Commit `feat(desktop): Windows-Installer auf Deutsch mit Tessera-Grafik und -Symbol; CHANGELOG, Handbuch`. Der eigentliche Nachweis (deutscher Dialog, Kopf-/Seitenbild, Symbol im Downloads-Fenster, Cookie im echten Client) folgt durch den Orchestrator nach dem CI-Bau auf der Windows-VM — im SUMMARY als offen fuehren. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser/WebView → Middleware | Query-Parameter `desktop=1` und Cookie `tessera_desktop` sind frei setzbar (jeder Browser kann sie senden) | +| Rust-Client → gespeicherte Server-Adresse | Nutzer-Eingabe wird als URL geparst und um ein Query-Paar erweitert | +| Installer-Konfiguration → NSIS-Bundler im CI | BMP/ICO-Dateien aus dem Repo werden in den Installer eingebettet | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-H2S-01 | Spoofing | `withDesktopCookie` / `isDesktopClient` | low | accept | Das Cookie steuert ausschliesslich Kosmetik (Download-Links, Kontextmenue). Kein Auth-, Rechte- oder Datenpfad haengt daran; ein manuell gesetztes Cookie im Browser blendet nur Links aus. Middleware-Reihenfolge (Session-Pruefung, Redirects) bleibt unveraendert. | +| T-H2S-02 | Information Disclosure | Cookie `tessera_desktop` (httpOnly false) | low | accept | Wert ist die Konstante `1`, kein Geheimnis; `sameSite: lax`, `secure` bei https. Bewusst per JavaScript lesbar, weil der Web-Helfer es braucht. | +| T-H2S-03 | Tampering | `with_desktop_marker` (Rust) | low | mitigate | `query_pairs_mut().append_pair` kodiert korrekt; die Nutzer-URL wurde zuvor von `tauri::Url::parse` validiert (Schema http/https in `check_server`). Der Store haelt weiterhin die unveraenderte Adresse. Unit-Test belegt Klon statt In-Place. | +| T-H2S-04 | Denial of Service | `DesktopContextMenuGuard` | low | mitigate | Listener nur im Desktop-Client aktiv; Ausnahme fuer Eingabefelder/contenteditable per Selektor UND `isContentEditable`, damit Kopieren/Einfuegen erreichbar bleibt; Aufraeumfunktion entfernt den Listener (Test). | +| T-H2S-05 | Tampering | `icons/nsis-*.bmp`, `bundle.windows.nsis` | low | accept | Bilder stammen aus dem eigenen, per resvg gerenderten Repo-Icon; Format per `magick identify` belegt; Schluessel gegen das lokale CLI-Schema und per `cargo check` (deny_unknown_fields) geprueft. | +| T-H2S-SC | Tampering | npm/pip/cargo installs | low | accept | Keine neue Abhaengigkeit in diesem Plan (kein `pnpm add`, kein `cargo add`); package-legitimacy gate entfaellt. | + + + +- Rust: `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` in apps/desktop/src-tauri gruen; beide `window.navigate`-Aufrufe nutzen `with_desktop_marker(&parsed)`. +- Web: `pnpm --filter @tessera/web exec vitest run` (alle Dateien, bisher 57/381 plus die neuen) und `pnpm --filter @tessera/web type-check` gruen. +- Middleware-Test belegt das Cookie auf Fruehausstieg, Redirect ohne Session, `next()` nach gueltigem JWT, `Secure` nur bei https. +- tauri.conf.json: nsis-Block besteht Schema-Pruefung (node) und `cargo check`; BMPs vorhanden mit BMP3 150x57 / 164x314. +- CHANGELOG: vier neue `Desktop-App:`-Stichpunkte; Handbuecher ergaenzt. +- Offen (nicht lokal pruefbar, Orchestrator auf der Windows-VM nach CI-Bau): Cookie im echten Client gesetzt, keine Download-Links auf der Anmeldeseite im Client, kein Browser-Kontextmenue, Beta-Label mit Commit, deutscher Installer mit Tessera-Grafik und -Symbol. + + + +- Alle `must_haves.truths` erfuellt; drei Commits ohne Push, ohne Docker-Build, ohne `tauri build`. +- Keine Datei ausserhalb von `files_modified` + `.planning/` veraendert (`git status` vor dem letzten Commit gegenpruefen). +- SUMMARY nennt die offenen VM-Nachweise ausdruecklich. + + + +Create `/home/vicolab/projects/tessera-ctl/.planning/quick/260917-h2s-desktop-client-web-erkennt-den-client-do/260917-h2s-SUMMARY.md` when done +