Hinweis beim Oeffnen, wenn eine neuere Fassung bereitliegt

Bisher gab es dafuer nur den schmalen Streifen der Hinweisleiste, der
dauerhaft mitlaeuft und leicht uebersehen wird. Ein neuer Stand ist aber ein
Ereignis und gehoert einmal nach vorn.

Zwei Faelle, zwei Texte: liegt ein passendes OTA-Buendel bereit, traegt das
Blatt den Knopf, der die App direkt erneuert (derselbe Weg wie in den
Einstellungen). Fehlt eines, hat sich Natives geaendert - dann sagt es das
und bietet keinen Knopf an, der nichts bewirken koennte.

Nur nativ: im Browser und im Panel laedt jeder Aufruf den aktuellen Stand.
Einmal je Fassung: die weggetippte Serverfassung wird gemerkt, erst eine
andere bringt das Blatt zurueck.

Die Entscheidung steckt in der reinen Funktion hinweisFaellig(), sechs Faelle
getestet. 179/179 Tests gruen, tsc sauber.
This commit is contained in:
Paul Nothaft
2026-09-03 09:50:44 +02:00
parent 2c466bf5a2
commit c254cf5e2d
4 changed files with 219 additions and 0 deletions
+8
View File
@@ -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. */}
<Versionshinweis
versionsstand={daten.versionsstand}
serverVersion={daten.serverVersion}
otaBuendel={daten.otaBuendel}
/>
</Shell>
)
}
@@ -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)
})
})
@@ -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<string | null>(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 (
<ActionSheet
open={offen}
onClose={schliessen}
title="Neue Version verfügbar"
description={
perOta
? `Diese App läuft auf ${eigeneVersion() ?? "einer unbekannten Fassung"}, bereit liegt ${serverVersion}. Sie kann sich direkt hier erneuern.`
: `Diese App läuft auf ${eigeneVersion() ?? "einer unbekannten Fassung"}, der Server liefert ${serverVersion}. Diese Fassung ändert auch Natives — dafür muss die App neu aufgespielt werden, ein Update in der App genügt nicht.`
}
actions={
perOta
? [{ label: laeuft ? "Wird geladen …" : "Jetzt aktualisieren", onClick: () => void jetztLaden(), disabled: laeuft }]
: []
}
cancelLabel={perOta ? "Später" : "Verstanden"}
>
{fehler ? <p className="dm-einrichtung__fehler">{fehler}</p> : null}
</ActionSheet>
)
}