Files
EVOLV/scripts/deploy-and-trace.js
znetsixe 6a6c04d34b 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>
2026-03-04 21:07:04 +01:00

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);
});