Migrate to new Gitea instance (gitea.wbd-rd.nl)

- 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>
This commit is contained in:
znetsixe
2026-03-04 21:07:04 +01:00
parent fbd9e6ec11
commit 6a6c04d34b
169 changed files with 21332 additions and 1512 deletions

158
scripts/monitor-runtime.js Normal file
View File

@@ -0,0 +1,158 @@
#!/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);
});