Files
unraid-docker-manager/.planning/phases/15-infrastructure-foundation/15-01-PLAN.md
T

244 lines
11 KiB
Markdown

---
phase: 15-infrastructure-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- n8n-workflow.json
autonomous: true
must_haves:
truths:
- "Container ID Registry maps container names to Unraid PrefixedID format"
- "Callback token encoder produces 8-char tokens from PrefixedIDs"
- "Callback token decoder resolves 8-char tokens back to PrefixedIDs"
- "Token collisions are detected and handled"
- "Registry and token store persist across workflow executions via static data JSON serialization"
artifacts:
- path: "n8n-workflow.json"
provides: "Container ID Registry, Callback Token Encoder, Callback Token Decoder utility nodes"
contains: "_containerIdMap"
key_links:
- from: "Container ID Registry node"
to: "static data _containerIdMap"
via: "JSON.parse/JSON.stringify pattern"
pattern: "JSON\\.parse\\(.*_containerIdMap"
- from: "Callback Token Encoder node"
to: "static data _callbackTokens"
via: "SHA-256 hash + JSON serialization"
pattern: "crypto\\.subtle\\.digest"
- from: "Callback Token Decoder node"
to: "static data _callbackTokens"
via: "JSON.parse lookup"
pattern: "_callbackTokens"
---
<objective>
Build the Container ID Registry and Callback Token Encoding system as utility Code nodes in the main workflow.
Purpose: Phase 16 (API Migration) needs to translate between container names and Unraid PrefixedIDs (129-char format like `server_hash:container_hash`). The registry provides centralized ID translation, and the token system compresses PrefixedIDs to 8-char tokens for Telegram callback_data (64-byte limit). These nodes are not wired into the active flow yet -- they are standalone utilities that Phase 16 will connect.
Output: Three new Code nodes in n8n-workflow.json: Container ID Registry, Callback Token Encoder, Callback Token Decoder.
</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
@.planning/phases/11-update-all-callback-limits/11-01-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Add Container ID Registry utility node</name>
<files>n8n-workflow.json</files>
<action>
Add a new Code node named "Container ID Registry" to n8n-workflow.json. This is a **standalone utility node** -- do NOT connect it to any existing nodes. Place it at position [200, 2400] (below the active workflow area, in an "infrastructure utilities" zone).
The node must implement:
1. **Registry initialization** using static data with JSON serialization pattern (CRITICAL: must use top-level assignment per CLAUDE.md):
```javascript
const registry = $getWorkflowStaticData('global');
if (!registry._containerIdMap) {
registry._containerIdMap = JSON.stringify({});
}
const containerMap = JSON.parse(registry._containerIdMap);
```
2. **updateRegistry(containers)** function that:
- Takes an array of Unraid GraphQL container objects (format: `{ id, names[], state }`)
- Maps each container: name (from `names[0]`, strip leading `/`, lowercase) -> `{ name, unraidId: id }`
- The Unraid `id` field IS the PrefixedID (129-char `server_hash:container_hash` format per ARCHITECTURE.md)
- Stores timestamp: `registry._lastRefresh = Date.now()`
- Serializes: `registry._containerIdMap = JSON.stringify(newMap)`
- Returns the new map
3. **getUnraidId(containerName)** function that:
- Strips leading `/`, lowercases input
- Looks up in containerMap
- If not found AND registry older than 60 seconds: throws `Container "${name}" not found. Registry may be stale - try "status" to refresh.`
- If not found AND registry fresh: throws `Container "${name}" not found. Check spelling or run "status" to see all containers.`
- Returns `entry.unraidId`
4. **getContainerByName(containerName)** function that:
- Same lookup as getUnraidId but returns the full entry object
- Returns `{ name, unraidId }`
5. **Node input/output contract:**
- Input: `$input.item.json.containers` (array) for registry updates, OR `$input.item.json.containerName` (string) for lookups
- Detect mode: if `containers` array exists, update registry. If `containerName` exists, do lookup.
- Output: Updated map (for updates) or `{ containerName, unraidId }` (for lookups)
Node properties:
- id: "code-container-id-registry"
- name: "Container ID Registry"
- type: "n8n-nodes-base.code"
- typeVersion: 2
- Do NOT add any connections for this node.
- Add `notesDisplayMode: "show"` and `notes: "UTILITY: Container name <-> Unraid PrefixedID translation. Not connected - Phase 16 will wire this in."` to parameters.
</action>
<verify>
Run: `python3 -c "import json; wf=json.load(open('n8n-workflow.json')); nodes={n['name']:n for n in wf['nodes']}; r=nodes.get('Container ID Registry'); print('EXISTS' if r else 'MISSING'); print('ID:', r['id'] if r else 'N/A'); print('Has _containerIdMap:', '_containerIdMap' in r['parameters'].get('jsCode','') if r else False); print('Has JSON.parse:', 'JSON.parse' in r['parameters'].get('jsCode','') if r else False); print('Has JSON.stringify:', 'JSON.stringify' in r['parameters'].get('jsCode','') if r else False)"`
</verify>
<done>Container ID Registry Code node exists in n8n-workflow.json with updateRegistry() and getUnraidId() functions, uses JSON serialization for static data persistence, is not connected to any other nodes.</done>
</task>
<task type="auto">
<name>Task 2: Add Callback Token Encoder and Decoder utility nodes</name>
<files>n8n-workflow.json</files>
<action>
Add two new Code nodes to n8n-workflow.json for callback token encoding/decoding. These are **standalone utility nodes** -- do NOT connect them to any existing nodes. Place them at positions [600, 2400] and [1000, 2400].
**Node 1: Callback Token Encoder**
id: "code-callback-token-encoder"
name: "Callback Token Encoder"
Position: [600, 2400]
Implement:
1. **Token store initialization** using static data JSON serialization:
```javascript
const staticData = $getWorkflowStaticData('global');
if (!staticData._callbackTokens) {
staticData._callbackTokens = JSON.stringify({});
}
const tokenStore = JSON.parse(staticData._callbackTokens);
```
2. **encodeToken(unraidId)** async function that:
- Generates SHA-256 hash of the unraidId string using `crypto.subtle.digest('SHA-256', new TextEncoder().encode(unraidId))`
- Takes first 8 hex characters as the token
- **Collision detection**: if `tokenStore[token]` exists AND `tokenStore[token] !== unraidId`, try next 8 chars from the hash (offset by 8). Repeat up to 7 times (56 chars of SHA-256 = 7 non-overlapping 8-char windows). Throw if all windows collide.
- Stores mapping: `tokenStore[token] = unraidId`
- Persists: `staticData._callbackTokens = JSON.stringify(tokenStore)`
- Returns the 8-char token
3. **Input/output contract:**
- Input: `$input.item.json.unraidId` (string, the 129-char PrefixedID)
- Also accepts optional `$input.item.json.action` (string) for convenience
- Output: `{ token, unraidId, callbackData: "action:{action}:{token}" (if action provided), byteSize }`
- Include byte size validation: if callbackData > 64 bytes, include `warning: "Callback data exceeds 64-byte limit"`
4. Add notes: "UTILITY: Encode Unraid PrefixedID to 8-char callback token. Not connected - Phase 16 will wire this in."
**Node 2: Callback Token Decoder**
id: "code-callback-token-decoder"
name: "Callback Token Decoder"
Position: [1000, 2400]
Implement:
1. **Token store read** (same JSON parse pattern):
```javascript
const staticData = $getWorkflowStaticData('global');
const tokenStore = JSON.parse(staticData._callbackTokens || '{}');
```
2. **decodeToken(token)** function that:
- Looks up token in tokenStore
- If not found: throws `Token not found: ${token}. Token store may have been cleared.`
- Returns the full unraidId (129-char PrefixedID)
3. **Input/output contract:**
- Input: `$input.item.json.token` (string, 8-char token) OR `$input.item.json.callbackData` (string like "action:start:a1b2c3d4")
- If callbackData provided, parse it: split by `:`, extract token from last segment
- Output: `{ token, unraidId, action (if parsed from callbackData) }`
4. Add notes: "UTILITY: Decode 8-char callback token to Unraid PrefixedID. Not connected - Phase 16 will wire this in."
**For both nodes:** Do NOT add any connections. These are standalone utilities.
After adding all nodes, push the updated workflow to n8n using the push recipe from CLAUDE.md.
</action>
<verify>
Run: `python3 -c "
import json
wf = json.load(open('n8n-workflow.json'))
nodes = {n['name']: n for n in wf['nodes']}
enc = nodes.get('Callback Token Encoder')
dec = nodes.get('Callback Token Decoder')
print('Encoder:', 'EXISTS' if enc else 'MISSING')
print('Decoder:', 'EXISTS' if dec else 'MISSING')
if enc:
code = enc['parameters'].get('jsCode', '')
print('Has crypto.subtle:', 'crypto.subtle' in code)
print('Has collision detection:', 'tokenStore[token]' in code)
print('Has _callbackTokens:', '_callbackTokens' in code)
if dec:
code = dec['parameters'].get('jsCode', '')
print('Has decodeToken:', 'decodeToken' in code)
print('Has _callbackTokens:', '_callbackTokens' in code)
# Verify no connections to/from utility nodes
conn = wf.get('connections', {})
for name in ['Container ID Registry', 'Callback Token Encoder', 'Callback Token Decoder']:
has_outgoing = name in conn
has_incoming = any(
any(any(c.get('node') == name for c in group) for group in outputs if group)
for outputs in conn.values()
for outputs in [list(outputs.values()) if isinstance(outputs, dict) else []]
)
print(f'{name} connections: outgoing={has_outgoing}')
print('Total nodes:', len(wf['nodes']))
"`
</verify>
<done>Callback Token Encoder and Decoder Code nodes exist in n8n-workflow.json. Encoder uses SHA-256 hashing with collision detection, produces 8-char tokens. Decoder resolves tokens back to PrefixedIDs. Both use JSON serialization for static data persistence. Neither node is connected to any other nodes. Workflow pushed to n8n.</done>
</task>
</tasks>
<verification>
1. Three new utility Code nodes exist in n8n-workflow.json: Container ID Registry, Callback Token Encoder, Callback Token Decoder
2. All three nodes use `$getWorkflowStaticData('global')` with JSON.parse/JSON.stringify pattern (not deep mutation)
3. None of the three nodes have connections to/from other nodes (standalone utilities)
4. Container ID Registry handles: updateRegistry(containers), getUnraidId(name), getContainerByName(name)
5. Token Encoder handles: encodeToken(unraidId) with SHA-256 + collision detection
6. Token Decoder handles: decodeToken(token) with error handling for missing tokens
7. Workflow JSON is valid and pushed to n8n
</verification>
<success_criteria>
- Container ID Registry node maps container names to Unraid PrefixedID format (129-char)
- Callback token encoding produces 8-char hex tokens that fit within Telegram's 64-byte callback_data limit
- Token collision detection prevents wrong-container scenarios
- All static data uses JSON serialization (top-level assignment) per CLAUDE.md convention
- Three standalone utility nodes ready for Phase 16 to wire in
</success_criteria>
<output>
After completion, create `.planning/phases/15-infrastructure-foundation/15-01-SUMMARY.md`
</output>