← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-executive-cfo/cfo-dashboard-agent.ts
740 lines
import cookieParser from 'cookie-parser';
/**
* DW-Agents: CFO Executive Dashboard
*
* Financial oversight and analysis for Designer Wallcoverings:
* - Live revenue tracking (today, week, month) from Shopify
* - Order analytics with financial/fulfillment status
* - Vendor catalog investment breakdown from PostgreSQL
* - Revenue trend comparison (today vs yesterday)
* - Action items auto-detected from financial data
* - AI-powered financial advisor chat
* - Inter-agent messaging system
*
* Port: 7121
* PM2: dw-executive-cfo
*/
import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import { requireGlobalAuth } from '../shared-global-auth';
import session from 'express-session';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import fetch from 'node-fetch';
import {
pool, queryDB, shopifyGraphQL, getShopifyOrders, getShopifyProductCount,
getPM2Status, getVendorStats, EXECUTIVE_AGENTS, SERVER_IP,
sendMessage, createMessageStore, createActionItemStore,
execDashboardHead, execNavBar, execChatPanel,
formatUptime, formatCurrency, formatNumber, timeAgo,
autoRefreshScript, notifySlack, getShopifyCustomerCount
} from '../shared-executive-core';
import { chatMiddleware } from '../shared-chat-integration';
// Fix __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load environment variables
dotenv.config({ path: path.join(__dirname, '..', '.env') });
const app = express();
const PORT = 7121;
// Stores
const messageStore = createMessageStore();
const actionStore = createActionItemStore();
// Anthropic AI
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || '',
});
// Middleware
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Chat widget injection
app.use(chatMiddleware({
agentId: 'agent-executive-cfo',
agentName: 'CFO 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-cfo-dashboard-2025-finance',
resave: false,
saveUninitialized: true,
rolling: true,
cookie: { secure: false, httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 }
}));
// ─── Sub-agents the CFO has direct access to ────────────────────────────
const CFO_SUB_AGENTS = [
{ name: 'Accounting', emoji: '📒', port: 9882, desc: 'Revenue tracking & reconciliation' },
{ name: 'Purchasing', emoji: '🛒', port: 9880, desc: 'Vendor orders & cost management' },
{ name: 'Shopify Store', emoji: '🛍️', port: 7238, desc: 'E-commerce operations & sales' },
{ name: 'Digital Samples', emoji: '🎨', port: 9879, desc: 'Sample fulfillment & tracking' }
];
// ─── Login / Logout ─────────────────────────────────────────────────────
app.get('/login', requireGlobalAuth, (req, res) => { res.redirect('/'); });
app.post('/login', requireGlobalAuth, (req, res) => { res.redirect('/'); });
app.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/login'));
});
// Helper: wrap async call with timeout and fallback
function withTimeout<T>(promise: Promise<T>, fallback: T, ms = 15000): Promise<T> {
return Promise.race([
promise,
new Promise<T>((resolve) => setTimeout(() => resolve(fallback), ms))
]);
}
// ─── Main Dashboard ─────────────────────────────────────────────────────
app.get('/', requireGlobalAuth, async (req: Request, res: Response) => {
try {
// Fetch all financial data in parallel with 15s timeout per call
const defaultOrders = { orders: 0, revenue: 0, avgValue: 0 };
const [todayData, weekData, monthData, yesterdayData, vendorStats, customerCount, recentOrdersData] = await Promise.all([
withTimeout(getShopifyOrders('today'), defaultOrders),
withTimeout(getShopifyOrders('week'), defaultOrders),
withTimeout(getShopifyOrders('month'), defaultOrders),
withTimeout(getYesterdayOrders(), defaultOrders),
withTimeout(getVendorStats(), []),
withTimeout(getShopifyCustomerCount(), 0),
withTimeout(getRecentOrders(), [])
]);
// Revenue trend calculation
const todayRev = todayData.revenue;
const yesterdayRev = yesterdayData.revenue;
const weekAvg = weekData.orders > 0 ? weekData.revenue / 7 : 0;
const revChange = yesterdayRev > 0 ? ((todayRev - yesterdayRev) / yesterdayRev * 100) : 0;
const revTrendDir = revChange >= 0 ? 'up' : 'down';
const revTrendClass = revChange >= 0 ? 'green' : 'red';
const revTrendArrow = revChange >= 0 ? '▲' : '▼';
// Auto-detect action items
actionStore.getAll().length === 0 && detectActionItems(todayRev, weekAvg, vendorStats);
// Build order rows HTML
const orderRows = (recentOrdersData || []).map((o: any) => {
const fin = o.financialStatus || 'UNKNOWN';
const ful = o.fulfillmentStatus || 'UNFULFILLED';
const finClass = fin === 'PAID' ? 'green' : fin === 'PENDING' ? 'amber' : 'red';
const fulClass = ful === 'FULFILLED' ? 'green' : ful === 'UNFULFILLED' ? 'amber' : 'blue';
const created = new Date(o.createdAt);
const dateStr = created.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
const timeStr = created.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
return `<tr>
<td style="font-weight:600;color:#3b82f6">${o.name}</td>
<td>${dateStr} ${timeStr}</td>
<td style="font-weight:600">${formatCurrency(parseFloat(o.amount || '0'))}</td>
<td><span class="badge ${finClass}">${fin}</span></td>
<td><span class="badge ${fulClass}">${ful}</span></td>
</tr>`;
}).join('');
// Vendor investment rows
const vendorRows = vendorStats.map(v => {
const pct = v.total > 0 ? Math.round(v.imported / v.total * 100) : 0;
const pctClass = pct >= 50 ? 'green' : pct >= 10 ? 'amber' : 'red';
return `<tr>
<td style="font-weight:600">${v.vendor}</td>
<td>${formatNumber(v.total)}</td>
<td>${formatNumber(v.imported)}</td>
<td><span class="badge ${pctClass}">${pct}%</span></td>
</tr>`;
}).join('');
// Sub-agent cards
const subAgentCards = CFO_SUB_AGENTS.map(a =>
`<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.desc}</div>
</a>`
).join('');
// Messages HTML
const msgs = messageStore.getAll();
const msgsHtml = msgs.length > 0
? `<ul class="msg-list">${msgs.map(m => `<li 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>
</li>`).join('')}</ul>`
: '<p style="color:#64748b;font-size:13px;padding:16px;">No messages yet.</p>';
// Action items HTML
const actions = actionStore.getUnresolved();
const actionsHtml = actions.length > 0
? actions.map(a => `<div class="action-item ${a.severity}">
<div class="title">${a.severity === 'critical' ? '!!' : a.severity === 'warning' ? '!' : 'i'} ${a.title}</div>
<div class="desc">${a.description}</div>
<div class="meta">${a.source} - ${timeAgo(a.timestamp)}</div>
</div>`).join('')
: '<p style="color:#64748b;font-size:13px;">No action items. All financials within expected ranges.</p>';
// Assemble full HTML
const html = `${execDashboardHead('CFO Dashboard', '@cfo')}
${execNavBar('CFO Dashboard', '💰', '@cfo', messageStore.unreadCount())}
<div class="main">
<div class="content">
<!-- KPI Cards -->
<div class="kpi-grid">
<div class="kpi-card green">
<div class="label">Revenue Today</div>
<div class="value">${formatCurrency(todayData.revenue)}</div>
<div class="sub">${todayData.orders} order${todayData.orders !== 1 ? 's' : ''} today</div>
</div>
<div class="kpi-card blue">
<div class="label">Revenue This Week</div>
<div class="value">${formatCurrency(weekData.revenue)}</div>
<div class="sub">${weekData.orders} orders / 7 days</div>
</div>
<div class="kpi-card">
<div class="label">Revenue This Month</div>
<div class="value">${formatCurrency(monthData.revenue)}</div>
<div class="sub">${monthData.orders} orders MTD</div>
</div>
<div class="kpi-card ${todayData.orders > 0 ? 'green' : 'amber'}">
<div class="label">Orders Today</div>
<div class="value">${formatNumber(todayData.orders)}</div>
<div class="sub">${todayData.orders > 0 ? 'Active selling day' : 'No orders yet'}</div>
</div>
<div class="kpi-card blue">
<div class="label">Avg Order Value</div>
<div class="value">${formatCurrency(monthData.avgValue)}</div>
<div class="sub">Month average</div>
</div>
<div class="kpi-card">
<div class="label">Total Customers</div>
<div class="value">${formatNumber(customerCount)}</div>
<div class="sub">Shopify customer base</div>
</div>
</div>
<!-- Revenue Trend -->
<div class="section">
<h2>📈 Revenue Trend</h2>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div style="background:#0f172a;border:1px solid #334155;border-radius:10px;padding:20px;">
<div style="color:#94a3b8;font-size:12px;text-transform:uppercase;margin-bottom:8px;">Yesterday</div>
<div style="font-size:24px;font-weight:700;color:#f8fafc;">${formatCurrency(yesterdayRev)}</div>
<div style="color:#64748b;font-size:12px;">${yesterdayData.orders} orders</div>
</div>
<div style="background:#0f172a;border:1px solid #334155;border-radius:10px;padding:20px;">
<div style="color:#94a3b8;font-size:12px;text-transform:uppercase;margin-bottom:8px;">Today</div>
<div style="font-size:24px;font-weight:700;color:#f8fafc;">${formatCurrency(todayRev)}</div>
<div style="color:#64748b;font-size:12px;">${todayData.orders} orders</div>
</div>
<div style="background:#0f172a;border:1px solid ${revTrendClass === 'green' ? '#166534' : '#7f1d1d'};border-radius:10px;padding:20px;">
<div style="color:#94a3b8;font-size:12px;text-transform:uppercase;margin-bottom:8px;">Day-over-Day</div>
<div style="font-size:24px;font-weight:700;color:${revTrendClass === 'green' ? '#22c55e' : '#ef4444'};">${revTrendArrow} ${Math.abs(revChange).toFixed(1)}%</div>
<div style="color:#64748b;font-size:12px;">vs yesterday</div>
</div>
</div>
</div>
<!-- Order Analytics -->
<div class="section">
<h2>📋 Order Analytics (Recent 20)</h2>
<div style="overflow-x:auto;">
<table class="agent-table">
<thead>
<tr>
<th>Order #</th>
<th>Date</th>
<th>Amount</th>
<th>Financial</th>
<th>Fulfillment</th>
</tr>
</thead>
<tbody>
${orderRows || '<tr><td colspan="5" style="text-align:center;color:#64748b;">No recent orders found</td></tr>'}
</tbody>
</table>
</div>
</div>
<!-- Vendor Catalog Investment -->
<div class="section">
<h2>🏭 Vendor Catalog Investment</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>% Imported</th>
</tr>
</thead>
<tbody>
${vendorRows || '<tr><td colspan="4" style="text-align:center;color:#64748b;">No vendor data available</td></tr>'}
</tbody>
</table>
</div>
</div>
<!-- Sub-Agents -->
<div class="section">
<h2>💼 Financial Sub-Agents</h2>
<div class="exec-grid">
${subAgentCards}
</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;">Revenue Dashboard</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Daily/weekly/monthly Shopify revenue 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;">Order Analytics</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Order count, avg value, fulfillment rates</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;">Vendor Cost Analysis</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Products per vendor, import cost 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;">Sample Order Revenue</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">DIG-series sample order financial 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;">Financial Anomaly Detection</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Revenue drop/spike auto-detection (>10%)</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;">Accounting Integration</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Subordinate accounting agent data aggregation</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;">Purchasing Oversight</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Vendor spend and purchase order review</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 Financial Analyst</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Claude-powered financial insights and forecasting</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;">Inter-Agent Financial Reports</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">@mention revenue alerts to CEO</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;">Profit Margin Analysis</td>
<td style="padding:8px 12px;font-size:12px;color:#94a3b8;">Cost vs revenue per vendor 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>
</tbody>
</table>
</details>
</div>
<style>details[open] > summary span:first-child { transform: rotate(90deg); } details summary::-webkit-details-marker { display: none; }</style>
<!-- Messages -->
<div class="section" id="messages-section">
<h2>📨 Messages ${messageStore.unreadCount() > 0 ? `<span class="msg-badge">${messageStore.unreadCount()}</span>` : ''}</h2>
<div style="margin-bottom:12px;">
<button onclick="document.getElementById('sendModal').style.display='block'" style="background:#3b82f6;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;">Send Message</button>
</div>
${msgsHtml}
</div>
<!-- Action Items -->
<div class="section">
<h2>⚡ Action Items</h2>
${actionsHtml}
</div>
</div>
<!-- Chat Panel -->
${execChatPanel('CFO', 'Financial Advisor')}
</div>
<!-- Send Message Modal -->
<div id="sendModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:9999;display:none;">
<div style="background:#1e293b;border:1px solid #334155;border-radius:12px;max-width:500px;margin:10% auto;padding:24px;">
<h3 style="color:#f8fafc;margin-bottom:16px;">Send Message</h3>
<div style="margin-bottom:12px;">
<label style="color:#94a3b8;font-size:12px;display:block;margin-bottom:4px;">To</label>
<select id="msgTo" style="width:100%;padding:8px;background:#0f172a;border:1px solid #475569;border-radius:6px;color:#f8fafc;font-size:13px;">
${Object.entries(EXECUTIVE_AGENTS).filter(([h]) => h !== '@cfo').map(([h, a]) => `<option value="${h}">${a.emoji} ${a.name}</option>`).join('')}
<option value="@all">All Executives</option>
</select>
</div>
<div style="margin-bottom:12px;">
<label style="color:#94a3b8;font-size:12px;display:block;margin-bottom:4px;">Subject</label>
<input id="msgSubject" style="width:100%;padding:8px;background:#0f172a;border:1px solid #475569;border-radius:6px;color:#f8fafc;font-size:13px;" placeholder="Message subject">
</div>
<div style="margin-bottom:12px;">
<label style="color:#94a3b8;font-size:12px;display:block;margin-bottom:4px;">Body</label>
<textarea id="msgBody" rows="4" style="width:100%;padding:8px;background:#0f172a;border:1px solid #475569;border-radius:6px;color:#f8fafc;font-size:13px;resize:vertical;" placeholder="Message body"></textarea>
</div>
<div style="margin-bottom:12px;">
<label style="color:#94a3b8;font-size:12px;display:block;margin-bottom:4px;">Priority</label>
<select id="msgPriority" style="width:100%;padding:8px;background:#0f172a;border:1px solid #475569;border-radius:6px;color:#f8fafc;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>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button onclick="document.getElementById('sendModal').style.display='none'" style="background:#334155;color:#94a3b8;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:13px;">Cancel</button>
<button onclick="sendMsg()" style="background:#3b82f6;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;">Send</button>
</div>
</div>
</div>
<style>
.badge { font-size:11px; padding:2px 8px; border-radius:4px; font-weight:600; text-transform:uppercase; }
.badge.green { background:#166534; color:#4ade80; }
.badge.amber { background:#78350f; color:#fcd34d; }
.badge.red { background:#7f1d1d; color:#fca5a5; }
.badge.blue { background:#1e3a5f; color:#93c5fd; }
</style>
<script>
async function sendMsg() {
const to = document.getElementById('msgTo').value;
const subject = document.getElementById('msgSubject').value;
const body = document.getElementById('msgBody').value;
const priority = document.getElementById('msgPriority').value;
if (!subject || !body) { alert('Subject and body required'); return; }
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) {
alert('Message sent');
document.getElementById('sendModal').style.display = 'none';
document.getElementById('msgSubject').value = '';
document.getElementById('msgBody').value = '';
} else {
alert('Failed: ' + (data.error || 'Unknown error'));
}
} catch(e) { alert('Error: ' + e.message); }
}
</script>
${autoRefreshScript(30000)}
</body>
</html>`;
res.send(html);
} catch (err: any) {
console.error('[CFO Dashboard] Render error:', err.message);
res.status(500).send(`<h1>CFO Dashboard Error</h1><pre>${err.message}</pre>`);
}
});
// ─── API: Financial Metrics ─────────────────────────────────────────────
app.get('/api/metrics', requireGlobalAuth, async (req: Request, res: Response) => {
try {
const defaultOrders = { orders: 0, revenue: 0, avgValue: 0 };
const [todayData, weekData, monthData, vendorStats, customerCount] = await Promise.all([
withTimeout(getShopifyOrders('today'), defaultOrders),
withTimeout(getShopifyOrders('week'), defaultOrders),
withTimeout(getShopifyOrders('month'), defaultOrders),
withTimeout(getVendorStats(), []),
withTimeout(getShopifyCustomerCount(), 0)
]);
res.json({
status: 'online',
agent: 'CFO Dashboard',
port: PORT,
uptime: process.uptime(),
lastActivity: new Date().toISOString(),
financial: {
revenueToday: todayData.revenue,
revenueWeek: weekData.revenue,
revenueMonth: monthData.revenue,
ordersToday: todayData.orders,
ordersWeek: weekData.orders,
ordersMonth: monthData.orders,
avgOrderValueMonth: monthData.avgValue,
totalCustomers: customerCount
},
vendors: vendorStats.map(v => ({
name: v.vendor,
totalProducts: v.total,
importedToShopify: v.imported,
percentImported: v.total > 0 ? Math.round(v.imported / v.total * 100) : 0
})),
messages: {
total: messageStore.count(),
unread: messageStore.unreadCount()
},
actionItems: actionStore.count()
});
} catch (err: any) {
console.error('[CFO Metrics Error]', err.message);
res.json({
status: 'online',
agent: 'CFO Dashboard',
port: PORT,
uptime: process.uptime(),
error: err.message
});
}
});
// ─── API: Messages ──────────────────────────────────────────────────────
app.get('/api/messages', requireGlobalAuth, (req: Request, res: Response) => {
res.json({
messages: messageStore.getAll(),
unread: messageStore.unreadCount(),
total: messageStore.count()
});
});
// ─── API: Receive Message ───────────────────────────────────────────────
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(`[CFO] Message received from ${msg.from}: ${msg.subject}`);
res.json({ success: true });
} else {
res.status(400).json({ success: false, error: 'Invalid message format' });
}
} catch (err: any) {
res.status(500).json({ success: false, error: err.message });
}
});
// ─── API: Send Message ──────────────────────────────────────────────────
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({ success: false, error: 'Missing required fields: to, subject, body' });
}
const sent = await sendMessage('@cfo', to, subject, body, priority || 'normal');
res.json({ success: sent, message: sent ? 'Message sent' : 'Failed to deliver' });
} catch (err: any) {
res.status(500).json({ success: false, error: err.message });
}
});
// ─── API: Chat (Claude Financial 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' });
}
// Initialize session chat history
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
req.session.chatHistory.push({ role: 'user', content: message });
// Gather live financial context if query relates to finances
let financialContext = '';
const lowerMsg = message.toLowerCase();
if (lowerMsg.match(/revenue|sales|order|finance|money|cost|margin|profit|budget|expense|vendor|product|customer|shopify/)) {
try {
const defOrd = { orders: 0, revenue: 0, avgValue: 0 };
const [today, week, month, vendors] = await Promise.all([
withTimeout(getShopifyOrders('today'), defOrd, 10000),
withTimeout(getShopifyOrders('week'), defOrd, 10000),
withTimeout(getShopifyOrders('month'), defOrd, 10000),
withTimeout(getVendorStats(), [], 10000)
]);
financialContext = `\n\nLIVE FINANCIAL DATA (real-time from Shopify + PostgreSQL):
- Revenue Today: ${formatCurrency(today.revenue)} (${today.orders} orders)
- Revenue This Week: ${formatCurrency(week.revenue)} (${week.orders} orders)
- Revenue This Month: ${formatCurrency(month.revenue)} (${month.orders} orders)
- Avg Order Value (month): ${formatCurrency(month.avgValue)}
- Vendor Catalog Summary: ${vendors.map(v => `${v.vendor}: ${v.total} products, ${v.imported} imported`).join('; ')}`;
} catch { /* continue without context */ }
}
const systemPrompt = `You are the CFO's financial AI advisor for Designer Wallcoverings, a luxury wallcovering company.
Your role is to analyze revenue trends, order patterns, and financial metrics. The company sells luxury wallcoverings through Shopify with 145K+ products across multiple premium vendors (Arte, Thibaut, Schumacher, Koroseal, and more).
You have direct access to live financial data from Shopify and PostgreSQL databases.
Focus on:
- Revenue analysis and forecasting
- Order pattern insights
- Vendor catalog ROI (products imported vs total catalog)
- Cost optimization recommendations
- Cash flow considerations
- Financial risk identification
- Margin analysis
Be concise, data-driven, and actionable. Present numbers clearly. Flag any concerns proactively.${financialContext}`;
const chatMessages = req.session.chatHistory.map(m => ({
role: m.role as 'user' | 'assistant',
content: m.content
}));
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 2048,
system: systemPrompt,
messages: chatMessages
});
const reply = response.content[0].type === 'text'
? response.content[0].text
: 'I encountered an error processing your request.';
req.session.chatHistory.push({ role: 'assistant', content: reply });
// Keep history manageable
if (req.session.chatHistory.length > 20) {
req.session.chatHistory = req.session.chatHistory.slice(-20);
}
res.json({ response: reply });
} catch (err: any) {
console.error('[CFO Chat Error]', err.message);
res.status(500).json({
error: 'Chat processing failed',
response: 'I apologize, but I encountered an error. Please try again.'
});
}
});
// ─── Helper: Get yesterday's orders ─────────────────────────────────────
async function getYesterdayOrders(): Promise<{ orders: number; revenue: number; avgValue: number }> {
const now = new Date();
const yesterdayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const yesterdayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const data = await shopifyGraphQL(`{
orders(first: 250, query: "created_at:>='${yesterdayStart.toISOString()}' AND created_at:<'${yesterdayEnd.toISOString()}'") {
edges { node { id totalPriceSet { shopMoney { amount } } createdAt } }
}
}`);
const edges = data?.data?.orders?.edges || [];
const revenue = edges.reduce((s: number, e: any) => s + parseFloat(e.node.totalPriceSet?.shopMoney?.amount || '0'), 0);
return {
orders: edges.length,
revenue: Math.round(revenue * 100) / 100,
avgValue: edges.length ? Math.round(revenue / edges.length * 100) / 100 : 0
};
}
// ─── Helper: Get recent orders for table ────────────────────────────────
async function getRecentOrders(): Promise<any[]> {
const data = await shopifyGraphQL(`{
orders(first: 20, reverse: true) {
edges {
node {
name
createdAt
totalPriceSet { shopMoney { amount } }
displayFinancialStatus
displayFulfillmentStatus
}
}
}
}`);
return (data?.data?.orders?.edges || []).map((e: any) => ({
name: e.node.name,
createdAt: e.node.createdAt,
amount: e.node.totalPriceSet?.shopMoney?.amount || '0',
financialStatus: e.node.displayFinancialStatus || 'UNKNOWN',
fulfillmentStatus: e.node.displayFulfillmentStatus || 'UNFULFILLED'
}));
}
// ─── Helper: Auto-detect action items ───────────────────────────────────
function detectActionItems(todayRev: number, weekAvg: number, vendorStats: Array<{ vendor: string; total: number; imported: number; withImages: number }>) {
// Revenue below average check
if (weekAvg > 0 && todayRev < weekAvg * 0.5) {
actionStore.add(
'Revenue Monitor',
'warning',
'Revenue Below Average',
`Today's revenue (${formatCurrency(todayRev)}) is below 50% of the weekly average (${formatCurrency(weekAvg)}). Consider promotional actions or investigate drop in traffic.`
);
}
// Low vendor import rate check
for (const v of vendorStats) {
if (v.total > 50) {
const pct = v.total > 0 ? (v.imported / v.total * 100) : 0;
if (pct < 10) {
actionStore.add(
'Vendor Investment',
'warning',
`Low Vendor Import Rate: ${v.vendor}`,
`${v.vendor} has only ${pct.toFixed(1)}% of products imported to Shopify (${v.imported}/${v.total}). Potential untapped revenue opportunity.`
);
}
}
}
}
// ─── Start Server ───────────────────────────────────────────────────────
app.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log('💰 CFO Executive Dashboard');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`🌍 External: http://${SERVER_IP}:${PORT}`);
console.log(`🏠 Local: http://localhost:${PORT}`);
console.log('');
console.log('✅ CFO Dashboard ready - All financial data is LIVE from Shopify + PostgreSQL');
});