* feat(dashboard): MCP catalog + enable/disable, webhook toggle, hook create/delete, system stats
Backend for the comprehensive admin pass:
- MCP: GET /api/mcp/catalog (browse Nous-approved optional-mcps), POST
/api/mcp/catalog/install, PUT /api/mcp/servers/{name}/enabled
- Webhooks: PUT /api/webhooks/{name}/enabled; gateway rejects disabled routes
with 403 (hot-reloaded, no restart)
- Hooks: POST/DELETE /api/ops/hooks — create (with consent approval) + remove;
list now reports accurate allowlist status + valid events
- System: GET /api/system/stats — OS/arch/python/cpu + psutil memory/disk/
uptime/process, stdlib fallback
All gated by dashboard auth; secrets never returned.
* feat(dashboard): MCP catalog UI, enable/disable toggles, hook create, system stats
- McpPage: catalog section (browse Nous-approved MCPs, one-click install with
env prompts) + per-server enable/disable toggle with gateway-restart note
- WebhooksPage: per-subscription enable/disable toggle (muted + badge when off)
- SystemPage: new Host stats section (OS/arch/python/cpu/mem/disk/uptime/load),
shell-hook create modal + delete, 'Create backup' label
- api.ts: client methods + types for catalog, toggles, hook CRUD, system stats
* test(dashboard): cover catalog, toggles, hook CRUD, system stats, webhook toggle
Adds tests for the comprehensive pass: MCP enable/disable + catalog list +
catalog-install-unknown, hook create/delete with consent, system stats shape,
and webhook enable/disable. 26 tests total, all green.
* docs(dashboard): document the comprehensive admin pass + fresh screenshots
Updates the MCP/Webhooks/Pairing/System sections for catalog browse+install,
enable/disable toggles, hook creation, and host system stats; adds the new
endpoints to the API table; replaces the screenshots with live captures of
the rebuilt pages (real data, no dummies) including the hook-create modal.
* feat(dashboard): curator, portal status, and prompt-size/dump/migrate ops
Closes the last in-scope CLI gaps from the coverage audit:
- Curator: GET /api/curator (status), PUT /api/curator/paused, POST
/api/curator/run (background)
- Portal: GET /api/portal (Nous auth + Tool Gateway routing, read-only)
- Diagnostics: POST /api/ops/prompt-size, /api/ops/dump, /api/ops/config-migrate
(backgrounded, tailed via action status)
Host-bound commands (secrets/proxy/lsp/acp/computer-use/desktop/completion/
postinstall/uninstall/claw) remain CLI-only by design.
* feat(dashboard): curator + portal + diagnostics UI, tests
- SystemPage: Nous Portal status section (auth + Tool Gateway routing),
Skill curator card (status + pause/resume + run now), and three new
Operations buttons (prompt size, support dump, migrate config)
- api.ts: client methods + CuratorStatus/PortalStatus types
- tests: curator pause/resume, portal shape, system-stats shape, + auth-gate
coverage for the new GET endpoints (31 tests total)
* docs(dashboard): document curator, portal, and diagnostics + refresh System screenshots
Updates the System section for the Nous Portal status, Skill curator
controls, and the new prompt-size/dump/migrate operations; adds them to the
API table; refreshes the System screenshots (now showing Portal + Curator)
and adds a dedicated curator/gateway/memory capture.
* feat(dashboard): session stats/export/prune + skills hub search endpoints
Completes the existing tabs' backend depth (audit vs CLI):
- Sessions: GET /api/sessions/stats (store stats), GET /api/sessions/{id}/export,
POST /api/sessions/prune. /stats is registered before /{session_id} so the
literal path isn't captured by the parameterized route.
- Skills: GET /api/skills/hub/search — parallel multi-source hub search (threaded),
returns installable identifiers
- (rename via PATCH and cron-edit via PUT already existed; now surfaced in UI)
* feat(dashboard): complete existing tabs — sessions mgmt, skills hub browse, cron edit
Audited every existing tab against its CLI command and filled the gaps:
- Sessions: store stats bar, per-row rename + export (JSON download), and a
prune-old-sessions control (mirrors hermes sessions rename/export/prune/stats)
- Skills: new 'Browse hub' view — search the skill hub across all sources,
install by identifier with a live install log, and 'Update all' (mirrors
hermes skills search/install/update)
- Cron: per-job Edit modal (pre-filled) calling updateCronJob (hermes cron edit)
- api.ts: renameSession/getSessionStats/exportSessionUrl/pruneSessions,
updateCronJob, searchSkillsHub + types
Models tab was already comprehensive (provider+model picker, dynamic per-provider
lists, main + all 11 aux-task assignments, reset) — verified, no change needed.
* test(dashboard): cover session stats/rename/export/prune + skills hub search
Adds the route-shadowing guard for /api/sessions/stats (must not be captured
by /api/sessions/{session_id}), rename/export/prune, and the empty-query
short-circuit for hub search. 36 tests total, all green.
* docs(dashboard): document enhanced Sessions, Skills hub, and Cron edit
Sessions: stats bar, rename, export, prune (+ screenshot). Skills: new Browse
hub view for search/install/update (+ screenshot). Cron: edit action. API
table updated with the new endpoints.
Hermes Agent — Web UI
Browser-based dashboard for managing Hermes Agent configuration, API keys, and monitoring active sessions.
Stack
- Vite + React 19 + TypeScript
- Tailwind CSS v4 with custom dark theme
- shadcn/ui-style components (hand-rolled, no CLI dependency)
Development
# Start the backend API server
cd ../
python -m hermes_cli.main web --no-open
# In another terminal, start the Vite dev server (with HMR + API proxy)
cd web/
npm install
npm run dev
Open the Vite URL printed in the terminal (usually http://localhost:5173). That is the live-reload UI.
hermes dashboard on port 9119 serves the built bundle from hermes_cli/web_dist/, not the Vite dev server — changes in web/src/ will not appear there until you run npm run build and restart the dashboard (or use web --no-open + Vite as above).
The Vite dev server proxies /api requests to http://127.0.0.1:9119 (the FastAPI backend).
Build
npm run build
This outputs to ../hermes_cli/web_dist/, which the FastAPI server serves as a static SPA. The built assets are included in the Python package via pyproject.toml package-data.
Structure
src/
├── components/ui/ # Reusable UI primitives (Card, Badge, Button, Input, etc.)
├── lib/
│ ├── api.ts # API client — typed fetch wrappers for all backend endpoints
│ └── utils.ts # cn() helper for Tailwind class merging
├── pages/
│ ├── StatusPage # Agent status, active/recent sessions
│ ├── ConfigPage # Dynamic config editor (reads schema from backend)
│ └── EnvPage # API key management with save/clear
├── App.tsx # Main layout and navigation
├── main.tsx # React entry point
└── index.css # Tailwind imports and theme variables
Typography & contrast rules
Read before adding or editing UI styles. These rules keep the dashboard legible across all built-in themes and stop drift back into the patterns the design system was just refactored out of.
Text size floor
- Minimum body size:
text-xs(12px / 0.75rem). Do not use arbitrarytext-[0.6rem],text-[0.65rem],text-[9px],text-[10px], ortext-[11px]on copy, hints, labels, counts, or badges. Use the standard scale:text-xs,text-sm,text-base. - Smaller sizes are only acceptable on decorative overlays (chart stripes, empty-state icons) — never on text the user is meant to read.
Opacity floor on text
- Never apply opacity below 0.7 to text. No
opacity-30,opacity-50,opacity-60on<span>s,<p>s, labels, etc. - Do not stack opacity tokens. Patterns like
text-muted-foreground/60,text-midground/70,text-foreground/50create unpredictable WCAG failures because the parent token already has alpha. - Use the semantic text tokens from
@nous-research/ui'sglobals.css:text-text-primary— default body text.text-text-secondary— subtitles, meta, inactive nav.text-text-tertiary— small chrome labels, counts, footnotes.text-text-disabled— disabled states.text-text-on-accent— text on filled accent surfaces.
Brand uppercase via text-display, not raw uppercase
- The dashboard preserves the Nous brand uppercase aesthetic, but it is opt-in per element, not global.
- Apply uppercase via the DS utility
text-displayon brand chrome only — page titles, nav section headings, badges, brand wordmark. DS components (Button,Badge,Tabs,Segmented, etc.) already self-applytext-display. - Do not introduce new
uppercase(the literal Tailwind class) inhermes-agent/web/src. Prefertext-displayfor new brand chrome. Legacyuppercasecall sites (e.g.components/ui/label.tsx,card.tsx) remain until migrated. - The app shell no longer forces uppercase globally, so blanket
normal-caseopt-outs are unnecessary. Usenormal-caseonly where a DS component appliestext-displaybut the label should stay sentence case — e.g. dynamic user content (model slugs, theme names) or fixed UI copy that is not brand chrome (EnvPage “not configured” toggle, sidebar “New chat”).
Fonts
Typography is opt-in per surface, not global on layout shells — the app shell and page header keep their original theme/expanded fonts; Mondwest applies only where explicitly set.
| Tier | Classes | Use for |
|---|---|---|
| Brand chrome | font-mondwest text-display (or themedChrome) |
Sidebar nav, card section headers (CardTitle), Segmented filter buttons, filter panel headings |
| Themed body | font-mondwest normal-case (or themedBody) |
Card content (Card, CardDescription), session/platform rows, analytics tables — scoped to the component |
| Page chrome | font-expanded |
Page header h1 (PageHeaderProvider) — sentence case, not text-display |
| Wordmark | Typography + size/tracking only |
Sidebar/mobile “Hermes Agent” — mixed case, no Mondwest, no text-display |
| Technical | font-mono-ui / font-mono / font-courier |
Model slugs, env keys, schedules, YAML, repo URLs |
- Do not put
themedBodyorthemedFonton<main>,App, or other layout wrappers — it overrides component-scoped styles. CardappliesthemedBody;CardTitleusestext-display(uppercase chrome);CardDescriptionusesthemedBody.NouiTypographydefaults tofont-sansunless a font prop is passed.- Do not use raw
font-sansorfont-display(theme sans variable) on new dashboard UI — prefer Mondwest tiers above where brand-appropriate.
Color tokens
- Prefer semantic tokens (
text-text-*,bg-card,border-border,text-foreground,text-destructive,text-success,text-warning) over raw layer references (text-midground,text-foreground). text-muted-foregroundis now wired to--color-text-secondary, so existing call sites stay correct, but new code should prefer the semantic name.- When you genuinely need a non-token color (icon de-emphasis on a chart, terminal foreground via inline style), keep alpha at
≥ 0.7for any text.