From dd30a899aa41d16789e876ec2dee10329a395918 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 8 Feb 2026 15:39:56 -0500 Subject: [PATCH] docs: add CLAUDE.md project instructions and debug analysis Co-Authored-By: Claude Opus 4.6 --- .../debug/keyword-router-debug-routing.md | 100 ++++++++++++ CLAUDE.md | 151 ++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 .planning/debug/keyword-router-debug-routing.md create mode 100644 CLAUDE.md diff --git a/.planning/debug/keyword-router-debug-routing.md b/.planning/debug/keyword-router-debug-routing.md new file mode 100644 index 0000000..f50d56e --- /dev/null +++ b/.planning/debug/keyword-router-debug-routing.md @@ -0,0 +1,100 @@ +--- +status: diagnosed +trigger: "Investigate why /debug status is treated as container status search instead of being ignored" +created: 2026-02-08T00:00:00Z +updated: 2026-02-08T00:00:00Z +--- + +## Current Focus + +hypothesis: "contains status" rule at index 1 matches "/debug status" because the check is a simple substring match +test: Read Keyword Router rules and verify rule ordering +expecting: No startsWith rules for /debug or /errors that would intercept before the contains rules +next_action: Document root cause + +## Symptoms + +expected: Removed debug commands (/debug status, /errors) should be ignored or fall through to unrecognized input handler +actual: "/debug status" is treated as a container status search for "/debug"; "/errors" shows commands menu (fallback) +errors: N/A (functional misbehavior, not an error) +reproduction: Send "/debug status" or "/errors" to the Telegram bot +started: After debug commands were removed in phase 10.2 + +## Eliminated + +(none needed - root cause found on first pass) + +## Evidence + +- timestamp: 2026-02-08T00:00:00Z + checked: Keyword Router rules (lines 156-371 of n8n-workflow.json) + found: | + 9 rules in this order (0-indexed): + 0: /start -> startsWith "/start" -> output "menu" -> Show Menu + 1: status -> contains "status" -> output "status" -> Prepare Status Input + 2: restart -> contains "restart" -> output "restart" -> Detect Batch Command + 3: start -> contains "start" -> output "start" -> Detect Batch Command + 4: stop -> contains "stop" -> output "stop" -> Detect Batch Command + 5: update all -> regex "update.?all|updateall" -> output "updateall" -> Get All Containers For Update All + 6: update -> contains "update" -> output "update" -> Detect Batch Command + 7: logs -> contains "logs" -> output "logs" -> Parse Logs Command + 8: list -> contains "list" -> output "status" -> Prepare Status Input + Fallback: "extra" (index 9) -> Show Menu + implication: | + No rules exist for /debug or /errors. These are NOT intercepted by any startsWith rule. + The n8n Switch node evaluates rules top-to-bottom and routes to the FIRST match. + +- timestamp: 2026-02-08T00:00:00Z + checked: How "/debug status" is routed + found: | + Rule 0 (/start startsWith): "/debug status" does NOT start with "/start" -> skip + Rule 1 (status contains): "/debug status" DOES contain "status" -> MATCH + Routes to output index 1 -> Prepare Status Input + The text "/debug status" is sent to the status sub-workflow which interprets "/debug" as a container search query. + implication: This is the confirmed root cause for "/debug status" misbehavior. + +- timestamp: 2026-02-08T00:00:00Z + checked: How "/errors" is routed + found: | + Rule 0-8: "/errors" does NOT match any of: startsWith /start, contains status, contains restart, + contains start, contains stop, regex update.?all, contains update, contains logs, contains list + -> Falls through all rules to fallback + Fallback output (index 9) -> Show Menu + implication: "/errors" hits the fallback which shows the commands menu. This is the "extra" output. + +- timestamp: 2026-02-08T00:00:00Z + checked: Connection map for Keyword Router (lines 5243-5315) + found: | + Output index mapping: + 0 (menu /start) -> Show Menu + 1 (status) -> Prepare Status Input + 2 (restart) -> Detect Batch Command + 3 (start) -> Detect Batch Command + 4 (stop) -> Detect Batch Command + 5 (updateall) -> Get All Containers For Update All + 6 (update) -> Detect Batch Command + 7 (logs) -> Parse Logs Command + 8 (list) -> Prepare Status Input + 9 (fallback/extra) -> Show Menu + implication: Fallback goes to Show Menu, which is why /errors shows the commands menu. + +## Resolution + +root_cause: | + The Keyword Router has no rules to intercept /debug or /errors commands (which were removed in phase 10.2). + + 1. "/debug status" matches rule index 1 ("contains status") because the substring "status" appears in the input. + This routes to Prepare Status Input, which treats "/debug" as a container name query — resulting in a + container status search for a non-existent container called "/debug". + + 2. "/errors" matches NO rules and falls through to the fallback output ("extra"), which is connected + to Show Menu — resulting in the commands menu being displayed. + + The fundamental issue: there are no startsWith rules for "/debug" or "/errors" that would catch these + inputs BEFORE the generic "contains" rules match substrings within them. Per CLAUDE.md conventions: + "startsWith rules (e.g., /debug, /errors) must come BEFORE generic contains rules, otherwise /debug status + matches contains 'status' first." + +fix: (read-only investigation - not applied) +verification: (read-only investigation - not applied) +files_changed: [] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..db11755 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,151 @@ +# Claude Code Instructions — Unraid Docker Manager + +## n8n API Access + +Credentials are stored in `.env.n8n-api` (gitignored). Contains `N8N_HOST` and `N8N_API_KEY`. + +**IMPORTANT: Each Bash tool call is a fresh shell.** You must source the env file in the SAME command chain as the curl call. Variables do not persist across Bash calls. + +### Loading credentials + +```bash +. .env.n8n-api; curl -s ... +``` + +Never do this (variables are lost between calls): +```bash +# WRONG - separate Bash calls +source .env.n8n-api # Call 1: variables set, then lost +curl ... $N8N_HOST # Call 2: N8N_HOST is empty +``` + +### API response handling + +n8n API responses for workflow GETs are very large (400KB+). **Never pipe curl directly to `python3 -c`** — it fails silently. Always save to a temp file first: + +```bash +. .env.n8n-api; curl -s -o /tmp/n8n-result.txt -w "%{http_code}" \ + "${N8N_HOST}/api/v1/workflows/${WF_ID}" \ + -H "X-N8N-API-KEY: ${N8N_API_KEY}" \ + && python3 -c " +import json +with open('/tmp/n8n-result.txt') as f: + d = json.load(f) +print(d.get('id'), d.get('updatedAt'), d.get('active')) +" +``` + +### Pushing workflows (PUT) + +The `active` field is **read-only** — do NOT include it in the PUT body or you get HTTP 400. + +```bash +. .env.n8n-api + +# Prepare payload (strip active field, keep nodes/connections/settings) +python3 -c " +import json +with open('n8n-workflow.json') as f: + wf = json.load(f) +payload = { + 'name': wf.get('name', 'Docker Manager'), + 'nodes': wf['nodes'], + 'connections': wf['connections'], + 'settings': wf.get('settings', {}), +} +if wf.get('staticData'): + payload['staticData'] = wf['staticData'] +with open('/tmp/n8n-push-payload.json', 'w') as f: + json.dump(payload, f) +" + +# Push via PUT +curl -s -o /tmp/n8n-push-result.txt -w "%{http_code}" \ + -X PUT "${N8N_HOST}/api/v1/workflows/${WF_ID}" \ + -H "X-N8N-API-KEY: ${N8N_API_KEY}" \ + -H "Content-Type: application/json" \ + -d @/tmp/n8n-push-payload.json +``` + +### Workflow IDs + +| Workflow | File | n8n ID | +|----------|------|--------| +| Main (Docker Manager) | n8n-workflow.json | `HmiXBlJefBRPMS0m4iNYc` | +| Container Update | n8n-update.json | `7AvTzLtKXM2hZTio92_mC` | +| Container Actions | n8n-actions.json | `fYSZS5PkH0VSEaT5` | +| Container Logs | n8n-logs.json | `oE7aO2GhbksXDEIw` | +| Batch UI | n8n-batch-ui.json | `ZJhnGzJT26UUmW45` | +| Container Status | n8n-status.json | `lqpg2CqesnKE2RJQ` | +| Confirmation | n8n-confirmation.json | `fZ1hu8eiovkCk08G` | +| Matching | n8n-matching.json | `kL4BoI8ITSP9Oxek` | + +### Push all workflows (copy-paste recipe) + +```bash +. .env.n8n-api + +push_workflow() { + local FILE=$1 WF_ID=$2 WF_NAME=$3 + python3 -c " +import json +with open('$FILE') as f: + wf = json.load(f) +payload = {'name': wf.get('name', '$WF_NAME'), 'nodes': wf['nodes'], 'connections': wf['connections'], 'settings': wf.get('settings', {})} +if wf.get('staticData'): payload['staticData'] = wf['staticData'] +with open('/tmp/n8n-push-payload.json', 'w') as f: + json.dump(payload, f) +" + local CODE=$(curl -s -o /tmp/n8n-push-result.txt -w "%{http_code}" \ + -X PUT "${N8N_HOST}/api/v1/workflows/${WF_ID}" \ + -H "X-N8N-API-KEY: ${N8N_API_KEY}" \ + -H "Content-Type: application/json" \ + -d @/tmp/n8n-push-payload.json) + echo " ${WF_NAME}: HTTP ${CODE}" +} + +push_workflow "n8n-workflow.json" "HmiXBlJefBRPMS0m4iNYc" "Main" +push_workflow "n8n-update.json" "7AvTzLtKXM2hZTio92_mC" "Update" +push_workflow "n8n-actions.json" "fYSZS5PkH0VSEaT5" "Actions" +push_workflow "n8n-logs.json" "oE7aO2GhbksXDEIw" "Logs" +push_workflow "n8n-batch-ui.json" "ZJhnGzJT26UUmW45" "Batch UI" +push_workflow "n8n-status.json" "lqpg2CqesnKE2RJQ" "Status" +push_workflow "n8n-confirmation.json" "fZ1hu8eiovkCk08G" "Confirmation" +push_workflow "n8n-matching.json" "kL4BoI8ITSP9Oxek" "Matching" +``` + +### Common API endpoints + +``` +GET /api/v1/workflows — List all workflows +GET /api/v1/workflows/{id} — Get workflow by ID +PUT /api/v1/workflows/{id} — Update workflow (no `active` field!) +POST /api/v1/workflows/{id}/activate — Activate workflow +POST /api/v1/workflows/{id}/deactivate — Deactivate workflow +``` + +## Project Structure + +- `n8n-workflow.json` — Main n8n workflow (Telegram bot entry point) +- `n8n-*.json` — Sub-workflows (7 total, see table above) +- `.planning/` — GSD planning directory (STATE.md, ROADMAP.md, phases/) +- `DEPLOY-SUBWORKFLOWS.md` — Architecture docs, contracts, node analysis +- `.env.n8n-api` — n8n API credentials (gitignored) + +## n8n Workflow Conventions + +- **typeVersion 1.2** for Execute Workflow nodes: `"workflowId": { "__rl": true, "mode": "list", "value": "" }` +- **Docker API success**: 204 No Content = success (empty response body). Check `!response.message && !response.error` +- **Data chain pattern**: Use `$('Node Name').item.json` to reference data across async nodes. Do NOT rely on `$json` after Telegram API calls (response overwrites data). +- **Dynamic input pattern**: Use `$input.item.json` for nodes with multiple predecessors. +- **Telegram credential**: ID `I0xTTiASl7C1NZhJ`, name "Telegram account" +- **Static data persistence**: `$getWorkflowStaticData('global')` only tracks **top-level** property changes. Deep nested mutations are silently lost. Always use JSON serialization: + ```javascript + // READ + const errorLog = JSON.parse(staticData._errorLog || '{}'); + // MODIFY + errorLog.debug.enabled = true; + // WRITE (top-level assignment — this is what n8n actually persists) + staticData._errorLog = JSON.stringify(errorLog); + ``` +- **Keyword Router rule ordering**: `startsWith` rules (e.g., `/debug`, `/errors`) must come BEFORE generic `contains` rules (e.g., `status`, `start`), otherwise `/debug status` matches `contains "status"` first. Connection array indices must match rule indices, with fallback as the last slot.