diff --git a/.planning/phases/16-api-migration/16-03-PLAN.md b/.planning/phases/16-api-migration/16-03-PLAN.md
index 5eced76..9ebc9cb 100644
--- a/.planning/phases/16-api-migration/16-03-PLAN.md
+++ b/.planning/phases/16-api-migration/16-03-PLAN.md
@@ -13,6 +13,7 @@ must_haves:
- "User sees 'already up to date' when no update is available"
- "User sees error message when update fails (pull error, container not found)"
- "Update success/failure messages sent via both text and inline keyboard response modes"
+ - "Unraid Docker tab shows no update badge after bot-initiated container update (badge cleared automatically by updateContainer mutation)"
artifacts:
- path: "n8n-update.json"
provides: "Single container update via Unraid GraphQL updateContainer mutation"
@@ -202,6 +203,7 @@ n8n-update.json uses single updateContainer GraphQL mutation instead of 5-step D
5. All 3 response paths work: success, no-update, error
6. Format Result Code nodes reference correct upstream nodes
7. Push to n8n with HTTP 200
+8. After a successful container update via bot, verify Unraid Docker tab shows no update badge for that container (badge cleared automatically by updateContainer mutation — requires Unraid 7.2+)
@@ -211,6 +213,7 @@ n8n-update.json uses single updateContainer GraphQL mutation instead of 5-step D
- Success/no-update/error messaging identical to user
- Container ID Registry refreshed after successful update
- Node count reduced by ~15 nodes
+- Unraid Docker tab update badge clears automatically after bot-initiated update (Unraid 7.2+ required)
- Workflow valid and pushed to n8n
diff --git a/.planning/phases/16-api-migration/16-05-PLAN.md b/.planning/phases/16-api-migration/16-05-PLAN.md
index 30936b6..dffbef3 100644
--- a/.planning/phases/16-api-migration/16-05-PLAN.md
+++ b/.planning/phases/16-api-migration/16-05-PLAN.md
@@ -11,7 +11,8 @@ must_haves:
truths:
- "Inline keyboard action callbacks resolve container and execute start/stop/restart/update via Unraid API"
- "Text command 'update all' shows :latest containers with update availability via Unraid API"
- - "Batch update loop calls update sub-workflow for each container successfully"
+ - "Batch update of <=5 containers uses single updateContainers (plural) mutation for parallel execution"
+ - "Batch update of >5 containers uses serial update sub-workflow calls with Telegram progress messages"
- "Callback update from inline keyboard works via Unraid API"
- "Batch stop confirmation resolves bitmap to container names via Unraid API"
- "Cancel-return-to-submenu resolves container via Unraid API"
@@ -32,14 +33,18 @@ must_haves:
to: "Sub-workflow Execute nodes"
via: "Name→PrefixedID mapping for mutation operations"
pattern: "unraidId|prefixedId"
+ - from: "Batch Update Code node"
+ to: "Unraid GraphQL API"
+ via: "updateContainers (plural) mutation for small batches"
+ pattern: "updateContainers"
---
Migrate all 6 Docker socket proxy HTTP Request nodes in the main workflow (n8n-workflow.json) to Unraid GraphQL API queries.
-Purpose: The main workflow is the Telegram bot entry point. It contains 6 Docker API calls for container lookups used by inline keyboard actions, update-all flow, callback updates, batch stop, and cancel-return navigation. These are read-only lookups (no mutations — mutations happen in sub-workflows), so this is a query-only migration with normalizer and registry updates.
+Purpose: The main workflow is the Telegram bot entry point. It contains 6 Docker API calls for container lookups used by inline keyboard actions, update-all flow, callback updates, batch stop, and cancel-return navigation. Additionally, the batch update flow currently calls the update sub-workflow serially per container — this plan also implements the `updateContainers` (plural) mutation for efficient parallel batch updates.
-Output: n8n-workflow.json with zero Docker socket proxy references, all container lookups via GraphQL, Container ID Registry updated on every query, Phase 15 utility nodes wired into active flows.
+Output: n8n-workflow.json with zero Docker socket proxy references, all container lookups via GraphQL, Container ID Registry updated on every query, Phase 15 utility nodes wired into active flows, and hybrid batch update strategy (plural mutation for small batches, serial with progress for large batches).
@@ -214,6 +219,135 @@ Callback data encoding verified or updated for Telegram's 64-byte limit. Token E
+
+ Task 3: Implement hybrid batch update with updateContainers (plural) mutation
+ n8n-workflow.json
+
+Implement the `updateContainers` (plural) GraphQL mutation for batch update operations in the main workflow. The current batch update loop calls the update sub-workflow (n8n-update.json) serially per container via Execute Workflow nodes. For small batches, this is inefficient — Unraid's `updateContainers` mutation handles parallelization internally.
+
+**Hybrid strategy (from research Pattern 4):**
+- Batches of 1-5 containers: Use single `updateContainers(ids: [PrefixedID!]!)` mutation directly in main workflow (fast, parallel, no progress updates needed for small count)
+- Batches of >5 containers: Keep existing serial loop calling update sub-workflow per container with Telegram message edits showing progress (user sees "Updated 3/10: plex" etc.)
+
+**Implementation in the batch update Code node ("Prepare Update All Batch" or equivalent):**
+
+Find the Code node that prepares the batch update execution. This node currently builds a list of containers to update and feeds them to a loop that calls Execute Workflow (n8n-update.json) per container.
+
+Add a branching IF node after the batch preparation:
+- IF `containerCount <= 5` → "Batch Update Via Mutation" path (new)
+- IF `containerCount > 5` → existing serial loop path (unchanged)
+
+**New "Batch Update Via Mutation" path:**
+
+1. **"Build Batch Update Mutation"** Code node:
+ ```javascript
+ const containers = $input.all().map(item => item.json);
+ // Look up PrefixedIDs from Container ID Registry (static data)
+ const staticData = $getWorkflowStaticData('global');
+ const registry = JSON.parse(staticData._containerIdRegistry || '{}');
+
+ const ids = [];
+ const nameMap = {};
+ for (const container of containers) {
+ const name = container.containerName || container.name;
+ const entry = registry[name];
+ if (entry && entry.prefixedId) {
+ ids.push(entry.prefixedId);
+ nameMap[entry.prefixedId] = name;
+ }
+ }
+
+ return [{
+ json: {
+ query: `mutation { docker { updateContainers(ids: ${JSON.stringify(ids)}) { id state image imageId } } }`,
+ ids,
+ nameMap,
+ containerCount: ids.length,
+ chatId: containers[0].chatId,
+ messageId: containers[0].messageId
+ }
+ }];
+ ```
+
+2. **"Execute Batch Update"** HTTP Request node:
+ - POST `={{ $env.UNRAID_HOST }}/graphql`
+ - Body: from $json (query field)
+ - Headers: `Content-Type: application/json`, `x-api-key: ={{ $env.UNRAID_API_KEY }}`
+ - **Timeout: 120000ms (120 seconds)** — batch updates pull multiple images, needs extended timeout
+ - Error handling: `continueRegularOutput`
+
+3. **"Handle Batch Update Response"** Code node:
+ ```javascript
+ const response = $input.item.json;
+ const prevData = $('Build Batch Update Mutation').item.json;
+
+ // Check for GraphQL errors
+ if (response.errors) {
+ return { json: {
+ success: false,
+ error: true,
+ errorMessage: response.errors[0].message,
+ chatId: prevData.chatId,
+ messageId: prevData.messageId
+ }};
+ }
+
+ const updated = response.data?.docker?.updateContainers || [];
+ const results = updated.map(container => ({
+ name: prevData.nameMap[container.id] || container.id,
+ imageId: container.imageId,
+ state: container.state
+ }));
+
+ return { json: {
+ success: true,
+ batchMode: 'parallel',
+ updatedCount: results.length,
+ results,
+ chatId: prevData.chatId,
+ messageId: prevData.messageId
+ }};
+ ```
+
+4. **Update Container ID Registry** after batch mutation — container IDs change after update:
+ ```javascript
+ const response = $input.item.json;
+ if (response.success && response.results) {
+ const staticData = $getWorkflowStaticData('global');
+ const registry = JSON.parse(staticData._containerIdRegistry || '{}');
+ // Refresh registry entries for updated containers
+ // The mutation response contains new IDs — update registry
+ staticData._containerIdRegistry = JSON.stringify(registry);
+ }
+ return $input.all();
+ ```
+
+5. Wire the batch mutation result into the existing batch update success messaging path (the same path that currently receives results from the serial loop). The response format should match what the existing success messaging expects.
+
+**Serial path (>5 containers) — UNCHANGED:**
+Keep the existing loop calling Execute Workflow (n8n-update.json) per container with Telegram progress edits. This path is already migrated by Plan 16-03 (n8n-update.json uses GraphQL internally).
+
+**Key wiring:**
+```
+Prepare Update All Batch → Check Batch Size (IF: count <= 5)
+ → True: Build Batch Mutation → Execute Batch Update (HTTP, 120s) → Handle Batch Response → Registry Update → Format Batch Result
+ → False: [existing serial loop with Execute Workflow calls, unchanged]
+```
+
+
+1. IF node exists that branches on container count (threshold: 5)
+2. Small batch path uses `updateContainers` (plural) mutation
+3. HTTP Request for batch mutation has 120000ms timeout
+4. Large batch path still uses serial Execute Workflow calls (unchanged)
+5. Container ID Registry updated after batch mutation
+6. Batch result messaging works for both paths
+7. Push to n8n via API and verify HTTP 200
+
+
+Hybrid batch update implemented: batches of 1-5 containers use single updateContainers mutation (parallel, fast), batches of >5 containers use serial sub-workflow calls with progress updates. Container ID Registry refreshed after batch mutation. Both paths produce consistent result messaging.
+
+
+
@@ -223,7 +357,10 @@ Callback data encoding verified or updated for Telegram's 64-byte limit. Token E
4. Callback data fits within Telegram's 64-byte limit
5. All sub-workflow Execute nodes pass correct data format (PrefixedIDs work with migrated sub-workflows)
6. Phase 15 utility nodes preserved as templates
-7. Push to n8n with HTTP 200
+7. Batch update of <=5 containers uses `updateContainers` (plural) mutation with 120s timeout
+8. Batch update of >5 containers uses serial sub-workflow calls with progress messaging
+9. Container ID Registry refreshed after batch mutation (container IDs change on update)
+10. Push to n8n with HTTP 200
@@ -232,6 +369,8 @@ Callback data encoding verified or updated for Telegram's 64-byte limit. Token E
- Container ID Registry refreshed on every query path
- Callback data encoding works within Telegram's 64-byte limit
- Sub-workflow integration verified (actions, update, status, batch-ui all receive correct data format)
+- Hybrid batch update: small batches (<=5) use updateContainers mutation, large batches (>5) use serial with progress
+- Container ID Registry refreshed after batch mutations
- Workflow valid and pushed to n8n