← 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...');
});