diff --git a/n8n-workflow.json b/n8n-workflow.json index fe406ea..1bdb91a 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -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": {},