UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

430 lines (361 loc) 10.6 kB
/** * Dashboard Integration Module * Connects the web dashboard with Shipdeck's workflow engine and AI systems */ const { DashboardServer } = require('./server'); const EventEmitter = require('events'); class DashboardIntegration extends EventEmitter { constructor(options = {}) { super(); this.dashboard = null; this.workflowEngine = options.workflowEngine; this.aiManager = options.aiManager; this.config = options.config; // Track metrics this.metrics = { workflows: { total: 0, completed: 0, failed: 0, averageTime: 0 }, agents: { total: 0, byType: {} }, costs: { total: 0, byModel: { haiku: 0, sonnet: 0, opus: 0 }, byAgent: {} } }; } /** * Initialize dashboard with proper event bindings */ async initialize() { // Create dashboard server this.dashboard = new DashboardServer({ port: this.config?.dashboardPort || 3456, workflowEngine: this.workflowEngine, aiManager: this.aiManager }); // Set up event bridges this.setupEventBridges(); // Start dashboard server await this.dashboard.start(); console.log(`📊 Dashboard initialized at http://localhost:${this.dashboard.port}`); return this; } /** * Bridge events between Shipdeck components and dashboard */ setupEventBridges() { // Workflow Engine Events if (this.workflowEngine) { // Workflow lifecycle this.workflowEngine.on('workflow:created', (workflow) => { this.handleWorkflowCreated(workflow); }); this.workflowEngine.on('node:started', (node) => { this.handleNodeStarted(node); }); this.workflowEngine.on('node:completed', (node) => { this.handleNodeCompleted(node); }); this.workflowEngine.on('workflow:completed', (workflow) => { this.handleWorkflowCompleted(workflow); }); this.workflowEngine.on('workflow:error', (error) => { this.handleWorkflowError(error); }); } // AI Manager Events if (this.aiManager) { // Agent lifecycle this.aiManager.on('agent:invoked', (data) => { this.handleAgentInvoked(data); }); this.aiManager.on('agent:response', (data) => { this.handleAgentResponse(data); }); this.aiManager.on('agent:error', (data) => { this.handleAgentError(data); }); // Cost tracking this.aiManager.on('api:call', (data) => { this.handleAPICall(data); }); } } /** * Handle workflow created event */ handleWorkflowCreated(workflow) { const workflowData = { id: workflow.id, name: workflow.name || 'MVP Build', nodes: this.extractNodeInfo(workflow.nodes), startTime: Date.now(), status: 'created' }; // Emit to dashboard this.dashboard.emit('workflow:started', workflowData); // Update metrics this.metrics.workflows.total++; // Log console.log(`🚀 Workflow started: ${workflowData.name} (${workflowData.id})`); } /** * Handle node started event */ handleNodeStarted(node) { const nodeData = { workflowId: node.workflowId, nodeId: node.id, name: node.name, agent: node.agent, progress: this.calculateProgress(node.workflowId) }; // If node has an agent, emit agent started if (node.agent) { this.dashboard.emit('agent:started', { id: `${node.workflowId}-${node.id}`, name: node.agent, task: node.name, workflowId: node.workflowId }); } // Update workflow progress this.dashboard.emit('workflow:progress', { id: node.workflowId, currentNode: node.name, progress: nodeData.progress }); } /** * Handle node completed event */ handleNodeCompleted(node) { const nodeData = { workflowId: node.workflowId, nodeId: node.id, name: node.name, duration: node.endTime - node.startTime, cost: node.cost || 0, tokensUsed: node.tokensUsed || 0 }; // If node had an agent, emit agent completed if (node.agent) { this.dashboard.emit('agent:completed', { id: `${node.workflowId}-${node.id}`, name: node.agent, tokensUsed: nodeData.tokensUsed, cost: nodeData.cost, duration: nodeData.duration }); // Update agent metrics this.metrics.agents.total++; this.metrics.agents.byType[node.agent] = (this.metrics.agents.byType[node.agent] || 0) + 1; this.metrics.costs.byAgent[node.agent] = (this.metrics.costs.byAgent[node.agent] || 0) + nodeData.cost; } // Update workflow progress const progress = this.calculateProgress(node.workflowId); this.dashboard.emit('workflow:progress', { id: node.workflowId, currentNode: 'Next: ' + this.getNextNode(node.workflowId), progress }); } /** * Handle workflow completed event */ handleWorkflowCompleted(workflow) { const duration = Date.now() - workflow.startTime; this.dashboard.emit('workflow:completed', { id: workflow.id, name: workflow.name, duration, totalCost: workflow.totalCost || 0, nodesCompleted: workflow.completedNodes?.length || 0 }); // Update metrics this.metrics.workflows.completed++; this.updateAverageTime(duration); console.log(`✅ Workflow completed: ${workflow.name} in ${(duration/1000/60).toFixed(1)} minutes`); } /** * Handle workflow error event */ handleWorkflowError(error) { this.dashboard.emit('workflow:error', { id: error.workflowId, error: error.message, node: error.nodeId }); // Update metrics this.metrics.workflows.failed++; console.error(`❌ Workflow error: ${error.message}`); } /** * Handle agent invoked event */ handleAgentInvoked(data) { const agentData = { id: data.id, name: data.agent, task: data.task, model: data.model, startTime: Date.now() }; this.dashboard.emit('agent:started', agentData); console.log(`🤖 Agent invoked: ${data.agent} for "${data.task}"`); } /** * Handle agent response event */ handleAgentResponse(data) { const responseData = { id: data.id, agent: data.agent, tokensUsed: data.tokensUsed, cost: data.cost, duration: data.duration, success: true }; this.dashboard.emit('agent:completed', responseData); // Update costs this.updateCosts(data.cost, data.model); } /** * Handle agent error event */ handleAgentError(data) { this.dashboard.emit('agent:error', { id: data.id, agent: data.agent, error: data.error, timestamp: Date.now() }); console.error(`❌ Agent error: ${data.agent} - ${data.error}`); } /** * Handle API call for cost tracking */ handleAPICall(data) { const cost = this.calculateCost(data.model, data.tokens); // Update metrics this.metrics.costs.total += cost; const modelTier = this.getModelTier(data.model); this.metrics.costs.byModel[modelTier] = (this.metrics.costs.byModel[modelTier] || 0) + cost; // Emit cost update this.dashboard.emit('cost:update', { model: data.model, tokens: data.tokens, cost, total: this.metrics.costs.total }); } /** * Calculate workflow progress */ calculateProgress(workflowId) { if (!this.workflowEngine) return 0; const workflow = this.workflowEngine.getWorkflow(workflowId); if (!workflow) return 0; const totalNodes = workflow.nodes?.length || 1; const completedNodes = workflow.completedNodes?.length || 0; return Math.round((completedNodes / totalNodes) * 100); } /** * Get next node in workflow */ getNextNode(workflowId) { if (!this.workflowEngine) return 'Processing...'; const workflow = this.workflowEngine.getWorkflow(workflowId); if (!workflow) return 'Unknown'; const nextNode = workflow.nodes?.find(n => n.status === 'pending'); return nextNode?.name || 'Completing...'; } /** * Extract node info for display */ extractNodeInfo(nodes) { if (!nodes) return []; return nodes.map(node => ({ id: node.id, name: node.name, agent: node.agent, dependencies: node.dependencies || [] })); } /** * Calculate API cost */ calculateCost(model, tokens) { const costs = { 'claude-3-5-haiku-20241022': { input: 0.001, output: 0.005 }, 'claude-3-5-sonnet-20241022': { input: 0.003, output: 0.015 }, 'claude-opus-4-1-20250805': { input: 0.015, output: 0.075 } }; const modelCost = costs[model] || costs['claude-3-5-sonnet-20241022']; return (tokens.input / 1000) * modelCost.input + (tokens.output / 1000) * modelCost.output; } /** * Get model tier from model name */ getModelTier(model) { if (model.includes('haiku')) return 'haiku'; if (model.includes('sonnet')) return 'sonnet'; if (model.includes('opus')) return 'opus'; return 'unknown'; } /** * Update costs */ updateCosts(cost, model) { this.metrics.costs.total += cost; const tier = this.getModelTier(model); this.metrics.costs.byModel[tier] = (this.metrics.costs.byModel[tier] || 0) + cost; // Update dashboard state this.dashboard.dashboardState.costs.session += cost; this.dashboard.dashboardState.costs.daily += cost; } /** * Update average workflow time */ updateAverageTime(duration) { const completed = this.metrics.workflows.completed; const currentAvg = this.metrics.workflows.averageTime; this.metrics.workflows.averageTime = ((currentAvg * (completed - 1)) + duration) / completed; } /** * Get current metrics */ getMetrics() { return { ...this.metrics, successRate: this.metrics.workflows.total > 0 ? Math.round((this.metrics.workflows.completed / this.metrics.workflows.total) * 100) : 100 }; } /** * Stop dashboard */ async stop() { if (this.dashboard) { await this.dashboard.stop(); } } } module.exports = { DashboardIntegration };