A personal single-page dashboard for a trusted LAN. Every widget is a TypeScript function on disk; the server runs it on a schedule, on an MQTT message or on a webhook, validates what it returns, and pushes the result to every open tab.
Every card above is one file. Writing one looks like this — the editor knows the
whole ctx API, because it is served the same ctx.d.ts the runtime uses:
- Widgets are functions. No plugin format, no config-driven renderer path. Summing a field, mixing two APIs, remembering what you already saw — all the same amount of work, because it is all just code.
- Typed, and checked in the browser. Monaco runs the real TypeScript service
against the same
ctx.d.tsthe server validates with, so a wrong return type is a red squiggle rather than a blank card at 3am. - Nothing silently stale. A failing widget keeps its last good value, dimmed, with the error on hover and a visible backoff.
Needs Node 26+ — the server runs its TypeScript directly, no build step and no transpiler, which is also what gives widget stack traces honest line numbers. npm too; Docker only for the container or the dev MQTT broker.
git clone <this repo> && cd n-dashboard
npm install
npm run devOpen http://localhost:5273. The first run copies the seventeen examples from
examples/ into data/widgets/, disabled — enable the ones you want under
/admin. Eight of them need no key and no connection, so crypto-price,
weather-hourly, hn-top, npm-downloads, endpoint-health, btc-candles,
api-sum and disk-usage all work on a fresh clone.
| script | what it does |
|---|---|
npm run dev |
server on :8080 + Vite on :5273, proxying /api |
npm run build |
builds the web bundle into dist/web |
npm start |
serves dist/web and the API from :8080 |
npm test |
Vitest |
npm run typecheck |
both projects |
For the always-on install there is a prebuilt image, linux/amd64 and
linux/arm64 so a Pi or an ARM NAS needs no local build:
docker run -d --name n-dashboard -p 8080:8080 \
-v n-dashboard-data:/app/data \
ghcr.io/devapro/n-dashboard:latest:latest tracks the newest release tag and :edge tracks main, deliberately —
a box on your LAN should not pick up a mid-refactor commit on restart. HOST and
PORT are already set in the image; add -e ADMIN_PASSWORD=… to put a login on
/admin.
To build it from source instead, with an optional MQTT broker alongside:
cd docker
docker compose up -d --build # then http://localhost:8080
docker compose --profile broker up -d # only if you have no broker of your owndata/ holds everything mutable — widgets, layout, secrets, history — so back up
that one volume and you have backed up the lot.
Configuration is a .env in the repository root — copy .env.example. Set
ADMIN_PASSWORD to put a login on /admin; the board itself stays readable by
design. See Configuration and deployment for the rest.
A widget is a folder under data/widgets/<id>/ whose widget.ts default-exports
one function:
import type { Ctx, StatData } from '@n-dashboard/widget'
export default async function (ctx: Ctx): Promise<StatData> {
const r = await ctx.fetch('https://api.example.com/price').then((r) => r.json())
return { value: r.usd, unit: 'USD', delta: r.change24h }
}It returns data matching its kind — stat, line-chart, candlestick, gauge,
list, alert, table, bar-chart or markdown — and if it does not, the run
is recorded as an error and the card keeps its previous value. Triggers are a
clock, an MQTT topic, a webhook, or several at once.
| Writing a widget | the folder layout, every kind, triggers, the full ctx, recorded history, isolation |
| Connections, webhooks and secrets | MQTT, Google Calendar (iCal and OAuth), webhook tokens, the secret store |
| Configuration and deployment | environment variables, the admin login, Docker, the GitHub Pages project page |
| How it fits together | the repo map, the design decisions and why they went that way, the test suite |
src/shared/ctx.d.ts |
the widget API itself — the same file the editor and the validator use |
CLAUDE.md |
invariants that are easy to break and library gotchas that cost real time |
Deliberately: multiple dashboards, user accounts and roles (the /admin login is
one shared password, not a user model), IMAP, widget import/export, alert
notifications (alert widgets display state only), long-retention downsampling, and
phone-optimised layouts.
MIT — do what you like with it.

