Back to projects

Case Study

ChargeHub

An operations dashboard for an EV charge-point operator

Solo project: architecture, implementation, tests, CI/CD, and deployment — a single developer, held to a team's bar.

Role
Full-Stack Developer
Timeframe
2026 — built to a 24-day plan live and feature-complete

Starting point

A charge-point operator needs an operational view of its network: where the stations are, which ones are online, how much energy and revenue they produce. ChargeHub is that dashboard, built solo as a portfolio project in the same problem domain. Station master data comes from a real public registry, Open Charge Map; everything a public registry can't provide — live status, charging sessions, KPIs — is generated by a deterministic server-side simulation.

It is built to the bar a team would set for itself: a branch and pull request for every change, branch protection on main, and a CI pipeline that lints, type-checks, tests behind a coverage gate, builds, runs Playwright across four browsers and viewports, and holds every page to its own Lighthouse budget. Every significant decision has an Architecture Decision Record.

What the dashboard does

  1. Lists and maps real charge points from the Open Charge Map registry, fetched through a server-side BFF route that keeps the API key off the client and caches the responses.
  2. Shows live status, power draw, and current sessions per station — produced server-side by a deterministic simulation that sits behind a swappable transport interface, so a real telemetry feed can replace it without touching the UI.
  3. Aggregates historical sessions into KPIs and time-series charts — utilisation, energy delivered, revenue — rendered with Chart.js.
  4. Lets an operator search the fleet in plain language ("fast CCS chargers over 150 kW from Ionity that are online"): Claude Haiku 4.5 returns a structured filter, validated with Zod and grounded in real station IDs.
  5. Separates an operator role (manage tariffs) from a read-only viewer role, with server-side sessions and localized DE / EN routing.

In numbers

  • 232commits
  • 40+merged pull requests, each on its own branch behind branch protection
  • 7Architecture Decision Records (docs/adr) — design system, telemetry simulation, live-update transport, module structure, Pinia scope, rendering strategy, AI search
  • ~92%unit-test coverage gate, enforced in CI (Vitest + @nuxt/test-utils)
  • 4Playwright browser / viewport targetsChromium, WebKit, Pixel 7, iPhone 14
  • 19hand-labelled queries in the eval suite for the natural-language search
  • 46 → ≥82Lighthouse Performance on the station detail page: initial score → blocking CI gatethrottled mobile, 3-run median in Lighthouse CI
  • 100 · 100Lighthouse Accessibility · SEO, required on every page (CLS < 0.1 everywhere)

Natural-language station search

Above the normal filter bar, an operator can type a request instead of clicking through dropdowns. Claude Haiku 4.5 returns a structured object — not free text — which is validated against a Zod schema and applied to the same filter state the UI controls use. Station references are constrained to real Open Charge Map IDs, so the model can't invent a location. The feature is optional: with no API key the dashboard runs exactly as before, just with the classic filters.

Engineering, not just a demo

  1. The API key never reaches the browser.

    Open Charge Map is called only from a Nitro server route that injects the key and caches the response; the client receives a narrowed, typed payload and never sees a credential.

  2. Simulated telemetry — but deterministic and stateless.

    ADR 0002: live status and history are derived from station ID plus timestamp, so every serverless invocation returns the same value with no shared state. ADR 0003: the whole feed sits behind one interface (polling, not WebSocket) that a real source can implement later.

  3. Station detail performance rebuilt from 46 to the CI gate.

    The station detail page started at 46/100 on throttled mobile. MapLibre is now lazy-loaded on demand, the icon set was cut from a 403 kB webfont to a 3.7 kB subset, and responses ship Brotli-compressed — enough to hold ≥ 82/100 as a blocking Lighthouse CI check.

  4. Per-page Lighthouse budgets, not one global number.

    Each route carries its own thresholds — station detail ≥ 82, dashboard ≥ 80, analytics ≥ 78, sessions ≥ 70, with Accessibility 100 and CLS < 0.1 everywhere — measured as a 3-run median in CI.

  5. The natural-language search has its own eval suite.

    19 hand-labelled queries check that the model's structured output maps to the right filters. It runs on demand, deliberately outside CI, because each run has an API cost.

  6. Every architectural decision is written down.

    Seven ADRs cover the design system, telemetry simulation, live-update transport, module structure, Pinia scope, per-route rendering, and the AI search — each with the context and the trade-off, not just the outcome.

Tech stack

  • Nuxt 4
  • Vue 3
  • TypeScript (strict)
  • Vuetify 3
  • SCSS
  • Pinia
  • MapLibre GL + OpenStreetMap
  • Chart.js (vue-chartjs)
  • Zod
  • Claude Haiku 4.5 (@anthropic-ai/sdk)
  • Nitro server routes
  • @nuxtjs/i18n
  • Vitest + @nuxt/test-utils
  • Playwright
  • axe-core
  • GitHub Actions
  • Lighthouse CI
  • Vercel

Honest limitations

  • Telemetry is simulated. It is deterministic and swappable, but there is no real hardware or live feed behind it.
  • Authentication is mocked — two hard-coded accounts (operator, viewer). Enough to demonstrate role separation, not a real identity system.
  • Station detail pages don't use app-level HTTP caching (swr / ISR): it conflicted with hydration, so it was left out rather than shipped half-working.
  • MapLibre marker clicks aren't covered by pixel-precise E2E tests — Canvas / WebGL projection makes them unreliable to assert on.
  • Single-author repository — every pull request was opened, checked by CI, and merged by the same person; no second reviewer.

What I took away

  • Designing a simulation you can throw away: a deterministic, stateless telemetry source behind an interface, so swapping in a real feed is a config change, not a rewrite.
  • Treating performance as a contract — per-page Lighthouse budgets in CI — instead of a number you check once and forget.
  • Keeping an LLM feature honest: structured output, schema validation, IDs grounded in real data, and an eval suite kept separate from the unit tests.
  • Writing ADRs while deciding, not afterwards — the ones that paid off most were the boring ones (Pinia scope, rendering strategy).

Screenshots

  • The operator dashboard: fleet map, live status tiles, and the KPI strip
  • A single station: connectors, live power draw, and its recent sessions
  • The analytics page: utilisation, energy delivered, and revenue over time
  • The natural-language search resolving a plain-language query into structured filters