diff --git a/n8n-status.json b/n8n-status.json index 1dc64ee..50a7074 100644 --- a/n8n-status.json +++ b/n8n-status.json @@ -158,18 +158,39 @@ }, { "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "bodyParameters": { + "parameters": [] + }, + "specifyBody": "json", + "jsonBody": "{\"query\": \"query {\\n docker {\\n containers {\\n id\\n names\\n state\\n image\\n status\\n }\\n }\\n}\"}", + "options": { + "timeout": 15000, + "response": { + "response": { + "fullResponse": false + } + } + } }, "id": "status-docker-list", - "name": "Docker List Containers", + "name": "Query Containers", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 900, 200 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -199,18 +220,39 @@ }, { "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "bodyParameters": { + "parameters": [] + }, + "specifyBody": "json", + "jsonBody": "{\"query\": \"query {\\n docker {\\n containers {\\n id\\n names\\n state\\n image\\n status\\n }\\n }\\n}\"}", + "options": { + "timeout": 15000, + "response": { + "response": { + "fullResponse": false + } + } + } }, "id": "status-docker-single", - "name": "Docker Get Container", + "name": "Query Container Status", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 900, 300 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -240,18 +282,39 @@ }, { "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} + "method": "POST", + "url": "={{ $env.UNRAID_HOST }}/graphql", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "bodyParameters": { + "parameters": [] + }, + "specifyBody": "json", + "jsonBody": "{\"query\": \"query {\\n docker {\\n containers {\\n id\\n names\\n state\\n image\\n status\\n }\\n }\\n}\"}", + "options": { + "timeout": 15000, + "response": { + "response": { + "fullResponse": false + } + } + } }, "id": "status-docker-paginate", - "name": "Docker List For Paginate", + "name": "Query Containers For Paginate", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 900, 400 - ] + ], + "credentials": { + "httpHeaderAuth": { + "id": "unraid-api-key-credential-id", + "name": "Unraid API Key" + } + } }, { "parameters": { @@ -265,6 +328,84 @@ 1120, 400 ] + }, + { + "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: container.status || dockerState, // Use Unraid status field or fallback to state\n Image: container.image || '', // Unraid provides image field\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 }));" + }, + "id": "status-normalizer-list", + "name": "Normalize GraphQL Response (List)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1000, + 200 + ] + }, + { + "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: container.status || dockerState, // Use Unraid status field or fallback to state\n Image: container.image || '', // Unraid provides image field\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 }));" + }, + "id": "status-normalizer-status", + "name": "Normalize GraphQL Response (Status)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1000, + 300 + ] + }, + { + "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: container.status || dockerState, // Use Unraid status field or fallback to state\n Image: container.image || '', // Unraid provides image field\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 }));" + }, + "id": "status-normalizer-paginate", + "name": "Normalize GraphQL Response (Paginate)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1000, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Update Container ID Registry with fresh container data\nconst containers = $input.all().map(item => item.json);\n\n// Initialize registry using static data with JSON serialization pattern\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst newMap = {};\n\nfor (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 newMap[name] = {\n name: name,\n unraidId: container.Id // Full PrefixedID from normalized data\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(newMap);\n\n// Pass through all containers unchanged\nreturn $input.all();" + }, + "id": "status-registry-list", + "name": "Update Container Registry (List)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1060, + 200 + ] + }, + { + "parameters": { + "jsCode": "// Update Container ID Registry with fresh container data\nconst containers = $input.all().map(item => item.json);\n\n// Initialize registry using static data with JSON serialization pattern\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst newMap = {};\n\nfor (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 newMap[name] = {\n name: name,\n unraidId: container.Id // Full PrefixedID from normalized data\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(newMap);\n\n// Pass through all containers unchanged\nreturn $input.all();" + }, + "id": "status-registry-status", + "name": "Update Container Registry (Status)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1060, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Update Container ID Registry with fresh container data\nconst containers = $input.all().map(item => item.json);\n\n// Initialize registry using static data with JSON serialization pattern\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst newMap = {};\n\nfor (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 newMap[name] = {\n name: name,\n unraidId: container.Id // Full PrefixedID from normalized data\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(newMap);\n\n// Pass through all containers unchanged\nreturn $input.all();" + }, + "id": "status-registry-paginate", + "name": "Update Container Registry (Paginate)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1060, + 400 + ] } ], "connections": { @@ -308,14 +449,36 @@ "main": [ [ { - "node": "Docker List Containers", + "node": "Query Containers", "type": "main", "index": 0 } ] ] }, - "Docker List Containers": { + "Query Containers": { + "main": [ + [ + { + "node": "Normalize GraphQL Response (List)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize GraphQL Response (List)": { + "main": [ + [ + { + "node": "Update Container Registry (List)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Container Registry (List)": { "main": [ [ { @@ -330,14 +493,36 @@ "main": [ [ { - "node": "Docker Get Container", + "node": "Query Container Status", "type": "main", "index": 0 } ] ] }, - "Docker Get Container": { + "Query Container Status": { + "main": [ + [ + { + "node": "Normalize GraphQL Response (Status)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize GraphQL Response (Status)": { + "main": [ + [ + { + "node": "Update Container Registry (Status)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Container Registry (Status)": { "main": [ [ { @@ -352,14 +537,36 @@ "main": [ [ { - "node": "Docker List For Paginate", + "node": "Query Containers For Paginate", "type": "main", "index": 0 } ] ] }, - "Docker List For Paginate": { + "Query Containers For Paginate": { + "main": [ + [ + { + "node": "Normalize GraphQL Response (Paginate)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize GraphQL Response (Paginate)": { + "main": [ + [ + { + "node": "Update Container Registry (Paginate)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Container Registry (Paginate)": { "main": [ [ {