From d1d13ca671ff5eb333de6e60e0c31b5a4a345b9c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 8 Feb 2026 12:46:51 -0500 Subject: [PATCH] feat(10.2-01): add hidden debug commands and error ring buffer foundation - Add 4 new keyword routes: /errors, /clear-errors, /debug, /trace - Create Process Debug Command code node with unified command handling - Initialize workflow static data structure (errorLog with debug, errors, traces) - Implement /errors command to display recent errors (default 5, max 50) - Implement /clear-errors command to reset error buffer - Implement /debug on|off|status for debug mode toggle - Implement /trace for correlation-based query - Add Send Debug Response Telegram node with HTML formatting - Wire Keyword Router -> Process Debug Command -> Send Debug Response - Commands remain hidden (not listed in /start menu) - Node count: 168 -> 170 (+2 nodes) --- n8n-workflow.json | 183 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 175 insertions(+), 8 deletions(-) diff --git a/n8n-workflow.json b/n8n-workflow.json index d0d65d9..5d09c39 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -363,6 +363,98 @@ }, "renameOutput": true, "outputKey": "status" + }, + { + "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" } ] }, @@ -1531,7 +1623,7 @@ "resource": "message", "operation": "sendMessage", "chatId": "={{ $json.message.chat.id }}", - "text": "Commands:\n\n• status\n• start [name]\n• stop [name]\n• restart [name]\n• update [name]\n• logs [name]", + "text": "Commands:\n\n\u2022 status\n\u2022 start [name]\n\u2022 stop [name]\n\u2022 restart [name]\n\u2022 update [name]\n\u2022 logs [name]", "additionalFields": { "parse_mode": "HTML" } @@ -2831,7 +2923,7 @@ }, { "parameters": { - "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `• ${c.name}`).join('\\n');\nconst moreText = count > 10 ? `\\n...and ${count - 10} more` : '';\n\nconst message = `Update ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerList}${moreText}`;\n\n// Create inline keyboard\nconst timestamp = Math.floor(Date.now() / 1000);\nconst containerNames = containers.map(c => c.name).join(',');\n\n// Encode container names in callback (will need to lookup IDs later)\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n message: message,\n keyboard: {\n inline_keyboard: [\n [\n { text: '✅ Confirm', callback_data: `uall:confirm:${timestamp}` },\n { text: '❌ Cancel', callback_data: 'uall:cancel' }\n ]\n ]\n },\n containerNames: containerNames,\n containers: containers,\n timestamp: timestamp\n }\n};" + "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\u2022 ${c.name}`).join('\\n');\nconst moreText = count > 10 ? `\\n...and ${count - 10} more` : '';\n\nconst message = `Update ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerList}${moreText}`;\n\n// Create inline keyboard\nconst timestamp = Math.floor(Date.now() / 1000);\nconst containerNames = containers.map(c => c.name).join(',');\n\n// Encode container names in callback (will need to lookup IDs later)\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n message: message,\n keyboard: {\n inline_keyboard: [\n [\n { text: '\u2705 Confirm', callback_data: `uall:confirm:${timestamp}` },\n { text: '\u274c Cancel', callback_data: 'uall:cancel' }\n ]\n ]\n },\n containerNames: containerNames,\n containers: containers,\n timestamp: timestamp\n }\n};" }, "id": "code-build-update-all-confirmation", "name": "Build Update All Confirmation", @@ -2872,7 +2964,7 @@ "resource": "message", "operation": "sendMessage", "chatId": "={{ $json.chatId }}", - "text": "All containers are up to date! 🎉", + "text": "All containers are up to date! \ud83c\udf89", "options": {} }, "id": "telegram-send-all-up-to-date", @@ -2895,7 +2987,7 @@ "resource": "callback", "operation": "answerQuery", "queryId": "={{ $json.queryId }}", - "text": "⏱️ Confirmation expired (30s timeout)", + "text": "\u23f1\ufe0f Confirmation expired (30s timeout)", "options": { "showAlert": true } @@ -2942,7 +3034,7 @@ "resource": "callback", "operation": "answerQuery", "queryId": "={{ $json.queryId }}", - "text": "❌ Update cancelled" + "text": "\u274c Update cancelled" }, "id": "telegram-answer-update-all-cancel", "name": "Answer Update All Cancel", @@ -3030,7 +3122,7 @@ "resource": "callback", "operation": "answerQuery", "queryId": "={{ $json.queryId }}", - "text": "✅ Starting batch update..." + "text": "\u2705 Starting batch update..." }, "id": "telegram-answer-update-all-confirm", "name": "Answer Update All Confirm", @@ -3498,7 +3590,7 @@ }, { "parameters": { - "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: '🔄 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '⬆️ Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '◀️ 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", @@ -4813,6 +4905,42 @@ 2110, -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// 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 } };" + }, + "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" + } + } } ], "connections": { @@ -5285,6 +5413,34 @@ "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": "Process Debug Command", + "type": "main", + "index": 0 + } ] ] }, @@ -6669,6 +6825,17 @@ } ] ] + }, + "Process Debug Command": { + "main": [ + [ + { + "node": "Send Debug Response", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {}, @@ -6679,4 +6846,4 @@ "tags": [], "triggerCount": 1, "active": false -} +} \ No newline at end of file