diff --git a/.gitignore b/.gitignore index 7098b21..e635d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ Thumbs.db # Development artifacts .overhaul/ +.worktrees/ *.local .claude/settings.local.json diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 80602e7..e8144bc 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -13,6 +13,7 @@ "date-fns": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "7.13.0", "recharts": "^3.7.0" }, "devDependencies": { @@ -2265,6 +2266,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3145,6 +3159,44 @@ } } }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -3279,6 +3331,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index c682fd6..9f33e12 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -15,6 +15,7 @@ "date-fns": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "7.13.0", "recharts": "^3.7.0" }, "devDependencies": { diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 858f3e6..5bbd019 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,21 +1,20 @@ -import { useState } from 'react'; +import { Suspense, lazy } from 'react'; +import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Layout } from './components/Layout'; import { ErrorBoundary } from './components/ErrorBoundary'; import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext'; import { AlertBanner } from './components/alerts/AlertBanner'; import { CommandPalette } from './components/CommandPalette'; -import { Home } from './pages/Home'; -import { Health } from './pages/Health'; -import { Autonomy } from './pages/Autonomy'; -import { Governance } from './pages/Governance'; -import { Memory } from './pages/Memory'; -import { Tools } from './pages/Tools'; -import { Agents } from './pages/Agents'; -import { Audit } from './pages/Audit'; -import { Cognition } from './pages/Cognition'; -import { E2E } from './pages/E2E'; -import { Budget } from './pages/Budget'; +import { PageSkeleton } from './components/PageSkeleton'; + +const SystemStatus = lazy(() => import('./pages/SystemStatus')); +const AgentsAndTools = lazy(() => import('./pages/AgentsAndTools')); +const Cognition = lazy(() => import('./pages/cognition')); +const Autonomy = lazy(() => import('./pages/Autonomy')); +const GovernanceAndAudit = lazy(() => import('./pages/GovernanceAndAudit')); +const Memory = lazy(() => import('./pages/Memory')); +const Operations = lazy(() => import('./pages/Operations')); const queryClient = new QueryClient({ defaultOptions: { @@ -28,47 +27,30 @@ const queryClient = new QueryClient({ }); function AppContent() { - const [currentPage, setCurrentPage] = useState('home'); + const location = useLocation(); + const navigate = useNavigate(); const { status: wsStatus } = useWebSocketContext(); - - const renderPage = () => { - switch (currentPage) { - case 'health': - return ; - case 'autonomy': - return ; - case 'cognition': - return ; - case 'governance': - return ; - case 'memory': - return ; - case 'tools': - return ; - case 'agents': - return ; - case 'audit': - return ; - case 'e2e': - return ; - case 'budget': - return ; - default: - return ; - } - }; + const currentPage = location.pathname.split('/')[1] || 'system'; return ( <> - {/* Critical Alert Banner */} - setCurrentPage('audit')} /> - - {/* Command Palette */} - - - - - {renderPage()} + navigate('/governance')} /> + navigate(`/${page}`)} /> + + + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + @@ -79,7 +61,9 @@ function App() { return ( - + + + ); diff --git a/dashboard/src/api/client.ts b/dashboard/src/api/client.ts index 3748913..212da1b 100644 --- a/dashboard/src/api/client.ts +++ b/dashboard/src/api/client.ts @@ -23,6 +23,10 @@ import type { BudgetProfileName, BudgetProfileChangeResult, ApprovalQueueResponse, + DissentReport, + EmergencyVote, + PendingFeedback, + FeedbackStats, } from '../types/api'; const API_BASE = '/api'; @@ -92,6 +96,18 @@ export const getGovernanceRules = (): Promise => export const getQualityGates = (): Promise => fetchAPI('/governance/gates'); +export const getDissentReports = (): Promise => + fetchAPI('/governance/dissent-reports'); + +export const getEmergencyVotes = (): Promise => + fetchAPI('/governance/emergency-votes'); + +export const getPendingFeedback = (): Promise => + fetchAPI('/governance/pending-feedback'); + +export const getFeedbackStats = (): Promise => + fetchAPI('/governance/feedback-stats'); + // Memory Endpoints export const getMemories = (params?: { type?: string; diff --git a/dashboard/src/components/BudgetPanel.tsx b/dashboard/src/components/BudgetPanel.tsx index 080e384..7da3fd7 100644 --- a/dashboard/src/components/BudgetPanel.tsx +++ b/dashboard/src/components/BudgetPanel.tsx @@ -74,9 +74,10 @@ export function BudgetPanel() { ); } - const throttleColor = THROTTLE_COLORS[data.throttle.level]; - const throttleLabel = THROTTLE_LABELS[data.throttle.level]; - const throttleIcon = THROTTLE_ICONS[data.throttle.level]; + const throttleLevel = data.throttle?.level ?? 'normal'; + const throttleColor = THROTTLE_COLORS[throttleLevel]; + const throttleLabel = THROTTLE_LABELS[throttleLevel]; + const throttleIcon = THROTTLE_ICONS[throttleLevel]; return (
- {data.usage.percentUsed.toFixed(1)}% + {(data.usage?.percentUsed ?? 0).toFixed(1)}% {/* Projected EOD */} - {data.throttle.projectedEOD > 0 && ( + {(data.throttle?.projectedEOD ?? 0) > 0 && (
{} }, - { id: 'health', type: 'page', title: 'Health', description: 'System status', icon: '♥', onSelect: () => {} }, + { id: 'system', type: 'page', title: 'System Status', description: 'Overview & Health', icon: '◉', onSelect: () => {} }, + { id: 'agents', type: 'page', title: 'Agents & Tools', description: 'Agent status & tool registry', icon: '⬡', onSelect: () => {} }, + { id: 'cognition', type: 'page', title: 'Cognition', description: 'LOGOS/ETHOS/PATHOS', icon: '🧠', onSelect: () => {} }, { id: 'autonomy', type: 'page', title: 'Autonomy', description: 'Scheduler & Subagents', icon: '↻', onSelect: () => {} }, - { id: 'agents', type: 'page', title: 'Agents', description: 'Agent status', icon: '⬡', onSelect: () => {} }, - { id: 'governance', type: 'page', title: 'Governance', description: 'Council & Rules', icon: '⚖', onSelect: () => {} }, + { id: 'governance', type: 'page', title: 'Governance', description: 'Council, Rules & Audit', icon: '⚖', onSelect: () => {} }, { id: 'memory', type: 'page', title: 'Memory', description: 'Knowledge base', icon: '⬢', onSelect: () => {} }, - { id: 'tools', type: 'page', title: 'Tools', description: 'Tool registry', icon: '⚙', onSelect: () => {} }, - { id: 'audit', type: 'page', title: 'Audit Trail', description: 'Hash chain', icon: '⊞', onSelect: () => {} }, + { id: 'operations', type: 'page', title: 'Operations', description: 'Budget & E2E Tests', icon: '⚙', onSelect: () => {} }, ]; export function CommandPalette({ onNavigate }: CommandPaletteProps) { diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index 18d6229..b8dd5e1 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -6,22 +6,15 @@ import type { ConnectionStatus } from '../hooks/useWebSocket'; interface LayoutProps { children: ReactNode; currentPage: string; - onNavigate: (page: string) => void; wsStatus?: ConnectionStatus; } -export function Layout({ children, currentPage, onNavigate, wsStatus = 'disconnected' }: LayoutProps) { +export function Layout({ children, currentPage, wsStatus = 'disconnected' }: LayoutProps) { return ( <> -
- +
+
+ {/* Header skeleton */} +
+
+
+
+
+
+
+ + {/* Tab bar skeleton */} +
+ + {/* Metric cards skeleton */} +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ + {/* Content skeleton */} +
+
+
+
+
+
+ ); +} diff --git a/dashboard/src/components/Sidebar.tsx b/dashboard/src/components/Sidebar.tsx index 1e6be3a..7430f35 100644 --- a/dashboard/src/components/Sidebar.tsx +++ b/dashboard/src/components/Sidebar.tsx @@ -1,3 +1,4 @@ +import { NavLink } from 'react-router-dom'; import { useHealth, useDetailedHealth } from '../hooks/useHealth'; import { useAlertSummary } from '../hooks/useAlerts'; import { NotificationBell } from './alerts/NotificationBell'; @@ -5,34 +6,29 @@ import { HealthScoreGauge } from './charts/HealthScoreGauge'; interface SidebarProps { currentPage: string; - onNavigate: (page: string) => void; wsStatus?: 'connecting' | 'connected' | 'disconnected' | 'reconnecting'; } const pages = [ - { id: 'home', label: 'Overview', icon: '◉', description: 'Dashboard', accent: 'purple' }, - { id: 'health', label: 'Health', icon: '♥', description: 'System status', accent: 'emerald' }, - { id: 'cognition', label: 'Cognition', icon: '🧠', description: 'LOGOS/ETHOS/PATHOS', accent: 'purple' }, - { id: 'autonomy', label: 'Autonomy', icon: '↻', description: 'Scheduler', accent: 'cyan' }, - { id: 'agents', label: 'Agents', icon: '⬡', description: 'Active agents', accent: 'blue' }, - { id: 'governance', label: 'Governance', icon: '⚖', description: 'Council', accent: 'indigo' }, - { id: 'memory', label: 'Memory', icon: '⬢', description: 'Knowledge', accent: 'cyan' }, - { id: 'tools', label: 'Tools', icon: '⚙', description: 'Registry', accent: 'amber' }, - { id: 'audit', label: 'Audit Trail', icon: '⊞', description: 'Hash chain', accent: 'purple' }, - { id: 'e2e', label: 'E2E Tests', icon: '✓', description: 'Test results', accent: 'emerald' }, - { id: 'budget', label: 'Budget', icon: '💰', description: '$75/month', accent: 'amber' }, + { id: 'system', label: 'System Status', icon: '◉', description: 'Overview & Health' }, + { id: 'agents', label: 'Agents & Tools', icon: '⬡', description: 'Active agents' }, + { id: 'cognition', label: 'Cognition', icon: '🧠', description: 'LOGOS/ETHOS/PATHOS' }, + { id: 'autonomy', label: 'Autonomy', icon: '↻', description: 'Scheduler' }, + { id: 'governance', label: 'Governance', icon: '⚖', description: 'Council & Audit' }, + { id: 'memory', label: 'Memory', icon: '⬢', description: 'Knowledge' }, + { id: 'operations', label: 'Operations', icon: '⚙', description: 'Budget & E2E' }, ]; -export function Sidebar({ currentPage, onNavigate, wsStatus = 'disconnected' }: SidebarProps) { +export function Sidebar({ currentPage, wsStatus = 'disconnected' }: SidebarProps) { const { data: health } = useHealth(); const { data: detailedHealth } = useDetailedHealth(); const { data: alertSummary } = useAlertSummary(); const getStatusColor = (status?: string) => { if (!status) return 'bg-gray-500'; - if (status === 'healthy' || status === 'ok') return 'bg-[var(--ari-success)]'; - if (status === 'degraded' || status === 'warning') return 'bg-[var(--ari-warning)]'; - return 'bg-[var(--ari-error)]'; + if (status === 'healthy' || status === 'ok') return 'bg-ari-success'; + if (status === 'degraded' || status === 'warning') return 'bg-ari-warning'; + return 'bg-ari-error'; }; const getStatusGlow = (status?: string) => { @@ -45,19 +41,17 @@ export function Sidebar({ currentPage, onNavigate, wsStatus = 'disconnected' }: const getWsStatusColor = () => { switch (wsStatus) { case 'connected': - return 'bg-[var(--ari-success)]'; + return 'bg-ari-success'; case 'connecting': case 'reconnecting': - return 'bg-[var(--ari-warning)] animate-pulse'; + return 'bg-ari-warning animate-pulse'; default: return 'bg-gray-500'; } }; - // Calculate health score (0-100) from component statuses const calculateHealthScore = (): number => { if (!detailedHealth) return 0; - const components = [ detailedHealth.gateway.status, detailedHealth.eventBus.status, @@ -66,42 +60,30 @@ export function Sidebar({ currentPage, onNavigate, wsStatus = 'disconnected' }: detailedHealth.agents.status, detailedHealth.governance.status, ]; - const healthyCount = components.filter((s) => s === 'healthy').length; const degradedCount = components.filter((s) => s === 'degraded').length; - - // Healthy = 100%, Degraded = 50%, Unhealthy = 0% return Math.round(((healthyCount * 100 + degradedCount * 50) / components.length)); }; const healthScore = calculateHealthScore(); - - // Real data from API const agentCount = detailedHealth?.agents.activeCount ?? 0; const councilCount = detailedHealth?.governance.councilMembers ?? 0; const patternCount = detailedHealth?.sanitizer.patternsLoaded ?? 21; - // Dynamic description for agents page const pagesWithData = pages.map((page) => { - if (page.id === 'agents') { - return { ...page, description: `${agentCount} active` }; - } - if (page.id === 'governance') { - return { ...page, description: `${councilCount} members` }; - } + if (page.id === 'agents') return { ...page, description: `${agentCount} active` }; + if (page.id === 'governance') return { ...page, description: `${councilCount} members` }; return page; }); return (