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

25 KiB
Raw Blame History


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) 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: 5a444ec8f2

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= 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: <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 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&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.