← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-executive-cto/cto-dashboard-agent.ts
551 lines
/**
* DW-Agents: CTO Executive Dashboard
*
* Technology-focused executive dashboard:
* - Catalog quality & data pipeline monitoring
* - Vendor integration completeness tracking
* - Data quality scorecard per vendor
* - Technology sub-agent oversight
* - AI-powered technical architect assistant
* - Inter-agent messaging and action items
*
* Port: 7124
* PM2: dw-executive-cto
*/
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, VENDOR_TABLES,
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 = 7124;
// 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-cto',
agentName: 'CTO 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-cto-dashboard-2025-technology',
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'));
});
// ---- Helper: Get detailed data quality per vendor ----
async function getDataQuality(): Promise<Array<{
vendor: string;
table: string;
total: number;
missingImages: number;
missingShopifyId: number;
missingAiColors: number;
}>> {
const results: Array<{
vendor: string;
table: string;
total: number;
missingImages: number;
missingShopifyId: number;
missingAiColors: number;
}> = [];
for (const v of VENDOR_TABLES) {
try {
// First check if ai_colors column exists
const colCheck = await queryDB(`
SELECT column_name FROM information_schema.columns
WHERE table_name = $1 AND column_name = 'ai_colors'
`, [v.table]);
const hasAiColors = colCheck.length > 0;
const aiColorsSelect = hasAiColors
? `COUNT(CASE WHEN ai_colors IS NULL OR array_length(ai_colors, 1) IS NULL THEN 1 END) as missing_ai_colors`
: `COUNT(*) as missing_ai_colors`;
const rows = await queryDB(`
SELECT
COUNT(*) as total,
COUNT(CASE WHEN image_url IS NULL OR image_url = '' THEN 1 END) as missing_images,
COUNT(CASE WHEN shopify_product_id IS NULL OR shopify_product_id = '' THEN 1 END) as missing_shopify,
${aiColorsSelect}
FROM ${v.table}
`);
if (rows[0]) {
results.push({
vendor: v.name,
table: v.table,
total: parseInt(rows[0].total),
missingImages: parseInt(rows[0].missing_images),
missingShopifyId: parseInt(rows[0].missing_shopify),
missingAiColors: parseInt(rows[0].missing_ai_colors)
});
}
} catch { /* table may not exist, skip */ }
}
return results;
}
// ---- Main Dashboard ----
app.get('/', requireGlobalAuth, async (req: Request, res: Response) => {
try {
// Fetch live data in parallel
const [vendorStats, dataQuality] = await Promise.all([
getVendorStats(),
getDataQuality()
]);
// Compute catalog KPIs
const totalProducts = vendorStats.reduce((sum, v) => sum + v.total, 0);
const totalImported = vendorStats.reduce((sum, v) => sum + v.imported, 0);
const totalWithImages = vendorStats.reduce((sum, v) => sum + v.withImages, 0);
const importRate = totalProducts > 0 ? Math.round((totalImported / totalProducts) * 100) : 0;
// Auto-detect action items from live data
if (importRate < 50) {
actionStore.add('Data Pipeline', 'warning', `Overall Import Rate: ${importRate}%`,
`Only ${importRate}% of catalog products are imported to Shopify. Prioritize vendor imports to increase coverage.`);
}
vendorStats.forEach(v => {
if (v.total > 0 && v.imported === 0) {
actionStore.add('Vendor Pipeline', 'critical', `${v.vendor}: 0 Imports`,
`${v.vendor} has ${v.total} products in the catalog but zero have been imported to Shopify.`);
}
});
// Build vendor pipeline table with color coding
const vendorPipelineRows = vendorStats.map(v => {
const pct = v.total > 0 ? Math.round((v.imported / v.total) * 100) : 0;
const rowBg = pct >= 80 ? '' : pct >= 50 ? 'background:rgba(245,158,11,0.06);' : 'background:rgba(239,68,68,0.06);';
const pctColor = pct >= 80 ? '#22c55e' : pct >= 50 ? '#f59e0b' : '#ef4444';
return `<tr style="${rowBg}">
<td>${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:#1e293b;border-radius:4px;height:6px;overflow:hidden;">
<div style="width:${pct}%;background:${pctColor};height:100%;"></div>
</div>
<span style="color:${pctColor};font-weight:700;min-width:40px;text-align:right;">${pct}%</span>
</div>
</td>
</tr>`;
}).join('');
// Build data quality scorecard
const dataQualityRows = dataQuality.map(dq => {
const imgPct = dq.total > 0 ? Math.round(((dq.total - dq.missingImages) / dq.total) * 100) : 0;
const shopifyPct = dq.total > 0 ? Math.round(((dq.total - dq.missingShopifyId) / dq.total) * 100) : 0;
const aiPct = dq.total > 0 ? Math.round(((dq.total - dq.missingAiColors) / dq.total) * 100) : 0;
const colorFor = (pct: number) => pct >= 80 ? '#22c55e' : pct >= 50 ? '#f59e0b' : '#ef4444';
return `<tr>
<td>${dq.vendor}</td>
<td>${formatNumber(dq.total)}</td>
<td style="color:${dq.missingImages > 0 ? '#ef4444' : '#22c55e'}">${formatNumber(dq.missingImages)}</td>
<td style="color:${dq.missingShopifyId > 0 ? '#f59e0b' : '#22c55e'}">${formatNumber(dq.missingShopifyId)}</td>
<td style="color:${dq.missingAiColors > 0 ? '#f59e0b' : '#22c55e'}">${formatNumber(dq.missingAiColors)}</td>
<td>
<div style="display:flex;gap:6px;">
<span style="font-size:11px;color:${colorFor(imgPct)};" title="Images">\u{1F5BC}${imgPct}%</span>
<span style="font-size:11px;color:${colorFor(shopifyPct)};" title="Shopify">\u{1F6D2}${shopifyPct}%</span>
<span style="font-size:11px;color:${colorFor(aiPct)};" title="AI Colors">\u{1F3A8}${aiPct}%</span>
</div>
</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. Technology pipelines healthy.</div>';
// Technology sub-agents
const subAgents = [
{ name: 'Product Scheduler', port: 9600, desc: 'Automated product scheduling pipeline' },
{ name: 'Shopify Expert', port: 9887, desc: 'Shopify API & data sync management' },
{ name: 'AI Analyzer', port: 9955, desc: 'Gemini 3.0 image analysis engine' },
{ name: 'Product Agent', port: 9604, desc: 'Product creation & updates' }
];
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('CTO Technology Dashboard', '@cto')}
${execNavBar('CTO Technology Dashboard', '\u{1F527}', '@cto', messageStore.unreadCount())}
<div class="main">
<div class="content">
<!-- KPI Cards -->
<div class="kpi-grid">
<div class="kpi-card blue">
<div class="label">Total Catalog Products</div>
<div class="value">${formatNumber(totalProducts)}</div>
<div class="sub">across ${vendorStats.length} vendors</div>
</div>
<div class="kpi-card ${totalImported > 0 ? 'green' : 'red'}">
<div class="label">Imported to Shopify</div>
<div class="value">${formatNumber(totalImported)}</div>
<div class="sub">${importRate}% of catalog</div>
</div>
<div class="kpi-card blue">
<div class="label">With Images</div>
<div class="value">${formatNumber(totalWithImages)}</div>
<div class="sub">${totalProducts > 0 ? Math.round((totalWithImages / totalProducts) * 100) : 0}% coverage</div>
</div>
<div class="kpi-card ${importRate >= 80 ? 'green' : importRate >= 50 ? 'amber' : 'red'}">
<div class="label">Import Rate</div>
<div class="value">${importRate}%</div>
<div class="sub">${importRate >= 80 ? 'Healthy pipeline' : importRate >= 50 ? 'Needs improvement' : 'Critical - pipeline stalled'}</div>
</div>
</div>
<!-- Vendor Pipeline Status -->
<div class="section">
<h2>\u{1F4CA} Vendor Pipeline Status</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Vendor</th>
<th>Total</th>
<th>Imported</th>
<th>Images</th>
<th>Completion</th>
</tr>
</thead>
<tbody>${vendorPipelineRows}</tbody>
</table>
</div>
</div>
<!-- Data Quality Scorecard -->
<div class="section">
<h2>\u{1F50D} Data Quality Scorecard</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Vendor</th>
<th>Total</th>
<th>Missing Images</th>
<th>Missing Shopify ID</th>
<th>Missing AI Colors</th>
<th>Health</th>
</tr>
</thead>
<tbody>${dataQualityRows}</tbody>
</table>
</div>
</div>
<!-- Technology Sub-Agents -->
<div class="section">
<h2>\u{1F4BB} Technology 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>Catalog Health Monitoring</td><td>Track products across 15+ vendor catalogs</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Vendor Pipeline Status</td><td>Per-vendor: crawled, imported, AI analyzed, published</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Data Quality Scoring</td><td>Completeness % for images, specs, metafields, tags</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Shopify Product Audit</td><td>Missing images, descriptions, metafields</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Product Scheduler Integration</td><td>Catalog orchestration job monitoring (port 9600)</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>AI Analysis Pipeline</td><td>Gemini image analysis completion tracking</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Import Rate Monitoring</td><td>Vendor import rates with anomaly detection</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>AI Technical Architect</td><td>Claude-powered technical strategy insights</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>API Integration Monitor</td><td>Shopify, Gemini, vendor scraper health</td><td><span class="skill-status">Active</span></td></tr>
<tr><td>Vendor Data Completeness</td><td>Per-vendor data quality breakdowns</td><td><span class="skill-status">Active</span></td></tr>
</tbody>
</table>
</details>
</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('CTO', 'technical architect')}
</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: 'CTO 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('@cto', 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 technology context
let techContext = '';
try {
const [vendorStats, dataQuality] = await Promise.all([
getVendorStats(),
getDataQuality()
]);
const totalProducts = vendorStats.reduce((s, v) => s + v.total, 0);
const totalImported = vendorStats.reduce((s, v) => s + v.imported, 0);
const totalImages = vendorStats.reduce((s, v) => s + v.withImages, 0);
const importRate = totalProducts > 0 ? Math.round((totalImported / totalProducts) * 100) : 0;
const zeroImports = vendorStats.filter(v => v.total > 0 && v.imported === 0);
const lowQuality = dataQuality.filter(dq => dq.total > 0 && dq.missingImages > dq.total * 0.5);
techContext = `\n\nLIVE TECHNOLOGY DATA:
- Total catalog: ${totalProducts} products across ${vendorStats.length} vendors
- Imported to Shopify: ${totalImported} (${importRate}%)
- With images: ${totalImages} (${totalProducts > 0 ? Math.round((totalImages / totalProducts) * 100) : 0}%)
- Vendors with zero imports: ${zeroImports.map(v => v.vendor).join(', ') || 'None'}
- Vendors with >50% missing images: ${lowQuality.map(v => v.vendor).join(', ') || 'None'}
- Vendor breakdown: ${vendorStats.map(v => `${v.vendor}(${v.imported}/${v.total})`).join(', ')}
- Data quality issues: ${dataQuality.filter(dq => dq.missingAiColors > dq.total * 0.5).map(dq => `${dq.vendor}: ${dq.missingAiColors} missing AI colors`).join('; ') || 'None critical'}`;
} catch { /* context fetch failed */ }
const systemPrompt = `You are the CTO AI assistant for Designer Wallcoverings. You specialize in TECHNOLOGY, data pipelines, and vendor integrations.
Your expertise includes:
- Product catalog data architecture and quality
- Vendor integration pipelines (scraping, parsing, importing)
- Shopify API and product data management
- PostgreSQL database optimization and queries
- AI/ML image analysis pipeline (Gemini 3.0)
- Data quality scoring and remediation
- ETL pipeline design and monitoring
- SKU registry and duplicate prevention systems
You have access to live vendor catalog statistics and data quality metrics.
${techContext}
Provide strategic technology insights. When data quality is poor, suggest specific SQL queries or pipeline fixes.
Prioritize catalog completeness and data integrity.
Reference specific vendor tables when discussing data issues.`;
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('\u{1F527} CTO Technology 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 CTO 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);
});