From 4ddbfb9ec1c82daaa9821b8ded001e0b51217b90 Mon Sep 17 00:00:00 2001 From: irtassedat Date: Mon, 6 Apr 2026 15:30:42 +0300 Subject: [PATCH] feat(dashboard): add Skills panel with executions and obstacles view - Add Skills section showing registered skills with success rate - Add Skill Executions section with obstacle reporting - Add Skills stat card in header - Update mock data with DEMO_SKILLS and DEMO_SKILL_EXECUTIONS - Showcase the new Skills system from the feat/skills-system branch --- packages/dashboard/src/app/page.tsx | 385 +++++++++++++++++++++--- packages/dashboard/src/lib/mock-data.ts | 196 +++++++++++- 2 files changed, 524 insertions(+), 57 deletions(-) diff --git a/packages/dashboard/src/app/page.tsx b/packages/dashboard/src/app/page.tsx index 4013fbf..41577f6 100644 --- a/packages/dashboard/src/app/page.tsx +++ b/packages/dashboard/src/app/page.tsx @@ -1,7 +1,14 @@ "use client"; import { useEffect, useState, useCallback } from "react"; -import { DEMO_AGENTS, DEMO_METRICS, DEMO_TASKS, DEMO_DLQ } from "../lib/mock-data"; +import { + DEMO_AGENTS, + DEMO_METRICS, + DEMO_TASKS, + DEMO_DLQ, + DEMO_SKILLS, + DEMO_SKILL_EXECUTIONS, +} from "../lib/mock-data"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:3000/ws"; @@ -23,7 +30,12 @@ interface Agent { interface Metrics { agents: { total: number; running: number; failed: number; idle: number }; - performance: { totalProcessed: number; totalFailed: number; avgUptime: number; totalMemoryMb: number }; + performance: { + totalProcessed: number; + totalFailed: number; + avgUptime: number; + totalMemoryMb: number; + }; system: { nodeVersion: string; uptime: number; memoryMb: number }; } @@ -37,12 +49,57 @@ interface Task { createdAt: string; } +interface Skill { + name: string; + description: string; + triggerPhrases: string[]; + executionCount: number; + lastExecuted: string | null; + avgDuration: number; + successRate: number; + cooldownRemaining: number; +} + +interface SkillObstacle { + type: string; + description: string; + severity: string; +} + +interface SkillExecution { + id: string; + skillName: string; + agentId: string; + status: "success" | "partial" | "failed"; + duration: number; + obstacles: SkillObstacle[]; + timestamp: string; + output: Record; +} + function Dot({ status }: { status: string }) { - const color = status === "running" ? "bg-ok pulse" : status === "idle" ? "bg-dim" : status === "paused" ? "bg-warn" : "bg-err"; + const color = + status === "running" + ? "bg-ok pulse" + : status === "idle" + ? "bg-dim" + : status === "paused" + ? "bg-warn" + : "bg-err"; return ; } -function StatCard({ label, value, sub, color = "text-text" }: { label: string; value: string | number; sub?: string; color?: string }) { +function StatCard({ + label, + value, + sub, + color = "text-text", +}: { + label: string; + value: string | number; + sub?: string; + color?: string; +}) { return (

{label}

@@ -64,6 +121,8 @@ export default function Dashboard() { const [metrics, setMetrics] = useState(null); const [tasks, setTasks] = useState([]); const [dlq, setDlq] = useState([]); + const [skills, setSkills] = useState([]); + const [skillExecutions, setSkillExecutions] = useState([]); const [connected, setConnected] = useState(false); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState>({}); @@ -72,9 +131,15 @@ export default function Dashboard() { try { const [agentsRes, metricsRes, tasksRes, dlqRes] = await Promise.allSettled([ fetch(`${API_URL}/api/agents`, { signal: AbortSignal.timeout(3000) }).then((r) => r.json()), - fetch(`${API_URL}/api/metrics`, { signal: AbortSignal.timeout(3000) }).then((r) => r.json()), - fetch(`${API_URL}/api/tasks?limit=20`, { signal: AbortSignal.timeout(3000) }).then((r) => r.json()), - fetch(`${API_URL}/api/tasks/dlq`, { signal: AbortSignal.timeout(3000) }).then((r) => r.json()), + fetch(`${API_URL}/api/metrics`, { signal: AbortSignal.timeout(3000) }).then((r) => + r.json() + ), + fetch(`${API_URL}/api/tasks?limit=20`, { signal: AbortSignal.timeout(3000) }).then((r) => + r.json() + ), + fetch(`${API_URL}/api/tasks/dlq`, { signal: AbortSignal.timeout(3000) }).then((r) => + r.json() + ), ]); const gotData = agentsRes.status === "fulfilled" && agentsRes.value?.data?.length > 0; @@ -84,6 +149,9 @@ export default function Dashboard() { if (metricsRes.status === "fulfilled") setMetrics(metricsRes.value.data); if (tasksRes.status === "fulfilled") setTasks(tasksRes.value.data); if (dlqRes.status === "fulfilled") setDlq(dlqRes.value.data); + // Skills not in API yet — use demo data + setSkills(DEMO_SKILLS); + setSkillExecutions(DEMO_SKILL_EXECUTIONS); setConnected(true); } else { // API unavailable or empty — load demo data @@ -91,6 +159,8 @@ export default function Dashboard() { setMetrics(DEMO_METRICS); setTasks(DEMO_TASKS); setDlq(DEMO_DLQ); + setSkills(DEMO_SKILLS); + setSkillExecutions(DEMO_SKILL_EXECUTIONS); setConnected(true); } } catch { @@ -98,6 +168,8 @@ export default function Dashboard() { setMetrics(DEMO_METRICS); setTasks(DEMO_TASKS); setDlq(DEMO_DLQ); + setSkills(DEMO_SKILLS); + setSkillExecutions(DEMO_SKILL_EXECUTIONS); setConnected(true); } finally { setLoading(false); @@ -111,9 +183,13 @@ export default function Dashboard() { function connect() { try { ws = new WebSocket(WS_URL); - } catch { return; } + } catch { + return; + } ws.onopen = () => setConnected(true); - ws.onerror = () => { /* demo mode — ignore WS errors */ }; + ws.onerror = () => { + /* demo mode — ignore WS errors */ + }; ws.onclose = () => { reconnectTimer = setTimeout(connect, 15000); }; @@ -130,12 +206,17 @@ export default function Dashboard() { if (event.type === "task_update") { setTasks((prev) => [event.data, ...prev.slice(0, 19)]); } - } catch { /* ignore */ } + } catch { + /* ignore */ + } }; } connect(); - return () => { ws?.close(); clearTimeout(reconnectTimer); }; + return () => { + ws?.close(); + clearTimeout(reconnectTimer); + }; }, []); useEffect(() => { @@ -162,7 +243,11 @@ export default function Dashboard() { const failed = agents.filter((a) => a.status === "failed").length; const successRate = metrics?.performance ? metrics.performance.totalProcessed + metrics.performance.totalFailed > 0 - ? Math.round((metrics.performance.totalProcessed / (metrics.performance.totalProcessed + metrics.performance.totalFailed)) * 100) + ? Math.round( + (metrics.performance.totalProcessed / + (metrics.performance.totalProcessed + metrics.performance.totalFailed)) * + 100 + ) : 100 : 0; @@ -180,75 +265,173 @@ export default function Dashboard() {
{IS_DEMO && agents.length > 0 && ( - DEMO + + DEMO + )} -
+
{connected ? "Live" : "Offline"}
-
-
+
0 ? "text-err" : "text-dim"} /> - - = 90 ? "text-ok" : "text-warn"} /> - + sum + s.executionCount, 0)} runs`} + color="text-accent" + /> + + = 90 ? "text-ok" : "text-warn"} + /> +

Agents

{agents.map((agent) => ( -
+
{agent.id} - {agent.status} + + {agent.status} +
-
Processed

{agent.tasksProcessed}

-
Failed

0 ? "text-err font-medium" : "text-dim"}>{agent.tasksFailed}

-
Memory

{Math.round(agent.memoryUsage / 1024 / 1024)}MB

-
Uptime

{agent.uptime > 0 ? formatUptime(agent.uptime) : "\u2014"}

+
+ Processed +

{agent.tasksProcessed}

+
+
+ Failed +

0 ? "text-err font-medium" : "text-dim"}> + {agent.tasksFailed} +

+
+
+ Memory +

{Math.round(agent.memoryUsage / 1024 / 1024)}MB

+
+
+ Uptime +

{agent.uptime > 0 ? formatUptime(agent.uptime) : "\u2014"}

+
- {agent.currentTask &&

Processing: {agent.currentTask}

} - {agent.lastError &&

Error: {agent.lastError}

} + {agent.currentTask && ( +

Processing: {agent.currentTask}

+ )} + {agent.lastError && ( +

Error: {agent.lastError}

+ )}
- {agent.status === "idle" && } + {agent.status === "idle" && ( + + )} {agent.status === "running" && ( <> - - + + )} - {agent.status === "paused" && } - {(agent.status === "failed" || agent.status === "terminated") && } + {agent.status === "paused" && ( + + )} + {(agent.status === "failed" || agent.status === "terminated") && ( + + )}
))} {agents.length === 0 && !loading && ( -
No agents registered. Start the API server first.
+
+ No agents registered. Start the API server first. +
)}
-

Recent Tasks

+

+ Recent Tasks +

{tasks.map((task) => (
- + {task.type} {task.status}
@@ -258,12 +441,102 @@ export default function Dashboard() {
))} - {tasks.length === 0 &&
No tasks yet
} + {tasks.length === 0 && ( +
No tasks yet
+ )}
-

Dead Letter Queue ({dlq.length})

+

+ Skills ({skills.length}) +

+
+ {skills.map((skill) => ( +
+
+ = 95 + ? "bg-ok" + : skill.successRate >= 85 + ? "bg-warn" + : "bg-err" + }`} + /> + {skill.name} + {skill.executionCount}x +
+

{skill.description}

+
+ + avg: {skill.avgDuration}ms + + = 95 + ? "text-ok" + : skill.successRate >= 85 + ? "text-warn" + : "text-err" + } + > + {skill.successRate}% success + +
+
+ ))} + {skills.length === 0 && ( +
+ No skills registered +
+ )} +
+
+ +
+

+ Skill Executions +

+
+ {skillExecutions.map((exec) => ( +
+
+ + {exec.skillName} + {exec.duration}ms +
+
+ {exec.agentId} + + {exec.status} + +
+ {exec.obstacles.length > 0 && ( +

+ ⚠ {exec.obstacles[0].description} +

+ )} +
+ ))} + {skillExecutions.length === 0 && ( +
No executions yet
+ )} +
+
+ +
+

+ Dead Letter Queue ({dlq.length}) +

{dlq.map((task) => (
@@ -274,18 +547,36 @@ export default function Dashboard() {
))} - {dlq.length === 0 &&
Queue empty
} + {dlq.length === 0 && ( +
Queue empty
+ )}
{metrics && (
-

System

+

+ System +

-
Node.js{metrics.system.nodeVersion}
-
Uptime{formatUptime(metrics.system.uptime * 1000)}
-
Heap{metrics.system.memoryMb}MB
-
API{connected ? "Connected" : "Disconnected"}
+
+ Node.js + {metrics.system.nodeVersion} +
+
+ Uptime + {formatUptime(metrics.system.uptime * 1000)} +
+
+ Heap + {metrics.system.memoryMb}MB +
+
+ API + + {connected ? "Connected" : "Disconnected"} + +
)} diff --git a/packages/dashboard/src/lib/mock-data.ts b/packages/dashboard/src/lib/mock-data.ts index 4724fcc..54ad972 100644 --- a/packages/dashboard/src/lib/mock-data.ts +++ b/packages/dashboard/src/lib/mock-data.ts @@ -88,17 +88,193 @@ export const DEMO_METRICS = { }; export const DEMO_TASKS = [ - { id: "tsk_a8f2c1d9", agentId: "http-worker-2", type: "health_check", status: "processing", priority: "high", attempts: 0, createdAt: new Date().toISOString() }, - { id: "tsk_b3e7a2f0", agentId: "http-worker-1", type: "api_monitor", status: "completed", priority: "normal", attempts: 1, createdAt: new Date(Date.now() - 15000).toISOString() }, - { id: "tsk_c9d1f4e8", agentId: "scheduler-1", type: "cron_trigger", status: "completed", priority: "normal", attempts: 0, createdAt: new Date(Date.now() - 30000).toISOString() }, - { id: "tsk_d2a6b8c3", agentId: "http-worker-1", type: "webhook_dispatch", status: "completed", priority: "high", attempts: 0, createdAt: new Date(Date.now() - 45000).toISOString() }, - { id: "tsk_e7f0c1a5", agentId: "http-worker-2", type: "data_sync", status: "completed", priority: "low", attempts: 0, createdAt: new Date(Date.now() - 60000).toISOString() }, - { id: "tsk_f1b9d4e2", agentId: "watchdog-1", type: "agent_health", status: "completed", priority: "critical", attempts: 0, createdAt: new Date(Date.now() - 90000).toISOString() }, - { id: "tsk_g4c8a3f7", agentId: "http-worker-1", type: "api_monitor", status: "failed", priority: "normal", attempts: 3, createdAt: new Date(Date.now() - 120000).toISOString() }, - { id: "tsk_h6d2b5e1", agentId: "analytics-1", type: "report_gen", status: "completed", priority: "low", attempts: 0, createdAt: new Date(Date.now() - 180000).toISOString() }, + { + id: "tsk_a8f2c1d9", + agentId: "http-worker-2", + type: "health_check", + status: "processing", + priority: "high", + attempts: 0, + createdAt: new Date().toISOString(), + }, + { + id: "tsk_b3e7a2f0", + agentId: "http-worker-1", + type: "api_monitor", + status: "completed", + priority: "normal", + attempts: 1, + createdAt: new Date(Date.now() - 15000).toISOString(), + }, + { + id: "tsk_c9d1f4e8", + agentId: "scheduler-1", + type: "cron_trigger", + status: "completed", + priority: "normal", + attempts: 0, + createdAt: new Date(Date.now() - 30000).toISOString(), + }, + { + id: "tsk_d2a6b8c3", + agentId: "http-worker-1", + type: "webhook_dispatch", + status: "completed", + priority: "high", + attempts: 0, + createdAt: new Date(Date.now() - 45000).toISOString(), + }, + { + id: "tsk_e7f0c1a5", + agentId: "http-worker-2", + type: "data_sync", + status: "completed", + priority: "low", + attempts: 0, + createdAt: new Date(Date.now() - 60000).toISOString(), + }, + { + id: "tsk_f1b9d4e2", + agentId: "watchdog-1", + type: "agent_health", + status: "completed", + priority: "critical", + attempts: 0, + createdAt: new Date(Date.now() - 90000).toISOString(), + }, + { + id: "tsk_g4c8a3f7", + agentId: "http-worker-1", + type: "api_monitor", + status: "failed", + priority: "normal", + attempts: 3, + createdAt: new Date(Date.now() - 120000).toISOString(), + }, + { + id: "tsk_h6d2b5e1", + agentId: "analytics-1", + type: "report_gen", + status: "completed", + priority: "low", + attempts: 0, + createdAt: new Date(Date.now() - 180000).toISOString(), + }, ]; export const DEMO_DLQ = [ - { id: "tsk_x1y2z3", agentId: "http-worker-1", type: "webhook_dispatch", status: "dead_letter", priority: "high", attempts: 3, createdAt: new Date(Date.now() - 300000).toISOString() }, - { id: "tsk_x4y5z6", agentId: "http-worker-2", type: "api_monitor", status: "dead_letter", priority: "normal", attempts: 3, createdAt: new Date(Date.now() - 600000).toISOString() }, + { + id: "tsk_x1y2z3", + agentId: "http-worker-1", + type: "webhook_dispatch", + status: "dead_letter", + priority: "high", + attempts: 3, + createdAt: new Date(Date.now() - 300000).toISOString(), + }, + { + id: "tsk_x4y5z6", + agentId: "http-worker-2", + type: "api_monitor", + status: "dead_letter", + priority: "normal", + attempts: 3, + createdAt: new Date(Date.now() - 600000).toISOString(), + }, +]; + +export const DEMO_SKILLS = [ + { + name: "health-check", + description: "Checks service health endpoints and reports status", + triggerPhrases: ["health", "status", "check", "monitor"], + executionCount: 247, + lastExecuted: new Date(Date.now() - 45000).toISOString(), + avgDuration: 312, + successRate: 98, + cooldownRemaining: 0, + }, + { + name: "collect", + description: "Collects data from configured sources", + triggerPhrases: ["collect", "fetch", "gather", "scrape"], + executionCount: 184, + lastExecuted: new Date(Date.now() - 120000).toISOString(), + avgDuration: 845, + successRate: 94, + cooldownRemaining: 0, + }, + { + name: "transform", + description: "Transforms and processes raw data", + triggerPhrases: ["transform", "process", "normalize"], + executionCount: 184, + lastExecuted: new Date(Date.now() - 118000).toISOString(), + avgDuration: 156, + successRate: 100, + cooldownRemaining: 0, + }, + { + name: "validate", + description: "Validates data against rules", + triggerPhrases: ["validate", "verify", "check"], + executionCount: 184, + lastExecuted: new Date(Date.now() - 115000).toISOString(), + avgDuration: 89, + successRate: 91, + cooldownRemaining: 0, + }, +]; + +export const DEMO_SKILL_EXECUTIONS = [ + { + id: "exec_1", + skillName: "health-check", + agentId: "monitor-1", + status: "success" as const, + duration: 312, + obstacles: [], + timestamp: new Date(Date.now() - 45000).toISOString(), + output: { overallStatus: "OK", servicesUp: 5, servicesDown: 0 }, + }, + { + id: "exec_2", + skillName: "collect", + agentId: "pipeline-1", + status: "success" as const, + duration: 867, + obstacles: [], + timestamp: new Date(Date.now() - 120000).toISOString(), + output: { source: "api.example.com", recordCount: 156 }, + }, + { + id: "exec_3", + skillName: "validate", + agentId: "pipeline-1", + status: "partial" as const, + duration: 94, + obstacles: [{ type: "other", description: "3 records failed validation", severity: "warning" }], + timestamp: new Date(Date.now() - 115000).toISOString(), + output: { totalRecords: 156, validCount: 153, invalidCount: 3, passRate: 98 }, + }, + { + id: "exec_4", + skillName: "health-check", + agentId: "monitor-1", + status: "partial" as const, + duration: 5234, + obstacles: [{ type: "network", description: "Endpoint slow (>5s)", severity: "warning" }], + timestamp: new Date(Date.now() - 300000).toISOString(), + output: { overallStatus: "WARN", servicesUp: 4, servicesDown: 1 }, + }, + { + id: "exec_5", + skillName: "transform", + agentId: "pipeline-1", + status: "success" as const, + duration: 142, + obstacles: [], + timestamp: new Date(Date.now() - 180000).toISOString(), + output: { inputCount: 156, outputCount: 156, transformations: 2 }, + }, ];