--- phase: 16-api-migration plan: 02 type: execute wave: 1 depends_on: [] files_modified: [n8n-actions.json] autonomous: true must_haves: truths: - "User can start a stopped container via Telegram and sees success message" - "User can stop a running container via Telegram and sees success message" - "User can restart a container via Telegram and sees success message" - "Starting an already-running container shows 'already started' (not an error)" - "Stopping an already-stopped container shows 'already stopped' (not an error)" artifacts: - path: "n8n-actions.json" provides: "Container lifecycle operations via Unraid GraphQL mutations" contains: "graphql" key_links: - from: "n8n-actions.json mutation nodes" to: "Unraid GraphQL API" via: "POST mutations (start, stop)" pattern: "mutation.*docker.*start|stop" - from: "GraphQL Error Handler" to: "Format Start/Stop/Restart Result Code nodes" via: "ALREADY_IN_STATE mapped to statusCode 304" pattern: "statusCode.*304" --- Migrate n8n-actions.json from Docker socket proxy to Unraid GraphQL API for container start, stop, and restart operations. Purpose: Container lifecycle actions are the second-most-used feature. This plan replaces 4 Docker API HTTP Request nodes (1 container list + 3 actions) with GraphQL equivalents, using Container ID Registry for name-to-PrefixedID translation and GraphQL Error Handler for ALREADY_IN_STATE mapping. Output: n8n-actions.json with all Docker API nodes replaced by Unraid GraphQL mutations, restart implemented as sequential stop+start (no native restart mutation), error handling preserving existing statusCode 304 pattern. @/home/luc/.claude/get-shit-done/workflows/execute-plan.md @/home/luc/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/16-api-migration/16-RESEARCH.md @.planning/phases/15-infrastructure-foundation/15-01-SUMMARY.md @.planning/phases/15-infrastructure-foundation/15-02-SUMMARY.md @n8n-actions.json @n8n-workflow.json (for Phase 15 utility node code — Container ID Registry, GraphQL Error Handler, HTTP Template) @ARCHITECTURE.md Task 1: Replace container list query and resolve with Container ID Registry n8n-actions.json Replace the container lookup flow in n8n-actions.json. Currently: - "Has Container ID?" IF node → "Get All Containers" HTTP Request → "Resolve Container ID" Code node The current flow fetches ALL containers from Docker API, then searches by name in Code node to find the container ID. Replace with Unraid GraphQL query + Container ID Registry: 1. **Replace "Get All Containers"** (GET docker-socket-proxy:2375/v1.47/containers/json?all=true): - Change to: POST `={{ $env.UNRAID_HOST }}/graphql` - Body: `{"query": "query { docker { containers { id names state image } } }"}` - Headers: `Content-Type: application/json`, `x-api-key: ={{ $env.UNRAID_API_KEY }}` - Timeout: 15000ms, error handling: `continueRegularOutput` - Rename to "Query All Containers" 2. **Add GraphQL Response Normalizer** Code node after the HTTP Request (before Resolve Container ID). Copy normalizer logic from n8n-workflow.json utility node. This transforms GraphQL response to Docker API contract format so "Resolve Container ID" Code node works unchanged. 3. **Add Container ID Registry update** after normalizer — a Code node that updates the static data registry with fresh name→PrefixedID mappings. This is critical because downstream mutations need PrefixedIDs. 4. **Update "Resolve Container ID"** Code node: After normalization, this node already works (it searches by `Names[0]`). However, enhance it to also output the `unraidId` (PrefixedID) from the `Id` field, so downstream mutation nodes can use it directly. Add to the output: `unraidId: matched.Id` (the normalizer preserves the full PrefixedID in the `Id` field). Wire: Has Container ID? (false) → Query All Containers → Normalizer → Registry Update → Resolve Container ID → Route Action Load n8n-actions.json and verify: 1. "Get All Containers" node replaced with GraphQL query 2. Normalizer Code node exists between HTTP Request and Resolve Container ID 3. Resolve Container ID outputs unraidId field 4. No "docker-socket-proxy" in any URL Container lookup uses Unraid GraphQL API with normalizer. Container ID Registry updated on every lookup. Resolve Container ID outputs unraidId (PrefixedID) for downstream mutations. Task 2: Replace start/stop/restart HTTP nodes with GraphQL mutations n8n-actions.json Replace the 3 Docker API action nodes with Unraid GraphQL mutations: 1. **Replace "Start Container"** (POST docker-socket-proxy:2375/v1.47/containers/{id}/start): - Add a **"Build Start Mutation"** Code node before the HTTP Request that constructs the GraphQL mutation body: ```javascript const data = $('Route Action').item.json; const unraidId = data.unraidId || data.containerId; return { json: { query: `mutation { docker { start(id: "${unraidId}") { id state } } }` } }; ``` - Change HTTP Request to: POST `={{ $env.UNRAID_HOST }}/graphql`, body from expression `={{ JSON.stringify({query: $json.query}) }}` - Headers: `Content-Type: application/json`, `x-api-key: ={{ $env.UNRAID_API_KEY }}` - Timeout: 15000ms, error handling: `continueRegularOutput` - Add **GraphQL Error Handler** Code node after HTTP Request (before Format Start Result). Copy error handler logic from n8n-workflow.json utility node. Maps `ALREADY_IN_STATE` → `{statusCode: 304}`, `NOT_FOUND` → `{statusCode: 404}`. - Wire: Route Action → Build Start Mutation → Start Container (HTTP) → Error Handler → Format Start Result 2. **Replace "Stop Container"** (POST docker-socket-proxy:2375/v1.47/containers/{id}/stop?t=10): - Same pattern as Start: Build Stop Mutation → HTTP Request → Error Handler → Format Stop Result - Mutation: `mutation { docker { stop(id: "${unraidId}") { id state } } }` - Timeout: 15000ms 3. **Replace "Restart Container"** (POST docker-socket-proxy:2375/v1.47/containers/{id}/restart?t=10): Unraid has NO native restart mutation. Implement as sequential stop + start: a. **Build Stop-for-Restart Mutation** Code node: ```javascript const data = $('Route Action').item.json; const unraidId = data.unraidId || data.containerId; return { json: { query: `mutation { docker { stop(id: "${unraidId}") { id state } } }`, unraidId } }; ``` b. **Stop For Restart** HTTP Request node (same config as Stop Container) c. **Handle Stop-for-Restart Result** Code node: - Check response: if success OR statusCode 304 (already stopped) → proceed to start - If error → fail restart ```javascript const response = $input.item.json; const prevData = $('Build Stop-for-Restart Mutation').item.json; if (response.statusCode && response.statusCode !== 304 && !response.data) { return { json: { error: true, statusCode: response.statusCode, message: 'Failed to stop container for restart' } }; } return { json: { query: `mutation { docker { start(id: "${prevData.unraidId}") { id state } } }` } }; ``` d. **Start After Stop** HTTP Request node (same config as Start Container) e. **Restart Error Handler** Code node (same GraphQL Error Handler logic) f. Wire: Route Action → Build Stop-for-Restart → Stop For Restart (HTTP) → Handle Stop-for-Restart → Start After Stop (HTTP) → Restart Error Handler → Format Restart Result **Critical:** The existing "Format Restart Result" Code node checks `response.statusCode === 304` which means "already running". For restart, 304 on the start step would mean the container didn't actually stop then start — it was already running. This is correct behavior for the existing Format Restart Result node. **Existing Format Start/Stop/Restart Result Code nodes remain UNCHANGED.** They already check: - `response.statusCode === 304` → "already in desired state" - `!response.message && !response.error` → success (Docker 204 No Content pattern) - The GraphQL Error Handler output maps to match these exact patterns. Rename Docker-centric HTTP Request nodes: - "Start Container" → "Start Container" (keep name, just change URL/method) - "Stop Container" → "Stop Container" (keep name) - Remove old "Restart Container" single-node and replace with stop+start chain Load n8n-actions.json and verify: 1. Zero "docker-socket-proxy" references in any node URL 2. Start and Stop nodes use POST to `$env.UNRAID_HOST/graphql` with mutation bodies 3. Restart implemented as 2 HTTP Request nodes (stop then start) with intermediate error handling 4. GraphQL Error Handler Code nodes exist after each mutation HTTP Request 5. Format Start/Stop/Restart Result Code nodes are UNCHANGED from pre-migration 6. All connections valid 7. Push to n8n via API and verify HTTP 200 Container start/stop use single GraphQL mutations. Restart uses sequential stop+start with ALREADY_IN_STATE tolerance on stop step. Error Handler maps GraphQL errors to statusCode 304 pattern. Format Result nodes unchanged. Workflow pushed to n8n. 1. Load n8n-actions.json and confirm zero "docker-socket-proxy" references 2. Confirm start/stop mutations use correct GraphQL syntax 3. Confirm restart is 2-step (stop → start) with 304 tolerance on stop 4. Confirm GraphQL Error Handler maps ALREADY_IN_STATE to statusCode 304 5. Confirm Format Start/Stop/Restart Result Code nodes are byte-for-byte identical to pre-migration 6. Push to n8n and verify HTTP 200 - n8n-actions.json has zero Docker socket proxy references - Start/stop operations use GraphQL mutations with Error Handler - Restart operates as sequential stop+start with ALREADY_IN_STATE tolerance - Format Result Code nodes unchanged (zero-change migration for output formatting) - Container ID Registry updated on container lookup - Workflow valid and pushed to n8n After completion, create `.planning/phases/16-api-migration/16-02-SUMMARY.md`