Files
unraid-docker-manager/.planning/phases/03-container-actions/03-01-PLAN.md
T
Lucas Berger 893412f405 docs(03): create phase plan
Phase 03: Container Actions
- 4 plans in 4 waves (sequential due to shared workflow file)
- Ready for execution
2026-01-29 21:48:58 -05:00

235 lines
8.8 KiB
Markdown

---
phase: 03-container-actions
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: [n8n-workflow.json]
autonomous: true
must_haves:
truths:
- "User can start a stopped container by name"
- "User can stop a running container by name"
- "User can restart a container by name"
- "Single container matches execute immediately without confirmation"
artifacts:
- path: "n8n-workflow.json"
provides: "Action routing and Docker API POST calls"
contains: "Route Action Message"
key_links:
- from: "Switch node (Route Message)"
to: "Action routing branch"
via: "contains start/stop/restart"
pattern: "start|stop|restart"
- from: "Execute Command node"
to: "Docker API"
via: "curl POST to /containers/{id}/start|stop|restart"
pattern: "-X POST.*containers"
---
<objective>
Implement basic container actions (start, stop, restart) for single-container matches.
Purpose: Enable users to control containers through Telegram by sending commands like "start plex" or "stop sonarr". When exactly one container matches, the action executes immediately.
Output: Extended n8n workflow with action command routing and Docker API POST calls.
</objective>
<execution_context>
@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
@/home/luc/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-container-actions/03-CONTEXT.md
@.planning/phases/03-container-actions/03-RESEARCH.md
@.planning/phases/02-docker-integration/02-02-SUMMARY.md
@n8n-workflow.json
</context>
<tasks>
<task type="auto">
<name>Task 1: Add action command routing to workflow</name>
<files>n8n-workflow.json</files>
<action>
Extend the existing "Route Message" Switch node to detect action commands.
Add new routes for patterns:
- "start <name>" → action branch
- "stop <name>" → action branch
- "restart <name>" → action branch
The route should match case-insensitively and capture the container name portion.
Add a Code node after the route that:
1. Parses the action type (start/stop/restart) from message text
2. Parses the container name from message text
3. Returns: { action, containerQuery, chatId, messageId }
Example parsing:
```javascript
const text = $json.message.text.toLowerCase().trim();
const match = text.match(/^(start|stop|restart)\s+(.+)$/i);
if (!match) {
return { json: { error: 'Invalid action format' } };
}
return {
json: {
action: match[1].toLowerCase(),
containerQuery: match[2].trim(),
chatId: $json.message.chat.id,
messageId: $json.message.message_id
}
};
```
</action>
<verify>Send "start test" to bot - should route to action branch (may fail on execution, but routing works)</verify>
<done>Action commands route to dedicated branch, action and container name are parsed</done>
</task>
<task type="auto">
<name>Task 2: Implement container matching and action execution</name>
<files>n8n-workflow.json</files>
<action>
After the action parsing node, add nodes to:
1. **Docker List Containers** (Execute Command node):
- Same as existing status query: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'`
2. **Match Container** (Code node):
- Reuse the fuzzy matching logic from Phase 2:
- Case-insensitive substring match
- Strip common prefixes (linuxserver-, binhex-)
- Return match results:
- `matches`: array of matching containers (Id, Name, State)
- `matchCount`: number of matches
- `action`: preserved from input
- `chatId`: preserved from input
3. **Check Match Count** (Switch node):
- Route based on matchCount:
- 0 matches → "No Match" branch
- 1 match → "Single Match" branch (execute action)
- >1 matches → "Multiple Matches" branch
4. **Execute Action** (Execute Command node on "Single Match" branch):
- Build curl command based on action:
```javascript
const containerId = $json.matches[0].Id;
const action = $json.action;
// stop and restart use ?t=10 for graceful timeout
const timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';
const cmd = `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;
return { json: { cmd, containerId, action, containerName: $json.matches[0].Name } };
```
5. **Parse Result** (Code node):
- Handle HTTP response codes:
- 204: Success
- 304: Already in state (also success for user)
- 404: Container not found (shouldn't happen after match)
- 500: Docker error
```javascript
const statusCode = parseInt($json.stdout.trim());
const containerName = $('Execute Action').first().json.containerName.replace(/^\//, '');
const action = $('Match Container').first().json.action;
if (statusCode === 204 || statusCode === 304) {
const verb = action === 'start' ? 'started' :
action === 'stop' ? 'stopped' : 'restarted';
return { json: { success: true, message: `${containerName} ${verb} successfully` } };
}
return { json: { success: false, message: `Failed to ${action} ${containerName}: HTTP ${statusCode}` } };
```
6. **Send Response** (Telegram Send Message node):
- Chat ID: `{{ $json.chatId }}`
- Text: `{{ $json.message }}`
- Parse Mode: HTML
For "No Match" and "Multiple Matches" branches, add placeholder Send Message nodes:
- No Match: "No container found matching '{{ $json.containerQuery }}'"
- Multiple Matches: "Found {{ $json.matchCount }} containers matching '{{ $json.containerQuery }}'. Confirmation required."
These placeholders will be replaced with proper callback flows in Plan 03-02.
</action>
<verify>
1. Start a stopped container: "start [container-name]" → should start and report success
2. Stop a running container: "stop [container-name]" → should stop and report success
3. Restart a container: "restart [container-name]" → should restart and report success
4. Try with partial name (fuzzy match): "restart plex" for container named "plex-server" → should work
</verify>
<done>Single-match container actions execute via Docker API and report results to Telegram</done>
</task>
<task type="auto">
<name>Task 3: Handle action errors gracefully</name>
<files>n8n-workflow.json</files>
<action>
Add error handling throughout the action flow:
1. **Docker List Error** (after Docker List Containers):
- Add IF node to check for curl errors
- On error: Send diagnostic message to user
```javascript
const hasError = !$json.stdout || $json.stdout.trim() === '' || !$json.stdout.startsWith('[');
return { json: { hasError, errorDetail: $json.stderr || 'Empty response from Docker API' } };
```
2. **Execute Action Error** (after Execute Command):
- The parse result node already handles non-success codes
- Add stderr check for curl-level failures:
```javascript
if ($json.stderr && $json.stderr.trim()) {
return { json: { success: false, message: `Docker error: ${$json.stderr}` } };
}
```
3. **Error Response Format** (per CONTEXT.md - diagnostic details):
- Include actual error info in messages
- Example: "Failed to stop plex: HTTP 500 - Container is not running"
- Don't hide technical details from user
Ensure all error paths eventually reach a Send Message node so the user always gets feedback.
</action>
<verify>
1. Try to stop an already-stopped container → should report success (304 treated as success)
2. Try to start an already-running container → should report success (304 treated as success)
3. Try action on non-existent container → should report "No container found"
</verify>
<done>All error cases report diagnostic details to user, no silent failures</done>
</task>
</tasks>
<verification>
End-to-end verification:
1. "start [stopped-container]" → Container starts, user sees "started successfully"
2. "stop [running-container]" → Container stops, user sees "stopped successfully"
3. "restart [any-container]" → Container restarts, user sees "restarted successfully"
4. "stop [already-stopped]" → User sees success (not error)
5. "stop nonexistent" → User sees "No container found matching 'nonexistent'"
6. "stop arr" (matches sonarr, radarr, lidarr) → User sees placeholder about multiple matches
Import updated workflow into n8n and verify all scenarios via Telegram.
</verification>
<success_criteria>
- Single-match actions execute immediately without confirmation
- All three actions (start/stop/restart) work correctly
- Fuzzy matching finds containers by partial name
- 204 and 304 responses both treated as success
- Error messages include diagnostic details
- No silent failures - user always gets response
</success_criteria>
<output>
After completion, create `.planning/phases/03-container-actions/03-01-SUMMARY.md`
</output>