docs(quick-260922-frg): Akte - Tray-Update-Befund: Proxy-401 vor alpha, Client nennt jetzt den Grund
Tessera CI/CD / Lint & Type Check (push) Successful in 51s
Tessera CI/CD / Tests (push) Successful in 1m11s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 5m27s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m3s

Plan mit Messungen (API am Proxy vorbei 200, Proxy 401 Basic von zwei
Netzen), Zusammenfassung des Executors, Zeile in der Quick-Tabelle und
Stopp-Punkt: die Behebung des Passwortschutzes liegt beim Nutzer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 11:28:06 +02:00
parent d73aad1ef1
commit 2c01f9d783
3 changed files with 283 additions and 8 deletions
@@ -0,0 +1,122 @@
---
phase: quick-260922-frg
plan: 01
type: tdd
autonomous: true
subsystem: apps/desktop/src-tauri
requirements: []
---
# Quick-Aufgabe 260922-frg: Update-Eintrag im Tray nie mehr stumm ausgegraut
## Befund (Orchestrator, 22.09.2026)
Der Nutzer sieht im Tray-Menü nur den grauen Eintrag „Update installieren“,
obwohl alpha das Paket `1.2.0-beta.gc001a08` anbietet und der Client auf
Stand `a6d1a64` steht (Neustart der App ändert nichts). Nachgemessen:
- Die Update-Anfrage genau in der Form des Clients
(`/api-proxy/desktop/update?target=windows&arch=x86_64&current=1.2.0&base=https://alpha.tessera.ctl.de`)
liefert **auf dem alpha-Server selbst** (Port 3000 am Proxy vorbei) `200` mit
gültigem Manifest. Tessera-seitig ist alles in Ordnung.
- **Vor** Tessera steht der Nginx Proxy Manager und antwortet auf jede Anfrage
an `alpha.tessera.ctl.de` mit `401 Authorization Required`
(`WWW-Authenticate: Basic`), gemessen vom Dev-Host (192.168.13.11) und vom
Testserver selbst (192.168.200.240 über die öffentliche Adresse 217.7.63.32).
Die Webansicht der App kann so ein Passwortfenster beantworten und sich das
merken; der Updater (`tauri-plugin-updater`, eigener `reqwest`-Client) nicht.
- Im Plugin führt ein Nicht-2xx-Status NICHT zu einem Fehler mit Statuscode:
`updater.rs` Z. 529-559 loggt nur „did not respond with a successful status
code“, lässt `last_error` leer und endet in `Err(Error::ReleaseNotFound)`.
Unser `spawn_version_check` fängt das mit `Err(_) => {}` — **stumm**. Der
Eintrag bleibt für immer „Update installieren“ (gesperrt), die App prüft
außerdem nur beim Start (und beim Serverwechsel).
Das ist der eigentliche Produktfehler dieser Aufgabe: ein fehlgeschlagener
Update-Check ist vom Zustand „kein Update“ nicht unterscheidbar, und es gibt
keinen Weg, die Prüfung ohne Neustart zu wiederholen. Den Passwortschutz am
Proxy selbst kann der Client nicht lösen (und soll es nicht: Zugangsdaten
gehören nicht in ausgelieferte Clients) — er wird dem Nutzer als Grund
angezeigt.
## Gebundene Entscheidungen (Orchestrator)
1. **Der Update-Eintrag hat drei Endzustände, alle anklickbar außer während
Prüfung/Installation und bei http-Server:**
- Update gefunden → `Auf Beta-Stand <sha7> aktualisieren` / `Auf Version X.Y.Z aktualisieren` (wie bisher), Klick installiert.
- Kein Update → `Kein Update verfügbar – erneut prüfen`, Klick startet die Prüfung erneut.
- Prüfung fehlgeschlagen → `Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen` bzw. ohne Status `Update-Prüfung fehlgeschlagen (keine Verbindung) – erneut prüfen`, Klick startet die Prüfung erneut.
- Während der Prüfung: `Suche nach Updates…` (gesperrt). Während Download/Installation wie bisher (gesperrt, Fortschritt im Text).
- http-Server: `Update nur über https möglich` (gesperrt, unverändert).
- Beim Bau des Menüs steht `Suche nach Updates…` (gesperrt), weil `setup` die Prüfung sofort startet; ohne gespeicherte Server-Adresse `Kein Update verfügbar – erneut prüfen` (Klick ohne Adresse: nichts tun).
2. **Statuscode nachliefern.** Bei `Err(ReleaseNotFound)` (= Server hat geantwortet, aber nicht 2xx/204) stellt der Client dieselbe Anfrage einmal mit seinem eigenen `reqwest`-Client (Timeout 8 s, Muster `check_server`) an die konkret gebaute Adresse (Platzhalter ersetzt: `target` = `std::env::consts::OS`, `arch` = `std::env::consts::ARCH`, `current` = `CARGO_PKG_VERSION`, `base` wie bisher) und liest NUR den Statuscode. Rumpf wird nicht ausgewertet. Bei `Err(Reqwest(..))`/`Err(Network(..))`/`Err(Io(..))` des Plugins (keine Verbindung, TLS, Timeout) keine zweite Anfrage: Status `None`.
3. **Benachrichtigung mit Erklärung**, einmal je unterschiedlichem Fehlertext (Mutex<String> mit dem zuletzt gemeldeten Text; gleicher Text wird bei der periodischen Prüfung nicht erneut gemeldet). Texte über eine reine Funktion `check_failure_labels(status: Option<u16>) -> (String, String)`:
- `Some(401)` / `Some(403)`: Menü `Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen`; Body `Der Server hat die Update-Anfrage mit HTTP 401 abgewiesen. Meist steht ein Passwortschutz oder eine Zugriffsliste am vorgeschalteten Proxy davor, die die App für Updates nicht durchlaufen kann. Anmeldung und Arbeiten in der App sind davon nicht betroffen.`
- `Some(n)` sonst: Menü `Update-Prüfung fehlgeschlagen (HTTP n) – erneut prüfen`; Body `Der Server hat auf die Update-Anfrage mit HTTP n geantwortet statt mit Paketdaten.`
- `None`: Menü `Update-Prüfung fehlgeschlagen (keine Verbindung) – erneut prüfen`; Body `Der Server war für die Update-Prüfung nicht erreichbar. Die App prüft in vier Stunden erneut – oder über den Menüeintrag.`
4. **Periodische Prüfung alle 4 Stunden** (`std::thread::spawn` mit `std::thread::sleep(Duration::from_secs(4 * 3600))` in Schleife; kein neues Crate). Je Durchlauf: gespeicherte Adresse frisch über `stored_server_url(app)` lesen (Serverwechsel berücksichtigt); wenn `PendingUpdate` bereits ein Update hält → überspringen (keine wiederholte Benachrichtigung „Neuer Beta-Stand“); sonst `spawn_version_check`. Der Thread wird in `setup` einmal gestartet.
5. **Klick auf „update“:** `PendingUpdate.take()` → vorhanden: `spawn_update_install` (wie bisher); sonst: wenn eine Server-Adresse gespeichert ist → `spawn_version_check` (manuelles „erneut prüfen“); ohne Adresse → nichts. Der bisherige Browser-Rückfall (`open_download_page`) bleibt NUR im Fehlerpfad der Installation.
6. Ein Mutex-Zustand für die Benachrichtigungs-Entprellung als eigener `app.manage`-Typ (`LastCheckNotice(Mutex<String>)`), damit `spawn_version_check` keine Signatur-Änderung nach außen braucht.
7. Alle Menütexte Deutsch (wie bisher, „Sie“ in Benachrichtigungen), Kommentare Deutsch im Stil der Datei, Verweise auf `updater.rs`-Zeilen wie im Bestand.
## Aufgabe (eine Datei Code, plus Changelog)
<tasks>
<task type="auto" tdd="true">
<name>Aufgabe 1: lib.rs — Zustände des Update-Eintrags, Statuscode-Diagnose, Entprellung, 4-Stunden-Prüfung, Klick = erneut prüfen; Tests; Changelog</name>
<files>apps/desktop/src-tauri/src/lib.rs, CHANGELOG.md</files>
<behavior>
- `check_failure_labels(Some(401))` → Menü enthält `HTTP 401` und endet auf `– erneut prüfen`; Body enthält `Passwortschutz` und `Zugriffsliste`. `Some(403)` gleiche Body-Erklärung mit `HTTP 403`. `Some(502)` → Menü `HTTP 502`, Body ohne `Passwortschutz`, enthält `statt mit Paketdaten`. `None` → Menü enthält `keine Verbindung`, Body enthält `vier Stunden`.
- `diagnostic_update_url("https://alpha.example", "windows", "x86_64", "1.2.0")` → `https://alpha.example/api-proxy/desktop/update?target=windows&arch=x86_64&current=1.2.0&base=https%3A%2F%2Falpha.example` (Schlussstrich getrimmt; `base` kodiert wie in `update_endpoint`).
- Konstanten: `UPDATE_ITEM_CHECKING == "Suche nach Updates…"`, `UPDATE_ITEM_NONE == "Kein Update verfügbar – erneut prüfen"`, `UPDATE_ITEM_INSECURE` unverändert.
- `is_update_newer`, `update_labels`, `release_labels`, `update_endpoint` unverändert (bestehende Tests bleiben grün).
</behavior>
<action>
1. Konstanten: `UPDATE_ITEM_DEFAULT` entfernen (durch `UPDATE_ITEM_CHECKING` und `UPDATE_ITEM_NONE` ersetzt), Kommentare anpassen. Neue Konstante `UPDATE_CHECK_INTERVAL: Duration = 4 h` mit Begründung (Tray-App läuft tagelang; nur Start-Prüfung → Update nie gesehen, Befund 22.09.2026).
2. Reine Funktionen `check_failure_labels(status: Option<u16>) -> (String, String)` und `diagnostic_update_url(server: &str, target: &str, arch: &str, current: &str) -> String` (nutzt `api_url` + `url::Url::query_pairs_mut` wie `update_endpoint`, damit die Kodierung identisch ist). Tests zuerst (rot: Funktionen fehlen), mindestens 6 `#[test]` gemäß `<behavior>`.
3. `async fn probe_update_status(url: String) -> Option<u16>`: `reqwest::Client::builder().timeout(8 s)`, `GET`, `Some(resp.status().as_u16())`, bei Fehler `None`. Keine Auswertung des Rumpfs, kein Folgen von Redirects nötig (Standard).
4. `spawn_version_check`: zu Beginn `UPDATE_ITEM_CHECKING` + gesperrt (statt DEFAULT). Ergebnisse:
- `Ok(Some(update))` wie bisher.
- `Ok(None)` → Text `UPDATE_ITEM_NONE`, `set_enabled(true)`.
- `Err(InsecureTransportProtocol)` → wie bisher (gesperrt).
- `Err(ReleaseNotFound)` → `probe_update_status(diagnostic_update_url(&server_url, std::env::consts::OS, std::env::consts::ARCH, env!("CARGO_PKG_VERSION"))).await` → `check_failure_labels(status)` → Menütext setzen, `set_enabled(true)`, Benachrichtigung nur, wenn der Body vom zuletzt gemeldeten (`LastCheckNotice`) abweicht; danach dort ablegen.
- `Err(_)` sonst → `check_failure_labels(None)`, gleiche Behandlung.
Bei `Ok(Some)` und `Ok(None)` `LastCheckNotice` leeren, damit ein späterer Fehler wieder gemeldet wird.
5. `setup`: Menüeintrag mit `UPDATE_ITEM_CHECKING` bauen, wenn eine Adresse gespeichert ist, sonst `UPDATE_ITEM_NONE`; `.enabled(server_url.is_none())` entsprechend. `app.manage(LastCheckNotice(Mutex::new(String::new())))`. Nach dem Start der Erstprüfung den Wiederhol-Thread starten: `let handle = app.handle().clone(); std::thread::spawn(move || loop { std::thread::sleep(UPDATE_CHECK_INTERVAL); let pending = handle.state::<PendingUpdate>().0.lock().map(|g| g.is_some()).unwrap_or(false); if pending { continue; } if let Some(url) = stored_server_url(&handle) { spawn_version_check(handle.clone(), url); } })`. Kommentar: warum kein `tokio::time` (kein neues Feature/Crate) und warum `PendingUpdate` den Durchlauf überspringt.
6. Klick „update“: `take()` wie bisher; `None` → `if let Some(url) = stored_server_url(app) { spawn_version_check(app.clone(), url) }`. Kommentar aktualisieren (der Browser-Weg ist nicht mehr der Rückfall des Klicks).
7. `cargo fmt`, `cargo clippy` (0 Warnungen, wie CI), `cargo test` in `apps/desktop/src-tauri` — alle bestehenden Tests plus die neuen grün. `cargo build` (Debug reicht lokal; Release/Windows baut die CI).
8. `CHANGELOG.md` unter „Unveröffentlicht → Behoben“ als erster Stichpunkt: „Desktop-App: der Update-Eintrag im Menü des Infobereich-Symbols bleibt nicht mehr stumm ausgegraut – schlägt die Update-Prüfung fehl, steht der Grund im Eintrag (z. B. „HTTP 401“, wenn ein Passwortschutz am Proxy die Anfrage abweist) und ein Klick prüft erneut; die App prüft außerdem alle vier Stunden, nicht mehr nur beim Start“ (kein Fließtext, Tonlage der Nachbarzeilen).
Commit: `fix(desktop): Update-Eintrag nennt den Grund einer fehlgeschlagenen Pruefung, Klick prueft erneut, Pruefung alle 4 h` (Wortlaut frei, Stil `git log --oneline -15`).
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri && cargo fmt --check && cargo clippy 2>&1 | tail -3 && cargo test 2>&1 | tail -5 && grep -q 'HTTP 401' /home/vicolab/projects/tessera-ctl/CHANGELOG.md</automated>
</verify>
<done>Tests in `lib.rs` ≥ 6 neue, alle grün, die Label-/URL-Tests nachweislich zuerst rot (Kompilierfehler „cannot find function“ zählt als rot — im SUMMARY nennen). `cargo clippy` ohne Warnung, `cargo fmt --check` sauber, `cargo build` erfolgreich. Menüzustände wie in Entscheidung 1; Klick ohne abgelegtes Update startet die Prüfung; Wiederhol-Thread alle 4 h; Benachrichtigung entprellt. CHANGELOG-Zeile steht. Genau ein Commit mit Scope `desktop`.</done>
</task>
</tasks>
## Hinweise für den Executor
- Nur `apps/desktop/src-tauri/src/lib.rs` und `CHANGELOG.md` anfassen. Keine neuen Crates, keine Cargo.toml-Änderung (reqwest ist da, `url` kommt über `tauri::Url`).
- `MenuItem::set_text`/`set_enabled` liefern `Result`, wie im Bestand mit `let _ =` ignorieren.
- `stored_server_url(app)` existiert (siehe `open_download_page`). `AppHandle` ist Clone + Send; `std::thread::spawn` mit dem Klon ist zulässig (die Setup-Funktion nutzt bereits `app.handle().clone()` für den Async-Task).
- Die Prüfung im CI: `cargo check` + `cargo clippy`; lokal zusätzlich `cargo test`. Der Cross-Bau für Windows läuft in der CI (Stempel ändert sich, weil `apps/desktop` berührt wird).
- Nicht anfassen: `is_update_newer`, `update_labels`, `release_labels`, `update_endpoint`, `spawn_update_install`.
## Verifikation durch den Orchestrator (nach CI)
- CI-Lauf grün, Job `desktop` hat neu gebaut (kein Cache-Treffer), Manifest im API-Abbild trägt den neuen Stempel.
- Optional auf der Windows-Test-VM gegen alpha: neuer Client zeigt `Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen` und eine Benachrichtigung mit der Proxy-Erklärung.
<threat_model>
ASVS 1, block on high.
| ID | Bedrohung | Schwere | Disposition |
|---|---|---|---|
| T-FRG-01 | Diagnose-Anfrage folgt Redirects zu fremden Hosts | low | Nur Statuscode wird gelesen, kein Rumpf; Ziel ist die vom Nutzer gespeicherte Server-Adresse; kein Geheimnis in der Anfrage. Akzeptiert. |
| T-FRG-02 | Benachrichtigungs-Spam durch periodische Prüfung gegen kaputten Proxy | low | Entprellung über `LastCheckNotice` (gleicher Text wird nicht erneut gemeldet). Mitigiert. |
| T-FRG-03 | Proxy-Zugangsdaten in den Client einbauen, um 401 zu umgehen | high | Ausdrücklich NICHT umgesetzt (Entscheidung Befund); der Grund wird angezeigt, die Behebung liegt am Proxy. Mitigiert durch Nicht-Bau. |
| T-FRG-04 | Wiederhol-Thread startet Prüfung während Installation | low | Während der Installation ist `PendingUpdate` durch `take()` leer, ein Durchlauf würde nur eine Prüfung anstoßen; `spawn_version_check` setzt den Menütext — Restrisiko: Fortschrittstext wird überschrieben, wenn genau im Download-Fenster die 4-h-Marke fällt. Akzeptiert (Download dauert Minuten, Intervall Stunden). |
</threat_model>
@@ -0,0 +1,152 @@
---
phase: quick-260922-frg
plan: 01
subsystem: apps/desktop/src-tauri
tags: [desktop, tauri, updater, tray, proxy, 401, tdd]
status: complete
requires:
- "quick-260917-kgc (In-App-Updater, PendingUpdate, spawn_version_check)"
provides:
- "Update-Eintrag im Tray mit drei sichtbaren Endzustaenden, nie mehr stumm gesperrt"
- "Statuscode-Diagnose nach Err(ReleaseNotFound) ueber eigene Anfrage"
- "Wiederholte Update-Pruefung alle vier Stunden"
- "Klick auf den Eintrag ohne abgelegtes Update prueft erneut"
affects:
- "apps/desktop/src-tauri/src/lib.rs"
- "CHANGELOG.md"
tech-stack:
added: []
patterns:
- "Fehlerzustand eines Hintergrund-Checks im Menuetext ausschreiben statt Eintrag stumm sperren"
- "Zweite Diagnose-Anfrage nur fuer den Statuscode, wenn eine Bibliothek ihn verschluckt"
- "Benachrichtigung ueber Mutex<String> mit dem zuletzt gemeldeten Text entprellen"
key-files:
created: []
modified:
- apps/desktop/src-tauri/src/lib.rs
- CHANGELOG.md
decisions:
- "Proxy-Zugangsdaten werden NICHT in den Client eingebaut (T-FRG-03); der Grund wird angezeigt, die Behebung liegt am Proxy"
- "Wiederhol-Thread als std::thread mit sleep statt tokio::time, damit kein neues Feature/Crate noetig ist"
- "diagnostic_update_url liefert String statt Option: bei unparsbarer Adresse faellt sie auf api_url zurueck, gespeicherte Adressen sind ohnehin immer parsebar"
- "report_check_failure und clear_check_notice als eigene Helfer, damit die drei Fehlerzweige in spawn_version_check kurz bleiben"
metrics:
duration: "ca. 20 min (11:05 bis 11:26 Uhr, 22.09.2026)"
completed: 2026-09-22
actuals:
tokens: 5600
tasks: 1
commits: 1
plan_head_before: ae36a22
---
# Quick-Aufgabe 260922-frg: Update-Eintrag im Tray nie mehr stumm ausgegraut Summary
Der Update-Eintrag im Menue des Infobereich-Symbols zeigt jetzt in jedem Fall,
was die Pruefung ergeben hat: ein Update, kein Update, oder den Grund des
Fehlschlags (z. B. „HTTP 401“, wenn der Passwortschutz am Proxy die Anfrage
abweist). Ein Klick auf den Eintrag prueft erneut, und die App prueft von
selbst alle vier Stunden statt nur beim Start. Nur der http-Fall bleibt
weiterhin dauerhaft gesperrt.
## Was sich fuer den Betrieb aendert
Bisher konnte der Nutzer nicht unterscheiden, ob es kein Update gibt oder ob
die Pruefung gescheitert ist — in beiden Faellen stand da grau „Update
installieren“, und ohne Neustart der App wurde nie wieder geprueft. Genau das
war auf dem Client gegen alpha passiert: der Nginx Proxy Manager antwortet auf
die Update-Anfrage mit 401, das Updater-Plugin macht daraus stumm
`ReleaseNotFound`, und der Eintrag blieb grau, obwohl alpha das Paket
`1.2.0-beta.gc001a08` bereithielt.
Jetzt steht in diesem Fall „Update-Prüfung fehlgeschlagen (HTTP 401) – erneut
prüfen“ im Menue, und einmalig erscheint eine Benachrichtigung, die den
Passwortschutz am vorgeschalteten Proxy als wahrscheinlichen Grund nennt und
klarstellt, dass Anmeldung und Arbeiten in der App nicht betroffen sind. Den
Passwortschutz selbst kann und soll der Client nicht umgehen; die Behebung
liegt am Proxy (Ausnahme fuer `/api-proxy/desktop/*` oder Aufhebung des
Schutzes fuer alpha).
## Umsetzung
- **Konstanten:** `UPDATE_ITEM_DEFAULT` („Update installieren“) ist weg,
ersetzt durch `UPDATE_ITEM_CHECKING` („Suche nach Updates…“, gesperrt
waehrend der Pruefung) und `UPDATE_ITEM_NONE` („Kein Update verfügbar –
erneut prüfen“, anklickbar). Neu `UPDATE_CHECK_INTERVAL = 4 h`.
- **Reine Funktionen:** `check_failure_labels(Option<u16>)` liefert Menue- und
Benachrichtigungstext (401/403 mit Proxy-Erklaerung, andere Codes neutral,
`None` = keine Verbindung). `diagnostic_update_url` baut die Update-Adresse
mit ersetzten Platzhaltern in genau der Kodierung von `update_endpoint`.
- **Statuscode-Diagnose:** Bei `Err(ReleaseNotFound)` stellt
`probe_update_status` dieselbe Anfrage einmal mit eigenem `reqwest`-Client
(8 s Timeout, Muster `check_server`) und liest nur den Statuscode. Bei
Verbindungsfehlern des Plugins (`Err(_)` sonst) keine zweite Anfrage.
- **Entprellung:** `LastCheckNotice(Mutex<String>)` als eigener
`app.manage`-Typ; `report_check_failure` meldet nur einen abweichenden Text,
`clear_check_notice` leert ihn nach `Ok(Some)`/`Ok(None)`.
- **Wiederhol-Thread:** in `setup` einmal gestartet, `std::thread::spawn` mit
`sleep(UPDATE_CHECK_INTERVAL)` in Schleife, liest die Adresse je Durchlauf
frisch und ueberspringt, wenn `PendingUpdate` bereits ein Update haelt.
- **Klick „update“:** `take()` wie bisher; ohne abgelegtes Update startet
`spawn_version_check` mit der gespeicherten Adresse; ohne Adresse nichts.
`open_download_page` bleibt nur im Fehlerpfad der Installation.
- **Menuebau:** „Suche nach Updates…“ (gesperrt) mit gespeicherter Adresse,
sonst „Kein Update verfügbar – erneut prüfen“ (aktiv).
- **Changelog:** neuer erster Stichpunkt unter „Unveröffentlicht → Behoben“.
## TDD Gate Compliance
**RED (nachgewiesen):** Die sieben neuen Tests wurden zuerst eingefuegt.
`cargo test` brach mit neun Fehlern `E0425` ab — `cannot find function
check_failure_labels`, `cannot find function diagnostic_update_url`,
`cannot find value UPDATE_ITEM_CHECKING / UPDATE_ITEM_NONE /
UPDATE_CHECK_INTERVAL`. Der Kompilierfehler zaehlt laut Plan als rot.
**GREEN:** Nach der Umsetzung `cargo test`: 44 bestanden, 0 fehlgeschlagen
(37 Bestandstests plus 7 neue). `is_update_newer`, `update_labels`,
`release_labels`, `update_endpoint`, `spawn_update_install` unveraendert.
**REFACTOR:** Zwei Helfer (`report_check_failure`, `clear_check_notice`)
herausgezogen, damit die Fehlerzweige in `spawn_version_check` lesbar bleiben;
der Kommentar bei `with_desktop_marker` nennt nicht mehr den alten Text
„Update installieren“.
## Cargo-Ergebnisse (apps/desktop/src-tauri, lokal)
| Schritt | Ergebnis |
|---|---|
| `cargo fmt --check` | sauber |
| `cargo clippy` | 0 Warnungen, 0 Fehler |
| `cargo test` | 44 passed, 0 failed |
| `cargo build` (Debug) | erfolgreich, Systembibliotheken vorhanden |
| `grep 'HTTP 401' CHANGELOG.md` | Treffer |
## Commit
- `d73aad1` fix(desktop): Update-Eintrag nennt den Grund einer fehlgeschlagenen Pruefung, Klick prueft erneut, Pruefung alle 4 h — `apps/desktop/src-tauri/src/lib.rs`, `CHANGELOG.md`
## Deviations from Plan
None - plan executed exactly as written. Einzige Ergaenzung ausserhalb der
Aufzaehlung: der Doc-Kommentar bei `with_desktop_marker` verwies noch auf den
entfernten Menuetext „Update installieren“ und wurde mitgezogen (reine
Kommentar-Korrektur, kein Verhalten).
## Known Stubs
Keine.
## Offen (fuer den Orchestrator, nach CI)
- CI-Lauf: Job `desktop` muss neu bauen (apps/desktop beruehrt), Manifest im
API-Abbild traegt den neuen Stempel.
- Optional auf der Windows-Test-VM gegen alpha: neuer Client zeigt
„Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen“ plus die
Benachrichtigung mit der Proxy-Erklaerung. Damit der Client danach das
Update tatsaechlich bekommt, muss der Proxy die Update-Anfrage durchlassen.
## Self-Check: PASSED
- `apps/desktop/src-tauri/src/lib.rs` vorhanden und geaendert
- `CHANGELOG.md` enthaelt die neue Zeile
- Commit `d73aad1` in `git log` vorhanden