docs: add CLAUDE.md project instructions and debug analysis
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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: []
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user