diff --git a/AGENTS.md b/AGENTS.md
index 723f1f6..670d963 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -420,11 +420,21 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
### B) Infrastructure / commissioning (partly waits for FMM003 hardware)
- [ ] Switch `datametric360.app` nameservers at all-inkl to Cloudflare ("full setup") —
- prerequisite for the tunnel; domain carries nothing else, so this is consequence-free
-- [ ] Decide hostname split (app on apex + API on `api.` subdomain, or vice versa)
-- [ ] Choose reverse proxy (Nginx Proxy Manager vs. Traefik) — **can be done before hardware**
- against the existing HA API
-- [x] Define the reverse-proxy path allowlist (depends on final entity/service names)
+ prerequisite for the tunnel; domain carries nothing else, so this is consequence-free. Step-by-
+ step runbook for this and everything below it now exists:
+ [`homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md`](homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md)
+ (written 2026-08-28) — account creation, DNS/nameserver changes, and clicking through the HA
+ add-on UI all remain the owner's own action; nothing here can be done unattended.
+- [x] Decide hostname split — **resolved 2026-08-28: no split, one hostname is enough.** The original
+ app-domain-vs-API-subdomain question assumed a served web build; distribution stayed native/
+ sideload only (never built as a web app), so `https://datametric360.app` alone covers both API
+ calls and the app's own OTA bundle. See `COMPANION_APP_ARCHITECTURE.md` §5 item 4.
+- [x] Choose reverse proxy — **decided 2026-08-28: Nginx Proxy Manager**, not Traefik. See
+ `COMPANION_APP_ARCHITECTURE.md` §5 item 3.
+- [x] Define the reverse-proxy path allowlist (depends on final entity/service names) — kept current
+ 2026-08-28: corrected the stale `/local/dm360/*` assumption (never built) to the app's real
+ remote need, its OTA bundle at `/audi_dashboard_static/app/*` (`@capgo/capacitor-updater`). See
+ `homeassistant/REVERSE_PROXY.md`.
- [ ] Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
- [x] Generate TLS certificates for Mosquitto + device (small private CA) — done 2026-08-11, 10-year
validity; Mosquitto configured (`certfile`/`keyfile`/`cafile`/`require_certificate: true`).
@@ -4515,6 +4525,71 @@ cluster (`total_vehicle_mileage_read_from_can`, `front_left_door`, ...) both sti
majority-vote fix didn't regress the two-simultaneous-sources case `.7` was built to solve. `node --check`
clean, manifest bumped to `2026.8.28.8`, `audi_ha_test` restarted and confirmed clean via log.
+## AH. Setup-Menü: fester Beispielname zusätzlich zum tatsächlich zugeordneten Sensor (2026.8.28.17)
+
+Owner: the sensor-identity hint next to each Setup field headline (`entitaetIdKurz()`, section AG) changes
+with whatever is currently mapped - useful for confirming a mapping, but it gives no orientation on what to
+search for *before* anything is mapped, or after resetting/remapping. Wanted a second, permanently fixed
+example name shown alongside it - `(z. B. can_fuel_volume)` / `(z. B. engine_ignition_or_acc_status)` -
+styled prominently ("Sensorname-Font", white/big), with the real currently-mapped sensor kept smaller/grey
+exactly as before.
+
+Added a static `"beispiel"` string to every entry in the `FELDER` catalog (`einstellungen.py`) - one
+plausible real-world sensor-ID suffix per role (`can_fuel_volume`, `engine_ignition_or_acc_status`,
+`external_power_voltage`, `oil_change_due`/`oil_change_distance`, `front_left_door`, ...), several taken
+directly from this project's own real Setup data (sections AC/AG). `FELDER` is serialized to the frontend
+verbatim (`zuordnung.py`'s `katalog`), so no publish/service change was needed - the new key just appears on
+`feld.beispiel` in JS.
+
+`setupFeldzeile()` (`audi-dashboard-app.js`) now renders two stacked lines on the right side of each field's
+header instead of one: `feld.beispiel` (fixed, never changes with the mapping) as
+`.setup-feld-beispiel` - monospace like the sensor-ID font used elsewhere in this file
+(`.entitaet-eintrag-id`), but larger (14px) and full-brightness (`var(--fg)`) - and the existing
+`entitaetIdKurz()`-derived real mapping as `.setup-feld-hinweis`, unchanged styling (12.5px, `var(--fg2)`).
+Both sit inside a new `.setup-feld-hinweise` flex-column wrapper (right-aligned, 2px gap); `.setup-feld-kopf`
+switched from `align-items:baseline` to `flex-start` to keep the label aligned with the top of the now
+two-line block instead of its vertical center. No change needed to `entitaetIdKurz()`/`praefixPaar()`
+themselves (section AG's majority-vote logic) - only what's displayed alongside their result changed, not
+how the real mapping is computed.
+
+Verified live in `audi_ha_test` (not just read from source): read the first six rendered Setup rows via the
+shadow DOM after a real restart - every row correctly shows both the fixed example and the real, currently
+mapped sensor's short ID side by side and independently (e.g. Tankfüllstand: beispiel `fuel_level`, real
+`fuel_level_in_tank_read_from_can` - genuinely different strings, confirming the example does not track the
+mapping). `node --check` clean, `py_compile` clean on `einstellungen.py`, manifest bumped to `2026.8.28.17`,
+`audi_ha_test` restarted and confirmed clean via log (single `audi_dashboard` loader warning, no traceback).
+No companion-app change needed - the Setup menu is an HA-panel-only concept (parity rule's own carve-out,
+already established for this exact screen in sections R/AF).
+
+**Correction (2026.8.28.18), owner rejected the two-line design outright.** The invented static per-field
+`beispiel` guesses were simply wrong for some fields ("Reichweite"/"Sofort-Aktualisierung" specifically
+called out) - a hand-picked guess can't be trusted the way the actually-mapped sensor's own derived name can.
+Owner's correction: **only one line**, showing the real, currently-mapped sensor (the existing
+`entitaetIdKurz()` result, dynamic - not a static guess) formatted as `(z. B. REAL_SENSOR_NAME)`; the
+`(z. B. `/`)` wrapper text in the same size/color as the field label on the left (`.setup-feld-label`,
+14.5px/`var(--fg)`), the sensor name itself inside in the same size/color as the existing unit bracket
+(`.setup-feld-einheit`, `[on/off]`/`[km]` - 12.5px/`var(--fg3)`); remove entirely for list fields (Türen/
+Fenster - already excluded from `erwartet` by `feld.liste`, just needed the static example line gated the
+same way, which it wasn't).
+
+Removed the `"beispiel"` catalog data from `einstellungen.py`'s `FELDER` entirely (dead/wrong data, not
+worth keeping around) and the `.setup-feld-hinweise`/`.setup-feld-beispiel` classes. `setupFeldzeile()`
+now renders a single `(z. B. ...)
+` - reusing the existing `.setup-feld-einheit` class for the inner sensor-name span instead of a new
+rule, since the requested style is byte-identical to what that class already provides. `.setup-feld-hinweis`
+itself changed from the old small/grey 12.5px to the label's own 14.5px/`var(--fg)`; `.setup-feld-kopf`
+reverted to `align-items:baseline` (single line again, no more two-line stacking).
+
+Verified live in `audi_ha_test` at `2026.8.28.18` (a fresh page load was required to see it - the panel's
+script URL is version-query-busted and a stale tab keeps the old bundle even after the backend restarts,
+confirmed via `performance.getEntriesByType('resource')` showing `?v=2026.8.28.18` only after navigating
+again, same lesson as sections N/W): read every rendered Setup row's markup and computed styles - "Reichweite"
+now shows the real `vehicle_range_read_from_can`, "Sofort-Aktualisierung" shows the real `refresh_now`
+(both previously wrong static guesses), "Türen"/"Fenster" show no hint at all, and the wrapper/inner-name
+computed styles matched the label (14.5px) and unit-bracket (12.5px, grey) styles exactly, byte for byte.
+`node --check` clean, `py_compile` clean on `einstellungen.py`, `audi_ha_test` restarted and confirmed clean
+via log (single `audi_dashboard` loader warning, no traceback).
+
---
## Working conventions (observed — keep them)
diff --git a/COMPANION_APP_ARCHITECTURE.md b/COMPANION_APP_ARCHITECTURE.md
index f5faec1..8f2a4f2 100644
--- a/COMPANION_APP_ARCHITECTURE.md
+++ b/COMPANION_APP_ARCHITECTURE.md
@@ -302,12 +302,13 @@ never reachable from the public internet. Only a narrow, purpose-built slice of
tunnel from the HAOS host (the OptiPlex) to Cloudflare's edge; no router port ever opens. Confirmed
capable of routing **multiple public hostnames to multiple local ports/services** — so it points at
the reverse-proxy add-on's port below, not at HA's own port 8123.
-2. **Nginx Proxy Manager or Traefik add-on** — a path-scoped reverse proxy sitting between the tunnel
- and HA's internal API, on the Supervisor's internal Docker network. Configured with an **allowlist**
- of only the specific paths the companion app needs (e.g. `/api/states/sensor.audi_*`,
- `/api/services/pyscript/audi_dashboard_*` — exact path list still TBD, depends on the final entity/
- service names once the FMM003 mapping is built). Everything else — `/lovelace`, `/config`, `/auth`,
- the HA frontend itself — is blocked at this layer, never reaching the tunnel at all.
+2. **Nginx Proxy Manager add-on** (decided, 2026-08-28 — see §5 item 3) — a path-scoped reverse proxy
+ sitting between the tunnel and HA's internal API, on the Supervisor's internal Docker network.
+ Configured with an **allowlist** of only the specific paths the companion app needs (`/api/states/
+ sensor.audi_dashboard_*`, `/api/services/audi_dashboard/*`, plus a handful more — the exact,
+ current list lives in `homeassistant/REVERSE_PROXY.md`, not duplicated here since entity/service
+ names can change and this doc would only go stale again). Everything else — `/lovelace`, `/config`,
+ `/auth`, the HA frontend itself — is blocked at this layer, never reaching the tunnel at all.
3. **Auth stays a second, independent layer:** every proxied call still requires the same Bearer LLAT
HA's REST API already demands. Even a path that slipped through the allowlist would be useless
without that token, and any individual LLAT can be revoked from the HA profile if the phone is ever
@@ -333,8 +334,16 @@ surface *is* now internet-facing (even though HA itself never is) — accepted a
Deliberately deferred until hardware arrives / the next architecture review — not blocking anything
else in this document:
-1. **Exact reverse-proxy path allowlist** — depends on the final pyscript entity/service names once the
- FMM003 combined-entity mapping is implemented; write these down here once decided.
+1. ✅ **ERLEDIGT (2026-08-28): reverse-proxy path allowlist defined and current.** Lives in
+ `homeassistant/REVERSE_PROXY.md`, kept in sync with the real entity/service names
+ (`sensor.audi_dashboard_*` / `audi_dashboard.` since the native-integration conversion,
+ `AGENTS.md` §H) rather than duplicated here. The step-by-step setup runbook that applies it in
+ Nginx Proxy Manager is `homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md`. One correction worth
+ recording: the allowlist originally (2026-08-11 planning) assumed the app itself would be served as
+ a web build under `/local/dm360/` — that was never built. Distribution stayed **native/sideload
+ only** (§1), so the allowlist instead needed the app's own OTA surface
+ (`/audi_dashboard_static/app/*`, the `@capgo/capacitor-updater` bundle) rather than a served
+ web app.
2. **No-hardcoding mapping system** for the FMM003 combined entity — discussed in an earlier session,
not yet written to a file or finalized in detail. **Kann erst nach der ersten echten
MQTT-Nachricht fertig werden** (§2b): die Feldnamen im Codec-JSON sind noch unbekannt und werden
@@ -389,9 +398,10 @@ else in this document:
Client-Zertifikat, privater Schlüssel) müssen noch in den FMM003-Configurator hochgeladen werden —
dabei auf die Dateinamen-Endung achten (siehe Anmerkung oben bei „Dateinamen-Endung ist strikt
geprüft").
-3. **Reverse-proxy add-on: Nginx Proxy Manager vs. Traefik** — both viable (§4), pick deferred to the
- next audit pass. Doesn't depend on the FMM003 — could be set up and tested against HA's existing API
- before the hardware arrives, if worth doing ahead of time.
+3. ✅ **ERLEDIGT (2026-08-28): Nginx Proxy Manager**, not Traefik. Both remained viable per §4; NPM was
+ picked and is what `homeassistant/REVERSE_PROXY.md` and `INTERNET_ZUGRIFF_EINRICHTEN.md` are written
+ against. Setup itself (Cloudflare account, nameserver switch, add-on installation) is still the
+ owner's own action to perform — see the runbook for the exact steps.
4. **Cloudflare Tunnel domain** — ✅ **ERLEDIGT: `datametric360.app` ist bei all-inkl registriert**
(Stand 2026-08-11). Eigens dafür angelegt, ohne Webseite und ohne Postfach darauf — genau so, wie
es die unten stehende Einschränkung verlangt. Vorgesehene Adresse der App später:
@@ -407,10 +417,12 @@ else in this document:
setup"), gibt es nur im Business-Tarif für 200 $/Monat. Weil auf dieser Domain nichts liegt außer
diesem Vorhaben, ist die Umstellung folgenlos.
- Noch zu entscheiden (Kleinigkeit, erst bei der Einrichtung): ob die App unter der Domain selbst
- liegt und die abgesicherte Schnittstelle unter einer Unteradresse (etwa `api.datametric360.app`),
- oder umgekehrt. Beides funktioniert; ein getrennter Hostname für die Schnittstelle macht die
- Pfad-Freigabeliste im Reverse Proxy übersichtlicher.
+ ✅ **ERLEDIGT (2026-08-28): ein einziger Hostname genügt**, kein Split nötig. Der ursprüngliche
+ Gedanke (App-Domain vs. API-Subdomain) setzte voraus, dass die App selbst als Web-Build unter einer
+ eigenen Adresse ausgeliefert würde — das wurde nie gebaut (Distribution blieb nativ/sideload, §1).
+ Es gibt nur eine Adresse, die überhaupt gebraucht wird: `https://datametric360.app`, die die App
+ sowohl für API-Aufrufe als auch für ihr eigenes OTA-Bündel verwendet (siehe §5 Punkt 1 oben,
+ `homeassistant/REVERSE_PROXY.md`).
Die Einschränkung, die zur eigenen Domain geführt hat:
diff --git a/homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md b/homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md
new file mode 100644
index 0000000..a3c96fe
--- /dev/null
+++ b/homeassistant/INTERNET_ZUGRIFF_EINRICHTEN.md
@@ -0,0 +1,130 @@
+# Internetzugriff für DataMetric360 einrichten
+
+Schritt-für-Schritt-Anleitung, damit die native Companion-App (iOS-Sideload, siehe
+`../COMPANION_APP_ARCHITECTURE.md` §1) von unterwegs (Mobilfunk, fremdes WLAN) funktioniert - nicht nur
+im Heimnetz/über Tailscale. Home Assistant selbst bleibt dabei durchgehend **lokal, ohne offenen Port**;
+nur ein eng begrenzter, pfad-gefilterter Ausschnitt der API wird über einen Cloudflare-Tunnel erreichbar
+gemacht. Architektur/Begründung: `../COMPANION_APP_ARCHITECTURE.md` §4. Die technische Pfad-Freigabeliste
+selbst, die in Schritt 6 unten eingetragen wird: [`REVERSE_PROXY.md`](REVERSE_PROXY.md).
+
+**Wer was macht:** Kontoerstellung, Domain-/Nameserver-Verwaltung bei Cloudflare bzw. all-inkl und das
+Klicken durch die HA-Add-on-Oberfläche sind ausschließlich Sache des Fahrzeughalters selbst - dafür gibt
+es hier keine Automatisierung, nur die genauen Schritte. Alles, was als Datei/Konfiguration vorbereitet
+werden konnte, ist bereits fertig (`REVERSE_PROXY.md`s Regeln).
+
+**Voraussetzungen, die bereits erfüllt sind** (nichts davon ist hier noch zu tun):
+- `datametric360.app` ist bei all-inkl registriert (siehe `../COMPANION_APP_ARCHITECTURE.md` §5 Punkt 4)
+ - eigens dafür, ohne Webseite/Postfach darauf.
+- Die companion-app spricht bereits ausschließlich REST/WebSocket gegen eine konfigurierbare
+ "Server-Adresse" (kein `hass`-Objekt, kein fest verdrahteter Hostname) - dieser Schritt ändert nur,
+ *welche* Adresse dort später eingetragen wird, nicht die App selbst.
+
+---
+
+## 1. Cloudflare-Konto anlegen, Domain hinzufügen
+
+1. Auf [cloudflare.com](https://cloudflare.com) ein (kostenloses) Konto anlegen.
+2. Im Dashboard *Add a Site* → `datametric360.app` eingeben.
+3. Plan **Free** wählen - reicht vollständig aus (Tunnel und DNS sind im Free-Plan enthalten, nur das
+ "partial/CNAME setup" ohne Nameserver-Wechsel wäre Business-only, siehe Schritt 2).
+4. Cloudflare scannt bestehende DNS-Einträge der Domain und schlägt zwei Nameserver vor (z. B.
+ `xxx.ns.cloudflare.com`, `yyy.ns.cloudflare.com`) - **notieren**, die werden in Schritt 2 gebraucht.
+
+## 2. Nameserver bei all-inkl auf Cloudflare umstellen
+
+**Das ist der eigentliche Knackpunkt** - erst danach kann der Tunnel überhaupt einen Hostnamen
+veröffentlichen.
+
+1. Bei all-inkl (KAS-Verwaltung) einloggen → *Domains* → `datametric360.app` → Nameserver-Einstellungen.
+2. Von all-inkls eigenen Nameservern auf die beiden von Cloudflare vorgeschlagenen (Schritt 1.4)
+ umstellen - das ist ein **"Full setup"**: die komplette DNS-Verwaltung der Domain wandert zu
+ Cloudflare, nicht nur ein einzelner Eintrag. Das ist hier folgenlos, weil auf dieser Domain nichts
+ anderes liegt (keine Webseite, kein Postfach) - siehe die Einschränkung in
+ `../COMPANION_APP_ARCHITECTURE.md` §5 Punkt 4, warum die Domain extra dafür angelegt wurde.
+3. Umstellung kann laut Cloudflare bis zu 24 Stunden dauern (meist deutlich schneller). Status im
+ Cloudflare-Dashboard prüfen - die Domain wechselt dort von "Pending Nameserver Update" auf "Active".
+4. Erst wenn "Active" angezeigt wird, mit Schritt 3 weitermachen (ein Tunnel-Hostname lässt sich vorher
+ zwar anlegen, aber die DNS-Auflösung funktioniert erst danach).
+
+## 3. Cloudflared-Add-on installieren und Tunnel einrichten
+
+1. In Home Assistant: *Einstellungen → Add-ons → Add-on Store* → nach "Cloudflared" suchen, installieren.
+ (Offizielles Home Assistant Community Add-on - nicht die eigenständige `cloudflared`-CLI von Hand
+ einrichten.)
+2. Im Cloudflare-Dashboard: *Zero Trust → Networks → Tunnels* → *Create a tunnel* → Typ "Cloudflared" →
+ Namen vergeben (z. B. `datametric360`).
+3. Cloudflare zeigt einen Tunnel-Token an - diesen im HA-Add-on unter *Konfiguration* eintragen (Feld
+ `tunnel_token` bzw. je nach Add-on-Version über die angezeigte `cloudflared service install`-Zeile;
+ das Add-on übernimmt das Token-Handling, kein manueller `cloudflared`-Aufruf per SSH nötig).
+4. Add-on starten. Im Cloudflare-Dashboard sollte der Tunnel danach als "Healthy"/verbunden angezeigt
+ werden - das bestätigt die **ausgehende** Verbindung vom HAOS-Host zu Cloudflares Edge (kein
+ Router-Port nötig, kein Port-Forward einzurichten).
+5. **Öffentlichen Hostnamen noch nicht auf Home Assistant selbst zeigen lassen** - das kommt erst in
+ Schritt 5, nachdem der Reverse Proxy (Schritt 4) steht. Der Tunnel zeigt am Ende auf den Reverse
+ Proxy, nie direkt auf Port 8123.
+
+## 4. Nginx Proxy Manager installieren und Pfad-Freigabeliste eintragen
+
+1. *Einstellungen → Add-ons → Add-on Store* → "Nginx Proxy Manager" installieren, starten.
+2. Die Weboberfläche des Add-ons öffnen (Standard-Login beim ersten Start: `admin@example.com` /
+ `changeme` - **sofort ändern**, siehe Add-on-Dokumentation).
+3. *Hosts → Proxy Hosts → Add Proxy Host*:
+ - **Domain Names:** `datametric360.app`
+ - **Forward Hostname/IP:** `homeassistant` (interner Name im Supervisor-Docker-Netz;
+ alternativ `localhost`/`homeassistant.local.hass.io`, je nach Add-on-Version - im Zweifel im
+ Add-on-Log nachsehen, mit welchem Namen sich `homeassistant:8123` von dort aus auflösen lässt)
+ - **Forward Port:** `8123`
+ - **SSL:** eigenes Cloudflare-Zertifikat reicht (der Tunnel terminiert TLS bereits an Cloudflares
+ Edge) - hier kann "Force SSL" aktiv bleiben, ein eigenes Let's-Encrypt-Zertifikat ist nicht
+ zwingend nötig, schadet aber auch nicht.
+4. Im Tab **Advanced** den kompletten `nginx`-Codeblock aus [`REVERSE_PROXY.md`](REVERSE_PROXY.md)
+ (Abschnitt "Nginx Proxy Manager") **unverändert** einfügen - das ist die eigentliche
+ Pfad-Freigabeliste: nur `/api/`, `/api/states/sensor.audi_dashboard_*`,
+ `/api/services/audi_dashboard/*`, `/api/services/homeassistant/restart`, `/api/websocket`,
+ `/audi_dashboard_static/app/*` und `/local/bilder/*.{webp,png,jpg,svg}` kommen durch - alles andere
+ (`/lovelace`, `/config`, `/auth/*`, die HA-Oberfläche selbst) liefert `404`.
+5. Speichern.
+
+## 5. Cloudflare-Tunnel auf den Reverse Proxy zeigen lassen
+
+1. Zurück im Cloudflare-Dashboard: *Zero Trust → Networks → Tunnels* → den Tunnel aus Schritt 3 öffnen →
+ *Public Hostname* → *Add a public hostname*.
+2. **Subdomain:** leer lassen (Apex-Domain `datametric360.app` selbst - kein Subdomain-Präfix, siehe
+ "Ein einziger Hostname genügt" in `REVERSE_PROXY.md`).
+3. **Domain:** `datametric360.app`
+4. **Service Type:** `HTTPS` (nicht `HTTP` - Nginx Proxy Manager terminiert selbst wieder TLS).
+5. **URL:** die interne Adresse des Nginx-Proxy-Manager-Add-ons, üblicherweise der Add-on-Hostname im
+ Supervisor-Netz plus dessen konfigurierten Port (im Add-on selbst unter *Info* nachsehen, welcher
+ interne Port/Hostname das ist - **nicht** Port 8123, das wäre HA direkt).
+6. Speichern.
+
+## 6. Prüfen
+
+Von einem Netz **ohne** VPN/Tailscale (z. B. Mobilfunk, WLAN-Tethering vom Handy) die Prüfbefehle aus
+[`REVERSE_PROXY.md`](REVERSE_PROXY.md) (Abschnitt "Nach der Einrichtung prüfen") ausführen. Kurzfassung:
+`GET /api/` und `GET /api/states/sensor.audi_dashboard_profil` (mit gültigem Zugriffstoken) müssen
+funktionieren; `GET /auth/authorize` und `GET /api/config` müssen `404`/`403` liefern; ohne Token muss
+`/api/states/...` mit `401` statt `200` antworten. **Und:** Port 8123 darf von außen gar nicht antworten
+(z. B. über einen externen Port-Checker prüfen, oder `curl` gegen `http://<öffentliche-WAN-IP>:8123`,
+das muss ins Leere laufen).
+
+## 7. Server-Adresse in der App eintragen
+
+Auf jedem Gerät, auf dem die companion-app sideload-installiert ist: beim (erneuten) Einrichten
+`https://datametric360.app` als Server-Adresse eintragen, den bestehenden Zugriffstoken wiederverwenden
+oder einen neuen erzeugen. Ab hier funktionieren sowohl die normalen Datenabrufe als auch
+Fern-OTA-Updates (`AGENTS.md`, Abschnitt zu `@capgo/capacitor-updater`) über denselben Weg - lokal im
+Heimnetz weiterhin genauso wie zuvor, da HA selbst unverändert nur lokal erreichbar bleibt und die App
+ohnehin dieselbe Server-Adresse für beide Fälle verwendet.
+
+## Was danach noch offen bleibt
+
+- Ein Fehlschlag in Schritt 6 zuerst hier prüfen, in dieser Reihenfolge: Ist die Domain in Cloudflare
+ "Active" (Schritt 2)? Zeigt der Tunnel "Healthy" (Schritt 3.4)? Antwortet der Nginx-Proxy-Manager-Host
+ lokal überhaupt (`curl` von der HA-Konsole gegen die in Schritt 5.5 eingetragene interne Adresse)?
+ Erst danach die Pfad-Regeln selbst (Schritt 4) noch einmal gegen `REVERSE_PROXY.md` vergleichen.
+- Ein Gerät verlieren/kompromittiert vermuten: den betroffenen Zugriffstoken im HA-Profil zurückziehen
+ (*Profil → Sicherheit → Zugriffstoken (Long-Lived Access Tokens)*) - das sperrt sofort aus, unabhängig
+ vom Tunnel/Reverse-Proxy.
+- Diese Anleitung deckt nur den Netzwerkweg ab. Für alles rund um Xcode-Signierung/Sideload der App
+ selbst: `../COMPANION_APP_ARCHITECTURE.md` §1 ("Distribution: sideload only").
diff --git a/homeassistant/REVERSE_PROXY.md b/homeassistant/REVERSE_PROXY.md
index 8f72566..4a3584e 100644
--- a/homeassistant/REVERSE_PROXY.md
+++ b/homeassistant/REVERSE_PROXY.md
@@ -15,9 +15,23 @@ Proxy, der ausschließlich die unten aufgeführten Pfade durchlässt.
> Seit der Umstellung auf eine native Integration (2026-08-23, siehe `AGENTS.md` Abschnitt H) heißen
> Entitäten `sensor.audi_dashboard_*` und Dienste `audi_dashboard.` - keine doppelten Präfixe
> mehr. Die Regeln unten sind auf den aktuellen Stand korrigiert und um zwei seither neu hinzugekommene
-> Bedarfe ergänzt (`homeassistant.restart` fürs Selbst-Update, `/local/dm360/*` für die App selbst -
-> beide fehlten in der alten Fassung ersatzlos, ohne sie hätte weder der Update-Neustart-Knopf noch die
-> ausgelieferte App über den Tunnel funktioniert).
+> Bedarfe ergänzt (`homeassistant.restart` fürs Selbst-Update, sowie das OTA-Bündel unter
+> `/audi_dashboard_static/app/*` - beide fehlten in der alten Fassung ersatzlos, ohne sie hätte weder
+> der Update-Neustart-Knopf noch ein OTA-Update über den Tunnel funktioniert).
+>
+> **Zweite Korrektur, selber Tag:** die erste Überarbeitung übernahm noch ungeprüft die alte Annahme
+> aus der frühen Planungsphase (2026-08-11), die App würde als eigener Web-Build unter
+> `/config/www/dm360/` bzw. `/local/dm360/` ausgeliefert. Das wurde nie gebaut - die Distributionsart
+> ist seit der Architekturentscheidung "sideload only" (siehe `COMPANION_APP_ARCHITECTURE.md` §1) eine
+> **native Capacitor-App**, direkt per Xcode aufs Gerät geladen, nicht über HA im Browser aufgerufen.
+> Ein `/local/dm360/*`-Pfad existiert auf dieser Instanz nicht und wird nie gebraucht. Was die native
+> App stattdessen tatsächlich extern erreichen muss, ist ihr eigener Oberflächen-Nachlieferweg (OTA,
+> `@capgo/capacitor-updater`, siehe `AGENTS.md` Abschnitt "Deployed-parity"-Unterabschnitt): das Bündel
+> liegt unter `/audi_dashboard_static/app/bundle.zip`, ausgeliefert von der Integration selbst
+> (`custom_components/audi_dashboard/frontend/app/`, `const.py`'s `BUENDEL_URL`) - dieselbe
+> `basisUrl`, die die App auch für ihre normalen API-Aufrufe verwendet (`ota.ts`'s
+> `buendelAnwenden()`), also derselbe Tunnel-Hostname, kein zweiter. Die Regel unten ist entsprechend
+> ersetzt, nicht nur umbenannt.
## Was die App wirklich braucht
@@ -31,7 +45,7 @@ nicht geschätzt - dort gibt es genau drei REST-Aufrufmuster plus WebSocket:
| `POST` | `/api/services/audi_dashboard/*` | alle schreibenden Vorgänge (Fahrt/Tankvorgang anlegen/ändern, CSV-/Historien-Import, Update prüfen/installieren, ...) |
| `POST` | `/api/services/homeassistant/restart` | genau ein Dienst außerhalb der eigenen Domain - der "Jetzt neu starten"-Knopf nach einem Integrations-Update (siehe `AGENTS.md` Abschnitt L) |
| `GET` (Upgrade) | `/api/websocket` | Live-Aktualisierung |
-| `GET` | `/local/dm360/*` | die ausgelieferte App selbst (`companion-app`, `vite.config.ts`: `base: "./"`, Ziel `/config/www/dm360/`) - ohne diesen Pfad lädt die Seite gar nicht erst |
+| `GET` | `/audi_dashboard_static/app/*` | das OTA-Oberflächen-Bündel (`bundle.zip`) für die native App - ohne diesen Pfad findet ein Update aus der Ferne nicht statt (die App selbst läuft nativ auf dem Gerät, wird nicht über diesen Pfad "geladen") |
| `GET` | `/local/bilder/*.{webp,png,jpg,svg}` | Fahrzeugfotos, die die App anzeigt |
Alles andere wird geblockt, insbesondere `/auth/*`, `/lovelace*`, `/config*`, `/api/config`,
@@ -101,8 +115,9 @@ location = /api/websocket {
include conf.d/include/proxy.conf;
}
-# Die App selbst (companion-app, ausgeliefert unter /config/www/dm360/).
-location ^~ /local/dm360/ {
+# Das OTA-Oberflächen-Bündel für die native App (siehe Hinweis oben - das ist
+# NICHT die App selbst, die läuft nativ; nur ihr nachladbares HTML/CSS/JS).
+location ^~ /audi_dashboard_static/app/ {
limit_except GET { deny all; }
proxy_pass http://homeassistant:8123;
include conf.d/include/proxy.conf;
@@ -121,11 +136,13 @@ location / {
}
```
-Wird die App selbst unter derselben Adresse ausgeliefert (`/local/dm360/` auf demselben Hostnamen wie
-die Schnittstelle), braucht dieser Proxy-Host nur die Blöcke oben - kein separater Host nötig. Ein
-getrennter Hostname für die Schnittstelle (`api.datametric360.app` statt `datametric360.app`) ist
-trotzdem sauberer, siehe `COMPANION_APP_ARCHITECTURE.md` §5 Punkt 4: dann bleibt der `/local/dm360/`-Block
-auf dem App-Host, die übrigen Blöcke auf dem API-Host.
+**Ein einziger Hostname genügt.** Die frühere Überlegung, App und Schnittstelle auf zwei getrennte
+Hostnamen zu legen (`datametric360.app` für die App, `api.datametric360.app` für die Schnittstelle),
+stammte aus der Annahme, die App würde selbst als Web-Build unter einer eigenen Adresse ausgeliefert
+(siehe Korrektur-Hinweis oben) - das entfällt mit der nativen Distribution ersatzlos. Es gibt nur noch
+*eine* Adresse, die überhaupt gebraucht wird: die, die die App beim Einrichten als "Server-Adresse"
+bekommt, und über die sie sowohl die API-Aufrufe als auch das OTA-Bündel erreicht. Ein zweiter Hostname
+wäre zusätzlicher Aufwand ohne Gegenwert.
## Nach der Einrichtung prüfen
@@ -133,13 +150,15 @@ Von einem Netz ohne VPN, etwa über Mobilfunk (Hostnamen unten sind Platzhalter
Einrichtung tatsächlich gewählt wurde):
```bash
-API=https://api.datametric360.app
+API=https://datametric360.app
T=
# muss gehen
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" "$API/api/"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" \
"$API/api/states/sensor.audi_dashboard_profil"
+# kein Token nötig - statische Auslieferung, kein API-Aufruf
+curl -s -o /dev/null -w '%{http_code}\n' "$API/audi_dashboard_static/app/bundle.zip"
# muss 404 oder 403 liefern
curl -s -o /dev/null -w '%{http_code}\n' "$API/auth/authorize"
@@ -160,6 +179,5 @@ geordneten Ablauf - kurz zusammengefasst bleibt vor der ersten echten Nutzung di
- Cloudflare-Konto anlegen, `datametric360.app`-Zone hinzufügen
- Nameserver von `datametric360.app` bei all-inkl auf Cloudflare umstellen („Full setup")
-- Cloudflared- und Nginx-Proxy-Manager-Add-ons installieren, Tunnel + öffentliche Hostnamen einrichten
-- Endgültige Hostnamen festlegen (App-Domain vs. API-Subdomain) und hier eintragen
-- companion-app-Web-Build nach `/config/www/dm360/` ausliefern
+- Cloudflared- und Nginx-Proxy-Manager-Add-ons installieren, Tunnel + öffentlichen Hostnamen einrichten
+- `https://datametric360.app` als Server-Adresse in der App eintragen (jedes Gerät, beim Einrichten)