diff --git a/README.md b/README.md
index d723b58..6b1084f 100644
--- a/README.md
+++ b/README.md
@@ -1,261 +1,214 @@
-# FatihsMG4
+# Nils' MG4
-

+An Android Automotive app for the **MG4 (SAIC eh32)** head unit, plus a small server: the car
+sends its telemetry to my own server at car.k8s.stinnesbeck.com, which stores it in InfluxDB 2,
+forwards live data to [A Better Route Planner](https://abetterrouteplanner.com) and feeds the
+Grafana dashboards.
-[](https://github.com/fatihdonmezdev/FatihsMG4/releases)
-[](LICENSE)
+No OBD dongle and no phone in the loop: the app runs on the head unit and reads the car's own
+APIs.
-Sends live telemetry from an **MG4 (SAIC eh32)** to
-[A Better Route Planner](https://abetterrouteplanner.com) — battery level, speed, range,
-temperature, charging state and position — straight from the car's own APIs.
-
-No OBD dongle. No Home Assistant. No phone in the loop. The app runs on the car's head
-unit and talks to ABRP directly.
+- Repository: https://git.stinnesbeck.com/nils/mg4
+- Fork of [fatihdonmezdev/FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4), itself built on malys' EVABRPUploader and Leon Kernan's `ABRP_Uploader`.
+- What changed and what is still to do: [`SPECS.md`](SPECS.md).
> ⚠️ **No warranty, no liability.** This software is provided "as is" and runs on a
-> **vehicle**. Installing it is your decision and your risk — see
+> **vehicle**. Installing it is your decision and your risk; see
> [`DISCLAIMER.md`](DISCLAIMER.md). Not affiliated with SAIC, MG Motor or Iternio.
-> MG and MG4 are third-party marks used only to identify compatibility; no official origin
-> or approval is claimed.
+> MG and MG4 are third-party marks used only to identify compatibility.
-> **Fork notice.** This is a fork of Leon Kernan's `ABRP_Uploader`, substantially
-> reworked. **Read [`LICENSE.md`](LICENSE.md) before redistributing anything** — the
-> upstream project carries no licence, which limits what may legally be published.
+> **Licence.** The original `ABRP_Uploader` carries no licence, which limits what may legally
+> be published. Read [`LICENSE.md`](LICENSE.md) before redistributing anything.
---
## Contents
-- [Screenshots](#screenshots)
-- [Overview](#overview)
-- [Install](#install)
-- [Configuration](#configuration)
+- [Screenshot](#screenshot)
+- [How it fits together](#how-it-fits-together)
+- [The app](#the-app)
+- [Install on the car](#install-on-the-car)
+- [Setup](#setup)
- [Building](#building)
+- [Server and dashboards](#server-and-dashboards)
- [Project layout](#project-layout)
- [Project documents](#project-documents)
-- [Security](#security)
-- [Contributing](#contributing)
-- [Legal](#legal)
- [Credits](#credits)
-## Screenshots
+## Screenshot
-Version 2.2.6 introduces an OLED-black interface with charcoal cards, muted gold and
-cyan accents, a vehicle overview, and responsive side or bottom navigation. This
-app-specific theme intentionally overrides the shared EVSuite visual tokens.
-Tablet UI previews at 1280×720; vehicle readings are unavailable on this device.
+Version 2.8.2 on the head unit's 1920x720 screen, in full screen (Android 9 emulator; no
+vehicle there, so every value shows as a dash).
-
-
+
----
+## How it fits together
-## Overview
-| Signal | Source | Firmwares |
-|---|---|---|
-| State of charge | `EVHardware` vendor charging service, with a SWI68 property fallback | all supported (fallback SWI68 only) |
-| Range | `EVHardware` vendor charging service, standard AAOS property, then SWI68 fallback | all supported |
-| Speed, parked state | `EVHardware` shared energy snapshot | all supported |
-| Outside temperature | The head unit's weather broadcast, falling back to its map-service query; the vehicle sensor first where one exists | all supported (the vehicle sensor is absent on MG4) |
-| Charging, DC fast charging | Charge port state + charge rate heuristic | where the VHAL implements them |
-| Charged energy this session | `EVHardware` charging-session integration | where the charge rate is readable |
-| State of energy, pack capacity, pack temperature | `EVHardware` typed standard-AAOS reads | where the VHAL implements them |
-| Odometer, tyre pressures | Standard AAOS properties (privileged) | platform-signed installs only |
-| Cabin temperature | `EVHardware` typed HVAC read | where the VHAL implements it |
-| Climate setpoint | `EVHardware` shared energy snapshot | all supported |
-| Position, elevation, heading | GPS | — |
+```
+car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go) ──▶ ABRP
+ ──daily chunks─────▶ │
+ ──charging sessions▶ ▼
+ InfluxDB 2 ◀── Grafana
+```
-All vehicle telemetry goes through **EVHardware**, which detects the generation
-(SWI68/69/131/132/133/165), owns property ids and units, and applies the shared nullable
-fallback order. `CarPropertyAdapter` remains only as transport for the debug-only VHAL probe
-and is never instantiated by the production service. See [`FIRMWARE.md`](FIRMWARE.md).
+The ABRP credentials live only on the server, never in the APK. The car authenticates with a
+token of its own, obtained by pairing with a phone (below).
-Odometer and tyre pressures sit behind privileged AAOS permissions: a platform-signed
-install sends them, any other install simply omits them.
+## The app
-A property the car will not give up is **omitted** from the payload, never sent as zero —
-so ABRP is never told the battery is empty because a read failed. Position is held to the
-same rule in time as well as in value: a GPS fix older than `max(5 min, 3 x your upload
-interval)` is dropped rather than resent, so a stale fix cannot report the car back at the
-place it set off from.
+| Tab | What it shows |
+|---|---|
+| **Overview** | Battery, range, range estimates at current power and at the 5-minute average, power, speed; odometer, climate setpoint and tyre pressures |
+| **Consumption** | Daily and period consumption, efficiency |
+| **Charging** | Charging sessions and curves, cost in euro |
+| **Upload** | Live telemetry on/off, full screen, voice key, upload cadence |
+| **Server** | Server URL, pairing by QR code, ingest token, installation ID, config import |
+| **Wi-Fi** | Joins a network the head unit already knows (e.g. the phone hotspot) |
+| **Logs** | Upload log and the last steering-wheel key reports |
-Telemetry only reads the car. Version 2.2.5 removes window automation entirely; there
-is no window service, door listener or window command path in the app.
+- **Opening the app:** the MG4 launcher does not list third-party apps. **Double-press the
+ voice key** on the steering wheel and the app comes to the front (switch on the Upload tab,
+ on by default).
+- **Full screen:** hides SAIC's top and bottom bars while the app is open; a swipe from the
+ edge shows them briefly (Upload tab, on by default).
+- **Live telemetry:** every 15 s while driving or charging, much less often while parked. A
+ value the car cannot read is left out, never sent as zero, so ABRP is never told the battery
+ is empty because a read failed.
+- **Read-only:** the app never writes a vehicle setting.
-## Install
-The MG4 head unit hides Settings and APK install. The known route in:
-The MG4 head unit has no visible way to open Settings or install an APK. The known route
-in (via the on-screen keyboard) is:
+Signals the MG4 does not publish (battery, cabin and outside temperature, pack energy and
+capacity) are listed in [`SPECS.md`](SPECS.md#signals-the-car-does-not-provide).
-1. Open any app with a text field and tap it to raise the on-screen keyboard — e.g. the
- Amazon Music app's email/login field.
-2. **Long-press** the comma `,` (or the `@`) key on the keyboard.
-3. Tap **"Language settings"**.
-4. Tap the **search** icon in the top bar and type **`backup`**. It opens an empty page —
- now press the **back** arrow, and you land in Android's Settings panel.
-5. Enable **Developer options**, and turn on **"Install unknown apps"** (unknown sources).
-6. In Settings, search **`storage`** — you now have access to internal storage and the USB
- key. Navigate to the APK and tap it to install.
+## Install on the car
-> ⚠️ You are enabling developer options and sideloading on a car. Do this **parked**, and
-> only with an APK you trust. See [DISCLAIMER.md](DISCLAIMER.md).
+The car build (`NilsMG4-unstable-.apk`) runs with the system UID and is signed with the
+AOSP test platform key that the MG4 head unit uses. It installs next to the old FatihsMG4 app
+(package `com.stinnesbeck.nilsmg4.unstable`), not over it.
-Two channels. Pick one — they install side by side.
+Without adb, through the head unit's hidden file manager:
-| Channel | Auto-update | Use it if |
-|---|---|---|
-| **Stable** | No. Contains no updater at all. | You want the car to run what you put on it |
-| **Unstable** | Yes, from GitHub pre-releases | You are testing and want fixes as they land |
+1. Copy the APK to a USB stick and plug it in.
+2. Open **Bluetooth Settings** and select the car name.
+3. Long-press the **comma** key until the keyboard settings appear.
+4. **Android Keyboard Settings → Languages**, then search for `file` in the box at the top.
+5. That opens the Files app: open the USB stick, tap the APK and install.
-Grab the APK from [Releases](https://github.com/fatihdonmezdev/FatihsMG4/releases). Stable builds are the tagged ones; unstable builds
-are marked pre-release.
+With adb:
-### Getting the APK onto the car
+```sh
+adb push build/car/NilsMG4-unstable-.apk /data/local/tmp/update.apk
+adb shell "pm install -r -f -d -t /data/local/tmp/update.apk"
+```
-The MG4 has no visible file manager. To reach it:
+> ⚠️ Do this **parked**, and only with an APK you trust. See [DISCLAIMER.md](DISCLAIMER.md).
-1. Open **Bluetooth Settings** and select the car name.
-2. Long-press the **comma** key until the keyboard settings appear.
-3. **Android Keyboard Settings → Languages**.
-4. Search for `file` in the search box at the top.
-5. That gives you the Files app.
+Self-updating (OTA) is switched off: the updater points at an unresolvable host until it is
+pointed at this repository's releases (see `SPECS.md`).
-From there, open your USB stick and tap the APK.
+## Setup
-### First run
-
-1. Open the app.
-2. By default, the app prefills the open-source telemetry API key published by the
- [SAIC Python MQTT Gateway](https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway#abrp-api-integration).
- You can replace it with your own key. Get a token from your ABRP account.
-3. Paste the token, press **Test** (read-only — it does not send telemetry), then **Save**.
-4. Turn the switch on. The service restarts with the car from then on.
-
-Typing a long API key and token on the car's on-screen keyboard is painful. Instead you can
-put them in a text file and tap **Import file** — see [Config file](#config-file) below.
-
-## Configuration
-
-| Setting | Default | Notes |
-|---|---|---|
-| Upload frequency | 60 s | 15 / 30 / 60 / 120 / 300 s. Applies while driving |
-| More often at low battery | On | Tightens to 15 s below the threshold |
-| Low battery threshold | 20 % | Editable, 1–99 |
-| Service on/off | Off | Also the "stop uploading" control |
-
-Parked and unplugged, the app drops to **one upload every 15 minutes** on its own,
-whatever the setting. A state change — parked, plugged in, unplugged — always uploads
-immediately.
+1. Open the app (voice key double press, or "Open" right after installing).
+2. On the **Server** tab the URL is already `https://car.k8s.stinnesbeck.com`.
+3. Tap **Pair with phone**. Scan the QR code with a phone (or open the address shown and enter
+ the code), and approve with the server's approval password. The car receives its token.
+4. On the **Upload** tab, turn live telemetry on. The service starts with the car from then on.
### Config file
-To avoid typing on the head unit, put your settings in a plain text file and tap **Import file**
-on the **ABRP** tab. The app fills in whatever the file contains. Nothing is sent — press
-**Test**, then **Save** as usual.
-
-The head unit has no file picker, so the file is not chosen by hand: copy it from a PC into
+Instead of typing, put the settings in a text file and tap **Import file** on the Server tab.
+The head unit has no file picker, so copy the file to
```
-Android/data/com.evsuite.abrp/files/
+Android/data/com.stinnesbeck.nilsmg4.unstable/files/
```
-on the USB stick (or on the head unit's internal storage), then tap **Import file**. The app
-scans that folder on every mounted volume and takes the first file that parses — the name does
-not matter. If nothing is found it shows the exact path to copy to. On a phone or tablet, where
-a document picker exists, the button falls back to letting you pick any file.
-
-The file is a `key = value` list, one per line. Blank lines and lines starting with `#` are
-ignored, keys are case-insensitive, and any key you leave out keeps its current value:
+on the USB stick or the internal storage. The app scans that folder on every mounted volume and
+takes the first file that parses. Format, one `key = value` per line, `#` for comments; keys left
+out keep their current value:
```
-# ABRP Uploader config
-api_key = your-abrp-api-key
-token = your-abrp-user-token
-
-# optional — the cadence controls, same values as the UI
-interval_sec = 60 # snapped to 15 / 30 / 60 / 120 / 300
+server_url = https://car.k8s.stinnesbeck.com
+token = the server's INGEST_TOKEN (not needed after pairing)
+installation_id = keep the old one when reinstalling, so history stays together
+interval_sec = 60 # 15 / 30 / 60 / 120 / 300
boost_low_soc = true
-low_soc_percent = 20 # clamped to 1–99
+low_soc_percent = 20
```
-Only `api_key` and `token` are usually needed. The file is read once on import and not kept —
-your credentials live only in the app's encrypted store afterwards.
+The file is read once and not kept; credentials live only in the app's encrypted store.
### Power
-The app holds **no wake lock** and schedules **no alarms**, so it cannot wake a sleeping
-head unit; it costs nothing while the car is off. While the car is on, both the upload tick
-and the GPS subscription cost power, and the GPS interval follows your configured
-frequency.
-
-At defaults that is roughly **59 uploads/hour driving** and **3/hour parked**.
+The app holds **no wake lock** and schedules **no alarms**, so it cannot wake a sleeping head
+unit and costs nothing while the car is off.
## Building
-Requires JDK 17 and the Android SDK. With [mise](https://mise.jdev):
-```bash
-mise install # JDK
-mise run bootstrap # Android SDK + local.properties
-mise run check # permission gate + lint + tests
-mise run build # debug APK
+With Nix:
+
+```sh
+nix develop # JDK 21, Android SDK, Go
+./gradlew assembleStableDebug # debug APK for a phone or tablet
+build-car # car APK, signed with the AOSP test platform key
```
-Without mise: set `JAVA_HOME` to a JDK 17, write `sdk.dir` into `local.properties`, then
-`./gradlew assembleStableDebug`.
+`build-car` writes `build/car/NilsMG4-unstable-.apk` and a `.sha256` next to it, and
+checks the signing certificate before it finishes.
-Release builds are signed from environment variables. These sign the **APK** and have
-nothing to do with your ABRP API key, which you type into the app itself:
+Tests and lint:
-| Variable | What it is |
-|---|---|
-| `SIGNING_KEYSTORE` | Path to the keystore file (CI decodes `SIGNING_KEYSTORE_BASE64` into one) |
-| `SIGNING_STORE_PASSWORD` | Opens the keystore container |
-| `SIGNING_KEY_ALIAS` | Which key inside it to sign with (default `platform`) |
-| `SIGNING_KEY_PASSWORD` | Opens that particular key — a keystore protects each key separately |
+```sh
+./gradlew testStableDebugUnitTest testUnstableDebugUnitTest lintStableDebug
+```
-**Never commit a keystore or a password.** Put them in `mise.local.toml`, which is
-gitignored, or in GitHub Actions secrets.
+[`mise`](https://mise.jdx.dev) tasks from upstream (`mise run check`, `mise run build`) still
+work for the plain Gradle builds.
+
+## Server and dashboards
+
+- **Server** ([`server/`](server/README.md)): Go, API only. Live telemetry, daily consumption
+ chunks, charging sessions, pairing; forwards live data to ABRP. Image at
+ `git.stinnesbeck.com/nils/mg4-server`.
+- **Kubernetes** ([`k8s/`](k8s/README.md)): plain manifests for the server, InfluxDB 2 and
+ Grafana, applied by hand with `kubectl apply -k k8s/`. Secrets go in the gitignored
+ `k8s/secrets/secrets.yaml`, from the template next to it.
+- **Grafana dashboards** (`k8s/grafana/dashboards/`): Live, Consumption and Charging, provisioned
+ with the InfluxDB datasource.
## Project layout
+
```
-app/src/main upload service, ABRP payload and UI
-EVHardware shared vehicle reads, energy snapshot and integration
-app/src/stable no-op update hook — the stable channel cannot self-update
-app/src/unstable audited updater policy; runtime trigger currently suspended
-app/src/debug VHAL probe tools, absent from every release build
+app/src/main app: upload service, server client, pairing, UI
+app/src/stable no-op update hook (no updater in the APK)
+app/src/unstable car build: system UID, updater (currently pointed nowhere)
+app/src/debug VHAL probe tools, absent from release builds
app/src/test JVM unit tests
+EVHardware shared vehicle reads per firmware generation
+server/ Go server
+k8s/ Kubernetes manifests and Grafana provisioning
+flake.nix dev shell and build-car
```
## Project documents
+
| Document | What it covers |
|---|---|
-| [DESIGN.md](DESIGN.md) | The EVSuite design system — colour, type, touch targets, icons |
+| [SPECS.md](SPECS.md) | Journal of this fork: architecture, status, to-dos, done log |
+| [FIRMWARE.md](FIRMWARE.md) | MG4 firmware generations and how the app reads each |
| [AGENTS.md](AGENTS.md) | Context for AI agents working in this repository |
-| [CONTRIBUTING.md](CONTRIBUTING.md) | How to build, test and submit a change |
-| [SECURITY.md](SECURITY.md) | Threat model and vulnerability disclosure |
-| [DISCLAIMER.md](DISCLAIMER.md) | Vehicle-safety disclaimer — read before installing |
-| [CHANGELOG.md](CHANGELOG.md) | Release history |
-| [LICENSE.md](LICENSE.md) | Licence text |
-
-## Security
-See [SECURITY.md](SECURITY.md) for the threat model and how to report a vulnerability
-privately.
-
-## Contributing
-See [`CONTRIBUTING.md`](CONTRIBUTING.md). Short version: this code runs on a moving
-vehicle, so changes need tests and a clear account of what you verified on a car and what
-you did not.
-
-## Legal
-- [`DISCLAIMER.md`](DISCLAIMER.md) — no warranty, no liability, not affiliated with the
- carmaker or with ABRP.
-- [`LICENSE.md`](LICENSE.md) — unresolved licence status inherited from the fork source.
- Read it before redistributing.
-- [`SECURITY.md`](SECURITY.md) — how to report a vulnerability privately.
+| [DESIGN.md](DESIGN.md) | The EVSuite design system (the app overrides its palette) |
+| [SECURITY.md](SECURITY.md) | Threat model |
+| [DISCLAIMER.md](DISCLAIMER.md) | Vehicle-safety disclaimer; read before installing |
+| [CHANGELOG.md](CHANGELOG.md) | Upstream release history |
+| [LICENSE.md](LICENSE.md) | Licence status |
## Credits
-- Original app: **Leon Kernan** — the car-API approach and the first working uploader.
+
+- **fatihdonmezdev**: [FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4), the app this fork
+ starts from.
+- **malys**: EVABRPUploader.
+- **Leon Kernan**: the original `ABRP_Uploader`, the car-API approach and the first working
+ uploader.
- ABRP telemetry API: [Iternio](https://documenter.getpostman.com/view/7396339/SWTK5a8w).
-- Sibling project: **EVProfile**, whose security and CI patterns this repo reuses.
diff --git a/screenshots/nilsmg4-2.8.2-overview.png b/screenshots/nilsmg4-2.8.2-overview.png
new file mode 100644
index 0000000..1bfb9bd
Binary files /dev/null and b/screenshots/nilsmg4-2.8.2-overview.png differ