Files
unraid-docker-manager/n8n-workflow.json
T
Lucas Berger ac2d745e1d fix(10.1-09): wire /list command connection to Prepare Status Input
The list keyword rule was added to Keyword Router but its connection
pointed to Show Menu (fallback) instead of Prepare Status Input.
Added proper connection at index 8 and shifted fallback to index 9.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 18:56:44 -05:00

6683 lines
219 KiB
JSON

{
"name": "Docker Manager Bot",
"nodes": [
{
"parameters": {
"updates": [
"message",
"callback_query"
]
},
"id": "telegram-trigger",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [
240,
300
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
},
"webhookId": "86b8d6bb-7c15-4aae-a749-fcaf2dfcb9e0"
},
{
"parameters": {
"rules": {
"values": [
{
"id": "route-message",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "has-message",
"leftValue": "={{ $json.message?.text }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "message"
},
{
"id": "route-callback",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "has-callback",
"leftValue": "={{ $json.callback_query?.id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "callback_query"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-update-type",
"name": "Route Update Type",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
460,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "user-auth-condition",
"leftValue": "={{ $json.message.from.id.toString() }}",
"rightValue": "563878771",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-auth",
"name": "IF User Authenticated",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
680,
200
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "callback-auth-condition",
"leftValue": "={{ $json.callback_query.from.id.toString() }}",
"rightValue": "563878771",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-callback-auth",
"name": "IF Callback Authenticated",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
680,
500
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "keyword-menu-start",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "starts-with-start-cmd",
"leftValue": "={{ $json.message.text }}",
"rightValue": "/start",
"operator": {
"type": "string",
"operation": "startsWith"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "menu"
},
{
"id": "keyword-status",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-status",
"leftValue": "={{ $json.message.text }}",
"rightValue": "status",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "status"
},
{
"id": "keyword-restart",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-restart",
"leftValue": "={{ $json.message.text }}",
"rightValue": "restart",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "restart"
},
{
"id": "keyword-start",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-start",
"leftValue": "={{ $json.message.text }}",
"rightValue": "start",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "start"
},
{
"id": "keyword-stop",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-stop",
"leftValue": "={{ $json.message.text }}",
"rightValue": "stop",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "stop"
},
{
"id": "keyword-update-all",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "matches-update-all",
"leftValue": "={{ $json.message.text }}",
"rightValue": "update.?all|updateall",
"operator": {
"type": "string",
"operation": "regex"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "updateall"
},
{
"id": "keyword-update",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-update",
"leftValue": "={{ $json.message.text }}",
"rightValue": "update",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "update"
},
{
"id": "keyword-logs",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-logs",
"leftValue": "={{ $json.message.text }}",
"rightValue": "logs",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "logs"
},
{
"id": "keyword-list",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"conditions": [
{
"id": "contains-list",
"leftValue": "={{ $json.message.text }}",
"rightValue": "list",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "status"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "switch-keyword-router",
"name": "Keyword Router",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
900,
200
]
},
{
"parameters": {
"jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};"
},
"id": "code-parse-action-command",
"name": "Parse Action Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
400
]
},
{
"parameters": {
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
"options": {}
},
"id": "exec-docker-list-action",
"name": "Docker List for Action",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1120,
400
]
},
{
"parameters": {
"jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};"
},
"id": "code-build-action-cmd",
"name": "Build Action Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
400
]
},
{
"parameters": {
"command": "={{ $json.cmd }}",
"options": {}
},
"id": "exec-action",
"name": "Execute Action",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2000,
400
]
},
{
"parameters": {
"jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `<b>${containerName}</b> ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};"
},
"id": "code-parse-action-result",
"name": "Parse Action Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
400
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-action-result",
"name": "Send Action Result",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2440,
400
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"id": "http-send-suggestion",
"name": "Send Suggestion",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
200
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "=No container found matching '<b>{{ $json.query }}</b>'",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-no-match",
"name": "Send No Match",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2220,
400
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.actionType;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp for 30s timeout\n\n// Build callback_data using new bexec format\n// Format: bexec:{action}:{comma-separated-names}:{timestamp}\n// Limit to 4 containers to stay within 64-byte callback_data limit\nlet limitedNames = names;\nlet limitedCount = names.length;\nif (names.length > 4) {\n limitedNames = names.slice(0, 4);\n limitedCount = 4;\n}\n\nconst namesStr = limitedNames.join(',');\nconst callbackData = `bexec:${action}:${namesStr}:${timestamp}`;\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found <b>${matches.length}</b> containers matching '<b>${query}</b>':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};"
},
"id": "code-build-batch-keyboard",
"name": "Build Batch Keyboard",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
500
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chat_id }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "HTML",
"reply_markup": "={{ JSON.stringify($json.reply_markup) }}"
}
},
"id": "telegram-send-batch-confirm",
"name": "Send Batch Confirmation",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2000,
500
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-docker-error",
"name": "Send Docker Error",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1780,
200
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"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",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
500
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "is-update-all",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "update-all-true",
"leftValue": "={{ $json.isUpdateAll }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "updateall"
},
{
"id": "is-update-all-cancel",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "update-all-cancel-true",
"leftValue": "={{ $json.isUpdateAllCancel }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "updateallcancel"
},
{
"id": "is-cancel",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cancel-true",
"leftValue": "={{ $json.isCancel }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "cancel"
},
{
"id": "is-expired",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "expired-true",
"leftValue": "={{ $json.expired }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"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"
}
},
{
"id": "not-batch-exec",
"leftValue": "={{ $json.isBatchExec }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batch"
},
{
"id": "is-select",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "select-true",
"leftValue": "={{ $json.isSelect }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "select"
},
{
"id": "is-list",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "list-true",
"leftValue": "={{ $json.isList }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "list"
},
{
"id": "is-action",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "action-true",
"leftValue": "={{ $json.isAction }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "action"
},
{
"id": "is-noop",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "noop-true",
"leftValue": "={{ $json.isNoop }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "noop"
},
{
"id": "is-confirm",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "confirm-true",
"leftValue": "={{ $json.isConfirm }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "confirm"
},
{
"id": "is-cancel-confirm",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cancel-confirm-true",
"leftValue": "={{ $json.isCancelConfirm }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "cancelConfirm"
},
{
"id": "is-batch-stop-confirm",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-stop-confirm-true",
"leftValue": "={{ $json.isBatchStopConfirm }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchStopConfirm"
},
{
"id": "is-batch-stop-cancel",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-stop-cancel-true",
"leftValue": "={{ $json.isBatchStopCancel }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchStopCancel"
},
{
"id": "is-bexec-text-cmd",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-exec-true",
"leftValue": "={{ $json.isBatchExec }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
},
{
"id": "is-batch-true",
"leftValue": "={{ $json.isBatch }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "bexecTextCmd"
},
{
"id": "is-batch-mode",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-mode-true",
"leftValue": "={{ $json.isBatchMode }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchmode"
},
{
"id": "is-batch-toggle",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-toggle-true",
"leftValue": "={{ $json.isBatchToggle }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchtoggle"
},
{
"id": "is-batch-nav",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-nav-true",
"leftValue": "={{ $json.isBatchNav }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchnav"
},
{
"id": "is-batch-exec",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-exec-true",
"leftValue": "={{ $json.isBatchExec }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchexec"
},
{
"id": "is-batch-clear",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-clear-true",
"leftValue": "={{ $json.isBatchClear }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchclear"
},
{
"id": "is-batch-cancel",
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-cancel-true",
"leftValue": "={{ $json.isBatchCancel }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batchcancel"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "switch-route-callback",
"name": "Route Callback",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1120,
500
]
},
{
"parameters": {
"jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};"
},
"id": "code-handle-cancel",
"name": "Handle Cancel",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
500
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
"options": {}
},
"id": "http-answer-cancel",
"name": "Answer Cancel Query",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
500
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}",
"options": {}
},
"id": "http-delete-cancel-msg",
"name": "Delete Cancel Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1780,
500
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};"
},
"id": "code-handle-expired",
"name": "Handle Expired",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
600
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}",
"options": {}
},
"id": "http-answer-expired",
"name": "Answer Expired Query",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
600
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}",
"options": {}
},
"id": "http-delete-expired-msg",
"name": "Delete Expired Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1780,
600
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};"
},
"id": "code-build-callback-cmd",
"name": "Build Callback Action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
700
]
},
{
"parameters": {
"command": "={{ $json.cmd }}",
"options": {}
},
"id": "exec-callback-action",
"name": "Execute Callback Action",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1560,
700
]
},
{
"parameters": {
"jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n};"
},
"id": "code-parse-callback-result",
"name": "Parse Callback Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
700
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
"options": {}
},
"id": "http-answer-action",
"name": "Answer Action Query",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2000,
700
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}",
"options": {}
},
"id": "http-delete-suggestion-msg",
"name": "Delete Suggestion Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2220,
700
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $('Parse Callback Result').item.json.chatId }}",
"text": "={{ $('Parse Callback Result').item.json.text }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-callback-result",
"name": "Send Callback Result",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2440,
700
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}",
"options": {}
},
"id": "http-delete-batch-confirm-msg",
"name": "Delete Batch Confirm Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2660,
800
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update <container-name>',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};"
},
"id": "code-parse-update",
"name": "Parse Update Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
1000
]
},
{
"parameters": {
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
"options": {}
},
"id": "exec-docker-list-update",
"name": "Docker List for Update",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1120,
1000
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-update-error",
"name": "Send Update Error",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1780,
900
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Update requires exact container name - multiple matches not allowed\nconst data = $input.item.json;\nconst matches = data.matches;\nconst query = data.containerQuery;\nconst chatId = data.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};"
},
"id": "code-update-multiple-handler",
"name": "Handle Update Multiple",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
1100
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-update-multiple",
"name": "Send Update Multiple",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2000,
1100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "=No container found matching '<b>{{ $json.containerQuery }}</b>'",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-update-no-match",
"name": "Send Update No Match",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1780,
1000
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};"
},
"id": "code-parse-logs",
"name": "Parse Logs Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
600
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $('Prepare Text Logs Input').item.json.chatId }}",
"text": "={{ $json.message }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-send-logs",
"name": "Send Logs Response",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2440,
500
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"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]",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-show-menu",
"name": "Show Menu",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1120,
300
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-container-list",
"name": "Send Container List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
0
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-container-submenu-direct",
"name": "Send Container Submenu Direct",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2000,
-100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}",
"options": {}
},
"id": "http-answer-select-callback",
"name": "Answer Select Callback",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
900
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-container-submenu",
"name": "Send Container Submenu",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2220,
900
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}",
"options": {}
},
"id": "http-answer-noop-callback",
"name": "Answer Noop Callback",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
1100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}",
"options": {}
},
"id": "http-answer-list-callback",
"name": "Answer List Callback",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
1000
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-edit-container-list",
"name": "Edit Container List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2220,
1000
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}",
"options": {}
},
"id": "http-answer-action-callback",
"name": "Answer Action Callback",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
1200
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"id": "action-start",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-start",
"leftValue": "={{ $('Parse Callback Data').item.json.action }}",
"rightValue": "start",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "start"
},
{
"id": "action-restart",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-restart",
"leftValue": "={{ $('Parse Callback Data').item.json.action }}",
"rightValue": "restart",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "restart"
},
{
"id": "action-stop",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-stop",
"leftValue": "={{ $('Parse Callback Data').item.json.action }}",
"rightValue": "stop",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "stop"
},
{
"id": "action-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-update",
"leftValue": "={{ $('Parse Callback Data').item.json.action }}",
"rightValue": "update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "update"
},
{
"id": "action-logs",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-logs",
"leftValue": "={{ $('Parse Callback Data').item.json.action }}",
"rightValue": "logs",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "logs"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-action-type",
"name": "Route Action Type",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1560,
1200
]
},
{
"parameters": {
"jsCode": "// Build start/restart action command for inline keyboard\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst action = data.action; // 'start' or 'restart'\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// For restart, use ?t=10 for graceful timeout\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n containerName,\n action,\n chatId,\n messageId\n }\n};"
},
"id": "code-prepare-immediate-action",
"name": "Prepare Immediate Action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
1100
]
},
{
"parameters": {
"method": "GET",
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
"options": {}
},
"id": "http-get-container-for-action",
"name": "Get Container For Action",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2000,
1100
]
},
{
"parameters": {
"jsCode": "// Find container and execute action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};"
},
"id": "code-build-immediate-action-cmd",
"name": "Build Immediate Action Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
1100
]
},
{
"parameters": {
"command": "={{ $json.cmd }}",
"options": {}
},
"id": "exec-immediate-action",
"name": "Execute Immediate Action",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2440,
1100
]
},
{
"parameters": {
"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 <b>${containerName}</b> started`,\n restart: `\\u{1F504} <b>${containerName}</b> 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} <b>${containerName}</b>`;\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",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
1100
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-immediate-result",
"name": "Send Immediate Result",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
1100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-logs-result",
"name": "Send Logs Result",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
1300
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Cancel confirmation - return to container submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};"
},
"id": "code-prepare-cancel-return",
"name": "Prepare Cancel Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
1800
]
},
{
"parameters": {
"method": "GET",
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
"options": {}
},
"id": "http-get-container-for-cancel",
"name": "Get Container For Cancel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1780,
1800
]
},
{
"parameters": {
"jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $input.item.json;\nconst searchName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} <b>${containerName}</b>\\n\\n`;\ntext += `<b>State:</b> ${state}\\n`;\ntext += `<b>Status:</b> ${status}\\n`;\ntext += `<b>Image:</b> ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];"
},
"id": "code-build-cancel-return-submenu",
"name": "Build Cancel Return Submenu",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
1800
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-cancel-return-submenu",
"name": "Send Cancel Return Submenu",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2220,
1800
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Detect if this is a batch command (multiple container names)\n// Batch commands: {action} {name1} {name2} ... where action is update/start/stop/restart\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action type from the routed keyword\n// This node receives from Keyword Router which already matched the action\nlet action = null;\nlet containerPart = '';\n\nif (text.startsWith('restart ')) {\n action = 'restart';\n containerPart = text.substring(8).trim();\n} else if (text.includes('restart')) {\n action = 'restart';\n containerPart = text.replace('restart', '').trim();\n} else if (text.startsWith('start ')) {\n action = 'start';\n containerPart = text.substring(6).trim();\n} else if (text.includes('start')) {\n action = 'start';\n containerPart = text.replace('start', '').trim();\n} else if (text.startsWith('stop ')) {\n action = 'stop';\n containerPart = text.substring(5).trim();\n} else if (text.includes('stop')) {\n action = 'stop';\n containerPart = text.replace('stop', '').trim();\n} else if (text.startsWith('update ')) {\n action = 'update';\n containerPart = text.substring(7).trim();\n} else if (text.includes('update')) {\n action = 'update';\n containerPart = text.replace('update', '').trim();\n}\n\nif (!action || !containerPart) {\n // No valid action found, pass through as non-batch (will be handled by existing flow)\n return {\n json: {\n isBatch: false,\n action: action,\n containerNames: [],\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n };\n}\n\n// Split container names by whitespace\nconst containerNames = containerPart.split(/\\s+/).filter(name => name.length > 0);\n\n// Batch = 2 or more containers\nconst isBatch = containerNames.length >= 2;\n\nreturn {\n json: {\n isBatch: isBatch,\n action: action,\n containerNames: containerNames,\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n};"
},
"id": "code-detect-batch",
"name": "Detect Batch Command",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
-200
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "is-batch-check",
"leftValue": "={{ $json.isBatch }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-is-batch",
"name": "Is Batch Command",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1120,
-200
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "action-start-stop-restart",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-action-type",
"leftValue": "={{ $json.action }}",
"rightValue": "update",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "action"
},
{
"id": "action-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-update-type",
"leftValue": "={{ $json.action }}",
"rightValue": "update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "update"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-single-action",
"name": "Route Single Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1340,
-100
]
},
{
"parameters": {
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
"options": {}
},
"id": "exec-docker-list-batch",
"name": "Get Containers for Batch",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1340,
-300
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"id": "http-send-disambiguation",
"name": "Send Disambiguation",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2220,
-400
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-not-found",
"name": "Send Not Found Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
-100
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "batch-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-update",
"leftValue": "={{ $json.action }}",
"rightValue": "update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "update"
},
{
"id": "batch-start",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-start",
"leftValue": "={{ $json.action }}",
"rightValue": "start",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "start"
},
{
"id": "batch-restart",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-restart",
"leftValue": "={{ $json.action }}",
"rightValue": "restart",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "restart"
},
{
"id": "batch-stop",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-stop",
"leftValue": "={{ $json.action }}",
"rightValue": "stop",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "stop"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-batch-action",
"name": "Route Batch Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2220,
-300
]
},
{
"parameters": {
"jsCode": "// Build batch stop confirmation message\n// Per context: Batch stop confirms due to fuzzy matching risk\nconst data = $json;\nconst allMatched = data.allMatched;\nconst chatId = data.chatId;\n\nconst count = allMatched.length;\nconst names = allMatched.map(c => c.Name);\nconst namesStr = names.join(',');\nconst timestamp = Math.floor(Date.now() / 1000);\n\n// Build confirmation message\nlet text = `<b>Stop ${count} container${count > 1 ? 's' : ''}?</b>\\n\\n`;\nfor (const name of names) {\n text += `\\u2022 ${name}\\n`;\n}\n\n// Callback format: bstop:confirm:{comma-separated-names}:{timestamp}\nconst confirmCallback = `bstop:confirm:${namesStr}:${timestamp}`;\nconst cancelCallback = 'bstop:cancel';\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: 'Confirm', callback_data: confirmCallback },\n { text: 'Cancel', callback_data: cancelCallback }\n ]\n ]\n }\n }\n};"
},
"id": "code-build-batch-stop-confirm",
"name": "Build Batch Stop Confirmation",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2440,
-200
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"id": "http-send-batch-stop-confirm",
"name": "Send Batch Stop Confirmation",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2660,
-200
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $(\"Parse Callback Data\").item.json.queryId }) }}",
"options": {}
},
"id": "http-answer-batch-stop-confirm",
"name": "Answer Batch Stop Confirm",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
700
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: 'Cancelled' }) }}",
"options": {}
},
"id": "http-answer-batch-stop-cancel",
"name": "Answer Batch Stop Cancel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
800
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId }) }}",
"options": {}
},
"id": "http-delete-batch-stop-cancel",
"name": "Delete Batch Stop Cancel Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1560,
800
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-stop-expired",
"leftValue": "={{ $(\"Parse Callback Data\").item.json.expired }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-batch-stop-expired",
"name": "Check Batch Stop Expired",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1560,
700
]
},
{
"parameters": {
"jsCode": "// Handle expired batch stop confirmation\nconst data = $json;\nreturn {\n json: {\n chat_id: data.chatId,\n message_id: data.messageId,\n text: 'Confirmation expired. Please try again.',\n parse_mode: 'HTML'\n }\n};"
},
"id": "code-batch-stop-expired",
"name": "Build Batch Stop Expired",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
600
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"id": "http-send-batch-stop-expired",
"name": "Send Batch Stop Expired",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2000,
600
]
},
{
"parameters": {
"jsCode": "// Initialize batch state for execution\n// Input comes from Route Batch Action OR batch callbacks (bexec, bstop confirmed)\nconst data = $json;\n\n// Handle different input sources\nlet containers, action, chatId, messageId, fromKeyboard;\n\nif (data.allMatched) {\n // From Route Batch Action or Prepare Batch Exec\n containers = data.allMatched;\n action = data.action;\n chatId = data.chatId;\n messageId = data.messageId || null;\n // Check if fromKeyboard was set by caller (e.g., Prepare Batch Exec for inline keyboard)\n fromKeyboard = data.fromKeyboard || false;\n} else if (data.containerNames) {\n // From batch callback (bexec or bstop:confirm) - keyboard flow\n // Need to resolve names to container objects\n containers = data.containerNames.map(name => ({ Name: name, Id: null }));\n action = data.batchAction || 'stop';\n chatId = data.chatId;\n messageId = data.messageId;\n fromKeyboard = data.fromKeyboard !== false; // Default true for callbacks\n} else {\n throw new Error('Invalid batch state input');\n}\n\nreturn {\n json: {\n containers: containers,\n action: action,\n totalCount: containers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: messageId,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: fromKeyboard\n }\n};"
},
"id": "code-init-batch-state",
"name": "Initialize Batch State",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2440,
-500
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/{{ $json.fromKeyboard ? 'editMessageText' : 'sendMessage' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.fromKeyboard ? { chat_id: $json.chatId, message_id: $json.messageId, text: '<b>Batch ' + $json.action + '</b>\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' } : { chat_id: $json.chatId, text: '<b>Batch ' + $json.action + '</b>\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' }) }}",
"options": {}
},
"id": "http-send-batch-start",
"name": "Send Batch Start Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2660,
-500
]
},
{
"parameters": {
"jsCode": "// Store progress message ID and prepare first iteration\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n throw new Error('Failed to get Initialize Batch State: ' + e.message);\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Get the message ID for progress updates\n// For keyboard: use original messageId (we edit in place)\n// For text commands: use the new message_id from sendMessage response\nlet progressMessageId;\nif (batchState.fromKeyboard) {\n progressMessageId = batchState.messageId;\n} else {\n // Get message_id from Send Batch Start Message response\n const sendResponse = $json;\n progressMessageId = sendResponse.result?.message_id || batchState.messageId;\n}\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false\n }\n};"
},
"id": "code-prepare-batch-loop",
"name": "Prepare Batch Loop",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2880,
-500
]
},
{
"parameters": {
"jsCode": "// Build progress message for current container\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\n\nconst containerName = container.Name || container;\nconst current = currentIndex + 1;\n\nlet progressText = `<b>Batch ${action} in progress...</b>\\n\\n`;\nprogressText += `Current: <b>${containerName}</b>\\n`;\nprogressText += `Progress: ${current}/${totalCount}\\n\\n`;\n\nif (successCount > 0 || failureCount > 0) {\n progressText += `Completed: ${successCount} success`;\n if (failureCount > 0) {\n progressText += `, ${failureCount} failed`;\n }\n}\n\nreturn {\n json: {\n ...data,\n progressText: progressText,\n containerName: containerName\n }\n};"
},
"id": "code-build-progress",
"name": "Build Progress Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3320,
-500
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.progressMessageId, text: $json.progressText, parse_mode: 'HTML' }) }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
}
},
"id": "http-edit-progress",
"name": "Edit Progress Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3540,
-500
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "loop-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-update",
"leftValue": "={{ $(\"Build Progress Message\").item.json.action }}",
"rightValue": "update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "update"
},
{
"id": "loop-start",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-start",
"leftValue": "={{ $(\"Build Progress Message\").item.json.action }}",
"rightValue": "start",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "start"
},
{
"id": "loop-stop",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-stop",
"leftValue": "={{ $(\"Build Progress Message\").item.json.action }}",
"rightValue": "stop",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "stop"
},
{
"id": "loop-restart",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-restart",
"leftValue": "={{ $(\"Build Progress Message\").item.json.action }}",
"rightValue": "restart",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "restart"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-batch-loop-action",
"name": "Route Batch Loop Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
3760,
-500
]
},
{
"parameters": {
"jsCode": "// Prepare data for next iteration or completion\nconst data = $json;\n\n// Check if more containers to process\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst containers = data.containers;\nconst nextIndex = currentIndex + 1;\nconst isComplete = nextIndex >= totalCount;\n\nreturn {\n json: {\n ...data,\n currentIndex: nextIndex,\n container: isComplete ? null : containers[nextIndex],\n isComplete: isComplete\n }\n};"
},
"id": "code-prepare-next-iteration",
"name": "Prepare Next Iteration",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5520,
-400
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "is-batch-complete",
"leftValue": "={{ $json.isComplete }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-batch-complete",
"name": "Is Batch Complete",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
5620,
-400
]
},
{
"parameters": {
"jsCode": "// Prepare batch stop data for execution\n// Input from Parse Callback Data (via Check Batch Stop Expired)\nconst data = $(\"Parse Callback Data\").item.json;\n\n// containerNames is an array from callback\nconst containerNames = data.containerNames || [];\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: data.fromKeyboard || false\n }\n};"
},
"id": "code-prepare-batch-stop-exec",
"name": "Prepare Batch Stop Exec",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
700
]
},
{
"parameters": {
"jsCode": "// Prepare batch exec data for execution\n// Input from Route Batch UI Result (sub-workflow output) or existing batch path\nconst data = $json;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// containerNames can be array or comma-separated string\nlet containerNames = data.containerNames || data.containers || [];\nif (typeof containerNames === 'string') {\n containerNames = containerNames.split(',').filter(n => n);\n}\nif (Array.isArray(containerNames) && containerNames[0] && containerNames[0].name) {\n containerNames = containerNames.map(c => c.name);\n}\n\n// action field might be 'action' or 'batchAction'\nconst action = data.batchAction || data.action || 'start';\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: action,\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: fromKeyboard\n }\n};"
},
"id": "code-prepare-batch-exec",
"name": "Prepare Batch Exec",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
900
]
},
{
"parameters": {
"jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `<b>Batch ${action} Complete</b>\\n\\n`;\n\n// Show failures first and prominently\nconst failures = results.filter(r => r.status === 'error');\nif (failures.length > 0) {\n summaryText += `<b>\\u274c Failed (${failures.length}):</b>\\n`;\n for (const f of failures) {\n summaryText += `\\u2022 ${f.name}: ${f.reason || 'Unknown error'}\\n`;\n }\n summaryText += '\\n';\n}\n\n// Show warnings summary (not individual) per context discretion\nconst warnings = results.filter(r => r.status === 'warning');\nif (warnings.length > 0) {\n // If 3 or fewer warnings, show details; otherwise just count\n if (warnings.length <= 3) {\n summaryText += `<b>\\u26a0\\ufe0f Warnings (${warnings.length}):</b>\\n`;\n for (const w of warnings) {\n summaryText += `\\u2022 ${w.name}: ${w.reason}\\n`;\n }\n summaryText += '\\n';\n } else {\n summaryText += `\\u26a0\\ufe0f Warnings: ${warnings.length} (containers already in desired state)\\n\\n`;\n }\n}\n\n// Show success count\nif (successCount > 0) {\n summaryText += `<b>\\u2705 Successful:</b> ${successCount}/${totalCount}\\n`;\n} else if (failureCount === 0 && warningCount > 0) {\n // All warnings, no success/failures\n summaryText += `No changes needed for ${totalCount} container(s)\\n`;\n}\n\n// Only include Back to List button if user came from inline keyboard\nconst replyMarkup = fromKeyboard ? { inline_keyboard: [[{ text: 'Back to List', callback_data: 'list:0' }]] } : null;\n\nreturn {\n json: {\n chatId: chatId,\n messageId: progressMessageId,\n text: summaryText,\n hasFailures: failureCount > 0,\n reply_markup: replyMarkup\n }\n};"
},
"id": "code-build-batch-summary",
"name": "Build Batch Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5740,
-400
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', ...$json.reply_markup ? { reply_markup: $json.reply_markup } : {} }) }}",
"options": {}
},
"id": "http-send-batch-summary",
"name": "Send Batch Summary",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
5960,
-400
]
},
{
"parameters": {
"url": "http://docker-socket-proxy:2375/containers/json?all=false",
"options": {}
},
"id": "http-get-all-containers-update-all",
"name": "Get All Containers For Update All",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1200,
2200
]
},
{
"parameters": {
"jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\nconst chatId = $('Keyword Router').first().json.message.chat.id;\nconst messageId = $('Keyword Router').first().json.message.message_id;\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to containers using :latest tag\n// For now, we'll mark all :latest containers as \"potentially need update\"\n// A full implementation would pull each image, but that's expensive\n// We'll show all :latest containers and let batch execution handle the actual check\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = c.Image || '';\n // Check if using :latest or no tag specified (defaults to :latest)\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n allContainers: allContainers\n }\n};"
},
"id": "code-check-available-updates",
"name": "Check Available Updates",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1400,
2200
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict"
},
"conditions": [
{
"id": "has-containers",
"leftValue": "={{ $json.count }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-has-updates-available",
"name": "Has Updates Available",
"type": "n8n-nodes-base.if",
"typeVersion": 2.1,
"position": [
1600,
2200
]
},
{
"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};"
},
"id": "code-build-update-all-confirmation",
"name": "Build Update All Confirmation",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1800,
2100
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.message }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}",
"options": {}
},
"id": "telegram-send-update-all-confirmation",
"name": "Send Update All Confirmation",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2000,
2100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.chatId }}",
"text": "All containers are up to date! 🎉",
"options": {}
},
"id": "telegram-send-all-up-to-date",
"name": "Send All Up To Date",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1800,
2300
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "⏱️ Confirmation expired (30s timeout)",
"options": {
"showAlert": true
}
},
"id": "telegram-answer-update-all-expired",
"name": "Answer Update All Expired",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1600,
2900
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.messageId }}"
},
"id": "telegram-delete-update-all-expired",
"name": "Delete Update All Expired",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1800,
2900
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "❌ Update cancelled"
},
"id": "telegram-answer-update-all-cancel",
"name": "Answer Update All Cancel",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1600,
3100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.messageId }}"
},
"id": "telegram-delete-update-all-cancel",
"name": "Delete Update All Cancel",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1800,
3100
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict"
},
"conditions": [
{
"id": "is-expired",
"leftValue": "={{ $json.expired }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-update-all-expired",
"name": "Check Update All Expired",
"type": "n8n-nodes-base.if",
"typeVersion": 2.1,
"position": [
1400,
2700
]
},
{
"parameters": {
"jsCode": "// Get container names from the original update all confirmation\n// The container names were stored in the confirmation flow\n// We need to extract them from context or re-fetch containers\n\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\n\n// For now, we'll re-fetch all containers with :latest tag\n// In production, would use workflow static data or context storage\nreturn {\n json: {\n chatId: chatId,\n messageId: $json.messageId,\n queryId: $json.queryId,\n action: 'update',\n needsContainerFetch: true\n }\n};"
},
"id": "code-get-update-all-data",
"name": "Get Update All Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1600,
2600
]
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "✅ Starting batch update..."
},
"id": "telegram-answer-update-all-confirm",
"name": "Answer Update All Confirm",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1800,
2600
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "message",
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.messageId }}"
},
"id": "telegram-delete-update-all-confirm",
"name": "Delete Update All Confirm",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2000,
2600
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"url": "http://docker-socket-proxy:2375/containers/json?all=false",
"options": {}
},
"id": "http-fetch-containers-update-all-exec",
"name": "Fetch Containers For Update All Exec",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2200,
2600
]
},
{
"parameters": {
"jsCode": "// Prepare batch execution data for update all\n// Get all :latest containers and prepare for batch loop\n\nconst containers = $input.all();\nconst chatId = $json.chatId;\n\n// Extract container data\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to :latest containers\nconst targetContainers = allContainers\n .filter(c => {\n const image = c.Image || '';\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\n// Format for batch execution (compatible with existing batch infrastructure)\nreturn {\n json: {\n chatId: chatId,\n action: 'update',\n containers: targetContainers,\n items: targetContainers.map(c => ({ name: c.name, id: c.id })),\n currentIndex: 0,\n totalCount: targetContainers.length,\n results: []\n }\n};"
},
"id": "code-prepare-update-all-batch",
"name": "Prepare Update All Batch",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2400,
2600
]
},
{
"parameters": {
"jsCode": "// Prepare data to return to container list\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: 0\n }\n};"
},
"id": "code-prepare-batch-cancel-return",
"name": "Prepare Batch Cancel Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2200,
5000
]
},
{
"parameters": {
"jsCode": "// Prepare input for container-update sub-workflow (text mode)\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId: 0, // No message to edit in text mode\n responseMode: 'text'\n }\n};"
},
"id": "code-prepare-text-update",
"name": "Prepare Text Update Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
1200
]
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $('Check Update Match Count').item.json.chatId }}",
"text": "=Updating <b>{{ $('Check Update Match Count').item.json.matches[0].Name }}</b>...",
"additionalFields": {
"parse_mode": "HTML"
}
},
"id": "telegram-text-update-started",
"name": "Send Text Update Started",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1780,
1400
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "7AvTzLtKXM2hZTio92_mC"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-text-update-subworkflow",
"name": "Execute Text Update",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2000,
1200
]
},
{
"parameters": {
"jsCode": "// Prepare input for container-update sub-workflow (inline mode)\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n containerId: '', // Will be resolved by sub-workflow from name\n containerName,\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};"
},
"id": "code-prepare-callback-update",
"name": "Prepare Callback Update Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
1650
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: '\\u2B06\\uFE0F <b>Updating ' + $json.containerName + '...</b>\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.', parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } }) }}",
"options": {}
},
"id": "http-callback-update-progress",
"name": "Show Callback Update Progress",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2220,
1650
]
},
{
"parameters": {
"method": "GET",
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
"options": {}
},
"id": "http-get-container-callback",
"name": "Get Container For Callback Update",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
1650
]
},
{
"parameters": {
"jsCode": "// Find container ID from name for callback update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Callback Update Input').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};"
},
"id": "code-find-container-callback",
"name": "Find Container For Callback Update",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
1650
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "7AvTzLtKXM2hZTio92_mC"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-callback-update-subworkflow",
"name": "Execute Callback Update",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2880,
1650
]
},
{
"parameters": {
"jsCode": "// Prepare input for container actions sub-workflow\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst containerName = data.containerName;\n// Get the actual requested action (stop/start/restart) from Parse Action Command\nconst actionType = $('Parse Action Command').item.json.action || 'restart';\nconst chatId = data.chatId;\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: actionType,\n chatId: chatId,\n messageId: 0, // Text mode doesn't have a message to edit\n responseMode: 'text'\n }\n};"
},
"id": "code-prepare-text-action-rr53pd94",
"name": "Prepare Text Action Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
500
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "fYSZS5PkH0VSEaT5"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-container-action-qokchnw8",
"name": "Execute Container Action",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2000,
500
]
},
{
"parameters": {
"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",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
500
]
},
{
"parameters": {
"jsCode": "// Prepare input for container actions sub-workflow from inline keyboard\n// Container lookup already done by Get Container For Action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: action,\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};"
},
"id": "code-prepare-inline-action-tyjn5pb1",
"name": "Prepare Inline Action Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
1200
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "fYSZS5PkH0VSEaT5"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-inline-action-8aoev7xt",
"name": "Execute Inline Action",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2440,
1200
]
},
{
"parameters": {
"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",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
1200
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};"
},
"id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65",
"name": "Prepare Batch Update Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4000,
-800
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"mode": "list",
"value": "7AvTzLtKXM2hZTio92_mC"
},
"options": {}
},
"id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c",
"name": "Execute Batch Update",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
4220,
-800
]
},
{
"parameters": {
"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",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4440,
-800
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Actions sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\nconst action = data.action;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};"
},
"id": "958f19ef-249b-42ca-8a29-ecb91548f1dd",
"name": "Prepare Batch Action Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4000,
-200
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"mode": "list",
"value": "fYSZS5PkH0VSEaT5"
},
"options": {}
},
"id": "3baebdc9-3cda-478a-b0cc-0fb33a542f03",
"name": "Execute Batch Action Sub-workflow",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
4220,
-200
]
},
{
"parameters": {
"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",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4440,
-200
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Logs sub-workflow (text command)\nconst data = $json;\n\n// Check if there's an error from Parse Logs Command\nif (data.error) {\n return {\n json: {\n error: true,\n chatId: data.chatId,\n text: data.text\n }\n };\n}\n\nreturn {\n json: {\n containerName: data.containerQuery,\n lineCount: data.lines,\n chatId: data.chatId,\n messageId: data.messageId || 0,\n responseMode: \"text\"\n }\n};"
},
"id": "a895bb2d-1f61-4466-b475-b32ec5f0e83a",
"name": "Prepare Text Logs Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
600
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"mode": "list",
"value": "oE7aO2GhbksXDEIw"
},
"options": {}
},
"id": "926c7683-c0e4-41a4-a983-e3f7ecb6ff41",
"name": "Execute Text Logs",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1340,
600
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Logs sub-workflow (inline action)\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n containerName: data.containerName,\n lineCount: 30,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: \"inline\"\n }\n};"
},
"id": "16b24086-5b5d-4980-82c7-4fb37b4e8f6c",
"name": "Prepare Inline Logs Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
1300
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"mode": "list",
"value": "oE7aO2GhbksXDEIw"
},
"options": {}
},
"id": "a88974bd-45c0-401e-b50a-c6171cfe06d4",
"name": "Execute Inline Logs",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2000,
1300
]
},
{
"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};"
},
"id": "b1800598-1ff6-4da3-8506-4e4e8127f902",
"name": "Format Inline Logs Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
1300
]
},
{
"parameters": {
"jsCode": "// Prepare input for Batch UI sub-workflow\nconst data = $json;\n\n// Determine action from callback data\nlet action = 'mode'; // default\nconst callbackData = data.callbackData || '';\n\nif (data.isBatchMode) action = 'mode';\nelse if (data.isBatchToggle) action = 'toggle';\nelse if (data.isBatchNav) action = 'nav';\nelse if (data.isBatchExec) action = 'exec';\nelse if (data.isBatchClear) action = 'clear';\nelse if (data.isBatchCancel) action = 'cancel';\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n queryId: data.queryId,\n callbackData: callbackData,\n action: action,\n batchAction: data.action || 'start',\n batchPage: data.batchPage || 0,\n selectedCsv: data.selectedCsv || '',\n toggleName: data.toggleName || ''\n }\n};"
},
"id": "code-prepare-batch-ui-input",
"name": "Prepare Batch UI Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
3400
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "ZJhnGzJT26UUmW45"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-batch-ui-subworkflow",
"name": "Execute Batch UI",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1780,
3400
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "route-keyboard",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-keyboard",
"leftValue": "={{ $json.action }}",
"rightValue": "keyboard",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "keyboard"
},
{
"id": "route-confirmation",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-confirmation",
"leftValue": "={{ $json.action }}",
"rightValue": "confirmation",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "confirmation"
},
{
"id": "route-execute",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-execute",
"leftValue": "={{ $json.action }}",
"rightValue": "execute",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "execute"
},
{
"id": "route-cancel",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-cancel",
"leftValue": "={{ $json.action }}",
"rightValue": "cancel",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "cancel"
},
{
"id": "route-limit-reached",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-limit-reached",
"leftValue": "={{ $json.action }}",
"rightValue": "limit_reached",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "limit_reached"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-batch-ui-result",
"name": "Route Batch UI Result",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2000,
3400
]
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "={{ $json.answerText || '' }}"
},
"id": "telegram-answer-batch-ui-keyboard",
"name": "Answer Batch UI Keyboard",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2220,
3200
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}",
"options": {}
},
"id": "http-edit-batch-ui-keyboard",
"name": "Edit Batch UI Keyboard",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
3200
]
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "={{ $json.answerText || 'Confirm...' }}"
},
"id": "telegram-answer-batch-ui-confirm",
"name": "Answer Batch UI Confirm",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2220,
3400
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}",
"options": {}
},
"id": "http-edit-batch-ui-confirm",
"name": "Edit Batch UI Confirmation",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
3400
]
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "={{ $json.answerText || 'Maximum selection reached' }}",
"options": {
"showAlert": true
}
},
"id": "telegram-answer-batch-ui-limit",
"name": "Answer Batch UI Limit",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2220,
3800
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"resource": "callback",
"operation": "answerQuery",
"queryId": "={{ $json.queryId }}",
"text": "={{ $json.answerText || 'Batch selection cancelled' }}"
},
"id": "telegram-answer-batch-ui-cancel",
"name": "Answer Batch UI Cancel",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2220,
3600
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
},
{
"parameters": {
"jsCode": "// Prepare input for Container Status sub-workflow from /status command\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\nconst text = (message.text || '').toLowerCase().trim();\n\n// Check if user specified a container name (e.g., \"/status plex\")\nlet searchTerm = null;\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n searchTerm = parts.filter(p => p !== 'status' && p !== '/status').join(' ');\n}\n\nreturn {\n json: {\n chatId: chatId,\n messageId: 0,\n action: 'list',\n containerId: null,\n containerName: null,\n page: 0,\n queryId: null,\n searchTerm: searchTerm\n }\n};"
},
"id": "code-prepare-status-input",
"name": "Prepare Status Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
100
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "lqpg2CqesnKE2RJQ"
},
"options": {
"waitForSubWorkflow": true
},
"mode": "once"
},
"id": "execute-container-status",
"name": "Execute Container Status",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1340,
100
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "route-list-result",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-list",
"leftValue": "={{ $json.action }}",
"rightValue": "list",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "list"
},
{
"id": "route-status-direct",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-status-direct",
"leftValue": "={{ $json.action }}",
"rightValue": "status_direct",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "status_direct"
},
{
"id": "route-error",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "has-error",
"leftValue": "={{ $json.success }}",
"rightValue": false,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "error"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "switch-route-status-result",
"name": "Route Status Result",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1560,
100
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Status sub-workflow from select callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'status',\n containerId: null,\n containerName: data.containerName,\n page: 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};"
},
"id": "code-prepare-select-input",
"name": "Prepare Select Status Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
900
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "lqpg2CqesnKE2RJQ"
},
"options": {
"waitForSubWorkflow": true
},
"mode": "once"
},
"id": "execute-select-status",
"name": "Execute Select Status",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1560,
900
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Status sub-workflow from list pagination callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};"
},
"id": "code-prepare-paginate-input",
"name": "Prepare Paginate Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
1000
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "lqpg2CqesnKE2RJQ"
},
"options": {
"waitForSubWorkflow": true
},
"mode": "once"
},
"id": "execute-paginate-status",
"name": "Execute Paginate Status",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1560,
1000
]
},
{
"parameters": {
"jsCode": "// Prepare input for Container Status sub-workflow from batch cancel return\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId || null,\n searchTerm: null\n }\n};"
},
"id": "code-prepare-batch-cancel-input",
"name": "Prepare Batch Cancel Return Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
1100
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "lqpg2CqesnKE2RJQ"
},
"options": {
"waitForSubWorkflow": true
},
"mode": "once"
},
"id": "execute-batch-cancel-status",
"name": "Execute Batch Cancel Status",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1780,
1100
]
},
{
"parameters": {
"jsCode": "// Prepare input for confirmation sub-workflow\nconst data = $('Parse Callback Data').item.json;\n\n// Determine action based on callback type\nlet action = 'confirm'; // Default\nif (data.isCancelConfirm) {\n action = 'cancel';\n} else if (data.isConfirm && data.expired) {\n action = 'expired';\n} else if (data.isConfirm) {\n action = 'confirm';\n}\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: action,\n containerId: data.containerId || '',\n containerName: data.containerName,\n confirmAction: data.confirmAction || '',\n confirmationToken: data.timestamp || '',\n expired: data.expired || false,\n responseMode: 'inline'\n }\n};"
},
"id": "code-prepare-confirm-input",
"name": "Prepare Confirm Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
1550
]
},
{
"parameters": {
"jsCode": "// Prepare input for showing stop confirmation dialog\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'show_stop',\n containerId: data.containerId || '',\n containerName: data.containerName,\n responseMode: 'inline'\n }\n};"
},
"id": "code-prepare-show-stop",
"name": "Prepare Show Stop Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
1350
]
},
{
"parameters": {
"jsCode": "// Prepare input for showing update confirmation dialog\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'show_update',\n containerId: data.containerId || '',\n containerName: data.containerName,\n responseMode: 'inline'\n }\n};"
},
"id": "code-prepare-show-update",
"name": "Prepare Show Update Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
1450
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "fZ1hu8eiovkCk08G"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-confirmation-workflow",
"name": "Execute Confirmation",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2000,
1500
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "show-dialog",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-show",
"leftValue": "={{ $json.action }}",
"rightValue": "show_",
"operator": {
"type": "string",
"operation": "startsWith"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "show"
},
{
"id": "confirm-stop-result",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-stop-result",
"leftValue": "={{ $json.action }}",
"rightValue": "confirm_stop_result",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "stop_result"
},
{
"id": "confirm-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-confirm-update",
"leftValue": "={{ $json.action }}",
"rightValue": "confirm_update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "confirm_update"
},
{
"id": "cancel",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-cancel",
"leftValue": "={{ $json.action }}",
"rightValue": "cancel",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "cancel"
},
{
"id": "expired",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-expired",
"leftValue": "={{ $json.action }}",
"rightValue": "expired",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "expired"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-confirm-result",
"name": "Route Confirmation Result",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2220,
1500
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-confirm-dialog",
"name": "Send Confirmation Dialog",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
1400
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-stop-result",
"name": "Send Stop Result",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
1500
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
"options": {}
},
"id": "http-send-expired-msg",
"name": "Send Expired Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2440,
1700
]
},
{
"parameters": {
"jsCode": "// Prepare cancel return data from confirmation result\nconst result = $input.item.json;\n\nreturn {\n json: {\n containerName: result.containerName,\n chatId: result.chatId,\n messageId: result.messageId\n }\n};"
},
"id": "code-prepare-cancel-from-confirm",
"name": "Prepare Cancel From Confirm",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2440,
1600
]
},
{
"parameters": {
"jsCode": "// Prepare input for matching sub-workflow (action commands)\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\nreturn {\n json: {\n action: \"match_action\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0\n }\n};"
},
"id": "code-prepare-action-match-input",
"name": "Prepare Action Match Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
400
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "kL4BoI8ITSP9Oxek"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-action-match",
"name": "Execute Action Match",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1560,
400
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "route-matched",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-matched",
"leftValue": "={{ $json.action }}",
"rightValue": "matched",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "matched"
},
{
"id": "route-suggestion",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-suggestion",
"leftValue": "={{ $json.action }}",
"rightValue": "suggestion",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "suggestion"
},
{
"id": "route-no-match",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-no-match",
"leftValue": "={{ $json.action }}",
"rightValue": "no_match",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "no_match"
},
{
"id": "route-error",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-error",
"leftValue": "={{ $json.action }}",
"rightValue": "error",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "error"
},
{
"id": "route-multiple",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-multiple",
"leftValue": "={{ $json.action }}",
"rightValue": "multiple",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "multiple"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-action-match-result",
"name": "Route Action Match Result",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1780,
400
]
},
{
"parameters": {
"jsCode": "// Prepare input for matching sub-workflow (update commands)\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\nreturn {\n json: {\n action: \"match_update\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0\n }\n};"
},
"id": "code-prepare-update-match-input",
"name": "Prepare Update Match Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
1000
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "kL4BoI8ITSP9Oxek"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-update-match",
"name": "Execute Update Match",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1560,
1000
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "route-matched-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-matched-update",
"leftValue": "={{ $json.action }}",
"rightValue": "matched_update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "matched_update"
},
{
"id": "route-multiple-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-multiple-update",
"leftValue": "={{ $json.action }}",
"rightValue": "multiple_update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "multiple_update"
},
{
"id": "route-no-match-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-no-match-update",
"leftValue": "={{ $json.action }}",
"rightValue": "no_match_update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "no_match_update"
},
{
"id": "route-update-error",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-update-error",
"leftValue": "={{ $json.action }}",
"rightValue": "error",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "error"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-update-match-result",
"name": "Route Update Match Result",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1780,
1000
]
},
{
"parameters": {
"jsCode": "// Prepare input for matching sub-workflow (batch commands)\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Convert containerNames array to CSV for sub-workflow input\nconst selectedContainers = Array.isArray(containerNames) ? containerNames.join(',') : containerNames;\n\nreturn {\n json: {\n action: \"match_batch\",\n containerList: dockerOutput,\n searchTerm: \"\",\n selectedContainers: selectedContainers,\n chatId: chatId,\n messageId: messageId\n }\n};"
},
"id": "code-prepare-batch-match-input",
"name": "Prepare Batch Match Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
-300
]
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "kL4BoI8ITSP9Oxek"
},
"mode": "once",
"options": {
"waitForSubWorkflow": true
}
},
"id": "exec-batch-match",
"name": "Execute Batch Match",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1780,
-300
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "route-batch-matched",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-batch-matched",
"leftValue": "={{ $json.action }}",
"rightValue": "batch_matched",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "batch_matched"
},
{
"id": "route-batch-disambig",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-batch-disambig",
"leftValue": "={{ $json.action }}",
"rightValue": "disambiguation",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "disambiguation"
},
{
"id": "route-batch-not-found",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-batch-not-found",
"leftValue": "={{ $json.action }}",
"rightValue": "not_found",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "not_found"
},
{
"id": "route-batch-error",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-batch-error",
"leftValue": "={{ $json.action }}",
"rightValue": "error",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "error"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-batch-match-result",
"name": "Route Batch Match Result",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2000,
-300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "has-message-id",
"leftValue": "={{ $json.messageId }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-has-status-message-id",
"name": "Has Status Message ID",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1890,
1000
]
},
{
"parameters": {
"jsCode": "// Strip inline keyboard for text-mode status display\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n text: data.text,\n parse_mode: 'HTML'\n }\n};"
},
"id": "code-strip-status-keyboard",
"name": "Strip Status Keyboard",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1945,
1100
]
},
{
"parameters": {
"jsCode": "// Transform matching sub-workflow output to batch execution format\nconst matchResult = $input.item.json;\nconst batchCmd = $('Detect Batch Command').item.json;\n\nreturn {\n json: {\n allMatched: matchResult.matchedContainers,\n action: batchCmd.action,\n chatId: matchResult.chatId,\n messageId: batchCmd.messageId || 0,\n originalContainerNames: matchResult.originalContainerNames\n }\n};"
},
"id": "code-prepare-batch-execution",
"name": "Prepare Batch Execution",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2110,
-300
]
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Route Update Type",
"type": "main",
"index": 0
}
]
]
},
"Route Update Type": {
"main": [
[
{
"node": "IF User Authenticated",
"type": "main",
"index": 0
}
],
[
{
"node": "IF Callback Authenticated",
"type": "main",
"index": 0
}
]
]
},
"IF User Authenticated": {
"main": [
[
{
"node": "Keyword Router",
"type": "main",
"index": 0
}
]
]
},
"IF Callback Authenticated": {
"main": [
[
{
"node": "Parse Callback Data",
"type": "main",
"index": 0
}
]
]
},
"Parse Callback Data": {
"main": [
[
{
"node": "Route Callback",
"type": "main",
"index": 0
}
]
]
},
"Route Callback": {
"main": [
[
{
"node": "Check Update All Expired",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Update All Cancel",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Cancel",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Expired",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Select Callback",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Select Callback",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer List Callback",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Action Callback",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Noop Callback",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Confirm Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Confirm Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Batch Stop Confirm",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Batch Stop Cancel",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch UI Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch UI Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch UI Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch UI Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch UI Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch UI Input",
"type": "main",
"index": 0
}
],
[]
]
},
"Handle Cancel": {
"main": [
[
{
"node": "Answer Cancel Query",
"type": "main",
"index": 0
}
]
]
},
"Answer Cancel Query": {
"main": [
[
{
"node": "Delete Cancel Message",
"type": "main",
"index": 0
}
]
]
},
"Handle Expired": {
"main": [
[
{
"node": "Answer Expired Query",
"type": "main",
"index": 0
}
]
]
},
"Answer Expired Query": {
"main": [
[
{
"node": "Delete Expired Message",
"type": "main",
"index": 0
}
]
]
},
"Build Callback Action": {
"main": [
[
{
"node": "Execute Callback Action",
"type": "main",
"index": 0
}
]
]
},
"Execute Callback Action": {
"main": [
[
{
"node": "Parse Callback Result",
"type": "main",
"index": 0
}
]
]
},
"Parse Callback Result": {
"main": [
[
{
"node": "Answer Action Query",
"type": "main",
"index": 0
}
]
]
},
"Answer Action Query": {
"main": [
[
{
"node": "Delete Suggestion Message",
"type": "main",
"index": 0
}
]
]
},
"Delete Suggestion Message": {
"main": [
[
{
"node": "Send Callback Result",
"type": "main",
"index": 0
}
]
]
},
"Docker List for Action": {
"main": [
[
{
"node": "Prepare Action Match Input",
"type": "main",
"index": 0
}
]
]
},
"Build Action Command": {
"main": [
[
{
"node": "Execute Action",
"type": "main",
"index": 0
}
]
]
},
"Execute Action": {
"main": [
[
{
"node": "Parse Action Result",
"type": "main",
"index": 0
}
]
]
},
"Parse Action Result": {
"main": [
[
{
"node": "Send Action Result",
"type": "main",
"index": 0
}
]
]
},
"Build Batch Keyboard": {
"main": [
[
{
"node": "Send Batch Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Parse Update Command": {
"main": [
[
{
"node": "Docker List for Update",
"type": "main",
"index": 0
}
]
]
},
"Docker List for Update": {
"main": [
[
{
"node": "Prepare Update Match Input",
"type": "main",
"index": 0
}
]
]
},
"Handle Update Multiple": {
"main": [
[
{
"node": "Send Update Multiple",
"type": "main",
"index": 0
}
]
]
},
"Parse Logs Command": {
"main": [
[
{
"node": "Prepare Text Logs Input",
"type": "main",
"index": 0
}
]
]
},
"Keyword Router": {
"main": [
[
{
"node": "Show Menu",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Status Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect Batch Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect Batch Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect Batch Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Get All Containers For Update All",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect Batch Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse Logs Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Status Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Show Menu",
"type": "main",
"index": 0
}
]
]
},
"Detect Batch Command": {
"main": [
[
{
"node": "Is Batch Command",
"type": "main",
"index": 0
}
]
]
},
"Is Batch Command": {
"main": [
[
{
"node": "Get Containers for Batch",
"type": "main",
"index": 0
}
],
[
{
"node": "Route Single Action",
"type": "main",
"index": 0
}
]
]
},
"Route Single Action": {
"main": [
[
{
"node": "Parse Action Command",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse Update Command",
"type": "main",
"index": 0
}
]
]
},
"Get Containers for Batch": {
"main": [
[
{
"node": "Prepare Batch Match Input",
"type": "main",
"index": 0
}
]
]
},
"Route Batch Action": {
"main": [
[
{
"node": "Initialize Batch State",
"type": "main",
"index": 0
}
],
[
{
"node": "Initialize Batch State",
"type": "main",
"index": 0
}
],
[
{
"node": "Initialize Batch State",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Batch Stop Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Build Batch Stop Confirmation": {
"main": [
[
{
"node": "Send Batch Stop Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Answer Batch Stop Confirm": {
"main": [
[
{
"node": "Check Batch Stop Expired",
"type": "main",
"index": 0
}
]
]
},
"Check Batch Stop Expired": {
"main": [
[
{
"node": "Build Batch Stop Expired",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch Stop Exec",
"type": "main",
"index": 0
}
]
]
},
"Build Batch Stop Expired": {
"main": [
[
{
"node": "Send Batch Stop Expired",
"type": "main",
"index": 0
}
]
]
},
"Answer Batch Stop Cancel": {
"main": [
[
{
"node": "Delete Batch Stop Cancel Message",
"type": "main",
"index": 0
}
]
]
},
"Parse Action Command": {
"main": [
[
{
"node": "Docker List for Action",
"type": "main",
"index": 0
}
]
]
},
"Answer Action Callback": {
"main": [
[
{
"node": "Route Action Type",
"type": "main",
"index": 0
}
]
]
},
"Route Action Type": {
"main": [
[
{
"node": "Prepare Immediate Action",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Immediate Action",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Show Stop Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Show Update Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Inline Logs Input",
"type": "main",
"index": 0
}
]
]
},
"Prepare Immediate Action": {
"main": [
[
{
"node": "Get Container For Action",
"type": "main",
"index": 0
}
]
]
},
"Get Container For Action": {
"main": [
[
{
"node": "Prepare Inline Action Input",
"type": "main",
"index": 0
}
]
]
},
"Build Immediate Action Command": {
"main": [
[
{
"node": "Execute Immediate Action",
"type": "main",
"index": 0
}
]
]
},
"Execute Immediate Action": {
"main": [
[
{
"node": "Format Immediate Result",
"type": "main",
"index": 0
}
]
]
},
"Format Immediate Result": {
"main": [
[
{
"node": "Send Immediate Result",
"type": "main",
"index": 0
}
]
]
},
"Prepare Cancel Return": {
"main": [
[
{
"node": "Get Container For Cancel",
"type": "main",
"index": 0
}
]
]
},
"Get Container For Cancel": {
"main": [
[
{
"node": "Build Cancel Return Submenu",
"type": "main",
"index": 0
}
]
]
},
"Build Cancel Return Submenu": {
"main": [
[
{
"node": "Send Cancel Return Submenu",
"type": "main",
"index": 0
}
]
]
},
"Initialize Batch State": {
"main": [
[
{
"node": "Send Batch Start Message",
"type": "main",
"index": 0
}
]
]
},
"Send Batch Start Message": {
"main": [
[
{
"node": "Prepare Batch Loop",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Loop": {
"main": [
[
{
"node": "Build Progress Message",
"type": "main",
"index": 0
}
]
]
},
"Build Progress Message": {
"main": [
[
{
"node": "Edit Progress Message",
"type": "main",
"index": 0
}
]
]
},
"Edit Progress Message": {
"main": [
[
{
"node": "Route Batch Loop Action",
"type": "main",
"index": 0
}
]
]
},
"Route Batch Loop Action": {
"main": [
[
{
"node": "Prepare Batch Update Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch Action Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch Action Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch Action Input",
"type": "main",
"index": 0
}
]
]
},
"Prepare Next Iteration": {
"main": [
[
{
"node": "Is Batch Complete",
"type": "main",
"index": 0
}
]
]
},
"Is Batch Complete": {
"main": [
[
{
"node": "Build Batch Summary",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Progress Message",
"type": "main",
"index": 0
}
]
]
},
"Build Batch Summary": {
"main": [
[
{
"node": "Send Batch Summary",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Stop Exec": {
"main": [
[
{
"node": "Initialize Batch State",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Exec": {
"main": [
[
{
"node": "Initialize Batch State",
"type": "main",
"index": 0
}
]
]
},
"Get All Containers For Update All": {
"main": [
[
{
"node": "Check Available Updates",
"type": "main",
"index": 0
}
]
]
},
"Check Available Updates": {
"main": [
[
{
"node": "Has Updates Available",
"type": "main",
"index": 0
}
]
]
},
"Has Updates Available": {
"main": [
[
{
"node": "Build Update All Confirmation",
"type": "main",
"index": 0
}
],
[
{
"node": "Send All Up To Date",
"type": "main",
"index": 0
}
]
]
},
"Build Update All Confirmation": {
"main": [
[
{
"node": "Send Update All Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Check Update All Expired": {
"main": [
[
{
"node": "Answer Update All Expired",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Update All Data",
"type": "main",
"index": 0
}
]
]
},
"Answer Update All Expired": {
"main": [
[
{
"node": "Delete Update All Expired",
"type": "main",
"index": 0
}
]
]
},
"Answer Update All Cancel": {
"main": [
[
{
"node": "Delete Update All Cancel",
"type": "main",
"index": 0
}
]
]
},
"Get Update All Data": {
"main": [
[
{
"node": "Answer Update All Confirm",
"type": "main",
"index": 0
}
]
]
},
"Answer Update All Confirm": {
"main": [
[
{
"node": "Delete Update All Confirm",
"type": "main",
"index": 0
}
]
]
},
"Delete Update All Confirm": {
"main": [
[
{
"node": "Fetch Containers For Update All Exec",
"type": "main",
"index": 0
}
]
]
},
"Fetch Containers For Update All Exec": {
"main": [
[
{
"node": "Prepare Update All Batch",
"type": "main",
"index": 0
}
]
]
},
"Prepare Update All Batch": {
"main": [
[
{
"node": "Send Batch Start Message",
"type": "main",
"index": 0
}
]
]
},
"Prepare Text Update Input": {
"main": [
[
{
"node": "Execute Text Update",
"type": "main",
"index": 0
}
]
]
},
"Prepare Callback Update Input": {
"main": [
[
{
"node": "Show Callback Update Progress",
"type": "main",
"index": 0
}
]
]
},
"Show Callback Update Progress": {
"main": [
[
{
"node": "Get Container For Callback Update",
"type": "main",
"index": 0
}
]
]
},
"Get Container For Callback Update": {
"main": [
[
{
"node": "Find Container For Callback Update",
"type": "main",
"index": 0
}
]
]
},
"Find Container For Callback Update": {
"main": [
[
{
"node": "Execute Callback Update",
"type": "main",
"index": 0
}
]
]
},
"Prepare Text Action Input": {
"main": [
[
{
"node": "Execute Container Action",
"type": "main",
"index": 0
}
]
]
},
"Execute Container Action": {
"main": [
[
{
"node": "Handle Text Action Result",
"type": "main",
"index": 0
}
]
]
},
"Handle Text Action Result": {
"main": [
[
{
"node": "Send Action Result",
"type": "main",
"index": 0
}
]
]
},
"Prepare Inline Action Input": {
"main": [
[
{
"node": "Execute Inline Action",
"type": "main",
"index": 0
}
]
]
},
"Execute Inline Action": {
"main": [
[
{
"node": "Handle Inline Action Result",
"type": "main",
"index": 0
}
]
]
},
"Handle Inline Action Result": {
"main": [
[
{
"node": "Send Immediate Result",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Update Input": {
"main": [
[
{
"node": "Execute Batch Update",
"type": "main",
"index": 0
}
]
]
},
"Execute Batch Update": {
"main": [
[
{
"node": "Handle Batch Update Result",
"type": "main",
"index": 0
}
]
]
},
"Handle Batch Update Result": {
"main": [
[
{
"node": "Prepare Next Iteration",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Action Input": {
"main": [
[
{
"node": "Execute Batch Action Sub-workflow",
"type": "main",
"index": 0
}
]
]
},
"Execute Batch Action Sub-workflow": {
"main": [
[
{
"node": "Handle Batch Action Result Sub",
"type": "main",
"index": 0
}
]
]
},
"Handle Batch Action Result Sub": {
"main": [
[
{
"node": "Prepare Next Iteration",
"type": "main",
"index": 0
}
]
]
},
"Prepare Text Logs Input": {
"main": [
[
{
"node": "Execute Text Logs",
"type": "main",
"index": 0
}
]
]
},
"Execute Text Logs": {
"main": [
[
{
"node": "Send Logs Response",
"type": "main",
"index": 0
}
]
]
},
"Prepare Inline Logs Input": {
"main": [
[
{
"node": "Execute Inline Logs",
"type": "main",
"index": 0
}
]
]
},
"Execute Inline Logs": {
"main": [
[
{
"node": "Format Inline Logs Result",
"type": "main",
"index": 0
}
]
]
},
"Format Inline Logs Result": {
"main": [
[
{
"node": "Send Logs Result",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch UI Input": {
"main": [
[
{
"node": "Execute Batch UI",
"type": "main",
"index": 0
}
]
]
},
"Execute Batch UI": {
"main": [
[
{
"node": "Route Batch UI Result",
"type": "main",
"index": 0
}
]
]
},
"Route Batch UI Result": {
"main": [
[
{
"node": "Answer Batch UI Keyboard",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Batch UI Confirm",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Batch Exec",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Batch UI Cancel",
"type": "main",
"index": 0
}
],
[
{
"node": "Answer Batch UI Limit",
"type": "main",
"index": 0
}
]
]
},
"Answer Batch UI Keyboard": {
"main": [
[
{
"node": "Edit Batch UI Keyboard",
"type": "main",
"index": 0
}
]
]
},
"Answer Batch UI Confirm": {
"main": [
[
{
"node": "Edit Batch UI Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Answer Batch UI Cancel": {
"main": [
[
{
"node": "Prepare Batch Cancel Return",
"type": "main",
"index": 0
}
]
]
},
"Prepare Status Input": {
"main": [
[
{
"node": "Execute Container Status",
"type": "main",
"index": 0
}
]
]
},
"Execute Container Status": {
"main": [
[
{
"node": "Route Status Result",
"type": "main",
"index": 0
}
]
]
},
"Route Status Result": {
"main": [
[
{
"node": "Send Container List",
"type": "main",
"index": 0
}
],
[
{
"node": "Has Status Message ID",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Docker Error",
"type": "main",
"index": 0
}
],
[]
]
},
"Answer Select Callback": {
"main": [
[
{
"node": "Prepare Select Status Input",
"type": "main",
"index": 0
}
]
]
},
"Prepare Select Status Input": {
"main": [
[
{
"node": "Execute Select Status",
"type": "main",
"index": 0
}
]
]
},
"Execute Select Status": {
"main": [
[
{
"node": "Send Container Submenu",
"type": "main",
"index": 0
}
]
]
},
"Answer List Callback": {
"main": [
[
{
"node": "Prepare Paginate Input",
"type": "main",
"index": 0
}
]
]
},
"Prepare Paginate Input": {
"main": [
[
{
"node": "Execute Paginate Status",
"type": "main",
"index": 0
}
]
]
},
"Execute Paginate Status": {
"main": [
[
{
"node": "Edit Container List",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Cancel Return": {
"main": [
[
{
"node": "Prepare Batch Cancel Return Input",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Cancel Return Input": {
"main": [
[
{
"node": "Execute Batch Cancel Status",
"type": "main",
"index": 0
}
]
]
},
"Execute Batch Cancel Status": {
"main": [
[
{
"node": "Edit Container List",
"type": "main",
"index": 0
}
]
]
},
"Prepare Confirm Input": {
"main": [
[
{
"node": "Execute Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Prepare Show Stop Input": {
"main": [
[
{
"node": "Execute Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Prepare Show Update Input": {
"main": [
[
{
"node": "Execute Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Execute Confirmation": {
"main": [
[
{
"node": "Route Confirmation Result",
"type": "main",
"index": 0
}
]
]
},
"Route Confirmation Result": {
"main": [
[
{
"node": "Send Confirmation Dialog",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Stop Result",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Callback Update Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Cancel From Confirm",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Expired Message",
"type": "main",
"index": 0
}
]
]
},
"Prepare Cancel From Confirm": {
"main": [
[
{
"node": "Get Container For Cancel",
"type": "main",
"index": 0
}
]
]
},
"Prepare Action Match Input": {
"main": [
[
{
"node": "Execute Action Match",
"type": "main",
"index": 0
}
]
]
},
"Execute Action Match": {
"main": [
[
{
"node": "Route Action Match Result",
"type": "main",
"index": 0
}
]
]
},
"Route Action Match Result": {
"main": [
[
{
"node": "Prepare Text Action Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Suggestion",
"type": "main",
"index": 0
}
],
[
{
"node": "Send No Match",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Docker Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Batch Keyboard",
"type": "main",
"index": 0
}
]
]
},
"Prepare Update Match Input": {
"main": [
[
{
"node": "Execute Update Match",
"type": "main",
"index": 0
}
]
]
},
"Execute Update Match": {
"main": [
[
{
"node": "Route Update Match Result",
"type": "main",
"index": 0
}
]
]
},
"Route Update Match Result": {
"main": [
[
{
"node": "Prepare Text Update Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Update Multiple",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Update No Match",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Update Error",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Match Input": {
"main": [
[
{
"node": "Execute Batch Match",
"type": "main",
"index": 0
}
]
]
},
"Execute Batch Match": {
"main": [
[
{
"node": "Route Batch Match Result",
"type": "main",
"index": 0
}
]
]
},
"Route Batch Match Result": {
"main": [
[
{
"node": "Prepare Batch Execution",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Disambiguation",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Not Found Message",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Docker Error",
"type": "main",
"index": 0
}
]
]
},
"Has Status Message ID": {
"main": [
[
{
"node": "Edit Container List",
"type": "main",
"index": 0
}
],
[
{
"node": "Strip Status Keyboard",
"type": "main",
"index": 0
}
]
]
},
"Strip Status Keyboard": {
"main": [
[
{
"node": "Send Container Submenu Direct",
"type": "main",
"index": 0
}
]
]
},
"Prepare Batch Execution": {
"main": [
[
{
"node": "Route Batch Action",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 1,
"active": false
}