- Update all submodule URLs from gitea.centraal.wbd-rd.nl to gitea.wbd-rd.nl - Add settler as proper submodule in .gitmodules - Add agent skills, function anchors, decisions, and improvements - Add Docker configuration and scripts - Add manuals and third_party docs - Update .gitignore with secrets and build artifacts - Remove stale .tgz build artifact Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
5.9 KiB
JavaScript
159 lines
5.9 KiB
JavaScript
#!/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);
|
|
});
|