ABRP keeps getting live data while the car server is down: with an ABRP
API key and user token set in the app (Live data tab, or abrp_api_key /
abrp_token in the config file), the car sends the newest sample to ABRP
itself at every upload. It tells the server the newest sample ABRP took
(abrpSentUtc), and the server forwards only newer ones, so ABRP gets
each sample once. Without credentials in the app the server forwards
everything, as before. The credentials sit in encrypted storage, go in
the request body, and neither they nor ABRP's answer are logged.
Settings are grouped by what they are for:
- Live data (was Upload): on/off, ABRP credentials, upload frequency.
- Settings (replaces the Wi-Fi tab): full screen and voice key,
usable capacity and manual SoH (were on Consumption), charging price
(was on Charging), Wi-Fi.
Charging and Live data get icons of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
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
The server's host is car.k8s.stinnesbeck.com, not car.stinnesbeck.com:
IngressRoute, PUBLIC_URL (so the pairing QR code points there), the
app's pre-filled server URL and hint, and the docs. Grafana's
placeholder host follows to car-grafana.k8s.stinnesbeck.com, so one
*.k8s.stinnesbeck.com default certificate covers both. App 2.4.1 for the
new default; the server image is unchanged (its URL comes from the
manifest).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Modelled on the OAuth device authorization grant (RFC 8628) and on
only when APPROVAL_PASSWORD is set (at least 12 characters):
- POST /v1/pair/start opens a request for an installation and returns
a short user code and the /pair URL for the car's QR code;
POST /v1/pair/poll returns pending, the car's own token (once),
denied or expired. Requests live 10 minutes, in memory.
- GET/POST /pair is a script-free approval page for the phone
(strict CSP, no framing). Five wrong passwords lock approvals for a
minute.
- Each car gets its own 43-character token; only its SHA-256 is stored
(measurement "device"), and the device is persisted before the car
can collect the token. Tokens are loaded at startup, so a restart
keeps paired cars working; the server refuses to start if they cannot
be loaded.
- GET /v1/devices and POST /v1/devices/{id}/revoke, authenticated with
the approval password, list and revoke cars.
- The shared INGEST_TOKEN keeps working.
31 tests (race detector clean), govulncheck clean. Tested end to end
against InfluxDB 2.7.12: pairing, wrong password, single collection,
persistence across restarts, revocation (also across a restart), and
that InfluxDB holds only the hash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Same endpoints, request formats, validation rules and InfluxDB layout as
the Next.js server, so the car needs no change: POST /v1/telemetry,
PUT /v1/consumption/daily, PUT /v1/charging-sessions/{id}, GET
/v1/consumption/history/{id}, GET /v1/health. The dashboard, login and
the /v1/history reads are gone; Grafana takes over the dashboards.
- Go 1.26 standard library (net/http routing, log/slog) plus the
official InfluxDB 2 client. Fails closed at startup on a missing
InfluxDB setting or an ingest token under 24 characters.
- ABRP forwarder ported: newest fresh sample, rate limited, never
queued, credentials only in the form body.
- 22 tests: validation, aggregation, forwarding, config, and the
handlers through a fake store. govulncheck clean (x/net 0.60.0,
toolchain go1.26.9).
- Image: static binary on distroless, about 11 MB, UID 65532, port
8080; the build runs vet and the tests.
- Dev shell: Go, gopls and govulncheck instead of Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The standalone service in server/ was never deployed anywhere, and keeping it
alongside a Vercel app would mean maintaining two implementations of the same
validation rules. It is deleted; web/ is the only ingest path now. Its routes
live at /v1/... rather than under /api, so the Android client's URL building is
unchanged and CONSUMPTION_API_URL stays a bare origin.
Credentials are split in two. INGEST_TOKEN is baked into the published APK and
must be assumed extractable — anyone can unzip an APK and read its strings — so
it opens the PUT endpoints and nothing else. Reading the history needs the
dashboard password, which is never compiled into anything and reaches the
browser as an HMAC-signed httpOnly cookie. The MongoDB URI stays a server-side
environment variable; the car has never held one and still does not.
The dashboard is a phone-shaped reader for the same data: totals for energy
taken and paid, a daily energy chart, an efficiency trend, and every DC session
with its power/SOC curve. It reuses the head unit's OLED palette so the two read
as one app. Every page is a server component and the charts are hand-rolled SVG,
so the whole thing renders without shipping client JavaScript.
MongoClient is cached on globalThis: a fresh client per request means a fresh
pool per request, and Atlas starts refusing connections long before the traffic
would justify it. Index creation moved to the first write of each warm lambda,
since a serverless deployment has no boot step to hang it on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>