Dashboard Plugin Surface
In addition to the standalone HTML dashboard (dashboard/serve.py, port 8765),
hermes-telemetry also ships as a plugin for the official Hermes web
dashboard. The two surfaces co-exist; they share the SQLite DB but zero
Python code.
File layout (verified against the Hermes loader)
Section titled “File layout (verified against the Hermes loader)”The Hermes dashboard discovers plugins by scanning
~/.hermes/plugins/<name>/dashboard/manifest.json
(hermes_cli/web_server.py::_discover_dashboard_plugins, lines 11333-11434
of NousResearch/hermes-agent@main). Because this repo is cloned into
~/.hermes/plugins/hermes-telemetry/, the plugin files MUST live inside
dashboard/ — the same directory as serve.py and index.html. The
discovery rule is non-negotiable.
dashboard/├── serve.py ← standalone surface (stdlib http.server, port 8765)├── index.html ← standalone SPA (vendored Chart.js, CDN fallback)├── manifest.json ← plugin manifest├── plugin_api.py ← plugin backend (FastAPI APIRouter; loaded by Hermes)└── dist/index.js ← plugin frontend (IIFE; no build step)Why a single file for the plugin backend
Section titled “Why a single file for the plugin backend”The Hermes loader imports plugin_api.py via
importlib.util.spec_from_file_location(module_name, api_path)
(hermes_cli/web_server.py:11856-11881). The module is not registered
as part of any package, so relative imports (from . import _db) fail at
load time. Hence plugin_api.py is self-contained — all DB helpers,
budget math, and FastAPI routes live in one file. No _db.py sibling.
Why the standalone and plugin share no code
Section titled “Why the standalone and plugin share no code”The user contract is: the standalone dashboard MUST keep working unchanged
when the plugin evolves. Co-locating in dashboard/ was forced by the
loader, but the two are independent products:
| Surface | Entry point | Server | Auth |
|---|---|---|---|
| Standalone | dashboard/serve.py |
stdlib BaseHTTPRequestHandler |
none (loopback default) |
| Plugin | dashboard/plugin_api.py |
Hermes FastAPI app | Hermes session cookie |
tests/test_dashboard_plugin_isolation.py enforces:
plugin_api.pydoes not importserve(in any module form).serve.pydoes not importplugin_api.- No third Python file in
dashboard/is imported by both surfaces (would re-couple them). - Manifest
versionequals the package__version__(lockstep release).
Read-only DB access
Section titled “Read-only DB access”plugin_api.py opens telemetry.db with PRAGMA query_only=ON. The
plugin is observability-only — capture still flows through the runtime
hooks (__init__.py). The test test_db_connection_is_read_only asserts
that any INSERT via the plugin connection raises OperationalError.
Manifest contract
Section titled “Manifest contract”{ "name": "hermes-telemetry", "label": "Telemetry", "icon": "Activity", "version": "<must equal __version__>", "tab": { "path": "/telemetry", "position": "after:analytics" }, "slots": ["sessions:top", "cron:top", "header-right", "analytics:bottom"], "entry": "dist/index.js", "api": "plugin_api.py"}Verified fields:
tab.positionaccepts"end","after:<path>","before:<path>"(web_server.py:11382-11391).slotsis documentation only. The real binding happens in the JS bundle viawindow.__HERMES_PLUGINS__.registerSlot(...)(extending-the-dashboard.md § Augmenting built-in pages).apiis validated by_safe_plugin_api_relpath(web_server.py:11296-11330); absolute paths or..traversal cause backend mount to be skipped (the static assets still load). This is fix for GHSA-5qr3-c538-wm9j.
Slot catalogue (verified 2026-08-24)
Section titled “Slot catalogue (verified 2026-08-24)”The shell renders <PluginSlot name="..." /> only for the names below. Source:
website/docs/user-guide/features/extending-the-dashboard.md § Slot catalogue
(upstream lines 577-604 at time of writing — prefer the section heading over the
line numbers, which move).
Shell-wide — render anywhere in the app chrome:
| Slot | Location |
|---|---|
backdrop |
Inside the <Backdrop /> layer stack, above the noise layer. |
header-left |
Before the Hermes brand in the top bar. |
header-right |
Before the theme/language switchers in the top bar. |
header-banner |
Full-width strip below the nav. |
sidebar |
Cockpit sidebar rail — only rendered when layoutVariant === "cockpit". |
pre-main |
Above the route outlet (inside <main>). |
post-main |
Below the route outlet (inside <main>). |
footer-left / footer-right |
Footer cell content (replaces the default). |
overlay |
Fixed-position layer above everything else. |
Page-scoped — :top and :bottom on each built-in page:
sessions:* · analytics:* · logs:* · cron:* · skills:* · config:* ·
env:* · docs:* · chat:*
logs:top sits above the filter toolbar and logs:bottom below the log viewer;
docs:top sits above the iframe; chat:* is only active when embedded chat is
enabled. Everything else is the plain top/bottom of the page content area.
This list grew. Until 2026-08-24 this section recorded four slots and called them “the entire verified catalogue”. That was true when written and silently stopped being true. Treat any slot list in this repo as a snapshot, re-verify against the upstream section before designing around it, and update the date above when you do.
Slot widgets we register
Section titled “Slot widgets we register”| Slot | Widget |
|---|---|
sessions:top |
Last-run summary card (cost · tokens · model). |
cron:top |
7-day cron cost + failure badge. |
header-right |
24h spend + budget level (variant=destructive on hard breach). |
analytics:bottom |
Daily cost line chart (vendored Chart.js served locally; CDN fallback only). |
Four of the twenty-eight available slots. manifest.json lists the same four,
but that field is documentation only — the binding happens in dist/index.js
via registerSlot().
Slot names are NOT free-form — verify before adding new ones
Section titled “Slot names are NOT free-form — verify before adding new ones”The shell only renders slots whose names appear in the catalogue above.
Registering an unknown slot via registerSlot() is a silent no-op: the widget
loads but nothing on the page ever mounts it. Do not invent slot names —
alerts:top and warnings:top, for instance, do not exist.
If you need a visible surface and no slot fits, render the widget inside
TelemetryPage (the plugin’s own tab) — that page is fully under our control.
Revisit candidate. The free→paid transitions widget renders inside
TelemetryPageon the recorded rationale that “no shell slot fits a tier change surface”.header-banner— a full-width strip below the nav — now exists and is plausibly that surface. The decision predates it; re-evaluate before assuming it still holds.
Re-verify the catalogue against upstream before adding a slot:
https://raw.githubusercontent.com/NousResearch/hermes-agent/main/website/docs/user-guide/features/extending-the-dashboard.mdNote the path: the doc lives under website/docs/user-guide/features/, not
docs/. A wrong path here 404s silently, which is exactly how the old
four-slot list survived long past its expiry.
The SDK does not currently expose an useActiveSession hook, so
sessions:top shows the most recent run instead of the per-row session.
When that hook lands upstream, swap the implementation — the backend
endpoint /session/{session_id} is already in place.
Chart.js delivery
Section titled “Chart.js delivery”Chart.js is vendored locally at dashboard/vendor/chart.umd.min.js and served
by the standalone dashboard itself first. index.html falls back to
cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js only if the local
asset fails to load. This removes the main failure mode behind Chart is not defined on offline, filtered, or flaky networks while keeping a fallback path
for manual recovery.
The dashboard still has no build step — the vendored asset is a checked-in static file, not a bundler output.
Update path
Section titled “Update path”Both surfaces are upgraded with a single git pull in
~/.hermes/plugins/hermes-telemetry. The manifest version is pinned to
__version__ by test_plugin_version_matches_package, so a release tag
implicitly ships both surfaces in lockstep.
hermes-telemetry is an independent community plugin for Hermes Agent. It is not affiliated with or endorsed by Nous Research.