Commit Graph
127 Commits
Author SHA1 Message Date
nilsandClaude Opus 5.5 4fe29189c0 Pin the pushed server image; drop the pull secret
mg4-server:2.0.0 is on git.stinnesbeck.com and pulls anonymously, so
the Deployment no longer references a registry pull secret. The image
is pinned by digest (sha256:6feea777…) so a re-pushed tag cannot
change what the cluster runs. README and secrets template updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-17 16:14:53 +02:00
nilsandClaude Opus 5.5 e874578e54 Replace the turquoise with MG red tones
ev_cyan becomes a light red (#FF9A9E, for values and secondary
highlights, readable on black) and ev_cyan_deep a deep red (#3A1215,
version badge). Turquoise hardcoded in the battery card, switch track,
input panel, car outline, background glow and charging-graph grid is
replaced with matching red tones, as are two gold leftovers (tab
outline, second background glow). The mint "OK" status colour and the
amber charging curve are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-17 15:45:18 +02:00
nilsandClaude Opus 5.5 5473ab1c2b Dashboards in Grafana; clean up after the Next.js app
- k8s/grafana.yaml: Grafana 13.2.3 with a 1Gi PVC, sign-up and
  anonymous access off. The InfluxDB datasource (URL and token from the
  environment) and three dashboards — Live, Consumption, Charging — are
  provisioned from k8s/grafana/ via kustomize-generated ConfigMaps.
- server.yaml for the Go server (port 8080, UID 65532, smaller limits),
  image tag 2.0.0. The NetworkPolicy now admits Grafana to InfluxDB.
- ingress.yaml: IngressRoutes for car.stinnesbeck.com and Grafana
  (placeholder host car-grafana.stinnesbeck.com) with tls: {}, using
  Traefik's default certificate; the cert-manager Certificate is gone.
- secrets.example.yaml: drops the dashboard secrets, adds grafana-admin.
- AGENTS.md loses its Next.js block; SPECS.md describes the new setup.

The removal of web/ (the Next.js app) and of the stale .next/ cache
upstream had committed already landed in 51e6f6f, which was staged
together with the Go server by mistake.

Checked end to end in podman (Go server + InfluxDB 2.7.12 + Grafana
13.2.3): all 25 dashboard queries return the expected data, and the
dashboards were rendered with headless Chromium. Manifests validated
offline with kustomize and kubeconform. Nothing applied to a cluster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-17 15:18:09 +02:00
nilsandClaude Opus 5.5 51e6f6fd48 Server: rewrite in Go as an API-only service
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
2026-10-17 15:17:42 +02:00
nilsandClaude Opus 5.5 538d03361d Translate the dashboard to English; euro instead of lira
Dashboard: every label, empty state, chart legend and the login page
are English; numbers and dates use en-GB (24-hour clock, day-month
dates), money is EUR, percentages read 80% instead of %80, durations
"2 h 05 min". html lang is en. Server version 1.1.2; manifests pin it.

Car app: the charging tab's price suffix, validation message and total
cost use € instead of ₺.

Checked by rendering all three pages from the 1.1.2 image against
InfluxDB 2.7.12 in podman.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-17 14:39:59 +02:00
nilsandClaude Opus 5.5 818442db08 Rename the web app from FatihsMG4 to Nils' MG4
Browser title, login heading, hidden password-manager username, session
cookie (nilsmg4_session) and package name. The README keeps the credit
to fatihdonmezdev/FatihsMG4. Server version 1.1.1; the manifests pin it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-17 14:34:16 +02:00
nilsandClaude Opus 5.5 e39d9cdcf9 Replace the muted gold accent with MG red
ev_accent is now #E0424C, the red of the launcher icon (about 4.6:1 on
the black background, enough for accent-coloured text; the darker MG
brand red is about 3:1). ev_on_accent and ev_gold_surface follow with a
dark red tint; the gold hardcoded in two drawables and the consumption
period buttons is replaced too. AGENTS.md's visual direction updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-17 14:29:01 +02:00
nilsandClaude Opus 5.5 6145736c97 App 2.3.0: Server tab, live telemetry to the car server
- New Server tab: server URL, ingest token and installation id, with
  Save, Test connection (health, then a read-only token check) and the
  config-file import that upstream removed (app folder / USB stick first,
  document picker as fallback). Settings are kept encrypted and read on
  every use, so the service picks up a change without a restart. Each
  installation generates its own id; the inherited upstream UUID and
  the baked-in CONSUMPTION_API_* build fields are gone.
- Live telemetry restored from before ea8032a (TelemetryPayload,
  UploadCadence, UploadSettings, GPS) and sent in batches to
  POST /v1/telemetry instead of ABRP; the server forwards to ABRP. Adds
  ext_temp, voltage and current. Unsent samples queue in memory (about
  8 h) while offline.
- Upload tab: the live-telemetry switch and cadence controls work again;
  the dead service and autostart switches are removed.
- Manifest: location permissions and foreground service types.
- Unit tests compile again: 114 (stable) / 136 (unstable) pass. Tests
  for the deleted AbrpApi and HeadUnitWeather removed; new tests for
  ServerConfig and TelemetryQueue. Lint passes.

Not yet tried on the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:23:04 +02:00
nilsandClaude Opus 5.5 c253876ed2 Add Kubernetes manifests for car.stinnesbeck.com
Namespace, InfluxDB 2.7.12 StatefulSet with PVC and a NetworkPolicy
that admits only the server, the server Deployment (non-root, read-only
root filesystem), a cert-manager Certificate from letsencrypt-prod and a
Traefik IngressRoute. Secrets are a template only. Nothing is applied;
k8s/README.md has the steps. Server version bumped to 1.1.0.

Validated offline with kustomize and kubeconform (including the
cert-manager and Traefik CRD schemas). The InfluxDB container was tested
locally as UID 1000 with no capabilities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:11:58 +02:00
nilsandClaude Opus 5.5 4ea66cf424 Server: InfluxDB 2 storage, live telemetry endpoint, ABRP forwarder
- MongoDB replaced by InfluxDB 2 (one bucket, installationId tag).
  Consumption is stored per chunk at its recordedAt and summed on read,
  so a chunk the car re-sends from its cache overwrites itself instead
  of being counted twice ($inc did count it twice). Lifetime is a
  running total derived on read.
- POST /v1/telemetry takes batches of samples in ABRP's tlm fields; every
  field is bounded, unknown fields are dropped, unread signals stay absent.
- After a successful write the newest fresh sample is forwarded to ABRP
  with ABRP_API_KEY / ABRP_USER_TOKEN from the server environment; rate
  limited, never queued. The car no longer needs ABRP credentials.
- Self-hosting: standalone output, Dockerfile (tests run in the build,
  runs as UID 10001), .env.example and README for the new setup.
- Dev shell gains Node 22 and InfluxDB 2.

Verified locally against InfluxDB 2.7.12: all endpoints, retry
idempotency, and the dashboard pages. Unit tests: 17 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:09:29 +02:00
nilsandClaude Opus 5.5 d4fb4ba3af Plan the car server (InfluxDB 2, ABRP forwarder, k8s manifests) in SPECS.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:51:07 +02:00
nilsandClaude Opus 5.5 de2428786a Journal diagnostics and upstream ABRP removal in SPECS.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:38:47 +02:00
nilsandClaude Opus 5.5 10029623e3 Remove on-screen diagnostics from the Logs tab
They served their purpose: vehicle values show on the car. This reverts
commit b2a5b32.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:38:36 +02:00
nilsandClaude Opus 5.5 b2a5b32b08 Add on-screen diagnostics to the Logs tab
Without adb there was no way to see why vehicle values stay blank:
refreshVehicle() swallowed every exception. The Logs tab now shows the
package, version, UID (and whether it is the system UID), signer
SHA-256, firmware, car permission grants, the last snapshot and read
error, and the newest 200 entries of the vehicle library's AppLogger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:26:39 +02:00
nilsandClaude Opus 5.5 a707b590f7 Rename package and APKs to NilsMG4, credit Fatih, add SPECS.md
- applicationId com.evsuite.abrp -> com.stinnesbeck.nilsmg4 (the car
  build is .unstable); the Java namespace is unchanged. Installs next to
  upstream's app rather than over it.
- APK names NilsMG4-*.apk, debug builds included; build-car follows.
- OTA install-result action, cache/download names, User-Agent and log
  tags follow the new name.
- About credits fatihdonmezdev / FatihsMG4.
- SPECS.md journals what changed and what is still to do, starting
  with pointing the About link and QR code at git.stinnesbeck.com/nils/mg4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:08:09 +02:00
nilsandClaude Opus 5.5 3da3fa9f49 Rename app to "Nils' MG4"
Changes the launcher label, header brand caption, About title, logo
description and OTA download labels. Package ID, APK file names, log
tags and the repository link are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:02:18 +02:00
nilsandClaude Opus 5.5 3af337ca89 Disable OTA update checks by pointing at an unresolvable host
The updater stays in place, but its releases URL now uses the .invalid
TLD (RFC 2606), so checks fail instead of offering upstream's Turkish
builds over this one. Swap in a fork's releases API to re-enable it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:59:52 +02:00
nilsandClaude Opus 5.5 8e7d8b8627 Add build-car: unstable APK signed with the AOSP platform key
The MG4 head unit's platform certificate (c8a2e9bc…192ab8, per
tools/README.md) is the public AOSP test key. build-car fetches that key
into the gitignored tools/ directory from a pinned android-9.0.0_r1 tag,
builds assembleUnstableDebug, zipaligns and signs it, and refuses to
finish unless both the key's certificate and the resulting signer match
the expected SHA-256. Output lands in build/car/ with a .sha256 sidecar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:51:58 +02:00
nilsandClaude Opus 5.5 9d647f2ce1 Match dev shell hint to the documented build commands
Drops the unit-test hint, since the test sources do not compile upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:49:17 +02:00
nilsandClaude Opus 5.5 bac887bfa6 Add Nix flake dev shell for building on NixOS
Provides JDK 21 and an Android SDK (platforms 36/36.1, build-tools
36.0.0) via `nix develop`. Points AGP at the SDK's aapt2 and Gradle at
the Nix JDK, since the binaries they would otherwise download do not
run on NixOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:46:37 +02:00
nilsandClaude Opus 5.5 864ac612da Translate app UI from Turkish to English
Translates string resources, hardcoded layout text and user-facing
strings in Java sources (toasts, dialogs, status labels, upload log).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:38:00 +02:00
TCFDONMEZandClaude 8f49871e6f fix: handle optional lifetime in consumption page UI
lifetime is now optional in DailyConsumption — guard all .lifetime
accesses. The Ömür boyu card uses the derived total (sum of days).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 23:56:04 +03:00
TCFDONMEZandClaude ed5e38bdb8 fix: lifetime optional in validation + type — app no longer sends it
validateConsumption required lifetime, but the app stopped sending it
(backend now derives it). asObject(undefined) threw 'body is required',
rejecting every chunk. Now lifetime is optional in both the validator
and the DailyConsumption type.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 23:49:57 +03:00
TCFDONMEZandClaude 06265bacc9 simplify: backend derives lifetime from day totals, app stops sending it
- backend daily PUT: no longer $set lifetime (app's local counter is unreliable after APK wipe)
- backend history GET: derives lifetime as sum of all days' totals, returns it at response level
- app: stops sending lifetime in chunks; reads it from history response
- app: refresh button on consumption pane + 'Şimdi yükle' on log pane (manual cache flush)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 21:41:15 +03:00
TCFDONMEZandClaude 8b25799d55 fix: remove day from $setOnInsert — and $setOnInsert conflict on same path
MongoDB error: 'Updating the path day would create a conflict at day'.
On insert, $inc treats a missing field as 0, so no seed needed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 21:30:21 +03:00
TCFDONMEZandClaude 6767293322 fix: day as nested object, not dotted paths
and  touching the same field (day.km) caused MongoDB
to throw a 500, holding all cached chunks back. Seed the whole day
subdocument instead.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 21:09:07 +03:00
TCFDONMEZandClaude e6ac3307ed feat: consumption page date filter — pick a day, see its detail
- HTML <input type=date> in a GET form, zero client JS (same server-component model)
- selected day detail card with tiles + rows above the period summary
- day-by-day list entries link to /consumption?date=YYYY-MM-DD and open+highlight the selected one
- 'Temizle' clears the filter

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 19:18:47 +03:00
TCFDONMEZ ea8032a813 chore 2026-10-09 17:31:08 +03:00
TCFDONMEZandClaude 64811a1508 feat: chunk-based consumption sync + ingest-token history read endpoint
- daily PUT:  →  for incremental 30-min delta chunks (day totals accumulate)
- new GET /v1/consumption/history/[installationId]: ingest-token-authenticated read-back
  so the head-unit app can display cloud history without the dashboard password

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-09 17:28:37 +03:00
TCFDONMEZ 056fc2a5bd Sync today's driving totals every 30 minutes 2026-10-08 16:46:47 +03:00
TCFDONMEZ c06c37761b Mark dashboard demo charging sessions 2026-10-08 15:55:48 +03:00
TCFDONMEZ ae036b9d65 Count driving only and include charging losses 2026-10-08 15:51:38 +03:00
TCFDONMEZandClaude Opus 5 050fe84d6a Release 2.2.37 with cloud sync configured
The published APK carries the Vercel origin and the write-only ingest token.
Verified against the signed artifact: the origin and token are present, and no
MongoDB URI, host or password appears anywhere in the dex.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 14:43:49 +03:00
TCFDONMEZandClaude Opus 5 44bf7ceb3d Let password managers save the dashboard password
A lone password field is usually ignored: managers key a saved entry on
(origin, username), and with no username there is nothing to key on. There is
only ever one account here, so the hidden field carries a fixed value and the
server never reads it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 14:39:46 +03:00
TCFDONMEZandClaude Opus 5 b45874c405 Stop the sign-out link from signing you out on render
Sign-out answered GET behind a <Link>, and Next prefetches links that are in the
viewport. The button sits in every page header, so the browser called it as soon
as a page rendered and cleared the session cookie before the reader had clicked
anything. The first page loaded; the next navigation landed back on the password
screen, which read as the session not persisting.

It is a POST form now, and the route answers POST only — a prefetch of it gets
405 and changes nothing. The rule this broke is the general one: an endpoint
with a side effect must be unreachable by a prefetch, a crawler, or a browser
preloading what it thinks you will click.

Both session cookies are now set on the returned response rather than through
the cookies() store, which a freshly constructed redirect does not reliably
carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 14:35:58 +03:00
TCFDONMEZandClaude Opus 5 84abd75104 Ignore every .env variant but the template
A file named .env.vercel or .env.production would have been committed by the
old two-line list, which is exactly the file someone reaches for when they need
somewhere to paste real credentials.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 14:27:41 +03:00
TCFDONMEZandClaude Opus 5 8242797ece Replace the Express service with a Next.js app on Vercel
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>
2026-10-08 14:21:37 +03:00
TCFDONMEZandClaude Opus 5 e9bfa1217b Meter DC charging only, on a monotonic clock
The charging session meter used the head unit's wall clock, which jumps when it
picks up GPS or NTP time. One forward jump mid-charge invented both kWh and
hours. The consumption counters already integrate on elapsedRealtime for exactly
this reason; the session meter now does too, and keeps wall-clock values only as
the start and end stamps a reader needs to place a session on a timeline.

Intervals longer than five minutes are no longer bridged. A head unit that slept
mid-charge came back and billed the whole gap at the last power reading, and a
session interrupted overnight reported the night as charging time. Reported
duration is metered charging time now, not the span from start to end.

Sessions are metered on DC alone, at or above 10 kW. AC charging on this car is
an overnight affair the head unit sleeps through, so its curve, duration and
energy were guesses either way. The charging screen says so rather than letting
the previous DC session's figures be read as the current AC charge.

Also:

- A daily consumption record uploaded after an offline spell carries the
  lifetime total as it stood at the close of that day. Every backlogged day used
  to be stamped with the current lifetime, which plots as a flat line and one
  cliff.
- The offline session queue is capped at 50. It is a single SharedPreferences
  string read into memory in full, and nothing drained it while the cloud
  endpoint was unconfigured.
- A long charge keeps the start of its curve: at 720 points the sample interval
  doubles and the curve is thinned, rather than dropping the oldest point.
- The cloud sync throttle runs on the monotonic clock, and catch-up sends up to
  10 days and 10 sessions per cycle rather than one and three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 14:20:29 +03:00
TCFDONMEZ 1a37e52eda Track charging sessions and daily consumption history 2026-10-08 13:39:40 +03:00
TCFDONMEZ 9a6c121d09 Rename app and persist all-time consumption to MongoDB 2026-10-08 13:23:00 +03:00
TCFDONMEZ 7366ba0480 build: use repo-local MG4 signing tools 2026-10-05 20:24:06 +03:00
TCFDONMEZ 90982580af Allow disabling ABRP uploads independently 2026-10-05 00:28:28 +03:00
TCFDONMEZ dbbfb9f803 Fix consumption tracking while charging 2026-10-02 11:15:22 +03:00
TCFDONMEZ ecbee4980c Match DriveHub lifetime consumption calculation 2026-09-30 23:24:15 +03:00
TCFDONMEZ 74723335e1 Remove automatic consumption reset migration 2026-09-30 23:10:03 +03:00
TCFDONMEZ eb40b3d31d Use vehicle consumption counter for energy 2026-09-30 23:06:27 +03:00
TCFDONMEZ d25c09ec8f Correct SWI69 distance scale 2026-09-30 13:46:51 +03:00
TCFDONMEZ c44722bf12 Retain four months of selectable daily consumption 2026-09-30 12:12:25 +03:00
TCFDONMEZ 1709fdf1ac Track rolling 7 and 30 day consumption 2026-09-30 12:08:54 +03:00
TCFDONMEZ 81483e7f48 Add weekly and monthly consumption periods 2026-09-30 12:04:40 +03:00