diff --git a/n8n-workflow.json b/n8n-workflow.json index 8b141a7..61f352e 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -180,98 +180,6 @@ "renameOutput": true, "outputKey": "menu" }, - { - "id": "keyword-errors", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-errors", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/errors", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "errors" - }, - { - "id": "keyword-clear-errors", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-clear", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/clear", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "clear-errors" - }, - { - "id": "keyword-debug", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-debug", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/debug", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "debug" - }, - { - "id": "keyword-trace", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-trace", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/trace", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "trace" - }, { "id": "keyword-status", "conditions": { @@ -678,7 +586,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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 +1934,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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 +3287,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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 +3335,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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 +3379,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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 +3423,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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 +3498,7 @@ }, { "parameters": { - "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};" + "jsCode": "// 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", @@ -4906,68 +4814,6 @@ -300 ] }, - { - "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// 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", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - -200 - ] - }, - { - "parameters": { - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-debug-response", - "name": "Send Debug Response", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1340, - -200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "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", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2600, - -200 - ] - }, - { - "parameters": { - "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", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2600, - -400 - ] - }, { "parameters": { "jsCode": "// Generate correlation ID for this request\nconst correlationId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\nreturn {\n json: {\n ...$input.item.json,\n correlationId\n }\n};" @@ -4993,66 +4839,6 @@ 2400, 200 ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "check-success", - "leftValue": "={{ $json.success }}", - "rightValue": false, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - } - }, - "id": "if-check-success-qokchnw8", - "name": "Check Execute Container Action Success", - "type": "n8n-nodes-base.if", - "typeVersion": 2.2, - "position": [ - 2240, - 500 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "check-success", - "leftValue": "={{ $json.success }}", - "rightValue": false, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - } - }, - "id": "if-check-success-8aoev7xt", - "name": "Check Execute Inline Action Success", - "type": "n8n-nodes-base.if", - "typeVersion": 2.2, - "position": [ - 2680, - 1200 - ] } ], "connections": { @@ -5463,34 +5249,6 @@ "index": 0 } ], - [ - { - "node": "Process Debug Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Process Debug Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Process Debug Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Process Debug Command", - "type": "main", - "index": 0 - } - ], [ { "node": "Prepare Status Input", @@ -6222,7 +5980,7 @@ "main": [ [ { - "node": "Check Execute Container Action Success", + "node": "Handle Text Action Result", "type": "main", "index": 0 } @@ -6255,7 +6013,7 @@ "main": [ [ { - "node": "Check Execute Inline Action Success", + "node": "Handle Inline Action Result", "type": "main", "index": 0 } @@ -6938,17 +6696,6 @@ ] ] }, - "Process Debug Command": { - "main": [ - [ - { - "node": "Send Debug Response", - "type": "main", - "index": 0 - } - ] - ] - }, "code-generate-correlation-id": { "main": [ [ @@ -6975,42 +6722,6 @@ "main": [ [] ] - }, - "Check Execute Container Action Success": { - "main": [ - [ - { - "node": "Log Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Text Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Execute Inline Action Success": { - "main": [ - [ - { - "node": "Log Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Inline Action Result", - "type": "main", - "index": 0 - } - ] - ] } }, "pinData": {},