docs: add CLAUDE.md project instructions and debug analysis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-02-08 15:39:56 -05:00
parent f86ed83d00
commit dd30a899aa
2 changed files with 251 additions and 0 deletions
@@ -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: []
+151
View File
@@ -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": "<id>" }`
- **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.