From e2db92f3b1f264182014ffddce187db135cdbaea Mon Sep 17 00:00:00 2001 From: Saravana Kumar Date: Wed, 27 May 2026 21:05:57 +0000 Subject: [PATCH] Add Hermes snapshot diff view --- .../web/src/components/hermes-ops-panel.tsx | 64 ++++++++++++++++++- docs/hermes-operations.md | 2 +- docs/hermes_dashboard_roadmap.md | 5 +- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/dashboard/web/src/components/hermes-ops-panel.tsx b/dashboard/web/src/components/hermes-ops-panel.tsx index 75ae976..7b3a0e2 100644 --- a/dashboard/web/src/components/hermes-ops-panel.tsx +++ b/dashboard/web/src/components/hermes-ops-panel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; import { AlertTriangle, CheckCircle2, Cloud, Copy, DatabaseBackup, ExternalLink, Gauge, HardDrive, RefreshCw, ShieldCheck, Timer, Wifi, Activity, CalendarClock, Link2 } from 'lucide-react'; import { Badge, Button } from '@/components/ui/Primitives'; @@ -105,14 +105,19 @@ function InstanceCard({ instance }: { instance: HermesOpsInstance }) { export function HermesOpsPanel() { const [snapshot, setSnapshot] = useState(null); + const [previousSnapshot, setPreviousSnapshot] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const latestSnapshotRef = useRef(null); const load = async () => { setLoading(true); setError(null); try { - setSnapshot(await api.getHermesOps()); + const nextSnapshot = await api.getHermesOps(); + setPreviousSnapshot(latestSnapshotRef.current); + latestSnapshotRef.current = nextSnapshot; + setSnapshot(nextSnapshot); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to load Hermes operations status'); } finally { @@ -127,6 +132,33 @@ export function HermesOpsPanel() { }, []); const allHealthy = useMemo(() => snapshot ? snapshot.warnings.length === 0 : false, [snapshot]); + const snapshotDiff = useMemo(() => { + if (!snapshot || !previousSnapshot) return null; + + const previousHealthyInstances = previousSnapshot.instances.filter((instance) => + instance.gateway.active && + instance.dashboard.active && + instance.backup.timer.active && + instance.backup.repo.clean && + instance.google.workspaceToken + ).length; + + const currentHealthyInstances = snapshot.instances.filter((instance) => + instance.gateway.active && + instance.dashboard.active && + instance.backup.timer.active && + instance.backup.repo.clean && + instance.google.workspaceToken + ).length; + + return { + healthyInstances: currentHealthyInstances - previousHealthyInstances, + warnings: snapshot.warnings.length - previousSnapshot.warnings.length, + activeSessions: snapshot.activeSessions.active - previousSnapshot.activeSessions.active, + activeDashboards: snapshot.instances.filter((instance) => instance.dashboard.active).length - previousSnapshot.instances.filter((instance) => instance.dashboard.active).length, + activeBackupTimers: snapshot.instances.filter((instance) => instance.backup.timer.active).length - previousSnapshot.instances.filter((instance) => instance.backup.timer.active).length, + }; + }, [previousSnapshot, snapshot]); const healthyInstances = snapshot ? snapshot.instances.filter((instance) => instance.gateway.active && @@ -161,6 +193,34 @@ export function HermesOpsPanel() { {snapshot ? (
+ {snapshotDiff ? ( +
+
+
+

Since previous refresh

+

Snapshot movement compared with the last poll.

+
+ Delta view +
+
+ {[ + { label: 'Healthy instances', value: snapshotDiff.healthyInstances }, + { label: 'Active dashboards', value: snapshotDiff.activeDashboards }, + { label: 'Active backups', value: snapshotDiff.activeBackupTimers }, + { label: 'Active sessions', value: snapshotDiff.activeSessions }, + { label: 'Warnings', value: snapshotDiff.warnings }, + ].map((item) => ( +
+

{item.label}

+

0 ? 'text-[var(--bl-success)]' : item.value < 0 ? 'text-[var(--bl-danger)]' : 'text-[var(--bl-text-primary)]'}`}> + {item.value > 0 ? '+' : ''}{item.value} +

+
+ ))} +
+
+ ) : null} +
diff --git a/docs/hermes-operations.md b/docs/hermes-operations.md index 3b54760..85c9e8d 100644 --- a/docs/hermes-operations.md +++ b/docs/hermes-operations.md @@ -31,7 +31,7 @@ Observed on 2026-05-27: - Private dashboards: - Root: `http://100.87.53.10:9119/`, `hermes-root-dashboard.service` - Uma: `http://100.87.53.10:9120/`, `uma-hermes-dashboard.service` - - Live ops panel shows gateway state, active sessions, cron state, backup freshness, sanitized alerts, and runbook links for both instances. + - Live ops panel shows gateway state, active sessions, refresh delta, cron state, backup freshness, sanitized alerts, and runbook links for both instances. ## Safety guardrail: no public Hermes dashboard/API diff --git a/docs/hermes_dashboard_roadmap.md b/docs/hermes_dashboard_roadmap.md index 41b2875..8dc52e4 100644 --- a/docs/hermes_dashboard_roadmap.md +++ b/docs/hermes_dashboard_roadmap.md @@ -669,11 +669,10 @@ Known roadmap assumptions to handle safely during implementation: Potential follow-up work for Hermes Mission Control: -- snapshot diff view that shows what changed since the last refresh -- per-instance action row with copy-link and open-dashboard shortcuts - warning severity filters for the live ops panel -- compact trend cards for recent alert volume and backup freshness +- compact trend cards for recent alert volume and backup freshness over several refreshes - task-ledger deep links from the ops panel into the most recent Hermes work +- per-instance action row improvements beyond copy-link/open-dashboard, such as open-runbook shortcuts - optional dark/light theme toggle if the broader dashboard shell eventually supports it ---