diff --git a/n8n-workflow.json b/n8n-workflow.json
index 833495b..39e53f8 100644
--- a/n8n-workflow.json
+++ b/n8n-workflow.json
@@ -272,6 +272,29 @@
"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": {
@@ -732,7 +755,7 @@
},
{
"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}\n const parts = rest.split(':'); // confirm, names, timestamp\n if (parts[0] === 'confirm' && parts.length >= 3) {\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).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 isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\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// 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};"
+ "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}\n const parts = rest.split(':'); // confirm, names, timestamp\n if (parts[0] === 'confirm' && parts.length >= 3) {\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).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 isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\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') {\n // Enter batch selection mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: 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.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const selectedCsv = parts[0]; // Currently selected (comma-separated)\n const toggleName = parts.slice(1).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\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: true,\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",
@@ -747,6 +770,54 @@
"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": {
@@ -1034,6 +1105,126 @@
},
"renameOutput": true,
"outputKey": "batchExec"
+ },
+ {
+ "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-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"
}
]
},
@@ -2402,7 +2593,7 @@
"resource": "message",
"operation": "sendMessage",
"chatId": "={{ $json.message.chat.id }}",
- "text": "Commands:\n\n• status\n• start [name]\n• stop [name]\n• restart [name]\n• update [name]\n• logs [name]",
+ "text": "Commands:\n\n\u2022 status\n\u2022 start [name]\n\u2022 stop [name]\n\u2022 restart [name]\n\u2022 update [name]\n\u2022 logs [name]",
"additionalFields": {
"parse_mode": "HTML"
}
@@ -4835,6 +5026,768 @@
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 => `\u2022 ${c.name}`).join('\\n');\nconst moreText = count > 10 ? `\\n...and ${count - 10} more` : '';\n\nconst message = `Update ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerList}${moreText}`;\n\n// Create inline keyboard\nconst timestamp = Math.floor(Date.now() / 1000);\nconst containerNames = containers.map(c => c.name).join(',');\n\n// Encode container names in callback (will need to lookup IDs later)\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n message: message,\n keyboard: {\n inline_keyboard: [\n [\n { text: '\u2705 Confirm', callback_data: `uall:confirm:${timestamp}` },\n { text: '\u274c Cancel', callback_data: 'uall:cancel' }\n ]\n ]\n },\n containerNames: containerNames,\n containers: containers,\n timestamp: timestamp\n }\n};"
+ },
+ "id": "code-build-update-all-confirmation",
+ "name": "Build Update All Confirmation",
+ "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! \ud83c\udf89",
+ "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": "\u23f1\ufe0f 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": "\u274c 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": "\u2705 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": {
+ "url": "http://docker-socket-proxy:2375/containers/json?all=false",
+ "options": {}
+ },
+ "id": "http-fetch-containers-batch-mode",
+ "name": "Fetch Containers For Batch Mode",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.2,
+ "position": [
+ 1800,
+ 3500
+ ]
+ },
+ {
+ "parameters": {
+ "jsCode": "// Build batch selection keyboard with toggle checkmarks\nconst containers = $input.all();\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\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// Sort: running first, then alphabetically\nconst sortedContainers = allContainers\n .map(c => ({\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n state: c.State,\n id: c.Id.substring(0, 12)\n }))\n .sort((a, b) => {\n if (a.state === 'running' && b.state !== 'running') return -1;\n if (a.state !== 'running' && b.state === 'running') return 1;\n return a.name.localeCompare(b.name);\n });\n\n// Limit to 8 containers to avoid callback_data overflow\nconst maxContainers = 8;\nconst displayContainers = sortedContainers.slice(0, maxContainers);\n\n// Build keyboard (no selection yet)\nconst keyboard = displayContainers.map(c => {\n const icon = c.state === 'running' ? '\ud83d\udfe2' : '\u26aa';\n return [\n {\n text: `${icon} ${c.name}`,\n callback_data: `batch:toggle::${c.name}`\n }\n ];\n});\n\n// Add action buttons (disabled initially)\nkeyboard.push([\n { text: 'Cancel', callback_data: 'batch:cancel' }\n]);\n\nconst message = `Select containers for batch action:\\n(Tap to select/deselect)`;\n\nreturn {\n json: {\n chatId: chatId,\n messageId: messageId,\n message: message,\n keyboard: {\n inline_keyboard: keyboard\n },\n containers: displayContainers\n }\n};"
+ },
+ "id": "code-build-batch-select-keyboard",
+ "name": "Build Batch Select Keyboard",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2000,
+ 3500
+ ]
+ },
+ {
+ "parameters": {
+ "resource": "callback",
+ "operation": "answerQuery",
+ "queryId": "={{ $json.queryId }}",
+ "text": "Select containers..."
+ },
+ "id": "telegram-answer-batch-mode",
+ "name": "Answer Batch Mode Query",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2200,
+ 3500
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "resource": "message",
+ "operation": "editMessageText",
+ "chatId": "={{ $json.chatId }}",
+ "messageId": "={{ $json.messageId }}",
+ "text": "={{ $json.message }}",
+ "replyMarkup": "inlineKeyboard",
+ "inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}",
+ "options": {}
+ },
+ "id": "telegram-edit-batch-select-keyboard",
+ "name": "Edit To Batch Select Keyboard",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2400,
+ 3500
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "// Handle container toggle in batch selection\nconst selectedCsv = $json.selectedCsv || '';\nconst toggleName = $json.toggleName;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\n// Parse current selection\nconst selectedSet = new Set(selectedCsv ? selectedCsv.split(',') : []);\n\n// Toggle the container\nif (selectedSet.has(toggleName)) {\n selectedSet.delete(toggleName);\n} else {\n selectedSet.add(toggleName);\n}\n\n// Convert back to CSV\nconst newSelected = Array.from(selectedSet).filter(n => n).join(',');\n\n// Calculate callback size limit (64 bytes)\n// Format: batch:toggle:{csv}:{name}\n// Need to leave room for longest container name\nconst callbackPrefix = 'batch:toggle:';\nconst longestName = 20; // Estimate max container name in this set\nconst maxCsvLength = 64 - callbackPrefix.length - longestName - 2; // -2 for colons\n\n// Check if we're at limit\nif (newSelected.length > maxCsvLength) {\n return {\n json: {\n queryId: queryId,\n chatId: chatId,\n messageId: messageId,\n atLimit: true,\n selectedCsv: selectedCsv, // Revert to previous\n toggleName: toggleName\n }\n };\n}\n\nreturn {\n json: {\n queryId: queryId,\n chatId: chatId,\n messageId: messageId,\n selectedCsv: newSelected,\n selectedCount: selectedSet.size,\n atLimit: false,\n needsKeyboardUpdate: true\n }\n};"
+ },
+ "id": "code-handle-batch-toggle",
+ "name": "Handle Batch Toggle",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1800,
+ 3900
+ ]
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "at-limit",
+ "leftValue": "={{ $json.atLimit }}",
+ "rightValue": true,
+ "operator": {
+ "type": "boolean",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "id": "if-at-limit",
+ "name": "Check At Limit",
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2.1,
+ "position": [
+ 2000,
+ 3900
+ ]
+ },
+ {
+ "parameters": {
+ "resource": "callback",
+ "operation": "answerQuery",
+ "queryId": "={{ $json.queryId }}",
+ "text": "\u26a0\ufe0f Maximum selection reached",
+ "options": {
+ "showAlert": true
+ }
+ },
+ "id": "telegram-answer-limit-reached",
+ "name": "Answer Limit Reached",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2200,
+ 4000
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "url": "http://docker-socket-proxy:2375/containers/json?all=false",
+ "options": {}
+ },
+ "id": "http-fetch-containers-toggle",
+ "name": "Fetch Containers For Toggle Update",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.2,
+ "position": [
+ 2200,
+ 3800
+ ]
+ },
+ {
+ "parameters": {
+ "jsCode": "// Rebuild batch selection keyboard with updated checkmarks\nconst containers = $input.all();\nconst selectedCsv = $json.selectedCsv || '';\nconst selectedCount = $json.selectedCount || 0;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\n// Parse selection\nconst selectedSet = new Set(selectedCsv ? selectedCsv.split(',') : []);\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// Sort and format\nconst sortedContainers = allContainers\n .map(c => ({\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n state: c.State,\n id: c.Id.substring(0, 12)\n }))\n .sort((a, b) => {\n if (a.state === 'running' && b.state !== 'running') return -1;\n if (a.state !== 'running' && b.state === 'running') return 1;\n return a.name.localeCompare(b.name);\n })\n .slice(0, 8);\n\n// Build keyboard with checkmarks\nconst keyboard = sortedContainers.map(c => {\n const isSelected = selectedSet.has(c.name);\n const icon = c.state === 'running' ? '\ud83d\udfe2' : '\u26aa';\n const checkmark = isSelected ? '\u2713 ' : '';\n return [\n {\n text: `${checkmark}${icon} ${c.name}`,\n callback_data: `batch:toggle:${selectedCsv}:${c.name}`\n }\n ];\n});\n\n// Add action buttons if selection exists\nif (selectedCount > 0) {\n keyboard.push([\n { text: `Update (${selectedCount})`, callback_data: `batch:exec:update:${selectedCsv}` },\n { text: `Stop (${selectedCount})`, callback_data: `batch:exec:stop:${selectedCsv}` }\n ]);\n keyboard.push([\n { text: 'Clear', callback_data: 'batch:clear' },\n { text: 'Cancel', callback_data: 'batch:cancel' }\n ]);\n} else {\n keyboard.push([\n { text: 'Cancel', callback_data: 'batch:cancel' }\n ]);\n}\n\nconst message = selectedCount > 0 \n ? `Selected: ${selectedCount} container${selectedCount !== 1 ? 's' : ''}\\n(Tap to select/deselect)`\n : 'Select containers for batch action:\\n(Tap to select/deselect)';\n\nreturn {\n json: {\n queryId: queryId,\n chatId: chatId,\n messageId: messageId,\n message: message,\n keyboard: {\n inline_keyboard: keyboard\n },\n selectedCsv: selectedCsv\n }\n};"
+ },
+ "id": "code-rebuild-batch-select-keyboard",
+ "name": "Rebuild Batch Select Keyboard",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2400,
+ 3800
+ ]
+ },
+ {
+ "parameters": {
+ "resource": "callback",
+ "operation": "answerQuery",
+ "queryId": "={{ $json.queryId }}"
+ },
+ "id": "telegram-answer-toggle",
+ "name": "Answer Toggle Query",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2600,
+ 3800
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "resource": "message",
+ "operation": "editMessageText",
+ "chatId": "={{ $json.chatId }}",
+ "messageId": "={{ $json.messageId }}",
+ "text": "={{ $json.message }}",
+ "replyMarkup": "inlineKeyboard",
+ "inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}",
+ "options": {}
+ },
+ "id": "telegram-edit-batch-keyboard",
+ "name": "Edit Batch Select Keyboard",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2800,
+ 3800
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "// Handle batch:exec callback - route to batch confirmation or execution\nconst action = $json.action;\nconst selectedCsv = $json.selectedCsv;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\n// Split names\nconst containerNames = selectedCsv.split(',').filter(n => n);\n\n// Check if stop action (needs confirmation per CONTEXT)\nconst needsConfirmation = action === 'stop';\n\nreturn {\n json: {\n chatId: chatId,\n messageId: messageId,\n queryId: queryId,\n action: action,\n containerNames: containerNames,\n selectedCsv: selectedCsv,\n count: containerNames.length,\n needsConfirmation: needsConfirmation\n }\n};"
+ },
+ "id": "code-handle-batch-exec",
+ "name": "Handle Batch Exec",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2000,
+ 4400
+ ]
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "needs-confirm",
+ "leftValue": "={{ $json.needsConfirmation }}",
+ "rightValue": true,
+ "operator": {
+ "type": "boolean",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "id": "if-needs-batch-confirmation",
+ "name": "Needs Batch Confirmation",
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2.1,
+ "position": [
+ 2200,
+ 4400
+ ]
+ },
+ {
+ "parameters": {
+ "jsCode": "// Build stop confirmation for batch selection\nconst containerNames = $json.containerNames;\nconst count = $json.count;\nconst chatId = $json.chatId;\nconst selectedCsv = $json.selectedCsv;\n\nconst timestamp = Math.floor(Date.now() / 1000);\n\nconst message = `Stop ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerNames.map(n => '\u2022 ' + n).join('\\n')}`;\n\nreturn {\n json: {\n chatId: chatId,\n messageId: $json.messageId,\n queryId: $json.queryId,\n message: message,\n keyboard: {\n inline_keyboard: [\n [\n { text: '\u2705 Confirm Stop', callback_data: `bstop:${selectedCsv}:${timestamp}` },\n { text: '\u274c Cancel', callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};"
+ },
+ "id": "code-build-batch-select-stop-confirm",
+ "name": "Build Batch Select Stop Confirmation",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2400,
+ 4300
+ ]
+ },
+ {
+ "parameters": {
+ "resource": "callback",
+ "operation": "answerQuery",
+ "queryId": "={{ $json.queryId }}",
+ "text": "Confirm stop..."
+ },
+ "id": "telegram-answer-batch-exec-stop",
+ "name": "Answer Batch Exec Stop Query",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2600,
+ 4300
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "resource": "message",
+ "operation": "editMessageText",
+ "chatId": "={{ $json.chatId }}",
+ "messageId": "={{ $json.messageId }}",
+ "text": "={{ $json.message }}",
+ "replyMarkup": "inlineKeyboard",
+ "inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}",
+ "options": {}
+ },
+ "id": "telegram-edit-stop-confirmation",
+ "name": "Edit To Stop Confirmation",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2800,
+ 4300
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "// Prepare immediate batch execution (non-stop actions)\nconst action = $json.action;\nconst containerNames = $json.containerNames;\nconst chatId = $json.chatId;\n\n// Format as bexec callback format for existing batch infrastructure\nreturn {\n json: {\n chatId: chatId,\n messageId: $json.messageId,\n queryId: $json.queryId,\n action: action,\n isBatch: true,\n containerNames: containerNames.join(','),\n containers: containerNames.map(name => ({ name: name })),\n items: containerNames.map(name => ({ name: name }))\n }\n};"
+ },
+ "id": "code-prepare-immediate-batch-exec",
+ "name": "Prepare Immediate Batch Exec",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2400,
+ 4500
+ ]
+ },
+ {
+ "parameters": {
+ "resource": "callback",
+ "operation": "answerQuery",
+ "queryId": "={{ $json.queryId }}",
+ "text": "Starting batch {{ $json.action }}..."
+ },
+ "id": "telegram-answer-batch-exec-immediate",
+ "name": "Answer Batch Exec Immediate Query",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2600,
+ 4500
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "resource": "message",
+ "operation": "deleteMessage",
+ "chatId": "={{ $json.chatId }}",
+ "messageId": "={{ $json.messageId }}"
+ },
+ "id": "telegram-delete-batch-select-message",
+ "name": "Delete Batch Select Message",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2800,
+ 4500
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "// Clear batch selection\nreturn {\n json: {\n chatId: $json.chatId,\n messageId: $json.messageId,\n queryId: $json.queryId,\n selectedCsv: '',\n selectedCount: 0,\n needsKeyboardUpdate: true\n }\n};"
+ },
+ "id": "code-handle-batch-clear",
+ "name": "Handle Batch Clear",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2000,
+ 4800
+ ]
+ },
+ {
+ "parameters": {
+ "resource": "callback",
+ "operation": "answerQuery",
+ "queryId": "={{ $json.queryId }}",
+ "text": "Batch selection cancelled"
+ },
+ "id": "telegram-answer-batch-cancel",
+ "name": "Answer Batch Cancel Query",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2000,
+ 5000
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "resource": "message",
+ "operation": "deleteMessage",
+ "chatId": "={{ $json.chatId }}",
+ "messageId": "={{ $json.messageId }}"
+ },
+ "id": "telegram-delete-batch-cancel-message",
+ "name": "Delete Batch Cancel Message",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 2200,
+ 5000
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "I0xTTiASl7C1NZhJ",
+ "name": "Telegram account"
+ }
+ }
}
],
"connections": {
@@ -4906,14 +5859,14 @@
"main": [
[
{
- "node": "Handle Cancel",
+ "node": "Check Update All Expired",
"type": "main",
"index": 0
}
],
[
{
- "node": "Handle Expired",
+ "node": "Answer Update All Cancel",
"type": "main",
"index": 0
}
@@ -4976,25 +5929,49 @@
],
[
{
- "node": "Answer Batch Stop Confirm",
+ "node": "Fetch Containers For Batch Mode",
"type": "main",
"index": 0
}
],
[
{
- "node": "Answer Batch Stop Cancel",
+ "node": "Handle Batch Toggle",
"type": "main",
"index": 0
}
],
[
{
- "node": "Answer Batch Exec",
+ "node": "Handle Batch Exec",
"type": "main",
"index": 0
}
- ]
+ ],
+ [
+ {
+ "node": "Handle Batch Clear",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Answer Batch Cancel Query",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ []
]
},
"Answer Select Callback": {
@@ -5998,7 +6975,7 @@
],
[
{
- "node": "Detect Batch Command",
+ "node": "Get All Containers For Update All",
"type": "main",
"index": 0
}
@@ -7087,6 +8064,353 @@
}
]
]
+ },
+ "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
+ }
+ ]
+ ]
+ },
+ "Fetch Containers For Batch Mode": {
+ "main": [
+ [
+ {
+ "node": "Build Batch Select Keyboard",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Build Batch Select Keyboard": {
+ "main": [
+ [
+ {
+ "node": "Answer Batch Mode Query",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Answer Batch Mode Query": {
+ "main": [
+ [
+ {
+ "node": "Edit To Batch Select Keyboard",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Handle Batch Toggle": {
+ "main": [
+ [
+ {
+ "node": "Check At Limit",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Check At Limit": {
+ "main": [
+ [
+ {
+ "node": "Answer Limit Reached",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Fetch Containers For Toggle Update",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Fetch Containers For Toggle Update": {
+ "main": [
+ [
+ {
+ "node": "Rebuild Batch Select Keyboard",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Rebuild Batch Select Keyboard": {
+ "main": [
+ [
+ {
+ "node": "Answer Toggle Query",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Answer Toggle Query": {
+ "main": [
+ [
+ {
+ "node": "Edit Batch Select Keyboard",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Handle Batch Exec": {
+ "main": [
+ [
+ {
+ "node": "Needs Batch Confirmation",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Needs Batch Confirmation": {
+ "main": [
+ [
+ {
+ "node": "Build Batch Select Stop Confirmation",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Prepare Immediate Batch Exec",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Build Batch Select Stop Confirmation": {
+ "main": [
+ [
+ {
+ "node": "Answer Batch Exec Stop Query",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Answer Batch Exec Stop Query": {
+ "main": [
+ [
+ {
+ "node": "Edit To Stop Confirmation",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Prepare Immediate Batch Exec": {
+ "main": [
+ [
+ {
+ "node": "Answer Batch Exec Immediate Query",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Answer Batch Exec Immediate Query": {
+ "main": [
+ [
+ {
+ "node": "Delete Batch Select Message",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Delete Batch Select Message": {
+ "main": [
+ [
+ {
+ "node": "Prepare Batch Exec",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Handle Batch Clear": {
+ "main": [
+ [
+ {
+ "node": "Fetch Containers For Toggle Update",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Answer Batch Cancel Query": {
+ "main": [
+ [
+ {
+ "node": "Delete Batch Cancel Message",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
}
},
"pinData": {},
@@ -7097,4 +8421,4 @@
"tags": [],
"triggerCount": 1,
"active": false
-}
+}
\ No newline at end of file