From 84bed86bcff975732dade2f628d8fdb79840f501 Mon Sep 17 00:00:00 2001 From: nehaaagre16-create Date: Thu, 4 Jun 2026 15:24:26 +0000 Subject: [PATCH] Add Intelligence Dashboard integration --- intelligence-dashboard/app.tsx | 791 +++++++++++++++++++++++++++++++ intelligence-dashboard/worker.ts | 612 ++++++++++++++++++++++++ 2 files changed, 1403 insertions(+) create mode 100644 intelligence-dashboard/app.tsx create mode 100644 intelligence-dashboard/worker.ts diff --git a/intelligence-dashboard/app.tsx b/intelligence-dashboard/app.tsx new file mode 100644 index 00000000000..5b36902a520 --- /dev/null +++ b/intelligence-dashboard/app.tsx @@ -0,0 +1,791 @@ +import { + type PluginPageProps, + type PluginSidebarProps, + useHostNavigation, + usePluginData, + usePluginAction, +} from "@paperclipai/plugin-sdk/ui"; +import { useState, useEffect, useCallback, useRef } from "react"; + +// ── Types ── + +type AgentActivity = { + agentId: string; + agentName: string; + tasksCompleted: number; + tasksFailed: number; + avgDuration: number; + lastActivity: string; + status: "active" | "idle" | "error"; +}; + +type ProjectEvent = { + id: string; + type: "commit" | "agent_run" | "error" | "warning" | "info"; + message: string; + timestamp: string; + metadata?: Record; +}; + +type DashboardStats = { + totalAgents: number; + activeAgents: number; + errorAgents: number; + idleAgents: number; + totalTasks: number; + completedTasks: number; + failedTasks: number; + openIssues: number; + inProgressIssues: number; + blockedIssues: number; + doneIssues: number; +}; + +type AlertItem = { + severity: "warning" | "critical"; + metric: string; + message: string; + currentValue: number; + threshold: number; + suggestedAssignee: string; +}; + +type RetryTask = { + id: string; + agentId: string; + agentName: string; + taskId: string; + taskName: string; + error: string; + failedAt: string; + retryCount: number; + maxRetries: number; + status: string; +}; + +type GitHubFailure = { + type: string; + issueNumber: number; + title: string; + description: string; + suggestedFix: string; + severity: "high" | "medium" | "low"; + htmlUrl: string; + repoOwner: string; + repoName: string; +}; + +type GitHubPR = { + number: number; + title: string; + state: string; + author: string; + createdAt: string; + updatedAt: string; + htmlUrl: string; + draft: boolean; + mergeable: boolean | null; + checksStatus: "passing" | "failing" | "pending" | "unknown"; + reviewStatus: "approved" | "changes_requested" | "pending" | "unknown"; + repoOwner: string; + repoName: string; +}; + +type DashboardData = { + timestamp: string; + agents: AgentActivity[]; + events: ProjectEvent[]; + stats: DashboardStats; + alerts: AlertItem[]; +}; + +type ActiveProjectData = { + activeProject: { + name: string; + repo: string; + score: number; + lastActivity: number; + currentBranch: string; + timeSpent: number; + } | null; + allProjects: Array<{ + name: string; + repo: string; + score: number; + lastActivity: number; + lastFileEdit: number; + lastCommit: number; + currentBranch: string; + timeSpent: number; + recentEdits: number; + recentCommits: number; + }>; +}; + +// ── CSS ── + +const CSS = ` +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); + +.intel-dashboard { min-height:100%; font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif; background:#09090b; color:#e4e4e7; } + +/* Hide Paperclip's Back button - target parent scope */ +a[href*="/plugins"] ~ main > a:first-child, +main > a:first-of-type, +[data-testid="plugin-back-button"], +.plugin-page-back, +main > a:only-of-type { display:none !important; } + +/* Alert Banners */ +.intel-alerts-container { display:flex; flex-direction:column; gap:0; } +.intel-alert-banner { display:flex; align-items:center; gap:12px; padding:14px 24px; font-size:13px; background:#2D1B69; color:#c4b5fd; } +.intel-alert-banner.critical { background:#450a0a; color:#fca5a5; } +.intel-alert-banner.warning { background:#3f2c06; color:#fcd34d; } +.intel-alert-icon { font-size:16px; flex-shrink:0; } +.intel-alert-content { flex:1; display:flex; align-items:center; gap:8px; flex-wrap:wrap; } +.intel-alert-title { font-weight:700; } +.intel-alert-actions { display:flex; gap:10px; flex-shrink:0; } +.intel-btn { padding:6px 16px; border-radius:6px; font-size:12px; font-weight:600; cursor:pointer; border:none; transition:all 0.15s; font-family:inherit; } +.intel-btn-green { background:#15803d; color:#fff; } +.intel-btn-green:hover { background:#166534; } +.intel-btn-red { background:#b91c1c; color:#fff; } +.intel-btn-red:hover { background:#991b1b; } +.intel-btn-outline { background:transparent; color:currentColor; border:1px solid currentColor; opacity:0.8; } +.intel-btn-outline:hover { opacity:1; } + +/* Header Section */ +.intel-header-section { padding:24px 32px 16px; } +.intel-header-top { display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:16px; } +.intel-header-title { font-size:24px; font-weight:800; color:#fafafa; margin:0; letter-spacing:-0.02em; } +.intel-header-subtitle { font-size:13px; color:#a1a1aa; margin-top:4px; } +.intel-header-actions { display:flex; align-items:center; gap:12px; } +.intel-status-pill { display:flex; align-items:center; gap:6px; padding:5px 12px; border-radius:20px; font-size:12px; font-weight:700; } +.intel-status-pill.critical { background:rgba(185,28,28,0.15); color:#f87171; border:1px solid rgba(185,28,28,0.3); } +.intel-status-pill.warning { background:rgba(161,98,7,0.15); color:#fbbf24; border:1px solid rgba(161,98,7,0.3); } +.intel-status-dot { width:6px; height:6px; border-radius:50%; } +.intel-status-dot.red { background:#dc2626; } +.intel-status-dot.yellow { background:#fbbf24; } +.intel-status-dot.green { background:#16a34a; } +.intel-refresh-btn { padding:6px 16px; background:transparent; color:#a1a1aa; border:1px solid rgba(161,161,170,0.2); border-radius:6px; font-size:12px; font-weight:700; cursor:pointer; } +.intel-refresh-btn:hover { background:rgba(161,161,170,0.1); color:#e4e4e7; } + +/* Tabs */ +.intel-tabs { display:flex; gap:4px; border-bottom:1px solid rgba(161,161,170,0.1); padding:0 32px; } +.intel-tab { padding:10px 20px; background:transparent; border:none; border-bottom:2px solid transparent; color:#71717a; font-size:13px; font-weight:600; cursor:pointer; transition:all 0.15s; margin-bottom:-1px; } +.intel-tab:hover { color:#a1a1aa; } +.intel-tab.active { color:#d4d4d8; border-bottom-color:#d4d4d8; } +.intel-tab-badge { display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:9px; background:#dc2626; color:#fff; font-size:10px; font-weight:700; margin-left:6px; } + +/* Content */ +.intel-content { padding:24px 32px; } +.intel-section-title { font-size:14px; font-weight:700; color:#a1a1aa; text-transform:uppercase; letter-spacing:0.08em; margin-bottom:16px; } + +/* Metric Cards */ +.intel-metrics-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:20px; margin-bottom:32px; } +.intel-metric-card { background:#18181b; border:1px solid rgba(161,161,170,0.08); border-radius:10px; padding:20px; text-align:center; transition:all 0.2s; } +.intel-metric-card:hover { border-color:rgba(212,212,216,0.2); transform:translateY(-1px); } +.intel-metric-value { font-size:28px; font-weight:800; color:#fafafa; line-height:1; margin-bottom:6px; } +.intel-metric-label { font-size:12px; color:#71717a; font-weight:600; text-transform:uppercase; letter-spacing:0.06em; } + +/* Two Column Layout */ +.intel-two-col { display:grid; grid-template-columns:1fr 1fr; gap:24px; } +.intel-panel { background:#18181b; border:1px solid rgba(161,161,170,0.08); border-radius:12px; padding:24px; } +.intel-panel-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; } +.intel-panel-title { font-size:16px; font-weight:700; color:#fafafa; } +.intel-panel-badge { padding:4px 10px; border-radius:6px; font-size:11px; font-weight:700; } + +/* Issues List */ +.intel-issues-list { width:100%; display:flex; flex-direction:column; gap:10px; } +.intel-issue-item { display:flex; align-items:center; gap:12px; padding:12px 16px; background:#09090b; border-radius:8px; font-size:13px; } +.intel-issue-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; } +.intel-issue-dot.critical { background:#dc2626; } +.intel-issue-dot.warning { background:#fbbf24; } +.intel-issue-dot.green { background:#16a34a; } +.intel-issue-text { color:#d4d4d8; font-weight:500; } +.intel-issue-count { font-size:11px; color:#71717a; font-weight:600; margin-left:auto; } + +/* Overview */ +.intel-overview-list { display:flex; flex-direction:column; gap:16px; } +.intel-overview-item { display:flex; align-items:center; justify-content:space-between; padding:12px 0; border-bottom:1px solid rgba(161,161,170,0.06); } +.intel-overview-item:last-child { border-bottom:none; } +.intel-overview-label { font-size:13px; color:#a1a1aa; font-weight:500; } +.intel-overview-value { font-size:14px; font-weight:700; color:#fafafa; } +.intel-overview-value.teal { color:#14b8a6; } +.intel-overview-value.gold { color:#fbbf24; } +.intel-overview-value.red { color:#ef4444; } + +/* Loading */ +.intel-loading { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100vh; gap:20px; color:#71717a; } +.intel-spinner { width:40px; height:40px; border:3px solid #18181b; border-top-color:#d4d4d8; border-radius:50%; animation:spin 0.8s linear infinite; } + +/* Animations */ +@keyframes spin { to { transform:rotate(360deg); } } + +/* Responsive */ +@media (max-width:1024px) { + .intel-metrics-grid { grid-template-columns:repeat(2,1fr); } + .intel-two-col { grid-template-columns:1fr; } +} +@media (max-width:640px) { + .intel-metrics-grid { grid-template-columns:1fr; } + .intel-header-section { padding:16px; } + .intel-content { padding:16px; } + .intel-tabs { padding:0 16px; } +} +`; + +// ── Helpers ── + +function formatNumber(num: number): string { + return num.toLocaleString(); +} + +function timeAgo(date: string): string { + const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +// ── Main Hook ── + +function useDashboardData(companyId: string | null) { + const params = companyId ? { companyId } : {}; + const { data, loading, error, refresh } = usePluginData<{ data: DashboardData }>("dashboard", params); + const { data: retriesData } = usePluginData<{ tasks: RetryTask[] }>("failed-tasks", params); + const { data: githubData } = usePluginData<{ failures: GitHubFailure[] }>("github-failures", params); + const { data: prsData } = usePluginData<{ prs: GitHubPR[] }>("github-prs", params); + const { data: activeProjectData, refresh: refreshActiveProject } = usePluginData("active-project", params); + + return { + dashboard: data?.data || null, + pendingRetries: retriesData?.tasks || [], + githubFailures: githubData?.failures || [], + githubPRs: prsData?.prs || [], + activeProject: activeProjectData?.activeProject || null, + allProjects: activeProjectData?.allProjects || [], + refreshActiveProject, + loading, + error, + refresh, + }; +} + +// ── Main Page ── + +export function IntelligencePage(props: PluginPageProps) { + const companyId = props.context?.companyId || null; + const { dashboard, pendingRetries, githubFailures, githubPRs, activeProject, allProjects, refreshActiveProject, loading, error, refresh } = useDashboardData(companyId); + const [tab, setTab] = useState<"overview" | "agents" | "activity" | "notifications" | "github-failures" | "github-prs">("overview"); + const [dismissedAlerts, setDismissedAlerts] = useState>(new Set()); + const [retryResults, setRetryResults] = useState>({}); + const [selectedProject, setSelectedProject] = useState(""); + + // Filter issues/PRs by selected project + const effectiveProjectRepo = selectedProject || activeProject?.repo || ""; + const filteredFailures = effectiveProjectRepo + ? githubFailures.filter(f => `${f.repoOwner}/${f.repoName}` === effectiveProjectRepo) + : githubFailures; + const filteredPRs = effectiveProjectRepo + ? githubPRs.filter(pr => `${pr.repoOwner}/${pr.repoName}` === effectiveProjectRepo) + : githubPRs; + + const retryTask = usePluginAction("retry-task"); + const skipTask = usePluginAction("skip-task"); + const refreshSnapshot = usePluginAction("refresh"); + + const handleRetry = useCallback(async (taskId: string) => { + try { + const result = await retryTask({ taskId }); + setRetryResults(prev => ({ ...prev, [taskId]: result as { success: boolean; message: string } })); + setTimeout(() => refresh(), 2000); + } catch (err) { + setRetryResults(prev => ({ ...prev, [taskId]: { success: false, message: String(err) } })); + } + }, [retryTask, refresh]); + + const handleSkip = useCallback(async (taskId: string) => { + try { + const result = await skipTask({ taskId }); + setRetryResults(prev => ({ ...prev, [taskId]: result as { success: boolean; message: string } })); + setTimeout(() => refresh(), 1000); + } catch (err) { + setRetryResults(prev => ({ ...prev, [taskId]: { success: false, message: String(err) } })); + } + }, [skipTask, refresh]); + + // Auto-refresh every 30 seconds + useEffect(() => { + const interval = setInterval(() => { + refreshSnapshot({}); + refresh(); + refreshActiveProject(); + }, 30000); + return () => clearInterval(interval); + }, [refreshSnapshot, refresh, refreshActiveProject]); + + if (loading) return ( +
+ +
+
+

Loading Intelligence Dashboard...

+
+
+ ); + + if (error || !dashboard) return ( +
+ +
+
--
+
{error ? `Error: ${error.message}` : "Could not load dashboard data."}
+ +
+
+ ); + + const activeAlerts = dashboard.alerts.filter(a => !dismissedAlerts.has(a.metric)); + const criticalCount = activeAlerts.filter(a => a.severity === "critical").length; + const warningCount = activeAlerts.filter(a => a.severity === "warning").length; + + return ( +
+ + + {/* Header */} +
+
+
+

Intelligence Dashboard

+
Real-time Paperclip analytics
+
+
+ {allProjects.length > 0 && ( + + )} + {criticalCount > 0 && ( + + + {criticalCount} Critical + + )} + {warningCount > 0 && ( + + + {warningCount} Warnings + + )} +
+
+
+ + {/* Tabs */} +
+ {[ + { key: "overview" as const, label: "Overview" }, + { key: "agents" as const, label: "Agents" }, + { key: "activity" as const, label: "Activity" }, + { key: "notifications" as const, label: "Notifications", badge: pendingRetries.length + activeAlerts.length }, + { key: "github-failures" as const, label: "GitHub Issues", badge: filteredFailures.length }, + { key: "github-prs" as const, label: "Pull Requests", badge: filteredPRs.length }, + ].map(t => ( + + ))} +
+ + {/* Content */} +
+ {tab === "overview" && ( + <> + {/* Metric Cards - REAL DATA */} +
Project Metrics
+
+
+
{formatNumber(dashboard.stats.totalAgents)}
+
Total Agents
+
+
+
{formatNumber(dashboard.stats.activeAgents)}
+
Active
+
+
+
{formatNumber(dashboard.stats.errorAgents)}
+
In Error
+
+
+
{formatNumber(dashboard.stats.idleAgents)}
+
Idle
+
+
+ +
+
+
{formatNumber(dashboard.stats.openIssues)}
+
Open Issues
+
+
+
{formatNumber(dashboard.stats.inProgressIssues)}
+
In Progress
+
+
+
{formatNumber(dashboard.stats.blockedIssues)}
+
Blocked
+
+
+
{formatNumber(dashboard.stats.doneIssues)}
+
Done
+
+
+ + {/* Two Column */} +
+ {/* Agents Status */} +
+
+ Agent Status + + {dashboard.stats.activeAgents} Active + +
+
+ {dashboard.agents.map((agent, idx) => ( +
+ + {agent.agentName} + + {agent.status === "active" ? "Active" : agent.status === "error" ? "Error" : "Idle"} + +
+ ))} +
+
+ + {/* Recent Activity */} +
+
+ Recent Activity +
+
+ {dashboard.events.slice(0, 8).map((event, idx) => ( +
+ + {event.message} + {timeAgo(event.timestamp)} +
+ ))} + {dashboard.events.length === 0 && ( +
+ No recent activity +
+ )} +
+
+
+ + )} + + {tab === "agents" && ( +
+
+ All Agents + + {dashboard.stats.activeAgents} Active / {dashboard.stats.totalAgents} Total + +
+
+ {dashboard.agents.map((agent, idx) => ( +
+ +
+ {agent.agentName} + ID: {agent.agentId.slice(0, 8)}... +
+ + {agent.status === "active" ? "Active" : agent.status === "error" ? "Error" : "Idle"} + +
+ ))} +
+
+ )} + + {tab === "activity" && ( +
+
+ Activity Feed + + {dashboard.events.length} Events + +
+
+ {dashboard.events.map((event, idx) => ( +
+ +
+ {event.message} + {new Date(event.timestamp).toLocaleString()} +
+ {timeAgo(event.timestamp)} +
+ ))} + {dashboard.events.length === 0 && ( +
+ No activity recorded +
+ )} +
+
+ )} + + {tab === "notifications" && ( + <> + {/* Alerts */} + {activeAlerts.length > 0 && ( +
+
Active Alerts
+
+ {activeAlerts.map((alert, idx) => ( +
+ {alert.severity === "critical" ? "(!)" : "(i)"} +
+ {alert.metric} + {alert.message} + Value: {alert.currentValue} (threshold: {alert.threshold}) +
+
+ +
+
+ ))} +
+
+ )} + + {/* Failed Tasks */} +
Failed Tasks
+
+
+ Pending Actions + + {pendingRetries.length} Pending + +
+
+ {pendingRetries.length === 0 ? ( +
+ No failed tasks - all agents healthy! +
+ ) : ( + pendingRetries.map((task, idx) => ( +
+ +
+ {task.agentName} — {task.taskName} + {task.error} +
+
+ {retryResults[task.id]?.success ? ( + {retryResults[task.id]?.message} + ) : ( + <> + + + + )} +
+
+ )) + )} +
+
+ + )} + + {tab === "github-failures" && ( + <> + {filteredFailures.length === 0 ? ( +
+
+ All Clear{effectiveProjectRepo ? ` — ${effectiveProjectRepo}` : ""} +
+
+
+ No GitHub issues need attention right now. +
+
+
+ ) : ( + <> +
Issues Need Attention ({filteredFailures.length}){effectiveProjectRepo ? ` — ${effectiveProjectRepo}` : ""}
+ {filteredFailures.map((f, idx) => ( +
+
+
+ + {f.severity.toUpperCase()} + + #{f.issueNumber} + {f.repoOwner}/{f.repoName} +
+ + {f.type.replace("_", " ")} + +
+

{f.title}

+

{f.description}

+
+ How to fix: +
{f.suggestedFix}
+
+ + View on GitHub → + +
+ ))} + + )} + + )} + + {tab === "github-prs" && ( + <> + {filteredPRs.length === 0 ? ( +
+
+ No Open PRs{effectiveProjectRepo ? ` — ${effectiveProjectRepo}` : ""} +
+
+
+ No open pull requests right now. +
+
+
+ ) : ( + <> +
Open Pull Requests ({filteredPRs.length}){effectiveProjectRepo ? ` — ${effectiveProjectRepo}` : ""}
+ {filteredPRs.map((pr, idx) => ( +
+
+
+ + {pr.draft ? "DRAFT" : pr.checksStatus.toUpperCase()} + + #{pr.number} + {pr.repoOwner}/{pr.repoName} +
+ + {pr.reviewStatus.replace("_", " ")} + +
+

{pr.title}

+

+ By {pr.author} • {timeAgo(pr.createdAt)} +

+
+ Updated {timeAgo(pr.updatedAt)} + {pr.mergeable !== null && ( + + {pr.mergeable ? "Mergeable" : "Has Conflicts"} + + )} +
+ + View on GitHub → + +
+ ))} + + )} + + )} +
+
+ ); +} + +// ── Sidebar Link ── + +export function SidebarLink(props: PluginSidebarProps) { + const nav = useHostNavigation(); + return ( + + ); +} diff --git a/intelligence-dashboard/worker.ts b/intelligence-dashboard/worker.ts new file mode 100644 index 00000000000..c8cd224f6bf --- /dev/null +++ b/intelligence-dashboard/worker.ts @@ -0,0 +1,612 @@ +import { definePlugin, runWorker, type PluginContext } from "@paperclipai/plugin-sdk"; +import * as fs from "fs"; + +const PLUGIN_NAME = "intelligence-dashboard"; +const PAPERCLIP_API = "http://localhost:3100/api"; +const FALLBACK_COMPANY_ID = "9975a9e0-9845-43eb-b33b-7e6b122b4a82"; + +// ── Load .env from plugin directory ── +function loadEnv() { + try { + const envPath = new URL("../.env", import.meta.url).pathname; + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, "utf-8"); + for (const line of content.split("\n")) { + const idx = line.indexOf("="); + if (idx > 0) { + const key = line.slice(0, idx).trim(); + const value = line.slice(idx + 1).trim(); + if (key && value && !process.env[key]) { + process.env[key] = value; + } + } + } + } + } catch { + // Ignore env loading errors + } +} +loadEnv(); + +// GitHub config from env +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; +let GITHUB_OWNER = process.env.GITHUB_OWNER || "nehaaagre16-create"; +let GITHUB_REPO = process.env.GITHUB_REPO || "Bug-reporter-dashboard"; + +// ── Multi-Project Config ── +interface ProjectConfig { + name: string; + repo: string; + path: string; +} + +function loadProjects(): ProjectConfig[] { + const projects: ProjectConfig[] = []; + let i = 1; + while (process.env[`PROJECT_${i}_NAME`]) { + projects.push({ + name: process.env[`PROJECT_${i}_NAME`]!, + repo: process.env[`PROJECT_${i}_REPO`]!, + path: process.env[`PROJECT_${i}_PATH`]!, + }); + i++; + } + return projects; +} + +const PROJECTS = loadProjects(); + +// ── Types ── +interface GitHubFailure { + type: string; + issueNumber: number; + title: string; + description: string; + suggestedFix: string; + severity: "high" | "medium" | "low"; + htmlUrl: string; + repoOwner: string; + repoName: string; +} + +interface GitHubPR { + number: number; + title: string; + state: string; + author: string; + createdAt: string; + updatedAt: string; + htmlUrl: string; + draft: boolean; + mergeable: boolean | null; + checksStatus: "passing" | "failing" | "pending" | "unknown"; + reviewStatus: "approved" | "changes_requested" | "pending" | "unknown"; + repoOwner: string; + repoName: string; +} + +interface AgentActivity { + agentId: string; + agentName: string; + tasksCompleted: number; + tasksFailed: number; + avgDuration: number; + lastActivity: string; + status: "active" | "idle" | "error"; +} + +interface ProjectEvent { + id: string; + type: "commit" | "agent_run" | "error" | "warning" | "info"; + message: string; + timestamp: string; + metadata?: Record; +} + +interface DashboardStats { + totalAgents: number; + activeAgents: number; + errorAgents: number; + idleAgents: number; + totalTasks: number; + completedTasks: number; + failedTasks: number; + openIssues: number; + inProgressIssues: number; + blockedIssues: number; + doneIssues: number; +} + +interface AlertItem { + severity: "warning" | "critical"; + metric: string; + message: string; + currentValue: number; + threshold: number; + suggestedAssignee: string; +} + +interface RetryTask { + id: string; + agentId: string; + agentName: string; + taskId: string; + taskName: string; + error: string; + failedAt: string; + retryCount: number; + maxRetries: number; + status: string; +} + +interface DashboardData { + timestamp: string; + agents: AgentActivity[]; + events: ProjectEvent[]; + stats: DashboardStats; + alerts: AlertItem[]; +} + +// ── API Clients ── +async function fetchPaperclipAPI(endpoint: string, options: RequestInit = {}) { + const response = await fetch(`${PAPERCLIP_API}${endpoint}`, { + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }); + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`); + } + return response.json(); +} + +async function fetchGitHubAPI(endpoint: string, options: RequestInit = {}, owner?: string, repo?: string) { + const o = owner || GITHUB_OWNER; + const r = repo || GITHUB_REPO; + const response = await fetch(`https://api.github.com/repos/${o}/${r}${endpoint}`, { + ...options, + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "Paperclip-Intelligence-Dashboard", + ...options.headers, + }, + }); + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + return response.json(); +} + +// ── GitHub PR Tracking ── +async function fetchGitHubPRsForRepo(ctx: PluginContext, owner: string, repo: string): Promise { + if (!GITHUB_TOKEN) return []; + + try { + const prs = await fetchGitHubAPI("/pulls?state=open&per_page=50", {}, owner, repo) as Array<{ + number: number; + title: string; + state: string; + user: { login: string }; + created_at: string; + updated_at: string; + html_url: string; + draft: boolean; + mergeable: boolean | null; + }>; + + // Return basic PR info without extra API calls (avoid timeout) + return prs.map(pr => ({ + number: pr.number, + title: pr.title, + state: pr.state, + author: pr.user.login, + createdAt: pr.created_at, + updatedAt: pr.updated_at, + htmlUrl: pr.html_url, + draft: pr.draft, + mergeable: pr.mergeable, + checksStatus: "unknown" as GitHubPR["checksStatus"], + reviewStatus: "pending" as GitHubPR["reviewStatus"], + repoOwner: owner, + repoName: repo, + })); + } catch (err) { + ctx.logger.warn(`GitHub PR tracking error for ${owner}/${repo}: ${err}`); + return []; + } +} + +async function fetchGitHubPRs(ctx: PluginContext): Promise { + if (!GITHUB_TOKEN) { + ctx.logger.warn("GITHUB_TOKEN not set, skipping GitHub PR tracking"); + return []; + } + + const allPRs: GitHubPR[] = []; + for (const project of PROJECTS) { + const [owner, repo] = project.repo.split("/"); + const prs = await fetchGitHubPRsForRepo(ctx, owner, repo); + allPRs.push(...prs); + } + + ctx.logger.info(`Tracked ${allPRs.length} total open PRs across all projects`); + return allPRs; +} + +// ── GitHub Failure Detection ── +async function fetchGitHubFailuresForRepo(ctx: PluginContext, owner: string, repo: string): Promise { + if (!GITHUB_TOKEN) return []; + + try { + const issues = await fetchGitHubAPI("/issues?state=open&per_page=50", {}, owner, repo) as Array<{ + number: number; + title: string; + body: string | null; + labels: Array<{ name: string }>; + html_url: string; + updated_at: string; + pull_request?: unknown; + }>; + + const failures: GitHubFailure[] = []; + const now = new Date(); + + for (const issue of issues) { + const daysSinceUpdate = Math.floor((now.getTime() - new Date(issue.updated_at).getTime()) / (1000 * 60 * 60 * 24)); + const labels = issue.labels.map(l => l.name.toLowerCase()); + + let type: GitHubFailure["type"] = "test_failed"; + let severity: "high" | "medium" | "low" = "medium"; + let suggestedFix = "1. Review the issue description\n2. Determine if action is needed\n3. Assign to appropriate team member\n4. Update status as work progresses\n5. Close when resolved"; + + if (labels.includes("bug") || labels.includes("critical")) { + type = "test_failed"; + severity = "high"; + suggestedFix = "1. Read the bug description carefully\n2. Reproduce the issue locally\n3. Write a failing test that captures the bug\n4. Fix the code and verify the test passes\n5. Close the issue with a reference to the fix"; + } else if (labels.includes("paperclip-sync")) { + type = "stale"; + severity = "low"; + suggestedFix = "1. Review the synced issue from Paperclip\n2. Check if it's still relevant\n3. Update or close if no longer needed\n4. Sync status back to Paperclip if changed"; + } else if (daysSinceUpdate > 7) { + type = "stale"; + severity = "low"; + suggestedFix = "1. Review the stale issue\n2. Update with current status\n3. Close if no longer relevant\n4. Reassign if needed"; + } + + failures.push({ + type, + issueNumber: issue.number, + title: issue.title, + description: `${labels.length > 0 ? `[${labels.join(", ")}] ` : ""}${issue.body?.substring(0, 100) || "No description"}`, + suggestedFix, + severity, + htmlUrl: issue.html_url, + repoOwner: owner, + repoName: repo, + }); + } + + return failures; + } catch (err) { + ctx.logger.warn(`GitHub failure detection error for ${owner}/${repo}: ${err}`); + return []; + } +} + +async function fetchGitHubFailures(ctx: PluginContext): Promise { + if (!GITHUB_TOKEN) { + ctx.logger.warn("GITHUB_TOKEN not set, skipping GitHub failure detection"); + return []; + } + + const allFailures: GitHubFailure[] = []; + for (const project of PROJECTS) { + const [owner, repo] = project.repo.split("/"); + const failures = await fetchGitHubFailuresForRepo(ctx, owner, repo); + allFailures.push(...failures); + } + + ctx.logger.info(`Detected ${allFailures.length} total GitHub failures across all projects`); + return allFailures; +} + +// ── Real Data Fetchers ── +async function fetchAgents(companyId: string): Promise { + const agents = await fetchPaperclipAPI(`/companies/${companyId}/agents`) as Array<{ + id: string; + name: string; + status: string; + lastHeartbeatAt?: string; + pauseReason?: string | null; + }>; + + return agents.map(agent => { + let mappedStatus: "active" | "idle" | "error" = "idle"; + if (agent.status === "running" || agent.status === "active") mappedStatus = "active"; + else if (agent.status === "error" || agent.status === "paused") mappedStatus = "error"; + + return { + agentId: agent.id, + agentName: agent.name, + tasksCompleted: 0, + tasksFailed: agent.status === "error" || agent.status === "paused" ? 1 : 0, + avgDuration: 0, + lastActivity: agent.lastHeartbeatAt || new Date().toISOString(), + status: mappedStatus, + }; + }); +} + +async function fetchDashboardStats(companyId: string): Promise { + const issues = await fetchPaperclipAPI(`/companies/${companyId}/issues?limit=100`) as Array<{ status: string }>; + + const backlogCount = issues.filter(i => i.status === "backlog").length; + const doneCount = issues.filter(i => i.status === "done").length; + const inProgressCount = issues.filter(i => i.status === "in_progress").length; + const blockedCount = issues.filter(i => i.status === "blocked").length; + + const agents = await fetchPaperclipAPI(`/companies/${companyId}/agents`) as Array<{ status: string }>; + + const activeAgentCount = agents.filter(a => a.status === "running").length; + const pausedAgentCount = agents.filter(a => a.status === "paused").length; + const errorAgentCount = agents.filter(a => a.status === "error").length; + const idleAgentCount = agents.filter(a => a.status === "idle").length; + + return { + totalAgents: agents.length, + activeAgents: activeAgentCount, + errorAgents: errorAgentCount, + idleAgents: idleAgentCount || pausedAgentCount, + totalTasks: issues.length, + completedTasks: doneCount, + failedTasks: blockedCount, + openIssues: backlogCount, + inProgressIssues: inProgressCount, + blockedIssues: blockedCount, + doneIssues: doneCount, + }; +} + +async function fetchActivity(companyId: string): Promise { + const activities = await fetchPaperclipAPI(`/companies/${companyId}/activity?limit=20`) as Array<{ + id: string; + action: string; + actorType: string; + actorId: string; + createdAt: string; + details?: Record; + }>; + + return activities.map(activity => { + let type: ProjectEvent["type"] = "info"; + if (activity.action.includes("error") || activity.action.includes("failed")) type = "error"; + else if (activity.action.includes("warning")) type = "warning"; + else if (activity.action.includes("run") || activity.action.includes("execute")) type = "agent_run"; + + return { + id: activity.id, + type, + message: `${activity.actorType}: ${activity.action}`, + timestamp: activity.createdAt, + metadata: activity.details, + }; + }); +} + +// ── Generate Dashboard Data ── +async function generateDashboardData(ctx: PluginContext, companyId?: string): Promise { + const effectiveCompanyId = companyId || ctx.company?.id || FALLBACK_COMPANY_ID; + + const [agents, stats, events] = await Promise.all([ + fetchAgents(effectiveCompanyId), + fetchDashboardStats(effectiveCompanyId), + fetchActivity(effectiveCompanyId), + ]); + + const alerts: DashboardData["alerts"] = []; + + const errorAgents = agents.filter(a => a.status === "error"); + for (const agent of errorAgents) { + alerts.push({ + severity: "critical", + metric: `agent_${agent.agentName}`, + message: `${agent.agentName} is in error state`, + currentValue: 1, + threshold: 0, + suggestedAssignee: "Board", + }); + } + + if (stats.failedTasks > 0) { + alerts.push({ + severity: stats.failedTasks > 5 ? "critical" : "warning", + metric: "failed_tasks", + message: `${stats.failedTasks} blocked tasks`, + currentValue: stats.failedTasks, + threshold: 3, + suggestedAssignee: "CTO", + }); + } + + if (stats.openIssues > 10) { + alerts.push({ + severity: "warning", + metric: "open_issues", + message: `${stats.openIssues} open issues`, + currentValue: stats.openIssues, + threshold: 10, + suggestedAssignee: "CEO", + }); + } + + return { + timestamp: new Date().toISOString(), + agents, + events, + stats, + alerts, + }; +} + +// ── Failed Tasks Detection ── +const failedTasksMap = new Map(); + +async function detectFailedTasks(ctx: PluginContext, companyId?: string): Promise { + const effectiveCompanyId = companyId || ctx.company?.id || FALLBACK_COMPANY_ID; + + try { + const agents = await fetchPaperclipAPI(`/companies/${effectiveCompanyId}/agents`) as Array<{ + id: string; + name: string; + status: string; + pauseReason?: string | null; + }>; + + failedTasksMap.clear(); + + for (const agent of agents) { + if (agent.status === "error" || agent.status === "paused") { + const taskId = `task-${agent.id}`; + failedTasksMap.set(taskId, { + id: taskId, + agentId: agent.id, + agentName: agent.name, + taskId, + taskName: `${agent.name} Task`, + error: agent.pauseReason || "Agent encountered an error", + failedAt: new Date().toISOString(), + retryCount: 0, + maxRetries: 3, + status: "failed", + }); + } + } + + return Array.from(failedTasksMap.values()); + } catch (err) { + ctx.logger.warn(`Failed to detect failed tasks: ${err}`); + return []; + } +} + +// ── Plugin Definition ── +const intelligenceDashboardPlugin = definePlugin({ + async setup(ctx: PluginContext) { + ctx.logger.info("Intelligence Dashboard plugin setup"); + + // Register data handler for GitHub failures + ctx.data.register("github-failures", async () => { + const failures = await fetchGitHubFailures(ctx); + return { success: true, failures }; + }); + + // Register data handler for GitHub PRs + ctx.data.register("github-prs", async () => { + const prs = await fetchGitHubPRs(ctx); + return { success: true, prs }; + }); + + // Register data handler for dashboard data + ctx.data.register("dashboard", async (params: Record) => { + const companyId = typeof params.companyId === "string" ? params.companyId : undefined; + const data = await generateDashboardData(ctx, companyId); + return { success: true, data }; + }); + + // Register data handler for failed tasks + ctx.data.register("failed-tasks", async (params: Record) => { + const companyId = typeof params.companyId === "string" ? params.companyId : undefined; + const tasks = await detectFailedTasks(ctx, companyId); + return { success: true, tasks }; + }); + + // Register data handler for active project + ctx.data.register("active-project", async () => { + return { + success: true, + activeProject: PROJECTS.length > 0 ? { + name: PROJECTS[0].name, + repo: PROJECTS[0].repo, + score: 0, + lastActivity: Date.now(), + currentBranch: "", + timeSpent: 0, + } : null, + allProjects: PROJECTS.map(p => ({ + name: p.name, + repo: p.repo, + score: 0, + lastActivity: Date.now(), + lastFileEdit: 0, + lastCommit: 0, + currentBranch: "", + timeSpent: 0, + recentEdits: 0, + recentCommits: 0, + })), + }; + }); + + // Register action handler for retry + ctx.actions.register("retry-task", async (params: Record) => { + const taskId = typeof params.taskId === "string" ? params.taskId : ""; + const task = failedTasksMap.get(taskId); + if (!task) { + return { success: false, error: "Task not found" }; + } + + await fetchPaperclipAPI(`/agents/${task.agentId}/resume`, { method: "POST" }); + failedTasksMap.delete(taskId); + ctx.logger.info(`Retried task ${taskId} for agent ${task.agentName}`); + return { success: true, message: `Resumed ${task.agentName}` }; + }); + + // Register action handler for skip + ctx.actions.register("skip-task", async (params: Record) => { + const taskId = typeof params.taskId === "string" ? params.taskId : ""; + const task = failedTasksMap.get(taskId); + if (!task) { + return { success: false, error: "Task not found" }; + } + failedTasksMap.delete(taskId); + ctx.logger.info(`Skipped task ${taskId}`); + return { success: true, message: "Task skipped" }; + }); + + // Register action handler for refresh + ctx.actions.register("refresh", async (params: Record) => { + const companyId = typeof params.companyId === "string" ? params.companyId : undefined; + failedTasksMap.clear(); + await detectFailedTasks(ctx, companyId); + const data = await generateDashboardData(ctx, companyId); + return { success: true, data }; + }); + + // Register action handler for test notification + ctx.actions.register("test-notification", async () => { + const companyId = FALLBACK_COMPANY_ID; + const issue = await fetchPaperclipAPI(`/companies/${companyId}/issues`, { + method: "POST", + body: JSON.stringify({ + title: "Test Alert from Intelligence Dashboard", + description: "This is a test alert triggered from the Intelligence Dashboard.", + status: "backlog", + priority: "medium", + }), + }); + return { success: true, issueId: issue.id }; + }); + + ctx.logger.info("Intelligence Dashboard ready"); + }, + + async onHealth() { + return { status: "ok", message: `${PLUGIN_NAME} ready` }; + }, +}); + +export default intelligenceDashboardPlugin; +runWorker(intelligenceDashboardPlugin, import.meta.url);