Commit Graph
16 Commits
Author SHA1 Message Date
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 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
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
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 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 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