Contract V1 of nous-account-service PR #180 ships no refresh tokens, so the original Phase 6 silent-refresh design is replaced with a thinner '401 → redirect to /login' UX. The dashboard's gated middleware now emits a structured envelope on any auth failure; the SPA's fetch wrapper sees it and full-page-navigates the user through re-auth. hermes_cli/dashboard_auth/cookies.py: set_session_cookies(refresh_token='') SKIPS writing the hermes_session_rt cookie. Forward-compat: a non-empty refresh_token still emits the cookie unchanged, so a future Portal contract that starts issuing RTs flips the persistence on with no other change. clear_session_cookies still emits a Max-Age=0 deletion for the RT cookie so stale cookies from earlier deployments get flushed on logout / session expiry. Deprecation marker + rationale in module docstring per the user's docstring-only deprecation pattern. hermes_cli/dashboard_auth/middleware.py: _unauth_response now builds a structured JSON envelope for API 401s: { error: 'session_expired' | 'unauthenticated', detail: 'Unauthorized', reason: <internal>, login_url: '/login?next=<safe-path>' } HTML redirects also carry next= so a user landing on /sessions without a cookie bounces back to /sessions after re-auth. _safe_next_target validates same-origin: drops protocol-relative paths (//evil.com), absolute URLs, and any /login or /auth/* loop. Dead cookies are cleared on the 401 path so the browser stops replaying invalid tokens. hermes_cli/dashboard_auth/routes.py: /auth/callback accepts next= query param and validates via _validate_post_login_target (same rules as the gate's _safe_next_target — defence-in-depth because next= survived a full IDP round trip and attacker-controlled state can re-enter via the callback URL). Open-redirect attempts land at '/' instead. web/src/lib/api.ts: fetchJSON parses the 401 envelope and full-page-navigates to body.login_url ONLY on the known session-expiry error codes. Domain-level 401s (e.g. permission errors) bubble up as regular errors. credentials: 'include' added so cookie auth works for all fetches routed through this wrapper. sessionStorage.lastLocation is preserved for future use by AuthWidget / hermes_status. Test files marked with pytest.mark.xdist_group so the four files that mutate web_server.app.state.auth_required serialize onto the same xdist worker — eliminates 'works locally, fails in CI' app-state bleed. 20 new tests in test_dashboard_auth_401_reauth.py: - set_session_cookies(refresh_token='') skips RT cookie - clear_session_cookies still emits RT deletion - 401 envelope shape (unauthenticated vs session_expired) - dead cookie cleared on invalid-token 401 - login_url carries next= for deep paths - login loop avoided when path is /login/auth/api-auth - protocol-relative URL rejected - _safe_next_target unit tests (accept same-origin, reject loops/abs) - /auth/callback respects safe next= but rejects open redirects 2 pre-existing tests updated to accept the new /login?next=%2F shape. Full dashboard-auth suite: 168 passed, 1 skipped (Phase 0 pre-existing). |
||
|---|---|---|
| .. | ||
| public | ||
| src | ||
| eslint.config.js | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
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.