← back to Port Viewer

server.js

973 lines

const http = require('http');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const nodemailer = require('nodemailer');
const crypto = require('crypto');

const PORT = 8888; // Permanent port assignment
const SERVER_IP = '45.61.58.125'; // Server public IP address
const SYSTEM_PORTS = [22, 53, 80, 443, 111, 123, 514]; // Common system ports to exclude

// Authentication
const AUTH_PASSWORD = 'serveraccess911'; // Change this to your desired password
const sessions = new Map(); // Store active sessions

// Store directory/port mappings in memory
const directoryData = {};

// Store previous port states for change detection
const previousPortStates = {};

// Email configuration
let emailConfig = {
    enabled: true,
    recipient: 'steve@designerwallcoverings.com',
    smtp: {
        host: 'localhost',
        port: 25,
        secure: false
    }
};

// Create email transporter
let transporter = null;
try {
    transporter = nodemailer.createTransport(emailConfig.smtp);
} catch (err) {
    console.error('Failed to create email transporter:', err.message);
}

// Helper functions for authentication
function generateToken() {
    return crypto.randomBytes(32).toString('hex');
}

function isAuthenticated(req) {
    const authHeader = req.headers['authorization'];
    if (!authHeader) return false;

    const token = authHeader.replace('Bearer ', '');
    return sessions.has(token);
}

function getCookie(req, name) {
    const cookies = req.headers.cookie;
    if (!cookies) return null;

    const cookie = cookies.split(';').find(c => c.trim().startsWith(name + '='));
    return cookie ? cookie.split('=')[1] : null;
}

const server = http.createServer((req, res) => {
    // Login endpoint - always accessible
    if (req.url === '/api/login' && req.method === 'POST') {
        let body = '';
        req.on('data', chunk => {
            body += chunk.toString();
        });
        req.on('end', () => {
            try {
                const { password } = JSON.parse(body);

                if (password === AUTH_PASSWORD) {
                    const token = generateToken();
                    sessions.set(token, { createdAt: Date.now() });

                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ success: true, token }));
                } else {
                    res.writeHead(401, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ success: false, error: 'Invalid password' }));
                }
            } catch (err) {
                res.writeHead(400, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'Invalid request' }));
            }
        });
        return;
    }

    // Logout endpoint
    if (req.url === '/api/logout' && req.method === 'POST') {
        const authHeader = req.headers['authorization'];
        if (authHeader) {
            const token = authHeader.replace('Bearer ', '');
            sessions.delete(token);
        }
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ success: true }));
        return;
    }

    // Check authentication for API routes (except login, server-info, suggest-port, domains, and proxy)
    if (req.url.startsWith('/api/') && req.url !== '/api/server-info' && req.url !== '/api/suggest-port' && req.url !== '/api/domains' && !req.url.startsWith('/api/proxy/') && !isAuthenticated(req)) {
        res.writeHead(401, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Unauthorized' }));
        return;
    }

    if (req.url === '/api/server-info') {
        // Return server information
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            serverIp: SERVER_IP,
            port: PORT
        }));
        return;
    }

    if (req.url === '/api/ports') {
        exec('ss -tulpn 2>/dev/null | grep LISTEN', (error, stdout, stderr) => {
            if (error && !stdout) {
                // Fallback to netstat if ss fails
                exec('netstat -tulpn 2>/dev/null | grep LISTEN', (error2, stdout2, stderr2) => {
                    if (error2 && !stdout2) {
                        res.writeHead(500, { 'Content-Type': 'application/json' });
                        res.end(JSON.stringify({ error: 'Failed to fetch port information' }));
                        return;
                    }
                    enrichPortData(parseNetstat(stdout2), (enrichedPorts) => {
                        const allServices = mergeWithOfflineDirectories(enrichedPorts);
                        res.writeHead(200, { 'Content-Type': 'application/json' });
                        res.end(JSON.stringify(allServices));
                    });
                });
            } else {
                enrichPortData(parseSS(stdout), (enrichedPorts) => {
                    const allServices = mergeWithOfflineDirectories(enrichedPorts);
                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify(allServices));
                });
            }
        });
    } else if (req.url.startsWith('/api/restart/')) {
        // Handle restart requests
        const directory = decodeURIComponent(req.url.replace('/api/restart/', ''));
        handleRestart(directory, res);
    } else if (req.url === '/api/directories') {
        // Get stored directory data
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(directoryData));
    } else if (req.url === '/api/domains') {
        // Get all domains from nginx configs
        exec('grep -h "server_name" /etc/nginx/sites-enabled/* 2>/dev/null', (error, stdout) => {
            const domains = [];

            if (stdout) {
                const lines = stdout.trim().split('\n');
                lines.forEach(line => {
                    // Parse server_name lines
                    const match = line.match(/server_name\s+([^;]+);/);
                    if (match) {
                        const serverNames = match[1].trim().split(/\s+/);
                        serverNames.forEach(domain => {
                            if (domain && !domain.includes('localhost') && !domain.match(/^\d+\.\d+\.\d+\.\d+$/)) {
                                domains.push(domain);
                            }
                        });
                    }
                });
            }

            // Get unique domains
            const uniqueDomains = [...new Set(domains)].sort();

            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ domains: uniqueDomains }));
        });
    } else if (req.url === '/api/suggest-port') {
        // Suggest an available port
        exec('ss -tulpn | grep LISTEN', (error, stdout) => {
            const usedPorts = new Set();

            // Parse all listening ports
            const lines = stdout.trim().split('\n');
            lines.forEach(line => {
                const match = line.match(/:(\d+)\s/);
                if (match) {
                    usedPorts.add(parseInt(match[1]));
                }
            });

            // Find available port starting from 9000
            let suggestedPort = 9000;
            while (usedPorts.has(suggestedPort)) {
                suggestedPort++;
            }

            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ port: suggestedPort }));
        });
    } else if (req.url === '/api/scan-directories') {
        // Scan for directories with package.json - deep scan in specific directories
        const scanPaths = [
            'find /root/DW-Websites -name "package.json" -type f 2>/dev/null',
            'find /root/WebsitesMisc -name "package.json" -type f 2>/dev/null',
            'find /root/Scripts -name "package.json" -type f 2>/dev/null',
            'find /opt -name "package.json" -type f 2>/dev/null',
            'find /root -maxdepth 3 -name "package.json" -type f 2>/dev/null'
        ];

        const combinedCommand = scanPaths.join(' ; ');
        exec(combinedCommand, (error, stdout) => {
            const directories = new Set(); // Use Set to avoid duplicates
            if (stdout) {
                const files = stdout.trim().split('\n');
                files.forEach(file => {
                    if (file) {
                        const dir = path.dirname(file);
                        // Exclude node_modules
                        if (!dir.includes('node_modules')) {
                            directories.add(dir);
                        }
                    }
                });
            }

            const uniqueDirectories = Array.from(directories).sort();

            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({
                directories: uniqueDirectories,
                count: uniqueDirectories.length
            }));
        });
    } else if (req.url === '/api/add-directory' && req.method === 'POST') {
        // Handle adding a directory manually
        let body = '';
        req.on('data', chunk => {
            body += chunk.toString();
        });
        req.on('end', () => {
            try {
                const { directory, port, appName } = JSON.parse(body);

                if (!directory) {
                    res.writeHead(400, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ error: 'Directory path is required' }));
                    return;
                }

                // Check if directory exists
                if (!fs.existsSync(directory)) {
                    res.writeHead(404, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ error: 'Directory does not exist' }));
                    return;
                }

                // Add to directoryData
                directoryData[directory] = {
                    lastPort: port || 'N/A',
                    lastSeen: new Date().toISOString(),
                    process: 'Unknown',
                    appName: appName || path.basename(directory)
                };

                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({
                    success: true,
                    message: 'Directory added successfully',
                    directory: directory
                }));
            } catch (err) {
                res.writeHead(400, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'Invalid request body' }));
            }
        });
    } else if (req.url === '/api/ask' && req.method === 'POST') {
        // Handle natural language server queries
        let body = '';
        req.on('data', chunk => {
            body += chunk.toString();
        });
        req.on('end', () => {
            try {
                const { query } = JSON.parse(body);
                handleServerQuery(query, res);
            } catch (err) {
                res.writeHead(400, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'Invalid request body' }));
            }
        });
    } else if (req.url.startsWith('/api/delete-directory/')) {
        // Handle deleting a directory
        const directory = decodeURIComponent(req.url.replace('/api/delete-directory/', ''));
        if (directoryData[directory]) {
            delete directoryData[directory];
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({
                success: true,
                message: 'Directory removed successfully'
            }));
        } else {
            res.writeHead(404, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Directory not found' }));
        }
    } else if (req.url.startsWith('/api/stop-port/') && req.method === 'POST') {
        // Stop/kill process on port
        const port = decodeURIComponent(req.url.replace('/api/stop-port/', ''));
        exec(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, (error, stdout, stderr) => {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({
                success: true,
                message: `Stopped process on port ${port}`
            }));
        });
    } else if (req.url === '/api/assign-port' && req.method === 'POST') {
        // Assign port to directory
        let body = '';
        req.on('data', chunk => {
            body += chunk.toString();
        });
        req.on('end', () => {
            try {
                const { directory, port, appName } = JSON.parse(body);

                if (!directory || !port) {
                    res.writeHead(400, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ error: 'Directory and port are required' }));
                    return;
                }

                // Update or create directory mapping
                directoryData[directory] = {
                    lastPort: port,
                    lastSeen: new Date().toISOString(),
                    process: 'Unknown',
                    appName: appName || path.basename(directory)
                };

                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({
                    success: true,
                    message: `Assigned port ${port} to ${directory}`,
                    directory: directory,
                    port: port
                }));
            } catch (err) {
                res.writeHead(400, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'Invalid request body' }));
            }
        });
    } else if (req.url === '/api/email-config') {
        // Get email configuration
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            enabled: emailConfig.enabled,
            recipient: emailConfig.recipient
        }));
    } else if (req.url === '/api/email-config' && req.method === 'POST') {
        // Update email configuration
        let body = '';
        req.on('data', chunk => {
            body += chunk.toString();
        });
        req.on('end', () => {
            try {
                const { enabled, recipient } = JSON.parse(body);

                if (enabled !== undefined) emailConfig.enabled = enabled;
                if (recipient) emailConfig.recipient = recipient;

                res.writeHead(200, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({
                    success: true,
                    message: 'Email configuration updated',
                    config: {
                        enabled: emailConfig.enabled,
                        recipient: emailConfig.recipient
                    }
                }));
            } catch (err) {
                res.writeHead(400, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'Invalid request body' }));
            }
        });
    } else if (req.url.startsWith('/api/proxy/')) {
        // Proxy endpoint to bypass X-Frame-Options
        const targetUrl = decodeURIComponent(req.url.replace('/api/proxy/', ''));

        exec(`curl -sL "${targetUrl}"`, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
            if (error) {
                res.writeHead(500, { 'Content-Type': 'text/html' });
                res.end(`<html><body style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;background:#f0f0f0;"><div style="text-align:center;"><h2>❌ Failed to load</h2><p>${targetUrl}</p><p style="font-size:0.9em;color:#666;">${error.message}</p></div></body></html>`);
                return;
            }

            res.writeHead(200, {
                'Content-Type': 'text/html',
                'X-Frame-Options': 'ALLOWALL',
                'Content-Security-Policy': ''
            });
            res.end(stdout);
        });
    } else if (req.url === '/test') {
        // Serve test page
        const filePath = path.join(__dirname, 'test.html');
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.writeHead(404);
                res.end('Not found');
                return;
            }
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end(data);
        });
    } else {
        // Serve index.html
        const filePath = path.join(__dirname, 'public', 'index.html');
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.writeHead(404);
                res.end('Not found');
                return;
            }
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end(data);
        });
    }
});

function parseSS(output) {
    const lines = output.trim().split('\n');
    const ports = [];

    lines.forEach(line => {
        const parts = line.trim().split(/\s+/);
        if (parts.length < 5) return;

        const protocol = parts[0];
        const localAddress = parts[4];
        const [address, port] = localAddress.split(':').slice(-2);

        // Extract process info and PID
        let process = 'Unknown';
        let pid = null;
        const usersMatch = line.match(/users:\(\("([^"]+)",pid=(\d+)/);
        if (usersMatch) {
            process = usersMatch[1];
            pid = usersMatch[2];
        }

        ports.push({
            port: port || 'N/A',
            protocol: protocol.toUpperCase(),
            address: address === '*' ? '0.0.0.0' : address,
            process: process,
            pid: pid
        });
    });

    return ports.sort((a, b) => parseInt(a.port) - parseInt(b.port));
}

function parseNetstat(output) {
    const lines = output.trim().split('\n');
    const ports = [];

    lines.forEach(line => {
        const parts = line.trim().split(/\s+/);
        if (parts.length < 7) return;

        const protocol = parts[0];
        const localAddress = parts[3];
        const [address, port] = localAddress.split(':').slice(-2);
        const processInfo = parts[6] || 'Unknown';

        // Extract PID from process info (format: pid/process)
        let process = processInfo;
        let pid = null;
        const pidMatch = processInfo.match(/(\d+)\/(.*)/);
        if (pidMatch) {
            pid = pidMatch[1];
            process = pidMatch[2];
        }

        ports.push({
            port: port || 'N/A',
            protocol: protocol.toUpperCase(),
            address: address === '*' ? '0.0.0.0' : address,
            process: process,
            pid: pid
        });
    });

    return ports.sort((a, b) => parseInt(a.port) - parseInt(b.port));
}

function enrichPortData(ports, callback) {
    // Get nginx config data - read actual config files
    exec('cat /etc/nginx/sites-enabled/* 2>/dev/null', (error, nginxOutput) => {
        const portMapping = parseNginxConfig(nginxOutput || '');

        // Get Docker data
        exec('docker ps --format "{{.Names}}|{{.Ports}}|{{.Image}}"', (error, dockerOutput) => {
            const dockerMapping = parseDockerPs(dockerOutput || '');

            // Get directory info for each port with PID
            let completed = 0;
            const enrichedPorts = ports.map(portInfo => {
                const portNum = portInfo.port;
                const websites = portMapping[portNum] || [];
                const docker = dockerMapping[portNum] || null;

                return {
                    ...portInfo,
                    websites: websites,
                    website: websites.length > 0 ? websites.join(', ') : null,
                    docker: docker,
                    appName: getAppName(portInfo, websites, docker),
                    directory: null,
                    isUserApp: false
                };
            });

            // Get directory for each port
            enrichedPorts.forEach((portInfo, index) => {
                if (portInfo.pid && !SYSTEM_PORTS.includes(parseInt(portInfo.port))) {
                    getProcessDirectory(portInfo.pid, (dir) => {
                        enrichedPorts[index].directory = dir;
                        enrichedPorts[index].isUserApp = dir && !dir.startsWith('/usr') && !dir.startsWith('/lib') && !dir.startsWith('/bin');

                        // Store directory data
                        if (dir && enrichedPorts[index].isUserApp) {
                            directoryData[dir] = {
                                lastPort: portInfo.port,
                                lastSeen: new Date().toISOString(),
                                process: portInfo.process,
                                appName: portInfo.appName
                            };
                        }

                        completed++;
                        if (completed === enrichedPorts.length) {
                            callback(enrichedPorts);
                        }
                    });
                } else {
                    completed++;
                    if (completed === enrichedPorts.length) {
                        callback(enrichedPorts);
                    }
                }
            });

            // If no async operations, call immediately
            if (enrichedPorts.length === 0 || enrichedPorts.every(p => !p.pid || SYSTEM_PORTS.includes(parseInt(p.port)))) {
                callback(enrichedPorts);
            }
        });
    });
}

function parseNginxConfig(output) {
    const mapping = {};

    // Split into server blocks
    const serverBlocks = output.split(/server\s*{/);

    serverBlocks.forEach(block => {
        // Find server_name
        const serverNameMatch = block.match(/server_name\s+([^;]+);/);
        // Find proxy_pass with port
        const proxyPassMatch = block.match(/proxy_pass\s+http:\/\/[^:]+:(\d+)/);

        if (serverNameMatch && proxyPassMatch) {
            const port = proxyPassMatch[1];
            const domains = serverNameMatch[1]
                .split(/\s+/)
                .filter(d => d && d !== '_' && d !== 'www._' && !d.match(/^\d+\.\d+\.\d+\.\d+$/))
                .map(d => d.replace(/^www\./, '')); // Remove www. prefix

            if (domains.length > 0) {
                if (!mapping[port]) mapping[port] = [];
                domains.forEach(domain => {
                    if (!mapping[port].includes(domain)) {
                        mapping[port].push(domain);
                    }
                });
            }
        }
    });

    return mapping;
}

function parseDockerPs(output) {
    const lines = output.trim().split('\n');
    const mapping = {};

    lines.forEach(line => {
        const [name, ports, image] = line.split('|');
        if (!ports) return;

        const portMatch = ports.match(/:(\d+)->/g);
        if (portMatch) {
            portMatch.forEach(pm => {
                const port = pm.match(/:(\d+)->/)[1];
                mapping[port] = { name, image };
            });
        }
    });

    return mapping;
}

function getAppName(portInfo, websites, docker) {
    if (docker) return docker.name;
    if (websites.length > 0) return websites[0];

    // Guess based on port
    const portNum = parseInt(portInfo.port);
    if (portNum === 80 || portNum === 443) return 'Web Server (nginx)';
    if (portNum === 22) return 'SSH Server';
    if (portNum === 3000) return 'Node.js App';
    if (portNum === 8080) return 'HTTP Server';
    if (portNum === 9000) return 'Node.js App';
    if (portInfo.process.includes('nginx')) return 'nginx';
    if (portInfo.process.includes('node')) return 'Node.js App';
    if (portInfo.process.includes('docker')) return 'Docker Service';

    return portInfo.process;
}

function getProcessDirectory(pid, callback) {
    exec(`readlink -f /proc/${pid}/cwd 2>/dev/null`, (error, stdout) => {
        if (error || !stdout) {
            callback(null);
        } else {
            callback(stdout.trim());
        }
    });
}

function mergeWithOfflineDirectories(activePorts) {
    const activeDirectories = new Set();
    const result = [...activePorts];

    // Mark active directories
    activePorts.forEach(port => {
        if (port.directory && port.isUserApp) {
            activeDirectories.add(port.directory);
        }
    });

    // Add offline directories
    Object.keys(directoryData).forEach(directory => {
        if (!activeDirectories.has(directory)) {
            const data = directoryData[directory];
            result.push({
                port: data.lastPort || 'N/A',
                protocol: 'N/A',
                address: 'N/A',
                process: data.process || 'Unknown',
                pid: null,
                websites: [],
                website: null,
                docker: null,
                appName: data.appName || path.basename(directory),
                directory: directory,
                isUserApp: true,
                isOffline: true
            });
        }
    });

    return result;
}

function handleServerQuery(query, res) {
    const q = query.toLowerCase().trim();

    let command = '';
    let type = 'general';

    // Parse the query and determine the command
    if (q === 'help' || q.includes('help')) {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            success: true,
            type: 'help',
            response: `**Available Commands:**
- uptime / show uptime
- memory / memory usage / ram
- cpu / cpu usage / load
- disk / disk usage / storage
- ports / open ports / listening ports
- processes / top processes
- directories / my directories
- network / network info
- users / logged in users
- services / systemd services`
        }));
        return;
    }

    if (q.includes('uptime')) {
        command = 'uptime -p';
        type = 'uptime';
    } else if (q.includes('memory') || q.includes('ram')) {
        command = 'free -h';
        type = 'memory';
    } else if (q.includes('cpu') || q.includes('load')) {
        command = 'top -bn1 | head -20';
        type = 'cpu';
    } else if (q.includes('disk') || q.includes('storage')) {
        command = 'df -h';
        type = 'disk';
    } else if (q.includes('port') && (q.includes('open') || q.includes('listening'))) {
        command = 'ss -tulpn | grep LISTEN';
        type = 'ports';
    } else if (q.includes('process') || q.includes('top')) {
        command = 'ps aux --sort=-%mem | head -15';
        type = 'processes';
    } else if (q.includes('director')) {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            success: true,
            type: 'directories',
            response: JSON.stringify(directoryData, null, 2),
            data: directoryData
        }));
        return;
    } else if (q.includes('network')) {
        command = 'ip addr show';
        type = 'network';
    } else if (q.includes('user') || q.includes('logged in')) {
        command = 'who';
        type = 'users';
    } else if (q.includes('service')) {
        command = 'systemctl list-units --type=service --state=running';
        type = 'services';
    } else if (q.includes('docker')) {
        command = 'docker ps';
        type = 'docker';
    } else {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            success: false,
            error: 'I didn\'t understand that question. Type "help" to see available commands.'
        }));
        return;
    }

    // Execute the command
    exec(command, (error, stdout, stderr) => {
        if (error) {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({
                success: false,
                error: stderr || error.message
            }));
            return;
        }

        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            success: true,
            type: type,
            query: query,
            response: stdout.trim()
        }));
    });
}

function handleRestart(directory, res) {
    console.log(`[RESTART] Request for directory: ${directory}`);

    if (!directory || !directoryData[directory]) {
        console.log(`[RESTART] Directory not found: ${directory}`);
        res.writeHead(404, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Directory not found' }));
        return;
    }

    const data = directoryData[directory];
    const lastPort = data.lastPort;
    console.log(`[RESTART] Port: ${lastPort}`);

    // Find package.json or main entry point
    const packageJsonPath = path.join(directory, 'package.json');

    fs.readFile(packageJsonPath, 'utf8', (err, content) => {
        let startCommand = 'npm start';

        if (!err) {
            try {
                const pkg = JSON.parse(content);
                if (pkg.scripts && pkg.scripts.start) {
                    startCommand = 'npm start';
                } else if (pkg.main) {
                    startCommand = `node ${pkg.main}`;
                }
            } catch (e) {
                // Fallback to npm start
            }
        }

        // Kill existing process on the port first
        exec(`lsof -ti:${lastPort} | xargs kill -9 2>/dev/null`, () => {
            // Also kill any node processes running in this directory
            exec(`ps aux | grep "node.*${directory}" | grep -v grep | awk '{print $2}' | xargs kill -9 2>/dev/null`, () => {
                // Wait a bit for processes to be killed
                setTimeout(() => {
                    // Start the process in the directory with PORT environment variable
                    const logFile = `/tmp/${path.basename(directory)}.log`;

                    // For Next.js apps, call next directly instead of through npm
                    let finalCommand = startCommand;
                    if (startCommand === 'npm start' || startCommand.includes('next')) {
                        // Check if package.json has next
                        const pkg = JSON.parse(content);
                        if (pkg.scripts && pkg.scripts.start && pkg.scripts.start.includes('next')) {
                            // Call next directly to avoid npm flag issues - use dev mode for instant start
                            finalCommand = `npx next dev -p ${lastPort}`;
                        } else {
                            finalCommand = `PORT=${lastPort} ${startCommand}`;
                        }
                    } else {
                        finalCommand = `PORT=${lastPort} ${startCommand}`;
                    }

                    // Use bash with NVM to ensure correct Node version
                    const bashCommand = `bash -c "source ~/.nvm/nvm.sh && cd '${directory}' && ${finalCommand} > ${logFile} 2>&1 &"`;
                    console.log(`[RESTART] Executing: ${finalCommand}`);

                    exec(bashCommand, (error, stdout, stderr) => {
                        if (error) {
                            console.log(`[RESTART] Error: ${error.message}`);
                            res.writeHead(500, { 'Content-Type': 'application/json' });
                            res.end(JSON.stringify({
                                error: 'Failed to restart',
                                message: stderr || error.message,
                                logFile: logFile
                            }));
                        } else {
                            console.log(`[RESTART] Command executed successfully`);
                            // Respond immediately - don't wait
                            res.writeHead(200, { 'Content-Type': 'application/json' });
                            res.end(JSON.stringify({
                                success: true,
                                message: `Restarted ${path.basename(directory)} on port ${lastPort}`,
                                directory: directory,
                                lastPort: lastPort,
                                openUrl: `http://localhost:${lastPort}`,
                                logFile: logFile
                            }));
                        }
                    });
                }, 500); // Reduced wait time
            });
        });
    });
}

async function sendDownAlert(directory, port, appName) {
    if (!emailConfig.enabled || !transporter) {
        console.log(`[Alert] Would send email: ${appName} (${directory}) on port ${port} is DOWN`);
        return;
    }

    const mailOptions = {
        from: '"Port Viewer Alert" <noreply@server.local>',
        to: emailConfig.recipient,
        subject: `🚨 ALERT: ${appName || 'Application'} is DOWN`,
        html: `
            <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
                <h2 style="color: #d63031; border-bottom: 3px solid #d63031; padding-bottom: 10px;">
                    🚨 Port Down Alert
                </h2>
                <div style="background: #fff3cd; padding: 20px; border-radius: 10px; margin: 20px 0;">
                    <h3 style="margin-top: 0;">Application Offline</h3>
                    <p><strong>Application:</strong> ${appName || 'Unknown'}</p>
                    <p><strong>Directory:</strong> <code>${directory}</code></p>
                    <p><strong>Port:</strong> ${port}</p>
                    <p><strong>Time:</strong> ${new Date().toLocaleString()}</p>
                </div>
                <p>The application has stopped responding and is no longer listening on its port.</p>
                <p style="color: #666; font-size: 12px; margin-top: 30px;">
                    This is an automated alert from Port Viewer monitoring system.
                </p>
            </div>
        `
    };

    try {
        await transporter.sendMail(mailOptions);
        console.log(`✉️ Email alert sent to ${emailConfig.recipient} for ${appName}`);
    } catch (error) {
        console.error(`Failed to send email alert:`, error.message);
    }
}

function checkPortChanges(currentPorts) {
    const currentState = {};

    // Build current state map
    currentPorts.forEach(port => {
        if (port.directory && port.isUserApp) {
            currentState[port.directory] = {
                isOnline: !port.isOffline,
                port: port.port,
                appName: port.appName
            };
        }
    });

    // Check for directories that went offline
    Object.keys(currentState).forEach(directory => {
        const current = currentState[directory];
        const previous = previousPortStates[directory];

        // If we have previous state and it was online but now is offline
        if (previous && previous.isOnline && !current.isOnline) {
            console.log(`⚠️ ALERT: ${current.appName} (${directory}) went offline!`);
            sendDownAlert(directory, current.port, current.appName);
        }
    });

    // Also check for directories that completely disappeared
    Object.keys(previousPortStates).forEach(directory => {
        if (!currentState[directory] && previousPortStates[directory].isOnline) {
            const prev = previousPortStates[directory];
            console.log(`⚠️ ALERT: ${prev.appName} (${directory}) disappeared!`);
            sendDownAlert(directory, prev.port, prev.appName);
        }
    });

    // Update previous state
    Object.keys(currentState).forEach(directory => {
        previousPortStates[directory] = currentState[directory];
    });
}

// Monitor ports every 60 seconds
setInterval(() => {
    exec('ss -tulpn 2>/dev/null | grep LISTEN', (error, stdout, stderr) => {
        if (error && !stdout) {
            exec('netstat -tulpn 2>/dev/null | grep LISTEN', (error2, stdout2, stderr2) => {
                if (!error2 || stdout2) {
                    enrichPortData(parseNetstat(stdout2), (enrichedPorts) => {
                        const allServices = mergeWithOfflineDirectories(enrichedPorts);
                        checkPortChanges(allServices);
                    });
                }
            });
        } else {
            enrichPortData(parseSS(stdout), (enrichedPorts) => {
                const allServices = mergeWithOfflineDirectories(enrichedPorts);
                checkPortChanges(allServices);
            });
        }
    });
}, 60000); // Check every 60 seconds

server.listen(PORT, '0.0.0.0', () => {
    console.log(`🚀 Port Viewer running at http://localhost:${PORT}`);
    console.log(`📊 View all active ports in your browser`);
    console.log(`📧 Email alerts enabled for: ${emailConfig.recipient}`);
    console.log(`⏱️  Port monitoring: Every 60 seconds`);
    console.log(`🌐 External access: http://${SERVER_IP}:${PORT}`);
});