Files
mg4/web/README.md
T
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

2.5 KiB

FatihsMG4 web

Next.js app on Vercel: the ingest API the car writes to, and a phone-shaped dashboard for reading the history back. Replaces the standalone Express service that used to live in server/.

The car never connects to MongoDB and never holds a MongoDB URI. The URI is a Vercel environment variable and is only ever read by a server-side route.

Two credentials, on purpose

Who holds it What it opens
INGEST_TOKEN the car, in the APK the PUT endpoints — write only
DASHBOARD_PASSWORD you, in a browser the dashboard and the GET history

The APK is published, so its token must be assumed extractable: anyone can unzip an APK and read its strings. That is survivable here because the write token cannot read anything back. The password is never compiled into anything.

Environment variables

Set all five in Vercel → Settings → Environment Variables, for Production and Preview.

Name Notes
MONGODB_URI Atlas connection string
MONGODB_DATABASE defaults to fatihsmg4
INGEST_TOKEN openssl rand -hex 32
DASHBOARD_PASSWORD at least 8 characters, or login is refused outright
SESSION_SECRET openssl rand -hex 32; rotating it signs every browser out

Atlas network access must allow Vercel's egress, which is not a fixed address on the Hobby plan — either allow 0.0.0.0/0 and rely on the database user's credentials, or use Atlas private networking on a paid plan.

Endpoints

Write, Authorization: Bearer <INGEST_TOKEN>:

  • PUT /v1/consumption/daily
  • PUT /v1/charging-sessions/:sessionId

Read, session cookie or Authorization: Bearer <DASHBOARD_PASSWORD>:

  • GET /v1/history/:installationId/consumption
  • GET /v1/history/:installationId/charging

GET /v1/health is unauthenticated and returns {"status":"ok"}.

The car builds its URLs as CONSUMPTION_API_URL + /v1/..., so the Android build variable is the bare origin — https://your-app.vercel.app, no trailing path.

Indexes

There is no boot step on Vercel, so ensureIndexes() runs on the first write of each warm lambda and is skipped afterwards. The unique indexes on (installationId, date) and (installationId, sessionId) are what make the upserts idempotent when the car retries a record it could not confirm.

Local development

cp .env.example .env.local   # Next loads .env.local automatically
npm install
npm run dev
npm test                     # pure validation and session-signing tests, no database