A read-only command center for a self-hosted AI fleet, with a live Three.js constellation of your machines and a built-in chat assistant that runs on your own model.
It watches:
- GPU nodes over SSH: unified-memory boxes (DGX Spark / GB10 class) and hosts with discrete GPUs (RTX, A-series). Temperature, power, utilization, clocks, memory, fans, CPU temperature, running containers, and a temperature sparkline per GPU.
- A fabric switch (MikroTik RouterOS): temperatures, fans, PSUs, uptime, and live throughput per port.
- Model servers (vLLM, SGLang, llama.cpp) through their Prometheus
/metrics: decode and prefill tok/s, TTFT, KV-cache use, running and waiting requests, and the live model id. - ComfyUI render lanes: up/idle/rendering, queue depth, and memory, taken from the driver (not ComfyUI's own estimate), with peak-while-rendering marks.
- Tokens served per model, banked across server restarts, with a per-day count.
- Optional extras: GPU clock caps (status plus, if you allow it, apply), links to other web apps on the host with an up/down probe, and Elgato key lights.
The chat ("Jarvis" by default) sees the same live data the board shows, so you can ask "what is down right now?" or "which node is running hottest?" and get an answer grounded in the current numbers.
Stack: Python 3.9+ standard library for the server (no pip installs), Vite + React + TypeScript + React Three Fiber + GSAP + Lenis for the UI.
git clone https://github.com/tonyd2wild/The-Sparky-Command-Center.git && cd The-Sparky-Command-Center
cp config.example.json config.json # then edit it for your machines
python3 server.py
# open http://localhost:8895The built UI ships in web/dist, so running it needs nothing but Python 3.9+. Node 18+ is only needed if you want to change the UI: cd web && npm install && npm run build.
v2 (September 2026) is a full rebuild: a Three.js constellation of your fleet, a new card layout, themes (Dark, Light, Sunset, Breeze, Matrix) and a built-in chat you can point at your own local model or agent. The original single-file v1 is still available at the
v1tag.
The machines you monitor need:
nvidia-smiinstalled.- Key-based SSH from the box running this server. It uses
BatchMode=yes, so a node that asks for a password fails fast instead of hanging. - For model cards, an inference server with
/metricsenabled (vLLM and SGLang expose it by default; for llama.cpp, startllama-serverwith--metrics).
Nothing needs to be installed on the nodes.
The chat speaks the OpenAI chat-completions protocol, so it works with anything that serves /v1/chat/completions. Set a base URL and a model name, either in config.json:
"chat": {
"base_url": "http://localhost:11434/v1",
"model": "llama3.1:8b"
}or in .env (copy .env.example), which overrides the config:
CC_CHAT_BASE_URL=http://localhost:11434/v1
CC_CHAT_MODEL=llama3.1:8b| Server | base_url |
Notes |
|---|---|---|
| Ollama | http://localhost:11434/v1 |
model = the Ollama tag, e.g. qwen2.5:14b |
| vLLM | http://localhost:8000/v1 |
model = the --served-model-name |
| SGLang | http://localhost:30000/v1 |
|
| llama.cpp server | http://localhost:8080/v1 |
model can be any string |
| LM Studio | http://localhost:1234/v1 |
|
| Any hosted OpenAI-compatible API | its /v1 URL |
put the key in CC_CHAT_API_KEY |
Restart server.py after changing the chat settings. If no model is set, or the one you set cannot be reached, the chat panel shows a "connect a model" screen with these steps. The rest of the board keeps working.
If you run an agent that exposes an OpenAI-compatible API (for example a Hermes profile with its API server turned on), point the chat at the agent instead of the raw model. You get the agent's memory and tools behind the same chat box:
CC_CHAT_BASE_URL=http://127.0.0.1:8643/v1
CC_CHAT_MODEL=<the model name the agent's API reports>
CC_CHAT_API_KEY=<the agent's API key>No other change is needed. The dashboard still adds its live-data context as the system message, and the agent answers.
- Grounded. Every turn gets a system message with a compact read of the live board, taken at the moment you ask: totals, a "down right now" list with error text, every node, the switch, each model server, each render lane, token counts and station status. Turn it off with
"grounding": false. - Careful. Temperature defaults to 0.1, and the prompt forbids inventing numbers and tells the model it cannot change anything.
- Streams tokens. Reasoning is stripped, whether it arrives as
reasoning_contentor inside<think>tags. - Fallbacks. List extra endpoints in
chat.fallbacks. A connection failure moves to the next endpoint. An HTTP error from a model that answered is shown as-is, not retried. - Local history. The conversation lives in the viewer's browser (localStorage), with a Clear button.
- Extra fields.
chat.extra_bodyis merged into every request. For example,{"chat_template_kwargs": {"enable_thinking": false}}turns thinking off on vLLM-served reasoning models. Leave it empty for servers that reject unknown fields.
config.json is gitignored and holds everything site-specific. config.example.json shows every block with made-up hosts. Keys starting with _ are ignored.
| Key | Default | Meaning |
|---|---|---|
title, subtitle, location |
Header text. The nav word is the title minus "Command Center". | |
timezone |
system | IANA name used for the UI clock and the chat's sense of time. |
bind |
["127.0.0.1"] |
One address or a list. Add a VPN/tailnet IP to reach it from your phone. Avoid 0.0.0.0 on untrusted networks. |
port |
8895 |
|
allowed_hosts |
[] |
Extra Host names allowed to POST (for example a tailnet DNS name). |
read_only |
true |
Refuses every route that changes another machine. |
browser_refresh_ms |
2500 |
How often the page re-polls /api/metrics. |
default_key, connect_timeout and options (extra -o flags). A node can override the key with ssh_key and the port with ssh_port.
| Key | Meaning |
|---|---|
key, name |
Id and display name. |
profile |
unified (GPU shares system RAM: DGX Spark, GB10, Jetson) or discrete (PCIe GPUs). |
user, host |
SSH target. |
jump |
Optional. {"user", "host", "mode": "proxy"} uses ssh -J. "mode": "nested" runs ssh from the jump host itself, for LAN nodes that only trust the jump host's key. |
node_id, rank, serving, pair |
Unified nodes: labels on the card (what the node is serving and its role). |
badge, list_containers, containers |
Discrete hosts: card badge, whether to list docker ps names, and friendly labels per container name. |
temp_warn, temp_hot, poll_interval |
Per-node overrides of defaults. |
The board groups nodes into sections: {"key", "eyebrow", "title", "subtitle", "nodes": [...], "switch": true, "units": [...], "models_title"}. A section shows its nodes, the switch card if switch is true, and every model whose unit is in units. Leave sections out and the server builds a sensible default.
MikroTik RouterOS over plain SSH (host, user, ssh_key), or any command you like through exec, for example an expect helper: {"argv": ["expect", "helper.exp", "{cmd}"], "cwd": "/path"}. ports lists the interfaces to chart. Delete the block, or set "enabled": false, to hide it.
{"key", "label", "unit", "node", "endpoint", "port", "gpus", "model", "api_key"}:
endpointis the server's base URL;/metricsand/v1/modelsare appended.nodeties the model to a host in the 3D view.modelpicks the preferred alias when a server lists several.
{"key", "lane", "name", "host", "url", "src"}. src points the memory reading at a GPU you already monitor:
{"kind": "gpu", "node": "<key>", "index": 0}for a discrete GPU.{"kind": "unified", "node": "<key>"}for a unified node.
mode: "bank"(default): polls each model's counters everypoll_secondsand banks them intostore, surviving server restarts. Rows for models you remove are archived under_retired, not deleted.mode: "read": only reads a store that another process maintains.
order sets the card order.
Off by default.
eco: GPU clock caps on unified nodes. Status is a read-only query. Apply needs"allow_writes": trueandserver.read_only: false, and usessudo nvidia-smi -lgc.stations: a list of{emoji, name, desc, url, probe_url}links with an up/down probe.keylights: proxies an Elgato key-light panel's/api/lights. Setting the lights needsallow_writes.
enabled, name, base_url, model, api_key (or api_key_file), system_prompt (replaces the default persona line; the rules and live data are always added), temperature, max_tokens, timeout, history_turns, grounding, extra_body, fallbacks[], suggestions[].
Environment variables:
| Variable | Overrides |
|---|---|
CC_CONFIG |
path to the config file |
CC_PORT, CC_BIND |
server.port, server.bind |
CC_READ_ONLY |
server.read_only |
CC_CHAT_BASE_URL, CC_CHAT_MODEL, CC_CHAT_API_KEY |
the matching chat fields |
CC_CHAT_SYSTEM_PROMPT, CC_CHAT_NAME |
the matching chat fields |
- Monitoring only runs queries:
nvidia-smi --query-*,/proc/meminfo,free,docker ps, and RouterOSprint/monitor once. Nothing restarts or reconfigures anything. - With
read_only: true(the default),POST /api/eco-setandPOST /api/lights-setreturn 403, and their buttons stay visible but disabled. POSTroutes are same-origin only: a known Host, a matching Origin, and a JSON content type. This stops another site from driving your chat or your actions from a browser.- The browser never receives hosts, keys or endpoints from the config.
/api/configonly sends layout and labels. Model and ComfyUI URLs do appear on cards and in/api/metrics, so keep the server on localhost or a private network.
| Route | Returns |
|---|---|
GET /api/metrics |
Every node, the switch, the model servers, temperature history and fleet totals. It also carries sparks / box / *_models for v1 consumers. |
GET /api/comfy |
Render lanes. |
GET /api/tokens |
The token store. |
GET /api/stations |
Stations with up/down. |
GET /api/lights |
The key-light panel's lights. |
GET /api/eco-status |
Current GPU clock / temperature / power per unified node (read-only). |
GET /api/config |
UI layout and labels only. |
GET /api/chat/status |
Whether a model is configured and reachable. |
POST /api/chat |
{"messages": [...]}, answered as a server-sent-event stream of {"delta"} / {"status"} / {"reset"} / {"meta"}, then [DONE]. |
GET /healthz |
ok |
python3 server.py # API on :8895
cd web && npm run dev # UI on :5176, proxies /api to :8895Useful query flags: ?nowebgl=1 shows the SVG fallback map, and ?motion=reduce forces the reduced-motion still frame.
MIT