Files
tessera-ctl/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-SUMMARY.md
T
schalli efbd6e8974
Tessera CI/CD / Lint & Type Check (push) Successful in 53s
Tessera CI/CD / Tests (push) Successful in 1m5s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m22s
docs(quick-260917-kgc): Aktenstand — Update in der Desktop-App, alle Nachweise erbracht, Wiedereinstieg bereinigt
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
2026-09-18 11:29:14 +02:00

250 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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=&current=&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<sha7>) 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<Option<(Update, String)>>) → 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=<Origin> 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<sha7> → 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<sha7>): 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: <origin>/api-proxy/desktop/download/<p>, 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 `<bundle>.sig` (genau eine Base64-Zeile, Zeilenzahl separat erzwungen) als `files.<p>.signature`, schreibt `updateVersion` (`X.Y.Z` bzw. `X.Y.Z-beta.g<sha7>`), 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<sha7>"` 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&current=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 <sha7> verfügbar – Aktualisieren über das Symbol im Infobereich." und der Eintrag heisst **„Auf Beta-Stand <sha7> 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: <Grund>. 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&current=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.