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
JavaScript
/**
* 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 };