Skip to content

eveys-mobility/Console

Repository files navigation

eveys-console

CI License: Apache 2.0

System-administration console for the OCPP gateway. Sign-in protected, single WebSocket per tab, live snapshot+tail subscriptions backed by the gateway's existing Kafka topics and REST API.

The console targets SRE / on-call engineers operating the gateway — not end-customer fleet managers. The front page (/) is an operator dashboard: alerts summary, headline metrics (chargers online, active sessions, faults), service status. Charge-point and transaction inspection live under /inspect; alerts management lives under /sys/alerts; configuration under /sys/config.

The gateway is consumed unchanged; everything the console offers is built on the gateway's existing surfaces.

Apache-2.0.

Surfaces

Surface Bind Direction Purpose
WebSocket :8090/ws browser → Console Subscriptions + RPCs in one connection. Subprotocol: eveys-console-v1 + bearer.<jwt>.
REST (auth) :8090/auth/{challenge,login} browser → Console Proof-of-work CAPTCHA + username/password login. Returns a short-lived JWT.
REST (status) :8090/sys/status browser → Console Aggregated service health (gateway probe + Kafka + WS connection count). JWT-protected.
REST (alerts) :8090/sys/alerts/* browser → Console Firing alerts + silences + channels + rules. Proxies Alertmanager/Prometheus. JWT-gated.
REST (config) :8090/sys/{config,gateway-config} browser → Console Read-only config introspection; Console + Gateway tabs.
REST (admin) :8090/sys/admin/console-config browser → Console Runtime overrides on allowlisted Console keys (Channels-style; persisted to disk).
REST (diag) :8090/sys/diagnostics/* browser → Console Console-minted upload URLs for GetDiagnostics / GetLog artefacts.
REST (metrics) :8090/metrics Prometheus Prometheus scrape endpoint. Unauthenticated (network-ACL'd in prod).
Health :8090/healthz, :8090/readyz k8s → Console Liveness / readiness probes. Unauthenticated.
Web :5180 (dev) browser React + shadcn/ui (Tailwind + Radix) + TanStack Router.

Pages

Path What
/ Operator dashboard — alerts summary, metrics, service status.
/inspect/charge-points Fleet view; per-charger AC/DC + power-rating chips, faults filter via ?faults=1.
/inspect/charge-points/$cpId Charger detail — connectors, statistics, transactions, diagnostics, device events.
/inspect/transactions Active + recent transactions across the fleet.
/inspect/transactions/$txId Per-transaction detail with kW-per-phase + cumulative-kWh charts.
/sys/alerts?tab=firing Alertmanager-backed firing alerts. Silence button per row.
/sys/alerts?tab=silences Active + pending silences. Expire-now per row.
/sys/alerts?tab=channels Slack / email / webhook receivers. Add / edit / test / set-default. Console writes Alertmanager's config.
/sys/alerts?tab=rules Read-only display of Prometheus's loaded rules, plus inline CRUD for the console-managed rule group.
/sys/config?tab=console Console keys, with inline edit on the allowlist. Overrides persist to data/console-overrides.json.
/sys/config?tab=gateway Gateway keys. Categories collapsible, sorted by mutability. Active-overrides pinned at the top.

Repo layout

apps/
├── server/                       Node + Fastify + ws + kafkajs Console server
│   ├── proto/events/v1/          vendored gateway event schema
│   ├── scripts/                  mint-dev-token, hash-password
│   └── src/
│       ├── auth/                 JWT verification, PoW CAPTCHA, user store (bcrypt)
│       ├── broker/               per-connection subscription state, query resolvers
│       ├── kafka/                Kafka tail + protobuf event-envelope decoder
│       ├── metrics/              Prometheus registry + per-route instrumentation
│       ├── rest/                 typed client to the gateway's /api/v1
│       ├── routes/               auth, ws, sys-status, sys-alerts, sys-config, …
│       ├── store/                channels-store, rules-store, override-store, diagnostics-store
│       └── main.ts               process entry — wires the components
└── web/                          React + shadcn/ui console
    └── src/
        ├── api/                  typed clients (auth, sys, alerts, config, ws)
        ├── components/           AppShell, AlertsPanel, ChannelsPanel, RulesPanel, ConfigView, …
        ├── hooks/                useSubscription, useFiringAlerts, useSilences, useChannels, …
        ├── lib/                  WS context, theme context, alerts derivation, charger-spec
        ├── pages/                LoginPage, SystemPage, FleetPage, ChargerDetailPage, AlertsPage, SystemConfigPage, …
        └── routeTree.ts          manual TanStack route tree

packages/
├── protocol/                     shared WS envelope contract (zod schemas + TS types)
└── api-types/                    types generated from the gateway's OpenAPI spec

deploy/
├── docker-compose.yml            server + web + (optional) prometheus + alertmanager
└── observability/                bundled prometheus.yml, alertmanager.yml, alerts.yml seed

Quick start

Prereqs: Node ≥ 20.10, pnpm 9.15 (corepack prepare pnpm@9.15.0 --activate), Docker, and the OCPP gateway running locally on :8080 with REST + Kafka up.

pnpm install
pnpm gen:api-types
cp apps/server/.env.example apps/server/.env
cp apps/web/.env.example apps/web/.env
# edit apps/server/.env: set JWT_SECRET, GATEWAY_TOKEN, KAFKA_BROKERS,
# CONSOLE_USERS (one or more username:bcrypthash pairs).
# Optional: set ALERTMANAGER_URL + PROMETHEUS_URL to light up /sys/alerts.

# Hash a password for CONSOLE_USERS:
echo -n "yourPassword" | pnpm --filter @eveys-console/server hash-password

pnpm dev

Server on http://localhost:8090, web on http://localhost:5180. Open the web URL, sign in with the username/password you put in CONSOLE_USERS. The login form runs a small client-side proof-of-work CAPTCHA before submitting (~50 ms in a real browser).

The mint-token script (pnpm --filter @eveys-console/server mint-token) is also kept as a dev-only fallback for headless tests.

Docker

Two images. The server is distroless Node 20 (with promtool bundled in for rule validation); the web is nginx serving the static SPA bundle. Compose ties them together:

# Set required env (see deploy/docker-compose.yml for all the keys
# the server reads). At minimum:
export JWT_SECRET=$(openssl rand -base64 32)
export GATEWAY_BASE_URL=http://gateway-host:8080
export GATEWAY_TOKEN=...
export KAFKA_BROKERS=kafka-host:9092

docker compose -f deploy/docker-compose.yml up -d --build

Server on :8090, web on :5180.

Observability

Compose ships an optional Prometheus + Alertmanager pair behind an observability profile so the default up stays lean. Bring them up with:

docker compose -f deploy/docker-compose.yml --profile observability up -d

Prometheus on :9091, Alertmanager on :9093. The starter scrape config targets the Console at server:8090/metrics and the gateway at host.docker.internal:9100/metrics.

/sys/alerts is the operator surface for both. Four tabs:

  • Firing — what Alertmanager is firing right now. 30 s poll. Silence button per row.
  • Silences — active + pending silences with matchers, comment, creator, remaining duration. Expire-now per row.
  • Channels — manage Alertmanager receivers (Slack / email / webhook). Add, edit, remove, send a test alert, switch the default. The Console writes data/alertmanager-managed.yml and reloads Alertmanager — Channels are persisted, not per-pod ephemeral.
  • Rules — read-only display of loaded Prometheus rule groups, with inline CRUD for the console-managed group. promtool check rules runs before every write so a malformed expression can't break Prometheus on reload.

Without the observability profile (or with ALERTMANAGER_URL / PROMETHEUS_URL unset), each tab renders a "not configured" hint instead of erroring.

Runtime configuration

The Configuration page (/sys/config) reads from the live process and edits flow through one of two override stores:

  • Console keysdata/console-overrides.json (persisted across restarts). Allowlisted in apps/server/src/store/override-store.ts; bind-time keys like HOST/PORT, secrets like JWT_SECRET, and consumer-state keys like KAFKA_* are deliberately excluded.
  • Gateway keys → the gateway's per-pod in-memory override map (ephemeral; cleared on restart). The Console proxies through /sys/gateway-admin-config.

Each tab surfaces an "Active overrides" section at the top when any override is in effect, plus a "Reset to env" button on every overridden row.

Realtime model

Each browser tab opens one WebSocket. Inside that connection it can:

  • subscribe to a named query (charge-points, charge-point, transactions-active, meter-history, status-history, device-events). The server returns a snapshot, then a stream of deltas. A single Kafka event can fan out to multiple deltas (e.g. one MeterValues report carries N samples → N appends; one StatusNotification produces one device-events row plus one status-history row).
  • unsubscribe when the component unmounts.
  • rpc to issue OCPP commands (remote-start, remote-stop, reset); the server forwards to the gateway's REST and relays the response back over the same WebSocket.

The wire format is defined in packages/protocol/. zod schemas validate every message in both directions; both apps import the same schemas so the contract is enforced symmetrically.

Snapshot+tail consistency is read-after-write with dedup: the client keys collection rows by primary ID so the small window between the snapshot fetch and the first delta is harmless. The FleetPage reduces snapshot + latest delta into a Map<cp_id, row> on every render.

Wire payloads from Kafka are protobuf-encoded EventEnvelopes (the gateway's own schema, vendored at apps/server/proto/events/v1/). The decoder lives in apps/server/src/kafka/event-decoder.ts.

Per-transaction detail (/inspect/transactions/$txId) is REST-polled rather than WS-subscribed: the WS broker only carries the active-tx list query, and an operator opening one session detail isn't watching it for hours. The page calls the Console's /sys/transactions/:txId and /sys/charge-points/:cpId/meter-values proxies (server-side forwarders to the gateway), then renders kW-per-phase and cumulative kWh charts using Recharts. Open sessions refetch every 5 s; closed sessions render a fixed window.

Firing alerts + silences + channels + rules are also REST-polled through the Console (30 s) rather than WS-subscribed — they change on the order of minutes, not seconds, and the broker isn't the right place for per-tab control-plane data.

Diagnostics uploads

OCPP GetDiagnostics and GetLog ask the charger to PUT its log file to a URL the operator supplies. Today the operator can either type any URL (legacy path; kept) or have the Console mint a one-shot URL bound to the command. When the latter is checked in the GetDiagnostics / GetLog form, the form first calls POST /sys/diagnostics/issue to mint a 32-byte token embedded in …/uploads/diag/<token>, then sends the OCPP command with that URL. The charger PUTs (or POSTs) the file back; the Console writes it to data/uploads/<cp_id>/<request_id> and records token, timestamps, byte count and SHA-256 in a SQLite metadata table at data/console.sqlite.

The artefact appears under the device page's Diagnostics history card within one 5-second poll. From there, downloads stream back through the Console; delete drops the row and the file.

Token rules: 32 random bytes (64 hex chars), one-shot, default TTL 1 hour (DIAGNOSTICS_UPLOAD_TTL_SECONDS). Pending tokens past their expires_at are rolled to expired lazily on every issue and upload — no cron needed. Body cap defaults to 50 MB (DIAGNOSTICS_MAX_UPLOAD_BYTES); enforced per-route, so other routes are unaffected.

Reachability is dev-only today: chargers reach the Console via the same host:port the operator's browser does, so this works on a laptop or inside a docker-compose network. Production ingress (TLS, public DNS, multi-pod fan-in, object storage) is a separate iteration.

Build, test, ship

pnpm format        # prettier
pnpm typecheck     # tsc --noEmit across all packages
pnpm test          # vitest, all packages (~600 tests)
pnpm build         # tsc + vite build, both apps

CI runs format:check + typecheck + test + build on every PR, plus a validate-observability job that runs promtool check config / check rules and amtool check-config against the bundled deploy/observability/ files.

License

Apache-2.0. See LICENSE for the licence text and NOTICE for attribution and trademark notices.

About

System-administration console for the OCPP gateway. Sign-in protected, single WebSocket per tab, live snapshot+tail subscriptions backed by the gateway's existing Kafka topics and REST API.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages