@@ -383,7 +481,7 @@ export function HermesOpsPanel() {
{snapshot.instances.map((instance) => (
-
+
))}
diff --git a/dashboard/web/src/lib/hermes-instance-context.tsx b/dashboard/web/src/lib/hermes-instance-context.tsx
index abfde18..d340c8b 100644
--- a/dashboard/web/src/lib/hermes-instance-context.tsx
+++ b/dashboard/web/src/lib/hermes-instance-context.tsx
@@ -1,6 +1,7 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
+import { useSearchParams } from 'next/navigation';
import type { HermesInstanceFilter } from './hermes';
// Persisted across navigation so the switcher choice sticks. Bumping the key
@@ -36,9 +37,28 @@ export function HermesInstanceProvider({ children }: { children: ReactNode }) {
// agree (avoids hydration mismatch). After mount we read the persisted value.
const [selectedInstance, setSelectedInstanceState] = useState
(DEFAULT_FILTER);
+ // Deep-link hydration: `?instance=vijay` (or `bheem` / `all`) wins over
+ // localStorage on first mount so links from the ops panel can open a pane
+ // pre-filtered to the relevant instance. We don't auto-strip the param —
+ // the URL stays meaningful for back/forward and copy-paste.
+ const searchParams = useSearchParams();
+
useEffect(() => {
+ const fromUrl = searchParams?.get('instance');
+ if (fromUrl && (VALID_FILTERS as string[]).includes(fromUrl)) {
+ const next = fromUrl as HermesInstanceFilter;
+ setSelectedInstanceState(next);
+ try {
+ if (typeof window !== 'undefined') window.localStorage.setItem(STORAGE_KEY, next);
+ } catch {
+ // ignore
+ }
+ return;
+ }
setSelectedInstanceState(readPersisted());
- }, []);
+ // We deliberately depend on the search params object so navigations that
+ // change `?instance=` re-apply.
+ }, [searchParams]);
const setSelectedInstance = useCallback((next: HermesInstanceFilter) => {
setSelectedInstanceState(next);
diff --git a/docs/hermes_dashboard_v2_roadmap.md b/docs/hermes_dashboard_v2_roadmap.md
index 21748cb..05c71e4 100644
--- a/docs/hermes_dashboard_v2_roadmap.md
+++ b/docs/hermes_dashboard_v2_roadmap.md
@@ -132,12 +132,12 @@ This is the biggest operational asymmetry and the reason half the ops-panel warn
## Phase 6 — Mission Control UX polish (G6)
-- [ ] Severity-tag warnings (info/warn/critical) and add a severity filter to the ops panel.
-- [ ] Trend cards: alert volume and backup-freshness across recent refreshes (per instance).
-- [ ] Deep links from the ops panel → Task Ledger filtered to the relevant instance/most-recent work.
-- [ ] Per-instance action rows beyond copy-link/open-dashboard: open-runbook, copy SSH/tunnel command, "how to restart this gateway".
-- [ ] Optional dark/light theme toggle if the shell supports it.
-- [ ] Unified alerts feed across both instances on the overview.
+- [x] Severity-tag warnings (info/warn/critical) and add a severity filter to the ops panel. *(`RecentAlerts` component classifies each warning by leading token (CRITICAL/ERROR/FATAL → critical; INFO/OK → info; default → warn) and renders a colour-coded badge; a per-severity radiogroup filter sits in the panel header with live counts. UI-only — no backend contract change.)*
+- [ ] Trend cards: alert volume and backup-freshness across recent refreshes (per instance). *(Deferred — needs client-side history persistence across refreshes; not enough value yet to justify the localStorage state machine.)*
+- [x] Deep links from the ops panel → Task Ledger filtered to the relevant instance/most-recent work. *(Per-instance "View tasks" button on each ops-panel `InstanceCard` links to `/hermes/tasks?instance=`. `HermesInstanceProvider` now hydrates from the `?instance=` URL param on mount (winning over the persisted localStorage selection) and keeps the param meaningful for back/forward + copy-paste.)*
+- [x] Per-instance action rows beyond copy-link/open-dashboard: open-runbook, copy SSH/tunnel command, "how to restart this gateway". *(InstanceCard now exposes "Copy SSH command" (Tailscale-scoped: `tailscale ssh root@` for Vijay, `tailscale ssh uma@` for Bheem — never raw `ssh`), "View tasks" deep link, and "Open runbook" pointing at `docs/hermes-operations.md`. "How to restart this gateway" is intentionally a runbook link rather than a button — restarting is a privileged action that should go through the runbook, not the dashboard.)*
+- [ ] Optional dark/light theme toggle if the shell supports it. *(Deferred — design system uses CSS custom properties throughout (`var(--bl-*)`) so a toggle is feasible, but the shell doesn't expose a switch primitive yet.)*
+- [ ] Unified alerts feed across both instances on the overview. *(Partially achieved by `recentAlerts` + the new severity filter on the ops panel; full per-instance roll-up of telemetry watchdog alerts is queued behind a UI consumer for the new `/api/hermes/telemetry/:instance` endpoint.)*
## Phase 7 — Security & access (G8)
@@ -192,8 +192,8 @@ Update only with evidence (source review, tests, build output, or browser/VM ver
- [x] Phase 3 — Real telemetry ingestion + Products pane converted (Task Ledger / Agents / History deferred — depend on JSONL session pipeline, see Phase 3 notes)
- [ ] Phase 4 — Bheem/Uma parity (backup, watchdog, restore drill)
- [x] Phase 5 — App/CI hardening (P0/P1/P2 done; P2 follow-ups in DEPLOYMENT.md mitigation roadmap remain)
-- [ ] Phase 6 — UX polish
-- [ ] Phase 7 — Security & access
+- [x] Phase 6 — UX polish (severity tags + deep links + per-instance actions; trend cards + theme toggle deferred)
+- [x] Phase 7 — Security & access (auth on hermes routes + privacy stance documented; redact_secrets/redact_pii decision deferred)
- [ ] Phase 8 — Notifications & Telegram
## Decisions (resolved 2026-05-30)