fix(16): resolve 3 UAT issues — update flow, batch cancel, text commands

- Fix update sub-workflow: remove unsupported GraphQL filter arg, fix node
  reference (Format Pull Error → Format Update Error), fix field case
  (data.image → data.Image)
- Fix batch cancel: connect Route Callback output 20 (batchcancel) to
  Prepare Batch UI Input (was empty connection array)
- Fix text commands: change .item.json to .first().json for paired item
  breakage after GraphQL chain expansion; convert Send Batch Confirmation
  from Telegram node to HTTP Request to fix double-serialized reply_markup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-02-09 12:28:21 -05:00
parent d4fcad827a
commit 07aeace1fd
6 changed files with 292 additions and 42 deletions
+4 -4
View File
@@ -167,7 +167,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers(filter: { id: \\\"\" + $json.containerId + \"\\\" }) { id names state image imageId } } }\"} }}",
"jsonBody": "={\"query\": \"query { docker { containers { id names state image imageId } } }\"}",
"options": {
"timeout": 15000
},
@@ -191,7 +191,7 @@
},
{
"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"
"jsCode": "// GraphQL Response Normalizer - Find and normalize single container by ID\n// Input: $input.item.json = raw GraphQL response (all containers)\n// Uses: trigger data containerId to filter to the target container\n\nconst response = $input.item.json;\nconst triggerData = $('When executed by another workflow').item.json;\nconst targetId = triggerData.containerId;\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',\n 'PAUSED': 'paused'\n};\n\nfunction normalizeState(unraidState) {\n return stateMap[unraidState] || unraidState.toLowerCase();\n}\n\n// Find the target container by ID\nconst allContainers = response.data.docker.containers;\nconst matched = allContainers.find(c => c.id === targetId);\n\nif (!matched) {\n throw new Error(`Container with ID '${targetId}' not found among ${allContainers.length} containers`);\n}\n\nconst dockerState = normalizeState(matched.state);\nreturn [{\n json: {\n Id: matched.id,\n Names: matched.names,\n State: dockerState,\n Status: dockerState,\n Image: matched.image || '',\n imageId: matched.imageId || '',\n _unraidId: matched.id\n }\n}];\n"
},
"id": "code-normalize-single",
"name": "Normalize Single Container",
@@ -204,7 +204,7 @@
},
{
"parameters": {
"jsCode": "// Capture pre-update state from input\nconst data = $input.item.json;\nconst triggerData = $('When executed by another workflow').item.json;\n\n// Check if we have container data already (from Resolve path) or need to extract (from direct ID path)\nlet unraidId, containerName, currentImageId, currentImage;\n\nif (data.unraidId) {\n // From Resolve Container ID path\n unraidId = data.unraidId;\n containerName = data.containerName;\n currentImageId = data.currentImageId;\n currentImage = data.currentImage;\n} else if (data.Id) {\n // From Query Single Container path (normalized)\n unraidId = data.Id;\n containerName = (data.Names?.[0] || '').replace(/^\\//, '');\n currentImageId = data.imageId || '';\n currentImage = data.image || '';\n} else {\n throw new Error('No container data found');\n}\n\nreturn {\n json: {\n unraidId,\n containerName,\n currentImageId,\n currentImage,\n chatId: triggerData.chatId,\n messageId: triggerData.messageId,\n responseMode: triggerData.responseMode,\n correlationId: triggerData.correlationId || ''\n }\n};\n"
"jsCode": "// Capture pre-update state from input\nconst data = $input.item.json;\nconst triggerData = $('When executed by another workflow').item.json;\n\n// Check if we have container data already (from Resolve path) or need to extract (from direct ID path)\nlet unraidId, containerName, currentImageId, currentImage;\n\nif (data.unraidId) {\n // From Resolve Container ID path\n unraidId = data.unraidId;\n containerName = data.containerName;\n currentImageId = data.currentImageId;\n currentImage = data.currentImage;\n} else if (data.Id) {\n // From Query Single Container path (normalized)\n unraidId = data.Id;\n containerName = (data.Names?.[0] || '').replace(/^\\//, '');\n currentImageId = data.imageId || '';\n currentImage = data.Image || '';\n} else {\n throw new Error('No container data found');\n}\n\nreturn {\n json: {\n unraidId,\n containerName,\n currentImageId,\n currentImage,\n chatId: triggerData.chatId,\n messageId: triggerData.messageId,\n responseMode: triggerData.responseMode,\n correlationId: triggerData.correlationId || ''\n }\n};\n"
},
"id": "code-capture-state",
"name": "Capture Pre-Update State",
@@ -734,7 +734,7 @@
},
{
"parameters": {
"jsCode": "// Return error result\nconst data = $('Format Pull Error').item.json;\nreturn {\n json: {\n success: false,\n updated: false,\n message: data.message\n }\n};"
"jsCode": "// Return error result\nconst data = $('Format Update Error').item.json;\nreturn {\n json: {\n success: false,\n updated: false,\n message: data.message\n }\n};"
},
"id": "code-return-error",
"name": "Return Error",