From efbd6e8974fcec5183d849771e0fa153e7c31d61 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 18 Sep 2026 11:29:14 +0200 Subject: [PATCH] =?UTF-8?q?docs(quick-260917-kgc):=20Aktenstand=20?= =?UTF-8?q?=E2=80=94=20Update=20in=20der=20Desktop-App,=20alle=20Nachweise?= =?UTF-8?q?=20erbracht,=20Wiedereinstieg=20bereinigt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick 260917-kgc (Plan/Recherche/Bericht/Verifikation) und Schnellfix a6d1a64 in der Quick-Task-Tabelle; Nachweise in allen sechs Berichten nachgetragen (Playwright lokal, CI-Laeufe 382-384, Windows-Test-VM: In-App-Update 7479cb4 -> a6d1a64). Ueberholte .continue-here-Dateien entfernt, Desktop-Client-Todo geschlossen. Dieser Push aendert nichts unter apps/desktop -- er ist zugleich der Beweisfall 2 des CI-Desktop-Skips (Pakete aus dem Zwischenspeicher). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh --- .planning/.continue-here.md | 87 ----- .planning/STATE.md | 18 +- .../.continue-here.md | 69 ---- .../260917-jdd-SUMMARY.md | 7 + .../260917-jdf-SUMMARY.md | 6 + .../260917-jdh-SUMMARY.md | 7 + .../260917-jn2-SUMMARY.md | 8 + .../260917-kgc-PLAN.md | 312 ++++++++++++++++++ .../260917-kgc-RESEARCH.md | 298 +++++++++++++++++ .../260917-kgc-SUMMARY.md | 249 ++++++++++++++ .../260917-kgc-VERIFICATION.md | 142 ++++++++ ...desktop-client-auslieferungsreif-machen.md | 0 12 files changed, 1039 insertions(+), 164 deletions(-) delete mode 100644 .planning/.continue-here.md delete mode 100644 .planning/phases/18-desktop-client-fertigstellen/.continue-here.md create mode 100644 .planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-PLAN.md create mode 100644 .planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-RESEARCH.md create mode 100644 .planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md create mode 100644 .planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-VERIFICATION.md rename .planning/todos/{pending => completed}/2026-09-15-desktop-client-auslieferungsreif-machen.md (100%) diff --git a/.planning/.continue-here.md b/.planning/.continue-here.md deleted file mode 100644 index ec8cef5..0000000 --- a/.planning/.continue-here.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -context: default -phase: betrieb-nach-live-gehen -task: null -total_tasks: 0 -status: paused -last_updated: 2026-09-16T10:28:05.211Z ---- - -# Wiedereinstieg — v1.1.0 ist live, nichts angefangen - -## Critical Anti-Patterns - -Alle aus tatsaechlichen Fehlschlaegen dieser und der vorigen Sitzungen. - -| Muster | Beschreibung | Schwere | Vermeidung | -|--------|--------------|---------|------------| -| Zaehlung ohne Ansehen | Der Planer zaehlte fuenf Tastenreihen im Rechner, es sind sechs — die Mindesthoehe schnitt die unterste Reihe ab. Erst der Browser-Blick fand es. | blocking | Zahlen, die eine Groesse tragen, im Browser messen (Bounding-Box, scrollHeight), nicht aus dem Quelltext ableiten. | -| Gespeicherte Werte vergessen | Konstanten zu aendern haette fuer den User NICHTS bewirkt — react-grid-layout nimmt gespeicherte Layout-Eintraege woertlich (inkl. minW/minH). | blocking | Bei jeder Aenderung an Vorgaben pruefen, ob persistierte Daten dieselben Werte tragen; dann beim Laden ueberschreiben/anheben. | -| `git checkout -- ` als Falsifizierungs-Rueckweg | setzte auch die noch unkommittierte Nutz-Aenderung zurueck. | advisory | Vor Rueckbau-Proben committen, oder Patchdatei + `git apply -R`. | -| Zwei Schreiber auf einer Datei | Uebersetzungs-Fix musste warten, weil der laufende Executor de.json/en.json anfasste. | advisory | Quick-Tasks mit ueberlappenden Dateien nacheinander ausfuehren. | -| Platte voll durch Bau-Cache | vier Docker-Bauten + CI: `no space left on device`. | advisory | `docker builder prune -af` und `docker image prune -f` bei Bedarf; Volumes nie anfassen. | -| Bericht statt Arbeitsbaum | (aus Etappe 2) Agenten brachen nach getaner Arbeit ab. | blocking | `git status` ist die Wahrheit. | - - -**Gemessen 2026-09-16 10:28Z:** `git status --porcelain` leer, `main == origin/main` -(29fe3d7), keine async-jobs, keine angefangene Arbeit. - -**Live:** tessera.ctl.de laeuft `v1.1.0` (User hat gepullt, "sieht gut aus"). -**Beta:** alpha.tessera.ctl.de holt `beta`/`latest` — derselbe Stand e0d4532. -**Registry:** beta/latest/live/v1.0.0/v1.1.0. **Gitea-Releases:** 1.0.0 und 1.1.0 -(letzterer von der Pipeline angelegt — erster CI-Beweis des Release-Wegs). -**Schalter:** AUS und bleibt es. **Mandantenfaehigkeit:** RUHT (User 2026-09-14). -**Tests:** Web 49/309, API 67/1078, Werkzeug 253/253. **Ledger:** 15 offen / 1 -zurueckgestellt / 23 geschlossen / 39. - - - -Seit 2026-09-14: WINDOWS #29, Etappe 3c, Kanalmodell + Versionsstempel, -Fehler-melden-Knopf, Erstfreigabe v1.0.0 (live 15.09.), Dashboard-Umbau + -Nachbesserung, Aenderungsliste (CHANGELOG.md, Seite "Was ist neu", -Gitea-Release je Tag), Uebersetzungs-Fix, Freigabe v1.1.0. Alle als Quick-Tasks -mit voller Kette; Details in der Quick-Task-Tabelle in `.planning/STATE.md`. - - - -Nichts Angefangenes. Naechste Arbeit kommt vom User (Feedback aus dem Betrieb). -Ohne Termin: Desktop-Client-Todo, Ship Phase 17 (blockiert bei open_count 15), -Ledger #35/#36/#37. Nicht ansprechen: Mandantenfaehigkeit, Lizenzierung. - - - -Siehe `HANDOFF.json` — Kanalmodell, Versionsnummern-Regel (Funktionen -> mittlere -Stelle, Fixes -> dritte), Mandantenfaehigkeit ruht, Lizenzmodell nur festgehalten, -Dashboard-Entscheidungen (inhaltsgetriebene Minima, Ueberschreiben gespeicherter -Minima, ganze Kachel Griff, preventCollision). - - - -Keine. Eine nicht-blockierende Handreichung fuer den User: auf alpha einmalig -`IMAGE_TAG=beta` und die zwei `image:`-Zeilen (Kap. 9) — bis dahin laeuft alpha -ueber `latest`, das dasselbe Abbild ist. - - -## Required Reading (in order) - -1. `.planning/STATE.md` — Session Continuity + Quick-Task-Tabelle -2. `CHANGELOG.md` — Regel: jede Aenderung sofort unter `## Unveröffentlicht` -3. `docs/anleitung-betrieb.md` Kapitel 9 — Freigabe/Hotfix-Rezept -4. `.planning/WINDOWS.md` — 15 offen, davon nur #35/#36/#37 ohne Mandantenbezug - -## Infrastructure State - -- Live-Server tessera.ctl.de (`IMAGE_TAG=live`, eigene DB); alpha 192.168.13.12 - (Beta); Deploy macht der User (pull + `up -d --force-recreate api web`). -- Lokal: `db`, `api`, `web`, `mailhog` laufen (web aus c3d8e16); Admin - admin/admin123; DB ohne Host-Port (IP per `docker inspect`, tessera:tessera_dev); - Prisma-Binary aus `apps/api/node_modules/.bin/prisma`. -- Gitea 1.26.2 + Runner auf diesem Rechner; Runner arbeitet EINEN Auftrag - gleichzeitig; CI ueber API localhost:3002 beobachtbar (Token aus Push-URL, nie - ausgeben). `grep` ist hier ugrep (`$` als Anker). - - -`/gsd-resume-work`, dann das, was der User nennt — als `/gsd-quick --validate` -mit voller Kette; Browser-Nachweis per Playwright MCP gegen lokale Container; -CHANGELOG pflegen; bei "Version freigeben" das Rezept aus Kap. 9. - diff --git a/.planning/STATE.md b/.planning/STATE.md index 797dc42..eaba40b 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -6,8 +6,8 @@ current_phase_name: desktop-client-fertigstellen status: verified stopped_at: "Quick-Task 260917-jn2 abgeschlossen (3 Commits: feat(desktop), feat(web), docs); Windows-VM-Bedienprobe steht durch Orchestrator aus" last_updated: "2026-09-17T12:55:59.549Z" -last_activity: 2026-09-17 -last_activity_desc: Version 1.2.0 freigegeben; publish-release.sh nimmt nie mehr die oeffentliche Gitea-Adresse (Host-Gateway/localhost) +last_activity: 2026-09-18 +last_activity_desc: Sechs Auftraege nach 1.2.0 gebaut, gepusht, getestet — u. a. Update in der Desktop-App (signiert), CI-Desktop-Skip, Favoriten-Symbol/-Sortierung state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2 progress: total_phases: 18 @@ -30,8 +30,8 @@ See: .planning/PROJECT.md (updated 2026-07-17) Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden) Plan: 6 of 6 -Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Naechste Arbeit: vier Quick-Tasks (drei Wuensche des Users + CI-Desktop-Bau ueberspringen, wenn apps/desktop unveraendert) -Last activity: 2026-09-17 - Quick 260917-jdf/jdh/jdd/jn2 abgeschlossen (Bildmarke, CI-Skip, Favoriten, Desktop-Adresse), Browser-Nachweis lokal; 260917-kgc (In-App-Updater) in Planung +Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen +Last activity: 2026-09-18 - Quick 260917-kgc (Update in der App) + Schnellfix a6d1a64 abgeschlossen; alle Nachweise erbracht (lokaler Browser, CI 382-384, Windows-VM: In-App-Update 7479cb4 → a6d1a64) Progress: [██████████] 99% @@ -441,6 +441,8 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests. | 260917-jdh | **CI: Job `desktop` ueberspringt den Rust-Bau, wenn der Desktop-Stand unveraendert ist.** Neues `.gitea/scripts/desktop-stamp.sh` (`stamp`: Version aus `desktop-version.sh --print` + voller SHA von `git log -1 -- apps/desktop desktop-version.sh desktop-collect.sh desktop-stamp.sh ci.yml`; `check`: Manifest/Kanal/Version/Groesse/sha256 des restaurierten `desktop-dist`). Drei neue Schritte direkt nach dem Checkout (stamp → `cache/restore` `desktop-dist-stamp-` nur auf main → check), 13 Bau-Schritte mit `if: steps.reuse.outputs.reuse != 'true'`, nach echtem Bau `cache/save` unter dem Stempel; `Uebergabe an publish` und `publish` unveraendert; Tags bauen immer. Doku: Betriebshandbuch Kap. 10, ci-cd-setup.md 4/6, Entwicklungsanleitung. Verifikation passed 9/9 (lokale Proben). **Offen: CI-Beweis nach Push** (baut → Docs-Push ueberspringt → Desktop-Push baut neu; `cache/save` bei belegtem Schluessel beobachten). | 2026-09-17 | 8c4aaa5,e7633e1 | [260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d](./quick/260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d/) | | 260917-jdd | **Favoriten-Widget: Symbol trotz Zertifikatsfehler/interner Adresse, Favoriten sortierbar.** API: `undici@7.28.0` (exakt, war schon im Lockfile) — `LENIENT_TLS_AGENT` (`rejectUnauthorized: false`) als Dispatcher NUR in `fetchWithRedirectGuard`, SSRF-Schutz (DNS/private IPs/Redirects/Timeouts/Deckel) byteweise unveraendert; `PUT /favorites/order` `{widgetId, ids}` VOR den `:id`-Routen, `reorder()` in `withTenantTransaction` mit `userId`+`widgetId` je Eintrag, eine 400-Meldung; Icon-Proxy mit `nosniff` + CSP sandbox. Web: `FavoriteIcon` Kette Proxy-Bild → bei Fehler Direktbild `{origin}/favicon.ico` (nur http/https, no-referrer) → Buchstabe; Pfeile „Nach oben/unten“ im Bearbeitungsmodus, optimistisch + Reload bei Fehler; Altbestand `position 0` normalisiert sich beim ersten Klick. Befund: `discoverFavoriteIconUrl` liefert nie null (immer Origin-Rueckfall) — deshalb haengt der Browser-Ersatzweg am Bildfehler. API 1101 / Web 429 Tests. Browser: `self-signed.badssl.com` → Proxy-Symbol; `http://192.168.13.11:3002` → Proxy 502 → Direktbild; Sortierung ueber Reload, DB-Positionen 0..3. Verifikation 15/15 + Browser. | 2026-09-17 | 2a562d0,b18ac25,b023d6f | [260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u](./quick/260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u/) | | 260917-jn2 | **Desktop-Client: Server-Adresse sichtbar und nachtraeglich aenderbar.** Rust: `TrayIconBuilder::with_id("main")`, `TrayItems { connected, update }` in `app.manage`, `apply_server()` setzt Tooltip `Tessera – {host}` + gesperrte Menuezeile `Verbunden mit {host}` an einer Stelle; Tray-Eintrag `Server-Adresse ändern…` navigiert zu `setup_page_url()` (`http://tauri.localhost/setup.html` unter Windows, sonst `tauri://localhost/setup.html`); Commands `get_server_url`/`open_server` (kein Capability-Eintrag noetig — Remote-Origin darf keine Commands rufen); `parse_server_url` (nur http/https) gemeinsam; `spawn_version_check` herausgezogen, `update`-Klick liest Adresse per `stored_server_url` beim Klick. setup.html: Vorbelegung, „Aktuell verbunden mit“, „Abbrechen“. Web: Einstellungen → Desktop-App zeigt im Client „Verbunden mit: {origin}“ + Hinweis (`settings.desktop.*`). 18 Rust-Tests, Web 431. Browser: Web-Block mit Cookie bestaetigt. Verifikation human_needed: **Windows-VM-Probe mit CI-Paket offen** (Tooltip, Menuezeile, Adresse aendern/Abbrechen, Wechsel ohne Neustart). | 2026-09-17 | 29c132e,4c79874,4d48543 | [260917-jn2-desktop-client-aktuelle-server-adresse-s](./quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/) | +| 260917-kgc | **Desktop-Client: Update in der App (tauri-plugin-updater, signierte Pakete).** Client: Plugin 2.11 + `semver`, `plugins.updater.pubkey` (minisign; privater Schluessel + Passwort NUR unter `~/.tessera/desktop-updater/` auf dem Dev-Rechner, Gitea-Secrets `TAURI_SIGNING_PRIVATE_KEY`/`_PASSWORD`), Endpunkt zur Laufzeit `{server}/api-proxy/desktop/update?target&arch¤t&base`, `is_update_newer` (hoehere Basis → Update; gleiche Basis nur bei `beta.g` mit anderem Commit; kleinere/gleiche Live → nichts), Pruefung 15 s / Download 600 s (Plugin-Timeout gilt fuer beides), Tray „Auf Version X / Beta-Stand aktualisieren" → Fortschritt → passiver NSIS-Installer startet die App neu (Linux: `app.restart()`), Fehler → Benachrichtigung + Download-Seite im Browser, `http://` → gesperrt „Update nur über https möglich". API: `GET /desktop/update` (statisch VOR `download/:platform`, `base` nur Origin, 204 ohne `signature`/`updateVersion`). CI: `createUpdaterArtifacts`, Secrets nur an den zwei `tauri build`-Schritten, `desktop-collect.sh` schreibt `signature` + `updateVersion` (`X.Y.Z-beta.g`), `desktop-stamp.sh check` verlangt beides. 33 Rust-Tests, 23 API-Tests. **Nachweise erbracht:** CI baut `.sig` fuer beide Plattformen (Cross-Bau rustls ok); alpha-Endpunkt 200/400; Windows-VM: Client 7479cb4 → Tray-Klick → Neustart als a6d1a64, Adresse erhalten. Bereits installierte Clients (≤ 1.2.0) brauchen einmal den Browser-Installer. | 2026-09-17 | 678ba51,de81c74,7004b5b,7479cb4 | [260917-kgc-desktop-client-update-in-der-app-herunte](./quick/260917-kgc-desktop-client-update-in-der-app-herunte/) | +| fast | **Desktop-Client: Setup-Seite zeigt Version und Stand der App** („Tessera-App 1.2.0 · Stand a6d1a64"; ohne Stempel nur Version) — Command `get_client_info`, Helfer `client_info_label` (2 Tests), `

` in setup.html, CHANGELOG. Diente zugleich als zweiter Desktop-Stand fuer den Update-Nachweis. 35 Rust-Tests. | 2026-09-18 | a6d1a64 | — | ## Deferred Items @@ -482,8 +484,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen. ## Session Continuity -Last session: 2026-09-17T12:55:59.226Z -Resumed: 2026-09-17 — Sitzung ueber /gsd-resume-work fortgesetzt (keine HANDOFF.json; beide .continue-here-Dateien ueberholt, Arbeitsbaum leer, main == origin/main 38c1400). -Stopped at: Quick-Task 260917-jn2 abgeschlossen (3 Commits: feat(desktop), feat(web), docs); Windows-VM-Bedienprobe steht durch Orchestrator aus +Last session: 2026-09-18T09:30:00Z +Resumed: 2026-09-17 — Sitzung ueber /gsd-resume-work fortgesetzt; sechs Auftraege des Users komplett abgearbeitet (Quick 260917-jdf/jdh/jdd/jn2/kgc + Schnellfix a6d1a64), alle mit Plan-Pruefung, Verifikation und Nachweis (lokaler Browser via Playwright, CI-Laeufe 382-384, Windows-Test-VM 8233). +Stopped at: Alles committet und gepusht. Nichts angefangen. Beta (alpha) laeuft mit a6d1a64 (User hat gepullt). Live (tessera.ctl.de) steht auf v1.2.0 — der dortige Client hat den Updater noch nicht; erst die naechste Freigabe (1.3.0, neue Funktionen) bringt ihn; danach aktualisieren sich Clients per Tray-Klick. Offen fuer den User: eigenen Arbeitsplatz-Client einmal per Browser-Installer erneuern (alter Stand ohne Beenden-Fix/Updater). Signierschluessel: ~/.tessera/desktop-updater/ (Dev-Rechner) — sichern; Verlust = neue Clients per Installer noetig. Letzter offener CI-Beweis (Docs-Push ueberspringt den Desktop-Bau) = der Lauf zu diesem Aktenstand-Commit. Beide .continue-here-Dateien entfernt (ueberholt), Desktop-Todo geschlossen. Resume file: None -Last activity: 2026-09-17 - Quick 260917-jdf/jdh/jdd/jn2 abgeschlossen (Bildmarke, CI-Skip, Favoriten, Desktop-Adresse), Browser-Nachweis lokal; 260917-kgc (In-App-Updater) in Planung +Last activity: 2026-09-18 - Quick 260917-kgc (Update in der App) + Schnellfix a6d1a64 abgeschlossen; alle Nachweise erbracht (lokaler Browser, CI 382-384, Windows-VM: In-App-Update 7479cb4 → a6d1a64) diff --git a/.planning/phases/18-desktop-client-fertigstellen/.continue-here.md b/.planning/phases/18-desktop-client-fertigstellen/.continue-here.md deleted file mode 100644 index 8334caa..0000000 --- a/.planning/phases/18-desktop-client-fertigstellen/.continue-here.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -context: phase -phase: 18-desktop-client-fertigstellen -task: 3 -total_tasks: 3 -status: complete -last_updated: 2026-09-17T07:38:47.449Z ---- - -## Critical Anti-Patterns - -| Pattern | Description | Severity | Prevention Mechanism | -|---------|-------------|----------|---------------------| -| Fenster ohne Startseite | Tauri-Fenster `main` hatte keine `url`; im gebauten Paket erschien "asset not found: index.html" (Altlast Phase 6, nie in einem echten Paket sichtbar). Lokaler `cargo check`/AppImage-Bau fing es nicht. | advisory | `tauri.conf.json` `app.windows[].url = "setup.html"` (b6d9013); Startseite im Binary per `strings target/release/tessera-desktop \| grep setup.html` pruefen | -| Rust-Bau frisst den Host | 8 parallele rustc-Prozesse des CI-Runners (auf demselben Host wie Gitea + Dev-Stack, 15 GB) -> Speichergrenze, Claude Code beendete eine Hintergrundaufgabe | advisory | `CARGO_BUILD_JOBS: "4"` im Job `desktop` (b6d9013); bei Rust-Vollbau lokal nicht parallel zur CI messen | -| Gitea-API ohne Token | `https://git.vicolab.de/api/v1/...` antwortet 401 ohne Token | advisory | Token aus `git config --get remote.origin.pushurl` lesen (nie ausgeben), gegen `http://localhost:3002/api/v1/...` | - - -Phase 18 (Desktop-Client) ist ABGESCHLOSSEN und gepusht (626f60e): Verifikation passed, Windows-Bedienprobe des Users bestanden (Pakete aus CI-Lauf 369, Commit 03fd85a). Nichts ist angefangen. Der User startet die VM nach einer RAM/CPU-Aenderung neu; als Naechstes soll der Bau-Benchmark wiederholt und mit der Referenz verglichen werden. - - - - -Completed Tasks (diese Sitzung, 2026-09-16/17): -- Sechs Quick-Tasks Dashboard (260916-hiv/htc/iex/j4f/jvj/k2z): URL-Platzhalter Kalenderquellen, Kalender-Widget nach Vorbild personal-dashboard (Monatsraster + Naechste Termine + 3 Einstellungen), Notiz-Haekchen abhakbar, Favoriten-Titel, Link-Widget entfernt (Migration), Tooltip-Umbruch, Notiz-Farbmodus, Changelog-Stichpunkte, Plaketten in Kalenderfarbe, Mehrfach-Kreise, Markdown-Aufzaehlungspunkte — alle gepusht, CI 365 gruen -- Phase 18, 6 Plaene: 18-01 Durchstich Linux (Manifest-Skript, API /desktop/latest + /desktop/download/:platform, Abbild), 18-02 CI-Job desktop + Cache-Uebergabe + Release-Anhaenge, 18-03 Web (Anmeldeseite-Link, Einstellungen -> Allgemein -> Desktop-App), 18-04 Client (Erststart-Seite per Rust-Kommandos, Versionspruefung, Tray Update/Autostart, Icons), 18-05 Windows-Cross-Bau (1 Korrekturrunde: clippy), 18-06 Handbuecher/Changelog/REQUIREMENTS -- Code-Review (1 kritisch, 3 Warnungen) behoben + Commit-Stempel via TESSERA_COMMIT -- Schnellkorrektur Startseite setup.html + CARGO_BUILD_JOBS=4 (b6d9013) -- Benchmark-Referenz vor dem Umbau erfasst (memory/reference_benchmark_dev_host.md) - - - - -- Benchmark nach dem Neustart wiederholen (vier Befehle, Rechner idle) und vergleichen -- Beim naechsten Freigabe-Tag (1.2.0): Release-Anhaenge am Gitea-Release + Update-Hinweis im Client beobachten (18-UAT.md #2/#3) — nur Beobachtung, kein Code offen -- Freigabe 1.2.0 selbst nur auf Zuruf des Users (Kap. 9 Betriebshandbuch) - - - - -- Installer in Tessera herunterladbar UND am Gitea-Release (User); Windows per Cross-Bau auf Linux; Pakete im API-Abbild (kein Gitea-Zugang vom Live-Server noetig); Server-Adresse beim Erststart; Update nur Hinweis + Link; keine Signierung (SmartScreen-Hinweis im Handbuch) -- Beta-Builds: Version X.Y.Z des letzten Tags + Commit-Stempel im Dateinamen/Manifest; Client vergleicht auf beta Version + Commit -- Kalender-Widget: keine Quellenauswahl pro Widget (User: nur Optik) -- Mandantenfaehigkeit und Lizenzierung ruhen weiterhin (nicht ansprechen) - - - -- keine - - -## Required Reading (in order) -1. `.planning/STATE.md` — Aktenstand, Quick-Task-Tabelle, Phase 18 Complete -2. `memory/reference_benchmark_dev_host.md` (Claude-Memory) — Benchmark-Referenz und Befehle -3. `.planning/phases/18-desktop-client-fertigstellen/18-UAT.md` — was beim naechsten Tag zu beobachten ist -4. `docs/anleitung-betrieb.md` Kap. 9 (Freigabe) und Kap. 10 (Desktop-Pakete) - -## Infrastructure State -- Beta (alpha.tessera.ctl.de): Stand main 03fd85a-Pakete, vom User gepullt; Live: v1.1.0 -- Lokaler Docker-Stack (api/web/db/mailhog) laeuft, API-Abbild mit 1.1.0-Desktop-Manifest; Gitea + Runner auf demselben Host -- Playwright MCP: Browser-Binary nachinstalliert (`npx @playwright/mcp@latest install-browser chrome-for-testing`) -- VM wird vom User neu gestartet (RAM/CPU-Aenderung) — danach `nproc`/`free -h` neu erfassen - - -Alles committet und gepusht, Arbeitsbaum leer. Naechste Sitzung beginnt mit dem Benchmark-Vergleich; danach gibt es keinen offenen Auftrag — auf den User warten (Freigabe 1.2.0 oder neue Wuensche). - - - -Start with: `nproc && free -h`, dann die vier Benchmark-Befehle aus memory/reference_benchmark_dev_host.md (kein CI-Lauf parallel), Tabelle vorher/nachher an den User. - diff --git a/.planning/quick/260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u/260917-jdd-SUMMARY.md b/.planning/quick/260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u/260917-jdd-SUMMARY.md index e3dd175..18dd655 100644 --- a/.planning/quick/260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u/260917-jdd-SUMMARY.md +++ b/.planning/quick/260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u/260917-jdd-SUMMARY.md @@ -194,3 +194,10 @@ Kein Blocker fuer weitere Arbeit — API-Suite 68 Dateien/1101 Tests, Web-Suite ## Self-Check: PASSED All 11 claimed files found on disk; all three task commits (2a562d0, b18ac25, b023d6f) found in git history. + +## Nachweis durch Orchestrator (2026-09-17, Playwright gegen lokale Container) — erbracht + +- `https://self-signed.badssl.com`: Symbol ueber den Server-Proxy geladen (180 px) — vorher Buchstabe. +- `http://192.168.13.11:3002` (interner Host): Proxy antwortet 502, Widget laedt `http://192.168.13.11:3002/favicon.ico` direkt (referrerPolicy no-referrer) — Symbol da. +- Sortierung: „Nach unten"/„Nach oben" aendern die Reihenfolge sofort; nach Reload bleibt sie; DB-Positionen nach dem ersten Klick 0..3 (Altbestand mit 0 normalisiert). +- Testfavoriten danach aus der lokalen DB entfernt. diff --git a/.planning/quick/260917-jdf-bildmarke-ganzes-t-uebernimmt-die-akzent/260917-jdf-SUMMARY.md b/.planning/quick/260917-jdf-bildmarke-ganzes-t-uebernimmt-die-akzent/260917-jdf-SUMMARY.md index 37cf643..14c8b28 100644 --- a/.planning/quick/260917-jdf-bildmarke-ganzes-t-uebernimmt-die-akzent/260917-jdf-SUMMARY.md +++ b/.planning/quick/260917-jdf-bildmarke-ganzes-t-uebernimmt-die-akzent/260917-jdf-SUMMARY.md @@ -136,3 +136,9 @@ None - no external service configuration required. ## Self-Check: PASSED All 5 claimed files found on disk; both task commits (ecff144, 29db4c0) found in git history. + +## Nachweis durch Orchestrator (2026-09-17, Playwright gegen lokale Container) + +- Akzentfarbe `#0057b8` gespeichert: gedrehte Kachel `#0057b8`, die vier Kacheln `#284a7b` (wie in der Referenzrechnung vorhergesagt). +- „Zuruecksetzen": Kacheln `#9a903f`, gedrehte Kachel `#fbe405` — Standardbild unveraendert (Nebenbefund `--primary` aus globals.css bestaetigt). +- Anmeldeseite: dieselben Werte, kein Unterschied zu vorher. diff --git a/.planning/quick/260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d/260917-jdh-SUMMARY.md b/.planning/quick/260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d/260917-jdh-SUMMARY.md index a3b12f1..29a805c 100644 --- a/.planning/quick/260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d/260917-jdh-SUMMARY.md +++ b/.planning/quick/260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d/260917-jdh-SUMMARY.md @@ -162,3 +162,10 @@ Zusaetzlicher Beobachtungspunkt aus dem Plan-Checker-Hinweis: Das Verhalten von ## Self-Check: PASSED All 6 claimed source/doc files found on disk; both task commits (8c4aaa5, e7633e1) found in git history. + +## Nachweis durch Orchestrator — Ergebnis + +- Fall 1 (Lauf 382, Push `5a444ec`): Stempel `1.2.0-29c132e…`, kein Zwischenspeicher → gebaut, `Cache saved with key: desktop-dist-stamp-…`. +- Fall 3 (Lauf 383, Push `7479cb4` mit Desktop-Aenderung): Stempel `1.2.0-7004b5b…` neu → Neubau; Lauf 384 (`a6d1a64`) ebenso. +- Fall 2 (Docs-Push ohne Desktop-Aenderung ueberspringt): siehe Aktenstand-Push nach diesem Eintrag (Lauf 385). +- `cache/save` bei bereits belegtem Schluessel: bisher nicht aufgetreten (jeder Lauf hatte einen neuen Stempel) — weiter beobachten. diff --git a/.planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.md b/.planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.md index 1dab3b5..b353cf6 100644 --- a/.planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.md +++ b/.planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.md @@ -208,3 +208,11 @@ Alle automatisierten Verifikationen (Rust: fmt/check/clippy/test, Web: vitest/ty --- *Quick Task: 260917-jn2* *Completed: 2026-09-17* + +## Nachweis durch Orchestrator (2026-09-17, Windows-Test-VM 8233, Paket 1.2.0-beta.5a444ec) — erbracht + +- Tooltip „Tessera – alpha.tessera.ctl.de", Menuezeile „Verbunden mit alpha.tessera.ctl.de" (gesperrt). +- „Server-Adresse ändern…" oeffnet die Setup-Seite mit „Aktuell verbunden mit: https://alpha.tessera.ctl.de/", Adresse vorbelegt; „Abbrechen" fuehrt zur Server-Seite zurueck. +- Wechsel auf `http://192.168.13.11:3000`: Anmeldeseite des neuen Servers, Tooltip/Menuezeile sofort „192.168.13.11:3000", Versionspruefung lief neu — ohne Neustart. +- „Beenden": Fenster zu, Tray-Symbol weg, `tasklist` ohne `tessera-desktop.exe`. +- Befund (Altlast, durch 260917-kgc behoben): gegen einen aelteren Server (1.1.0) bot der Client „Version 1.1.0 herunterladen" an. diff --git a/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-PLAN.md b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-PLAN.md new file mode 100644 index 0000000..dc84dae --- /dev/null +++ b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-PLAN.md @@ -0,0 +1,312 @@ +--- +phase: quick-260917-kgc +plan: 01 +type: execute +wave: 1 +depends_on: [] +autonomous: true +requirements: [QUICK-260917-KGC] + +files_modified: + - apps/desktop/src-tauri/Cargo.toml + - apps/desktop/src-tauri/Cargo.lock + - apps/desktop/src-tauri/tauri.conf.json + - apps/desktop/src-tauri/src/lib.rs + - packages/shared/src/index.ts + - apps/api/src/desktop/desktop.service.ts + - apps/api/src/desktop/desktop.controller.ts + - apps/api/src/desktop/desktop.service.spec.ts + - .gitea/scripts/desktop-collect.sh + - .gitea/scripts/desktop-stamp.sh + - .gitea/workflows/ci.yml + - .gitignore + - docs/anleitung-anwender.md + - docs/anleitung-betrieb.md + - docs/anleitung-entwicklung.md + - docs/ci-cd-setup.md + - CHANGELOG.md + +estimate: + tokens: 95000 + raw_tokens: 95000 + tasks: 4 + confidence: low + +must_haves: + truths: + - "Client (lib.rs): `tauri-plugin-updater = \"2\"` und `semver = \"1\"` in Cargo.toml, `.plugin(tauri_plugin_updater::Builder::new().build())` in `run()`; keine JS-Nutzung, capabilities/default.json unveraendert. Cargo.lock loest `tauri-plugin-updater` auf 2.11.x auf (nicht 3.0.0-alpha), `semver` bleibt 1.0.28." + - "Client (tauri.conf.json): `bundle.createUpdaterArtifacts: true`; `plugins.updater.pubkey` = exakt der vom Orchestrator genannte Schluessel; `plugins.updater.windows.installMode: \"passive\"`; KEIN `endpoints`-Eintrag (Laufzeit), keine `dangerous*`-Schalter." + - "Client: reine, getestete Funktionen `is_update_newer(current: &Version, current_commit: &str, release: &Version) -> bool` (Basis groesser → true; kleiner → false; gleich → true genau dann, wenn `release.pre` die Form `beta.g` hat, `current_commit` nicht leer ist und `` != `current_commit`; gleiche Basis ohne Prerelease → false), `beta_commit(release: &Version) -> Option<&str>`, `update_endpoint(server: &str) -> Option` (Pfad `/api-proxy/desktop/update`, Query `target`/`arch`/`current` mit den Roh-Platzhaltern `{{target}}`/`{{arch}}`/`{{current_version}}` per `query_pairs_mut` und `base=`), `release_labels(current, release)` und `update_labels` mit den neuen Texten `Auf Version {v} aktualisieren` / `Auf Beta-Stand {sha7} aktualisieren`. `cargo fmt --check`, `cargo check`, `cargo clippy`, `cargo test --lib` in apps/desktop/src-tauri gruen (CARGO_BUILD_JOBS=4)." + - "Client: `spawn_version_check(app, server_url)` ruft `app.updater_builder().endpoints(vec![endpoint])?.timeout(15 s).version_comparator(is_update_newer …).build()?.check().await` statt `reqwest::get(/desktop/latest)`; `Some(update)` → Benachrichtigung, Menuetext aus `release_labels`, Eintrag aktiv, `update.timeout = Some(600 s)` und Ablage in `app.state::()`; `None` → Standardtext gesperrt; `Err(InsecureTransportProtocol)` (http-Server im Release-Bau) → Menuetext `Update nur über https möglich`, gesperrt, kein Absturz; andere Fehler still wie bisher. Der Eintrag wird zu Beginn jeder Pruefung zurueckgesetzt und der abgelegte Update-Stand geleert." + - "Client: Tray-Klick `update` nimmt den abgelegten Stand (`take()`), sperrt den Eintrag, zeigt die Benachrichtigung „Update wird heruntergeladen…“, ruft in `tauri::async_runtime::spawn` `update.download_and_install(on_chunk, on_finish)` (Fortschritt als Menuetext `Lädt … {n} %`, nach dem Download `Wird installiert…`), danach `app.restart()` (Windows: nie erreicht, das Plugin beendet den Prozess selbst; Linux: AppImage ersetzt → Neustart). Fehler → Menuetext/Stand zurueck, Eintrag wieder aktiv, Benachrichtigung „Update fehlgeschlagen: …“ und die Einstellungsseite `{server}/settings/general/desktop` oeffnet im Browser (bisheriges Verhalten als Rueckfall). Ist kein Stand abgelegt, oeffnet der Klick nur die Einstellungsseite." + - "API: `GET /desktop/update?target=&arch=¤t=&base=` (`@Public()`, im Controller VOR `download/:platform`): `base` ist Pflicht und muss ein reiner http/https-Origin sein (kein Pfad ausser `/`, keine Query, kein Fragment, keine Userinfo) — sonst 400; `target` nicht `windows`/`linux`, `arch` nicht `x86_64`, Manifest fehlt, Plattform-Eintrag fehlt, `signature` fehlt oder `updateVersion` fehlt/ungueltig → 204 ohne Body; sonst 200 `{ version: , pub_date: , url: \"/api-proxy/desktop/download/\", signature, notes: \"Tessera \" }`. Kein serverseitiger Abruf von `base`. `/desktop/latest` und `download/:platform` unveraendert. `pnpm --filter @tessera/api exec vitest run src/desktop` gruen (Bestand 11 Tests + neue), `pnpm --filter @tessera/api type-check` und `pnpm --filter @tessera/shared type-check` gruen." + - "Manifest/Bau: `desktop-collect.sh` schreibt `updateVersion` (`X.Y.Z` bei live/dev, `X.Y.Z-beta.g` bei beta) und je Plattform `files.

.signature` = Inhalt der `.sig`-Datei neben dem Bundle (`.sig`, eine Base64-Zeile); fehlt die `.sig`, bricht das Skript ab, wenn `TAURI_SIGNING_PRIVATE_KEY` gesetzt ist ODER der Kanal nicht `dev` ist (main/Tag), sonst Warnung und Feld weglassen. Dateinamen der Pakete bleiben (`Tessera-Setup-X.Y.Z[-beta.].exe`, `Tessera-X.Y.Z[-beta.].AppImage`), `.sig`-Dateien werden nicht kopiert. `desktop-stamp.sh check` liefert `reuse=false`, wenn `updateVersion` oder eine `files.

.signature` im gecachten Manifest fehlt." + - "CI: die beiden `tauri build`-Schritte im Job `desktop` tragen `env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}` und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}` — nur dort, kein Job-`env`, kein `--no-sign`; alle anderen Jobs/Schritte unveraendert (js-yaml-Tiefenvergleich gegen `git show HEAD:.gitea/workflows/ci.yml`). `.gitignore` bekommt `*.key`." + - "Doku: Anwenderhandbuch (Update per Klick im Infobereich, Ablauf Windows/Linux, https-Bedingung, einmaliger manueller Wechsel fuer bereits installierte 1.2.0-Clients); Betriebshandbuch Kap. 10 (Signierschluessel: Secret-Namen, Ablage `~/.tessera/desktop-updater/` auf dem Entwicklungsrechner, Sicherung, Verlust = alle Clients einmal manuell neu installieren, `--no-sign` nur lokal, Tempordner unter Windows, neuer Endpunkt in der Kontrollzeile); Entwicklungshandbuch (`tauri build --no-sign` lokal); ci-cd-setup.md (zwei neue Secrets, ASCII ohne Umlaute); CHANGELOG `## Unveröffentlicht` `### Neu` eine Zeile `Desktop-App: …`." + - "Vier Commits: `feat(desktop): …` (Task 1), `feat(api): …` (Task 2), `ci: …` (Task 3), `docs: …` (Task 4). Kein `git push`, keine `.planning/`-Commits, kein `tauri build`, kein Docker; `~/.tessera/desktop-updater/` wird nie gelesen oder ausgegeben." + artifacts: + - "apps/desktop/src-tauri/src/lib.rs — `is_update_newer`, `beta_commit`, `update_endpoint`, `release_labels`, `update_labels` (neue Texte), `PendingUpdate`, `check_for_update`, `spawn_update_install`, `open_download_page`, umgebautes `spawn_version_check`, Tray-Klick `update`, `mod tests` erweitert; `DesktopLatest`-Struct entfernt" + - "apps/desktop/src-tauri/Cargo.toml + Cargo.lock — `tauri-plugin-updater = \"2\"`, `semver = \"1\"`; tauri.conf.json — `createUpdaterArtifacts`, `plugins.updater`" + - "packages/shared/src/index.ts — `DesktopManifestFile.signature?`, `DesktopManifest.updateVersion?`, `DesktopUpdateResponse`" + - "apps/api/src/desktop/desktop.service.ts — `safeOrigin` (exportiert), `getUpdate`, `isValidManifestFileEntry` mit optionaler `signature`; desktop.controller.ts — `update()`; desktop.service.spec.ts — Tests 12-20" + - ".gitea/scripts/desktop-collect.sh — `.sig` → `signature`, `updateVersion`, Pflichtregel; desktop-stamp.sh — `check` verlangt `updateVersion` + `signature`; .gitea/workflows/ci.yml — Secrets an beiden Bau-Schritten; .gitignore — `*.key`" + - "docs/anleitung-anwender.md, docs/anleitung-betrieb.md, docs/anleitung-entwicklung.md, docs/ci-cd-setup.md, CHANGELOG.md" + key_links: + - "Vertrauenskette: CI signiert das Bundle mit dem privaten Schluessel (nur als Gitea-Secret) → `.sig` neben dem Bundle → `desktop-collect.sh` traegt den Inhalt als `files.

.signature` ins Manifest → API liefert `signature` + absolute `url` → Plugin laedt die Bytes, prueft die minisign-Signatur gegen `plugins.updater.pubkey` und installiert erst danach. Bricht ein Glied (kein `.sig`, falscher Schluessel, manipulierte Bytes), gibt es KEIN Update — nie eine Installation ohne gueltige Signatur." + - "Der `version_comparator` ersetzt den Standardvergleich vollstaendig (updater.rs Z. 576-579). Ohne ihn gilt SemVer `1.2.0-beta.gXXXX < 1.2.0` und ein Beta-Client saehe nie einen neueren Beta-Bau. Deshalb muss `updateVersion` fuer beta IMMER `X.Y.Z-beta.g` sein (Praefix `g`: ein rein numerischer SHA mit fuehrender Null waere kein gueltiger SemVer-Identifier — `1.2.0-beta.0123456` scheitert beim Parsen, `check()` liefert Err)." + - "`UpdaterBuilder::timeout` wird in `Update.timeout` uebernommen und beim Download als reqwest-GESAMT-Timeout angewandt (updater.rs Z. 504-506 und 698-700). 15 s fuer die Pruefung sind richtig, wuerden aber den ~100-MB-Download abbrechen — darum `update.timeout = Some(Duration::from_secs(600))` (pub-Feld) vor der Ablage." + - "Die Endpunkt-URL traegt die Platzhalter als Query-Werte; `query_pairs_mut` kodiert sie zu `%7B%7Btarget%7D%7D` — das Plugin ersetzt BEIDE Schreibweisen (updater.rs Z. 476-486). `base` wird dadurch sauber prozent-kodiert; die API nimmt nur den Origin." + - "NestJS: `@Res({ passthrough: true })` + `res.status(204)` + `return undefined` ergibt 204 ohne Body, weil Nest den Standardstatus VOR dem Handler setzt (`setStatus(res, 200)`) und `reply()` ohne eigenen Statuscode aufruft (router-execution-context.js Z. 44-48 und createHandleResponseFn ohne 4. Argument) — der Handler-Status gewinnt." + - "Neustart nach dem Update: Windows — `Update::install` startet den NSIS-Installer (`/P /UPDATE /R /ARGS`) und beendet den Prozess per `std::process::exit(0)` aus dem Tokio-Thread; der Run-Handler (`ExitRequested { code: None }` → `prevent_exit`) wird nicht durchlaufen. Linux — `app.restart()` aus dem Nebenthread setzt `restart_on_exit` und `request_exit(RESTART_EXIT_CODE)` = `code: Some(i32::MAX)` (tauri app.rs Z. 588-611), das der Handler durchlaesst. Run-Handler und `on_window_event` bleiben unangetastet." + - "Skip-Mechanismus (jdh): das gecachte Manifest traegt `signature`/`updateVersion` mit; ein Cache-Stand VOR dieser Aenderung hat beides nicht → `check` gibt `reuse=false` → Neubau. Ein Schluesselwechsel (`pubkey` in tauri.conf.json unter `apps/desktop`) aendert den Stempel automatisch." +--- + + +Der Desktop-Client aktualisiert sich selbst: Die Versionspruefung beim Start (und nach einem Serverwechsel) fragt ueber `tauri-plugin-updater` den neuen API-Endpunkt `GET /desktop/update`, und ein Klick auf den Tray-Eintrag laedt das signierte Paket, prueft die Signatur, installiert es (Windows: NSIS passiv, Linux: AppImage an Ort und Stelle) und startet die App neu. Die Pakete werden im CI mit dem minisign-Schluessel signiert; der oeffentliche Schluessel steckt im Client. + +1. **Client (Vorgaben 1, 2, 3, 6, 7).** Plugin einbauen, `pubkey`/`installMode`/`createUpdaterArtifacts` in tauri.conf.json, eigener `version_comparator` (`is_update_newer`), Endpunkt zur Laufzeit aus `stored_server_url`, Tray-Eintrag `update` wird zum Installieren (`Auf Version {v} aktualisieren` / `Auf Beta-Stand {sha7} aktualisieren`), Download mit Fortschritt im Menuetext, Neustart, Fehler-Rueckfall auf die Einstellungsseite im Browser. `http://`-Server: Plugin lehnt im Release ab → Menuetext `Update nur über https möglich`, gesperrt, kein Absturz. +2. **API (Vorgabe 4).** `GET /desktop/update` mit `base`-Origin-Validierung (400), 204-Faellen und dynamischem Updater-Format; `signature`/`updateVersion` als optionale Manifest-Felder in `@tessera/shared`. +3. **Manifest/Bau/CI (Vorgabe 5).** `desktop-collect.sh` sammelt die `.sig` als `signature`, schreibt `updateVersion`; `desktop-stamp.sh check` verlangt beides; Secrets nur an den zwei `tauri build`-Schritten; `*.key` in `.gitignore`. +4. **Doku (Vorgabe 8).** Anwender-, Betriebs-, Entwicklungshandbuch, ci-cd-setup.md, CHANGELOG. + +**Entscheidung zur Vergleichsregel (Vorgabe 3, „einfachste tragfaehige Regel“):** gleiche Basis X.Y.Z und Server-Version OHNE Prerelease (Live-Freigabe) → KEIN Update. Begruendung: (a) Das ist exakt die heutige Regel in `spawn_version_check` (`version_changed || (channel == "beta" && commit != app_commit)`) — Verhalten bleibt fuer alle Live-Clients identisch, ein Live-Client 1.2.0 kann sich nie selbst 1.2.0 anbieten (keine Update-Schleife). (b) Die Variante „JA, wenn der Client selbst ein Beta-Stand ist“ ist client-seitig nicht entscheidbar: der Client kennt nur `CARGO_PKG_VERSION` (immer reines X.Y.Z, D-07) und `APP_COMMIT`, aber nicht den Tag-Commit der Live-Freigabe; die Live-Antwort traegt per Vorgabe 4 die reine `X.Y.Z` ohne Commit. Jede Loesung braeuchte ein zusaetzliches Feld (Build-Metadaten `+g` oder maschinenlesbare `notes`) und damit eine Aenderung am Endpunkt-Vertrag. (c) Der einzige betroffene Fall — ein Beta-Client wird auf einen Live-Server mit derselben Basisversion umgestellt — bekommt das Update spaetestens mit der naechsten Freigabe X.Y.Z+1; bis dahin bleibt der Browser-Download. Die Regel steht als Kommentar an `is_update_newer`. Leerer `APP_COMMIT` (Quell-Tarball ohne git) → nur Basisvergleich, wie heute. + +**Entscheidung `http://`-Meldung (Vorgabe 2/6):** keine Benachrichtigung bei jedem Start (das waere fuer Dauer-`http`-Server eine Nervmeldung), sondern der gesperrte Menuetext `Update nur über https möglich` — dauerhaft sichtbar, sobald der Nutzer das Menue oeffnet. Im Debug-Bau (`tauri dev`) erlaubt das Plugin `http` mit Warnung; dort darf `download_and_install` NICHT ausgeloest werden (ohne `APPIMAGE` wuerde es die Binary in `target/` ueberschreiben) — Handbuch-Hinweis, keine Code-Sperre noetig, weil CI nur Release baut. + +Task-Zuschnitt: Task 1 ist der Tracer (Client-Kette Pruefung → Anzeige → Klick → Download → Signatur → Installation → Neustart), Task 2 die API, Task 3 Skripte + CI, Task 4 Doku. Vier Commits `feat(desktop)`/`feat(api)`/`ci`/`docs` (Vorgabe 10). Der echte Beweis — CI baut signierte Pakete, ein alter Client auf der Windows-VM aktualisiert sich per Tray-Klick — ist „Nachweis durch Orchestrator“ (Vorgabe 9) und wird im SUMMARY als offen gefuehrt. + +Purpose: Der Nutzer aktualisiert die Desktop-App mit einem Klick im Infobereich statt Download, Installer und Neustart von Hand — und jedes installierte Paket ist nachweislich vom eigenen CI signiert. +Output: lib.rs mit Updater-Kette + Tests; Cargo.toml/Cargo.lock/tauri.conf.json; `@tessera/shared`-Typen; API-Endpunkt + Tests; collect/stamp-Skripte; ci.yml; .gitignore; vier Handbuch-/Doku-Dateien; CHANGELOG; vier 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/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-RESEARCH.md +@/home/vicolab/projects/tessera-ctl/.planning/quick/260917-jn2-desktop-client-aktuelle-server-adresse-s/260917-jn2-SUMMARY.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/desktop/src-tauri/Cargo.toml +@/home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri/build.rs +@/home/vicolab/projects/tessera-ctl/apps/api/src/desktop/desktop.service.ts +@/home/vicolab/projects/tessera-ctl/apps/api/src/desktop/desktop.controller.ts +@/home/vicolab/projects/tessera-ctl/apps/api/src/desktop/desktop.service.spec.ts +@/home/vicolab/projects/tessera-ctl/packages/shared/src/index.ts +@/home/vicolab/projects/tessera-ctl/.gitea/scripts/desktop-collect.sh +@/home/vicolab/projects/tessera-ctl/.gitea/scripts/desktop-stamp.sh +@/home/vicolab/projects/tessera-ctl/.gitea/workflows/ci.yml + + + + + + Task 1: Client — Updater-Plugin, Config, Vergleichsregel, Endpunkt-URL, Tray-Fluss Pruefen → Herunterladen → Installieren → Neustart, Tests + apps/desktop/src-tauri/Cargo.toml, apps/desktop/src-tauri/Cargo.lock, apps/desktop/src-tauri/tauri.conf.json, apps/desktop/src-tauri/src/lib.rs + + - apps/desktop/src-tauri/src/lib.rs komplett (599 Zeilen, Stand nach jn2): Z. 12-28 `UPDATE_ITEM_DEFAULT`, `VersionResponse`, `DesktopLatest`; Z. 30-78 reine Helfer inkl. `update_labels`; Z. 142-216 `stored_server_url`, `TrayItems`, `apply_server`, `spawn_version_check` (der reqwest-Block Z. 188-215 wird ersetzt); Z. 291-306 `run()` mit Plugins/Handlern; Z. 385-393 Menue-Zweig `"update"` (Browser-Fallback, wird zum Rueckfall); Z. 440-461 `on_window_event` + Run-Handler (NICHT anfassen); Z. 464-599 `mod tests` (Stil: deutsche snake_case-Namen) + - apps/desktop/src-tauri/build.rs (setzt `APP_COMMIT`, 7 Zeichen, aus `TESSERA_COMMIT` oder `git rev-parse --short=7`; die Vorgabe nennt `env!("TESSERA_COMMIT")` — im Code heisst die Variable `APP_COMMIT`, das ist die zu nutzende) + - apps/desktop/src-tauri/tauri.conf.json (Z. 28-51 `bundle`; es gibt noch keinen `plugins`-Block) und Cargo.toml (Z. 13-22) + - ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-updater-2.11.0/src/updater.rs: Z. 88-110 `RemoteRelease { version: semver::Version, notes, pub_date, data }`, Z. 197 `current_version: app.package_info().version` (semver::Version), Z. 211-215 `version_comparator bool + Send + Sync + 'static>`, Z. 224-231 `endpoints(Vec) -> Result` (https-Zwang im Release), Z. 264 `timeout`, Z. 365-388 `build()`, Z. 432 `check() -> Result>`, Z. 476-486 Platzhalter-Ersetzung (beide Schreibweisen), Z. 504-506 + 698-700 Timeout = Gesamt-Timeout auch fuer den Download, Z. 576-579 Comparator-Aufruf, Z. 642-676 `Update` (pub-Felder `version: String`, `timeout: Option`, `download_url`), Z. 680 `download), D: FnOnce()>`, Z. 752-768 `download_and_install(&self, on_chunk, on_download_finish)` (Windows: beendet den Prozess; Linux: Neustart selbst ausloesen) + - …/tauri-plugin-updater-2.11.0/src/lib.rs Z. 41 `UpdaterExt` (`updater_builder()` fuer jeden Manager), Z. 217 `Builder::build()`; src/error.rs Z. 91 `Error::InsecureTransportProtocol`; src/config.rs Z. 127-137 (`pubkey: String` Pflicht, `endpoints` default leer, `windows.installMode` camelCase) + - …/tauri-2.11.3/src/app.rs Z. 588-611 (`restart()` aus dem Nebenthread → `request_exit(RESTART_EXIT_CODE)`), …/semver-1.0.28/src/lib.rs Z. 309 `Prerelease`, Z. 544 `as_str()` + - .planning/quick/260917-kgc-…/260917-kgc-RESEARCH.md Abschnitte 1, 2, 4, 5, 7, 8 und „Common Pitfalls“ + + + Neue/geaenderte Tests in `#[cfg(test)] mod tests` (deutsche snake_case-Namen wie im Bestand; `use semver::Version;` im Testmodul, Hilfsfunktion `v(s: &str) -> Version { Version::parse(s).unwrap() }`): + - `is_update_newer(&v("1.2.0"), "abc1234", &v("1.3.0"))` → true (hoehere Basis, Live) + - `is_update_newer(&v("1.2.0"), "abc1234", &v("1.3.0-beta.gdef5678"))` → true (hoehere Basis, Beta) + - `is_update_newer(&v("1.3.0"), "abc1234", &v("1.2.0-beta.g9999999"))` → false (niedrigere Basis) + - `is_update_newer(&v("1.2.0"), "abc1234", &v("1.2.0-beta.gdef5678"))` → true (gleiche Basis, anderer Beta-Stempel) + - `is_update_newer(&v("1.2.0"), "abc1234", &v("1.2.0-beta.gabc1234"))` → false (gleicher Stempel) + - `is_update_newer(&v("1.2.0"), "abc1234", &v("1.2.0"))` → false (gleiche Basis, Live ohne Prerelease — siehe Entscheidung im Objective) + - `is_update_newer(&v("1.2.0"), "", &v("1.2.0-beta.gdef5678"))` → false (leerer Client-Commit → nur Basisvergleich) + - `is_update_newer(&v("1.2.0"), "abc1234", &v("1.2.0-rc.1"))` → false (fremdes Prerelease-Format) + - `Version::parse("1.2.0-beta.g0123456").is_ok()` → true und `beta_commit(&v("1.2.0-beta.g0123456"))` → `Some("0123456")` (Praefix `g` macht den fuehrende-Null-SHA gueltig); `beta_commit(&v("1.2.0"))` → None; `beta_commit(&v("1.2.0-rc.1"))` → None + - `update_endpoint("https://tessera.ctl.de/").unwrap().as_str()` → `"https://tessera.ctl.de/api-proxy/desktop/update?target=%7B%7Btarget%7D%7D&arch=%7B%7Barch%7D%7D¤t=%7B%7Bcurrent_version%7D%7D&base=https%3A%2F%2Ftessera.ctl.de"` (Reihenfolge target, arch, current, base; `base` ohne Schlussstrich); `update_endpoint("kein url")` → None + - `update_labels(true, "1.2.0", "abc1234")` → (`"Auf Version 1.2.0 aktualisieren"`, `"Neue Version 1.2.0 verfügbar – Aktualisieren über das Symbol im Infobereich."`); `update_labels(false, "1.1.0", "abc1234")` → (`"Auf Beta-Stand abc1234 aktualisieren"`, `"Neuer Beta-Stand abc1234 verfügbar – Aktualisieren über das Symbol im Infobereich."`) — die beiden bestehenden `update_labels_*`-Tests werden auf diese Texte umgestellt + - `release_labels(&v("1.2.0"), &v("1.3.0-beta.gdef5678")).0` → `"Auf Version 1.3.0 aktualisieren"` (neue Basis wird als Version genannt, auch bei Beta); `release_labels(&v("1.2.0"), &v("1.2.0-beta.gdef5678")).0` → `"Auf Beta-Stand def5678 aktualisieren"` + - Alle 18 Bestandstests (ausser den zwei umgestellten `update_labels_*`) bleiben unveraendert gruen. + + + Zuerst die Tests aus `` in `mod tests` schreiben und `CARGO_BUILD_JOBS=4 cargo test --lib` rot sehen (Compile-Fehler wegen fehlender Funktionen sind der erwartete RED-Zustand). Dann: + + 1. **Cargo.toml** (Vorgabe 1): unter `[dependencies]` `tauri-plugin-updater = "2"` und `semver = "1"` ergaenzen (das Plugin re-exportiert `semver` nicht, `is_update_newer` braucht `semver::Version` als eigenen Typ; `semver 1.0.28` liegt bereits transitiv im Cargo.lock). `cargo check` aktualisiert Cargo.lock; danach pruefen, dass `tauri-plugin-updater` auf `2.11.x` aufgeloest ist (nicht `3.0.0-alpha`) — die Crates liegen laut Recherche schon unter `~/.cargo/registry`. Das Projekt-`reqwest` bleibt auf `0.12` (Pitfall 8: nicht anheben, `aws-lc-rs` waere ein Cross-Bau-Risiko). + 2. **tauri.conf.json** (Vorgabe 1): in `bundle` `"createUpdaterArtifacts": true` ergaenzen; neuer Top-Level-Block `"plugins": { "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxNDIxMTZDMUFGRkJCRjEKUldUeHUvOGFiQkZDc2V0cnJZcmJVSURraGJHcE5TODVDeUp3UEVpM0xOc3I5TUZrNW5qZUw0d2gK", "windows": { "installMode": "passive" } } }` — exakt dieser Schluessel, kein `endpoints`, keine `dangerous*`-Schalter. `version` (1.1.0) und alles andere unveraendert (desktop-version.sh schreibt die Version im CI). + 3. **lib.rs — Imports/Konstanten**: `use semver::Version;` und `use tauri_plugin_updater::{Update, UpdaterExt};`. `UPDATE_ITEM_DEFAULT` auf `"Update installieren"` setzen und den Doc-Kommentar anpassen (der Eintrag installiert kuenftig, er oeffnet nicht mehr den Browser). Neue Konstante `UPDATE_ITEM_INSECURE: &str = "Update nur über https möglich"`. Struct `DesktopLatest` ersatzlos entfernen (sonst `dead_code`-Warnung; `VersionResponse` bleibt fuer `check_server`). + 4. **lib.rs — reine Helfer** (Doc-Kommentare deutsch mit ae/oe/ue wie im Bestand): + - `fn beta_commit(release: &Version) -> Option<&str>`: `release.pre.as_str().strip_prefix("beta.g")`, nur nicht-leere Reste. Doc: Beta-Stempel `beta.g`; das `g` ist Pflicht, weil ein rein numerischer SHA mit fuehrender Null (`0123456`) kein gueltiger SemVer-Identifier waere und `check()` dann mit Err endet (Recherche Pitfall 2). + - `fn is_update_newer(current: &Version, current_commit: &str, release: &Version) -> bool` (Vorgabe 3): Basistupel `(major, minor, patch)` vergleichen — groesser → true, kleiner → false; gleich → `match beta_commit(release) { Some(sha) => !current_commit.is_empty() && sha != current_commit, None => false }`. Doc-Kommentar mit der Regel und ihrer Begruendung (Text aus dem Objective, Kurzform: heutige Regel beibehalten; Beta-Client auf Live-Server gleicher Basis ist client-seitig nicht entscheidbar, bekommt das Update mit der naechsten Freigabe; leerer Commit → nur Basisvergleich; ersetzt den Standardvergleich vollstaendig, updater.rs Z. 576-579). + - `fn update_endpoint(server: &str) -> Option`: `tauri::Url::parse(&api_url(server, "/desktop/update")).ok()?`, dann `query_pairs_mut()` mit `append_pair("target", "{{target}}")`, `("arch", "{{arch}}")`, `("current", "{{current_version}}")`, `("base", server.trim_end_matches('/'))`. Doc: Platzhalter stehen als Rohtext im Wert, `query_pairs_mut` kodiert sie zu `%7B%7B…%7D%7D`, das Plugin ersetzt beide Schreibweisen (updater.rs Z. 476-486); `base` ist der Origin fuer die absolute Rueckgabe-URL (das Plugin verlangt `url::Url`, relative Pfade scheitern); `api_url` bleibt die einzige Stelle mit dem `/api-proxy`-Praefix. + - `update_labels(version_changed: bool, version: &str, commit: &str)` behaelt die Signatur, Texte neu: `Auf Version {version} aktualisieren` / `Neue Version {version} verfügbar – Aktualisieren über das Symbol im Infobereich.` bzw. `Auf Beta-Stand {commit} aktualisieren` / `Neuer Beta-Stand {commit} verfügbar – Aktualisieren über das Symbol im Infobereich.` (Gedankenstrich U+2013 wie bisher). Doc-Kommentar anpassen (Vorgabe 6). + - `fn release_labels(current: &Version, release: &Version) -> (String, String)`: `version_changed` = Basistupel verschieden; `version` = `format!("{}.{}.{}", release.major, release.minor, release.patch)`; `commit` = `beta_commit(release).unwrap_or("")`; Rueckgabe `update_labels(version_changed, &version, commit)`. + 5. **lib.rs — State und Pruefung**: `struct PendingUpdate(std::sync::Mutex>)` (gefundenes Update + zugehoeriger Menuetext; `Update` ist `Clone + Send + Sync`, `impl Resource`), in `setup` direkt nach `app.manage(TrayItems …)` per `app.manage(PendingUpdate(Mutex::new(None)))` anlegen. `async fn check_for_update(app: &AppHandle, endpoint: tauri::Url) -> tauri_plugin_updater::Result>`: `let current_commit = env!("APP_COMMIT");` dann `app.updater_builder().endpoints(vec![endpoint])?.timeout(Duration::from_secs(15)).version_comparator(move |current, release| is_update_newer(¤t, current_commit, &release.version)).build()?.check().await`. `spawn_version_check(app, server_url)` umbauen: Reset des Eintrags wie bisher (`UPDATE_ITEM_DEFAULT`, gesperrt) PLUS `*app.state::().0.lock()… = None` (Lock-Fehler ignorieren, `if let Ok(mut g)`); `let Some(endpoint) = update_endpoint(&server_url) else { return; };` (gespeicherte Adressen sind immer parsebar — reine Absicherung); im `tauri::async_runtime::spawn`: `match check_for_update(&app, endpoint).await { Ok(Some(mut update)) => { let (menu_text, body) = Version::parse(&update.version).map(|r| release_labels(¤t, &r)).unwrap_or_else(|_| update_labels(true, &update.version, "")); update.timeout = Some(Duration::from_secs(600)); Benachrichtigung (Titel `Tessera-Update`, `body`) wie bisher; `update_item.set_text(&menu_text)`, `set_enabled(true)`; Ablage `Some((update, menu_text))` im State }, Ok(None) => {}, Err(tauri_plugin_updater::Error::InsecureTransportProtocol) => { update_item.set_text(UPDATE_ITEM_INSECURE) — gesperrt lassen }, Err(_) => {} }` — `current` = `Version::parse(env!("CARGO_PKG_VERSION"))` (immer reines X.Y.Z per D-07; `expect` mit Meldung ist hier vertretbar, weil Cargo die Version garantiert). Kommentar am Timeout: 15 s gelten fuer die Pruefung; das Plugin uebernimmt denselben Wert als Gesamt-Timeout des Downloads (updater.rs Z. 698-700), darum vor der Ablage `update.timeout = Some(600 s)` — sonst braeche der ~100-MB-Download ab. Kommentar am `InsecureTransportProtocol`-Zweig: Release-Bau verweigert `http` (config.rs `validate_endpoints`), Debug-Bau warnt nur; keine `dangerousInsecureTransportProtocol`-Freigabe (T-KGC-08). Der bisherige Kommentar zum Beta-Kanal (WR-02) wandert sinngemaess an `is_update_newer`. + 6. **lib.rs — Installation** (Vorgabe 6/7): `fn open_download_page(app: &AppHandle)` = der bisherige Inhalt des `"update"`-Zweigs (`stored_server_url` → `{server}/settings/general/desktop` → `app.opener().open_url(target, None::<&str>)`), ohne `desktop=1` wie bisher. `fn spawn_update_install(app: AppHandle, update: Update, menu_text: String)`: `let item = app.state::().update.clone(); item.set_enabled(false); item.set_text("Update wird heruntergeladen…");` Benachrichtigung „Update wird heruntergeladen…“; dann `tauri::async_runtime::spawn(async move { … })` mit: Fortschritts-Closure (`FnMut(usize, Option)`, Zaehler `received: u64`, `last_percent: Option`; bei bekanntem `total > 0` Prozent berechnen und nur bei Aenderung `item_progress.set_text(format!("Lädt … {} %", percent))`), Abschluss-Closure (`FnOnce`, `item_finish.set_text("Wird installiert…")`), `let result = update.download_and_install(on_chunk, on_finish).await;` — `Ok(())` → `app.restart()` (Kommentar: Windows nie erreicht — das Plugin startet den Installer mit `/P /UPDATE /R /ARGS` und ruft `std::process::exit(0)`, der Run-Handler wird dabei nicht durchlaufen; Linux — AppImage ist ersetzt, `restart()` aus dem Nebenthread laeuft ueber `RESTART_EXIT_CODE` = `code: Some(..)` am `prevent_exit` vorbei); `Err(e)` → Menuetext zurueck auf `menu_text`, Stand zurueck in `PendingUpdate` (`Some((update, menu_text.clone()))`), `set_enabled(true)`, Benachrichtigung Titel `Tessera-Update`, Body `format!("Update fehlgeschlagen: {}. Die Download-Seite wird im Browser geöffnet.", e)`, dann `open_download_page(&app)`. Menue-Zweig `"update"`: `let pending = app.state::().0.lock().ok().and_then(|mut g| g.take()); match pending { Some((update, menu_text)) => spawn_update_install(app.clone(), update, menu_text), None => open_download_page(app) }`. Kommentar im Zweig: `take()` verhindert Doppelklick-Downloads, der gesperrte Eintrag ebenso; ohne abgelegten Stand bleibt der Browser-Weg. Menue-Kommentar in `setup` (Z. 318-323) auf „Update installieren“ anpassen. + 7. **`run()`**: `.plugin(tauri_plugin_updater::Builder::new().build())` nach `tauri_plugin_opener::init()`. `on_window_event`, Run-Handler, `generate_handler!`, Commands, `apply_server`, `TrayItems`, `stored_server_url`: unveraendert (Vorgabe 7 — keine Aenderung an der `prevent_exit`-Logik). `capabilities/default.json` bleibt unangetastet (Rust-Aufrufe laufen am ACL vorbei; kein `updater:default` noetig). + 8. `CARGO_BUILD_JOBS=4 cargo fmt` anwenden; erster `cargo check` laedt Plugin + reqwest 0.13 + rustls/ring und dauert mehrere Minuten (Timeout grosszuegig setzen). Kein `tauri build`, kein `tauri dev`, kein Docker. `~/.tessera/desktop-updater/` nie lesen. + 9. Commit nur der vier Task-Dateien: `git add apps/desktop/src-tauri/Cargo.toml apps/desktop/src-tauri/Cargo.lock apps/desktop/src-tauri/tauri.conf.json apps/desktop/src-tauri/src/lib.rs` — nie `git add -A` (parallele Arbeiten im selben Baum). + + + 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 && command grep -q '^tauri-plugin-updater = "2"' Cargo.toml && command grep -q '^semver = "1"' Cargo.toml && command grep -q '^reqwest = { version = "0.12"' Cargo.toml && command grep -A1 '^name = "tauri-plugin-updater"' Cargo.lock | command grep -q '^version = "2\.' && jq -e '.bundle.createUpdaterArtifacts == true and .plugins.updater.pubkey == "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxNDIxMTZDMUFGRkJCRjEKUldUeHUvOGFiQkZDc2V0cnJZcmJVSURraGJHcE5TODVDeUp3UEVpM0xOc3I5TUZrNW5qZUw0d2gK" and .plugins.updater.windows.installMode == "passive" and ((.plugins.updater | has("endpoints")) | not) and ((.plugins.updater | keys | map(startswith("dangerous")) | any) | not) and .version == "1.1.0"' tauri.conf.json >/dev/null && command grep -q 'tauri_plugin_updater::Builder::new().build()' src/lib.rs && command grep -q 'fn is_update_newer(current: &Version, current_commit: &str, release: &Version) -> bool' src/lib.rs && command grep -q 'fn beta_commit(' src/lib.rs && command grep -q 'fn update_endpoint(' src/lib.rs && command grep -q 'fn release_labels(' src/lib.rs && command grep -q 'fn spawn_update_install(' src/lib.rs && command grep -q 'fn open_download_page(' src/lib.rs && command grep -q 'struct PendingUpdate(' src/lib.rs && command grep -q 'download_and_install(' src/lib.rs && command grep -q 'version_comparator(' src/lib.rs && command grep -q 'Duration::from_secs(600)' src/lib.rs && command grep -q 'InsecureTransportProtocol' src/lib.rs && command grep -q 'env!("APP_COMMIT")' src/lib.rs && ! command grep -q 'struct DesktopLatest' src/lib.rs && ! command grep -q '"/desktop/latest"' src/lib.rs && command grep -q 'code: None, api' src/lib.rs && git -C /home/vicolab/projects/tessera-ctl diff --quiet -- apps/desktop/src-tauri/capabilities/default.json apps/desktop/src/setup.html && echo CLIENT-OK + + Plugin eingebaut, `pubkey`/`installMode`/`createUpdaterArtifacts` gesetzt; `is_update_newer`, `beta_commit`, `update_endpoint`, `release_labels`, `update_labels` rein und getestet (mindestens 12 neue Tests, 16 unveraenderte Bestandstests, 2 umgestellte); Versionspruefung laeuft ueber `check()` mit eigenem Comparator, http-Server ergibt den gesperrten Text `Update nur über https möglich`; Tray-Klick laedt mit Fortschritt, installiert, startet neu, faellt bei Fehler auf die Browser-Seite zurueck; fmt/check/clippy/test gruen; Cargo.lock auf 2.11.x; Commit `feat(desktop): Update in der App — Herunterladen, Installieren und Neustart per Klick im Infobereich (tauri-plugin-updater)`. Der Nachweis am Bildschirm (Windows-VM: Tray-Text, Klick, passiver Installer, Neustart auf neuem Stand; SmartScreen-Verhalten notieren) folgt durch den Orchestrator mit dem CI-Paket — im SUMMARY als offen fuehren. + + + + Task 2: API — `GET /desktop/update` (dynamisches Updater-Format, `base`-Origin-Validierung, 204-Faelle), Manifest-Felder `signature`/`updateVersion` in `@tessera/shared`, Spec-Tests + packages/shared/src/index.ts, apps/api/src/desktop/desktop.service.ts, apps/api/src/desktop/desktop.controller.ts, apps/api/src/desktop/desktop.service.spec.ts + + - apps/api/src/desktop/desktop.service.ts komplett (176 Zeilen): Z. 16-40 `PLATFORMS`, `SHA256_HEX_RE`, `isValidManifestFileEntry`; Z. 64-95 `getManifest()`; Z. 102-121 `getLatest()` (unveraendert lassen, spreadet den Manifest-Eintrag — ein optionales `signature` landet dort mit, ist oeffentlich und unkritisch); Z. 128-175 `getPackage()` (unveraendert) + - apps/api/src/desktop/desktop.controller.ts (37 Zeilen): `@Inject(DesktopService)`-Begruendung Z. 8-13 (Vitest/esbuild), `@Public()` an beiden Routen + - apps/api/src/desktop/desktop.service.spec.ts komplett (218 Zeilen): HTTP-Durchstich ueber `NestFactory.create(DesktopModule)` + `fetch`, `writeManifest()` Z. 37-48, `afterEach` stellt das Standard-Manifest wieder her, Test 11 prueft `IS_PUBLIC_KEY` + - packages/shared/src/index.ts Z. 22-53 (Desktop-Typen) + - Recherche Abschnitt 2 (Antwortformat: `version` SemVer Pflicht, `url` absolut Pflicht, `signature` Pflicht, `pub_date` RFC 3339 optional, `notes` optional; 204 → `Ok(None)`) und Abschnitt 6 („Neuer Endpunkt“) + - Nest-Statuslogik fuer 204 (bereits geprueft, siehe key_links): `@Res({ passthrough: true })` + `res.status(204)` + `return undefined` → 204 ohne Body + + + desktop.service.spec.ts — `writeManifest(files, head = {})` erweitern: `files`-Eintraege duerfen `signature?: string` tragen; `head` ueberschreibt `version`/`channel`/`commit`/`buildTime`/`updateVersion` (Standard wie bisher: `1.1.0`/`dev`/`abc1234`/`2026-09-16T00:00:00Z`, ohne `updateVersion`). Konstante `ORIGIN = 'https://tessera.example.com'`, `SIG = 'dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZQo='` (beliebige Base64-Zeile). Neue Tests (Nummern fortlaufend, deutsch wie Bestand): + - Test 12 (update, beta, signiert): Manifest `{ linux: { name, size, sha256, signature: SIG } }`, head `{ channel: 'beta', updateVersion: '1.1.0-beta.gabc1234' }`; `GET /desktop/update?target=linux&arch=x86_64¤t=1.1.0&base=${ORIGIN}` → 200, `content-type` enthaelt `application/json`, Body `toEqual({ version: '1.1.0-beta.gabc1234', pub_date: '2026-09-16T00:00:00Z', url: `${ORIGIN}/api-proxy/desktop/download/linux`, signature: SIG, notes: 'Tessera 1.1.0-beta.gabc1234' })` + - Test 13 (update, live): head `{ channel: 'live', updateVersion: '1.1.0' }` → 200, `version === '1.1.0'`, `notes === 'Tessera 1.1.0'` + - Test 14 (base mit Schlussstrich): `base=${ORIGIN}/` → 200 und `url === `${ORIGIN}/api-proxy/desktop/download/linux`` (Origin ohne Schlussstrich) + - Test 15 (ohne Signatur): Standard-Manifest (keine `signature`, kein `updateVersion`) → 204, `await res.text()` ist `''` + - Test 16 (updateVersion fehlt trotz Signatur): `signature: SIG`, kein `updateVersion` → 204; (ungueltige Form) `updateVersion: '1.1.0-beta.abc1234'` (ohne `g`) → 204 + - Test 17 (Plattform/Architektur): signiertes Manifest; `target=darwin` → 204; `target=windows` (kein windows-Eintrag) → 204; `arch=aarch64` → 204; `target` fehlt → 204 + - Test 18 (base-Validierung, HTTP): jeweils 400 fuer `base` fehlt, `base=ftp://host`, `base=https://user:pw@host`, `base=https://host/pfad`, `base=https://host/?x=1`, `base=https://host/#f`, `base=kein url` + - Test 19 (`safeOrigin` direkt): `safeOrigin('https://tessera.example.com')` → `'https://tessera.example.com'`; `safeOrigin('http://localhost:3000/')` → `'http://localhost:3000'`; `safeOrigin('HTTPS://Tessera.Example.com')` → `'https://tessera.example.com'`; `safeOrigin(['https://a', 'https://b'])` → null (Array aus doppeltem Query-Parameter); `safeOrigin(undefined)`, `safeOrigin('')`, `safeOrigin('https://host/pfad')`, `safeOrigin('javascript:alert(1)')` → null + - Test 20 (Manifest ohne Datei → 204): `fs.rmSync(manifest.json)` im Test, Anfrage → 204 (afterEach stellt es wieder her) + - Test 21 (Manifest-Validierung): `signature: 123` (Zahl) im linux-Eintrag → `GET /desktop/download/linux` → 404 (Eintrag ungueltig, wie Test 10) + - Test 11 erweitern: `DesktopController.prototype.update` traegt `@Public()` + - Tests 1-10 bleiben unveraendert gruen (Test 1 `toEqual` ist ohne `signature` im Standard-Manifest weiterhin exakt). + + + Tests zuerst schreiben, `pnpm --filter @tessera/api exec vitest run src/desktop` rot sehen, dann: + + 1. **packages/shared/src/index.ts**: `DesktopManifestFile` um `signature?: string` (Kommentar: Base64-Inhalt der `.sig`-Datei des Tauri-Bundlers, minisign; geschrieben von desktop-collect.sh, gelesen nur von `GET /desktop/update`), `DesktopManifest` um `updateVersion?: string` (Kommentar: `X.Y.Z` bei live, `X.Y.Z-beta.g` bei beta — die SemVer-Form, die der Updater vergleicht; Praefix `g` Pflicht). Neues `export interface DesktopUpdateResponse { version: string; pub_date?: string; url: string; signature: string; notes?: string }` mit Kommentar: dynamisches Antwortformat von `tauri-plugin-updater` (Feldnamen sind vom Plugin vorgegeben, darum snake_case `pub_date`). + 2. **desktop.service.ts**: + - `isValidManifestFileEntry`: zusaetzlich `(candidate.signature === undefined || typeof candidate.signature === 'string')`. + - `getManifest()`: `updateVersion`, falls vorhanden, muss ein String sein — sonst warnen und `null` (gleiche Stelle wie die Kopf-Form-Pruefung). + - `export function safeOrigin(base: unknown): string | null` (reine Funktion, oberhalb der Klasse, mit Doc-Kommentar T-KGC-02): `typeof base !== 'string'` oder leer oder laenger als 2048 → null; `new URL(base)` in try/catch → null; Protokoll nur `http:`/`https:`; `username`/`password` leer; `pathname === '/'`, `search === ''`, `hash === ''`; Rueckgabe `url.origin` (kleingeschriebener Host, ohne Schlussstrich). Kommentar: `base` wird NUR zum Bauen der Rueckgabe-URL fuer den Anfragenden verwendet, nie serverseitig abgerufen (kein SSRF); ein Angreifer koennte damit hoechstens seinen eigenen Client auf einen fremden Download lenken, den die Signaturpruefung im Client ablehnt. + - Konstante `UPDATE_VERSION_RE = /^\d+\.\d+\.\d+(-beta\.g[0-9a-f]{7})?$/` und `RFC3339_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/`. + - `getUpdate(input: { target: unknown; arch: unknown; origin: string }): DesktopUpdateResponse | null`: Reihenfolge (1) `target` per Whitelist auf `DesktopPlatform` (`'windows' | 'linux'`, sonst null), (2) `arch !== 'x86_64'` → null, (3) `getManifest()` null → null, (4) `updateVersion` fehlt oder nicht `UPDATE_VERSION_RE` → null, (5) Eintrag der Plattform fehlt oder `signature` fehlt/leer → null, (6) Antwort `{ version: manifest.updateVersion, url: `${origin}/api-proxy/desktop/download/${platform}`, signature: entry.signature, notes: `Tessera ${manifest.updateVersion}` }` plus `pub_date: manifest.buildTime` nur wenn `RFC3339_RE` passt (sonst weglassen — ein unparsebares Datum liesse `check()` im Client scheitern). Doc-Kommentar: Vertrag des Plugins (Pflichtfelder), 204 = „kein Update“, `current` wird bewusst nicht ausgewertet (die Entscheidung trifft der Client-Comparator, die API kennt den Client-Commit nicht); `/desktop/latest` bleibt fuer die Web-Oberflaeche mit relativen URLs. + 3. **desktop.controller.ts**: Imports `BadRequestException`, `Query`, `Res` aus `@nestjs/common`, `type { Response } from 'express'`, `DesktopUpdateResponse` aus `@tessera/shared`, `safeOrigin` aus `./desktop.service`. Neue Methode VOR `download` (Konvention Route-Order, auch wenn `update` und `download/:platform` sich hier nicht ueberschatten): `@Public() @Get('update') update(@Query('target') target: unknown, @Query('arch') arch: unknown, @Query('base') base: unknown, @Res({ passthrough: true }) res: Response): DesktopUpdateResponse | undefined` — `const origin = safeOrigin(base); if (!origin) throw new BadRequestException('base must be an http(s) origin without path, query or credentials');` `const result = this.desktopService.getUpdate({ target, arch, origin }); if (!result) { res.status(204); return undefined; } return result;`. Kommentar: bewusst oeffentlich (D-10, wie `latest`/`download`: der Client prueft vor der Anmeldung); 204 ist der vom Plugin definierte „kein Update“-Status; `passthrough` + `res.status(204)` funktioniert, weil Nest den Standardstatus vor dem Handler setzt. `@Query('current')` nicht deklarieren (nicht genutzt). + 4. Keine Aenderung an `getLatest`/`getPackage`/`DesktopModule`; keine neue Abhaengigkeit. Formatierung im Stil der Datei (Biome-Konfiguration ist derzeit nicht ausfuehrbar — Zeilenbreite 100, zwei Leerzeichen, einfache Anfuehrungszeichen von Hand einhalten). + 5. Commit nur der vier Task-Dateien: `git add packages/shared/src/index.ts apps/api/src/desktop/desktop.service.ts apps/api/src/desktop/desktop.controller.ts apps/api/src/desktop/desktop.service.spec.ts`. + + + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/desktop && pnpm --filter @tessera/api type-check && pnpm --filter @tessera/shared type-check && command grep -q 'signature?: string' packages/shared/src/index.ts && command grep -q 'updateVersion?: string' packages/shared/src/index.ts && command grep -q 'export interface DesktopUpdateResponse' packages/shared/src/index.ts && command grep -q 'export function safeOrigin(base: unknown): string | null' apps/api/src/desktop/desktop.service.ts && command grep -q 'getUpdate(' apps/api/src/desktop/desktop.service.ts && command grep -q "@Get('update')" apps/api/src/desktop/desktop.controller.ts && test "$(awk "/@Get\('update'\)/{u=NR} /@Get\('download\/:platform'\)/{d=NR} END{print (u>0 && d>0 && u + + `GET /desktop/update` liefert fuer signierte Pakete das dynamische Updater-Format mit absoluter URL aus dem validierten `base`-Origin, 204 in allen „kein Update“-Faellen, 400 bei ungueltigem `base`; `/desktop/latest` und `download/:platform` unveraendert; Typen in `@tessera/shared`; mindestens 21 Spec-Tests (11 Bestand, davon Test 11 erweitert, + 10 neue) gruen, api- und shared-type-check gruen; Commit `feat(api): GET /desktop/update — signierte Desktop-Pakete im Format des Tauri-Updaters ausliefern`. + + + + Task 3: Skripte + CI — `.sig` als `signature` und `updateVersion` ins Manifest, `stamp check` verlangt beides, Signier-Secrets an den zwei `tauri build`-Schritten, `*.key` in .gitignore + .gitea/scripts/desktop-collect.sh, .gitea/scripts/desktop-stamp.sh, .gitea/workflows/ci.yml, .gitignore + + - .gitea/scripts/desktop-collect.sh komplett (156 Zeilen): Kanalentscheidung Z. 49-63, Versionspruefung Z. 65-77, Linux-Block Z. 97-112 (`find … -name '*.AppImage'` matcht `.AppImage.sig` NICHT — Zaehler bleiben korrekt), Windows-Block Z. 114-129, `jq -n`-Manifest Z. 131-154 + - .gitea/scripts/desktop-stamp.sh komplett (162 Zeilen): `cmd_check` Z. 85-149, `check_file` Z. 99-115, `no_reuse` Z. 92-97 + - .gitea/workflows/ci.yml komplett (241 Zeilen): Job `desktop` Z. 67-208, Bau-Schritte Z. 179-185 (`Linux-AppImage bauen`, `Windows-Installer bauen (Cross-Bau)`), Kopfkommentar Z. 1-16 (Stil fuer den neuen Absatz) + - .gitignore Z. 44-45 (`desktop-dist/*`, `!desktop-dist/.gitkeep`) — es gibt keine getrackten `*.key`-Dateien (in der Ausgabe von `git ls-files` endet keine Zeile auf `.key`) + - Recherche Abschnitt 3 (Signatur entsteht in der CLI nach dem Buendeln als `.sig`, Base64-Zeile, host-unabhaengig — auch im xwin-Cross-Bau; Bau ohne Schluessel bei gesetztem `pubkey` bricht ab, Ausweg nur `tauri build --no-sign`), Abschnitt 6 („desktop-collect.sh“, „Skip-Mechanismus“, „ci.yml“) + - .planning/quick/260917-jdh-…/260917-jdh-PLAN.md Task-1-`` (Muster fuer Mini-Fixtures im Scratchpad und die js-yaml-Pruefung ueber `node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml`) + - WICHTIG: das echte `desktop-dist/` im Arbeitsbaum enthaelt eine lokale AppImage — NIE als Probe-Ziel verwenden, Proben nur in `mktemp -d`-Ordnern mit `DESKTOP_DIST`/`TAURI_DIR` + + + 1. **desktop-collect.sh** (Vorgabe 5): + - Kopfkommentar: Absatz „Signatur (quick-260917-kgc)“: die Tauri-CLI legt beim Bau mit `createUpdaterArtifacts` neben jedem Bundle eine `.sig` ab (minisign, eine Base64-Zeile); dieses Skript traegt deren INHALT als `files..signature` ins Manifest — die `.sig`-Datei selbst wird nicht kopiert (die API liefert JSON). Pflichtregel: fehlt die `.sig`, bricht das Skript ab, wenn `TAURI_SIGNING_PRIVATE_KEY` gesetzt ist ODER der Kanal nicht `dev` ist (main/Tag — dort sind Signaturen Pflicht, `--no-sign` gibt es nur lokal); sonst Warnung und Feld weglassen (lokaler Bau mit `tauri build --no-sign` bleibt moeglich, die API antwortet dann 204). Neues Feld `updateVersion` = die SemVer-Form fuer den Updater. Der Satz „Dieses Skript kennt kein Secret.“ bleibt wahr: der Schluessel wird nur auf Gesetztsein geprueft, nie gelesen oder ausgegeben. + - Nach der Kanalentscheidung: `UPDATE_VERSION="$VERSION"`, im `refs/heads/main`-Zweig `UPDATE_VERSION="$VERSION-beta.g$SHA_SHORT"` (nach `VERSION` bekannt ist — Reihenfolge im Skript beachten: `SUFFIX` wird vor `VERSION` gesetzt, `UPDATE_VERSION` also NACH der Versionspruefung aus `CHANNEL`/`SHA_SHORT` bilden). `SIGN_REQUIRED=0; if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ "$CHANNEL" != dev ]; then SIGN_REQUIRED=1; fi`. + - Funktion `read_signature` (Argument: Bundle-Pfad; schreibt den Inhalt von `.sig` nach stdout): existiert die Datei → Inhalt lesen, gegen `^[A-Za-z0-9+/=]+$` pruefen (genau eine Base64-Zeile; sonst Fehler „Signaturdatei … hat nicht die erwartete Form“ → exit 1); existiert sie nicht → bei `SIGN_REQUIRED=1` Fehlermeldung nach stderr (Text: Signaturdatei fehlt; im CI muessen `TAURI_SIGNING_PRIVATE_KEY`/`…_PASSWORD` an den Bau-Schritten gesetzt sein; `--no-sign` ist nur lokal erlaubt) → exit 1, sonst Warnung nach stderr und leere Ausgabe. Aufruf jeweils als eigene Zeile `LINUX_SIG="$(read_signature "$APPIMAGE_SRC")"` bzw. `WINDOWS_SIG="$(read_signature "$NSIS_SRC")"` — mit `set -e` beendet ein `exit 1` in der Kommandosubstitution das Skript ueber den Status der Zuweisung; zur Sicherheit direkt danach `[ -n "$LINUX_SIG" ] || [ "$SIGN_REQUIRED" = 0 ] || exit 1`. Variablen `LINUX_SIG=""`/`WINDOWS_SIG=""` bei den anderen Vorbelegungen. Log-Zeilen um `signiert`/`ohne Signatur` ergaenzen. + - `jq -n`: `--arg updateVersion "$UPDATE_VERSION" --arg linuxSig "$LINUX_SIG" --arg windowsSig "$WINDOWS_SIG"`; Ausgabe `updateVersion: $updateVersion` nach `version`; Plattform-Objekte per `+ (if $linuxSig != "" then { signature: $linuxSig } else {} end)` bzw. windows analog erweitern (weiterhin nur `jq -n` mit `--arg`, kein String-Zusammenbau). + - `rm -f`-Zeile unveraendert (keine `.sig` im Zielordner). Dateinamen der Pakete unveraendert (Vorgabe 5: die CLI erzeugt `Tessera_X.Y.Z_x64-setup.exe` + `.sig` bzw. `Tessera_X.Y.Z_amd64.AppImage` + `.sig`; `find -name '*.exe'`/`'*.AppImage'` matchen die `.sig` nicht, die `.sig` liegt exakt unter `.sig`). + 2. **desktop-stamp.sh** `cmd_check`: nach der Versionspruefung `UPDATE_VERSION="$(jq -r '.updateVersion // empty' "$MANIFEST")"`; leer → `no_reuse "updateVersion fehlt im Manifest (Stand vor der Update-Funktion)"`. In `check_file` nach der Pruefsumme: `SIG="$(jq -r ".files.$1.signature // empty" "$MANIFEST")"`; leer → `no_reuse "Signatur fuer $2 fehlt im Manifest"`. Kopfkommentar: Absatz, dass ein Cache-Stand ohne `updateVersion`/`signature` nie uebernommen wird (sonst lieferte der Update-Endpunkt dauerhaft 204) und dass ein Schluesselwechsel (`pubkey` in tauri.conf.json unter `apps/desktop`) den Stempel automatisch aendert. `DESKTOP_PATHS` unveraendert. + 3. **ci.yml**: Kopfkommentar-Absatz `# quick-260917-kgc: Die beiden tauri-build-Schritte signieren die Pakete mit dem` … `# Updater-Schluessel (Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD, nur an diesen` … `# zwei Schritten); desktop-collect.sh traegt die .sig-Inhalte ins Manifest.` An den Schritten `Linux-AppImage bauen` und `Windows-Installer bauen (Cross-Bau)` jeweils zwischen `if:` und `run:` einen `env:`-Block: `TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}` und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}` — Entscheidung Schritt- statt Job-Ebene: geringste Sichtbarkeit (`pnpm install`, `apt-get`, `cargo install cargo-xwin`, Cache-Schritte sehen den Schluessel nicht); Gitea maskiert Secret-Werte im Log, das Skript gibt sie nie aus. Sonst NICHTS aendern (kein `--no-sign`, kein Job-`env`, `Pakete einsammeln` unveraendert — die Pflichtregel greift dort ueber den Kanal). + 4. **.gitignore**: nach dem `desktop-dist`-Block eine Zeile `*.key` mit Kommentar `# Privater Updater-Signierschluessel liegt ausserhalb des Repos (~/.tessera/desktop-updater/) -- nie einchecken`. Vorher bestaetigen, dass in der Ausgabe von `git ls-files` keine Zeile auf `.key` endet. + 5. Lokale Proben laut `` nur mit Mini-Fixtures im Scratchpad (`TAURI_DIR`, `DESKTOP_DIST`, `GITHUB_REF`, `TAURI_SIGNING_PRIVATE_KEY=dummy` — der Dummy ist ein beliebiger Platzhalterstring, KEIN echter Schluessel; `~/.tessera/desktop-updater/` nie lesen). + 6. Commit nur der vier Task-Dateien: `git add .gitea/scripts/desktop-collect.sh .gitea/scripts/desktop-stamp.sh .gitea/workflows/ci.yml .gitignore`. + + + cd /home/vicolab/projects/tessera-ctl && sh -n .gitea/scripts/desktop-collect.sh && sh -n .gitea/scripts/desktop-stamp.sh && command grep -q '^\*\.key$' .gitignore && TRACKED="$(git ls-files)" && ! printf '%s\n' "$TRACKED" | command grep -q '\.key$' && T=$(mktemp -d) && mkdir -p "$T/tauri/target/release/bundle/appimage" "$T/tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis" "$T/dist" && printf '{"version":"1.2.0"}' > "$T/tauri/tauri.conf.json" && printf 'linuxbytes' > "$T/tauri/target/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage" && printf 'dW50cnVzdGVkIGNvbW1lbnQ6IGxpbnV4Cg==' > "$T/tauri/target/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage.sig" && printf 'winbytes' > "$T/tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/Tessera_1.2.0_x64-setup.exe" && printf 'dW50cnVzdGVkIGNvbW1lbnQ6IHdpbmRvd3MK' > "$T/tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/Tessera_1.2.0_x64-setup.exe.sig" && SHA7="$(git rev-parse --short=7 HEAD)" && GITHUB_REF=refs/heads/main TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux,windows && jq -e --arg u "1.2.0-beta.g$SHA7" '.version == "1.2.0" and .channel == "beta" and .updateVersion == $u and .files.linux.signature == "dW50cnVzdGVkIGNvbW1lbnQ6IGxpbnV4Cg==" and .files.windows.signature == "dW50cnVzdGVkIGNvbW1lbnQ6IHdpbmRvd3MK" and (.files.linux.name | test("^Tessera-1\\.2\\.0-beta\\.[0-9a-f]{7}\\.AppImage$")) and (.files.windows.name | test("^Tessera-Setup-1\\.2\\.0-beta\\.[0-9a-f]{7}\\.exe$"))' "$T/dist/manifest.json" >/dev/null && test -z "$(find "$T/dist" -name '*.sig')" && GITHUB_REF=refs/tags/v1.2.0 TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux,windows && jq -e '.channel == "live" and .updateVersion == "1.2.0" and .files.linux.name == "Tessera-1.2.0.AppImage" and .files.windows.name == "Tessera-Setup-1.2.0.exe" and (.files.linux.signature | length > 0)' "$T/dist/manifest.json" >/dev/null && CACHE_HIT=true STAMP_VERSION=1.2.0 STAMP_SHA7="$SHA7" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-stamp.sh check | command grep -qx 'reuse=false' && GITHUB_REF=refs/heads/main TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux,windows >/dev/null && CACHE_HIT=true STAMP_VERSION=1.2.0 STAMP_SHA7="$SHA7" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-stamp.sh check | command grep -qx 'reuse=true' && jq 'del(.files.windows.signature)' "$T/dist/manifest.json" > "$T/m2.json" && cp "$T/m2.json" "$T/dist/manifest.json" && CACHE_HIT=true STAMP_VERSION=1.2.0 STAMP_SHA7="$SHA7" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-stamp.sh check | command grep -qx 'reuse=false' && GITHUB_REF=refs/heads/main TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux,windows >/dev/null && jq 'del(.updateVersion)' "$T/dist/manifest.json" > "$T/m3.json" && cp "$T/m3.json" "$T/dist/manifest.json" && CACHE_HIT=true STAMP_VERSION=1.2.0 STAMP_SHA7="$SHA7" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-stamp.sh check | command grep -qx 'reuse=false' && rm -f "$T/tauri/target/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage.sig" && ! GITHUB_REF=refs/heads/main TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux,windows 2>/dev/null && ! GITHUB_REF=refs/tags/v1.2.0 TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux,windows 2>/dev/null && ! GITHUB_REF= TAURI_SIGNING_PRIVATE_KEY=dummy TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux 2>/dev/null && GITHUB_REF= TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux 2>"$T/warn.txt" && command grep -qi 'signatur' "$T/warn.txt" && jq -e '.channel == "dev" and .updateVersion == "1.2.0" and ((.files.linux | has("signature")) | not)' "$T/dist/manifest.json" >/dev/null && printf 'zwei\nzeilen\n' > "$T/tauri/target/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage.sig" && ! GITHUB_REF= TAURI_DIR="$T/tauri" DESKTOP_DIST="$T/dist" sh .gitea/scripts/desktop-collect.sh --require linux 2>/dev/null && rm -rf "$T" && node -e 'const y=require("./node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml");const fs=require("fs");const cp=require("child_process");const d=y.load(fs.readFileSync(".gitea/workflows/ci.yml","utf8"));const o=y.load(cp.execSync("git show HEAD:.gitea/workflows/ci.yml").toString());for(const j of["quality","test","publish"]){if(JSON.stringify(d.jobs[j])!==JSON.stringify(o.jobs[j]))throw new Error("Job veraendert: "+j)}if(JSON.stringify(d.on)!==JSON.stringify(o.on))throw new Error("on veraendert");const n=d.jobs.desktop,p=o.jobs.desktop;for(const k of["env","needs","if","runs-on"]){if(JSON.stringify(n[k])!==JSON.stringify(p[k]))throw new Error("desktop."+k+" veraendert")}if(n.steps.length!==p.steps.length)throw new Error("Schrittzahl veraendert");const names=["Linux-AppImage bauen","Windows-Installer bauen (Cross-Bau)"];let signed=0;for(let i=0;i + + `desktop-collect.sh` schreibt `updateVersion` und je Plattform `signature` aus der `.sig` neben dem Bundle, bricht auf main/Tag oder bei gesetztem Schluessel ohne `.sig` ab und warnt sonst nur; `desktop-stamp.sh check` uebernimmt keinen Cache-Stand ohne `updateVersion`/`signature`; die beiden `tauri build`-Schritte tragen die zwei Secrets, alles andere in ci.yml ist byte- bzw. strukturgleich; `*.key` ignoriert; alle Proben (Beta/Live/Dev, fehlende und zweizeilige `.sig`, Cache-Uebernahme mit/ohne Felder, js-yaml-Tiefenvergleich) gruen; Commit `ci: Desktop-Pakete mit dem Updater-Schluessel signieren, Signatur und updateVersion ins Manifest`. + + + + Task 4: Doku — Anwender-, Betriebs- und Entwicklungshandbuch, ci-cd-setup.md, CHANGELOG + docs/anleitung-anwender.md, docs/anleitung-betrieb.md, docs/anleitung-entwicklung.md, docs/ci-cd-setup.md, CHANGELOG.md + + - docs/anleitung-anwender.md Z. 161-222 (Desktop-App; Tray-Liste Z. 190-197 mit `**Update herunterladen**`, Abschnitt „Neue Version“ Z. 213-215, „Wenn etwas nicht klappt“ Z. 217-222); Anfuehrungszeichen: oeffnend „ (U+201E), schliessend gerades " — beibehalten; Sie-Form; Alltagssprache ohne Fachbegriffe + - docs/anleitung-betrieb.md Z. 552-660 (Kap. 10: „Wo die Pakete im Abbild liegen“ mit `curl`-Kontrollzeile Z. 617-627, „Umgebungsvariablen“ Z. 638-644, „Fehlerbilder“ Z. 646-660); Umlaute echt + - docs/anleitung-entwicklung.md Z. 128-172 („Desktop-App lokal bauen“ — der Befehl `tauri build --bundles appimage` bricht seit Task 1 ohne Schluessel ab) + - docs/ci-cd-setup.md Z. 81-107 (Secrets-Tabelle „Benoetigt wird genau eines“) und Z. 135-210 (Job `desktop`, Punkt 5/6) — diese Datei ist ASCII ohne Umlaute (ae/oe/ue), so bleiben + - CHANGELOG.md Z. 1-25 — FRISCH lesen: `## Unveröffentlicht` hat bereits `### Neu` (drei Zeilen, davon zwei `Desktop-App:`), `### Geändert`, `### Behoben`; Anfuehrungszeichen „…“ (U+201E/U+201C), Praefix `Desktop-App:`, kein Punkt am Ende + - Recherche Abschnitte 4 (Windows-Ablauf, Tempordner bleibt liegen), 5 (AppImage muss beschreibbar sein, Tempordner auf demselben Dateisystem), 8 (SmartScreen vermutlich ohne Dialog — nicht versprechen), „Open Questions“ 3 (erster Rollout) + + + 1. **docs/anleitung-anwender.md**: + - Tray-Liste: `- **Update installieren** — wird aktiv, sobald eine neue Version vorliegt, und heißt dann „Auf Version X.Y.Z aktualisieren" (auf dem Beta-Kanal: „Auf Beta-Stand … aktualisieren")`. + - Abschnitt `### Neue Version` komplett neu: Absatz 1 — beim Start (und nach einem Serverwechsel) prueft die App, ob der Server eine neuere Version hat; dann Meldung „Neue Version X.Y.Z verfügbar" und der Menueeintrag heisst „Auf Version X.Y.Z aktualisieren". Absatz 2 — ein Klick genuegt: die App laedt das Paket im Hintergrund (Fortschritt im Menue als „Lädt … 42 %"), prueft, dass es unveraendert von Ihrem Tessera-Server stammt, und installiert es. Unter Windows erscheint kurz das Installationsfenster mit Fortschrittsbalken, danach startet Tessera von selbst neu; die Server-Adresse und die Fensterposition bleiben erhalten (die Position kann nach einem Update einmal auf den Standard zurueckfallen). Unter Linux wird die AppImage-Datei an ihrem Speicherort ersetzt, die App startet neu — die Datei muss dafuer an einem Ort liegen, an dem Sie schreiben duerfen (z. B. Ihr Home-Ordner). Absatz 3 — schlaegt das Update fehl, meldet die App den Grund und oeffnet die Seite Einstellungen → Desktop-App im Browser; dort laden und installieren Sie die neue Version wie oben beschrieben. Absatz 4 — Voraussetzung: die Server-Adresse beginnt mit `https`; bei `http` steht im Menue „Update nur über https möglich", der Weg ueber den Browser bleibt. Absatz 5 — **Einmaliger Wechsel:** Wer die Desktop-App 1.2.0 oder aelter installiert hat, muss die naechste Version ein letztes Mal ueber den Browser herunterladen und installieren; ab dann laeuft das Update ueber den Menueeintrag. + - „Wenn etwas nicht klappt“: zwei Zeilen — **„Update fehlgeschlagen"** (Netz kurz weg, Paket unvollstaendig oder nicht vom eigenen Server: Browser-Weg, wie in „Neue Version" beschrieben) und **Linux: Update meldet fehlende Schreibrechte** (AppImage in den Home-Ordner legen und von dort starten). + 2. **docs/anleitung-betrieb.md** Kap. 10: + - Neuer Unterabschnitt `### Updates in der App und der Signierschlüssel` VOR „Umgebungsvariablen“: (a) Seit September 2026 aktualisiert sich die Desktop-App per Klick; sie fragt `GET /api-proxy/desktop/update` und installiert nur Pakete, deren Signatur zum im Client hinterlegten oeffentlichen Schluessel passt — ein manipuliertes oder fremdes Paket wird abgelehnt. (b) Der private Schluessel liegt NICHT im Repository: als Gitea-Secrets `TAURI_SIGNING_PRIVATE_KEY` und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (Repository → Einstellungen → Actions → Secrets) und als Sicherung auf dem Entwicklungsrechner unter `~/.tessera/desktop-updater/` (`tessera-updater.key`, `password.txt`, `tessera-updater.key.pub`). (c) Sicherung: die beiden Dateien an einem zweiten sicheren Ort ablegen. Geht der private Schluessel verloren, koennen bereits installierte Clients kein Update mehr annehmen — es muss ein neues Schluesselpaar erzeugt, der oeffentliche Teil in `apps/desktop/src-tauri/tauri.conf.json` eingetragen und jeder Client einmal von Hand neu installiert werden. (d) Ohne die Secrets bricht der CI-Bau ab („no private key"); `tauri build --no-sign` ist nur fuer lokale Proben gedacht, im CI nicht vorgesehen. (e) Windows legt je Update einen Ordner `%TEMP%\Tessera--updater-…` (~100 MB) ab und raeumt ihn nicht auf — kein Handlungsbedarf, kann geloescht werden. (f) `http`-Adressen: der Client erlaubt Updates nur ueber `https`; Nutzer mit `http`-Adresse sehen im Menue den Hinweis und nutzen den Browser-Weg. + - „Wo die Pakete im Abbild liegen“: `manifest.json`-Beschreibung um `updateVersion` und je Plattform `signature` ergaenzen; nach der `curl … /desktop/latest`-Zeile eine zweite Kontrollzeile `curl -si "https://{ihre-adresse}/api-proxy/desktop/update?target=windows&arch=x86_64¤t=0.0.0&base=https://{ihre-adresse}"` mit Erklaerung: `200` mit `version`/`url`/`signature` = Update-faehig; `204` = kein signiertes Paket fuer diese Plattform (z. B. Stand vor September 2026 oder Bau ohne Schluessel). + - Fehlerbilder-Tabelle, zwei Zeilen: „Client meldet ‚Update fehlgeschlagen'" (Ursachen: Download ueber den Proxy abgebrochen — Groessengrenze/Timeout wie in der Zeile darueber; Signatur passt nicht — Pakete stammen nicht aus dem CI-Bau mit dem aktuellen Schluessel; Pruefen: `curl`-Kontrollzeile, Pipeline-Lauf, Proxy) und „`/desktop/update` antwortet dauerhaft 204, obwohl Pakete da sind" (Manifest ohne `signature`/`updateVersion`: Pakete aus einem Bau vor der Update-Funktion oder mit `--no-sign`; Beheben: Aenderung unter `apps/desktop/` pushen oder Tag neu bauen; im CI die Secrets pruefen). + 3. **docs/anleitung-entwicklung.md** „Desktop-App lokal bauen“: den Bau-Befehl auf `pnpm --filter @tessera/desktop exec tauri build --bundles appimage --no-sign` aendern und einen Absatz ergaenzen: Seit der Update-Funktion verlangt `tauri build` den Signierschluessel (`TAURI_SIGNING_PRIVATE_KEY`), weil `createUpdaterArtifacts` und der oeffentliche Schluessel in `tauri.conf.json` gesetzt sind; lokal baut man mit `--no-sign` (dann entsteht keine `.sig`, `desktop-collect.sh` warnt und die API antwortet auf `/desktop/update` mit 204 — der In-App-Update-Weg laesst sich lokal nur mit dem echten Schluessel durchspielen). Hinweis: `tauri dev` laeuft ohne AppImage — dort `download_and_install` nie ausloesen (es wuerde die Binary in `target/` ueberschreiben); die Versionspruefung selbst ist im Dev-Bau auch gegen `http://localhost` erlaubt. + 4. **docs/ci-cd-setup.md**: Secrets-Tabelle: Satz „Benoetigt wird genau eines“ → „Benoetigt werden drei“; zwei Zeilen `TAURI_SIGNING_PRIVATE_KEY` (Inhalt der privaten Schluesseldatei des Tauri-Updaters, eine Base64-Zeile; nur an den zwei `tauri build`-Schritten des Jobs `desktop` als `env`) und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (Passwort dazu). Absatz: der Schluessel wird nie ausgegeben; die Skripte kennen ihn nicht (`desktop-collect.sh` prueft nur, ob die Variable gesetzt ist). Job-`desktop`-Beschreibung Punkt 5: Bau-Schritte signieren die Bundles (`.sig` neben `-setup.exe`/`.AppImage`, host-unabhaengig auch im Cross-Bau); Punkt 6: `desktop-collect.sh` traegt `signature` und `updateVersion` ins Manifest und bricht auf `main`/Tags ohne `.sig` ab. Fehlerbehebung: neuer Unterabschnitt `### Job desktop: "A public key has been found, but no private key"` (Secrets fehlen/umbenannt → in Gitea pruefen; nie `--no-sign` in ci.yml eintragen). ASCII beibehalten (keine Umlaute). + 5. **CHANGELOG.md** `## Unveröffentlicht` → `### Neu`: am ENDE der Liste eine Zeile `- Desktop-App: Update mit einem Klick – „Auf Version X.Y.Z aktualisieren“ im Menü des Infobereich-Symbols lädt das signierte Paket, installiert es und startet die App neu (Windows und Linux); Voraussetzung ist eine https-Adresse, bereits installierte Versionen bis 1.2.0 wechseln einmal noch über den Browser`. Fremde Zeilen unveraendert; `git diff CHANGELOG.md` vor dem Commit gegenpruefen (nur diese eine Zeile neu). + 6. Commit nur der fuenf Task-Dateien: `git add docs/anleitung-anwender.md docs/anleitung-betrieb.md docs/anleitung-entwicklung.md docs/ci-cd-setup.md CHANGELOG.md`. + + + cd /home/vicolab/projects/tessera-ctl && command grep -q '\*\*Update installieren\*\*' docs/anleitung-anwender.md && ! command grep -q '\*\*Update herunterladen\*\*' docs/anleitung-anwender.md && command grep -q 'Auf Version X.Y.Z aktualisieren' docs/anleitung-anwender.md && command grep -q 'Update nur über https möglich' docs/anleitung-anwender.md && command grep -q 'Einmaliger Wechsel' docs/anleitung-anwender.md && ! command grep -q 'Ein automatisches Aktualisieren gibt es nicht' docs/anleitung-anwender.md && command grep -q '^### Updates in der App und der Signierschlüssel' docs/anleitung-betrieb.md && command grep -q 'TAURI_SIGNING_PRIVATE_KEY_PASSWORD' docs/anleitung-betrieb.md && command grep -q '~/.tessera/desktop-updater/' docs/anleitung-betrieb.md && command grep -q 'api-proxy/desktop/update?target=windows' docs/anleitung-betrieb.md && command grep -q 'updateVersion' docs/anleitung-betrieb.md && command grep -q 'tauri build --bundles appimage --no-sign' docs/anleitung-entwicklung.md && command grep -q 'TAURI_SIGNING_PRIVATE_KEY' docs/anleitung-entwicklung.md && command grep -q '`TAURI_SIGNING_PRIVATE_KEY`' docs/ci-cd-setup.md && command grep -q '`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`' docs/ci-cd-setup.md && command grep -q 'no private key' docs/ci-cd-setup.md && ! command grep -q -E '[äöüÄÖÜß]' docs/ci-cd-setup.md && awk '/^## Unveröffentlicht/{u=1;next} /^## /{u=0} u' CHANGELOG.md | command grep -q '^- Desktop-App: Update mit einem Klick' && ! awk '/^## 1\.2\.0/{u=1} u' CHANGELOG.md | command grep -q 'Update mit einem Klick' && test "$(awk '/^## Unveröffentlicht/{u=1;next} /^## /{u=0} u' CHANGELOG.md | command grep -c '^- Desktop-App: ')" -ge 4 && pnpm --filter @tessera/web exec vitest run src/lib/changelog.test.ts && echo DOCS-OK + + Anwenderhandbuch beschreibt das Update per Klick (Ablauf Windows/Linux, Fehlerfall, https-Bedingung, einmaliger Wechsel); Betriebshandbuch erklaert Schluessel, Secrets, Sicherung/Verlust, Kontrollzeile und zwei Fehlerbilder; Entwicklungshandbuch nennt `--no-sign`; ci-cd-setup.md fuehrt die zwei Secrets (ASCII); CHANGELOG eine neue Zeile; changelog.test.ts gruen; Commit `docs: Desktop-App — Update in der App (Handbücher, CI-Secrets, CHANGELOG)`. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Tessera-Server (API/Proxy) → Desktop-Client | Update-Antwort und Paket-Bytes kommen ueber das Netz; der Server (oder ein Angreifer dazwischen) ist fuer den Client nicht vertrauenswuerdig — die Signatur ist die einzige Vertrauensbasis | +| Anfragender (Query `base`, `target`, `arch`) → API | Freie Eingabe ohne Anmeldung (`@Public()`); wird nur reflektiert bzw. per Whitelist gefiltert | +| CI-Runner (Secrets) → Bundle-Signatur | Privater Schluessel existiert nur als Gitea-Secret und auf dem Dev-Rechner; der Bau-Schritt ist die einzige Stelle, die ihn sieht | +| manifest.json (aus dem CI-Bau) → API-Antwort | Manifest-Felder `signature`/`updateVersion` werden ungeprueft weitergereicht — Manipulation kann keine gueltige Signatur erzeugen | +| Client-Prozess → Installer (Windows) / Dateisystem (Linux) | Installation laeuft im Nutzerkontext (`currentUser`), ohne Erhoehung | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-KGC-01 | Tampering | Paket auf dem Transportweg / auf dem Server, `Update::download` | high | mitigate | minisign-Signaturpruefung durch das Plugin gegen `plugins.updater.pubkey` VOR jeder Installation (updater.rs Z. 740); privater Schluessel offline (Gitea-Secret + `~/.tessera/desktop-updater/`). Falsche/fehlende Signatur → `Error::Minisign`, keine Installation, Rueckfall Browser-Seite. Kein Manifest-Hash-Vergleich als Ersatz — die Signatur ist die Vertrauensbasis. | +| T-KGC-02 | Spoofing / SSRF | `GET /desktop/update?base=` | medium | mitigate | `safeOrigin`: nur `http:`/`https:`, keine Userinfo, Pfad genau `/`, keine Query/Fragment, Rueckgabe `url.origin`; sonst 400. `base` wird ausschliesslich zum Bauen der Rueckgabe-URL fuer den Anfragenden genutzt, nie serverseitig abgerufen (Gate: kein `fetch`/`http.get` im Service). Ein Angreifer lenkt hoechstens seinen eigenen Client auf einen fremden Download, den T-KGC-01 ablehnt. | +| T-KGC-03 | Information Disclosure | Privater Signierschluessel (Secrets, Dev-Rechner) | high | mitigate | Secrets nur als `env` an den zwei `tauri build`-Schritten (nicht Job-Ebene; `pnpm install`/`apt`/`cargo install`/Cache sehen ihn nicht); Skripte kennen ihn nicht (`desktop-collect.sh` prueft nur Gesetztsein); Gitea maskiert Secret-Werte; `*.key` in `.gitignore`; der Executor liest `~/.tessera/desktop-updater/` nie. js-yaml-Gate verhindert Secrets an fremden Schritten. | +| T-KGC-04 | Tampering (Downgrade) | `is_update_newer`, Server-Antwort mit aelterer Version | medium | mitigate | Comparator installiert nur eine hoehere Basisversion oder einen anderen Beta-Stempel bei gleicher Basis; niedrigere Basis nie; gleiche Basis Live nie. Ein Angreifer braeuchte ohnehin den privaten Schluessel (T-KGC-01). Rest-Risiko: Beta-Stempel-Wechsel innerhalb derselben Basis kann zeitlich rueckwaerts sein — hingenommen, weil nur signierte CI-Baue in Frage kommen. | +| T-KGC-05 | Tampering | manifest.json (`signature`, `updateVersion`) | low | mitigate | Manifest entsteht nur im CI (`desktop-collect.sh`, `jq --arg`); API prueft Form (`UPDATE_VERSION_RE`, `signature` String, `pub_date` nur bei RFC-3339-Form) und liefert sonst 204; T-18-01/02 (Dateiname nur aus dem Manifest, Pfadpruefung) bleiben. Manipulierte Felder koennen keine gueltige Signatur erzeugen. | +| T-KGC-06 | Tampering | Transport `http://` | medium | mitigate | Plugin verweigert im Release-Bau Nicht-https-Endpunkte (`Error::InsecureTransportProtocol`); keine `dangerousInsecureTransportProtocol`/`dangerousAcceptInvalidCerts`/`dangerousAcceptInvalidHostnames`-Schalter (jq-Gate in Task 1). Zertifikatspruefung ueber den Betriebssystem-Speicher (rustls-platform-verifier). | +| T-KGC-07 | Elevation of Privilege | NSIS-Installer per `ShellExecuteW`, AppImage-Ersetzung | low | accept | Installer laeuft im Nutzerkontext (`installMode: currentUser`), Argumente (`/P /UPDATE /R /ARGS`) stammen fest aus dem Plugin, keine Nutzereingabe; Linux ersetzt nur die eigene AppImage (`APPIMAGE`-Pfad), Rechte werden uebernommen, bei Fehler Ruecksicherung. | +| T-KGC-08 | Denial of Service | Download (~100 MB im RAM), Doppelklick, Timeout | low | accept | `take()` aus `PendingUpdate` + gesperrter Eintrag verhindern parallele Downloads; Pruefung 15 s, Download 600 s Gesamt-Timeout; ein fehlgeschlagener Download endet in Benachrichtigung + Browser-Rueckfall, nie in einem haengenden Zustand. RAM-Bedarf einmalig, hingenommen. | +| T-KGC-09 | Information Disclosure | `GET /desktop/update` (`@Public()`) | low | accept | Antwort enthaelt nur Version, Datum, Download-URL und die (oeffentliche) Signatur — dieselben Daten wie `/desktop/latest`; keine Zugangsdaten, kein Server-Interna. | +| T-KGC-SC | Tampering | cargo installs (`tauri-plugin-updater`, `semver`) | high | mitigate | Beide Crates gegen den Quelltext im lokalen Registry-Spiegel geprueft: `tauri-plugin-updater 2.11.0` (tauri-apps/plugins-workspace, offizielles Tauri-Plugin, crates.io seit 2023), `semver 1.0.28` (dtolnay, bereits transitiv in Cargo.lock) — Einstufung `[OK]`, kein `[ASSUMED]`/`[SUS]`, darum kein Human-Checkpoint. Executor-Gate: Cargo.lock loest `tauri-plugin-updater` auf `2.x` auf (kein `3.0.0-alpha`), Projekt-`reqwest` bleibt 0.12. Kein `pnpm add`. | + + + +- Rust (Task 1): `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` in apps/desktop/src-tauri mit `CARGO_BUILD_JOBS=4` gruen (≥ 30 Tests); Plugin registriert; Comparator/Endpunkt/Labels rein und getestet; `Duration::from_secs(600)` fuer den Download; `InsecureTransportProtocol` behandelt; `/desktop/latest` im Client nicht mehr referenziert; Run-Handler unveraendert (`code: None, api`); capabilities/default.json und setup.html unveraendert; Cargo.lock auf 2.11.x. +- API (Task 2): `pnpm --filter @tessera/api exec vitest run src/desktop` (≥ 21 Tests) gruen, api- und shared-type-check gruen; `update` vor `download/:platform`; `passthrough` + `res.status(204)`; kein serverseitiger Abruf im Service. +- Skripte/CI (Task 3): `sh -n` beider Skripte; Mini-Fixture-Proben fuer collect (beta/live/dev, fehlende `.sig` bei main/Tag/gesetztem Schluessel → Abbruch, dev → Warnung, zweizeilige `.sig` → Abbruch) und stamp (`reuse=true` nur mit `updateVersion` + beiden Signaturen); js-yaml-Tiefenvergleich: nur `env` an den zwei Bau-Schritten neu, sonst strukturgleich, kein `--no-sign`; `*.key` ignoriert. +- Doku (Task 4): Grep-Gates + `changelog.test.ts`. +- Offen (Nachweis durch Orchestrator, Vorgabe 9): (1) CI-Lauf auf main baut beide Pakete mit `.sig`, Manifest traegt `signature`/`updateVersion`, `publish` gruen; (2) `curl -si …/api-proxy/desktop/update?target=windows&arch=x86_64¤t=0.0.0&base=https://alpha.…` liefert 200 mit absoluter `url`; (3) Windows-VM: Client alten Standes zeigt nach Start „Auf Beta-Stand … aktualisieren“, Klick → Fortschritt im Menue → passiver Installer → App startet neu, Tray zeigt neuen Stand, Server-Adresse erhalten; SmartScreen-Verhalten des vom Updater gestarteten Installers notieren (Recherche A2); (4) Cross-Bau von `ring`/`rustls` in der Pipeline gruen (Recherche A1 — Fallback `native-tls`-Feature laut Recherche Abschnitt 8, falls rot). + + + +- Alle `must_haves.truths` erfuellt; vier Commits (`feat(desktop)`, `feat(api)`, `ci`, `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 Arbeiten im selben Baum — nur eigene Dateien per `git add` nennen, nie `git add -A`). +- `~/.tessera/desktop-updater/` wurde nie gelesen, kopiert oder ausgegeben; kein Secret-Wert erscheint in Code, Doku, Logs oder SUMMARY. +- SUMMARY nennt: die gewaehlte Vergleichsregel (gleiche Basis + Live → kein Update) samt Begruendung, die Timeout-Trennung (15 s Pruefung / 600 s Download), die Entscheidung zur `http`-Anzeige (Menuetext statt Benachrichtigung), die Secrets auf Schritt-Ebene, und die vier offenen Nachweise fuer den Orchestrator. + + + +Create `/home/vicolab/projects/tessera-ctl/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md` when done + diff --git a/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-RESEARCH.md b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-RESEARCH.md new file mode 100644 index 0000000..7efeff7 --- /dev/null +++ b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-RESEARCH.md @@ -0,0 +1,298 @@ +# Quick 260917-kgc: Desktop-Client — Update in der App (tauri-plugin-updater) — Research + +**Researched:** 2026-09-17 +**Domain:** Tauri 2 Updater-Plugin (Rust-API), NSIS-Update-Modus, AppImage-Ersetzung, minisign-Signatur im Cross-Bau, Endpunkt in der NestJS-API +**Confidence:** HIGH fuer Plugin-/Bundler-/NSIS-Verhalten (Quelltext der installierten bzw. per `cargo fetch` geholten Crates gelesen), MEDIUM fuer den Cross-Bau des neuen TLS-Stacks (nur in der Pipeline beweisbar), LOW fuer SmartScreen-Verhalten des vom Updater gestarteten Installers + +Quellenkuerzel: `$REG` = `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f`. Gelesene Crate-Staende: `tauri-plugin-updater-2.11.0` (aktuellste 2.x, 2026-08-31; 3.0.0-alpha wird von `"2"` nicht gewaehlt), `tauri-2.11.3`, `tauri-utils-2.9.3`, `tauri-bundler-2.9.4` (die CLI 2.11.3 lockt `tauri-bundler 2.9.3`, laut `tauri-cli-2.11.3/Cargo.lock` Z. 6606-6607; 2.9.4 ist der Patch dazu, der NSIS-Teil ist identisch aufgebaut), `tauri-cli-2.11.3` (Tarball von crates.io, Scratchpad), `reqwest-0.13.5`, `ring-0.17.14`, `semver-1`. Repo unveraendert (nur diese Datei). + +## Summary + +Das offizielle `tauri-plugin-updater` 2.11.0 deckt genau den gewuenschten Ablauf ab, komplett von Rust aus: `app.updater_builder().endpoints(vec![url])?.version_comparator(..).build()?.check().await` liefert `Option`; `update.download_and_install(on_chunk, on_finish).await` laedt die Datei komplett in den Speicher, prueft die minisign-Signatur gegen `plugins.updater.pubkey`, und startet unter Windows den NSIS-Installer mit `/P /UPDATE /R /ARGS …` und beendet den eigenen Prozess per `std::process::exit(0)` — der Installer startet die App danach selbst neu (`.onInstSuccess` → `RunAsUser`). Unter Linux ersetzt das Plugin die laufende AppImage-Datei an Ort und Stelle (Pfad aus `APPIMAGE`), danach muss der Client selbst `app.restart()` rufen. Das Tray-Menue/`prevent_close`-Muster ist seit Tauri-PR #12313 (`RESTART_EXIT_CODE`) kein Hindernis mehr; unser Run-Handler laesst `code: Some(..)` bereits durch. + +Zwei harte Vorgaben ergeben sich aus dem Quelltext: (1) `plugins.updater.pubkey` MUSS in `tauri.conf.json` stehen — sonst bricht sowohl der Bau (`createUpdaterArtifacts: true` → CLI: „plugins > updater doesn't exist") als auch der App-Start ab (Plugin-Config-Deserialisierung, `pubkey: String` ohne Default). `endpoints` darf dagegen fehlen (`#[serde(default)]`) und wird zur Laufzeit gesetzt. (2) Mit `createUpdaterArtifacts: true` und gesetztem `pubkey` verlangt die CLI beim `tauri build` zwingend `TAURI_SIGNING_PRIVATE_KEY` (Inhalt ODER Pfad) — ohne Schluessel bricht der Bau ab; der Ausweg fuer lokale Baue ist `tauri build --no-sign` (dann entsteht keine `.sig`). Die `.sig`-Dateien entstehen host-unabhaengig in der CLI (reines Rust/minisign), also auch im `cargo-xwin`-Cross-Bau. + +Der Versionsvergleich ist der eigentliche Fallstrick: `RemoteRelease.version` ist bereits ein `semver::Version` (kein Rohstring), `1.2.0-beta.38c1400` ist gueltig, aber `1.2.0-beta.0123456` NICHT (fuehrende Null in numerischem Prerelease-Identifier → Deserialisierung schlaegt fehl, Check liefert Err). Deshalb Commit-Stempel immer mit Praefix: `1.2.0-beta.g38c1400` (wie `git describe`). `version_comparator` ersetzt den Standardvergleich (`release.version > current`) vollstaendig. + +**Primary recommendation:** `tauri-plugin-updater = "2"` mit Standard-Features (rustls+ring, Plattform-Zertifikatspruefung) einbauen; neuer API-Endpunkt `GET /desktop/update?target=&arch=¤t=&base=` (dynamisches Format, absolute `url` aus validiertem `base`, 204 ohne signiertes Paket); `desktop-collect.sh` schreibt den `.sig`-Inhalt als Feld `signature` ins Manifest; Version im Manifest bleibt `X.Y.Z`, der Endpunkt bildet `X.Y.Z` (live) bzw. `X.Y.Z-beta.g` (beta); eigener `version_comparator` (Basisversion groesser ODER Beta-Stempel verschieden). Schluesselpaar per `tauri signer generate -w`, privater Schluessel + Passwort als Gitea-Secrets (per `PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{name}`, in Gitea 1.26.2 vorhanden), oeffentlicher Schluessel in `tauri.conf.json`. + +## Antworten auf die acht Fragen + +### 1. Plugin-API (Rust) + +- `UpdaterExt` ist fuer jeden `Manager` implementiert (`App`, `AppHandle`, Fenster): `fn updater_builder(&self) -> UpdaterBuilder` und `fn updater(&self) -> Result` [VERIFIED: `$REG/tauri-plugin-updater-2.11.0/src/lib.rs:58-121`]. `updater_builder()` haengt automatisch an: Windows `current_exe_args` (fuer `/ARGS`), Linux `executable_path(APPIMAGE)` (Z. 108-112), `on_before_exit(|| app_handle.cleanup_before_exit())` (Z. 116-118). +- `UpdaterBuilder`: `version_comparator(Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static)` (Z. 211), `endpoints(Vec) -> Result` (Z. 224, validiert https), `header(k, v) -> Result`, `headers(HeaderMap)`, `timeout(Duration)`, `pubkey(..)`, `installer_arg(s)`, `restart_after_install(bool)` (Windows, Default `true`), `configure_client(Fn(reqwest::ClientBuilder) -> ClientBuilder)`, `build() -> Result`; `build()` gibt `Error::EmptyEndpoints`, wenn weder Laufzeit- noch Config-Endpunkte da sind (Z. 365-371) [VERIFIED: `updater.rs:211-388`]. +- `Updater::check(&self).await -> Result>` (Z. 432). `Update` (pub-Felder): `body: Option`, `current_version: String`, `version: String`, `date: Option`, `target: String`, `download_url: Url`, `signature: String`, `raw_json: serde_json::Value`, `timeout`, `proxy`, `no_proxy`, `headers` (Z. 642-673). `Update: Clone + Resource` (also `Send + Sync`) — kann in `app.manage(Mutex>)` liegen [VERIFIED: `updater.rs:642-676`]. +- `update.download(on_chunk: FnMut(usize, Option), on_finish: FnOnce()) -> Result>` (ganze Datei im Speicher, danach `verify_signature`, Z. 680-742); `update.install(bytes)`; `update.download_and_install(on_chunk, on_finish).await` (Z. 761-768). Doc-Kommentar: „Windows: This function exits the app after launching the updater installer successfully — macOS / Linux: You need to relaunch the app" (Z. 754-760) [VERIFIED]. +- `RemoteRelease { version: semver::Version, notes: Option, pub_date: Option, data: RemoteReleaseInner }` mit `RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { url: Url, signature: String })` oder `Static { platforms: HashMap }` (Z. 70-96) [VERIFIED]. +- `plugins.updater.endpoints` in `tauri.conf.json` ist optional (`#[serde(default)] pub endpoints: Vec`, `config.rs:136`); Laufzeit-`endpoints()` ersetzt die Config-Liste (`build()`: `self.endpoints.unwrap_or_else(|| config.endpoints)`, `updater.rs:366-368`) [VERIFIED]. +- `plugins.updater.pubkey` ist Pflicht: `pub pubkey: String` ohne Default (`config.rs:137`). Fehlt `plugins.updater` ganz, uebergibt Tauri `JsonValue::Null` (`$REG/tauri-2.11.3/src/plugin.rs:1007`, `unwrap_or_default()`) → `serde_json::from_value` scheitert → „Error deserializing 'plugins.updater' within your Tauri configuration" (`plugin.rs:800-805`) → unser `.build(...).expect(..)` in `lib.rs:287-288` panict beim Start. Zusaetzlich verlangt die CLI beim Bau mit `createUpdaterArtifacts != false` den Block (`tauri-cli-2.11.3/src/interface/rust.rs:855-870`: „failed to get updater configuration: plugins > updater doesn't exist") [VERIFIED]. +- Capabilities: Die Permissions (`updater:default` = `allow-check`, `allow-download`, `allow-install`, `allow-download-and-install`, `$REG/tauri-plugin-updater-2.11.0/permissions/default.toml`) gaten nur die vier `#[tauri::command]`-Handler fuer JS (`lib.rs:236-241`). Der Rust-Aufruf ueber `UpdaterExt` laeuft am ACL vorbei — `capabilities/default.json` bleibt unveraendert [VERIFIED]. + +### 2. Antwortformat des Endpunkts + +- Dynamisches Format: JSON mit `version` (alias `name`), optional `notes`, optional `pub_date` (RFC 3339, sonst Deserialisierungsfehler), `url`, `signature`; fehlt `platforms`, wird `url`+`signature` verlangt („the `url` field was not set on the updater response") [VERIFIED: `updater.rs:1454-1497`]. HTTP 204 → `Ok(None)` (Z. 531-534). Andere Nicht-2xx-Status werden nur geloggt; ohne parsebare Antwort endet `check()` mit `Error::ReleaseNotFound` (Z. 573) [VERIFIED]. Doku: 204 „No Content", Felder `url`/`version`/`signature` Pflicht [CITED: https://v2.tauri.app/plugin/updater/]. +- `url` ist typisiert `url::Url` → muss absolut sein (relative Pfade wie `/desktop/download/windows` scheitern beim Parsen) [VERIFIED: `updater.rs:72-76`]. Unser `/desktop/latest` liefert heute bewusst relative URLs (`desktop.service.ts`, `getLatest()`) — fuer den Updater braucht es einen eigenen Endpunkt mit absoluter URL (siehe Frage 6). +- `version` muss gueltiges SemVer sein; ein fuehrendes `v` wird abgeschnitten (`parse_version`, Z. 1514-1521). `version_comparator` bekommt den **geparsten** `semver::Version` (kein Rohstring); Prerelease liegt in `release.version.pre`. Probe (Scratchpad, semver 1.x): `1.2.0-beta.38c1400` OK, `1.2.0-beta.1234567` OK, `1.2.0-beta.0123456` → `Err("invalid leading zero in pre-release identifier")`, `1.2.0-beta.g0123456` OK, `1.2.0-beta.g38c1400 < 1.2.0` = true [VERIFIED: Probe-Ausgabe, `scratchpad/semver-probe`]. Ein 7-stelliger Git-SHA kann rein numerisch mit fuehrender Null sein (≈0,4 % der Commits) → **immer `g`-Praefix**. +- Client-Version: `current_version = app.package_info().version` (`updater.rs:197`), also `1.2.0` aus `tauri.conf.json` (`desktop-version.sh` schreibt reines X.Y.Z, D-07) [VERIFIED]. +- Platzhalter in der Endpunkt-URL: `{{current_version}}`, `{{target}}`, `{{arch}}`, `{{bundle_type}}` — werden sowohl im Pfad (URL-kodiert `%7B%7B…%7D%7D`) als auch in Query-Parametern ersetzt (`updater.rs:473-486`). Werte: `target` = `linux` | `darwin` | `windows`; `arch` = `i686` | `x86_64` | `armv7` | `aarch64` | `riscv64`; `bundle_type` = `nsis` | `appimage` | `msi` | `deb` | `rpm` | `app` | `unknown` (`updater.rs:1395-1421`, `installer_for_bundle_type`). Windows x64 → `windows`/`x86_64`, AppImage x64 → `linux`/`x86_64` [VERIFIED]. +- Der Request traegt `Accept: application/json` (Check) bzw. `application/octet-stream` (Download) und User-Agent `tauri-plugin-updater/2.11.0`; der Download liest `Content-Length` fuer die Fortschrittsanzeige (`updater.rs:434-437, 687-690, 722-727`) [VERIFIED]. Unser `StreamableFile` setzt `length: entry.size` → `Content-Length` vorhanden [VERIFIED: `apps/api/src/desktop/desktop.controller.ts`, `download()`]. + +### 3. Artefakte und Signatur + +- `bundle.createUpdaterArtifacts: true` (Typ `Updater::Bool`; `"v1Compatible"` ist `Updater::String`, `$REG/tauri-utils-2.9.3/src/config.rs:1532-1571`). Im v2-Modus erzeugt der Bundler fuer NSIS/AppImage **kein** Zip/Tar mehr („Self contained updater, no need to zip", `$REG/tauri-bundler-2.9.4/src/bundle.rs:206-239`); der NSIS-Installer wird einmal mit `updater=false` gebaut (`bundle.rs:178`) — die normale `Tessera_X.Y.Z_x64-setup.exe` IST das Update-Artefakt, ebenso `Tessera_X.Y.Z_amd64.AppImage` [VERIFIED]. +- Signatur passiert in der CLI nach dem Buendeln (`tauri-cli-2.11.3/src/bundle.rs:221, 226-314`, `sign_updaters`): fuer jedes Bundle vom Typ Nsis/Msi/AppImage/Deb/Rpm/Updater wird `.sig` daneben geschrieben (`helpers/updater_signature.rs:117-160`: Extension + `.sig`, Inhalt = Base64 der minisign-Signaturbox). Ergebnis: `target/x86_64-pc-windows-msvc/release/bundle/nsis/Tessera_1.2.0_x64-setup.exe.sig` und `target/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage.sig` [VERIFIED]. Kein `cfg(windows)`/Host-Gating in `sign_updaters` → im `cargo-xwin`-Cross-Bau entsteht die `.sig` genauso [VERIFIED: Code-Lesung; Pipeline-Nachweis steht aus]. +- Umgebung: `TAURI_SIGNING_PRIVATE_KEY` — Wert ist Inhalt ODER Pfad (Code prueft `Path::exists()`, `bundle.rs:277-289`); `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` optional — fehlt sie, gilt mit `--ci`/`CI`-Umgebung leeres Passwort, sonst interaktive Abfrage (`bundle.rs:272-275, 290-292`). Die vom Generator ausgegebene Variable `TAURI_SIGNING_PRIVATE_KEY_PATH` (`signer/generate.rs:60`) wird im Bau-Code NICHT gelesen — nur `TAURI_SIGNING_PRIVATE_KEY` [VERIFIED]. `pubkey` in `tauri.conf.json` darf Inhalt oder Dateipfad sein (`bundle.rs:261-269`); beim Signieren warnt die CLI, wenn `keynum` von privatem und oeffentlichem Schluessel nicht zusammenpassen (Z. 304-306) [VERIFIED]. +- Generator: `pnpm --filter @tessera/desktop exec tauri signer generate -w [-p ] [--ci] [--force]` → schreibt `` (privat) und `.pub` (`signer/generate.rs:14-49`, `updater_signature.rs:61-72`) [VERIFIED]. Doku-Form: `npm run tauri signer generate -- -w ~/.tauri/myapp.key` [CITED: v2.tauri.app/plugin/updater/]. +- Bau ohne Schluessel in der Umgebung, aber `createUpdaterArtifacts: true` + `pubkey` gesetzt → **Abbruch**: „A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable." (`bundle.rs:277-279`). Ausweg: `tauri build --no-sign` (`build.rs:81-88`, `bundle.rs:255-258`: „Updater signing is skipped due to --no-sign flag") → keine `.sig` [VERIFIED]. Konsequenz fuer das Konzept „ohne Schluessel → Feld fehlt → 204": funktioniert nur mit `--no-sign` (lokale Proben, CI-Fallback), nicht durch blosses Weglassen der Variable. + +### 4. Windows-Installation durch den Updater + +- Ablauf `install_inner` (`updater.rs:835-877`): Bytes in `%TEMP%\Tessera--updater-\Tessera--installer.exe` schreiben (`make_temp_dir`/`write_to_temp`, Z. 956-1020; Ordner bleibt liegen, `.keep()`), `on_before_exit` → `cleanup_before_exit()` (Tray-Icons leeren, Fenster verstecken, `$REG/tauri-2.11.3/src/app.rs:1108-1120`), dann `ShellExecuteW(NULL, "open", , , SW_SHOW)`; Fehler (<=32) wird zurueckgegeben; sonst `std::process::exit(0)` [VERIFIED]. +- Parameter (`updater_parameters`, Z. 879-907): `nsis_args(install_mode)` + `/UPDATE` + bei `restart_after_install` (Default `true`) `/R` (nicht bei `basicUi`) + `/ARGS ` + `installerArgs` aus Config. `installMode`: `passive` → `/P` (Default), `quiet` → `/S`, `basicUi` → keine Flags (`config.rs:41-56`; Config-Schluessel `plugins.updater.windows.installMode` / `installerArgs`, camelCase, Z. 80-92) [VERIFIED]. Doku: passive = kleines Fenster mit Fortschrittsbalken, quiet = keine Rueckmeldung [CITED: v2.tauri.app/plugin/updater/]. +- NSIS-Template (`$REG/tauri-bundler-2.9.4/src/bundle/windows/nsis/installer.nsi`): `.onInit` liest `/P`, `/NS`, `/UPDATE` (Z. 477-491); im Update-Modus wird bei gleicher/hoeherer Version ohne Deinstallation direkt installiert (`PageLeaveReinstall`, Z. 318-321 „In update mode, always proceeds without uninstalling"); Startmenue-/Desktop-Verknuepfungen werden im Update-Modus nicht neu angelegt (Z. 937-941, 966-970); Registry-Werte bleiben erhalten (Z. 863-864); `.onInstSuccess` startet die App nur bei `/P` oder `/S` und nur mit `/R`: `nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0"` mit `$R0` = Wert hinter `/ARGS` (Z. 743-754) [VERIFIED]. → **`app.restart()` ist unter Windows nicht noetig und wird nie erreicht** (Prozess endet in `install`); unter Linux ist es Pflicht (Frage 5). Der Aufruf nach `download_and_install` ist trotzdem korrekt, weil plattformuebergreifend harmlos. +- Laufender Prozess/Tray: `CheckIfAppIsRunning` (`utils.nsh:22-62`) sucht `tessera-desktop.exe` — bei `INSTALLMODE == currentUser` per `FindProcessCurrentUser`, killt ohne Rueckfrage bei `/P` oder `/S` (`KillProcessCurrentUser`, Sleep 500 ms). Da der Updater den Prozess bereits per `exit(0)` beendet hat, greift das nur im Rennen; ein verstecktes Fenster oder das Tray-Symbol spielen keine Rolle (Prozess-, nicht Fenster-Suche) [VERIFIED]. +- `installMode: currentUser` (unser `tauri.conf.json:48`): Installer laeuft ohne UAC im Nutzerkontext, `ShellExecuteW "open"` verlangt keine Erhoehung; `SetContext`/`SHCTX` bleibt HKCU (`installer.nsi:105-112`) [VERIFIED: Template; Bedienprobe auf der Windows-VM ist der Nachweis]. +- Bekannte Faelle: tauri#11392 („App::restart does not restart after update.download_and_install", Tray + `prevent_close`) wurde durch tauri PR #12313 (`RESTART_EXIT_CODE`, `restart_on_exit`) behoben [CITED: https://github.com/tauri-apps/tauri/issues/11392, https://github.com/tauri-apps/tauri/pull/12313]; im installierten Tauri 2.11.3 enthalten: `restart()` von einem Nebenthread setzt `restart_on_exit` und ruft `request_exit(RESTART_EXIT_CODE)` (= `i32::MAX`), was `RunEvent::ExitRequested { code: Some(i32::MAX) }` ausloest (`app.rs:77, 588-611, 1434-1437`) — unser Handler in `lib.rs:296-301` blockt nur `code: None` → Neustart geht durch [VERIFIED]. tauri#7560 („NSIS quiet update do not restart") ist „closed as not planned" (2023, v1) — mit `/R` im heutigen Template gegenstandslos [CITED: https://github.com/tauri-apps/tauri/issues/7560]. `window-state` speichert beim harten `exit(0)` unter Windows nicht (kein `RunEvent::Exit`) — Fensterposition kann nach einem Update einmal verloren gehen [ASSUMED, aus Plugin-Semantik abgeleitet]. + +### 5. Linux AppImage + +- `updater_builder()` setzt `executable_path` auf `app.env().appimage` (= Umgebungsvariable `APPIMAGE`, `$REG/tauri-utils-2.9.3/src/lib.rs:269-287`); `build()` nimmt unter Linux diesen Pfad als `extract_path` (`updater.rs:373-379`) [VERIFIED]. +- `install_appimage` (`updater.rs:1047-1118`): sucht ein temporaeres Verzeichnis **auf demselben Dateisystem** wie die AppImage (Reihenfolge `std::env::temp_dir()`, `dirs::cache_dir()` = `~/.cache`, Elternordner der AppImage), verschiebt die laufende Datei per `rename` als Sicherung dorthin, schreibt die neuen Bytes unter dem alten Pfad, uebernimmt die alten Rechte (Ausfuehrbit), stellt bei Fehler die Sicherung zurueck; passt kein Tempordner → `Error::TempDirNotOnSameMountPoint` [VERIFIED]. Braucht also Schreibrecht auf Datei UND Ordner. Laeuft die AppImage aus `~/Downloads`, ist das gegeben (Tempordner `~/.cache` liegt auf demselben Dateisystem wie `$HOME`; ist `/tmp` ein tmpfs, scheitert nur der erste Kandidat). Bei einer AppImage unter `/opt` ohne Schreibrecht schlaegt das Update fehl — Fehlertext im Tray zeigen. +- Neustart: `app.restart()` → `tauri::process::restart` → `current_binary` liefert unter Linux **nur** den `APPIMAGE`-Pfad (`$REG/tauri-2.11.3/src/process.rs:48-56, 74-89`), startet also die neue Datei und beendet den alten Prozess [VERIFIED]. Der alte Prozess haelt die geloeschte Inode offen — unkritisch. +- Ohne `APPIMAGE` (nackte Binary aus `target/release/`, `tauri dev`) faellt `extract_path` auf `current_exe()` und wuerde die Binary ueberschreiben — Update-Pfad im Dev-Modus nicht ausloesen (nur `check()` testen). + +### 6. Integration bei uns + +- **Absolute Download-URL:** Die API sieht die Anfrage ueber NPM → Next.js-Rewrite (`apps/web/next.config.ts:34-43`, Ziel `http://api:3001`). Next' Proxy setzt `x-forwarded-host: req.headers.host` (`node_modules/.pnpm/next@15.5.19_*/node_modules/next/dist/server/lib/router-utils/proxy-request.js:26-36`) [VERIFIED], das Schema (`x-forwarded-proto`) kaeme nur aus NPMs Header-Vorlage [ASSUMED]. Verlaesslicher und ohne Proxy-Annahme: **der Client haengt `base=` an** (er kennt sie aus dem Store), die API validiert (`URL`-Parse, nur `http`/`https`, keine Credentials, nur Origin uebernehmen, Pfad verwerfen) und bildet `url = ${origin}/api-proxy/desktop/download/${platform}`. Reflektierte Eingabe ist unkritisch: der Client verifiziert die Signatur, eine fremde URL kann nur zu einem fehlgeschlagenen Download fuehren. Alternative ohne API-Aenderung: `Update.download_url` ist ein `pub`-Feld und darf nach `check()` vom Client auf `api_url(server, "/desktop/download/")` gesetzt werden (`updater.rs:655`) [VERIFIED] — als Notnagel dokumentieren, nicht als Hauptweg. +- **Neuer Endpunkt** (statt `/desktop/latest` zu aendern, das die Web-UI weiter mit relativen URLs nutzt): `GET /desktop/update?target=&arch=¤t=&base=` (`@Public()`, statische Route VOR `download/:platform` — Route-Order-Falle, Memory `project_nest_route_order.md`). Logik: `target` per Whitelist auf Plattform (`windows`→`windows`, `linux`→`linux`), `arch` muss `x86_64` sein (sonst 204); Manifest lesen; fehlt Eintrag oder `signature` → **204**; sonst 200 mit `{ version, pub_date: buildTime, notes: "channel=;commit=", url, signature }`, `version` = `manifest.version` (live) bzw. `${manifest.version}-beta.g${manifest.commit}` (beta). Die 204-Entscheidung „gleicher Stand" bleibt beim Client-Comparator (die API kennt den Client-Commit nicht; den Placeholder `{{current_version}}` nur zum Loggen mitschicken). Optional zusaetzlich `&commit=` und die API antwortet 204 bei gleichem Commit — spart einen Download-Link, aendert an der Sicherheit nichts. +- **`desktop-collect.sh`:** neben `*.exe`/`*.AppImage` die zugehoerige `*.sig` suchen (`find -name '*-setup.exe.sig'` / `'*.AppImage.sig'`; die bestehenden `-name '*.exe'`/`'*.AppImage'`-Zaehler matchen `.sig` nicht) und den Dateiinhalt (einzeilige Base64, ~200 Zeichen) per `--arg linuxSig "$(cat …)"` als `files..signature` in `manifest.json` schreiben; Datei selbst nicht kopieren (kein Nutzen, die API liefert JSON). Fehlt die `.sig` (Bau mit `--no-sign`), Feld weglassen und eine Warnzeile loggen; bei `GITHUB_REF` = Tag oder `main` hart abbrechen (Signatur ist dort Pflicht). `desktop.service.ts`: `isValidManifestFileEntry` um optionales `signature: string` erweitern, `DesktopManifestFile` in `packages/shared/src/index.ts:29-33` ebenso. +- **Skip-Mechanismus (`desktop-stamp.sh check`):** prueft Groesse/sha256 der beiden Dateien und liest die Manifest-Felder — das `signature`-Feld liegt im gecachten Manifest und wird mit uebernommen; keine `.sig`-Datei zu pruefen. Zwei Ergaenzungen: `check` verlangt fuer beide Plattformen ein nicht-leeres `files.

.signature` (alter Cache-Stand ohne Signatur → `no_reuse`), und ein Schluesselwechsel wird automatisch zum Neubau, weil `pubkey` in `apps/desktop/src-tauri/tauri.conf.json` liegt und `apps/desktop` Teil von `DESKTOP_PATHS` ist (`desktop-stamp.sh:50`) [VERIFIED]. Die Signatur gilt fuer die Bytes der `-setup.exe`; `cp` in `desktop-collect.sh` aendert nichts daran — niemals nachtraeglich signieren/patchen. +- **Gitea-Secrets per API:** Gitea 1.26.2 (`curl localhost:3002/api/v1/version`) bietet `PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{secretname}` mit Body `{"data": "", "description": ""}`; Antwort 201 (angelegt) / 204 (aktualisiert) [VERIFIED: `localhost:3002/swagger.v1.json`, `CreateOrUpdateSecretOption`, `required: ["data"]`; CITED: https://docs.gitea.com/api/1.26/operations/update-repo-secret/]. Zugriff: `reqToken()` + `reqOwner()` (Token-Inhaber muss Repo-Eigentuemer sein; Kategorie `repository` → `write:repository`) [VERIFIED: `routers/api/v1/api.go` (release/v1.26) Z. 947-958, 1225]. Der vorhandene `REGISTRY_TOKEN` hat `repository: write` (docs/ci-cd-setup.md Z. 88) — genuegt, sofern er dem Repo-Eigentuemer `schalli` gehoert. Aufruf vom Dev-Host ueber `localhost:3002` (Memory `project_ci_registry_push.md`), nie ueber `git.vicolab.de`. Secrets: `TAURI_SIGNING_PRIVATE_KEY` (Dateiinhalt, eine Base64-Zeile) und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (Schluessel MIT Passwort erzeugen — ein leerer Secret-Wert ist in Gitea nicht sicher moeglich [ASSUMED]; `CI=true` als Fallback fuer leeres Passwort setzt act_runner wie GitHub [ASSUMED]). +- **ci.yml:** Job `desktop` bekommt `env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}` und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.… }}` an beiden `tauri build`-Schritten. Der Skip-Pfad braucht die Secrets nicht. `publish-release.sh` kann die `.sig` optional als Release-Anhang mitgeben — nicht noetig, das Manifest traegt sie. + +### 7. Versionsvergleich Beta + +- `version_comparator` ersetzt den Standard vollstaendig: `let should_update = match self.version_comparator { Some(c) => c(self.current_version.clone(), release.clone()), None => release.version > self.current_version }` (`updater.rs:576-579`) [VERIFIED]. Ohne eigenen Vergleich gilt SemVer: `1.2.0-beta.g38c1400 < 1.2.0` (Probe) → ein Beta-Client (Version `1.2.0`) saehe nie einen neueren Beta-Bau. Standardverhalten nur fuer Live sinnvoll. +- Empfohlene Logik (spiegelt `lib.rs:259-261`, D-07/WR-02): + ```rust + // Source: eigene Ableitung aus updater.rs:576-579 + semver-Probe + fn is_newer(current: &semver::Version, remote: &semver::Version, app_commit: &str) -> bool { + let base = |v: &semver::Version| (v.major, v.minor, v.patch); + if base(remote) > base(current) { return true; } + if base(remote) < base(current) { return false; } + // gleiche X.Y.Z: Beta-Stempel "beta.g" vs. env!("APP_COMMIT") + match remote.pre.as_str().strip_prefix("beta.g") { + Some(sha) => !app_commit.is_empty() && sha != app_commit, + None => false, // Live, gleiche Version: kein Update + } + } + ``` + `current` ist immer reines X.Y.Z (D-07) — `current.pre` ist leer, darum genuegt der Tupelvergleich. `app_commit` = `env!("APP_COMMIT")` (7-stellig, `build.rs:18-32`) und `manifest.commit` = `git rev-parse --short=7` (`desktop-collect.sh:78`) — gleiches Format [VERIFIED]. Leerer `APP_COMMIT` (Quell-Tarball) → nur Versionsvergleich, wie heute. +- Reine Funktion in `lib.rs` + Tests (Basis groesser, Basis kleiner, gleiche Basis/anderer Stempel, gleicher Stempel, Live ohne Pre, leerer Commit) — wie die bestehenden `mod tests`. + +### 8. Gotchas (Abhaengigkeiten, TLS, Groesse, Zertifikate) + +- Plugin-Abhaengigkeiten: `reqwest = "0.13"` (Features `json`, `stream`, default-features = false), Standard-Features `rustls-tls` (= `reqwest/rustls-no-provider` + `rustls 0.23` mit `ring`), `system-proxy`, `zip`; `tauri = "2.10"` (unser 2.11.3 passt) [VERIFIED: `$REG/tauri-plugin-updater-2.11.0/Cargo.toml:66-116`]. Unser `reqwest = "0.12"` bleibt daneben bestehen → zwei reqwest-Majors und zwei TLS-Stacks (native-tls/schannel bzw. OpenSSL + rustls/ring) im Binary. Mehr Bauzeit/Groesse (Groessenordnung wenige MB [ASSUMED]), funktional unproblematisch. `rustls-no-provider` zieht `rustls-platform-verifier` (`$REG/reqwest-0.13.5/Cargo.toml:106-109`) → Zertifikatspruefung ueber den Betriebssystem-Speicher (Windows-Zertifikatspeicher, Linux CA-Bundle; das Plugin setzt unter Linux notfalls `SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt`, `updater.rs:440-448`) [VERIFIED] — Let's-Encrypt-Zertifikate von alpha/live werden akzeptiert. +- Cross-Bau-Risiko: `ring 0.17.14` ist neu im Abhaengigkeitsgraphen. Das Crate liefert fuer `x86_64-pc-windows-msvc` vorassemblierte `.o`-Objekte mit (`$REG/ring-0.17.14/build.rs:340-349, 430-445`, `pregenerated/*-x86_64-nasm.o`) — kein `nasm` noetig; die C-Teile baut `cc` mit dem Clang aus `cargo-xwin` [VERIFIED: Code-Lesung; der tatsaechliche xwin-Bau ist nur in der Pipeline beweisbar, `cargo-xwin` ist auf dem Dev-Host nicht installiert]. Fallback bei Bauproblemen: `tauri-plugin-updater = { version = "2", default-features = false, features = ["native-tls", "system-proxy"] }` (schannel unter Windows, OpenSSL unter Linux — `libssl-dev` steht bereits in der apt-Liste, ci.yml Z. 120-125). **Nicht** das Projekt-`reqwest` auf 0.13 heben: dessen Default `default-tls = rustls` zieht `aws-lc-rs` (cmake/nasm-Bau), ein echtes xwin-Risiko [VERIFIED: `reqwest-0.13.5/Cargo.toml:45-51, 101-105`]. +- Kein `tauri-plugin-http` noetig; der Updater bringt seinen Client mit [VERIFIED: keine Abhaengigkeit in `Cargo.toml`]. +- Endpunkt-Schema: im Release-Bau wird `http://` abgelehnt (`Error::InsecureTransportProtocol`, `config.rs:160-179`) — `endpoints()` gibt dann `Err`; Fehler loggen, Tray-Eintrag gesperrt lassen. `dangerousInsecureTransportProtocol`/`dangerousAcceptInvalidCerts`/`dangerousAcceptInvalidHostnames` existieren als Config-Schalter (`config.rs:105-118`) — nicht setzen, nur im Handbuch als „nicht vorgesehen" nennen [VERIFIED]. Der Store erlaubt heute `http`-Adressen (`check_server`); ein Kunde mit `http://` bekommt einfach kein In-App-Update (Hinweis-Download bleibt). +- `download()` haelt die ganze Datei (~100 MB) im RAM, bevor sie geschrieben wird (`updater.rs:730-742`) [VERIFIED] — akzeptabel. Next' Rewrite-Proxy hat ein 30-s-**Inaktivitaets**-Timeout (`proxyTimeout` → `ClientRequest.setTimeout`, `next/dist/compiled/http-proxy`), kein Gesamtlimit — ein laufender Stream bricht nicht ab [VERIFIED]; NPM-Groessengrenzen wie in Kap. 10 des Betriebshandbuchs. +- SmartScreen: Die vom Updater geschriebene `…-installer.exe` erhaelt keine Mark-of-the-Web (kein Browser-Download, `std::fs::write` ohne `Zone.Identifier`) — voraussichtlich kein SmartScreen-Dialog beim In-App-Update, D-09 (keine Code-Signierung) bleibt bestehen [ASSUMED — auf der Windows-VM pruefen]. +- Windows-Reste: der Tempordner `%TEMP%\Tessera--updater-*` wird nicht aufgeraeumt (`.keep()`, Z. 956-964) — ~100 MB je Update; im Betriebshandbuch erwaehnen, kein Handlungsbedarf. +- `Update::install` unter Windows beendet den Prozess aus dem Tokio-Thread heraus; unser `RunEvent::ExitRequested`-Handler wird dabei nicht durchlaufen (kein `prevent_exit`-Konflikt) [VERIFIED: `updater.rs:876`]. + +## Architektur / Datenfluss + +``` +Client-Start / Serverwechsel (spawn_version_check, jn2) + └─ updater_builder().endpoints([ {server}/api-proxy/desktop/update?target={{target}}&arch={{arch}}¤t={{current_version}}&base={server} ]) + .version_comparator(is_newer(.., APP_COMMIT)).timeout(..).build()?.check().await + │ NPM → Next /api-proxy → API GET /desktop/update + │ manifest.json (files.

.signature vorhanden?) ── nein → 204 → Ok(None) + │ ja → 200 { version: X.Y.Z | X.Y.Z-beta.g, url: {base}/api-proxy/desktop/download/

, signature, pub_date, notes } + ├─ Some(update) → app.state::().set(update); Tray "update" = "Version … installieren" / "Neuen Beta-Stand installieren", enabled + └─ None/Err → Tray gesperrt (Err loggen) +Tray-Klick "update" + └─ update.download_and_install(|chunk,total| Tray-Text "… lädt 42 %", || Tray-Text "… wird installiert").await + ├─ Windows: %TEMP%\…installer.exe, ShellExecuteW "/P /UPDATE /R /ARGS", exit(0) → NSIS installiert, RunAsUser startet App neu + └─ Linux: AppImage in place ersetzt → app.restart() (RESTART_EXIT_CODE, laeuft an prevent_exit vorbei) + Fehler → Tray-Text zurueck + Benachrichtigung mit Fehlertext; Fallback bleibt der Browser-Download (heutiger Weg) +``` + +Aufsetzpunkt ist der Stand nach Quick jn2 (`TrayIconBuilder::with_id("main")`, `TrayItems { connected, update }`, `apply_server`, `spawn_version_check`): `spawn_version_check` tauscht den reqwest-Aufruf gegen `check()`, `TrayItems` bekommt keinen neuen Handle, aber `app.manage(PendingUpdate(Mutex>))`; der Menue-Zweig `"update"` startet den Download statt `opener`. Der Browser-Link (Einstellungen → Desktop-App) bleibt als zweiter Eintrag oder als Fallback bei Fehler erhalten — Produktentscheidung fuer den Plan. + +## Code-Skizzen + +### tauri.conf.json (Ergaenzung) +```json +// Source: v2.tauri.app/plugin/updater/ + config.rs:80-92 (Schluesselnamen camelCase) +"bundle": { "createUpdaterArtifacts": true, ... }, +"plugins": { + "updater": { + "pubkey": "", + "windows": { "installMode": "passive" } + } +} +``` +Kein `endpoints`-Eintrag (Laufzeit). `pubkey` ist oeffentlich und gehoert ins Repo. + +### Cargo.toml +```toml +# Source: $REG/tauri-plugin-updater-2.11.0/Cargo.toml (Features); Legitimitaet: OK (crates.io seit 2023, 361k/Woche, tauri-apps/plugins-workspace) +tauri-plugin-updater = "2" +``` +Per `cargo add tauri-plugin-updater@2` einpflegen (Cargo.lock konsistent, wie 18-04 mit opener). Cargo waehlt 2.11.0; `3.0.0-alpha.*` wird von `"2"` nicht erfasst. + +### lib.rs (Kern) +```rust +// Source: updater.rs:211-233, 365-388, 432, 761; lib.rs:58-121 (Plugin); Doku-Beispiel v2.tauri.app/plugin/updater/ +use tauri_plugin_updater::UpdaterExt; + +// in run(): .plugin(tauri_plugin_updater::Builder::new().build()) + +async fn check_for_update(app: &AppHandle, server: &str) -> tauri_plugin_updater::Result> { + let endpoint = format!( + "{}?target={{{{target}}}}&arch={{{{arch}}}}¤t={{{{current_version}}}}&base={}", + api_url(server, "/desktop/update"), + urlencoding_of(server) // percent-encodieren, z. B. mit url::form_urlencoded + ); + let app_commit = env!("APP_COMMIT"); + app.updater_builder() + .endpoints(vec![endpoint.parse()?])? // Err bei http:// im Release-Bau + .timeout(Duration::from_secs(15)) + .version_comparator(move |current, release| is_newer(¤t, &release.version, app_commit)) + .build()? + .check() + .await +} + +// Tray-Klick "update": +// let update = state.take(); update.download_and_install(|got, total| {..}, || {..}).await?; app.restart(); +``` +`{{target}}` usw. muessen wortwoertlich in der URL stehen — das Plugin ersetzt sie auch in Query-Parametern (`updater.rs:481-486`). + +### NestJS `GET /desktop/update` (Skizze) +```ts +// Source: eigene Ableitung; Formatvorgabe updater.rs:1454-1497 (Felder version/url/signature/pub_date/notes) +@Public() @Get('update') // VOR download/:platform registrieren (Route-Order) +update(@Query() q, @Res({ passthrough: true }) res) { + const platform = q.target === 'windows' ? 'windows' : q.target === 'linux' ? 'linux' : null; + const manifest = this.desktopService.getManifest(); + const entry = platform && manifest?.files[platform]; + const origin = this.desktopService.safeOrigin(q.base); // URL-Parse, http/https, nur origin + if (!entry?.signature || !origin || q.arch !== 'x86_64') { res.status(204); return; } + const version = manifest.channel === 'beta' ? `${manifest.version}-beta.g${manifest.commit}` : manifest.version; + return { version, pub_date: manifest.buildTime, notes: `channel=${manifest.channel};commit=${manifest.commit}`, + url: `${origin}/api-proxy/desktop/download/${platform}`, signature: entry.signature }; +} +``` + +### CI-Schluessel anlegen (einmalig, Dev-Host) +```bash +# Source: tauri-cli-2.11.3/src/signer/generate.rs:14-49; Gitea swagger.v1.json (updateRepoSecret) +pnpm --filter @tessera/desktop exec tauri signer generate -w ~/.tauri/tessera-desktop.key -p '' +# -> ~/.tauri/tessera-desktop.key (GEHEIM, ausserhalb des Repos) und ~/.tauri/tessera-desktop.key.pub (in tauri.conf.json) +curl -sS -X PUT -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \ + --data "$(jq -n --arg d "$(cat ~/.tauri/tessera-desktop.key)" '{data:$d}')" \ + http://localhost:3002/api/v1/repos/schalli/tessera-ctl/actions/secrets/TAURI_SIGNING_PRIVATE_KEY # 201/204 +``` +Verlust des privaten Schluessels = keine Updates mehr fuer installierte Clients („if you lose this key you will NOT be able to publish new updates" [CITED: v2.tauri.app/plugin/updater/]) — Sicherungsort im Betriebshandbuch festhalten (Kap. 10). Keine Passwort-Hinweise an den User (Memory `feedback_no_password_leak_warnings.md`). + +## Common Pitfalls + +1. **`pubkey` fehlt / Bau ohne Secret:** App startet nicht (Plugin-Config) bzw. Bau bricht ab („no private key"). Lokale Baue: `tauri build --no-sign`; die Skripte muessen den Fall „keine .sig" sauber als 204 abbilden, aber auf `main`/Tags hart abbrechen. +2. **Fuehrende Null im Commit-Stempel:** `1.2.0-beta.0123456` ist kein SemVer → `check()` liefert Err → nie ein Update. Immer `beta.g`. +3. **Relative `url`:** `url::Url` verlangt absolut; `/desktop/latest` bleibt fuer die Web-UI relativ, der Updater bekommt einen eigenen Endpunkt. +4. **Route-Shadowing:** `update` statisch VOR `download/:platform` (Unit-Tests fangen es nicht — Memory). +5. **`http://`-Server im Release:** `endpoints()` gibt Err — abfangen, nicht `dangerousInsecureTransportProtocol` setzen. +6. **Alter Cache-Stand ohne Signatur:** `desktop-stamp.sh check` muss `signature` je Plattform verlangen, sonst wird ein unsigniertes Paket uebernommen und der Endpunkt liefert dauerhaft 204. +7. **Dev-Modus (`tauri dev`, keine AppImage):** `download_and_install` wuerde die Binary im `target/` ueberschreiben — Update nur mit gebautem AppImage/Installer ausloesen. +8. **Zwei reqwest-Majors:** `reqwest 0.12` (Projekt) und `0.13` (Plugin) koexistieren; Projekt-`reqwest` NICHT auf 0.13 heben (aws-lc-rs im Cross-Bau). +9. **Endpunkt-Placeholder in der Query:** `{{target}}` darf nicht vorab URL-kodiert werden — als Rohtext in den String; `url::Url::parse` laesst `{`/`}` in der Query stehen und das Plugin ersetzt beide Schreibweisen (`updater.rs:481-486`). + +## Environment Availability + +| Abhaengigkeit | Benoetigt von | Verfuegbar | Version | Fallback | +|---|---|---|---|---| +| `cargo`/Rust (Dev-Host) | lokaler Bau/Tests | ✓ | cargo 1.96.0 | — | +| `cargo-xwin` (Dev-Host) | lokaler Cross-Check | ✗ | — | Nachweis in der Pipeline (Runner installiert es, ci.yml Z. 156-160) | +| `@tauri-apps/cli` | `tauri signer generate`, `--no-sign` | ✓ | 2.11.3 (pnpm) | — | +| Gitea API | Secrets anlegen | ✓ | 1.26.2 (`localhost:3002`) | UI Settings → Actions → Secrets | +| `tauri-plugin-updater` (crates.io) | Client | ✓ | 2.11.0, Legitimitaet OK | — | + +## Validation Architecture + +| Property | Value | +|---|---| +| Rust | `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` in `apps/desktop/src-tauri` (`CARGO_BUILD_JOBS=4`) | +| API | Vitest 3.2.6, `pnpm --filter @tessera/api test -- desktop` (Datei `apps/api/src/desktop/desktop.service.spec.ts` erweitern) | +| Skripte | lokale Probe `sh .gitea/scripts/desktop-collect.sh --require linux` nach `tauri build --bundles appimage` (mit Schluessel in der Umgebung) + `jq -e '.files.linux.signature' desktop-dist/manifest.json` | + +| Verhalten | Testart | Datei | +|---|---|---| +| `is_newer`: 6 Faelle (Basis >, <, gleich+anderer Stempel, gleicher Stempel, Live ohne Pre, leerer Commit) | unit (Rust) | `lib.rs mod tests` ❌ neu | +| Endpunkt-URL-Bildung mit Rohplatzhaltern und kodiertem `base` | unit (Rust) | `lib.rs mod tests` ❌ neu | +| `/desktop/update`: 204 ohne Manifest / ohne Signatur / falsche arch / ungueltiges base; 200 live vs beta-Version; `url` absolut aus `base` | unit (API) | `desktop.service.spec.ts` ❌ erweitern | +| Manifest-Validierung akzeptiert optionales `signature` | unit (API) | `desktop.service.spec.ts` ❌ erweitern | +| Ende-zu-Ende Linux: lokales AppImage (Version A) gegen lokale API mit Manifest (Version B) → Datei ersetzt, Neustart | manuell (Dev-Host, grafische Sitzung) | — | +| Ende-zu-Ende Windows: CI-Paket auf der Test-VM, Tray-Klick → Fortschritt → Installer passiv → App neu gestartet, Tray zurueck, Version geaendert; SmartScreen-Verhalten notieren | manuell (Orchestrator/User, Windows-VM 8233) | — | + +## Security Domain + +| ASVS | Gilt | Kontrolle | +|---|---|---| +| V5 Eingabevalidierung | ja | `target`/`arch` Whitelist, `base` nur `http(s)`-Origin ohne Credentials/Pfad; Dateiname weiterhin nur aus dem Manifest (T-18-01/02 bleiben) | +| V6 Kryptografie | ja | minisign (Ed25519) durch das Plugin — nie eigene Signaturpruefung; privater Schluessel nur als Gitea-Secret, `.key` nie ins Repo (`.gitignore`-Eintrag `*.key` unter `apps/desktop/`) | +| V10 Schadcode/Integritaet | ja | Signaturpruefung vor Installation (`updater.rs:740`); Transport nur https (Plugin-Zwang im Release) | + +| Muster | STRIDE | Mitigation | +|---|---|---| +| Manipuliertes Paket ueber MITM/kompromittierte API | Tampering | Signatur mit Offline-Schluessel; falsche Signatur → `Error::Minisign`, keine Installation | +| Offene Weiterleitung ueber `base` | Spoofing | Origin-Validierung; selbst bei Missbrauch nur fehlgeschlagener Download (Signatur) | +| Downgrade-Angriff (aelteres signiertes Paket) | Tampering | Comparator installiert nur hoehere Basisversion bzw. anderen Beta-Stempel; ein Angreifer braeuchte ohnehin den Schluessel | + +## Assumptions Log + +| # | Annahme | Abschnitt | Risiko | +|---|---|---|---| +| A1 | Der xwin-Cross-Bau von `ring`/`rustls` gelingt mit dem vorhandenen Clang-Setup | 8 | Pipeline rot → Fallback `native-tls`-Feature (dokumentiert) | +| A2 | Vom Updater gestartete Installer-EXE loest keinen SmartScreen-Dialog aus (keine MOTW) | 8 | Nur Bedienkomfort; Handbuchtext | +| A3 | act_runner setzt `CI=true` (leeres Passwort-Fallback) | 6 | Umgangen durch explizites Passwort-Secret | +| A4 | Gitea erlaubt keinen leeren Secret-Wert | 6 | Umgangen durch Schluessel mit Passwort | +| A5 | NPM setzt `X-Forwarded-Proto` | 6 | Irrelevant, weil `base` vom Client kommt | +| A6 | `window-state` speichert beim `exit(0)`-Update nicht | 4 | Kosmetik | +| A7 | Binary-Zuwachs „wenige MB" durch zweiten HTTP/TLS-Stack | 8 | Kosmetik; nach erstem CI-Bau messen | + +## Open Questions + +1. **Produktfrage:** Soll der Browser-Download-Eintrag im Tray neben dem In-App-Update bleiben (Fallback), oder nur bei Fehler erscheinen? Empfehlung: ein Eintrag, der bei Fehlschlag des In-App-Updates die Einstellungsseite oeffnet. +2. **Wo liegt die Sicherung des privaten Schluessels?** (Betriebshandbuch Kap. 10; nicht im Repo, nicht auf dem Runner.) +3. **Erster Rollout:** Bereits installierte Clients (1.2.0 ohne Plugin) koennen sich nicht selbst aktualisieren — einmal noch der Browser-Weg; im CHANGELOG nennen. + +## Sources + +### Primary (HIGH) +- `$REG/tauri-plugin-updater-2.11.0/src/{lib.rs,updater.rs,config.rs,error.rs}`, `Cargo.toml`, `permissions/default.toml`, `CHANGELOG.md` +- `$REG/tauri-2.11.3/src/{app.rs,plugin.rs,process.rs}`, `$REG/tauri-utils-2.9.3/src/{lib.rs,config.rs,platform.rs}` +- `$REG/tauri-bundler-2.9.4/src/bundle.rs`, `src/bundle/windows/nsis/{installer.nsi,utils.nsh,mod.rs}` +- `tauri-cli-2.11.3` (crates.io-Tarball): `src/bundle.rs`, `src/build.rs`, `src/interface/rust.rs`, `src/helpers/updater_signature.rs`, `src/signer/generate.rs`, `Cargo.lock` +- `$REG/reqwest-0.13.5/Cargo.toml`, `$REG/ring-0.17.14/build.rs` + `pregenerated/` +- Gitea 1.26.2 `http://localhost:3002/swagger.v1.json`; `go-gitea/gitea` `release/v1.26` `routers/api/v1/api.go` +- Repo: `apps/desktop/src-tauri/{src/lib.rs,tauri.conf.json,Cargo.toml,build.rs,capabilities/default.json}`, `.gitea/workflows/ci.yml`, `.gitea/scripts/{desktop-collect.sh,desktop-version.sh,desktop-stamp.sh,publish-images.sh}`, `apps/api/src/desktop/*`, `apps/web/next.config.ts`, `packages/shared/src/index.ts`, `.planning/quick/260917-jn2-*/260917-jn2-PLAN.md`, `18-RESEARCH.md`, `18-04-SUMMARY.md`, `docs/anleitung-betrieb.md` Kap. 10, `docs/ci-cd-setup.md` +- Probe: `scratchpad/semver-probe` (semver 1.x, Ausgabe oben) + +### Secondary (MEDIUM) +- https://v2.tauri.app/plugin/updater/ (Signierung, Config, Antwortformat, installMode, Rust-Beispiel) +- https://docs.gitea.com/api/1.26/operations/update-repo-secret/ +- https://github.com/tauri-apps/tauri/pull/12313, https://github.com/tauri-apps/tauri/issues/11392, https://github.com/tauri-apps/tauri/issues/7560 + +### Tertiary (LOW) +- WebSearch-Treffer zu NSIS/Tray-Problemen (nur zur Auffindung der oben genannten Issues genutzt) + +## Metadata + +- Standard stack: HIGH — Plugin-Quelltext gelesen, Legitimitaet OK +- Architecture: HIGH — Ablauf aus Code abgeleitet, Integrationspunkte im Repo gelesen +- Pitfalls: HIGH fuer SemVer/Config/NSIS (Code + Probe), MEDIUM fuer Cross-Bau, LOW fuer SmartScreen +- Research date: 2026-09-17 — gueltig ~30 Tage (Plugin 2.x stabil; 3.0.0-alpha laeuft parallel, wird von `"2"` nicht gewaehlt) diff --git a/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md new file mode 100644 index 0000000..70dfb49 --- /dev/null +++ b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md @@ -0,0 +1,249 @@ +--- +phase: quick-260917-kgc +plan: 01 +subsystem: desktop-client +tags: [tauri, rust, tauri-plugin-updater, minisign, nestjs, ci, gitea-actions, desktop] + +# Dependency graph +requires: + - phase: quick-260917-jn2 + provides: TrayItems { connected, update } im State, apply_server, spawn_version_check(app, server_url), stored_server_url als einzige Store-Lesestelle — hier angedockt, unveraendert uebernommen + - phase: quick-260917-jdh + provides: desktop-stamp.sh (Stempel/Skip) — `check` verlangt jetzt zusaetzlich updateVersion + Signaturen + - phase: 18-desktop-client-fertigstellen + provides: /desktop/latest, /desktop/download/:platform, manifest.json aus desktop-collect.sh, Run-Handler-Muster (code: None → prevent_exit) +provides: + - "Desktop-Client aktualisiert sich selbst: Pruefung ueber tauri-plugin-updater gegen GET /api-proxy/desktop/update, Tray-Klick laedt das signierte Paket (Fortschritt im Menuetext), prueft die minisign-Signatur gegen plugins.updater.pubkey, installiert (Windows NSIS passiv, Linux AppImage in place) und startet neu" + - "API GET /desktop/update?target=&arch=¤t=&base= (oeffentlich, vor download/:platform) im dynamischen Updater-Format; 400 bei unreinem base, 204 in allen 'kein Update'-Faellen" + - "Manifest traegt updateVersion (X.Y.Z bzw. X.Y.Z-beta.g) und je Plattform signature (Inhalt der .sig); CI signiert beide Bundles mit den Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD an genau den zwei tauri-build-Schritten" + - "Reine, getestete Helfer is_update_newer, beta_commit, update_endpoint, release_labels, update_labels (neue Texte 'Auf Version {v} aktualisieren' / 'Auf Beta-Stand {sha7} aktualisieren')" +affects: [desktop-client, updater, ci, api-desktop, docs] + +# Actuals (#2632) +actuals: + tokens: 66460 + tasks: 4 + commits: 4 + plan_head_before: 5a444ec8f21f9a50d78aa7c0c6ab629fa6ebd85a + +# Tech tracking +tech-stack: + added: + - "tauri-plugin-updater 2.11.0 (Cargo.lock; zieht reqwest 0.13 + rustls/ring + rustls-platform-verifier neben dem Projekt-reqwest 0.12)" + - "semver 1.0.28 als direkte Abhaengigkeit (lag bereits transitiv im Lock)" + patterns: + - "Updater-Kette komplett in Rust: app.updater_builder().endpoints(vec![url])?.timeout(15 s).version_comparator(is_update_newer …).build()?.check().await → PendingUpdate(Mutex>) → Tray-Klick take() → download_and_install(on_chunk, on_finish) → app.restart(); capabilities/default.json unveraendert (Rust-Aufrufe laufen am ACL vorbei)" + - "Endpunkt-URL zur Laufzeit aus stored_server_url mit Roh-Platzhaltern {{target}}/{{arch}}/{{current_version}} als Query-Werte (query_pairs_mut kodiert, das Plugin ersetzt beide Schreibweisen) plus base= fuer die absolute Rueckgabe-URL" + - "NestJS 204 ohne Body: @Res({ passthrough: true }) + res.status(204) + return undefined" + - "Vertrauenskette: CI signiert (Secret nur am Bau-Schritt) → .sig → desktop-collect.sh → manifest.signature → API reicht durch → Plugin prueft VOR der Installation; nie eine Installation ohne gueltige Signatur" + +key-files: + created: [] + modified: + - apps/desktop/src-tauri/Cargo.toml + - apps/desktop/src-tauri/Cargo.lock + - apps/desktop/src-tauri/tauri.conf.json + - apps/desktop/src-tauri/src/lib.rs + - apps/desktop/src-tauri/gen/schemas/acl-manifests.json + - apps/desktop/src-tauri/gen/schemas/desktop-schema.json + - apps/desktop/src-tauri/gen/schemas/linux-schema.json + - packages/shared/src/index.ts + - apps/api/src/desktop/desktop.service.ts + - apps/api/src/desktop/desktop.controller.ts + - apps/api/src/desktop/desktop.service.spec.ts + - .gitea/scripts/desktop-collect.sh + - .gitea/scripts/desktop-stamp.sh + - .gitea/workflows/ci.yml + - .gitignore + - docs/anleitung-anwender.md + - docs/anleitung-betrieb.md + - docs/anleitung-entwicklung.md + - docs/ci-cd-setup.md + - CHANGELOG.md + +key-decisions: + - "Vergleichsregel (is_update_newer): hoehere Basis X.Y.Z → Update; niedrigere → keins; gleiche Basis mit Beta-Stempel beta.g → Update nur bei bekanntem UND verschiedenem Client-Commit; gleiche Basis ohne Prerelease (Live) → KEIN Update. Das ist exakt die bisherige Regel (version_changed || channel==beta && commit != APP_COMMIT) — Live-Clients verhalten sich identisch, keine Update-Schleife; der Fall 'Beta-Client auf Live-Server gleicher Basis' ist client-seitig nicht entscheidbar und bekommt das Update mit der naechsten Freigabe." + - "Timeout-Trennung: 15 s fuer die Pruefung (UpdaterBuilder::timeout), aber das Plugin uebernimmt denselben Wert als Gesamt-Timeout des Downloads — darum vor der Ablage update.timeout = Some(600 s), sonst braeche der ~100-MB-Download ab." + - "http-Server im Release-Bau: kein Absturz, keine Benachrichtigung bei jedem Start, sondern dauerhaft der gesperrte Menuetext 'Update nur über https möglich' (Err(InsecureTransportProtocol)-Zweig); keine dangerous*-Schalter." + - "Secrets auf Schritt-Ebene (env an den zwei tauri-build-Schritten), nicht Job-Ebene: pnpm install, apt-get, cargo install cargo-xwin und die Cache-Schritte sehen den Schluessel nie; js-yaml-Tiefenvergleich bestaetigt, dass sonst nichts in ci.yml geaendert ist." + - "gen/schemas/*.json (vom Tauri-Bau generiert, im Repo getrackt) wurden mit committet — Praezedenz 18-04 (Opener-Plugin); ein liegengelassener Diff haette den Arbeitsbaum schmutzig hinterlassen (Rule 3, siehe Abweichungen)." + +patterns-established: + - "Beta-Stempel im SemVer-Prerelease immer mit Praefix g (beta.g): ein rein numerischer SHA mit fuehrender Null waere kein gueltiger Identifier und liesse check() mit Err enden" + - "Tray-Eintrag 'update' ist zustandsbehaftet: PendingUpdate.take() beim Klick, Reset + Leeren zu Beginn jeder Pruefung, Rueckgabe des Standes bei Fehler" + +requirements-completed: [QUICK-260917-KGC] + +coverage: + - id: D1 + description: "Client: Plugin, Config (pubkey/installMode/createUpdaterArtifacts), Comparator, Endpunkt-URL, Tray-Fluss Pruefen → Anzeige → Klick → Download → Signatur → Installation → Neustart, http-Rueckfall" + requirement: "QUICK-260917-KGC" + verification: + - kind: unit + ref: "apps/desktop/src-tauri/src/lib.rs mod tests — 33 Tests (15 neu, 2 umgestellt, 16 unveraendert), cargo test --lib" + status: pass + - kind: other + ref: "cargo fmt --check, cargo check, cargo clippy — 0 Warnungen; jq-Gate tauri.conf.json; Cargo.lock: tauri-plugin-updater 2.11.0, semver 1.0.28, reqwest-Projekt bleibt 0.12" + status: pass + human_judgment: true + rationale: "Der Ende-zu-Ende-Nachweis (Windows-VM: Tray-Text, Klick, passiver Installer, Neustart auf neuem Stand) braucht ein vom CI signiertes Paket — Orchestrator, siehe unten." + - id: D2 + description: "API GET /desktop/update: base-Origin-Validierung (400), 204-Faelle, dynamisches Updater-Format mit absoluter URL; Manifest-Typen in @tessera/shared" + requirement: "QUICK-260917-KGC" + verification: + - kind: unit + ref: "apps/api/src/desktop/desktop.service.spec.ts — 23 Tests (10 neu, Test 11 erweitert), vitest run src/desktop" + status: pass + - kind: other + ref: "pnpm --filter @tessera/api type-check, pnpm --filter @tessera/shared type-check — 0 Fehler; Route-Order-Gate (update vor download/:platform); kein fetch/axios im Service" + status: pass + human_judgment: false + - id: D3 + description: "desktop-collect.sh (signature/updateVersion/Pflichtregel), desktop-stamp.sh check, ci.yml Secrets an zwei Schritten, .gitignore *.key" + requirement: "QUICK-260917-KGC" + verification: + - kind: other + ref: "Mini-Fixture-Proben im Scratchpad (beta/live/dev, fehlende .sig auf main/Tag/mit Schluessel → Abbruch, dev → Warnung, zweizeilige .sig → Abbruch, stamp check reuse=true nur mit updateVersion + beiden Signaturen) + js-yaml-Tiefenvergleich gegen HEAD (CI-OK, SCRIPTS-OK)" + status: pass + human_judgment: true + rationale: "Ob die Tauri-CLI im xwin-Cross-Bau tatsaechlich .sig-Dateien erzeugt und ring/rustls dort bauen, ist nur in der Pipeline beweisbar." + - id: D4 + description: "Anwender-, Betriebs-, Entwicklungshandbuch, ci-cd-setup.md (ASCII), CHANGELOG" + requirement: "QUICK-260917-KGC" + verification: + - kind: other + ref: "Grep-Gate Task 4 + git diff CHANGELOG.md (genau eine neue Zeile)" + status: pass + - kind: unit + ref: "apps/web/src/lib/changelog.test.ts — 10/10 gruen" + status: pass + human_judgment: false + +duration: ~20min (erster RED-Lauf 15:2x, letzter Commit 15:36:58 +02:00; nicht exakt gestoppt) +completed: 2026-09-17 +status: complete +--- + +# Quick Task 260917-kgc: Desktop-Client — Update in der App herunterladen und installieren Summary + +**Der Desktop-Client prueft beim Start ueber `tauri-plugin-updater` den neuen Endpunkt `GET /api-proxy/desktop/update`, und ein Klick auf „Auf Version X.Y.Z aktualisieren" im Infobereich laedt das im CI mit dem minisign-Schluessel signierte Paket, prueft die Signatur, installiert es (Windows: NSIS passiv, Linux: AppImage an Ort und Stelle) und startet die App neu — der Browser-Download bleibt nur noch als Rueckfall.** + +## Performance + +- **Duration:** ~20 min (vier Tasks, je RED → GREEN bei Task 1/2, vier Verifikations-Gates; der erste `cargo check` mit dem Plugin lief 23 s, weil reqwest 0.13/rustls bereits im Cargo-Zwischenspeicher lagen) +- **Completed:** 2026-09-17T15:36:58+02:00 +- **Tasks:** 4/4 +- **Files modified:** 20 (davon 3 generierte Schema-Dateien, siehe Abweichungen) + +## Accomplishments + +- **Client (`lib.rs`, Cargo.toml/Cargo.lock, tauri.conf.json):** `tauri-plugin-updater = "2"` (→ 2.11.0) und `semver = "1"` eingebunden, `.plugin(tauri_plugin_updater::Builder::new().build())` in `run()`; `bundle.createUpdaterArtifacts: true`, `plugins.updater.pubkey` (exakt der vorgegebene Schluessel), `windows.installMode: "passive"`, kein `endpoints`, keine `dangerous*`-Schalter. Reine Helfer `beta_commit`, `is_update_newer`, `update_endpoint`, `release_labels`, `update_labels` (neue Texte). `check_for_update` ruft `updater_builder().endpoints(…)?.timeout(15 s).version_comparator(…).build()?.check()`; `spawn_version_check` setzt Eintrag und `PendingUpdate` zurueck, legt bei Treffer `(Update, Menuetext)` mit `timeout = 600 s` ab, zeigt die Benachrichtigung und aktiviert den Eintrag; `InsecureTransportProtocol` → „Update nur über https möglich" gesperrt. Tray-Klick `update`: `take()` → `spawn_update_install` (Fortschritt `Lädt … {n} %`, `Wird installiert…`, `app.restart()`; Fehler → Text/Stand zurueck, Benachrichtigung „Update fehlgeschlagen: …", Einstellungsseite im Browser) bzw. `open_download_page` ohne abgelegten Stand. `DesktopLatest` entfernt, `/desktop/latest` wird im Client nicht mehr referenziert; Run-Handler, `on_window_event`, Commands, `capabilities/default.json`, `setup.html` unveraendert. +- **API:** `GET /desktop/update` (`@Public()`, vor `download/:platform`): `safeOrigin(base)` (nur http/https-Origin, sonst 400), `getUpdate({ target, arch, origin })` mit Whitelist → `x86_64` → Manifest → `updateVersion` (`UPDATE_VERSION_RE`) → Eintrag mit `signature` → Antwort `{ version, pub_date (nur RFC 3339), url: /api-proxy/desktop/download/

, signature, notes }`; sonst 204 ohne Body. `isValidManifestFileEntry` akzeptiert nur String-Signaturen; `getManifest` verwirft ein Nicht-String-`updateVersion`. Typen `DesktopManifestFile.signature?`, `DesktopManifest.updateVersion?`, `DesktopUpdateResponse` in `@tessera/shared`. +- **Skripte/CI:** `desktop-collect.sh` liest `.sig` (genau eine Base64-Zeile, Zeilenzahl separat erzwungen) als `files.

.signature`, schreibt `updateVersion` (`X.Y.Z` bzw. `X.Y.Z-beta.g`), bricht ohne `.sig` ab, wenn `TAURI_SIGNING_PRIVATE_KEY` gesetzt ist oder der Kanal nicht `dev` ist, warnt sonst; `.sig` wird nicht kopiert, Dateinamen unveraendert. `desktop-stamp.sh check` gibt `reuse=false` ohne `updateVersion` oder ohne eine der beiden Signaturen. `ci.yml`: `env` mit den zwei Secrets an genau den Schritten „Linux-AppImage bauen" und „Windows-Installer bauen (Cross-Bau)", sonst strukturgleich. `.gitignore`: `*.key`. +- **Doku:** Anwenderhandbuch (Update per Klick, Windows/Linux-Ablauf, Fehlerfall, https-Bedingung, einmaliger Wechsel fuer Clients bis 1.2.0, zwei neue „Wenn etwas nicht klappt"-Zeilen), Betriebshandbuch Kap. 10 (neuer Unterabschnitt Signierschluessel, `updateVersion`/`signature` im Manifest, zweite `curl`-Kontrollzeile, zwei Fehlerbilder), Entwicklungshandbuch (`--no-sign` lokal, `tauri dev`-Hinweise), ci-cd-setup.md (drei Secrets, signierende Bau-Schritte, Fehlerbild „no private key", ASCII), CHANGELOG eine Zeile. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Client — Updater-Plugin, Config, Vergleichsregel, Endpunkt-URL, Tray-Fluss, Tests** - `678ba51` (feat(desktop)) +2. **Task 2: API — GET /desktop/update, Manifest-Felder, Spec-Tests** - `de81c74` (feat(api)) +3. **Task 3: Skripte + CI — signature/updateVersion, stamp check, Secrets, .gitignore** - `7004b5b` (ci) +4. **Task 4: Doku — Handbuecher, ci-cd-setup.md, CHANGELOG** - `7479cb4` (docs) + +_Hinweis: kein separater `test(...)`-Commit trotz `tdd="true"` an Task 1 und 2 — die Orchestrator-Vorgabe lautet ausdruecklich vier Commits (`feat(desktop)`/`feat(api)`/`ci`/`docs`). RED und GREEN liefen innerhalb der Tasks, siehe „TDD Gate Compliance"._ + +## Files Created/Modified + +- `apps/desktop/src-tauri/src/lib.rs` — Imports (`semver::Version`, `Mutex`, `Update`, `UpdaterExt`), `UPDATE_ITEM_DEFAULT = "Update installieren"`, `UPDATE_ITEM_INSECURE`, `PendingUpdate`, Helfer `beta_commit`/`is_update_newer`/`update_endpoint`/`release_labels`, `update_labels` mit neuen Texten, `check_for_update`, umgebautes `spawn_version_check`, `open_download_page`, `spawn_update_install`, Plugin-Registrierung, `app.manage(PendingUpdate…)`, Menue-Zweig `update`; `mod tests` 33 Tests +- `apps/desktop/src-tauri/Cargo.toml`, `Cargo.lock` — `tauri-plugin-updater = "2"` (2.11.0), `semver = "1"` (1.0.28); Projekt-`reqwest` bleibt 0.12 +- `apps/desktop/src-tauri/tauri.conf.json` — `createUpdaterArtifacts`, `plugins.updater` +- `apps/desktop/src-tauri/gen/schemas/{acl-manifests,desktop-schema,linux-schema}.json` — vom Tauri-Bau regeneriert (Updater-Permissions), siehe Abweichungen +- `packages/shared/src/index.ts` — `signature?`, `updateVersion?`, `DesktopUpdateResponse` +- `apps/api/src/desktop/desktop.service.ts` — `safeOrigin` (exportiert), `UPDATE_VERSION_RE`, `RFC3339_RE`, `getUpdate`, erweiterte Manifest-Pruefung +- `apps/api/src/desktop/desktop.controller.ts` — `update()` vor `download()` +- `apps/api/src/desktop/desktop.service.spec.ts` — `writeManifest(files, head)`, `linuxEntry`, `updateUrl`, Tests 12-21, Test 11 erweitert +- `.gitea/scripts/desktop-collect.sh` — `UPDATE_VERSION`, `SIGN_REQUIRED`, `read_signature`, `jq`-Manifest mit `updateVersion`/`signature` +- `.gitea/scripts/desktop-stamp.sh` — `check` verlangt `updateVersion` + Signaturen +- `.gitea/workflows/ci.yml` — Kopfkommentar + `env` an zwei Bau-Schritten +- `.gitignore` — `*.key` +- `docs/anleitung-anwender.md`, `docs/anleitung-betrieb.md`, `docs/anleitung-entwicklung.md`, `docs/ci-cd-setup.md`, `CHANGELOG.md` + +## Decisions Made + +- **Vergleichsregel (gleiche Basis + Live → kein Update):** siehe `key-decisions`; steht als Doc-Kommentar an `is_update_newer`. Leerer `APP_COMMIT` (Quell-Tarball ohne git) → reiner Basisvergleich wie bisher. Der `version_comparator` ersetzt den Standardvergleich vollstaendig — ohne ihn gaelte `1.2.0-beta.gXXXX < 1.2.0` und ein Beta-Client saehe nie einen neueren Beta-Bau. +- **Timeout-Trennung 15 s / 600 s:** `UpdaterBuilder::timeout` wandert ins pub-Feld `Update.timeout` und wird beim Download als reqwest-Gesamt-Timeout angewandt; darum wird es nach `check()` und vor der Ablage auf 600 s gesetzt. +- **`http`-Anzeige:** gesperrter Menuetext statt Benachrichtigung — fuer Dauer-`http`-Server waere eine Meldung bei jedem Start eine Nervmeldung; der Text ist sichtbar, sobald das Menue geoeffnet wird. Kein `dangerousInsecureTransportProtocol`. +- **Secrets auf Schritt-Ebene**, nicht Job-Ebene (geringste Sichtbarkeit; Gitea maskiert die Werte; die Skripte kennen den Schluessel nicht). +- **`read_signature` prueft die Zeilenzahl getrennt:** `grep -qE '^…$'` arbeitet zeilenweise und haette eine zweizeilige `.sig` durchgelassen, sobald eine Zeile passt — darum zusaetzlich `wc -l` = 1 (die Plan-Probe „zweizeilige .sig → Abbruch" haette sonst nicht gehalten). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Generierte Tauri-Schema-Dateien mit committet** +- **Found during:** Task 1, `cargo check` +- **Issue:** `tauri-build` regeneriert bei jedem Bau `apps/desktop/src-tauri/gen/schemas/{acl-manifests,desktop-schema,linux-schema}.json` (die Updater-Permissions kommen hinzu). Die drei Dateien sind im Repo getrackt und stehen nicht in `files_modified`; sie unveraendert zu lassen haette den Arbeitsbaum dauerhaft schmutzig hinterlassen bzw. bei jedem CI-Bau erneut abweichen lassen. +- **Fix:** Die drei Dateien in den Task-1-Commit aufgenommen — exakt wie beim Einbau des Opener-Plugins in 18-04 (`8b130fd`). Inhalt ausschliesslich generiert, kein manueller Eingriff. +- **Files modified:** `apps/desktop/src-tauri/gen/schemas/acl-manifests.json`, `desktop-schema.json`, `linux-schema.json` +- **Verification:** `git status` nach dem Commit leer (bis auf `.planning/`); `cargo check`/`clippy`/`test` gruen +- **Committed in:** `678ba51` + +**2. [Rule 1 - Bug, vor dem Commit behoben] Zeilenzahl-Pruefung in `read_signature`** +- **Found during:** Task 3, beim Schreiben der Funktion +- **Issue:** Die im Plan vorgesehene Pruefung `grep -qE '^[A-Za-z0-9+/=]+$'` allein ist zeilenweise — eine zweizeilige `.sig` (`zwei\nzeilen`) haette bestanden, obwohl der Plan den Abbruch verlangt. +- **Fix:** Zusaetzlich `SIG_LINES="$(printf '%s\n' "$SIG_CONTENT" | wc -l)"` muss `1` sein. +- **Files modified:** `.gitea/scripts/desktop-collect.sh` +- **Verification:** Plan-Probe „zweizeilige .sig → Abbruch" gruen (Teil von SCRIPTS-OK) +- **Committed in:** `7004b5b` + +--- + +**Total deviations:** 2 auto-fixed (1 blocking/generierte Dateien, 1 Bug in der geplanten Pruefung) +**Impact on plan:** Kein Verhalten ausserhalb der `must_haves.truths` geaendert; das Threat-Register (T-KGC-01 … T-KGC-SC) ist wie geplant umgesetzt: Cargo.lock loest `tauri-plugin-updater` auf 2.11.0 (kein 3.0.0-alpha), `semver` 1.0.28, Projekt-`reqwest` 0.12; `~/.tessera/desktop-updater/` wurde nie gelesen; kein Secret-Wert in Code, Doku, Logs oder hier. + +## TDD Gate Compliance + +Task 1 und 2 liefen als RED → GREEN innerhalb eines Commits je Task (Orchestrator-Vorgabe: genau vier Commits): + +- **Task 1 (Rust, tdd="true"):** RED — 15 neue Tests plus 2 umgestellte `update_labels_*`-Tests in `mod tests`; `cargo test --lib` (Exit 101) scheiterte mit 18 Compile-Fehlern, alle auf die fehlenden Funktionen `is_update_newer`/`beta_commit`/`update_endpoint`/`release_labels` und das noch fehlende `semver` zurueckfuehrbar — genau der im Plan als RED vorgesehene Zustand (streng nach `tdd.md` ein Compile-RED, kein Assertion-RED; vom Plan ausdruecklich so definiert). GREEN — Abhaengigkeiten, Config und Implementierung; `cargo test --lib` 33/33 gruen, `cargo fmt --check`/`check`/`clippy` ohne Warnung. Ein Commit (`678ba51`). +- **Task 2 (API, tdd="true"):** RED — Tests 12-21 und die Erweiterung von Test 11; `vitest run src/desktop` zeigte 11 fehlgeschlagene Zieltests auf Assertions der geplanten Behauptungen (404/200 statt 204, `safeOrigin` nicht vorhanden, `update` nicht `@Public()`), 12 Bestandstests gruen. GREEN — Typen, Service, Controller; 23/23 gruen, api- und shared-type-check gruen. Ein Commit (`de81c74`). +- **Task 3/4 (kein `tdd="true"`):** Standard-Tasks. + +Kein separater `test(...)`-Commit — bewusst, wie bei 260917-jn2; die formale `gsd_run check tdd-red-evidence`-Pruefung gilt fuer Plaene mit `type: tdd`, dieser Plan hat `type: execute`. + +## Testzahlen + +| Pruefung | Ergebnis | +|---|---| +| `cargo test --lib` (apps/desktop/src-tauri) | 33 passed, 0 failed (vorher 18) | +| `cargo fmt --check`, `cargo check`, `cargo clippy` | gruen, 0 Warnungen | +| `vitest run src/desktop` (apps/api) | 23 passed (vorher 11) | +| `pnpm --filter @tessera/api type-check`, `@tessera/shared type-check` | 0 Fehler | +| Skript-Proben (Scratchpad-Fixtures) + js-yaml-Tiefenvergleich | CI-OK, SCRIPTS-OK | +| `vitest run src/lib/changelog.test.ts` (apps/web) | 10 passed | + +## Issues Encountered + +None ueber die dokumentierten Abweichungen hinaus. Der Cargo-Zwischenspeicher enthielt reqwest 0.13/rustls/ring bereits (aus der Recherche-`cargo fetch`), darum kein minutenlanger Erst-`cargo check`. + +## Nachweis durch Orchestrator (offen) + +Der echte Beweis der Kette braucht ein vom CI signiertes Paket und ist hier nicht erbracht: + +- **(a) CI baut signierte Pakete:** Nach dem Push auf `main` muss der Job `desktop` beide `tauri build`-Schritte mit den Secrets durchlaufen (Voraussetzung: `TAURI_SIGNING_PRIVATE_KEY` und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` sind in Gitea angelegt — sonst „A public key has been found, but no private key"). „Pakete einsammeln" muss `signiert` fuer linux und windows loggen; das Manifest im Abbild traegt `updateVersion: "X.Y.Z-beta.g"` und je Plattform `signature`. `publish` gruen. Der alte Stempel-Cache (ohne die Felder) wird durch `desktop-stamp.sh check` verworfen — der erste Lauf baut also zwingend neu. +- **(b) Endpunkt gegen alpha:** `curl -si "https://alpha.tessera.ctl.de/api-proxy/desktop/update?target=windows&arch=x86_64¤t=0.0.0&base=https://alpha.tessera.ctl.de"` → `200` mit `version`, `url` (absolut, `https://alpha.…/api-proxy/desktop/download/windows`), `signature`, `pub_date`, `notes`; `target=linux` analog; `base=https://alpha.tessera.ctl.de/x` → `400`; vor dem Pull des neuen Abbilds noch `404` (Route existiert nicht). +- **(c) Windows-VM (8233): alter Client aktualisiert sich per Tray-Klick.** Der bereits installierte Client 1.2.0 hat das Plugin NICHT — er zeigt weiterhin „Version … herunterladen"/Browser-Weg (Einmaliger Wechsel: das erste Paket mit Plugin muss ein letztes Mal ueber den Browser installiert werden). Danach, mit einem weiteren Beta-Bau auf dem Server: Nach dem Start Benachrichtigung „Neuer Beta-Stand verfügbar – Aktualisieren über das Symbol im Infobereich." und der Eintrag heisst **„Auf Beta-Stand aktualisieren"** (bei neuer Basisversion: „Auf Version X.Y.Z aktualisieren"). Klick → Benachrichtigung „Update wird heruntergeladen…", Eintrag gesperrt, Text „Lädt … NN %", dann „Wird installiert…" → passives NSIS-Fenster mit Fortschrittsbalken → Tessera startet neu, Tray zeigt den neuen Stand, Server-Adresse erhalten. SmartScreen-Verhalten des vom Updater gestarteten Installers notieren (Recherche A2: vermutlich kein Dialog, keine Mark-of-the-Web). **Bei Fehler:** Menuetext springt zurueck, Eintrag wieder aktiv, Benachrichtigung „Update fehlgeschlagen: . Die Download-Seite wird im Browser geöffnet." und die Seite Einstellungen → Desktop-App oeffnet im Browser. Bei `http://`-Adresse: Eintrag gesperrt mit „Update nur über https möglich". +- **(d) Cross-Bau von ring/rustls in der Pipeline:** `ring 0.17.14` ist neu im Graph; fuer `x86_64-pc-windows-msvc` liefert es vorassemblierte Objekte mit (kein nasm), die C-Teile baut `cc` mit dem Clang aus `cargo-xwin` — nur im Runner beweisbar. Fallback bei rotem Bau: `tauri-plugin-updater = { version = "2", default-features = false, features = ["native-tls", "system-proxy"] }` (schannel/OpenSSL; `libssl-dev` steht bereits in der apt-Liste). Das Projekt-`reqwest` NICHT auf 0.13 heben (aws-lc-rs). + +## Self-Check: PASSED + +- Dateien vorhanden: `apps/desktop/src-tauri/src/lib.rs`, `apps/api/src/desktop/desktop.service.ts`, `.gitea/scripts/desktop-collect.sh`, `docs/anleitung-betrieb.md` — FOUND +- Commits vorhanden: `678ba51`, `de81c74`, `7004b5b`, `7479cb4` — FOUND (`git rev-list --count 5a444ec..HEAD` = 4) + +## Nachweis durch Orchestrator — Ergebnis (2026-09-17/18) + +- (a) CI-Lauf 383 (`7479cb4`): `Finished 1 updater signature` fuer AppImage und NSIS (Cross-Bau mit rustls/ring lief durch), Manifest `updateVersion 1.2.0-beta.g7479cb4`, `files.*.signature` gesetzt; Lauf 384 (`a6d1a64`) ebenso. (d) damit erledigt, kein native-tls-Fallback noetig. +- (b) alpha nach Pull: `GET /api-proxy/desktop/update?target=windows&arch=x86_64¤t=1.2.0&base=https://alpha.tessera.ctl.de` → 200 mit absoluter `url` und `signature`; `base=https://evil.example/x?y` → 400. +- (c) Windows-VM: Client `7479cb4` (einmalig per Browser installiert), gegen den http-Dev-Stack „Update nur über https möglich" (gesperrt); gegen alpha mit `a6d1a64`: Tray „Auf Beta-Stand a6d1a64 aktualisieren" → Klick → Download in Sekunden, App beendet sich, passiver NSIS-Installer laeuft durch, App startet selbst neu (kein SmartScreen), Server-Adresse erhalten, Update-Eintrag wieder gesperrt, Setup-Seite zeigt „Tessera-App 1.2.0 · Stand a6d1a64" (Quick-Fix `a6d1a64`). +- Nebenbefund: NSIS-Seite „Bereits installiert" nennt „Eine -Version von Tessera" (leere Versionsangabe aus der Registry) — kosmetisch, Tauri-Vorlage, nicht angefasst. diff --git a/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-VERIFICATION.md b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-VERIFICATION.md new file mode 100644 index 0000000..6e79a78 --- /dev/null +++ b/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-VERIFICATION.md @@ -0,0 +1,142 @@ +--- +phase: quick-260917-kgc +verified: 2026-09-17T15:45:00Z +status: human_needed +score: 8/10 must-haves verified +covered_files: + - ".gitea/scripts/desktop-collect.sh" + - ".gitea/scripts/desktop-stamp.sh" + - ".gitea/workflows/ci.yml" + - ".gitignore" + - ".planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-PLAN.md" + - ".planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md" + - "CHANGELOG.md" + - "apps/api/src/desktop/desktop.controller.ts" + - "apps/api/src/desktop/desktop.service.spec.ts" + - "apps/api/src/desktop/desktop.service.ts" + - "apps/desktop/src-tauri/Cargo.lock" + - "apps/desktop/src-tauri/Cargo.toml" + - "apps/desktop/src-tauri/src/lib.rs" + - "apps/desktop/src-tauri/tauri.conf.json" + - "docs/anleitung-anwender.md" + - "docs/anleitung-betrieb.md" + - "docs/anleitung-entwicklung.md" + - "docs/ci-cd-setup.md" + - "packages/shared/src/index.ts" +covered_digest: "v1:sha256:dc01a09c0bf779e0c311f942b53240c8ad8a984254ebbc3a3a36b247441cf3cd" +behavior_unverified: 2 +overrides_applied: 0 +behavior_unverified_items: + - truth: "spawn_version_check setzt den Tray-Eintrag und den abgelegten PendingUpdate-Stand zu Beginn jeder Pruefung zurueck (Server-Wechsel darf keinen alten Hinweis stehen lassen)" + test: "In der laufenden App (oder einem AppHandle-Mock) zweimal hintereinander die Server-Adresse wechseln, waehrend ein Update-Fund im State liegt, und pruefen, dass der Eintrag sofort auf den Standardtext springt und PendingUpdate leer ist, bevor die neue Pruefung antwortet" + expected: "Menuetext == UPDATE_ITEM_DEFAULT, Eintrag gesperrt, PendingUpdate == None unmittelbar nach dem Wechsel" + why_human: "Die 33 Unit-Tests decken ausschliesslich die reinen Helfer (is_update_newer, beta_commit, update_endpoint, release_labels, update_labels, parse_server_url, server_host, tray_labels, setup_page_url, with_desktop_marker) ab; spawn_version_check haengt an AppHandle/TrayItems/PendingUpdate-State und wird von keinem Test aufgerufen — Reset-Verhalten ist nur am Laufzeit-Client oder mit einem Tauri-Mock zu beobachten" + - truth: "Tray-Klick 'update' nimmt den abgelegten Stand per take() (verhindert Doppelklick-Downloads), laedt mit Fortschritt im Menuetext, installiert (Windows NSIS passiv / Linux AppImage) und startet neu; bei Fehler springt Menuetext/Stand zurueck und der Browser-Rueckfall oeffnet" + test: "Am echten (oder in der Windows-VM installierten) Client: Update-Fund abwarten, Tray-Eintrag zweimal schnell hintereinander anklicken, danach den vollen Ablauf bis zum Neustart beobachten; anschliessend denselben Ablauf mit einem absichtlich fehlerhaften Download (z. B. Netz trennen) wiederholen" + expected: "Zweiter Klick loest keinen zweiten Download aus (Eintrag bleibt gesperrt); bei Erfolg Fortschritt 'Laedt ... N %' -> 'Wird installiert...' -> Neustart auf neuem Stand; bei Fehler Menuetext/Stand zurueck, Eintrag wieder aktiv, Benachrichtigung 'Update fehlgeschlagen: ...' und Einstellungsseite im Browser" + why_human: "spawn_update_install und der Menue-Zweig 'update' (take()-Semantik, download_and_install, app.restart()) werden von keinem Unit-Test ausgeloest — das ist explizit als offener Orchestrator-Nachweis (Windows-VM) im PLAN/SUMMARY vermerkt und braucht ein vom CI signiertes Paket" +coincidental_reliance_items: [] +human_verification: + - test: "(a) CI-Bau auf main: beide tauri-build-Schritte mit den Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD durchlaufen lassen" + expected: "'Pakete einsammeln' loggt 'signiert' fuer linux und windows; Manifest im Abbild traegt updateVersion (X.Y.Z-beta.g) und je Plattform signature; Job publish gruen; alter Stempel-Cache (ohne die Felder) wird verworfen (Neubau)" + why_human: "Nur im Gitea-Runner beweisbar (Secrets, echte tauri-CLI, Cross-Bau-Toolchain) — lokal simuliert per Mini-Fixture-Proben (siehe unten), aber nicht der echte Bau" + - test: "(b) Endpunkt gegen alpha.tessera.ctl.de nach dem Pull des neuen Abbilds: curl -si \"https://alpha.tessera.ctl.de/api-proxy/desktop/update?target=windows&arch=x86_64¤t=0.0.0&base=https://alpha.tessera.ctl.de\"" + expected: "200 mit version, absoluter url (.../api-proxy/desktop/download/windows), signature, pub_date, notes; target=linux analog; base=https://alpha.tessera.ctl.de/x -> 400; vor dem Pull des neuen Abbilds noch 404 (Route existiert nicht)" + why_human: "Braucht den laufenden Server mit dem neuen Abbild — nicht lokal simulierbar" + - test: "(c) Windows-VM (8233): bereits installierter Client 1.2.0 aktualisiert sich per Tray-Klick, sobald ein neuer signierter Beta-Bau auf dem Server liegt" + expected: "Benachrichtigung 'Neuer Beta-Stand verfuegbar', Eintrag 'Auf Beta-Stand aktualisieren'; Klick -> Fortschritt im Menuetext -> passives NSIS-Fenster -> Tessera startet neu, Tray zeigt neuen Stand, Server-Adresse bleibt erhalten; SmartScreen-Verhalten notieren" + why_human: "Reale Installation/Neustart/Betriebssystem-Verhalten (SmartScreen) ist nur am Bildschirm zu pruefen; Client 1.2.0 hat das Plugin noch nicht, der erste Wechsel muss einmal ueber den Browser laufen (Einmaliger Wechsel, wie im Anwenderhandbuch dokumentiert)" + - test: "(d) Cross-Bau von ring/rustls (x86_64-pc-windows-msvc, cargo-xwin) in der CI-Pipeline" + expected: "Bau gruen; Fallback bei rotem Bau: tauri-plugin-updater mit default-features = false, features = [\"native-tls\", \"system-proxy\"]" + why_human: "Nur im Runner mit der echten xwin/Clang-Toolchain beweisbar" +--- + +# Quick Task 260917-kgc: Desktop-Client — Update in der App Verifizierung + +**Ziel:** Der Desktop-Client aktualisiert sich selbst per Tray-Klick (tauri-plugin-updater 2, minisign-Signaturpruefung, Endpunkt zur Laufzeit aus der gespeicherten Server-Adresse); API `GET /desktop/update` liefert das Format im Updater-Vertrag; Manifest/CI signieren und stempeln entsprechend; Doku + CHANGELOG. + +**Verifiziert:** 2026-09-17 +**Status:** human_needed +**Hinweis:** Kein einziger `truth` ist FEHLGESCHLAGEN. Die zwei offenen Punkte sind Laufzeitverhalten (Zustandswechsel/Installation), die von keinem der bestehenden Unit-Tests ausgeloest werden koennen und laut PLAN/SUMMARY selbst als "Nachweis durch Orchestrator" gefuehrt werden (Windows-VM, echtes CI-Paket). Das ist keine Luecke in der Umsetzung, sondern der erwartete Zustand fuer diesen Task-Typ. + +## Beobachtete Wahrheiten (must_haves.truths) + +| # | Truth (gekuerzt) | Status | Evidenz | +|---|---|---|---| +| 1 | Cargo.toml/Cargo.lock: tauri-plugin-updater "2" (-> 2.11.x), semver "1" (1.0.28), reqwest bleibt 0.12 | ✓ VERIFIED | `grep`-Gates gruen; `cargo test --lib` 33/33 gruen | +| 2 | tauri.conf.json: createUpdaterArtifacts, exakter pubkey, installMode passive, kein endpoints/dangerous* | ✓ VERIFIED | `jq`-Gate mit dem exakten Orchestrator-Schluessel bestanden | +| 3 | Reine Helfer is_update_newer/beta_commit/update_endpoint/release_labels/update_labels + fmt/check/clippy/test gruen | ✓ VERIFIED | `cargo fmt --check`, `cargo clippy`, `cargo test --lib` (33/33) selbst ausgefuehrt; is_update_newer-Regel Zeile fuer Zeile gelesen (Z. 123-135) — hoehere Basis true, niedrigere false, gleiche Basis+Beta+anderer Commit true, gleicher Commit false, gleiche Basis ohne Prerelease false, leerer Commit false; alle 8 zugehoerigen Tests gruen | +| 4 | spawn_version_check: check() mit eigenem Comparator, 15 s Pruefung, 600 s Download-Timeout vor Ablage, InsecureTransportProtocol -> gesperrter Text, Reset zu Beginn | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code exakt wie gefordert (Z. 263-338), Timeout-Trennung im Code bestaetigt; kein Test ruft die Funktion auf (State-Reset ist ein Laufzeit-Zustandswechsel) | +| 5 | Tray-Klick update: take(), Fortschritt, Installation, Neustart, Fehler-Rueckfall Browser | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code exakt wie gefordert (Z. 352-411, 591-601); kein Test loest den Menue-Zweig/spawn_update_install aus — explizit als offener Windows-VM-Nachweis im PLAN gefuehrt | +| 6 | API GET /desktop/update: base-Origin-Validierung 400, 204-Faelle, dynamisches Format, /latest und download unveraendert | ✓ VERIFIED | 23/23 vitest-Tests selbst ausgefuehrt, Route-Order-Gate bestanden, Code gelesen (safeOrigin, getUpdate, UPDATE_VERSION_RE, RFC3339_RE) — deckt sich exakt mit dem Vertrag | +| 7 | desktop-collect.sh schreibt signature+updateVersion mit Pflichtregel; desktop-stamp.sh check verlangt beides | ✓ VERIFIED | Alle Mini-Fixture-Proben aus dem PLAN selbst nachgestellt (Beta/Live/Dev-Kanal, fehlende .sig auf main/Tag/mit Schluessel -> Abbruch, dev -> Warnung, zweizeilige .sig -> Abbruch, stamp reuse=true/false-Faelle) — alle bestanden | +| 8 | CI: Secrets nur an den zwei tauri-build-Schritten, kein Job-env, kein --no-sign, sonst strukturgleich | ✓ VERIFIED | js-yaml-Tiefenvergleich gegen `git show 5a444ec:.gitea/workflows/ci.yml` selbst ausgefuehrt — CI-OK; Negativ-Grep `--no-sign` leer; `.gitignore` traegt `*.key`; `git ls-files '*.key'` leer | +| 9 | Doku (Anwender/Betrieb/Entwicklung/ci-cd-setup/CHANGELOG) | ✓ VERIFIED | Alle Grep-Gates aus dem PLAN selbst ausgefuehrt (14 Pruefungen) — alle gruen; `changelog.test.ts` 10/10 gruen | +| 10 | Vier Commits, kein Push/`.planning`/tauri build/Docker | ✓ VERIFIED | `git rev-list --count 5a444ec..HEAD` = 4; keine `.planning/`-Dateien in den vier Commits | + +**Score:** 8/10 truths verified (2 present, behavior-unverified) + +### Hinweis zu Wahrheit 3 (kein coincidental reliance) + +Die is_update_newer-Regel wird von acht dedizierten Tests direkt und unmittelbar geprueft (kein Fixture-Only-Fall, keine unbekannte Vorbedingung) — als sauber VERIFIED eingestuft, keine Advisory-Markierung noetig. + +## Artefakt-Pruefung + +| Artefakt | Erwartet | Status | Details | +|---|---|---|---| +| `apps/desktop/src-tauri/src/lib.rs` | Updater-Kette, Helfer, Tests | ✓ VERIFIED | Alle geforderten Symbole vorhanden (is_update_newer, beta_commit, update_endpoint, release_labels, spawn_update_install, open_download_page, PendingUpdate, download_and_install, version_comparator); DesktopLatest entfernt, `/desktop/latest` nicht mehr referenziert | +| `apps/desktop/src-tauri/Cargo.toml`/`Cargo.lock`/`tauri.conf.json` | Neue Abhaengigkeiten + Config | ✓ VERIFIED | Versionen exakt wie gefordert | +| `packages/shared/src/index.ts` | `signature?`, `updateVersion?`, `DesktopUpdateResponse` | ✓ VERIFIED | Alle drei per grep bestaetigt, type-check gruen | +| `apps/api/src/desktop/desktop.service.ts` + `.controller.ts` + `.spec.ts` | safeOrigin, getUpdate, update()-Route, Tests | ✓ VERIFIED | Code gelesen, Tests ausgefuehrt, Route-Order bestaetigt | +| `.gitea/scripts/desktop-collect.sh`/`desktop-stamp.sh`/`ci.yml`/`.gitignore` | .sig -> signature, updateVersion, Secrets, `*.key` | ✓ VERIFIED | Proben + Deep-Compare selbst ausgefuehrt | +| Vier Dokudateien + CHANGELOG | Update-in-der-App-Beschreibung | ✓ VERIFIED | Grep-Gates gruen | + +## Schluesselverbindungen (key_links) + +| Von | Nach | Via | Status | +|---|---|---|---| +| CI-Signatur (`.sig`) | `desktop-collect.sh` -> Manifest `signature` | `read_signature` liest `.sig`, jq schreibt es ins Manifest | ✓ VERIFIED (Probe bestanden) | +| Manifest `signature`/`updateVersion` | API-Antwort `GET /desktop/update` | `getUpdate()` liest beide Felder, 204 wenn eines fehlt | ✓ VERIFIED (Code + Tests) | +| `version_comparator` | Plugin-Standardvergleich | `is_update_newer` ersetzt den Vergleich vollstaendig, Aufruf in `check_for_update` bestaetigt | ✓ VERIFIED (Code gelesen, Z. 273-276) | +| `UpdaterBuilder::timeout(15s)` | `update.timeout = 600s` | Beide Stellen im Code gefunden und in der richtigen Reihenfolge (Pruefung vor Ablage) | ✓ VERIFIED (Z. 275, 316) | +| Client-Klick "update" | `PendingUpdate.take()` -> `spawn_update_install` | Menue-Zweig ruft take(), dann spawn_update_install oder open_download_page | ✓ WIRED (Code), ⚠️ Laufzeitverhalten nicht getestet (siehe Wahrheit 5) | + +## Verhaltens-Stichproben + +| Verhalten | Befehl | Ergebnis | Status | +|---|---|---|---| +| Rust fmt/check/clippy/test | `cargo fmt --check && cargo clippy --lib && cargo test --lib` (CARGO_BUILD_JOBS=4) | fmt clean, clippy 0 Warnungen, 33/33 Tests gruen | ✓ PASS | +| API-Tests | `pnpm --filter @tessera/api exec vitest run src/desktop` | 23/23 gruen | ✓ PASS | +| API/Shared type-check | `pnpm --filter @tessera/api type-check` / `@tessera/shared type-check` | 0 Fehler je | ✓ PASS | +| Skript-Syntax | `sh -n desktop-collect.sh` / `sh -n desktop-stamp.sh` | beide clean | ✓ PASS | +| Route-Order-Gate | awk-Pruefung `@Get('update')` vor `@Get('download/:platform')` | Manuell im Quelltext bestaetigt (update() vor download()) | ✓ PASS | +| pubkey-Gate | `jq -e .plugins.updater.pubkey == ""` | true | ✓ PASS | +| CI js-yaml-Tiefenvergleich | Node-Skript gegen `git show 5a444ec:.gitea/workflows/ci.yml` | "CI-OK" | ✓ PASS | +| Skript-Mini-Fixtures (Beta/Live/Dev, fehlende/zweizeilige .sig, stamp reuse) | Eigenstaendig im Scratchpad nachgestellt (siehe PLAN Task 3 ``) | Alle 15 Teilpruefungen gruen | ✓ PASS | +| Doku-Grep-Gates (14 Pruefungen) | siehe PLAN Task 4 `` | Alle gruen | ✓ PASS | +| changelog.test.ts | `pnpm --filter @tessera/web exec vitest run src/lib/changelog.test.ts` | 10/10 gruen | ✓ PASS | +| Secret-Leck-Pruefung | grep auf private-key-Werte im Diff | Nichts gefunden | ✓ PASS | +| `.key`-Dateien im Repo | `git ls-files '*.key'` | leer | ✓ PASS | + +## Requirements Coverage + +| Requirement | Quelle | Beschreibung | Status | +|---|---|---|---| +| QUICK-260917-KGC | 260917-kgc-PLAN.md | Desktop-Client Update in der App (siehe Task-Ziel) | ✓ SATISFIED (bis auf die zwei Laufzeit-Nachweise, siehe oben) | + +## Anti-Pattern-Scan + +Keine TBD/FIXME/XXX-Debt-Marker (der einzige Treffer auf "XXXX" ist ein Doc-Kommentar-Beispiel `1.2.0-beta.gXXXX`, kein Debt-Marker); keine TODO/HACK/PLACEHOLDER; keine "not implemented"-Stellen ausser zwei bereits bestehenden, legitimen `NotFoundException`-Meldungen ("Desktop packages are not available on this server"). Keine unerwarteten Dateien ausserhalb `files_modified` veraendert (3 generierte `gen/schemas/*.json`-Dateien sind dokumentierte Abweichung, Praezedenz aus Phase 18-04). + +## Erforderliche menschliche Verifizierung + +Siehe `human_verification` im Frontmatter — die vier vom Orchestrator selbst als offen gefuehrten Nachweise (a) CI-Bau mit echten Secrets, (b) Endpunkt-curl gegen alpha, (c) Windows-VM Update-Durchlauf, (d) Cross-Bau ring/rustls. Diese sind **keine Luecken**, sondern der erwartete naechste Schritt nach diesem Quick Task (siehe SUMMARY.md "Nachweis durch Orchestrator (offen)"). + +## Zusammenfassung + +Alle zehn `must_haves.truths` sind im Code vorhanden, exakt wie im PLAN spezifiziert verdrahtet, und alle automatisierten Gates (Rust fmt/clippy/test, API-Tests/Type-Checks, Skript-Proben, CI-Deep-Compare, Doku-Grep-Gates, Changelog-Test) wurden von mir selbst ausgefuehrt und bestanden — keine SUMMARY-Behauptung wurde ungeprueft uebernommen. Zwei Wahrheiten (State-Reset bei Server-Wechsel, Tray-Klick-Installationsfluss inkl. take()-Semantik) haengen an Tauri-Laufzeitzustand, den keiner der 33 Unit-Tests ausloest; das deckt sich mit den vier vom Orchestrator selbst als offen gefuehrten Nachweisen und ist fuer einen Quick Task dieser Art normal, nicht ein Zeichen unvollstaendiger Umsetzung. + +--- + +_Verifiziert: 2026-09-17_ +_Verifizierer: Claude (gsd-verifier)_ diff --git a/.planning/todos/pending/2026-09-15-desktop-client-auslieferungsreif-machen.md b/.planning/todos/completed/2026-09-15-desktop-client-auslieferungsreif-machen.md similarity index 100% rename from .planning/todos/pending/2026-09-15-desktop-client-auslieferungsreif-machen.md rename to .planning/todos/completed/2026-09-15-desktop-client-auslieferungsreif-machen.md