OTA-Updates für die iOS-App; HACS-Fehlannahme korrigiert

HACS kann laut eigener Dokumentation grundsätzlich nicht mit privaten
GitHub-Repositories arbeiten (hacs.xyz/docs/faq/private_repositories) - keine
Ausnahme für Tokens oder verbundene Konten. Meine frühere Annahme, HACS käme
damit zurecht, wenn es unter dem richtigen Konto angemeldet ist, war falsch.
Da das Repository aus Lizenzgründen privat bleiben muss (Audi-Hausschrift,
Typenschilder), ist install.ps1 damit nicht die Rückfallebene, sondern der
einzige Installationsweg - README, INSTALL.md, ANLEITUNG.md, install.ps1 und
VERSIONIERUNG.md korrigiert.

Oberflächen-Updates für die iOS-App laufen jetzt ohne Xcode:
@capgo/capacitor-updater eingebaut, ein Update-Abschnitt in den
Einstellungen lädt ein neues Bündel und tauscht die Oberfläche aus. Kein
Selbstlauf (autoUpdate: false) - nur auf Tastendruck, nie während der
Benutzung.

Das Bündel liegt in der Integration selbst
(custom_components/audi_dashboard/frontend/app/), nicht unter /local/: so
reist es bei jeder Installation automatisch mit, ohne zweiten
Auslieferungsweg. Gebaut von companion-app/scripts/ota-paket.ps1 (neuer
Befehl: npm run ota), gemeldet über sensor.audi_dashboard_app_version
(neues Feld daten.buendel).

Ein echter Bug beim Bauen gefunden: [IO.Compression.ZipFile]::CreateFrom-
Directory schreibt unter Windows PowerShell 5.1 Backslashes in die
Zip-Einträge - iOS hätte das Archiv falsch entpackt. Behoben, indem die
Einträge von Hand mit "/" geschrieben werden.

Rückfallebene: notifyAppReady() läuft erst, wenn React nachweislich
gerendert hat (App.tsx). Kommt diese Meldung nicht, rollt das Plugin nach
20 Sekunden von selbst auf das vorherige Bündel zurück.

Am laufenden Testcontainer verifiziert: die ausgelieferte Zip hasht exakt
auf den in bundle.json hinterlegten Wert, 13 Einträge, index.html in der
Wurzel, keine Backslashes, keine Beschädigung. tsc sauber, 117/117 Tests
(5 davon neu für buendelPasst() - dabei eine echte Lücke gefunden: die
Funktion hätte bei unbekannter eigener Version fälschlich ein Update
angeboten, jetzt genauso vorsichtig wie versionVergleichen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 09:20:14 +02:00
parent d8b12da36d
commit 1354ca6b06
22 changed files with 695 additions and 99 deletions
+64 -22
View File
@@ -188,7 +188,7 @@ assets. Never mix these the other way around.
| Area | What | Status |
|---|---|---|
| `custom_components/audi_dashboard/` | The HA integration: backend + panel, installable via HACS | ✅ **finished, in use** — replaced the pyscript backend on 2026-08-23 (section H) |
| `custom_components/audi_dashboard/` | The HA integration: backend, panel, OTA app bundle. HACS-shaped but HACS-installable only in principle — the repo is private and HACS categorically refuses private repos, so `install.ps1` is the actual, only install path (section H) | ✅ **finished, in use** — replaced the pyscript backend on 2026-08-23 (section H) |
| `homeassistant/` | Install package, recorder snippet, guides — **no code any more** | ✅ |
| `testumgebung/` | Script that rebuilds a throwaway Home Assistant with the real backend | ✅ new, reproducible |
| `design-system/` | React component library `@audi-dash/ui`, brand-free, feeds Claude Design | ✅ done as a kit (20 components, 1,690 lines) |
@@ -216,10 +216,11 @@ the repo.
**`companion-app/` — what exists:** the full app. Data layer (`src/api/`: REST, WebSocket with
reconnect backoff, persistent offline write queue, credential storage), domain logic
(`src/daten/`: profile adapter, statistics, service forecast, data context), all 21 screens
(`src/screens/`), Audi assets (`src/assets/audi/`), PWA manifest and icons. Verified by 90 unit
and render tests plus 9 checks against a live Home Assistant. `npm run dev` in the repo root
starts it; `testumgebung/aufsetzen.sh` provides the server side.
(`src/daten/`: profile adapter, statistics, service forecast, data context, OTA update flow — see
section H), all 21 screens (`src/screens/`), Audi assets (`src/assets/audi/`), PWA manifest and
icons. Verified by 117 unit and render tests (`npm test`) plus the two smoke suites against a live
Home Assistant (`npm run smoke`, `npm run smoke:auth`). `npm run dev` in the repo root starts it;
`testumgebung/aufsetzen.sh` provides the server side.
**DataMetric360 architecture (short — details in `COMPANION_APP_ARCHITECTURE.md`):**
- Future data source: **Teltonika FMM003** on the CAN bus, fully replacing the iPhone WLAN sensor
@@ -1814,10 +1815,20 @@ HACS 2.0.5 rather than from memory:
`custom_templates/` and `appdaemon/apps/<name>/` (verified in `repositories/*.py``localpath`).
The app needed `/config/pyscript/` **and** `/config/audi_dashboard/` **and** `configuration.yaml`
entries. This is what the conversion solved.
- **HACS is GitHub-only.** `github.com`/`api.github.com` are hardcoded throughout; the repo lives
on `gitea.nothaft.cloud`. This is **not** solved and cannot be solved from this side — it needs
the repo mirrored to GitHub. Until then `homeassistant/installationspaket/install.ps1` installs
the identical folder without HACS, so the conversion is useful either way.
- **HACS is GitHub-only.** `github.com`/`api.github.com` are hardcoded throughout; the repo lived
on `gitea.nothaft.cloud`. 2026-08-24: the owner mirrored it to `github.com/T130B/DM360`, and the
manifest URLs/codeowners now point there — but this does **not** make HACS usable. **HACS
categorically refuses private repositories**, confirmed against its own docs
(hacs.xyz/docs/faq/private_repositories: "Private GitHub repositories can not be used with HACS
at all... HACS can only get publicly available information") — no token, no signed-in-account
exception. An earlier note in this file claimed the opposite ("HACS must be signed in with the
account that owns it"); that was wrong and is corrected here. The GitHub mirror is **private on
purpose** (it carries the Audi typeface and the model badges, licensed for this one private
install only), so making it public to satisfy HACS is not on the table.
`homeassistant/installationspaket/install.ps1` is therefore not a fallback — it is **the only
installation path**, and the docs (`README.md`, `homeassistant/INSTALL.md`,
`installationspaket/ANLEITUNG.md`, `VERSIONIERUNG.md`, the script's own header) were rewritten
2026-08-24 to say so instead of presenting a HACS path that cannot work.
**The shape.** `custom_components/audi_dashboard/` at repo root (HACS looks for
`custom_components/<first dir>` there and nowhere else), `hacs.json` beside it. Config flow, one
@@ -1904,8 +1915,14 @@ left to add), `ANLEITUNG.pdf` (stale and not regenerable here; the `.md` beside
**Do not resurrect the pyscript tree.** If something is missing, port it — running both backends
writes two sets of trips into the same files.
**Still open, unchanged by this:** the repo must reach GitHub before HACS can install it. Everything
else about the HACS path is built and tested.
**HACS is not the delivery path — confirmed dead-end, not a gap.** 2026-08-24, checked against
HACS's own docs: private repos are refused categorically, no exception. The layout is still HACS
shape (`custom_components/audi_dashboard/` + `hacs.json` at repo root, manifest with `domain`,
`name`, `version`, `documentation`, `issue_tracker`, `codeowners`) in case the repo is ever
restructured to separate the licensed Audi assets from the installable code — but there is no
current plan to do that, and `install.ps1` is not standing in for HACS temporarily; it is the
permanent, only path. Don't spend effort re-verifying HACS compatibility for this repo again
unless the private/public situation changes.
### The deployed-parity problem, and what was built for it (2026-08-23)
@@ -1913,8 +1930,8 @@ The parity rule (binding, above) guarantees *source* parity: both codebases chan
session. It guarantees nothing about what is *running*. After the conversion the two sides are
wired differently in a new way:
- the panel ships **inside** the integration — one HACS update moves backend and panel together,
and it cannot go stale at all
- the panel ships **inside** the integration — one `install.ps1` run (the only delivery path — HACS
is a dead end here, see section H) moves backend and panel together, and it cannot go stale at all
- the companion app is a Capacitor shell with `webDir: "dist"` and **bundled** assets, sideloaded
through Xcode. It stays on whatever was bundled at signing time, silently, indefinitely
@@ -1928,15 +1945,40 @@ silently on a format change. Either side missing yields `"unbekannt"` and **no**
older backend or an offline start cannot produce a false alarm. And the vite build **fails hard** if
the manifest version is absent rather than emitting an app that cannot detect its own staleness.
**OTA delivery: verified viable, not yet built.** `@capgo/capacitor-updater` 8.51.14 checked against
the real package: MPL-2.0, peer `@capacitor/core: ^8.0.0` against our `^8.5.0`, and self-hosting is
first-class (`updateUrl`, or manual mode entirely). Manual mode is the good fit —
`download({version, url})` + `set()` against a plain zip under `/local/`, needing **no** custom
endpoint at all, with automatic rollback to the last good bundle via `notifyAppReady()`. It pairs
exactly with the version entity above: that entity is already the signal that a newer bundle exists.
Owner approved going this route, and a paid Apple developer account is available through Paul (Paul
Nothaft, the Gitea repo owner) — which is what makes it worthwhile, since the free account's 7-day
signature expiry would otherwise force Xcode weekly anyway.
**OTA delivery: built and verified end to end, 2026-08-24.** `@capgo/capacitor-updater` 8.51.14
installed (MPL-2.0, peer `@capacitor/core: ^8.0.0` against our `^8.5.0`). Manual mode, not
`autoUpdate` (that stays `false` on purpose — an update must never swap the UI out from under
someone mid-entry): `companion-app/src/daten/ota.ts` wraps `download({url, version, checksum})` +
`set({id})`, wired to a button in the Einstellungen screen (`otaUpdateVerfuegbar` /
`otaAusloesen`). `notifyAppReady()` runs from a `useEffect` in `AngemeldeteApp` (`App.tsx`) —
deliberately there and not in `main.tsx`, so a crash before React actually renders never gets
confirmed and the plugin's `appReadyTimeout` rollback (`capacitor.config.ts`, 20s, more generous
than the 10s default because first load also fetches profile/trips/refuels) takes over.
The bundle itself lives **inside the integration**, not under `/local/` as first sketched:
`custom_components/audi_dashboard/frontend/app/{bundle.zip,bundle.json}`, built by
`companion-app/scripts/ota-paket.ps1`, served at `/audi_dashboard_static/app/bundle.zip`. Reason
for the relocation: `/local/` would have needed its own delivery step that `install.ps1` doesn't
touch and could be forgotten; inside the integration folder it travels with every install/update
automatically, same as the panel. `sensor.audi_dashboard_app_version`'s `daten` gained a `buendel`
field (`koordinator._buendel_lesen()`) carrying `{version, sha256, bytes, gebaut, url}``null`
when no bundle is published, a normal state, not an error.
One real bug the build caught: `[IO.Compression.ZipFile]::CreateFromDirectory` on Windows
PowerShell 5.1 (.NET Framework, not Core) writes **backslashes** as path separators for nested
entries — a spec-violating zip that iOS would not have unpacked correctly. Caught by inspecting the
actual zip contents after the first build, not assumed; fixed by writing entries by hand via
`ZipFile::Open` + `CreateEntryFromFile` with `\` replaced by `/`. The plugin verifies SHA-256 over
the *downloaded zip itself* before unpacking (`CapgoUpdater.swift` `calcChecksum`, confirmed by
reading the plugin source, not the README) — `ota-paket.ps1` hashes the same file it writes, so the
two can't drift.
Verified against the running test container: entry reload picks up code changes but **not** a
manifest version bump (HA caches the `Integration` object — needs a full restart, same lesson as
the pyscript-removal step in section G); after a real restart, `sensor.audi_dashboard_app_version`
published the new manifest version and a complete `buendel` block; the zip served over HTTP hashed
identically to `bundle.json`'s `sha256`; `unzip -l`/`-t` confirmed 13 entries, `index.html` at the
root, no backslashes, no corruption.
Capacitor's `server.url` pointed at HA was considered and **rejected**: it would make the app as
current as the panel, but the shell then cannot boot without reaching HA, gutting the deliberately