← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-task-orchestrator/task-orchestrator-agent.ts.backup-oauth-20251112
1841 lines
import cookieParser from "cookie-parser";
/**
* DW-Agents: Task Orchestrator Agent
*
* Master task management system that:
* - Auto-tracks every action as a task
* - Routes tasks to appropriate specialized agents
* - Maintains full audit trail with timestamps
* - Self-healing capabilities for server errors
* - Memory system for context persistence
* - Interactive mode for clarifications
*
* Port: 9900
*/
import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import fs from 'fs';
import path from 'path';
import { chatMiddleware } from '../shared-chat-integration';
const { getUniversalHeader } = require('./shared-ui-components.js');
const app = express();
// Global Authentication
const PORT = 9900;
// Global Authentication - MUST come before any other middleware
app.use(cookieParser());
// Add inter-agent chat widget (after auth)
app.use(chatMiddleware({
agentId: 'agent-task-orchestrator',
agentName: 'Task Orchestrator',
port: 9900,
category: 'agent'
}));
// Task database file
const TASKS_DB_FILE = path.join(__dirname, 'tasks-database.json');
const MEMORY_FILE = path.join(__dirname, 'task-memory.json');
// Task status types
type TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'blocked' | 'completed' | 'failed';
type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
assignedAgent?: string;
createdAt: string;
assignedAt?: string;
startedAt?: string;
completedAt?: string;
blockedAt?: string;
blockReason?: string;
estimatedDuration?: number; // minutes
actualDuration?: number; // minutes
autoGenerated: boolean;
parentTaskId?: string;
subtasks: string[]; // task IDs
tags: string[];
metadata: Record<string, any>;
actionLog: TaskAction[];
}
interface TaskAction {
timestamp: string;
action: string;
agent?: string;
details?: string;
success: boolean;
}
interface Memory {
context: Record<string, any>;
recentTasks: string[];
agentPerformance: Record<string, {
tasksCompleted: number;
averageDuration: number;
successRate: number;
}>;
commonPatterns: string[];
lastUpdated: string;
}
// Agent registry for routing (alphabetical order)
const AGENT_REGISTRY = {
'accounting': { port: 9882, capabilities: ['invoices', 'payments', 'financial-reports'] },
'digital-samples': { port: 9879, capabilities: ['sample-orders', 'dig-series', 'file-delivery'] },
'in-parallel': { port: 9891, capabilities: ['batch-processing', 'bulk-operations'] },
'legal': { port: 9878, capabilities: ['settlement', 'compliance', 'legal-docs'] },
'marketing': { port: 9881, capabilities: ['content', 'campaigns', 'social-media'] },
'needs-attention': { port: 9886, capabilities: ['urgent-issues', 'customer-complaints'] },
'purchasing': { port: 9880, capabilities: ['inventory', 'vendors', 'purchase-orders', 'office-supplies', 'amazon-personal', 'amazon-business', 'price-comparison', 'order-tracking', 'price-tracking', 'deals'] },
'server-uptime': { port: 9888, capabilities: ['monitoring', 'health-checks', 'auto-restart'] },
'shopify-store': { port: 7238, capabilities: ['shopify', 'products', 'orders', 'customers', 'transactions', 'store-management', 'new-products'] },
'york': { port: 7234, capabilities: ['york-products', 'dwjs-updater', 'product-sync'] },
'zendesk-chat': { port: 9884, capabilities: ['customer-support', 'live-chat'] }
};
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
declare module 'express-session' {
interface SessionData {
authenticated: boolean;
}
}
app.use(
session({
secret: 'dw-task-orchestrator-2025',
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
},
})
);
// Initialize databases
function initializeDatabases() {
if (!fs.existsSync(TASKS_DB_FILE)) {
fs.writeFileSync(TASKS_DB_FILE, JSON.stringify({ tasks: [] }, null, 2));
console.log('✅ Initialized tasks database');
}
if (!fs.existsSync(MEMORY_FILE)) {
const initialMemory: Memory = {
context: {},
recentTasks: [],
agentPerformance: {},
commonPatterns: [],
lastUpdated: new Date().toISOString()
};
fs.writeFileSync(MEMORY_FILE, JSON.stringify(initialMemory, null, 2));
console.log('✅ Initialized memory system');
}
}
// Load tasks from database
function loadTasks(): Task[] {
try {
const fileContent = fs.readFileSync(TASKS_DB_FILE, 'utf-8');
// Check if file is empty or truncated
if (!fileContent || fileContent.trim().length === 0) {
console.warn('⚠️ Tasks database is empty, initializing...');
return [];
}
const data = JSON.parse(fileContent);
return data.tasks || [];
} catch (error: any) {
console.error('❌ Error loading tasks:', error.message);
// Try to recover from backup if exists
const backupFile = TASKS_DB_FILE + '.backup';
if (fs.existsSync(backupFile)) {
try {
console.log('🔄 Attempting to restore from backup...');
const backupContent = fs.readFileSync(backupFile, 'utf-8');
const backupData = JSON.parse(backupContent);
fs.copyFileSync(backupFile, TASKS_DB_FILE);
console.log('✅ Restored from backup successfully');
return backupData.tasks || [];
} catch (backupError) {
console.error('❌ Backup restore failed:', backupError);
}
}
return [];
}
}
// Save tasks to database (atomic write to prevent corruption)
function saveTasks(tasks: Task[]) {
try {
const tempFile = TASKS_DB_FILE + '.tmp';
const backupFile = TASKS_DB_FILE + '.backup';
const content = JSON.stringify({ tasks }, null, 2);
// Create backup of current file if it exists
if (fs.existsSync(TASKS_DB_FILE)) {
try {
fs.copyFileSync(TASKS_DB_FILE, backupFile);
} catch (backupError) {
console.error('⚠️ Failed to create backup:', backupError);
}
}
// Write to temp file first
fs.writeFileSync(tempFile, content, 'utf-8');
// Verify temp file is valid JSON before replacing
const verification = JSON.parse(fs.readFileSync(tempFile, 'utf-8'));
if (!verification.tasks) {
throw new Error('Invalid task data structure');
}
// Atomic rename (replaces old file)
fs.renameSync(tempFile, TASKS_DB_FILE);
} catch (error) {
console.error('❌ Error saving tasks:', error);
// Clean up temp file if it exists
const tempFile = TASKS_DB_FILE + '.tmp';
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
}
// Load memory
function loadMemory(): Memory {
try {
return JSON.parse(fs.readFileSync(MEMORY_FILE, 'utf-8'));
} catch (error) {
console.error('Error loading memory:', error);
return {
context: {},
recentTasks: [],
agentPerformance: {},
commonPatterns: [],
lastUpdated: new Date().toISOString()
};
}
}
// Save memory
function saveMemory(memory: Memory) {
try {
memory.lastUpdated = new Date().toISOString();
fs.writeFileSync(MEMORY_FILE, JSON.stringify(memory, null, 2));
} catch (error) {
console.error('Error saving memory:', error);
}
}
// Generate unique task ID
function generateTaskId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `TASK-${timestamp}-${random}`;
}
// Create a new task
function createTask(
title: string,
description: string,
priority: TaskPriority = 'medium',
autoGenerated: boolean = false,
metadata: Record<string, any> = {}
): Task {
const task: Task = {
id: generateTaskId(),
title,
description,
status: 'pending',
priority,
createdAt: new Date().toISOString(),
autoGenerated,
subtasks: [],
tags: [],
metadata,
actionLog: [{
timestamp: new Date().toISOString(),
action: 'created',
details: `Task created: ${title}`,
success: true
}]
};
const tasks = loadTasks();
tasks.push(task);
saveTasks(tasks);
// Update memory
const memory = loadMemory();
memory.recentTasks.unshift(task.id);
if (memory.recentTasks.length > 100) {
memory.recentTasks = memory.recentTasks.slice(0, 100);
}
saveMemory(memory);
console.log(`✅ Created task: ${task.id} - ${title}`);
return task;
}
// Route task to appropriate agent
function routeTask(task: Task): string | null {
const keywords = (task.title + ' ' + task.description).toLowerCase();
for (const [agentName, agent] of Object.entries(AGENT_REGISTRY)) {
for (const capability of agent.capabilities) {
if (keywords.includes(capability.toLowerCase())) {
return agentName;
}
}
}
// Default routing logic
if (keywords.includes('urgent') || keywords.includes('critical')) {
return 'needs-attention';
}
if (keywords.includes('customer') || keywords.includes('support')) {
return 'zendesk-chat';
}
if (keywords.includes('order') || keywords.includes('sample')) {
return 'digital-samples';
}
if (keywords.includes('server') || keywords.includes('error') || keywords.includes('down')) {
return 'server-uptime';
}
return null;
}
// Assign task to agent
function assignTask(taskId: string, agentName: string): boolean {
const tasks = loadTasks();
const task = tasks.find(t => t.id === taskId);
if (!task) return false;
task.assignedAgent = agentName;
task.status = 'assigned';
task.assignedAt = new Date().toISOString();
task.actionLog.push({
timestamp: new Date().toISOString(),
action: 'assigned',
agent: agentName,
details: `Assigned to ${agentName} agent`,
success: true
});
saveTasks(tasks);
console.log(`📋 Assigned task ${taskId} to ${agentName}`);
return true;
}
// Update task status
function updateTaskStatus(taskId: string, status: TaskStatus, details?: string): boolean {
const tasks = loadTasks();
const task = tasks.find(t => t.id === taskId);
if (!task) return false;
task.status = status;
if (status === 'in_progress' && !task.startedAt) {
task.startedAt = new Date().toISOString();
} else if (status === 'completed' && !task.completedAt) {
task.completedAt = new Date().toISOString();
// Calculate actual duration
if (task.startedAt) {
const start = new Date(task.startedAt).getTime();
const end = new Date(task.completedAt).getTime();
task.actualDuration = Math.round((end - start) / 60000); // minutes
}
// Update agent performance in memory
if (task.assignedAgent) {
const memory = loadMemory();
if (!memory.agentPerformance[task.assignedAgent]) {
memory.agentPerformance[task.assignedAgent] = {
tasksCompleted: 0,
averageDuration: 0,
successRate: 0
};
}
memory.agentPerformance[task.assignedAgent].tasksCompleted++;
saveMemory(memory);
}
} else if (status === 'blocked') {
task.blockedAt = new Date().toISOString();
}
task.actionLog.push({
timestamp: new Date().toISOString(),
action: 'status_change',
details: details || `Status changed to ${status}`,
success: true
});
saveTasks(tasks);
console.log(`📝 Updated task ${task.id} status to ${status}`);
return true;
}
// Auto-detect and create tasks from system events
async function autoDetectTasks() {
console.log('🔍 Auto-detecting tasks from system events...');
// 1. Sync TodoWrite tasks
try {
const { execSync } = require('child_process');
const syncScript = require('path').join(__dirname, 'sync-todos-to-orchestrator.js');
execSync(`node "${syncScript}"`, { cwd: __dirname, stdio: 'pipe' });
console.log('✅ TodoWrite sync completed');
} catch (error: any) {
if (error.message && !error.message.includes('No changes')) {
console.error('❌ TodoWrite sync error:', error.message);
}
}
// 2. Monitor for new Shopify orders (last 5 minutes)
try {
const https = require('https');
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
const options = {
hostname: 'designer-laboratory-sandbox.myshopify.com',
path: `/admin/api/2024-01/orders.json?status=any&created_at_min=${fiveMinutesAgo}&limit=10`,
headers: { 'X-Shopify-Access-Token': 'shpat_REDACTED' }
};
https.get(options, (res: any) => {
let data = '';
res.on('data', (chunk: any) => data += chunk);
res.on('end', () => {
try {
const { orders } = JSON.parse(data);
if (orders && orders.length > 0) {
orders.forEach((order: any) => {
const tasks = loadTasks();
const exists = tasks.tasks.some((t: any) =>
t.metadata?.orderId === order.id.toString()
);
if (!exists) {
const newTask = {
id: generateTaskId(),
title: `New Order: ${order.name} - ${order.customer?.first_name || 'Customer'}`,
description: `Order from ${order.customer?.email || 'unknown'} - ${order.line_items?.length || 0} items - $${order.total_price}`,
status: 'pending',
priority: parseFloat(order.total_price) > 500 ? 'high' : 'medium',
createdAt: order.created_at,
assignedAgent: 'purchasing-office',
metadata: {
orderId: order.id.toString(),
orderName: order.name,
source: 'Shopify',
autoDetected: true
},
tags: ['order', 'shopify', 'auto-detected'],
actionLog: [{
timestamp: new Date().toISOString(),
action: 'created',
agent: 'task-orchestrator',
details: 'Auto-detected from Shopify',
success: true
}]
};
tasks.tasks.push(newTask);
saveTasks(tasks);
console.log(`📦 New order detected: ${order.name}`);
}
});
}
} catch (e) { /* Silent fail */ }
});
}).on('error', () => { /* Silent fail */ });
} catch (error) { /* Silent fail */ }
// 3. Monitor for new client signups (check agent status)
try {
const http = require('http');
const req = http.get('http://localhost:9885/api/stats', (res: any) => {
let data = '';
res.on('data', (chunk: any) => data += chunk);
res.on('end', () => {
try {
const stats = JSON.parse(data);
if (stats.pendingSignups && stats.pendingSignups > 0) {
const tasks = loadTasks();
const exists = tasks.tasks.some((t: any) =>
t.title.includes('New Client Signup') && t.status === 'pending'
);
if (!exists) {
const newTask = {
id: generateTaskId(),
title: `New Client Signup - ${stats.pendingSignups} pending`,
description: 'Review and approve new client registrations',
status: 'pending',
priority: 'high',
createdAt: new Date().toISOString(),
assignedAgent: 'marketing',
metadata: { source: 'ClientSignup', autoDetected: true },
tags: ['client', 'signup', 'auto-detected'],
actionLog: [{
timestamp: new Date().toISOString(),
action: 'created',
agent: 'task-orchestrator',
details: 'Auto-detected pending signups',
success: true
}]
};
tasks.tasks.push(newTask);
saveTasks(tasks);
console.log(`👤 New client signup detected`);
}
}
} catch (e) { /* Silent fail */ }
});
});
req.on('error', () => { /* Silent fail */ });
} catch (error) { /* Silent fail */ }
// 4. Monitor PM2 processes for failures
try {
const { execSync } = require('child_process');
const pm2List = execSync('pm2 jlist', { encoding: 'utf8' });
const processes = JSON.parse(pm2List);
processes.forEach((proc: any) => {
if (proc.pm2_env?.status === 'errored' || proc.pm2_env?.restart_time > 5) {
const tasks = loadTasks();
const exists = tasks.tasks.some((t: any) =>
t.metadata?.processName === proc.name &&
t.status !== 'completed' &&
t.createdAt > new Date(Date.now() - 10 * 60 * 1000).toISOString()
);
if (!exists) {
const newTask = {
id: generateTaskId(),
title: `Process Issue: ${proc.name}`,
description: `Process ${proc.name} has ${proc.pm2_env.restart_time} restarts. Status: ${proc.pm2_env.status}`,
status: 'pending',
priority: 'critical',
createdAt: new Date().toISOString(),
assignedAgent: 'server-uptime',
metadata: {
processName: proc.name,
pid: proc.pid,
restarts: proc.pm2_env.restart_time,
source: 'PM2',
autoDetected: true
},
tags: ['server', 'pm2', 'auto-detected', 'critical'],
actionLog: [{
timestamp: new Date().toISOString(),
action: 'created',
agent: 'task-orchestrator',
details: 'Auto-detected from PM2 monitoring',
success: true
}]
};
tasks.tasks.push(newTask);
saveTasks(tasks);
console.log(`🚨 Process issue detected: ${proc.name}`);
}
}
});
} catch (error) { /* Silent fail */ }
}
// SSO Authentication
app.get('/login', handleLogin);
app.get('/sso/callback', (req, res) => {
setSSOToken();
res.redirect('/');
});
// Dashboard
app.get('/', ssoAuth, (req: Request, res: Response) => {
const tasks = loadTasks();
const memory = loadMemory();
const tasksByStatus = {
pending: tasks.filter(t => t.status === 'pending').length,
assigned: tasks.filter(t => t.status === 'assigned').length,
in_progress: tasks.filter(t => t.status === 'in_progress').length,
blocked: tasks.filter(t => t.status === 'blocked').length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length
};
// Sort tasks: in_progress first, then blocked, then pending, then completed
const statusPriority = {
'in_progress': 1,
'blocked': 2,
'pending': 3,
'assigned': 4,
'completed': 5,
'failed': 6
};
// Group similar tasks (like multiple log errors from same file)
const groupedTasks: any[] = [];
const taskGroups = new Map<string, any[]>();
tasks.forEach(task => {
// Create a group key for similar tasks
const titleBase = task.title.replace(/\[HIGH\]|\[MEDIUM\]|\[LOW\]/g, '').trim();
const groupKey = `${titleBase}-${task.assignedAgent}-${task.status}`;
if (!taskGroups.has(groupKey)) {
taskGroups.set(groupKey, []);
}
taskGroups.get(groupKey)!.push(task);
});
// Convert groups to display format
taskGroups.forEach((groupTasks, key) => {
if (groupTasks.length > 1) {
// Multiple similar tasks - create a group
groupedTasks.push({
isGroup: true,
title: groupTasks[0].title.replace(/\[HIGH\]|\[MEDIUM\]|\[LOW\]/g, '').trim(),
count: groupTasks.length,
tasks: groupTasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
status: groupTasks[0].status,
priority: groupTasks[0].priority,
assignedAgent: groupTasks[0].assignedAgent,
createdAt: groupTasks[0].createdAt
});
} else {
// Single task
groupedTasks.push(groupTasks[0]);
}
});
// Sort grouped tasks
const recentTasks = groupedTasks
.sort((a, b) => {
const statusA = a.isGroup ? a.status : a.status;
const statusB = b.isGroup ? b.status : b.status;
const priorityDiff = (statusPriority[statusA] || 99) - (statusPriority[statusB] || 99);
if (priorityDiff !== 0) return priorityDiff;
const dateA = a.isGroup ? a.createdAt : a.createdAt;
const dateB = b.isGroup ? b.createdAt : b.createdAt;
return new Date(dateB).getTime() - new Date(dateA).getTime();
});
const taskListHtml = recentTasks.length > 0 ? recentTasks.map(taskOrGroup => {
// Helper function to render a single task
const renderSingleTask = (task: any, isSubTask = false) => {
const createdDate = new Date(task.createdAt).toLocaleString();
const startedDate = task.startedAt ? new Date(task.startedAt).toLocaleString() : '-';
const completedDate = task.completedAt ? new Date(task.completedAt).toLocaleString() : '-';
// Calculate duration
let duration = '-';
if (task.status === 'completed' && task.actualDuration) {
duration = `${task.actualDuration} min`;
} else if (task.startedAt) {
const start = new Date(task.startedAt).getTime();
const now = Date.now();
const minutes = Math.round((now - start) / 60000);
duration = `${minutes} min (ongoing)`;
}
// Progress indicator for in-progress tasks
let progressHtml = '';
if (task.status === 'in_progress') {
// Prioritize work progress (e.g., "44/2000 products") over action log progress
if (task.metadata && task.metadata.workProgress) {
const { current, total } = task.metadata.workProgress;
const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
progressHtml = `
<div style="margin: 10px 0; padding: 12px; background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); border-left: 4px solid #667eea; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-weight: 700; color: #667eea; font-size: 16px;">⚙️ Work Progress: ${current.toLocaleString()}/${total.toLocaleString()}</span>
<span style="font-weight: 700; color: #667eea; font-size: 18px;">${percentage}%</span>
</div>
<div style="background: #e0e0e0; height: 12px; border-radius: 6px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #667eea, #764ba2); height: 100%; width: ${percentage}%; transition: width 0.3s ease;"></div>
</div>
</div>
`;
} else if (task.metadata && task.metadata.progress) {
const { completed, total } = task.metadata.progress;
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
progressHtml = `
<div style="margin: 10px 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<span style="font-weight: 600; color: #667eea;">📊 Progress: ${completed}/${total} steps</span>
<span style="font-weight: 600; color: #667eea;">${percentage}%</span>
</div>
<div style="background: #e0e0e0; height: 8px; border-radius: 4px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #667eea, #764ba2); height: 100%; width: ${percentage}%; transition: width 0.3s ease;"></div>
</div>
</div>
`;
}
}
// Needs intervention alert for blocked tasks
const interventionBanner = task.status === 'blocked' ? `
<div style="background: #ff6b6b; color: white; padding: 8px 12px; border-radius: 6px; font-weight: 700; display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
<span style="font-size: 18px;">⚠️</span>
<span style="font-size: 13px;">NEEDS INTERVENTION</span>
${task.blockReason ? `<span style="font-size: 12px; font-weight: 400; opacity: 0.9;">- ${task.blockReason}</span>` : ''}
</div>
` : '';
// Get agent display name and suggestion
let agentDisplay = task.assignedAgent || 'unassigned';
const agentIcon = task.assignedAgent ? '🤖' : '⏳';
// Suggest agent if unassigned
let suggestedAgent = '';
if (!task.assignedAgent) {
const keywords = (task.title + ' ' + task.description).toLowerCase();
// Check capabilities
for (const [agentName, agent] of Object.entries(AGENT_REGISTRY)) {
for (const capability of agent.capabilities) {
if (keywords.includes(capability.toLowerCase())) {
suggestedAgent = agentName;
agentDisplay = `unassigned <span style="color: #28a745; font-size: 11px;">(💡 suggest: ${agentName})</span>`;
break;
}
}
if (suggestedAgent) break;
}
}
// Agent dropdown options
const agentOptions = Object.keys(AGENT_REGISTRY).map(agentName => {
const selected = task.assignedAgent === agentName ? 'selected' : '';
return `<option value="${agentName}" ${selected}>${agentName}</option>`;
}).join('');
// Action buttons - ALWAYS show STOP, RESTART, IN PROGRESS for ALL tasks
const actionButtons = `
<span class="task-actions">
<select class="agent-dropdown" onchange="assignTaskToAgent('${task.id}', this.value)" style="padding: 4px 8px; font-size: 11px; border: 2px solid #667eea; border-radius: 6px; cursor: pointer; font-weight: 600; background: white; color: #667eea;">
<option value="">-- Reassign --</option>
${agentOptions}
</select>
<button class="action-btn ${task.status === 'in_progress' ? 'start-btn' : 'restart-btn'}" onclick="updateStatus('${task.id}', 'in_progress')" title="Set to In Progress">
${task.status === 'in_progress' ? '▶️ In Progress' : '▶️ Start'}
</button>
<button class="action-btn stop-btn" onclick="stopTask('${task.id}')" title="Stop (set to Pending)">⏸️ Stop</button>
<button class="action-btn restart-btn" onclick="restartTask('${task.id}')" title="Restart (set to In Progress)">🔄 Restart</button>
${task.status === 'in_progress' ? `<button class="action-btn complete-btn" onclick="updateStatus('${task.id}', 'completed')">✅ Complete</button>` : ''}
${task.status !== 'completed' ? `<button class="action-btn block-btn" onclick="blockTask('${task.id}')">⚠️ Block</button>` : ''}
</span>
`;
// Agent link and live metrics
let agentLink = '';
let agentPort = task.metadata?.agentPort || (task.assignedAgent && AGENT_REGISTRY[task.assignedAgent] ? AGENT_REGISTRY[task.assignedAgent].port : null);
if (agentPort) {
agentLink = `<div style="margin: 10px 0;"><a href="http://45.61.58.125:${agentPort}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 600; background: rgba(102, 126, 234, 0.1); padding: 8px 12px; border-radius: 6px; display: inline-block;">🔗 Agent Dashboard: http://45.61.58.125:${agentPort}</a></div>`;
}
// Live metrics container (will be populated via JavaScript)
let liveMetricsHtml = '';
if (task.status === 'in_progress' && task.assignedAgent) {
liveMetricsHtml = `
<div id="live-metrics-${task.id}" class="live-metrics" style="margin: 10px 0; padding: 12px; background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(40, 167, 69, 0.1)); border-left: 4px solid #00d4ff; border-radius: 8px;">
<div style="font-weight: 700; color: #00d4ff; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
<span>📊 Live Metrics</span>
<span style="font-size: 12px; color: #999;">Updating...</span>
</div>
<div class="metrics-content" style="font-size: 14px; color: #666;">
Loading live data...
</div>
</div>
`;
}
const taskStyle = isSubTask ? 'margin-left: 20px; margin-top: 10px; border-left: 3px solid #667eea; padding-left: 15px;' : '';
return `
<div class="task-item ${isSubTask ? 'sub-task' : ''}" data-status="${task.status}" data-task-id="${task.id}" style="${taskStyle}">
${interventionBanner}
<div class="task-header">
<div class="task-title" title="${task.title}">${task.title}</div>
<div class="task-status status-${task.status}">${task.status.replace('_', ' ')}</div>
</div>
<div class="task-description">${task.description}</div>
${progressHtml}
${liveMetricsHtml}
${agentLink}
${task.metadata && task.metadata.url ? `<div style="margin: 10px 0;"><a href="${task.metadata.url}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 600;">🔗 ${task.metadata.url}</a></div>` : ''}
<div class="task-meta">
<span class="task-agent">${agentIcon} ${agentDisplay}</span>
<span class="task-priority">⚡ ${task.priority}</span>
<span class="task-created">📅 ${createdDate}</span>
<span class="task-started">▶️ ${startedDate}</span>
<span class="task-completed">✅ ${completedDate}</span>
<span class="task-duration">⏱️ ${duration}</span>
${actionButtons}
</div>
</div>
`;
};
// Check if this is a grouped task or a single task
if (taskOrGroup.isGroup) {
// Render grouped tasks with collapsible accordion
const groupId = `group-${taskOrGroup.title.replace(/[^a-zA-Z0-9]/g, '-')}-${taskOrGroup.status}`;
const firstTask = taskOrGroup.tasks[0];
const createdDate = new Date(taskOrGroup.createdAt).toLocaleString();
// Count badge styling based on count
const countColor = taskOrGroup.count >= 5 ? '#ff6b6b' : taskOrGroup.count >= 3 ? '#ff9800' : '#667eea';
return `
<div class="task-group" style="background: white; border-radius: 15px; padding: 20px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.07); border-left: 5px solid ${countColor};">
<div class="group-header" style="cursor: pointer; display: flex; align-items: center; justify-content: space-between;" onclick="toggleGroup('${groupId}')">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
<span style="background: ${countColor}; color: white; padding: 4px 12px; border-radius: 20px; font-weight: 700; font-size: 14px;">${taskOrGroup.count} similar tasks</span>
<div class="task-status status-${taskOrGroup.status}" style="display: inline-block;">${taskOrGroup.status.replace('_', ' ')}</div>
<span style="color: #999; font-size: 13px;">📅 ${createdDate}</span>
</div>
<div style="font-size: 18px; font-weight: 600; color: #333; margin-bottom: 5px;">${taskOrGroup.title}</div>
<div style="color: #666; font-size: 13px;">${firstTask.description.substring(0, 150)}${firstTask.description.length > 150 ? '...' : ''}</div>
</div>
<div style="font-size: 24px; color: #667eea; transition: transform 0.3s;" id="${groupId}-icon">▼</div>
</div>
<div id="${groupId}" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 2px dashed #e0e0e0;">
${taskOrGroup.tasks.map((t: any) => renderSingleTask(t, true)).join('')}
</div>
</div>
`;
} else {
// Render single task normally
return renderSingleTask(taskOrGroup);
}
}).join('') : '<div style="padding: 20px; text-align: center; color: #999;">No tasks yet. Create your first task below!</div>';
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Task Orchestrator - DW Agents</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container { max-width: 1400px; margin: 0 auto; }
.header {
background: white;
padding: 30px;
border-radius: 20px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 42px;
color: #1a1a1a;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 15px;
}
.header p { color: #666; font-size: 18px; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.stat-card h3 {
font-size: 14px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.stat-card .number {
font-size: 36px;
font-weight: 700;
color: #667eea;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.panel {
background: white;
padding: 30px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.panel h2 {
font-size: 24px;
margin-bottom: 20px;
color: #1a1a1a;
display: flex;
align-items: center;
gap: 10px;
}
.task-list { max-height: 600px; overflow-y: auto; }
.task-item {
padding: 15px;
border: 2px solid #f0f0f0;
border-radius: 10px;
margin-bottom: 15px;
transition: all 0.3s;
}
.task-item:hover {
border-color: #667eea;
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.1);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 10px;
}
.task-title {
font-weight: 600;
font-size: 16px;
color: #1a1a1a;
}
.task-status {
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-pending { background: #fff3cd; color: #856404; }
.status-assigned { background: #cce5ff; color: #004085; }
.status-in_progress { background: #d1ecf1; color: #0c5460; }
.status-blocked { background: #f8d7da; color: #721c24; }
.status-completed { background: #d4edda; color: #155724; }
.task-meta {
display: flex;
gap: 15px;
font-size: 13px;
color: #888;
margin-top: 8px;
flex-wrap: wrap;
}
/* List view styles - spreadsheet layout */
.view-toggle {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.view-btn {
padding: 8px 16px;
background: #f0f0f0;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #666;
transition: all 0.3s;
}
.view-btn:hover {
background: #e0e0e0;
}
.view-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* Spreadsheet-style table for list view */
.task-list.list-view {
display: table;
width: 100%;
border-collapse: collapse;
border: 1px solid #e0e0e0;
}
.task-list.list-view .task-item {
display: table-row;
padding: 0;
margin: 0;
border: none;
border-radius: 0;
}
.task-list.list-view .task-item:hover {
background: #f0f7ff;
}
.task-list.list-view .task-item > div {
display: table-cell;
padding: 10px 8px;
vertical-align: middle;
font-size: 12px;
border-bottom: 1px solid #e0e0e0;
}
/* Hide description in list view */
.task-list.list-view .task-description {
display: none;
}
.task-list.list-view .task-header {
display: contents;
}
.task-list.list-view .task-title {
font-size: 13px;
font-weight: 600;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-list.list-view .task-status {
padding: 4px 8px;
border-radius: 10px;
font-size: 10px;
text-align: center;
white-space: nowrap;
}
.task-list.list-view .task-meta {
display: contents;
}
.task-list.list-view .task-meta span {
display: table-cell;
padding: 10px 8px;
font-size: 11px;
white-space: nowrap;
border-bottom: 1px solid #e0e0e0;
}
/* Blocked row highlight */
.task-list.list-view .task-item[data-status="blocked"] {
background: #fff5f5;
}
.task-list.list-view .task-item[data-status="blocked"]:hover {
background: #ffe5e5;
}
/* Agent column styling */
.task-agent {
font-weight: 600;
color: #667eea !important;
background: #f0f4ff;
padding: 6px 10px !important;
border-radius: 6px;
}
.task-list.list-view .task-agent {
background: transparent;
font-weight: 600;
color: #667eea !important;
}
/* Action buttons */
.task-actions {
display: inline-flex;
gap: 5px;
margin-left: 10px;
align-items: center;
}
.agent-dropdown {
padding: 4px 8px;
font-size: 11px;
border: 2px solid #667eea;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
background: white;
color: #667eea;
transition: all 0.2s;
}
.agent-dropdown:hover {
background: #f0f4ff;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
}
.agent-dropdown:focus {
outline: none;
border-color: #5568d3;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.action-btn {
padding: 4px 10px;
font-size: 11px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.assign-btn {
background: #667eea;
color: white;
}
.start-btn {
background: #00d4ff;
color: white;
}
.stop-btn {
background: #ff9800;
color: white;
}
.restart-btn {
background: #9c27b0;
color: white;
}
.complete-btn {
background: #28a745;
color: white;
}
.block-btn {
background: #ff6b6b;
color: white;
}
.task-list.list-view .task-actions {
display: table-cell;
padding: 10px 8px;
border-bottom: 1px solid #e0e0e0;
}
.btn {
display: inline-block;
padding: 12px 24px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
border: none;
cursor: pointer;
width: 100%;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.create-task-form {
display: grid;
gap: 15px;
margin-top: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
.form-group textarea { resize: vertical; min-height: 100px; }
@media (max-width: 968px) {
.main-content { grid-template-columns: 1fr; }
}
</style>
</head>
<body style="padding: 20px;">
${getUniversalHeader('Task Orchestrator', PORT, 5)}
<div class="container" style="margin-top: 20px;">
<div class="header">
<h1>🎯 Task Orchestrator</h1>
<p>Master task management & auto-routing system for all DW agents</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<h3>Pending</h3>
<div class="number">${tasksByStatus.pending}</div>
</div>
<div class="stat-card">
<h3>In Progress</h3>
<div class="number">${tasksByStatus.in_progress}</div>
</div>
<div class="stat-card">
<h3>Completed</h3>
<div class="number">${tasksByStatus.completed}</div>
</div>
<div class="stat-card">
<h3>Blocked</h3>
<div class="number">${tasksByStatus.blocked}</div>
</div>
<div class="stat-card">
<h3>Total Tasks</h3>
<div class="number">${tasks.length}</div>
</div>
<div class="stat-card">
<h3>Active Agents</h3>
<div class="number">${Object.keys(AGENT_REGISTRY).length}</div>
</div>
</div>
<div class="main-content">
<div class="panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">📋 Recent Tasks</h2>
<button class="btn" onclick="togglePanel()" style="width: auto; padding: 10px 20px;" id="toggleBtn">➕ Create Task</button>
</div>
<div id="taskListPanel">
<div class="view-toggle">
<button class="view-btn" onclick="setView('card')">🎴 Card View</button>
<button class="view-btn active" onclick="setView('list')">📋 List View</button>
</div>
<div class="task-list list-view" id="taskList">
${taskListHtml}
</div>
</div>
<div id="createFormPanel" style="display: none;">
<h3 style="font-size: 20px; margin-bottom: 20px; color: #333;">➕ Create New Task</h3>
<form class="create-task-form" action="/api/tasks" method="POST">
<div class="form-group">
<label>Task Title</label>
<input type="text" name="title" placeholder="Brief description of the task" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" placeholder="Detailed task description" required></textarea>
</div>
<div class="form-group">
<label>Priority</label>
<select name="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<button type="submit" class="btn">Create Task</button>
</form>
</div>
</div>
</div>
<div class="panel" style="margin-top: 20px;">
<h2>📊 System Memory</h2>
<div style="background: #f8f9fa; padding: 20px; border-radius: 10px; font-size: 14px; color: #555;">
<p style="margin-bottom: 10px;"><strong>Recent Tasks Tracked:</strong> ${memory.recentTasks.length}</p>
<p style="margin-bottom: 10px;"><strong>Last Updated:</strong> ${new Date(memory.lastUpdated).toLocaleString()}</p>
<p><strong>Agents Monitored:</strong> ${Object.keys(memory.agentPerformance).length}</p>
</div>
</div>
</div>
<script>
let showingForm = false;
function togglePanel() {
const taskListPanel = document.getElementById('taskListPanel');
const createFormPanel = document.getElementById('createFormPanel');
const toggleBtn = document.getElementById('toggleBtn');
showingForm = !showingForm;
if (showingForm) {
taskListPanel.style.display = 'none';
createFormPanel.style.display = 'block';
toggleBtn.innerHTML = '📋 View Tasks';
} else {
taskListPanel.style.display = 'block';
createFormPanel.style.display = 'none';
toggleBtn.innerHTML = '➕ Create Task';
}
}
function setView(viewType) {
const taskList = document.getElementById('taskList');
const buttons = document.querySelectorAll('.view-btn');
buttons.forEach(btn => btn.classList.remove('active'));
if (viewType === 'list') {
taskList.classList.add('list-view');
buttons[1].classList.add('active');
} else {
taskList.classList.remove('list-view');
buttons[0].classList.add('active');
}
// Save preference to localStorage
localStorage.setItem('taskViewPreference', viewType);
}
// Restore saved view preference on load
window.addEventListener('DOMContentLoaded', () => {
const savedView = localStorage.getItem('taskViewPreference') || 'list';
setView(savedView);
// Start live metrics polling for in_progress tasks
startLiveMetricsPolling();
});
// Live metrics polling for in_progress tasks
async function startLiveMetricsPolling() {
const liveMetricElements = document.querySelectorAll('.live-metrics');
if (liveMetricElements.length === 0) return;
// Poll every 5 seconds
setInterval(async () => {
for (const element of liveMetricElements) {
const taskId = element.id.replace('live-metrics-', '');
await updateLiveMetrics(taskId);
}
}, 5000);
// Initial update
for (const element of liveMetricElements) {
const taskId = element.id.replace('live-metrics-', '');
await updateLiveMetrics(taskId);
}
}
async function updateLiveMetrics(taskId) {
try {
const response = await fetch(\`/api/agent-metrics/\${taskId}\`);
const data = await response.json();
const metricsElement = document.getElementById(\`live-metrics-\${taskId}\`);
if (!metricsElement) return;
const contentDiv = metricsElement.querySelector('.metrics-content');
if (!contentDiv) return;
// Update timestamp
const timestampSpan = metricsElement.querySelector('span:last-child');
if (timestampSpan) {
const now = new Date();
timestampSpan.textContent = \`Updated \${now.toLocaleTimeString()}\`;
timestampSpan.style.color = '#28a745';
}
// Parse and display metrics
if (data.error) {
contentDiv.innerHTML = \`<span style="color: #999;">⚠️ \${data.error}</span>\`;
} else if (data.metrics && data.metrics.job) {
const job = data.metrics.job;
const percentage = job.progress.total > 0
? Math.round((job.progress.processed / job.progress.total) * 100)
: 0;
let statusColor = '#00d4ff';
if (job.status === 'completed') statusColor = '#28a745';
if (job.status === 'failed') statusColor = '#ff6b6b';
// Extract current SKU from logs
let currentSku = 'Unknown';
if (job.logs && job.logs.length > 0) {
const lastLog = job.logs[job.logs.length - 1];
const skuMatch = lastLog.match(/DWJS-\\d+/);
if (skuMatch) currentSku = skuMatch[0];
}
// Get latest logs (last 10)
const latestLogs = job.logs && job.logs.length > 0
? job.logs.slice(-10).map(log => {
let color = '#f0f0f0';
let bgColor = 'transparent';
if (log.includes('✅') || log.includes('Successfully')) {
color = '#4ade80';
bgColor = 'rgba(74, 222, 128, 0.1)';
}
if (log.includes('❌') || log.includes('Failed')) {
color = '#ff6b6b';
bgColor = 'rgba(255, 107, 107, 0.1)';
}
if (log.includes('📄 Page')) {
color = '#60a5fa';
bgColor = 'rgba(96, 165, 250, 0.1)';
}
return \`<div style="font-size: 11px; padding: 4px 6px; margin: 2px 0; color: \${color}; background: \${bgColor}; border-radius: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\${log}</div>\`;
}).join('')
: '<div style="color: #999; font-size: 11px;">No logs yet</div>';
contentDiv.innerHTML = \`
<div style="display: grid; gap: 8px;">
<div style="display: flex; justify-content: space-between;">
<span style="font-weight: 600;">Status:</span>
<span style="color: \${statusColor}; font-weight: 700;">\${job.status.toUpperCase()}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="font-weight: 600;">Processed:</span>
<span style="font-weight: 700; color: #667eea;">\${job.progress.processed.toLocaleString()}\${job.progress.total > 0 ? ' / ' + job.progress.total.toLocaleString() : ''}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="font-weight: 600;">Success Rate:</span>
<span style="font-weight: 700; color: #28a745;">\${job.progress.successful.toLocaleString()} ✓</span>
</div>
\${job.progress.failed > 0 ? \`
<div style="display: flex; justify-content: space-between;">
<span style="font-weight: 600;">Failed:</span>
<span style="font-weight: 700; color: #ff6b6b;">\${job.progress.failed} ✗</span>
</div>
\` : ''}
<div style="display: flex; justify-content: space-between;">
<span style="font-weight: 600;">Current SKU:</span>
<span style="font-weight: 700; color: #764ba2;">\${currentSku}</span>
</div>
\${job.progress.total > 0 ? \`
<div style="margin-top: 8px;">
<div style="background: #e0e0e0; height: 6px; border-radius: 3px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #00d4ff, #28a745); height: 100%; width: \${percentage}%; transition: width 0.3s ease;"></div>
</div>
<div style="text-align: center; margin-top: 4px; font-size: 12px; color: #999;">\${percentage}% Complete</div>
</div>
\` : ''}
<div style="margin-top: 12px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px;">
<div style="font-weight: 600; margin-bottom: 6px; color: #ffffff; font-size: 12px;">📋 Live Logs:</div>
<div style="max-height: 150px; overflow-y: auto; background: #1a1a2e; padding: 8px; border-radius: 6px; font-family: 'Courier New', monospace; border: 1px solid rgba(255,255,255,0.1);">
\${latestLogs}
</div>
</div>
</div>
\`;
} else {
contentDiv.innerHTML = '<span style="color: #999;">No metrics available</span>';
}
} catch (error) {
console.error('Failed to update live metrics:', error);
}
}
// Auto-refresh every 30 seconds (increased from 15 to avoid interrupting live updates)
setInterval(() => {
// Only refresh if not showing the form
if (!showingForm) {
location.reload();
}
}, 30000);
// Task action functions
async function assignTaskToAgent(taskId, agentName) {
if (!agentName) return; // Don't do anything if empty option selected
try {
const response = await fetch(\`/api/tasks/\${taskId}/assign\`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent: agentName })
});
if (response.ok) {
// Update the task item to show the Start button
const taskItem = document.querySelector(\`[data-task-id="\${taskId}"]\`);
if (taskItem) {
const actionsSpan = taskItem.querySelector('.task-actions');
if (actionsSpan) {
// Add Start button if it doesn't exist
if (!actionsSpan.querySelector('.start-btn')) {
const startBtn = document.createElement('button');
startBtn.className = 'action-btn start-btn';
startBtn.innerHTML = '▶️ Start';
startBtn.onclick = () => updateStatus(taskId, 'in_progress');
actionsSpan.appendChild(startBtn);
}
// Update agent display in task-meta
const agentSpan = taskItem.querySelector('.task-agent');
if (agentSpan) {
agentSpan.innerHTML = \`🤖 \${agentName}\`;
}
// Update task status badge
const statusBadge = taskItem.querySelector('.task-status');
if (statusBadge) {
statusBadge.className = 'task-status status-assigned';
statusBadge.textContent = 'assigned';
}
}
}
}
} catch (error) {
alert('Failed to assign task');
}
}
async function updateStatus(taskId, status) {
try {
const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status })
});
if (response.ok) {
location.reload();
}
} catch (error) {
alert('Failed to update status');
}
}
async function blockTask(taskId) {
const reason = prompt('Why is this task blocked?');
if (reason) {
try {
const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'blocked', details: reason })
});
if (response.ok) {
location.reload();
}
} catch (error) {
alert('Failed to block task');
}
}
}
async function stopTask(taskId) {
if (confirm('Stop this task? You can restart it later.')) {
try {
const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'pending', details: 'Task stopped by user' })
});
if (response.ok) {
location.reload();
}
} catch (error) {
alert('Failed to stop task');
}
}
}
async function restartTask(taskId) {
if (confirm('Restart this task?')) {
try {
const response = await fetch(\`/api/tasks/\${taskId}/status\`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'in_progress', details: 'Task restarted by user' })
});
if (response.ok) {
location.reload();
}
} catch (error) {
alert('Failed to restart task');
}
}
}
function toggleGroup(groupId) {
const groupContent = document.getElementById(groupId);
const icon = document.getElementById(groupId + '-icon');
if (groupContent && icon) {
if (groupContent.style.display === 'none') {
groupContent.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
} else {
groupContent.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
}
}
</script>
</body>
</html>
`);
});
// API: Create task
app.post('/api/tasks', ssoAuth, (req: Request, res: Response) => {
const { title, description, priority } = req.body;
if (!title || !description) {
return res.status(400).json({ error: 'Title and description required' });
}
const task = createTask(title, description, priority as TaskPriority, false);
// Auto-route to appropriate agent
const agent = routeTask(task);
if (agent) {
assignTask(task.id, agent);
}
res.redirect('/');
});
// API: Get all tasks
app.get('/api/tasks', ssoAuth, (req: Request, res: Response) => {
const tasks = loadTasks();
res.json({ tasks });
});
// API: Get task by ID
app.get('/api/tasks/:id', ssoAuth, (req: Request, res: Response) => {
const tasks = loadTasks();
const task = tasks.find(t => t.id === req.params.id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ task });
});
// API: Update task status
app.patch('/api/tasks/:id/status', ssoAuth, (req: Request, res: Response) => {
const { status, details } = req.body;
if (!status) {
return res.status(400).json({ error: 'Status required' });
}
const success = updateTaskStatus(req.params.id, status as TaskStatus, details);
if (!success) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ success: true });
});
// API: Assign task to agent
app.post('/api/tasks/:id/assign', ssoAuth, (req: Request, res: Response) => {
const { agent } = req.body;
if (!agent) {
return res.status(400).json({ error: 'Agent name required' });
}
const success = assignTask(req.params.id, agent);
if (!success) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ success: true });
});
// API: Get memory/context
app.get('/api/memory', ssoAuth, (req: Request, res: Response) => {
const memory = loadMemory();
res.json({ memory });
});
// API: Get live agent metrics
app.get('/api/agent-metrics/:taskId', async (req: Request, res: Response) => {
const { taskId } = req.params;
const tasks = loadTasks();
const task = tasks.find(t => t.id === taskId);
if (!task) {
return res.json({ error: 'Task not found' });
}
// Check if task has an agent and port metadata
const agentName = task.assignedAgent;
let agentPort = task.metadata?.agentPort;
// If no port in metadata, look up in registry
if (!agentPort && agentName && AGENT_REGISTRY[agentName]) {
agentPort = AGENT_REGISTRY[agentName].port;
}
// Try to fetch live metrics from the agent
if (agentPort) {
try {
const http = require('http');
const response = await new Promise<any>((resolve, reject) => {
const req = http.get(`http://localhost:${agentPort}/api/status`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
resolve({ error: 'Invalid JSON response' });
}
});
});
req.on('error', () => resolve({ error: 'Agent not responding' }));
req.setTimeout(2000, () => {
req.destroy();
resolve({ error: 'Timeout' });
});
});
return res.json({
taskId,
agentName,
agentPort,
agentUrl: `http://45.61.58.125:${agentPort}`,
metrics: response,
timestamp: new Date().toISOString()
});
} catch (error) {
return res.json({
taskId,
agentName,
agentPort,
error: 'Failed to fetch metrics'
});
}
}
return res.json({
taskId,
agentName,
error: 'No agent port available'
});
});
// Webhook endpoint for other agents to submit tasks (no auth required)
app.post('/api/webhook/task', (req: Request, res: Response) => {
try {
const { title, description, priority, source, metadata } = req.body;
if (!title || !description) {
return res.status(400).json({ error: 'Title and description required' });
}
console.log(`📥 Webhook task received from: ${source || 'unknown'}`);
console.log(` Title: ${title}`);
const task = createTask(
title,
description,
priority as TaskPriority || 'medium',
true // auto-generated
);
// Add source metadata
if (source) {
task.metadata.source = source;
}
if (metadata) {
task.metadata = { ...task.metadata, ...metadata };
}
// Save the updated task
const tasks = loadTasks();
const index = tasks.findIndex(t => t.id === task.id);
if (index !== -1) {
tasks[index] = task;
saveTasks(tasks);
}
// Auto-route to appropriate agent
const agent = routeTask(task);
if (agent) {
assignTask(task.id, agent);
console.log(` ✓ Auto-assigned to: ${agent}`);
}
res.json({
success: true,
taskId: task.id,
assignedAgent: agent
});
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Health check
app.get('/health', (req: Request, res: Response) => {
res.json({
status: 'ok',
totalTasks: loadTasks().length,
timestamp: new Date().toISOString()
});
});
// Start server
// Metrics endpoint for daily reporting
app.get("/api/metrics", (req: Request, res: Response) => {
res.json({
status: "online",
uptime: process.uptime(),
responseTime: 0,
tasksCompleted: 0,
errorsToday: 0,
lastActivity: new Date().toISOString(),
qnaReadiness: 100
});
});
app.listen(PORT, () => {
initializeDatabases();
console.log(`
╔════════════════════════════════════════════════════════════╗
║ 🎯 Task Orchestrator Agent ║
╠════════════════════════════════════════════════════════════╣
║ Status: RUNNING ║
║ Port: ${PORT} ║
║ Dashboard: http://45.61.58.125:${PORT}/ ║
║ ║
║ Features: ║
║ ✓ Auto task tracking ║
║ ✓ Smart agent routing ║
║ ✓ Full audit trail ║
║ ✓ Memory system ║
║ ✓ Self-healing detection ║
╚════════════════════════════════════════════════════════════╝
`);
// Start auto-detection
setInterval(autoDetectTasks, 60000); // Every minute
});