docs(03): create phase plan
Phase 03: Container Actions - 4 plans in 4 waves (sequential due to shared workflow file) - Ready for execution
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user