From 216f3a406a44a978e2b6770ca95c8f904ad4b8a9 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Mon, 9 Feb 2026 11:29:40 -0500 Subject: [PATCH] fix(16): repair broken connections, auth credentials, and dead code across 4 workflows Phase 16 plans 16-02 through 16-05 introduced three classes of defects: 1. Connection keys used node IDs instead of node names (33 broken links across n8n-workflow.json, n8n-batch-ui.json, n8n-actions.json) 2. GraphQL HTTP nodes used $env.UNRAID_API_KEY manual headers instead of Header Auth credential, causing CSRF/UNAUTHENTICATED errors (20 nodes) 3. Duplicate node name "Execute Batch Update" (serial vs parallel paths) Also fixes Build Cancel Return Submenu using $input.item.json instead of $('Prepare Cancel From Confirm').item.json after GraphQL query chain. Removes 12 dead/orphan nodes (6 pre-migration dead code chains, 6 unused utility templates). Node count: 193 -> 181. Co-Authored-By: Claude Opus 4.6 --- n8n-actions.json | 84 +++++--- n8n-batch-ui.json | 215 ++++++--------------- n8n-update.json | 53 ++--- n8n-workflow.json | 481 +++++++++++----------------------------------- 4 files changed, 259 insertions(+), 574 deletions(-) diff --git a/n8n-actions.json b/n8n-actions.json index 7f941be..a5e45da 100644 --- a/n8n-actions.json +++ b/n8n-actions.json @@ -88,10 +88,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -100,7 +96,9 @@ "jsonBody": "={\"query\": \"query { docker { containers { id names state image } } }\"}", "options": { "timeout": 15000 - } + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" }, "id": "http-get-containers", "name": "Query All Containers", @@ -110,7 +108,13 @@ 600, 400 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -223,10 +227,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -235,7 +235,9 @@ "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 - } + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" }, "id": "http-start-container", "name": "Start Container", @@ -245,7 +247,13 @@ 1160, 200 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -257,10 +265,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -269,7 +273,9 @@ "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 - } + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" }, "id": "http-stop-container", "name": "Stop Container", @@ -279,7 +285,13 @@ 1160, 300 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -291,10 +303,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -303,7 +311,9 @@ "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 - } + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" }, "id": "http-stop-for-restart", "name": "Stop For Restart", @@ -313,7 +323,13 @@ 1160, 400 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -469,10 +485,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -481,7 +493,9 @@ "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 - } + }, + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth" }, "id": "http-start-after-stop", "name": "Start After Stop", @@ -491,7 +505,13 @@ 1480, 400 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -530,7 +550,7 @@ ], [ { - "node": "Get All Containers", + "node": "Query All Containers", "type": "main", "index": 0 } @@ -732,4 +752,4 @@ "executionOrder": "v1", "callerPolicy": "any" } -} \ No newline at end of file +} diff --git a/n8n-batch-ui.json b/n8n-batch-ui.json index 75b7bc9..d1307f4 100644 --- a/n8n-batch-ui.json +++ b/n8n-batch-ui.json @@ -220,7 +220,7 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendBody": true, "specifyBody": "json", "jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}", @@ -230,10 +230,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -244,7 +240,8 @@ "errorHandling": "continueRegularOutput" } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-mode", "name": "Fetch Containers For Mode", @@ -253,7 +250,13 @@ "position": [ 680, 100 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -316,7 +319,7 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendBody": true, "specifyBody": "json", "jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}", @@ -326,10 +329,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -340,7 +339,8 @@ "errorHandling": "continueRegularOutput" } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-toggle", "name": "Fetch Containers For Update", @@ -349,7 +349,13 @@ "position": [ 1120, 100 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -368,7 +374,7 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendBody": true, "specifyBody": "json", "jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}", @@ -378,10 +384,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -392,7 +394,8 @@ "errorHandling": "continueRegularOutput" } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-exec", "name": "Fetch Containers For Exec", @@ -401,7 +404,13 @@ "position": [ 680, 400 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -433,7 +442,7 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendBody": true, "specifyBody": "json", "jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}", @@ -443,10 +452,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -457,7 +462,8 @@ "errorHandling": "continueRegularOutput" } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-nav", "name": "Fetch Containers For Nav", @@ -466,7 +472,13 @@ "position": [ 900, 300 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -498,7 +510,7 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendBody": true, "specifyBody": "json", "jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}", @@ -508,10 +520,6 @@ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -522,7 +530,8 @@ "errorHandling": "continueRegularOutput" } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-clear", "name": "Fetch Containers For Clear", @@ -531,7 +540,13 @@ "position": [ 900, 500 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -687,7 +702,7 @@ "main": [ [ { - "node": "Build Batch Keyboard", + "node": "Normalize GraphQL Response (Mode)", "type": "main", "index": 0 } @@ -720,7 +735,7 @@ "main": [ [ { - "node": "Rebuild Keyboard After Toggle", + "node": "Normalize GraphQL Response (Toggle)", "type": "main", "index": 0 } @@ -731,7 +746,7 @@ "main": [ [ { - "node": "Handle Exec", + "node": "Normalize GraphQL Response (Exec)", "type": "main", "index": 0 } @@ -753,7 +768,7 @@ "main": [ [ { - "node": "Rebuild Keyboard For Nav", + "node": "Normalize GraphQL Response (Nav)", "type": "main", "index": 0 } @@ -775,25 +790,14 @@ "main": [ [ { - "node": "Rebuild Keyboard After Clear", + "node": "Normalize GraphQL Response (Clear)", "type": "main", "index": 0 } ] ] }, - "http-fetch-containers-mode": { - "main": [ - [ - { - "node": "Normalize GraphQL Response (Mode)", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-normalizer-mode": { + "Normalize GraphQL Response (Mode)": { "main": [ [ { @@ -804,18 +808,7 @@ ] ] }, - "http-fetch-containers-toggle": { - "main": [ - [ - { - "node": "Normalize GraphQL Response (Toggle)", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-normalizer-toggle": { + "Normalize GraphQL Response (Toggle)": { "main": [ [ { @@ -826,18 +819,7 @@ ] ] }, - "http-fetch-containers-exec": { - "main": [ - [ - { - "node": "Normalize GraphQL Response (Exec)", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-normalizer-exec": { + "Normalize GraphQL Response (Exec)": { "main": [ [ { @@ -848,18 +830,7 @@ ] ] }, - "http-fetch-containers-nav": { - "main": [ - [ - { - "node": "Normalize GraphQL Response (Nav)", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-normalizer-nav": { + "Normalize GraphQL Response (Nav)": { "main": [ [ { @@ -870,18 +841,7 @@ ] ] }, - "http-fetch-containers-clear": { - "main": [ - [ - { - "node": "Normalize GraphQL Response (Clear)", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-normalizer-clear": { + "Normalize GraphQL Response (Clear)": { "main": [ [ { @@ -891,63 +851,10 @@ } ] ] - }, - "switch-route-batch-action": { - "main": [ - [ - { - "node": "Fetch Containers For Mode", - "type": "main", - "index": 0 - } - ], - [], - [], - [ - { - "node": "Fetch Containers For Exec", - "type": "main", - "index": 0 - } - ] - ] - }, - "if-needs-keyboard-update": { - "main": [ - [ - { - "node": "Fetch Containers For Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-handle-nav": { - "main": [ - [ - { - "node": "Fetch Containers For Nav", - "type": "main", - "index": 0 - } - ] - ] - }, - "code-handle-clear": { - "main": [ - [ - { - "node": "Fetch Containers For Clear", - "type": "main", - "index": 0 - } - ] - ] } }, "settings": { "executionOrder": "v1", "callerPolicy": "any" } -} \ No newline at end of file +} diff --git a/n8n-update.json b/n8n-update.json index b3cef3e..1491055 100644 --- a/n8n-update.json +++ b/n8n-update.json @@ -77,17 +77,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -96,7 +92,8 @@ "jsonBody": "={\"query\": \"query { docker { containers { id names state image imageId } } }\"}", "options": { "timeout": 15000 - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-query-containers", "name": "Query All Containers", @@ -106,7 +103,13 @@ 560, 400 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -152,17 +155,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -171,7 +170,8 @@ "jsonBody": "={{ {\"query\": \"query { docker { containers(filter: { id: \\\"\" + $json.containerId + \"\\\" }) { id names state image imageId } } }\"} }}", "options": { "timeout": 15000 - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-query-single", "name": "Query Single Container", @@ -181,7 +181,13 @@ 560, 300 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -226,17 +232,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -245,7 +247,8 @@ "jsonBody": "={{ {\"query\": $json.query} }}", "options": { "timeout": 60000 - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-update-container", "name": "Update Container", @@ -255,7 +258,13 @@ 1320, 300 ], - "onError": "continueRegularOutput" + "onError": "continueRegularOutput", + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -1091,4 +1100,4 @@ "settings": { "executionOrder": "v1" } -} \ No newline at end of file +} diff --git a/n8n-workflow.json b/n8n-workflow.json index f6014e5..7d52165 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -429,46 +429,6 @@ 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", @@ -1804,17 +1764,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -1829,7 +1785,8 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-get-container-for-action", "name": "Get Container For Action", @@ -1838,47 +1795,13 @@ "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 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -1945,17 +1868,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -1970,7 +1889,8 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-get-container-for-cancel", "name": "Get Container For Cancel", @@ -1979,11 +1899,17 @@ "position": [ 1780, 1800 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { - "jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $input.item.json;\nconst searchName = prevData.containerName.toLowerCase();\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"running\") {\n keyboard.push([\n { text: \"\\u23F9\\uFE0F Stop\", callback_data: `action:stop:${containerName}` },\n { text: \"\\u{1F504} Restart\", callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: \"\\u25B6\\uFE0F Start\", callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: \"\\u{1F4CB} Logs\", callback_data: `action:logs:${containerName}` },\n { text: \"\\u2B06\\uFE0F Update\", callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }\n]);\n\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} ${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}];" + "jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Cancel From Confirm').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", @@ -2735,17 +2661,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -2760,7 +2682,8 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-get-all-containers-update-all", "name": "Get All Containers For Update All", @@ -2769,7 +2692,13 @@ "position": [ 1200, 2200 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -3052,17 +2981,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -3077,7 +3002,8 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-update-all-exec", "name": "Fetch Containers For Update All Exec", @@ -3086,7 +3012,13 @@ "position": [ 2200, 2600 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -3184,17 +3116,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -3209,7 +3137,8 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-get-container-callback", "name": "Get Container For Callback Update", @@ -3218,7 +3147,13 @@ "position": [ 2440, 1650 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -4888,17 +4823,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -4913,7 +4844,8 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-fetch-containers-bitmap-stop", "name": "Fetch Containers For Bitmap Stop", @@ -4922,7 +4854,13 @@ "position": [ 2000, 700 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -5035,115 +4973,6 @@ } } }, - { - "parameters": { - "jsCode": "// GraphQL Response Normalizer - Transform Unraid GraphQL to Docker API contract\n// Input: $input.item.json = raw GraphQL response\n// Output: Array of normalized containers\n\nconst response = $input.item.json;\n\n// Validation: Check for GraphQL errors\nif (response.errors && response.errors.length > 0) {\n const messages = response.errors.map(e => e.message).join('; ');\n throw new Error(`GraphQL error: ${messages}`);\n}\n\n// Validation: Check response structure\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure: missing data.docker.containers');\n}\n\n// State mapping: Unraid UPPERCASE -> Docker lowercase\nconst stateMap = {\n 'RUNNING': 'running',\n 'STOPPED': 'exited', // Docker convention: stopped = exited\n 'PAUSED': 'paused'\n};\n\nfunction normalizeState(unraidState) {\n return stateMap[unraidState] || unraidState.toLowerCase();\n}\n\n// Transform each container\nconst containers = response.data.docker.containers;\nconst normalized = containers.map(container => {\n const dockerState = normalizeState(container.state);\n \n return {\n // Core fields matching Docker API contract\n Id: container.id, // Keep full PrefixedID (registry handles translation)\n Names: container.names, // Already has '/' prefix (Phase 14 verified)\n State: dockerState, // Normalized lowercase state\n Status: dockerState, // Docker has separate Status field\n Image: '', // Not available in basic query\n \n // Debug field: preserve original Unraid ID\n _unraidId: container.id\n };\n});\n\n// Return as array of items (n8n multi-item output format)\nreturn normalized.map(container => ({ json: container }));\n" - }, - "id": "code-graphql-normalizer", - "name": "GraphQL Response Normalizer", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 200, - 2600 - ] - }, - { - "id": "code-container-id-registry", - "name": "Container ID Registry", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 200, - 2400 - ], - "parameters": { - "mode": "runOnceForAllItems", - "jsCode": "// Container ID Registry\n// Maps container names to Unraid PrefixedID format (129-char server_hash:container_hash)\n\n// Initialize registry using static data with JSON serialization pattern\n// CRITICAL: Must use top-level assignment per CLAUDE.md\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\nif (!registry._lastRefresh) {\n registry._lastRefresh = 0;\n}\n\nconst containerMap = JSON.parse(registry._containerIdMap);\n\n/**\n * Update registry with fresh container data from Unraid GraphQL API\n * @param {Array} containers - Array of Unraid container objects {id, names[], state}\n * @returns {Object} Updated container map\n */\nfunction updateRegistry(containers) {\n const newMap = {};\n \n for (const container of containers) {\n // Extract container name: strip leading '/', lowercase\n const rawName = container.names[0];\n const name = rawName.startsWith('/') ? rawName.substring(1).toLowerCase() : rawName.toLowerCase();\n \n // Map name -> {name, unraidId}\n // The Unraid 'id' field IS the PrefixedID (129-char format)\n newMap[name] = {\n name: name,\n unraidId: container.id\n };\n }\n \n // Store timestamp\n registry._lastRefresh = Date.now();\n \n // Serialize (top-level assignment - this is what n8n persists)\n registry._containerIdMap = JSON.stringify(newMap);\n \n return newMap;\n}\n\n/**\n * Get Unraid PrefixedID for a container name\n * @param {string} containerName - Container name (e.g., 'plex', '/plex')\n * @returns {string} Unraid PrefixedID (129-char)\n * @throws {Error} If container not found\n */\nfunction getUnraidId(containerName) {\n // Normalize: strip leading '/', lowercase\n const name = containerName.startsWith('/') \n ? containerName.substring(1).toLowerCase() \n : containerName.toLowerCase();\n \n const entry = containerMap[name];\n \n if (!entry) {\n const registryAge = Date.now() - registry._lastRefresh;\n const isStale = registryAge > 60000; // 60 seconds\n \n if (isStale) {\n throw new Error(`Container \"${name}\" not found. Registry may be stale - try \"status\" to refresh.`);\n } else {\n throw new Error(`Container \"${name}\" not found. Check spelling or run \"status\" to see all containers.`);\n }\n }\n \n return entry.unraidId;\n}\n\n/**\n * Get full container entry by name\n * @param {string} containerName - Container name\n * @returns {Object} Container entry {name, unraidId}\n * @throws {Error} If container not found\n */\nfunction getContainerByName(containerName) {\n // Normalize name\n const name = containerName.startsWith('/') \n ? containerName.substring(1).toLowerCase() \n : containerName.toLowerCase();\n \n const entry = containerMap[name];\n \n if (!entry) {\n const registryAge = Date.now() - registry._lastRefresh;\n const isStale = registryAge > 60000; // 60 seconds\n \n if (isStale) {\n throw new Error(`Container \"${name}\" not found. Registry may be stale - try \"status\" to refresh.`);\n } else {\n throw new Error(`Container \"${name}\" not found. Check spelling or run \"status\" to see all containers.`);\n }\n }\n \n return entry;\n}\n\n// Detect mode based on input\nconst input = $input.item.json;\n\nif (input.containers && Array.isArray(input.containers)) {\n // Update mode: refresh registry with new container data\n const updatedMap = updateRegistry(input.containers);\n return {\n mode: 'update',\n registrySize: Object.keys(updatedMap).length,\n lastRefresh: new Date(registry._lastRefresh).toISOString(),\n containers: Object.keys(updatedMap)\n };\n} else if (input.containerName) {\n // Lookup mode: resolve container name to Unraid ID\n const unraidId = getUnraidId(input.containerName);\n const entry = getContainerByName(input.containerName);\n \n return {\n mode: 'lookup',\n containerName: entry.name,\n unraidId: unraidId\n };\n} else {\n throw new Error('Invalid input: provide either \"containers\" array or \"containerName\" string');\n}\n" - } - }, - { - "parameters": { - "jsCode": "// GraphQL Error Handler - Standardized error checking and HTTP status mapping\n// Input: $input.item.json = raw response from HTTP Request node\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (used in n8n-actions.json)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n throw new Error(`Container not found: ${message}`);\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n throw new Error(`Permission denied: ${message}. Check API key permissions.`);\n }\n \n // Any other GraphQL error\n throw new Error(`Unraid API error: ${message}`);\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n throw new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`);\n}\n\n// Check missing data field\nif (!response.data) {\n throw new Error('GraphQL response missing data field');\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" - }, - "id": "code-graphql-error-handler", - "name": "GraphQL Error Handler", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 600, - 2600 - ] - }, - { - "parameters": { - "method": "POST", - "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\"query\": \"query { docker { containers { id names state } } }\"}", - "options": { - "timeout": 15000, - "allowUnauthorizedCerts": true, - "response": { - "response": { - "fullResponse": true - } - } - } - }, - "id": "http-unraid-api-template", - "name": "Unraid API HTTP Template", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 1000, - 2600 - ], - "onError": "continueRegularOutput" - }, - { - "parameters": { - "mode": "runOnceForAllItems", - "jsCode": "// Callback Token Encoder\n// Compresses 129-char Unraid PrefixedID to 8-char token for Telegram callback_data\n\n// Initialize token store using static data with JSON serialization\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData._callbackTokens) {\n staticData._callbackTokens = JSON.stringify({});\n}\nconst tokenStore = JSON.parse(staticData._callbackTokens);\n\n/**\n * Encode Unraid PrefixedID to 8-character token\n * Uses SHA-256 with collision detection\n * @param {string} unraidId - 129-char PrefixedID (server_hash:container_hash)\n * @returns {string} 8-character hex token\n */\nasync function encodeToken(unraidId) {\n // Generate SHA-256 hash\n const encoder = new TextEncoder();\n const data = encoder.encode(unraidId);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n \n // Try up to 7 non-overlapping 8-char windows (56 chars of SHA-256)\n for (let offset = 0; offset < 56; offset += 8) {\n const token = hashHex.substring(offset, offset + 8);\n \n // Check for collision\n if (tokenStore[token]) {\n // If same unraidId, reuse token (idempotent)\n if (tokenStore[token] === unraidId) {\n return token;\n }\n // Collision with different ID - try next window\n continue;\n }\n \n // No collision - store and return\n tokenStore[token] = unraidId;\n staticData._callbackTokens = JSON.stringify(tokenStore);\n return token;\n }\n \n // All 7 windows collided (extremely unlikely)\n throw new Error(`Token collision: all 7 hash windows collided for ${unraidId.substring(0, 20)}...`);\n}\n\n// Process input\nconst input = $input.item.json;\nconst unraidId = input.unraidId;\n\nif (!unraidId) {\n throw new Error('Missing required input: unraidId');\n}\n\n// Encode the token\nconst token = await encodeToken(unraidId);\n\n// Build callback data if action provided\nlet callbackData = null;\nlet byteSize = null;\nlet warning = null;\n\nif (input.action) {\n callbackData = `action:${input.action}:${token}`;\n byteSize = new TextEncoder().encode(callbackData).length;\n \n if (byteSize > 64) {\n warning = `Callback data exceeds 64-byte limit: ${byteSize} bytes`;\n }\n}\n\nreturn {\n token,\n unraidId,\n callbackData,\n byteSize,\n warning\n};\n" - }, - "id": "code-callback-token-encoder", - "name": "Callback Token Encoder", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 600, - 2400 - ] - }, - { - "parameters": { - "mode": "runOnceForAllItems", - "jsCode": "// Callback Token Decoder\n// Resolves 8-char token back to 129-char Unraid PrefixedID\n\n// Load token store from static data\nconst staticData = $getWorkflowStaticData('global');\nconst tokenStore = JSON.parse(staticData._callbackTokens || '{}');\n\n/**\n * Decode token to Unraid PrefixedID\n * @param {string} token - 8-character hex token\n * @returns {string} Unraid PrefixedID (129-char)\n * @throws {Error} If token not found\n */\nfunction decodeToken(token) {\n const unraidId = tokenStore[token];\n \n if (!unraidId) {\n throw new Error(`Token not found: ${token}. Token store may have been cleared.`);\n }\n \n return unraidId;\n}\n\n// Process input\nconst input = $input.item.json;\nlet token = input.token;\nlet action = null;\n\n// If callbackData provided, parse it\nif (!token && input.callbackData) {\n const parts = input.callbackData.split(':');\n \n // Expected format: \"action:start:a1b2c3d4\" or \"action:stop:a1b2c3d4\"\n if (parts.length >= 3) {\n action = parts[1]; // e.g., \"start\", \"stop\"\n token = parts[parts.length - 1]; // Last segment is token\n } else {\n throw new Error(`Invalid callbackData format: ${input.callbackData}. Expected \"action::\"`);\n }\n}\n\nif (!token) {\n throw new Error('Missing required input: token or callbackData');\n}\n\n// Decode the token\nconst unraidId = decodeToken(token);\n\nreturn {\n token,\n unraidId,\n action\n};\n" - }, - "id": "code-callback-token-decoder", - "name": "Callback Token Decoder", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1000, - 2400 - ] - }, { "parameters": { "jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n" @@ -5348,17 +5177,13 @@ "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", - "authentication": "none", + "authentication": "genericCredentialType", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $env.UNRAID_API_KEY }}" } ] }, @@ -5373,16 +5198,23 @@ "fullResponse": false } } - } + }, + "genericAuthType": "httpHeaderAuth" }, "id": "http-execute-batch-update", - "name": "Execute Batch Update", + "name": "Execute Batch Mutation", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 3000, 2500 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -5695,39 +5527,6 @@ ] ] }, - "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": [ [ @@ -6085,40 +5884,7 @@ "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", + "node": "Normalize GraphQL Response (Action)", "type": "main", "index": 0 } @@ -6140,7 +5906,7 @@ "main": [ [ { - "node": "Build Cancel Return Submenu", + "node": "Normalize GraphQL Response (Cancel)", "type": "main", "index": 0 } @@ -6173,7 +5939,7 @@ "main": [ [ { - "node": "Prepare Batch Loop", + "node": "Check Batch Size", "type": "main", "index": 0 } @@ -6311,7 +6077,7 @@ "main": [ [ { - "node": "Check Available Updates", + "node": "Normalize GraphQL Response (Update All)", "type": "main", "index": 0 } @@ -6435,7 +6201,7 @@ "main": [ [ { - "node": "Prepare Update All Batch", + "node": "Normalize GraphQL Response (Update All Exec)", "type": "main", "index": 0 } @@ -6490,7 +6256,7 @@ "main": [ [ { - "node": "Find Container For Callback Update", + "node": "Normalize GraphQL Response (Callback)", "type": "main", "index": 0 } @@ -7283,7 +7049,7 @@ "main": [ [ { - "node": "Resolve Batch Stop Names", + "node": "Normalize GraphQL Response (Bitmap Stop)", "type": "main", "index": 0 } @@ -7334,211 +7100,194 @@ ] ] }, - "http-get-container-for-action": { + "Normalize GraphQL Response (Action)": { "main": [ [ { - "node": "normalize-graphql-action", + "node": "Update Container Registry (Action)", "type": "main", "index": 0 } ] ] }, - "normalize-graphql-action": { + "Update Container Registry (Action)": { "main": [ [ { - "node": "registry-update-action", + "node": "Prepare Inline Action Input", "type": "main", "index": 0 } ] ] }, - "registry-update-action": {}, - "http-get-container-for-cancel": { + "Normalize GraphQL Response (Cancel)": { "main": [ [ { - "node": "normalize-graphql-cancel", + "node": "Update Container Registry (Cancel)", "type": "main", "index": 0 } ] ] }, - "normalize-graphql-cancel": { + "Update Container Registry (Cancel)": { "main": [ [ { - "node": "registry-update-cancel", + "node": "Build Cancel Return Submenu", "type": "main", "index": 0 } ] ] }, - "registry-update-cancel": {}, - "http-get-all-containers-update-all": { + "Normalize GraphQL Response (Update All)": { "main": [ [ { - "node": "normalize-graphql-update-all", + "node": "Update Container Registry (Update All)", "type": "main", "index": 0 } ] ] }, - "normalize-graphql-update-all": { + "Update Container Registry (Update All)": { "main": [ [ { - "node": "registry-update-update-all", + "node": "Check Available Updates", "type": "main", "index": 0 } ] ] }, - "registry-update-update-all": {}, - "http-fetch-containers-update-all-exec": { + "Normalize GraphQL Response (Update All Exec)": { "main": [ [ { - "node": "normalize-graphql-update-all-exec", + "node": "Update Container Registry (Update All Exec)", "type": "main", "index": 0 } ] ] }, - "normalize-graphql-update-all-exec": { + "Update Container Registry (Update All Exec)": { "main": [ [ { - "node": "registry-update-update-all-exec", + "node": "Prepare Update All Batch", "type": "main", "index": 0 } ] ] }, - "registry-update-update-all-exec": {}, - "http-get-container-callback": { + "Normalize GraphQL Response (Callback)": { "main": [ [ { - "node": "normalize-graphql-callback", + "node": "Update Container Registry (Callback)", "type": "main", "index": 0 } ] ] }, - "normalize-graphql-callback": { + "Update Container Registry (Callback)": { "main": [ [ { - "node": "registry-update-callback", + "node": "Find Container For Callback Update", "type": "main", "index": 0 } ] ] }, - "registry-update-callback": {}, - "http-fetch-containers-bitmap-stop": { + "Normalize GraphQL Response (Bitmap Stop)": { "main": [ [ { - "node": "normalize-graphql-bitmap-stop", + "node": "Update Container Registry (Bitmap Stop)", "type": "main", "index": 0 } ] ] }, - "normalize-graphql-bitmap-stop": { + "Update Container Registry (Bitmap Stop)": { "main": [ [ { - "node": "registry-update-bitmap-stop", + "node": "Resolve Batch Stop Names", "type": "main", "index": 0 } ] ] }, - "registry-update-bitmap-stop": {}, - "code-prepare-update-all-batch": { + "Check Batch Size": { "main": [ [ { - "node": "if-check-batch-size", - "type": "main", - "index": 0 - } - ] - ] - }, - "if-check-batch-size": { - "main": [ - [ - { - "node": "code-build-batch-update-mutation", + "node": "Build Batch Update Mutation", "type": "main", "index": 0 } ], [ { - "node": "code-prepare-batch-loop", + "node": "Prepare Batch Loop", "type": "main", "index": 0 } ] ] }, - "code-build-batch-update-mutation": { + "Build Batch Update Mutation": { "main": [ [ { - "node": "http-execute-batch-update", + "node": "Execute Batch Mutation", "type": "main", "index": 0 } ] ] }, - "http-execute-batch-update": { + "Handle Batch Update Response": { "main": [ [ { - "node": "code-handle-batch-update-response", + "node": "Format Batch Result", "type": "main", "index": 0 } ] ] }, - "code-handle-batch-update-response": { + "Format Batch Result": { "main": [ [ { - "node": "code-format-batch-result", + "node": "Send Batch Result", "type": "main", "index": 0 } ] ] }, - "code-format-batch-result": { + "Execute Batch Mutation": { "main": [ [ { - "node": "telegram-send-batch-result", + "node": "Handle Batch Update Response", "type": "main", "index": 0 } @@ -7554,4 +7303,4 @@ "tags": [], "triggerCount": 1, "active": false -} \ No newline at end of file +}