fix(12-02): Update All flow — 9 bug fixes from UAT
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 <noreply@anthropic.com>
This commit is contained in:
+132
-12
@@ -370,6 +370,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"id": "is-inline",
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"options": {
|
"options": {
|
||||||
"caseSensitive": true,
|
"caseSensitive": true,
|
||||||
@@ -378,7 +382,7 @@
|
|||||||
},
|
},
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"id": "is-inline",
|
"id": "inline-check",
|
||||||
"leftValue": "={{ $json.responseMode }}",
|
"leftValue": "={{ $json.responseMode }}",
|
||||||
"rightValue": "inline",
|
"rightValue": "inline",
|
||||||
"operator": {
|
"operator": {
|
||||||
@@ -389,12 +393,41 @@
|
|||||||
],
|
],
|
||||||
"combinator": "and"
|
"combinator": "and"
|
||||||
},
|
},
|
||||||
"options": {}
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"fallbackOutput": "extra"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"id": "if-response-mode-success",
|
"id": "if-response-mode-success",
|
||||||
"name": "Check Response Mode (Success)",
|
"name": "Check Response Mode (Success)",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.switch",
|
||||||
"typeVersion": 2,
|
"typeVersion": 3.2,
|
||||||
"position": [
|
"position": [
|
||||||
3760,
|
3760,
|
||||||
100
|
100
|
||||||
@@ -489,6 +522,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"id": "is-inline-no-update",
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"options": {
|
"options": {
|
||||||
"caseSensitive": true,
|
"caseSensitive": true,
|
||||||
@@ -497,7 +534,7 @@
|
|||||||
},
|
},
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"id": "is-inline-no-update",
|
"id": "inline-check",
|
||||||
"leftValue": "={{ $json.responseMode }}",
|
"leftValue": "={{ $json.responseMode }}",
|
||||||
"rightValue": "inline",
|
"rightValue": "inline",
|
||||||
"operator": {
|
"operator": {
|
||||||
@@ -508,12 +545,41 @@
|
|||||||
],
|
],
|
||||||
"combinator": "and"
|
"combinator": "and"
|
||||||
},
|
},
|
||||||
"options": {}
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"fallbackOutput": "extra"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"id": "if-response-mode-no-update",
|
"id": "if-response-mode-no-update",
|
||||||
"name": "Check Response Mode (No Update)",
|
"name": "Check Response Mode (No Update)",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.switch",
|
||||||
"typeVersion": 2,
|
"typeVersion": 3.2,
|
||||||
"position": [
|
"position": [
|
||||||
2440,
|
2440,
|
||||||
300
|
300
|
||||||
@@ -590,6 +656,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"id": "is-inline-error",
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"options": {
|
"options": {
|
||||||
"caseSensitive": true,
|
"caseSensitive": true,
|
||||||
@@ -598,7 +668,7 @@
|
|||||||
},
|
},
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"id": "is-inline-error",
|
"id": "inline-check",
|
||||||
"leftValue": "={{ $json.responseMode }}",
|
"leftValue": "={{ $json.responseMode }}",
|
||||||
"rightValue": "inline",
|
"rightValue": "inline",
|
||||||
"operator": {
|
"operator": {
|
||||||
@@ -609,12 +679,41 @@
|
|||||||
],
|
],
|
||||||
"combinator": "and"
|
"combinator": "and"
|
||||||
},
|
},
|
||||||
"options": {}
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"fallbackOutput": "extra"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"id": "if-response-mode-error",
|
"id": "if-response-mode-error",
|
||||||
"name": "Check Response Mode (Error)",
|
"name": "Check Response Mode (Error)",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.switch",
|
||||||
"typeVersion": 2,
|
"typeVersion": 3.2,
|
||||||
"position": [
|
"position": [
|
||||||
1780,
|
1780,
|
||||||
400
|
400
|
||||||
@@ -917,6 +1016,13 @@
|
|||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Remove Old Image (Success)",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Send Text Success",
|
"node": "Send Text Success",
|
||||||
@@ -979,6 +1085,13 @@
|
|||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Return No Update",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Send Text No Update",
|
"node": "Send Text No Update",
|
||||||
@@ -1030,6 +1143,13 @@
|
|||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Return Error",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Send Text Error",
|
"node": "Send Text Error",
|
||||||
|
|||||||
+22
-28
@@ -2397,7 +2397,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"id": "code-prepare-batch-loop",
|
||||||
"name": "Prepare Batch Loop",
|
"name": "Prepare Batch Loop",
|
||||||
@@ -2629,7 +2629,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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};"
|
"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// 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",
|
"id": "code-build-batch-summary",
|
||||||
"name": "Build Batch Summary",
|
"name": "Build Batch Summary",
|
||||||
@@ -2674,7 +2674,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"id": "code-check-available-updates",
|
||||||
"name": "Check Available Updates",
|
"name": "Check Available Updates",
|
||||||
@@ -2718,7 +2718,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"id": "code-build-update-all-confirmation",
|
||||||
"name": "Build Update All Confirmation",
|
"name": "Build Update All Confirmation",
|
||||||
@@ -2731,28 +2731,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"resource": "message",
|
"method": "POST",
|
||||||
"operation": "sendMessage",
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/{{ $json.isCallback ? 'editMessageText' : 'sendMessage' }}",
|
||||||
"chatId": "={{ $json.chatId }}",
|
"sendBody": true,
|
||||||
"text": "={{ $json.message }}",
|
"specifyBody": "json",
|
||||||
"replyMarkup": "inlineKeyboard",
|
"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 } : {})) }}",
|
||||||
"inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}",
|
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "telegram-send-update-all-confirmation",
|
"id": "telegram-send-update-all-confirmation",
|
||||||
"name": "Send Update All Confirmation",
|
"name": "Send Update All Confirmation",
|
||||||
"type": "n8n-nodes-base.telegram",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 1.2,
|
"typeVersion": 4.2,
|
||||||
"position": [
|
"position": [
|
||||||
2000,
|
2000,
|
||||||
2100
|
2100
|
||||||
],
|
]
|
||||||
"credentials": {
|
|
||||||
"telegramApi": {
|
|
||||||
"id": "I0xTTiASl7C1NZhJ",
|
|
||||||
"name": "Telegram account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -2806,8 +2799,8 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"resource": "message",
|
"resource": "message",
|
||||||
"operation": "deleteMessage",
|
"operation": "deleteMessage",
|
||||||
"chatId": "={{ $json.chatId }}",
|
"chatId": "={{ $('Parse Callback Data').item.json.chatId }}",
|
||||||
"messageId": "={{ $json.messageId }}"
|
"messageId": "={{ $('Parse Callback Data').item.json.messageId }}"
|
||||||
},
|
},
|
||||||
"id": "telegram-delete-update-all-expired",
|
"id": "telegram-delete-update-all-expired",
|
||||||
"name": "Delete Update All Expired",
|
"name": "Delete Update All Expired",
|
||||||
@@ -2850,8 +2843,8 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"resource": "message",
|
"resource": "message",
|
||||||
"operation": "deleteMessage",
|
"operation": "deleteMessage",
|
||||||
"chatId": "={{ $json.chatId }}",
|
"chatId": "={{ $('Parse Callback Data').item.json.chatId }}",
|
||||||
"messageId": "={{ $json.messageId }}"
|
"messageId": "={{ $('Parse Callback Data').item.json.messageId }}"
|
||||||
},
|
},
|
||||||
"id": "telegram-delete-update-all-cancel",
|
"id": "telegram-delete-update-all-cancel",
|
||||||
"name": "Delete Update All Cancel",
|
"name": "Delete Update All Cancel",
|
||||||
@@ -2938,8 +2931,8 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"resource": "message",
|
"resource": "message",
|
||||||
"operation": "deleteMessage",
|
"operation": "deleteMessage",
|
||||||
"chatId": "={{ $json.chatId }}",
|
"chatId": "={{ $('Parse Callback Data').item.json.chatId }}",
|
||||||
"messageId": "={{ $json.messageId }}"
|
"messageId": "={{ $('Parse Callback Data').item.json.messageId }}"
|
||||||
},
|
},
|
||||||
"id": "telegram-delete-update-all-confirm",
|
"id": "telegram-delete-update-all-confirm",
|
||||||
"name": "Delete Update All Confirm",
|
"name": "Delete Update All Confirm",
|
||||||
@@ -2972,7 +2965,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"id": "code-prepare-update-all-batch",
|
||||||
"name": "Prepare Update All Batch",
|
"name": "Prepare Update All Batch",
|
||||||
@@ -3210,7 +3203,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65",
|
||||||
"name": "Prepare Batch Update Input",
|
"name": "Prepare Batch Update Input",
|
||||||
@@ -4783,7 +4776,8 @@
|
|||||||
"id": "I0xTTiASl7C1NZhJ",
|
"id": "I0xTTiASl7C1NZhJ",
|
||||||
"name": "Telegram account"
|
"name": "Telegram account"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"onError": "continueRegularOutput"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": {
|
"connections": {
|
||||||
|
|||||||
Reference in New Issue
Block a user