Native iOS-Huelle in Betrieb, drei Darstellungsfehler behoben
Xcode ist vorhanden, die Huelle liess sich also wirklich bauen statt nur vorzubereiten: Capacitor 8 fuer iOS und Android, Build fuer den Simulator erfolgreich, App laeuft und zeigt echte Daten vom Server. CapacitorHttp eingeschaltet. Das ist keine Feinheit: die native Huelle liefert die Oberflaeche unter eigenem Ursprung aus, jede Anfrage an Home Assistant ist damit ursprungsuebergreifend, und die WebView lehnt sie ohne CORS-Freigabe ab. Nativ gestellte Anfragen kennen keine CORS-Pruefung - die App braucht dadurch keine CORS-Einstellung am Server. Drei Fehler, die erst die Bildschirmfotos zeigten: 1. Der Fahrzeug-Block auf der Uebersicht war unsichtbar. Ursache war der Flexbox-Fallstrick: "overflow: hidden" setzt die automatische Mindesthoehe auf 0, und sobald die Seite laenger ist als der Bildschirm, quetscht flex-shrink den Block auf Hoehe 0. Modellname, Typenschild und Kennzeichen waren damit schlicht weg. 2. Neben dem Typenschild stand der Modellname noch einmal komplett, obwohl das Schild ihn bereits zeigt. Das alte Panel loest das laengst richtig - nur der Zusatz gehoert daneben. Nachgezogen, samt Entdopplung, wenn die Ausfuehrung schon im Namen steckt. 3. In der Fotogalerie liefen die Dateinamen ueber den Rand der Miniaturen. Dazu: Datumszeile in Listen bricht um statt abzuschneiden, Statistik nutzt auf der Flaeche mehrere Spalten. Im Backend war das Kilometerstand-Screening kaputt: es rief urlopen direkt auf, was Home Assistant seit 2026.8 als blockierenden Aufruf abbricht. Jede Fahrt blieb dadurch ohne Strecke - sichtbar nur als Warnung im Protokoll. Laeuft jetzt ueber task.executor und rechnet an der Testinstanz wieder echte Strecken aus dem Verlauf. Die CORS-Freigabe fuer die Web-Fassung ist in der Testkonfiguration hinterlegt. Sie greift in Home Assistant 2026.8 allerdings nicht - deshalb wird die App aus Home Assistant selbst ausgeliefert (gleicher Ursprung, kein CORS), was ohnehin der geplante Weg ist. Die erzeugten Ordner ios/ und android/ bleiben ungetrackt; sie entstehen jederzeit neu aus dem Webbuendel.
This commit is contained in:
@@ -2,3 +2,9 @@ node_modules/
|
||||
dist/
|
||||
*.local
|
||||
.DS_Store
|
||||
|
||||
# Native Huellen: von `npx cap add` erzeugt, aus dem Webbuendel jederzeit
|
||||
# wiederherstellbar. Die Projektdateien selbst (capacitor.config.ts) sind
|
||||
# getrackt.
|
||||
ios/
|
||||
android/
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
import type { CapacitorConfig } from "@capacitor/cli"
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ausschließlich für diese eine 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.
|
||||
* Ablauf: `npm run build && npx cap sync`, dann `npx cap open ios` und in
|
||||
* Xcode signieren. Siehe `../UMSETZUNGSPLAN.md` Phase 10.
|
||||
*/
|
||||
|
||||
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 = {
|
||||
const konfiguration: CapacitorConfig = {
|
||||
appId: "app.datametric360",
|
||||
appName: "DataMetric360",
|
||||
webDir: "dist",
|
||||
@@ -46,6 +28,17 @@ const konfiguration: CapacitorKonfiguration = {
|
||||
android: {
|
||||
backgroundColor: "#161b23",
|
||||
},
|
||||
plugins: {
|
||||
// Leitet fetch() über die native HTTP-Schicht statt über die WebView.
|
||||
//
|
||||
// Das ist hier nicht Feinschliff, sondern notwendig: Die native Hülle
|
||||
// liefert die Oberfläche unter einem eigenen Ursprung aus, jede Anfrage an
|
||||
// Home Assistant ist damit ursprungsübergreifend, und die WebView lehnt sie
|
||||
// ohne CORS-Freigabe ab. Nativ gestellte Anfragen kennen keine
|
||||
// CORS-Prüfung — die App braucht dadurch keinerlei CORS-Einstellung am
|
||||
// Server. Die WebSocket-Verbindung läuft unberührt weiter.
|
||||
CapacitorHttp: { enabled: true },
|
||||
},
|
||||
}
|
||||
|
||||
export default konfiguration
|
||||
|
||||
@@ -14,12 +14,17 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
||||
"@audi-dash/ui": "*",
|
||||
"@capacitor/android": "^8.5.0",
|
||||
"@capacitor/core": "^8.5.0",
|
||||
"@capacitor/ios": "^8.5.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^8.5.0",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@types/node": "^24.13.3",
|
||||
|
||||
@@ -23,13 +23,16 @@ export function Bild({
|
||||
groesse?: "default" | "mini" | "round"
|
||||
}) {
|
||||
const url = bildUrl(datei)
|
||||
// Der Dateiname hilft nur, wenn er lesbar ist — in der 78px breiten
|
||||
// Miniaturansicht liefe er über den Rand hinaus.
|
||||
const mitDateiname = groesse !== "mini" && groesse !== "round"
|
||||
return (
|
||||
<ImagePlaceholder
|
||||
{...(url ? { src: url } : {})}
|
||||
{...(groesse ? { size: groesse } : {})}
|
||||
{...(mitDateiname && datei ? { hint: datei } : {})}
|
||||
alt={beschreibung}
|
||||
label={name}
|
||||
hint={datei ?? undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function Statistik() {
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dm-raster">
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Distanz und Fahrzeit {a.jahr}</span>
|
||||
<Fig value={de(a.kmJahr)} unit="km" size={40} />
|
||||
@@ -74,7 +74,7 @@ export function Statistik() {
|
||||
/>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<Tile className="dm-raster__breit">
|
||||
<span className="ads-eyebrow">Langzeitverbrauch</span>
|
||||
<Fig value={deOderStrich(a.langzeit.wert, 1)} unit="l/100 km" size={40} />
|
||||
<Werteliste
|
||||
@@ -150,6 +150,6 @@ export function Statistik() {
|
||||
}
|
||||
/>
|
||||
</Tile>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,23 +17,30 @@ const SCHILDER = import.meta.glob<string>("../assets/audi/typenschilder/*.svg",
|
||||
query: "?url",
|
||||
})
|
||||
|
||||
/** Modellname aus dem Profil → Dateikürzel des Typenschilds. */
|
||||
export function schildKuerzel(titel: string): string | null {
|
||||
const norm = titel.toLowerCase().replace(/\s+/g, " ").trim()
|
||||
const treffer = /\b(rs)\s?([34567])\b/.exec(norm) ?? /\b(s)\s?(q7|3)\b/.exec(norm)
|
||||
if (!treffer) return null
|
||||
const [, reihe, zahl] = treffer
|
||||
return `${reihe}${zahl}`.replace("sq7", "sq7")
|
||||
/**
|
||||
* Erkennt aus dem Modellnamen das passende Typenschild und trennt ab, was
|
||||
* darüber hinaus im Namen steht.
|
||||
*
|
||||
* Das ist die Regel aus dem alten Panel (`badge()`): Der Modellname selbst
|
||||
* steckt schon im Schild — daneben gehört nur, was danach kommt („Avant
|
||||
* competition"). Sonst stünde „RS 4" zweimal da.
|
||||
*/
|
||||
export function schildErkennen(
|
||||
titel: string,
|
||||
): { kuerzel: string; zusatz: string } | null {
|
||||
const treffer = /^\s*(RS\s?[34567]|S\s?Q7|S\s?3)\b/i.exec(titel)
|
||||
if (!treffer?.[1]) return null
|
||||
const kuerzel = treffer[1].replace(/\s+/g, "").toLowerCase()
|
||||
return { kuerzel, zusatz: titel.slice(treffer[0].length).trim() }
|
||||
}
|
||||
|
||||
export function Typenschild({ modell, hoehe = 22 }: { modell: string; hoehe?: number }) {
|
||||
const [theme] = useTheme()
|
||||
const kuerzel = schildKuerzel(modell)
|
||||
if (!kuerzel) return null
|
||||
const erkannt = schildErkennen(modell)
|
||||
if (!erkannt) return null
|
||||
|
||||
const variante = theme === "nacht" ? "negative" : "positive"
|
||||
const pfad = `../assets/audi/typenschilder/${kuerzel}-${variante}.svg`
|
||||
const quelle = SCHILDER[pfad]
|
||||
const quelle = SCHILDER[`../assets/audi/typenschilder/${erkannt.kuerzel}-${variante}.svg`]
|
||||
if (!quelle) return null
|
||||
|
||||
return <img src={quelle} alt={modell} height={hoehe} style={{ height: hoehe, width: "auto" }} />
|
||||
|
||||
@@ -15,7 +15,7 @@ import { datum, datumZeit, dauer, de, deOderStrich, eur } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { Leerzustand, NaviKachel, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { Bild } from "./Bild"
|
||||
import { Typenschild } from "./Typenschild"
|
||||
import { Typenschild, schildErkennen } from "./Typenschild"
|
||||
|
||||
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
||||
const { fahrzeug, einstellungen, fahrten, tankvorgaenge, jetztAktualisieren, statusStand } =
|
||||
@@ -24,6 +24,22 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
||||
|
||||
if (!fahrzeug || !einstellungen) return null
|
||||
|
||||
// Der Zusatz stammt aus dem Modellnamen selbst (alles nach dem Typenschild)
|
||||
// plus der eigens gepflegten Ausführung — so macht es auch das alte Panel.
|
||||
const schild = schildErkennen(einstellungen.fahrzeugtitel)
|
||||
const ausfuehrung = einstellungen.ausfuehrung.trim()
|
||||
// Die Ausführung steht oft schon im Modellnamen („RS 4 Avant competition"
|
||||
// plus Ausführung „competition") — dann nicht doppelt anhängen.
|
||||
const zusatz = [
|
||||
schild?.zusatz,
|
||||
ausfuehrung && !schild?.zusatz.toLowerCase().includes(ausfuehrung.toLowerCase())
|
||||
? ausfuehrung
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.trim()
|
||||
|
||||
const letzteFahrt = fahrten
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.ts_start).getTime() - new Date(a.ts_start).getTime())[0]
|
||||
@@ -59,11 +75,18 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
||||
/>
|
||||
<div className="dm-szene__text">
|
||||
<div className="dm-szene__marke">
|
||||
<Typenschild modell={einstellungen.fahrzeugtitel} hoehe={20} />
|
||||
<div className="ads-title">
|
||||
{einstellungen.fahrzeugtitel}{" "}
|
||||
{einstellungen.fahrzeugzusatz && (
|
||||
<span className="ads-sport">{einstellungen.fahrzeugzusatz}</span>
|
||||
<Typenschild modell={einstellungen.fahrzeugtitel} hoehe={26} />
|
||||
{/* Neben dem Schild steht nur, was im Namen darüber hinausgeht —
|
||||
das Modell selbst zeigt bereits das Schild. Ohne erkanntes
|
||||
Schild tritt der volle Name an dessen Stelle. */}
|
||||
<div className="ads-title dm-szene__zusatz">
|
||||
{schild ? (
|
||||
zusatz && <span className="ads-sport">{zusatz}</span>
|
||||
) : (
|
||||
<>
|
||||
{einstellungen.fahrzeugtitel}{" "}
|
||||
{zusatz && <span className="ads-sport">{zusatz}</span>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,15 @@
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
/* Inhalte dürfen nie gestaucht werden — die Fläche scrollt stattdessen.
|
||||
Ohne das quetscht Flex jeden Block mit `overflow: hidden` auf Höhe 0,
|
||||
sobald die Seite länger ist als der Bildschirm: `overflow: hidden` setzt
|
||||
die automatische Mindesthöhe auf 0, und dann greift flex-shrink ungebremst.
|
||||
Genau so verschwand das Fahrzeugbild samt Modellname und Kennzeichen. */
|
||||
.dm-inhalt > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.dm-inhalt--breit {
|
||||
padding: var(--sp-5) 32px 32px;
|
||||
}
|
||||
|
||||
@@ -421,9 +421,10 @@
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--fg3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
/* Umbrechen statt abschneiden: Datum, Uhrzeit und Dauer sind alle drei
|
||||
wichtig, und "11.08.2026 · 12:27 Uhr · 3…" verliert genau die Angabe,
|
||||
die man sucht. */
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Detailseiten */
|
||||
@@ -568,3 +569,27 @@
|
||||
background: var(--canvas);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.dm-szene__zusatz {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Der Zusatz neben dem Typenschild darf umbrechen — abgeschnittener Text mit
|
||||
Auslassungspunkten sieht nach Fehler aus, ein Umbruch nicht. */
|
||||
.dm-szene__zusatz {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
font-size: 19px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Die Galerie richtet sich nach der festen Kachelbreite der Bibliothek (78px),
|
||||
damit die Kacheln nicht auseinandergezogen werden und ihre Beschriftung
|
||||
innerhalb des Rahmens bleibt. */
|
||||
.dm-galerie {
|
||||
grid-template-columns: repeat(auto-fill, 78px);
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
@@ -51,10 +51,19 @@ def _token_lesen():
|
||||
|
||||
|
||||
def historie_abfragen(entity_id, start_iso, end_iso):
|
||||
"""urllib.request.urlopen läuft direkt, nicht über task.executor - anders
|
||||
als ein bloßes open() ist das ein modulqualifizierter Aufruf einer echten
|
||||
externen Funktion, der an der Testinstanz ohne Fehler durchlief. Blockiert
|
||||
kurz, läuft nur beim Fahrtabschluss-Screening, nicht bei jedem Tick."""
|
||||
"""Fragt die Verlaufsdaten des Kilometerstand-Sensors über die REST-API ab.
|
||||
|
||||
Beide blockierenden Schritte laufen über task.executor. Ein früherer Stand
|
||||
rief urlopen() direkt auf - das lief jahrelang durch, bis Home Assistant
|
||||
anfing, blockierende Aufrufe im Ereignisschleifen-Thread zu erkennen und
|
||||
abzubrechen (real beobachtet ab 2026.8: "Caught blocking call to
|
||||
putrequest ... inside the event loop"). Seitdem schlug jedes Screening
|
||||
fehl, und manuell angelegte Fahrten blieben ohne Strecke - sichtbar nur
|
||||
als Warnung im Protokoll, weil der Aufrufer den Fehler bewusst abfängt.
|
||||
|
||||
task.executor braucht echte externe Funktionen; urllib.request.urlopen und
|
||||
die read-Methode der Antwort sind genau das (dieselbe Einschränkung wie bei
|
||||
io.open in profil.py)."""
|
||||
token = _token_lesen()
|
||||
start_enc = urllib.parse.quote(start_iso)
|
||||
end_enc = urllib.parse.quote(end_iso)
|
||||
@@ -63,10 +72,10 @@ def historie_abfragen(entity_id, start_iso, end_iso):
|
||||
f"?filter_entity_id={entity_id}&end_time={end_enc}"
|
||||
)
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
rohtext = resp.read().decode("utf-8")
|
||||
resp = task.executor(urllib.request.urlopen, req, timeout=10)
|
||||
rohdaten = task.executor(resp.read)
|
||||
resp.close()
|
||||
daten = json.loads(rohtext)
|
||||
daten = json.loads(rohdaten.decode("utf-8"))
|
||||
return daten[0] if daten else []
|
||||
|
||||
|
||||
|
||||
Generated
+1220
-2
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,17 @@
|
||||
|
||||
default_config:
|
||||
|
||||
# Die App laeuft unter einer anderen Adresse als Home Assistant. Ohne diese
|
||||
# Freigabe lehnt der Browser jede Anfrage ab, noch bevor sie den Server
|
||||
# erreicht (CORS). Gilt genauso im Echtbetrieb - siehe
|
||||
# ../homeassistant/REVERSE_PROXY.md.
|
||||
http:
|
||||
cors_allowed_origins:
|
||||
- http://localhost:4173 # Vorschau des Produktionsbuendels
|
||||
- http://localhost:5173 # Entwicklungsserver
|
||||
- capacitor://localhost # native Huelle iOS
|
||||
- ionic://localhost # native Huelle Android
|
||||
|
||||
pyscript:
|
||||
allow_all_imports: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user