A beautiful realtime dashboard and live activity log for a local
MTPLX inference server. A small Node/TypeScript server polls MTPLX's
/metrics endpoint itself and pushes updates to the browser over Server-Sent Events — the two
pages (public/index.html, public/log.html) stay plain HTML/CSS/JS, no client framework, no
build step for the frontend.
What it's for: MTPLX runs LLMs on Apple Silicon using MTP (multi-token-prediction) speculative decoding. Its server exposes a rich
/metricsendpoint — this project turns that into (1) a dashboard that tells the speculative-decoding story at a glance, and (2) a "tail -f for the model" live log of what's being generated right now.
The hero is speculative decoding: tokens committed per verify pass (an autoregressive decoder yields 1.0), accepted-vs-drafted per depth, and acceptance probability — the numbers that explain why MTP is fast. Around it:
- Decode & prefill throughput (tok/s) with live sparklines
- Time to first token
- Context window usage with a cached-vs-fresh-prefill split
- Verify-time breakdown — where decode time actually goes
- KV cache (RAM/SSD source + hit) and tool-call parse health
One row per completed request, newest first:
- Headline: the prompt (server-truncated preview)
- Chips: tokens in→out · decode tok/s · TTFT · elapsed · conversation depth · tool-calls made · acceptance % · reasoning/thinking flag · client · short request id · live "Ns ago"
- Click any row to expand a full detail drawer: every timing/token field, per-depth acceptance bars, the conversation role sequence, and available tools.
The two pages cross-link via a header nav.
You need a running MTPLX server with its OpenAI-compatible endpoint (and /metrics) on
http://127.0.0.1:8000 — the default target this server polls.
git clone https://github.com/devty/mtplx-dashboard.git
cd mtplx-dashboard
npm install
npm run dev
# then open:
# http://127.0.0.1:8123/ → dashboard
# http://127.0.0.1:8123/log.html → live lognpm run dev runs the TypeScript server directly (via tsx watch, auto-restarting on change) —
no separate compile step needed for day-to-day development. For production, build once and run
the compiled output:
npm run build
npm startThe server polls a single, configured MTPLX target — set these as environment variables
(.env.example documents the same list; this project has no dotenv dependency, so either
export them in your shell, pass them inline, or use Node's native --env-file=.env flag):
| Variable | Default | Meaning |
|---|---|---|
MTPLX_URL |
http://127.0.0.1:8000 |
MTPLX server this process polls |
PORT |
8123 |
Port this dashboard server listens on |
POLL_INTERVAL_MS |
1000 |
How often to poll MTPLX's /metrics |
MTPLX_TIMEOUT_MS |
2500 |
Timeout per poll request |
RING_SIZE |
120 |
Sparkline history depth (dashboard) |
LOG_BUFFER_SIZE |
300 |
Live-log rolling buffer depth |
MAX_BACKOFF_MS |
10000 |
Ceiling for poll-retry backoff when MTPLX is down |
MTPLX_URL=http://box.local:8000 npm run devmtplx-dashboard/
├── server/ TypeScript server — polls MTPLX, pushes SSE
│ ├── server.ts Express app: serves public/, /api/events (SSE), /api/metrics
│ ├── metricsPoller.ts Poll loop, retry/backoff, ring/log buffers, change detection
│ ├── sse.ts SSE client registry, broadcast, heartbeat
│ ├── config.ts Env var → config
│ └── types.ts Shared MetricsRecord / StatePayload shapes
├── public/ Static frontend — plain HTML/CSS/JS, no build step
│ ├── index.html Metrics dashboard
│ └── log.html Live activity log
├── docs/ README screenshots
├── package.json Scripts: dev / build / start / typecheck
├── tsconfig.json
└── .env.example Documents the env vars below (not auto-loaded)
npm run dev/npm start compile nothing on their own from public/ — those two files are served
as-is by express.static. Only server/**/*.ts goes through TypeScript.
- A Node/TypeScript server (
server/) pollsGET {MTPLX_URL}/metricson an interval, server-side — not the browser. The response is{ latest, recent[32], tool_parse_counters }—latestis the most recent request,recentis MTPLX's own rolling 32-deep history. - The server keeps its own deeper in-memory history (sparkline ring buffers sized
RING_SIZE, a live-log buffer sizedLOG_BUFFER_SIZE, deduped byrequest_id) and retries with exponential backoff (capped atMAX_BACKOFF_MS) when MTPLX is unreachable. - Browsers connect once via
EventSourceto/api/events: an initialsnapshotevent delivers full history immediately (a reload or a brand-new tab never starts from empty), and atickevent pushes out on every genuine change thereafter — no client-side polling. - Sparklines are still hand-drawn inline SVG on the client; only where the history comes from changed (the server, not a per-tab ring buffer).
- Because polling happens server-to-server, MTPLX's CORS reflection is no longer relevant — the browser only ever talks same-origin to this Node server.
- Both pages are light/dark aware (
prefers-color-scheme) and degrade gracefully when MTPLX is unreachable (dim + reconnect banner, last values retained) or when the SSE connection itself drops (nativeEventSourceauto-reconnect, no custom retry logic needed).
- Completed requests only. A long generation appears when it finishes, not mid-flight.
- Prompt is a server-truncated preview, and there is no assistant response body in
/metrics— this is a live pulse, not a full trace store. - Caller attribution is approximate. OpenAI-compatible clients report the same
client_label, so multiple apps hitting one server aren't cleanly distinguished. - For full prompt/response bodies and tool-call arguments, you'd put a logging proxy in front of the server — out of scope here.
MIT © 2026 Tyler Singletary
Not affiliated with or endorsed by MTPLX — a community tool built against its public
/metrics endpoint.

