#!/usr/bin/env node /** * Monitor WWTP runtime via Node-RED debug WebSocket and container logs. * Captures process data every few seconds and displays trends. */ const http = require('http'); const { execSync } = require('child_process'); const NR_URL = 'http://localhost:1880'; const SAMPLE_INTERVAL = 5000; // ms const NUM_SAMPLES = 12; // 60 seconds total function fetchJSON(url) { return new Promise((resolve, reject) => { http.get(url, res => { const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => { try { resolve(JSON.parse(Buffer.concat(chunks))); } catch (e) { reject(new Error('Parse: ' + e.message)); } }); }).on('error', reject); }); } function getRecentLogs(lines = 50) { try { return execSync('docker logs evolv-nodered --tail ' + lines + ' 2>&1', { encoding: 'utf8', timeout: 5000, }); } catch (e) { return 'Failed to get logs: ' + e.message; } } function parseSafeGuardLogs(logs) { const lines = logs.split('\n'); const safeGuards = []; const pressures = []; const others = []; lines.forEach(line => { const volMatch = line.match(/Safe guard triggered: vol=([-\d.]+) m3/); if (volMatch) { safeGuards.push(parseFloat(volMatch[1])); } const pressMatch = line.match(/New f =([\d.]+) is constrained/); if (pressMatch) { pressures.push(parseFloat(pressMatch[1])); } if (line.includes('_controlLevelBased') || line.includes('Mode changed') || line.includes('execSequence') || line.includes('startup') || line.includes('shutdown') || line.includes('setMode')) { others.push(line.trim().substring(0, 200)); } }); return { safeGuards, pressures, others }; } (async () => { console.log('=== WWTP Runtime Monitor ==='); console.log('Capturing ' + NUM_SAMPLES + ' samples at ' + (SAMPLE_INTERVAL/1000) + 's intervals\n'); // Wait for nodes to initialize after deploy console.log('Waiting 10s for nodes to initialize...\n'); await new Promise(r => setTimeout(r, 10000)); for (let i = 0; i < NUM_SAMPLES; i++) { const elapsed = (i * SAMPLE_INTERVAL / 1000 + 10).toFixed(0); console.log('--- Sample ' + (i+1) + '/' + NUM_SAMPLES + ' (t=' + elapsed + 's after deploy) ---'); // Capture container logs (last 30 lines since last sample) const logs = getRecentLogs(30); const parsed = parseSafeGuardLogs(logs); if (parsed.safeGuards.length > 0) { const latest = parsed.safeGuards[parsed.safeGuards.length - 1]; const trend = parsed.safeGuards.length > 1 ? (parsed.safeGuards[parsed.safeGuards.length-1] - parsed.safeGuards[0] > 0 ? 'RISING' : 'FALLING') : 'STABLE'; console.log(' SAFETY: vol=' + latest.toFixed(2) + ' m3 (' + parsed.safeGuards.length + ' triggers, ' + trend + ')'); } else { console.log(' SAFETY: No safe guard triggers (GOOD)'); } if (parsed.pressures.length > 0) { const avg = parsed.pressures.reduce((a,b) => a+b, 0) / parsed.pressures.length; console.log(' PRESSURE CLAMP: avg f=' + avg.toFixed(0) + ' (' + parsed.pressures.length + ' warnings)'); } else { console.log(' PRESSURE: No interpolation warnings (GOOD)'); } if (parsed.others.length > 0) { console.log(' CONTROL: ' + parsed.others.slice(-3).join('\n ')); } // Check if there are state change or mode messages const logLines = logs.split('\n'); const stateChanges = logLines.filter(l => l.includes('machine state') || l.includes('State:') || l.includes('draining') || l.includes('filling') || l.includes('q_in') || l.includes('netFlow') ); if (stateChanges.length > 0) { console.log(' STATE: ' + stateChanges.slice(-3).map(s => s.trim().substring(0, 150)).join('\n ')); } console.log(''); if (i < NUM_SAMPLES - 1) { await new Promise(r => setTimeout(r, SAMPLE_INTERVAL)); } } // Final log dump console.log('\n=== Final Log Analysis (last 200 lines) ==='); const finalLogs = getRecentLogs(200); const finalParsed = parseSafeGuardLogs(finalLogs); console.log('Safe guard triggers: ' + finalParsed.safeGuards.length); if (finalParsed.safeGuards.length > 0) { console.log(' First vol: ' + finalParsed.safeGuards[0].toFixed(2) + ' m3'); console.log(' Last vol: ' + finalParsed.safeGuards[finalParsed.safeGuards.length-1].toFixed(2) + ' m3'); const delta = finalParsed.safeGuards[finalParsed.safeGuards.length-1] - finalParsed.safeGuards[0]; console.log(' Delta: ' + (delta > 0 ? '+' : '') + delta.toFixed(2) + ' m3 (' + (delta > 0 ? 'RECOVERING' : 'STILL DRAINING') + ')'); } console.log('Pressure clamp warnings: ' + finalParsed.pressures.length); if (finalParsed.pressures.length > 0) { const min = Math.min(...finalParsed.pressures); const max = Math.max(...finalParsed.pressures); console.log(' Range: ' + min.toFixed(0) + ' - ' + max.toFixed(0)); } console.log('\nControl events: ' + finalParsed.others.length); finalParsed.others.slice(-10).forEach(l => console.log(' ' + l)); // Overall assessment console.log('\n=== ASSESSMENT ==='); if (finalParsed.safeGuards.length === 0 && finalParsed.pressures.length === 0) { console.log('HEALTHY: No safety triggers, no pressure warnings'); } else if (finalParsed.safeGuards.length > 0) { const trend = finalParsed.safeGuards[finalParsed.safeGuards.length-1] - finalParsed.safeGuards[0]; if (trend > 0) { console.log('RECOVERING: Volume rising but still negative'); } else { console.log('CRITICAL: Volume still dropping - control issue persists'); } } else if (finalParsed.pressures.length > 0) { console.log('WARNING: Pressure values exceeding curve bounds'); } })().catch(err => { console.error('Monitor failed:', err); process.exit(1); });