← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-executive-coo/coo-dashboard-agent.ts
477 lines
/**
* DW-Agents: COO Executive Dashboard
*
* Operations-focused executive dashboard:
* - Agent fleet health & performance monitoring
* - Workflow status and task throughput
* - Operations sub-agent oversight
* - Vendor operations pipeline tracking
* - AI-powered operations assistant
* - Inter-agent messaging and action items
*
* Port: 7122
* PM2: dw-executive-coo
*/
import cookieParser from 'cookie-parser';
import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import { requireGlobalAuth } from '../shared-global-auth';
import session from 'express-session';
import fetch from 'node-fetch';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { chatMiddleware } from '../shared-chat-integration';
import {
pool, queryDB, getPM2Status, getVendorStats,
EXECUTIVE_AGENTS, SERVER_IP,
sendMessage, createMessageStore, createActionItemStore,
execDashboardHead, execNavBar, execChatPanel,
formatUptime, formatNumber, timeAgo,
autoRefreshScript, notifySlack,
AgentStatus
} from '../shared-executive-core';
// ES module __dirname fix
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config({ path: path.join(__dirname, '..', '.env') });
// Global error handlers
process.on('uncaughtException', (error) => {
console.error('UNCAUGHT EXCEPTION:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION at:', promise, 'reason:', reason);
process.exit(1);
});
const app = express();
const PORT = 7122;
// Anthropic AI
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || '',
});
// Message & action item stores
const messageStore = createMessageStore();
const actionStore = createActionItemStore();
// Middleware
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(chatMiddleware({
agentId: 'agent-executive-coo',
agentName: 'COO Dashboard',
port: PORT,
category: 'executive'
}));
declare module 'express-session' {
interface SessionData {
authenticated: boolean;
chatHistory: Array<{ role: 'user' | 'assistant'; content: string }>;
}
}
app.use(session({
secret: 'dw-coo-dashboard-2025-operations',
resave: false,
saveUninitialized: true,
rolling: true,
cookie: { secure: false, httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 }
}));
// ---- Logout ----
app.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/login'));
});
// ---- Main Dashboard ----
app.get('/', requireGlobalAuth, async (req: Request, res: Response) => {
try {
// Fetch live data in parallel
const [agents, vendorStats] = await Promise.all([
getPM2Status(),
getVendorStats()
]);
// Compute KPIs from live PM2 data
const onlineAgents = agents.filter(a => a.status === 'online');
const offlineAgents = agents.filter(a => a.status !== 'online');
const totalRestarts = agents.reduce((sum, a) => sum + a.restarts, 0);
const avgMemory = agents.length > 0
? Math.round(agents.reduce((sum, a) => sum + a.memoryMB, 0) / agents.length)
: 0;
// Auto-detect action items from live data
if (offlineAgents.length > 3) {
actionStore.add('PM2 Monitor', 'critical', `${offlineAgents.length} Agents Offline`,
`${offlineAgents.map(a => a.name).join(', ')} are currently stopped or errored.`);
}
const highRestartAgents = agents.filter(a => a.restarts > 20);
highRestartAgents.forEach(a => {
actionStore.add('Crash Monitor', 'warning', `${a.name} - ${a.restarts} Restarts`,
`Agent has restarted ${a.restarts} times. Investigate logs and consider increasing memory limit.`);
});
// Sort agents: stopped first, then by restarts desc
const sortedAgents = [...agents].sort((a, b) => {
if (a.status !== 'online' && b.status === 'online') return -1;
if (a.status === 'online' && b.status !== 'online') return 1;
return b.restarts - a.restarts;
});
// Build agent table rows
const agentTableRows = sortedAgents.map(a => {
const statusClass = a.status === 'online' ? 'online' : (a.status === 'errored' ? 'errored' : 'stopped');
return `<tr>
<td><span class="status-dot ${statusClass}"></span>${a.name}</td>
<td>${a.status}</td>
<td>${a.cpu}%</td>
<td>${a.memoryMB} MB</td>
<td>${formatUptime(a.uptime)}</td>
<td style="color: ${a.restarts > 20 ? '#ef4444' : a.restarts > 10 ? '#f59e0b' : '#cbd5e1'}">${a.restarts}</td>
</tr>`;
}).join('');
// Build vendor operations table
const vendorTableRows = vendorStats.map(v => {
const pct = v.total > 0 ? Math.round((v.imported / v.total) * 100) : 0;
const pctColor = pct >= 80 ? '#22c55e' : pct >= 50 ? '#f59e0b' : '#ef4444';
return `<tr>
<td>${v.vendor}</td>
<td>${formatNumber(v.total)}</td>
<td>${formatNumber(v.imported)}</td>
<td>${formatNumber(v.withImages)}</td>
<td style="color: ${pctColor}; font-weight: 600">${pct}%</td>
</tr>`;
}).join('');
// Build messages HTML
const messages = messageStore.getAll();
const messagesHTML = messages.length > 0
? messages.slice(0, 10).map(m => `
<div class="msg-item ${m.read ? '' : 'unread'}">
<span class="from">${m.from}</span>
<div>
<div class="subject">${m.subject} <span class="priority-badge ${m.priority}">${m.priority}</span></div>
<div class="body">${m.body.substring(0, 120)}${m.body.length > 120 ? '...' : ''}</div>
</div>
<span class="time">${timeAgo(m.timestamp)}</span>
</div>`).join('')
: '<div style="padding:16px;color:#64748b;">No messages yet.</div>';
// Build action items HTML
const actions = actionStore.getUnresolved();
const actionsHTML = actions.length > 0
? actions.slice(0, 8).map(a => `
<div class="action-item ${a.severity}">
<div class="title">${a.title}</div>
<div class="desc">${a.description}</div>
<div class="meta">${a.source} - ${timeAgo(a.timestamp)}</div>
</div>`).join('')
: '<div style="padding:16px;color:#64748b;">No action items. Operations running smoothly.</div>';
// Sub-agents for operations
const subAgents = [
{ name: 'Needs Attention', port: 9886, desc: 'Priority alerts & issues' },
{ name: 'Zendesk Chat', port: 9884, desc: 'Customer support integration' },
{ name: 'Marketing', port: 9881, desc: 'Campaign management' },
{ name: 'Digital Samples', port: 9879, desc: 'Sample order processing' },
{ name: 'Legal Team', port: 9878, desc: 'Settlement compliance' }
];
const subAgentsHTML = subAgents.map(sa => `
<div class="sub-agent">
<a href="http://${SERVER_IP}:${sa.port}" target="_blank">${sa.name}</a>
<div class="desc">${sa.desc}</div>
</div>`).join('');
const html = `
${execDashboardHead('COO Operations Dashboard', '@coo')}
${execNavBar('COO Operations Dashboard', '\u2699\uFE0F', '@coo', messageStore.unreadCount())}
<div class="main">
<div class="content">
<!-- KPI Cards -->
<div class="kpi-grid">
<div class="kpi-card green">
<div class="label">Agents Online</div>
<div class="value">${onlineAgents.length}</div>
<div class="sub">of ${agents.length} total agents</div>
</div>
<div class="kpi-card ${offlineAgents.length > 0 ? 'red' : 'green'}">
<div class="label">Agents Offline</div>
<div class="value">${offlineAgents.length}</div>
<div class="sub">${offlineAgents.length > 0 ? offlineAgents.slice(0, 3).map(a => a.name).join(', ') : 'All systems operational'}</div>
</div>
<div class="kpi-card ${totalRestarts > 100 ? 'amber' : 'blue'}">
<div class="label">Total Restarts</div>
<div class="value">${formatNumber(totalRestarts)}</div>
<div class="sub">across all agents</div>
</div>
<div class="kpi-card blue">
<div class="label">Avg Memory</div>
<div class="value">${avgMemory} MB</div>
<div class="sub">per agent average</div>
</div>
</div>
<!-- Agent Performance Grid -->
<div class="section">
<h2>\u{1F4CA} Agent Performance Grid</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Agent</th>
<th>Status</th>
<th>CPU</th>
<th>Memory</th>
<th>Uptime</th>
<th>Restarts</th>
</tr>
</thead>
<tbody>${agentTableRows}</tbody>
</table>
</div>
</div>
<!-- Operations Sub-Agents -->
<div class="section">
<h2>\u{1F3AF} Operations Sub-Agents</h2>
<div class="sub-agents">${subAgentsHTML}</div>
</div>
<!-- Skills & Capabilities -->
<div class="section">
<details style="cursor:pointer;">
<summary style="font-size:16px;font-weight:700;color:#f8fafc;padding:8px 0;list-style:none;display:flex;align-items:center;gap:8px;">
<span style="transition:transform 0.2s;display:inline-block;">▶</span> 🛠️ Skills & Capabilities
<span style="font-size:12px;color:#94a3b8;margin-left:auto;">(10 skills)</span>
</summary>
<table class="skills-table">
<thead>
<tr>
<th>Skill</th>
<th>Description</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr><td>Agent Performance Monitoring</td><td>Real-time PM2 status for all 45+ agents</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Workflow Efficiency Tracking</td><td>Task completion rates and queue depths</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Customer Service Metrics</td><td>Zendesk ticket counts, resolution times</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Priority Alert Management</td><td>Needs Attention agent integration</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Legal Compliance Monitoring</td><td>Settlement compliance status via Legal Team</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Marketing Operations</td><td>Campaign operational metrics</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Digital Samples Fulfillment</td><td>Order processing and delivery tracking</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>AI Operations Manager</td><td>Claude-powered operational insights</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Agent Health Auto-Detection</td><td>Auto-detect >3 agents offline</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Crash Loop Alerting</td><td>Alert VP Ops when agents exceed 20 restarts</td><td><span class="skill-status">Active</span></td></tr>
</tbody>
</table>
</details>
</div>
<!-- Vendor Operations -->
<div class="section">
<h2>\u{1F4E6} Vendor Operations</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Vendor</th>
<th>Total Products</th>
<th>Imported</th>
<th>With Images</th>
<th>Completion</th>
</tr>
</thead>
<tbody>${vendorTableRows}</tbody>
</table>
</div>
</div>
<!-- Messages & Action Items -->
<div class="section" id="messages-section">
<h2>\u{1F4EC} Messages & Action Items</h2>
<div class="tab-bar">
<button class="active" onclick="showTab('msgs',this)">Messages (${messages.length})</button>
<button onclick="showTab('actions',this)">Action Items (${actions.length})</button>
</div>
<div id="tab-msgs">${messagesHTML}</div>
<div id="tab-actions" style="display:none">${actionsHTML}</div>
</div>
</div>
<!-- Chat Panel -->
${execChatPanel('COO', 'operations manager')}
</div>
<script>
function showTab(tab, btn) {
document.getElementById('tab-msgs').style.display = tab === 'msgs' ? 'block' : 'none';
document.getElementById('tab-actions').style.display = tab === 'actions' ? 'block' : 'none';
btn.parentElement.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
</script>
${autoRefreshScript(30000)}
</body></html>`;
res.send(html);
} catch (error: any) {
console.error('Dashboard render error:', error);
res.status(500).send('Dashboard error: ' + error.message);
}
});
// ---- API: Metrics ----
app.get('/api/metrics', requireGlobalAuth, (req: Request, res: Response) => {
res.json({
status: 'online',
agent: 'COO Dashboard',
port: PORT,
uptime: process.uptime(),
lastActivity: new Date().toISOString(),
messages: messageStore.count(),
unreadMessages: messageStore.unreadCount(),
actionItems: actionStore.count()
});
});
// ---- API: Messages ----
app.get('/api/messages', requireGlobalAuth, (req: Request, res: Response) => {
res.json({ messages: messageStore.getAll(), unread: messageStore.unreadCount() });
});
// ---- API: Receive Message (inter-agent) ----
app.post('/api/message', (req: Request, res: Response) => {
try {
const msg = req.body;
if (msg && msg.id) {
messageStore.receive(msg);
res.json({ success: true });
} else {
res.status(400).json({ error: 'Invalid message format' });
}
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});
// ---- API: Send Message (to another agent) ----
app.post('/api/send-message', requireGlobalAuth, async (req: Request, res: Response) => {
try {
const { to, subject, body, priority } = req.body;
const success = await sendMessage('@coo', to, subject, body, priority || 'normal');
res.json({ success });
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});
// ---- API: Chat ----
app.post('/api/chat', requireGlobalAuth, async (req: Request, res: Response) => {
try {
const { message, history } = req.body;
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
req.session.chatHistory.push({ role: 'user', content: message });
// Gather live context for the AI
let opsContext = '';
try {
const [agents, vendorStats] = await Promise.all([
getPM2Status(),
getVendorStats()
]);
const online = agents.filter(a => a.status === 'online').length;
const offline = agents.filter(a => a.status !== 'online');
const highRestart = agents.filter(a => a.restarts > 10);
opsContext = `\n\nLIVE OPERATIONS DATA:
- Agents: ${online}/${agents.length} online
- Offline: ${offline.map(a => a.name).join(', ') || 'None'}
- High restarts (>10): ${highRestart.map(a => `${a.name}(${a.restarts})`).join(', ') || 'None'}
- Total fleet restarts: ${agents.reduce((s, a) => s + a.restarts, 0)}
- Avg memory: ${Math.round(agents.reduce((s, a) => s + a.memoryMB, 0) / (agents.length || 1))}MB
- Vendor pipeline: ${vendorStats.map(v => `${v.vendor}: ${v.imported}/${v.total}`).join(', ')}`;
} catch { /* context fetch failed, continue without */ }
const systemPrompt = `You are the COO AI assistant for Designer Wallcoverings. You specialize in OPERATIONS management.
Your expertise includes:
- Agent fleet health monitoring and troubleshooting
- Workflow optimization and task throughput
- Vendor pipeline operations and import tracking
- Team coordination and resource allocation
- Process improvement and operational efficiency
- Incident response and system recovery
You have access to live PM2 agent data and vendor catalog statistics.
${opsContext}
Provide actionable operational insights. Be direct and solution-oriented.
When agents are down, suggest specific remediation steps.
When vendors have low import rates, suggest prioritization.`;
const chatHistory = (history || req.session.chatHistory).slice(-16);
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1500,
system: systemPrompt,
messages: chatHistory.map((m: any) => ({ role: m.role, content: m.content }))
});
const assistantMessage = response.content[0].type === 'text'
? response.content[0].text
: 'Unable to process request.';
req.session.chatHistory.push({ role: 'assistant', content: assistantMessage });
if (req.session.chatHistory.length > 20) {
req.session.chatHistory = req.session.chatHistory.slice(-20);
}
res.json({ response: assistantMessage });
} catch (error: any) {
console.error('Chat error:', error);
res.status(500).json({ response: 'Error processing chat request: ' + error.message });
}
});
// ---- Start Server ----
const server = app.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log('\u2699\uFE0F COO Operations Dashboard');
console.log('\u2501'.repeat(45));
console.log(`\u{1F30D} External: http://${SERVER_IP}:${PORT}`);
console.log(`\u{1F3E0} Local: http://localhost:${PORT}`);
console.log('');
console.log('\u2705 COO Dashboard ready...');
});
server.on('error', (error: any) => {
console.error('SERVER ERROR:', error);
if (error.code === 'EADDRINUSE') {
console.error(`Port ${PORT} is already in use`);
}
process.exit(1);
});