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,329 @@
|
||||
---
|
||||
phase: 03-container-actions
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["03-02"]
|
||||
files_modified: [n8n-workflow.json]
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
artifacts:
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Batch confirmation flow with inline buttons"
|
||||
contains: "Multiple Matches"
|
||||
key_links:
|
||||
- from: "Multiple Matches branch"
|
||||
to: "HTTP Request for keyboard"
|
||||
via: "Build confirmation keyboard"
|
||||
pattern: "inline_keyboard.*Yes.*containers"
|
||||
- from: "Callback handler"
|
||||
to: "Batch execution loop"
|
||||
via: "Execute action for each container"
|
||||
pattern: "for.*containers"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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/03-container-actions/03-02-SUMMARY.md
|
||||
@n8n-workflow.json
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Build batch confirmation message with inline keyboard</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Replace the placeholder "Multiple Matches" branch from Plan 03-01:
|
||||
|
||||
1. **Build Batch Keyboard** (Code node):
|
||||
```javascript
|
||||
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 }) }}`
|
||||
</action>
|
||||
<verify>
|
||||
1. "stop arr" when sonarr, radarr, lidarr exist → shows confirmation with list
|
||||
2. Verify button text shows "Yes, stop 3 containers"
|
||||
3. Both buttons are visible and clickable
|
||||
</verify>
|
||||
<done>Multiple matches show batch confirmation message with inline buttons</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Handle batch confirmation callback</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
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:
|
||||
```javascript
|
||||
// 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):
|
||||
```javascript
|
||||
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):
|
||||
```javascript
|
||||
// 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):
|
||||
```javascript
|
||||
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
|
||||
}
|
||||
};
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
1. "stop arr" → confirm → all matching containers stop
|
||||
2. Verify success message shows correct count
|
||||
3. If one container fails, message shows partial success
|
||||
</verify>
|
||||
<done>Batch confirmation executes actions on all matching containers</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Clean up UI after batch action</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
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
|
||||
</verify>
|
||||
<done>UI cleaned up after batch action, only result message remains</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-container-actions/03-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user