Files
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

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
03-02
n8n-workflow.json
true
truths artifacts key_links
Multiple container matches show confirmation with inline buttons
Confirmation shows list of matching containers
User can confirm batch action with single button click
Batch actions execute all matching containers in sequence
path provides contains
n8n-workflow.json Batch confirmation flow with inline buttons Multiple Matches
from to via pattern
Multiple Matches branch HTTP Request for keyboard Build confirmation keyboard inline_keyboard.*Yes.*containers
from to via pattern
Callback handler Batch execution loop Execute action for each container for.*containers
Implement batch confirmation flow for actions matching multiple containers.

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:
  1. 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
        }
      }
    };
    
  2. 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 }) }}
  3. "stop arr" when sonarr, radarr, lidarr exist → shows confirmation with list

  4. Verify button text shows "Yes, stop 3 containers"

  5. Both buttons are visible and clickable Multiple matches show batch confirmation message with inline buttons

Task 2: Handle batch confirmation callback n8n-workflow.json Extend the callback handler from Plan 03-02 to handle batch confirmations:
  1. 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'
      }
    };
    
  2. 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
  3. 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
      }
    };
    
  4. 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.

  5. 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
      }
    };
    
1. "stop arr" → confirm → all matching containers stop 2. Verify success message shows correct count 3. If one container fails, message shows partial success Batch confirmation executes actions on all matching containers Task 3: Clean up UI after batch action n8n-workflow.json After batch execution, clean up the Telegram UI:
  1. Answer Callback Query (Telegram node or HTTP Request):

    • Query ID: {{ $json.queryId }}
    • Text: (empty or brief toast)
    • Show Alert: false
  2. 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 }} }
  3. Send Result Message (Telegram Send Message):

    • Chat ID: {{ $json.chatId }}
    • Text: {{ $json.message }}
    • Parse Mode: HTML

Ensure the flow is:

  1. User clicks confirm → callback query answered (removes loading state)
  2. Confirmation message deleted
  3. Result message sent

This keeps the chat clean - only the result remains, not the intermediate confirmation.

  1. Click confirm → confirmation message disappears
  2. Result message appears with count
  3. No duplicate messages
  4. Click cancel → confirmation message disappears, no result message UI cleaned up after batch action, only result message remains
End-to-end batch confirmation verification:
  1. "stop arr" (matches 3 containers) → confirmation with list → click confirm → all stop, "Successfully stopped 3 containers"
  2. "restart arr" → confirmation → click confirm → all restart
  3. "stop arr" → confirmation → click cancel → confirmation deleted, no action
  4. "stop arr" → wait 2+ minutes → click → "expired"
  5. Single container match (e.g., "stop plex") → still works (no confirmation, direct execution)
  6. 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>
After completion, create `.planning/phases/03-container-actions/03-03-SUMMARY.md`