EV Charging Optimizer
EV Charging Optimizer is a hybrid mobile web app that picks the cheapest, cleanest hours to charge an electric vehicle before a stated departure time. Wallbox and Tesla apps schedule by clock; this one schedules by the grid. By blending Nord Pool spot prices with grid carbon intensity and the user's deadline, it can shift the same kWh into the cheapest, lowest-CO₂ window — typically 20–60% cheaper than 'plug in now until full,' which is the headline number returned as `savingsVsNaive` on every optimize response.
The optimizer itself is a few dozen lines of pure TypeScript in `backend/src/services/optimizer.ts`. For each hourly slot in the forecast window it computes `score = (1 - w) · norm(price) + w · norm(carbon)` where `w` is the user's `carbonWeight` (0 = pure cost, 1 = pure carbon, 0.5 = balanced), and `norm()` is min-max across the window. The K cheapest-by-score full hours are picked, plus a fractional last hour for the remainder, then reassembled in chronological order so the schedule renders cleanly as a bar chart. `savingsVsNaive` compares the result against 'charge straight through from now' so the cost win is provable per-request, not aspirational.
The stack is Express 4 + Node 20 + strict TypeScript on the backend (Zod validation at every API boundary, helmet + cors + morgan + compression for chrome, dotenv for config), Ionic 8 + Angular 19 on the frontend (standalone components, signals, OnPush components with eventCoalescing zones, no NgModules, ngx-translate for i18n). The REST surface covers `/api/optimize`, `/api/forecast`, `/api/push/key`, `/api/push/schedule`, `/api/health`, `/api/version`, and `/api/metrics`. On top of that a WebSocket endpoint at `/api/live` broadcasts a 5-second price + carbon tick to subscribed regions, with origin allowlist, per-IP connection caps, and ping/pong heartbeats — the frontend's `live-tick-pill` consumes it for ambient grid awareness. Live data comes from elprisetjustnu.se (Nord Pool spot, region-aware: SE1–SE4) and carbonintensity.org.uk (UK National Grid CO₂); both have deterministic mock fallbacks so the demo runs offline and during API outages. Web Push is wired via VAPID + the `web-push` library, gated by env vars — without `VAPID_PUBLIC_KEY` set, push routes degrade gracefully and the rest of the app keeps working.
The deploy is the punchline: one Railway service, one Node process, one origin. A multi-step Nixpacks build compiles the Angular app into `frontend/www/` and the Express server into `backend/dist/`, then `node backend/dist/server.js` mounts `/api/*` for the API, the WS upgrade handler at `/api/live`, and serves the built PWA from the same Express instance with a SPA fallback. No CDN, no separate frontend host, no CORS dance. The 'hybrid mobile' part lives in the service worker — installable via Add to Home Screen on iOS/Android, with offline shell caching and Web Push handling — so the portfolio brief gets satisfied without burning weeks on App Store review. Capacitor wrapping (`npx cap init` + `cap add ios/android`) is a one-evening add behind the same code if a native binary is later needed.