From 45567c738653a9a38f343611ce81ccb08e6ba41a Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:21:44 -0500 Subject: [PATCH 01/20] chore: add .worktrees/ to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 45fe49aa4c27aa5102a46317f1637213e1d0b12a Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:27:46 -0500 Subject: [PATCH 02/20] fix(dashboard): CRIT-001 BudgetPanel crash from missing throttle field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add defensive null checks for throttle.level, usage.percentUsed, throttle.projectedEOD - Fix mock interceptor: /api/budget/status → budgetStatus (has throttle object) - Add separate /api/budget/page-status route for Budget page's flat shape Co-Authored-By: Claude Opus 4.6 --- dashboard/src/components/BudgetPanel.tsx | 11 +- dashboard/src/mocks/data.ts | 652 +++++++++++++++++++++++ dashboard/src/mocks/interceptor.ts | 195 +++++++ 3 files changed, 853 insertions(+), 5 deletions(-) create mode 100644 dashboard/src/mocks/data.ts create mode 100644 dashboard/src/mocks/interceptor.ts 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: `audit-${String(i + 1).padStart(4, '0')}`, + action: auditActions[i % auditActions.length], + agent: agents[i % agents.length].type, + timestamp: minutesAgo(i * 3), + details: { source: 'mock', iteration: i + 1 }, + previousHash: i === 0 ? '0'.repeat(64) : sha256Mock(i - 1), + hash: sha256Mock(i), +})); + +export const auditLog = { + events: auditEvents, + total: 2847, + limit: 50, + offset: 0, +}; + +export const auditVerification = { + valid: true, + entryCount: 2847, + genesisHash: '0'.repeat(64), + lastHash: sha256Mock(2846), + message: 'Audit chain verified: 2847 entries, no broken links', +}; + +// ─── Tools ────────────────────────────────────── + +export const tools = [ + { id: 'tool-001', name: 'file_read', description: 'Read file contents from local filesystem', category: 'System', trustLevel: 'STANDARD' as const, permissionTier: 'READ' as const, enabled: true, executionCount: 3421, errorCount: 12, lastUsed: minutesAgo(5) }, + { id: 'tool-002', name: 'file_write', description: 'Write content to local filesystem', category: 'System', trustLevel: 'OPERATOR' as const, permissionTier: 'WRITE' as const, enabled: true, executionCount: 1847, errorCount: 23, lastUsed: minutesAgo(15) }, + { id: 'tool-003', name: 'bash_execute', description: 'Execute shell commands', category: 'System', trustLevel: 'OPERATOR' as const, permissionTier: 'ADMIN' as const, enabled: true, executionCount: 2156, errorCount: 89, lastUsed: minutesAgo(8) }, + { id: 'tool-004', name: 'memory_store', description: 'Store entry in memory with provenance', category: 'Memory', trustLevel: 'VERIFIED' as const, permissionTier: 'WRITE' as const, enabled: true, executionCount: 445, errorCount: 2, lastUsed: minutesAgo(30) }, + { id: 'tool-005', name: 'memory_search', description: 'Search memories by content and tags', category: 'Memory', trustLevel: 'STANDARD' as const, permissionTier: 'READ' as const, enabled: true, executionCount: 1923, errorCount: 0, lastUsed: minutesAgo(3) }, + { id: 'tool-006', name: 'audit_query', description: 'Query audit log entries', category: 'Audit', trustLevel: 'VERIFIED' as const, permissionTier: 'READ' as const, enabled: true, executionCount: 312, errorCount: 0, lastUsed: hoursAgo(1) }, + { id: 'tool-007', name: 'web_search', description: 'Search the web for information', category: 'System', trustLevel: 'OPERATOR' as const, permissionTier: 'READ' as const, enabled: true, executionCount: 567, errorCount: 34, lastUsed: hoursAgo(2) }, + { id: 'tool-008', name: 'governance_vote', description: 'Cast a governance vote', category: 'Governance', trustLevel: 'SYSTEM' as const, permissionTier: 'WRITE' as const, enabled: true, executionCount: 89, errorCount: 0, lastUsed: daysAgo(1) }, + { id: 'tool-009', name: 'agent_spawn', description: 'Spawn a subagent for parallel work', category: 'Agents', trustLevel: 'OPERATOR' as const, permissionTier: 'ADMIN' as const, enabled: true, executionCount: 203, errorCount: 15, lastUsed: hoursAgo(3) }, + { id: 'tool-010', name: 'notification_send', description: 'Send notification to operator', category: 'System', trustLevel: 'VERIFIED' as const, permissionTier: 'WRITE' as const, enabled: true, executionCount: 1456, errorCount: 8, lastUsed: minutesAgo(45) }, +]; + +// ─── Contexts ────────────────────────────────────── + +export const contexts = [ + { id: 'ctx-001', name: 'ARI Development', type: 'VENTURE' as const, active: true, lastAccessed: minutesAgo(5), memoryCount: 234, taskCount: 18 }, + { id: 'ctx-002', name: 'Daily Operations', type: 'LIFE' as const, active: false, lastAccessed: hoursAgo(6), memoryCount: 156, taskCount: 5 }, + { id: 'ctx-003', name: 'System Maintenance', type: 'SYSTEM' as const, active: false, lastAccessed: daysAgo(1), memoryCount: 89, taskCount: 3 }, +]; + +// ─── Scheduler ────────────────────────────────────── + +export const schedulerStatus = { + running: true, + taskCount: 6, + enabledCount: 5, + nextTask: { + id: 'sched-001', + name: 'Morning Brief', + nextRun: new Date(Date.now() + 3600000).toISOString(), + }, +}; + +export const scheduledTasks = [ + { id: 'sched-001', name: 'Morning Brief', cron: '0 7 * * *', handler: 'morning-brief', enabled: true, lastRun: daysAgo(0).split('T')[0] + 'T07:00:00Z', nextRun: new Date(Date.now() + 3600000).toISOString() }, + { id: 'sched-002', name: 'Evening Summary', cron: '0 21 * * *', handler: 'evening-summary', enabled: true, lastRun: daysAgo(1).split('T')[0] + 'T21:00:00Z', nextRun: daysAgo(0).split('T')[0] + 'T21:00:00Z' }, + { id: 'sched-003', name: 'Health Check', cron: '*/5 * * * *', handler: 'health-check', enabled: true, lastRun: minutesAgo(3), nextRun: new Date(Date.now() + 120000).toISOString() }, + { id: 'sched-004', name: 'Memory Consolidation', cron: '0 3 * * *', handler: 'memory-consolidate', enabled: true, lastRun: daysAgo(0).split('T')[0] + 'T03:00:00Z', nextRun: new Date(Date.now() + 7200000).toISOString() }, + { id: 'sched-005', name: 'Audit Chain Verify', cron: '0 */6 * * *', handler: 'audit-verify', enabled: true, lastRun: hoursAgo(4), nextRun: new Date(Date.now() + 7200000).toISOString() }, + { id: 'sched-006', name: 'Weekly Report', cron: '0 18 * * 0', handler: 'weekly-report', enabled: false, lastRun: daysAgo(7), nextRun: null }, +]; + +// ─── Subagents ────────────────────────────────────── + +export const subagents = [ + { + id: 'sub-001', task: 'Implement dashboard mock data', branch: 'feat/dashboard-mocks', + worktreePath: '/tmp/ari-worktree-001', status: 'running' as const, + createdAt: hoursAgo(1), completedAt: null, progress: 65, + lastMessage: 'Creating mock data for all API endpoints...', error: null, + tmuxSession: 'ari-sub-001', + }, + { + id: 'sub-002', task: 'Fix test failures in governance module', branch: 'fix/governance-tests', + worktreePath: '/tmp/ari-worktree-002', status: 'completed' as const, + createdAt: hoursAgo(3), completedAt: hoursAgo(2), progress: 100, + lastMessage: 'All 47 governance tests passing', error: null, + tmuxSession: null, + }, + { + id: 'sub-003', task: 'Research MCP server integration patterns', branch: 'research/mcp-patterns', + worktreePath: '/tmp/ari-worktree-003', status: 'completed' as const, + createdAt: daysAgo(1), completedAt: hoursAgo(18), progress: 100, + lastMessage: 'Research complete — findings documented in docs/mcp/', error: null, + tmuxSession: null, + }, +]; + +export const subagentStats = { + total: 3, + running: 1, + completed: 2, + failed: 0, + spawning: 0, +}; + +// ─── System Metrics ────────────────────────────────────── + +export const systemMetrics = { + uptime: 847293, + uptimeFormatted: '9d 19h 21m', + memory: { + heapUsed: 67_234_816, + heapTotal: 134_217_728, + external: 8_388_608, + rss: 201_326_592, + heapUsedMB: 64.1, + heapTotalMB: 128.0, + rssMB: 192.0, + }, + nodeVersion: 'v20.11.0', + platform: 'darwin', + arch: 'arm64', + pid: 42187, +}; + +// ─── Cognition ────────────────────────────────────── + +export const cognitiveHealth = { + overall: 0.84, + pillars: [ + { pillar: 'LOGOS' as const, health: 0.89, apisActive: 5, apisTotal: 6, lastActivity: minutesAgo(10), topFramework: 'Bayesian Reasoning', sourcesCount: 12 }, + { pillar: 'ETHOS' as const, health: 0.82, apisActive: 4, apisTotal: 5, lastActivity: hoursAgo(1), topFramework: 'Stoic Ethics', sourcesCount: 8 }, + { pillar: 'PATHOS' as const, health: 0.79, apisActive: 3, apisTotal: 4, lastActivity: hoursAgo(2), topFramework: 'CBT Framework', sourcesCount: 6 }, + ], + learningLoopActive: true, + knowledgeSources: 26, +}; + +export const cognitivePillars = [ + { + pillar: 'LOGOS', name: 'Reason', icon: '🧠', + description: 'Bayesian reasoning, bias detection, calibration tracking', + apis: ['bayesian_update', 'bias_check', 'calibration_predict', 'decision_matrix', 'logical_validate', 'argument_evaluate'], + sourcesCount: 12, + }, + { + pillar: 'ETHOS', name: 'Character', icon: '⚖', + description: 'Ethical reasoning, value alignment, constitutional compliance', + apis: ['ethical_evaluate', 'value_align', 'constitutional_check', 'virtue_assess', 'stakeholder_impact'], + sourcesCount: 8, + }, + { + pillar: 'PATHOS', name: 'Growth', icon: '🌱', + description: 'Emotional intelligence, growth tracking, self-improvement', + apis: ['growth_track', 'insight_capture', 'review_schedule', 'practice_log'], + sourcesCount: 6, + }, +]; + +export const cognitiveSources = { + total: 26, + byTrustLevel: { verified: 18, standard: 8 }, + sources: [ + { id: 'src-001', name: 'Thinking, Fast and Slow', pillar: 'LOGOS', trustLevel: 'verified', category: 'Book', frameworks: ['System 1/2', 'Heuristics'] }, + { id: 'src-002', name: 'Meditations', pillar: 'ETHOS', trustLevel: 'verified', category: 'Book', frameworks: ['Stoicism', 'Virtue Ethics'] }, + { id: 'src-003', name: 'Principles', pillar: 'LOGOS', trustLevel: 'verified', category: 'Book', frameworks: ['Radical Transparency', 'Decision Journal'] }, + { id: 'src-004', name: 'Feeling Good', pillar: 'PATHOS', trustLevel: 'standard', category: 'Book', frameworks: ['CBT', 'Cognitive Distortions'] }, + { id: 'src-005', name: 'The Art of War', pillar: 'LOGOS', trustLevel: 'verified', category: 'Book', frameworks: ['Strategy', 'Resource Management'] }, + ], +}; + +export const learningStatus = { + currentStage: 'REVIEW', + lastReview: hoursAgo(4), + lastGapAnalysis: daysAgo(2), + lastAssessment: daysAgo(7), + nextReview: new Date(Date.now() + 7200000).toISOString(), + nextGapAnalysis: daysAgo(-5), + nextAssessment: daysAgo(-7), + recentInsightsCount: 14, + improvementTrend: 'improving' as const, +}; + +export const learningAnalytics = { + period: { start: daysAgo(30), end: now }, + retentionMetrics: { reviews: 87, successfulReviews: 74, retentionRate: 0.85, dueNow: 4 }, + practiceQuality: { deliberateHours: 42.5, distractedHours: 8.2, focusRatio: 0.84 }, + insights: [ + { pattern: 'Peak learning occurs between 9-11 AM', recommendation: 'Schedule complex reviews for morning sessions', impact: 'HIGH' as const }, + { pattern: 'Spaced repetition intervals improving retention', recommendation: 'Continue SM-2 algorithm adjustments', impact: 'MEDIUM' as const }, + { pattern: 'Bayesian calibration improving steadily', recommendation: 'Add more prediction tracking', impact: 'HIGH' as const }, + ], +}; + +export const frameworkUsage = [ + { framework: 'Bayesian Reasoning', pillar: 'LOGOS' as const, usageCount: 342, successRate: 0.87 }, + { framework: 'Decision Matrix', pillar: 'LOGOS' as const, usageCount: 156, successRate: 0.91 }, + { framework: 'Bias Detection', pillar: 'LOGOS' as const, usageCount: 89, successRate: 0.78 }, + { framework: 'Stoic Ethics', pillar: 'ETHOS' as const, usageCount: 201, successRate: 0.85 }, + { framework: 'Virtue Assessment', pillar: 'ETHOS' as const, usageCount: 67, successRate: 0.82 }, + { framework: 'CBT Framework', pillar: 'PATHOS' as const, usageCount: 134, successRate: 0.88 }, + { framework: 'Growth Tracking', pillar: 'PATHOS' as const, usageCount: 245, successRate: 0.92 }, +]; + +export const cognitiveInsights = [ + { id: 'ins-001', type: 'SUCCESS' as const, description: 'Bayesian reasoning correctly predicted deployment risk', confidence: 0.91, timestamp: hoursAgo(3), framework: 'Bayesian Reasoning' }, + { id: 'ins-002', type: 'PATTERN' as const, description: 'Operator prefers conservative approach for security changes', confidence: 0.87, timestamp: hoursAgo(8), framework: 'Decision Matrix' }, + { id: 'ins-003', type: 'MISTAKE' as const, description: 'Overconfidence in test coverage estimation — actual was 12% lower', confidence: 0.65, timestamp: daysAgo(1), framework: 'Calibration' }, + { id: 'ins-004', type: 'PRINCIPLE' as const, description: 'Shadow integration: log suspicious patterns instead of suppressing', confidence: 0.95, timestamp: daysAgo(2), framework: 'Stoic Ethics' }, + { id: 'ins-005', type: 'ANTIPATTERN' as const, description: 'Avoid over-engineering simple features with unnecessary abstraction', confidence: 0.89, timestamp: daysAgo(3), framework: 'Ruthless Simplicity' }, +]; + +export const councilProfiles = [ + { + memberId: 'core-orchestrator', memberName: 'Core Orchestrator', memberAvatar: '🎯', + primaryPillar: 'LOGOS' as const, pillarWeights: { logos: 0.5, ethos: 0.3, pathos: 0.2 }, + primaryFrameworks: [{ name: 'Decision Matrix', domain: 'Task Routing', application: 'Multi-agent coordination', why: 'Optimal task distribution' }], + knowledgeSources: ['Principles'], expertiseAreas: ['Orchestration', 'Coordination'], + consultedFor: 'Task routing and agent coordination decisions', + typicalAPIUsage: ['decision_matrix', 'logical_validate'], + learningPlan: { current: 'Multi-agent coordination patterns', next: 'Consensus algorithms', cadence: 'Weekly', quarterlyGoals: ['Improve task routing accuracy to 95%'] }, + cognitiveBiasAwareness: { naturalTendency: 'Anchoring', compensationStrategy: 'Seek multiple perspectives', historicalPattern: 'Improving', improvementGoal: 'Reduce anchoring bias by 20%' }, + performanceMetrics: { keyMetric: 'Task routing accuracy', baseline: 0.87, target: 0.95 }, + }, + { + memberId: 'guardian-sentinel', memberName: 'Guardian Sentinel', memberAvatar: '🛡', + primaryPillar: 'ETHOS' as const, pillarWeights: { logos: 0.3, ethos: 0.5, pathos: 0.2 }, + primaryFrameworks: [{ name: 'Threat Assessment', domain: 'Security', application: 'Input validation', why: 'Prevent injection attacks' }], + knowledgeSources: ['OWASP', 'Security Best Practices'], expertiseAreas: ['Security', 'Threat Detection'], + consultedFor: 'Security decisions and threat assessment', + typicalAPIUsage: ['ethical_evaluate', 'constitutional_check'], + learningPlan: { current: 'Advanced prompt injection patterns', next: 'Zero-day detection', cadence: 'Daily', quarterlyGoals: ['Zero false negatives on injection detection'] }, + cognitiveBiasAwareness: { naturalTendency: 'Confirmation bias in threat detection', compensationStrategy: 'Use Bayesian priors', historicalPattern: 'Stable', improvementGoal: 'Improve false positive rate' }, + performanceMetrics: { keyMetric: 'Threat detection rate', baseline: 0.96, target: 0.99 }, + }, +]; + +// ─── Budget ────────────────────────────────────── + +export const budgetStatus = { + profile: 'balanced' as const, + budget: { maxTokens: 50_000_000, maxCost: 75 }, + usage: { tokensUsed: 18_432_100, tokensRemaining: 31_567_900, costUsed: 28.47, percentUsed: 0.38 }, + throttle: { level: 'normal' as const, projectedEOD: 32.10 }, + breakdown: { + byModel: { + 'claude-haiku-4.5': { tokens: 8_200_000, cost: 8.20 }, + 'claude-sonnet-4': { tokens: 6_100_000, cost: 12.20 }, + 'claude-opus-4.5': { tokens: 4_132_100, cost: 8.07 }, + }, + byTaskType: [ + { taskType: 'code-generation', tokens: 7_500_000, cost: 11.25, count: 34, percentOfTotal: 39.5 }, + { taskType: 'analysis', tokens: 5_200_000, cost: 7.80, count: 21, percentOfTotal: 27.4 }, + { taskType: 'review', tokens: 3_800_000, cost: 5.70, count: 18, percentOfTotal: 20.0 }, + { taskType: 'simple-query', tokens: 1_932_100, cost: 3.72, count: 45, percentOfTotal: 13.1 }, + ], + }, + resetAt: daysAgo(-12), + date: now.split('T')[0], +}; + +// Budget status for Budget.tsx (different shape from types/api.ts BudgetStatus) +export const budgetPageStatus = { + mode: 'balanced' as const, + spent: 28.47, + remaining: 46.53, + budget: 75, + percentUsed: 0.38, + daysInCycle: 18, + daysRemaining: 12, + projectedSpend: 47.20, + avgDailySpend: 1.58, + recommendedDailySpend: 3.88, + status: 'ok' as const, +}; + +// Budget state for daily usage chart +export const budgetState = { + config: { monthlyBudget: 75, warningThreshold: 0.7, criticalThreshold: 0.9, pauseThreshold: 0.95 }, + dailyUsage: Array.from({ length: 18 }, (_, i) => { + const date = daysAgo(17 - i).split('T')[0]; + const baseCost = 1.2 + Math.random() * 1.5; + return { + date, + totalCost: Number(baseCost.toFixed(4)), + totalTokens: Math.round(baseCost * 500000), + requestCount: Math.round(5 + Math.random() * 15), + modelBreakdown: { + 'claude-haiku-4.5': { cost: Number((baseCost * 0.3).toFixed(4)), tokens: Math.round(baseCost * 150000), requests: Math.round(3 + Math.random() * 5) }, + 'claude-sonnet-4': { cost: Number((baseCost * 0.4).toFixed(4)), tokens: Math.round(baseCost * 200000), requests: Math.round(2 + Math.random() * 4) }, + 'claude-opus-4.5': { cost: Number((baseCost * 0.3).toFixed(4)), tokens: Math.round(baseCost * 150000), requests: Math.round(1 + Math.random() * 3) }, + }, + taskBreakdown: { + 'code-generation': { cost: Number((baseCost * 0.4).toFixed(4)), requests: Math.round(2 + Math.random() * 3) }, + 'analysis': { cost: Number((baseCost * 0.3).toFixed(4)), requests: Math.round(1 + Math.random() * 3) }, + 'review': { cost: Number((baseCost * 0.2).toFixed(4)), requests: Math.round(1 + Math.random() * 2) }, + 'simple-query': { cost: Number((baseCost * 0.1).toFixed(4)), requests: Math.round(2 + Math.random() * 5) }, + }, + }; + }), + totalSpent: 28.47, + mode: 'balanced', + currentCycleStart: daysAgo(18), + currentCycleEnd: daysAgo(-12), +}; + +export const approvalQueue = { + pending: [ + { + id: 'appr-001', type: 'INITIATIVE' as const, title: 'Automated dependency updates', + description: 'Run npm audit fix and update non-breaking dependencies weekly', + risk: 'LOW' as const, estimatedCost: 0.50, createdAt: hoursAgo(2), + }, + ], + history: [ + { itemId: 'appr-prev-001', decision: 'APPROVED' as const, decidedAt: daysAgo(1), note: 'Good initiative' }, + { itemId: 'appr-prev-002', decision: 'REJECTED' as const, decidedAt: daysAgo(3), reason: 'Too risky without review' }, + ], +}; + +// ─── Billing Cycle ────────────────────────────────────── + +export const billingCycle = { + daysElapsed: 18, + daysRemaining: 12, + percentComplete: 0.6, + totalSpent: 28.47, + remaining: 46.53, + dailyAverage: 1.58, + projectedTotal: 47.20, + onTrack: true, + status: 'under_budget' as const, + recommendation: 'Spending on track. You can safely increase model tier for complex tasks.', + recommended: { dailyBudget: 3.88, confidence: 0.85, reason: 'Based on 18-day spending pattern' }, + cycle: { totalBudget: 75 }, +}; + +// ─── Value Analytics ────────────────────────────────────── + +export const valueAnalytics = { + days: Array.from({ length: 7 }, (_, i) => ({ + date: daysAgo(6 - i).split('T')[0], + cost: Number((1.2 + Math.random() * 2).toFixed(2)), + tokens: Math.round(500000 + Math.random() * 1000000), + metrics: { + testsGenerated: Math.round(Math.random() * 8), + docsWritten: Math.round(Math.random() * 3), + bugsFixed: Math.round(Math.random() * 4), + codeImprovements: Math.round(Math.random() * 6), + patternsLearned: Math.round(Math.random() * 5), + tasksAttempted: Math.round(5 + Math.random() * 10), + tasksSucceeded: Math.round(4 + Math.random() * 8), + }, + deliverablesScore: Number((15 + Math.random() * 10).toFixed(1)), + improvementsScore: Number((8 + Math.random() * 8).toFixed(1)), + insightsScore: Number((5 + Math.random() * 5).toFixed(1)), + efficiencyScore: Number((3 + Math.random() * 4).toFixed(1)), + totalValueScore: Number((35 + Math.random() * 20).toFixed(1)), + costPerPoint: Number((0.03 + Math.random() * 0.04).toFixed(3)), + roi: Number((2.5 + Math.random() * 3).toFixed(1)), + efficiency: ['excellent', 'good', 'good', 'moderate', 'good', 'excellent', 'good'][i] as 'excellent' | 'good' | 'moderate', + breakdown: ['Tests: 5', 'Docs: 2', 'Fixes: 3'], + })), + totalCost: 12.84, + totalValuePoints: 312, + averageValueScore: 44.6, + averageCostPerPoint: 0.041, + bestDay: { date: daysAgo(1).split('T')[0], score: 52.3 }, + worstDay: { date: daysAgo(4).split('T')[0], score: 35.1 }, + weeklyTrend: 'improving' as const, + recommendations: [ + 'Morning sessions show 23% higher value output — schedule complex tasks before noon', + 'Code generation tasks have best ROI — consider batching reviews for efficiency', + ], +}; + +export const valueToday = { + metrics: { + testsGenerated: 3, + docsWritten: 1, + bugsFixed: 2, + codeImprovements: 4, + patternsLearned: 2, + tasksAttempted: 8, + tasksSucceeded: 7, + }, + currentScore: 41.2, + breakdown: ['Tests: 3 (+7.5pts)', 'Docs: 1 (+3pts)', 'Bug fixes: 2 (+10pts)', 'Improvements: 4 (+12pts)'], +}; + +// ─── Adaptive Learning ────────────────────────────────────── + +export const adaptivePatterns = [ + { id: 'pat-001', patternType: 'peak_hours', observations: 14, confidence: 0.87, lastObserved: hoursAgo(2), data: { hours: [9, 10, 11, 14, 15] }, appliedCount: 8, successCount: 7, failureCount: 1 }, + { id: 'pat-002', patternType: 'model_preference', observations: 23, confidence: 0.92, lastObserved: hoursAgo(1), data: { complex: 'opus', simple: 'haiku' }, appliedCount: 15, successCount: 14, failureCount: 1 }, + { id: 'pat-003', patternType: 'task_batching', observations: 9, confidence: 0.78, lastObserved: daysAgo(1), data: { batchSize: 3, taskType: 'review' }, appliedCount: 5, successCount: 4, failureCount: 1 }, +]; + +export const adaptiveRecommendations = [ + { id: 'rec-001', type: 'model_selection', recommendation: 'Use Haiku 4.5 for simple queries — 3x cost savings', confidence: 0.91 }, + { id: 'rec-002', type: 'scheduling', recommendation: 'Schedule complex analysis during 9-11 AM peak performance', confidence: 0.85, appliedAt: daysAgo(2), result: 'success' as const }, + { id: 'rec-003', type: 'budget', recommendation: 'Reduce aggressive mode usage — 18% overspend on low-value tasks', confidence: 0.78 }, +]; + +export const adaptiveSummaries = [ + { + weekStart: daysAgo(7), weekEnd: daysAgo(0), + peakHours: [9, 10, 11, 14, 15], + avgDailySpend: 1.58, avgDailyValue: 44.6, + preferredModels: { complex: 'claude-opus-4.5', standard: 'claude-sonnet-4', simple: 'claude-haiku-4.5' }, + approvedInitiativeTypes: ['code-review', 'test-generation', 'documentation'], + rejectedInitiativeTypes: ['large-refactor'], + adjustmentsMade: ['Reduced Opus usage for simple queries', 'Increased batch size for reviews'], + }, +]; + +// ─── E2E Tests ────────────────────────────────────── + +export const e2eRuns = { + runs: Array.from({ length: 10 }, (_, i) => { + const passed = i < 8 ? 12 : 11; + const failed = i < 8 ? 0 : 1; + return { + id: `run-${String(i + 1).padStart(3, '0')}`, + status: (failed === 0 ? 'passed' : 'failed') as 'passed' | 'failed', + startedAt: daysAgo(9 - i), + completedAt: new Date(new Date(daysAgo(9 - i)).getTime() + 45000).toISOString(), + duration: 42000 + Math.round(Math.random() * 8000), + scenarios: [ + { id: 'sc-001', name: 'Gateway Health Check', status: 'passed' as const, duration: 1200 }, + { id: 'sc-002', name: 'Message Pipeline', status: 'passed' as const, duration: 3400 }, + { id: 'sc-003', name: 'Injection Detection', status: 'passed' as const, duration: 2100 }, + { id: 'sc-004', name: 'Audit Chain Integrity', status: 'passed' as const, duration: 1800 }, + { id: 'sc-005', name: 'Agent Coordination', status: 'passed' as const, duration: 4200 }, + { id: 'sc-006', name: 'Governance Voting', status: 'passed' as const, duration: 3600 }, + { id: 'sc-007', name: 'Memory Provenance', status: 'passed' as const, duration: 2900 }, + { id: 'sc-008', name: 'Permission Enforcement', status: 'passed' as const, duration: 1500 }, + { id: 'sc-009', name: 'Trust Level Escalation', status: 'passed' as const, duration: 2300 }, + { id: 'sc-010', name: 'Rate Limiting', status: 'passed' as const, duration: 1700 }, + { id: 'sc-011', name: 'Schema Validation', status: 'passed' as const, duration: 1100 }, + { id: 'sc-012', name: 'EventBus Routing', status: (failed === 0 ? 'passed' : 'failed') as 'passed' | 'failed', duration: 2800, ...(failed > 0 ? { error: 'Timeout: EventBus subscriber did not respond within 5000ms' } : {}) }, + ], + passed, + failed, + total: 12, + }; + }), + stats: { + totalRuns: 10, + passRate: 0.80, + lastRun: daysAgo(0), + consecutiveFailures: 0, + }, +}; + +// ─── Alerts ────────────────────────────────────── + +export const alerts = { + alerts: [ + { + id: 'alert-001', severity: 'info' as const, status: 'active' as const, + title: 'Morning brief delivered', message: 'Daily briefing sent to operator at 7:00 AM', + source: 'scheduler', count: 1, firstSeenAt: hoursAgo(5), lastSeenAt: hoursAgo(5), + }, + { + id: 'alert-002', severity: 'warning' as const, status: 'active' as const, + title: 'Memory consolidation skipped', message: 'Memory consolidation task skipped due to low memory count', + source: 'memory-manager', count: 2, firstSeenAt: daysAgo(1), lastSeenAt: hoursAgo(3), + }, + { + id: 'alert-003', severity: 'info' as const, status: 'resolved' as const, + title: 'E2E test suite completed', message: 'All 12 scenarios passed in 45.2s', + source: 'e2e-runner', count: 1, firstSeenAt: daysAgo(1), lastSeenAt: daysAgo(1), + resolvedAt: daysAgo(1), resolvedBy: 'system', + }, + ], + total: 3, +}; + +export const alertSummary = { + total: 3, + active: 2, + acknowledged: 0, + resolved: 1, + bySeverity: { info: 2, warning: 1, critical: 0 }, +}; diff --git a/dashboard/src/mocks/interceptor.ts b/dashboard/src/mocks/interceptor.ts new file mode 100644 index 0000000..bb9a157 --- /dev/null +++ b/dashboard/src/mocks/interceptor.ts @@ -0,0 +1,195 @@ +// Mock fetch interceptor for ARI Dashboard +// Intercepts /api/* requests and returns mock data when the backend is offline + +import * as mock from './data'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MockResolver = () => any; + +// Map endpoint patterns to mock data +const mockRoutes: Array<[RegExp, MockResolver]> = [ + // Health + [/^\/api\/health\/detailed$/, () => mock.detailedHealth], + [/^\/api\/health$/, () => mock.health], + + // Agents + [/^\/api\/agents$/, () => mock.agents], + [/^\/api\/agents\/[^/]+\/stats$/, () => ({ + agentId: 'agt-core-001', type: 'CORE', tasksCompleted: 1247, + tasksInProgress: 3, tasksFailed: 2, averageTaskDuration: 1200, + lastActive: new Date().toISOString(), uptime: 847293, + })], + + // Governance + [/^\/api\/proposals$/, () => mock.proposals], + [/^\/api\/proposals\/[^/]+$/, () => mock.proposals[0]], + [/^\/api\/governance\/rules$/, () => mock.governanceRules], + [/^\/api\/governance\/gates$/, () => mock.qualityGates], + + // Memory + [/^\/api\/memory(\?.*)?$/, () => mock.memories], + [/^\/api\/memory\/[^/]+$/, () => mock.memories[0]], + + // Audit + [/^\/api\/audit\/verify$/, () => mock.auditVerification], + [/^\/api\/audit(\?.*)?$/, () => mock.auditLog], + + // Tools + [/^\/api\/tools$/, () => mock.tools], + + // Contexts + [/^\/api\/contexts\/active$/, () => mock.contexts[0]], + [/^\/api\/contexts$/, () => mock.contexts], + + // Scheduler + [/^\/api\/scheduler\/status$/, () => mock.schedulerStatus], + [/^\/api\/scheduler\/tasks$/, () => mock.scheduledTasks], + + // Subagents + [/^\/api\/subagents\/stats$/, () => mock.subagentStats], + [/^\/api\/subagents$/, () => mock.subagents], + [/^\/api\/subagents\/[^/]+$/, () => mock.subagents[0]], + + // System + [/^\/api\/system\/metrics$/, () => mock.systemMetrics], + + // Cognition + [/^\/api\/cognition\/health$/, () => mock.cognitiveHealth], + [/^\/api\/cognition\/pillars$/, () => mock.cognitivePillars], + [/^\/api\/cognition\/sources$/, () => mock.cognitiveSources], + [/^\/api\/cognition\/council-profiles\/[^/]+$/, () => mock.councilProfiles[0]], + [/^\/api\/cognition\/council-profiles$/, () => mock.councilProfiles], + [/^\/api\/cognition\/learning\/status$/, () => mock.learningStatus], + [/^\/api\/cognition\/learning\/analytics(\?.*)?$/, () => mock.learningAnalytics], + [/^\/api\/cognition\/learning\/calibration$/, () => ({ + overconfidenceBias: 0.08, underconfidenceBias: -0.03, + calibrationCurve: [ + { confidenceBucket: '0-20%', statedConfidence: 0.1, actualAccuracy: 0.12, delta: 0.02, count: 5 }, + { confidenceBucket: '20-40%', statedConfidence: 0.3, actualAccuracy: 0.28, delta: -0.02, count: 8 }, + { confidenceBucket: '40-60%', statedConfidence: 0.5, actualAccuracy: 0.47, delta: -0.03, count: 15 }, + { confidenceBucket: '60-80%', statedConfidence: 0.7, actualAccuracy: 0.63, delta: -0.07, count: 22 }, + { confidenceBucket: '80-100%', statedConfidence: 0.9, actualAccuracy: 0.82, delta: -0.08, count: 30 }, + ], + predictions: [], + })], + [/^\/api\/cognition\/frameworks\/usage$/, () => mock.frameworkUsage], + [/^\/api\/cognition\/insights(\?.*)?$/, () => mock.cognitiveInsights], + + // Budget + [/^\/api\/budget\/status$/, () => mock.budgetStatus], + [/^\/api\/budget\/page-status$/, () => mock.budgetPageStatus], + [/^\/api\/budget\/state$/, () => mock.budgetState], + [/^\/api\/budget\/profile$/, () => ({ success: true, profile: 'balanced' })], + + // Billing + [/^\/api\/billing\/cycle$/, () => mock.billingCycle], + + // Value Analytics + [/^\/api\/analytics\/value\/today$/, () => mock.valueToday], + [/^\/api\/analytics\/value\/daily(\?.*)?$/, () => mock.valueAnalytics.days], + [/^\/api\/analytics\/value\/weekly$/, () => ({ + weekStart: mock.valueAnalytics.days[0].date, + weekEnd: mock.valueAnalytics.days[6].date, + totalCost: mock.valueAnalytics.totalCost, + totalValuePoints: mock.valueAnalytics.totalValuePoints, + averageScore: mock.valueAnalytics.averageValueScore, + bestDay: null, worstDay: null, + trend: 'improving', + recommendations: mock.valueAnalytics.recommendations, + costBreakdown: [ + { category: 'code-generation', cost: 5.12, percentage: 39.9 }, + { category: 'analysis', cost: 3.52, percentage: 27.4 }, + { category: 'review', cost: 2.57, percentage: 20.0 }, + { category: 'simple-query', cost: 1.63, percentage: 12.7 }, + ], + })], + [/^\/api\/analytics\/value$/, () => mock.valueAnalytics], + + // Adaptive + [/^\/api\/adaptive\/patterns$/, () => mock.adaptivePatterns], + [/^\/api\/adaptive\/recommendations$/, () => mock.adaptiveRecommendations], + [/^\/api\/adaptive\/summaries$/, () => mock.adaptiveSummaries], + [/^\/api\/adaptive\/peak-hours$/, () => ({ hours: [9, 10, 11, 14, 15] })], + + // E2E + [/^\/api\/e2e\/runs$/, () => mock.e2eRuns], + + // Alerts + [/^\/api\/alerts\/summary$/, () => mock.alertSummary], + [/^\/api\/alerts(\?.*)?$/, () => mock.alerts], + + // Approval Queue + [/^\/api\/approval-queue$/, () => mock.approvalQueue], +]; + +function findMockData(url: string): unknown | undefined { + // Extract the path portion (remove origin if present) + const path = url.startsWith('http') ? new URL(url).pathname + new URL(url).search : url; + + for (const [pattern, resolver] of mockRoutes) { + if (pattern.test(path)) { + return resolver(); + } + } + return undefined; +} + +const originalFetch = window.fetch.bind(window); +let mockActive = false; + +export function enableMockInterceptor(): void { + if (mockActive) return; + mockActive = true; + + window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const url = typeof input === 'string' + ? input + : input instanceof URL + ? input.href + : input.url; + + // Only intercept /api/* requests + if (!url.includes('/api/')) { + return originalFetch(input, init); + } + + // Try the real server first + try { + const response = await originalFetch(input, init); + if (response.ok) { + return response; + } + throw new Error(`API error: ${response.status}`); + } catch { + // Backend unreachable — use mock data + const data = findMockData(url); + if (data !== undefined) { + console.debug(`[ARI Mock] ${init?.method || 'GET'} ${url}`); + return new Response(JSON.stringify(data), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // No mock data for this endpoint — return empty success for POSTs, throw for GETs + if (init?.method === 'POST' || init?.method === 'DELETE') { + console.debug(`[ARI Mock] ${init.method} ${url} → default success`); + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + console.warn(`[ARI Mock] No mock data for: ${url}`); + return new Response(JSON.stringify({ error: 'No mock data available' }), { + status: 503, + headers: { 'Content-Type': 'application/json' }, + }); + } + }; + + console.info( + '%c[ARI] Mock mode active — using demo data', + 'color: #a78bfa; font-weight: bold;', + ); +} From 23415321336ebaf64f6e2e6f9a87b4fff90904ef Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:33:26 -0500 Subject: [PATCH 03/20] fix(dashboard): CRIT-002 ErrorBoundary resets on page navigation Add key={currentPage} to ErrorBoundary so React unmounts/remounts it when navigating, clearing any caught error state. Co-Authored-By: Claude Opus 4.6 --- dashboard/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 858f3e6..9b78f6b 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -67,7 +67,7 @@ function AppContent() { - + {renderPage()} From 9c1f6c120e3feb1a854427b141d02ad746b8f8d3 Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:33:48 -0500 Subject: [PATCH 04/20] fix(dashboard): HIGH-004 WebSocket subscription memory leak Replace useState initializer with useEffect for proper cleanup. The unsubscribe function is now returned as useEffect's cleanup, ensuring handlers are removed when components unmount. Co-Authored-By: Claude Opus 4.6 --- dashboard/src/contexts/WebSocketContext.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dashboard/src/contexts/WebSocketContext.tsx b/dashboard/src/contexts/WebSocketContext.tsx index af56b2c..0bc8f41 100644 --- a/dashboard/src/contexts/WebSocketContext.tsx +++ b/dashboard/src/contexts/WebSocketContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useCallback, useState, type ReactNode } from 'react'; +import { createContext, useContext, useCallback, useEffect, useState, type ReactNode } from 'react'; import { useWebSocket, type WebSocketMessage, type ConnectionStatus } from '../hooks/useWebSocket'; interface WebSocketContextValue { @@ -119,7 +119,8 @@ export function useWebSocketEvent( const stableHandler = useCallback(handler, deps); // Subscribe on mount, unsubscribe on unmount - useState(() => { - return subscribe(eventType, stableHandler); - }); + useEffect(() => { + const unsubscribe = subscribe(eventType, stableHandler); + return unsubscribe; + }, [eventType, stableHandler, subscribe]); } From 84253b80f19587e62916d5fb578cbe80292dc009 Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:36:57 -0500 Subject: [PATCH 05/20] feat(dashboard): configure Tailwind 4 @theme for CSS var utilities Register all design system CSS variables as Tailwind theme tokens: - bg-bg-card, text-text-muted, border-border-muted, etc. - Brand: bg-ari-purple, text-ari-success, etc. - Pillar: bg-pillar-logos, text-pillar-ethos, etc. Also add prefers-reduced-motion media query for accessibility. Co-Authored-By: Claude Opus 4.6 --- dashboard/src/index.css | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/dashboard/src/index.css b/dashboard/src/index.css index 39ff88d..87261d3 100644 --- a/dashboard/src/index.css +++ b/dashboard/src/index.css @@ -1,5 +1,53 @@ @import "tailwindcss"; +@theme { + /* Background colors → bg-bg-* */ + --color-bg-void: var(--bg-void); + --color-bg-primary: var(--bg-primary); + --color-bg-secondary: var(--bg-secondary); + --color-bg-tertiary: var(--bg-tertiary); + --color-bg-elevated: var(--bg-elevated); + --color-bg-interactive: var(--bg-interactive); + --color-bg-card: var(--bg-card); + --color-bg-card-hover: var(--bg-card-hover); + + /* Text colors → text-text-* */ + --color-text-primary: var(--text-primary); + --color-text-secondary: var(--text-secondary); + --color-text-tertiary: var(--text-tertiary); + --color-text-muted: var(--text-muted); + --color-text-disabled: var(--text-disabled); + + /* Border colors → border-border-* */ + --color-border-subtle: var(--border-subtle); + --color-border-muted: var(--border-muted); + --color-border-default: var(--border-default); + --color-border-strong: var(--border-strong); + --color-border-purple: var(--border-purple); + + /* ARI brand colors → bg-ari-*, text-ari-*, border-ari-* */ + --color-ari-purple: var(--ari-purple); + --color-ari-purple-muted: var(--ari-purple-muted); + --color-ari-purple-dark: var(--ari-purple-dark); + --color-ari-success: var(--ari-success); + --color-ari-success-muted: var(--ari-success-muted); + --color-ari-error: var(--ari-error); + --color-ari-error-muted: var(--ari-error-muted); + --color-ari-warning: var(--ari-warning); + --color-ari-warning-muted: var(--ari-warning-muted); + --color-ari-info: var(--ari-info); + --color-ari-info-muted: var(--ari-info-muted); + --color-ari-cyan: var(--ari-cyan); + + /* Cognitive pillar colors → bg-pillar-*, text-pillar-* */ + --color-pillar-logos: var(--pillar-logos); + --color-pillar-logos-muted: var(--pillar-logos-muted); + --color-pillar-ethos: var(--pillar-ethos); + --color-pillar-ethos-muted: var(--pillar-ethos-muted); + --color-pillar-pathos: var(--pillar-pathos); + --color-pillar-pathos-muted: var(--pillar-pathos-muted); +} + * { margin: 0; padding: 0; @@ -885,3 +933,19 @@ select:focus-visible { visibility: visible; transform: translateX(-50%) translateY(-8px); } + +/* ═══════════════════════════════════════════════════════════════════════════ + * REDUCED MOTION — Respect user accessibility preferences + * ═══════════════════════════════════════════════════════════════════════════ */ + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + .shimmer, .shimmer-purple, + .animate-pulse-glow, .animate-breathe-purple, + .status-dot-healthy { animation: none !important; } +} From 20ef93fad4ec4e14d97e39e1291d7f908d251ad7 Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:39:58 -0500 Subject: [PATCH 06/20] feat(dashboard): shared Card, MetricCard, CollapsibleSection, TabGroup, DataTable Zero-inline-style component library using Tailwind @theme utilities: - Card: variant borders (pillar/status colors), padding/hover props - MetricCard: color-coded stat display with size variants - PageHeader/SectionHeader: consistent header patterns - CollapsibleSection: progressive disclosure primitive - TabGroup: accessible tab navigation with role/aria - DataTable: generic sortable table with loading/empty states - PageSkeleton: Suspense fallback for lazy-loaded pages Co-Authored-By: Claude Opus 4.6 --- dashboard/src/components/PageSkeleton.tsx | 31 +++++ dashboard/src/components/ui/Card.tsx | 54 +++++++++ .../src/components/ui/CollapsibleSection.tsx | 56 +++++++++ dashboard/src/components/ui/DataTable.tsx | 111 ++++++++++++++++++ dashboard/src/components/ui/MetricCard.tsx | 46 ++++++++ dashboard/src/components/ui/PageHeader.tsx | 27 +++++ dashboard/src/components/ui/SectionHeader.tsx | 21 ++++ dashboard/src/components/ui/TabGroup.tsx | 38 ++++++ 8 files changed, 384 insertions(+) create mode 100644 dashboard/src/components/PageSkeleton.tsx create mode 100644 dashboard/src/components/ui/Card.tsx create mode 100644 dashboard/src/components/ui/CollapsibleSection.tsx create mode 100644 dashboard/src/components/ui/DataTable.tsx create mode 100644 dashboard/src/components/ui/MetricCard.tsx create mode 100644 dashboard/src/components/ui/PageHeader.tsx create mode 100644 dashboard/src/components/ui/SectionHeader.tsx create mode 100644 dashboard/src/components/ui/TabGroup.tsx diff --git a/dashboard/src/components/PageSkeleton.tsx b/dashboard/src/components/PageSkeleton.tsx new file mode 100644 index 0000000..777a683 --- /dev/null +++ b/dashboard/src/components/PageSkeleton.tsx @@ -0,0 +1,31 @@ +export function PageSkeleton() { + return ( +
+ {/* Header skeleton */} +
+
+
+
+
+
+
+ + {/* Tab bar skeleton */} +
+ + {/* Metric cards skeleton */} +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ + {/* Content skeleton */} +
+
+
+
+
+
+ ); +} diff --git a/dashboard/src/components/ui/Card.tsx b/dashboard/src/components/ui/Card.tsx new file mode 100644 index 0000000..20df363 --- /dev/null +++ b/dashboard/src/components/ui/Card.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from 'react'; + +const VARIANT_BORDERS: Record = { + default: 'border-border-muted', + logos: 'border-[var(--pillar-logos-border)]', + ethos: 'border-[var(--pillar-ethos-border)]', + pathos: 'border-[var(--pillar-pathos-border)]', + success: 'border-ari-success/20', + error: 'border-ari-error/20', + warning: 'border-ari-warning/20', +}; + +const PADDING: Record = { + none: '', + sm: 'p-3', + md: 'p-5', + lg: 'p-6', +}; + +interface CardProps { + children: ReactNode; + className?: string; + variant?: 'default' | 'logos' | 'ethos' | 'pathos' | 'success' | 'error' | 'warning'; + padding?: 'none' | 'sm' | 'md' | 'lg'; + hoverable?: boolean; + onClick?: () => void; +} + +export function Card({ + children, + className = '', + variant = 'default', + padding = 'md', + hoverable = false, + onClick, +}: CardProps) { + const base = 'rounded-xl border bg-bg-card'; + const border = VARIANT_BORDERS[variant]; + const pad = PADDING[padding]; + const hover = hoverable ? 'card-ari-hover cursor-pointer' : ''; + const interactive = onClick ? 'cursor-pointer' : ''; + + return ( +
{ if (e.key === 'Enter' || e.key === ' ') onClick(); } : undefined} + > + {children} +
+ ); +} diff --git a/dashboard/src/components/ui/CollapsibleSection.tsx b/dashboard/src/components/ui/CollapsibleSection.tsx new file mode 100644 index 0000000..790e072 --- /dev/null +++ b/dashboard/src/components/ui/CollapsibleSection.tsx @@ -0,0 +1,56 @@ +import { useState, type ReactNode } from 'react'; + +interface CollapsibleSectionProps { + title: string; + summary?: string; + children: ReactNode; + defaultCollapsed?: boolean; + badge?: ReactNode; + headerAction?: ReactNode; +} + +export function CollapsibleSection({ + title, + summary, + children, + defaultCollapsed = false, + badge, + headerAction, +}: CollapsibleSectionProps) { + const [collapsed, setCollapsed] = useState(defaultCollapsed); + + return ( +
+ + {!collapsed && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/dashboard/src/components/ui/DataTable.tsx b/dashboard/src/components/ui/DataTable.tsx new file mode 100644 index 0000000..a7ef350 --- /dev/null +++ b/dashboard/src/components/ui/DataTable.tsx @@ -0,0 +1,111 @@ +import { useState, type ReactNode } from 'react'; + +interface Column { + key: string; + header: string; + render: (item: T) => ReactNode; + align?: 'left' | 'center' | 'right'; + sortable?: boolean; +} + +interface DataTableProps { + columns: Column[]; + data: T[]; + getRowKey: (item: T) => string; + isLoading?: boolean; + emptyMessage?: string; + onRowClick?: (item: T) => void; +} + +export function DataTable({ + columns, + data, + getRowKey, + isLoading = false, + emptyMessage = 'No data available', + onRowClick, +}: DataTableProps) { + const [sortKey, setSortKey] = useState(null); + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); + + const handleSort = (key: string) => { + if (sortKey === key) { + setSortDir(sortDir === 'asc' ? 'desc' : 'asc'); + } else { + setSortKey(key); + setSortDir('asc'); + } + }; + + const alignClass = (align?: string) => { + if (align === 'center') return 'text-center'; + if (align === 'right') return 'text-right'; + return 'text-left'; + }; + + if (isLoading) { + return ( +
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ ))} +
+
+ ); + } + + if (data.length === 0) { + return ( +
+

{emptyMessage}

+
+ ); + } + + return ( +
+
+ + + + {columns.map((col) => ( + + ))} + + + + {data.map((item) => ( + onRowClick(item) : undefined} + > + {columns.map((col) => ( + + ))} + + ))} + +
handleSort(col.key) : undefined} + > + + {col.header} + {col.sortable && sortKey === col.key && ( + {sortDir === 'asc' ? '↑' : '↓'} + )} + +
+ {col.render(item)} +
+
+
+ ); +} diff --git a/dashboard/src/components/ui/MetricCard.tsx b/dashboard/src/components/ui/MetricCard.tsx new file mode 100644 index 0000000..83aee1d --- /dev/null +++ b/dashboard/src/components/ui/MetricCard.tsx @@ -0,0 +1,46 @@ +const COLOR_MAP: Record = { + default: { value: 'text-text-primary', bg: 'bg-bg-tertiary' }, + purple: { value: 'text-ari-purple', bg: 'bg-ari-purple-muted' }, + success: { value: 'text-ari-success', bg: 'bg-ari-success-muted' }, + error: { value: 'text-ari-error', bg: 'bg-ari-error-muted' }, + warning: { value: 'text-ari-warning', bg: 'bg-ari-warning-muted' }, + info: { value: 'text-ari-info', bg: 'bg-ari-info-muted' }, + logos: { value: 'text-pillar-logos', bg: 'bg-pillar-logos-muted' }, + ethos: { value: 'text-pillar-ethos', bg: 'bg-pillar-ethos-muted' }, + pathos: { value: 'text-pillar-pathos', bg: 'bg-pillar-pathos-muted' }, +}; + +const SIZE_MAP: Record = { + sm: { value: 'text-lg', label: 'text-[9px]', pad: 'p-2.5' }, + md: { value: 'text-2xl', label: 'text-[10px]', pad: 'p-4' }, + lg: { value: 'text-3xl', label: 'text-xs', pad: 'p-5' }, +}; + +interface MetricCardProps { + label: string; + value: string | number; + color?: keyof typeof COLOR_MAP; + sublabel?: string; + size?: 'sm' | 'md' | 'lg'; +} + +export function MetricCard({ + label, + value, + color = 'default', + sublabel, + size = 'md', +}: MetricCardProps) { + const colors = COLOR_MAP[color]; + const sizes = SIZE_MAP[size]; + + return ( +
+
{value}
+
{label}
+ {sublabel && ( +
{sublabel}
+ )} +
+ ); +} diff --git a/dashboard/src/components/ui/PageHeader.tsx b/dashboard/src/components/ui/PageHeader.tsx new file mode 100644 index 0000000..ae1ef43 --- /dev/null +++ b/dashboard/src/components/ui/PageHeader.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; + +interface PageHeaderProps { + title: string; + subtitle?: string; + icon?: string; + actions?: ReactNode; +} + +export function PageHeader({ title, subtitle, icon, actions }: PageHeaderProps) { + return ( +
+
+ {icon && ( + + )} +
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ {actions &&
{actions}
} +
+ ); +} diff --git a/dashboard/src/components/ui/SectionHeader.tsx b/dashboard/src/components/ui/SectionHeader.tsx new file mode 100644 index 0000000..025aaa5 --- /dev/null +++ b/dashboard/src/components/ui/SectionHeader.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react'; + +interface SectionHeaderProps { + title: string; + icon?: string; + badge?: ReactNode; + action?: ReactNode; +} + +export function SectionHeader({ title, icon, badge, action }: SectionHeaderProps) { + return ( +
+
+ {icon && } +

{title}

+ {badge} +
+ {action} +
+ ); +} diff --git a/dashboard/src/components/ui/TabGroup.tsx b/dashboard/src/components/ui/TabGroup.tsx new file mode 100644 index 0000000..e17f116 --- /dev/null +++ b/dashboard/src/components/ui/TabGroup.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from 'react'; + +interface Tab { + id: string; + label: string; + icon?: string; + badge?: ReactNode; +} + +interface TabGroupProps { + tabs: Tab[]; + activeTab: string; + onTabChange: (tabId: string) => void; +} + +export function TabGroup({ tabs, activeTab, onTabChange }: TabGroupProps) { + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +} From 92f861ae3032dc95904b43f23128322223fd29c3 Mon Sep 17 00:00:00 2001 From: PryceWayne <85907457+PryceWayne@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:45:24 -0500 Subject: [PATCH 07/20] feat(dashboard): CRIT-003 React Router v7 with lazy loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install react-router-dom v7 - Rewrite App.tsx with BrowserRouter, lazy imports, Suspense fallback - Update Sidebar: NavLink replaces onClick, 11 pages → 7 consolidated - Layout: remove onNavigate prop (router handles navigation) - CommandPalette: update page list to match 7-page structure Co-Authored-By: Claude Opus 4.6 --- dashboard/package-lock.json | 58 +++++ dashboard/package.json | 1 + dashboard/src/App.tsx | 82 +++---- dashboard/src/components/CommandPalette.tsx | 11 +- dashboard/src/components/Layout.tsx | 13 +- dashboard/src/components/Sidebar.tsx | 233 ++++++++------------ 6 files changed, 189 insertions(+), 209 deletions(-) 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 9b78f6b..4daf025 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 */} - - - + navigate('/governance')} /> + navigate(`/${page}`)} /> + - {renderPage()} + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + @@ -79,7 +61,9 @@ function App() { return ( - + + + ); diff --git a/dashboard/src/components/CommandPalette.tsx b/dashboard/src/components/CommandPalette.tsx index 643d451..b0f4827 100644 --- a/dashboard/src/components/CommandPalette.tsx +++ b/dashboard/src/components/CommandPalette.tsx @@ -17,14 +17,13 @@ interface SearchItem { } const PAGES: SearchItem[] = [ - { id: 'home', type: 'page', title: 'Overview', description: 'Dashboard home', icon: '◉', onSelect: () => {} }, - { 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 ( <> -
- +
+
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 (