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