- 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>
218 lines
7.1 KiB
JavaScript
218 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Deploy the demo flow fresh and trace the first 60 seconds of behavior.
|
|
* Captures: container logs, PS volume evolution, flow events.
|
|
*/
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
const NR_URL = 'http://localhost:1880';
|
|
const FLOW_FILE = path.join(__dirname, '..', 'docker', 'demo-flow.json');
|
|
const TRACE_SECONDS = 45;
|
|
|
|
function httpReq(method, urlPath, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const parsed = new URL(NR_URL + urlPath);
|
|
const opts = {
|
|
hostname: parsed.hostname,
|
|
port: parsed.port,
|
|
path: parsed.pathname,
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Node-RED-Deployment-Type': 'full',
|
|
},
|
|
};
|
|
if (body) {
|
|
const buf = Buffer.from(JSON.stringify(body));
|
|
opts.headers['Content-Length'] = buf.length;
|
|
}
|
|
const req = http.request(opts, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (c) => chunks.push(c));
|
|
res.on('end', () => {
|
|
const text = Buffer.concat(chunks).toString();
|
|
resolve({ status: res.statusCode, body: text });
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
if (body) req.write(JSON.stringify(body));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function getLogs(since) {
|
|
try {
|
|
// Get ALL logs since our timestamp
|
|
const cmd = `docker logs evolv-nodered --since ${since} 2>&1`;
|
|
return execSync(cmd, { encoding: 'utf8', timeout: 5000 });
|
|
} catch (e) {
|
|
return 'Log error: ' + e.message;
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
console.log('=== Deploy & Trace ===');
|
|
console.log('Loading flow from', FLOW_FILE);
|
|
|
|
const flow = JSON.parse(fs.readFileSync(FLOW_FILE, 'utf8'));
|
|
console.log(`Flow has ${flow.length} nodes`);
|
|
|
|
// Deploy
|
|
const deployTime = new Date().toISOString();
|
|
console.log(`\nDeploying at ${deployTime}...`);
|
|
const res = await httpReq('POST', '/flows', flow);
|
|
console.log(`Deploy response: ${res.status}`);
|
|
|
|
if (res.status !== 204 && res.status !== 200) {
|
|
console.error('Deploy failed:', res.body);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Wait 3 seconds for initial setup
|
|
console.log('Waiting 3s for init...\n');
|
|
await new Promise((r) => setTimeout(r, 3000));
|
|
|
|
// Trace loop
|
|
const traceStart = Date.now();
|
|
const volumeHistory = [];
|
|
let lastLogPos = 0;
|
|
|
|
for (let i = 0; i < Math.ceil(TRACE_SECONDS / 3); i++) {
|
|
const elapsed = ((Date.now() - traceStart) / 1000).toFixed(1);
|
|
|
|
// Get new logs since deploy
|
|
const logs = getLogs(deployTime);
|
|
const newLines = logs.split('\n').slice(lastLogPos);
|
|
lastLogPos = logs.split('\n').length;
|
|
|
|
// Parse interesting log lines
|
|
const safeGuards = [];
|
|
const pressureChanges = [];
|
|
const modeChanges = [];
|
|
const stateChanges = [];
|
|
const other = [];
|
|
|
|
newLines.forEach((line) => {
|
|
if (!line.trim()) return;
|
|
|
|
const volMatch = line.match(/vol=([-\d.]+) m3.*remainingTime=([\w.]+)/);
|
|
if (volMatch) {
|
|
safeGuards.push({ vol: parseFloat(volMatch[1]), remaining: volMatch[2] });
|
|
return;
|
|
}
|
|
|
|
if (line.includes('Pressure change detected')) {
|
|
pressureChanges.push(1);
|
|
return;
|
|
}
|
|
|
|
if (line.includes('Mode changed') || line.includes('setMode') || line.includes('Control mode')) {
|
|
modeChanges.push(line.trim().substring(0, 200));
|
|
return;
|
|
}
|
|
|
|
if (line.includes('machine state') || line.includes('State:') || line.includes('startup') || line.includes('shutdown')) {
|
|
stateChanges.push(line.trim().substring(0, 200));
|
|
return;
|
|
}
|
|
|
|
if (line.includes('q_in') || line.includes('netflow') || line.includes('Volume') ||
|
|
line.includes('Height') || line.includes('Level') || line.includes('Controllevel')) {
|
|
other.push(line.trim().substring(0, 200));
|
|
return;
|
|
}
|
|
});
|
|
|
|
console.log(`--- t=${elapsed}s ---`);
|
|
|
|
if (safeGuards.length > 0) {
|
|
const latest = safeGuards[safeGuards.length - 1];
|
|
const first = safeGuards[0];
|
|
console.log(` SAFETY: ${safeGuards.length} triggers, vol: ${first.vol} → ${latest.vol} m3, remaining: ${latest.remaining}s`);
|
|
volumeHistory.push({ t: parseFloat(elapsed), vol: latest.vol });
|
|
} else {
|
|
console.log(' SAFETY: none (good)');
|
|
}
|
|
|
|
if (pressureChanges.length > 0) {
|
|
console.log(` PRESSURE: ${pressureChanges.length} changes`);
|
|
}
|
|
|
|
if (modeChanges.length > 0) {
|
|
modeChanges.forEach((m) => console.log(` MODE: ${m}`));
|
|
}
|
|
|
|
if (stateChanges.length > 0) {
|
|
stateChanges.slice(-5).forEach((s) => console.log(` STATE: ${s}`));
|
|
}
|
|
|
|
if (other.length > 0) {
|
|
other.slice(-5).forEach((o) => console.log(` INFO: ${o}`));
|
|
}
|
|
|
|
console.log('');
|
|
await new Promise((r) => setTimeout(r, 3000));
|
|
}
|
|
|
|
// Final analysis
|
|
console.log('\n=== Volume Trajectory ===');
|
|
volumeHistory.forEach((v) => {
|
|
const bar = '#'.repeat(Math.max(0, Math.round(v.vol / 2)));
|
|
console.log(` t=${String(v.t).padStart(5)}s: ${String(v.vol.toFixed(2)).padStart(8)} m3 ${bar}`);
|
|
});
|
|
|
|
if (volumeHistory.length >= 2) {
|
|
const first = volumeHistory[0];
|
|
const last = volumeHistory[volumeHistory.length - 1];
|
|
const dt = last.t - first.t;
|
|
const dv = last.vol - first.vol;
|
|
const rate = dt > 0 ? (dv / dt * 3600).toFixed(1) : 'N/A';
|
|
console.log(`\n Rate: ${rate} m3/h (${dv > 0 ? 'FILLING' : 'DRAINING'})`);
|
|
}
|
|
|
|
// Get ALL logs for comprehensive analysis
|
|
console.log('\n=== Full Log Analysis ===');
|
|
const allLogs = getLogs(deployTime);
|
|
const allLines = allLogs.split('\n');
|
|
|
|
// Count different message types
|
|
const counts = { safety: 0, pressure: 0, mode: 0, state: 0, error: 0, warn: 0, flow: 0 };
|
|
allLines.forEach((l) => {
|
|
if (l.includes('Safe guard')) counts.safety++;
|
|
if (l.includes('Pressure change')) counts.pressure++;
|
|
if (l.includes('Mode') || l.includes('mode')) counts.mode++;
|
|
if (l.includes('startup') || l.includes('shutdown') || l.includes('machine state')) counts.state++;
|
|
if (l.includes('[ERROR]') || l.includes('Error')) counts.error++;
|
|
if (l.includes('[WARN]')) counts.warn++;
|
|
if (l.includes('netflow') || l.includes('q_in') || l.includes('flow')) counts.flow++;
|
|
});
|
|
|
|
console.log('Message counts:', JSON.stringify(counts, null, 2));
|
|
|
|
// Print errors
|
|
const errors = allLines.filter((l) => l.includes('[ERROR]') || l.includes('Error'));
|
|
if (errors.length > 0) {
|
|
console.log('\nErrors:');
|
|
errors.slice(0, 20).forEach((e) => console.log(' ' + e.trim().substring(0, 200)));
|
|
}
|
|
|
|
// Print first few non-pressure, non-safety lines
|
|
console.log('\nKey events (first 30):');
|
|
let keyCount = 0;
|
|
allLines.forEach((l) => {
|
|
if (keyCount >= 30) return;
|
|
if (l.includes('Pressure change detected')) return;
|
|
if (l.includes('Safe guard triggered')) return;
|
|
if (!l.trim()) return;
|
|
console.log(' ' + l.trim().substring(0, 200));
|
|
keyCount++;
|
|
});
|
|
})().catch((err) => {
|
|
console.error('Failed:', err);
|
|
process.exit(1);
|
|
});
|