feat(03-03): handle batch confirmation callback execution

- Update Parse Callback Data to detect batch (c is array) vs single
- Add isBatch and containerIds fields to callback data
- Add 'batch' route in Route Callback switch (output 2)
- Add Build Batch Commands node to prepare curl commands for each container
- Add Prepare Batch Execution to combine commands with result markers
- Add Execute Batch Action to run all container actions sequentially
- Add Parse Batch Result to parse RESULT_N:statusCode output
- Add Format Batch Result to build success/failure message
This commit is contained in:
Lucas Berger
2026-01-30 08:47:27 -05:00
parent ab8d5282c0
commit 25a7994fcb
+127 -2
View File
@@ -643,7 +643,7 @@
},
{
"parameters": {
"jsCode": "// Parse callback data from suggestion button click\nconst callback = $json.callback_query;\nlet data;\ntry {\n data = JSON.parse(callback.data);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\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\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerId: data.c || null,\n expired: isExpired,\n isCancel: action === 'cancel'\n }\n};"
"jsCode": "// Parse callback data from button click (single suggestion or batch)\nconst callback = $json.callback_query;\nlet data;\ntry {\n data = JSON.parse(callback.data);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\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, // Array for batch support\n containerId: containerIds[0] || null, // For single-container compat\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel'\n }\n};"
},
"id": "code-parse-callback",
"name": "Parse Callback Data",
@@ -702,6 +702,30 @@
},
"renameOutput": true,
"outputKey": "expired"
},
{
"id": "is-batch",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-true",
"leftValue": "={{ $json.isBatch }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batch"
}
]
},
@@ -913,6 +937,57 @@
"name": "Telegram API"
}
}
},
{
"parameters": {
"jsCode": "// Execute batch action on all containers sequentially\nconst containerIds = $json.containerIds;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\nconst results = [];\n\n// Execute each container action sequentially using fetch\nfor (const containerId of containerIds) {\n try {\n // Use n8n's built-in $http or construct command for later execution\n // Since we can't use execSync easily, we'll build commands for chained execution\n results.push({\n containerId,\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`\n });\n } catch (err) {\n results.push({ containerId, error: err.message });\n }\n}\n\nreturn {\n json: {\n commands: results,\n action,\n queryId,\n chatId,\n messageId,\n totalCount: containerIds.length\n }\n};"
},
"id": "code-build-batch-commands",
"name": "Build Batch Commands",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1340, 800]
},
{
"parameters": {
"jsCode": "// Execute all batch commands and collect results\nconst { commands, action, queryId, chatId, messageId, totalCount } = $json;\nconst results = [];\n\n// Build a single shell command that runs all curl commands sequentially\n// and outputs results in a parseable format\nconst allCommands = commands.map((c, i) => \n `echo \"RESULT_${i}:$(${c.cmd})\"`\n).join(' && ');\n\nreturn {\n json: {\n batchCmd: allCommands,\n containerIds: commands.map(c => c.containerId),\n action,\n queryId,\n chatId,\n messageId,\n totalCount\n }\n};"
},
"id": "code-prepare-batch-exec",
"name": "Prepare Batch Execution",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1560, 800]
},
{
"parameters": {
"command": "={{ $json.batchCmd }}",
"options": {}
},
"id": "exec-batch-action",
"name": "Execute Batch Action",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [1780, 800]
},
{
"parameters": {
"jsCode": "// Parse batch execution results\nconst stdout = $input.item.json.stdout || '';\nconst stderr = $input.item.json.stderr || '';\nconst prevData = $('Prepare Batch Execution').item.json;\nconst { containerIds, action, queryId, chatId, messageId, totalCount } = prevData;\n\n// Parse results from output like: RESULT_0:204 RESULT_1:204 RESULT_2:304\nconst results = [];\nfor (let i = 0; i < containerIds.length; i++) {\n const match = stdout.match(new RegExp(`RESULT_${i}:(\\\\d+)`));\n if (match) {\n const statusCode = parseInt(match[1]);\n results.push({\n containerId: containerIds[i],\n success: statusCode === 204 || statusCode === 304,\n statusCode\n });\n } else {\n results.push({\n containerId: containerIds[i],\n success: false,\n error: 'No response'\n });\n }\n}\n\nconst successCount = results.filter(r => r.success).length;\nconst failCount = results.length - successCount;\n\nreturn {\n json: {\n results,\n successCount,\n failCount,\n totalCount: results.length,\n action,\n queryId,\n chatId,\n messageId\n }\n};"
},
"id": "code-parse-batch-result",
"name": "Parse Batch Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [2000, 800]
},
{
"parameters": {
"jsCode": "// Format batch result message\nconst { successCount, failCount, totalCount, action } = $json;\nconst verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n\nlet message;\nif (failCount === 0) {\n message = `Successfully ${verb} ${successCount} container${successCount > 1 ? 's' : ''}`;\n} else if (successCount === 0) {\n message = `Failed to ${action} all ${totalCount} containers`;\n} else {\n message = `${verb.charAt(0).toUpperCase() + verb.slice(1)} ${successCount}/${totalCount} containers (${failCount} failed)`;\n}\n\nreturn {\n json: {\n message,\n chatId: $json.chatId,\n queryId: $json.queryId,\n messageId: $json.messageId\n }\n};"
},
"id": "code-format-batch-result",
"name": "Format Batch Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [2220, 800]
}
],
"connections": {
@@ -996,7 +1071,13 @@
"index": 0
}
],
[],
[
{
"node": "Build Batch Commands",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Callback Action",
@@ -1324,6 +1405,50 @@
}
]
]
},
"Build Batch Commands": {
"main": [
[
{
"node": "Prepare Batch Execution",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Execution": {
"main": [
[
{
"node": "Execute Batch Action",
"type": "main",
"index": 0
}
]
]
},
"Execute Batch Action": {
"main": [
[
{
"node": "Parse Batch Result",
"type": "main",
"index": 0
}
]
]
},
"Parse Batch Result": {
"main": [
[
{
"node": "Format Batch Result",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},