Real-time monitoring of GB grid-scale Battery Energy Storage Systems (BESS). All data comes from free public APIs. No API keys are needed.
Live at grid.xelantis.com.
Positive MW means discharging; negative means charging. The page auto-refreshes every 5 minutes.
Browser → /api/elexon → Elexon PN + BOALF + FUELINST (main series)
Browser → /api/elexon/history → Elexon PN + BOALF for a past date, optionally one site
Browser → /api/sites → Elexon PN + BOALF, aggregated per site
Browser → /api/units → Elexon /reference/bmunits/all
Browser → /api/bm-prices → Elexon BOD/stream (submitted bid/offer prices)
Browser → /api/market-price → Elexon market index (APXMIDP)
Browser → /api/system-prices → Elexon system prices (SSP/SBP, NIV)
Browser → /api/frequency → Elexon system frequency
Browser → /api/prices → Octopus Agile (Region A)
Browser → /api/carbon → api.carbonintensity.org.uk
The proxy routes avoid CORS problems: every upstream request is made server-side. /api is kept as an alias of /api/elexon so existing links still work. If Elexon is unreachable, /api/elexon returns clearly flagged simulated data.
src/app/page.tsx is a server component that fetches the initial data at request time, so the first render is complete.
[timeFrom, timeTo). When acceptances overlap, the latest one wins. If PN is unavailable, the app falls back to BOALF only, which shows SO dispatch but not merchant charging.<site>B-<n> pattern (PILLB-1, KILSB-3). bmUnitType: "S" means supplier unit, not storage.PS) and wind. It has no solar field, and its OTHER category never shows battery charging.npm install
npm run dev # http://localhost:3000
npm test # unit + behaviour tests
npm run lint # includes Sonar cognitive complexity ≤ 15
Deployed on Railway as a long-running Node.js process (Dockerfile, output: "standalone"), which keeps the in-memory unit cache warm. Every route is dynamic, so the app can’t be exported as a static site.