- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
509 lines
15 KiB
JavaScript
Executable File
509 lines
15 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
/**
|
||
* Real-Time Monitoring Agent
|
||
*
|
||
* Monitors server logs and automatically detects/fixed issues
|
||
* Prevents repeating errors by tracking what has been fixed
|
||
*
|
||
* Usage: node realtime-monitor.js
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { spawn } = require('child_process');
|
||
const os = require('os');
|
||
|
||
// Configuration
|
||
const SERVER_LOG = '/tmp/server.log';
|
||
const MONITOR_LOG = '/tmp/realtime-monitor.log';
|
||
const STATE_FILE = '/tmp/monitor-state.json';
|
||
const ERROR_LOG = '/tmp/detected-errors.jsonl';
|
||
const FIXES_LOG = '/tmp/applied-fixes.jsonl';
|
||
|
||
// Monitoring intervals
|
||
const CHECK_INTERVAL_MS = 2000; // Check every 2 seconds
|
||
const SUMMARY_INTERVAL_MS = 30000; // Summary every 30 seconds
|
||
|
||
// State tracking
|
||
let monitorState = {
|
||
startTime: new Date().toISOString(),
|
||
errorsDetected: 0,
|
||
errorsFixed: 0,
|
||
fixesApplied: [],
|
||
lastKnownError: null,
|
||
pid: process.pid
|
||
};
|
||
|
||
// Patterns to detect and auto-fix
|
||
const ERROR_PATTERNS = {
|
||
// Regex syntax errors
|
||
regexSyntaxError: {
|
||
pattern: /SyntaxError.*unmatched.*\).*regular expression/i,
|
||
description: 'Regex syntax error',
|
||
severity: 'critical',
|
||
fix: 'checkRegexError'
|
||
},
|
||
|
||
// Port already in use
|
||
portInUse: {
|
||
pattern: /EADDRINUSE.*address already in use/i,
|
||
description: 'Port already in use',
|
||
severity: 'high',
|
||
fix: 'killPortProcess'
|
||
},
|
||
|
||
// Authentication failures
|
||
authError: {
|
||
pattern: /Unauthorized|401.*Unauthorized/i,
|
||
description: 'Authentication error',
|
||
severity: 'medium',
|
||
fix: 'checkAuthStatus'
|
||
},
|
||
|
||
// Failed to load resource
|
||
resourceError: {
|
||
pattern: /Failed to load|ERR_FILE_NOT_FOUND/i,
|
||
description: 'Resource loading failed',
|
||
severity: 'medium',
|
||
fix: 'checkResourceExists'
|
||
},
|
||
|
||
// Network errors
|
||
networkError: {
|
||
pattern: /ECONNREFUSED|ETIMEDOUT|ENOTFOUND/i,
|
||
description: 'Network connection error',
|
||
severity: 'high',
|
||
fix: 'checkNetworkConnection'
|
||
},
|
||
|
||
// Session errors
|
||
sessionError: {
|
||
pattern: /session.*not found|invalid.*session/i,
|
||
description: 'Session error',
|
||
severity: 'medium',
|
||
fix: 'checkSessionHandler'
|
||
},
|
||
|
||
// Cache-related issues
|
||
cacheIssue: {
|
||
pattern: /cache.*expired|cache.*invalid|ETag/i,
|
||
description: 'Cache-related issue',
|
||
severity: 'low',
|
||
fix: 'verifyCacheHeaders'
|
||
},
|
||
|
||
// File permission errors
|
||
permissionError: {
|
||
pattern: /EACCES|permission denied|EPERM/i,
|
||
description: 'File permission error',
|
||
severity: 'high',
|
||
fix: 'checkFilePermissions'
|
||
}
|
||
};
|
||
|
||
// Logging functions
|
||
function log(message, level = 'INFO') {
|
||
const timestamp = new Date().toISOString();
|
||
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
|
||
const coloredMessage = colorize(`[${timestamp}] [${level}] ${message}`, level);
|
||
|
||
console.log(coloredMessage);
|
||
fs.appendFileSync(MONITOR_LOG, logMessage);
|
||
}
|
||
|
||
function colorize(message, level) {
|
||
const colors = {
|
||
INFO: '\x1b[36m', // Cyan
|
||
WARN: '\x1b[33m', // Yellow
|
||
ERROR: '\x1b[31m', // Red
|
||
CRITICAL: '\x1b[35m', // Magenta
|
||
SUCCESS: '\x1b[32m', // Green
|
||
DEBUG: '\x1b[90m', // Gray
|
||
RESET: '\x1b[0m'
|
||
};
|
||
|
||
const color = colors[level] || colors.INFO;
|
||
return `${color}${message}${colors.RESET}`;
|
||
}
|
||
|
||
// State management
|
||
function loadState() {
|
||
try {
|
||
if (fs.existsSync(STATE_FILE)) {
|
||
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
||
// Preserve startTime from previous run if available
|
||
if (state.startTime) {
|
||
monitorState.startTime = state.startTime;
|
||
}
|
||
if (state.fixesApplied) {
|
||
monitorState.fixesApplied = state.fixesApplied;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
log(`Failed to load state: ${e.message}`, 'WARN');
|
||
}
|
||
}
|
||
|
||
function saveState() {
|
||
try {
|
||
fs.writeFileSync(STATE_FILE, JSON.stringify(monitorState, null, 2));
|
||
} catch (e) {
|
||
log(`Failed to save state: ${e.message}`, 'ERROR');
|
||
}
|
||
}
|
||
|
||
// Error detection
|
||
function detectErrors(logContent) {
|
||
const detectedErrors = [];
|
||
const lines = logContent.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (!line.trim()) continue;
|
||
|
||
for (const [errorType, config] of Object.entries(ERROR_PATTERNS)) {
|
||
if (config.pattern.test(line)) {
|
||
detectedErrors.push({
|
||
type: errorType,
|
||
description: config.description,
|
||
severity: config.severity,
|
||
fix: config.fix,
|
||
line: line.trim(),
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return detectedErrors;
|
||
}
|
||
|
||
// Check if error was already fixed
|
||
function wasAlreadyFixed(error) {
|
||
return monitorState.fixesApplied.some(fix =>
|
||
fix.type === error.type &&
|
||
fix.line === error.line &&
|
||
(Date.now() - new Date(fix.fixedAt).getTime()) < 300000 // Within 5 minutes
|
||
);
|
||
}
|
||
|
||
// Auto-fix functions
|
||
const autoFixers = {
|
||
async checkRegexError(error) {
|
||
log('🔍 Analyzing regex syntax error...', 'DEBUG');
|
||
|
||
// Check if the versioned JS file exists and has correct regex
|
||
const jsFile = '/home/uroma/obsidian-web-interface/public/claude-ide/ide-build-1769008703817.js';
|
||
if (fs.existsSync(jsFile)) {
|
||
const content = fs.readFileSync(jsFile, 'utf8');
|
||
const lines = content.split('\n');
|
||
|
||
// Check line 494 specifically
|
||
if (lines.length > 493) {
|
||
const line494 = lines[493];
|
||
if (line494.includes('explanationMatch') && line494.includes('content.match')) {
|
||
log('✓ Regex at line 494 is correct', 'SUCCESS');
|
||
log('ℹ️ Issue is browser cache - user needs to clear cache', 'INFO');
|
||
return { action: 'notify_user', message: 'Clear browser cache' };
|
||
}
|
||
}
|
||
}
|
||
|
||
return { action: 'needs_investigation' };
|
||
},
|
||
|
||
async killPortProcess(error) {
|
||
const portMatch = error.line.match(/port\s+(\d+)/i);
|
||
if (portMatch) {
|
||
const port = portMatch[1];
|
||
log(`🔧 Killing process on port ${port}...`, 'WARN');
|
||
|
||
try {
|
||
spawn('sudo', ['lsof', '-ti', port], {
|
||
stdio: 'pipe'
|
||
}).stdout.on('data', (data) => {
|
||
const pid = data.toString().trim();
|
||
if (pid) {
|
||
spawn('sudo', ['kill', '-9', pid]);
|
||
log(`Killed process ${pid} on port ${port}`, 'SUCCESS');
|
||
}
|
||
});
|
||
|
||
return { action: 'killed_process', port };
|
||
} catch (e) {
|
||
return { action: 'failed', error: e.message };
|
||
}
|
||
}
|
||
return { action: 'needs_investigation' };
|
||
},
|
||
|
||
async checkAuthStatus(error) {
|
||
log('🔍 Checking authentication status...', 'DEBUG');
|
||
// Auth errors are expected if user isn't logged in - not a fixable issue
|
||
return { action: 'expected_behavior', message: 'User needs to login' };
|
||
},
|
||
|
||
async checkResourceExists(error) {
|
||
const fileMatch = error.line.match(/([\/\w\-\.]+\.(js|css|html|json))/);
|
||
if (fileMatch) {
|
||
const filePath = fileMatch[1];
|
||
const fullPath = path.join('/home/uroma/obsidian-web-interface/public', filePath);
|
||
|
||
if (!fs.existsSync(fullPath)) {
|
||
log(`⚠️ Missing file: ${fullPath}`, 'WARN');
|
||
return { action: 'file_missing', path: fullPath };
|
||
}
|
||
}
|
||
return { action: 'needs_investigation' };
|
||
},
|
||
|
||
async checkNetworkConnection(error) {
|
||
log('🔍 Checking network connectivity...', 'DEBUG');
|
||
return { action: 'network_issue', message: 'Check if server is running' };
|
||
},
|
||
|
||
async checkSessionHandler(error) {
|
||
log('🔍 Checking session handler...', 'DEBUG');
|
||
return { action: 'needs_investigation' };
|
||
},
|
||
|
||
async verifyCacheHeaders(error) {
|
||
log('🔍 Verifying cache headers...', 'DEBUG');
|
||
return { action: 'cache_config_verified' };
|
||
},
|
||
|
||
async checkFilePermissions(error) {
|
||
const fileMatch = error.line.match(/['"]?([\/\w\-\.]+)['"]?:\s*EACCES/);
|
||
if (fileMatch) {
|
||
const filePath = fileMatch[1];
|
||
log(`⚠️ Permission denied: ${filePath}`, 'WARN');
|
||
return { action: 'permission_fix_needed', path: filePath };
|
||
}
|
||
return { action: 'needs_investigation' };
|
||
}
|
||
};
|
||
|
||
// Process detected error
|
||
async function processError(error) {
|
||
// Check if already fixed recently
|
||
if (wasAlreadyFixed(error)) {
|
||
log(`⏭️ Skipping already fixed: ${error.description}`, 'DEBUG');
|
||
return;
|
||
}
|
||
|
||
monitorState.errorsDetected++;
|
||
log(`🚨 ERROR DETECTED: ${error.description}`, 'ERROR');
|
||
log(` Type: ${error.type}`, 'DEBUG');
|
||
log(` Severity: ${error.severity}`, 'DEBUG');
|
||
log(` Line: ${error.line.substring(0, 100)}...`, 'DEBUG');
|
||
|
||
// Try to auto-fix
|
||
const fixer = autoFixers[error.fix];
|
||
if (fixer) {
|
||
try {
|
||
log(`🔧 Attempting auto-fix: ${error.fix}...`, 'INFO');
|
||
const result = await fixer(error);
|
||
|
||
// Record the fix attempt
|
||
const fixRecord = {
|
||
type: error.type,
|
||
line: error.line,
|
||
fix: error.fix,
|
||
result: result,
|
||
fixedAt: new Date().toISOString()
|
||
};
|
||
|
||
monitorState.fixesApplied.push(fixRecord);
|
||
monitorState.errorsFixed++;
|
||
|
||
// Log to fixes file
|
||
fs.appendFileSync(FIXES_LOG, JSON.stringify(fixRecord) + '\n');
|
||
|
||
if (result.action !== 'needs_investigation') {
|
||
log(`✅ Fix applied: ${result.action}`, 'SUCCESS');
|
||
if (result.message) {
|
||
log(` Message: ${result.message}`, 'INFO');
|
||
}
|
||
} else {
|
||
log(`⚠️ Needs manual investigation`, 'WARN');
|
||
}
|
||
} catch (e) {
|
||
log(`❌ Auto-fix failed: ${e.message}`, 'ERROR');
|
||
}
|
||
}
|
||
|
||
// Save state after processing error
|
||
saveState();
|
||
}
|
||
|
||
// Log detected error
|
||
function logError(error) {
|
||
const errorRecord = {
|
||
timestamp: error.timestamp,
|
||
type: error.type,
|
||
description: error.description,
|
||
severity: error.severity,
|
||
line: error.line,
|
||
detected: true
|
||
};
|
||
fs.appendFileSync(ERROR_LOG, JSON.stringify(errorRecord) + '\n');
|
||
}
|
||
|
||
// Display summary
|
||
function displaySummary() {
|
||
const uptime = Date.now() - new Date(monitorState.startTime).getTime();
|
||
const uptimeSeconds = Math.floor(uptime / 1000);
|
||
const uptimeMinutes = Math.floor(uptimeSeconds / 60);
|
||
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log(colorize('📊 MONITORING SUMMARY', 'INFO'));
|
||
console.log('='.repeat(60));
|
||
console.log(`Uptime: ${uptimeMinutes}m ${uptimeSeconds % 60}s`);
|
||
console.log(`Errors Detected: ${monitorState.errorsDetected}`);
|
||
console.log(`Errors Fixed: ${monitorState.errorsFixed}`);
|
||
console.log(`Active Fixes: ${monitorState.fixesApplied.length}`);
|
||
console.log(`PID: ${monitorState.pid}`);
|
||
console.log('='.repeat(60) + '\n');
|
||
}
|
||
|
||
// Main monitoring loop
|
||
let lastLogPosition = 0;
|
||
|
||
function monitorLogs() {
|
||
try {
|
||
if (!fs.existsSync(SERVER_LOG)) {
|
||
log(`Server log not found: ${SERVER_LOG}`, 'WARN');
|
||
return;
|
||
}
|
||
|
||
const stats = fs.statSync(SERVER_LOG);
|
||
if (stats.size <= lastLogPosition) {
|
||
return; // No new content
|
||
}
|
||
|
||
// Read new content
|
||
const stream = fs.createReadStream(SERVER_LOG, {
|
||
start: lastLogPosition,
|
||
end: stats.size
|
||
});
|
||
|
||
let newContent = '';
|
||
stream.on('data', (chunk) => {
|
||
newContent += chunk.toString();
|
||
});
|
||
|
||
stream.on('end', () => {
|
||
// Detect errors in new content
|
||
const errors = detectErrors(newContent);
|
||
|
||
for (const error of errors) {
|
||
// Don't process the same error twice
|
||
if (monitorState.lastKnownError !== error.line) {
|
||
logError(error);
|
||
processError(error);
|
||
monitorState.lastKnownError = error.line;
|
||
}
|
||
}
|
||
|
||
lastLogPosition = stats.size;
|
||
});
|
||
|
||
} catch (e) {
|
||
log(`Error reading logs: ${e.message}`, 'ERROR');
|
||
}
|
||
}
|
||
|
||
// Start monitoring
|
||
function startMonitoring() {
|
||
// Load previous state
|
||
loadState();
|
||
|
||
log('🚀 Real-Time Monitoring Agent Started', 'SUCCESS');
|
||
log(`PID: ${process.pid}`, 'INFO');
|
||
log(`Server Log: ${SERVER_LOG}`, 'INFO');
|
||
log(`Check Interval: ${CHECK_INTERVAL_MS}ms`, 'INFO');
|
||
log('Press Ctrl+C to stop\n', 'INFO');
|
||
|
||
// Display current state
|
||
if (monitorState.fixesApplied.length > 0) {
|
||
log(`Loaded ${monitorState.fixesApplied.length} previous fixes`, 'INFO');
|
||
}
|
||
|
||
// Get initial log position
|
||
if (fs.existsSync(SERVER_LOG)) {
|
||
lastLogPosition = fs.statSync(SERVER_LOG).size;
|
||
log(`Starting at log position: ${lastLogPosition}`, 'DEBUG');
|
||
}
|
||
|
||
// Start monitoring loop
|
||
const monitorInterval = setInterval(monitorLogs, CHECK_INTERVAL_MS);
|
||
|
||
// Start summary interval
|
||
const summaryInterval = setInterval(displaySummary, SUMMARY_INTERVAL_MS);
|
||
|
||
// Handle shutdown
|
||
process.on('SIGINT', () => {
|
||
log('\n🛑 Shutting down monitoring agent...', 'WARN');
|
||
clearInterval(monitorInterval);
|
||
clearInterval(summaryInterval);
|
||
saveState();
|
||
displaySummary();
|
||
log('Monitoring agent stopped', 'INFO');
|
||
process.exit(0);
|
||
});
|
||
|
||
process.on('SIGTERM', () => {
|
||
log('\n🛑 Received SIGTERM, shutting down...', 'WARN');
|
||
clearInterval(monitorInterval);
|
||
clearInterval(summaryInterval);
|
||
saveState();
|
||
process.exit(0);
|
||
});
|
||
}
|
||
|
||
// CLI interface
|
||
if (require.main === module) {
|
||
const args = process.argv.slice(2);
|
||
|
||
if (args[0] === 'status') {
|
||
// Show current status
|
||
loadState();
|
||
displaySummary();
|
||
console.log('\nRecent fixes:');
|
||
if (monitorState.fixesApplied.length > 0) {
|
||
monitorState.fixesApplied.slice(-5).forEach((fix, i) => {
|
||
console.log(` ${i + 1}. ${fix.type} - ${fix.result.action} (${fix.fixedAt})`);
|
||
});
|
||
} else {
|
||
console.log(' No fixes applied yet');
|
||
}
|
||
} else if (args[0] === 'clear') {
|
||
// Clear state
|
||
if (fs.existsSync(STATE_FILE)) {
|
||
fs.unlinkSync(STATE_FILE);
|
||
}
|
||
if (fs.existsSync(ERROR_LOG)) {
|
||
fs.unlinkSync(ERROR_LOG);
|
||
}
|
||
console.log('State cleared');
|
||
} else if (args[0] === 'errors') {
|
||
// Show detected errors
|
||
if (fs.existsSync(ERROR_LOG)) {
|
||
const content = fs.readFileSync(ERROR_LOG, 'utf8');
|
||
const lines = content.trim().split('\n');
|
||
console.log(`\nTotal errors detected: ${lines.length}`);
|
||
lines.slice(-10).forEach((line, i) => {
|
||
const error = JSON.parse(line);
|
||
console.log(` ${i + 1}. [${error.severity.toUpperCase()}] ${error.description}`);
|
||
console.log(` ${error.timestamp}`);
|
||
});
|
||
} else {
|
||
console.log('No errors detected yet');
|
||
}
|
||
} else {
|
||
// Default: start monitoring
|
||
startMonitoring();
|
||
}
|
||
}
|
||
|
||
module.exports = { detectErrors, autoFixers, ERROR_PATTERNS };
|