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 <correlationId> 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)
This commit is contained in:
Lucas Berger
2026-02-08 12:46:51 -05:00
parent c79a3fbf87
commit d1d13ca671
+174 -7
View File
@@ -363,6 +363,98 @@
}, },
"renameOutput": true, "renameOutput": true,
"outputKey": "status" "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", "resource": "message",
"operation": "sendMessage", "operation": "sendMessage",
"chatId": "={{ $json.message.chat.id }}", "chatId": "={{ $json.message.chat.id }}",
"text": "<b>Commands:</b>\n\n status\n start [name]\n stop [name]\n restart [name]\n update [name]\n logs [name]", "text": "<b>Commands:</b>\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": { "additionalFields": {
"parse_mode": "HTML" "parse_mode": "HTML"
} }
@@ -2831,7 +2923,7 @@
}, },
{ {
"parameters": { "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", "id": "code-build-update-all-confirmation",
"name": "Build Update All Confirmation", "name": "Build Update All Confirmation",
@@ -2872,7 +2964,7 @@
"resource": "message", "resource": "message",
"operation": "sendMessage", "operation": "sendMessage",
"chatId": "={{ $json.chatId }}", "chatId": "={{ $json.chatId }}",
"text": "All containers are up to date! 🎉", "text": "All containers are up to date! \ud83c\udf89",
"options": {} "options": {}
}, },
"id": "telegram-send-all-up-to-date", "id": "telegram-send-all-up-to-date",
@@ -2895,7 +2987,7 @@
"resource": "callback", "resource": "callback",
"operation": "answerQuery", "operation": "answerQuery",
"queryId": "={{ $json.queryId }}", "queryId": "={{ $json.queryId }}",
"text": "⏱️ Confirmation expired (30s timeout)", "text": "\u23f1\ufe0f Confirmation expired (30s timeout)",
"options": { "options": {
"showAlert": true "showAlert": true
} }
@@ -2942,7 +3034,7 @@
"resource": "callback", "resource": "callback",
"operation": "answerQuery", "operation": "answerQuery",
"queryId": "={{ $json.queryId }}", "queryId": "={{ $json.queryId }}",
"text": " Update cancelled" "text": "\u274c Update cancelled"
}, },
"id": "telegram-answer-update-all-cancel", "id": "telegram-answer-update-all-cancel",
"name": "Answer Update All Cancel", "name": "Answer Update All Cancel",
@@ -3030,7 +3122,7 @@
"resource": "callback", "resource": "callback",
"operation": "answerQuery", "operation": "answerQuery",
"queryId": "={{ $json.queryId }}", "queryId": "={{ $json.queryId }}",
"text": " Starting batch update..." "text": "\u2705 Starting batch update..."
}, },
"id": "telegram-answer-update-all-confirm", "id": "telegram-answer-update-all-confirm",
"name": "Answer Update All Confirm", "name": "Answer Update All Confirm",
@@ -3498,7 +3590,7 @@
}, },
{ {
"parameters": { "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\\n<i>Updated: ${timestamp}</i>`;\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\\n<i>Updated: ${timestamp}</i>`;\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", "id": "b1800598-1ff6-4da3-8506-4e4e8127f902",
"name": "Format Inline Logs Result", "name": "Format Inline Logs Result",
@@ -4813,6 +4905,42 @@
2110, 2110,
-300 -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 = `<b>Recent Errors (${recentErrors.length})</b>\\n\\n`;\n recentErrors.forEach(err => {\n const timestamp = new Date(err.timestamp).toISOString().replace('T', ' ').substring(0, 19);\n text += `<b>#${err.id}</b> - ${timestamp}\\n`;\n text += `Workflow: ${err.workflow} &gt; ${err.node}\\n`;\n text += `<pre>${err.userMessage}</pre>\\n`;\n if (err.error?.httpCode) {\n text += `HTTP: ${err.error.httpCode}\\n`;\n }\n text += '\\n';\n });\n \n text += `<i>Total errors: ${staticData.errorLog.errors.count}\\n`;\n text += `Debug mode: ${staticData.errorLog.debug.enabled ? 'ON' : 'OFF'}</i>`;\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 = `<b>Error log cleared.</b> ${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 = '<b>Debug mode enabled.</b>\\nTracing sub-workflow boundaries and callback routing.';\n } else if (arg1 === 'off') {\n staticData.errorLog.debug.enabled = false;\n text = '<b>Debug mode disabled.</b>';\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 = `<b>Debug Status</b>\\n\\n`;\n text += `Debug mode: ${debug.enabled ? '<b>ON</b>' : '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 = '<b>Usage:</b> /trace &lt;correlationId&gt;';\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: <code>${correlationId}</code>`;\n } else {\n text = `<b>Trace for ${correlationId}</b>\\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 += `<b>[${type}]</b> ${timestamp}\\n`;\n text += `${entry.workflow || 'main'} &gt; ${entry.node}\\n`;\n \n if (type === 'ERROR') {\n text += `<pre>${entry.userMessage}</pre>\\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": { "connections": {
@@ -5285,6 +5413,34 @@
"type": "main", "type": "main",
"index": 0 "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": {}, "pinData": {},