← 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;">&#9654;</span> Skills &amp; 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('');
});