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>
This commit is contained in:
TCFDONMEZ
2026-10-08 14:21:37 +03:00
co-authored by Claude Opus 5
parent e9bfa1217b
commit 8242797ece
40 changed files with 3120 additions and 1319 deletions
+68
View File
@@ -0,0 +1,68 @@
# 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
```sh
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
```