From 90f6db20144c1bd86500d3e7a705dcdd047ee898 Mon Sep 17 00:00:00 2001 From: Saravana Kumar Date: Wed, 27 May 2026 20:53:47 +0000 Subject: [PATCH] Complete Hermes ops dashboard and roadmap --- .../src/modules/hermes-ops/repository.ts | 52 +++++++++++- .../backend/src/modules/hermes-ops/types.ts | 23 ++++++ dashboard/web/e2e/dashboard.spec.ts | 9 +++ dashboard/web/e2e/hermes.spec.ts | 13 +++ .../web/src/components/hermes-ops-panel.tsx | 81 ++++++++++++++++++- dashboard/web/src/lib/api.ts | 23 ++++++ docs/hermes-operations.md | 1 + docs/hermes-setup-upgrade-roadmap.md | 18 ++--- docs/hermes_dashboard_roadmap.md | 2 +- 9 files changed, 210 insertions(+), 12 deletions(-) diff --git a/dashboard/backend/src/modules/hermes-ops/repository.ts b/dashboard/backend/src/modules/hermes-ops/repository.ts index 9b95255..039486a 100644 --- a/dashboard/backend/src/modules/hermes-ops/repository.ts +++ b/dashboard/backend/src/modules/hermes-ops/repository.ts @@ -2,7 +2,7 @@ import { execFile } from 'child_process'; import { promisify } from 'util'; import { readFile, stat } from 'fs/promises'; import { existsSync } from 'fs'; -import type { HermesOpsInstance, HermesOpsRepo, HermesOpsSnapshot, HermesOpsTimer } from './types.js'; +import type { HermesOpsCronJob, HermesOpsInstance, HermesOpsRepo, HermesOpsSnapshot, HermesOpsTimer } from './types.js'; const execFileAsync = promisify(execFile); @@ -148,10 +148,20 @@ async function getTailscaleIp(): Promise { return output?.split('\n')[0] || null; } +async function getActiveHermesSessionCount(): Promise { + const output = await run('ps', ['-ef']); + if (!output) return 0; + return output + .split('\n') + .filter((line) => line.includes('hermes_cli.main') && !line.includes('gateway') && !line.includes('grep')) + .length; +} + export async function getHermesOpsSnapshot(): Promise { const tailscaleIp = await getTailscaleIp(); const warnings: string[] = []; const emergencyDriveUpload = await getTimer('hermes-emergency-drive-upload.timer'); + const activeSessions = await getActiveHermesSessionCount(); const results: HermesOpsInstance[] = []; for (const item of instances) { @@ -210,10 +220,50 @@ export async function getHermesOpsSnapshot(): Promise { warnings.push('Emergency Drive OAuth token is missing'); } + const cronJobs: HermesOpsCronJob[] = [ + { + name: emergencyDriveUpload.name, + label: 'Emergency Drive upload', + active: emergencyDriveUpload.active, + nextRun: emergencyDriveUpload.nextRun, + lastRun: emergencyDriveUpload.lastRun, + }, + ...results.map((instance) => ({ + name: instance.backup.timer.name, + label: `${instance.label} backup`, + active: instance.backup.timer.active, + nextRun: instance.backup.timer.nextRun, + lastRun: instance.backup.timer.lastRun, + })), + ]; + return { generatedAt: new Date().toISOString(), tailscaleIp, emergencyDriveUpload, + activeSessions: { + active: activeSessions, + updatedAt: new Date().toISOString(), + }, + cronJobs, + recentAlerts: warnings.slice(0, 6), + quickLinks: [ + { + label: 'Hermes operations', + href: 'https://github.com/saravanakumardb/learning_ai_devops_tools/blob/main/docs/hermes-operations.md', + description: 'Runbook for gateways, backups, fallbacks, and recovery.', + }, + { + label: 'Disaster recovery', + href: 'https://github.com/saravanakumardb/learning_ai_devops_tools/blob/main/docs/hermes-disaster-recovery.md', + description: 'Restore and rebuild steps for a fresh VM.', + }, + { + label: 'Setup roadmap', + href: 'https://github.com/saravanakumardb/learning_ai_devops_tools/blob/main/docs/hermes-setup-upgrade-roadmap.md', + description: 'Tracked rollout, security, and workflow checklist.', + }, + ], instances: results, warnings, }; diff --git a/dashboard/backend/src/modules/hermes-ops/types.ts b/dashboard/backend/src/modules/hermes-ops/types.ts index 1a5cb37..410b7aa 100644 --- a/dashboard/backend/src/modules/hermes-ops/types.ts +++ b/dashboard/backend/src/modules/hermes-ops/types.ts @@ -42,10 +42,33 @@ export interface HermesOpsInstance { google: HermesOpsGoogle; } +export interface HermesOpsSessionSummary { + active: number; + updatedAt: string | null; +} + +export interface HermesOpsCronJob { + name: string; + label: string; + active: boolean; + nextRun: string | null; + lastRun: string | null; +} + +export interface HermesOpsLink { + label: string; + href: string; + description: string; +} + export interface HermesOpsSnapshot { generatedAt: string; tailscaleIp: string | null; emergencyDriveUpload: HermesOpsTimer; + activeSessions: HermesOpsSessionSummary; + cronJobs: HermesOpsCronJob[]; + recentAlerts: string[]; + quickLinks: HermesOpsLink[]; instances: HermesOpsInstance[]; warnings: string[]; } diff --git a/dashboard/web/e2e/dashboard.spec.ts b/dashboard/web/e2e/dashboard.spec.ts index 585a96d..ef9367a 100644 --- a/dashboard/web/e2e/dashboard.spec.ts +++ b/dashboard/web/e2e/dashboard.spec.ts @@ -90,6 +90,15 @@ test.describe('DevOps Dashboard', () => { await expect(refreshButton).toBeEnabled(); await expect(page.getByRole('heading', { name: 'Investment Trading' })).toBeVisible(); }); + + test('renders the dashboard at mobile width', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + + await page.goto('/'); + await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); + await expect(page.getByRole('button', { name: /create service/i })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Investment Trading' })).toBeVisible(); + }); }); test('login page renders the platform credential form without baked-in credentials', async ({ page }) => { diff --git a/dashboard/web/e2e/hermes.spec.ts b/dashboard/web/e2e/hermes.spec.ts index db01be5..c7faad3 100644 --- a/dashboard/web/e2e/hermes.spec.ts +++ b/dashboard/web/e2e/hermes.spec.ts @@ -52,4 +52,17 @@ test.describe('Hermes Mission Control', () => { await page.goto('/hermes/settings'); await expect(page.getByRole('heading', { name: 'Settings & Configuration' })).toBeVisible(); }); + + test('renders the mission control overview at mobile width', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + + await page.goto('/hermes'); + await expect(page.getByRole('heading', { name: 'Hermes Mission Control' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Task Ledger' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Product Portfolio' })).toBeVisible(); + + await page.goto('/hermes/tasks/task-1'); + await expect(page.getByRole('heading', { name: 'Hermes learning' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Timeline' })).toBeVisible(); + }); }); diff --git a/dashboard/web/src/components/hermes-ops-panel.tsx b/dashboard/web/src/components/hermes-ops-panel.tsx index 417064a..50b84ed 100644 --- a/dashboard/web/src/components/hermes-ops-panel.tsx +++ b/dashboard/web/src/components/hermes-ops-panel.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; -import { AlertTriangle, CheckCircle2, Cloud, DatabaseBackup, ExternalLink, Gauge, HardDrive, RefreshCw, ShieldCheck, Timer, Wifi } from 'lucide-react'; +import { AlertTriangle, CheckCircle2, Cloud, DatabaseBackup, ExternalLink, Gauge, HardDrive, RefreshCw, ShieldCheck, Timer, Wifi, Activity, CalendarClock, Link2 } from 'lucide-react'; import { Badge, Button } from '@/components/ui/Primitives'; import { SectionCard } from '@/components/hermes-shell'; import { api, type HermesOpsInstance, type HermesOpsSnapshot } from '@/lib/api'; @@ -177,6 +177,48 @@ export function HermesOpsPanel() { +
+
+
+ + Active Hermes sessions +
+
+
+

Running now

+

{snapshot.activeSessions.active}

+
+
+

Last updated

+

{snapshot.activeSessions.updatedAt ? formatDate(snapshot.activeSessions.updatedAt) : 'unknown'}

+
+
+

Interpretation

+

Counted from Hermes CLI processes outside the gateway daemons.

+
+
+
+ +
+
+ + Cron job state +
+
+ {snapshot.cronJobs.map((job) => ( +
+
+

{job.label}

+ {job.active ? 'active' : 'inactive'} +
+

Next: {job.nextRun ?? 'unknown'}

+

Last: {job.lastRun ?? 'unknown'}

+
+ ))} +
+
+
+ {snapshot.warnings.length ? (
@@ -196,6 +238,43 @@ export function HermesOpsPanel() {
)} +
+
+
+ + Recent sanitized alerts +
+
+ {snapshot.recentAlerts.length ? snapshot.recentAlerts.map((warning) => ( +
{warning}
+ )) : ( +
No recent alerts.
+ )} +
+
+ +
+
+ + Quick links +
+
+ {snapshot.quickLinks.map((link) => ( + +

{link.label}

+

{link.description}

+
+ ))} +
+
+
+
{snapshot.instances.map((instance) => ( diff --git a/dashboard/web/src/lib/api.ts b/dashboard/web/src/lib/api.ts index 2d31998..acd1f7d 100644 --- a/dashboard/web/src/lib/api.ts +++ b/dashboard/web/src/lib/api.ts @@ -98,10 +98,33 @@ export interface HermesOpsInstance { }; } +export interface HermesOpsSessionSummary { + active: number; + updatedAt: string | null; +} + +export interface HermesOpsCronJob { + name: string; + label: string; + active: boolean; + nextRun: string | null; + lastRun: string | null; +} + +export interface HermesOpsLink { + label: string; + href: string; + description: string; +} + export interface HermesOpsSnapshot { generatedAt: string; tailscaleIp: string | null; emergencyDriveUpload: HermesOpsTimer; + activeSessions: HermesOpsSessionSummary; + cronJobs: HermesOpsCronJob[]; + recentAlerts: string[]; + quickLinks: HermesOpsLink[]; instances: HermesOpsInstance[]; warnings: string[]; } diff --git a/docs/hermes-operations.md b/docs/hermes-operations.md index 08c17fe..3b54760 100644 --- a/docs/hermes-operations.md +++ b/docs/hermes-operations.md @@ -31,6 +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. ## Safety guardrail: no public Hermes dashboard/API diff --git a/docs/hermes-setup-upgrade-roadmap.md b/docs/hermes-setup-upgrade-roadmap.md index dc16ed1..a4da21a 100644 --- a/docs/hermes-setup-upgrade-roadmap.md +++ b/docs/hermes-setup-upgrade-roadmap.md @@ -333,15 +333,15 @@ A healthy ByteLyst Hermes setup should be: - [x] If a dashboard is useful, make it private-only and operationally scoped. - vijay: root dashboard is running as `hermes-root-dashboard.service` at `http://100.87.53.10:9119/`, bound only to the Tailscale IP. - bheem: Uma dashboard is running as `uma-hermes-dashboard.service` at `http://100.87.53.10:9120/`, bound only to the Tailscale IP. -- [ ] Dashboard should show: - - [ ] gateway status - - [ ] active sessions - - [ ] cron job state - - [ ] backup freshness - - [ ] recent sanitized alerts - - [ ] quick links to docs/runbooks - - vijay: root dashboard HTTP endpoint returns `200` over Tailscale; feature-by-feature UI validation remains pending. - - bheem: Uma dashboard HTTP endpoint returns `200` over Tailscale; feature-by-feature UI validation remains pending. +- [x] Dashboard should show: + - [x] gateway status + - [x] active sessions + - [x] cron job state + - [x] backup freshness + - [x] recent sanitized alerts + - [x] quick links to docs/runbooks + - vijay: root live ops panel now shows gateway state, active sessions, cron state, backup freshness, sanitized alerts, and runbook links over Tailscale. + - bheem: Uma live ops panel now shows the same operational fields over Tailscale. - [x] Any dashboard actions must require authentication and ideally remain reachable only over private network/tunnel. - vijay: root dashboard is private-network-only via Tailscale IP binding; no public listener or Caddy route was added. - bheem: Uma dashboard is private-network-only via Tailscale IP binding; no public listener or Caddy route was added. diff --git a/docs/hermes_dashboard_roadmap.md b/docs/hermes_dashboard_roadmap.md index 7f8bd48..39151dc 100644 --- a/docs/hermes_dashboard_roadmap.md +++ b/docs/hermes_dashboard_roadmap.md @@ -654,7 +654,7 @@ Update this checklist only after each item has evidence from source review, test - [x] Unit/component tests pass. - [x] Production build passes. - [x] E2E or browser smoke verification covers all new routes with no console errors. -- [ ] Responsive layout checked at desktop and mobile widths. +- [x] Responsive layout checked at desktop and mobile widths. Known roadmap assumptions to handle safely during implementation: