Code review fixes: app 2.9.0, server 2.2.0

Telemetry no longer loses queued batches. The server drops a bad value,
or a sample without a plausible time, instead of rejecting the batch,
and answers 200 with what it dropped. The app sends only values inside
the server's ranges (speed as a magnitude, so reversing is fine) and no
samples before the head unit's clock is set.

Server:
- Pairing hands out and accepts a token only after the device is saved;
  a failed save leaves the request pending.
- A paired token serves only the installation id it was paired with
  (403 otherwise); the shared INGEST_TOKEN still serves any.
- Wrong-password lockouts and open pairing requests are limited per
  client address (last X-Forwarded-For entry), so a stranger cannot
  lock the owner out; a car starting again replaces its own request.
- ABRP forwarding skips samples stamped ahead of the server's clock,
  which used to stop it until a restart.

App:
- Settings written to plaintext during a keystore failure move back
  into encrypted storage; the stored installation id wins.
- The installation id is derived from ANDROID_ID on first start, so a
  reinstall finds its history; a stored or imported id still wins.
- The scanned config file is deleted after import (it holds the token).
- GPS stops while live telemetry is off or the server unconfigured, and
  pauses while parked and idle, keeping the fix the car parked with.
- Range estimates use the monotonic clock.
- One HTTP helper (ServerHttp) for telemetry, sync and pairing.
- Last Turkish strings translated; mise logs shows the NilsMG4 tags.

Grafana reads InfluxDB with its own read-only token (new grafana-influx
Secret, created by hand as described in k8s/README.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 00:36:19 +02:00
co-authored by Claude Opus 5.5
parent 9bebf56b3a
commit a5db5604d3
35 changed files with 962 additions and 323 deletions
+13 -7
View File
@@ -31,18 +31,22 @@ The ABRP credentials never reach the car.
## Endpoints
The ingest endpoints accept `Authorization: Bearer <token>` with either the shared `INGEST_TOKEN`
or a paired car's own token.
or a paired car's own token. A paired token only works for the installation id it was paired
with (403 otherwise); the shared token works for any.
- `POST /v1/telemetry` — `{ installationId, samples: [...] }`, up to 500 samples in ABRP's `tlm`
field names and units (`utc` in epoch seconds; flags as 0/1 or booleans). Unknown fields are
dropped, known ones bounded. Stored, then the newest sample is forwarded to ABRP if it is under
5 minutes old.
dropped, known ones bounded: a value out of range is dropped on its own, and a sample without
a plausible time (before 2001, or more than an hour ahead) is dropped whole. The rest is stored
and the answer is `200 {stored, droppedCount, dropped}`, even when nothing was usable, since
the car would only send it again. The newest sample is forwarded to ABRP if it is under
5 minutes old and not ahead of the server's clock.
- `PUT /v1/consumption/daily` — one delta chunk of a day's consumption.
- `PUT /v1/charging-sessions/{sessionId}` — a finished DC session with its curve.
- `GET /v1/consumption/history/{installationId}` — the car reads its own totals back.
- `GET /v1/health` — `{"status":"ok"}`.
A bad request is a 400 naming the field; a database failure is a 500 without detail.
A malformed request is a 400 naming the field; a database failure is a 500 without detail.
## Pairing a car by QR code
@@ -52,13 +56,15 @@ on only when `APPROVAL_PASSWORD` is set.
1. The car calls `POST /v1/pair/start` `{installationId}` and shows the returned
`verificationUriComplete` as a QR code, plus the short `userCode` (e.g. `K7F-3QX`).
2. The phone opens `/pair?code=…`, the owner checks the code and approves with the approval
password. Five wrong passwords lock approvals for a minute.
password. Five wrong passwords from one address lock that address out for a minute; other
addresses are unaffected (the address is the last `X-Forwarded-For` entry, which Traefik sets).
An address may hold three open requests; a car starting again replaces its own.
3. The car polls `POST /v1/pair/poll` `{deviceCode}`: 202 pending, 200 with its own token
(handed out once), 403 denied, 410 expired (after 10 minutes) or unknown.
Each car gets its own 43-character token; only its SHA-256 is stored (measurement `device` in
the same bucket), and the device is persisted before the car can collect the token, so a
restart never loses a paired car. Manage cars with the approval password:
the same bucket), and the token is only handed out, and only accepted, once the device is
persisted, so a restart never loses a paired car. Manage cars with the approval password:
```sh
curl -H "Authorization: Bearer $APPROVAL_PASSWORD" https://car.k8s.stinnesbeck.com/v1/devices