diff --git a/AGENTS.md b/AGENTS.md index a435313..2fb2cd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8690,3 +8690,40 @@ WebP sichern und noch einmal versuchen." — und auf der Platte entsteht **nicht Verifiziert: 5/5 Backend-Tests (und ohne die Sperre nachweislich rot), `tsc --noEmit` sauber, 173/173 App-Tests, Panel als Modul geparst, `audi_ha_test` auf `2026.9.3.3` sauber gestartet. + +--- + +## AL. Popup beim Öffnen, wenn eine neuere Fassung bereitliegt (2026-09-03) + +Wunsch des Besitzers: die App soll beim Start sagen, dass es eine neue Fassung gibt — so wie andere +Apps es tun, nur ohne App Store. Es gab bisher nur den schmalen Streifen der `Hinweisleiste` +("Diese App ist älter als der Server"), der dauerhaft mitläuft und deshalb leicht übersehen wird. + +Neu: `companion-app/src/screens/Versionshinweis.tsx`, ein `ActionSheet`, eingehängt in `App.tsx` +neben der bestehenden Leiste. Die beiden ersetzen einander **nicht** — ein neuer Stand ist ein +Ereignis und gehört einmal nach vorn, danach übernimmt wieder der Streifen für den fortbestehenden +Zustand. + +**Zwei Fälle, zwei Texte, weil zwei verschiedene Dinge zu tun sind.** Liegt ein passendes OTA-Bündel +bereit (`buendelPasst()`), kann die App sich selbst erneuern — dann trägt das Blatt den Knopf, der +genau das auslöst, über denselben `buendelAnwenden()`-Weg wie der Knopf in den Einstellungen. Fehlt +eines, hat sich Natives geändert und es hilft nur neu aufspielen; dann sagt das Blatt genau das und +bietet keinen Knopf an, der nichts bewirken könnte. + +**Nur nativ.** Im Browser und im HA-Panel lädt jeder Aufruf den aktuellen Stand — dort kann die App +gar nicht veralten, dieselbe Begründung wie in `appVersion.ts`. + +**Einmal je Fassung, nicht bei jedem Start.** Weggetippt wird die gesehene Serverfassung in +`localStorage` gemerkt; erst eine *andere* bringt das Blatt zurück. Ohne das wäre es binnen einer +Woche unsichtbar geworden. Die Entscheidung steckt in der reinen Funktion `hinweisFaellig()` und ist +mit sechs Fällen getestet (`Versionshinweis.test.ts`) — ein Fehler dort hat genau zwei Ausprägungen, +beide schlecht: eine Meldung bei jedem Start, die niemand mehr liest, oder ein Update, von dem nie +jemand erfährt. + +**Geerbte Ungenauigkeit, bewusst nicht angefasst:** der Vergleich ist reine Gleichheit (siehe +`VERSIONIERUNG.md`), also meldet das Blatt auch dann "neue Version", wenn die App *neuer* ist als das +Backend — etwa direkt nach einem Xcode-Bau, bevor die Integration nachgezogen wurde. Der +Hinweisstreifen hat dieselbe Eigenschaft seit jeher. Sortierbar zu vergleichen wäre scheingenau; die +Alternative wäre ein zweites Signal vom Backend, und das lohnt für diesen Randfall nicht. + +Verifiziert: `tsc --noEmit` sauber, 179/179 Tests (6 neue). diff --git a/companion-app/src/App.tsx b/companion-app/src/App.tsx index 3dacae3..58d635f 100644 --- a/companion-app/src/App.tsx +++ b/companion-app/src/App.tsx @@ -11,6 +11,7 @@ import type { Route, SeitenName } from "./navigation" import { useTabBeschriftung, useTheme } from "./theme" import { Einrichtung } from "./screens/Einrichtung" import { Hinweisleiste } from "./screens/Hinweisleiste" +import { Versionshinweis } from "./screens/Versionshinweis" import { SeiteFuer } from "./screens/register" import { zugangMerken } from "./screens/zugang" @@ -141,6 +142,13 @@ function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) { setzeTabBeschriftung={setzeTabBeschriftung} beiAbmeldung={beiAbmeldung} /> + {/* Erscheint von selbst, sobald der erste Datenabruf eine neuere + Fassung meldet - siehe screens/Versionshinweis.tsx. */} + ) } diff --git a/companion-app/src/screens/Versionshinweis.test.ts b/companion-app/src/screens/Versionshinweis.test.ts new file mode 100644 index 0000000..454b3eb --- /dev/null +++ b/companion-app/src/screens/Versionshinweis.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest" + +import { hinweisFaellig } from "./Versionshinweis" + +/* hinweisFaellig() entscheidet, ob der Nutzer von einer neuen Fassung + erfährt. Ein Fehler hier hat genau zwei Ausprägungen, beide schlecht: eine + Meldung bei jedem Start, die niemand mehr liest, oder ein Update, von dem + nie jemand erfährt. */ +describe("hinweisFaellig", () => { + it("meldet, wenn App und Server auseinanderlaufen", () => { + expect(hinweisFaellig("abweichend", "2026.9.3.3", null)).toBe(true) + }) + + it("schweigt bei gleichem Stand", () => { + expect(hinweisFaellig("gleich", "2026.9.3.3", null)).toBe(false) + }) + + it("schweigt, solange kein Vergleich möglich ist", () => { + // "unbekannt" heißt offline oder altes Backend - kein Grund für eine + // Meldung, sonst erschiene sie bei jedem Start ohne Verbindung. + expect(hinweisFaellig("unbekannt", null, null)).toBe(false) + expect(hinweisFaellig("unbekannt", "2026.9.3.3", null)).toBe(false) + }) + + it("schweigt ohne Serverfassung, auch wenn abweichend gemeldet wird", () => { + expect(hinweisFaellig("abweichend", null, null)).toBe(false) + }) + + it("schweigt zur bereits weggetippten Fassung", () => { + expect(hinweisFaellig("abweichend", "2026.9.3.3", "2026.9.3.3")).toBe(false) + }) + + it("meldet erneut, sobald eine andere Fassung bereitliegt", () => { + // Der Kern der Merkfunktion: weggetippt gilt für genau eine Fassung, + // nicht für "Updates" im Allgemeinen. + expect(hinweisFaellig("abweichend", "2026.9.4.1", "2026.9.3.3")).toBe(true) + }) +}) diff --git a/companion-app/src/screens/Versionshinweis.tsx b/companion-app/src/screens/Versionshinweis.tsx new file mode 100644 index 0000000..0f1b1ff --- /dev/null +++ b/companion-app/src/screens/Versionshinweis.tsx @@ -0,0 +1,136 @@ +/** + * Meldung beim Öffnen, wenn eine neuere Fassung bereitliegt. + * + * Warum zusätzlich zur Hinweisleiste: die Leiste ist ein schmaler Streifen, + * der dauerhaft mitläuft — richtig für einen Zustand, der bestehen bleibt, + * aber leicht zu übersehen. Dass eine neue Fassung *neu* da ist, ist dagegen + * ein Ereignis und gehört einmal nach vorn. Danach übernimmt wieder die + * Leiste; die beiden ersetzen einander nicht. + * + * Nur in der nativen Hülle. Im Browser und im HA-Panel lädt jeder Aufruf den + * aktuellen Stand, dort kann die App gar nicht veralten (dieselbe Begründung + * wie in daten/appVersion.ts). + * + * ZWEI FÄLLE, ZWEI TEXTE + * ---------------------- + * Liegt ein passendes OTA-Bündel bereit, kann die App sich selbst erneuern — + * dann steht hier der Knopf, der genau das tut. Fehlt eines, hat sich Natives + * geändert (Plugins, Berechtigungen, die Hülle selbst), und dagegen hilft nur + * neu aufspielen. Beides zu vermengen wäre die schlechtere Auskunft: ein + * "Jetzt aktualisieren", das nichts ändern kann, oder ein "bitte manuell", + * obwohl ein Knopf genügt hätte. + * + * EINMAL, NICHT BEI JEDEM START + * ----------------------------- + * Weggetippt wird die gesehene Serverfassung gemerkt. Erst eine *andere* + * bringt die Meldung zurück — sonst wäre sie bei jedem Start dieselbe und + * damit binnen einer Woche unsichtbar. + */ + +import { useState } from "react" + +import { ActionSheet } from "@audi-dash/ui" + +import { zugangLesen, type Buendelangabe } from "../api" +import { eigeneVersion, type Versionsstand } from "../daten/appVersion" +import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota" + +const SCHLUESSEL = "dm360.version.gesehen" + +/** + * Soll die Meldung erscheinen? + * + * Bewusst als reine Funktion: sie entscheidet, ob der Nutzer von einer neuen + * Fassung erfährt oder nicht, und das soll prüfbar sein, ohne die Oberfläche + * zu bauen. + */ +export function hinweisFaellig( + versionsstand: Versionsstand, + serverVersion: string | null, + gesehen: string | null, +): boolean { + // "unbekannt" heißt: kein Vergleich möglich (offline, altes Backend). Das + // ist kein Grund für eine Meldung — genauso wenig wie "gleich". + if (versionsstand !== "abweichend") return false + if (!serverVersion) return false + return serverVersion !== gesehen +} + +function gesehenLesen(): string | null { + try { + return localStorage.getItem(SCHLUESSEL) + } catch { + // Privater Modus oder gesperrter Speicher: dann eben jedes Mal fragen. + return null + } +} + +function gesehenSchreiben(version: string): void { + try { + localStorage.setItem(SCHLUESSEL, version) + } catch { + /* siehe oben */ + } +} + +export function Versionshinweis({ + versionsstand, + serverVersion, + otaBuendel, +}: { + versionsstand: Versionsstand + serverVersion: string | null + otaBuendel: Buendelangabe | null +}) { + const [weggetippt, setzeWeggetippt] = useState(false) + const [laeuft, setzeLaeuft] = useState(false) + const [fehler, setzeFehler] = useState(null) + + const nativ = otaMoeglich() + const offen = + nativ && !weggetippt && hinweisFaellig(versionsstand, serverVersion, gesehenLesen()) + + const perOta = buendelPasst(otaBuendel, eigeneVersion()) + + const schliessen = () => { + if (laeuft) return + if (serverVersion) gesehenSchreiben(serverVersion) + setzeWeggetippt(true) + } + + const jetztLaden = async () => { + if (!otaBuendel) return + setzeLaeuft(true) + setzeFehler(null) + try { + const zugang = await zugangLesen() + if (!zugang) throw new OtaFehler("Kein Zugang eingerichtet.") + // Kehrt im Erfolgsfall nicht zurück — set() lädt die App neu. + await buendelAnwenden(otaBuendel, zugang.basisUrl) + } catch (f) { + setzeFehler(f instanceof Error ? f.message : "Das Update konnte nicht installiert werden.") + setzeLaeuft(false) + } + } + + return ( + void jetztLaden(), disabled: laeuft }] + : [] + } + cancelLabel={perOta ? "Später" : "Verstanden"} + > + {fehler ?

{fehler}

: null} +
+ ) +}