Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c01f9d783 | |||
| d73aad1ef1 |
+9
-8
@@ -4,10 +4,10 @@ milestone: v1.2
|
||||
current_phase: 18
|
||||
current_phase_name: desktop-client-fertigstellen
|
||||
status: verified
|
||||
stopped_at: "Zwei Dashboard-Widgets am 2026-09-21 abends gebaut, im Browser nachgewiesen und gepusht: Bilderrahmen (quick-260921-pi9, 4 Commits inkl. Portal-Korrektur der Grossansicht) und XFrame (quick-260921-qd3, 2 Commits). NICHTS OFFEN aus diesen Auftraegen. Offen beim Nutzer: alpha ziehen (beide Widgets kommen mit dem naechsten Beta-Pull), Windows-Client pruefen, Freigabe 1.3.0 auf Zuruf. Kein weiterer Auftrag benannt."
|
||||
last_updated: "2026-09-21T17:40:00.000Z"
|
||||
stopped_at: "22.09.2026: Tray-Update-Befund des Nutzers geklaert (quick-260922-frg, d73aad1): der Proxy vor alpha verlangt Basic-Auth (401), der Updater kann das nicht; Client zeigt jetzt den Grund und prueft alle 4 h. OFFEN BEIM NUTZER: Passwortschutz vor alpha im Nginx Proxy Manager entfernen (oder /api-proxy/desktop/* freigeben), alpha ziehen, neuen Client einmal per Browser installieren. Davor am 21./22.09.: Bilderrahmen + XFrame gebaut, nachgewiesen, gepusht. Freigabe 1.3.0 auf Zuruf."
|
||||
last_updated: "2026-09-22T10:05:00.000Z"
|
||||
last_activity: 2026-09-21
|
||||
last_activity_desc: Quick 260921-pi9 (Bilderrahmen) und 260921-qd3 (XFrame) — zwei neue Dashboard-Widgets, beide im Browser nachgewiesen, drei Rundgang-Befunde beim Bilderrahmen behoben (Grossansicht per Portal, ICU-Plural, Kachel 8x12); api 1175, web 603
|
||||
last_activity_desc: Quick 260922-frg — Tray-Update-Eintrag nennt den Grund (HTTP 401 am Proxy), Klick prueft erneut, Pruefung alle 4 h; Ursache beim Nutzer: Basic-Auth vor alpha
|
||||
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
|
||||
progress:
|
||||
total_phases: 18
|
||||
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||
Plan: 6 of 6
|
||||
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-22 - Kosmetik am Bilderrahmen (fast, 8b45a28): „1 Stunde“ statt „60 Minuten“, Bildanzahl in der Einstellungs-Kopfzeile; am 21.09. davor Quick 260921-pi9 und 260921-qd3: die zwei bestellten Dashboard-Widgets „Bilderrahmen“ und „XFrame“ gebaut, im Browser nachgewiesen, gepusht
|
||||
Last activity: 2026-09-22 - Quick 260922-frg: Tray-Update-Eintrag nennt den Grund einer fehlgeschlagenen Pruefung (HTTP 401 durch Passwortschutz am Proxy vor alpha), Klick prueft erneut, Pruefung alle 4 h; davor Kosmetik am Bilderrahmen (fast, 8b45a28): „1 Stunde“ statt „60 Minuten“, Bildanzahl in der Einstellungs-Kopfzeile; am 21.09. davor Quick 260921-pi9 und 260921-qd3: die zwei bestellten Dashboard-Widgets „Bilderrahmen“ und „XFrame“ gebaut, im Browser nachgewiesen, gepusht
|
||||
|
||||
Progress: [██████████] 99%
|
||||
|
||||
@@ -458,6 +458,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260921-pi9 | **Dashboard-Widget „Bilderrahmen“: eigene Bilder oder https-Adressen als Diashow.** Erstes der zwei vom Nutzer bestellten Widgets. **API:** neues Prisma-Modell `DashboardImage` (Bytes in der Datenbank — kein neues Docker-Volume, Sicherung ueber den DB-Dump), handgeschriebene Migration `20260921120000_dashboard_image` mit RLS-Regel inklusive Benutzerdimension; Routen `GET/POST /dashboard/images`, `GET/DELETE /dashboard/images/:id`; Bildtyp ausschliesslich ueber Magic Bytes (PNG/JPEG/GIF/WebP), nicht ueber den behaupteten MIME-Typ; 5 MiB je Datei (multer-Grenze, 413), 30 Bilder je Benutzer; fremde Kennung → 404, nie 403; Binaerantwort mit `Cache-Control: private`, `nosniff`, `Content-Disposition: inline` ohne Dateinamen, CSP `default-src 'none'; sandbox`. **Web:** Widget `picture-frame` mit einer geordneten Liste `images` aus Eintraegen mit `kind`-Unterscheider (`upload` oder `url`), Bildausschnitt contain/cover, Intervall 0/5…3600 s, Reihenfolge oder Zufall (nie dasselbe zweimal), Unterschrift-Streifen, Grossansicht per Klick (nicht im Bearbeitungsmodus), kaputte Bilder fallen aus dem Umlauf; Bildverwaltung im `WidgetSettingsPanel` (Upload, https-Adresse, Unterschrift, Pfeile, Entfernen loescht den Upload auch serverseitig). Fremdbilder laedt AUSSCHLIESSLICH der Browser (`<img referrerPolicy="no-referrer">`) — die API ruft nie eine Adresse ab, keine SSRF-Flaeche; https-Pflicht web-seitig zweifach (Formular + Render-Resolver), weil die API Widget-Configs nicht inhaltlich prueft. **Browser-Rundgang (Orchestrator, zehn Punkte) fand drei Dinge, behoben in 8bf3601:** die Grossansicht war auf die Kachelflaeche beschraenkt (ein `react-grid-item` mit CSS-`transform` wird fuer `position: fixed` zum Bezugsrahmen → `createPortal` in `document.body` wie der Kalender-Tooltip), „1 Minuten“ → ICU-Plural, Standardkachel 8x8 zu flach → 8x12. curl-Rundgang gegen die lebende API belegt 201/400/413/404/401 und fremder Benutzer → 404. **Zahlen:** api 1148 → 1175, web 531 → 569, type-check 4/4, lint 5/5, RLS-Waechter 78/78, Zaehler unveraendert (`as unknown as` 27/6, `noNonNullAssertion` 56, `noExplicitAny` 13). Anwenderhandbuch und CHANGELOG ergaenzt. | 2026-09-21 | 737974b,c080580,c3b4597,8bf3601 | [260921-pi9-dashboard-widget-bilderrahmen-bilder-hoc](./quick/260921-pi9-dashboard-widget-bilderrahmen-bilder-hoc/) |
|
||||
| 260921-qd3 | **Dashboard-Widget „XFrame“: eine Webseite per https-Adresse als Rahmen in der Kachel.** Zweites der zwei vom Nutzer bestellten Widgets, vom Nutzer so benannt. Config `url` (https-Pflicht ueber dieselbe `isHttpsUrl`-Regel wie der Bilderrahmen, web-seitig doppelt: Formular + Render-Resolver), `title` (max. 100 Zeichen, Kopfleiste), `reloadSeconds` (0/60/300/600/1800/3600; Timer haengt den Rahmen per `key` neu ein, nicht im Bearbeitungsmodus). `<iframe sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox">` — bewusst OHNE `allow-top-navigation*` (die eingebettete Seite kann den Tessera-Tab nicht umleiten) und OHNE `allow-modals`; `allow=""` (keine Kamera/Mikro/Standort-Delegation), `referrerPolicy="no-referrer"`, `loading="lazy"`. Die API ruft die Adresse nie ab (nur `'xframe'` im `@IsIn` des DTO; kein CSP/Frame-Header in apps/web noetig, per grep belegt). Im Bearbeitungsmodus liegt eine unsichtbare Flaeche ueber dem Rahmen, sonst schluckt der iframe die Zeigerereignisse und die Kachel liesse sich nicht ziehen. Ob eine Seite das Einbetten verweigert, entscheidet die fremde Seite (`X-Frame-Options`/`frame-ancestors`, cross-origin nicht erkennbar) — deshalb dauerhafter Hinweis im Formular und immer ein Link „In neuem Tab öffnen“ (`rel="noopener noreferrer"`, in der Kopfleiste oder als Ecksymbol). Formular als eigenes Modul `xframe-config-form.tsx` wie beim Bilderrahmen; Kachel-Vorgabe 12x12. Browser-Rundgang (Orchestrator, neun Punkte + Tests) ohne Befund: example.com im Rahmen, google.com verweigert mit `X-Frame-Options: sameorigin` und der Link fuehrt trotzdem hin, Neuladen nach 60 s mit genau einem zweiten Dokumentabruf, Ziehen und Groesse aendern ueber dem Rahmen, API-Log ohne Fremdabruf. **Zahlen:** web 569 → 603, api 1175 unveraendert, type-check 4/4, lint 5/5, Zaehler unveraendert (`as unknown as` 27/6, `noNonNullAssertion` 56, `noExplicitAny` 13). Biome `useAnchorContent` wertet `aria-label` nicht als Linkinhalt → `sr-only`-Text statt `biome-ignore`. | 2026-09-21 | d63d9f5,20a9eb2 | [260921-qd3-dashboard-widget-xframe-eine-webseite-pe](./quick/260921-qd3-dashboard-widget-xframe-eine-webseite-pe/) |
|
||||
| fast-260922 | **Kosmetik nach dem Browser-Rundgang (fast, ohne Akte).** Bilderrahmen: das laengste Wechselintervall (3600 s) hiess „60 Minuten“, beim XFrame dieselbe Stufe „Jede Stunde“ → neuer Schluessel `pictureFrame.intervalHours` (ICU-Plural, de + en); Kopfzeile „Bilderrahmen #N“ unter Einstellungen → Dashboard nennt jetzt „— 1 Bild“ / „— N Bilder“ (zwei Schluessel statt ICU, weil die Panel-Tests eine einfache Uebersetzungs-Attrappe nutzen). Web-Tests 603 → 604. Hinweis fuer spaeter: `gsd-tools quick-tasks-append` scheitert an dieser Tabelle, weil aeltere Zeilen (260918-gza, 260921-iwr, 260921-m34) unmaskierte `\|` im Text tragen — Zeilen daher von Hand anfuegen. | 2026-09-22 | 8b45a28 | — |
|
||||
| 260922-frg | **Desktop-Client: Update-Eintrag im Tray nie mehr stumm ausgegraut.** Befund des Nutzers: „Update installieren“ bleibt grau, obwohl alpha `1.2.0-beta.gc001a08` anbietet und der Client auf `a6d1a64` steht — auch nach App-Neustart. Nachgemessen: Tessera-seitig antwortet `/desktop/update` auf dem alpha-Server selbst (am Proxy vorbei) mit 200 und gueltigem Manifest; DAVOR antwortet der Nginx Proxy Manager auf jede Anfrage an alpha mit `401 Basic` (vom Dev-Host und vom Testserver ueber 217.7.63.32 gemessen). Die Webansicht der App merkt sich das Proxy-Passwort, der Updater (`tauri-plugin-updater`, eigener reqwest) nicht. **Produktfehler:** das Plugin verschluckt Nicht-2xx-Status (`updater.rs` 529-559: `last_error` bleibt leer → `Err(ReleaseNotFound)`), unser `Err(_) => {}` machte daraus stumm denselben grauen Eintrag wie „kein Update“; geprueft wurde nur beim Start. **Fix (d73aad1, nur lib.rs + CHANGELOG):** drei Endzustaende, alle anklickbar — „Auf Beta-Stand … aktualisieren“ (installiert), „Kein Update verfügbar – erneut prüfen“, „Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen“ (Statuscode per eigener Diagnose-Anfrage nachgeliefert, nur Status gelesen); Benachrichtigung mit Erklaerung (Passwortschutz/Zugriffsliste am Proxy), entprellt ueber `LastCheckNotice`; Wiederhol-Thread alle 4 h (`std::thread`, ueberspringt bei abgelegtem Update); http-Server weiterhin „Update nur über https möglich“. Proxy-Zugangsdaten NICHT in den Client (T-FRG-03). `cargo fmt/clippy/test/build` gruen, 37 → 44 Tests, Rot-Nachweis 9x E0425. **Behebung beim Nutzer:** Passwortschutz vor alpha im Proxy Manager entfernen oder `/api-proxy/desktop/*` durchlassen; neuen Client einmal ueber den Browser installieren. | 2026-09-22 | d73aad1 | [260922-frg-desktop-client-update-eintrag-im-tray-ni](./quick/260922-frg-desktop-client-update-eintrag-im-tray-ni/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -499,8 +500,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-09-21T17:40:00Z
|
||||
Resumed: 2026-09-21 (abends) — Sitzung ueber /gsd-resume-work fortgesetzt; danach die zwei bestellten Widgets gebaut.
|
||||
Stopped at: Beide Widgets fertig, nachgewiesen, gepusht (main == origin/main). Naechster Auftrag nicht benannt. Offen beim Nutzer: alpha ziehen, Windows-Client pruefen, Freigabe 1.3.0 auf Zuruf.
|
||||
Last session: 2026-09-22T10:05:00Z
|
||||
Resumed: 2026-09-21 (abends) — Sitzung ueber /gsd-resume-work; danach Bilderrahmen + XFrame, Kosmetik, am 22.09. der Tray-Update-Befund.
|
||||
Stopped at: quick-260922-frg gepusht, CI-Lauf mit Desktop-Neubau laeuft. Beim Nutzer: Passwortschutz vor alpha im Proxy Manager entfernen (sonst kann KEIN Client per Tray aktualisieren), alpha ziehen, neuen Client einmal per Browser installieren. Kein weiterer Auftrag benannt.
|
||||
Resume file: None
|
||||
Last activity: 2026-09-22 - Kosmetik am Bilderrahmen (fast, 8b45a28): „1 Stunde“ statt „60 Minuten“, Bildanzahl in der Einstellungs-Kopfzeile; davor am 21.09. die zwei bestellten Widgets „Bilderrahmen“ und „XFrame“
|
||||
Last activity: 2026-09-22 - Quick 260922-frg: Tray-Update-Eintrag nennt den Grund einer fehlgeschlagenen Pruefung (HTTP 401 durch Passwortschutz am Proxy vor alpha), Klick prueft erneut, Pruefung alle 4 h
|
||||
|
||||
+122
@@ -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¤t=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¤t=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>
|
||||
+152
@@ -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
|
||||
@@ -22,6 +22,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
|
||||
### Behoben
|
||||
|
||||
- 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
|
||||
- Favoriten-Widget: kein Symbol bei Seiten mit Zertifikatsfehler oder internen Adressen – das Symbol wird jetzt trotz Zertifikatsfehler geholt, bei internen Adressen versucht es der Browser direkt
|
||||
|
||||
## 1.2.0 – 2026-09-17
|
||||
|
||||
@@ -12,13 +12,24 @@ use tauri_plugin_opener::OpenerExt;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tauri_plugin_updater::{Update, UpdaterExt};
|
||||
|
||||
/// Standardtext des Update-Eintrags im Tray-Menü, bevor eine neuere Version
|
||||
/// gefunden wurde. Der Eintrag installiert das Update seit quick-260917-kgc
|
||||
/// direkt in der App (Download, Signaturpruefung, Installation, Neustart);
|
||||
/// den Browser oeffnet er nur noch als Rueckfall. `spawn_version_check`
|
||||
/// setzt den Eintrag nach einem Serverwechsel hierauf zurück, damit kein
|
||||
/// Hinweis des alten Servers stehen bleibt.
|
||||
const UPDATE_ITEM_DEFAULT: &str = "Update installieren";
|
||||
/// Menuetext des Update-Eintrags, solange eine Pruefung laeuft (gesperrt).
|
||||
/// `spawn_version_check` setzt den Eintrag zu Beginn hierauf, auch nach einem
|
||||
/// Serverwechsel, damit kein Hinweis des alten Servers stehen bleibt. Der
|
||||
/// Eintrag installiert ein gefundenes Update seit quick-260917-kgc direkt in
|
||||
/// der App (Download, Signaturpruefung, Installation, Neustart).
|
||||
const UPDATE_ITEM_CHECKING: &str = "Suche nach Updates…";
|
||||
|
||||
/// Menuetext, wenn die Pruefung kein Update ergab. Der Eintrag bleibt
|
||||
/// anklickbar und startet die Pruefung erneut (quick-260922-frg): der Nutzer
|
||||
/// soll nie vor einem stumm ausgegrauten Eintrag stehen. Auch der Starttext
|
||||
/// ohne gespeicherte Server-Adresse (Klick ohne Adresse tut nichts).
|
||||
const UPDATE_ITEM_NONE: &str = "Kein Update verfügbar – erneut prüfen";
|
||||
|
||||
/// Abstand der wiederholten Update-Pruefung. Die Tray-App laeuft tagelang
|
||||
/// durch; mit der Pruefung nur beim Start hat ein Client ein neues Paket auf
|
||||
/// dem Server nie gesehen (Befund 22.09.2026, quick-260922-frg). Vier
|
||||
/// Stunden reichen fuer den Beta-Kanal und belasten den Server nicht.
|
||||
const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(4 * 3600);
|
||||
|
||||
/// Menuetext, wenn die gespeicherte Server-Adresse mit `http://` beginnt:
|
||||
/// das Updater-Plugin verweigert im Release-Bau unverschluesselte Endpunkte
|
||||
@@ -37,6 +48,15 @@ struct VersionResponse {
|
||||
/// `Update` ist Clone + Send + Sync, `app.manage` verlangt das.
|
||||
struct PendingUpdate(Mutex<Option<(Update, String)>>);
|
||||
|
||||
/// Benachrichtigungstext der zuletzt gemeldeten fehlgeschlagenen
|
||||
/// Update-Pruefung. Die Pruefung laeuft alle `UPDATE_CHECK_INTERVAL` erneut;
|
||||
/// gegen einen dauerhaft sperrenden Proxy wuerde sonst alle vier Stunden
|
||||
/// dieselbe Meldung erscheinen (T-FRG-02). Nur ein ABWEICHENDER Text wird
|
||||
/// erneut gemeldet; ein Erfolg (`Ok(..)`) leert den Wert, damit ein spaeterer
|
||||
/// Fehler wieder sichtbar wird. Eigener `app.manage`-Typ, damit
|
||||
/// `spawn_version_check` seine Signatur behaelt.
|
||||
struct LastCheckNotice(Mutex<String>);
|
||||
|
||||
/// Baut die Adresse eines API-Pfads aus der gespeicherten Server-Adresse.
|
||||
/// Die API ist vom Client nur ueber den Web-Ursprung erreichbar
|
||||
/// (Next.js-Rewrite `/api-proxy/*`, siehe 18-01) -- niemals direkt unter dem
|
||||
@@ -76,7 +96,7 @@ fn with_client_marker(url: &tauri::Url, version: &str, commit: &str, os: &str) -
|
||||
/// aeltere Middleware weiterhin als Desktop-App erkannt wird. Die Werte
|
||||
/// gehen NUR in die Navigation, niemals in den gespeicherten Server-Wert
|
||||
/// (`server_url` im Store bleibt die reine Adresse). Der Browser-Rueckfall
|
||||
/// des Tray-Eintrags "Update installieren" (`open_download_page`) bekommt
|
||||
/// einer fehlgeschlagenen Update-Installation (`open_download_page`) bekommt
|
||||
/// den Marker bewusst NICHT -- er oeffnet im System-Browser, dort muessen
|
||||
/// die Download-Links sichtbar bleiben.
|
||||
fn with_desktop_marker(url: &tauri::Url) -> tauri::Url {
|
||||
@@ -179,6 +199,78 @@ fn update_endpoint(server: &str) -> Option<tauri::Url> {
|
||||
Some(url)
|
||||
}
|
||||
|
||||
/// Baut die Update-Adresse mit ERSETZTEN Platzhaltern -- dieselbe Anfrage,
|
||||
/// die das Plugin stellt (`update_endpoint`, gleiche Kodierung ueber
|
||||
/// `query_pairs_mut`), nur mit konkreten Werten. `spawn_version_check` stellt
|
||||
/// sie nach `Err(ReleaseNotFound)` einmal selbst, um den Statuscode zu
|
||||
/// erfahren: das Plugin verschluckt einen Nicht-2xx-Status (updater.rs
|
||||
/// Z. 529-559 loggt nur, `last_error` bleibt leer, Ergebnis ist
|
||||
/// `ReleaseNotFound`). Reine Funktion, darum in den Tests mit festen Werten
|
||||
/// geprueft.
|
||||
fn diagnostic_update_url(server: &str, target: &str, arch: &str, current: &str) -> String {
|
||||
let base = server.trim_end_matches('/');
|
||||
match tauri::Url::parse(&api_url(server, "/desktop/update")) {
|
||||
Ok(mut url) => {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("target", target)
|
||||
.append_pair("arch", arch)
|
||||
.append_pair("current", current)
|
||||
.append_pair("base", base);
|
||||
url.into()
|
||||
}
|
||||
// Gespeicherte Adressen sind immer parsebar (parse_server_url);
|
||||
// reine Absicherung, damit die Funktion kein Option liefern muss.
|
||||
Err(_) => api_url(server, "/desktop/update"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Liefert (Menuetext, Benachrichtigungstext) fuer eine fehlgeschlagene
|
||||
/// Update-Pruefung. `status` ist der Antwortcode des Servers, wenn er
|
||||
/// geantwortet hat (`Some`), sonst `None` (keine Verbindung, TLS, Timeout).
|
||||
/// 401/403 bekommen die Proxy-Erklaerung: Befund 22.09.2026 -- der Nginx
|
||||
/// Proxy Manager vor alpha verlangt Basic-Auth, die Webansicht beantwortet
|
||||
/// das Passwortfenster, der Updater (eigener `reqwest`-Client) kann es nicht.
|
||||
/// Zugangsdaten gehoeren nicht in ausgelieferte Clients (T-FRG-03), darum
|
||||
/// wird der Grund angezeigt, nicht umgangen. Alle Texte enden auf
|
||||
/// "– erneut prüfen", weil der Klick die Pruefung wiederholt.
|
||||
fn check_failure_labels(status: Option<u16>) -> (String, String) {
|
||||
match status {
|
||||
Some(code @ (401 | 403)) => (
|
||||
format!("Update-Prüfung fehlgeschlagen (HTTP {code}) – erneut prüfen"),
|
||||
format!(
|
||||
"Der Server hat die Update-Anfrage mit HTTP {code} 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(code) => (
|
||||
format!("Update-Prüfung fehlgeschlagen (HTTP {code}) – erneut prüfen"),
|
||||
format!("Der Server hat auf die Update-Anfrage mit HTTP {code} geantwortet statt mit Paketdaten."),
|
||||
),
|
||||
None => (
|
||||
"Update-Prüfung fehlgeschlagen (keine Verbindung) – erneut prüfen".to_string(),
|
||||
"Der Server war für die Update-Prüfung nicht erreichbar. Die App prüft in vier \
|
||||
Stunden erneut – oder über den Menüeintrag."
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stellt die Update-Anfrage ein zweites Mal mit eigenem `reqwest`-Client
|
||||
/// (Timeout 8 s, Muster `check_server`) und liest NUR den Statuscode -- der
|
||||
/// Rumpf wird nicht ausgewertet, Redirects folgen dem Standard (T-FRG-01:
|
||||
/// Ziel ist die vom Nutzer gespeicherte Adresse, kein Geheimnis in der
|
||||
/// Anfrage). `None`, wenn keine Antwort kam.
|
||||
async fn probe_update_status(url: String) -> Option<u16> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(8))
|
||||
.build()
|
||||
.ok()?;
|
||||
let resp = client.get(url).send().await.ok()?;
|
||||
Some(resp.status().as_u16())
|
||||
}
|
||||
|
||||
/// Leitet (Menuetext, Benachrichtigungstext) aus Client- und Server-Version
|
||||
/// ab: eine neue Basis X.Y.Z wird als Version genannt (auch bei Beta), bei
|
||||
/// gleicher Basis der Beta-Stempel.
|
||||
@@ -321,14 +413,57 @@ async fn check_for_update(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Meldet eine fehlgeschlagene Update-Pruefung: Menuetext mit Grund, Eintrag
|
||||
/// wieder anklickbar (Klick = erneut pruefen), Benachrichtigung nur, wenn
|
||||
/// der Text vom zuletzt gemeldeten abweicht (`LastCheckNotice`, T-FRG-02).
|
||||
fn report_check_failure(app: &AppHandle, update_item: &MenuItem<Wry>, status: Option<u16>) {
|
||||
let (menu_text, body) = check_failure_labels(status);
|
||||
let _ = update_item.set_text(&menu_text);
|
||||
let _ = update_item.set_enabled(true);
|
||||
let changed = app
|
||||
.state::<LastCheckNotice>()
|
||||
.0
|
||||
.lock()
|
||||
.map(|mut last| {
|
||||
if *last == body {
|
||||
false
|
||||
} else {
|
||||
*last = body.clone();
|
||||
true
|
||||
}
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if changed {
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Tessera-Update")
|
||||
.body(body)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Leert den zuletzt gemeldeten Fehlertext -- nach einer gelungenen Pruefung
|
||||
/// soll ein spaeterer Fehler wieder gemeldet werden.
|
||||
fn clear_check_notice(app: &AppHandle) {
|
||||
if let Ok(mut last) = app.state::<LastCheckNotice>().0.lock() {
|
||||
last.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Versionsprüfung, aus `setup` herausgezogen, damit sie nach einem
|
||||
/// Serverwechsel erneut laufen kann (async, non-blocking wie bisher). Setzt
|
||||
/// den Update-Eintrag zuerst auf den Standardtext zurück und leert den
|
||||
/// abgelegten Update-Stand -- nach einem Wechsel darf kein Hinweis des alten
|
||||
/// Servers stehen bleiben.
|
||||
/// Serverwechsel, beim Klick auf den Eintrag und alle
|
||||
/// `UPDATE_CHECK_INTERVAL` erneut laufen kann (async, non-blocking wie
|
||||
/// bisher). Setzt den Update-Eintrag zuerst auf "Suche nach Updates…"
|
||||
/// (gesperrt) und leert den abgelegten Update-Stand -- nach einem Wechsel
|
||||
/// darf kein Hinweis des alten Servers stehen bleiben. Jeder Ausgang der
|
||||
/// Pruefung endet in einem sichtbaren, anklickbaren Zustand (Update gefunden
|
||||
/// / kein Update / fehlgeschlagen mit Grund); nur der http-Fall bleibt
|
||||
/// gesperrt. Ein fehlgeschlagener Check war bisher vom Zustand "kein Update"
|
||||
/// nicht unterscheidbar (quick-260922-frg).
|
||||
fn spawn_version_check(app: AppHandle, server_url: String) {
|
||||
let update_item = app.state::<TrayItems>().update.clone();
|
||||
let _ = update_item.set_text(UPDATE_ITEM_DEFAULT);
|
||||
let _ = update_item.set_text(UPDATE_ITEM_CHECKING);
|
||||
let _ = update_item.set_enabled(false);
|
||||
if let Ok(mut pending) = app.state::<PendingUpdate>().0.lock() {
|
||||
*pending = None;
|
||||
@@ -365,8 +500,13 @@ fn spawn_version_check(app: AppHandle, server_url: String) {
|
||||
if let Ok(mut pending) = app.state::<PendingUpdate>().0.lock() {
|
||||
*pending = Some((update, menu_text));
|
||||
}
|
||||
clear_check_notice(&app);
|
||||
}
|
||||
Ok(None) => {
|
||||
let _ = update_item.set_text(UPDATE_ITEM_NONE);
|
||||
let _ = update_item.set_enabled(true);
|
||||
clear_check_notice(&app);
|
||||
}
|
||||
Ok(None) => {}
|
||||
// Der Release-Bau verweigert http-Endpunkte (config.rs
|
||||
// `validate_endpoints`), der Debug-Bau warnt nur. Keine
|
||||
// `dangerousInsecureTransportProtocol`-Freigabe (T-KGC-06): der
|
||||
@@ -374,15 +514,33 @@ fn spawn_version_check(app: AppHandle, server_url: String) {
|
||||
Err(tauri_plugin_updater::Error::InsecureTransportProtocol) => {
|
||||
let _ = update_item.set_text(UPDATE_ITEM_INSECURE);
|
||||
}
|
||||
Err(_) => {}
|
||||
// Der Server hat geantwortet, aber nicht mit 2xx/204: das Plugin
|
||||
// verschluckt den Statuscode (updater.rs Z. 529-559). Dieselbe
|
||||
// Anfrage einmal selbst stellen und nur den Status lesen -- so
|
||||
// steht im Eintrag "HTTP 401", wenn ein Passwortschutz am Proxy
|
||||
// die Anfrage abweist (Befund 22.09.2026).
|
||||
Err(tauri_plugin_updater::Error::ReleaseNotFound) => {
|
||||
let status = probe_update_status(diagnostic_update_url(
|
||||
&server_url,
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
))
|
||||
.await;
|
||||
report_check_failure(&app, &update_item, status);
|
||||
}
|
||||
// Keine Verbindung, TLS, Timeout (Reqwest/Network/Io) oder
|
||||
// unlesbares Manifest: keine zweite Anfrage, Status unbekannt.
|
||||
Err(_) => report_check_failure(&app, &update_item, None),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Oeffnet die Seite Einstellungen → Desktop-App im System-Browser -- der
|
||||
/// bisherige Weg des Tray-Eintrags, jetzt Rueckfall, wenn kein Update
|
||||
/// abgelegt ist oder die Installation in der App fehlschlaegt. Bewusst ohne
|
||||
/// `desktop=1` (siehe `with_desktop_marker`).
|
||||
/// bisherige Weg des Tray-Eintrags, seit quick-260922-frg NUR noch Rueckfall,
|
||||
/// wenn die Installation in der App fehlschlaegt (ein Klick ohne abgelegtes
|
||||
/// Update prueft stattdessen erneut). Bewusst ohne `desktop=1` (siehe
|
||||
/// `with_desktop_marker`).
|
||||
fn open_download_page(app: &AppHandle) {
|
||||
if let Some(server) = stored_server_url(app) {
|
||||
let target = format!("{}/settings/general/desktop", server.trim_end_matches('/'));
|
||||
@@ -566,13 +724,16 @@ pub fn run() {
|
||||
}
|
||||
|
||||
// Tray menu: Verbunden mit ... (gesperrt) · — · Öffnen ·
|
||||
// Server-Adresse ändern... · Update installieren · — ·
|
||||
// Server-Adresse ändern... · Suche nach Updates… · — ·
|
||||
// Autostart-Haken · — · Beenden.
|
||||
// "connected" ist nicht anklickbar; "update" bleibt gesperrt, bis
|
||||
// die Versionspruefung eine neuere Version findet, und heisst dann
|
||||
// "Auf Version X.Y.Z aktualisieren" bzw. "Auf Beta-Stand ...
|
||||
// aktualisieren"; "autostart" spiegelt den tatsaechlichen
|
||||
// Systemzustand beim Start.
|
||||
// "connected" ist nicht anklickbar; "update" startet gesperrt als
|
||||
// "Suche nach Updates…", weil die Erstpruefung unten sofort
|
||||
// anlaeuft, und heisst danach "Auf Version X.Y.Z aktualisieren"
|
||||
// bzw. "Auf Beta-Stand ... aktualisieren", "Kein Update verfügbar
|
||||
// – erneut prüfen" oder "Update-Prüfung fehlgeschlagen (...) –
|
||||
// erneut prüfen" (alle anklickbar). Ohne gespeicherte Adresse
|
||||
// "Kein Update verfügbar – erneut prüfen" (Klick tut nichts);
|
||||
// "autostart" spiegelt den tatsaechlichen Systemzustand beim Start.
|
||||
let connected =
|
||||
MenuItemBuilder::with_id("connected", tray_labels(server_url.as_deref()).1)
|
||||
.enabled(false)
|
||||
@@ -580,8 +741,13 @@ pub fn run() {
|
||||
let open = MenuItemBuilder::with_id("open", "Öffnen").build(app)?;
|
||||
let change_server =
|
||||
MenuItemBuilder::with_id("change_server", "Server-Adresse ändern…").build(app)?;
|
||||
let update = MenuItemBuilder::with_id("update", UPDATE_ITEM_DEFAULT)
|
||||
.enabled(false)
|
||||
let update_text = if server_url.is_some() {
|
||||
UPDATE_ITEM_CHECKING
|
||||
} else {
|
||||
UPDATE_ITEM_NONE
|
||||
};
|
||||
let update = MenuItemBuilder::with_id("update", update_text)
|
||||
.enabled(server_url.is_none())
|
||||
.build(app)?;
|
||||
let autostart_label = if cfg!(target_os = "windows") {
|
||||
"Mit Windows starten"
|
||||
@@ -610,6 +776,7 @@ pub fn run() {
|
||||
update: update.clone(),
|
||||
});
|
||||
app.manage(PendingUpdate(Mutex::new(None)));
|
||||
app.manage(LastCheckNotice(Mutex::new(String::new())));
|
||||
|
||||
let autostart_for_menu = autostart.clone();
|
||||
|
||||
@@ -637,8 +804,12 @@ pub fn run() {
|
||||
}
|
||||
"update" => {
|
||||
// `take()` verhindert Doppelklick-Downloads, der
|
||||
// gesperrte Eintrag ebenso; ohne abgelegten Stand
|
||||
// bleibt der Browser-Weg (Einstellungen → Desktop-App).
|
||||
// gesperrte Eintrag ebenso. Ohne abgelegten Stand
|
||||
// ("Kein Update verfügbar" / "Update-Prüfung
|
||||
// fehlgeschlagen") prueft der Klick erneut -- der
|
||||
// Browser-Weg (`open_download_page`) ist nur noch
|
||||
// Rueckfall einer fehlgeschlagenen Installation.
|
||||
// Ohne gespeicherte Adresse gibt es nichts zu pruefen.
|
||||
let pending = app
|
||||
.state::<PendingUpdate>()
|
||||
.0
|
||||
@@ -649,7 +820,11 @@ pub fn run() {
|
||||
Some((update, menu_text)) => {
|
||||
spawn_update_install(app.clone(), update, menu_text)
|
||||
}
|
||||
None => open_download_page(app),
|
||||
None => {
|
||||
if let Some(url) = stored_server_url(app) {
|
||||
spawn_version_check(app.clone(), url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"autostart" => {
|
||||
@@ -696,6 +871,34 @@ pub fn run() {
|
||||
spawn_version_check(app.handle().clone(), url);
|
||||
}
|
||||
|
||||
// Wiederholte Pruefung alle UPDATE_CHECK_INTERVAL. Bewusst ein
|
||||
// schlichter Betriebssystem-Thread mit `sleep` statt
|
||||
// `tokio::time`: das Tauri-Async-Runtime braeuchte dafuer das
|
||||
// `time`-Feature bzw. ein eigenes Crate, und ein schlafender
|
||||
// Thread kostet nichts. Je Durchlauf wird die Adresse frisch
|
||||
// gelesen (Serverwechsel zaehlt). Haelt `PendingUpdate` bereits
|
||||
// ein Update, wird der Durchlauf uebersprungen: die Pruefung
|
||||
// wuerde den Stand leeren und "Neuer Beta-Stand" erneut melden.
|
||||
// Waehrend einer Installation ist der Stand per `take()` leer;
|
||||
// faellt die 4-h-Marke genau in den Download, wird nur der
|
||||
// Fortschrittstext ueberschrieben (T-FRG-04, akzeptiert).
|
||||
let handle = app.handle().clone();
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::sleep(UPDATE_CHECK_INTERVAL);
|
||||
let pending = handle
|
||||
.state::<PendingUpdate>()
|
||||
.0
|
||||
.lock()
|
||||
.map(|guard| guard.is_some())
|
||||
.unwrap_or(false);
|
||||
if pending {
|
||||
continue;
|
||||
}
|
||||
if let Some(url) = stored_server_url(&handle) {
|
||||
spawn_version_check(handle.clone(), url);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
@@ -1008,4 +1211,63 @@ mod tests {
|
||||
Err("Diese Adresse ist ungültig.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_failure_labels_401_nennt_proxy_passwortschutz() {
|
||||
let (menu_text, body) = check_failure_labels(Some(401));
|
||||
assert!(menu_text.contains("HTTP 401"));
|
||||
assert!(menu_text.ends_with("– erneut prüfen"));
|
||||
assert!(body.contains("HTTP 401"));
|
||||
assert!(body.contains("Passwortschutz"));
|
||||
assert!(body.contains("Zugriffsliste"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_failure_labels_403_gleiche_erklaerung() {
|
||||
let (menu_text, body) = check_failure_labels(Some(403));
|
||||
assert!(menu_text.contains("HTTP 403"));
|
||||
assert!(body.contains("HTTP 403"));
|
||||
assert!(body.contains("Passwortschutz"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_failure_labels_anderer_status_ohne_proxy_erklaerung() {
|
||||
let (menu_text, body) = check_failure_labels(Some(502));
|
||||
assert!(menu_text.contains("HTTP 502"));
|
||||
assert!(menu_text.ends_with("– erneut prüfen"));
|
||||
assert!(!body.contains("Passwortschutz"));
|
||||
assert!(body.contains("statt mit Paketdaten"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_failure_labels_ohne_status_keine_verbindung() {
|
||||
let (menu_text, body) = check_failure_labels(None);
|
||||
assert!(menu_text.contains("keine Verbindung"));
|
||||
assert!(menu_text.ends_with("– erneut prüfen"));
|
||||
assert!(body.contains("vier Stunden"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_update_url_ersetzt_platzhalter_und_kodiert_base() {
|
||||
assert_eq!(
|
||||
diagnostic_update_url("https://alpha.example", "windows", "x86_64", "1.2.0"),
|
||||
"https://alpha.example/api-proxy/desktop/update?target=windows&arch=x86_64¤t=1.2.0&base=https%3A%2F%2Falpha.example"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_update_url_trimmt_schlussstrich() {
|
||||
assert_eq!(
|
||||
diagnostic_update_url("https://alpha.example/", "linux", "x86_64", "1.2.0"),
|
||||
"https://alpha.example/api-proxy/desktop/update?target=linux&arch=x86_64¤t=1.2.0&base=https%3A%2F%2Falpha.example"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_item_konstanten() {
|
||||
assert_eq!(UPDATE_ITEM_CHECKING, "Suche nach Updates…");
|
||||
assert_eq!(UPDATE_ITEM_NONE, "Kein Update verfügbar – erneut prüfen");
|
||||
assert_eq!(UPDATE_ITEM_INSECURE, "Update nur über https möglich");
|
||||
assert_eq!(UPDATE_CHECK_INTERVAL, Duration::from_secs(4 * 3600));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user