Phase 03: Container Actions - 4 plans in 4 waves (sequential due to shared workflow file) - Ready for execution
11 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 | 03 | execute | 3 |
|
|
true |
|
Purpose: When a user's query matches multiple containers (e.g., "stop arr" matches sonarr, radarr, lidarr), show a confirmation with the list and an inline button. Per CONTEXT.md, batch actions require confirmation before execution.
Output: Extended n8n workflow with batch confirmation and sequential execution.
<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/03-container-actions/03-02-SUMMARY.md @n8n-workflow.json Task 1: Build batch confirmation message with inline keyboard n8n-workflow.json Replace the placeholder "Multiple Matches" branch from Plan 03-01:-
Build Batch Keyboard (Code node):
const matches = $json.matches; const action = $json.action; const chatId = $json.chatId; const query = $json.containerQuery; // List matched container names const names = matches.map(m => m.Names[0].replace(/^\//, '')); const shortIds = matches.map(m => m.Id.substring(0, 12)); // Build callback_data - must be ≤64 bytes // For batch: a=action code, c=array of short IDs, t=timestamp const actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r'; const timestamp = Date.now(); // Check size - if too many containers, callback_data might exceed 64 bytes // Each short ID is 12 chars, plus overhead. Max ~3-4 containers safely let callbackData; if (shortIds.length <= 4) { callbackData = JSON.stringify({ a: actionCode, c: shortIds, t: timestamp }); } else { // Too many containers - use abbreviated approach or split // For now, limit to first 4 and note limitation callbackData = JSON.stringify({ a: actionCode, c: shortIds.slice(0, 4), t: timestamp }); } // Format container list const listText = names.map((n, i) => ` • ${n}`).join('\n'); return { json: { chat_id: chatId, text: `Found <b>${matches.length}</b> containers matching '<b>${query}</b>':\n\n${listText}\n\n${action.charAt(0).toUpperCase() + action.slice(1)} all?`, parse_mode: "HTML", reply_markup: { inline_keyboard: [ [ { text: `Yes, ${action} ${matches.length} containers`, callback_data: callbackData }, { text: "Cancel", callback_data: '{"a":"x"}' } ] ] }, // Store full data for potential later use _meta: { action, containers: matches, timestamp } } }; -
Send Batch Confirmation (HTTP Request node):
- Method: POST
- URL:
https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage - Body Content Type: JSON
- Body:
{{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}
-
"stop arr" when sonarr, radarr, lidarr exist → shows confirmation with list
-
Verify button text shows "Yes, stop 3 containers"
-
Both buttons are visible and clickable Multiple matches show batch confirmation message with inline buttons
-
Update Parse Callback Data (modify existing Code node): Add detection for batch vs single suggestion:
// Existing code from Plan 03-02... // Detect batch (c is array vs single string) const isBatch = Array.isArray(data.c); const containerIds = isBatch ? data.c : [data.c].filter(Boolean); return { json: { queryId, chatId, messageId, action, containerIds, // Array for batch support containerId: containerIds[0], // For single-container compat expired: isExpired, isBatch, isCancel: action === 'cancel' } }; -
Route for Batch (update Switch node): Add rule before single execution:
- Rule:
{{ $json.isBatch }}equals true AND not cancel AND not expired → Batch Execute branch
- Rule:
-
Batch Execute (Code node that builds commands):
const containerIds = $json.containerIds; const action = $json.action; const timeout = (action === 'stop' || action === 'restart') ? '?t=10' : ''; // Build array of commands const commands = containerIds.map(id => ({ cmd: `curl -s -o /dev/null -w "%{http_code}" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${id}/${action}${timeout}'`, containerId: id })); return { json: { commands, action, queryId: $json.queryId, chatId: $json.chatId, messageId: $json.messageId, totalCount: containerIds.length } }; -
Execute Batch Loop (use n8n's SplitInBatches or loop approach):
Option A - Sequential Execute (simpler):
// In n8n, use a Code node that executes sequentially const { execSync } = require('child_process'); const commands = $json.commands; const results = []; for (const { cmd, containerId } of commands) { try { const output = execSync(cmd, { encoding: 'utf8' }).trim(); const statusCode = parseInt(output); results.push({ containerId, success: statusCode === 204 || statusCode === 304, statusCode }); } catch (err) { results.push({ containerId, success: false, error: err.message }); } } const successCount = results.filter(r => r.success).length; const failCount = results.length - successCount; return { json: { results, successCount, failCount, totalCount: results.length, action: $json.action, queryId: $json.queryId, chatId: $json.chatId, messageId: $json.messageId } };NOTE: Using execSync in n8n Code node requires allowedModules in n8n settings. If not available, use multiple Execute Command nodes with SplitInBatches node.
-
Format Batch Result (Code node):
const { successCount, failCount, totalCount, action } = $json; const verb = action === 'start' ? 'started' : action === 'stop' ? 'stopped' : 'restarted'; let message; if (failCount === 0) { message = `Successfully ${verb} ${successCount} container${successCount > 1 ? 's' : ''}`; } else if (successCount === 0) { message = `Failed to ${action} all ${totalCount} containers`; } else { message = `${verb.charAt(0).toUpperCase() + verb.slice(1)} ${successCount}/${totalCount} containers (${failCount} failed)`; } return { json: { message, chatId: $json.chatId, queryId: $json.queryId, messageId: $json.messageId } };
-
Answer Callback Query (Telegram node or HTTP Request):
- Query ID:
{{ $json.queryId }} - Text: (empty or brief toast)
- Show Alert: false
- Query ID:
-
Delete Confirmation Message (HTTP Request node):
- POST to
https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage - Body:
{ "chat_id": {{ $json.chatId }}, "message_id": {{ $json.messageId }} }
- POST to
-
Send Result Message (Telegram Send Message):
- Chat ID:
{{ $json.chatId }} - Text:
{{ $json.message }} - Parse Mode: HTML
- Chat ID:
Ensure the flow is:
- User clicks confirm → callback query answered (removes loading state)
- Confirmation message deleted
- Result message sent
This keeps the chat clean - only the result remains, not the intermediate confirmation.
- Click confirm → confirmation message disappears
- Result message appears with count
- No duplicate messages
- Click cancel → confirmation message disappears, no result message UI cleaned up after batch action, only result message remains
- "stop arr" (matches 3 containers) → confirmation with list → click confirm → all stop, "Successfully stopped 3 containers"
- "restart arr" → confirmation → click confirm → all restart
- "stop arr" → confirmation → click cancel → confirmation deleted, no action
- "stop arr" → wait 2+ minutes → click → "expired"
- Single container match (e.g., "stop plex") → still works (no confirmation, direct execution)
- Suggestion flow (e.g., "stop plx") → still works (single suggestion button)
Import updated workflow and test all scenarios.
<success_criteria>
- Multiple matches show batch confirmation with container list
- Confirm button executes all containers in sequence
- Cancel button dismisses without action
- Expired confirmations handled gracefully
- Success message shows accurate count
- Partial failures reported correctly
- UI cleaned up after action (confirmation deleted) </success_criteria>