diff --git a/n8n-workflow.json b/n8n-workflow.json index 7d52165..c02fbbb 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -415,20 +415,6 @@ 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": { "resource": "message", @@ -1256,20 +1242,6 @@ 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": { "resource": "message", @@ -2054,20 +2026,6 @@ -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": { "method": "POST", @@ -4245,14 +4203,14 @@ }, { "parameters": { - "jsCode": "// Prepare input for matching sub-workflow (action commands)\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\nreturn {\n json: {\n action: \"match_action\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};" + "jsCode": "// Prepare input for matching sub-workflow (action commands)\nconst containers = $input.all().map(item => item.json);\nconst dockerOutput = JSON.stringify(containers);\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\nreturn {\n json: {\n action: \"match_action\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};" }, "id": "code-prepare-action-match-input", "name": "Prepare Action Match Input", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1340, + 1450, 400 ] }, @@ -4414,14 +4372,14 @@ }, { "parameters": { - "jsCode": "// Prepare input for matching sub-workflow (update commands)\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\nreturn {\n json: {\n action: \"match_update\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};" + "jsCode": "// Prepare input for matching sub-workflow (update commands)\nconst containers = $input.all().map(item => item.json);\nconst dockerOutput = JSON.stringify(containers);\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\nreturn {\n json: {\n action: \"match_update\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};" }, "id": "code-prepare-update-match-input", "name": "Prepare Update Match Input", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1340, + 1450, 1000 ] }, @@ -4560,14 +4518,14 @@ }, { "parameters": { - "jsCode": "// Prepare input for matching sub-workflow (batch commands)\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// Convert containerNames array to CSV for sub-workflow input\nconst selectedContainers = Array.isArray(containerNames) ? containerNames.join(',') : containerNames;\n\nreturn {\n json: {\n action: \"match_batch\",\n containerList: dockerOutput,\n searchTerm: \"\",\n selectedContainers: selectedContainers,\n chatId: chatId,\n messageId: messageId,\n correlationId: $input.item.json.correlationId || ''\n }\n};" + "jsCode": "// Prepare input for matching sub-workflow (batch commands)\nconst containers = $input.all().map(item => item.json);\nconst dockerOutput = JSON.stringify(containers);\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// Convert containerNames array to CSV for sub-workflow input\nconst selectedContainers = Array.isArray(containerNames) ? containerNames.join(',') : containerNames;\n\nreturn {\n json: {\n action: \"match_batch\",\n containerList: dockerOutput,\n searchTerm: \"\",\n selectedContainers: selectedContainers,\n chatId: chatId,\n messageId: messageId,\n correlationId: $input.item.json.correlationId || ''\n }\n};" }, "id": "code-prepare-batch-match-input", "name": "Prepare Batch Match Input", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1560, + 1670, -300 ] }, @@ -5262,6 +5220,213 @@ "name": "Telegram account" } } + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "genericCredentialType", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image status } } }\"} }}", + "options": { + "timeout": 15000, + "response": { + "response": { + "errorRedirection": "continue", + "fullResponse": false + } + } + }, + "genericAuthType": "httpHeaderAuth" + }, + "id": "http-query-containers-action", + "name": "Query Containers for Action", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1120, + 400 + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } + }, + { + "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" + }, + "id": "code-normalize-action-containers", + "name": "Normalize Action Containers", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1230, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();" + }, + "id": "code-registry-update-action-text", + "name": "Update Registry (Action)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1340, + 400 + ] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "genericCredentialType", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image status } } }\"} }}", + "options": { + "timeout": 15000, + "response": { + "response": { + "errorRedirection": "continue", + "fullResponse": false + } + } + }, + "genericAuthType": "httpHeaderAuth" + }, + "id": "http-query-containers-update", + "name": "Query Containers for Update", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1120, + 1000 + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } + }, + { + "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" + }, + "id": "code-normalize-update-containers", + "name": "Normalize Update Containers", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1230, + 1000 + ] + }, + { + "parameters": { + "jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();" + }, + "id": "code-registry-update-update-text", + "name": "Update Registry (Update)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1340, + 1000 + ] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "genericCredentialType", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image status } } }\"} }}", + "options": { + "timeout": 15000, + "response": { + "response": { + "errorRedirection": "continue", + "fullResponse": false + } + } + }, + "genericAuthType": "httpHeaderAuth" + }, + "id": "http-query-containers-batch", + "name": "Query Containers for Batch", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1340, + -300 + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } + }, + { + "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" + }, + "id": "code-normalize-batch-containers", + "name": "Normalize Batch Containers", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1450, + -300 + ] + }, + { + "parameters": { + "jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();" + }, + "id": "code-registry-update-batch-text", + "name": "Update Registry (Batch)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1560, + -300 + ] } ], "connections": { @@ -5516,17 +5681,6 @@ ] ] }, - "Docker List for Action": { - "main": [ - [ - { - "node": "Prepare Action Match Input", - "type": "main", - "index": 0 - } - ] - ] - }, "Build Batch Keyboard": { "main": [ [ @@ -5542,18 +5696,7 @@ "main": [ [ { - "node": "Docker List for Update", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Update": { - "main": [ - [ - { - "node": "Prepare Update Match Input", + "node": "Query Containers for Update", "type": "main", "index": 0 } @@ -5671,7 +5814,7 @@ "main": [ [ { - "node": "Get Containers for Batch", + "node": "Query Containers for Batch", "type": "main", "index": 0 } @@ -5703,17 +5846,6 @@ ] ] }, - "Get Containers for Batch": { - "main": [ - [ - { - "node": "Prepare Batch Match Input", - "type": "main", - "index": 0 - } - ] - ] - }, "Route Batch Action": { "main": [ [ @@ -5812,7 +5944,7 @@ "main": [ [ { - "node": "Docker List for Action", + "node": "Query Containers for Action", "type": "main", "index": 0 } @@ -7293,6 +7425,105 @@ } ] ] + }, + "Query Containers for Action": { + "main": [ + [ + { + "node": "Normalize Action Containers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Action Containers": { + "main": [ + [ + { + "node": "Update Registry (Action)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Registry (Action)": { + "main": [ + [ + { + "node": "Prepare Action Match Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Query Containers for Update": { + "main": [ + [ + { + "node": "Normalize Update Containers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Update Containers": { + "main": [ + [ + { + "node": "Update Registry (Update)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Registry (Update)": { + "main": [ + [ + { + "node": "Prepare Update Match Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Query Containers for Batch": { + "main": [ + [ + { + "node": "Normalize Batch Containers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Batch Containers": { + "main": [ + [ + { + "node": "Update Registry (Batch)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Registry (Batch)": { + "main": [ + [ + { + "node": "Prepare Batch Match Input", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {}, @@ -7303,4 +7534,4 @@ "tags": [], "triggerCount": 1, "active": false -} +} \ No newline at end of file