328442554c
Restructured as a proper technical architecture document: - Added Observability section (correlation IDs, structured errors, debugging) - Reorganized into logical flow: overview, request flow, contracts, internals - Removed stale rollback/backup references - Updated all references in README, CLAUDE.md, PROJECT.md, STATE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
5.8 KiB
Markdown
152 lines
5.8 KiB
Markdown
# 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/)
|
|
- `ARCHITECTURE.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.
|