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:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Capacitor-Konfiguration für die native Hülle.
|
||||
*
|
||||
* **Noch nicht in Betrieb.** Die Pakete sind bewusst nicht installiert; diese
|
||||
* Datei liegt fertig da, damit die Inbetriebnahme nur noch aus den Befehlen in
|
||||
* `../UMSETZUNGSPLAN.md` Phase 10 Stufe 2 besteht:
|
||||
*
|
||||
* npm i -D @capacitor/cli
|
||||
* npm i @capacitor/core @capacitor/ios @capacitor/android
|
||||
* npx cap add ios && npx cap add android
|
||||
* npm run build && npx cap sync
|
||||
* npx cap open ios # in Xcode signieren und aufs Gerät bauen
|
||||
*
|
||||
* Nur Sideload, nie App Store: die Audi-Schrift und die Typenschilder sind
|
||||
* ausschließlich für diese private Installation freigegeben.
|
||||
*
|
||||
* Der Typ ist absichtlich nicht aus @capacitor/cli importiert — das Paket
|
||||
* fehlt ja noch, und ein toter Import würde die Typprüfung brechen.
|
||||
*/
|
||||
|
||||
interface CapacitorKonfiguration {
|
||||
appId: string
|
||||
appName: string
|
||||
webDir: string
|
||||
server?: { androidScheme?: string; iosScheme?: string; cleartext?: boolean }
|
||||
ios?: { contentInset?: string; backgroundColor?: string }
|
||||
android?: { backgroundColor?: string }
|
||||
}
|
||||
|
||||
const konfiguration: CapacitorKonfiguration = {
|
||||
appId: "app.datametric360",
|
||||
appName: "DataMetric360",
|
||||
webDir: "dist",
|
||||
server: {
|
||||
// https, damit die App als sicherer Ursprung gilt — sonst verweigern
|
||||
// Browser-Bausteine wie die Zwischenablage ihren Dienst.
|
||||
androidScheme: "https",
|
||||
iosScheme: "https",
|
||||
},
|
||||
ios: {
|
||||
// Die App zeichnet bis unter die Statusleiste und hält den sicheren
|
||||
// Bereich selbst über env(safe-area-inset-*) frei.
|
||||
contentInset: "never",
|
||||
backgroundColor: "#161b23",
|
||||
},
|
||||
android: {
|
||||
backgroundColor: "#161b23",
|
||||
},
|
||||
}
|
||||
|
||||
export default konfiguration
|
||||
@@ -8,6 +8,15 @@
|
||||
/>
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<meta name="theme-color" content="#161b23" />
|
||||
<link rel="manifest" href="./manifest.webmanifest" />
|
||||
<link rel="icon" href="./symbol.svg" type="image/svg+xml" />
|
||||
<!-- iOS wertet das Manifest nicht aus und braucht diese beiden Angaben,
|
||||
damit "Zum Home-Bildschirm" ein eigenes Symbol und keine Browserleiste
|
||||
zeigt. -->
|
||||
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="DataMetric360" />
|
||||
<title>DataMetric360</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "DataMetric360",
|
||||
"short_name": "DM360",
|
||||
"description": "Fahrzeugdaten, Fahrtenbuch und Tankvorgänge vom eigenen Server.",
|
||||
"lang": "de",
|
||||
"dir": "ltr",
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"background_color": "#161b23",
|
||||
"theme_color": "#161b23",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./symbol.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "./symbol-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "./symbol-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'>
|
||||
<rect width='512' height='512' rx='114' fill='#161b23'/>
|
||||
<path d='M150 176h74c48 0 78 31 78 80s-30 80-78 80h-74z' fill='none' stroke='#ffffff' stroke-width='26' stroke-linejoin='round'/>
|
||||
<path d='M330 336V176l46 54 46-54v160' fill='none' stroke='#F50537' stroke-width='26' stroke-linecap='round' stroke-linejoin='round'/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user