diff --git a/AGENTS.md b/AGENTS.md index e0d5e78..e6cf69f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,10 @@ dazu die Kopfmarke statt der Firmierung des Betreibers und ein Cache-Brecher auf der .ipa-Adresse; die .ipa liess sich zuletzt nicht mehr installieren - der Erweiterung fehlte die CFBundleVersion; dazu der QR-Scanner fuer den Zugangs-Token; dazu Debug-Mode, eine Version-Kachel, das Knopfpaar nach -HIG und Bilder ohne Verzoegerung, `2026.9.4.11`, -Abschnitte CC bis CF. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt: +HIG und Bilder ohne Verzoegerung; dazu eine Musterseite, die die App ohne Token +und ohne Netz rendert; dazu der QR-Code aus einem gespeicherten Bild, ein stiller +Debug-Knopf und der HA-Zugang bei den Zugaengen, `2026.9.4.12`, +Abschnitte CC bis CH. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt: das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie @@ -9942,3 +9944,160 @@ ist ausgeschlossen. Abgedeckt sind sie durch `tsc`, den Alle-Seiten-Rendertest und die Tatsache, dass das Panel dieselbe Struktur aus denselben Backend-Daten baut - der Zweig MIT Datensatz ist in der App damit strukturell, nicht optisch belegt. + +## CG. Eine Musterseite loest den wiederkehrenden Pruefblocker (2026.9.4.12) + +**Das Problem, das seit Wochen wiederkehrt:** die App zeigt ohne gespeicherten +Zugang gar nichts, jede OPTISCHE Pruefung haengt damit an einem Zugangstoken - +und fast jeder Fehler, den dieses Projekt findet, ist nur am gerenderten Bild +sichtbar (CSS-Spezifitaet, Flex-Basis in Spalten, ein unsichtbarer Rahmen). +`tsc` und die Tests finden diese Klasse nachweislich nicht (Abschnitt S). + +**`companion-app/muster/`** mountet dieselben Bildschirme mit demselben +Stylesheet, aber mit `beispielApi()` aus den Tests: kein Netz, kein Token, +keine echten Daten - und trotzdem echtes CSS. Gebaut wird sie ueber eine +**eigene** Konfiguration (`vite.muster.config.ts`) nach `dist-muster/`; der +normale `npm run build` fasst sie nicht an, sie liegt also auch nicht in der +`.ipa`. Aufruf mit `?seite=` fuer jeden Namen aus `TITEL`. + +Ausgeliefert wird sie bei Bedarf in den Testcontainer: + +```bash +npx vite build --config vite.muster.config.ts +MSYS_NO_PATHCONV=1 docker cp companion-app/dist-muster/. audi_ha_test:/config/www/dmmuster/ +# http://localhost:18123/local/dmmuster/index.html?seite=einst +``` + +### Was sie sofort gefunden hat + +**Der Zielrahmen des QR-Scanners war unsichtbar.** `border: 2px solid var(--fg)` +liegt auf dem Kamerabild - und das ist dunkel. Im Tagmodus ist `--fg` fast +schwarz, der Rahmen also nicht zu sehen. Jetzt fest weiss mit einem dunklen +Saum darum, damit er auch vor einer hellen Wand ablesbar bleibt. Genau die +Sorte Fehler, die kein Test findet. + +### Was sie bestaetigt hat + +- **Letzter Datensatz**: Geraet, Ankunft, Geraetezeit mit Rueckstand + (40.686 s), Werte samt Einheit, Fussnote - erst nach dem Aufklappen. +- **Version**: „Installiert 2026.9.4.11 / Home-Assistant-Integration" und der + Update-Teil in EINER Kachel. +- **Knopfpaar**: „Verwerfen" und „Speichern" exakt **148 x 50 px** beide, + nebeneinander, der bestaetigende gefuellt und rechts. +- Zum ersten Mal live in der App gesehen: der Schalter ist **gruen** und der + Nacht/Tag-Umschalter zeigt die gefuellte Bahn - beides Korrekturen aus + Abschnitt AL, bis dahin nur strukturell belegt. + +### Die Bild-Verzoegerung, gemessen + +Am selben Bild (`seitenansicht.webp`, 1,2 MB, 2000x1125) in der Musterseite: + +| Fall | Dauer bis das Bild fertig ist | +|---|---| +| frisch geholt und dekodiert | 28,7 ms | +| **im HTTP-Zwischenspeicher, erstes Dekodieren** | **14,3 ms** | +| nach `decode()` vorab (was `bilderVorladen()` hinterlaesst) | **0,1 ms** | + +Die mittlere Zeile ist genau der gemeldete Menuewechsel: die Datei liegt im +Cache, das Dekodieren kostet trotzdem. Das Vorladen entfernt es vollstaendig. + +**Der Preis, und er gehoert genannt:** die vier vorhandenen Fotos sind zusammen +**5,3 MB**, und `bildVersion` haengt an `Date.now()` - die Adresse aendert sich +also bei JEDEM App-Start, der HTTP-Zwischenspeicher greift zwischen zwei Starts +nie. Bisher zahlte man das nur fuer Bilder, die man ansieht; mit dem Vorladen +fuer alle. **Offen, dem Eigentuemer vorgelegt:** entweder so lassen, oder nur +das Uebersichtsbild vorladen, oder - die eigentliche Loesung - den +Cache-Brecher an eine echte Fotoversion aus dem Backend haengen statt an die +Startzeit, dann faellt der wiederholte Abruf ganz weg. + +### Nicht geprueft + +Der Kamerapfad des Scanners (die Vorschau gibt keine Kamera heraus) und das +Vorladen in der App selbst - `bildUrl()` braucht dafuer einen gespeicherten +Zugang, den die Musterseite bewusst nicht hat. Im Panel ist das Vorladen live +belegt (alle neun Plaetze in `performance.getEntriesByType`), und beide +Codebasen fuehren denselben Ablauf. + +## CH. Der QR-Code darf auch aus einem gespeicherten Bild kommen (2026.9.4.12) + +### Ohne Kamera war der Scanner eine Sackgasse + +Vorgabe des Eigentuemers: ein Knopf im Scanner-Blatt, der ein **gespeichertes +Bild** des QR-Codes einliest. Der Anlass war die Testumgebung ohne Kamera, aber +der Fall ist auch auf dem Telefon der haeufigere: der Token entsteht in Home +Assistant am **Rechner**, und ein Bildschirmfoto davon liegt danach ohnehin auf +dem Geraet. Das Telefon vor den Monitor zu halten ist der Umweg. + +`qrAusDatei()` bekommt dieselben Bilddaten wie der Kamerapfad und benutzt +denselben Leser (jsQR) - es ist eine zweite Quelle, kein zweiter Mechanismus. +Zwei Unterschiede mit Grund: das Bild wird auf `MAX_KANTE = 2000` verkleinert +(ein Handyfoto hat leicht 4000 px, und jsQR laeuft Pixel fuer Pixel darueber), +und es wird mit `inversionAttempts: "attemptBoth"` gelesen - ein Bildschirmfoto +aus dem Dunkelmodus zeigt den Code hell auf dunkel. + +Ohne Kamera verschwindet ausserdem das schwarze Quadrat: es waere totes Beiwerk, +und die Flaeche gehoert dann dem Weg, der noch funktioniert. + +### Der Fund, den erst die Musterseite gezeigt hat: `decode()` bleibt stehen + +Die erste Fassung dekodierte mit `new Image()` plus `await bild.decode()`. Am +04.09.2026 an der Musterseite gemessen: bei einem **GIF** loest `decode()` in +Chromium **nie** auf - das Bild war laengst geladen (`naturalWidth` 294), das +Versprechen blieb offen. Folge: das Blatt haette ohne Fehlermeldung, ohne +Kreisel und ohne Absage ewig gewartet. + +Jetzt `createImageBitmap(datei)` (der dafuer vorgesehene Weg, nimmt einen Blob +direkt), und als Rueckfall bewusst das **`load`-Ereignis** statt `decode()`. + +Genau die Fehlerklasse, fuer die die Musterseite gebaut wurde: `tsc` und 215 +Tests waren gruen, der Fehler stand trotzdem drin - er zeigt sich nur beim +Ausfuehren, und ausfuehren liess sich der Bildschirm bisher nur mit einem Token. + +### Die Musterseite kann jetzt auch die Ersteinrichtung + +`?seite=einrichtung` mountet den Willkommensbildschirm. Er ist keine Route (er +steht vor der Anmeldung) und brauchte deshalb einen eigenen Zweig in +`muster/muster.tsx`. + +**Damit ist der ganze Weg nachgewiesen, ohne einen echten Token anzufassen** - +gepruefte QR-Codes waren selbst erzeugte (die im Repo liegende MIT-Bibliothek +`homeassistant/www/dm360-qrcode-lib.js` erzeugt sie, in den Container kopiert +und ueber `/local/` geladen): + +| Eingespieltes Bild | Ergebnis | +|---|---| +| QR mit einem Text in JWT-Form | steht **zeichengleich** im Token-Feld, Blatt schliesst, Adressfeld unberuehrt | +| QR mit `http://muster.local:8123` | landet im **Adressfeld**, nicht im Token-Feld | +| graues Bild ohne Code | „In diesem Bild ist kein QR-Code zu erkennen", Blatt bleibt offen | + +### Debug-Mode ist nur noch ein stiller Knopf + +Vorgabe des Eigentuemers: „moeglichst unauffaellig". Kachel, Ueberschrift und +der erklaerende Satz sind entfallen; es bleibt ein Textknopf am Seitenende +(`.debugknopf` / `.dm-debugknopf`, wortgleich in beiden Codebasen): kein +Kachelgrund, kein Rahmen, 12,5 px in der Drittfarbe. Die 44 px Trefferhoehe +kommen aus dem Innenabstand - die Flaeche stimmt, ohne dass der Knopf gross +aussieht. Live gemessen: Panel 45 px, `rgb(107,107,112)`, durchsichtig, letztes +Element der Seite; App 12,5 px, `rgb(142,142,147)`. + +### Der Zugang zu Home Assistant steht jetzt bei den Zugaengen + +Ebenfalls Vorgabe: die eigene Kachel „Zugang" mit „Verbindung trennen" wandert +in den Debug-Mode, und zwar **in** die Kachel „Zugaenge". Das ist dieselbe Sorte +Angabe wie die Dienste-Token, und eine Verbindung wird ungefaehr so oft getrennt, +wie ein Token getauscht wird - also fast nie. Als Akkordeon „Home Assistant" mit +der Server-Adresse (aus `zugangLesenSynchron()`) und dem Trennen-Knopf; die +Kachel wird deshalb jetzt immer gezeigt, nicht mehr nur bei vorhandenen +Dienste-Token. + +Ohne Entsprechung im Panel, und zwar notwendigerweise: es laeuft INNERHALB von +Home Assistant und kennt weder Adresse noch Token. + +### Merkposten: die App am Entwicklungsserver anmelden + +Gemeldet als „die Adresse stimmt nicht". Aus dem Dev-Server (`:5173`) heraus ist +`http://localhost:18123` **fremder Ursprung**, der Preflight endet in 403 - die +App meldet daraufhin voellig zu Recht „Der Server ist unter dieser Adresse nicht +erreichbar". Einzutragen ist die **eigene** Adresse `http://localhost:5173`; der +Vite-Proxy reicht `/api` serverseitig an `:18123` weiter (Abschnitt AK). Nach der +Korrektur stand die Uebersicht mit echten Daten. diff --git a/companion-app/.gitignore b/companion-app/.gitignore index f879a22..1a27a41 100644 --- a/companion-app/.gitignore +++ b/companion-app/.gitignore @@ -1,5 +1,7 @@ node_modules/ dist/ +# Bau der Musterseite (vite.muster.config.ts) - reines Ergebnis, nie in der .ipa. +dist-muster/ *.local .DS_Store diff --git a/companion-app/muster/index.html b/companion-app/muster/index.html new file mode 100644 index 0000000..70933f4 --- /dev/null +++ b/companion-app/muster/index.html @@ -0,0 +1,12 @@ + + + + + + DataMetric360 – Musterseite + + +
+ + + diff --git a/companion-app/muster/muster.tsx b/companion-app/muster/muster.tsx new file mode 100644 index 0000000..5f4b48e --- /dev/null +++ b/companion-app/muster/muster.tsx @@ -0,0 +1,77 @@ +/** + * Musterseite: die Oberflaeche der App mit Beispieldaten, ohne Server. + * + * WOFUER + * ------ + * Die App braucht sonst einen gespeicherten Zugang, um ueberhaupt etwas + * anzuzeigen - und damit war jede OPTISCHE Pruefung an ein Zugangstoken + * gebunden. Genau daran ist in diesem Projekt wiederholt die Verifikation + * haengengeblieben, obwohl fast jeder gefundene Fehler (CSS-Spezifitaet, + * Flex-Basis in Spalten, unsichtbare Rahmen) nur am gerenderten Bild + * sichtbar war und von tsc und den Tests nicht gefunden werden konnte. + * + * Diese Seite mountet dieselben Bildschirme mit demselben Stylesheet, aber + * mit `beispielApi()` aus den Tests. Kein Netz, kein Token, keine echten + * Daten - und trotzdem echtes CSS. + * + * NICHT TEIL DER APP + * ------------------ + * Gebaut wird sie ueber `vite.muster.config.ts` nach `dist-muster/`. Der + * normale `npm run build` fasst sie nicht an, sie liegt also auch nicht in + * der .ipa. + * + * Aufruf: `?seite=einst` (Vorgabe), jeder Name aus navigation.ts TITEL. + * Dazu `?seite=einrichtung` fuer den Willkommensbildschirm - der ist keine + * Route (er steht VOR der Anmeldung) und braucht deshalb einen eigenen Zweig. + */ + +import { StrictMode, useState } from "react" +import { createRoot } from "react-dom/client" + +import "@audi-dash/ui/styles.css" +import "../src/stile/grundlage.css" +import "../src/stile/audi-schrift.css" +import "../src/stile/rahmen.css" +import "../src/stile/screens.css" + +import { DatenAnbieter } from "../src/daten/DatenKontext" +import { Shell } from "../src/Shell" +import type { SeitenName } from "../src/navigation" +import { TITEL } from "../src/navigation" +import { Einrichtung } from "../src/screens/Einrichtung" +import { SeiteFuer } from "../src/screens/register" +import { beispielApi } from "../src/tests/beispieldaten" + +function Muster() { + const gewaehlt = new URLSearchParams(location.search).get("seite") || "einst" + const start = gewaehlt as SeitenName + const [route, setzeRoute] = useState<{ name: SeitenName; id?: string }>({ name: start }) + const geheZu = (name: SeitenName, id?: string) => + setzeRoute(id === undefined ? { name } : { name, id }) + // Der Willkommensbildschirm haengt an keinem Zugang und an keiner Route. + if (gewaehlt === "einrichtung") return {}} /> + return ( + + + {}} + beiAbmeldung={() => {}} + /> + + + ) +} + +// Zum schnellen Springen zwischen den Seiten, ohne die Adresse zu tippen. +;(window as unknown as { SEITEN: string[] }).SEITEN = Object.keys(TITEL) + +const wurzel = document.getElementById("wurzel") +if (!wurzel) throw new Error("Wurzelelement #wurzel fehlt") +createRoot(wurzel).render( + + + , +) diff --git a/companion-app/package.json b/companion-app/package.json index af402d6..b629d6f 100644 --- a/companion-app/package.json +++ b/companion-app/package.json @@ -39,6 +39,7 @@ "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", "jsdom": "^29.1.1", + "qrcode": "^1.5.4", "typescript": "^5.7.2", "vite": "^6.0.7", "vitest": "^2.1.8", diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index 8c7d405..c163a87 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -22,7 +22,7 @@ import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder" import { DatensatzPopup } from "./DatensatzPopup" import { HistorienImport } from "./HistorienImport" import type { ServicebuchEintrag } from "./Service" -import { zugangVergessen } from "./zugang" +import { zugangLesenSynchron, zugangVergessen } from "./zugang" export function Einstellungen({ tabBeschriftung, @@ -857,42 +857,41 @@ export function Einstellungen({ !updateNeustartLaeuft &&

{integrationUpdate.fehler}

} - {/* Ohne Entsprechung im Panel - und zwar notwendigerweise: das Panel - laeuft innerhalb von Home Assistant und kennt keine Serveradresse und - kein Token, die man wechseln koennte. Bleibt deshalb bestehen (wie - der Willkommensbildschirm). Die beiden reinen Zaehlzeilen darueber - („Fahrten gespeichert"/„Tankvorgaenge gespeichert") sind entfallen - - die waren Zusatz ohne Panel-Vorbild. */} - - Zugang - void abmelden()}> - Verbindung trennen - - - {/* Debug-Mode: Zugaenge, Dongle und der letzte Datensatz des Geraets. Drei Kacheln, die im Alltag nichts zu suchen haben und beim Suchen - eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten - (Vorgabe des Eigentuemers, 04.09.2026). */} - - Debug-Mode -

- Zugangs-Token, die Einstellungen des Dongles und der letzte Datensatz, den das Gerät - gemeldet hat. Für die Fehlersuche, nicht für den Alltag. -

- setzeDebugOffen(!debugOffen)} - > - {debugOffen ? "Fertig" : "Öffnen"} - -
+ eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten. + + Bewusst KEINE Kachel und keine Erlaeuterung, sondern ein stiller + Textknopf am Seitenende (Vorgabe des Eigentuemers, 04.09.2026: + „moeglichst unauffaellig"). */} + {debugOffen && ( <> - {zugaenge.length > 0 && ( - - Zugänge + {/* Der Zugang zu Home Assistant selbst steht hier mit drin (Vorgabe des + Eigentuemers, 04.09.2026): es ist dieselbe Sorte Angabe wie die + Dienste-Token, und getrennt wird eine Verbindung ungefaehr so oft, + wie ein Token getauscht wird - also fast nie. Ohne Entsprechung im + Panel, und zwar notwendigerweise: das Panel laeuft INNERHALB von + Home Assistant und kennt weder Adresse noch Token. */} + + Zugänge + verbunden} + > +

+ Adresse und Zugangs-Token dieser Instanz. „Verbindung trennen“ löscht beides vom + Gerät — die App fragt beim nächsten Start wieder danach, die Daten auf dem Server + bleiben unberührt. +

+ + void abmelden()}> + Verbindung trennen + +
{/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben — man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */} {zugaenge.map((z) => ( @@ -947,8 +946,7 @@ export function Einstellungen({ Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard → Konfigurieren. -
- )} +
{dongle?.aktiv && ( Dongle diff --git a/companion-app/src/screens/QrScanner.tsx b/companion-app/src/screens/QrScanner.tsx index 450a5ea..9f24530 100644 --- a/companion-app/src/screens/QrScanner.tsx +++ b/companion-app/src/screens/QrScanner.tsx @@ -23,6 +23,15 @@ * Steht im Code trotzdem eine http-Adresse, ist es offensichtlich die * Server-Adresse - dann wandert sie in das andere Feld, statt als Token * abgelehnt zu werden. + * + * ZWEITER WEG: EIN GESPEICHERTES BILD + * ----------------------------------- + * Der QR-Code steht in Home Assistant auf demselben Bildschirm, auf dem der + * Token entsteht - und das ist beim Einrichten meist der Rechner, nicht das + * Telefon. Ein abfotografierter oder als Bildschirmfoto gesicherter Code ist + * dann der kuerzere Weg, als das Telefon vor den Monitor zu halten - und ohne + * Kamera (Simulator, Testumgebung) der einzige. jsQR bekommt dafuer dieselben + * Bilddaten wie aus dem Kamerabild: derselbe Leser, nur eine andere Quelle. */ import { useEffect, useRef, useState } from "react" @@ -43,6 +52,78 @@ export function qrFundDeuten(roh: string): QrFund | null { return { art: "token", wert: t } } +/** + * Laengste Kante, auf die ein Foto vor dem Lesen verkleinert wird. Ein + * Handyfoto hat leicht 4000 px, und jsQR laeuft darueber Pixel fuer Pixel; + * ein QR-Code bleibt auch bei 2000 px klar genug. Deutlich kleiner waere + * riskant - dann verschwimmen die Module eines nicht formatfuellend + * aufgenommenen Codes. + */ +const MAX_KANTE = 2000 + +/** + * Dekodiert die Datei zu etwas, das auf eine Leinwand gezeichnet werden kann. + * + * `createImageBitmap` ist dafuer der vorgesehene Weg und nimmt einen Blob + * direkt. Der Ruckfall ist bewusst das `load`-Ereignis und NICHT `decode()`: + * am 04.09.2026 an der Musterseite gemessen, dass `decode()` bei einem GIF in + * Chromium nie aufloest - das Bild war laengst geladen (294x294), das + * Versprechen blieb offen. Das Blatt haette dann ohne Fehlermeldung ewig + * gewartet, und genau das ist schlimmer als eine klare Absage. + */ +async function bildDekodieren(datei: File): Promise { + if (typeof createImageBitmap === "function") { + try { + return await createImageBitmap(datei) + } catch { + // Weiter mit dem Rueckfall - manche Formate kennt nur der -Pfad. + } + } + const url = URL.createObjectURL(datei) + try { + const bild = new Image() + await new Promise((fertig, fehler) => { + bild.onload = () => fertig() + bild.onerror = () => fehler(new Error("Bild nicht lesbar")) + bild.src = url + }) + return Object.assign(bild, { width: bild.naturalWidth, height: bild.naturalHeight }) + } finally { + // Ohne das haelt der Browser die Datei bis zum Neuladen im Speicher. + URL.revokeObjectURL(url) + } +} + +/** + * Liest den QR-Code aus einer gespeicherten Bilddatei - `null`, wenn keiner + * darin steht. Wirft, wenn der Browser das Bildformat gar nicht dekodieren + * kann (HEIC am Rechner ist der praktische Fall). + */ +export async function qrAusDatei(datei: File): Promise { + const bild = await bildDekodieren(datei) + try { + const faktor = Math.min(1, MAX_KANTE / Math.max(bild.width, bild.height)) + const breite = Math.max(1, Math.round(bild.width * faktor)) + const hoehe = Math.max(1, Math.round(bild.height * faktor)) + const leinwand = document.createElement("canvas") + leinwand.width = breite + leinwand.height = hoehe + const stift = leinwand.getContext("2d", { willReadFrequently: true }) + if (!stift) return null + stift.drawImage(bild, 0, 0, breite, hoehe) + const { default: jsQR } = await import("jsqr") + const daten = stift.getImageData(0, 0, breite, hoehe) + // attemptBoth, anders als im Kamerabild: ein Bildschirmfoto aus dem + // Dunkelmodus zeigt den Code hell auf dunkel, also invertiert. + const treffer = jsQR(daten.data, daten.width, daten.height, { + inversionAttempts: "attemptBoth", + }) + return treffer ? treffer.data : null + } finally { + if ("close" in bild && typeof bild.close === "function") bild.close() + } +} + /** Wie oft ein Bild ausgewertet wird. Öfter kostet Akku, ohne schneller zu treffen. */ const TAKT_MS = 250 @@ -56,7 +137,11 @@ export function QrScanner({ gefunden: (fund: QrFund) => void }) { const videoRef = useRef(null) + const dateiRef = useRef(null) const [fehler, setzeFehler] = useState(null) + // Ohne Kamera waere das schwarze Quadrat totes Beiwerk - dann bleibt der + // Weg ueber die Bilddatei uebrig, und der soll die Flaeche haben. + const [kameraAus, setzeKameraAus] = useState(false) useEffect(() => { if (!offen) return @@ -66,6 +151,7 @@ export function QrScanner({ const starten = async () => { if (!navigator.mediaDevices?.getUserMedia) { + setzeKameraAus(true) setzeFehler("Dieses Gerät stellt der App keine Kamera zur Verfügung.") return } @@ -75,6 +161,7 @@ export function QrScanner({ audio: false, }) } catch { + setzeKameraAus(true) setzeFehler( "Kein Zugriff auf die Kamera. In den Einstellungen des Geräts unter „DataMetric360“ freigeben.", ) @@ -121,23 +208,70 @@ export function QrScanner({ } }, [offen, gefunden, schliessen]) + const bildLesen = async (datei: File | undefined) => { + if (!datei) return + setzeFehler(null) + let text: string | null + try { + text = await qrAusDatei(datei) + } catch { + setzeFehler( + "Dieses Bildformat kann der Browser nicht lesen. Bitte als JPEG, PNG oder WebP sichern.", + ) + return + } + if (!text) { + setzeFehler( + "In diesem Bild ist kein QR-Code zu erkennen. Am besten den Code formatfüllend aufnehmen.", + ) + return + } + const fund = qrFundDeuten(text) + if (!fund) { + setzeFehler("Dieser Code enthält weder einen Token noch eine Adresse.") + return + } + gefunden(fund) + schliessen() + } + return ( -
-
+ {!kameraAus && ( +
+
+ )} {fehler && (

{fehler}

)} - Abbrechen + { + const datei = e.target.files?.[0] + // Zuruecksetzen, damit dasselbe Bild ein zweites Mal gewaehlt werden + // kann - sonst feuert onChange beim gleichen Namen nicht. + e.target.value = "" + void bildLesen(datei) + }} + /> +
+ Abbrechen + dateiRef.current?.click()}> + Bild auswählen + +
) } diff --git a/companion-app/src/screens/screens.test.tsx b/companion-app/src/screens/screens.test.tsx index 1f26fbd..8ec1e22 100644 --- a/companion-app/src/screens/screens.test.tsx +++ b/companion-app/src/screens/screens.test.tsx @@ -50,8 +50,12 @@ describe("Einstellungen: Debug-Mode", () => { await waitFor(() => expect(screen.getByText("Debug-Mode")).toBeTruthy()) // Zugeklappt: die Kacheln sind nicht bloss unsichtbar, sie stehen nicht da. expect(screen.queryByText("Letzter Datensatz")).toBeNull() + // Der Zugang zu Home Assistant steht ebenfalls erst im Debug-Mode. + expect(screen.queryByText("Verbindung trennen")).toBeNull() - fireEvent.click(screen.getByText("Öffnen")) + // Der stille Textknopf am Seitenende ist die einzige Beschriftung - + // Ueberschrift und Erlaeuterung sind entfallen (Vorgabe 04.09.2026). + fireEvent.click(screen.getByText("Debug-Mode")) await waitFor(() => expect(screen.getByText("Letzter Datensatz")).toBeTruthy()) // Der gefuellte Zweig, nicht der Leerzustand: Geraet, Werte und der @@ -60,6 +64,7 @@ describe("Einstellungen: Debug-Mode", () => { expect(screen.getByText("CAN fuel volume")).toBeTruthy() expect(screen.getByText("7 liters")).toBeTruthy() expect(container.textContent).toContain("Rückstand") + expect(screen.getByText("Verbindung trennen")).toBeTruthy() }) }) diff --git a/companion-app/src/stile/screens.css b/companion-app/src/stile/screens.css index 0ff78ff..9c2a21f 100644 --- a/companion-app/src/stile/screens.css +++ b/companion-app/src/stile/screens.css @@ -2118,11 +2118,36 @@ button.dm-listenzeile { object-fit: cover; display: block; } +/* Fest weiss, NICHT var(--fg): der Rahmen liegt auf dem Kamerabild, und das + ist dunkel. Im Tagmodus waere --fg fast schwarz - der Rahmen war dort + unsichtbar (am gerenderten Blatt gesehen, 04.09.2026). Der Schatten haelt + ihn auch vor einer hellen Wand ablesbar. */ .dm-qr__rahmen { position: absolute; inset: 18%; - border: 2px solid var(--fg); + border: 2px solid #fff; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); border-radius: 12px; opacity: 0.85; pointer-events: none; } + +/* Wortgleich zu .debugknopf im Panel: der stille Knopf zum Debug-Mode am + Seitenende. Kein Kachelgrund, keine Ueberschrift, Zweitfarbe - er soll + gefunden werden koennen, ohne sich anzubieten. Die 44px Trefferhoehe + kommen aus dem Innenabstand. */ +.dm-debugknopf { + display: block; + width: 100%; + padding: 15px 0; + border: 0; + background: none; + font: inherit; + font-size: 12.5px; + color: var(--fg3); + text-align: center; + cursor: pointer; +} +.dm-debugknopf:active { + color: var(--fg2); +} diff --git a/companion-app/vite.muster.config.ts b/companion-app/vite.muster.config.ts new file mode 100644 index 0000000..58e9827 --- /dev/null +++ b/companion-app/vite.muster.config.ts @@ -0,0 +1,17 @@ +/** + * Bau der Musterseite (siehe muster/muster.tsx). + * + * Bewusst eine EIGENE Konfiguration mit eigenem Ausgabeordner: die Seite darf + * nicht in `dist/` landen, weil `dist/` die Huelle der iOS-App ist. Was dort + * liegt, wird mit ausgeliefert. + */ +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" + +export default defineConfig({ + root: "muster", + base: "./", + plugins: [react()], + define: { __APP_VERSION__: JSON.stringify("muster") }, + build: { outDir: "../dist-muster", emptyOutDir: true }, +}) diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index e61f11a..af6f935 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.9.4.11","sha256":"937302e85e87c62326c4f7013cb026f50243b8cc5d521d752b5a3708120d2b91","bytes":320679,"gebaut":"2026-09-04T10:13:31Z"} \ No newline at end of file +{"version":"2026.9.4.12","sha256":"bb66727726eac2e6be98dc1132bd3b532c8c6b9525f9885020a65d99d2d7fc55","bytes":321283,"gebaut":"2026-09-04T11:05:31Z"} \ No newline at end of file diff --git a/custom_components/audi_dashboard/frontend/app/bundle.zip b/custom_components/audi_dashboard/frontend/app/bundle.zip index 5053e5a..68d4ffe 100644 Binary files a/custom_components/audi_dashboard/frontend/app/bundle.zip and b/custom_components/audi_dashboard/frontend/app/bundle.zip differ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index 07cbe92..43f85e6 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -3442,15 +3442,13 @@ function vEinst() { -
Debug-Mode - - Zugangs-Token, die Einstellungen des Dongles und der letzte Datensatz, den - das Gerät gemeldet hat. Für die Fehlersuche, nicht für den Alltag. - -
+ eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten. + + Bewusst KEINE Kachel und keine Erlaeuterung, sondern ein stiller + Textknopf am Seitenende (Vorgabe des Eigentuemers, 04.09.2026: + „moeglichst unauffaellig"). Wer ihn braucht, weiss was er tut; wer + ihn nicht braucht, soll nicht darueber stolpern. --> + ${debugOffen ? ` ${ZUGAENGE.length ? `
Zugänge ${ZUGAENGE.map((z, i) => ` diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard.css b/custom_components/audi_dashboard/frontend/audi-dashboard.css index fcff5d1..43c471e 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard.css @@ -1671,3 +1671,24 @@ img.carfix.klein { aspect-ratio: auto; height: auto; } flex: 1 1 0; margin-top: 0; } + +/* Der stille Knopf zum Debug-Mode am Seitenende: kein Kachelgrund, keine + Ueberschrift, Zweitfarbe - er soll gefunden werden koennen, ohne sich + anzubieten. Die 44px Trefferhoehe kommen aus dem Innenabstand, damit die + Flaeche stimmt, ohne dass der Knopf gross aussieht. */ +.debugknopf { + display: block; + width: 100%; + margin-top: var(--sp-4); + padding: 15px 0; + border: 0; + background: none; + font: inherit; + font-size: 12.5px; + color: var(--fg3); + text-align: center; + cursor: pointer; +} +.debugknopf:active { + color: var(--fg2); +} diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 3ce3554..fbea92f 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "audi_dashboard", "name": "Audi Dashboard", - "version": "2026.9.4.11", + "version": "2026.9.4.12", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "codeowners": [ diff --git a/package-lock.json b/package-lock.json index 0fd6348..51b5c17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", "jsdom": "^29.1.1", + "qrcode": "^1.5.4", "typescript": "^5.7.2", "vite": "^6.0.7", "vitest": "^2.1.8", @@ -2337,6 +2338,16 @@ "node": ">=8" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001809", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", @@ -2411,6 +2422,49 @@ "node": ">=18" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2537,6 +2591,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -2575,6 +2639,13 @@ "node": ">=6" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -2740,6 +2811,20 @@ } } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fix-dts-default-cjs-exports": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", @@ -2792,6 +2877,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3058,6 +3153,19 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3269,6 +3377,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3289,6 +3436,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3407,6 +3564,16 @@ "node": ">=10.4.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", @@ -3529,6 +3696,24 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3601,6 +3786,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3611,6 +3806,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -3747,6 +3949,13 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6055,6 +6264,13 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -6171,6 +6387,13 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6178,6 +6401,43 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",