#!/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); });