Phase 10-13: PWA fertig, blockierte Phasen vorbereitet

PWA laeuft: Manifest, Symbole in allen noetigen Groessen und die
iOS-Angaben, die Apple statt des Manifests auswertet. Das Startsymbol ist
bewusst neutral - die Vier Ringe auf einem Homescreen waeren nach aussen
sichtbar und fielen nicht mehr unter die private Nutzung.

Vorbereitet, aber nicht ausgefuehrt: Capacitor-Konfiguration, ein
Ablage-Adapter fuer Schluesselbund und Keystore (greift defensiv auf die
Laufzeit zu, damit die App ohne das Plugin unveraendert weiterlaeuft), die
Pfad-Freigabeliste fuer den Reverse Proxy und das Geruest der
FMM003-Zuordnungstabelle.

Die QR-Seite fuer die Einrichtung war zunaechst ein Fehlgriff: Ich hatte den
QR-Erzeuger selbst geschrieben. Der Vergleich gegen eine erprobte
Implementierung zeigte 1239 abweichende Module von 3249 - der Code waere
unlesbar gewesen. Jetzt liegt eine bewaehrte Bibliothek (MIT, 57 KB) neben
der Seite im eigenen Home Assistant. Das erfuellt die eigentliche
Anforderung genauso: kein Netzzugriff, der Token verlaesst das eigene Netz
nicht. Geprueft ueber einen Umlauf - 233 Zeichen hinein, identisch wieder
heraus.
This commit is contained in:
Paul Nothaft
2026-08-11 11:27:20 +02:00
parent a68817065b
commit f8b877d555
14 changed files with 2866 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
/* ================================================================
Sichere Ablage auf dem Gerät (iOS Keychain / Android Keystore)
================================================================
Der Zugangstoken hat vollen Zugriff auf die Home-Assistant-Schnittstelle.
Im Browser bleibt nur localStorage; in der nativen Hülle gehört er in den
Schlüsselbund des Betriebssystems.
**Noch nicht in Betrieb**: das Plugin ist bewusst keine Abhängigkeit,
solange die Capacitor-Hülle nicht gebaut ist. Der Adapter greift deshalb
defensiv auf die Laufzeit zu, statt zu importieren — fehlt das Plugin,
bleibt es bei der Browser-Variante, und die App läuft trotzdem.
In Betrieb nehmen (UMSETZUNGSPLAN.md Phase 10 Stufe 2):
npm i @aparajita/capacitor-secure-storage
Danach ruft main.tsx `nativeAblageEinrichten()` auf. */
import { BrowserAblage, ablageSetzen, umgebungErkennen, type Ablage } from "./umgebung.ts";
/** Form des Plugins, so weit die App sie braucht. */
interface SicherheitsPlugin {
get(optionen: { key: string }): Promise<{ value: string | null }>;
set(optionen: { key: string; value: string }): Promise<void>;
remove(optionen: { key: string }): Promise<void>;
}
function pluginHolen(): SicherheitsPlugin | null {
if (typeof window === "undefined") return null;
const global = window as unknown as {
Capacitor?: { Plugins?: Record<string, unknown> };
};
const plugin = global.Capacitor?.Plugins?.["SecureStorage"];
if (!plugin) return null;
const kandidat = plugin as Partial<SicherheitsPlugin>;
return typeof kandidat.get === "function" &&
typeof kandidat.set === "function" &&
typeof kandidat.remove === "function"
? (plugin as SicherheitsPlugin)
: null;
}
export class NativeAblage implements Ablage {
readonly #plugin: SicherheitsPlugin;
/* Fällt einzeln auf den Browser zurück, wenn ein Aufruf scheitert - besser
eine Sitzung ohne Schlüsselbund als eine App, die nicht startet. */
readonly #ersatz = new BrowserAblage();
constructor(plugin: SicherheitsPlugin) {
this.#plugin = plugin;
}
async lesen(schluessel: string): Promise<string | null> {
try {
const { value } = await this.#plugin.get({ key: schluessel });
return value ?? null;
} catch {
return this.#ersatz.lesen(schluessel);
}
}
async schreiben(schluessel: string, wert: string): Promise<void> {
try {
await this.#plugin.set({ key: schluessel, value: wert });
} catch {
await this.#ersatz.schreiben(schluessel, wert);
}
}
async loeschen(schluessel: string): Promise<void> {
try {
await this.#plugin.remove({ key: schluessel });
} catch {
await this.#ersatz.loeschen(schluessel);
}
}
}
/**
* Schaltet auf die sichere Ablage um, sofern die App nativ läuft und das
* Plugin vorhanden ist. Gibt zurück, ob umgeschaltet wurde — praktisch für
* eine Anzeige in den Einstellungen.
*/
export function nativeAblageEinrichten(): boolean {
if (umgebungErkennen() !== "capacitor") return false;
const plugin = pluginHolen();
if (!plugin) return false;
ablageSetzen(new NativeAblage(plugin));
return true;
}
+1
View File
@@ -11,6 +11,7 @@ export * from "./umgebung.ts";
export * from "./rest.ts";
export * from "./live.ts";
export * from "./warteschlange.ts";
export * from "./ablageNativ.ts";
import { ENTITAETEN } from "./types.ts";
import type { Fahrt, Fahrzeugstatus, Profil, Tankvorgang } from "./types.ts";
+5
View File
@@ -4,8 +4,13 @@ import { createRoot } from "react-dom/client"
import "@audi-dash/ui/styles.css"
import "./stile/grundlage.css"
import "./stile/audi-schrift.css"
import { nativeAblageEinrichten } from "./api"
import { App } from "./App"
// In der nativen Hülle den Token in den Schlüsselbund legen statt in
// localStorage. Ohne Hülle passiert hier nichts.
nativeAblageEinrichten()
const wurzel = document.getElementById("wurzel")
if (!wurzel) throw new Error("Wurzelelement #wurzel fehlt in index.html")