← back to Designer Wallcoverings

DW-Agents/dw-agents/agents-viewer.ts

960 lines

/**
 * DW-Agents: All Agents Viewer
 *
 * Central dashboard showing ALL running agents and skills:
 * - Lists all active agents with ports, URLs, and status
 * - Shows skills from skills manager
 * - Real-time status monitoring
 * - Quick access links to all agent dashboards
 * - Agent statistics and uptime
 *
 * Port: 9111
 */

import express, { Request, Response } from 'express';
import session from 'express-session';
import { exec } from 'child_process';
import { promisify } from 'util';
import fetch from 'node-fetch';
import fs from 'fs/promises';
import path from 'path';

const execAsync = promisify(exec);

const app = express();
const PORT = 9111;

// Authentication
const AUTH_USERNAME = 'admin2025';
const AUTH_PASSWORD = 'Otis';

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

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

app.use(
  session({
    secret: 'dw-agents-viewer-2025',
    resave: false,
    saveUninitialized: true,
    rolling: true, // Extends session on each request
    cookie: {
      secure: false,
      httpOnly: true,
      maxAge: 2 * 60 * 60 * 1000, // 2 hours
    },
  })
);

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

// Login page
app.get('/login', (req, res) => {
  res.send(`
<!DOCTYPE html>
<html>
<head>
  <title>Login - Agents Viewer</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background: linear-gradient(135deg, #36454f 0%, #708090 100%);
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }
    .login-container {
      background: white;
      padding: 40px;
      border-radius: 15px;
      box-shadow: 0 15px 35px rgba(0,0,0,0.2);
      max-width: 400px;
      width: 100%;
    }
    h1 { color: #36454f; text-align: center; margin-bottom: 30px; }
    input {
      width: 100%;
      padding: 12px;
      margin: 10px 0;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
    }
    button {
      width: 100%;
      background: linear-gradient(135deg, #36454f 0%, #708090 100%);
      color: white;
      border: none;
      padding: 15px;
      border-radius: 8px;
      cursor: pointer;
      font-weight: 600;
      margin-top: 10px;
    }
    .error { color: red; text-align: center; margin-bottom: 15px; }
  </style>
</head>
<body>
  <div class="login-container">
    <h1>🤖 Agents Viewer</h1>
    <p style="text-align: center; color: #666; margin-bottom: 20px;">System Dashboard</p>
    ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
    <form method="POST">
      <input type="text" name="username" placeholder="Username" required>
      <input type="password" name="password" placeholder="Password" required>
      <button type="submit">Login</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');
  }
});

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

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

// Get firewall status for all ports
async function getFirewallStatus() {
  try {
    const { stdout } = await execAsync(`sudo ufw status | grep -E "^[0-9]+" | awk '{print $1}' | cut -d'/' -f1`);
    const openPorts = new Set(stdout.trim().split('\n').filter(p => p));
    return openPorts;
  } catch (error) {
    console.error('Error getting firewall status:', error);
    return new Set();
  }
}

// Get all running agents
async function getAllAgents() {
  try {
    const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep node | awk '{print $4, $7}' | sed 's/.*://' | sort -n`);
    const lines = stdout.trim().split('\n');

    const runningPorts = new Set<string>();
    const portToPid: { [key: string]: string } = {};
    const firewallPorts = await getFirewallStatus();

    for (const line of lines) {
      const [port, pidInfo] = line.split(' ');
      const pid = pidInfo?.split('/')[0];
      if (port && pid) {
        runningPorts.add(port);
        portToPid[port] = pid;
      }
    }

    const agents = [];

    // First, add all running agents from netstat
    for (const line of lines) {
      const [port, pidInfo] = line.split(' ');
      const pid = pidInfo?.split('/')[0];

      if (pid) {
        try {
          const { stdout: cmdOut } = await execAsync(`ps -p ${pid} -o args= 2>/dev/null | tail -1`);
          const agentFile = cmdOut.match(/([a-z-]+agent[^/\s]*\.ts)/i)?.[1] || '';

          // Try to get title from web page
          let title = agentRegistry[port]?.name || 'Unknown';
          let category = 'Other';

          try {
            const response = await fetch(`http://localhost:${port}`, { timeout: 1000 });
            if (response.ok) {
              const html = await response.text();
              const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
              if (titleMatch && !agentRegistry[port]) {
                title = titleMatch[1];
              }
            }
          } catch (e) {
            // Couldn't fetch title
          }

          // Categorize agents
          if (title.includes('DW-Agents') || agentFile.includes('agent') || agentRegistry[port]) {
            category = 'DW-Agents';
          } else if (parseInt(port) < 8000) {
            category = 'Websites';
          } else if (parseInt(port) >= 9000) {
            category = 'Services';
          }

          agents.push({
            port,
            pid,
            title,
            agentFile: agentFile || agentRegistry[port]?.script || '',
            description: agentRegistry[port]?.description || '',
            category,
            url: `http://45.61.58.125:${port}/`,
            running: true,
            firewallOpen: firewallPorts.has(port)
          });
        } catch (e) {
          // Skip this agent
        }
      }
    }

    // Add stopped agents from registry
    for (const [port, agentInfo] of Object.entries(agentRegistry)) {
      if (!runningPorts.has(port)) {
        agents.push({
          port,
          pid: null,
          title: (agentInfo as any).name,
          agentFile: (agentInfo as any).script,
          description: (agentInfo as any).description || '',
          category: 'DW-Agents',
          url: `http://45.61.58.125:${port}/`,
          running: false,
          firewallOpen: firewallPorts.has(port),
          dir: (agentInfo as any).dir
        });
      }
    }

    return agents;
  } catch (error) {
    console.error('Error getting agents:', error);
    return [];
  }
}

// Main dashboard
app.get('/', requireAuth, async (req, res) => {
  const agents = await getAllAgents();

  const dwAgents = agents.filter(a => a.category === 'DW-Agents').sort((a, b) => a.title.localeCompare(b.title));
  const websites = agents.filter(a => a.category === 'Websites').sort((a, b) => a.title.localeCompare(b.title));
  const services = agents.filter(a => a.category === 'Services').sort((a, b) => a.title.localeCompare(b.title));

  res.send(`
<!DOCTYPE html>
<html>
<head>
  <title>All Agents Viewer - DW-Agents System</title>
  <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: #e8e8e8;
      padding: 15px;
      min-height: 100vh;
    }
    .container {
      max-width: 1400px;
      margin: 0 auto;
    }
    .header {
      background: white;
      padding: 20px 25px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      margin-bottom: 15px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .header h1 { color: #333; font-size: 1.5em; font-weight: 600; }
    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 10px;
      margin-bottom: 15px;
    }
    .stat-card {
      background: white;
      padding: 15px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      text-align: center;
    }
    .stat-card h3 {
      color: #999;
      font-size: 0.75em;
      text-transform: uppercase;
      margin-bottom: 8px;
      font-weight: 500;
    }
    .stat-card .value {
      font-size: 2em;
      font-weight: 600;
      color: #333;
    }
    .section {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      margin-bottom: 15px;
    }
    .section h2 {
      color: #333;
      margin-bottom: 15px;
      font-size: 1.1em;
      font-weight: 600;
      border-bottom: 1px solid #e0e0e0;
      padding-bottom: 10px;
    }
    .agents-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
      gap: 12px;
    }
    .agent-card {
      background: #f5f5f5;
      border: 1px solid #d0d0d0;
      border-radius: 6px;
      padding: 15px;
      transition: all 0.2s;
    }
    .agent-card:hover {
      box-shadow: 0 4px 8px rgba(0,0,0,0.15);
      border-color: #888;
      background: #fafafa;
    }
    .agent-card-header {
      cursor: pointer;
    }
    .agent-card h3 {
      color: #333;
      font-size: 0.95em;
      margin-bottom: 8px;
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }
    .port-badge {
      background: #666;
      color: white;
      padding: 3px 10px;
      border-radius: 4px;
      font-size: 0.8em;
      font-weight: 500;
    }
    .agent-description {
      color: #666;
      font-size: 0.85em;
      margin: 8px 0;
      line-height: 1.4;
    }
    .agent-url {
      color: #999;
      font-size: 0.75em;
      word-break: break-all;
      margin-top: 8px;
    }
    .status-badge {
      display: inline-block;
      color: white;
      padding: 3px 10px;
      border-radius: 4px;
      font-size: 0.7em;
      font-weight: 500;
    }
    .status-running {
      background: #555;
    }
    .status-stopped {
      background: #999;
    }
    .firewall-badge {
      display: inline-block;
      color: white;
      padding: 3px 8px;
      border-radius: 4px;
      font-size: 0.65em;
      font-weight: 500;
      margin-left: 4px;
    }
    .firewall-open {
      background: #4a4a4a;
    }
    .firewall-closed {
      background: #c44;
    }
    .refresh-btn {
      background: #666;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 500;
      font-size: 0.9em;
    }
    .refresh-btn:hover {
      background: #555;
    }
    .select-checkbox {
      width: 20px;
      height: 20px;
      cursor: pointer;
      margin-right: 10px;
    }
    .card-with-checkbox {
      display: flex;
      align-items: flex-start;
      gap: 10px;
    }
    .card-content {
      flex: 1;
    }
    .bulk-action-bar {
      position: fixed;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: #333;
      color: white;
      padding: 12px 24px;
      border-radius: 6px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.3);
      display: none;
      align-items: center;
      gap: 15px;
      z-index: 1000;
    }
    .bulk-action-bar.show {
      display: flex;
    }
    .bulk-action-bar button {
      background: white;
      color: #333;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 500;
      font-size: 0.9em;
      transition: all 0.2s;
    }
    .bulk-action-bar button:hover {
      background: #f0f0f0;
    }
    .action-buttons {
      display: flex;
      gap: 6px;
      margin-top: 12px;
    }
    .btn {
      flex: 1;
      padding: 8px 12px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 500;
      font-size: 0.8em;
      transition: all 0.2s;
    }
    .btn:hover {
      opacity: 0.9;
    }
    .btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
    .btn-stop {
      background: #999;
      color: white;
    }
    .btn-restart {
      background: #666;
      color: white;
    }
    .btn-start {
      background: #333;
      color: white;
    }
    .notification {
      position: fixed;
      top: 20px;
      right: 20px;
      background: white;
      padding: 15px 20px;
      border-radius: 8px;
      box-shadow: 0 5px 20px rgba(0,0,0,0.2);
      z-index: 10000;
      display: none;
      animation: slideIn 0.3s;
    }
    .notification.show {
      display: block;
    }
    .notification.success {
      border-left: 4px solid #28a745;
    }
    .notification.error {
      border-left: 4px solid #dc3545;
    }
    @keyframes slideIn {
      from { transform: translateX(400px); opacity: 0; }
      to { transform: translateX(0); opacity: 1; }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <div>
        <h1>All Agents</h1>
      </div>
      <div style="display: flex; align-items: center; gap: 12px;">
        <span id="countdown" style="color: #999; font-size: 0.9em;">30s</span>
        <button class="refresh-btn" onclick="refreshNow()">Refresh</button>
        <a href="/logout" style="color: #666; text-decoration: none; font-size: 0.9em;">Logout</a>
      </div>
    </div>

    <div class="stats">
      <div class="stat-card">
        <h3>Total Agents</h3>
        <div class="value">${agents.length}</div>
      </div>
      <div class="stat-card">
        <h3>DW-Agents</h3>
        <div class="value">${dwAgents.length}</div>
      </div>
      <div class="stat-card">
        <h3>Websites</h3>
        <div class="value">${websites.length}</div>
      </div>
      <div class="stat-card">
        <h3>Services</h3>
        <div class="value">${services.length}</div>
      </div>
    </div>

    <div class="section">
      <h2>DW-Agents (${dwAgents.length})</h2>
      <div class="agents-grid">
        ${dwAgents.map(agent => `
          <div class="agent-card">
            <div class="card-with-checkbox">
              <input type="checkbox" class="select-checkbox" data-url="${agent.url}" onchange="updateBulkActions()" ${!agent.running ? 'disabled' : ''}>
              <div class="card-content">
                <div class="agent-card-header" onclick="${agent.running ? `window.open('${agent.url}', '_blank')` : ''}" style="cursor: ${agent.running ? 'pointer' : 'default'};">
                  <h3>
                    <span>${agent.title}</span>
                    <span class="status-badge ${agent.running ? 'status-running' : 'status-stopped'}">
                      ${agent.running ? 'RUNNING' : 'STOPPED'}
                    </span>
                    <span class="firewall-badge ${agent.firewallOpen ? 'firewall-open' : 'firewall-closed'}">
                      ${agent.firewallOpen ? 'FW: OPEN' : 'FW: CLOSED'}
                    </span>
                  </h3>
                  ${agent.description ? `<div class="agent-description">${agent.description}</div>` : ''}
                  <div class="agent-url">Port ${agent.port}</div>
                </div>
                <div class="action-buttons">
                  ${agent.running ? `
                    <button class="btn btn-stop" onclick="stopAgent('${agent.pid}', '${agent.port}', event)">
                      Stop
                    </button>
                    <button class="btn btn-restart" onclick="restartAgent('${agent.pid}', '${agent.agentFile}', '${agent.port}', event)">
                      Restart
                    </button>
                  ` : `
                    <button class="btn btn-start" onclick="startAgent('${agent.agentFile}', '${agent.port}', event)">
                      Start Now
                    </button>
                  `}
                </div>
              </div>
            </div>
          </div>
        `).join('')}
      </div>
    </div>

    <div class="section">
      <h2>Websites (${websites.length})</h2>
      <div class="agents-grid">
        ${websites.map(agent => `
          <div class="agent-card">
            <div class="card-with-checkbox">
              <input type="checkbox" class="select-checkbox" data-url="${agent.url}" onchange="updateBulkActions()">
              <div class="card-content">
                <div onclick="window.open('${agent.url}', '_blank')" style="cursor: pointer;">
                  <h3>
                    <span>${agent.title}</span>
                    <span class="status-badge status-running">RUNNING</span>
                  </h3>
                  <div class="agent-url">Port ${agent.port}</div>
                </div>
              </div>
            </div>
          </div>
        `).join('')}
      </div>
    </div>

    <div class="section">
      <h2>Services (${services.length})</h2>
      <div class="agents-grid">
        ${services.map(agent => `
          <div class="agent-card">
            <div class="card-with-checkbox">
              <input type="checkbox" class="select-checkbox" data-url="${agent.url}" onchange="updateBulkActions()">
              <div class="card-content">
                <div onclick="window.open('${agent.url}', '_blank')" style="cursor: pointer;">
                  <h3>
                    <span>${agent.title}</span>
                    <span class="status-badge status-running">RUNNING</span>
                  </h3>
                  <div class="agent-url">Port ${agent.port}</div>
                </div>
              </div>
            </div>
          </div>
        `).join('')}
      </div>
    </div>
  </div>

  <div id="notification" class="notification"></div>

  <div id="bulkActionBar" class="bulk-action-bar">
    <span id="selectedCount">0 selected</span>
    <button onclick="openSelectedInTabs()">🚀 Open Selected in Tabs</button>
    <button onclick="clearSelection()">✖ Clear Selection</button>
  </div>

  <script>
    function showNotification(message, type = 'success') {
      const notif = document.getElementById('notification');
      notif.textContent = message;
      notif.className = \`notification \${type} show\`;

      setTimeout(() => {
        notif.classList.remove('show');
      }, 3000);
    }

    async function stopAgent(pid, port, event) {
      event.stopPropagation();

      if (!confirm(\`Stop agent on port \${port}?\`)) return;

      const btn = event.target;
      btn.disabled = true;
      btn.textContent = '⏳ Stopping...';

      try {
        const response = await fetch('/api/stop', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ pid, port })
        });

        const data = await response.json();

        if (response.ok) {
          showNotification(data.message, 'success');
          setTimeout(() => location.reload(), 1500);
        } else {
          showNotification(data.error || 'Failed to stop agent', 'error');
          btn.disabled = false;
          btn.textContent = '🛑 Stop';
        }
      } catch (error) {
        showNotification('Network error', 'error');
        btn.disabled = false;
        btn.textContent = '🛑 Stop';
      }
    }

    async function restartAgent(pid, agentFile, port, event) {
      event.stopPropagation();

      if (!confirm(\`Restart agent on port \${port}?\`)) return;

      const btn = event.target;
      btn.disabled = true;
      btn.textContent = '⏳ Restarting...';

      try {
        const response = await fetch('/api/restart', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ pid, agentFile, port })
        });

        const data = await response.json();

        if (response.ok) {
          showNotification(data.message, 'success');
          setTimeout(() => location.reload(), 2500);
        } else {
          showNotification(data.error || 'Failed to restart agent', 'error');
          btn.disabled = false;
          btn.textContent = '🔄 Restart';
        }
      } catch (error) {
        showNotification('Network error', 'error');
        btn.disabled = false;
        btn.textContent = '🔄 Restart';
      }
    }

    async function startAgent(agentFile, port, event) {
      event.stopPropagation();

      if (!confirm(\`Start agent on port \${port}?\`)) return;

      const btn = event.target;
      btn.disabled = true;
      btn.textContent = '⏳ Starting...';

      try {
        const response = await fetch('/api/start', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ agentFile, port })
        });

        const data = await response.json();

        if (response.ok) {
          showNotification(data.message, 'success');
          setTimeout(() => location.reload(), 2500);
        } else {
          showNotification(data.error || 'Failed to start agent', 'error');
          btn.disabled = false;
          btn.textContent = '▶️ Start Now';
        }
      } catch (error) {
        showNotification('Network error', 'error');
        btn.disabled = false;
        btn.textContent = '▶️ Start Now';
      }
    }

    // Bulk selection functions
    function updateBulkActions() {
      const checkboxes = document.querySelectorAll('.select-checkbox:checked');
      const count = checkboxes.length;
      const bulkBar = document.getElementById('bulkActionBar');
      const countSpan = document.getElementById('selectedCount');

      if (count > 0) {
        bulkBar.classList.add('show');
        countSpan.textContent = \`\${count} selected\`;
      } else {
        bulkBar.classList.remove('show');
      }
    }

    function openSelectedInTabs() {
      const checkboxes = document.querySelectorAll('.select-checkbox:checked');
      const urls = Array.from(checkboxes).map(cb => cb.getAttribute('data-url'));

      if (urls.length === 0) {
        showNotification('No agents selected', 'error');
        return;
      }

      // Open each URL in a new tab
      urls.forEach(url => {
        window.open(url, '_blank');
      });

      showNotification(\`Opened \${urls.length} tabs\`, 'success');
      clearSelection();
    }

    function clearSelection() {
      const checkboxes = document.querySelectorAll('.select-checkbox:checked');
      checkboxes.forEach(cb => cb.checked = false);
      updateBulkActions();
    }

    // Countdown timer and auto-refresh every 30 seconds
    let timeLeft = 30;
    const countdownEl = document.getElementById('countdown');

    function updateCountdown() {
      countdownEl.textContent = timeLeft + 's';
      timeLeft--;

      if (timeLeft < 0) {
        timeLeft = 30;
        location.reload();
      }
    }

    function refreshNow() {
      timeLeft = 30;
      location.reload();
    }

    // Update countdown every second
    setInterval(updateCountdown, 1000);
    updateCountdown();
  </script>
</body>
</html>
  `);
});

// API endpoint to get agents data
app.get('/api/agents', requireAuth, async (req, res) => {
  const agents = await getAllAgents();
  res.json(agents);
});

// API endpoint to stop an agent
app.post('/api/stop', requireAuth, async (req, res) => {
  try {
    const { pid, port } = req.body;

    if (!pid) {
      return res.status(400).json({ error: 'PID is required' });
    }

    console.log(`🛑 Stopping agent on port ${port} (PID: ${pid})`);

    // Kill the process
    await execAsync(`kill -9 ${pid}`);

    res.json({
      success: true,
      message: `Agent stopped (Port ${port}, PID ${pid})`
    });
  } catch (error: any) {
    console.error('Error stopping agent:', error);
    res.status(500).json({
      error: 'Failed to stop agent',
      message: error.message
    });
  }
});

// API endpoint to start an agent
app.post('/api/start', requireAuth, async (req, res) => {
  try {
    const { agentFile, port } = req.body;

    if (!agentFile) {
      return res.status(400).json({ error: 'Agent file is required' });
    }

    console.log(`▶️  Starting agent: ${agentFile} (Port: ${port})`);

    // Get agent info from registry if available
    const agentInfo = agentRegistry[port];
    const dir = agentInfo?.dir || '/root/Projects/Designer-Wallcoverings/DW-Agents';
    const logFile = agentInfo?.logFile || `/tmp/${agentFile.replace('.ts', '')}.log`;

    await execAsync(`cd ${dir} && npx tsx ${agentFile} > ${logFile} 2>&1 &`);

    // Wait a bit and check if it started
    await new Promise(resolve => setTimeout(resolve, 2000));

    res.json({
      success: true,
      message: `Agent started on port ${port}`,
      logFile
    });
  } catch (error: any) {
    console.error('Error starting agent:', error);
    res.status(500).json({
      error: 'Failed to start agent',
      message: error.message
    });
  }
});

// API endpoint to restart an agent
app.post('/api/restart', requireAuth, async (req, res) => {
  try {
    const { pid, agentFile, port } = req.body;

    if (!pid || !agentFile) {
      return res.status(400).json({ error: 'PID and agent file are required' });
    }

    console.log(`🔄 Restarting agent: ${agentFile} (Port ${port}, PID ${pid})`);

    // Stop the agent
    await execAsync(`kill -9 ${pid}`);

    // Wait a bit
    await new Promise(resolve => setTimeout(resolve, 1000));

    // Get agent info from registry if available
    const agentInfo = agentRegistry[port];
    const dir = agentInfo?.dir || '/root/Projects/Designer-Wallcoverings/DW-Agents';
    const logFile = agentInfo?.logFile || `/tmp/${agentFile.replace('.ts', '')}.log`;

    await execAsync(`cd ${dir} && npx tsx ${agentFile} > ${logFile} 2>&1 &`);

    // Wait and verify
    await new Promise(resolve => setTimeout(resolve, 2000));

    res.json({
      success: true,
      message: `Agent restarted on port ${port}`,
      logFile
    });
  } catch (error: any) {
    console.error('Error restarting agent:', error);
    res.status(500).json({
      error: 'Failed to restart agent',
      message: error.message
    });
  }
});

app.listen(PORT, '0.0.0.0', () => {
  console.log('');
  console.log('🤖 All Agents Viewer');
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  console.log('🌍 External: http://45.61.58.125:' + PORT);
  console.log('🏠 Local: http://localhost:' + PORT);
  console.log('');
  console.log('✅ Agents Viewer ready...');
});