← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-log-monitor/log-monitor-agent.ts.backup-oauth-20251112
946 lines
import cookieParser from "cookie-parser";
/**
* DW-Agents: Log Monitor Agent
*
* Automated log analysis and error detection:
* - Continuous monitoring of all PM2 logs
* - Error pattern detection
* - Automatic task creation for issues
* - Alert escalation
* - Real-time dashboard
*
* Port: 7239
*/
import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import session from 'express-session';
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { requireAuth as ssoAuth, handleLogin, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from '../shared-auth';
import cookieParser from 'cookie-parser';
import { chatMiddleware } from '../shared-chat-integration';
const { getUniversalHeader, getThemeStyles } = require('../shared-ui-components.js');
const execAsync = promisify(exec);
const app = express();
// Global Authentication
const PORT = 7239;
// Global Authentication - MUST come before any other middleware
app.use(cookieParser());
// Add inter-agent chat widget (after auth)
app.use(chatMiddleware({
agentId: 'agent-log-monitor',
agentName: 'Log Monitor',
port: 7239,
category: 'agent'
}));
// Paths
const LOGS_DIR = '/root/DW-Agents/logs';
const CLAUDE_DEBUG_DIR = '/root/.claude/debug';
const ISSUES_FILE = path.join(__dirname, 'detected-issues.json');
const TASKS_FILE = path.join(__dirname, '../tasks-database.json');
// Anthropic Claude
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || '';
const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
// Error patterns to detect
const ERROR_PATTERNS = [
{ pattern: /error|exception|fatal|crash|failed/i, severity: 'high', category: 'error' },
{ pattern: /warn|warning/i, severity: 'medium', category: 'warning' },
{ pattern: /ECONNREFUSED|ETIMEDOUT|ENOTFOUND/i, severity: 'high', category: 'network' },
{ pattern: /Cannot find module|MODULE_NOT_FOUND/i, severity: 'critical', category: 'dependency' },
{ pattern: /Port \d+ is already in use/i, severity: 'critical', category: 'port_conflict' },
{ pattern: /EADDRINUSE/i, severity: 'critical', category: 'port_conflict' },
{ pattern: /Memory usage|heap out of memory/i, severity: 'critical', category: 'memory' },
{ pattern: /401|403|500|502|503|504/i, severity: 'high', category: 'http_error' },
{ pattern: /restart/i, severity: 'medium', category: 'restart' },
{ pattern: /Unhandled|UnhandledPromiseRejection/i, severity: 'high', category: 'unhandled' },
];
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
declare module 'express-session' {
interface SessionData {
authenticated: boolean;
}
}
app.use(
session({
secret: 'dw-agents-log-monitor-2025',
resave: false,
saveUninitialized: true,
rolling: true,
cookie: {
secure: false,
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
},
})
);
// Authentication
app.post('/login', handleLogin);
const requireAuth = (req: Request, res: Response, next: any) => {
if (req.cookies[SSO_TOKEN_NAME] === SSO_TOKEN_VALUE || req.session.authenticated) {
next();
} else {
res.redirect('/login');
}
};
app.get('/login', (req: Request, res: Response) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Log Monitor Agent - Login</title>
${getThemeStyles('modern-minimalist')}
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.login-box {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
width: 300px;
}
h2 { margin-top: 0; color: #333; }
input {
width: 100%;
padding: 12px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 12px;
background: #f5576c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover { background: #e04555; }
</style>
</head>
<body>
<div class="login-box">
<h2>📋 Log Monitor Agent</h2>
<form method="POST" action="/login">
<input type="password" name="password" placeholder="Password" required autofocus />
<button type="submit">Login</button>
</form>
</div>
</body>
</html>
`);
});
// Store detected issues
interface DetectedIssue {
id: string;
timestamp: string;
agent: string;
severity: 'critical' | 'high' | 'medium' | 'low';
category: string;
message: string;
logFile: string;
taskCreated: boolean;
taskId?: string;
resolved: boolean;
}
let detectedIssues: DetectedIssue[] = [];
// Load existing issues
function loadIssues() {
if (fs.existsSync(ISSUES_FILE)) {
try {
detectedIssues = JSON.parse(fs.readFileSync(ISSUES_FILE, 'utf-8'));
} catch (e) {
detectedIssues = [];
}
}
}
function saveIssues() {
fs.writeFileSync(ISSUES_FILE, JSON.stringify(detectedIssues, null, 2));
}
loadIssues();
// Analyze log entry
function analyzeLogEntry(logLine: string, agentName: string, logFile: string): DetectedIssue | null {
for (const pattern of ERROR_PATTERNS) {
if (pattern.pattern.test(logLine)) {
return {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
agent: agentName,
severity: pattern.severity as any,
category: pattern.category,
message: logLine.substring(0, 500),
logFile,
taskCreated: false,
resolved: false,
};
}
}
return null;
}
// Create task in orchestrator
async function createTask(issue: DetectedIssue) {
const task = {
id: `task-${issue.id}`,
title: `[${issue.severity.toUpperCase()}] ${issue.category} in ${issue.agent}`,
description: `Automated detection from log monitor:\n\n${issue.message}\n\nLog: ${issue.logFile}`,
status: 'pending',
priority: issue.severity === 'critical' ? 'critical' : issue.severity === 'high' ? 'high' : 'medium',
assignedAgent: determineAgent(issue),
createdAt: new Date().toISOString(),
autoGenerated: true,
source: 'log-monitor',
issueId: issue.id,
};
// Save to tasks database
let tasks: any[] = [];
if (fs.existsSync(TASKS_FILE)) {
try {
const data = JSON.parse(fs.readFileSync(TASKS_FILE, 'utf-8'));
tasks = data.tasks || [];
} catch (e) {
tasks = [];
}
}
tasks.push(task);
fs.writeFileSync(TASKS_FILE, JSON.stringify({ tasks }, null, 2));
issue.taskCreated = true;
issue.taskId = task.id;
console.log(`✅ Created task ${task.id} for ${issue.agent} - ${issue.category}`);
return task.id;
}
// Determine which agent should handle the issue
function determineAgent(issue: DetectedIssue): string {
if (issue.severity === 'critical') return 'needs-attention';
if (issue.category === 'port_conflict' || issue.category === 'memory') return 'server-uptime';
if (issue.agent === 'claude-code') return 'task-orchestrator'; // Claude Code errors go to orchestrator
if (issue.agent.includes('shopify')) return 'shopify-store';
if (issue.agent.includes('purchasing')) return 'purchasing-office';
if (issue.agent.includes('marketing')) return 'marketing';
if (issue.agent.includes('zendesk')) return 'zendesk-chat';
if (issue.agent.includes('accounting')) return 'accounting';
if (issue.agent.includes('legal')) return 'legal-team';
return 'task-orchestrator'; // Default
}
// Monitor logs continuously
async function monitorLogs() {
try {
// Monitor DW-Agents logs
const logFiles = fs.readdirSync(LOGS_DIR).filter(f => f.endsWith('.log'));
for (const logFile of logFiles) {
const filePath = path.join(LOGS_DIR, logFile);
const agentName = logFile.replace('-error.log', '').replace('-out.log', '');
// Read last 100 lines
try {
const { stdout } = await execAsync(`tail -n 100 "${filePath}"`);
const lines = stdout.split('\n').filter(l => l.trim());
for (const line of lines) {
const issue = analyzeLogEntry(line, agentName, logFile);
if (issue) {
// Check if we already detected this recently (avoid duplicates)
const recentDuplicate = detectedIssues.find(
i => i.agent === issue.agent &&
i.category === issue.category &&
i.message === issue.message &&
Date.now() - new Date(i.timestamp).getTime() < 5 * 60 * 1000 // 5 min
);
if (!recentDuplicate) {
detectedIssues.push(issue);
console.log(`🚨 Detected ${issue.severity} issue in ${issue.agent}: ${issue.category}`);
// Create task for high and critical issues
if (issue.severity === 'high' || issue.severity === 'critical') {
await createTask(issue);
}
}
}
}
} catch (error) {
// Skip if file doesn't exist or can't be read
}
}
// Monitor Claude Code debug logs
try {
if (fs.existsSync(CLAUDE_DEBUG_DIR)) {
const claudeDebugFiles = fs.readdirSync(CLAUDE_DEBUG_DIR)
.filter(f => f.endsWith('.txt'))
.sort((a, b) => {
const statA = fs.statSync(path.join(CLAUDE_DEBUG_DIR, a));
const statB = fs.statSync(path.join(CLAUDE_DEBUG_DIR, b));
return statB.mtimeMs - statA.mtimeMs;
})
.slice(0, 10); // Check only 10 most recent files
for (const debugFile of claudeDebugFiles) {
const filePath = path.join(CLAUDE_DEBUG_DIR, debugFile);
const stats = fs.statSync(filePath);
// Only check files modified in last hour
if (Date.now() - stats.mtimeMs < 60 * 60 * 1000) {
try {
const { stdout } = await execAsync(`tail -n 200 "${filePath}"`);
const lines = stdout.split('\n').filter(l => l.trim());
for (const line of lines) {
const issue = analyzeLogEntry(line, 'claude-code', `debug/${debugFile}`);
if (issue) {
const recentDuplicate = detectedIssues.find(
i => i.agent === 'claude-code' &&
i.category === issue.category &&
i.message === issue.message &&
Date.now() - new Date(i.timestamp).getTime() < 5 * 60 * 1000
);
if (!recentDuplicate) {
detectedIssues.push(issue);
console.log(`🚨 Detected ${issue.severity} Claude Code issue: ${issue.category}`);
// Create task for high and critical issues
if (issue.severity === 'high' || issue.severity === 'critical') {
await createTask(issue);
}
}
}
}
} catch (error) {
// Skip if file can't be read
}
}
}
}
} catch (error) {
// Claude debug dir might not exist
}
// Keep only last 1000 issues
if (detectedIssues.length > 1000) {
detectedIssues = detectedIssues.slice(-1000);
}
saveIssues();
} catch (error) {
console.error('Error monitoring logs:', error);
}
}
// Check PM2 process status
async function checkPM2Status() {
try {
const { stdout } = await execAsync('pm2 jlist');
const processes = JSON.parse(stdout);
for (const proc of processes) {
// Check for frequent restarts
if (proc.pm2_env.restart_time > 5 && proc.pm2_env.uptime < 60000) {
const issue: DetectedIssue = {
id: `pm2-${Date.now()}`,
timestamp: new Date().toISOString(),
agent: proc.name,
severity: 'critical',
category: 'frequent_restarts',
message: `Process ${proc.name} has restarted ${proc.pm2_env.restart_time} times recently`,
logFile: 'pm2',
taskCreated: false,
resolved: false,
};
detectedIssues.push(issue);
await createTask(issue);
}
// Check memory usage
if (proc.monit && proc.monit.memory > 400 * 1024 * 1024) { // > 400MB
const issue: DetectedIssue = {
id: `mem-${Date.now()}`,
timestamp: new Date().toISOString(),
agent: proc.name,
severity: 'high',
category: 'high_memory',
message: `Process ${proc.name} using ${Math.round(proc.monit.memory / 1024 / 1024)}MB memory`,
logFile: 'pm2',
taskCreated: false,
resolved: false,
};
const recentDuplicate = detectedIssues.find(
i => i.agent === issue.agent && i.category === 'high_memory' &&
Date.now() - new Date(i.timestamp).getTime() < 10 * 60 * 1000
);
if (!recentDuplicate) {
detectedIssues.push(issue);
await createTask(issue);
}
}
}
saveIssues();
} catch (error) {
console.error('Error checking PM2 status:', error);
}
}
// Start monitoring interval
setInterval(monitorLogs, 30000); // Every 30 seconds
setInterval(checkPM2Status, 60000); // Every 1 minute
// Initial scan
monitorLogs();
checkPM2Status();
// Dashboard
app.get('/', requireAuth, (req: Request, res: Response) => {
const criticalCount = detectedIssues.filter(i => !i.resolved && i.severity === 'critical').length;
const highCount = detectedIssues.filter(i => !i.resolved && i.severity === 'high').length;
const mediumCount = detectedIssues.filter(i => !i.resolved && i.severity === 'medium').length;
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Log Monitor Agent - Dashboard</title>
${getThemeStyles('modern-minimalist')}
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
}
.header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header h1 { font-size: 24px; margin-bottom: 10px; }
.header p { font-size: 14px; opacity: 0.9; }
.stats {
display: flex;
gap: 20px;
padding: 20px;
flex-wrap: wrap;
}
.stat-box {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
min-width: 150px;
text-align: center;
}
.stat-box h3 {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.stat-box p {
font-size: 32px;
font-weight: bold;
}
.stat-box.critical p { color: #e74c3c; }
.stat-box.high p { color: #e67e22; }
.stat-box.medium p { color: #f39c12; }
.stat-box.total p { color: #3498db; }
.content {
margin: 20px;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
font-size: 13px;
}
th {
background: #f9f9f9;
font-weight: 600;
}
tr:hover { background: #fafafa; }
.severity {
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.severity.critical {
background: #fee;
color: #e74c3c;
}
.severity.high {
background: #fef3e0;
color: #e67e22;
}
.severity.medium {
background: #fef9e0;
color: #f39c12;
}
.severity.low {
background: #e8f5e9;
color: #27ae60;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
background: #3498db;
color: white;
}
.badge.task { background: #27ae60; }
.badge.resolved { background: #95a5a6; }
.btn {
padding: 6px 12px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn:hover { background: #2980b9; }
.btn.resolve { background: #27ae60; }
.btn.resolve:hover { background: #229954; }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#scroll-to-top:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
}
</style>
</head>
<body style="padding: 20px;">
${getUniversalHeader('Log Monitor', PORT, 5)}
<div class="header" style="margin-top: 20px;">
<h1>📋 Log Monitor Agent</h1>
<p>Real-time log analysis and automated issue detection</p>
<p>Monitoring: DW-Agents logs (/root/DW-Agents/logs) + Claude Code debug logs (/root/.claude/debug)</p>
</div>
<div class="stats">
<div class="stat-box critical">
<h3>Critical Issues</h3>
<p>${criticalCount}</p>
</div>
<div class="stat-box high">
<h3>High Priority</h3>
<p>${highCount}</p>
</div>
<div class="stat-box medium">
<h3>Medium Priority</h3>
<p>${mediumCount}</p>
</div>
<div class="stat-box total">
<h3>Total Detected</h3>
<p>${detectedIssues.length}</p>
</div>
</div>
<div class="content">
<h2>Recent Issues <span id="issue-count" style="color: #666; font-size: 14px;">(Loading...)</span></h2>
<div id="table-container" style="max-height: 600px; overflow-y: auto; position: relative;">
<table id="issues-table">
<thead style="position: sticky; top: 0; background: white; z-index: 10;">
<tr>
<th>Time</th>
<th>Agent</th>
<th>Severity</th>
<th>Category</th>
<th>Message</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="issues-tbody">
<!-- Issues will be loaded dynamically -->
</tbody>
</table>
<div id="loading-indicator" style="display: none; text-align: center; padding: 20px; color: #666;">
<div style="display: inline-block; width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></div>
<p style="margin-top: 10px;">Loading issues...</p>
</div>
<!-- Pagination Controls -->
<div id="pagination-controls" style="display: flex; justify-content: center; align-items: center; padding: 30px; gap: 20px;">
<button id="prev-btn" onclick="loadPreviousPage()"
style="padding: 12px 30px; background: #3498db; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: bold; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(52,152,219,0.3);">
← Back
</button>
<div id="page-info" style="font-size: 16px; color: #666; font-weight: 500;">
Page 1
</div>
<button id="next-btn" onclick="loadNextPage()"
style="padding: 12px 30px; background: #3498db; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: bold; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(52,152,219,0.3);">
Next →
</button>
</div>
</div>
</div>
<button id="scroll-to-top" style="display: none; position: fixed; bottom: 30px; right: 30px; background: #3498db; color: white; border: none; padding: 15px 20px; border-radius: 50px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); font-size: 14px; font-weight: bold; z-index: 1000; transition: all 0.3s ease;">
↑ Back to Top
</button>
<script>
// Pagination state management
let currentOffset = 0;
let isLoading = false;
let hasMore = true;
let totalIssues = 0;
const LIMIT = 250;
let loadedIssueIds = new Set(); // Track loaded issues to prevent duplicates
// Create issue row HTML
function createIssueRow(issue) {
return \`
<tr data-issue-id="\${issue.id}">
<td>\${new Date(issue.timestamp).toLocaleString()}</td>
<td><strong>\${issue.agent}</strong></td>
<td><span class="severity \${issue.severity}">\${issue.severity}</span></td>
<td>\${issue.category}</td>
<td style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">\${issue.message}</td>
<td>
\${issue.resolved ? '<span class="badge resolved">Resolved</span>' : ''}
\${issue.taskCreated ? \`<span class="badge task">Task \${issue.taskId}</span>\` : ''}
</td>
<td>
\${!issue.resolved ? \`<button class="btn resolve" onclick="resolveIssue('\${issue.id}')">Mark Resolved</button>\` : ''}
\${!issue.taskCreated ? \`<button class="btn" onclick="createTask('\${issue.id}')">Create Task</button>\` : ''}
</td>
</tr>
\`;
}
// Load issues from API
async function loadIssues() {
if (isLoading) return;
isLoading = true;
const loadingIndicator = document.getElementById('loading-indicator');
loadingIndicator.style.display = 'block';
try {
const response = await fetch(\`/api/issues?limit=\${LIMIT}&offset=\${currentOffset}\`);
const data = await response.json();
const tbody = document.getElementById('issues-tbody');
tbody.innerHTML = '';
loadedIssueIds.clear();
// Add rows
data.issues.forEach(issue => {
if (!loadedIssueIds.has(issue.id)) {
loadedIssueIds.add(issue.id);
tbody.insertAdjacentHTML('beforeend', createIssueRow(issue));
}
});
// Update state
hasMore = data.hasMore;
totalIssues = data.total;
// Update pagination controls
updatePaginationControls();
// Update counter
const start = currentOffset + 1;
const end = Math.min(currentOffset + data.issues.length, totalIssues);
document.getElementById('issue-count').textContent =
\`(Showing \${start}-\${end} of \${totalIssues})\`;
loadingIndicator.style.display = 'none';
// Scroll to top of table
document.querySelector('.card').scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (error) {
console.error('Error loading issues:', error);
loadingIndicator.innerHTML = '<p style="color: #e74c3c;">Error loading issues</p>';
} finally {
isLoading = false;
}
}
// Update pagination button states
function updatePaginationControls() {
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const pageInfo = document.getElementById('page-info');
const currentPage = Math.floor(currentOffset / LIMIT) + 1;
const totalPages = Math.ceil(totalIssues / LIMIT);
pageInfo.textContent = \`Page \${currentPage} of \${totalPages}\`;
// Disable/enable buttons
prevBtn.disabled = currentOffset === 0;
nextBtn.disabled = !hasMore;
// Style disabled buttons
prevBtn.style.opacity = prevBtn.disabled ? '0.5' : '1';
prevBtn.style.cursor = prevBtn.disabled ? 'not-allowed' : 'pointer';
nextBtn.style.opacity = nextBtn.disabled ? '0.5' : '1';
nextBtn.style.cursor = nextBtn.disabled ? 'not-allowed' : 'pointer';
}
// Load next page
function loadNextPage() {
if (!hasMore || isLoading) return;
currentOffset += LIMIT;
loadIssues();
}
// Load previous page
function loadPreviousPage() {
if (currentOffset === 0 || isLoading) return;
currentOffset = Math.max(0, currentOffset - LIMIT);
loadIssues();
}
// Removed infinite scroll - now using pagination buttons
// Scroll to top button functionality
const scrollToTopBtn = document.getElementById('scroll-to-top');
const tableContainer = document.getElementById('table-container');
tableContainer.addEventListener('scroll', () => {
if (tableContainer.scrollTop > 300) {
scrollToTopBtn.style.display = 'block';
} else {
scrollToTopBtn.style.display = 'none';
}
});
scrollToTopBtn.addEventListener('click', () => {
tableContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// Handle resolve and create task actions
function resolveIssue(id) {
fetch('/api/resolve/' + id, { method: 'POST' })
.then(() => {
// Update UI without full reload
const row = document.querySelector(\`tr[data-issue-id="\${id}"]\`);
if (row) {
const statusCell = row.cells[5];
statusCell.innerHTML = '<span class="badge resolved">Resolved</span>';
const actionsCell = row.cells[6];
// Remove "Mark Resolved" button
actionsCell.innerHTML = actionsCell.innerHTML.replace(/<button[^>]*Mark Resolved<\\/button>/, '');
// Change "Create Task" button from blue to green
const createTaskBtn = actionsCell.querySelector('button');
if (createTaskBtn && createTaskBtn.textContent.includes('Create Task')) {
createTaskBtn.classList.add('resolve'); // Add green class
createTaskBtn.style.background = '#27ae60'; // Green color
}
}
});
}
function createTask(id) {
fetch('/api/create-task/' + id, { method: 'POST' })
.then(res => res.json())
.then(data => {
if (data.success) {
// Update UI without full reload
const row = document.querySelector(\`tr[data-issue-id="\${id}"]\`);
if (row) {
const statusCell = row.cells[5];
statusCell.innerHTML += \`<span class="badge task">Task \${data.taskId}</span>\`;
const actionsCell = row.cells[6];
actionsCell.innerHTML = actionsCell.innerHTML.replace(/<button[^>]*Create Task<\\/button>/, '');
}
}
});
}
// Auto-refresh for new issues (check every 30 seconds)
let lastTotalIssues = 0;
setInterval(async () => {
try {
const response = await fetch('/api/issues?limit=1&offset=0');
const data = await response.json();
// If new issues detected, reload from beginning but maintain scroll
if (data.total > lastTotalIssues && lastTotalIssues > 0) {
const scrollPos = tableContainer.scrollTop;
currentOffset = 0;
hasMore = true;
// Reload first page
await loadIssues(false);
// Try to restore scroll position
tableContainer.scrollTop = scrollPos;
console.log(\`New issues detected: \${data.total - lastTotalIssues}\`);
}
lastTotalIssues = data.total;
} catch (error) {
console.error('Error checking for new issues:', error);
}
}, 30000);
// Initial load
loadIssues(false).then(() => {
lastTotalIssues = totalIssues;
});
</script>
</body>
</html>
`);
});
// API: Get issues (paginated for infinite scroll)
app.get('/api/issues', requireAuth, (req: Request, res: Response) => {
const limit = parseInt(req.query.limit as string) || 250;
const offset = parseInt(req.query.offset as string) || 0;
// Return issues in reverse chronological order (newest first)
const allIssues = [...detectedIssues].reverse();
const paginatedIssues = allIssues.slice(offset, offset + limit);
res.json({
issues: paginatedIssues,
total: detectedIssues.length,
hasMore: offset + limit < detectedIssues.length,
offset,
limit
});
});
// API: Resolve issue
app.post('/api/resolve/:id', requireAuth, (req: Request, res: Response) => {
const issue = detectedIssues.find(i => i.id === req.params.id);
if (issue) {
issue.resolved = true;
saveIssues();
}
res.json({ success: true });
});
// API: Create task manually
app.post('/api/create-task/:id', requireAuth, async (req: Request, res: Response) => {
const issue = detectedIssues.find(i => i.id === req.params.id);
if (issue && !issue.taskCreated) {
await createTask(issue);
saveIssues();
res.json({ success: true, taskId: issue.taskId });
} else {
res.json({ success: false });
}
});
// Health check
app.get('/health', (req: Request, res: Response) => {
res.json({
status: 'healthy',
agent: 'log-monitor',
port: PORT,
issuesDetected: detectedIssues.length,
unresolvedCritical: detectedIssues.filter(i => !i.resolved && i.severity === 'critical').length,
timestamp: new Date().toISOString()
});
});
// Start server
// Metrics endpoint for daily reporting
app.get("/api/metrics", (req: Request, res: Response) => {
res.json({
status: "online",
uptime: process.uptime(),
responseTime: 0,
tasksCompleted: 0,
errorsToday: 0,
lastActivity: new Date().toISOString(),
qnaReadiness: 100
});
});
app.listen(PORT, () => {
console.log(`📋 Log Monitor Agent running on port ${PORT}`);
console.log(`📊 Dashboard: http://localhost:${PORT}`);
console.log(`🔍 Monitoring ${LOGS_DIR}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully...');
saveIssues();
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully...');
saveIssues();
process.exit(0);
});