From 10ad20f495e7fa5bc320f0a76b6f1d4a4fc91c2e Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 8 Feb 2026 13:20:09 -0500 Subject: [PATCH] fix(10.2-03): use JSON-serialized top-level key for n8n static data persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n8n $getWorkflowStaticData only reliably tracks top-level property changes. Deep nested mutations (staticData.errorLog.debug.enabled = true) are not persisted between executions. Fix: serialize errorLog as JSON string in staticData._errorLog — every read does JSON.parse, every write does JSON.stringify as a top-level assignment. Fixed in: Process Debug Command, Log Error, Log Trace, 6 inline trace nodes, Parse Callback Data callback routing trace. Co-Authored-By: Claude Opus 4.6 --- n8n-workflow.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/n8n-workflow.json b/n8n-workflow.json index 18fbf74..8b141a7 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -678,7 +678,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture callback routing\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'callback-routing',\n node: 'Parse Callback Data',\n data: {\n callbackData: $json.callback_query?.data || 'unknown',\n parsedPrefix: ($json.callback_query?.data || '').split(':')[0]\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n}\n\n// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp} or bstop:confirm:{names}:{timestamp}:kb\n const parts = rest.split(':'); // confirm, names, timestamp[, kb]\n if (parts[0] === 'confirm' && parts.length >= 3) {\n // Check for :kb marker (indicates inline keyboard origin)\n const fromKeyboard = parts[parts.length - 1] === 'kb';\n const timestampIdx = fromKeyboard ? parts.length - 2 : parts.length - 1;\n const timestamp = parseInt(parts[timestampIdx]);\n const namesStr = parts.slice(1, timestampIdx).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n fromKeyboard: fromKeyboard,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n\n\nif (rawData === 'batch:mode' || rawData.startsWith('batch:mode:')) {\n // Enter batch selection mode with optional page: batch:mode or batch:mode:{page}\n let page = 0;\n if (rawData.startsWith('batch:mode:')) {\n page = parseInt(rawData.substring(11)) || 0;\n }\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: true,\n batchPage: page,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:nav:')) {\n // Navigate batch selection with preserved selection: batch:nav:{page}:{selected_csv}\n const parts = rawData.substring(10).split(':'); // Remove 'batch:nav:'\n const page = parseInt(parts[0]) || 0;\n const selectedCsv = parts.slice(1).join(':') || '';\n const selectedCount = selectedCsv ? selectedCsv.split(',').length : 0;\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchNav: true,\n batchPage: page,\n selectedCsv: selectedCsv,\n selectedCount: selectedCount,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{page}:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const page = parseInt(parts[0]) || 0; // Current page\n const selectedCsv = parts[1] || ''; // Currently selected (comma-separated)\n const toggleName = parts.slice(2).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\n batchPage: page,\n selectedCsv: selectedCsv === '' ? '' : selectedCsv,\n toggleName: toggleName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:exec:')) {\n // Execute batch action: batch:exec:{action}:{selected_csv}\n const parts = rawData.substring(11).split(':'); // Remove 'batch:exec:'\n const action = parts[0]; // update/start/stop/restart\n const selectedCsv = parts.slice(1).join(':'); // Comma-separated names\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n action: action,\n selectedCsv: selectedCsv,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:clear') {\n // Clear selection\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchClear: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:cancel') {\n // Cancel batch mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchCancel: true,\n isCancel: false,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('uall:confirm:')) {\n // Update all confirmation: uall:confirm:{timestamp}\n const parts = rawData.substring(13).split(':'); // Remove 'uall:confirm:'\n const timestamp = parseInt(parts[0]);\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAll: true,\n confirmAction: 'updateall',\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n isConfirm: false\n }\n };\n}\n\nif (rawData === 'uall:cancel') {\n // Update all cancel\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAllCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n isConfirm: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\n }\n};" + "jsCode": "// Debug trace: capture callback routing\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'callback-routing',\n node: 'Parse Callback Data',\n data: {\n callbackData: $json.callback_query?.data || 'unknown',\n parsedPrefix: ($json.callback_query?.data || '').split(':')[0]\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n}\n\n// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp} or bstop:confirm:{names}:{timestamp}:kb\n const parts = rest.split(':'); // confirm, names, timestamp[, kb]\n if (parts[0] === 'confirm' && parts.length >= 3) {\n // Check for :kb marker (indicates inline keyboard origin)\n const fromKeyboard = parts[parts.length - 1] === 'kb';\n const timestampIdx = fromKeyboard ? parts.length - 2 : parts.length - 1;\n const timestamp = parseInt(parts[timestampIdx]);\n const namesStr = parts.slice(1, timestampIdx).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n fromKeyboard: fromKeyboard,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n\n\nif (rawData === 'batch:mode' || rawData.startsWith('batch:mode:')) {\n // Enter batch selection mode with optional page: batch:mode or batch:mode:{page}\n let page = 0;\n if (rawData.startsWith('batch:mode:')) {\n page = parseInt(rawData.substring(11)) || 0;\n }\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: true,\n batchPage: page,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:nav:')) {\n // Navigate batch selection with preserved selection: batch:nav:{page}:{selected_csv}\n const parts = rawData.substring(10).split(':'); // Remove 'batch:nav:'\n const page = parseInt(parts[0]) || 0;\n const selectedCsv = parts.slice(1).join(':') || '';\n const selectedCount = selectedCsv ? selectedCsv.split(',').length : 0;\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchNav: true,\n batchPage: page,\n selectedCsv: selectedCsv,\n selectedCount: selectedCount,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{page}:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const page = parseInt(parts[0]) || 0; // Current page\n const selectedCsv = parts[1] || ''; // Currently selected (comma-separated)\n const toggleName = parts.slice(2).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\n batchPage: page,\n selectedCsv: selectedCsv === '' ? '' : selectedCsv,\n toggleName: toggleName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:exec:')) {\n // Execute batch action: batch:exec:{action}:{selected_csv}\n const parts = rawData.substring(11).split(':'); // Remove 'batch:exec:'\n const action = parts[0]; // update/start/stop/restart\n const selectedCsv = parts.slice(1).join(':'); // Comma-separated names\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n action: action,\n selectedCsv: selectedCsv,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:clear') {\n // Clear selection\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchClear: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:cancel') {\n // Cancel batch mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchCancel: true,\n isCancel: false,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('uall:confirm:')) {\n // Update all confirmation: uall:confirm:{timestamp}\n const parts = rawData.substring(13).split(':'); // Remove 'uall:confirm:'\n const timestamp = parseInt(parts[0]);\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAll: true,\n confirmAction: 'updateall',\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n isConfirm: false\n }\n };\n}\n\nif (rawData === 'uall:cancel') {\n // Update all cancel\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAllCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n isConfirm: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\n }\n};" }, "id": "code-parse-callback", "name": "Parse Callback Data", @@ -2026,7 +2026,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n staticData.errorLog.debug.executionCount = (staticData.errorLog.debug.executionCount || 0) + 1;\n if (staticData.errorLog.debug.executionCount > 100) {\n staticData.errorLog.debug.enabled = false;\n } else {\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Immediate Action',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n }\n}\n\n// Build action completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F ${containerName} started`,\n restart: `\\u{1F504} ${containerName} restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} ${containerName}`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" + "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n _traceLog.debug.executionCount = (_traceLog.debug.executionCount || 0) + 1;\n if (_traceLog.debug.executionCount > 100) {\n _traceLog.debug.enabled = false;\n staticData._errorLog = JSON.stringify(_traceLog);\n } else {\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Immediate Action',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n }\n}\n\n// Build action completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F ${containerName} started`,\n restart: `\\u{1F504} ${containerName} restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} ${containerName}`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" }, "id": "code-format-immediate-result", "name": "Format Immediate Result", @@ -3379,7 +3379,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n staticData.errorLog.debug.executionCount = (staticData.errorLog.debug.executionCount || 0) + 1;\n if (staticData.errorLog.debug.executionCount > 100) {\n staticData.errorLog.debug.enabled = false;\n } else {\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Container Action',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n }\n}\n\n// Handle sub-workflow result for text command path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst chatId = result.chatId;\n\nreturn {\n json: {\n success: success,\n chatId: chatId,\n text: message\n }\n};" + "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n _traceLog.debug.executionCount = (_traceLog.debug.executionCount || 0) + 1;\n if (_traceLog.debug.executionCount > 100) {\n _traceLog.debug.enabled = false;\n staticData._errorLog = JSON.stringify(_traceLog);\n } else {\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Container Action',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n }\n}\n\n// Handle sub-workflow result for text command path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst chatId = result.chatId;\n\nreturn {\n json: {\n success: success,\n chatId: chatId,\n text: message\n }\n};" }, "id": "code-handle-text-result-c6ha90fh", "name": "Handle Text Action Result", @@ -3427,7 +3427,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n staticData.errorLog.debug.executionCount = (staticData.errorLog.debug.executionCount || 0) + 1;\n if (staticData.errorLog.debug.executionCount > 100) {\n staticData.errorLog.debug.enabled = false;\n } else {\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Inline Action',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n }\n}\n\n// Handle sub-workflow result for inline keyboard path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst action = result.action;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'action:' + action + ':' + containerName }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" + "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n _traceLog.debug.executionCount = (_traceLog.debug.executionCount || 0) + 1;\n if (_traceLog.debug.executionCount > 100) {\n _traceLog.debug.enabled = false;\n staticData._errorLog = JSON.stringify(_traceLog);\n } else {\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Inline Action',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n }\n}\n\n// Handle sub-workflow result for inline keyboard path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst action = result.action;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'action:' + action + ':' + containerName }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" }, "id": "code-handle-inline-result-x19h97t3", "name": "Handle Inline Action Result", @@ -3471,7 +3471,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n staticData.errorLog.debug.executionCount = (staticData.errorLog.debug.executionCount || 0) + 1;\n if (staticData.errorLog.debug.executionCount > 100) {\n staticData.errorLog.debug.enabled = false;\n } else {\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Update',\n node: 'Execute Batch Update',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n }\n}\n\n// Handle update result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: 'update',\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" + "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n _traceLog.debug.executionCount = (_traceLog.debug.executionCount || 0) + 1;\n if (_traceLog.debug.executionCount > 100) {\n _traceLog.debug.enabled = false;\n staticData._errorLog = JSON.stringify(_traceLog);\n } else {\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Update',\n node: 'Execute Batch Update',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n }\n}\n\n// Handle update result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: 'update',\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" }, "id": "f5576e75-8cfa-4ac3-9fea-64fae8d31bec", "name": "Handle Batch Update Result", @@ -3515,7 +3515,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n staticData.errorLog.debug.executionCount = (staticData.errorLog.debug.executionCount || 0) + 1;\n if (staticData.errorLog.debug.executionCount > 100) {\n staticData.errorLog.debug.enabled = false;\n } else {\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Batch Action Sub-workflow',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n }\n}\n\n// Handle action result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: data.action,\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" + "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n _traceLog.debug.executionCount = (_traceLog.debug.executionCount || 0) + 1;\n if (_traceLog.debug.executionCount > 100) {\n _traceLog.debug.enabled = false;\n staticData._errorLog = JSON.stringify(_traceLog);\n } else {\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Actions',\n node: 'Execute Batch Action Sub-workflow',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n }\n}\n\n// Handle action result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: data.action,\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" }, "id": "dc8db8cb-3ba8-471d-98df-c47dcdb4e6ea", "name": "Handle Batch Action Result Sub", @@ -3590,7 +3590,7 @@ }, { "parameters": { - "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.errorLog?.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!staticData.errorLog.traces) {\n staticData.errorLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n staticData.errorLog.debug.executionCount = (staticData.errorLog.debug.executionCount || 0) + 1;\n if (staticData.errorLog.debug.executionCount > 100) {\n staticData.errorLog.debug.enabled = false;\n } else {\n const traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Logs',\n node: 'Execute Inline Logs',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n staticData.errorLog.traces.buffer.push(traceEntry);\n if (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n }\n staticData.errorLog.traces.nextId++;\n }\n}\n\n// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\nconst containerName = result.containerName;\n\n// Add timestamp to prevent 'message not modified' error on refresh\nconst timestamp = new Date().toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n});\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\n// Append timestamp to message\nconst messageWithTimestamp = result.message + `\\n\\nUpdated: ${timestamp}`;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: messageWithTimestamp,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" + "jsCode": "// Debug trace: capture sub-workflow boundary\nconst staticData = $getWorkflowStaticData('global');\nconst _traceLog = JSON.parse(staticData._errorLog || '{}');\nif (_traceLog.debug?.enabled) {\n const MAX_TRACES = 50;\n if (!_traceLog.traces) {\n _traceLog.traces = { buffer: [], nextId: 1 };\n }\n\n // Auto-disable after 100 executions\n _traceLog.debug.executionCount = (_traceLog.debug.executionCount || 0) + 1;\n if (_traceLog.debug.executionCount > 100) {\n _traceLog.debug.enabled = false;\n staticData._errorLog = JSON.stringify(_traceLog);\n } else {\n const traceEntry = {\n id: `trace_${String(_traceLog.traces.nextId).padStart(3, '0')}`,\n correlationId: $input.item.json.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: 'sub-workflow-call',\n workflow: 'Container Logs',\n node: 'Execute Inline Logs',\n data: {\n output: {\n success: $input.item.json.success,\n action: $input.item.json.action || 'unknown',\n hasError: !!$input.item.json.error\n }\n }\n };\n\n _traceLog.traces.buffer.push(traceEntry);\n if (_traceLog.traces.buffer.length > MAX_TRACES) {\n _traceLog.traces.buffer.shift();\n }\n _traceLog.traces.nextId++;\n staticData._errorLog = JSON.stringify(_traceLog);\n }\n}\n\n// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\nconst containerName = result.containerName;\n\n// Add timestamp to prevent 'message not modified' error on refresh\nconst timestamp = new Date().toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n});\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\n// Append timestamp to message\nconst messageWithTimestamp = result.message + `\\n\\nUpdated: ${timestamp}`;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: messageWithTimestamp,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" }, "id": "b1800598-1ff6-4da3-8506-4e4e8127f902", "name": "Format Inline Logs Result", @@ -4908,7 +4908,7 @@ }, { "parameters": { - "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst input = $input.item.json;\nconst command = (input.message?.text || '').toLowerCase().trim();\nconst chatId = input.message.chat.id;\n\n// Initialize static data structure if missing\nif (!staticData.errorLog) {\n staticData.errorLog = {\n debug: { enabled: false, executionCount: 0 },\n errors: { buffer: [], nextId: 1, count: 0, lastCleared: new Date().toISOString() },\n traces: { buffer: [], nextId: 1 }\n };\n}\n\nlet text = '';\n\n// Parse command and arguments\nconst parts = command.split(' ');\nconst cmd = parts[0];\nconst arg1 = parts[1] || '';\n\nif (cmd === '/errors') {\n // Show recent errors (default 5, max 50)\n const count = parseInt(arg1) || 5;\n const requestedCount = Math.min(count, 50);\n const errors = staticData.errorLog.errors.buffer || [];\n const recentErrors = errors.slice(-requestedCount).reverse();\n \n if (recentErrors.length === 0) {\n text = 'No errors recorded.';\n } else {\n text = `Recent Errors (${recentErrors.length})\\n\\n`;\n recentErrors.forEach(err => {\n const timestamp = new Date(err.timestamp).toISOString().replace('T', ' ').substring(0, 19);\n text += `#${err.id} - ${timestamp}\\n`;\n text += `Workflow: ${err.workflow} > ${err.node}\\n`;\n text += `
${err.userMessage}
\\n`;\n if (err.error?.httpCode) {\n text += `HTTP: ${err.error.httpCode}\\n`;\n }\n text += '\\n';\n });\n \n text += `Total errors: ${staticData.errorLog.errors.count}\\n`;\n text += `Debug mode: ${staticData.errorLog.debug.enabled ? 'ON' : 'OFF'}`;\n }\n \n} else if (cmd === '/clear' || cmd === '/clear-errors') {\n // Clear error buffer\n const count = staticData.errorLog.errors.buffer.length;\n staticData.errorLog.errors.buffer = [];\n staticData.errorLog.errors.nextId = 1;\n staticData.errorLog.errors.lastCleared = new Date().toISOString();\n text = `Error log cleared. ${count} entries removed.`;\n \n} else if (cmd === '/debug') {\n // Toggle debug mode\n if (arg1 === 'on') {\n staticData.errorLog.debug.enabled = true;\n staticData.errorLog.debug.executionCount = 0;\n text = 'Debug mode enabled.\\nTracing sub-workflow boundaries and callback routing.';\n } else if (arg1 === 'off') {\n staticData.errorLog.debug.enabled = false;\n text = 'Debug mode disabled.';\n } else {\n // Status (default)\n const debug = staticData.errorLog.debug;\n const errors = staticData.errorLog.errors;\n const traces = staticData.errorLog.traces;\n text = `Debug Status\\n\\n`;\n text += `Debug mode: ${debug.enabled ? 'ON' : 'OFF'}\\n`;\n text += `Execution count: ${debug.executionCount || 0}\\n`;\n text += `Error buffer: ${errors.buffer.length} entries\\n`;\n text += `Trace buffer: ${traces.buffer.length} entries\\n`;\n text += `Total errors: ${errors.count}`;\n }\n \n} else if (cmd === '/trace') {\n // Trace by correlation ID\n const correlationId = arg1;\n \n if (!correlationId) {\n text = 'Usage: /trace <correlationId>';\n } else {\n const errors = staticData.errorLog.errors.buffer || [];\n const traces = staticData.errorLog.traces.buffer || [];\n \n // Find all entries matching correlation ID\n const errorMatches = errors.filter(e => e.correlationId === correlationId);\n const traceMatches = traces.filter(t => t.correlationId === correlationId);\n \n // Combine and sort chronologically\n const allMatches = [...errorMatches, ...traceMatches].sort((a, b) => \n new Date(a.timestamp) - new Date(b.timestamp)\n );\n \n if (allMatches.length === 0) {\n text = `No entries found for correlation ID: ${correlationId}`;\n } else {\n text = `Trace for ${correlationId}\\n\\n`;\n allMatches.forEach(entry => {\n const timestamp = new Date(entry.timestamp).toISOString().replace('T', ' ').substring(0, 19);\n const type = entry.id.startsWith('err') ? 'ERROR' : 'TRACE';\n text += `[${type}] ${timestamp}\\n`;\n text += `${entry.workflow || 'main'} > ${entry.node}\\n`;\n \n if (type === 'ERROR') {\n text += `
${entry.userMessage}
\\n`;\n } else if (entry.event) {\n text += `Event: ${entry.event}\\n`;\n }\n text += '\\n';\n });\n }\n }\n}\n\nreturn { json: { chatId, text } };" + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst input = $input.item.json;\nconst command = (input.message?.text || '').toLowerCase().trim();\nconst chatId = input.message.chat.id;\n\n// Read persisted error log (top-level key for reliable n8n static data tracking)\nconst errorLog = JSON.parse(staticData._errorLog || '{}');\nif (!errorLog.debug) {\n errorLog.debug = { enabled: false, executionCount: 0 };\n}\nif (!errorLog.errors) {\n errorLog.errors = { buffer: [], nextId: 1, count: 0, lastCleared: new Date().toISOString() };\n}\nif (!errorLog.traces) {\n errorLog.traces = { buffer: [], nextId: 1 };\n}\n\nlet text = '';\n\n// Parse command and arguments\nconst parts = command.split(' ');\nconst cmd = parts[0];\nconst arg1 = parts[1] || '';\n\nif (cmd === '/errors') {\n const count = parseInt(arg1) || 5;\n const requestedCount = Math.min(count, 50);\n const errors = errorLog.errors.buffer || [];\n const recentErrors = errors.slice(-requestedCount).reverse();\n \n if (recentErrors.length === 0) {\n text = 'No errors recorded.';\n } else {\n text = `Recent Errors (${recentErrors.length})\\n\\n`;\n recentErrors.forEach(err => {\n const timestamp = new Date(err.timestamp).toISOString().replace('T', ' ').substring(0, 19);\n text += `#${err.id} - ${timestamp}\\n`;\n text += `Workflow: ${err.workflow} > ${err.node}\\n`;\n text += `
${err.userMessage}
\\n`;\n if (err.error?.httpCode) {\n text += `HTTP: ${err.error.httpCode}\\n`;\n }\n text += '\\n';\n });\n \n text += `Total errors: ${errorLog.errors.count}\\n`;\n text += `Debug mode: ${errorLog.debug.enabled ? 'ON' : 'OFF'}`;\n }\n \n} else if (cmd === '/clear' || cmd === '/clear-errors') {\n const count = errorLog.errors.buffer.length;\n errorLog.errors.buffer = [];\n errorLog.errors.nextId = 1;\n errorLog.errors.lastCleared = new Date().toISOString();\n text = `Error log cleared. ${count} entries removed.`;\n \n} else if (cmd === '/debug') {\n if (arg1 === 'on') {\n errorLog.debug.enabled = true;\n errorLog.debug.executionCount = 0;\n text = 'Debug mode enabled.\\nTracing sub-workflow boundaries and callback routing.';\n } else if (arg1 === 'off') {\n errorLog.debug.enabled = false;\n text = 'Debug mode disabled.';\n } else {\n const debug = errorLog.debug;\n const errors = errorLog.errors;\n const traces = errorLog.traces;\n text = `Debug Status\\n\\n`;\n text += `Debug mode: ${debug.enabled ? 'ON' : 'OFF'}\\n`;\n text += `Execution count: ${debug.executionCount || 0}\\n`;\n text += `Error buffer: ${errors.buffer.length} entries\\n`;\n text += `Trace buffer: ${traces.buffer.length} entries\\n`;\n text += `Total errors: ${errors.count}`;\n }\n \n} else if (cmd === '/trace') {\n const correlationId = arg1;\n \n if (!correlationId) {\n text = 'Usage: /trace <correlationId>';\n } else {\n const errors = errorLog.errors.buffer || [];\n const traces = errorLog.traces.buffer || [];\n \n const errorMatches = errors.filter(e => e.correlationId === correlationId);\n const traceMatches = traces.filter(t => t.correlationId === correlationId);\n \n const allMatches = [...errorMatches, ...traceMatches].sort((a, b) => \n new Date(a.timestamp) - new Date(b.timestamp)\n );\n \n if (allMatches.length === 0) {\n text = `No entries found for correlation ID: ${correlationId}`;\n } else {\n text = `Trace for ${correlationId}\\n\\n`;\n allMatches.forEach(entry => {\n const timestamp = new Date(entry.timestamp).toISOString().replace('T', ' ').substring(0, 19);\n const type = entry.id.startsWith('err') ? 'ERROR' : 'TRACE';\n text += `[${type}] ${timestamp}\\n`;\n text += `${entry.workflow || 'main'} > ${entry.node}\\n`;\n \n if (type === 'ERROR') {\n text += `
${entry.userMessage}
\\n`;\n } else if (entry.event) {\n text += `Event: ${entry.event}\\n`;\n }\n text += '\\n';\n });\n }\n }\n}\n\n// Persist back as top-level assignment (n8n only tracks top-level changes reliably)\nstaticData._errorLog = JSON.stringify(errorLog);\n\nreturn { json: { chatId, text } };" }, "id": "code-process-debug-command", "name": "Process Debug Command", @@ -4944,7 +4944,7 @@ }, { "parameters": { - "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst input = $input.item.json;\n\n// Initialize if needed\nif (!staticData.errorLog) {\n staticData.errorLog = {\n debug: { enabled: false, executionCount: 0 },\n errors: { buffer: [], nextId: 1, count: 0, lastCleared: new Date().toISOString() },\n traces: { buffer: [], nextId: 1 }\n };\n}\n\nconst MAX_ERRORS = 50;\nconst errorEntry = {\n id: `err_${String(staticData.errorLog.errors.nextId).padStart(3, '0')}`,\n correlationId: input.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n workflow: input.workflow || 'main',\n node: input.node || 'unknown',\n operation: input.operation || 'unknown',\n userMessage: input.userMessage || input.errorMessage || 'Unknown error',\n error: {\n message: input.errorMessage || 'Unknown error',\n stack: (input.errorStack || '').substring(0, 500),\n httpCode: input.httpCode || null,\n rawResponse: (input.rawResponse || '').substring(0, 1000)\n },\n context: input.contextData || {}\n};\n\n// Ring buffer: push and rotate\nstaticData.errorLog.errors.buffer.push(errorEntry);\nif (staticData.errorLog.errors.buffer.length > MAX_ERRORS) {\n staticData.errorLog.errors.buffer.shift();\n}\nstaticData.errorLog.errors.nextId++;\nstaticData.errorLog.errors.count++;\n\n// Pass through all input data so downstream nodes still work\nreturn { json: { ...input, _errorLogged: true, _errorId: errorEntry.id } };" + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst input = $input.item.json;\n\n// Read persisted error log\nconst errorLog = JSON.parse(staticData._errorLog || '{}');\nif (!errorLog.errors) {\n errorLog.errors = { buffer: [], nextId: 1, count: 0, lastCleared: new Date().toISOString() };\n}\nif (!errorLog.debug) {\n errorLog.debug = { enabled: false, executionCount: 0 };\n}\n\nconst MAX_ERRORS = 50;\nconst errorEntry = {\n id: `err_${String(errorLog.errors.nextId).padStart(3, '0')}`,\n correlationId: input.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n workflow: input.workflow || 'main',\n node: input.node || 'unknown',\n operation: input.operation || 'unknown',\n userMessage: input.userMessage || input.errorMessage || 'Unknown error',\n error: {\n message: input.errorMessage || 'Unknown error',\n stack: (input.errorStack || '').substring(0, 500),\n httpCode: input.httpCode || null,\n rawResponse: (input.rawResponse || '').substring(0, 1000)\n },\n context: input.contextData || {}\n};\n\n// Ring buffer: push and rotate\nerrorLog.errors.buffer.push(errorEntry);\nif (errorLog.errors.buffer.length > MAX_ERRORS) {\n errorLog.errors.buffer.shift();\n}\nerrorLog.errors.nextId++;\nerrorLog.errors.count++;\n\n// Persist back as top-level assignment\nstaticData._errorLog = JSON.stringify(errorLog);\n\n// Pass through all input data so downstream nodes still work\nreturn { json: { ...input, _errorLogged: true, _errorId: errorEntry.id } };" }, "id": "code-log-error", "name": "Log Error", @@ -4957,7 +4957,7 @@ }, { "parameters": { - "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst input = $input.item.json;\n\n// Initialize if needed\nif (!staticData.errorLog) {\n staticData.errorLog = {\n debug: { enabled: false, executionCount: 0 },\n errors: { buffer: [], nextId: 1, count: 0, lastCleared: new Date().toISOString() },\n traces: { buffer: [], nextId: 1 }\n };\n}\n\n// Check if debug mode is enabled\nif (!staticData.errorLog.debug.enabled) {\n // Debug mode off, pass through unchanged\n return { json: input };\n}\n\n// Increment execution count and check auto-disable threshold\nstaticData.errorLog.debug.executionCount++;\nif (staticData.errorLog.debug.executionCount >= 100) {\n staticData.errorLog.debug.enabled = false;\n // Note: Auto-disable happened, but we don't have a way to notify here\n // The /debug status command will show it's disabled\n}\n\nconst MAX_TRACES = 50;\nconst traceEntry = {\n id: `trace_${String(staticData.errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: input.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: input.event || 'unknown', // \"sub-workflow-call\" | \"callback-routing\"\n workflow: input.workflow || 'main',\n node: input.node || 'unknown',\n data: input.traceData || {}\n};\n\n// Ring buffer: push and rotate\nstaticData.errorLog.traces.buffer.push(traceEntry);\nif (staticData.errorLog.traces.buffer.length > MAX_TRACES) {\n staticData.errorLog.traces.buffer.shift();\n}\nstaticData.errorLog.traces.nextId++;\n\n// Pass through all input data unchanged\nreturn { json: { ...input, _traceLogged: true, _traceId: traceEntry.id } };" + "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst input = $input.item.json;\n\n// Read persisted error log\nconst errorLog = JSON.parse(staticData._errorLog || '{}');\nif (!errorLog.debug) {\n errorLog.debug = { enabled: false, executionCount: 0 };\n}\nif (!errorLog.traces) {\n errorLog.traces = { buffer: [], nextId: 1 };\n}\n\n// Only trace if debug mode is enabled\nif (!errorLog.debug.enabled) {\n return { json: input };\n}\n\nconst MAX_TRACES = 50;\n\n// Auto-disable after 100 executions\nerrorLog.debug.executionCount = (errorLog.debug.executionCount || 0) + 1;\nif (errorLog.debug.executionCount > 100) {\n errorLog.debug.enabled = false;\n // Persist and pass through\n staticData._errorLog = JSON.stringify(errorLog);\n return { json: input };\n}\n\nconst traceEntry = {\n id: `trace_${String(errorLog.traces.nextId).padStart(3, '0')}`,\n correlationId: input.correlationId || $execution.id,\n timestamp: new Date().toISOString(),\n executionId: $execution.id,\n event: input._traceEvent || 'unknown',\n workflow: input._traceWorkflow || 'main',\n node: input._traceNode || 'unknown',\n data: input._traceData || {}\n};\n\nerrorLog.traces.buffer.push(traceEntry);\nif (errorLog.traces.buffer.length > MAX_TRACES) {\n errorLog.traces.buffer.shift();\n}\nerrorLog.traces.nextId++;\n\n// Persist back as top-level assignment\nstaticData._errorLog = JSON.stringify(errorLog);\n\nreturn { json: input };" }, "id": "code-log-trace", "name": "Log Trace",