docs(quick-260917-kgc): Aktenstand — Update in der Desktop-App, alle Nachweise erbracht, Wiedereinstieg bereinigt
Quick 260917-kgc (Plan/Recherche/Bericht/Verifikation) und Schnellfixa6d1a64in der Quick-Task-Tabelle; Nachweise in allen sechs Berichten nachgetragen (Playwright lokal, CI-Laeufe 382-384, Windows-Test-VM: In-App-Update7479cb4->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
This commit is contained in:
@@ -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 -- <Datei>` 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. |
|
|
||||||
|
|
||||||
<current_state>
|
|
||||||
**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.
|
|
||||||
</current_state>
|
|
||||||
|
|
||||||
<completed_work>
|
|
||||||
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`.
|
|
||||||
</completed_work>
|
|
||||||
|
|
||||||
<remaining_work>
|
|
||||||
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.
|
|
||||||
</remaining_work>
|
|
||||||
|
|
||||||
<decisions_made>
|
|
||||||
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).
|
|
||||||
</decisions_made>
|
|
||||||
|
|
||||||
<blockers>
|
|
||||||
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.
|
|
||||||
</blockers>
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|
||||||
<next_action>
|
|
||||||
`/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.
|
|
||||||
</next_action>
|
|
||||||
+10
-8
@@ -6,8 +6,8 @@ current_phase_name: desktop-client-fertigstellen
|
|||||||
status: verified
|
status: verified
|
||||||
stopped_at: "Quick-Task 260917-jn2 abgeschlossen (3 Commits: feat(desktop), feat(web), docs); Windows-VM-Bedienprobe steht durch Orchestrator aus"
|
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_updated: "2026-09-17T12:55:59.549Z"
|
||||||
last_activity: 2026-09-17
|
last_activity: 2026-09-18
|
||||||
last_activity_desc: Version 1.2.0 freigegeben; publish-release.sh nimmt nie mehr die oeffentliche Gitea-Adresse (Host-Gateway/localhost)
|
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
|
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
|
||||||
progress:
|
progress:
|
||||||
total_phases: 18
|
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)
|
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||||
Plan: 6 of 6
|
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)
|
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-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)
|
||||||
|
|
||||||
Progress: [██████████] 99%
|
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-<Stempel>` 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-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-<Stempel>` 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-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-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<sha7>` mit anderem Commit; kleinere/gleiche Live → nichts), Pruefung 15 s / Download 600 s (Plugin-Timeout gilt fuer beides), Tray „Auf Version X / Beta-Stand <sha7> 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<sha7>`), `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), `<p id="client-info">` in setup.html, CHANGELOG. Diente zugleich als zweiter Desktop-Stand fuer den Update-Nachweis. 35 Rust-Tests. | 2026-09-18 | a6d1a64 | — |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
|
|
||||||
@@ -482,8 +484,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-09-17T12:55:59.226Z
|
Last session: 2026-09-18T09:30:00Z
|
||||||
Resumed: 2026-09-17 — Sitzung ueber /gsd-resume-work fortgesetzt (keine HANDOFF.json; beide .continue-here-Dateien ueberholt, Arbeitsbaum leer, main == origin/main 38c1400).
|
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: Quick-Task 260917-jn2 abgeschlossen (3 Commits: feat(desktop), feat(web), docs); Windows-VM-Bedienprobe steht durch Orchestrator aus
|
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
|
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)
|
||||||
|
|||||||
@@ -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/...` |
|
|
||||||
|
|
||||||
<current_state>
|
|
||||||
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.
|
|
||||||
</current_state>
|
|
||||||
|
|
||||||
<completed_work>
|
|
||||||
|
|
||||||
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)
|
|
||||||
</completed_work>
|
|
||||||
|
|
||||||
<remaining_work>
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
</remaining_work>
|
|
||||||
|
|
||||||
<decisions_made>
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
</decisions_made>
|
|
||||||
|
|
||||||
<blockers>
|
|
||||||
- keine
|
|
||||||
</blockers>
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
<context>
|
|
||||||
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).
|
|
||||||
</context>
|
|
||||||
|
|
||||||
<next_action>
|
|
||||||
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.
|
|
||||||
</next_action>
|
|
||||||
+7
@@ -194,3 +194,10 @@ Kein Blocker fuer weitere Arbeit — API-Suite 68 Dateien/1101 Tests, Web-Suite
|
|||||||
## Self-Check: PASSED
|
## Self-Check: PASSED
|
||||||
|
|
||||||
All 11 claimed files found on disk; all three task commits (2a562d0, b18ac25, b023d6f) found in git history.
|
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.
|
||||||
|
|||||||
+6
@@ -136,3 +136,9 @@ None - no external service configuration required.
|
|||||||
## Self-Check: PASSED
|
## Self-Check: PASSED
|
||||||
|
|
||||||
All 5 claimed files found on disk; both task commits (ecff144, 29db4c0) found in git history.
|
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.
|
||||||
|
|||||||
+7
@@ -162,3 +162,10 @@ Zusaetzlicher Beobachtungspunkt aus dem Plan-Checker-Hinweis: Das Verhalten von
|
|||||||
## Self-Check: PASSED
|
## Self-Check: PASSED
|
||||||
|
|
||||||
All 6 claimed source/doc files found on disk; both task commits (8c4aaa5, e7633e1) found in git history.
|
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.
|
||||||
|
|||||||
+8
@@ -208,3 +208,11 @@ Alle automatisierten Verifikationen (Rust: fmt/check/clippy/test, Web: vitest/ty
|
|||||||
---
|
---
|
||||||
*Quick Task: 260917-jn2*
|
*Quick Task: 260917-jn2*
|
||||||
*Completed: 2026-09-17*
|
*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.
|
||||||
|
|||||||
+312
File diff suppressed because one or more lines are too long
+298
@@ -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>`; `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<sha7>` (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<Updater>` [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<Url>) -> Result<Self>` (Z. 224, validiert https), `header(k, v) -> Result<Self>`, `headers(HeaderMap)`, `timeout(Duration)`, `pubkey(..)`, `installer_arg(s)`, `restart_after_install(bool)` (Windows, Default `true`), `configure_client(Fn(reqwest::ClientBuilder) -> ClientBuilder)`, `build() -> Result<Updater>`; `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<Option<Update>>` (Z. 432). `Update` (pub-Felder): `body: Option<String>`, `current_version: String`, `version: String`, `date: Option<OffsetDateTime>`, `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<Option<Update>>)` liegen [VERIFIED: `updater.rs:642-676`].
|
||||||
|
- `update.download(on_chunk: FnMut(usize, Option<u64>), on_finish: FnOnce()) -> Result<Vec<u8>>` (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<String>, pub_date: Option<OffsetDateTime>, data: RemoteReleaseInner }` mit `RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { url: Url, signature: String })` oder `Static { platforms: HashMap<String, ReleaseManifestPlatform> }` (Z. 70-96) [VERIFIED].
|
||||||
|
- `plugins.updater.endpoints` in `tauri.conf.json` ist optional (`#[serde(default)] pub endpoints: Vec<Url>`, `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 `<datei>.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 <pfad> [-p <passwort>] [--ci] [--force]` → schreibt `<pfad>` (privat) und `<pfad>.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-<version>-updater-<rand>\Tessera-<version>-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", <exe>, <parameter>, 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 <aktuelle Prozessargumente, escaped>` + `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=<server_url>` 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/<platform>")` 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=<c>;commit=<sha7>", 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=<APP_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.<platform>.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.<p>.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": "<wert>", "description": "<optional>"}`; 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<sha7>" 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-<version>-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.<p>.signature vorhanden?) ── nein → 204 → Ok(None)
|
||||||
|
│ ja → 200 { version: X.Y.Z | X.Y.Z-beta.g<sha7>, url: {base}/api-proxy/desktop/download/<p>, signature, pub_date, notes }
|
||||||
|
├─ Some(update) → app.state::<PendingUpdate>().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<Option<Update>>))`; 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": "<Inhalt der .pub-Datei, eine Base64-Zeile>",
|
||||||
|
"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<Option<tauri_plugin_updater::Update>> {
|
||||||
|
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 '<passwort>'
|
||||||
|
# -> ~/.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<sha7>`.
|
||||||
|
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)
|
||||||
+249
@@ -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<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¤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 <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¤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.
|
||||||
+142
@@ -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<sha7>) 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 <sha7> verfuegbar', Eintrag 'Auf Beta-Stand <sha7> 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 `<bundle>.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 == "<exakter Schluessel>"` | 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 `<verify>`) | Alle 15 Teilpruefungen gruen | ✓ PASS |
|
||||||
|
| Doku-Grep-Gates (14 Pruefungen) | siehe PLAN Task 4 `<verify>` | 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)_
|
||||||
Reference in New Issue
Block a user