A central fleet console for Nexus Dashboard nodes. Enroll your single-host Nexus dashboards as "nodes," then monitor and control the whole fleet from one pane of glass — over each node's existing token-authed REST API.
The per-node dashboards keep working standalone; the controller is a console on top of them, not a replacement.
Unprivileged by design. The controller needs no root, no sudo, no shell-outs — it only ever speaks HTTPS to nodes. All privileged work stays on the node, behind that node's own auth + RBAC + audit. That keeps the controller's own attack surface tiny.
- Nexus Dashboard nodes first and foremost — the single-host dashboard (ZFS/LVM/md storage, SMB/NFS/iSCSI sharing, services, scheduled tasks, replication, LXD VMs/containers, GPU + llama.cpp AI serving) is the controller's native host type and the richest integration: enroll with a URL + API token (a connection test captures the node's TLS cert fingerprint, role, version, and capabilities) and you get storage/share/service chips and alerts in the fleet view, fleet-wide service actions, drill-in to the node's full SPA through the controller (websocket consoles included), and LXD instance / Docker container counts with lifecycle control (see Guest control).
- Virtualization hosts too — enroll Proxmox VE, VMware vCenter, or standalone ESXi hosts (username/password) right alongside Nexus nodes. Their row shows host count, running/total VMs & containers, CPU/RAM, and datastore usage; Open console ▸ deep-links to the native web UI. Slow hypervisor APIs are polled in the background so they never stall the fleet view.
- NAS appliances too — enroll TrueNAS (SCALE / CORE) with a read-only API key. The row shows pool health (✓ / ⚠ degraded), disk count, and capacity/CPU/memory, and its status dot goes amber on a degraded pool or an active alert. Read-only calls over the JSON-RPC 2.0 WebSocket API, polled in the background like the hypervisors. Synology DSM enrolls the same way (username/password of a local no-2FA admin account, DSM Web API, volumes map to pools), as does ZimaOS / ZimaCube (local account over the LAN HTTP API; storages map to pools), Unraid 7.x (webGui username/password driving its GraphQL API; the parity array + every mounted pool appear as pools, and disk/pool problems plus unread Unraid alerts surface as alerts), and OpenMediaVault (web-UI admin credentials driving its JSON-RPC API; managed filesystems appear as pools with mdadm array state folded in, and SMART failures on monitored disks surface as alerts).
- Any bare Linux or Windows machine — for hosts that don't warrant a full
dashboard, drop the Nexus Agent on them (
agent/in this repo). Linux: a single stdlib-only Python file + systemd unit (agent/install.sh). Windows: a PowerShell 5.1 script + Scheduled Task (agent/install.ps1, TLS bound in http.sys so the agent runs as NETWORK SERVICE). Both speak the same read-only HTTPS contract (bearer token, self-signed cert TOFU-pinned by the controller): up/down, CPU, memory, and per-mount/per-drive storage utilization. No dependencies, no write endpoints. Enroll either as host type Nexus Agent. - DGX Spark clusters too — enroll a SparkDash instance to monitor a whole sparkrun DGX Spark cluster as one host: nodes online, GPU utilization, VRAM, vLLM health + loaded model, running recipe, and cluster disk/CPU/memory; the dot goes amber when the cluster reports unhealthy. An API token is optional (reads are public) — supply one to arm write actions through the controller's proxy.
- dnsmasq managers too — enroll a DNSMAQ-MGR instance (the DNS/DHCP management appliance) with a read-only API token. The row shows the dnsmasq service state, DNS cache hit ratio, active DHCP leases, and — the headline chip — its mirror role: ⬆ primary (with N/N mirrors in sync) or ⬇ secondary (a read-only replica, synced from its primary). dnsmasq itself also appears on the Services page with a green/red status dot. Same token/self-signed-TLS-pinning model as a Nexus node.
- Fleet Overview — hosts as compact horizontal rows grouped by type (Storage / Virtualization / AI / General): each row shows reachability, the reachable IP, CPU/mem/storage mini-bars, and type-specific chips (ZFS/shares/ disks, VM & container counts, or llama-server health + model + tok/s).
- Fleet-wide views — every alert across the fleet, storage totals, and a services matrix (node × service status).
- Push notifications — a background monitor watches every host's state and posts state-transition events (host down/up, degraded pool, new alerts, certificate change, version drift) to a chat webhook (Google Chat, Slack, Discord, ntfy, or Gotify). Debounced against flapping, with all-clear recovery messages. Configure under 🔔 Notify (admin).
- User management — create operator / viewer / admin logins, reset passwords, all from the UI (👥 Users); first login on a new account forces a password change. Failed logins are rate-limited (per-account and per-IP sliding windows).
- Audit viewer — the controller-side audit trail (every mutation: who, from where, what, result) is browsable from the UI (📜 Audit, admin) with free-text filtering — no shell access needed.
- Host detail view — click any host name for a detail panel: 24-hour CPU and memory charts, availability, active health conditions (and for how long), pinned certificate fingerprint, capabilities, enrollment info.
- History & capacity forecasting — a 30-day SQLite ring buffer records every host each minute. Overview rows show a CPU sparkline; the Storage tab projects days-to-full per pool (and fleet-wide) from the observed fill rate, plus a rolling availability % per host.
- Certificate re-pin — if a host starts serving a new TLS certificate (a renewal, or something worse), it goes unreachable on the pin. Admins get a 🔐 Review cert action showing the pinned vs. now-serving fingerprint side-by-side; re-pinning is guarded so a certificate that changes again between review and click is refused rather than blindly trusted.
- Control at scale — start / stop / restart / enable / disable services on a
node, view its logs, or run a fleet-wide action ("restart
smbdeverywhere", or only on nodes taggedprod) with per-node success/failure reporting. - Tags — label hosts (
prod,storage,rack-3) and filter the overview to just those hosts, or target a fleet action at a single tag. - Tag-scoped accounts — confine an operator or viewer login to hosts bearing any of a set of tags. Scoped accounts get a fleet view, rollup, history, and action surface containing only their hosts — everything else is invisible (404), enforced server-side. Admins are always fleet-wide.
- Guest control — start / stop / shut down / reboot VMs and containers on Proxmox and VMware hosts straight from the controller (🖥 Guests), with the same pin-verify + audit trail as every other action. Nexus Dashboard nodes running the LXD (instances) or Docker module get the same modal — their guest lists load on demand and actions go through the authenticated reverse-proxy, so the node's own RBAC, validators, and audit still apply. Power operations only — no create or destroy.
- Drill-in — "Open dashboard ▸" opens a node's own dashboard SPA through the controller; the node's token stays server-side, and every action is audited on the controller in addition to the node. This includes the node's Containers console (xterm over websocket) — the controller bridges the websocket with the node's token attached server-side.
- Graceful degradation — a slow or unreachable node never blocks the fleet view; results are briefly cached so auto-refresh doesn't hammer nodes.
Browser ──HTTPS──▶ Nexus Controller ──bearer token + cert-pinned TLS──▶ Node A /api/*
(operator) (Flask + SPA, ──(parallel fan-out, per-node ─▶ Node B /api/*
NO sudo) timeout, brief cache) ─▶ Node C /api/*
The controller is a node registry + a fan-out aggregator + an action reverse-proxy. Nodes never call back — communication is pull-only, so the controller's own IP can change without breaking anything (see Networking).
Host-type support lives in the adapters/ package — one self-contained
module per host type (Nexus node, Proxmox, vCenter, ESXi, TrueNAS, Synology,
ZimaOS, Unraid, OpenMediaVault, SparkDash, Nexus Agent, DNSMAQ-MGR). Each adapter
describes its own enrollment UI (label, credential fields, placeholders),
served to the SPA via GET /api/host-types, so adding a host type is one
new module + one registry line — no route or frontend changes.
- Controller host: Linux with
python3+python3-venv, systemd, and network reach to your nodes. ~40 MB disk, ~40 MB RAM, near-zero CPU. - Nodes: Nexus Dashboard
v1.0.0+ (needs
/api/versionand token-aware/api/me, returningrole+version+capabilities). Older nodes will reject enrollment with a 401 — upgrade the node first. - An API token from each node (Nexus Dashboard → System → Users & Tokens). A readonly token is enough to monitor; an admin token is required to control the node or drill in with write access.
Run from the repo directory, as root:
sudo ./install.shThis creates a dedicated unprivileged nexuscontroller user, installs to
/opt/nexus-controller, sets up a venv, writes a hardened systemd unit, and
starts the service on HTTPS :9443 (self-signed cert auto-generated).
Set a known admin password up front (otherwise one is generated and printed to the journal):
sudo CONTROLLER_ADMIN_PASSWORD='choose-a-strong-one' ./install.shConfiguration (environment variables):
| Variable | Default | Meaning |
|---|---|---|
CONTROLLER_DIR |
/opt/nexus-controller |
Install directory |
CONTROLLER_USER |
nexuscontroller |
Service user |
CONTROLLER_SERVICE |
nexus-controller |
systemd unit name |
CONTROLLER_PORT |
9443 |
Listen port |
CONTROLLER_TLS |
1 |
1 = HTTPS, 0 = HTTP (e.g. behind a TLS proxy) |
CONTROLLER_ADMIN_PASSWORD |
(random) | Seed the admin password |
After install, browse to https://<host>:9443 and log in as admin. Get the
generated password with:
journalctl -u nexus-controller | grep -A2 'created initial admin account'Reset it anytime:
sudo -u nexuscontroller /opt/nexus-controller/venv/bin/python \
/opt/nexus-controller/app.py set-password adminThe controller serves HTTPS with a self-signed certificate generated on first
start (using the cryptography lib — no openssl binary required). Replace it
with a real certificate at any time; the key is validated against the cert before
install, and the service must be restarted to apply:
- In the UI — log in as admin → 🔒 Cert → paste your certificate + key (PEM) → Install certificate, then restart the service. (You can also regenerate the self-signed cert here.)
- CLI — ideal for Let's Encrypt renewal hooks or
docker exec:sudo -u nexuscontroller /opt/nexus-controller/venv/bin/python \ /opt/nexus-controller/app.py install-cert /etc/letsencrypt/live/HOST/fullchain.pem \ /etc/letsencrypt/live/HOST/privkey.pem sudo systemctl restart nexus-controllerapp.py cert-infoprints the current cert's subject / issuer / expiry.
Or run HTTP-only (CONTROLLER_TLS=0) behind a reverse proxy that terminates TLS.
The controller containerizes cleanly (it has no host dependencies — no root, no
sudo, no external binaries). A Dockerfile + docker-compose.yml are included.
# from a checkout on the Docker host:
echo "CONTROLLER_BIND_IP=192.168.1.10" > .env # host IP to expose on (optional)
echo "CONTROLLER_ADMIN_PASSWORD=choose-one" >> .env # first run only
docker compose pull && docker compose up -d # prebuilt image (amd64/arm64)
# or build from source instead: docker compose up -d --buildA prebuilt image is published automatically to
ghcr.io/brainchillz/nexuscontroller (:latest plus a pinned sha- tag per
commit) on every push to the GitHub mirror, so pulling is the fastest way to
install or upgrade.
- State persists in the
./databind mount (CONTROLLER_DATA_DIR=/data): the encrypted registry, credentials, audit log, and TLS cert. Back it up by copying that directory. - Runs as a non-root user (uid 10001) with a healthcheck on the SPA root.
- Bind to one interface: set
CONTROLLER_BIND_IPto publish HTTPS only on a specific host IP (default0.0.0.0). - Upgrade:
git pull && docker compose pull && docker compose up -d—./datasurvives. - Migrating an existing install: copy the source controller's
controller-auth.jsonandnodes.jsoninto./data(keep them together — the Fernet key in the auth file decrypts the node tokens),chown 10001:10001 data -R, thendocker compose up -d. - TLS: self-signed by default (swap a real cert via the Cert UI/CLI — see TLS
certificate), or set
CONTROLLER_TLS=0to run HTTP behind a reverse proxy.
install.sh is idempotent — re-run it from a fresh checkout to upgrade in
place. It refreshes the code and dependencies, rewrites the unit, and restarts,
while preserving nodes.json (the encrypted registry) and
controller-auth.json (credentials).
sudo ./uninstall.sh # remove service + dir; back up registry/auth/audit
# to /var/backups, keep the service user
sudo ./uninstall.sh --purge # remove everything incl. the user and all stateThe default backs up your enrolled-node tokens before deleting, so you don't lose the registry by accident.
Add Host → pick a host type, then fill the fields it shows. Test connection validates + pins the host's cert; Enroll saves it. Secrets (API tokens/keys and virtualization passwords) are encrypted at rest and never returned through the API.
- Nexus Dashboard node — name, base URL (e.g.
https://192.168.1.10:8443), API token, optional tags. - Proxmox VE — base URL (
https://host:8006), username (root@pam), password, TLS-verify toggle. - VMware vCenter / ESXi — base URL (
https://host), username (administrator@vsphere.localfor vCenter,rootfor ESXi), password. - TrueNAS (SCALE / CORE) — base URL (
https://host), an API key, and a TLS-verify toggle. Create the key under a user with the Read Only Admin role (Credentials → Users → Roles) — a key without it authenticates but gets403on every call. - Synology DSM — base URL (
https://host:5001), username + password of a local account without 2FA in the administrators group (DSM has no read-only admin role; the controller only ever issues read calls). All DSM volumes appear as pools. - ZimaOS (ZimaCube) — base URL (
http://host— ZimaOS serves plain HTTP on the LAN, so there is no certificate to pin; use an https reverse proxy in front if you want TLS + pinning), username + password of a local ZimaOS account. Storages appear as pools; RAID status, missing/faulty members, and unhealthy disks surface as alerts. - Unraid (7.x) — base URL (
http://host, orhttps://if you've enabled SSL — then the cert is pinned), webGui username + password (e.g.root). The controller drives Unraid's GraphQL API through a cached webGui session; read-only queries only. The parity array and each mounted pool appear as pools; unread Unraid alert/warning notifications count as alerts. - OpenMediaVault — base URL (
http://host, orhttps://with SSL enabled — then pinned), the web-UI admin username + password (OMV's UI login is separate from the box's SSH/system accounts). Managed filesystems appear as pools, mdadm array state folds into pool health, and SMART problems on monitored disks raise alerts. - SparkDash (DGX Spark cluster) — base URL (
https://head-node:7862). An API token is optional: blank enrolls monitor-only (SparkDash reads are public); a token is validated at probe and arms write actions through the proxy. - Nexus Agent — base URL (
https://host:9143) + the token the agent minted on first start (printed by the installer;na_…). Read-only by design — the row shows OS, mounts, load, and uptime, with no Open link. - DNSMAQ-MGR — base URL (
https://host:8443) + a read-only API token (Settings → API Tokens on the instance). Self-signed TLS is pinned on the test-connection, like a Nexus node. The row shows dnsmasq up/down, cache hit ratio, active leases, and the primary/secondary mirror role; Open UI ▸ deep-links to the instance's own web UI.
Virtualization and NAS hosts are polled in the background (default every 60s); their row shows the last poll. Open console ▸ / Open UI ▸ links to the host's own web UI.
The ✎ button on a host row opens an editor — change the display name, base URL, tags, type, or install a new credential (token / API key / password; leave blank to keep the current one). No need to delete and re-enroll.
Changing the base URL or a credential re-probes the node: it re-validates
reachability, re-pins the new certificate, and refreshes the role / version /
capabilities. A failed probe leaves the node unchanged. (API:
PUT /api/nodes/<id> with any of name, tags, type, base_url, token,
username, password.)
Each host is auto-classified from what it actually runs:
- Storage — serving ZFS / SMB / NFS / iSCSI (a TrueNAS appliance classifies here too).
- AI — running
llama.cppwith a model loaded. - Mixed — a meaningful amount of both.
- Virtualization — a Proxmox / vCenter / ESXi host.
The suggestion (type_auto) refreshes each poll. In the Edit dialog the Type
dropdown lets you keep Auto (follow detection) or pin a manual override
(Storage / AI / Mixed / Unknown). Picking Auto un-pins it again.
Controller logins have a role: admin (manage nodes + full control), operator (control, no enroll/remove), viewer (read-only). Write controls are also gated by the node's enrolled token role — a node enrolled with a readonly token shows as read-only.
An operator or viewer can additionally carry scope tags (👥 Users → 🏷): the login is then confined to hosts bearing any of those tags — its fleet view, rollup, history, and actions cover only those hosts, and every other host 404s. Blank scope = whole fleet; admins are always fleet-wide.
Scopes can be named: save a scope preset ("role") — a named tag grouping
like media = nas, docker — and bind users to it from a dropdown instead of
typing tags. Presets resolve at request time, so editing one instantly
re-scopes every user bound to it; deleting a preset is refused while any
login references it.
- No privilege: the service runs as an unprivileged user with no sudo; privileged work happens on the node behind its own auth.
- Tokens encrypted at rest (Fernet) in
nodes.json; never returned via the API. - Per-node TLS cert pinning (trust-on-first-use): the fingerprint is captured at enroll and verified in-handshake on every call (the pin is asserted on the same connection that carries the request); a changed cert fails closed. Background-polled hosts (hypervisors/NAS) pre-check the pin before each poll and additionally support full CA verification (verify-TLS toggle).
- RBAC enforced centrally (viewer can't write; enroll/remove is admin-only).
- Audit log of every controller-side mutation (operator, node, method, path, result) — in addition to the node's own audit.
All under the install dir (/opt/nexus-controller), mode 0600, gitignored:
| File | Contents |
|---|---|
controller-auth.json |
secret key, Fernet key, controller users |
nodes.json |
the node registry (encrypted tokens, cert fingerprints) |
audit.log |
append-only controller audit trail |
certs/ |
auto-generated self-signed TLS cert |
Communication is controller → node only; nodes never call back and store no
reference to the controller. So you can change the controller's IP freely —
nothing to re-enroll — as long as it can still reach the node IPs/ports. (Node IP
changes do matter: a node's base_url is stored in the registry; update it with
PUT /api/nodes/<id> or re-enroll.)
| Method | Path | Notes |
|---|---|---|
POST |
/api/login, /api/logout |
session auth |
GET |
/api/me |
current user + role |
POST |
/api/account/password |
change own password |
GET |
/api/nodes |
list nodes (tokens stripped) |
POST |
/api/nodes |
enroll (admin) |
POST |
/api/nodes/test |
test-connection without enrolling |
PUT |
/api/nodes/<id> |
update name/tags/type/token (admin) |
DELETE |
/api/nodes/<id> |
un-enroll (admin) |
GET |
/api/host-types |
adapter descriptors (drive the Add/Edit modal) |
GET |
/api/fleet/summary |
fan-out rollup (?fresh=1 bypasses cache) |
POST |
/api/fleet/action |
fleet-wide service action (node_ids or tags selector) |
POST |
/api/nodes/<id>/vm/<vm_id>/<action> |
guest start/stop/shutdown/reboot (Proxmox/VMware) |
GET |
/api/nodes/<id>/cert |
pinned vs. now-serving cert fingerprints (admin) |
POST |
/api/nodes/<id>/repin |
accept a changed cert as the new pin (admin) |
* |
/api/nodes/<id>/proxy/<path> |
reverse-proxy to a node's /api/<path> |
GET |
/nodes/<id>/ |
drill-in: the node's SPA, retargeted |
WS |
/nodes/<id>/ws/<path> |
drill-in websocket bridge (node console) |
GET/POST/PUT/DELETE |
/api/users… |
controller login management (admin) |
GET/POST |
/api/notifications (+/test) |
webhook notification config (admin) |
GET |
/api/notifications/events |
recent monitor state transitions (admin) |
GET |
/api/audit |
audit-trail tail, filterable (admin) |
GET |
/api/history/spark |
recent CPU series per host (sparklines) |
GET |
/api/history/summary |
availability % + storage forecast per host |
GET |
/api/history/<id> |
full CPU/mem series for one host |
GET |
/api/tls/info |
current serving certificate metadata |
POST |
/api/tls/regenerate |
regenerate the self-signed cert (admin) |
POST |
/api/tls/cert |
install a supplied cert + key (admin) |
python3 -m venv venv && ./venv/bin/pip install -r requirements-dev.txt
CONTROLLER_TLS=0 ./venv/bin/python app.py # HTTP on :9080 for local dev
./venv/bin/python -m pytest tests/ -qConventions mirror the node app: one Flask app + vanilla-JS SPA, no build step,
atomic JSON writes, esc() on all server text, central RBAC guard.
Implemented: enrollment + in-UI editing + encrypted registry, in-handshake
cert-pinning NodeClient, cached fan-out fleet view, alerts/storage/services
aggregation, fleet-wide service actions (tag-targetable), drill-in
reverse-proxy incl. a websocket bridge for the node's Containers console,
AI/llama status, node-type classification, version-skew warnings, a
self-describing host-adapter package (Proxmox / vCenter / ESXi
virtualization; TrueNAS / Synology / ZimaOS / Unraid / OpenMediaVault NAS;
SparkDash DGX clusters; Linux + Windows agents), guest lifecycle control
(Proxmox/VMware VMs, and LXD instances + Docker containers on nodes), push
notifications (webhooks, debounced state transitions), user management
with tag-scoped RBAC, history + capacity forecasting (sparklines,
days-to-full), certificate review / re-pin, tag filtering, TLS certificate
management, an embedded-gunicorn runtime, and both systemd (install.sh) and
Docker (docker-compose.yml) deployment. The UI matches the Nexus Dashboard
v2 dark-grey/orange theme.
Roadmap: fan-out rate-limiting for very large fleets.