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

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
15-infrastructure-foundation 01 execute 1
n8n-workflow.json
true
truths artifacts key_links
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
path provides contains
n8n-workflow.json Container ID Registry, Callback Token Encoder, Callback Token Decoder utility nodes _containerIdMap
from to via pattern
Container ID Registry node static data _containerIdMap JSON.parse/JSON.stringify pattern JSON.parse(.*_containerIdMap
from to via pattern
Callback Token Encoder node static data _callbackTokens SHA-256 hash + JSON serialization crypto.subtle.digest
from to via pattern
Callback Token Decoder node static data _callbackTokens JSON.parse lookup _callbackTokens
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.

<execution_context> @/home/luc/.claude/get-shit-done/workflows/execute-plan.md @/home/luc/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Add Container ID Registry utility node n8n-workflow.json 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):
const registry = $getWorkflowStaticData('global');
if (!registry._containerIdMap) {
  registry._containerIdMap = JSON.stringify({});
}
const containerMap = JSON.parse(registry._containerIdMap);
  1. 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
  2. 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
  3. getContainerByName(containerName) function that:

    • Same lookup as getUnraidId but returns the full entry object
    • Returns { name, unraidId }
  4. 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. 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)" 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.
Task 2: Add Callback Token Encoder and Decoder utility nodes n8n-workflow.json 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:
const staticData = $getWorkflowStaticData('global');
if (!staticData._callbackTokens) {
  staticData._callbackTokens = JSON.stringify({});
}
const tokenStore = JSON.parse(staticData._callbackTokens);
  1. 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
  2. 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"
  3. 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):
const staticData = $getWorkflowStaticData('global');
const tokenStore = JSON.parse(staticData._callbackTokens || '{}');
  1. 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)
  2. 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) }
  3. 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. 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'])) "` 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.

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

<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>
After completion, create `.planning/phases/15-infrastructure-foundation/15-01-SUMMARY.md`