← back to Designer Wallcoverings
DW-Agents/dw-agents/master-hub.ts
2913 lines
/**
* DW-Agents Master Hub
*
* Central dashboard for all Designer Wallcoverings AI agents
* Port: 9712
*
* Integrated Agents:
* 1. Legal Team (Port 9878) - Settlement Compliance
* 2. Digital Samples Agent (Port 9879) - DIG-series order processing
* 3. Purchasing Agent (Port 9880) - Inventory & vendor management
* 4. Marketing Agent (Port 9881) - Content creation & campaigns
* 5. Restart Analyzer (Port 7241) - System health & restart pattern analysis
*/
import express from 'express';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import Anthropic from '@anthropic-ai/sdk';
import fetch from 'node-fetch';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
const execAsync = promisify(exec);
import { chatMiddleware } from './shared-chat-integration.js';
import { getUniversalHeader, getThemeStyles } from './shared-ui-components.js';
const app = express();
const PORT = parseInt(process.env.PORT || '9893', 10);
// Health check cache to reduce PM2 command overhead
let healthCache: {data: any, timestamp: number} | null = null;
const HEALTH_CACHE_TTL = 5000; // 5 seconds
// Global Authentication - MUST come before any other middleware
app.use(cookieParser());
// Simple response compression for JSON to improve performance
app.use((req, res, next) => {
const originalJson = res.json.bind(res);
res.json = function(data: any) {
if (req.headers['accept-encoding']?.includes('gzip')) {
res.set('Content-Encoding', 'gzip');
}
return originalJson(data);
};
next();
});
// Add inter-agent chat widget (after auth)
app.use(chatMiddleware({
agentId: 'DW-Agents',
agentName: 'Dw Agents',
port: 9893,
category: 'agent'
}));
// Global Authentication
// Agent registry - ALPHABETICALLY SORTED
const AGENTS = [
{
id: 'control-panel',
name: 'Agent Control Panel',
port: 7200,
url: 'http://45.61.58.125:7200',
status: 'online',
description: 'Advanced agent control with restart, monitoring & configuration management',
icon: '🎛️',
color: '#6366f1'
},
{
id: 'ceo-dashboard',
name: 'CEO Dashboard',
port: 7121,
url: 'http://45.61.58.125:7121',
status: 'online',
description: 'Executive overview with real-time KPIs, metrics & business intelligence',
icon: '👔',
color: '#1e3a8a'
},
{
id: 'completed-tasks',
name: 'Completed Tasks',
port: 9889,
url: 'http://45.61.58.125:9889',
status: 'online',
description: 'All completed tasks from all agents in chronological order',
icon: '✅',
color: '#28a745'
},
{
id: 'digital-samples',
name: 'Digital Samples',
port: 9879,
url: 'http://45.61.58.125:9879',
status: 'online',
description: 'DIG-series order processing & file delivery',
icon: '📦',
color: '#4facfe'
},
{
id: 'import-sku',
name: 'Import New SKU',
port: 3002,
url: 'http://45.61.58.125:3002',
status: 'online',
description: 'Import new products from vendor URLs with automated data extraction',
icon: '📥',
color: '#10b981'
},
{
id: 'in-parallel',
name: 'In Parallel',
port: 9891,
url: 'http://45.61.58.125:9891',
status: 'online',
description: 'Execute multiple tasks concurrently with real-time progress tracking',
icon: '⚡',
color: '#FF6B35'
},
{
id: 'legal',
name: 'Legal Team',
port: 9878,
url: 'http://45.61.58.125:9878',
status: 'online',
description: 'Settlement compliance monitoring',
icon: '⚖️',
color: '#dc143c'
},
{
id: 'log-monitor',
name: 'Log Monitor',
port: 7239,
url: 'http://45.61.58.125:7239',
status: 'online',
description: 'Automated log analysis, error detection & task creation - monitors all agents + Claude Code',
icon: '📋',
color: '#f5576c'
},
{
id: 'marketing',
name: 'Marketing',
port: 9881,
url: 'http://45.61.58.125:9881',
status: 'online',
description: 'Blog posts from SKUs, social media & email campaigns with chat',
icon: '📢',
color: '#f093fb'
},
{
id: 'master-hub-main',
name: 'Master Hub',
port: 9893,
url: 'http://45.61.58.125:9893',
status: 'online',
description: 'Central control dashboard for all agents',
icon: '🤖',
color: '#764ba2'
},
{
id: 'needs-attention',
name: 'Needs Attention',
port: 9886,
url: 'http://45.61.58.125:9886',
status: 'online',
description: 'Urgent matters requiring immediate action across all agents',
icon: '🚨',
color: '#DC143C'
},
{
id: 'new-client-signup',
name: 'New Client Signups',
port: 9890,
url: 'http://45.61.58.125:9890',
status: 'online',
description: 'Live monitoring of #new-client-signup Slack channel (refreshes every 5 seconds)',
icon: '🎯',
color: '#7C3AED'
},
{
id: 'parallel-processes',
name: 'Parallel Processes',
port: 9887,
url: 'http://45.61.58.125:9887',
status: 'online',
description: 'Real-time monitor of all running processes, background tasks, and parallel operations',
icon: '⚡',
color: '#667eea'
},
{
id: 'purchasing',
name: 'Purchasing (Office)',
port: 9880,
url: 'http://45.61.58.125:9880',
status: 'online',
description: 'Office supplies with Amazon price comparison & chat',
icon: '🛒',
color: '#667eea'
},
{
id: 'restart-analyzer',
name: 'Restart Analyzer',
port: 7241,
url: 'http://45.61.58.125:7241',
status: 'online',
description: 'AI-powered analysis of PM2 restart patterns with root cause identification',
icon: '🔄',
color: '#667eea'
},
{
id: 'security',
name: 'Security',
port: 9892,
url: 'http://45.61.58.125:9892',
status: 'online',
description: 'System security monitoring, updates tracking & auto-reporting to Urgent agent',
icon: '🔒',
color: '#DC143C'
},
{
id: 'server-uptime',
name: 'Server Uptime',
port: 9882,
url: 'http://45.61.58.125:9882',
status: 'online',
description: 'Auto-restart crashed processes every 15 seconds - ensures 100% uptime',
icon: '🟢',
color: '#00C851'
},
{
id: 'shopify-store',
name: 'Shopify Store',
port: 7238,
url: 'http://45.61.58.125:7238',
status: 'online',
description: 'Full store management with transactions, products, orders, customers & AI chat',
icon: '🛍️',
color: '#96bf48'
},
{
id: 'skills-manager',
name: 'Skills Manager',
port: 9894,
url: 'http://45.61.58.125:9894',
status: 'online',
description: 'Central dashboard for managing and monitoring Claude Code skills',
icon: '🎯',
color: '#764ba2'
},
{
id: 'task-orchestrator',
name: 'Task Orchestrator',
port: 9900,
url: 'http://45.61.58.125:9900',
status: 'online',
description: 'Master task management & auto-routing system - tracks all Claude todos and agent activities',
icon: '🎯',
color: '#667eea'
},
{
id: 'todays-highlights',
name: "Today's Highlights",
port: 9885,
url: 'http://45.61.58.125:9885',
status: 'online',
description: 'Notable activities and achievements across all agents today',
icon: '✨',
color: '#FFD700'
},
{
id: 'trend-research',
name: 'Trend Research',
port: 9883,
url: 'http://45.61.58.125:9883',
status: 'online',
description: 'Market intelligence, trend forecasting & vendor relations with chat',
icon: '📊',
color: '#f5576c'
},
{
id: 'ui-manager',
name: 'UI Manager',
port: 7240,
url: 'http://45.61.58.125:7240',
status: 'online',
description: 'Central UI hub with unified design system, live preview & universal chat assistant',
icon: '🎨',
color: '#764ba2'
},
{
id: 'vp-ops-dashboard',
name: 'VP of Operations Dashboard',
port: 7123,
url: 'http://45.61.58.125:7123',
status: 'online',
description: 'Day-to-day operations management, team coordination & efficiency metrics',
icon: '📊',
color: '#4c1d95'
}
];
// Get running services
async function getRunningServices() {
try {
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep LISTEN | awk '{print $4}' | sed 's/.*://'`);
return new Set(stdout.trim().split('\n').filter(p => p));
} catch (error) {
return new Set();
}
}
// Get firewall status
async function getFirewallStatus() {
try {
const { stdout } = await execAsync(`sudo ufw status | grep -E "^[0-9]+" | awk '{print $1}' | cut -d'/' -f1`);
return new Set(stdout.trim().split('\n').filter(p => p));
} catch (error) {
return new Set();
}
}
// Get enriched agent data with real-time status
async function getEnrichedAgents() {
const runningPorts = await getRunningServices();
const firewallPorts = await getFirewallStatus();
return AGENTS.map(agent => ({
...agent,
isRunning: runningPorts.has(agent.port.toString()),
firewallOpen: firewallPorts.has(agent.port.toString()),
status: runningPorts.has(agent.port.toString()) ? 'online' : 'offline'
}));
}
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(
session({
secret: 'dw-agents-master-hub-2025',
resave: false,
saveUninitialized: true,
rolling: true,
cookie: {
secure: false,
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
},
})
);
declare module 'express-session' {
interface SessionData {
authenticated: boolean;
chatHistory: Array<{ role: 'user' | 'assistant', content: string | any }>;
}
}
// Old requireAuth and login routes removed - using requireGlobalAuth middleware (line 32)
// Login/logout handled by global auth server at http://45.61.58.125:9999
// Chat API endpoint
app.post('/api/chat', async (req, res) => {
try {
const { message, targetAgent } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
// Special handling for Task Orchestrator - it doesn't have chat, query tasks directly
if (targetAgent === 'task-orchestrator') {
try {
// Forward session cookie for authentication
const sessionId = req.cookies?.dw_session;
const tasksResponse = await fetch('http://localhost:9900/api/tasks', {
headers: {
'Cookie': sessionId ? `dw_session=${sessionId}` : ''
}
});
const tasksData: any = await tasksResponse.json();
const messageLower = message.toLowerCase();
let response = '';
if (messageLower.includes('last task') || messageLower.includes('latest task')) {
const tasks = tasksData.tasks || [];
if (tasks.length === 0) {
response = 'There are no tasks in the system yet.';
} else {
const lastTask = tasks[tasks.length - 1];
response = `**Last Task:**\n\n**Title:** ${lastTask.title}\n**Status:** ${lastTask.status}\n**Priority:** ${lastTask.priority}\n**Description:** ${lastTask.description}\n**Created:** ${new Date(lastTask.createdAt).toLocaleString()}\n\nYou can view all tasks at the Task Orchestrator dashboard: http://45.61.58.125:9900`;
}
} else if (messageLower.includes('how many') || messageLower.includes('count')) {
const tasks = tasksData.tasks || [];
const pending = tasks.filter((t: any) => t.status === 'pending').length;
const completed = tasks.filter((t: any) => t.status === 'completed').length;
const inProgress = tasks.filter((t: any) => t.status === 'in_progress').length;
response = `**Task Statistics:**\n\n- Total Tasks: ${tasks.length}\n- Pending: ${pending}\n- In Progress: ${inProgress}\n- Completed: ${completed}\n\nView dashboard: http://45.61.58.125:9900`;
} else if (messageLower.includes('pending') || messageLower.includes('todo')) {
const tasks = tasksData.tasks || [];
const pending = tasks.filter((t: any) => t.status === 'pending');
if (pending.length === 0) {
response = 'No pending tasks at the moment.';
} else {
response = `**Pending Tasks (${pending.length}):**\n\n${pending.slice(0, 5).map((t: any, i: number) => `${i + 1}. ${t.title} (Priority: ${t.priority})`).join('\n')}${pending.length > 5 ? `\n\n...and ${pending.length - 5} more` : ''}\n\nView all: http://45.61.58.125:9900`;
}
} else {
response = `The Task Orchestrator manages all tasks across the system.\n\n**Current Status:**\n- Total Tasks: ${tasksData.tasks?.length || 0}\n- Endpoint: http://45.61.58.125:9900\n\n**You can ask me about:**\n- Last task\n- Pending tasks\n- Task counts\n- Task statistics\n\nOr visit the dashboard to manage tasks directly.`;
}
return res.json({ response });
} catch (error) {
console.error('Task Orchestrator query error:', error);
return res.json({
response: `I couldn't connect to the Task Orchestrator. The service might be starting up or experiencing issues.\n\nError: ${error instanceof Error ? error.message : String(error)}\n\nYou can try accessing it directly at: http://45.61.58.125:9900`
});
}
}
// Initialize chat history
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
req.session.chatHistory.push({ role: 'user', content: message });
const systemPrompt = `You are the Master DW-Agents orchestrator for Designer Wallcoverings.
You coordinate between multiple specialized agents:
1. Digital Samples Agent (DIG-series order processing)
2. Legal Team Agent (Settlement compliance)
3. Purchasing Agent (Office supplies and Amazon ordering)
4. Marketing Agent (Blog posts, social media, email campaigns)
5. Task Orchestrator (Master task management and routing)
${targetAgent ? `The user is specifically asking about ${targetAgent}.` : 'Determine which agent(s) can best help with this request.'}
Current system status:
- All agents online and operational
- Master Hub at http://45.61.58.125:9893
- Task Orchestrator at http://45.61.58.125:9900
- Digital Samples at http://45.61.58.125:9879
- Legal Team at http://45.61.58.125:9878
- Purchasing at http://45.61.58.125:9880
Your role:
- Answer questions about the system
- Route requests to appropriate agents
- Provide status updates
- Explain agent capabilities
- Help with navigation
Be helpful, concise, and direct users to the right agent dashboard when needed.`;
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey || apiKey === 'your_anthropic_api_key_here') {
// Fallback to simple rule-based responses when API key is not available
console.warn('⚠️ Anthropic API key not configured, using fallback responses');
let fallbackResponse = '';
const messageLower = message.toLowerCase();
if (messageLower.includes('task') || messageLower.includes('orchestrator')) {
fallbackResponse = 'The Task Orchestrator is available at http://45.61.58.125:9900. It manages and routes all tasks across the system.';
} else if (messageLower.includes('agent') || messageLower.includes('help')) {
fallbackResponse = `I can help you navigate the DW-Agents system! Here are the main agents:\n\n- **Task Orchestrator** (Port 9900): Master task management\n- **Digital Samples** (Port 9879): DIG-series orders\n- **Legal Team** (Port 9878): Settlement compliance\n- **Purchasing** (Port 9880): Office supplies\n- **Marketing** (Port 9881): Content & campaigns\n\nClick on any agent card to access their dashboard!`;
} else {
fallbackResponse = 'I\'m here to help! Ask me about specific agents, system status, or what each agent does. You can also click the "Open Dashboard" button on any agent card to access it directly.';
}
return res.json({ response: fallbackResponse });
}
const anthropic = new Anthropic({
apiKey: apiKey,
});
// Define tools for calling other agents
const tools = [
{
name: 'query_digital_samples',
description: 'Query the Digital Samples agent about DIG-series orders, file delivery, and sample processing',
input_schema: {
type: 'object' as const,
properties: {
question: {
type: 'string',
description: 'The question to ask the Digital Samples agent'
}
},
required: ['question']
}
},
{
name: 'query_legal_team',
description: 'Query the Legal Team about settlement compliance, product checking, and legal matters',
input_schema: {
type: 'object' as const,
properties: {
question: {
type: 'string',
description: 'The question to ask the Legal Team'
}
},
required: ['question']
}
},
{
name: 'get_shopify_orders',
description: 'Get recent Shopify orders and order status information',
input_schema: {
type: 'object' as const,
properties: {
limit: {
type: 'number',
description: 'Number of orders to fetch (default 10)'
}
}
}
}
] as const;
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 2048,
system: systemPrompt,
messages: req.session.chatHistory,
tools: tools as any
});
let finalMessage = '';
// Handle tool calls
if (response.stop_reason === 'tool_use') {
const toolUse = response.content.find(block => block.type === 'tool_use');
if (toolUse && toolUse.type === 'tool_use') {
let toolResult = '';
// Type assertion for toolUse.input
const toolInput = toolUse.input as any;
try {
if (toolUse.name === 'get_shopify_orders') {
const ordersResponse = await fetch(
`https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-07/orders.json?limit=${toolInput.limit || 10}&status=any`,
{
headers: {
'X-Shopify-Access-Token': (process.env.SHOPIFY_ADMIN_TOKEN || '')
}
}
);
const ordersData: any = await ordersResponse.json();
toolResult = JSON.stringify(ordersData.orders || [], null, 2);
} else {
// For agent queries, make HTTP request to their chat endpoints
const agentPorts: Record<string, number> = {
'query_digital_samples': 9879,
'query_legal_team': 9878
};
const port = agentPorts[toolUse.name];
if (port) {
const agentResponse = await fetch(`http://localhost:${port}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: toolInput.question })
});
const agentData: any = await agentResponse.json();
toolResult = agentData.response || 'No response from agent';
}
}
} catch (error) {
toolResult = `Error calling tool: ${error}`;
}
// Continue conversation with tool result
req.session.chatHistory.push({
role: 'assistant',
content: JSON.stringify(response.content.map(block => {
if (block.type === 'text') return { type: 'text', text: block.text };
if (block.type === 'tool_use') return { type: 'tool_use', id: block.id, name: block.name, input: block.input };
return block;
}))
});
req.session.chatHistory.push({
role: 'user',
content: JSON.stringify([
{
type: 'tool_result',
tool_use_id: toolUse.id,
content: toolResult
}
])
});
// Get final response
const finalResponse = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 2048,
system: systemPrompt,
messages: req.session.chatHistory
});
finalMessage = finalResponse.content[0].type === 'text'
? finalResponse.content[0].text
: 'I apologize, but I encountered an error.';
req.session.chatHistory.push({ role: 'assistant', content: finalMessage });
}
} else {
finalMessage = response.content[0].type === 'text'
? response.content[0].text
: 'I apologize, but I encountered an error.';
req.session.chatHistory.push({ role: 'assistant', content: finalMessage });
}
if (req.session.chatHistory.length > 20) {
req.session.chatHistory = req.session.chatHistory.slice(-20);
}
res.json({ response: finalMessage });
} catch (error) {
console.error('Chat error:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
const errorDetails = error instanceof Error && 'status' in error ? ` (Status: ${(error as any).status})` : '';
res.status(500).json({
error: 'Failed to process message',
response: `I apologize, but I encountered an error: ${errorMessage}${errorDetails}\n\nThis might be due to API configuration issues. You can still navigate to agent dashboards directly by clicking "Open Dashboard" on any agent card.`
});
}
});
// Health check endpoint for broken services
app.get('/api/health', async (req, res) => {
try {
// Check cache first
const now = Date.now();
if (healthCache && (now - healthCache.timestamp < HEALTH_CACHE_TTL)) {
return res.json({ ...healthCache.data, cached: true });
}
// Cache miss or expired - fetch fresh data
const { stdout } = await execAsync('pm2 jlist');
const processes = JSON.parse(stdout);
const offlineServices: any[] = [];
const highRestartServices: any[] = [];
const memoryIssues: any[] = [];
processes.forEach((proc: any) => {
// Check if process is offline
if (proc.pm2_env.status !== 'online') {
offlineServices.push({
name: proc.name,
status: proc.pm2_env.status,
port: proc.pm2_env.PORT || 'N/A'
});
}
// Check for high restart count (>20) - CRITICAL THRESHOLD with auto-fix
if (proc.pm2_env.restart_time > 20) {
console.log(`🚨 CRITICAL: ${proc.name} has ${proc.pm2_env.restart_time} restarts - Flagged for auto-fix`);
highRestartServices.push({
name: proc.name,
restarts: proc.pm2_env.restart_time,
port: proc.pm2_env.PORT || 'N/A',
level: 'critical'
});
} else if (proc.pm2_env.restart_time > 10) {
// Warning level (10-20 restarts)
highRestartServices.push({
name: proc.name,
restarts: proc.pm2_env.restart_time,
port: proc.pm2_env.PORT || 'N/A',
level: 'warning'
});
}
// Check for memory issues (>500MB)
const memoryMB = proc.monit.memory / 1024 / 1024;
if (memoryMB > 500) {
memoryIssues.push({
name: proc.name,
memory: Math.round(memoryMB),
port: proc.pm2_env.PORT || 'N/A'
});
}
});
const hasIssues = offlineServices.length > 0 || highRestartServices.length > 0 || memoryIssues.length > 0;
const responseData = {
healthy: !hasIssues,
timestamp: new Date().toISOString(),
offline: offlineServices,
highRestarts: highRestartServices,
memoryIssues: memoryIssues,
cached: false
};
// Update cache
healthCache = { data: responseData, timestamp: now };
res.json(responseData);
} catch (error) {
console.error('Health check error:', error);
res.status(500).json({
healthy: false,
error: 'Failed to check PM2 processes',
timestamp: new Date().toISOString(),
cached: false
});
}
});
// Auto-fix endpoint for high-restart services
app.post('/api/auto-fix/:serviceName', async (req, res) => {
const { serviceName } = req.params;
try {
console.log(`🔧 AUTO-FIX: Starting automated fix for ${serviceName}...`);
// Get current process info
const { stdout } = await execAsync('pm2 jlist');
const processes = JSON.parse(stdout);
const proc = processes.find((p: any) => p.name === serviceName);
if (!proc) {
return res.status(404).json({ success: false, error: 'Service not found' });
}
const restartCount = proc.pm2_env.restart_time;
const fixLog: string[] = [];
// Get recent logs to diagnose issue
const { stdout: logs } = await execAsync(`pm2 logs ${serviceName} --lines 30 --nostream`);
fixLog.push(`Analyzed last 30 log lines`);
// Auto-fix logic based on common issues
let fixApplied = false;
// Check for port conflicts
if (logs.includes('EADDRINUSE') || logs.includes('address already in use')) {
const portMatch = logs.match(/port (\d+)/);
if (portMatch) {
const port = portMatch[1];
fixLog.push(`Detected port conflict on ${port}`);
await execAsync(`lsof -ti:${port} | grep -v ${proc.pid} | xargs -r kill -9 || true`);
fixLog.push(`Killed conflicting process on port ${port}`);
fixApplied = true;
}
}
// Check for missing Next.js build
if ((logs.includes('Could not find a production build') ||
(logs.includes('ENOENT') && logs.includes('.next'))) &&
serviceName.includes('bubbe')) {
fixLog.push(`Detected missing Next.js build`);
const projectPath = '/root/Projects/dear-bubbe-nextjs';
try {
fixLog.push(`Running npm run build in ${projectPath}...`);
await execAsync(`cd ${projectPath} && npm run build`, { timeout: 180000 });
fixLog.push(`Build completed successfully`);
fixApplied = true;
} catch (buildError: any) {
fixLog.push(`Build failed: ${buildError.message}`);
}
}
// Always stop, clean, and restart with reset counter
fixLog.push(`Stopping ${serviceName}...`);
await execAsync(`pm2 stop ${serviceName}`);
await new Promise(resolve => setTimeout(resolve, 2000));
fixLog.push(`Starting ${serviceName}...`);
await execAsync(`pm2 start ${serviceName}`);
await new Promise(resolve => setTimeout(resolve, 1000));
fixLog.push(`Resetting restart counter...`);
await execAsync(`pm2 reset ${serviceName}`);
await execAsync(`pm2 save`);
fixLog.push(`✅ Auto-fix completed`);
console.log(`✅ AUTO-FIX: Completed for ${serviceName}`);
res.json({
success: true,
service: serviceName,
previousRestarts: restartCount,
fixApplied: fixApplied,
log: fixLog,
timestamp: new Date().toISOString()
});
} catch (error: any) {
console.error(`❌ AUTO-FIX FAILED for ${serviceName}:`, error);
res.status(500).json({
success: false,
error: error.message,
service: serviceName
});
}
});
// Block all search engine crawlers - robots.txt
app.get('/robots.txt', (req, res) => {
res.type('text/plain');
res.send(`# DW-Agents Internal System - NO CRAWLING ALLOWED
User-agent: *
Disallow: /
# Block specific bots
User-agent: Googlebot
Disallow: /
User-agent: Bingbot
Disallow: /
User-agent: Slurp
Disallow: /
User-agent: DuckDuckBot
Disallow: /
User-agent: Baiduspider
Disallow: /
User-agent: YandexBot
Disallow: /
User-agent: facebookexternalhit
Disallow: /
User-agent: LinkedInBot
Disallow: /
User-agent: Twitterbot
Disallow: /`);
});
// Anti-crawler middleware - add X-Robots-Tag header to ALL responses
app.use((req, res, next) => {
res.setHeader('X-Robots-Tag', 'noindex, nofollow, noarchive, nosnippet, noimageindex');
next();
});
// Main dashboard
app.get('/', async (req, res) => {
const agents = await getEnrichedAgents();
const currentUrl = 'http://45.61.58.125:9893';
const pageTitle = 'AI Agent Monitoring Dashboard - Real-Time Business Automation System | DW-Agents Master Hub';
const metaDescription = 'Real-time monitoring dashboard for 21 autonomous AI agents managing business operations. Features live health tracking, auto-restart capabilities, task orchestration, and comprehensive business automation for Designer Wallcoverings.';
const metaKeywords = 'AI agent monitoring, business automation dashboard, real-time system monitoring, autonomous agents, task orchestration, process automation, AI operations management, business intelligence, system health monitoring, auto-restart services';
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<!-- Primary Meta Tags - SEO Foundation -->
<title>${pageTitle}</title>
<meta name="title" content="${pageTitle}">
<meta name="description" content="${metaDescription}">
<meta name="keywords" content="${metaKeywords}">
<meta name="author" content="Designer Wallcoverings">
<meta name="publisher" content="Designer Wallcoverings">
<!-- BLOCK ALL SEARCH ENGINE CRAWLERS - Internal Agent System Only -->
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
<meta name="googlebot" content="noindex, nofollow">
<meta name="bingbot" content="noindex, nofollow">
<meta name="slurp" content="noindex, nofollow">
<meta name="duckduckbot" content="noindex, nofollow">
<!-- Canonical URL -->
<link rel="canonical" href="${currentUrl}">
<!-- Language & Region -->
<meta name="language" content="English">
<meta name="geo.region" content="US">
<meta name="geo.placename" content="United States">
<!-- Open Graph / Facebook Meta Tags -->
<meta property="og:type" content="website">
<meta property="og:url" content="${currentUrl}">
<meta property="og:site_name" content="DW-Agents Master Hub">
<meta property="og:title" content="${pageTitle}">
<meta property="og:description" content="${metaDescription}">
<meta property="og:image" content="${currentUrl}/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="DW-Agents Master Hub - AI Agent Monitoring Dashboard">
<meta property="og:locale" content="en_US">
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="${currentUrl}">
<meta name="twitter:title" content="${pageTitle}">
<meta name="twitter:description" content="${metaDescription}">
<meta name="twitter:image" content="${currentUrl}/twitter-image.png">
<meta name="twitter:image:alt" content="DW-Agents Master Hub Dashboard">
<meta name="twitter:site" content="@DesignerWallcoverings">
<meta name="twitter:creator" content="@DesignerWallcoverings">
<!-- Mobile & Browser Theme -->
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="DW-Agents Hub">
<meta name="mobile-web-app-capable" content="yes">
<!-- Performance & Resource Hints -->
<link rel="dns-prefetch" href="//45.61.58.125">
<link rel="preconnect" href="http://45.61.58.125">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Favicon & Icons -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<!-- Schema.org Structured Data - Organization -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Designer Wallcoverings",
"url": "http://45.61.58.125:9893",
"logo": "${currentUrl}/logo.png",
"description": "AI-powered business automation with 21 autonomous agents managing operations",
"address": {
"@type": "PostalAddress",
"addressCountry": "US",
"addressRegion": "United States"
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "Technical Support",
"availableLanguage": "English"
}
}
</script>
<!-- Schema.org Structured Data - WebPage -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "${pageTitle}",
"description": "${metaDescription}",
"url": "${currentUrl}",
"inLanguage": "en-US",
"isPartOf": {
"@type": "WebSite",
"name": "DW-Agents Master Hub",
"url": "http://45.61.58.125:9893"
},
"about": {
"@type": "Thing",
"name": "AI Agent Monitoring System",
"description": "Comprehensive real-time monitoring and management system for autonomous AI agents"
},
"primaryImageOfPage": {
"@type": "ImageObject",
"url": "${currentUrl}/dashboard-preview.png",
"width": 1200,
"height": 630
}
}
</script>
<!-- Schema.org Structured Data - SoftwareApplication -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "DW-Agents Master Hub",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web-based",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"description": "${metaDescription}",
"featureList": [
"Real-time agent health monitoring",
"Automated service restart",
"Task orchestration and routing",
"Multi-agent chat system",
"Business process automation",
"Financial reporting and analytics",
"Marketing automation",
"Legal compliance monitoring",
"Digital asset management",
"Customer support integration"
],
"screenshot": "${currentUrl}/screenshot.png",
"url": "${currentUrl}",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5.0",
"ratingCount": "1"
}
}
</script>
<!-- Schema.org Structured Data - BreadcrumbList -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "${currentUrl}"
}, {
"@type": "ListItem",
"position": 2,
"name": "Master Hub Dashboard",
"item": "${currentUrl}"
}]
}
</script>
${getThemeStyles('professional-dark')}
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* Improved focus visibility for accessibility */
*:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 2px;
}
body {
font-family: var(--font-primary);
background: var(--color-background-alt);
min-height: 100vh;
padding: clamp(15px, 3vw, 30px);
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: var(--color-surface);
padding: clamp(20px, 4vw, 40px);
border-radius: 20px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.header-left h1 {
color: var(--color-primary);
font-size: var(--font-size-3xl);
margin-bottom: 10px;
font-weight: var(--font-weight-bold);
}
.header-left .subtitle {
color: var(--color-text-secondary);
font-size: var(--font-size-lg);
}
.logout-btn {
background: linear-gradient(135deg, var(--color-error-light) 0%, var(--color-error) 100%);
color: var(--color-text-inverse);
border: none;
padding: 12px 30px;
border-radius: 10px;
font-size: var(--font-size-base);
cursor: pointer;
font-weight: var(--font-weight-semibold);
text-decoration: none;
display: inline-block;
transition: transform 0.2s;
}
.logout-btn:hover {
transform: translateY(-2px);
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(350px, 100%), 1fr));
gap: clamp(15px, 3vw, 25px);
margin-bottom: 30px;
}
/* Responsive breakpoints */
@media (max-width: 768px) {
.agents-grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 769px) and (max-width: 1200px) {
.agents-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.agent-card {
background: var(--color-surface);
padding: clamp(20px, 3vw, 30px);
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
overflow: hidden;
will-change: transform;
}
.agent-card:hover,
.agent-card:focus-within {
transform: translateY(-5px);
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15);
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
.agent-card {
transition: none;
}
.agent-card:hover {
transform: none;
}
}
.agent-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
background: var(--agent-color);
}
.agent-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.agent-icon {
font-size: 3em;
margin-right: 20px;
}
.agent-info h2 {
color: var(--color-text-primary);
font-size: var(--font-size-xl);
margin-bottom: 5px;
font-weight: var(--font-weight-semibold);
}
.agent-info .port {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-family: var(--font-mono);
}
.agent-description {
color: var(--color-text-secondary);
margin-bottom: 20px;
line-height: 1.6;
}
.status-badges {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.status-badge {
display: inline-block;
padding: 6px 15px;
border-radius: 20px;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-online {
background: #555;
color: white;
}
.status-offline {
background: #999;
color: white;
}
.status-planned {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning);
}
.firewall-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.fw-open {
background: #4a4a4a;
color: white;
}
.fw-closed {
background: #c44;
color: white;
}
.agent-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 10px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
text-align: center;
display: inline-block;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.btn:active::before {
width: 300px;
height: 300px;
}
.btn-primary {
background: var(--agent-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
filter: brightness(1.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-secondary {
background: var(--color-neutral100);
color: var(--color-text-primary);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-neutral200);
transform: translateY(-1px);
}
.btn-chat {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
color: var(--color-text-inverse);
}
.btn-chat:hover:not(:disabled) {
filter: brightness(1.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-restart {
background: #666;
color: white;
}
.btn-restart:hover:not(:disabled) {
background: #555;
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.btn.loading {
pointer-events: none;
opacity: 0.7;
}
.btn.loading::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin-left: -8px;
margin-top: -8px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
.agent-chat-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
align-items: center;
justify-content: center;
}
.agent-chat-modal.active {
display: flex;
}
.agent-chat-container {
background: var(--color-surface);
width: 90%;
max-width: 600px;
max-height: 80vh;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
overflow: hidden;
}
.agent-chat-header {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
color: var(--color-text-inverse);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-chat-header h3 {
margin: 0;
font-size: 1.3em;
}
.agent-chat-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 1.5em;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.agent-chat-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.agent-chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
max-height: 400px;
}
.agent-chat-input-area {
padding: 20px;
border-top: 2px solid #e0e0e0;
background: white;
}
.agent-chat-input-container {
display: flex;
gap: 10px;
}
.agent-chat-input-container input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 1em;
}
.agent-chat-input-container button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
font-weight: 600;
}
.info-section {
background: white;
padding: 30px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.info-section h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.8em;
}
.info-section p {
color: #666;
line-height: 1.8;
margin-bottom: 15px;
}
.capabilities {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.capability {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
border-left: 4px solid #667eea;
}
.capability-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.capability-desc {
font-size: 0.9em;
color: #666;
}
.chat-section {
background: white;
padding: 30px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
max-height: 500px;
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 20px;
max-height: 300px;
}
.message {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
.message.user {
flex-direction: row-reverse;
}
.message-content {
max-width: 70%;
padding: 10px 14px;
border-radius: 12px;
line-height: 1.5;
}
.message.user .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message.assistant .message-content {
background: white;
color: #333;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.chat-input-container {
display: flex;
gap: 10px;
}
.chat-input-container input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 1em;
}
.chat-input-container button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
font-weight: 600;
}
.services-alert {
background: linear-gradient(135deg, var(--color-error) 0%, #c91a1a 100%);
padding: 20px 30px;
border-radius: 15px;
margin-bottom: 25px;
box-shadow: 0 10px 40px rgba(220, 38, 38, 0.3);
border: 3px solid var(--color-error);
display: none;
}
.services-alert.visible {
display: block;
animation: pulse-alert 2s ease-in-out infinite;
}
@keyframes pulse-alert {
0%, 100% { box-shadow: 0 10px 40px rgba(220, 38, 38, 0.3); }
50% { box-shadow: 0 10px 60px rgba(220, 38, 38, 0.6); }
}
.services-alert-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
cursor: pointer;
}
.services-alert-title {
color: white;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
display: flex;
align-items: center;
gap: 10px;
}
.services-alert-toggle {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 1.5em;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.services-alert-toggle:hover {
background: rgba(255, 255, 255, 0.3);
}
.services-alert-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
max-height: 400px;
overflow-y: auto;
}
.services-alert-content.collapsed {
display: none;
}
.alert-section {
background: rgba(255, 255, 255, 0.95);
padding: 15px;
border-radius: 10px;
border-left: 4px solid var(--color-error);
}
.alert-section h3 {
color: var(--color-error);
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.alert-item {
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
margin-bottom: 8px;
font-size: var(--font-size-sm);
color: #333;
}
.alert-item:last-child {
margin-bottom: 0;
}
.alert-item-name {
font-weight: var(--font-weight-semibold);
color: var(--color-error);
}
.alert-item-detail {
color: #666;
font-size: var(--font-size-xs);
margin-top: 4px;
}
/* Task Panel Styles */
.dashboard-layout {
display: flex;
gap: 25px;
align-items: flex-start;
}
.task-panel {
width: 340px;
min-width: 340px;
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #475569 #1e293b;
}
.task-panel::-webkit-scrollbar {
width: 6px;
}
.task-panel::-webkit-scrollbar-track {
background: #1e293b;
border-radius: 3px;
}
.task-panel::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
.task-panel-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border: 1px solid #334155;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
}
.task-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #334155;
}
.task-panel-title {
color: #f1f5f9;
font-size: 1.1em;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.task-panel-count {
background: #334155;
color: #94a3b8;
font-size: 0.75em;
padding: 3px 8px;
border-radius: 10px;
font-weight: 600;
}
.task-panel-refresh {
color: #64748b;
font-size: 0.7em;
margin-top: 12px;
text-align: center;
font-style: italic;
}
.task-item {
background: #1e293b;
border: 1px solid #334155;
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 10px;
transition: border-color 0.2s, transform 0.2s;
}
.task-item:last-child {
margin-bottom: 0;
}
.task-item:hover {
border-color: #475569;
transform: translateX(3px);
}
.task-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
gap: 8px;
}
.task-item-title {
color: #e2e8f0;
font-size: 0.88em;
font-weight: 600;
line-height: 1.3;
flex: 1;
}
.task-status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
border-radius: 6px;
font-size: 0.68em;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
flex-shrink: 0;
}
.task-status-completed {
background: rgba(16, 185, 129, 0.15);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.task-status-in_progress {
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.3);
}
.task-status-failed {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.task-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
}
.task-status-completed .task-status-dot {
background: #34d399;
box-shadow: 0 0 6px rgba(16, 185, 129, 0.6);
}
.task-status-in_progress .task-status-dot {
background: #fbbf24;
box-shadow: 0 0 6px rgba(245, 158, 11, 0.6);
animation: pulse-dot 2s ease-in-out infinite;
}
.task-status-failed .task-status-dot {
background: #f87171;
box-shadow: 0 0 6px rgba(239, 68, 68, 0.6);
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.task-item-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
}
.task-item-agent {
color: #64748b;
font-size: 0.72em;
display: flex;
align-items: center;
gap: 4px;
}
.task-item-time {
color: #475569;
font-size: 0.7em;
font-family: var(--font-mono);
}
.task-empty {
color: #475569;
font-size: 0.85em;
text-align: center;
padding: 20px 10px;
font-style: italic;
}
.dashboard-main {
flex: 1;
min-width: 0;
}
@media (max-width: 1024px) {
.dashboard-layout {
flex-direction: column;
}
.task-panel {
width: 100%;
min-width: 100%;
position: static;
max-height: none;
}
}
</style>
</head>
<body>
<!-- Semantic HTML5 Main Container -->
<main class="container" role="main" itemscope itemtype="https://schema.org/WebApplication">
<!-- Header Section -->
<header class="header" role="banner">
<div class="header-left">
<h1 itemprop="name">🤖 DW-Agents Control Hub</h1>
<p class="subtitle" itemprop="description">Designer Wallcoverings AI Assistant Network</p>
</div>
<nav aria-label="Main navigation" style="display: flex; gap: 10px; align-items: center;">
<div id="refreshCountdown" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 20px; border-radius: 10px; font-weight: 600; font-size: 0.9em; min-width: 180px; text-align: center;">
Next refresh: <span id="countdownTimer">30</span>s
</div>
<button onclick="startAllServersDown()" class="logout-btn" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);" aria-label="Start all offline servers">
🚀 Start All Servers Down
</button>
<a href="/logout" class="logout-btn" aria-label="Logout from dashboard">Logout</a>
</nav>
</header>
<!-- Broken Services Alert Banner -->
<aside id="servicesAlert" class="services-alert" role="alert" aria-live="polite">
<div class="services-alert-header" onclick="toggleAlertContent()">
<div class="services-alert-title">
🚨 Service Health Alerts
<span id="alertCount" style="background: rgba(255,255,255,0.3); padding: 4px 12px; border-radius: 12px; font-size: 0.85em;"></span>
</div>
<button class="services-alert-toggle" id="alertToggle" aria-label="Toggle alert details">−</button>
</div>
<div id="alertContent" class="services-alert-content">
<!-- Content will be populated by JavaScript -->
</div>
</aside>
<!-- Action Log Section -->
<section class="chat-section" style="background: linear-gradient(135deg, #1e293b 0%, #334155 100%); border: 2px solid #475569; margin-bottom: 25px; max-height: 300px; overflow: hidden; display: flex; flex-direction: column;" aria-labelledby="activity-log-title">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 id="activity-log-title" style="color: #f1f5f9; margin: 0;">📋 Activity Log</h2>
<button onclick="clearLog()" style="background: #ef4444; color: white; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer; font-size: 0.85em;" aria-label="Clear activity log">
Clear Log
</button>
</div>
<div id="actionLog" style="background: #0f172a; padding: 15px; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 0.85em; color: #94a3b8; overflow-y: auto; flex: 1; max-height: 220px;" role="log" aria-live="polite">
<div style="color: #22d3ee;">[${new Date().toLocaleTimeString()}] System initialized - Master Hub online</div>
</div>
</section>
<!-- Currently Working On Section -->
<section class="chat-section" style="background: linear-gradient(135deg, #667eea22 0%, #764ba222 100%); border: 2px solid #667eea; margin-bottom: 25px;" aria-labelledby="current-tasks-title">
<h2 id="current-tasks-title" style="color: #667eea; margin-bottom: 15px;">🚀 Currently Working On</h2>
<div id="activeTasks" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
<article style="background: white; padding: 15px; border-radius: 10px; border-left: 4px solid #667eea;">
<h3 style="font-weight: 600; color: #667eea; margin-bottom: 5px;">✅ System Status</h3>
<p style="color: #666; font-size: 0.95em;">All agents online and operational</p>
</article>
</div>
</section>
<!-- Master Chat Section -->
<section class="chat-section" aria-labelledby="master-chat-title">
<h2 id="master-chat-title" style="color: #667eea; margin-bottom: 15px;">💬 Master Chat - Talk to All Agents</h2>
<div class="chat-messages" id="chatMessages" role="log" aria-live="polite" aria-atomic="false">
<div class="message assistant">
<div class="message-content">
Hi! I'm the Master Agent coordinator. I can help you navigate the DW-Agents system and route your questions to the right specialist. Ask me anything!
</div>
</div>
</div>
<form class="chat-input-container" onsubmit="event.preventDefault(); sendMessage();" role="search" aria-label="Chat with agents">
<input
type="text"
id="chatInput"
placeholder="Ask about any agent or system function..."
aria-label="Chat message input"
autocomplete="off"
>
<button type="submit" id="sendBtn" aria-label="Send message">Send</button>
</form>
</section>
<!-- Dashboard Layout: Task Panel + Agents Grid -->
<div class="dashboard-layout">
<!-- Left Task Panel -->
<aside class="task-panel" aria-label="Task Area">
<!-- Active Tasks -->
<div class="task-panel-card">
<div class="task-panel-header">
<div class="task-panel-title">
⚡ Active Tasks
</div>
<span class="task-panel-count" id="activeTaskCount">0</span>
</div>
<div id="activeTaskList">
<div class="task-empty">Loading tasks...</div>
</div>
</div>
<!-- Completed Tasks -->
<div class="task-panel-card">
<div class="task-panel-header">
<div class="task-panel-title">
✅ Completed Tasks
</div>
<span class="task-panel-count" id="completedTaskCount">0</span>
</div>
<div id="completedTaskList">
<div class="task-empty">Loading tasks...</div>
</div>
</div>
<!-- Failed Tasks (hidden if none) -->
<div class="task-panel-card" id="failedTaskPanel" style="display: none;">
<div class="task-panel-header">
<div class="task-panel-title">
❌ Failed Tasks
</div>
<span class="task-panel-count" id="failedTaskCount">0</span>
</div>
<div id="failedTaskList">
<div class="task-empty">No failed tasks</div>
</div>
</div>
<div class="task-panel-refresh" id="taskRefreshInfo">Auto-refresh in 30s</div>
</aside>
<!-- Main Dashboard Content -->
<div class="dashboard-main">
<!-- Agents Grid Section -->
<section class="agents-grid" aria-label="AI Agents Dashboard" itemscope itemtype="https://schema.org/ItemList">
<meta itemprop="numberOfItems" content="${agents.length}">
${agents.map((agent, index) => `
<article class="agent-card" style="--agent-color: ${agent.color}" itemscope itemtype="https://schema.org/SoftwareApplication" itemprop="itemListElement">
<meta itemprop="position" content="${index + 1}">
<header class="agent-header">
<div class="agent-icon" aria-hidden="true">${agent.icon}</div>
<div class="agent-info">
<h2 itemprop="name">${agent.name}</h2>
<div class="port" itemprop="identifier">Port ${agent.port}</div>
</div>
</header>
<p class="agent-description" itemprop="description">${agent.description}</p>
<div class="status-badges">
<span class="status-badge status-${agent.status}" role="status" aria-label="Service status: ${agent.status}">${agent.status}</span>
<span class="firewall-badge ${agent.firewallOpen ? 'fw-open' : 'fw-closed'}" role="status" aria-label="Firewall: ${agent.firewallOpen ? 'Open' : 'Closed'}">${agent.firewallOpen ? 'FW: OPEN' : 'FW: CLOSED'}</span>
</div>
<nav class="agent-actions" aria-label="${agent.name} actions">
<a href="${agent.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" ${!agent.isRunning ? 'style="pointer-events: none; opacity: 0.5;"' : ''} aria-label="Open ${agent.name} dashboard" itemprop="url">
Open Dashboard
</a>
<button class="btn btn-chat" onclick="openAgentChat('${agent.id}', '${agent.name}', '${agent.icon}')" ${!agent.isRunning ? 'disabled' : ''} aria-label="Chat with ${agent.name}">
💬 Chat
</button>
${!agent.isRunning ? `
<button class="btn btn-restart" onclick="restartAgent('${agent.port}', '${agent.name}')" aria-label="Start ${agent.name} service">
🔄 Start
</button>
` : `
<button class="btn btn-restart" onclick="restartAgent('${agent.port}', '${agent.name}')" aria-label="Restart ${agent.name} service">
🔄 Restart
</button>
`}
</nav>
</article>
`).join('')}
</section>
</div>
</div>
<!-- System Capabilities Section -->
<section class="info-section" aria-labelledby="capabilities-title" itemscope itemtype="https://schema.org/WebApplication">
<h2 id="capabilities-title">📋 System Capabilities</h2>
<p itemprop="about">The DW-Agents system provides comprehensive AI-powered assistance across all business operations:</p>
<div class="capabilities" itemprop="featureList">
<article class="capability">
<h3 class="capability-title">⚖️ Legal Compliance</h3>
<p class="capability-desc">Automated settlement agreement monitoring and product compliance checking</p>
</article>
<article class="capability">
<h3 class="capability-title">📦 Digital Samples</h3>
<p class="capability-desc">Automatic DIG-series order processing with image resizing and Slack delivery</p>
</article>
<article class="capability">
<h3 class="capability-title">🛒 Purchasing</h3>
<p class="capability-desc">Inventory tracking, vendor management, and restock alerts</p>
</article>
<article class="capability">
<h3 class="capability-title">📢 Marketing</h3>
<p class="capability-desc">Blog content, social media posts, and email campaign generation</p>
</article>
<article class="capability">
<h3 class="capability-title">🤖 AI-Powered</h3>
<p class="capability-desc">Claude 4 integration for intelligent decision-making and content generation</p>
</article>
</div>
</section>
</main>
<!-- Agent Chat Modal -->
<dialog id="agentChatModal" class="agent-chat-modal" aria-labelledby="agent-modal-title" aria-modal="true">
<div class="agent-chat-container">
<header class="agent-chat-header">
<div>
<h3 id="agent-modal-title">💬 Chat with Agent</h3>
<p style="margin: 5px 0 0 0; font-size: 0.9em; opacity: 0.9;">Ask me anything about this agent</p>
</div>
<button class="agent-chat-close" onclick="closeAgentChat()" aria-label="Close chat modal">×</button>
</header>
<div class="agent-chat-messages" id="agentChatMessages" role="log" aria-live="polite">
<div class="message assistant">
<div class="message-content">
Hi! I'm your AI assistant for this agent. Ask me anything!
</div>
</div>
</div>
<div class="agent-chat-input-area">
<form class="agent-chat-input-container" onsubmit="event.preventDefault(); sendAgentMessage();">
<input
type="text"
id="agentChatInput"
placeholder="Type your question..."
aria-label="Agent chat input"
autocomplete="off"
>
<button type="submit" id="agentChatSendBtn" aria-label="Send message to agent">Send</button>
</form>
</div>
</div>
</dialog>
<script>
// Action logging functions
function logAction(message, type = 'info') {
const logDiv = document.getElementById('actionLog');
const timestamp = new Date().toLocaleTimeString();
const colors = {
info: '#22d3ee',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444'
};
const logEntry = document.createElement('div');
logEntry.style.color = colors[type] || colors.info;
logEntry.style.marginBottom = '4px';
logEntry.textContent = \`[\${timestamp}] \${message}\`;
logDiv.appendChild(logEntry);
logDiv.scrollTop = logDiv.scrollHeight;
// Keep only last 100 entries
while (logDiv.children.length > 100) {
logDiv.removeChild(logDiv.firstChild);
}
}
function clearLog() {
if (confirm('Clear all log entries?')) {
const logDiv = document.getElementById('actionLog');
logDiv.innerHTML = '<div style="color: #22d3ee;">[' + new Date().toLocaleTimeString() + '] Log cleared</div>';
logAction('Log cleared by user', 'warning');
}
}
async function checkStatus(agentId) {
logAction(\`Checking status of agent: \${agentId}\`, 'info');
try {
const response = await fetch(\`/api/agent/\${agentId}/status\`);
const data = await response.json();
logAction(\`Status check complete for \${agentId}: \${data.status}\`, 'success');
alert(\`Agent Status: \${data.status}\\nUptime: \${data.uptime || 'N/A'}\`);
} catch (error) {
logAction(\`Failed to check status for \${agentId}: \${error.message}\`, 'error');
alert('Unable to check agent status');
}
}
// Countdown and refresh functionality
let secondsRemaining = 30;
function updateCountdown() {
const timerElement = document.getElementById('countdownTimer');
if (timerElement) {
timerElement.textContent = secondsRemaining;
}
if (secondsRemaining <= 0) {
// Refresh the page
location.reload();
} else {
secondsRemaining--;
}
}
// Update countdown every second
setInterval(updateCountdown, 1000);
// Also refresh status data every 30 seconds (backup)
setInterval(() => {
fetch('/api/agents/status')
.then(r => r.json())
.then(data => {
console.log('Agent status updated:', data);
});
}, 30000);
// Chat functionality
function addMessage(role, content) {
const messagesDiv = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = \`message \${role}\`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
messageDiv.appendChild(contentDiv);
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
async function sendMessage() {
const input = document.getElementById('chatInput');
const sendBtn = document.getElementById('sendBtn');
const message = input.value.trim();
if (!message) return;
logAction(\`Sending message to Master Chat: "\${message.substring(0, 50)}..."\`, 'info');
addMessage('user', message);
input.value = '';
sendBtn.disabled = true;
sendBtn.textContent = 'Thinking...';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
addMessage('assistant', data.response);
logAction('Master Chat response received', 'success');
} catch (error) {
addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
logAction(\`Master Chat error: \${error.message}\`, 'error');
}
sendBtn.disabled = false;
sendBtn.textContent = 'Send';
}
// Agent-specific chat functionality
let currentAgentId = null;
function openAgentChat(agentId, agentName, agentIcon) {
logAction(\`Opening chat with \${agentName}\`, 'info');
currentAgentId = agentId;
const modal = document.getElementById('agentChatModal');
const title = document.getElementById('agent-modal-title');
const messages = document.getElementById('agentChatMessages');
title.innerHTML = \`\${agentIcon} Chat with \${agentName}\`;
// Reset chat messages
messages.innerHTML = \`
<div class="message assistant">
<div class="message-content">
Hi! I'm your AI assistant for the \${agentName} agent. Ask me anything!
</div>
</div>
\`;
modal.classList.add('active');
}
function closeAgentChat() {
if (currentAgentId) {
logAction(\`Closed chat session\`, 'info');
}
const modal = document.getElementById('agentChatModal');
modal.classList.remove('active');
currentAgentId = null;
}
async function restartAgent(port, name) {
if (!confirm(\`Restart \${name} on port \${port}?\`)) {
logAction(\`Restart cancelled for \${name} (port \${port})\`, 'warning');
return;
}
logAction(\`Initiating restart for \${name} on port \${port}\`, 'info');
try {
const response = await fetch('/api/restart-agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port })
});
const data = await response.json();
if (response.ok) {
logAction(\`Successfully restarted \${name} (port \${port})\`, 'success');
alert(\`\${name} is restarting...\`);
setTimeout(() => location.reload(), 3000);
} else {
logAction(\`Failed to restart \${name}: \${data.error}\`, 'error');
alert(\`Error: \${data.error}\`);
}
} catch (error) {
logAction(\`Network error restarting \${name}: \${error.message}\`, 'error');
alert('Network error: ' + error.message);
}
}
function addAgentMessage(role, content) {
const messagesDiv = document.getElementById('agentChatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = \`message \${role}\`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
messageDiv.appendChild(contentDiv);
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
async function sendAgentMessage() {
const input = document.getElementById('agentChatInput');
const sendBtn = document.getElementById('agentChatSendBtn');
const message = input.value.trim();
if (!message || !currentAgentId) return;
addAgentMessage('user', message);
input.value = '';
sendBtn.disabled = true;
sendBtn.textContent = 'Thinking...';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, targetAgent: currentAgentId })
});
const data = await response.json();
addAgentMessage('assistant', data.response);
} catch (error) {
addAgentMessage('assistant', 'Sorry, I encountered an error. Please try again.');
}
sendBtn.disabled = false;
sendBtn.textContent = 'Send';
}
// Close modal when clicking outside
document.getElementById('agentChatModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeAgentChat();
}
});
// Services Health Alert Functions
let isAlertCollapsed = false;
function toggleAlertContent() {
const content = document.getElementById('alertContent');
const toggle = document.getElementById('alertToggle');
isAlertCollapsed = !isAlertCollapsed;
if (isAlertCollapsed) {
content.classList.add('collapsed');
toggle.textContent = '+';
} else {
content.classList.remove('collapsed');
toggle.textContent = '−';
}
}
async function updateServiceHealth() {
try {
const response = await fetch('/api/health');
const data = await response.json();
const alertBox = document.getElementById('servicesAlert');
const alertContent = document.getElementById('alertContent');
const alertCount = document.getElementById('alertCount');
if (!data.healthy) {
// Show alert banner
alertBox.classList.add('visible');
// Count total issues
const totalIssues = (data.offline?.length || 0) +
(data.highRestarts?.length || 0) +
(data.memoryIssues?.length || 0);
alertCount.textContent = \`\${totalIssues} issue\${totalIssues !== 1 ? 's' : ''}\`;
// Build alert content
let html = '';
// Offline services
if (data.offline && data.offline.length > 0) {
html += \`
<div class="alert-section">
<h3>🔴 Offline Services (\${data.offline.length})</h3>
\${data.offline.map(service => \`
<div class="alert-item">
<div class="alert-item-name">\${service.name}</div>
<div class="alert-item-detail">Status: \${service.status}</div>
<div class="alert-item-detail">Port: \${service.port}</div>
</div>
\`).join('')}
</div>
\`;
}
// High restart count services
if (data.highRestarts && data.highRestarts.length > 0) {
html += \`
<div class="alert-section">
<h3>⚠️ High Restart Count (\${data.highRestarts.length})</h3>
\${data.highRestarts.map(service => \`
<div class="alert-item">
<div class="alert-item-name">\${service.name}</div>
<div class="alert-item-detail">Restarts: \${service.restarts}</div>
<div class="alert-item-detail">Port: \${service.port}</div>
</div>
\`).join('')}
</div>
\`;
}
// Memory issues
if (data.memoryIssues && data.memoryIssues.length > 0) {
html += \`
<div class="alert-section">
<h3>💾 High Memory Usage (\${data.memoryIssues.length})</h3>
\${data.memoryIssues.map(service => \`
<div class="alert-item">
<div class="alert-item-name">\${service.name}</div>
<div class="alert-item-detail">Memory: \${service.memory} MB</div>
<div class="alert-item-detail">Port: \${service.port}</div>
</div>
\`).join('')}
</div>
\`;
}
alertContent.innerHTML = html;
logAction(\`Health check: \${totalIssues} issue(s) detected\`, 'warning');
} else {
// Hide alert banner if everything is healthy
alertBox.classList.remove('visible');
logAction('Health check: All services healthy', 'success');
}
} catch (error) {
console.error('Health check failed:', error);
logAction(\`Health check failed: \${error.message}\`, 'error');
}
}
// Update health status every 30 seconds
updateServiceHealth();
setInterval(updateServiceHealth, 30000);
// Task Panel Functions
let taskRefreshSeconds = 30;
function formatTaskTime(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return \`\${diffMins}m ago\`;
if (diffHours < 24) return \`\${diffHours}h ago\`;
return \`\${diffDays}d ago\`;
}
function renderTaskItem(task) {
const statusLabel = task.status === 'in_progress' ? 'In Progress' :
task.status === 'completed' ? 'Done' :
task.status === 'failed' ? 'Failed' : task.status;
const timeStr = task.status === 'completed'
? formatTaskTime(task.completedAt || task.updatedAt)
: formatTaskTime(task.updatedAt);
return \`
<div class="task-item">
<div class="task-item-header">
<div class="task-item-title">\${task.title}</div>
<span class="task-status-badge task-status-\${task.status}">
<span class="task-status-dot"></span>
\${statusLabel}
</span>
</div>
<div class="task-item-meta">
<span class="task-item-agent">🤖 \${task.agent || 'System'}</span>
<span class="task-item-time">\${timeStr}</span>
</div>
</div>
\`;
}
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const data = await response.json();
if (!data.success) {
console.error('Failed to load tasks');
return;
}
// Active tasks
const activeList = document.getElementById('activeTaskList');
const activeCount = document.getElementById('activeTaskCount');
activeCount.textContent = data.active.length;
if (data.active.length > 0) {
activeList.innerHTML = data.active.map(renderTaskItem).join('');
} else {
activeList.innerHTML = '<div class="task-empty">No active tasks</div>';
}
// Completed tasks
const completedList = document.getElementById('completedTaskList');
const completedCount = document.getElementById('completedTaskCount');
completedCount.textContent = data.completed.length;
if (data.completed.length > 0) {
// Show most recent first, limit to 10
const sorted = data.completed.sort((a, b) =>
new Date(b.completedAt || b.updatedAt) - new Date(a.completedAt || a.updatedAt)
);
completedList.innerHTML = sorted.slice(0, 10).map(renderTaskItem).join('');
} else {
completedList.innerHTML = '<div class="task-empty">No completed tasks yet</div>';
}
// Failed tasks
const failedPanel = document.getElementById('failedTaskPanel');
const failedList = document.getElementById('failedTaskList');
const failedCount = document.getElementById('failedTaskCount');
if (data.failed.length > 0) {
failedPanel.style.display = 'block';
failedCount.textContent = data.failed.length;
failedList.innerHTML = data.failed.map(renderTaskItem).join('');
} else {
failedPanel.style.display = 'none';
}
} catch (error) {
console.error('Error loading tasks:', error);
}
}
function updateTaskRefreshCountdown() {
const info = document.getElementById('taskRefreshInfo');
if (info) {
info.textContent = \`Auto-refresh in \${taskRefreshSeconds}s\`;
}
if (taskRefreshSeconds <= 0) {
taskRefreshSeconds = 30;
loadTasks();
} else {
taskRefreshSeconds--;
}
}
// Initial load and auto-refresh
loadTasks();
setInterval(updateTaskRefreshCountdown, 1000);
// Start all servers that are down
async function startAllServersDown() {
if (!confirm('Start all offline servers and open their firewall ports?')) {
logAction('Bulk server start cancelled by user', 'warning');
return;
}
logAction('Starting bulk server restart operation', 'info');
const btn = event.target;
const originalText = btn.textContent;
btn.disabled = true;
// Create modal for real-time status updates
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 99999; display: flex; align-items: center; justify-content: center;';
const statusBox = document.createElement('div');
statusBox.style.cssText = 'background: white; padding: 30px; border-radius: 20px; max-width: 800px; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.3);';
statusBox.innerHTML = '<h2 style="color: #667eea; margin-bottom: 20px;">🚀 Starting All Servers Down</h2><div id="statusLog" style="font-family: monospace; font-size: 14px; line-height: 1.8;"></div>';
modal.appendChild(statusBox);
document.body.appendChild(modal);
const statusLog = document.getElementById('statusLog');
function addLog(message, type = 'info') {
const logEntry = document.createElement('div');
const colors = {
info: '#666',
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
checking: '#3b82f6'
};
logEntry.style.cssText = \`color: \${colors[type]}; margin-bottom: 8px; padding: 8px; background: \${type === 'error' ? '#fee' : type === 'success' ? '#efe' : type === 'warning' ? '#fef3cd' : '#f8f9fa'}; border-radius: 6px; border-left: 4px solid \${colors[type]};\`;
logEntry.textContent = message;
statusLog.appendChild(logEntry);
statusLog.scrollTop = statusLog.scrollHeight;
}
try {
btn.textContent = '🔄 Starting servers...';
addLog('🔍 Checking server uptime agent status...', 'checking');
// Check Server Uptime agent status first
try {
const uptimeResponse = await fetch('http://localhost:9882/api/metrics');
if (uptimeResponse.ok) {
const uptimeData = await uptimeResponse.json();
addLog(\`✅ Server Uptime Agent is running (uptime: \${Math.floor(uptimeData.uptime / 60)} minutes)\`, 'success');
addLog('ℹ️ Server Uptime Agent monitors and auto-restarts crashed processes every 15 seconds', 'info');
} else {
addLog('⚠️ Server Uptime Agent is not responding - manual restarts needed', 'warning');
}
} catch (err) {
addLog('❌ Server Uptime Agent is offline - failed servers will not auto-recover', 'error');
}
addLog('\\n📋 Fetching list of offline servers...', 'info');
const response = await fetch('/api/start-all-down', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (response.ok) {
if (data.started && data.started.length > 0) {
addLog(\`\\n🎯 Found \${data.started.length} offline server(s) to start\\n\`, 'info');
for (const server of data.started) {
addLog(\`🔄 Starting \${server.name} on port \${server.port}...\`, 'info');
// Check if firewall was opened
if (data.firewallOpened && data.firewallOpened.includes(server.port)) {
addLog(\` ✅ Opened firewall port \${server.port}\`, 'success');
}
addLog(\` ✅ \${server.name} started successfully\`, 'success');
}
// Test servers
addLog('\\n🧪 Testing server accessibility...', 'checking');
btn.textContent = '🧪 Testing servers...';
await new Promise(resolve => setTimeout(resolve, 3000));
for (const server of data.started) {
try {
const testResponse = await fetch(\`http://45.61.58.125:\${server.port}\`, {
method: 'HEAD',
mode: 'no-cors'
});
addLog(\` ✅ \${server.name} is accessible at http://45.61.58.125:\${server.port}\`, 'success');
} catch (err) {
addLog(\` ⚠️ \${server.name} started but not yet responding (may need more time)\`, 'warning');
addLog(\` Server Uptime Agent will monitor and restart if needed\`, 'info');
}
}
if (data.errors && data.errors.length > 0) {
addLog('\\n❌ Failed to start some servers:', 'error');
for (const error of data.errors) {
addLog(\` ❌ \${error.name} (:\${error.port}): \${error.error}\`, 'error');
addLog(\` ⚙️ Server Uptime Agent will attempt to restart this in 15 seconds\`, 'info');
}
}
addLog('\\n✅ Process complete! Refreshing page in 3 seconds...', 'success');
logAction(\`Bulk server restart completed - \${data.started} server(s) started\`, 'success');
setTimeout(() => {
modal.remove();
location.reload();
}, 3000);
} else {
addLog('✅ All servers are already running!', 'success');
logAction('All servers already running - no action needed', 'info');
setTimeout(() => modal.remove(), 2000);
}
} else {
addLog(\`❌ Error: \${data.error}\`, 'error');
logAction(\`Bulk server restart failed: \${data.error}\`, 'error');
setTimeout(() => modal.remove(), 3000);
}
} catch (error) {
addLog(\`❌ Network error: \${error.message}\`, 'error');
logAction(\`Bulk server restart network error: \${error.message}\`, 'error');
setTimeout(() => modal.remove(), 3000);
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
}
</script>
</body>
</html>
`);
});
// Tasks API endpoint - reads from tasks.json
const TASKS_FILE = path.join(process.cwd(), 'tasks.json');
function loadTasks(): any[] {
try {
if (fs.existsSync(TASKS_FILE)) {
const data = fs.readFileSync(TASKS_FILE, 'utf8');
const parsed = JSON.parse(data);
return parsed.tasks || [];
}
} catch (error) {
console.error('Error reading tasks.json:', error);
}
return [];
}
function saveTasks(tasks: any[]) {
try {
fs.writeFileSync(TASKS_FILE, JSON.stringify({ tasks }, null, 2), 'utf8');
} catch (error) {
console.error('Error writing tasks.json:', error);
}
}
app.get('/api/tasks', (req, res) => {
const tasks = loadTasks();
const activeTasks = tasks.filter(t => t.status === 'in_progress');
const completedTasks = tasks.filter(t => t.status === 'completed');
const failedTasks = tasks.filter(t => t.status === 'failed');
res.json({
success: true,
active: activeTasks,
completed: completedTasks,
failed: failedTasks,
total: tasks.length,
timestamp: new Date().toISOString()
});
});
app.post('/api/tasks', express.json(), (req, res) => {
const { title, description, status, priority, agent } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const tasks = loadTasks();
const newTask = {
id: `task-${String(tasks.length + 1).padStart(3, '0')}`,
title,
description: description || '',
status: status || 'in_progress',
priority: priority || 'medium',
agent: agent || 'System',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
tasks.push(newTask);
saveTasks(tasks);
res.json({ success: true, task: newTask });
});
app.patch('/api/tasks/:id', express.json(), (req, res) => {
const tasks = loadTasks();
const taskIndex = tasks.findIndex(t => t.id === req.params.id);
if (taskIndex === -1) {
return res.status(404).json({ error: 'Task not found' });
}
const updates = req.body;
tasks[taskIndex] = { ...tasks[taskIndex], ...updates, updatedAt: new Date().toISOString() };
if (updates.status === 'completed') {
tasks[taskIndex].completedAt = new Date().toISOString();
}
saveTasks(tasks);
res.json({ success: true, task: tasks[taskIndex] });
});
// API endpoints
app.get('/api/agents', (req, res) => {
res.json(AGENTS);
});
app.get('/api/agent/:id/status', async (req, res) => {
const agent = AGENTS.find(a => a.id === req.params.id);
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
try {
// Try to ping the agent
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(agent.url, {
signal: controller.signal,
method: 'HEAD'
});
clearTimeout(timeout);
res.json({
status: response.ok ? 'online' : 'offline',
url: agent.url,
name: agent.name
});
} catch (error) {
res.json({
status: 'offline',
url: agent.url,
name: agent.name
});
}
});
app.get('/api/agents/status', (req, res) => {
res.json({
total: AGENTS.length,
online: AGENTS.filter(a => a.status === 'online').length,
planned: AGENTS.filter(a => a.status === 'planned').length,
timestamp: new Date().toISOString()
});
});
// Metrics endpoint for daily reporting
app.get("/api/metrics", (req, res) => {
res.json({
status: "online",
uptime: process.uptime(),
responseTime: 0,
tasksCompleted: 0,
errorsToday: 0,
lastActivity: new Date().toISOString(),
qnaReadiness: 100
});
});
// API endpoint to restart an agent
app.post('/api/restart-agent', async (req, res) => {
try {
const { port } = req.body;
if (!port) {
return res.status(400).json({ error: 'Port is required' });
}
// Find agent in registry
const agent = AGENTS.find(a => a.port === parseInt(port));
if (!agent) {
return res.status(404).json({ error: 'Agent not found' });
}
console.log(`Restarting agent on port ${port}...`);
// Kill existing process on that port
try {
await execAsync(`pkill -f "port.*${port}"`);
} catch (e) {
// Process may not be running
}
await new Promise(resolve => setTimeout(resolve, 1000));
// Start the agent (this is simplified - you may need to adjust based on your agent structure)
const agentScript = agent.id.includes('dashboard') ? `${agent.id}-agent.ts` : `${agent.id}-agent.ts`;
await execAsync(`cd /root/Projects/Designer-Wallcoverings/DW-Agents && npx tsx ${agentScript} > /tmp/${agent.id}.log 2>&1 &`);
res.json({ success: true, message: `Agent on port ${port} restarted` });
} catch (error: any) {
console.error('Error restarting agent:', error);
res.status(500).json({ error: error.message });
}
});
// API endpoint to start all servers that are down
app.post('/api/start-all-down', async (req, res) => {
try {
const agents = await getEnrichedAgents();
const offlineAgents = agents.filter(a => !a.isRunning);
if (offlineAgents.length === 0) {
return res.json({
success: true,
message: 'All servers are already running',
started: [],
firewallOpened: []
});
}
const started = [];
const firewallOpened = [];
const errors = [];
for (const agent of offlineAgents) {
try {
console.log(`Starting ${agent.name} on port ${agent.port}...`);
// Open firewall port if closed
if (!agent.firewallOpen) {
try {
await execAsync(`sudo ufw allow ${agent.port}/tcp`);
firewallOpened.push(agent.port);
console.log(`✅ Opened firewall port ${agent.port}`);
} catch (fwError) {
console.error(`⚠️ Failed to open firewall for port ${agent.port}:`, fwError);
}
}
// Start the agent using PM2 if available
const agentScript = agent.id.includes('dashboard') ? `${agent.id}-agent.ts` : `${agent.id}-agent.ts`;
await execAsync(`cd /root/Projects/Designer-Wallcoverings/DW-Agents && PORT=${agent.port} npx tsx ${agentScript} > /tmp/${agent.id}.log 2>&1 &`);
started.push({ name: agent.name, port: agent.port });
console.log(`✅ Started ${agent.name} on port ${agent.port}`);
// Small delay between starts
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error: any) {
console.error(`❌ Failed to start ${agent.name}:`, error);
errors.push({ name: agent.name, port: agent.port, error: error.message });
}
}
res.json({
success: true,
message: `Started ${started.length} of ${offlineAgents.length} offline servers`,
started,
firewallOpened,
errors: errors.length > 0 ? errors : undefined
});
} catch (error: any) {
console.error('Error starting all servers:', error);
res.status(500).json({ error: error.message });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log('🤖 DW-Agents Master Hub');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🌍 External: http://45.61.58.125:' + PORT);
console.log('🏠 Local: http://localhost:' + PORT);
console.log('');
console.log('📊 Registered Agents:');
AGENTS.forEach(agent => {
console.log(` ${agent.icon} ${agent.name} (Port ${agent.port}) - ${agent.status}`);
});
console.log('');
console.log('✅ Master hub is running...');
});