← back to Designer Wallcoverings

DW-Agents/dw-agents/agent-control-panel.ts

372 lines

/**
 * Agent Control Panel - Port 7200
 * Centralized start/stop/restart control for all DW agents
 */
import express from 'express';
import session from 'express-session';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';

import { fileURLToPath } from 'url';
import { dirname } from 'path';
import VendorTestWebhook from './vendor-test-webhook-integration.ts';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const execAsync = promisify(exec);
const app = express();
const PORT = 7200;
const AUTH_USERNAME = 'admin';
const AUTH_PASSWORD = '*exec2025*';

// Load agent registry
let agentRegistry: any = {};
(async () => {
  const registryData = await fs.readFile(path.join(__dirname, 'agent-registry.json'), 'utf-8');
  agentRegistry = JSON.parse(registryData);
})();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

declare module 'express-session' {
  interface SessionData { authenticated: boolean; }
}

app.use(session({
  secret: 'dw-agent-control-2025',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false, httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 }
}));

const requireAuth = (req: any, res: any, next: any) => {
  if (req.session.authenticated) return next();
  res.redirect('/login');
};

app.get('/login', (req, res) => {
  res.send(`<!DOCTYPE html><html><head><title>Agent Control Panel - Login</title><style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'DejaVu Sans, Segoe UI', sans-serif; background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.login-container { background: white; padding: 50px; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); max-width: 450px; width: 100%; }
.logo { text-align: center; font-size: 3em; margin-bottom: 10px; }
h1 { color: #2c3e50; text-align: center; margin-bottom: 10px; font-size: 1.8em; }
.subtitle { text-align: center; color: #666; margin-bottom: 30px; font-size: 1.1em; }
input { width: 100%; padding: 15px; margin: 10px 0; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 1em; }
input:focus { border-color: #2c3e50; outline: none; }
button { width: 100%; background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); color: white; border: none; padding: 15px; border-radius: 10px; cursor: pointer; font-weight: 600; font-size: 1.1em; margin-top: 10px; }
</style></head><body>
<div class="login-container">
<div class="logo">⚙️</div>
<h1>Agent Control Panel</h1>
<div class="subtitle">System Control Center</div>
${req.query.error ? '<div style="color:#dc3545;text-align:center;margin-bottom:15px;font-weight:600;">Invalid credentials</div>' : ''}
<form method="POST">
<input type="text" name="username" placeholder="Username" required autofocus>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Access Control Panel</button>
</form>
</div></body></html>`);
});

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
    req.session.authenticated = true;
    res.redirect('/');
  } else {
    res.redirect('/login?error=1');
  }
});

// API: Get status of all agents
app.get('/api/status', async (req, res) => {
  try {
    // Get PM2 process list
    const { stdout } = await execAsync('pm2 jlist');
    const pm2Processes = JSON.parse(stdout);
    
    const status: any = {};
    for (const [port, agent] of Object.entries(agentRegistry)) {
      // Find matching PM2 process by port
      const pm2Process = pm2Processes.find((p: any) => 
        p.pm2_env.env.PORT == port || p.pm2_env.env.PORT === port.toString()
      );
      
      status[port] = {
        name: (agent as any).name,
        running: pm2Process ? pm2Process.pm2_env.status === 'online' : false,
        pm2Name: pm2Process ? pm2Process.name : null,
        restarts: pm2Process ? pm2Process.pm2_env.restart_time : 0
      };
    }
    res.json(status);
  } catch (error) {
    console.error('Failed to get PM2 status:', error);
    res.status(500).json({ error: 'Failed to get status' });
  }
});

// API: Start agent
app.post('/api/start/:port', async (req, res) => {
  const { port } = req.params;
  const agent = agentRegistry[port];

  if (!agent) {
    return res.status(404).json({ error: 'Agent not found' });
  }

  try {
    // Find PM2 process name by port
    const { stdout } = await execAsync('pm2 jlist');
    const pm2Processes = JSON.parse(stdout);
    const pm2Process = pm2Processes.find((p: any) => 
      p.pm2_env.env.PORT == port || p.pm2_env.env.PORT === port.toString()
    );

    if (pm2Process) {
      await execAsync(`pm2 start ${pm2Process.name}`);
      
      // Send webhook notification
      await VendorTestWebhook.onAgentStart(agent.name, Number(port), {
        pm2Name: pm2Process.name,
        source: 'agent-control-panel'
      });
      
      res.json({ success: true, message: `${agent.name} started via PM2` });
    } else {
      res.status(404).json({ error: 'PM2 process not found for this agent' });
    }
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

// API: Stop agent
app.post('/api/stop/:port', async (req, res) => {
  const { port } = req.params;
  const agent = agentRegistry[port];

  if (!agent) {
    return res.status(404).json({ error: 'Agent not found' });
  }

  try {
    // Find PM2 process name by port
    const { stdout } = await execAsync('pm2 jlist');
    const pm2Processes = JSON.parse(stdout);
    const pm2Process = pm2Processes.find((p: any) => 
      p.pm2_env.env.PORT == port || p.pm2_env.env.PORT === port.toString()
    );

    if (pm2Process) {
      await execAsync(`pm2 stop ${pm2Process.name}`);
      
      // Send webhook notification
      await VendorTestWebhook.onAgentStop(agent.name, Number(port), {
        pm2Name: pm2Process.name,
        source: 'agent-control-panel'
      });
      
      res.json({ success: true, message: `${agent.name} stopped via PM2` });
    } else {
      res.json({ success: true, message: `${agent.name} - no PM2 process found` });
    }
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

// API: Restart agent
app.post('/api/restart/:port', async (req, res) => {
  const { port } = req.params;
  const agent = agentRegistry[port];

  if (!agent) {
    return res.status(404).json({ error: 'Agent not found' });
  }

  try {
    // Find PM2 process name by port
    const { stdout } = await execAsync('pm2 jlist');
    const pm2Processes = JSON.parse(stdout);
    const pm2Process = pm2Processes.find((p: any) => 
      p.pm2_env.env.PORT == port || p.pm2_env.env.PORT === port.toString()
    );

    if (pm2Process) {
      await execAsync(`pm2 restart ${pm2Process.name}`);
      
      // Send webhook notification
      await VendorTestWebhook.onAgentRestart(agent.name, Number(port), {
        pm2Name: pm2Process.name,
        source: 'agent-control-panel'
      });
      
      res.json({ success: true, message: `${agent.name} restarted via PM2` });
    } else {
      res.status(404).json({ error: 'PM2 process not found for this agent' });
    }
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/', (req, res) => {
  res.send(`<!DOCTYPE html><html><head><title>Agent Control Panel</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --primary: #2c3e50; --primary-light: #34495e; --success: #27ae60; --danger: #e74c3c; --bg: #ffffff; --card-bg: #f8f9fa; --text: #2c3e50; }
body { font-family: 'DejaVu Sans, Segoe UI', sans-serif; background: var(--bg); padding: 0; min-height: 100vh; font-size: 18px; line-height: 1.8; }
.header { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); color: white; padding: 25px 40px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 2.2em; display: flex; align-items: center; gap: 15px; }
.header-actions { display: flex; gap: 15px; align-items: center; }
.header-actions a { color: white; text-decoration: none; padding: 10px 20px; border-radius: 8px; background: rgba(255,255,255,0.2); }
.container { max-width: 1800px; margin: 0 auto; padding: 30px; }
.agents-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
.agent-card { background: var(--card-bg); padding: 25px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid var(--primary); }
.agent-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.agent-name { font-weight: 700; font-size: 1.3em; color: var(--primary); }
.agent-status { display: inline-block; padding: 6px 14px; border-radius: 20px; font-size: 0.85em; font-weight: 600; }
.status-running { background: var(--success); color: white; }
.status-stopped { background: var(--danger); color: white; }
.agent-port { color: #708090; font-size: 1em; font-family: monospace; margin-bottom: 15px; }
.agent-controls { display: flex; gap: 10px; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95em; transition: all 0.3s; }
.btn-start { background: var(--success); color: white; }
.btn-stop { background: var(--danger); color: white; }
.btn-restart { background: var(--primary); color: white; }
.btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.loading { display: none; text-align: center; padding: 20px; color: var(--primary); font-weight: 600; }
</style>
</head><body>
<div class="header">
<h1><span>⚙️</span><span>Agent Control Panel</span></h1>
<div class="header-actions">
<span>System Control</span>
<a href="/logout">Logout</a>
</div>
</div>
<div class="container">
<div class="loading" id="loading">Loading agent status...</div>
<div class="agents-grid" id="agents-grid"></div>
</div>

<script>
async function fetchStatus() {
  try {
    const res = await fetch('/api/status');
    const status = await res.json();
    renderAgents(status);
  } catch (error) {
    console.error('Failed to fetch status:', error);
  }
}

function renderAgents(status) {
  const grid = document.getElementById('agents-grid');
  grid.innerHTML = '';

  for (const [port, agent] of Object.entries(status)) {
    const card = document.createElement('div');
    card.className = 'agent-card';
    const restartWarning = agent.restarts > 20 ? ' ⚠️' : '';
    const restartColor = agent.restarts > 20 ? 'color: #e74c3c; font-weight: 700;' : agent.restarts > 10 ? 'color: #f39c12;' : '';
    
    card.innerHTML = \`
      <div class="agent-header">
        <div class="agent-name">\${agent.name}\${restartWarning}</div>
        <span class="agent-status status-\${agent.running ? 'running' : 'stopped'}">
          \${agent.running ? '● Running' : '○ Stopped'}
        </span>
      </div>
      <div class="agent-port">
        <a href="http://45.61.58.125:\${port}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 600;">
          Port \${port} →
        </a>
        \${agent.pm2Name ? \`<span style="color: #888; font-size: 0.9em; margin-left: 10px;">(\${agent.pm2Name})</span>\` : ''}
      </div>
      \${agent.restarts > 0 ? \`<div style="font-size: 0.85em; \${restartColor} margin-bottom: 10px;">Restarts: \${agent.restarts}</div>\` : ''}
      <div class="agent-controls">
        <button class="btn btn-start" onclick="controlAgent('\${port}', 'start')" \${agent.running ? 'disabled' : ''}>▶ Start</button>
        <button class="btn btn-stop" onclick="controlAgent('\${port}', 'stop')" \${!agent.running ? 'disabled' : ''}>■ Stop</button>
        <button class="btn btn-restart" onclick="controlAgent('\${port}', 'restart')">↻ Restart</button>
      </div>
    \`;
    grid.appendChild(card);
  }
}

async function controlAgent(port, action) {
  try {
    const res = await fetch(\`/api/\${action}/\${port}\`, { method: 'POST' });
    const data = await res.json();
    alert(data.message || data.error);
    setTimeout(fetchStatus, 2000);
  } catch (error) {
    alert('Failed to ' + action + ' agent');
  }
}

// Initial load and auto-refresh every 10 seconds
fetchStatus();
setInterval(fetchStatus, 10000);

// Vendor Test Webhook Integration
(function() {
  const WEBHOOK_URL = 'http://45.61.58.125:3002/vendor-test-status';
  
  // Override controlAgent to send webhooks
  const originalControlAgent = window.controlAgent;
  window.controlAgent = async function(port, action) {
    // Send webhook before action
    try {
      const agentInfo = document.querySelector(\`[onclick*="controlAgent('\${port}'"]\`).closest('.agent-card');
      const agentName = agentInfo.querySelector('.agent-name').innerText.replace(' ⚠️', '');
      
      await fetch(WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          source: 'agent-control-panel',
          action: 'agent_' + action,
          agentName: agentName,
          agentPort: Number(port),
          timestamp: new Date().toISOString()
        })
      });
    } catch (error) {
      console.error('Webhook failed:', error);
    }
    
    // Call original function
    return originalControlAgent.apply(this, arguments);
  };
})();
</script>
</body></html>`);
});

app.get('/logout', (req, res) => {
  req.session.destroy(() => res.redirect('/login'));
});

app.listen(PORT, '0.0.0.0', () => {
  console.log('');
  console.log('⚙️  Agent Control Panel');
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  console.log('🌍 External: http://45.61.58.125:' + PORT);
  console.log('🏠 Local: http://localhost:' + PORT);
  console.log('');
  console.log('🔐 Username: ' + AUTH_USERNAME);
  console.log('🔐 Password: ' + AUTH_PASSWORD);
  console.log('');
  console.log('✅ Control Panel ready...');
});