docs(15): create phase plan — 2 plans for infrastructure foundation
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
---
|
||||
phase: 15-infrastructure-foundation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- n8n-workflow.json
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "GraphQL response normalizer transforms Unraid API shape to Docker API contract"
|
||||
- "Normalized containers have Id, Names (with leading slash), State (lowercase) fields"
|
||||
- "GraphQL error handler checks response.errors[] array and maps error codes"
|
||||
- "ALREADY_IN_STATE error code maps to HTTP 304 equivalent"
|
||||
- "HTTP Request template node has 15-second timeout configured"
|
||||
artifacts:
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "GraphQL Response Normalizer, GraphQL Error Handler, Unraid API HTTP Template utility nodes"
|
||||
contains: "normalizeContainers"
|
||||
key_links:
|
||||
- from: "GraphQL Response Normalizer node"
|
||||
to: "Docker API contract"
|
||||
via: "Field mapping (id->Id, names->Names with slash, state->State lowercase)"
|
||||
pattern: "normalizeContainers"
|
||||
- from: "GraphQL Error Handler node"
|
||||
to: "HTTP 304 pattern"
|
||||
via: "ALREADY_IN_STATE error code mapping"
|
||||
pattern: "ALREADY_IN_STATE"
|
||||
- from: "Unraid API HTTP Template node"
|
||||
to: "myunraid.net cloud relay"
|
||||
via: "15-second timeout configuration"
|
||||
pattern: "timeout.*15000"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the GraphQL Response Normalizer, Error Handler, and HTTP Request template as utility nodes in the main workflow.
|
||||
|
||||
Purpose: Phase 16 (API Migration) needs to transform Unraid GraphQL responses into the Docker API contract that 60+ existing Code nodes expect. The normalizer ensures zero changes to downstream nodes. The error handler standardizes GraphQL error checking (response.errors[] array) with equivalent HTTP status codes. The HTTP template provides correctly-configured timeout and retry settings for the myunraid.net cloud relay.
|
||||
|
||||
Output: Three new utility nodes in n8n-workflow.json: GraphQL Response Normalizer, GraphQL Error Handler, Unraid API HTTP Template.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/luc/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@ARCHITECTURE.md
|
||||
@.planning/phases/15-infrastructure-foundation/15-RESEARCH.md
|
||||
@.planning/phases/14-unraid-api-access/14-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add GraphQL Response Normalizer utility node</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add a new Code node named "GraphQL Response Normalizer" to n8n-workflow.json. This is a **standalone utility node** -- do NOT connect it to any existing nodes. Place it at position [200, 2600] (below the registry nodes from Plan 01).
|
||||
|
||||
The node must implement a `normalizeContainers(graphqlResponse)` function that transforms Unraid GraphQL API responses to match the Docker API contract used by existing workflow nodes.
|
||||
|
||||
**CRITICAL field mappings based on ARCHITECTURE.md verified schema:**
|
||||
|
||||
Unraid GraphQL format (from Phase 14 verification):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"docker": {
|
||||
"containers": [
|
||||
{ "id": "server_hash:container_hash", "names": ["/n8n"], "state": "RUNNING" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Docker API format (expected by existing nodes):
|
||||
```json
|
||||
{ "Id": "abc123...", "Names": ["/n8n"], "State": "running", "Status": "running" }
|
||||
```
|
||||
|
||||
Normalization rules:
|
||||
1. `Id`: Copy from Unraid `id` (keep the full PrefixedID -- downstream will use Container ID Registry for translation)
|
||||
2. `Names`: Copy from Unraid `names` array as-is (Phase 14 verified they already have `/` prefix)
|
||||
3. `State`: Convert Unraid UPPERCASE to lowercase (`"RUNNING"` -> `"running"`, `"STOPPED"` -> `"exited"` to match Docker convention)
|
||||
4. `Status`: Set to same as State (Docker has separate Status field, set to normalized state value)
|
||||
5. `Image`: Set to `""` (not available in basic Unraid query, add when schema is extended)
|
||||
6. `_unraidId`: Preserve original Unraid PrefixedID for debugging
|
||||
|
||||
**State mapping** (Unraid -> Docker equivalents):
|
||||
- `RUNNING` -> `running`
|
||||
- `STOPPED` -> `exited`
|
||||
- `PAUSED` -> `paused`
|
||||
- Default: lowercase the value
|
||||
|
||||
**Validation:**
|
||||
- If `graphqlResponse.errors` exists, throw with error messages joined
|
||||
- If `graphqlResponse.data?.docker?.containers` is missing, throw "Invalid GraphQL response structure"
|
||||
|
||||
**Input/output contract:**
|
||||
- Input: `$input.item.json` is the raw GraphQL response object
|
||||
- Output: Array of items, each `{ json: normalizedContainer }` (n8n multi-item output format)
|
||||
|
||||
Add notes: "UTILITY: Transform Unraid GraphQL response to Docker API contract. Not connected - Phase 16 will wire this in."
|
||||
|
||||
Node properties:
|
||||
- id: "code-graphql-normalizer"
|
||||
- name: "GraphQL Response Normalizer"
|
||||
- type: "n8n-nodes-base.code"
|
||||
- typeVersion: 2
|
||||
</action>
|
||||
<verify>
|
||||
Run: `python3 -c "
|
||||
import json
|
||||
wf = json.load(open('n8n-workflow.json'))
|
||||
nodes = {n['name']: n for n in wf['nodes']}
|
||||
norm = nodes.get('GraphQL Response Normalizer')
|
||||
print('EXISTS' if norm else 'MISSING')
|
||||
if norm:
|
||||
code = norm['parameters'].get('jsCode', '')
|
||||
print('Has normalizeContainers:', 'normalizeContainers' in code)
|
||||
print('Has State mapping:', 'RUNNING' in code and 'running' in code)
|
||||
print('Has STOPPED->exited:', 'STOPPED' in code and 'exited' in code)
|
||||
print('Has Names pass-through:', 'Names' in code)
|
||||
print('Has error check:', 'errors' in code)
|
||||
print('Has _unraidId:', '_unraidId' in code)
|
||||
"`
|
||||
</verify>
|
||||
<done>GraphQL Response Normalizer Code node exists with normalizeContainers() function that maps Unraid fields (id, names, state) to Docker fields (Id, Names, State), handles UPPERCASE->lowercase state conversion, validates response structure, and is not connected to any other nodes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add GraphQL Error Handler and Unraid API HTTP Template nodes</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add two new utility nodes to n8n-workflow.json. These are **standalone utility nodes** -- do NOT connect them to any existing nodes.
|
||||
|
||||
**Node 1: GraphQL Error Handler (Code node)**
|
||||
|
||||
id: "code-graphql-error-handler"
|
||||
name: "GraphQL Error Handler"
|
||||
Position: [600, 2600]
|
||||
|
||||
Implement a `checkGraphQLErrors(response)` function that:
|
||||
|
||||
1. **Check GraphQL errors array:**
|
||||
- If `response.errors` exists and has entries, examine first error
|
||||
- Extract `error.extensions?.code` and `error.message`
|
||||
- Map error codes to HTTP equivalents:
|
||||
- `ALREADY_IN_STATE` -> `{ statusCode: 304, alreadyInState: true, message }` (matches Docker API `statusCode === 304` pattern used in n8n-actions.json)
|
||||
- `NOT_FOUND` -> throw `Container not found: ${message}`
|
||||
- `FORBIDDEN` / `UNAUTHORIZED` -> throw `Permission denied: ${message}. Check API key permissions.`
|
||||
- Any other code -> throw `Unraid API error: ${message}`
|
||||
|
||||
2. **Check HTTP-level errors:**
|
||||
- If `response.statusCode >= 400`: throw `HTTP ${statusCode}: ${statusMessage}`
|
||||
|
||||
3. **Check missing data:**
|
||||
- If no `response.data`: throw `GraphQL response missing data field`
|
||||
|
||||
4. **Success return:**
|
||||
- `{ statusCode: 200, alreadyInState: false, data: response.data }`
|
||||
|
||||
5. **Input/output contract:**
|
||||
- Input: `$input.item.json` is the raw response from HTTP Request node
|
||||
- Output: `{ json: { success, statusCode, alreadyInState, message, data } }`
|
||||
- On `alreadyInState: true`: `success: true, message: "Container already in desired state"`
|
||||
- On error: throw (n8n error handling catches it)
|
||||
- On success: `success: true, data: response.data`
|
||||
|
||||
Add notes: "UTILITY: Standardized GraphQL error checking. Maps ALREADY_IN_STATE to HTTP 304. Not connected - Phase 16 will wire this in."
|
||||
|
||||
**Node 2: Unraid API HTTP Template (HTTP Request node)**
|
||||
|
||||
id: "http-unraid-api-template"
|
||||
name: "Unraid API HTTP Template"
|
||||
Position: [1000, 2600]
|
||||
|
||||
This is an n8n HTTP Request node pre-configured for Unraid GraphQL API calls. It serves as a **copy-paste template** for Phase 16 -- when migrating each API call, duplicate this node and update the query body.
|
||||
|
||||
Configuration:
|
||||
```json
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "none",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "x-api-key", "value": "={{ $env.UNRAID_API_KEY }}" }
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\"query\": \"query { docker { containers { id names state } } }\"}",
|
||||
"options": {
|
||||
"timeout": 15000,
|
||||
"allowUnauthorizedCerts": true,
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"onError": "continueRegularOutput"
|
||||
}
|
||||
```
|
||||
|
||||
Key settings rationale:
|
||||
- **15000ms timeout**: myunraid.net cloud relay adds 200-500ms latency (Phase 14 research). 15s provides safety margin for slow operations (container start/stop can take 3-5s + relay overhead).
|
||||
- **allowUnauthorizedCerts: true**: Matches Phase 14 verified configuration.
|
||||
- **genericAuthType: none + manual x-api-key header**: Phase 14 decision -- environment variables more reliable than n8n Header Auth credentials.
|
||||
- **onError: continueRegularOutput**: Allows downstream Code node (GraphQL Error Handler) to check errors instead of n8n catching them.
|
||||
- **fullResponse: true**: Returns statusCode, statusMessage alongside body for HTTP-level error checking.
|
||||
|
||||
Add notes: "TEMPLATE: Pre-configured HTTP Request for Unraid GraphQL API. 15s timeout for cloud relay. Duplicate and modify query for Phase 16 migration."
|
||||
|
||||
After adding all nodes, push the updated workflow to n8n using the push recipe from CLAUDE.md. Verify total node count is previous + 3 (from Plan 01) + 3 (from this plan) = original + 6.
|
||||
</action>
|
||||
<verify>
|
||||
Run: `python3 -c "
|
||||
import json
|
||||
wf = json.load(open('n8n-workflow.json'))
|
||||
nodes = {n['name']: n for n in wf['nodes']}
|
||||
|
||||
# Check Error Handler
|
||||
eh = nodes.get('GraphQL Error Handler')
|
||||
print('Error Handler:', 'EXISTS' if eh else 'MISSING')
|
||||
if eh:
|
||||
code = eh['parameters'].get('jsCode', '')
|
||||
print(' Has checkGraphQLErrors:', 'checkGraphQLErrors' in code)
|
||||
print(' Has ALREADY_IN_STATE:', 'ALREADY_IN_STATE' in code)
|
||||
print(' Has 304:', '304' in code)
|
||||
print(' Has NOT_FOUND:', 'NOT_FOUND' in code)
|
||||
print(' Has FORBIDDEN:', 'FORBIDDEN' in code)
|
||||
|
||||
# Check HTTP Template
|
||||
ht = nodes.get('Unraid API HTTP Template')
|
||||
print('HTTP Template:', 'EXISTS' if ht else 'MISSING')
|
||||
if ht:
|
||||
params = ht.get('parameters', {})
|
||||
print(' Has timeout 15000:', '15000' in json.dumps(params))
|
||||
print(' Has UNRAID_HOST:', 'UNRAID_HOST' in json.dumps(params))
|
||||
print(' Has UNRAID_API_KEY:', 'UNRAID_API_KEY' in json.dumps(params))
|
||||
print(' Has continueRegularOutput:', ht.get('onError') == 'continueRegularOutput' or 'continueRegularOutput' in json.dumps(params))
|
||||
|
||||
print('Total nodes:', len(wf['nodes']))
|
||||
|
||||
# Verify no connections for new utility nodes
|
||||
conn = wf.get('connections', {})
|
||||
for name in ['GraphQL Response Normalizer', 'GraphQL Error Handler', 'Unraid API HTTP Template']:
|
||||
has_outgoing = name in conn
|
||||
print(f'{name}: outgoing connections = {has_outgoing}')
|
||||
"`
|
||||
</verify>
|
||||
<done>GraphQL Error Handler Code node exists with checkGraphQLErrors() function that maps ALREADY_IN_STATE to HTTP 304 equivalent, handles NOT_FOUND/FORBIDDEN/UNAUTHORIZED errors. Unraid API HTTP Template node exists with 15-second timeout, correct auth headers, and continueRegularOutput error handling. Neither node is connected. Workflow pushed to n8n.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Three new utility nodes exist: GraphQL Response Normalizer, GraphQL Error Handler, Unraid API HTTP Template
|
||||
2. Normalizer correctly maps Unraid fields to Docker API contract (id->Id, UPPERCASE->lowercase state, names preserved)
|
||||
3. Error Handler maps ALREADY_IN_STATE to 304 (matches Docker API pattern in n8n-actions.json)
|
||||
4. HTTP Template has 15-second timeout, x-api-key header via env var, continueRegularOutput
|
||||
5. None of the three nodes have connections to/from other nodes (standalone utilities)
|
||||
6. Workflow JSON is valid and pushed to n8n
|
||||
7. Combined with Plan 01, total of 6 new utility nodes added to workflow
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- GraphQL response normalization transforms Unraid API shape to match Docker API contract exactly
|
||||
- State values properly converted (RUNNING->running, STOPPED->exited)
|
||||
- GraphQL error handling checks response.errors[] and maps to HTTP status codes
|
||||
- ALREADY_IN_STATE maps to 304 (same pattern as Docker API's HTTP 304 for "already started/stopped")
|
||||
- HTTP template has 15-second timeout for myunraid.net cloud relay latency
|
||||
- Six standalone utility nodes total (3 from Plan 01 + 3 from Plan 02) ready for Phase 16
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/15-infrastructure-foundation/15-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user