- 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>
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>
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>
- 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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>