893412f405
Phase 03: Container Actions - 4 plans in 4 waves (sequential due to shared workflow file) - Ready for execution
235 lines
8.8 KiB
Markdown
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>
|