From c2c2ce70921d656f04bfe0783b5b0239457dffc0 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 8 Feb 2026 18:04:20 -0500 Subject: [PATCH] =?UTF-8?q?fix(12-02):=20Update=20All=20flow=20=E2=80=94?= =?UTF-8?q?=209=20bug=20fixes=20from=20UAT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes discovered during BATCH-04/BATCH-05 UAT testing: - Convert confirmation to HTTP Request (editMessageText for inline kb, sendMessage for text command) with dynamic endpoint selection - Fix data chain breaks: use named node refs ($('Parse Callback Data'), $('Get Update All Data')) instead of $json after API calls - Add infrastructure container exclusion (n8n, socket-proxy) by image and container name to prevent bot self-destruction during updates - Add batch responseMode to update sub-workflow (skip Telegram messages) - Reorder infra check before :latest filter so sha256-digest images appear in skipped list - Add onError:continueRegularOutput to Answer Update All Start for expired callback queries - Show "Back to List" button in batch summary for update-all flow - Add Prepare Update All Batch fallback in Prepare Batch Loop Co-Authored-By: Claude Opus 4.6 --- n8n-update.json | 228 +++++++++++++++++++++++++++++++++++----------- n8n-workflow.json | 50 +++++----- 2 files changed, 196 insertions(+), 82 deletions(-) diff --git a/n8n-update.json b/n8n-update.json index 0c7a55a..2cc7478 100644 --- a/n8n-update.json +++ b/n8n-update.json @@ -370,31 +370,64 @@ }, { "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ + "rules": { + "values": [ { "id": "is-inline", - "leftValue": "={{ $json.responseMode }}", - "rightValue": "inline", - "operator": { - "type": "string", - "operation": "equals" - } + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "inline-check", + "leftValue": "={{ $json.responseMode }}", + "rightValue": "inline", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "outputKey": "inline" + }, + { + "id": "is-batch", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "batch-check", + "leftValue": "={{ $json.responseMode }}", + "rightValue": "batch", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "outputKey": "batch" } - ], - "combinator": "and" + ] }, - "options": {} + "options": { + "fallbackOutput": "extra" + } }, "id": "if-response-mode-success", "name": "Check Response Mode (Success)", - "type": "n8n-nodes-base.if", - "typeVersion": 2, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, "position": [ 3760, 100 @@ -489,31 +522,64 @@ }, { "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ + "rules": { + "values": [ { "id": "is-inline-no-update", - "leftValue": "={{ $json.responseMode }}", - "rightValue": "inline", - "operator": { - "type": "string", - "operation": "equals" - } + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "inline-check", + "leftValue": "={{ $json.responseMode }}", + "rightValue": "inline", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "outputKey": "inline" + }, + { + "id": "is-batch-no-update", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "batch-check", + "leftValue": "={{ $json.responseMode }}", + "rightValue": "batch", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "outputKey": "batch" } - ], - "combinator": "and" + ] }, - "options": {} + "options": { + "fallbackOutput": "extra" + } }, "id": "if-response-mode-no-update", "name": "Check Response Mode (No Update)", - "type": "n8n-nodes-base.if", - "typeVersion": 2, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, "position": [ 2440, 300 @@ -590,31 +656,64 @@ }, { "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ + "rules": { + "values": [ { "id": "is-inline-error", - "leftValue": "={{ $json.responseMode }}", - "rightValue": "inline", - "operator": { - "type": "string", - "operation": "equals" - } + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "inline-check", + "leftValue": "={{ $json.responseMode }}", + "rightValue": "inline", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "outputKey": "inline" + }, + { + "id": "is-batch-error", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "batch-check", + "leftValue": "={{ $json.responseMode }}", + "rightValue": "batch", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "outputKey": "batch" } - ], - "combinator": "and" + ] }, - "options": {} + "options": { + "fallbackOutput": "extra" + } }, "id": "if-response-mode-error", "name": "Check Response Mode (Error)", - "type": "n8n-nodes-base.if", - "typeVersion": 2, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, "position": [ 1780, 400 @@ -917,6 +1016,13 @@ "index": 0 } ], + [ + { + "node": "Remove Old Image (Success)", + "type": "main", + "index": 0 + } + ], [ { "node": "Send Text Success", @@ -979,6 +1085,13 @@ "index": 0 } ], + [ + { + "node": "Return No Update", + "type": "main", + "index": 0 + } + ], [ { "node": "Send Text No Update", @@ -1030,6 +1143,13 @@ "index": 0 } ], + [ + { + "node": "Return Error", + "type": "main", + "index": 0 + } + ], [ { "node": "Send Text Error", diff --git a/n8n-workflow.json b/n8n-workflow.json index 0632f79..ba9d945 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -2397,7 +2397,7 @@ }, { "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};" + "jsCode": "// Store progress message ID and prepare first iteration\n// Support both regular batch path (Initialize Batch State) and update-all path (Prepare Update All Batch)\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n try {\n batchState = $('Prepare Update All Batch').item.json;\n } catch (e2) {\n throw new Error('Failed to get batch state from either source');\n }\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 fromBatchUpdateAll: batchState.fromBatchUpdateAll || false\n }\n};" }, "id": "code-prepare-batch-loop", "name": "Prepare Batch Loop", @@ -2629,7 +2629,7 @@ }, { "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 = `Batch ${action} Complete\\n\\n`;\n\n// Show failures first and prominently\nconst failures = results.filter(r => r.status === 'error');\nif (failures.length > 0) {\n summaryText += `\\u274c Failed (${failures.length}):\\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 += `\\u26a0\\ufe0f Warnings (${warnings.length}):\\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 += `\\u2705 Successful: ${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};" + "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 = `Batch ${action} Complete\\n\\n`;\n\n// Show failures first and prominently\nconst failures = results.filter(r => r.status === 'error');\nif (failures.length > 0) {\n summaryText += `\\u274c Failed (${failures.length}):\\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 += `\\u26a0\\ufe0f Warnings (${warnings.length}):\\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 += `\\u2705 Successful: ${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// Include Back to List button if user came from inline keyboard or update-all flow\nconst fromBatchUpdateAll = data.fromBatchUpdateAll || false;\nconst replyMarkup = (fromKeyboard || fromBatchUpdateAll) ? { 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", @@ -2674,7 +2674,7 @@ }, { "parameters": { - "jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\n\n// Get chatId and messageId from either text command or callback origin\nlet chatId, messageId;\ntry {\n const kwData = $('Keyword Router').first().json;\n chatId = kwData.message.chat.id;\n messageId = kwData.message.message_id;\n} catch(e) {\n // Inline keyboard origin -- data from Parse Callback Data\n const cbData = $('Parse Callback Data').first().json;\n chatId = cbData.chatId;\n messageId = cbData.messageId;\n}\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};" + "jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\n\n// Get chatId and messageId from either text command or callback origin\nlet chatId, messageId;\ntry {\n const kwData = $('Keyword Router').first().json;\n chatId = kwData.message.chat.id;\n messageId = kwData.message.message_id;\n} catch(e) {\n // Inline keyboard origin -- data from Parse Callback Data\n const cbData = $('Parse Callback Data').first().json;\n chatId = cbData.chatId;\n messageId = cbData.messageId;\n}\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 :latest containers, excluding infrastructure\n// Infrastructure containers would break the bot if updated mid-execution\n// Check both image AND container name (image may be sha256 digest)\nconst infraPatterns = ['n8n', 'socket-proxy'];\nconst skipped = [];\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = (c.Image || '').toLowerCase();\n const name = ((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : '').toLowerCase();\n // Check infrastructure FIRST so they always appear in skipped list\n const isInfra = infraPatterns.some(p => image.includes(p) || name.includes(p));\n if (isInfra) {\n skipped.push((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown');\n return false;\n }\n // Then filter to :latest or untagged only\n if (!image.includes(':latest') && image.includes(':')) return false;\n return true;\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// isCallback = true when triggered via inline keyboard button\nlet isCallback = false;\ntry { $('Parse Callback Data').first(); isCallback = true; } catch(e) {}\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n isCallback: isCallback,\n allContainers: allContainers,\n skippedInfra: skipped\n }\n};" }, "id": "code-check-available-updates", "name": "Check Available Updates", @@ -2718,7 +2718,7 @@ }, { "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};" + "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\nconst skipped = data.skippedInfra || [];\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` : '';\nconst skipText = skipped.length > 0 ? `\\n\\n\u26a0\ufe0f Skipped (infrastructure): ${skipped.join(', ')}` : '';\n\nconst message = `Update ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerList}${moreText}${skipText}`;\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 text: message,\n reply_markup: {\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 isCallback: data.isCallback || false,\n containerNames: containerNames,\n containers: containers,\n timestamp: timestamp\n }\n};" }, "id": "code-build-update-all-confirmation", "name": "Build Update All Confirmation", @@ -2731,28 +2731,21 @@ }, { "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.message }}", - "replyMarkup": "inlineKeyboard", - "inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}", + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/{{ $json.isCallback ? 'editMessageText' : 'sendMessage' }}", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify(Object.assign({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }, $json.isCallback ? { message_id: $json.messageId } : {})) }}", "options": {} }, "id": "telegram-send-update-all-confirmation", "name": "Send Update All Confirmation", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, "position": [ 2000, 2100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } + ] }, { "parameters": { @@ -2806,8 +2799,8 @@ "parameters": { "resource": "message", "operation": "deleteMessage", - "chatId": "={{ $json.chatId }}", - "messageId": "={{ $json.messageId }}" + "chatId": "={{ $('Parse Callback Data').item.json.chatId }}", + "messageId": "={{ $('Parse Callback Data').item.json.messageId }}" }, "id": "telegram-delete-update-all-expired", "name": "Delete Update All Expired", @@ -2850,8 +2843,8 @@ "parameters": { "resource": "message", "operation": "deleteMessage", - "chatId": "={{ $json.chatId }}", - "messageId": "={{ $json.messageId }}" + "chatId": "={{ $('Parse Callback Data').item.json.chatId }}", + "messageId": "={{ $('Parse Callback Data').item.json.messageId }}" }, "id": "telegram-delete-update-all-cancel", "name": "Delete Update All Cancel", @@ -2938,8 +2931,8 @@ "parameters": { "resource": "message", "operation": "deleteMessage", - "chatId": "={{ $json.chatId }}", - "messageId": "={{ $json.messageId }}" + "chatId": "={{ $('Parse Callback Data').item.json.chatId }}", + "messageId": "={{ $('Parse Callback Data').item.json.messageId }}" }, "id": "telegram-delete-update-all-confirm", "name": "Delete Update All Confirm", @@ -2972,7 +2965,7 @@ }, { "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};" + "jsCode": "// Prepare batch execution data for update all\n// Output same shape as Initialize Batch State for batch loop compatibility\n\nconst containers = $input.all();\nconst prevData = $('Get Update All Data').item.json;\nconst chatId = prevData.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// Exclude infrastructure containers that would break the bot if updated:\n// - n8n: updating kills the running workflow mid-execution\n// - docker-socket-proxy: updating kills Docker API access\nconst infraPatterns = ['n8n', 'socket-proxy'];\nconst skipped = [];\n\nconst targetContainers = allContainers\n .filter(c => {\n const image = (c.Image || '').toLowerCase();\n const name = ((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : '').toLowerCase();\n // Check infrastructure FIRST so they always appear in skipped list\n const isInfra = infraPatterns.some(p => image.includes(p) || name.includes(p));\n if (isInfra) {\n skipped.push((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown');\n return false;\n }\n // Then filter to :latest or untagged\n if (!image.includes(':latest') && image.includes(':')) return false;\n return true;\n })\n .map(c => ({\n Name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n Id: c.Id ? c.Id.substring(0, 12) : null,\n Image: c.Image\n }));\n\n// Match Initialize Batch State output shape exactly\nreturn {\n json: {\n containers: targetContainers,\n action: 'update',\n totalCount: targetContainers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: null,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: false,\n fromBatchUpdateAll: true,\n skippedInfra: skipped\n }\n};" }, "id": "code-prepare-update-all-batch", "name": "Prepare Update All Batch", @@ -3210,7 +3203,7 @@ }, { "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 correlationId: $input.item.json.correlationId || ''\n }\n};" + "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\n// Use batch mode (suppresses sub-workflow Telegram messages) for update-all path\nconst responseMode = data.fromBatchUpdateAll ? 'batch' : 'inline';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: responseMode,\n correlationId: $input.item.json.correlationId || ''\n }\n};" }, "id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65", "name": "Prepare Batch Update Input", @@ -4783,7 +4776,8 @@ "id": "I0xTTiASl7C1NZhJ", "name": "Telegram account" } - } + }, + "onError": "continueRegularOutput" } ], "connections": {