← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-executive-ceo/ceo-dashboard-agent.ts
726 lines
/**
* DW-Agents: CEO Executive Dashboard
*
* The CEO is the Executive Director - directs ALL other agents.
* Company-wide KPIs, revenue tracking, agent fleet oversight,
* inter-agent messaging, and strategic AI advisor.
*
* Port: 7120
* PM2: dw-executive-ceo
* Access: Full system access - highest authority
*/
import express, { Request, Response } from 'express';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import Anthropic from '@anthropic-ai/sdk';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import fetch from 'node-fetch';
import { chatMiddleware } from '../shared-chat-integration';
import { requireGlobalAuth } from '../shared-global-auth';
import {
pool,
queryDB,
shopifyGraphQL,
getShopifyOrders,
getShopifyProductCount,
getShopifyCustomerCount,
getPM2Status,
getSystemHealth,
getVendorStats,
EXECUTIVE_AGENTS,
SERVER_IP,
sendMessage,
createMessageStore,
createActionItemStore,
execDashboardHead,
execNavBar,
execChatPanel,
formatUptime,
formatCurrency,
formatNumber,
timeAgo,
autoRefreshScript,
notifySlack,
AgentStatus
} from '../shared-executive-core';
// ES module __dirname fix
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load environment variables
dotenv.config({ path: path.join(__dirname, '..', '.env') });
// ── Constants ───────────────────────────────────────────────────────────────
const PORT = 7120;
const AGENT_HANDLE = '@ceo';
const AGENT_NAME = 'CEO';
const AGENT_ROLE = 'Executive Director';
// ── Anthropic Client ────────────────────────────────────────────────────────
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || ''
});
// ── Message & Action Stores ─────────────────────────────────────────────────
const messageStore = createMessageStore();
const actionStore = createActionItemStore();
// Seed initial action items based on system state
(async () => {
try {
const pm2 = await getPM2Status();
const stopped = pm2.filter(a => a.status !== 'online');
if (stopped.length > 0) {
actionStore.add('System Monitor', 'warning', `${stopped.length} Agent(s) Offline`,
`The following agents are not running: ${stopped.map(a => a.name).join(', ')}`);
}
const highRestart = pm2.filter(a => a.restarts > 10);
if (highRestart.length > 0) {
actionStore.add('Crash Monitor', 'critical', 'High Restart Count Detected',
`Agents with >10 restarts: ${highRestart.map(a => `${a.name} (${a.restarts})`).join(', ')}`);
}
} catch { /* startup check, non-fatal */ }
})();
// ── Sub-Agent Definitions ───────────────────────────────────────────────────
const SUB_AGENTS = [
{ name: 'Master Hub', port: 9893, emoji: '🎯', desc: 'Central control panel for all agents' },
{ name: 'Shopify Store', port: 7238, emoji: '🛍️', desc: 'E-commerce operations & product management' },
{ name: 'Marketing', port: 9881, emoji: '📢', desc: 'Campaign management & growth strategy' },
{ name: 'Digital Samples', port: 9879, emoji: '🎨', desc: 'DIG-series sample order processing' }
];
// ── Express Setup ───────────────────────────────────────────────────────────
const app = express();
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
declare module 'express-session' {
interface SessionData {
authenticated: boolean;
chatHistory: Array<{ role: 'user' | 'assistant'; content: string }>;
}
}
app.use(session({
secret: 'dw-ceo-executive-director-2025',
resave: false,
saveUninitialized: true,
rolling: true,
cookie: {
secure: false,
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000
}
}));
// Chat widget middleware
app.use(chatMiddleware({
agentId: 'agent-executive-ceo',
agentName: 'CEO Dashboard',
port: PORT,
category: 'executive'
}));
// ── Helper: Ping Executive Agent ────────────────────────────────────────────
async function pingAgent(port: number): Promise<{ online: boolean; data?: any }> {
try {
const resp = await fetch(`http://localhost:${port}/api/metrics`, {
headers: {
'Authorization': 'Basic ' + Buffer.from(process.env.BASIC_AUTH).toString('base64')
},
signal: AbortSignal.timeout(3000)
});
if (resp.ok) {
const data = await resp.json();
return { online: true, data };
}
return { online: false };
} catch {
return { online: false };
}
}
// ══════════════════════════════════════════════════════════════════════════════
// ROUTES
// ══════════════════════════════════════════════════════════════════════════════
// ── Logout ──────────────────────────────────────────────────────────────────
app.get('/logout', (req, res) => {
if (req.session) {
req.session.authenticated = false;
req.session.destroy?.(() => {});
}
res.clearCookie('dw_global_sso');
res.redirect('/login');
});
// ── API: Metrics (for other agents to fetch) ────────────────────────────────
app.get('/api/metrics', (_req: Request, res: Response) => {
res.json({
agent: AGENT_NAME,
handle: AGENT_HANDLE,
role: AGENT_ROLE,
status: 'online',
port: PORT,
uptime: process.uptime(),
unreadMessages: messageStore.unreadCount(),
actionItems: actionStore.count(),
lastActivity: new Date().toISOString()
});
});
// ── API: Messages (inbox) ───────────────────────────────────────────────────
app.get('/api/messages', requireGlobalAuth, (_req: Request, res: Response) => {
res.json({
messages: messageStore.getAll(),
unread: messageStore.unreadCount(),
total: messageStore.count()
});
});
// ── API: Receive message from another agent ─────────────────────────────────
app.post('/api/message', (req: Request, res: Response) => {
try {
const msg = req.body;
if (msg && msg.id && msg.from && msg.subject) {
messageStore.receive(msg);
console.log(`[CEO] Message received from ${msg.from}: ${msg.subject}`);
res.json({ success: true, received: msg.id });
} 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;
if (!to || !subject || !body) {
return res.status(400).json({ error: 'Missing required fields: to, subject, body' });
}
const success = await sendMessage(AGENT_HANDLE, to, subject, body, priority || 'normal');
if (success) {
console.log(`[CEO] Message sent to ${to}: ${subject}`);
res.json({ success: true, message: `Message sent to ${to}` });
} else {
res.status(404).json({ error: `Agent ${to} not found or unreachable` });
}
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});
// ── API: Chat with Claude AI (CEO strategic advisor) ────────────────────────
app.post('/api/chat', requireGlobalAuth, async (req: Request, res: Response) => {
try {
const { message, history } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message required' });
}
// Gather live context for the AI
let liveContext = '';
try {
const [todayData, weekData, productCount, customerCount] = await Promise.all([
getShopifyOrders('today'),
getShopifyOrders('week'),
getShopifyProductCount(),
getShopifyCustomerCount()
]);
liveContext = `
LIVE SHOPIFY DATA (real-time):
- Today: ${todayData.orders} orders, ${formatCurrency(todayData.revenue)} revenue, ${formatCurrency(todayData.avgValue)} avg order
- This Week: ${weekData.orders} orders, ${formatCurrency(weekData.revenue)} revenue
- Total Products: ${formatNumber(productCount)}
- Total Customers: ${formatNumber(customerCount)}
- Unread Messages: ${messageStore.unreadCount()}
- Active Action Items: ${actionStore.count()}`;
} catch { /* context fetch failed, continue without it */ }
const systemPrompt = `You are the CEO's strategic AI advisor for Designer Wallcoverings, a luxury wallcovering e-commerce company with 145K+ products across 15+ vendors on Shopify.
You provide strategic insights about revenue, growth, operations, and business decisions. You have access to live Shopify and agent data. Be concise, actionable, and executive-level in your responses.
The CEO directs all executive agents:
${Object.entries(EXECUTIVE_AGENTS).map(([h, a]) => `- ${a.emoji} ${a.name} (${a.role}) on port ${a.port}`).join('\n')}
Key business context:
- Vendors include Arte, Thibaut, Koroseal, Schumacher, Elitis, Phillip Jeffries, Maya Romanoff, York, Brewster, and more
- Products are primarily commercial/architectural wallcoverings
- Sample pricing is standardized at $4.25
- The business serves interior designers, architects, and trade professionals
${liveContext}`;
const chatHistory = history || [];
const messages = [
...chatHistory.slice(-16),
{ role: 'user' as const, content: message }
];
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 2048,
system: systemPrompt,
messages
});
const assistantMessage = response.content[0].type === 'text'
? response.content[0].text
: 'I apologize, but I encountered an error processing your request.';
res.json({ response: assistantMessage });
} catch (e: any) {
console.error('[CEO Chat Error]', e.message);
res.status(500).json({
response: 'I apologize, but I encountered an error. Please try again.',
error: e.message
});
}
});
// ── API: Dashboard data (all live data as JSON) ─────────────────────────────
app.get('/api/dashboard-data', requireGlobalAuth, async (_req: Request, res: Response) => {
try {
const [todayOrders, weekOrders, monthOrders, productCount, customerCount, pm2Status, systemHealth, vendorStats] = await Promise.all([
getShopifyOrders('today'),
getShopifyOrders('week'),
getShopifyOrders('month'),
getShopifyProductCount(),
getShopifyCustomerCount(),
getPM2Status(),
getSystemHealth(),
getVendorStats()
]);
res.json({
kpis: {
today: todayOrders,
week: weekOrders,
month: monthOrders,
products: productCount,
customers: customerCount
},
agents: pm2Status,
system: systemHealth,
vendors: vendorStats,
messages: {
unread: messageStore.unreadCount(),
total: messageStore.count()
},
actions: actionStore.count(),
timestamp: new Date().toISOString()
});
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});
// ══════════════════════════════════════════════════════════════════════════════
// MAIN DASHBOARD (GET /)
// ══════════════════════════════════════════════════════════════════════════════
app.get('/', requireGlobalAuth, async (req: Request, res: Response) => {
try {
// ── Parallel data loading ────────────────────────────────────────────
const [todayOrders, weekOrders, monthOrders, productCount, customerCount, pm2Status, systemHealth, vendorStats] = await Promise.all([
getShopifyOrders('today'),
getShopifyOrders('week'),
getShopifyOrders('month'),
getShopifyProductCount(),
getShopifyCustomerCount(),
getPM2Status(),
getSystemHealth(),
getVendorStats()
]);
const onlineAgents = pm2Status.filter(a => a.status === 'online').length;
const totalAgents = pm2Status.length;
const unread = messageStore.unreadCount();
const messages = messageStore.getAll();
const actionItems = actionStore.getAll();
// Ping executive agents for status
const execStatuses: Record<string, boolean> = {};
const execPings = Object.entries(EXECUTIVE_AGENTS)
.filter(([h]) => h !== AGENT_HANDLE)
.map(async ([h, a]) => {
const result = await pingAgent(a.port);
execStatuses[h] = result.online;
});
await Promise.all(execPings);
// ── Build HTML ───────────────────────────────────────────────────────
const html = `${execDashboardHead('CEO Dashboard', AGENT_HANDLE)}
${execNavBar('CEO Executive Dashboard', '👔', AGENT_HANDLE, unread)}
<div class="main">
<div class="content">
<!-- KPI Cards Row -->
<div class="kpi-grid">
<div class="kpi-card green">
<div class="label">Revenue Today</div>
<div class="value">${formatCurrency(todayOrders.revenue)}</div>
<div class="sub">${todayOrders.orders} order${todayOrders.orders !== 1 ? 's' : ''} | Avg ${formatCurrency(todayOrders.avgValue)}</div>
</div>
<div class="kpi-card blue">
<div class="label">Orders Today</div>
<div class="value">${todayOrders.orders}</div>
<div class="sub">Avg value ${formatCurrency(todayOrders.avgValue)}</div>
</div>
<div class="kpi-card green">
<div class="label">Revenue This Week</div>
<div class="value">${formatCurrency(weekOrders.revenue)}</div>
<div class="sub">${weekOrders.orders} orders | Month: ${formatCurrency(monthOrders.revenue)}</div>
</div>
<div class="kpi-card">
<div class="label">Total Products</div>
<div class="value">${formatNumber(productCount)}</div>
<div class="sub">Across 15+ vendors</div>
</div>
<div class="kpi-card blue">
<div class="label">Total Customers</div>
<div class="value">${formatNumber(customerCount)}</div>
<div class="sub">Trade professionals & designers</div>
</div>
<div class="kpi-card ${onlineAgents === totalAgents ? 'green' : 'amber'}">
<div class="label">Agents Online</div>
<div class="value">${onlineAgents}/${totalAgents}</div>
<div class="sub">${onlineAgents === totalAgents ? 'All systems operational' : `${totalAgents - onlineAgents} agent(s) offline`}</div>
</div>
</div>
<!-- Executive Team Status -->
<div class="section">
<h2>👔 Executive Team Status</h2>
<div class="exec-grid">
${Object.entries(EXECUTIVE_AGENTS)
.filter(([h]) => h !== AGENT_HANDLE)
.map(([h, a]) => {
const isOnline = execStatuses[h] || false;
return `<a href="http://${SERVER_IP}:${a.port}" target="_blank" class="exec-card">
<div class="emoji">${a.emoji}</div>
<div class="name">${a.name}</div>
<div class="role">${a.role}</div>
<div class="status ${isOnline ? 'online' : 'offline'}">${isOnline ? 'Online' : 'Offline'}</div>
</a>`;
}).join('')}
</div>
</div>
<!-- Sub-Agents -->
<div class="section">
<h2>🎯 Sub-Agents</h2>
<div class="sub-agents">
${SUB_AGENTS.map(a => `
<div class="sub-agent">
<a href="http://${SERVER_IP}:${a.port}" target="_blank">${a.emoji} ${a.name}</a>
<div class="desc">${a.desc} (port ${a.port})</div>
</div>
`).join('')}
</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 style="width:100%;border-collapse:collapse;margin-top:12px;">
<thead>
<tr style="border-bottom:1px solid #334155;">
<th style="text-align:left;padding:8px 12px;font-size:12px;color:#94a3b8;font-weight:600;">Skill</th>
<th style="text-align:left;padding:8px 12px;font-size:12px;color:#94a3b8;font-weight:600;">Description</th>
<th style="text-align:center;padding:8px 12px;font-size:12px;color:#94a3b8;font-weight:600;">Status</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Strategic Revenue Analysis</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Shopify revenue tracking, trend analysis, anomaly detection</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Agent Fleet Management</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Monitor and direct all 45+ DW agents</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Executive Team Coordination</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">@mention messaging to CFO, COO, VP, CTO</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Market Intelligence</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Trend research integration via sub-agents</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Shopify Store Oversight</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Product catalog, orders, customer analytics</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Marketing Campaign Review</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Campaign performance and ROI tracking</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">AI Strategic Advisor</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Claude-powered strategic business insights</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Slack Notifications</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Critical alerts to #claude-to-steve</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">Action Item Detection</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Auto-detect revenue/ops anomalies</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
<tr style="border-bottom:1px solid #1e293b;">
<td style="padding:8px 12px;font-size:13px;font-weight:600;color:#e2e8f0;">24/7 Automated Reporting</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Continuous KPI monitoring and reporting</td>
<td style="padding:8px 12px;text-align:center;"><span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#065f46;color:#34d399;">Active</span></td>
</tr>
</tbody>
</table>
</details>
</div>
<style>details[open] > summary span:first-child { transform: rotate(90deg); } details summary::-webkit-details-marker { display: none; }</style>
<!-- Action Items -->
<div class="section">
<h2>🔔 Action Items <span style="font-size:12px;color:#94a3b8;">(${actionItems.filter(i => !i.resolved).length} unresolved)</span></h2>
${actionItems.length === 0
? '<div style="color:#64748b;font-size:13px;padding:12px;">No action items at this time. All clear.</div>'
: actionItems.filter(i => !i.resolved).map(item => `
<div class="action-item ${item.severity}">
<div class="title">${item.title}</div>
<div class="desc">${item.description}</div>
<div class="meta">Source: ${item.source} | ${timeAgo(item.timestamp)} | Severity: ${item.severity.toUpperCase()}</div>
</div>
`).join('')}
</div>
<!-- Messages -->
<div class="section" id="messages-section">
<h2>📬 Messages <span style="font-size:12px;color:#94a3b8;">(${unread} unread of ${messages.length})</span></h2>
${messages.length === 0
? '<div style="color:#64748b;font-size:13px;padding:12px;">No messages yet. Use the send form below to communicate with other agents.</div>'
: `<ul class="msg-list">${messages.slice(0, 20).map(m => `
<li class="msg-item ${m.read ? '' : 'unread'}">
<span class="from">${m.from}</span>
<div style="flex:1;min-width:0;">
<div class="subject">${m.subject}
${m.priority !== 'normal' ? `<span class="priority-badge ${m.priority}">${m.priority}</span>` : ''}
</div>
<div class="body">${m.body.length > 120 ? m.body.substring(0, 120) + '...' : m.body}</div>
</div>
<span class="time">${timeAgo(m.timestamp)}</span>
</li>
`).join('')}</ul>`}
<!-- Send Message Form -->
<div style="margin-top:16px;padding:16px;background:#0f172a;border-radius:8px;border:1px solid #334155;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;margin-bottom:12px;">Send Message to Agent</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">
<select id="msgTo" style="background:#1e293b;border:1px solid #475569;color:#e2e8f0;padding:8px;border-radius:6px;font-size:13px;">
${Object.entries(EXECUTIVE_AGENTS).filter(([h]) => h !== AGENT_HANDLE).map(([h, a]) =>
`<option value="${h}">${a.emoji} ${a.name} (${h})</option>`
).join('')}
<option value="@all">All Agents (@all)</option>
</select>
<select id="msgPriority" style="background:#1e293b;border:1px solid #475569;color:#e2e8f0;padding:8px;border-radius:6px;font-size:13px;">
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="critical">Critical</option>
<option value="low">Low</option>
</select>
</div>
<input id="msgSubject" placeholder="Subject..." style="width:100%;background:#1e293b;border:1px solid #475569;color:#e2e8f0;padding:8px 12px;border-radius:6px;font-size:13px;margin-bottom:8px;">
<textarea id="msgBody" placeholder="Message body..." rows="3" style="width:100%;background:#1e293b;border:1px solid #475569;color:#e2e8f0;padding:8px 12px;border-radius:6px;font-size:13px;resize:vertical;margin-bottom:8px;"></textarea>
<button onclick="sendAgentMessage()" style="background:#3b82f6;color:#fff;border:none;padding:8px 20px;border-radius:6px;cursor:pointer;font-weight:600;font-size:13px;">Send Message</button>
<span id="msgStatus" style="margin-left:12px;font-size:12px;color:#64748b;"></span>
</div>
</div>
<!-- Agent Fleet Overview -->
<div class="section">
<h2>🖥️ Agent Fleet Overview</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Agent Name</th>
<th>Status</th>
<th>CPU %</th>
<th>Memory</th>
<th>Uptime</th>
<th>Restarts</th>
</tr>
</thead>
<tbody>
${pm2Status.map(a => `
<tr>
<td style="font-weight:500;">${a.name}</td>
<td>
<span class="status-dot ${a.status === 'online' ? 'online' : a.status === 'stopped' ? 'stopped' : 'errored'}"></span>
${a.status}
</td>
<td>${a.cpu}%</td>
<td>${a.memoryMB} MB</td>
<td>${formatUptime(a.uptime)}</td>
<td style="${a.restarts > 10 ? 'color:#ef4444;font-weight:600;' : ''}">${a.restarts}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div style="margin-top:12px;display:flex;gap:16px;font-size:12px;color:#64748b;">
<span>System Load: ${systemHealth.cpuLoad}</span>
<span>Memory: ${systemHealth.memUsed} / ${systemHealth.memTotal}</span>
<span>Disk: ${systemHealth.diskUsed} / ${systemHealth.diskTotal}</span>
</div>
</div>
<!-- Vendor Catalog Stats -->
${vendorStats.length > 0 ? `
<div class="section">
<h2>📦 Vendor Catalog Overview</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Vendor</th>
<th>Total Products</th>
<th>Imported to Shopify</th>
<th>With Images</th>
<th>Import %</th>
</tr>
</thead>
<tbody>
${vendorStats.map(v => {
const pct = v.total > 0 ? Math.round((v.imported / v.total) * 100) : 0;
return `<tr>
<td style="font-weight:500;">${v.vendor}</td>
<td>${formatNumber(v.total)}</td>
<td>${formatNumber(v.imported)}</td>
<td>${formatNumber(v.withImages)}</td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;background:#334155;border-radius:4px;height:6px;max-width:120px;">
<div style="width:${pct}%;background:${pct > 75 ? '#22c55e' : pct > 50 ? '#f59e0b' : '#ef4444'};height:100%;border-radius:4px;"></div>
</div>
<span style="font-size:11px;">${pct}%</span>
</div>
</td>
</tr>`;
}).join('')}
</tbody>
</table>
</div>
</div>` : ''}
</div>
<!-- Chat Panel (Right Side) -->
${execChatPanel('CEO', 'strategic advisor')}
</div>
<!-- Send Agent Message Script -->
<script>
async function sendAgentMessage() {
const to = document.getElementById('msgTo').value;
const subject = document.getElementById('msgSubject').value;
const body = document.getElementById('msgBody').value;
const priority = document.getElementById('msgPriority').value;
const status = document.getElementById('msgStatus');
if (!subject || !body) {
status.textContent = 'Subject and body required';
status.style.color = '#ef4444';
return;
}
status.textContent = 'Sending...';
status.style.color = '#94a3b8';
try {
const resp = await fetch('/api/send-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to, subject, body, priority })
});
const data = await resp.json();
if (data.success) {
status.textContent = 'Sent!';
status.style.color = '#22c55e';
document.getElementById('msgSubject').value = '';
document.getElementById('msgBody').value = '';
} else {
status.textContent = data.error || 'Failed';
status.style.color = '#ef4444';
}
} catch(e) {
status.textContent = 'Error: ' + e.message;
status.style.color = '#ef4444';
}
}
</script>
${autoRefreshScript(30000)}
</body>
</html>`;
res.send(html);
} catch (e: any) {
console.error('[CEO Dashboard Error]', e.message);
res.status(500).send(`
${execDashboardHead('CEO Dashboard - Error', AGENT_HANDLE)}
${execNavBar('CEO Executive Dashboard', '👔', AGENT_HANDLE, 0)}
<div class="main"><div class="content">
<div class="section" style="text-align:center;padding:60px;">
<h2 style="color:#ef4444;">Dashboard Error</h2>
<p style="color:#94a3b8;margin-top:12px;">Failed to load dashboard data. The system will auto-retry in 30 seconds.</p>
<p style="color:#475569;font-size:12px;margin-top:8px;">${e.message}</p>
<button onclick="location.reload()" style="margin-top:20px;background:#3b82f6;color:#fff;border:none;padding:10px 24px;border-radius:8px;cursor:pointer;font-weight:600;">Retry Now</button>
</div>
</div><div class="chat-panel"></div></div>
${autoRefreshScript(30000)}
</body></html>
`);
}
});
// ══════════════════════════════════════════════════════════════════════════════
// START SERVER
// ══════════════════════════════════════════════════════════════════════════════
app.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log(' 👔 CEO Executive Dashboard');
console.log(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(` 🌍 External: http://${SERVER_IP}:${PORT}`);
console.log(` 🏠 Local: http://localhost:${PORT}`);
console.log(` 📡 Metrics: http://localhost:${PORT}/api/metrics`);
console.log(` 💬 Messages: http://localhost:${PORT}/api/messages`);
console.log(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(' CEO Dashboard ready - Executive Director online');
console.log('');
});