From 821230613d75964c23fdea803f6e991c5209fc66 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 12 Feb 2026 08:47:57 -0500 Subject: [PATCH] Removing stale backups --- n8n-workflow.json.backup-batch | 6672 ---------------------------- n8n-workflow.json.backup-confirm | 6517 ---------------------------- n8n-workflow.json.backup-matching | 6524 ---------------------------- n8n-workflow.json.backup-status | 6741 ----------------------------- 4 files changed, 26454 deletions(-) delete mode 100644 n8n-workflow.json.backup-batch delete mode 100644 n8n-workflow.json.backup-confirm delete mode 100644 n8n-workflow.json.backup-matching delete mode 100644 n8n-workflow.json.backup-status diff --git a/n8n-workflow.json.backup-batch b/n8n-workflow.json.backup-batch deleted file mode 100644 index 8ebff59..0000000 --- a/n8n-workflow.json.backup-batch +++ /dev/null @@ -1,6672 +0,0 @@ -{ - "name": "Docker Manager Bot", - "nodes": [ - { - "parameters": { - "updates": [ - "message", - "callback_query" - ] - }, - "id": "telegram-trigger", - "name": "Telegram Trigger", - "type": "n8n-nodes-base.telegramTrigger", - "typeVersion": 1.1, - "position": [ - 240, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - }, - "webhookId": "86b8d6bb-7c15-4aae-a749-fcaf2dfcb9e0" - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-message", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-message", - "leftValue": "={{ $json.message?.text }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "message" - }, - { - "id": "route-callback", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-callback", - "leftValue": "={{ $json.callback_query?.id }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "callback_query" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-update-type", - "name": "Route Update Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 460, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "user-auth-condition", - "leftValue": "={{ $json.message.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-auth", - "name": "IF User Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "callback-auth-condition", - "leftValue": "={{ $json.callback_query.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-callback-auth", - "name": "IF Callback Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "keyword-menu-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-start-cmd", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/start", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "menu" - }, - { - "id": "keyword-status", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-status", - "leftValue": "={{ $json.message.text }}", - "rightValue": "status", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status" - }, - { - "id": "keyword-restart", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-restart", - "leftValue": "={{ $json.message.text }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "keyword-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-start", - "leftValue": "={{ $json.message.text }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "keyword-stop", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-stop", - "leftValue": "={{ $json.message.text }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "keyword-update-all", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "matches-update-all", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update.?all|updateall", - "operator": { - "type": "string", - "operation": "regex" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "keyword-update", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-update", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "keyword-logs", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-logs", - "leftValue": "={{ $json.message.text }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-keyword-router", - "name": "Keyword Router", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 900, - 200 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list", - "name": "Docker List Containers", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 100 - ] - }, - { - "parameters": { - "jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-action-command", - "name": "Parse Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 400 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-action", - "name": "Docker List for Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-container", - "name": "Match Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-match-count", - "name": "Check Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};" - }, - "id": "code-build-action-cmd", - "name": "Build Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 400 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-action", - "name": "Execute Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2000, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};" - }, - "id": "code-parse-action-result", - "name": "Parse Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 400 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-action-result", - "name": "Send Action Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};" - }, - "id": "code-find-closest", - "name": "Find Closest Match", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-suggestion", - "leftValue": "={{ $json.hasSuggestion }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-suggestion", - "name": "Check Suggestion", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - 300 - ] - }, - { - "parameters": { - "jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// Use modern action callback format: action:{action}:{containerName}\nconst callbackData = `action:${action}:${suggestedName}`;\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '${query}' found.\\n\\nDid you mean ${suggestedName}?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-suggestion", - "name": "Build Suggestion Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-suggestion", - "name": "Send Suggestion", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.query }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-no-match", - "name": "Send No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp for 30s timeout\n\n// Build callback_data using new bexec format\n// Format: bexec:{action}:{comma-separated-names}:{timestamp}\n// Limit to 4 containers to stay within 64-byte callback_data limit\nlet limitedNames = names;\nlet limitedCount = names.length;\nif (names.length > 4) {\n limitedNames = names.slice(0, 4);\n limitedCount = 4;\n}\n\nconst namesStr = limitedNames.join(',');\nconst callbackData = `bexec:${action}:${namesStr}:${timestamp}`;\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found ${matches.length} containers matching '${query}':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};" - }, - "id": "code-build-batch-keyboard", - "name": "Build Batch Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chat_id }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML", - "reply_markup": "={{ JSON.stringify($json.reply_markup) }}" - } - }, - "id": "telegram-send-batch-confirm", - "name": "Send Batch Confirmation", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-docker-error", - "name": "Send Docker Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp} or bstop:confirm:{names}:{timestamp}:kb\n const parts = rest.split(':'); // confirm, names, timestamp[, kb]\n if (parts[0] === 'confirm' && parts.length >= 3) {\n // Check for :kb marker (indicates inline keyboard origin)\n const fromKeyboard = parts[parts.length - 1] === 'kb';\n const timestampIdx = fromKeyboard ? parts.length - 2 : parts.length - 1;\n const timestamp = parseInt(parts[timestampIdx]);\n const namesStr = parts.slice(1, timestampIdx).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n fromKeyboard: fromKeyboard,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n\n\nif (rawData === 'batch:mode' || rawData.startsWith('batch:mode:')) {\n // Enter batch selection mode with optional page: batch:mode or batch:mode:{page}\n let page = 0;\n if (rawData.startsWith('batch:mode:')) {\n page = parseInt(rawData.substring(11)) || 0;\n }\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: true,\n batchPage: page,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:nav:')) {\n // Navigate batch selection with preserved selection: batch:nav:{page}:{selected_csv}\n const parts = rawData.substring(10).split(':'); // Remove 'batch:nav:'\n const page = parseInt(parts[0]) || 0;\n const selectedCsv = parts.slice(1).join(':') || '';\n const selectedCount = selectedCsv ? selectedCsv.split(',').length : 0;\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchNav: true,\n batchPage: page,\n selectedCsv: selectedCsv,\n selectedCount: selectedCount,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{page}:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const page = parseInt(parts[0]) || 0; // Current page\n const selectedCsv = parts[1] || ''; // Currently selected (comma-separated)\n const toggleName = parts.slice(2).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\n batchPage: page,\n selectedCsv: selectedCsv === '' ? '' : selectedCsv,\n toggleName: toggleName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:exec:')) {\n // Execute batch action: batch:exec:{action}:{selected_csv}\n const parts = rawData.substring(11).split(':'); // Remove 'batch:exec:'\n const action = parts[0]; // update/start/stop/restart\n const selectedCsv = parts.slice(1).join(':'); // Comma-separated names\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n action: action,\n selectedCsv: selectedCsv,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:clear') {\n // Clear selection\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchClear: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:cancel') {\n // Cancel batch mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchCancel: true,\n isCancel: false,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('uall:confirm:')) {\n // Update all confirmation: uall:confirm:{timestamp}\n const parts = rawData.substring(13).split(':'); // Remove 'uall:confirm:'\n const timestamp = parseInt(parts[0]);\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAll: true,\n confirmAction: 'updateall',\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n isConfirm: false\n }\n };\n}\n\nif (rawData === 'uall:cancel') {\n // Update all cancel\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAllCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n isConfirm: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\n }\n};" - }, - "id": "code-parse-callback", - "name": "Parse Callback Data", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "is-update-all", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-true", - "leftValue": "={{ $json.isUpdateAll }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "is-update-all-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-cancel-true", - "leftValue": "={{ $json.isUpdateAllCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateallcancel" - }, - { - "id": "is-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-true", - "leftValue": "={{ $json.isCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "is-expired", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "expired-true", - "leftValue": "={{ $json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "expired" - }, - { - "id": "is-batch", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "not-batch-exec", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batch" - }, - { - "id": "is-select", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "select-true", - "leftValue": "={{ $json.isSelect }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "select" - }, - { - "id": "is-list", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "list-true", - "leftValue": "={{ $json.isList }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "is-action", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "action-true", - "leftValue": "={{ $json.isAction }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "is-noop", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "noop-true", - "leftValue": "={{ $json.isNoop }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "noop" - }, - { - "id": "is-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "confirm-true", - "leftValue": "={{ $json.isConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirm" - }, - { - "id": "is-cancel-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-confirm-true", - "leftValue": "={{ $json.isCancelConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancelConfirm" - }, - { - "id": "is-batch-stop-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-confirm-true", - "leftValue": "={{ $json.isBatchStopConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopConfirm" - }, - { - "id": "is-batch-stop-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-cancel-true", - "leftValue": "={{ $json.isBatchStopCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopCancel" - }, - { - "id": "is-bexec-text-cmd", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "is-batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "bexecTextCmd" - }, - { - "id": "is-batch-mode", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-mode-true", - "leftValue": "={{ $json.isBatchMode }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchmode" - }, - { - "id": "is-batch-toggle", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-toggle-true", - "leftValue": "={{ $json.isBatchToggle }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchtoggle" - }, - { - "id": "is-batch-nav", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-nav-true", - "leftValue": "={{ $json.isBatchNav }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchnav" - }, - { - "id": "is-batch-exec", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchexec" - }, - { - "id": "is-batch-clear", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-clear-true", - "leftValue": "={{ $json.isBatchClear }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchclear" - }, - { - "id": "is-batch-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-cancel-true", - "leftValue": "={{ $json.isBatchCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchcancel" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-callback", - "name": "Route Callback", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1120, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};" - }, - "id": "code-handle-cancel", - "name": "Handle Cancel", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-cancel", - "name": "Answer Cancel Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-cancel-msg", - "name": "Delete Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};" - }, - "id": "code-handle-expired", - "name": "Handle Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}", - "options": {} - }, - "id": "http-answer-expired", - "name": "Answer Expired Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-expired-msg", - "name": "Delete Expired Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};" - }, - "id": "code-build-callback-cmd", - "name": "Build Callback Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-callback-action", - "name": "Execute Callback Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n};" - }, - "id": "code-parse-callback-result", - "name": "Parse Callback Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-action", - "name": "Answer Action Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-suggestion-msg", - "name": "Delete Suggestion Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Parse Callback Result').item.json.chatId }}", - "text": "={{ $('Parse Callback Result').item.json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-callback-result", - "name": "Send Callback Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-confirm-msg", - "name": "Delete Batch Confirm Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - 800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-update", - "name": "Parse Update Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 1000 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-update", - "name": "Docker List for Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-update-container", - "name": "Match Update Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "update-docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - }, - { - "id": "update-no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "no-match" - }, - { - "id": "update-single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "single" - }, - { - "id": "update-multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "multiple" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-update-match-count", - "name": "Check Update Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-error", - "name": "Send Update Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};" - }, - "id": "code-update-multiple-handler", - "name": "Handle Update Multiple", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-multiple", - "name": "Send Update Multiple", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.containerQuery }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-no-match", - "name": "Send Update No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-logs", - "name": "Parse Logs Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 600 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Prepare Text Logs Input').item.json.chatId }}", - "text": "={{ $json.message }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-logs", - "name": "Send Logs Response", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.message.chat.id }}", - "text": "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" - } - }, - "id": "telegram-show-menu", - "name": "Show Menu", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1120, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build Container List Keyboard for /status command\nconst dockerOutput = $input.item.json.stdout;\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: \"Cannot connect to Docker\",\n isSingleContainer: false\n }\n }];\n}\n\n// Function to normalize container names (strip prefixes)\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Check if user specified a container name (e.g., \"/status plex\")\nconst text = (message.text || '').toLowerCase().trim();\nlet requestedName = null;\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n requestedName = parts.filter(p => p !== 'status' && p !== '/status').join(' ');\n}\n\n// If specific container requested, route to submenu\nif (requestedName) {\n const matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n });\n \n if (matches.length === 1) {\n const container = matches[0];\n return [{\n json: {\n isSingleContainer: true,\n chatId: chatId,\n containerName: normalizeName(container.Names[0]),\n containerId: container.Id,\n containerState: container.State,\n containerStatus: container.Status,\n containerImage: container.Image\n }\n }];\n } else if (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: 'No container found matching \"' + requestedName + '\"',\n isSingleContainer: false\n }\n }];\n }\n // Multiple matches - show them all in keyboard below\n}\n\n// Build paginated container list keyboard\nconst page = 0; // Initial page\nconst containersPerPage = 6;\n\n// Group by state (running first)\nconst running = containers.filter(c => c.State === 'running');\nconst stopped = containers.filter(c => c.State !== 'running');\nconst sortedContainers = [...running, ...stopped];\n\nconst totalPages = Math.ceil(sortedContainers.length / containersPerPage);\nconst start = page * containersPerPage;\nconst pageContainers = sortedContainers.slice(start, start + containersPerPage);\n\n// Build keyboard rows\nconst keyboard = [];\n\npageContainers.forEach(container => {\n const name = normalizeName(container.Names[0]);\n const stateIcon = container.State === 'running' ? '\\u{1F7E2}' : '\\u26AA'; // Green circle or white circle\n const stateText = container.State === 'running' ? 'Running' : 'Stopped';\n keyboard.push([{\n text: `${stateIcon} ${name} - ${stateText}`,\n callback_data: `select:${name}`\n }]);\n});\n\n// Add navigation row if needed\nif (totalPages > 1) {\n const navRow = [];\n if (page > 0) {\n navRow.push({ text: '\\u25C0\\uFE0F Previous', callback_data: `list:${page - 1}` });\n }\n navRow.push({ text: `${page + 1}/${totalPages}`, callback_data: 'noop' });\n if (page < totalPages - 1) {\n navRow.push({ text: 'Next \\u25B6\\uFE0F', callback_data: `list:${page + 1}` });\n }\n keyboard.push(navRow);\n}\n\n// Add Select Multiple button for batch operations\nkeyboard.push([{ text: \"\u2611\ufe0f Select Multiple\", callback_data: \"batch:mode\" }]);\n\n// Build header text\nconst runningCount = running.length;\nconst totalCount = sortedContainers.length;\nlet headerText = `\\u{1F5C2} Containers (${runningCount}/${totalCount} running)`;\nif (totalPages > 1) {\n headerText += `\\n\\nPage ${page + 1} of ${totalPages}`;\n}\nheaderText += '\\n\\nTap a container to manage it:';\n\nreturn [{\n json: {\n chatId: chatId,\n text: headerText,\n reply_markup: { inline_keyboard: keyboard },\n isSingleContainer: false\n }\n}];" - }, - "id": "code-build-container-list-keyboard", - "name": "Build Container List Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 0 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-list", - "name": "Send Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 0 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-single-container", - "leftValue": "={{ $json.isSingleContainer }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-single-container", - "name": "Check Single Container", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - -100 - ] - }, - { - "parameters": { - "jsCode": "// Build Container Submenu for direct access (/status plex) or callback selection\nconst data = $input.item.json;\nconst chatId = data.chatId;\nconst containerName = data.containerName;\nconst state = data.containerState;\nconst status = data.containerStatus;\nconst image = data.containerImage;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === 'running') {\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: '\\u25B6\\uFE0F Start', callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: '\\u{1F4CB} Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n]);\n\n// Build status text\nconst stateIcon = state === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n chatId: chatId,\n text: text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-container-submenu-direct", - "name": "Build Container Submenu Direct", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - -100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu-direct", - "name": "Send Container Submenu Direct", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - -100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-select-callback", - "name": "Answer Select Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare container fetch for submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n containerName: data.containerName\n }\n};" - }, - "id": "code-prepare-container-fetch", - "name": "Prepare Container Fetch", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-single-container", - "name": "Get Single Container", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Build Container Submenu for callback selection\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Container Fetch\").item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst searchName = prevData.containerName.toLowerCase();\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-container-submenu", - "name": "Build Container Submenu", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 900 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu", - "name": "Send Container Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-noop-callback", - "name": "Answer Noop Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-list-callback", - "name": "Answer List Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare list pagination request\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: data.page || 0\n }\n};" - }, - "id": "code-prepare-list-fetch", - "name": "Prepare List Fetch", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-containers-for-list", - "name": "Get Containers For List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Build Paginated Container List Keyboard for callback pagination\nconst containers = $input.all().map(item => item.json);\n// Try to get data from Prepare List Fetch, fallback to Prepare Batch Cancel Return\nlet prevData;\ntry {\n prevData = $(\"Prepare List Fetch\").item.json;\n} catch (e) {\n try {\n prevData = $(\"Prepare Batch Cancel Return\").item.json;\n } catch (e2) {\n prevData = $json; // Fallback to current item\n }\n}\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst page = prevData.page || 0;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\nconst containersPerPage = 6;\n\n// Group by state (running first)\nconst running = containers.filter(c => c.State === \"running\");\nconst stopped = containers.filter(c => c.State !== \"running\");\nconst sortedContainers = [...running, ...stopped];\n\nconst totalPages = Math.ceil(sortedContainers.length / containersPerPage);\nconst start = page * containersPerPage;\nconst pageContainers = sortedContainers.slice(start, start + containersPerPage);\n\n// Build keyboard rows\nconst keyboard = [];\n\npageContainers.forEach(container => {\n const name = normalizeName(container.Names[0]);\n const stateIcon = container.State === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\n const stateText = container.State === \"running\" ? \"Running\" : \"Stopped\";\n keyboard.push([{\n text: `${stateIcon} ${name} - ${stateText}`,\n callback_data: `select:${name}`\n }]);\n});\n\n// Add navigation row if needed\nif (totalPages > 1) {\n const navRow = [];\n if (page > 0) {\n navRow.push({ text: \"\\u25C0\\uFE0F Previous\", callback_data: `list:${page - 1}` });\n }\n navRow.push({ text: `${page + 1}/${totalPages}`, callback_data: \"noop\" });\n if (page < totalPages - 1) {\n navRow.push({ text: \"Next \\u25B6\\uFE0F\", callback_data: `list:${page + 1}` });\n }\n keyboard.push(navRow);\n}\n\n// Add Select Multiple button for batch operations\nkeyboard.push([{ text: \"\u2611\ufe0f Select Multiple\", callback_data: \"batch:mode\" }]);\n\n// Build header text\nconst runningCount = running.length;\nconst totalCount = sortedContainers.length;\nlet headerText = `\\u{1F5C2} Containers (${runningCount}/${totalCount} running)`;\nif (totalPages > 1) {\n headerText += `\\n\\nPage ${page + 1} of ${totalPages}`;\n}\nheaderText += \"\\n\\nTap a container to manage it:\";\n\nreturn [{\n json: {\n chatId,\n messageId,\n text: headerText,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-paginated-list", - "name": "Build Paginated List", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1000 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-edit-container-list", - "name": "Edit Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-action-callback", - "name": "Answer Action Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "action-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "action-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "action-logs", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-logs", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-action-type", - "name": "Route Action Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Build start/restart action command for inline keyboard\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst action = data.action; // 'start' or 'restart'\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// For restart, use ?t=10 for graceful timeout\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n containerName,\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-prepare-immediate-action", - "name": "Prepare Immediate Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-action", - "name": "Get Container For Action", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Find container and execute action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-immediate-action-cmd", - "name": "Build Immediate Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1100 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-immediate-action", - "name": "Execute Immediate Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2440, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Build action completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F ${containerName} started`,\n restart: `\\u{1F504} ${containerName} restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} ${containerName}`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-immediate-result", - "name": "Format Immediate Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-immediate-result", - "name": "Send Immediate Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-logs-result", - "name": "Send Logs Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build Stop Confirmation dialog with timestamp for 30-second timeout\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Stop ${containerName}?\\n\\nThis will stop the container immediately.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-build-stop-confirmation", - "name": "Build Stop Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-stop-confirmation", - "name": "Send Stop Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build Update Confirmation dialog with timestamp for 30-second timeout\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Update', callback_data: `confirm:update:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Update ${containerName}?\\n\\nThis will pull the latest image and recreate the container.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-build-update-confirmation", - "name": "Build Update Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-update-confirmation", - "name": "Send Update Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-confirm-callback", - "name": "Answer Confirm Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "check-expired", - "leftValue": "={{ $('Parse Callback Data').item.json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-confirm-expired", - "name": "Check Confirm Expired", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - 1600 - ] - }, - { - "parameters": { - "jsCode": "// Confirmation expired - return to submenu\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Build keyboard for expired message\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for ${containerName} has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-confirm-expired", - "name": "Handle Confirm Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-expired-confirm", - "name": "Send Expired Confirm", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "confirm-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop-confirm", - "leftValue": "={{ $('Parse Callback Data').item.json.confirmAction }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "confirm-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update-confirm", - "leftValue": "={{ $('Parse Callback Data').item.json.confirmAction }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-confirm-action", - "name": "Route Confirm Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1780, - 1600 - ] - }, - { - "parameters": { - "jsCode": "// Prepare stop action from confirmation\nconst data = $('Parse Callback Data').item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};" - }, - "id": "code-prepare-confirmed-stop", - "name": "Prepare Confirmed Stop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1550 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-stop", - "name": "Get Container For Stop", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1550 - ] - }, - { - "parameters": { - "jsCode": "// Find container and build stop command\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-confirmed-stop-cmd", - "name": "Build Confirmed Stop Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - 1550 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-confirmed-stop", - "name": "Execute Confirmed Stop", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2660, - 1550 - ] - }, - { - "parameters": { - "jsCode": "// Build stop completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Confirmed Stop Command').item.json;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already stopped\nconst success = statusCode === 204 || statusCode === 304;\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = `\\u23F9\\uFE0F ${containerName} stopped`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to stop ${containerName}`;\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:stop:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-confirmed-stop-result", - "name": "Format Confirmed Stop Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - 1550 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-confirmed-stop-result", - "name": "Send Confirmed Stop Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3100, - 1550 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-cancel-confirm-callback", - "name": "Answer Cancel Confirm Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Cancel confirmation - return to container submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};" - }, - "id": "code-prepare-cancel-return", - "name": "Prepare Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1800 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-cancel", - "name": "Get Container For Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 1800 - ] - }, - { - "parameters": { - "jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Cancel Return\").item.json;\nconst searchName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-cancel-return-submenu", - "name": "Build Cancel Return Submenu", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-cancel-return-submenu", - "name": "Send Cancel Return Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Detect if this is a batch command (multiple container names)\n// Batch commands: {action} {name1} {name2} ... where action is update/start/stop/restart\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action type from the routed keyword\n// This node receives from Keyword Router which already matched the action\nlet action = null;\nlet containerPart = '';\n\nif (text.startsWith('restart ')) {\n action = 'restart';\n containerPart = text.substring(8).trim();\n} else if (text.includes('restart')) {\n action = 'restart';\n containerPart = text.replace('restart', '').trim();\n} else if (text.startsWith('start ')) {\n action = 'start';\n containerPart = text.substring(6).trim();\n} else if (text.includes('start')) {\n action = 'start';\n containerPart = text.replace('start', '').trim();\n} else if (text.startsWith('stop ')) {\n action = 'stop';\n containerPart = text.substring(5).trim();\n} else if (text.includes('stop')) {\n action = 'stop';\n containerPart = text.replace('stop', '').trim();\n} else if (text.startsWith('update ')) {\n action = 'update';\n containerPart = text.substring(7).trim();\n} else if (text.includes('update')) {\n action = 'update';\n containerPart = text.replace('update', '').trim();\n}\n\nif (!action || !containerPart) {\n // No valid action found, pass through as non-batch (will be handled by existing flow)\n return {\n json: {\n isBatch: false,\n action: action,\n containerNames: [],\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n };\n}\n\n// Split container names by whitespace\nconst containerNames = containerPart.split(/\\s+/).filter(name => name.length > 0);\n\n// Batch = 2 or more containers\nconst isBatch = containerNames.length >= 2;\n\nreturn {\n json: {\n isBatch: isBatch,\n action: action,\n containerNames: containerNames,\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n};" - }, - "id": "code-detect-batch", - "name": "Detect Batch Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - -200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-check", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-is-batch", - "name": "Is Batch Command", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1120, - -200 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start-stop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-action-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-single-action", - "name": "Route Single Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1340, - -100 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-batch", - "name": "Get Containers for Batch", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1340, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Match batch container names with exact-match priority\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Parse Docker container list\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n error: true,\n errorMessage: 'Cannot connect to Docker',\n chatId: chatId\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Match results\nconst allMatched = []; // Containers that matched exactly\nconst needsDisambiguation = []; // { input, matches: [...] }\nconst notFound = []; // Container names with no matches\n\nfor (const inputName of containerNames) {\n const normalized = inputName.toLowerCase().trim();\n \n // 1. Check for exact match first (highest priority)\n const exactMatch = containers.find(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName === normalized;\n });\n \n if (exactMatch) {\n // Exact match wins - no disambiguation needed\n allMatched.push({\n Id: exactMatch.Id,\n Name: exactMatch.Names[0].replace(/^\\//, ''),\n State: exactMatch.State,\n Image: exactMatch.Image,\n inputName: inputName,\n matchType: 'exact'\n });\n continue;\n }\n \n // 2. Check for substring matches (fuzzy)\n const fuzzyMatches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n });\n \n if (fuzzyMatches.length === 0) {\n // No matches at all\n notFound.push(inputName);\n } else if (fuzzyMatches.length === 1) {\n // Single fuzzy match - treat as found\n const match = fuzzyMatches[0];\n allMatched.push({\n Id: match.Id,\n Name: match.Names[0].replace(/^\\//, ''),\n State: match.State,\n Image: match.Image,\n inputName: inputName,\n matchType: 'fuzzy'\n });\n } else {\n // Multiple fuzzy matches - needs disambiguation\n needsDisambiguation.push({\n input: inputName,\n matches: fuzzyMatches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n }))\n });\n }\n}\n\nreturn [{\n json: {\n allMatched: allMatched,\n needsDisambiguation: needsDisambiguation,\n notFound: notFound,\n hasDisambiguation: needsDisambiguation.length > 0,\n hasNotFound: notFound.length > 0,\n action: action,\n chatId: chatId,\n messageId: messageId,\n originalContainerNames: containerNames,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-batch-containers", - "name": "Match Batch Containers", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - -300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-disambiguation", - "leftValue": "={{ $json.hasDisambiguation }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-needs-disambiguation", - "name": "Needs Disambiguation", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1780, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build disambiguation message with inline keyboard\nconst data = $json;\nconst disambig = data.needsDisambiguation;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message text\nlet text = 'Multiple matches found:\\n\\n';\n\nfor (const item of disambig) {\n const matchNames = item.matches.map(m => m.Name).join(', ');\n text += `\\u2022 '${item.input}' matches: ${matchNames}\\n`;\n}\n\ntext += '\\nPlease be more specific or select from options below.';\n\n// Build inline keyboard with exact container names\n// Each row has one disambiguation option\nconst keyboard = [];\n\nfor (const item of disambig) {\n // Create a row for each ambiguous input\n const row = [];\n for (const match of item.matches.slice(0, 3)) { // Max 3 options per row\n // Callback format: batch:{action}:{containerName}\n // This will be handled by callback router to add to batch\n row.push({\n text: match.Name,\n callback_data: `bselect:${action}:${match.Name}`\n });\n }\n keyboard.push(row);\n}\n\n// Add cancel button\nkeyboard.push([{ text: 'Cancel', callback_data: '{\"a\":\"x\"}' }]);\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: keyboard\n }\n }\n};" - }, - "id": "code-build-disambiguation", - "name": "Build Disambiguation Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-disambiguation", - "name": "Send Disambiguation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-not-found", - "leftValue": "={{ $json.hasNotFound }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-not-found", - "name": "Has Not Found", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Build not found message\nconst data = $json;\nconst notFound = data.notFound;\nconst allMatched = data.allMatched;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message\nlet text = '';\n\nif (notFound.length > 0) {\n text += 'Container(s) not found:\\n';\n for (const name of notFound) {\n text += `\\u2022 ${name}\\n`;\n }\n text += '\\n';\n}\n\nif (allMatched.length > 0) {\n text += `Found ${allMatched.length} container(s):\\n`;\n for (const c of allMatched) {\n text += `\\u2022 ${c.Name}\\n`;\n }\n text += `\\nProceed with ${action} on found containers?`;\n \n // Build confirmation keyboard\n const names = allMatched.map(c => c.Name).join(',');\n const timestamp = Math.floor(Date.now() / 1000);\n \n return {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${allMatched.length} containers`, callback_data: `bexec:${action}:${names}:${timestamp}` },\n { text: 'Cancel', callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n hasMatched: true,\n matchedContainers: allMatched\n }\n };\n} else {\n // No containers matched at all\n return {\n json: {\n chat_id: chatId,\n text: text + 'No valid containers to ' + action + '.',\n parse_mode: 'HTML',\n hasMatched: false\n }\n };\n}" - }, - "id": "code-build-not-found", - "name": "Build Not Found Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - -100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-not-found", - "name": "Send Not Found Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - -100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "batch-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "batch-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "batch-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "batch-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-action", - "name": "Route Batch Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2220, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build batch stop confirmation message\n// Per context: Batch stop confirms due to fuzzy matching risk\nconst data = $json;\nconst allMatched = data.allMatched;\nconst chatId = data.chatId;\n\nconst count = allMatched.length;\nconst names = allMatched.map(c => c.Name);\nconst namesStr = names.join(',');\nconst timestamp = Math.floor(Date.now() / 1000);\n\n// Build confirmation message\nlet text = `Stop ${count} container${count > 1 ? 's' : ''}?\\n\\n`;\nfor (const name of names) {\n text += `\\u2022 ${name}\\n`;\n}\n\n// Callback format: bstop:confirm:{comma-separated-names}:{timestamp}\nconst confirmCallback = `bstop:confirm:${namesStr}:${timestamp}`;\nconst cancelCallback = 'bstop:cancel';\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: 'Confirm', callback_data: confirmCallback },\n { text: 'Cancel', callback_data: cancelCallback }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-batch-stop-confirm", - "name": "Build Batch Stop Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-confirm", - "name": "Send Batch Stop Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $(\"Parse Callback Data\").item.json.queryId }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-confirm", - "name": "Answer Batch Stop Confirm", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: 'Cancelled' }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-cancel", - "name": "Answer Batch Stop Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-stop-cancel", - "name": "Delete Batch Stop Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 800 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-expired", - "leftValue": "={{ $(\"Parse Callback Data\").item.json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-stop-expired", - "name": "Check Batch Stop Expired", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Handle expired batch stop confirmation\nconst data = $json;\nreturn {\n json: {\n chat_id: data.chatId,\n message_id: data.messageId,\n text: 'Confirmation expired. Please try again.',\n parse_mode: 'HTML'\n }\n};" - }, - "id": "code-batch-stop-expired", - "name": "Build Batch Stop Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-expired", - "name": "Send Batch Stop Expired", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Initialize batch state for execution\n// Input comes from Route Batch Action OR batch callbacks (bexec, bstop confirmed)\nconst data = $json;\n\n// Handle different input sources\nlet containers, action, chatId, messageId, fromKeyboard;\n\nif (data.allMatched) {\n // From Route Batch Action or Prepare Batch Exec\n containers = data.allMatched;\n action = data.action;\n chatId = data.chatId;\n messageId = data.messageId || null;\n // Check if fromKeyboard was set by caller (e.g., Prepare Batch Exec for inline keyboard)\n fromKeyboard = data.fromKeyboard || false;\n} else if (data.containerNames) {\n // From batch callback (bexec or bstop:confirm) - keyboard flow\n // Need to resolve names to container objects\n containers = data.containerNames.map(name => ({ Name: name, Id: null }));\n action = data.batchAction || 'stop';\n chatId = data.chatId;\n messageId = data.messageId;\n fromKeyboard = data.fromKeyboard !== false; // Default true for callbacks\n} else {\n throw new Error('Invalid batch state input');\n}\n\nreturn {\n json: {\n containers: containers,\n action: action,\n totalCount: containers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: messageId,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-init-batch-state", - "name": "Initialize Batch State", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: 'Batch ' + $json.action + '\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' }) }}", - "options": {} - }, - "id": "http-send-batch-start", - "name": "Send Batch Start Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Store progress message ID and prepare first iteration\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n throw new Error('Failed to get Initialize Batch State: ' + e.message);\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Use the original messageId from Initialize Batch State\n// (we edit the batch select message in place)\nconst progressMessageId = batchState.messageId || null;\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-loop", - "name": "Prepare Batch Loop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Build progress message for current container\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\n\nconst containerName = container.Name || container;\nconst current = currentIndex + 1;\n\nlet progressText = `Batch ${action} in progress...\\n\\n`;\nprogressText += `Current: ${containerName}\\n`;\nprogressText += `Progress: ${current}/${totalCount}\\n\\n`;\n\nif (successCount > 0 || failureCount > 0) {\n progressText += `Completed: ${successCount} success`;\n if (failureCount > 0) {\n progressText += `, ${failureCount} failed`;\n }\n}\n\nreturn {\n json: {\n ...data,\n progressText: progressText,\n containerName: containerName\n }\n};" - }, - "id": "code-build-progress", - "name": "Build Progress Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3320, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.progressMessageId, text: $json.progressText, parse_mode: 'HTML' }) }}", - "options": { - "response": { - "response": { - "neverError": true - } - } - } - }, - "id": "http-edit-progress", - "name": "Edit Progress Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3540, - -500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "loop-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "loop-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "loop-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "loop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-loop-action", - "name": "Route Batch Loop Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 3760, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare data for next iteration or completion\nconst data = $json;\n\n// Check if more containers to process\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst containers = data.containers;\nconst nextIndex = currentIndex + 1;\nconst isComplete = nextIndex >= totalCount;\n\nreturn {\n json: {\n ...data,\n currentIndex: nextIndex,\n container: isComplete ? null : containers[nextIndex],\n isComplete: isComplete\n }\n};" - }, - "id": "code-prepare-next-iteration", - "name": "Prepare Next Iteration", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5520, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-complete", - "leftValue": "={{ $json.isComplete }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-complete", - "name": "Is Batch Complete", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 5620, - -400 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch stop data for execution\n// Input from Parse Callback Data (via Check Batch Stop Expired)\nconst data = $(\"Parse Callback Data\").item.json;\n\n// containerNames is an array from callback\nconst containerNames = data.containerNames || [];\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: data.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-stop-exec", - "name": "Prepare Batch Stop Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch exec data for execution\n// Input from Route Batch UI Result (sub-workflow output) or existing batch path\nconst data = $json;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// containerNames can be array or comma-separated string\nlet containerNames = data.containerNames || data.containers || [];\nif (typeof containerNames === 'string') {\n containerNames = containerNames.split(',').filter(n => n);\n}\nif (Array.isArray(containerNames) && containerNames[0] && containerNames[0].name) {\n containerNames = containerNames.map(c => c.name);\n}\n\n// action field might be 'action' or 'batchAction'\nconst action = data.batchAction || data.action || 'start';\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: action,\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-prepare-batch-exec", - "name": "Prepare Batch Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `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};" - }, - "id": "code-build-batch-summary", - "name": "Build Batch Summary", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5740, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', ...$json.reply_markup ? { reply_markup: $json.reply_markup } : {} }) }}", - "options": {} - }, - "id": "http-send-batch-summary", - "name": "Send Batch Summary", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 5960, - -400 - ] - }, - { - "parameters": { - "url": "http://docker-socket-proxy:2375/containers/json?all=false", - "options": {} - }, - "id": "http-get-all-containers-update-all", - "name": "Get All Containers For Update All", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1200, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\nconst chatId = $('Keyword Router').first().json.message.chat.id;\nconst messageId = $('Keyword Router').first().json.message.message_id;\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to containers using :latest tag\n// For now, we'll mark all :latest containers as \"potentially need update\"\n// A full implementation would pull each image, but that's expensive\n// We'll show all :latest containers and let batch execution handle the actual check\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = c.Image || '';\n // Check if using :latest or no tag specified (defaults to :latest)\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n allContainers: allContainers\n }\n};" - }, - "id": "code-check-available-updates", - "name": "Check Available Updates", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1400, - 2200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-containers", - "leftValue": "={{ $json.count }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-updates-available", - "name": "Has Updates Available", - "type": "n8n-nodes-base.if", - "typeVersion": 2.1, - "position": [ - 1600, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\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": { - "jsCode": "// Prepare data to return to container list\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: 0\n }\n};" - }, - "id": "code-prepare-batch-cancel-return", - "name": "Prepare Batch Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2200, - 5000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (text mode)\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId: 0, // No message to edit in text mode\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-update", - "name": "Prepare Text Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1200 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Check Update Match Count').item.json.chatId }}", - "text": "=Updating {{ $('Check Update Match Count').item.json.matches[0].Name }}...", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-text-update-started", - "name": "Send Text Update Started", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-text-update-subworkflow", - "name": "Execute Text Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (inline mode)\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n containerId: '', // Will be resolved by sub-workflow from name\n containerName,\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-callback-update", - "name": "Prepare Callback Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1650 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: '\\u2B06\\uFE0F Updating ' + $json.containerName + '...\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.', parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } }) }}", - "options": {} - }, - "id": "http-callback-update-progress", - "name": "Show Callback Update Progress", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1650 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-callback", - "name": "Get Container For Callback Update", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Find container ID from name for callback update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Callback Update Input').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-find-container-callback", - "name": "Find Container For Callback Update", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-callback-update-subworkflow", - "name": "Execute Callback Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2880, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst containerName = data.matches[0].Name;\nconst action = data.action;\nconst chatId = data.chatId;\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId,\n messageId: 0, // Text mode doesn't have a message to edit\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-action-rr53pd94", - "name": "Prepare Text Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-container-action-qokchnw8", - "name": "Execute Container Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for text command path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst chatId = result.chatId;\n\nreturn {\n json: {\n success: success,\n chatId: chatId,\n text: message\n }\n};" - }, - "id": "code-handle-text-result-c6ha90fh", - "name": "Handle Text Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow from inline keyboard\n// Container lookup already done by Get Container For Action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: action,\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-inline-action-tyjn5pb1", - "name": "Prepare Inline Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1200 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-inline-action-8aoev7xt", - "name": "Execute Inline Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2440, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for inline keyboard path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst action = result.action;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'action:' + action + ':' + containerName }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-inline-result-x19h97t3", - "name": "Handle Inline Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow from confirmed stop\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: 'stop',\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-confirmed-stop-vt9cw9tl", - "name": "Prepare Confirmed Stop Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-confirmed-stop-sub-qmm011fk", - "name": "Execute Confirmed Stop Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for confirmed stop path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'confirm:stop:' + containerName + ':' + timestamp }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-confirmed-stop-f2r86fwr", - "name": "Handle Confirmed Stop Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65", - "name": "Prepare Batch Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -800 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "options": {} - }, - "id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c", - "name": "Execute Batch Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Handle update result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: 'update',\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "f5576e75-8cfa-4ac3-9fea-64fae8d31bec", - "name": "Handle Batch Update Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Actions sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\nconst action = data.action;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "958f19ef-249b-42ca-8a29-ecb91548f1dd", - "name": "Prepare Batch Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -200 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "options": {} - }, - "id": "3baebdc9-3cda-478a-b0cc-0fb33a542f03", - "name": "Execute Batch Action Sub-workflow", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Handle action result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: data.action,\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "dc8db8cb-3ba8-471d-98df-c47dcdb4e6ea", - "name": "Handle Batch Action Result Sub", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (text command)\nconst data = $json;\n\n// Check if there's an error from Parse Logs Command\nif (data.error) {\n return {\n json: {\n error: true,\n chatId: data.chatId,\n text: data.text\n }\n };\n}\n\nreturn {\n json: {\n containerName: data.containerQuery,\n lineCount: data.lines,\n chatId: data.chatId,\n messageId: data.messageId || 0,\n responseMode: \"text\"\n }\n};" - }, - "id": "a895bb2d-1f61-4466-b475-b32ec5f0e83a", - "name": "Prepare Text Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 600 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "926c7683-c0e4-41a4-a983-e3f7ecb6ff41", - "name": "Execute Text Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (inline action)\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n containerName: data.containerName,\n lineCount: 30,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: \"inline\"\n }\n};" - }, - "id": "16b24086-5b5d-4980-82c7-4fb37b4e8f6c", - "name": "Prepare Inline Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1300 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "a88974bd-45c0-401e-b50a-c6171cfe06d4", - "name": "Execute Inline Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\nconst containerName = result.containerName;\n\n// Add timestamp to prevent 'message not modified' error on refresh\nconst timestamp = new Date().toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n});\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\n// Append timestamp to message\nconst messageWithTimestamp = result.message + `\\n\\nUpdated: ${timestamp}`;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: messageWithTimestamp,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" - }, - "id": "b1800598-1ff6-4da3-8506-4e4e8127f902", - "name": "Format Inline Logs Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Batch UI sub-workflow\nconst data = $json;\n\n// Determine action from callback data\nlet action = 'mode'; // default\nconst callbackData = data.callbackData || '';\n\nif (data.isBatchMode) action = 'mode';\nelse if (data.isBatchToggle) action = 'toggle';\nelse if (data.isBatchNav) action = 'nav';\nelse if (data.isBatchExec) action = 'exec';\nelse if (data.isBatchClear) action = 'clear';\nelse if (data.isBatchCancel) action = 'cancel';\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n queryId: data.queryId,\n callbackData: callbackData,\n action: action,\n batchPage: data.batchPage || 0,\n selectedCsv: data.selectedCsv || '',\n toggleName: data.toggleName || ''\n }\n};" - }, - "id": "code-prepare-batch-ui-input", - "name": "Prepare Batch UI Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 3400 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_BATCH_UI_WORKFLOW" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-batch-ui-subworkflow", - "name": "Execute Batch UI", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 3400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-keyboard", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-keyboard", - "leftValue": "={{ $json.action }}", - "rightValue": "keyboard", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "keyboard" - }, - { - "id": "route-confirmation", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-confirmation", - "leftValue": "={{ $json.action }}", - "rightValue": "confirmation", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirmation" - }, - { - "id": "route-execute", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-execute", - "leftValue": "={{ $json.action }}", - "rightValue": "execute", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "execute" - }, - { - "id": "route-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-cancel", - "leftValue": "={{ $json.action }}", - "rightValue": "cancel", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "route-limit-reached", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-limit-reached", - "leftValue": "={{ $json.action }}", - "rightValue": "limit_reached", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "limit_reached" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-ui-result", - "name": "Route Batch UI Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2000, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || '' }}" - }, - "id": "telegram-answer-batch-ui-keyboard", - "name": "Answer Batch UI Keyboard", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-keyboard", - "name": "Edit Batch UI Keyboard", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3200 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Confirm...' }}" - }, - "id": "telegram-answer-batch-ui-confirm", - "name": "Answer Batch UI Confirm", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-confirm", - "name": "Edit Batch UI Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Maximum selection reached' }}", - "options": { - "showAlert": true - } - }, - "id": "telegram-answer-batch-ui-limit", - "name": "Answer Batch UI Limit", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Batch selection cancelled' }}" - }, - "id": "telegram-answer-batch-ui-cancel", - "name": "Answer Batch UI Cancel", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - } - ], - "connections": { - "Telegram Trigger": { - "main": [ - [ - { - "node": "Route Update Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Update Type": { - "main": [ - [ - { - "node": "IF User Authenticated", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "IF Callback Authenticated", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF User Authenticated": { - "main": [ - [ - { - "node": "Keyword Router", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF Callback Authenticated": { - "main": [ - [ - { - "node": "Parse Callback Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Data": { - "main": [ - [ - { - "node": "Route Callback", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Callback": { - "main": [ - [ - { - "node": "Check Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Update All Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Expired", - "type": "main", - "index": 0 - } - ], - [], - [ - { - "node": "Answer Select Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer List Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Action Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Noop Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Confirm Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Cancel Confirm Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Callback Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Select Callback": { - "main": [ - [ - { - "node": "Prepare Container Fetch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Container Fetch": { - "main": [ - [ - { - "node": "Get Single Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Single Container": { - "main": [ - [ - { - "node": "Build Container Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Container Submenu": { - "main": [ - [ - { - "node": "Send Container Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer List Callback": { - "main": [ - [ - { - "node": "Prepare List Fetch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare List Fetch": { - "main": [ - [ - { - "node": "Get Containers For List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Containers For List": { - "main": [ - [ - { - "node": "Build Paginated List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Paginated List": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Cancel": { - "main": [ - [ - { - "node": "Answer Cancel Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Cancel Query": { - "main": [ - [ - { - "node": "Delete Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Expired": { - "main": [ - [ - { - "node": "Answer Expired Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Expired Query": { - "main": [ - [ - { - "node": "Delete Expired Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Callback Action": { - "main": [ - [ - { - "node": "Execute Callback Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Callback Action": { - "main": [ - [ - { - "node": "Parse Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Result": { - "main": [ - [ - { - "node": "Answer Action Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Query": { - "main": [ - [ - { - "node": "Delete Suggestion Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Suggestion Message": { - "main": [ - [ - { - "node": "Send Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List Containers": { - "main": [ - [ - { - "node": "Build Container List Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Container List Keyboard": { - "main": [ - [ - { - "node": "Check Single Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Single Container": { - "main": [ - [ - { - "node": "Build Container Submenu Direct", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Container Submenu Direct": { - "main": [ - [ - { - "node": "Send Container Submenu Direct", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Action": { - "main": [ - [ - { - "node": "Match Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Container": { - "main": [ - [ - { - "node": "Check Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Match Count": { - "main": [ - [ - { - "node": "Send Docker Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Find Closest Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Closest Match": { - "main": [ - [ - { - "node": "Check Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Suggestion": { - "main": [ - [ - { - "node": "Build Suggestion Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send No Match", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Suggestion Keyboard": { - "main": [ - [ - { - "node": "Send Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Action Command": { - "main": [ - [ - { - "node": "Execute Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Action": { - "main": [ - [ - { - "node": "Parse Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Keyboard": { - "main": [ - [ - { - "node": "Send Batch Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Update Command": { - "main": [ - [ - { - "node": "Docker List for Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Update": { - "main": [ - [ - { - "node": "Match Update Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Update Container": { - "main": [ - [ - { - "node": "Check Update Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update Match Count": { - "main": [ - [ - { - "node": "Send Update Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Update No Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Update Multiple": { - "main": [ - [ - { - "node": "Send Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Logs Command": { - "main": [ - [ - { - "node": "Prepare Text Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Keyword Router": { - "main": [ - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Docker List Containers", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get All Containers For Update All", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Logs Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Detect Batch Command": { - "main": [ - [ - { - "node": "Is Batch Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Command": { - "main": [ - [ - { - "node": "Get Containers for Batch", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Single Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Single Action": { - "main": [ - [ - { - "node": "Parse Action Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Update Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Containers for Batch": { - "main": [ - [ - { - "node": "Match Batch Containers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Batch Containers": { - "main": [ - [ - { - "node": "Needs Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Needs Disambiguation": { - "main": [ - [ - { - "node": "Build Disambiguation Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Has Not Found", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Disambiguation Message": { - "main": [ - [ - { - "node": "Send Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Not Found": { - "main": [ - [ - { - "node": "Build Not Found Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Batch Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Not Found Message": { - "main": [ - [ - { - "node": "Send Not Found Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Action": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Confirmation": { - "main": [ - [ - { - "node": "Send Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Confirm": { - "main": [ - [ - { - "node": "Check Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Batch Stop Expired": { - "main": [ - [ - { - "node": "Build Batch Stop Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Stop Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Expired": { - "main": [ - [ - { - "node": "Send Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Cancel": { - "main": [ - [ - { - "node": "Delete Batch Stop Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Command": { - "main": [ - [ - { - "node": "Docker List for Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Callback": { - "main": [ - [ - { - "node": "Route Action Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Action Type": { - "main": [ - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Stop Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Update Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Inline Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Immediate Action": { - "main": [ - [ - { - "node": "Get Container For Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Action": { - "main": [ - [ - { - "node": "Prepare Inline Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Immediate Action Command": { - "main": [ - [ - { - "node": "Execute Immediate Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Immediate Action": { - "main": [ - [ - { - "node": "Format Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Immediate Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Stop Confirmation": { - "main": [ - [ - { - "node": "Send Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update Confirmation": { - "main": [ - [ - { - "node": "Send Update Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Confirm Callback": { - "main": [ - [ - { - "node": "Check Confirm Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Confirm Expired": { - "main": [ - [ - { - "node": "Handle Confirm Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Confirm Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Confirm Expired": { - "main": [ - [ - { - "node": "Send Expired Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Confirm Action": { - "main": [ - [ - { - "node": "Prepare Confirmed Stop", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Callback Update Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Confirmed Stop": { - "main": [ - [ - { - "node": "Get Container For Stop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Stop": { - "main": [ - [ - { - "node": "Prepare Confirmed Stop Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Confirmed Stop Command": { - "main": [ - [ - { - "node": "Execute Confirmed Stop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Confirmed Stop": { - "main": [ - [ - { - "node": "Format Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Confirmed Stop Result": { - "main": [ - [ - { - "node": "Send Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Cancel Confirm Callback": { - "main": [ - [ - { - "node": "Prepare Cancel Return", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Cancel Return": { - "main": [ - [ - { - "node": "Get Container For Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Cancel": { - "main": [ - [ - { - "node": "Build Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Cancel Return Submenu": { - "main": [ - [ - { - "node": "Send Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Initialize Batch State": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send Batch Start Message": { - "main": [ - [ - { - "node": "Prepare Batch Loop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Loop": { - "main": [ - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Progress Message": { - "main": [ - [ - { - "node": "Edit Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Edit Progress Message": { - "main": [ - [ - { - "node": "Route Batch Loop Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Loop Action": { - "main": [ - [ - { - "node": "Prepare Batch Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Next Iteration": { - "main": [ - [ - { - "node": "Is Batch Complete", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Complete": { - "main": [ - [ - { - "node": "Build Batch Summary", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Summary": { - "main": [ - [ - { - "node": "Send Batch Summary", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Stop Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get All Containers For Update All": { - "main": [ - [ - { - "node": "Check Available Updates", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Available Updates": { - "main": [ - [ - { - "node": "Has Updates Available", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Updates Available": { - "main": [ - [ - { - "node": "Build Update All Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send All Up To Date", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update All Confirmation": { - "main": [ - [ - { - "node": "Send Update All Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update All Expired": { - "main": [ - [ - { - "node": "Answer Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get Update All Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Expired": { - "main": [ - [ - { - "node": "Delete Update All Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Cancel": { - "main": [ - [ - { - "node": "Delete Update All Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Update All Data": { - "main": [ - [ - { - "node": "Answer Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Confirm": { - "main": [ - [ - { - "node": "Delete Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Update All Confirm": { - "main": [ - [ - { - "node": "Fetch Containers For Update All Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Fetch Containers For Update All Exec": { - "main": [ - [ - { - "node": "Prepare Update All Batch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Update All Batch": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return": { - "main": [ - [ - { - "node": "Get Containers For List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Update Input": { - "main": [ - [ - { - "node": "Execute Text Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Callback Update Input": { - "main": [ - [ - { - "node": "Show Callback Update Progress", - "type": "main", - "index": 0 - } - ] - ] - }, - "Show Callback Update Progress": { - "main": [ - [ - { - "node": "Get Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Callback Update": { - "main": [ - [ - { - "node": "Find Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Container For Callback Update": { - "main": [ - [ - { - "node": "Execute Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Action Input": { - "main": [ - [ - { - "node": "Execute Container Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Action": { - "main": [ - [ - { - "node": "Handle Text Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Text Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Action Input": { - "main": [ - [ - { - "node": "Execute Inline Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Action": { - "main": [ - [ - { - "node": "Handle Inline Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Inline Action Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Confirmed Stop Input": { - "main": [ - [ - { - "node": "Execute Confirmed Stop Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Confirmed Stop Action": { - "main": [ - [ - { - "node": "Handle Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Confirmed Stop Result": { - "main": [ - [ - { - "node": "Send Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Update Input": { - "main": [ - [ - { - "node": "Execute Batch Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Update": { - "main": [ - [ - { - "node": "Handle Batch Update Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Update Result": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Action Input": { - "main": [ - [ - { - "node": "Execute Batch Action Sub-workflow", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Action Sub-workflow": { - "main": [ - [ - { - "node": "Handle Batch Action Result Sub", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Action Result Sub": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Logs Input": { - "main": [ - [ - { - "node": "Execute Text Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Text Logs": { - "main": [ - [ - { - "node": "Send Logs Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Logs Input": { - "main": [ - [ - { - "node": "Execute Inline Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Logs": { - "main": [ - [ - { - "node": "Format Inline Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Inline Logs Result": { - "main": [ - [ - { - "node": "Send Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch UI Input": { - "main": [ - [ - { - "node": "Execute Batch UI", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch UI": { - "main": [ - [ - { - "node": "Route Batch UI Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch UI Result": { - "main": [ - [ - { - "node": "Answer Batch UI Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Exec", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Limit", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Keyboard": { - "main": [ - [ - { - "node": "Edit Batch UI Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Confirm": { - "main": [ - [ - { - "node": "Edit Batch UI Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Cancel": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "pinData": {}, - "settings": { - "executionOrder": "v1" - }, - "staticData": null, - "tags": [], - "triggerCount": 1, - "active": false -} \ No newline at end of file diff --git a/n8n-workflow.json.backup-confirm b/n8n-workflow.json.backup-confirm deleted file mode 100644 index 075085c..0000000 --- a/n8n-workflow.json.backup-confirm +++ /dev/null @@ -1,6517 +0,0 @@ -{ - "name": "Docker Manager Bot", - "nodes": [ - { - "parameters": { - "updates": [ - "message", - "callback_query" - ] - }, - "id": "telegram-trigger", - "name": "Telegram Trigger", - "type": "n8n-nodes-base.telegramTrigger", - "typeVersion": 1.1, - "position": [ - 240, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - }, - "webhookId": "86b8d6bb-7c15-4aae-a749-fcaf2dfcb9e0" - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-message", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-message", - "leftValue": "={{ $json.message?.text }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "message" - }, - { - "id": "route-callback", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-callback", - "leftValue": "={{ $json.callback_query?.id }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "callback_query" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-update-type", - "name": "Route Update Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 460, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "user-auth-condition", - "leftValue": "={{ $json.message.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-auth", - "name": "IF User Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "callback-auth-condition", - "leftValue": "={{ $json.callback_query.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-callback-auth", - "name": "IF Callback Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "keyword-menu-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-start-cmd", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/start", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "menu" - }, - { - "id": "keyword-status", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-status", - "leftValue": "={{ $json.message.text }}", - "rightValue": "status", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status" - }, - { - "id": "keyword-restart", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-restart", - "leftValue": "={{ $json.message.text }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "keyword-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-start", - "leftValue": "={{ $json.message.text }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "keyword-stop", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-stop", - "leftValue": "={{ $json.message.text }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "keyword-update-all", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "matches-update-all", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update.?all|updateall", - "operator": { - "type": "string", - "operation": "regex" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "keyword-update", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-update", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "keyword-logs", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-logs", - "leftValue": "={{ $json.message.text }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-keyword-router", - "name": "Keyword Router", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 900, - 200 - ] - }, - { - "parameters": { - "jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-action-command", - "name": "Parse Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 400 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-action", - "name": "Docker List for Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-container", - "name": "Match Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-match-count", - "name": "Check Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};" - }, - "id": "code-build-action-cmd", - "name": "Build Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 400 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-action", - "name": "Execute Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2000, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};" - }, - "id": "code-parse-action-result", - "name": "Parse Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 400 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-action-result", - "name": "Send Action Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};" - }, - "id": "code-find-closest", - "name": "Find Closest Match", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-suggestion", - "leftValue": "={{ $json.hasSuggestion }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-suggestion", - "name": "Check Suggestion", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - 300 - ] - }, - { - "parameters": { - "jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// Use modern action callback format: action:{action}:{containerName}\nconst callbackData = `action:${action}:${suggestedName}`;\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '${query}' found.\\n\\nDid you mean ${suggestedName}?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-suggestion", - "name": "Build Suggestion Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-suggestion", - "name": "Send Suggestion", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.query }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-no-match", - "name": "Send No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp for 30s timeout\n\n// Build callback_data using new bexec format\n// Format: bexec:{action}:{comma-separated-names}:{timestamp}\n// Limit to 4 containers to stay within 64-byte callback_data limit\nlet limitedNames = names;\nlet limitedCount = names.length;\nif (names.length > 4) {\n limitedNames = names.slice(0, 4);\n limitedCount = 4;\n}\n\nconst namesStr = limitedNames.join(',');\nconst callbackData = `bexec:${action}:${namesStr}:${timestamp}`;\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found ${matches.length} containers matching '${query}':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};" - }, - "id": "code-build-batch-keyboard", - "name": "Build Batch Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chat_id }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML", - "reply_markup": "={{ JSON.stringify($json.reply_markup) }}" - } - }, - "id": "telegram-send-batch-confirm", - "name": "Send Batch Confirmation", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-docker-error", - "name": "Send Docker Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp} or bstop:confirm:{names}:{timestamp}:kb\n const parts = rest.split(':'); // confirm, names, timestamp[, kb]\n if (parts[0] === 'confirm' && parts.length >= 3) {\n // Check for :kb marker (indicates inline keyboard origin)\n const fromKeyboard = parts[parts.length - 1] === 'kb';\n const timestampIdx = fromKeyboard ? parts.length - 2 : parts.length - 1;\n const timestamp = parseInt(parts[timestampIdx]);\n const namesStr = parts.slice(1, timestampIdx).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n fromKeyboard: fromKeyboard,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n\n\nif (rawData === 'batch:mode' || rawData.startsWith('batch:mode:')) {\n // Enter batch selection mode with optional page: batch:mode or batch:mode:{page}\n let page = 0;\n if (rawData.startsWith('batch:mode:')) {\n page = parseInt(rawData.substring(11)) || 0;\n }\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: true,\n batchPage: page,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:nav:')) {\n // Navigate batch selection with preserved selection: batch:nav:{page}:{selected_csv}\n const parts = rawData.substring(10).split(':'); // Remove 'batch:nav:'\n const page = parseInt(parts[0]) || 0;\n const selectedCsv = parts.slice(1).join(':') || '';\n const selectedCount = selectedCsv ? selectedCsv.split(',').length : 0;\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchNav: true,\n batchPage: page,\n selectedCsv: selectedCsv,\n selectedCount: selectedCount,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{page}:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const page = parseInt(parts[0]) || 0; // Current page\n const selectedCsv = parts[1] || ''; // Currently selected (comma-separated)\n const toggleName = parts.slice(2).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\n batchPage: page,\n selectedCsv: selectedCsv === '' ? '' : selectedCsv,\n toggleName: toggleName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:exec:')) {\n // Execute batch action: batch:exec:{action}:{selected_csv}\n const parts = rawData.substring(11).split(':'); // Remove 'batch:exec:'\n const action = parts[0]; // update/start/stop/restart\n const selectedCsv = parts.slice(1).join(':'); // Comma-separated names\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n action: action,\n selectedCsv: selectedCsv,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:clear') {\n // Clear selection\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchClear: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:cancel') {\n // Cancel batch mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchCancel: true,\n isCancel: false,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('uall:confirm:')) {\n // Update all confirmation: uall:confirm:{timestamp}\n const parts = rawData.substring(13).split(':'); // Remove 'uall:confirm:'\n const timestamp = parseInt(parts[0]);\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAll: true,\n confirmAction: 'updateall',\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n isConfirm: false\n }\n };\n}\n\nif (rawData === 'uall:cancel') {\n // Update all cancel\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAllCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n isConfirm: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\n }\n};" - }, - "id": "code-parse-callback", - "name": "Parse Callback Data", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "is-update-all", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-true", - "leftValue": "={{ $json.isUpdateAll }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "is-update-all-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-cancel-true", - "leftValue": "={{ $json.isUpdateAllCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateallcancel" - }, - { - "id": "is-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-true", - "leftValue": "={{ $json.isCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "is-expired", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "expired-true", - "leftValue": "={{ $json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "expired" - }, - { - "id": "is-batch", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "not-batch-exec", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batch" - }, - { - "id": "is-select", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "select-true", - "leftValue": "={{ $json.isSelect }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "select" - }, - { - "id": "is-list", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "list-true", - "leftValue": "={{ $json.isList }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "is-action", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "action-true", - "leftValue": "={{ $json.isAction }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "is-noop", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "noop-true", - "leftValue": "={{ $json.isNoop }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "noop" - }, - { - "id": "is-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "confirm-true", - "leftValue": "={{ $json.isConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirm" - }, - { - "id": "is-cancel-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-confirm-true", - "leftValue": "={{ $json.isCancelConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancelConfirm" - }, - { - "id": "is-batch-stop-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-confirm-true", - "leftValue": "={{ $json.isBatchStopConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopConfirm" - }, - { - "id": "is-batch-stop-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-cancel-true", - "leftValue": "={{ $json.isBatchStopCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopCancel" - }, - { - "id": "is-bexec-text-cmd", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "is-batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "bexecTextCmd" - }, - { - "id": "is-batch-mode", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-mode-true", - "leftValue": "={{ $json.isBatchMode }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchmode" - }, - { - "id": "is-batch-toggle", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-toggle-true", - "leftValue": "={{ $json.isBatchToggle }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchtoggle" - }, - { - "id": "is-batch-nav", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-nav-true", - "leftValue": "={{ $json.isBatchNav }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchnav" - }, - { - "id": "is-batch-exec", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchexec" - }, - { - "id": "is-batch-clear", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-clear-true", - "leftValue": "={{ $json.isBatchClear }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchclear" - }, - { - "id": "is-batch-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-cancel-true", - "leftValue": "={{ $json.isBatchCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchcancel" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-callback", - "name": "Route Callback", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1120, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};" - }, - "id": "code-handle-cancel", - "name": "Handle Cancel", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-cancel", - "name": "Answer Cancel Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-cancel-msg", - "name": "Delete Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};" - }, - "id": "code-handle-expired", - "name": "Handle Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}", - "options": {} - }, - "id": "http-answer-expired", - "name": "Answer Expired Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-expired-msg", - "name": "Delete Expired Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};" - }, - "id": "code-build-callback-cmd", - "name": "Build Callback Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-callback-action", - "name": "Execute Callback Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n};" - }, - "id": "code-parse-callback-result", - "name": "Parse Callback Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-action", - "name": "Answer Action Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-suggestion-msg", - "name": "Delete Suggestion Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Parse Callback Result').item.json.chatId }}", - "text": "={{ $('Parse Callback Result').item.json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-callback-result", - "name": "Send Callback Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-confirm-msg", - "name": "Delete Batch Confirm Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - 800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-update", - "name": "Parse Update Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 1000 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-update", - "name": "Docker List for Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-update-container", - "name": "Match Update Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "update-docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - }, - { - "id": "update-no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "no-match" - }, - { - "id": "update-single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "single" - }, - { - "id": "update-multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "multiple" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-update-match-count", - "name": "Check Update Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-error", - "name": "Send Update Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};" - }, - "id": "code-update-multiple-handler", - "name": "Handle Update Multiple", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-multiple", - "name": "Send Update Multiple", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.containerQuery }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-no-match", - "name": "Send Update No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-logs", - "name": "Parse Logs Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 600 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Prepare Text Logs Input').item.json.chatId }}", - "text": "={{ $json.message }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-logs", - "name": "Send Logs Response", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.message.chat.id }}", - "text": "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" - } - }, - "id": "telegram-show-menu", - "name": "Show Menu", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1120, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-list", - "name": "Send Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 0 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu-direct", - "name": "Send Container Submenu Direct", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - -100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-select-callback", - "name": "Answer Select Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu", - "name": "Send Container Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-noop-callback", - "name": "Answer Noop Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-list-callback", - "name": "Answer List Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-edit-container-list", - "name": "Edit Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-action-callback", - "name": "Answer Action Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "action-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "action-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "action-logs", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-logs", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-action-type", - "name": "Route Action Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Build start/restart action command for inline keyboard\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst action = data.action; // 'start' or 'restart'\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// For restart, use ?t=10 for graceful timeout\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n containerName,\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-prepare-immediate-action", - "name": "Prepare Immediate Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-action", - "name": "Get Container For Action", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Find container and execute action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-immediate-action-cmd", - "name": "Build Immediate Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1100 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-immediate-action", - "name": "Execute Immediate Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2440, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Build action completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F ${containerName} started`,\n restart: `\\u{1F504} ${containerName} restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} ${containerName}`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-immediate-result", - "name": "Format Immediate Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-immediate-result", - "name": "Send Immediate Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-logs-result", - "name": "Send Logs Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Cancel confirmation - return to container submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};" - }, - "id": "code-prepare-cancel-return", - "name": "Prepare Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1800 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-cancel", - "name": "Get Container For Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 1800 - ] - }, - { - "parameters": { - "jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Cancel Return\").item.json;\nconst searchName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-cancel-return-submenu", - "name": "Build Cancel Return Submenu", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-cancel-return-submenu", - "name": "Send Cancel Return Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Detect if this is a batch command (multiple container names)\n// Batch commands: {action} {name1} {name2} ... where action is update/start/stop/restart\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action type from the routed keyword\n// This node receives from Keyword Router which already matched the action\nlet action = null;\nlet containerPart = '';\n\nif (text.startsWith('restart ')) {\n action = 'restart';\n containerPart = text.substring(8).trim();\n} else if (text.includes('restart')) {\n action = 'restart';\n containerPart = text.replace('restart', '').trim();\n} else if (text.startsWith('start ')) {\n action = 'start';\n containerPart = text.substring(6).trim();\n} else if (text.includes('start')) {\n action = 'start';\n containerPart = text.replace('start', '').trim();\n} else if (text.startsWith('stop ')) {\n action = 'stop';\n containerPart = text.substring(5).trim();\n} else if (text.includes('stop')) {\n action = 'stop';\n containerPart = text.replace('stop', '').trim();\n} else if (text.startsWith('update ')) {\n action = 'update';\n containerPart = text.substring(7).trim();\n} else if (text.includes('update')) {\n action = 'update';\n containerPart = text.replace('update', '').trim();\n}\n\nif (!action || !containerPart) {\n // No valid action found, pass through as non-batch (will be handled by existing flow)\n return {\n json: {\n isBatch: false,\n action: action,\n containerNames: [],\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n };\n}\n\n// Split container names by whitespace\nconst containerNames = containerPart.split(/\\s+/).filter(name => name.length > 0);\n\n// Batch = 2 or more containers\nconst isBatch = containerNames.length >= 2;\n\nreturn {\n json: {\n isBatch: isBatch,\n action: action,\n containerNames: containerNames,\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n};" - }, - "id": "code-detect-batch", - "name": "Detect Batch Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - -200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-check", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-is-batch", - "name": "Is Batch Command", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1120, - -200 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start-stop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-action-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-single-action", - "name": "Route Single Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1340, - -100 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-batch", - "name": "Get Containers for Batch", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1340, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Match batch container names with exact-match priority\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Parse Docker container list\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n error: true,\n errorMessage: 'Cannot connect to Docker',\n chatId: chatId\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Match results\nconst allMatched = []; // Containers that matched exactly\nconst needsDisambiguation = []; // { input, matches: [...] }\nconst notFound = []; // Container names with no matches\n\nfor (const inputName of containerNames) {\n const normalized = inputName.toLowerCase().trim();\n \n // 1. Check for exact match first (highest priority)\n const exactMatch = containers.find(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName === normalized;\n });\n \n if (exactMatch) {\n // Exact match wins - no disambiguation needed\n allMatched.push({\n Id: exactMatch.Id,\n Name: exactMatch.Names[0].replace(/^\\//, ''),\n State: exactMatch.State,\n Image: exactMatch.Image,\n inputName: inputName,\n matchType: 'exact'\n });\n continue;\n }\n \n // 2. Check for substring matches (fuzzy)\n const fuzzyMatches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n });\n \n if (fuzzyMatches.length === 0) {\n // No matches at all\n notFound.push(inputName);\n } else if (fuzzyMatches.length === 1) {\n // Single fuzzy match - treat as found\n const match = fuzzyMatches[0];\n allMatched.push({\n Id: match.Id,\n Name: match.Names[0].replace(/^\\//, ''),\n State: match.State,\n Image: match.Image,\n inputName: inputName,\n matchType: 'fuzzy'\n });\n } else {\n // Multiple fuzzy matches - needs disambiguation\n needsDisambiguation.push({\n input: inputName,\n matches: fuzzyMatches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n }))\n });\n }\n}\n\nreturn [{\n json: {\n allMatched: allMatched,\n needsDisambiguation: needsDisambiguation,\n notFound: notFound,\n hasDisambiguation: needsDisambiguation.length > 0,\n hasNotFound: notFound.length > 0,\n action: action,\n chatId: chatId,\n messageId: messageId,\n originalContainerNames: containerNames,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-batch-containers", - "name": "Match Batch Containers", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - -300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-disambiguation", - "leftValue": "={{ $json.hasDisambiguation }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-needs-disambiguation", - "name": "Needs Disambiguation", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1780, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build disambiguation message with inline keyboard\nconst data = $json;\nconst disambig = data.needsDisambiguation;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message text\nlet text = 'Multiple matches found:\\n\\n';\n\nfor (const item of disambig) {\n const matchNames = item.matches.map(m => m.Name).join(', ');\n text += `\\u2022 '${item.input}' matches: ${matchNames}\\n`;\n}\n\ntext += '\\nPlease be more specific or select from options below.';\n\n// Build inline keyboard with exact container names\n// Each row has one disambiguation option\nconst keyboard = [];\n\nfor (const item of disambig) {\n // Create a row for each ambiguous input\n const row = [];\n for (const match of item.matches.slice(0, 3)) { // Max 3 options per row\n // Callback format: batch:{action}:{containerName}\n // This will be handled by callback router to add to batch\n row.push({\n text: match.Name,\n callback_data: `bselect:${action}:${match.Name}`\n });\n }\n keyboard.push(row);\n}\n\n// Add cancel button\nkeyboard.push([{ text: 'Cancel', callback_data: '{\"a\":\"x\"}' }]);\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: keyboard\n }\n }\n};" - }, - "id": "code-build-disambiguation", - "name": "Build Disambiguation Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-disambiguation", - "name": "Send Disambiguation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-not-found", - "leftValue": "={{ $json.hasNotFound }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-not-found", - "name": "Has Not Found", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Build not found message\nconst data = $json;\nconst notFound = data.notFound;\nconst allMatched = data.allMatched;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message\nlet text = '';\n\nif (notFound.length > 0) {\n text += 'Container(s) not found:\\n';\n for (const name of notFound) {\n text += `\\u2022 ${name}\\n`;\n }\n text += '\\n';\n}\n\nif (allMatched.length > 0) {\n text += `Found ${allMatched.length} container(s):\\n`;\n for (const c of allMatched) {\n text += `\\u2022 ${c.Name}\\n`;\n }\n text += `\\nProceed with ${action} on found containers?`;\n \n // Build confirmation keyboard\n const names = allMatched.map(c => c.Name).join(',');\n const timestamp = Math.floor(Date.now() / 1000);\n \n return {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${allMatched.length} containers`, callback_data: `bexec:${action}:${names}:${timestamp}` },\n { text: 'Cancel', callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n hasMatched: true,\n matchedContainers: allMatched\n }\n };\n} else {\n // No containers matched at all\n return {\n json: {\n chat_id: chatId,\n text: text + 'No valid containers to ' + action + '.',\n parse_mode: 'HTML',\n hasMatched: false\n }\n };\n}" - }, - "id": "code-build-not-found", - "name": "Build Not Found Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - -100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-not-found", - "name": "Send Not Found Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - -100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "batch-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "batch-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "batch-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "batch-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-action", - "name": "Route Batch Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2220, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build batch stop confirmation message\n// Per context: Batch stop confirms due to fuzzy matching risk\nconst data = $json;\nconst allMatched = data.allMatched;\nconst chatId = data.chatId;\n\nconst count = allMatched.length;\nconst names = allMatched.map(c => c.Name);\nconst namesStr = names.join(',');\nconst timestamp = Math.floor(Date.now() / 1000);\n\n// Build confirmation message\nlet text = `Stop ${count} container${count > 1 ? 's' : ''}?\\n\\n`;\nfor (const name of names) {\n text += `\\u2022 ${name}\\n`;\n}\n\n// Callback format: bstop:confirm:{comma-separated-names}:{timestamp}\nconst confirmCallback = `bstop:confirm:${namesStr}:${timestamp}`;\nconst cancelCallback = 'bstop:cancel';\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: 'Confirm', callback_data: confirmCallback },\n { text: 'Cancel', callback_data: cancelCallback }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-batch-stop-confirm", - "name": "Build Batch Stop Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-confirm", - "name": "Send Batch Stop Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $(\"Parse Callback Data\").item.json.queryId }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-confirm", - "name": "Answer Batch Stop Confirm", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: 'Cancelled' }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-cancel", - "name": "Answer Batch Stop Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-stop-cancel", - "name": "Delete Batch Stop Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 800 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-expired", - "leftValue": "={{ $(\"Parse Callback Data\").item.json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-stop-expired", - "name": "Check Batch Stop Expired", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Handle expired batch stop confirmation\nconst data = $json;\nreturn {\n json: {\n chat_id: data.chatId,\n message_id: data.messageId,\n text: 'Confirmation expired. Please try again.',\n parse_mode: 'HTML'\n }\n};" - }, - "id": "code-batch-stop-expired", - "name": "Build Batch Stop Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-expired", - "name": "Send Batch Stop Expired", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Initialize batch state for execution\n// Input comes from Route Batch Action OR batch callbacks (bexec, bstop confirmed)\nconst data = $json;\n\n// Handle different input sources\nlet containers, action, chatId, messageId, fromKeyboard;\n\nif (data.allMatched) {\n // From Route Batch Action or Prepare Batch Exec\n containers = data.allMatched;\n action = data.action;\n chatId = data.chatId;\n messageId = data.messageId || null;\n // Check if fromKeyboard was set by caller (e.g., Prepare Batch Exec for inline keyboard)\n fromKeyboard = data.fromKeyboard || false;\n} else if (data.containerNames) {\n // From batch callback (bexec or bstop:confirm) - keyboard flow\n // Need to resolve names to container objects\n containers = data.containerNames.map(name => ({ Name: name, Id: null }));\n action = data.batchAction || 'stop';\n chatId = data.chatId;\n messageId = data.messageId;\n fromKeyboard = data.fromKeyboard !== false; // Default true for callbacks\n} else {\n throw new Error('Invalid batch state input');\n}\n\nreturn {\n json: {\n containers: containers,\n action: action,\n totalCount: containers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: messageId,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-init-batch-state", - "name": "Initialize Batch State", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: 'Batch ' + $json.action + '\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' }) }}", - "options": {} - }, - "id": "http-send-batch-start", - "name": "Send Batch Start Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Store progress message ID and prepare first iteration\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n throw new Error('Failed to get Initialize Batch State: ' + e.message);\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Use the original messageId from Initialize Batch State\n// (we edit the batch select message in place)\nconst progressMessageId = batchState.messageId || null;\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-loop", - "name": "Prepare Batch Loop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Build progress message for current container\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\n\nconst containerName = container.Name || container;\nconst current = currentIndex + 1;\n\nlet progressText = `Batch ${action} in progress...\\n\\n`;\nprogressText += `Current: ${containerName}\\n`;\nprogressText += `Progress: ${current}/${totalCount}\\n\\n`;\n\nif (successCount > 0 || failureCount > 0) {\n progressText += `Completed: ${successCount} success`;\n if (failureCount > 0) {\n progressText += `, ${failureCount} failed`;\n }\n}\n\nreturn {\n json: {\n ...data,\n progressText: progressText,\n containerName: containerName\n }\n};" - }, - "id": "code-build-progress", - "name": "Build Progress Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3320, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.progressMessageId, text: $json.progressText, parse_mode: 'HTML' }) }}", - "options": { - "response": { - "response": { - "neverError": true - } - } - } - }, - "id": "http-edit-progress", - "name": "Edit Progress Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3540, - -500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "loop-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "loop-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "loop-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "loop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-loop-action", - "name": "Route Batch Loop Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 3760, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare data for next iteration or completion\nconst data = $json;\n\n// Check if more containers to process\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst containers = data.containers;\nconst nextIndex = currentIndex + 1;\nconst isComplete = nextIndex >= totalCount;\n\nreturn {\n json: {\n ...data,\n currentIndex: nextIndex,\n container: isComplete ? null : containers[nextIndex],\n isComplete: isComplete\n }\n};" - }, - "id": "code-prepare-next-iteration", - "name": "Prepare Next Iteration", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5520, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-complete", - "leftValue": "={{ $json.isComplete }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-complete", - "name": "Is Batch Complete", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 5620, - -400 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch stop data for execution\n// Input from Parse Callback Data (via Check Batch Stop Expired)\nconst data = $(\"Parse Callback Data\").item.json;\n\n// containerNames is an array from callback\nconst containerNames = data.containerNames || [];\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: data.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-stop-exec", - "name": "Prepare Batch Stop Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch exec data for execution\n// Input from Route Batch UI Result (sub-workflow output) or existing batch path\nconst data = $json;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// containerNames can be array or comma-separated string\nlet containerNames = data.containerNames || data.containers || [];\nif (typeof containerNames === 'string') {\n containerNames = containerNames.split(',').filter(n => n);\n}\nif (Array.isArray(containerNames) && containerNames[0] && containerNames[0].name) {\n containerNames = containerNames.map(c => c.name);\n}\n\n// action field might be 'action' or 'batchAction'\nconst action = data.batchAction || data.action || 'start';\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: action,\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-prepare-batch-exec", - "name": "Prepare Batch Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `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};" - }, - "id": "code-build-batch-summary", - "name": "Build Batch Summary", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5740, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', ...$json.reply_markup ? { reply_markup: $json.reply_markup } : {} }) }}", - "options": {} - }, - "id": "http-send-batch-summary", - "name": "Send Batch Summary", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 5960, - -400 - ] - }, - { - "parameters": { - "url": "http://docker-socket-proxy:2375/containers/json?all=false", - "options": {} - }, - "id": "http-get-all-containers-update-all", - "name": "Get All Containers For Update All", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1200, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\nconst chatId = $('Keyword Router').first().json.message.chat.id;\nconst messageId = $('Keyword Router').first().json.message.message_id;\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to containers using :latest tag\n// For now, we'll mark all :latest containers as \"potentially need update\"\n// A full implementation would pull each image, but that's expensive\n// We'll show all :latest containers and let batch execution handle the actual check\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = c.Image || '';\n // Check if using :latest or no tag specified (defaults to :latest)\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n allContainers: allContainers\n }\n};" - }, - "id": "code-check-available-updates", - "name": "Check Available Updates", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1400, - 2200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-containers", - "leftValue": "={{ $json.count }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-updates-available", - "name": "Has Updates Available", - "type": "n8n-nodes-base.if", - "typeVersion": 2.1, - "position": [ - 1600, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\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": { - "jsCode": "// Prepare data to return to container list\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: 0\n }\n};" - }, - "id": "code-prepare-batch-cancel-return", - "name": "Prepare Batch Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2200, - 5000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (text mode)\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId: 0, // No message to edit in text mode\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-update", - "name": "Prepare Text Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1200 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Check Update Match Count').item.json.chatId }}", - "text": "=Updating {{ $('Check Update Match Count').item.json.matches[0].Name }}...", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-text-update-started", - "name": "Send Text Update Started", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-text-update-subworkflow", - "name": "Execute Text Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (inline mode)\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n containerId: '', // Will be resolved by sub-workflow from name\n containerName,\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-callback-update", - "name": "Prepare Callback Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1650 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: '\\u2B06\\uFE0F Updating ' + $json.containerName + '...\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.', parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } }) }}", - "options": {} - }, - "id": "http-callback-update-progress", - "name": "Show Callback Update Progress", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1650 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-callback", - "name": "Get Container For Callback Update", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Find container ID from name for callback update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Callback Update Input').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-find-container-callback", - "name": "Find Container For Callback Update", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-callback-update-subworkflow", - "name": "Execute Callback Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2880, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst containerName = data.matches[0].Name;\nconst action = data.action;\nconst chatId = data.chatId;\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId,\n messageId: 0, // Text mode doesn't have a message to edit\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-action-rr53pd94", - "name": "Prepare Text Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-container-action-qokchnw8", - "name": "Execute Container Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for text command path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst chatId = result.chatId;\n\nreturn {\n json: {\n success: success,\n chatId: chatId,\n text: message\n }\n};" - }, - "id": "code-handle-text-result-c6ha90fh", - "name": "Handle Text Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow from inline keyboard\n// Container lookup already done by Get Container For Action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: action,\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-inline-action-tyjn5pb1", - "name": "Prepare Inline Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1200 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-inline-action-8aoev7xt", - "name": "Execute Inline Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2440, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for inline keyboard path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst action = result.action;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'action:' + action + ':' + containerName }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-inline-result-x19h97t3", - "name": "Handle Inline Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65", - "name": "Prepare Batch Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -800 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "options": {} - }, - "id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c", - "name": "Execute Batch Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Handle update result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: 'update',\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "f5576e75-8cfa-4ac3-9fea-64fae8d31bec", - "name": "Handle Batch Update Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Actions sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\nconst action = data.action;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "958f19ef-249b-42ca-8a29-ecb91548f1dd", - "name": "Prepare Batch Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -200 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "options": {} - }, - "id": "3baebdc9-3cda-478a-b0cc-0fb33a542f03", - "name": "Execute Batch Action Sub-workflow", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Handle action result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: data.action,\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "dc8db8cb-3ba8-471d-98df-c47dcdb4e6ea", - "name": "Handle Batch Action Result Sub", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (text command)\nconst data = $json;\n\n// Check if there's an error from Parse Logs Command\nif (data.error) {\n return {\n json: {\n error: true,\n chatId: data.chatId,\n text: data.text\n }\n };\n}\n\nreturn {\n json: {\n containerName: data.containerQuery,\n lineCount: data.lines,\n chatId: data.chatId,\n messageId: data.messageId || 0,\n responseMode: \"text\"\n }\n};" - }, - "id": "a895bb2d-1f61-4466-b475-b32ec5f0e83a", - "name": "Prepare Text Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 600 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "926c7683-c0e4-41a4-a983-e3f7ecb6ff41", - "name": "Execute Text Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (inline action)\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n containerName: data.containerName,\n lineCount: 30,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: \"inline\"\n }\n};" - }, - "id": "16b24086-5b5d-4980-82c7-4fb37b4e8f6c", - "name": "Prepare Inline Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1300 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "a88974bd-45c0-401e-b50a-c6171cfe06d4", - "name": "Execute Inline Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\nconst containerName = result.containerName;\n\n// Add timestamp to prevent 'message not modified' error on refresh\nconst timestamp = new Date().toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n});\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\n// Append timestamp to message\nconst messageWithTimestamp = result.message + `\\n\\nUpdated: ${timestamp}`;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: messageWithTimestamp,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" - }, - "id": "b1800598-1ff6-4da3-8506-4e4e8127f902", - "name": "Format Inline Logs Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Batch UI sub-workflow\nconst data = $json;\n\n// Determine action from callback data\nlet action = 'mode'; // default\nconst callbackData = data.callbackData || '';\n\nif (data.isBatchMode) action = 'mode';\nelse if (data.isBatchToggle) action = 'toggle';\nelse if (data.isBatchNav) action = 'nav';\nelse if (data.isBatchExec) action = 'exec';\nelse if (data.isBatchClear) action = 'clear';\nelse if (data.isBatchCancel) action = 'cancel';\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n queryId: data.queryId,\n callbackData: callbackData,\n action: action,\n batchPage: data.batchPage || 0,\n selectedCsv: data.selectedCsv || '',\n toggleName: data.toggleName || ''\n }\n};" - }, - "id": "code-prepare-batch-ui-input", - "name": "Prepare Batch UI Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 3400 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_BATCH_UI_WORKFLOW" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-batch-ui-subworkflow", - "name": "Execute Batch UI", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 3400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-keyboard", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-keyboard", - "leftValue": "={{ $json.action }}", - "rightValue": "keyboard", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "keyboard" - }, - { - "id": "route-confirmation", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-confirmation", - "leftValue": "={{ $json.action }}", - "rightValue": "confirmation", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirmation" - }, - { - "id": "route-execute", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-execute", - "leftValue": "={{ $json.action }}", - "rightValue": "execute", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "execute" - }, - { - "id": "route-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-cancel", - "leftValue": "={{ $json.action }}", - "rightValue": "cancel", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "route-limit-reached", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-limit-reached", - "leftValue": "={{ $json.action }}", - "rightValue": "limit_reached", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "limit_reached" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-ui-result", - "name": "Route Batch UI Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2000, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || '' }}" - }, - "id": "telegram-answer-batch-ui-keyboard", - "name": "Answer Batch UI Keyboard", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-keyboard", - "name": "Edit Batch UI Keyboard", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3200 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Confirm...' }}" - }, - "id": "telegram-answer-batch-ui-confirm", - "name": "Answer Batch UI Confirm", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-confirm", - "name": "Edit Batch UI Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Maximum selection reached' }}", - "options": { - "showAlert": true - } - }, - "id": "telegram-answer-batch-ui-limit", - "name": "Answer Batch UI Limit", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Batch selection cancelled' }}" - }, - "id": "telegram-answer-batch-ui-cancel", - "name": "Answer Batch UI Cancel", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from /status command\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\nconst text = (message.text || '').toLowerCase().trim();\n\n// Check if user specified a container name (e.g., \"/status plex\")\nlet searchTerm = null;\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n searchTerm = parts.filter(p => p !== 'status' && p !== '/status').join(' ');\n}\n\nreturn {\n json: {\n chatId: chatId,\n messageId: 0,\n action: 'list',\n containerId: null,\n containerName: null,\n page: 0,\n queryId: null,\n searchTerm: searchTerm\n }\n};" - }, - "id": "code-prepare-status-input", - "name": "Prepare Status Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 100 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-container-status", - "name": "Execute Container Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-list-result", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-list", - "leftValue": "={{ $json.action }}", - "rightValue": "list", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "route-status-direct", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-status-direct", - "leftValue": "={{ $json.action }}", - "rightValue": "status_direct", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status_direct" - }, - { - "id": "route-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-error", - "leftValue": "={{ $json.success }}", - "rightValue": false, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-status-result", - "name": "Route Status Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 100 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from select callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'status',\n containerId: null,\n containerName: data.containerName,\n page: 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-select-input", - "name": "Prepare Select Status Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 900 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-select-status", - "name": "Execute Select Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from list pagination callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-paginate-input", - "name": "Prepare Paginate Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-paginate-status", - "name": "Execute Paginate Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from batch cancel return\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId || null,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-batch-cancel-input", - "name": "Prepare Batch Cancel Return Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1100 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-batch-cancel-status", - "name": "Execute Batch Cancel Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for confirmation sub-workflow\nconst data = $('Parse Callback Data').item.json;\n\n// Determine action based on callback type\nlet action = 'confirm'; // Default\nif (data.isCancelConfirm) {\n action = 'cancel';\n} else if (data.isConfirm && data.expired) {\n action = 'expired';\n} else if (data.isConfirm) {\n action = 'confirm';\n}\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: action,\n containerId: data.containerId || '',\n containerName: data.containerName,\n confirmAction: data.confirmAction || '',\n confirmationToken: data.timestamp || '',\n expired: data.expired || false,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-confirm-input", - "name": "Prepare Confirm Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1550 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for showing stop confirmation dialog\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'show_stop',\n containerId: data.containerId || '',\n containerName: data.containerName,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-show-stop", - "name": "Prepare Show Stop Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1350 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for showing update confirmation dialog\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'show_update',\n containerId: data.containerId || '',\n containerName: data.containerName,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-show-update", - "name": "Prepare Show Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1450 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_CONFIRMATION_WORKFLOW" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-confirmation-workflow", - "name": "Execute Confirmation", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "show-dialog", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-show", - "leftValue": "={{ $json.action }}", - "rightValue": "show_", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "show" - }, - { - "id": "confirm-stop-result", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop-result", - "leftValue": "={{ $json.action }}", - "rightValue": "confirm_stop_result", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop_result" - }, - { - "id": "confirm-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-confirm-update", - "leftValue": "={{ $json.action }}", - "rightValue": "confirm_update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirm_update" - }, - { - "id": "cancel", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-cancel", - "leftValue": "={{ $json.action }}", - "rightValue": "cancel", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "expired", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-expired", - "leftValue": "={{ $json.action }}", - "rightValue": "expired", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "expired" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-confirm-result", - "name": "Route Confirmation Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2220, - 1500 - ] - }, - { - "parameters": { - "operation": "editMessageText", - "messageId": "={{ $json.messageId }}", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "replyMarkup": "inlineKeyboard", - "inlineKeyboard": "={{ JSON.stringify($json.reply_markup) }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "http-send-confirm-dialog", - "name": "Send Confirmation Dialog", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "operation": "editMessageText", - "messageId": "={{ $json.messageId }}", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "replyMarkup": "inlineKeyboard", - "inlineKeyboard": "={{ JSON.stringify($json.reply_markup) }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "http-send-stop-result", - "name": "Send Stop Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 1500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "operation": "editMessageText", - "messageId": "={{ $json.messageId }}", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "replyMarkup": "inlineKeyboard", - "inlineKeyboard": "={{ JSON.stringify($json.reply_markup) }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "http-send-expired-msg", - "name": "Send Expired Message", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 1700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare cancel return data from confirmation result\nconst result = $input.item.json;\n\nreturn {\n json: {\n containerName: result.containerName,\n chatId: result.chatId,\n messageId: result.messageId\n }\n};" - }, - "id": "code-prepare-cancel-from-confirm", - "name": "Prepare Cancel From Confirm", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - 1600 - ] - } - ], - "connections": { - "Telegram Trigger": { - "main": [ - [ - { - "node": "Route Update Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Update Type": { - "main": [ - [ - { - "node": "IF User Authenticated", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "IF Callback Authenticated", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF User Authenticated": { - "main": [ - [ - { - "node": "Keyword Router", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF Callback Authenticated": { - "main": [ - [ - { - "node": "Parse Callback Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Data": { - "main": [ - [ - { - "node": "Route Callback", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Callback": { - "main": [ - [ - { - "node": "Check Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Update All Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Select Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer List Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Action Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Noop Callback", - "type": "main", - "index": 0 - } - ], - [], - [ - { - "node": "Prepare Confirm Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Confirm Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Callback Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Cancel": { - "main": [ - [ - { - "node": "Answer Cancel Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Cancel Query": { - "main": [ - [ - { - "node": "Delete Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Expired": { - "main": [ - [ - { - "node": "Answer Expired Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Expired Query": { - "main": [ - [ - { - "node": "Delete Expired Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Callback Action": { - "main": [ - [ - { - "node": "Execute Callback Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Callback Action": { - "main": [ - [ - { - "node": "Parse Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Result": { - "main": [ - [ - { - "node": "Answer Action Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Query": { - "main": [ - [ - { - "node": "Delete Suggestion Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Suggestion Message": { - "main": [ - [ - { - "node": "Send Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Action": { - "main": [ - [ - { - "node": "Match Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Container": { - "main": [ - [ - { - "node": "Check Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Match Count": { - "main": [ - [ - { - "node": "Send Docker Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Find Closest Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Closest Match": { - "main": [ - [ - { - "node": "Check Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Suggestion": { - "main": [ - [ - { - "node": "Build Suggestion Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send No Match", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Suggestion Keyboard": { - "main": [ - [ - { - "node": "Send Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Action Command": { - "main": [ - [ - { - "node": "Execute Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Action": { - "main": [ - [ - { - "node": "Parse Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Keyboard": { - "main": [ - [ - { - "node": "Send Batch Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Update Command": { - "main": [ - [ - { - "node": "Docker List for Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Update": { - "main": [ - [ - { - "node": "Match Update Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Update Container": { - "main": [ - [ - { - "node": "Check Update Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update Match Count": { - "main": [ - [ - { - "node": "Send Update Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Update No Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Update Multiple": { - "main": [ - [ - { - "node": "Send Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Logs Command": { - "main": [ - [ - { - "node": "Prepare Text Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Keyword Router": { - "main": [ - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Status Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get All Containers For Update All", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Logs Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Detect Batch Command": { - "main": [ - [ - { - "node": "Is Batch Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Command": { - "main": [ - [ - { - "node": "Get Containers for Batch", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Single Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Single Action": { - "main": [ - [ - { - "node": "Parse Action Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Update Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Containers for Batch": { - "main": [ - [ - { - "node": "Match Batch Containers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Batch Containers": { - "main": [ - [ - { - "node": "Needs Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Needs Disambiguation": { - "main": [ - [ - { - "node": "Build Disambiguation Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Has Not Found", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Disambiguation Message": { - "main": [ - [ - { - "node": "Send Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Not Found": { - "main": [ - [ - { - "node": "Build Not Found Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Batch Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Not Found Message": { - "main": [ - [ - { - "node": "Send Not Found Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Action": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Confirmation": { - "main": [ - [ - { - "node": "Send Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Confirm": { - "main": [ - [ - { - "node": "Check Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Batch Stop Expired": { - "main": [ - [ - { - "node": "Build Batch Stop Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Stop Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Expired": { - "main": [ - [ - { - "node": "Send Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Cancel": { - "main": [ - [ - { - "node": "Delete Batch Stop Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Command": { - "main": [ - [ - { - "node": "Docker List for Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Callback": { - "main": [ - [ - { - "node": "Route Action Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Action Type": { - "main": [ - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Show Stop Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Show Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Inline Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Immediate Action": { - "main": [ - [ - { - "node": "Get Container For Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Action": { - "main": [ - [ - { - "node": "Prepare Inline Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Immediate Action Command": { - "main": [ - [ - { - "node": "Execute Immediate Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Immediate Action": { - "main": [ - [ - { - "node": "Format Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Immediate Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Cancel Return": { - "main": [ - [ - { - "node": "Get Container For Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Cancel": { - "main": [ - [ - { - "node": "Build Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Cancel Return Submenu": { - "main": [ - [ - { - "node": "Send Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Initialize Batch State": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send Batch Start Message": { - "main": [ - [ - { - "node": "Prepare Batch Loop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Loop": { - "main": [ - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Progress Message": { - "main": [ - [ - { - "node": "Edit Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Edit Progress Message": { - "main": [ - [ - { - "node": "Route Batch Loop Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Loop Action": { - "main": [ - [ - { - "node": "Prepare Batch Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Next Iteration": { - "main": [ - [ - { - "node": "Is Batch Complete", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Complete": { - "main": [ - [ - { - "node": "Build Batch Summary", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Summary": { - "main": [ - [ - { - "node": "Send Batch Summary", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Stop Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get All Containers For Update All": { - "main": [ - [ - { - "node": "Check Available Updates", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Available Updates": { - "main": [ - [ - { - "node": "Has Updates Available", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Updates Available": { - "main": [ - [ - { - "node": "Build Update All Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send All Up To Date", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update All Confirmation": { - "main": [ - [ - { - "node": "Send Update All Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update All Expired": { - "main": [ - [ - { - "node": "Answer Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get Update All Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Expired": { - "main": [ - [ - { - "node": "Delete Update All Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Cancel": { - "main": [ - [ - { - "node": "Delete Update All Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Update All Data": { - "main": [ - [ - { - "node": "Answer Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Confirm": { - "main": [ - [ - { - "node": "Delete Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Update All Confirm": { - "main": [ - [ - { - "node": "Fetch Containers For Update All Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Fetch Containers For Update All Exec": { - "main": [ - [ - { - "node": "Prepare Update All Batch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Update All Batch": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Update Input": { - "main": [ - [ - { - "node": "Execute Text Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Callback Update Input": { - "main": [ - [ - { - "node": "Show Callback Update Progress", - "type": "main", - "index": 0 - } - ] - ] - }, - "Show Callback Update Progress": { - "main": [ - [ - { - "node": "Get Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Callback Update": { - "main": [ - [ - { - "node": "Find Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Container For Callback Update": { - "main": [ - [ - { - "node": "Execute Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Action Input": { - "main": [ - [ - { - "node": "Execute Container Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Action": { - "main": [ - [ - { - "node": "Handle Text Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Text Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Action Input": { - "main": [ - [ - { - "node": "Execute Inline Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Action": { - "main": [ - [ - { - "node": "Handle Inline Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Inline Action Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Update Input": { - "main": [ - [ - { - "node": "Execute Batch Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Update": { - "main": [ - [ - { - "node": "Handle Batch Update Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Update Result": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Action Input": { - "main": [ - [ - { - "node": "Execute Batch Action Sub-workflow", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Action Sub-workflow": { - "main": [ - [ - { - "node": "Handle Batch Action Result Sub", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Action Result Sub": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Logs Input": { - "main": [ - [ - { - "node": "Execute Text Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Text Logs": { - "main": [ - [ - { - "node": "Send Logs Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Logs Input": { - "main": [ - [ - { - "node": "Execute Inline Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Logs": { - "main": [ - [ - { - "node": "Format Inline Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Inline Logs Result": { - "main": [ - [ - { - "node": "Send Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch UI Input": { - "main": [ - [ - { - "node": "Execute Batch UI", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch UI": { - "main": [ - [ - { - "node": "Route Batch UI Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch UI Result": { - "main": [ - [ - { - "node": "Answer Batch UI Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Exec", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Limit", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Keyboard": { - "main": [ - [ - { - "node": "Edit Batch UI Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Confirm": { - "main": [ - [ - { - "node": "Edit Batch UI Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Cancel": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Status Input": { - "main": [ - [ - { - "node": "Execute Container Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Status": { - "main": [ - [ - { - "node": "Route Status Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Status Result": { - "main": [] - }, - "Answer Select Callback": { - "main": [ - [ - { - "node": "Prepare Select Status Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Select Status Input": { - "main": [ - [ - { - "node": "Execute Select Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Select Status": { - "main": [ - [ - { - "node": "Send Container Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer List Callback": { - "main": [ - [ - { - "node": "Prepare Paginate Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Paginate Input": { - "main": [ - [ - { - "node": "Execute Paginate Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Paginate Status": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return Input": { - "main": [ - [ - { - "node": "Execute Batch Cancel Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Cancel Status": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Confirm Input": { - "main": [ - [ - { - "node": "Execute Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Show Stop Input": { - "main": [ - [ - { - "node": "Execute Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Show Update Input": { - "main": [ - [ - { - "node": "Execute Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Confirmation": { - "main": [ - [ - { - "node": "Route Confirmation Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Confirmation Result": { - "main": [ - [ - { - "node": "Send Confirmation Dialog", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Stop Result", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Callback Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Cancel From Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Expired Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Cancel From Confirm": { - "main": [ - [ - { - "node": "Get Container For Cancel", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "pinData": {}, - "settings": { - "executionOrder": "v1" - }, - "staticData": null, - "tags": [], - "triggerCount": 1, - "active": false -} \ No newline at end of file diff --git a/n8n-workflow.json.backup-matching b/n8n-workflow.json.backup-matching deleted file mode 100644 index 5769bbb..0000000 --- a/n8n-workflow.json.backup-matching +++ /dev/null @@ -1,6524 +0,0 @@ -{ - "name": "Docker Manager Bot", - "nodes": [ - { - "parameters": { - "updates": [ - "message", - "callback_query" - ] - }, - "id": "telegram-trigger", - "name": "Telegram Trigger", - "type": "n8n-nodes-base.telegramTrigger", - "typeVersion": 1.1, - "position": [ - 240, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - }, - "webhookId": "86b8d6bb-7c15-4aae-a749-fcaf2dfcb9e0" - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-message", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-message", - "leftValue": "={{ $json.message?.text }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "message" - }, - { - "id": "route-callback", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-callback", - "leftValue": "={{ $json.callback_query?.id }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "callback_query" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-update-type", - "name": "Route Update Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 460, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "user-auth-condition", - "leftValue": "={{ $json.message.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-auth", - "name": "IF User Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "callback-auth-condition", - "leftValue": "={{ $json.callback_query.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-callback-auth", - "name": "IF Callback Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "keyword-menu-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-start-cmd", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/start", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "menu" - }, - { - "id": "keyword-status", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-status", - "leftValue": "={{ $json.message.text }}", - "rightValue": "status", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status" - }, - { - "id": "keyword-restart", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-restart", - "leftValue": "={{ $json.message.text }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "keyword-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-start", - "leftValue": "={{ $json.message.text }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "keyword-stop", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-stop", - "leftValue": "={{ $json.message.text }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "keyword-update-all", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "matches-update-all", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update.?all|updateall", - "operator": { - "type": "string", - "operation": "regex" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "keyword-update", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-update", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "keyword-logs", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-logs", - "leftValue": "={{ $json.message.text }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-keyword-router", - "name": "Keyword Router", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 900, - 200 - ] - }, - { - "parameters": { - "jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-action-command", - "name": "Parse Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 400 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-action", - "name": "Docker List for Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-container", - "name": "Match Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-match-count", - "name": "Check Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};" - }, - "id": "code-build-action-cmd", - "name": "Build Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 400 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-action", - "name": "Execute Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2000, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};" - }, - "id": "code-parse-action-result", - "name": "Parse Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 400 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-action-result", - "name": "Send Action Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};" - }, - "id": "code-find-closest", - "name": "Find Closest Match", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-suggestion", - "leftValue": "={{ $json.hasSuggestion }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-suggestion", - "name": "Check Suggestion", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - 300 - ] - }, - { - "parameters": { - "jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// Use modern action callback format: action:{action}:{containerName}\nconst callbackData = `action:${action}:${suggestedName}`;\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '${query}' found.\\n\\nDid you mean ${suggestedName}?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-suggestion", - "name": "Build Suggestion Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-suggestion", - "name": "Send Suggestion", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.query }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-no-match", - "name": "Send No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp for 30s timeout\n\n// Build callback_data using new bexec format\n// Format: bexec:{action}:{comma-separated-names}:{timestamp}\n// Limit to 4 containers to stay within 64-byte callback_data limit\nlet limitedNames = names;\nlet limitedCount = names.length;\nif (names.length > 4) {\n limitedNames = names.slice(0, 4);\n limitedCount = 4;\n}\n\nconst namesStr = limitedNames.join(',');\nconst callbackData = `bexec:${action}:${namesStr}:${timestamp}`;\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found ${matches.length} containers matching '${query}':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};" - }, - "id": "code-build-batch-keyboard", - "name": "Build Batch Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chat_id }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML", - "reply_markup": "={{ JSON.stringify($json.reply_markup) }}" - } - }, - "id": "telegram-send-batch-confirm", - "name": "Send Batch Confirmation", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-docker-error", - "name": "Send Docker Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp} or bstop:confirm:{names}:{timestamp}:kb\n const parts = rest.split(':'); // confirm, names, timestamp[, kb]\n if (parts[0] === 'confirm' && parts.length >= 3) {\n // Check for :kb marker (indicates inline keyboard origin)\n const fromKeyboard = parts[parts.length - 1] === 'kb';\n const timestampIdx = fromKeyboard ? parts.length - 2 : parts.length - 1;\n const timestamp = parseInt(parts[timestampIdx]);\n const namesStr = parts.slice(1, timestampIdx).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n fromKeyboard: fromKeyboard,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n\n\nif (rawData === 'batch:mode' || rawData.startsWith('batch:mode:')) {\n // Enter batch selection mode with optional page: batch:mode or batch:mode:{page}\n let page = 0;\n if (rawData.startsWith('batch:mode:')) {\n page = parseInt(rawData.substring(11)) || 0;\n }\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: true,\n batchPage: page,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:nav:')) {\n // Navigate batch selection with preserved selection: batch:nav:{page}:{selected_csv}\n const parts = rawData.substring(10).split(':'); // Remove 'batch:nav:'\n const page = parseInt(parts[0]) || 0;\n const selectedCsv = parts.slice(1).join(':') || '';\n const selectedCount = selectedCsv ? selectedCsv.split(',').length : 0;\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchNav: true,\n batchPage: page,\n selectedCsv: selectedCsv,\n selectedCount: selectedCount,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{page}:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const page = parseInt(parts[0]) || 0; // Current page\n const selectedCsv = parts[1] || ''; // Currently selected (comma-separated)\n const toggleName = parts.slice(2).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\n batchPage: page,\n selectedCsv: selectedCsv === '' ? '' : selectedCsv,\n toggleName: toggleName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:exec:')) {\n // Execute batch action: batch:exec:{action}:{selected_csv}\n const parts = rawData.substring(11).split(':'); // Remove 'batch:exec:'\n const action = parts[0]; // update/start/stop/restart\n const selectedCsv = parts.slice(1).join(':'); // Comma-separated names\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n action: action,\n selectedCsv: selectedCsv,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:clear') {\n // Clear selection\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchClear: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:cancel') {\n // Cancel batch mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchCancel: true,\n isCancel: false,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('uall:confirm:')) {\n // Update all confirmation: uall:confirm:{timestamp}\n const parts = rawData.substring(13).split(':'); // Remove 'uall:confirm:'\n const timestamp = parseInt(parts[0]);\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAll: true,\n confirmAction: 'updateall',\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n isConfirm: false\n }\n };\n}\n\nif (rawData === 'uall:cancel') {\n // Update all cancel\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAllCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n isConfirm: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\n }\n};" - }, - "id": "code-parse-callback", - "name": "Parse Callback Data", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "is-update-all", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-true", - "leftValue": "={{ $json.isUpdateAll }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "is-update-all-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-cancel-true", - "leftValue": "={{ $json.isUpdateAllCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateallcancel" - }, - { - "id": "is-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-true", - "leftValue": "={{ $json.isCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "is-expired", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "expired-true", - "leftValue": "={{ $json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "expired" - }, - { - "id": "is-batch", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "not-batch-exec", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batch" - }, - { - "id": "is-select", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "select-true", - "leftValue": "={{ $json.isSelect }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "select" - }, - { - "id": "is-list", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "list-true", - "leftValue": "={{ $json.isList }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "is-action", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "action-true", - "leftValue": "={{ $json.isAction }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "is-noop", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "noop-true", - "leftValue": "={{ $json.isNoop }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "noop" - }, - { - "id": "is-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "confirm-true", - "leftValue": "={{ $json.isConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirm" - }, - { - "id": "is-cancel-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-confirm-true", - "leftValue": "={{ $json.isCancelConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancelConfirm" - }, - { - "id": "is-batch-stop-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-confirm-true", - "leftValue": "={{ $json.isBatchStopConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopConfirm" - }, - { - "id": "is-batch-stop-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-cancel-true", - "leftValue": "={{ $json.isBatchStopCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopCancel" - }, - { - "id": "is-bexec-text-cmd", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "is-batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "bexecTextCmd" - }, - { - "id": "is-batch-mode", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-mode-true", - "leftValue": "={{ $json.isBatchMode }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchmode" - }, - { - "id": "is-batch-toggle", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-toggle-true", - "leftValue": "={{ $json.isBatchToggle }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchtoggle" - }, - { - "id": "is-batch-nav", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-nav-true", - "leftValue": "={{ $json.isBatchNav }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchnav" - }, - { - "id": "is-batch-exec", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchexec" - }, - { - "id": "is-batch-clear", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-clear-true", - "leftValue": "={{ $json.isBatchClear }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchclear" - }, - { - "id": "is-batch-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-cancel-true", - "leftValue": "={{ $json.isBatchCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchcancel" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-callback", - "name": "Route Callback", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1120, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};" - }, - "id": "code-handle-cancel", - "name": "Handle Cancel", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-cancel", - "name": "Answer Cancel Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-cancel-msg", - "name": "Delete Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};" - }, - "id": "code-handle-expired", - "name": "Handle Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}", - "options": {} - }, - "id": "http-answer-expired", - "name": "Answer Expired Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-expired-msg", - "name": "Delete Expired Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};" - }, - "id": "code-build-callback-cmd", - "name": "Build Callback Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-callback-action", - "name": "Execute Callback Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n};" - }, - "id": "code-parse-callback-result", - "name": "Parse Callback Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-action", - "name": "Answer Action Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-suggestion-msg", - "name": "Delete Suggestion Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Parse Callback Result').item.json.chatId }}", - "text": "={{ $('Parse Callback Result').item.json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-callback-result", - "name": "Send Callback Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-confirm-msg", - "name": "Delete Batch Confirm Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - 800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-update", - "name": "Parse Update Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 1000 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-update", - "name": "Docker List for Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-update-container", - "name": "Match Update Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "update-docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - }, - { - "id": "update-no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "no-match" - }, - { - "id": "update-single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "single" - }, - { - "id": "update-multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "multiple" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-update-match-count", - "name": "Check Update Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-error", - "name": "Send Update Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};" - }, - "id": "code-update-multiple-handler", - "name": "Handle Update Multiple", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-multiple", - "name": "Send Update Multiple", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.containerQuery }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-no-match", - "name": "Send Update No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-logs", - "name": "Parse Logs Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 600 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Prepare Text Logs Input').item.json.chatId }}", - "text": "={{ $json.message }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-logs", - "name": "Send Logs Response", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.message.chat.id }}", - "text": "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" - } - }, - "id": "telegram-show-menu", - "name": "Show Menu", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1120, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-list", - "name": "Send Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 0 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu-direct", - "name": "Send Container Submenu Direct", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - -100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-select-callback", - "name": "Answer Select Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu", - "name": "Send Container Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-noop-callback", - "name": "Answer Noop Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-list-callback", - "name": "Answer List Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-edit-container-list", - "name": "Edit Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-action-callback", - "name": "Answer Action Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "action-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "action-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "action-logs", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-logs", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-action-type", - "name": "Route Action Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Build start/restart action command for inline keyboard\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst action = data.action; // 'start' or 'restart'\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// For restart, use ?t=10 for graceful timeout\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n containerName,\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-prepare-immediate-action", - "name": "Prepare Immediate Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-action", - "name": "Get Container For Action", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Find container and execute action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-immediate-action-cmd", - "name": "Build Immediate Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1100 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-immediate-action", - "name": "Execute Immediate Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2440, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Build action completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F ${containerName} started`,\n restart: `\\u{1F504} ${containerName} restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} ${containerName}`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-immediate-result", - "name": "Format Immediate Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-immediate-result", - "name": "Send Immediate Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-logs-result", - "name": "Send Logs Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Cancel confirmation - return to container submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};" - }, - "id": "code-prepare-cancel-return", - "name": "Prepare Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1800 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-cancel", - "name": "Get Container For Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 1800 - ] - }, - { - "parameters": { - "jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Cancel Return\").item.json;\nconst searchName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-cancel-return-submenu", - "name": "Build Cancel Return Submenu", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-cancel-return-submenu", - "name": "Send Cancel Return Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Detect if this is a batch command (multiple container names)\n// Batch commands: {action} {name1} {name2} ... where action is update/start/stop/restart\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action type from the routed keyword\n// This node receives from Keyword Router which already matched the action\nlet action = null;\nlet containerPart = '';\n\nif (text.startsWith('restart ')) {\n action = 'restart';\n containerPart = text.substring(8).trim();\n} else if (text.includes('restart')) {\n action = 'restart';\n containerPart = text.replace('restart', '').trim();\n} else if (text.startsWith('start ')) {\n action = 'start';\n containerPart = text.substring(6).trim();\n} else if (text.includes('start')) {\n action = 'start';\n containerPart = text.replace('start', '').trim();\n} else if (text.startsWith('stop ')) {\n action = 'stop';\n containerPart = text.substring(5).trim();\n} else if (text.includes('stop')) {\n action = 'stop';\n containerPart = text.replace('stop', '').trim();\n} else if (text.startsWith('update ')) {\n action = 'update';\n containerPart = text.substring(7).trim();\n} else if (text.includes('update')) {\n action = 'update';\n containerPart = text.replace('update', '').trim();\n}\n\nif (!action || !containerPart) {\n // No valid action found, pass through as non-batch (will be handled by existing flow)\n return {\n json: {\n isBatch: false,\n action: action,\n containerNames: [],\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n };\n}\n\n// Split container names by whitespace\nconst containerNames = containerPart.split(/\\s+/).filter(name => name.length > 0);\n\n// Batch = 2 or more containers\nconst isBatch = containerNames.length >= 2;\n\nreturn {\n json: {\n isBatch: isBatch,\n action: action,\n containerNames: containerNames,\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n};" - }, - "id": "code-detect-batch", - "name": "Detect Batch Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - -200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-check", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-is-batch", - "name": "Is Batch Command", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1120, - -200 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start-stop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-action-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-single-action", - "name": "Route Single Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1340, - -100 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-batch", - "name": "Get Containers for Batch", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1340, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Match batch container names with exact-match priority\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Parse Docker container list\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n error: true,\n errorMessage: 'Cannot connect to Docker',\n chatId: chatId\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Match results\nconst allMatched = []; // Containers that matched exactly\nconst needsDisambiguation = []; // { input, matches: [...] }\nconst notFound = []; // Container names with no matches\n\nfor (const inputName of containerNames) {\n const normalized = inputName.toLowerCase().trim();\n \n // 1. Check for exact match first (highest priority)\n const exactMatch = containers.find(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName === normalized;\n });\n \n if (exactMatch) {\n // Exact match wins - no disambiguation needed\n allMatched.push({\n Id: exactMatch.Id,\n Name: exactMatch.Names[0].replace(/^\\//, ''),\n State: exactMatch.State,\n Image: exactMatch.Image,\n inputName: inputName,\n matchType: 'exact'\n });\n continue;\n }\n \n // 2. Check for substring matches (fuzzy)\n const fuzzyMatches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n });\n \n if (fuzzyMatches.length === 0) {\n // No matches at all\n notFound.push(inputName);\n } else if (fuzzyMatches.length === 1) {\n // Single fuzzy match - treat as found\n const match = fuzzyMatches[0];\n allMatched.push({\n Id: match.Id,\n Name: match.Names[0].replace(/^\\//, ''),\n State: match.State,\n Image: match.Image,\n inputName: inputName,\n matchType: 'fuzzy'\n });\n } else {\n // Multiple fuzzy matches - needs disambiguation\n needsDisambiguation.push({\n input: inputName,\n matches: fuzzyMatches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n }))\n });\n }\n}\n\nreturn [{\n json: {\n allMatched: allMatched,\n needsDisambiguation: needsDisambiguation,\n notFound: notFound,\n hasDisambiguation: needsDisambiguation.length > 0,\n hasNotFound: notFound.length > 0,\n action: action,\n chatId: chatId,\n messageId: messageId,\n originalContainerNames: containerNames,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-batch-containers", - "name": "Match Batch Containers", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - -300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-disambiguation", - "leftValue": "={{ $json.hasDisambiguation }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-needs-disambiguation", - "name": "Needs Disambiguation", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1780, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build disambiguation message with inline keyboard\nconst data = $json;\nconst disambig = data.needsDisambiguation;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message text\nlet text = 'Multiple matches found:\\n\\n';\n\nfor (const item of disambig) {\n const matchNames = item.matches.map(m => m.Name).join(', ');\n text += `\\u2022 '${item.input}' matches: ${matchNames}\\n`;\n}\n\ntext += '\\nPlease be more specific or select from options below.';\n\n// Build inline keyboard with exact container names\n// Each row has one disambiguation option\nconst keyboard = [];\n\nfor (const item of disambig) {\n // Create a row for each ambiguous input\n const row = [];\n for (const match of item.matches.slice(0, 3)) { // Max 3 options per row\n // Callback format: batch:{action}:{containerName}\n // This will be handled by callback router to add to batch\n row.push({\n text: match.Name,\n callback_data: `bselect:${action}:${match.Name}`\n });\n }\n keyboard.push(row);\n}\n\n// Add cancel button\nkeyboard.push([{ text: 'Cancel', callback_data: '{\"a\":\"x\"}' }]);\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: keyboard\n }\n }\n};" - }, - "id": "code-build-disambiguation", - "name": "Build Disambiguation Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-disambiguation", - "name": "Send Disambiguation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-not-found", - "leftValue": "={{ $json.hasNotFound }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-not-found", - "name": "Has Not Found", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Build not found message\nconst data = $json;\nconst notFound = data.notFound;\nconst allMatched = data.allMatched;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message\nlet text = '';\n\nif (notFound.length > 0) {\n text += 'Container(s) not found:\\n';\n for (const name of notFound) {\n text += `\\u2022 ${name}\\n`;\n }\n text += '\\n';\n}\n\nif (allMatched.length > 0) {\n text += `Found ${allMatched.length} container(s):\\n`;\n for (const c of allMatched) {\n text += `\\u2022 ${c.Name}\\n`;\n }\n text += `\\nProceed with ${action} on found containers?`;\n \n // Build confirmation keyboard\n const names = allMatched.map(c => c.Name).join(',');\n const timestamp = Math.floor(Date.now() / 1000);\n \n return {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${allMatched.length} containers`, callback_data: `bexec:${action}:${names}:${timestamp}` },\n { text: 'Cancel', callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n hasMatched: true,\n matchedContainers: allMatched\n }\n };\n} else {\n // No containers matched at all\n return {\n json: {\n chat_id: chatId,\n text: text + 'No valid containers to ' + action + '.',\n parse_mode: 'HTML',\n hasMatched: false\n }\n };\n}" - }, - "id": "code-build-not-found", - "name": "Build Not Found Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - -100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-not-found", - "name": "Send Not Found Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - -100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "batch-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "batch-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "batch-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "batch-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-action", - "name": "Route Batch Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2220, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build batch stop confirmation message\n// Per context: Batch stop confirms due to fuzzy matching risk\nconst data = $json;\nconst allMatched = data.allMatched;\nconst chatId = data.chatId;\n\nconst count = allMatched.length;\nconst names = allMatched.map(c => c.Name);\nconst namesStr = names.join(',');\nconst timestamp = Math.floor(Date.now() / 1000);\n\n// Build confirmation message\nlet text = `Stop ${count} container${count > 1 ? 's' : ''}?\\n\\n`;\nfor (const name of names) {\n text += `\\u2022 ${name}\\n`;\n}\n\n// Callback format: bstop:confirm:{comma-separated-names}:{timestamp}\nconst confirmCallback = `bstop:confirm:${namesStr}:${timestamp}`;\nconst cancelCallback = 'bstop:cancel';\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: 'Confirm', callback_data: confirmCallback },\n { text: 'Cancel', callback_data: cancelCallback }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-batch-stop-confirm", - "name": "Build Batch Stop Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-confirm", - "name": "Send Batch Stop Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $(\"Parse Callback Data\").item.json.queryId }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-confirm", - "name": "Answer Batch Stop Confirm", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: 'Cancelled' }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-cancel", - "name": "Answer Batch Stop Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-stop-cancel", - "name": "Delete Batch Stop Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 800 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-expired", - "leftValue": "={{ $(\"Parse Callback Data\").item.json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-stop-expired", - "name": "Check Batch Stop Expired", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Handle expired batch stop confirmation\nconst data = $json;\nreturn {\n json: {\n chat_id: data.chatId,\n message_id: data.messageId,\n text: 'Confirmation expired. Please try again.',\n parse_mode: 'HTML'\n }\n};" - }, - "id": "code-batch-stop-expired", - "name": "Build Batch Stop Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-expired", - "name": "Send Batch Stop Expired", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Initialize batch state for execution\n// Input comes from Route Batch Action OR batch callbacks (bexec, bstop confirmed)\nconst data = $json;\n\n// Handle different input sources\nlet containers, action, chatId, messageId, fromKeyboard;\n\nif (data.allMatched) {\n // From Route Batch Action or Prepare Batch Exec\n containers = data.allMatched;\n action = data.action;\n chatId = data.chatId;\n messageId = data.messageId || null;\n // Check if fromKeyboard was set by caller (e.g., Prepare Batch Exec for inline keyboard)\n fromKeyboard = data.fromKeyboard || false;\n} else if (data.containerNames) {\n // From batch callback (bexec or bstop:confirm) - keyboard flow\n // Need to resolve names to container objects\n containers = data.containerNames.map(name => ({ Name: name, Id: null }));\n action = data.batchAction || 'stop';\n chatId = data.chatId;\n messageId = data.messageId;\n fromKeyboard = data.fromKeyboard !== false; // Default true for callbacks\n} else {\n throw new Error('Invalid batch state input');\n}\n\nreturn {\n json: {\n containers: containers,\n action: action,\n totalCount: containers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: messageId,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-init-batch-state", - "name": "Initialize Batch State", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/{{ $json.fromKeyboard ? 'editMessageText' : 'sendMessage' }}", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json.fromKeyboard ? { chat_id: $json.chatId, message_id: $json.messageId, text: 'Batch ' + $json.action + '\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' } : { chat_id: $json.chatId, text: 'Batch ' + $json.action + '\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' }) }}", - "options": {} - }, - "id": "http-send-batch-start", - "name": "Send Batch Start Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Store progress message ID and prepare first iteration\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n throw new Error('Failed to get Initialize Batch State: ' + e.message);\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Get the message ID for progress updates\n// For keyboard: use original messageId (we edit in place)\n// For text commands: use the new message_id from sendMessage response\nlet progressMessageId;\nif (batchState.fromKeyboard) {\n progressMessageId = batchState.messageId;\n} else {\n // Get message_id from Send Batch Start Message response\n const sendResponse = $json;\n progressMessageId = sendResponse.result?.message_id || batchState.messageId;\n}\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-loop", - "name": "Prepare Batch Loop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Build progress message for current container\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\n\nconst containerName = container.Name || container;\nconst current = currentIndex + 1;\n\nlet progressText = `Batch ${action} in progress...\\n\\n`;\nprogressText += `Current: ${containerName}\\n`;\nprogressText += `Progress: ${current}/${totalCount}\\n\\n`;\n\nif (successCount > 0 || failureCount > 0) {\n progressText += `Completed: ${successCount} success`;\n if (failureCount > 0) {\n progressText += `, ${failureCount} failed`;\n }\n}\n\nreturn {\n json: {\n ...data,\n progressText: progressText,\n containerName: containerName\n }\n};" - }, - "id": "code-build-progress", - "name": "Build Progress Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3320, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.progressMessageId, text: $json.progressText, parse_mode: 'HTML' }) }}", - "options": { - "response": { - "response": { - "neverError": true - } - } - } - }, - "id": "http-edit-progress", - "name": "Edit Progress Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3540, - -500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "loop-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "loop-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "loop-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "loop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-loop-action", - "name": "Route Batch Loop Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 3760, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare data for next iteration or completion\nconst data = $json;\n\n// Check if more containers to process\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst containers = data.containers;\nconst nextIndex = currentIndex + 1;\nconst isComplete = nextIndex >= totalCount;\n\nreturn {\n json: {\n ...data,\n currentIndex: nextIndex,\n container: isComplete ? null : containers[nextIndex],\n isComplete: isComplete\n }\n};" - }, - "id": "code-prepare-next-iteration", - "name": "Prepare Next Iteration", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5520, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-complete", - "leftValue": "={{ $json.isComplete }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-complete", - "name": "Is Batch Complete", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 5620, - -400 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch stop data for execution\n// Input from Parse Callback Data (via Check Batch Stop Expired)\nconst data = $(\"Parse Callback Data\").item.json;\n\n// containerNames is an array from callback\nconst containerNames = data.containerNames || [];\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: data.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-stop-exec", - "name": "Prepare Batch Stop Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch exec data for execution\n// Input from Route Batch UI Result (sub-workflow output) or existing batch path\nconst data = $json;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// containerNames can be array or comma-separated string\nlet containerNames = data.containerNames || data.containers || [];\nif (typeof containerNames === 'string') {\n containerNames = containerNames.split(',').filter(n => n);\n}\nif (Array.isArray(containerNames) && containerNames[0] && containerNames[0].name) {\n containerNames = containerNames.map(c => c.name);\n}\n\n// action field might be 'action' or 'batchAction'\nconst action = data.batchAction || data.action || 'start';\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: action,\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-prepare-batch-exec", - "name": "Prepare Batch Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `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};" - }, - "id": "code-build-batch-summary", - "name": "Build Batch Summary", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5740, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', ...$json.reply_markup ? { reply_markup: $json.reply_markup } : {} }) }}", - "options": {} - }, - "id": "http-send-batch-summary", - "name": "Send Batch Summary", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 5960, - -400 - ] - }, - { - "parameters": { - "url": "http://docker-socket-proxy:2375/containers/json?all=false", - "options": {} - }, - "id": "http-get-all-containers-update-all", - "name": "Get All Containers For Update All", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1200, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\nconst chatId = $('Keyword Router').first().json.message.chat.id;\nconst messageId = $('Keyword Router').first().json.message.message_id;\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to containers using :latest tag\n// For now, we'll mark all :latest containers as \"potentially need update\"\n// A full implementation would pull each image, but that's expensive\n// We'll show all :latest containers and let batch execution handle the actual check\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = c.Image || '';\n // Check if using :latest or no tag specified (defaults to :latest)\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n allContainers: allContainers\n }\n};" - }, - "id": "code-check-available-updates", - "name": "Check Available Updates", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1400, - 2200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-containers", - "leftValue": "={{ $json.count }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-updates-available", - "name": "Has Updates Available", - "type": "n8n-nodes-base.if", - "typeVersion": 2.1, - "position": [ - 1600, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\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": { - "jsCode": "// Prepare data to return to container list\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: 0\n }\n};" - }, - "id": "code-prepare-batch-cancel-return", - "name": "Prepare Batch Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2200, - 5000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (text mode)\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId: 0, // No message to edit in text mode\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-update", - "name": "Prepare Text Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1200 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Check Update Match Count').item.json.chatId }}", - "text": "=Updating {{ $('Check Update Match Count').item.json.matches[0].Name }}...", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-text-update-started", - "name": "Send Text Update Started", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-text-update-subworkflow", - "name": "Execute Text Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (inline mode)\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n containerId: '', // Will be resolved by sub-workflow from name\n containerName,\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-callback-update", - "name": "Prepare Callback Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1650 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: '\\u2B06\\uFE0F Updating ' + $json.containerName + '...\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.', parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } }) }}", - "options": {} - }, - "id": "http-callback-update-progress", - "name": "Show Callback Update Progress", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1650 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-callback", - "name": "Get Container For Callback Update", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Find container ID from name for callback update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Callback Update Input').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-find-container-callback", - "name": "Find Container For Callback Update", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-callback-update-subworkflow", - "name": "Execute Callback Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2880, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst containerName = data.matches[0].Name;\nconst action = data.action;\nconst chatId = data.chatId;\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId,\n messageId: 0, // Text mode doesn't have a message to edit\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-action-rr53pd94", - "name": "Prepare Text Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-container-action-qokchnw8", - "name": "Execute Container Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for text command path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst chatId = result.chatId;\n\nreturn {\n json: {\n success: success,\n chatId: chatId,\n text: message\n }\n};" - }, - "id": "code-handle-text-result-c6ha90fh", - "name": "Handle Text Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow from inline keyboard\n// Container lookup already done by Get Container For Action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: action,\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-inline-action-tyjn5pb1", - "name": "Prepare Inline Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1200 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-inline-action-8aoev7xt", - "name": "Execute Inline Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2440, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for inline keyboard path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst action = result.action;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'action:' + action + ':' + containerName }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-inline-result-x19h97t3", - "name": "Handle Inline Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65", - "name": "Prepare Batch Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -800 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "options": {} - }, - "id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c", - "name": "Execute Batch Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Handle update result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: 'update',\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "f5576e75-8cfa-4ac3-9fea-64fae8d31bec", - "name": "Handle Batch Update Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Actions sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\nconst action = data.action;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "958f19ef-249b-42ca-8a29-ecb91548f1dd", - "name": "Prepare Batch Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -200 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "options": {} - }, - "id": "3baebdc9-3cda-478a-b0cc-0fb33a542f03", - "name": "Execute Batch Action Sub-workflow", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Handle action result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: data.action,\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "dc8db8cb-3ba8-471d-98df-c47dcdb4e6ea", - "name": "Handle Batch Action Result Sub", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (text command)\nconst data = $json;\n\n// Check if there's an error from Parse Logs Command\nif (data.error) {\n return {\n json: {\n error: true,\n chatId: data.chatId,\n text: data.text\n }\n };\n}\n\nreturn {\n json: {\n containerName: data.containerQuery,\n lineCount: data.lines,\n chatId: data.chatId,\n messageId: data.messageId || 0,\n responseMode: \"text\"\n }\n};" - }, - "id": "a895bb2d-1f61-4466-b475-b32ec5f0e83a", - "name": "Prepare Text Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 600 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "926c7683-c0e4-41a4-a983-e3f7ecb6ff41", - "name": "Execute Text Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (inline action)\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n containerName: data.containerName,\n lineCount: 30,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: \"inline\"\n }\n};" - }, - "id": "16b24086-5b5d-4980-82c7-4fb37b4e8f6c", - "name": "Prepare Inline Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1300 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "a88974bd-45c0-401e-b50a-c6171cfe06d4", - "name": "Execute Inline Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\nconst containerName = result.containerName;\n\n// Add timestamp to prevent 'message not modified' error on refresh\nconst timestamp = new Date().toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n});\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\n// Append timestamp to message\nconst messageWithTimestamp = result.message + `\\n\\nUpdated: ${timestamp}`;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: messageWithTimestamp,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" - }, - "id": "b1800598-1ff6-4da3-8506-4e4e8127f902", - "name": "Format Inline Logs Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Batch UI sub-workflow\nconst data = $json;\n\n// Determine action from callback data\nlet action = 'mode'; // default\nconst callbackData = data.callbackData || '';\n\nif (data.isBatchMode) action = 'mode';\nelse if (data.isBatchToggle) action = 'toggle';\nelse if (data.isBatchNav) action = 'nav';\nelse if (data.isBatchExec) action = 'exec';\nelse if (data.isBatchClear) action = 'clear';\nelse if (data.isBatchCancel) action = 'cancel';\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n queryId: data.queryId,\n callbackData: callbackData,\n action: action,\n batchAction: data.action || 'start',\n batchPage: data.batchPage || 0,\n selectedCsv: data.selectedCsv || '',\n toggleName: data.toggleName || ''\n }\n};" - }, - "id": "code-prepare-batch-ui-input", - "name": "Prepare Batch UI Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 3400 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "ZJhnGzJT26UUmW45" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-batch-ui-subworkflow", - "name": "Execute Batch UI", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 3400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-keyboard", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-keyboard", - "leftValue": "={{ $json.action }}", - "rightValue": "keyboard", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "keyboard" - }, - { - "id": "route-confirmation", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-confirmation", - "leftValue": "={{ $json.action }}", - "rightValue": "confirmation", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirmation" - }, - { - "id": "route-execute", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-execute", - "leftValue": "={{ $json.action }}", - "rightValue": "execute", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "execute" - }, - { - "id": "route-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-cancel", - "leftValue": "={{ $json.action }}", - "rightValue": "cancel", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "route-limit-reached", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-limit-reached", - "leftValue": "={{ $json.action }}", - "rightValue": "limit_reached", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "limit_reached" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-ui-result", - "name": "Route Batch UI Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2000, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || '' }}" - }, - "id": "telegram-answer-batch-ui-keyboard", - "name": "Answer Batch UI Keyboard", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-keyboard", - "name": "Edit Batch UI Keyboard", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3200 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Confirm...' }}" - }, - "id": "telegram-answer-batch-ui-confirm", - "name": "Answer Batch UI Confirm", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-confirm", - "name": "Edit Batch UI Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Maximum selection reached' }}", - "options": { - "showAlert": true - } - }, - "id": "telegram-answer-batch-ui-limit", - "name": "Answer Batch UI Limit", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Batch selection cancelled' }}" - }, - "id": "telegram-answer-batch-ui-cancel", - "name": "Answer Batch UI Cancel", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from /status command\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\nconst text = (message.text || '').toLowerCase().trim();\n\n// Check if user specified a container name (e.g., \"/status plex\")\nlet searchTerm = null;\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n searchTerm = parts.filter(p => p !== 'status' && p !== '/status').join(' ');\n}\n\nreturn {\n json: {\n chatId: chatId,\n messageId: 0,\n action: 'list',\n containerId: null,\n containerName: null,\n page: 0,\n queryId: null,\n searchTerm: searchTerm\n }\n};" - }, - "id": "code-prepare-status-input", - "name": "Prepare Status Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 100 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "lqpg2CqesnKE2RJQ" - }, - "options": { - "waitForSubWorkflow": true - }, - "mode": "once" - }, - "id": "execute-container-status", - "name": "Execute Container Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-list-result", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-list", - "leftValue": "={{ $json.action }}", - "rightValue": "list", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "route-status-direct", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-status-direct", - "leftValue": "={{ $json.action }}", - "rightValue": "status_direct", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status_direct" - }, - { - "id": "route-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-error", - "leftValue": "={{ $json.success }}", - "rightValue": false, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-status-result", - "name": "Route Status Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 100 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from select callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'status',\n containerId: null,\n containerName: data.containerName,\n page: 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-select-input", - "name": "Prepare Select Status Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 900 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "lqpg2CqesnKE2RJQ" - }, - "options": { - "waitForSubWorkflow": true - }, - "mode": "once" - }, - "id": "execute-select-status", - "name": "Execute Select Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from list pagination callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-paginate-input", - "name": "Prepare Paginate Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "lqpg2CqesnKE2RJQ" - }, - "options": { - "waitForSubWorkflow": true - }, - "mode": "once" - }, - "id": "execute-paginate-status", - "name": "Execute Paginate Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from batch cancel return\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId || null,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-batch-cancel-input", - "name": "Prepare Batch Cancel Return Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1100 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "lqpg2CqesnKE2RJQ" - }, - "options": { - "waitForSubWorkflow": true - }, - "mode": "once" - }, - "id": "execute-batch-cancel-status", - "name": "Execute Batch Cancel Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for confirmation sub-workflow\nconst data = $('Parse Callback Data').item.json;\n\n// Determine action based on callback type\nlet action = 'confirm'; // Default\nif (data.isCancelConfirm) {\n action = 'cancel';\n} else if (data.isConfirm && data.expired) {\n action = 'expired';\n} else if (data.isConfirm) {\n action = 'confirm';\n}\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: action,\n containerId: data.containerId || '',\n containerName: data.containerName,\n confirmAction: data.confirmAction || '',\n confirmationToken: data.timestamp || '',\n expired: data.expired || false,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-confirm-input", - "name": "Prepare Confirm Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1550 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for showing stop confirmation dialog\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'show_stop',\n containerId: data.containerId || '',\n containerName: data.containerName,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-show-stop", - "name": "Prepare Show Stop Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1350 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for showing update confirmation dialog\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'show_update',\n containerId: data.containerId || '',\n containerName: data.containerName,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-show-update", - "name": "Prepare Show Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1450 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "fZ1hu8eiovkCk08G" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-confirmation-workflow", - "name": "Execute Confirmation", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "show-dialog", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-show", - "leftValue": "={{ $json.action }}", - "rightValue": "show_", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "show" - }, - { - "id": "confirm-stop-result", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop-result", - "leftValue": "={{ $json.action }}", - "rightValue": "confirm_stop_result", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop_result" - }, - { - "id": "confirm-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-confirm-update", - "leftValue": "={{ $json.action }}", - "rightValue": "confirm_update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirm_update" - }, - { - "id": "cancel", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-cancel", - "leftValue": "={{ $json.action }}", - "rightValue": "cancel", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "expired", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-expired", - "leftValue": "={{ $json.action }}", - "rightValue": "expired", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "expired" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-confirm-result", - "name": "Route Confirmation Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2220, - 1500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-confirm-dialog", - "name": "Send Confirmation Dialog", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-stop-result", - "name": "Send Stop Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-expired-msg", - "name": "Send Expired Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1700 - ] - }, - { - "parameters": { - "jsCode": "// Prepare cancel return data from confirmation result\nconst result = $input.item.json;\n\nreturn {\n json: {\n containerName: result.containerName,\n chatId: result.chatId,\n messageId: result.messageId\n }\n};" - }, - "id": "code-prepare-cancel-from-confirm", - "name": "Prepare Cancel From Confirm", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - 1600 - ] - } - ], - "connections": { - "Telegram Trigger": { - "main": [ - [ - { - "node": "Route Update Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Update Type": { - "main": [ - [ - { - "node": "IF User Authenticated", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "IF Callback Authenticated", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF User Authenticated": { - "main": [ - [ - { - "node": "Keyword Router", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF Callback Authenticated": { - "main": [ - [ - { - "node": "Parse Callback Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Data": { - "main": [ - [ - { - "node": "Route Callback", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Callback": { - "main": [ - [ - { - "node": "Check Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Update All Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Select Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Select Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer List Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Action Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Noop Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Confirm Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Confirm Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [] - ] - }, - "Handle Cancel": { - "main": [ - [ - { - "node": "Answer Cancel Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Cancel Query": { - "main": [ - [ - { - "node": "Delete Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Expired": { - "main": [ - [ - { - "node": "Answer Expired Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Expired Query": { - "main": [ - [ - { - "node": "Delete Expired Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Callback Action": { - "main": [ - [ - { - "node": "Execute Callback Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Callback Action": { - "main": [ - [ - { - "node": "Parse Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Result": { - "main": [ - [ - { - "node": "Answer Action Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Query": { - "main": [ - [ - { - "node": "Delete Suggestion Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Suggestion Message": { - "main": [ - [ - { - "node": "Send Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Action": { - "main": [ - [ - { - "node": "Match Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Container": { - "main": [ - [ - { - "node": "Check Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Match Count": { - "main": [ - [ - { - "node": "Send Docker Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Find Closest Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Closest Match": { - "main": [ - [ - { - "node": "Check Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Suggestion": { - "main": [ - [ - { - "node": "Build Suggestion Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send No Match", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Suggestion Keyboard": { - "main": [ - [ - { - "node": "Send Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Action Command": { - "main": [ - [ - { - "node": "Execute Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Action": { - "main": [ - [ - { - "node": "Parse Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Keyboard": { - "main": [ - [ - { - "node": "Send Batch Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Update Command": { - "main": [ - [ - { - "node": "Docker List for Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Update": { - "main": [ - [ - { - "node": "Match Update Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Update Container": { - "main": [ - [ - { - "node": "Check Update Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update Match Count": { - "main": [ - [ - { - "node": "Send Update Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Update No Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Update Multiple": { - "main": [ - [ - { - "node": "Send Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Logs Command": { - "main": [ - [ - { - "node": "Prepare Text Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Keyword Router": { - "main": [ - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Status Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get All Containers For Update All", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Logs Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Detect Batch Command": { - "main": [ - [ - { - "node": "Is Batch Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Command": { - "main": [ - [ - { - "node": "Get Containers for Batch", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Single Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Single Action": { - "main": [ - [ - { - "node": "Parse Action Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Update Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Containers for Batch": { - "main": [ - [ - { - "node": "Match Batch Containers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Batch Containers": { - "main": [ - [ - { - "node": "Needs Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Needs Disambiguation": { - "main": [ - [ - { - "node": "Build Disambiguation Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Has Not Found", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Disambiguation Message": { - "main": [ - [ - { - "node": "Send Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Not Found": { - "main": [ - [ - { - "node": "Build Not Found Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Batch Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Not Found Message": { - "main": [ - [ - { - "node": "Send Not Found Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Action": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Confirmation": { - "main": [ - [ - { - "node": "Send Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Confirm": { - "main": [ - [ - { - "node": "Check Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Batch Stop Expired": { - "main": [ - [ - { - "node": "Build Batch Stop Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Stop Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Expired": { - "main": [ - [ - { - "node": "Send Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Cancel": { - "main": [ - [ - { - "node": "Delete Batch Stop Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Command": { - "main": [ - [ - { - "node": "Docker List for Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Callback": { - "main": [ - [ - { - "node": "Route Action Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Action Type": { - "main": [ - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Show Stop Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Show Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Inline Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Immediate Action": { - "main": [ - [ - { - "node": "Get Container For Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Action": { - "main": [ - [ - { - "node": "Prepare Inline Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Immediate Action Command": { - "main": [ - [ - { - "node": "Execute Immediate Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Immediate Action": { - "main": [ - [ - { - "node": "Format Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Immediate Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Cancel Return": { - "main": [ - [ - { - "node": "Get Container For Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Cancel": { - "main": [ - [ - { - "node": "Build Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Cancel Return Submenu": { - "main": [ - [ - { - "node": "Send Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Initialize Batch State": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send Batch Start Message": { - "main": [ - [ - { - "node": "Prepare Batch Loop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Loop": { - "main": [ - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Progress Message": { - "main": [ - [ - { - "node": "Edit Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Edit Progress Message": { - "main": [ - [ - { - "node": "Route Batch Loop Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Loop Action": { - "main": [ - [ - { - "node": "Prepare Batch Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Next Iteration": { - "main": [ - [ - { - "node": "Is Batch Complete", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Complete": { - "main": [ - [ - { - "node": "Build Batch Summary", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Summary": { - "main": [ - [ - { - "node": "Send Batch Summary", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Stop Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get All Containers For Update All": { - "main": [ - [ - { - "node": "Check Available Updates", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Available Updates": { - "main": [ - [ - { - "node": "Has Updates Available", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Updates Available": { - "main": [ - [ - { - "node": "Build Update All Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send All Up To Date", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update All Confirmation": { - "main": [ - [ - { - "node": "Send Update All Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update All Expired": { - "main": [ - [ - { - "node": "Answer Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get Update All Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Expired": { - "main": [ - [ - { - "node": "Delete Update All Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Cancel": { - "main": [ - [ - { - "node": "Delete Update All Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Update All Data": { - "main": [ - [ - { - "node": "Answer Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Confirm": { - "main": [ - [ - { - "node": "Delete Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Update All Confirm": { - "main": [ - [ - { - "node": "Fetch Containers For Update All Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Fetch Containers For Update All Exec": { - "main": [ - [ - { - "node": "Prepare Update All Batch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Update All Batch": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Update Input": { - "main": [ - [ - { - "node": "Execute Text Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Callback Update Input": { - "main": [ - [ - { - "node": "Show Callback Update Progress", - "type": "main", - "index": 0 - } - ] - ] - }, - "Show Callback Update Progress": { - "main": [ - [ - { - "node": "Get Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Callback Update": { - "main": [ - [ - { - "node": "Find Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Container For Callback Update": { - "main": [ - [ - { - "node": "Execute Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Action Input": { - "main": [ - [ - { - "node": "Execute Container Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Action": { - "main": [ - [ - { - "node": "Handle Text Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Text Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Action Input": { - "main": [ - [ - { - "node": "Execute Inline Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Action": { - "main": [ - [ - { - "node": "Handle Inline Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Inline Action Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Update Input": { - "main": [ - [ - { - "node": "Execute Batch Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Update": { - "main": [ - [ - { - "node": "Handle Batch Update Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Update Result": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Action Input": { - "main": [ - [ - { - "node": "Execute Batch Action Sub-workflow", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Action Sub-workflow": { - "main": [ - [ - { - "node": "Handle Batch Action Result Sub", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Action Result Sub": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Logs Input": { - "main": [ - [ - { - "node": "Execute Text Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Text Logs": { - "main": [ - [ - { - "node": "Send Logs Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Logs Input": { - "main": [ - [ - { - "node": "Execute Inline Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Logs": { - "main": [ - [ - { - "node": "Format Inline Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Inline Logs Result": { - "main": [ - [ - { - "node": "Send Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch UI Input": { - "main": [ - [ - { - "node": "Execute Batch UI", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch UI": { - "main": [ - [ - { - "node": "Route Batch UI Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch UI Result": { - "main": [ - [ - { - "node": "Answer Batch UI Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Exec", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Limit", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Keyboard": { - "main": [ - [ - { - "node": "Edit Batch UI Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Confirm": { - "main": [ - [ - { - "node": "Edit Batch UI Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Cancel": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Status Input": { - "main": [ - [ - { - "node": "Execute Container Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Status": { - "main": [ - [ - { - "node": "Route Status Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Status Result": { - "main": [ - [ - { - "node": "Send Container List", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Docker Error", - "type": "main", - "index": 0 - } - ], - [] - ] - }, - "Answer Select Callback": { - "main": [ - [ - { - "node": "Prepare Select Status Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Select Status Input": { - "main": [ - [ - { - "node": "Execute Select Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Select Status": { - "main": [ - [ - { - "node": "Send Container Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer List Callback": { - "main": [ - [ - { - "node": "Prepare Paginate Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Paginate Input": { - "main": [ - [ - { - "node": "Execute Paginate Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Paginate Status": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return Input": { - "main": [ - [ - { - "node": "Execute Batch Cancel Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Cancel Status": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Confirm Input": { - "main": [ - [ - { - "node": "Execute Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Show Stop Input": { - "main": [ - [ - { - "node": "Execute Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Show Update Input": { - "main": [ - [ - { - "node": "Execute Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Confirmation": { - "main": [ - [ - { - "node": "Route Confirmation Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Confirmation Result": { - "main": [ - [ - { - "node": "Send Confirmation Dialog", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Stop Result", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Callback Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Cancel From Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Expired Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Cancel From Confirm": { - "main": [ - [ - { - "node": "Get Container For Cancel", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "pinData": {}, - "settings": { - "executionOrder": "v1" - }, - "staticData": null, - "tags": [], - "triggerCount": 1, - "active": false -} \ No newline at end of file diff --git a/n8n-workflow.json.backup-status b/n8n-workflow.json.backup-status deleted file mode 100644 index c9c86f1..0000000 --- a/n8n-workflow.json.backup-status +++ /dev/null @@ -1,6741 +0,0 @@ -{ - "name": "Docker Manager Bot", - "nodes": [ - { - "parameters": { - "updates": [ - "message", - "callback_query" - ] - }, - "id": "telegram-trigger", - "name": "Telegram Trigger", - "type": "n8n-nodes-base.telegramTrigger", - "typeVersion": 1.1, - "position": [ - 240, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - }, - "webhookId": "86b8d6bb-7c15-4aae-a749-fcaf2dfcb9e0" - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-message", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-message", - "leftValue": "={{ $json.message?.text }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "message" - }, - { - "id": "route-callback", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-callback", - "leftValue": "={{ $json.callback_query?.id }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "callback_query" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-update-type", - "name": "Route Update Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 460, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "user-auth-condition", - "leftValue": "={{ $json.message.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-auth", - "name": "IF User Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "callback-auth-condition", - "leftValue": "={{ $json.callback_query.from.id.toString() }}", - "rightValue": "563878771", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-callback-auth", - "name": "IF Callback Authenticated", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 680, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "keyword-menu-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "starts-with-start-cmd", - "leftValue": "={{ $json.message.text }}", - "rightValue": "/start", - "operator": { - "type": "string", - "operation": "startsWith" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "menu" - }, - { - "id": "keyword-status", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-status", - "leftValue": "={{ $json.message.text }}", - "rightValue": "status", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status" - }, - { - "id": "keyword-restart", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-restart", - "leftValue": "={{ $json.message.text }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "keyword-start", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-start", - "leftValue": "={{ $json.message.text }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "keyword-stop", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-stop", - "leftValue": "={{ $json.message.text }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "keyword-update-all", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "matches-update-all", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update.?all|updateall", - "operator": { - "type": "string", - "operation": "regex" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "keyword-update", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-update", - "leftValue": "={{ $json.message.text }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "keyword-logs", - "conditions": { - "options": { - "caseSensitive": false, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "contains-logs", - "leftValue": "={{ $json.message.text }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "contains" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-keyword-router", - "name": "Keyword Router", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 900, - 200 - ] - }, - { - "parameters": { - "jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-action-command", - "name": "Parse Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 400 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-action", - "name": "Docker List for Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-container", - "name": "Match Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-match-count", - "name": "Check Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};" - }, - "id": "code-build-action-cmd", - "name": "Build Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 400 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-action", - "name": "Execute Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2000, - 400 - ] - }, - { - "parameters": { - "jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};" - }, - "id": "code-parse-action-result", - "name": "Parse Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 400 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-action-result", - "name": "Send Action Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};" - }, - "id": "code-find-closest", - "name": "Find Closest Match", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-suggestion", - "leftValue": "={{ $json.hasSuggestion }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-suggestion", - "name": "Check Suggestion", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - 300 - ] - }, - { - "parameters": { - "jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// Use modern action callback format: action:{action}:{containerName}\nconst callbackData = `action:${action}:${suggestedName}`;\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '${query}' found.\\n\\nDid you mean ${suggestedName}?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-suggestion", - "name": "Build Suggestion Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-suggestion", - "name": "Send Suggestion", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.query }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-no-match", - "name": "Send No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp for 30s timeout\n\n// Build callback_data using new bexec format\n// Format: bexec:{action}:{comma-separated-names}:{timestamp}\n// Limit to 4 containers to stay within 64-byte callback_data limit\nlet limitedNames = names;\nlet limitedCount = names.length;\nif (names.length > 4) {\n limitedNames = names.slice(0, 4);\n limitedCount = 4;\n}\n\nconst namesStr = limitedNames.join(',');\nconst callbackData = `bexec:${action}:${namesStr}:${timestamp}`;\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found ${matches.length} containers matching '${query}':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};" - }, - "id": "code-build-batch-keyboard", - "name": "Build Batch Keyboard", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chat_id }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML", - "reply_markup": "={{ JSON.stringify($json.reply_markup) }}" - } - }, - "id": "telegram-send-batch-confirm", - "name": "Send Batch Confirmation", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-docker-error", - "name": "Send Docker Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp} or bstop:confirm:{names}:{timestamp}:kb\n const parts = rest.split(':'); // confirm, names, timestamp[, kb]\n if (parts[0] === 'confirm' && parts.length >= 3) {\n // Check for :kb marker (indicates inline keyboard origin)\n const fromKeyboard = parts[parts.length - 1] === 'kb';\n const timestampIdx = fromKeyboard ? parts.length - 2 : parts.length - 1;\n const timestamp = parseInt(parts[timestampIdx]);\n const namesStr = parts.slice(1, timestampIdx).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n fromKeyboard: fromKeyboard,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n\n\nif (rawData === 'batch:mode' || rawData.startsWith('batch:mode:')) {\n // Enter batch selection mode with optional page: batch:mode or batch:mode:{page}\n let page = 0;\n if (rawData.startsWith('batch:mode:')) {\n page = parseInt(rawData.substring(11)) || 0;\n }\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchMode: true,\n batchPage: page,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:nav:')) {\n // Navigate batch selection with preserved selection: batch:nav:{page}:{selected_csv}\n const parts = rawData.substring(10).split(':'); // Remove 'batch:nav:'\n const page = parseInt(parts[0]) || 0;\n const selectedCsv = parts.slice(1).join(':') || '';\n const selectedCount = selectedCsv ? selectedCsv.split(',').length : 0;\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchNav: true,\n batchPage: page,\n selectedCsv: selectedCsv,\n selectedCount: selectedCount,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:toggle:')) {\n // Toggle container selection: batch:toggle:{page}:{selected_csv}:{container_name}\n const parts = rawData.substring(13).split(':'); // Remove 'batch:toggle:'\n const page = parseInt(parts[0]) || 0; // Current page\n const selectedCsv = parts[1] || ''; // Currently selected (comma-separated)\n const toggleName = parts.slice(2).join(':'); // Container being toggled\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchToggle: true,\n batchPage: page,\n selectedCsv: selectedCsv === '' ? '' : selectedCsv,\n toggleName: toggleName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('batch:exec:')) {\n // Execute batch action: batch:exec:{action}:{selected_csv}\n const parts = rawData.substring(11).split(':'); // Remove 'batch:exec:'\n const action = parts[0]; // update/start/stop/restart\n const selectedCsv = parts.slice(1).join(':'); // Comma-separated names\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n action: action,\n selectedCsv: selectedCsv,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:clear') {\n // Clear selection\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchClear: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData === 'batch:cancel') {\n // Cancel batch mode\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchCancel: true,\n isCancel: false,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('uall:confirm:')) {\n // Update all confirmation: uall:confirm:{timestamp}\n const parts = rawData.substring(13).split(':'); // Remove 'uall:confirm:'\n const timestamp = parseInt(parts[0]);\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAll: true,\n confirmAction: 'updateall',\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n isConfirm: false\n }\n };\n}\n\nif (rawData === 'uall:cancel') {\n // Update all cancel\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isUpdateAllCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n isConfirm: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\n }\n};" - }, - "id": "code-parse-callback", - "name": "Parse Callback Data", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "is-update-all", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-true", - "leftValue": "={{ $json.isUpdateAll }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateall" - }, - { - "id": "is-update-all-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "update-all-cancel-true", - "leftValue": "={{ $json.isUpdateAllCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "updateallcancel" - }, - { - "id": "is-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-true", - "leftValue": "={{ $json.isCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "is-expired", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "expired-true", - "leftValue": "={{ $json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "expired" - }, - { - "id": "is-batch", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "not-batch-exec", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batch" - }, - { - "id": "is-select", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "select-true", - "leftValue": "={{ $json.isSelect }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "select" - }, - { - "id": "is-list", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "list-true", - "leftValue": "={{ $json.isList }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "is-action", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "action-true", - "leftValue": "={{ $json.isAction }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "is-noop", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "noop-true", - "leftValue": "={{ $json.isNoop }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "noop" - }, - { - "id": "is-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "confirm-true", - "leftValue": "={{ $json.isConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirm" - }, - { - "id": "is-cancel-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "cancel-confirm-true", - "leftValue": "={{ $json.isCancelConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancelConfirm" - }, - { - "id": "is-batch-stop-confirm", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-confirm-true", - "leftValue": "={{ $json.isBatchStopConfirm }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopConfirm" - }, - { - "id": "is-batch-stop-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-cancel-true", - "leftValue": "={{ $json.isBatchStopCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchStopCancel" - }, - { - "id": "is-bexec-text-cmd", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - }, - { - "id": "is-batch-true", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "bexecTextCmd" - }, - { - "id": "is-batch-mode", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-mode-true", - "leftValue": "={{ $json.isBatchMode }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchmode" - }, - { - "id": "is-batch-toggle", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-toggle-true", - "leftValue": "={{ $json.isBatchToggle }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchtoggle" - }, - { - "id": "is-batch-nav", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-nav-true", - "leftValue": "={{ $json.isBatchNav }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchnav" - }, - { - "id": "is-batch-exec", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-exec-true", - "leftValue": "={{ $json.isBatchExec }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchexec" - }, - { - "id": "is-batch-clear", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-clear-true", - "leftValue": "={{ $json.isBatchClear }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchclear" - }, - { - "id": "is-batch-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-cancel-true", - "leftValue": "={{ $json.isBatchCancel }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "batchcancel" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-callback", - "name": "Route Callback", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1120, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};" - }, - "id": "code-handle-cancel", - "name": "Handle Cancel", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-cancel", - "name": "Answer Cancel Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-cancel-msg", - "name": "Delete Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};" - }, - "id": "code-handle-expired", - "name": "Handle Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}", - "options": {} - }, - "id": "http-answer-expired", - "name": "Answer Expired Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-expired-msg", - "name": "Delete Expired Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};" - }, - "id": "code-build-callback-cmd", - "name": "Build Callback Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-callback-action", - "name": "Execute Callback Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n};" - }, - "id": "code-parse-callback-result", - "name": "Parse Callback Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", - "options": {} - }, - "id": "http-answer-action", - "name": "Answer Action Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-suggestion-msg", - "name": "Delete Suggestion Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Parse Callback Result').item.json.chatId }}", - "text": "={{ $('Parse Callback Result').item.json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-callback-result", - "name": "Send Callback Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-confirm-msg", - "name": "Delete Batch Confirm Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - 800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-update", - "name": "Parse Update Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 1000 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-update", - "name": "Docker List for Update", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-update-container", - "name": "Match Update Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "update-docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - }, - { - "id": "update-no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "no-match" - }, - { - "id": "update-single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "single" - }, - { - "id": "update-multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-gt-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "multiple" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-update-match-count", - "name": "Check Update Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-error", - "name": "Send Update Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};" - }, - "id": "code-update-multiple-handler", - "name": "Handle Update Multiple", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-multiple", - "name": "Send Update Multiple", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2000, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "=No container found matching '{{ $json.containerQuery }}'", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-update-no-match", - "name": "Send Update No Match", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};" - }, - "id": "code-parse-logs", - "name": "Parse Logs Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - 600 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Prepare Text Logs Input').item.json.chatId }}", - "text": "={{ $json.message }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-logs", - "name": "Send Logs Response", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2440, - 500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.message.chat.id }}", - "text": "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" - } - }, - "id": "telegram-show-menu", - "name": "Show Menu", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1120, - 300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-list", - "name": "Send Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 0 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu-direct", - "name": "Send Container Submenu Direct", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - -100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-select-callback", - "name": "Answer Select Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-container-submenu", - "name": "Send Container Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 900 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-noop-callback", - "name": "Answer Noop Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-list-callback", - "name": "Answer List Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-edit-container-list", - "name": "Edit Container List", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1000 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-action-callback", - "name": "Answer Action Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "action-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "action-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "action-logs", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-logs", - "leftValue": "={{ $('Parse Callback Data').item.json.action }}", - "rightValue": "logs", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "logs" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-action-type", - "name": "Route Action Type", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Build start/restart action command for inline keyboard\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst action = data.action; // 'start' or 'restart'\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// For restart, use ?t=10 for graceful timeout\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n containerName,\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-prepare-immediate-action", - "name": "Prepare Immediate Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1100 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-action", - "name": "Get Container For Action", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Find container and execute action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-immediate-action-cmd", - "name": "Build Immediate Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1100 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-immediate-action", - "name": "Execute Immediate Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2440, - 1100 - ] - }, - { - "parameters": { - "jsCode": "// Build action completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F ${containerName} started`,\n restart: `\\u{1F504} ${containerName} restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} ${containerName}`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-immediate-result", - "name": "Format Immediate Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-immediate-result", - "name": "Send Immediate Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1100 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-logs-result", - "name": "Send Logs Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2880, - 1300 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build Stop Confirmation dialog with timestamp for 30-second timeout\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Stop ${containerName}?\\n\\nThis will stop the container immediately.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-build-stop-confirmation", - "name": "Build Stop Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-stop-confirmation", - "name": "Send Stop Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Build Update Confirmation dialog with timestamp for 30-second timeout\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Update', callback_data: `confirm:update:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Update ${containerName}?\\n\\nThis will pull the latest image and recreate the container.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-build-update-confirmation", - "name": "Build Update Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-update-confirmation", - "name": "Send Update Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1500 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-confirm-callback", - "name": "Answer Confirm Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "check-expired", - "leftValue": "={{ $('Parse Callback Data').item.json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-confirm-expired", - "name": "Check Confirm Expired", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - 1600 - ] - }, - { - "parameters": { - "jsCode": "// Confirmation expired - return to submenu\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Build keyboard for expired message\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for ${containerName} has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-confirm-expired", - "name": "Handle Confirm Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-expired-confirm", - "name": "Send Expired Confirm", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "confirm-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop-confirm", - "leftValue": "={{ $('Parse Callback Data').item.json.confirmAction }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "confirm-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update-confirm", - "leftValue": "={{ $('Parse Callback Data').item.json.confirmAction }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-confirm-action", - "name": "Route Confirm Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1780, - 1600 - ] - }, - { - "parameters": { - "jsCode": "// Prepare stop action from confirmation\nconst data = $('Parse Callback Data').item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};" - }, - "id": "code-prepare-confirmed-stop", - "name": "Prepare Confirmed Stop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1550 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-stop", - "name": "Get Container For Stop", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1550 - ] - }, - { - "parameters": { - "jsCode": "// Find container and build stop command\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-confirmed-stop-cmd", - "name": "Build Confirmed Stop Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - 1550 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-confirmed-stop", - "name": "Execute Confirmed Stop", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2660, - 1550 - ] - }, - { - "parameters": { - "jsCode": "// Build stop completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Confirmed Stop Command').item.json;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already stopped\nconst success = statusCode === 204 || statusCode === 304;\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = `\\u23F9\\uFE0F ${containerName} stopped`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to stop ${containerName}`;\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:stop:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-format-confirmed-stop-result", - "name": "Format Confirmed Stop Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - 1550 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-confirmed-stop-result", - "name": "Send Confirmed Stop Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3100, - 1550 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-cancel-confirm-callback", - "name": "Answer Cancel Confirm Callback", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 1800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Cancel confirmation - return to container submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};" - }, - "id": "code-prepare-cancel-return", - "name": "Prepare Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1800 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-cancel", - "name": "Get Container For Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1780, - 1800 - ] - }, - { - "parameters": { - "jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Cancel Return\").item.json;\nconst searchName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];" - }, - "id": "code-build-cancel-return-submenu", - "name": "Build Cancel Return Submenu", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-cancel-return-submenu", - "name": "Send Cancel Return Submenu", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Detect if this is a batch command (multiple container names)\n// Batch commands: {action} {name1} {name2} ... where action is update/start/stop/restart\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action type from the routed keyword\n// This node receives from Keyword Router which already matched the action\nlet action = null;\nlet containerPart = '';\n\nif (text.startsWith('restart ')) {\n action = 'restart';\n containerPart = text.substring(8).trim();\n} else if (text.includes('restart')) {\n action = 'restart';\n containerPart = text.replace('restart', '').trim();\n} else if (text.startsWith('start ')) {\n action = 'start';\n containerPart = text.substring(6).trim();\n} else if (text.includes('start')) {\n action = 'start';\n containerPart = text.replace('start', '').trim();\n} else if (text.startsWith('stop ')) {\n action = 'stop';\n containerPart = text.substring(5).trim();\n} else if (text.includes('stop')) {\n action = 'stop';\n containerPart = text.replace('stop', '').trim();\n} else if (text.startsWith('update ')) {\n action = 'update';\n containerPart = text.substring(7).trim();\n} else if (text.includes('update')) {\n action = 'update';\n containerPart = text.replace('update', '').trim();\n}\n\nif (!action || !containerPart) {\n // No valid action found, pass through as non-batch (will be handled by existing flow)\n return {\n json: {\n isBatch: false,\n action: action,\n containerNames: [],\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n };\n}\n\n// Split container names by whitespace\nconst containerNames = containerPart.split(/\\s+/).filter(name => name.length > 0);\n\n// Batch = 2 or more containers\nconst isBatch = containerNames.length >= 2;\n\nreturn {\n json: {\n isBatch: isBatch,\n action: action,\n containerNames: containerNames,\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n};" - }, - "id": "code-detect-batch", - "name": "Detect Batch Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 900, - -200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-check", - "leftValue": "={{ $json.isBatch }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-is-batch", - "name": "Is Batch Command", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1120, - -200 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "action-start-stop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-action-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "notEquals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "action" - }, - { - "id": "action-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update-type", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-single-action", - "name": "Route Single Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1340, - -100 - ] - }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-batch", - "name": "Get Containers for Batch", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1340, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Match batch container names with exact-match priority\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Parse Docker container list\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n error: true,\n errorMessage: 'Cannot connect to Docker',\n chatId: chatId\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Match results\nconst allMatched = []; // Containers that matched exactly\nconst needsDisambiguation = []; // { input, matches: [...] }\nconst notFound = []; // Container names with no matches\n\nfor (const inputName of containerNames) {\n const normalized = inputName.toLowerCase().trim();\n \n // 1. Check for exact match first (highest priority)\n const exactMatch = containers.find(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName === normalized;\n });\n \n if (exactMatch) {\n // Exact match wins - no disambiguation needed\n allMatched.push({\n Id: exactMatch.Id,\n Name: exactMatch.Names[0].replace(/^\\//, ''),\n State: exactMatch.State,\n Image: exactMatch.Image,\n inputName: inputName,\n matchType: 'exact'\n });\n continue;\n }\n \n // 2. Check for substring matches (fuzzy)\n const fuzzyMatches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n });\n \n if (fuzzyMatches.length === 0) {\n // No matches at all\n notFound.push(inputName);\n } else if (fuzzyMatches.length === 1) {\n // Single fuzzy match - treat as found\n const match = fuzzyMatches[0];\n allMatched.push({\n Id: match.Id,\n Name: match.Names[0].replace(/^\\//, ''),\n State: match.State,\n Image: match.Image,\n inputName: inputName,\n matchType: 'fuzzy'\n });\n } else {\n // Multiple fuzzy matches - needs disambiguation\n needsDisambiguation.push({\n input: inputName,\n matches: fuzzyMatches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n }))\n });\n }\n}\n\nreturn [{\n json: {\n allMatched: allMatched,\n needsDisambiguation: needsDisambiguation,\n notFound: notFound,\n hasDisambiguation: needsDisambiguation.length > 0,\n hasNotFound: notFound.length > 0,\n action: action,\n chatId: chatId,\n messageId: messageId,\n originalContainerNames: containerNames,\n allContainers: containers\n }\n}];" - }, - "id": "code-match-batch-containers", - "name": "Match Batch Containers", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - -300 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-disambiguation", - "leftValue": "={{ $json.hasDisambiguation }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-needs-disambiguation", - "name": "Needs Disambiguation", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1780, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build disambiguation message with inline keyboard\nconst data = $json;\nconst disambig = data.needsDisambiguation;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message text\nlet text = 'Multiple matches found:\\n\\n';\n\nfor (const item of disambig) {\n const matchNames = item.matches.map(m => m.Name).join(', ');\n text += `\\u2022 '${item.input}' matches: ${matchNames}\\n`;\n}\n\ntext += '\\nPlease be more specific or select from options below.';\n\n// Build inline keyboard with exact container names\n// Each row has one disambiguation option\nconst keyboard = [];\n\nfor (const item of disambig) {\n // Create a row for each ambiguous input\n const row = [];\n for (const match of item.matches.slice(0, 3)) { // Max 3 options per row\n // Callback format: batch:{action}:{containerName}\n // This will be handled by callback router to add to batch\n row.push({\n text: match.Name,\n callback_data: `bselect:${action}:${match.Name}`\n });\n }\n keyboard.push(row);\n}\n\n// Add cancel button\nkeyboard.push([{ text: 'Cancel', callback_data: '{\"a\":\"x\"}' }]);\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: keyboard\n }\n }\n};" - }, - "id": "code-build-disambiguation", - "name": "Build Disambiguation Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-disambiguation", - "name": "Send Disambiguation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-not-found", - "leftValue": "={{ $json.hasNotFound }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-not-found", - "name": "Has Not Found", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 2000, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Build not found message\nconst data = $json;\nconst notFound = data.notFound;\nconst allMatched = data.allMatched;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message\nlet text = '';\n\nif (notFound.length > 0) {\n text += 'Container(s) not found:\\n';\n for (const name of notFound) {\n text += `\\u2022 ${name}\\n`;\n }\n text += '\\n';\n}\n\nif (allMatched.length > 0) {\n text += `Found ${allMatched.length} container(s):\\n`;\n for (const c of allMatched) {\n text += `\\u2022 ${c.Name}\\n`;\n }\n text += `\\nProceed with ${action} on found containers?`;\n \n // Build confirmation keyboard\n const names = allMatched.map(c => c.Name).join(',');\n const timestamp = Math.floor(Date.now() / 1000);\n \n return {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${allMatched.length} containers`, callback_data: `bexec:${action}:${names}:${timestamp}` },\n { text: 'Cancel', callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n hasMatched: true,\n matchedContainers: allMatched\n }\n };\n} else {\n // No containers matched at all\n return {\n json: {\n chat_id: chatId,\n text: text + 'No valid containers to ' + action + '.',\n parse_mode: 'HTML',\n hasMatched: false\n }\n };\n}" - }, - "id": "code-build-not-found", - "name": "Build Not Found Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - -100 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}", - "options": {} - }, - "id": "http-send-not-found", - "name": "Send Not Found Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - -100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "batch-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "batch-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "batch-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - }, - { - "id": "batch-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-action", - "name": "Route Batch Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2220, - -300 - ] - }, - { - "parameters": { - "jsCode": "// Build batch stop confirmation message\n// Per context: Batch stop confirms due to fuzzy matching risk\nconst data = $json;\nconst allMatched = data.allMatched;\nconst chatId = data.chatId;\n\nconst count = allMatched.length;\nconst names = allMatched.map(c => c.Name);\nconst namesStr = names.join(',');\nconst timestamp = Math.floor(Date.now() / 1000);\n\n// Build confirmation message\nlet text = `Stop ${count} container${count > 1 ? 's' : ''}?\\n\\n`;\nfor (const name of names) {\n text += `\\u2022 ${name}\\n`;\n}\n\n// Callback format: bstop:confirm:{comma-separated-names}:{timestamp}\nconst confirmCallback = `bstop:confirm:${namesStr}:${timestamp}`;\nconst cancelCallback = 'bstop:cancel';\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: 'Confirm', callback_data: confirmCallback },\n { text: 'Cancel', callback_data: cancelCallback }\n ]\n ]\n }\n }\n};" - }, - "id": "code-build-batch-stop-confirm", - "name": "Build Batch Stop Confirmation", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-confirm", - "name": "Send Batch Stop Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -200 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $(\"Parse Callback Data\").item.json.queryId }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-confirm", - "name": "Answer Batch Stop Confirm", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 700 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: 'Cancelled' }) }}", - "options": {} - }, - "id": "http-answer-batch-stop-cancel", - "name": "Answer Batch Stop Cancel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1340, - 800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId }) }}", - "options": {} - }, - "id": "http-delete-batch-stop-cancel", - "name": "Delete Batch Stop Cancel Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1560, - 800 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "batch-stop-expired", - "leftValue": "={{ $(\"Parse Callback Data\").item.json.expired }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-stop-expired", - "name": "Check Batch Stop Expired", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 1560, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Handle expired batch stop confirmation\nconst data = $json;\nreturn {\n json: {\n chat_id: data.chatId,\n message_id: data.messageId,\n text: 'Confirmation expired. Please try again.',\n parse_mode: 'HTML'\n }\n};" - }, - "id": "code-batch-stop-expired", - "name": "Build Batch Stop Expired", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify($json) }}", - "options": {} - }, - "id": "http-send-batch-stop-expired", - "name": "Send Batch Stop Expired", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Initialize batch state for execution\n// Input comes from Route Batch Action OR batch callbacks (bexec, bstop confirmed)\nconst data = $json;\n\n// Handle different input sources\nlet containers, action, chatId, messageId, fromKeyboard;\n\nif (data.allMatched) {\n // From Route Batch Action or Prepare Batch Exec\n containers = data.allMatched;\n action = data.action;\n chatId = data.chatId;\n messageId = data.messageId || null;\n // Check if fromKeyboard was set by caller (e.g., Prepare Batch Exec for inline keyboard)\n fromKeyboard = data.fromKeyboard || false;\n} else if (data.containerNames) {\n // From batch callback (bexec or bstop:confirm) - keyboard flow\n // Need to resolve names to container objects\n containers = data.containerNames.map(name => ({ Name: name, Id: null }));\n action = data.batchAction || 'stop';\n chatId = data.chatId;\n messageId = data.messageId;\n fromKeyboard = data.fromKeyboard !== false; // Default true for callbacks\n} else {\n throw new Error('Invalid batch state input');\n}\n\nreturn {\n json: {\n containers: containers,\n action: action,\n totalCount: containers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: messageId,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-init-batch-state", - "name": "Initialize Batch State", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: 'Batch ' + $json.action + '\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' }) }}", - "options": {} - }, - "id": "http-send-batch-start", - "name": "Send Batch Start Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2660, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Store progress message ID and prepare first iteration\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n throw new Error('Failed to get Initialize Batch State: ' + e.message);\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Use the original messageId from Initialize Batch State\n// (we edit the batch select message in place)\nconst progressMessageId = batchState.messageId || null;\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-loop", - "name": "Prepare Batch Loop", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Build progress message for current container\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\n\nconst containerName = container.Name || container;\nconst current = currentIndex + 1;\n\nlet progressText = `Batch ${action} in progress...\\n\\n`;\nprogressText += `Current: ${containerName}\\n`;\nprogressText += `Progress: ${current}/${totalCount}\\n\\n`;\n\nif (successCount > 0 || failureCount > 0) {\n progressText += `Completed: ${successCount} success`;\n if (failureCount > 0) {\n progressText += `, ${failureCount} failed`;\n }\n}\n\nreturn {\n json: {\n ...data,\n progressText: progressText,\n containerName: containerName\n }\n};" - }, - "id": "code-build-progress", - "name": "Build Progress Message", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3320, - -500 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.progressMessageId, text: $json.progressText, parse_mode: 'HTML' }) }}", - "options": { - "response": { - "response": { - "neverError": true - } - } - } - }, - "id": "http-edit-progress", - "name": "Edit Progress Message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3540, - -500 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "loop-update", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-update", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "update", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "update" - }, - { - "id": "loop-start", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-start", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "start", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "start" - }, - { - "id": "loop-stop", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-stop", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "stop", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "stop" - }, - { - "id": "loop-restart", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-restart", - "leftValue": "={{ $(\"Build Progress Message\").item.json.action }}", - "rightValue": "restart", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "restart" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-loop-action", - "name": "Route Batch Loop Action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 3760, - -500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare data for next iteration or completion\nconst data = $json;\n\n// Check if more containers to process\nconst currentIndex = data.currentIndex;\nconst totalCount = data.totalCount;\nconst containers = data.containers;\nconst nextIndex = currentIndex + 1;\nconst isComplete = nextIndex >= totalCount;\n\nreturn {\n json: {\n ...data,\n currentIndex: nextIndex,\n container: isComplete ? null : containers[nextIndex],\n isComplete: isComplete\n }\n};" - }, - "id": "code-prepare-next-iteration", - "name": "Prepare Next Iteration", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5520, - -400 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "is-batch-complete", - "leftValue": "={{ $json.isComplete }}", - "rightValue": true, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-batch-complete", - "name": "Is Batch Complete", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 5620, - -400 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch stop data for execution\n// Input from Parse Callback Data (via Check Batch Stop Expired)\nconst data = $(\"Parse Callback Data\").item.json;\n\n// containerNames is an array from callback\nconst containerNames = data.containerNames || [];\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: data.fromKeyboard || false\n }\n};" - }, - "id": "code-prepare-batch-stop-exec", - "name": "Prepare Batch Stop Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 700 - ] - }, - { - "parameters": { - "jsCode": "// Prepare batch exec data for execution\n// Input from Route Batch UI Result (sub-workflow output) or existing batch path\nconst data = $json;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// containerNames can be array or comma-separated string\nlet containerNames = data.containerNames || data.containers || [];\nif (typeof containerNames === 'string') {\n containerNames = containerNames.split(',').filter(n => n);\n}\nif (Array.isArray(containerNames) && containerNames[0] && containerNames[0].name) {\n containerNames = containerNames.map(c => c.name);\n}\n\n// action field might be 'action' or 'batchAction'\nconst action = data.batchAction || data.action || 'start';\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: action,\n chatId: data.chatId,\n messageId: data.messageId,\n fromKeyboard: fromKeyboard\n }\n};" - }, - "id": "code-prepare-batch-exec", - "name": "Prepare Batch Exec", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `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};" - }, - "id": "code-build-batch-summary", - "name": "Build Batch Summary", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5740, - -400 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', ...$json.reply_markup ? { reply_markup: $json.reply_markup } : {} }) }}", - "options": {} - }, - "id": "http-send-batch-summary", - "name": "Send Batch Summary", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 5960, - -400 - ] - }, - { - "parameters": { - "url": "http://docker-socket-proxy:2375/containers/json?all=false", - "options": {} - }, - "id": "http-get-all-containers-update-all", - "name": "Get All Containers For Update All", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1200, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\nconst chatId = $('Keyword Router').first().json.message.chat.id;\nconst messageId = $('Keyword Router').first().json.message.message_id;\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to containers using :latest tag\n// For now, we'll mark all :latest containers as \"potentially need update\"\n// A full implementation would pull each image, but that's expensive\n// We'll show all :latest containers and let batch execution handle the actual check\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = c.Image || '';\n // Check if using :latest or no tag specified (defaults to :latest)\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n allContainers: allContainers\n }\n};" - }, - "id": "code-check-available-updates", - "name": "Check Available Updates", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1400, - 2200 - ] - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict" - }, - "conditions": [ - { - "id": "has-containers", - "leftValue": "={{ $json.count }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "id": "if-has-updates-available", - "name": "Has Updates Available", - "type": "n8n-nodes-base.if", - "typeVersion": 2.1, - "position": [ - 1600, - 2200 - ] - }, - { - "parameters": { - "jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\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": { - "jsCode": "// Prepare data to return to container list\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: 0\n }\n};" - }, - "id": "code-prepare-batch-cancel-return", - "name": "Prepare Batch Cancel Return", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2200, - 5000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (text mode)\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n containerId,\n containerName,\n chatId,\n messageId: 0, // No message to edit in text mode\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-update", - "name": "Prepare Text Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1200 - ] - }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Check Update Match Count').item.json.chatId }}", - "text": "=Updating {{ $('Check Update Match Count').item.json.matches[0].Name }}...", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-text-update-started", - "name": "Send Text Update Started", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 1400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-text-update-subworkflow", - "name": "Execute Text Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container-update sub-workflow (inline mode)\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n containerId: '', // Will be resolved by sub-workflow from name\n containerName,\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-callback-update", - "name": "Prepare Callback Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 1650 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: '\\u2B06\\uFE0F Updating ' + $json.containerName + '...\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.', parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } }) }}", - "options": {} - }, - "id": "http-callback-update-progress", - "name": "Show Callback Update Progress", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2220, - 1650 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-callback", - "name": "Get Container For Callback Update", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Find container ID from name for callback update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Callback Update Input').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-find-container-callback", - "name": "Find Container For Callback Update", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-callback-update-subworkflow", - "name": "Execute Callback Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2880, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst containerName = data.matches[0].Name;\nconst action = data.action;\nconst chatId = data.chatId;\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId,\n messageId: 0, // Text mode doesn't have a message to edit\n responseMode: 'text'\n }\n};" - }, - "id": "code-prepare-text-action-rr53pd94", - "name": "Prepare Text Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-container-action-qokchnw8", - "name": "Execute Container Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for text command path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst chatId = result.chatId;\n\nreturn {\n json: {\n success: success,\n chatId: chatId,\n text: message\n }\n};" - }, - "id": "code-handle-text-result-c6ha90fh", - "name": "Handle Text Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow from inline keyboard\n// Container lookup already done by Get Container For Action\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: action,\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-inline-action-tyjn5pb1", - "name": "Prepare Inline Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1200 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-inline-action-8aoev7xt", - "name": "Execute Inline Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2440, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for inline keyboard path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst action = result.action;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'action:' + action + ':' + containerName }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-inline-result-x19h97t3", - "name": "Handle Inline Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for container actions sub-workflow from confirmed stop\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: 'Container not found'\n }\n };\n}\n\nreturn {\n json: {\n containerId: container.Id,\n containerName: normalizeName(container.Names[0]),\n action: 'stop',\n chatId: chatId,\n messageId: messageId,\n responseMode: 'inline'\n }\n};" - }, - "id": "code-prepare-confirmed-stop-vt9cw9tl", - "name": "Prepare Confirmed Stop Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2440, - 1650 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-confirmed-stop-sub-qmm011fk", - "name": "Execute Confirmed Stop Action", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2660, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Handle sub-workflow result for confirmed stop path\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: 'confirm:stop:' + containerName + ':' + timestamp }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" - }, - "id": "code-handle-confirmed-stop-f2r86fwr", - "name": "Handle Confirmed Stop Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2880, - 1650 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65", - "name": "Prepare Batch Update Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -800 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "7AvTzLtKXM2hZTio92_mC" - }, - "options": {} - }, - "id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c", - "name": "Execute Batch Update", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Handle update result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: 'update',\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "f5576e75-8cfa-4ac3-9fea-64fae8d31bec", - "name": "Handle Batch Update Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -800 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Actions sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\nconst action = data.action;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\"\n }\n};" - }, - "id": "958f19ef-249b-42ca-8a29-ecb91548f1dd", - "name": "Prepare Batch Action Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4000, - -200 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "fYSZS5PkH0VSEaT5" - }, - "options": {} - }, - "id": "3baebdc9-3cda-478a-b0cc-0fb33a542f03", - "name": "Execute Batch Action Sub-workflow", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 4220, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Handle action result from sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst result = $json;\n\n// Update counters based on result\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\nif (result.success) {\n successCount++;\n} else {\n failureCount++;\n}\n\n// Add to results array\nconst results = data.results || [];\nresults.push({\n container: data.containerName,\n action: data.action,\n success: result.success,\n message: result.message || ''\n});\n\nreturn {\n json: {\n ...data,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount,\n results: results\n }\n};" - }, - "id": "dc8db8cb-3ba8-471d-98df-c47dcdb4e6ea", - "name": "Handle Batch Action Result Sub", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 4440, - -200 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (text command)\nconst data = $json;\n\n// Check if there's an error from Parse Logs Command\nif (data.error) {\n return {\n json: {\n error: true,\n chatId: data.chatId,\n text: data.text\n }\n };\n}\n\nreturn {\n json: {\n containerName: data.containerQuery,\n lineCount: data.lines,\n chatId: data.chatId,\n messageId: data.messageId || 0,\n responseMode: \"text\"\n }\n};" - }, - "id": "a895bb2d-1f61-4466-b475-b32ec5f0e83a", - "name": "Prepare Text Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 600 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "926c7683-c0e4-41a4-a983-e3f7ecb6ff41", - "name": "Execute Text Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Logs sub-workflow (inline action)\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n containerName: data.containerName,\n lineCount: 30,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: \"inline\"\n }\n};" - }, - "id": "16b24086-5b5d-4980-82c7-4fb37b4e8f6c", - "name": "Prepare Inline Logs Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1300 - ] - }, - { - "parameters": { - "workflowId": { - "__rl": true, - "mode": "list", - "value": "oE7aO2GhbksXDEIw" - }, - "options": {} - }, - "id": "a88974bd-45c0-401e-b50a-c6171cfe06d4", - "name": "Execute Inline Logs", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 2000, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\nconst containerName = result.containerName;\n\n// Add timestamp to prevent 'message not modified' error on refresh\nconst timestamp = new Date().toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n});\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\n// Append timestamp to message\nconst messageWithTimestamp = result.message + `\\n\\nUpdated: ${timestamp}`;\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: messageWithTimestamp,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" - }, - "id": "b1800598-1ff6-4da3-8506-4e4e8127f902", - "name": "Format Inline Logs Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Batch UI sub-workflow\nconst data = $json;\n\n// Determine action from callback data\nlet action = 'mode'; // default\nconst callbackData = data.callbackData || '';\n\nif (data.isBatchMode) action = 'mode';\nelse if (data.isBatchToggle) action = 'toggle';\nelse if (data.isBatchNav) action = 'nav';\nelse if (data.isBatchExec) action = 'exec';\nelse if (data.isBatchClear) action = 'clear';\nelse if (data.isBatchCancel) action = 'cancel';\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n queryId: data.queryId,\n callbackData: callbackData,\n action: action,\n batchPage: data.batchPage || 0,\n selectedCsv: data.selectedCsv || '',\n toggleName: data.toggleName || ''\n }\n};" - }, - "id": "code-prepare-batch-ui-input", - "name": "Prepare Batch UI Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 3400 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_BATCH_UI_WORKFLOW" - }, - "mode": "once", - "options": { - "waitForSubWorkflow": true - } - }, - "id": "exec-batch-ui-subworkflow", - "name": "Execute Batch UI", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 3400 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-keyboard", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-keyboard", - "leftValue": "={{ $json.action }}", - "rightValue": "keyboard", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "keyboard" - }, - { - "id": "route-confirmation", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-confirmation", - "leftValue": "={{ $json.action }}", - "rightValue": "confirmation", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "confirmation" - }, - { - "id": "route-execute", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-execute", - "leftValue": "={{ $json.action }}", - "rightValue": "execute", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "execute" - }, - { - "id": "route-cancel", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-cancel", - "leftValue": "={{ $json.action }}", - "rightValue": "cancel", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "cancel" - }, - { - "id": "route-limit-reached", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-limit-reached", - "leftValue": "={{ $json.action }}", - "rightValue": "limit_reached", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "limit_reached" - } - ] - }, - "options": { - "fallbackOutput": "none" - } - }, - "id": "switch-route-batch-ui-result", - "name": "Route Batch UI Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 2000, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || '' }}" - }, - "id": "telegram-answer-batch-ui-keyboard", - "name": "Answer Batch UI Keyboard", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3200 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-keyboard", - "name": "Edit Batch UI Keyboard", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3200 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Confirm...' }}" - }, - "id": "telegram-answer-batch-ui-confirm", - "name": "Answer Batch UI Confirm", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3400 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $('Route Batch UI Result').item.json.chatId, message_id: $('Route Batch UI Result').item.json.messageId, text: $('Route Batch UI Result').item.json.text, parse_mode: 'HTML', reply_markup: $('Route Batch UI Result').item.json.keyboard }) }}", - "options": {} - }, - "id": "http-edit-batch-ui-confirm", - "name": "Edit Batch UI Confirmation", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 3400 - ] - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Maximum selection reached' }}", - "options": { - "showAlert": true - } - }, - "id": "telegram-answer-batch-ui-limit", - "name": "Answer Batch UI Limit", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "resource": "callback", - "operation": "answerQuery", - "queryId": "={{ $json.queryId }}", - "text": "={{ $json.answerText || 'Batch selection cancelled' }}" - }, - "id": "telegram-answer-batch-ui-cancel", - "name": "Answer Batch UI Cancel", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2220, - 3600 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from /status command\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\nconst text = (message.text || '').toLowerCase().trim();\n\n// Check if user specified a container name (e.g., \"/status plex\")\nlet searchTerm = null;\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n searchTerm = parts.filter(p => p !== 'status' && p !== '/status').join(' ');\n}\n\nreturn {\n json: {\n chatId: chatId,\n messageId: 0,\n action: 'list',\n containerId: null,\n containerName: null,\n page: 0,\n queryId: null,\n searchTerm: searchTerm\n }\n};" - }, - "id": "code-prepare-status-input", - "name": "Prepare Status Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1120, - 100 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-container-status", - "name": "Execute Container Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1340, - 100 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "route-list-result", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-list", - "leftValue": "={{ $json.action }}", - "rightValue": "list", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "list" - }, - { - "id": "route-status-direct", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "is-status-direct", - "leftValue": "={{ $json.action }}", - "rightValue": "status_direct", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "status_direct" - }, - { - "id": "route-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "has-error", - "leftValue": "={{ $json.success }}", - "rightValue": false, - "operator": { - "type": "boolean", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "error" - } - ] - }, - "options": { - "fallbackOutput": "extra" - } - }, - "id": "switch-route-status-result", - "name": "Route Status Result", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 100 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from select callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'status',\n containerId: null,\n containerName: data.containerName,\n page: 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-select-input", - "name": "Prepare Select Status Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 900 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-select-status", - "name": "Execute Select Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1560, - 900 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from list pagination callback\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-paginate-input", - "name": "Prepare Paginate Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 1000 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-paginate-status", - "name": "Execute Paginate Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1560, - 1000 - ] - }, - { - "parameters": { - "jsCode": "// Prepare input for Container Status sub-workflow from batch cancel return\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n action: 'paginate',\n containerId: null,\n containerName: null,\n page: data.page || 0,\n queryId: data.queryId || null,\n searchTerm: null\n }\n};" - }, - "id": "code-prepare-batch-cancel-input", - "name": "Prepare Batch Cancel Return Input", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 1100 - ] - }, - { - "parameters": { - "source": "database", - "workflowId": { - "__rl": true, - "mode": "id", - "value": "TODO_DEPLOY_STATUS_WORKFLOW" - }, - "options": { - "waitForSubWorkflow": true - } - }, - "id": "execute-batch-cancel-status", - "name": "Execute Batch Cancel Status", - "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.2, - "position": [ - 1780, - 1100 - ] - } - ], - "connections": { - "Telegram Trigger": { - "main": [ - [ - { - "node": "Route Update Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Update Type": { - "main": [ - [ - { - "node": "IF User Authenticated", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "IF Callback Authenticated", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF User Authenticated": { - "main": [ - [ - { - "node": "Keyword Router", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF Callback Authenticated": { - "main": [ - [ - { - "node": "Parse Callback Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Data": { - "main": [ - [ - { - "node": "Route Callback", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Callback": { - "main": [ - [ - { - "node": "Check Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Update All Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Select Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer List Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Action Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Noop Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Confirm Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Cancel Confirm Callback", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch Stop Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Callback Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch UI Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Cancel": { - "main": [ - [ - { - "node": "Answer Cancel Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Cancel Query": { - "main": [ - [ - { - "node": "Delete Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Expired": { - "main": [ - [ - { - "node": "Answer Expired Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Expired Query": { - "main": [ - [ - { - "node": "Delete Expired Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Callback Action": { - "main": [ - [ - { - "node": "Execute Callback Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Callback Action": { - "main": [ - [ - { - "node": "Parse Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Callback Result": { - "main": [ - [ - { - "node": "Answer Action Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Query": { - "main": [ - [ - { - "node": "Delete Suggestion Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Suggestion Message": { - "main": [ - [ - { - "node": "Send Callback Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Action": { - "main": [ - [ - { - "node": "Match Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Container": { - "main": [ - [ - { - "node": "Check Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Match Count": { - "main": [ - [ - { - "node": "Send Docker Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Find Closest Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Closest Match": { - "main": [ - [ - { - "node": "Check Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Suggestion": { - "main": [ - [ - { - "node": "Build Suggestion Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send No Match", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Suggestion Keyboard": { - "main": [ - [ - { - "node": "Send Suggestion", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Action Command": { - "main": [ - [ - { - "node": "Execute Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Action": { - "main": [ - [ - { - "node": "Parse Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Keyboard": { - "main": [ - [ - { - "node": "Send Batch Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Update Command": { - "main": [ - [ - { - "node": "Docker List for Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Update": { - "main": [ - [ - { - "node": "Match Update Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Update Container": { - "main": [ - [ - { - "node": "Check Update Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update Match Count": { - "main": [ - [ - { - "node": "Send Update Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Update No Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Text Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Handle Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Update Multiple": { - "main": [ - [ - { - "node": "Send Update Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Logs Command": { - "main": [ - [ - { - "node": "Prepare Text Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Keyword Router": { - "main": [ - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Status Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get All Containers For Update All", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Detect Batch Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Logs Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Show Menu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Detect Batch Command": { - "main": [ - [ - { - "node": "Is Batch Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Command": { - "main": [ - [ - { - "node": "Get Containers for Batch", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Single Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Single Action": { - "main": [ - [ - { - "node": "Parse Action Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Parse Update Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Containers for Batch": { - "main": [ - [ - { - "node": "Match Batch Containers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Batch Containers": { - "main": [ - [ - { - "node": "Needs Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Needs Disambiguation": { - "main": [ - [ - { - "node": "Build Disambiguation Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Has Not Found", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Disambiguation Message": { - "main": [ - [ - { - "node": "Send Disambiguation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Not Found": { - "main": [ - [ - { - "node": "Build Not Found Message", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Batch Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Not Found Message": { - "main": [ - [ - { - "node": "Send Not Found Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Action": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Confirmation": { - "main": [ - [ - { - "node": "Send Batch Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Confirm": { - "main": [ - [ - { - "node": "Check Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Batch Stop Expired": { - "main": [ - [ - { - "node": "Build Batch Stop Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Stop Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Stop Expired": { - "main": [ - [ - { - "node": "Send Batch Stop Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Stop Cancel": { - "main": [ - [ - { - "node": "Delete Batch Stop Cancel Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Action Command": { - "main": [ - [ - { - "node": "Docker List for Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Action Callback": { - "main": [ - [ - { - "node": "Route Action Type", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Action Type": { - "main": [ - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Immediate Action", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Stop Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Update Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Inline Logs Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Immediate Action": { - "main": [ - [ - { - "node": "Get Container For Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Action": { - "main": [ - [ - { - "node": "Prepare Inline Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Immediate Action Command": { - "main": [ - [ - { - "node": "Execute Immediate Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Immediate Action": { - "main": [ - [ - { - "node": "Format Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Immediate Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Stop Confirmation": { - "main": [ - [ - { - "node": "Send Stop Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update Confirmation": { - "main": [ - [ - { - "node": "Send Update Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Confirm Callback": { - "main": [ - [ - { - "node": "Check Confirm Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Confirm Expired": { - "main": [ - [ - { - "node": "Handle Confirm Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Route Confirm Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Confirm Expired": { - "main": [ - [ - { - "node": "Send Expired Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Confirm Action": { - "main": [ - [ - { - "node": "Prepare Confirmed Stop", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Callback Update Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Confirmed Stop": { - "main": [ - [ - { - "node": "Get Container For Stop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Stop": { - "main": [ - [ - { - "node": "Prepare Confirmed Stop Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Confirmed Stop Command": { - "main": [ - [ - { - "node": "Execute Confirmed Stop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Confirmed Stop": { - "main": [ - [ - { - "node": "Format Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Confirmed Stop Result": { - "main": [ - [ - { - "node": "Send Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Cancel Confirm Callback": { - "main": [ - [ - { - "node": "Prepare Cancel Return", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Cancel Return": { - "main": [ - [ - { - "node": "Get Container For Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Cancel": { - "main": [ - [ - { - "node": "Build Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Cancel Return Submenu": { - "main": [ - [ - { - "node": "Send Cancel Return Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Initialize Batch State": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send Batch Start Message": { - "main": [ - [ - { - "node": "Prepare Batch Loop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Loop": { - "main": [ - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Progress Message": { - "main": [ - [ - { - "node": "Edit Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Edit Progress Message": { - "main": [ - [ - { - "node": "Route Batch Loop Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch Loop Action": { - "main": [ - [ - { - "node": "Prepare Batch Update Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Action Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Next Iteration": { - "main": [ - [ - { - "node": "Is Batch Complete", - "type": "main", - "index": 0 - } - ] - ] - }, - "Is Batch Complete": { - "main": [ - [ - { - "node": "Build Batch Summary", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Progress Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Batch Summary": { - "main": [ - [ - { - "node": "Send Batch Summary", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Stop Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Exec": { - "main": [ - [ - { - "node": "Initialize Batch State", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get All Containers For Update All": { - "main": [ - [ - { - "node": "Check Available Updates", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Available Updates": { - "main": [ - [ - { - "node": "Has Updates Available", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Updates Available": { - "main": [ - [ - { - "node": "Build Update All Confirmation", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send All Up To Date", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Update All Confirmation": { - "main": [ - [ - { - "node": "Send Update All Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Update All Expired": { - "main": [ - [ - { - "node": "Answer Update All Expired", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Get Update All Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Expired": { - "main": [ - [ - { - "node": "Delete Update All Expired", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Cancel": { - "main": [ - [ - { - "node": "Delete Update All Cancel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Update All Data": { - "main": [ - [ - { - "node": "Answer Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Update All Confirm": { - "main": [ - [ - { - "node": "Delete Update All Confirm", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Update All Confirm": { - "main": [ - [ - { - "node": "Fetch Containers For Update All Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "Fetch Containers For Update All Exec": { - "main": [ - [ - { - "node": "Prepare Update All Batch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Update All Batch": { - "main": [ - [ - { - "node": "Send Batch Start Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Update Input": { - "main": [ - [ - { - "node": "Execute Text Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Callback Update Input": { - "main": [ - [ - { - "node": "Show Callback Update Progress", - "type": "main", - "index": 0 - } - ] - ] - }, - "Show Callback Update Progress": { - "main": [ - [ - { - "node": "Get Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Callback Update": { - "main": [ - [ - { - "node": "Find Container For Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Find Container For Callback Update": { - "main": [ - [ - { - "node": "Execute Callback Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Action Input": { - "main": [ - [ - { - "node": "Execute Container Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Action": { - "main": [ - [ - { - "node": "Handle Text Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Text Action Result": { - "main": [ - [ - { - "node": "Send Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Action Input": { - "main": [ - [ - { - "node": "Execute Inline Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Action": { - "main": [ - [ - { - "node": "Handle Inline Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Inline Action Result": { - "main": [ - [ - { - "node": "Send Immediate Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Confirmed Stop Input": { - "main": [ - [ - { - "node": "Execute Confirmed Stop Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Confirmed Stop Action": { - "main": [ - [ - { - "node": "Handle Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Confirmed Stop Result": { - "main": [ - [ - { - "node": "Send Confirmed Stop Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Update Input": { - "main": [ - [ - { - "node": "Execute Batch Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Update": { - "main": [ - [ - { - "node": "Handle Batch Update Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Update Result": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Action Input": { - "main": [ - [ - { - "node": "Execute Batch Action Sub-workflow", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Action Sub-workflow": { - "main": [ - [ - { - "node": "Handle Batch Action Result Sub", - "type": "main", - "index": 0 - } - ] - ] - }, - "Handle Batch Action Result Sub": { - "main": [ - [ - { - "node": "Prepare Next Iteration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Text Logs Input": { - "main": [ - [ - { - "node": "Execute Text Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Text Logs": { - "main": [ - [ - { - "node": "Send Logs Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Inline Logs Input": { - "main": [ - [ - { - "node": "Execute Inline Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Inline Logs": { - "main": [ - [ - { - "node": "Format Inline Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Inline Logs Result": { - "main": [ - [ - { - "node": "Send Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch UI Input": { - "main": [ - [ - { - "node": "Execute Batch UI", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch UI": { - "main": [ - [ - { - "node": "Route Batch UI Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Batch UI Result": { - "main": [ - [ - { - "node": "Answer Batch UI Keyboard", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Confirm", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Prepare Batch Exec", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Cancel", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Answer Batch UI Limit", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Keyboard": { - "main": [ - [ - { - "node": "Edit Batch UI Keyboard", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Confirm": { - "main": [ - [ - { - "node": "Edit Batch UI Confirmation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch UI Cancel": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Status Input": { - "main": [ - [ - { - "node": "Execute Container Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Container Status": { - "main": [ - [ - { - "node": "Route Status Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Route Status Result": { - "list": [ - [ - { - "node": "Send Container List", - "type": "main", - "index": 0 - } - ] - ], - "status_direct": [ - [ - { - "node": "Send Container Submenu Direct", - "type": "main", - "index": 0 - } - ] - ], - "error": [ - [ - { - "node": "Send Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Select Callback": { - "main": [ - [ - { - "node": "Prepare Select Status Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Select Status Input": { - "main": [ - [ - { - "node": "Execute Select Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Select Status": { - "main": [ - [ - { - "node": "Send Container Submenu", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer List Callback": { - "main": [ - [ - { - "node": "Prepare Paginate Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Paginate Input": { - "main": [ - [ - { - "node": "Execute Paginate Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Paginate Status": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return": { - "main": [ - [ - { - "node": "Prepare Batch Cancel Return Input", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Cancel Return Input": { - "main": [ - [ - { - "node": "Execute Batch Cancel Status", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Cancel Status": { - "main": [ - [ - { - "node": "Edit Container List", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "pinData": {}, - "settings": { - "executionOrder": "v1" - }, - "staticData": null, - "tags": [], - "triggerCount": 1, - "active": false -} \ No newline at end of file