Phase 03: Container Actions - 4 plans in 4 waves (sequential due to shared workflow file) - Ready for execution
8.8 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-container-actions | 01 | execute | 1 |
|
true |
|
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.
<execution_context> @/home/luc/.claude/get-shit-done/workflows/execute-plan.md @/home/luc/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Add action command routing to workflow n8n-workflow.json Extend the existing "Route Message" Switch node to detect action commands.Add new routes for patterns:
- "start " → action branch
- "stop " → action branch
- "restart " → action branch
The route should match case-insensitively and capture the container name portion.
Add a Code node after the route that:
- Parses the action type (start/stop/restart) from message text
- Parses the container name from message text
- Returns: { action, containerQuery, chatId, messageId }
Example parsing:
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
}
};
-
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'
- Same as existing status query:
-
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 matchesaction: preserved from inputchatId: preserved from input
- Reuse the fuzzy matching logic from Phase 2:
-
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
- Route based on matchCount:
-
Execute Action (Execute Command node on "Single Match" branch):
- Build curl command based on action:
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 } }; -
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
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}` } }; - Handle HTTP response codes:
-
Send Response (Telegram Send Message node):
- Chat ID:
{{ $json.chatId }} - Text:
{{ $json.message }} - Parse Mode: HTML
- Chat ID:
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.
- Start a stopped container: "start [container-name]" → should start and report success
- Stop a running container: "stop [container-name]" → should stop and report success
- Restart a container: "restart [container-name]" → should restart and report success
- Try with partial name (fuzzy match): "restart plex" for container named "plex-server" → should work Single-match container actions execute via Docker API and report results to Telegram
-
Docker List Error (after Docker List Containers):
- Add IF node to check for curl errors
- On error: Send diagnostic message to user
const hasError = !$json.stdout || $json.stdout.trim() === '' || !$json.stdout.startsWith('['); return { json: { hasError, errorDetail: $json.stderr || 'Empty response from Docker API' } }; -
Execute Action Error (after Execute Command):
- The parse result node already handles non-success codes
- Add stderr check for curl-level failures:
if ($json.stderr && $json.stderr.trim()) { return { json: { success: false, message: `Docker error: ${$json.stderr}` } }; } -
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.
- Try to stop an already-stopped container → should report success (304 treated as success)
- Try to start an already-running container → should report success (304 treated as success)
- Try action on non-existent container → should report "No container found" All error cases report diagnostic details to user, no silent failures
- "start [stopped-container]" → Container starts, user sees "started successfully"
- "stop [running-container]" → Container stops, user sees "stopped successfully"
- "restart [any-container]" → Container restarts, user sees "restarted successfully"
- "stop [already-stopped]" → User sees success (not error)
- "stop nonexistent" → User sees "No container found matching 'nonexistent'"
- "stop arr" (matches sonarr, radarr, lidarr) → User sees placeholder about multiple matches
Import updated workflow into n8n and verify all scenarios via Telegram.
<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>