docs(08): create inline keyboard infrastructure plans
Phase 8: Inline Keyboard Infrastructure - 3 plans in 3 waves (sequential dependency) - Plan 01: Container list keyboard and submenu navigation - Plan 02: Action execution and confirmation flow - Plan 03: Progress feedback and completion messages Covers KEY-01 through KEY-05 requirements. Ready for execution. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,11 +63,18 @@ Plans:
|
||||
|
||||
**Requirements:** KEY-01, KEY-02, KEY-03, KEY-04, KEY-05
|
||||
|
||||
**Plans:** 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 08-01-PLAN.md — Container list keyboard and submenu navigation
|
||||
- [ ] 08-02-PLAN.md — Action execution and confirmation flow
|
||||
- [ ] 08-03-PLAN.md — Progress feedback and completion messages
|
||||
|
||||
**Success Criteria:**
|
||||
1. Status command returns a message with inline buttons showing available actions per container
|
||||
2. Tapping an action button (start/stop/restart) executes that action on the target container
|
||||
3. Dangerous actions (stop, restart, update) show a confirmation prompt before executing
|
||||
4. During operation execution, the message updates to show progress (e.g., "Stopping plex...")
|
||||
3. Dangerous actions (stop, update) show a confirmation prompt before executing
|
||||
4. During operation execution, the message updates to show progress (e.g., "Updating plex...")
|
||||
5. After action completes, buttons are removed and final status is shown in the message
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
phase: 08-inline-keyboard-infrastructure
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified: [n8n-workflow.json]
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User sees container list with tappable buttons when typing /status"
|
||||
- "Tapping a container name shows submenu with status details and action buttons"
|
||||
- "Pagination works for container lists longer than 6 containers"
|
||||
- "Direct access (/status plex) shows that container's submenu directly"
|
||||
artifacts:
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Container list keyboard and submenu nodes"
|
||||
contains: "Build Container List Keyboard"
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Submenu keyboard builder"
|
||||
contains: "Build Container Submenu"
|
||||
key_links:
|
||||
- from: "Keyword Router (status)"
|
||||
to: "Build Container List Keyboard"
|
||||
via: "workflow connection"
|
||||
pattern: "inline_keyboard"
|
||||
- from: "Route Callback"
|
||||
to: "Build Container Submenu"
|
||||
via: "select: callback routing"
|
||||
pattern: "select:"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the container list inline keyboard and container submenu for Phase 8 Inline Keyboard Infrastructure.
|
||||
|
||||
Purpose: Enable users to interact with containers via tappable buttons. The `/status` command shows a paginated container list with buttons. Tapping a container shows a submenu with status details and action buttons.
|
||||
|
||||
Output: Updated n8n-workflow.json with container list keyboard and submenu nodes wired to existing status flow.
|
||||
</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/08-inline-keyboard-infrastructure/08-CONTEXT.md
|
||||
@.planning/phases/08-inline-keyboard-infrastructure/08-RESEARCH.md
|
||||
@n8n-workflow.json
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add Container List Inline Keyboard</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Modify the status command flow to return an inline keyboard instead of text.
|
||||
|
||||
1. Create a Code node "Build Container List Keyboard" that:
|
||||
- Takes container list from "Get Containers" node
|
||||
- Strips `linuxserver-` prefix from names for display
|
||||
- Groups by state (running containers first)
|
||||
- Builds paginated keyboard (6 containers per page)
|
||||
- Uses callback_data format: `select:{containerName}` for container buttons
|
||||
- Uses `list:{page}` for pagination (e.g., `list:0`, `list:1`)
|
||||
- Each row: one container button with "name — Running/Stopped" text
|
||||
- Navigation row at bottom: "Previous" / "Next" buttons if needed
|
||||
- Returns: `{ chatId, text, reply_markup: { inline_keyboard: [...] } }`
|
||||
|
||||
2. Create an HTTP Request node "Send Container List" that:
|
||||
- Method: POST
|
||||
- URL: `https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage`
|
||||
- Body (JSON):
|
||||
```json
|
||||
{
|
||||
"chat_id": "={{ $json.chatId }}",
|
||||
"text": "={{ $json.text }}",
|
||||
"parse_mode": "HTML",
|
||||
"reply_markup": {{ JSON.stringify($json.reply_markup) }}
|
||||
}
|
||||
```
|
||||
|
||||
3. Wire the flow:
|
||||
- "Get Containers" (status branch) -> "Build Container List Keyboard" -> "Send Container List"
|
||||
- Remove/bypass the old text-only status response for this flow
|
||||
|
||||
4. Handle `/status {name}` direct access:
|
||||
- In "Build Container List Keyboard", check if input has a container name filter
|
||||
- If single container requested, output should route to submenu builder instead
|
||||
- Add output pin for "single container" case
|
||||
|
||||
Code template for keyboard builder:
|
||||
```javascript
|
||||
const containers = $input.all().map(item => item.json);
|
||||
const chatId = $('IF User Authenticated').item.json.message.chat.id;
|
||||
const messageText = $('IF User Authenticated').item.json.message.text || '';
|
||||
|
||||
// Check for direct container access: "/status plex" or "status plex"
|
||||
const match = messageText.match(/status\s+(\S+)/i);
|
||||
const filterName = match ? match[1].toLowerCase() : null;
|
||||
|
||||
// If single container requested, find it and route to submenu
|
||||
if (filterName) {
|
||||
const container = containers.find(c => {
|
||||
const name = c.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '').toLowerCase();
|
||||
return name === filterName || name.includes(filterName);
|
||||
});
|
||||
|
||||
if (container) {
|
||||
return {
|
||||
json: {
|
||||
singleContainer: true,
|
||||
containerName: container.Names[0].replace(/^\//, '').replace(/^linuxserver-/, ''),
|
||||
container: container,
|
||||
chatId: chatId
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build paginated list
|
||||
const page = 0; // Default to first page
|
||||
const perPage = 6;
|
||||
const start = page * perPage;
|
||||
|
||||
// Sort: running first, then by name
|
||||
const sorted = containers.sort((a, b) => {
|
||||
if (a.State === 'running' && b.State !== 'running') return -1;
|
||||
if (a.State !== 'running' && b.State === 'running') return 1;
|
||||
const nameA = a.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '');
|
||||
const nameB = b.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '');
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
const pageContainers = sorted.slice(start, start + perPage);
|
||||
const totalPages = Math.ceil(sorted.length / perPage);
|
||||
|
||||
const keyboard = [];
|
||||
|
||||
// Container buttons
|
||||
pageContainers.forEach(container => {
|
||||
const name = container.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '');
|
||||
const state = container.State === 'running' ? 'Running' : 'Stopped';
|
||||
const icon = container.State === 'running' ? '🟢' : '⚪';
|
||||
keyboard.push([{
|
||||
text: `${icon} ${name} — ${state}`,
|
||||
callback_data: `select:${name}`
|
||||
}]);
|
||||
});
|
||||
|
||||
// Navigation row
|
||||
const navRow = [];
|
||||
if (page > 0) {
|
||||
navRow.push({ text: '◀️ Previous', callback_data: `list:${page - 1}` });
|
||||
}
|
||||
if (page < totalPages - 1) {
|
||||
navRow.push({ text: 'Next ▶️', callback_data: `list:${page + 1}` });
|
||||
}
|
||||
if (navRow.length > 0) keyboard.push(navRow);
|
||||
|
||||
return {
|
||||
json: {
|
||||
singleContainer: false,
|
||||
chatId: chatId,
|
||||
text: `<b>Containers</b> (${start + 1}-${Math.min(start + perPage, sorted.length)} of ${sorted.length})\n\nTap a container to manage it:`,
|
||||
reply_markup: { inline_keyboard: keyboard }
|
||||
}
|
||||
};
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
1. Load workflow in n8n UI
|
||||
2. Send "/status" to bot
|
||||
3. Verify: Response is inline keyboard (not text)
|
||||
4. Verify: Containers shown with status icons
|
||||
5. Verify: Tapping button shows callback in n8n logs (even if not handled yet)
|
||||
</verify>
|
||||
<done>
|
||||
- /status returns inline keyboard with container list
|
||||
- Each container is a tappable button with name and state
|
||||
- Running containers shown first with green icon
|
||||
- Pagination navigation appears when >6 containers
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add Container Submenu with Action Buttons</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add routing and nodes to handle `select:{name}` callbacks and display container submenu.
|
||||
|
||||
1. Update "Parse Callback Data" code node to recognize `select:` prefix:
|
||||
- If callback_data starts with `select:`, extract container name
|
||||
- Set `isSelect: true` and `containerName: extractedName`
|
||||
- Pass through to routing
|
||||
|
||||
2. Add new output to "Route Callback" switch node:
|
||||
- New rule: `isSelect === true` -> output "select"
|
||||
- Position it before the fallback output
|
||||
|
||||
3. Create Code node "Prepare Container Fetch" that:
|
||||
- Takes parsed callback data with containerName
|
||||
- Outputs container name and callback context for Docker API call
|
||||
- Preserves queryId, chatId, messageId for response
|
||||
|
||||
4. Create HTTP Request node "Get Single Container":
|
||||
- Method: GET
|
||||
- URL: `http://docker-socket-proxy:2375/containers/json?all=true&filters={"name":["{{ $json.containerName }}"]}`
|
||||
- Returns container details
|
||||
|
||||
5. Create Code node "Build Container Submenu" that:
|
||||
- Takes container details and callback context
|
||||
- Builds action keyboard based on container state:
|
||||
- If running: [Stop] [Restart] row, [Logs] [Update] row
|
||||
- If stopped: [Start] row, [Logs] [Update] row
|
||||
- Uses callback_data format: `action:{action}:{containerName}` (e.g., `action:stop:plex`)
|
||||
- Adds "Back to List" button: `list:0`
|
||||
- Builds text with container status details
|
||||
- Returns structure for editMessageText
|
||||
|
||||
6. Create HTTP Request node "Send Container Submenu" that:
|
||||
- Method: POST
|
||||
- URL: `https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/editMessageText`
|
||||
- Body:
|
||||
```json
|
||||
{
|
||||
"chat_id": "={{ $json.chatId }}",
|
||||
"message_id": {{ $json.messageId }},
|
||||
"text": "={{ $json.text }}",
|
||||
"parse_mode": "HTML",
|
||||
"reply_markup": {{ JSON.stringify($json.reply_markup) }}
|
||||
}
|
||||
```
|
||||
|
||||
7. Create HTTP Request node "Answer Select Callback" (place BEFORE submenu fetch):
|
||||
- Method: POST
|
||||
- URL: `https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery`
|
||||
- Body: `{ "callback_query_id": "={{ $json.queryId }}" }`
|
||||
- CRITICAL: Must answer callback FIRST to prevent Telegram loading indicator
|
||||
|
||||
8. Wire the flow:
|
||||
- Route Callback (select output) -> Answer Select Callback -> Prepare Container Fetch -> Get Single Container -> Build Container Submenu -> Send Container Submenu
|
||||
|
||||
Code template for Build Container Submenu:
|
||||
```javascript
|
||||
const container = $input.all()[0].json[0]; // First container from filtered list
|
||||
const { queryId, chatId, messageId, containerName } = $('Prepare Container Fetch').item.json;
|
||||
|
||||
const keyboard = [];
|
||||
|
||||
// Action row 1: state-dependent
|
||||
if (container.State === 'running') {
|
||||
keyboard.push([
|
||||
{ text: '⏹️ Stop', callback_data: `action:stop:${containerName}` },
|
||||
{ text: '🔄 Restart', callback_data: `action:restart:${containerName}` }
|
||||
]);
|
||||
} else {
|
||||
keyboard.push([
|
||||
{ text: '▶️ Start', callback_data: `action:start:${containerName}` }
|
||||
]);
|
||||
}
|
||||
|
||||
// Action row 2: always available
|
||||
keyboard.push([
|
||||
{ text: '📋 Logs', callback_data: `action:logs:${containerName}` },
|
||||
{ text: '⬆️ Update', callback_data: `action:update:${containerName}` }
|
||||
]);
|
||||
|
||||
// Navigation row
|
||||
keyboard.push([
|
||||
{ text: '◀️ Back to List', callback_data: 'list:0' }
|
||||
]);
|
||||
|
||||
// Build status text
|
||||
const stateIcon = container.State === 'running' ? '🟢' : '⚪';
|
||||
const status = container.Status || container.State;
|
||||
const image = container.Image.split(':')[0].split('/').pop(); // Get image name without registry/tag
|
||||
|
||||
return {
|
||||
json: {
|
||||
chatId: chatId,
|
||||
messageId: messageId,
|
||||
text: `${stateIcon} <b>${containerName}</b>\n\n` +
|
||||
`<b>State:</b> ${container.State}\n` +
|
||||
`<b>Status:</b> ${status}\n` +
|
||||
`<b>Image:</b> ${image}`,
|
||||
reply_markup: { inline_keyboard: keyboard }
|
||||
}
|
||||
};
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
1. Send "/status" to bot
|
||||
2. Tap a container button
|
||||
3. Verify: Message edits in-place to show container details
|
||||
4. Verify: Action buttons match container state (Stop/Restart for running, Start for stopped)
|
||||
5. Verify: "Back to List" button present
|
||||
6. Tap "Back to List"
|
||||
7. Verify: Returns to container list
|
||||
</verify>
|
||||
<done>
|
||||
- Tapping container in list shows submenu with details and action buttons
|
||||
- Submenu shows container state, status, image
|
||||
- Action buttons match container state (Start vs Stop/Restart)
|
||||
- "Back to List" returns to container list
|
||||
- All transitions are message edits (no new messages)
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Handle List Pagination Callbacks</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add routing to handle `list:{page}` callbacks for pagination navigation.
|
||||
|
||||
1. Update "Parse Callback Data" to recognize `list:` prefix:
|
||||
- If callback_data starts with `list:`, extract page number
|
||||
- Set `isList: true` and `page: extractedPage`
|
||||
|
||||
2. Add new output to "Route Callback":
|
||||
- New rule: `isList === true` -> output "list"
|
||||
|
||||
3. Create Code node "Build Paginated List" (or reuse container list logic):
|
||||
- Similar to Task 1 keyboard builder but:
|
||||
- Uses page number from callback
|
||||
- Uses chatId/messageId from callback (for edit, not send)
|
||||
- Returns structure for editMessageText
|
||||
|
||||
4. Create HTTP Request "Answer List Callback":
|
||||
- answerCallbackQuery to prevent loading indicator
|
||||
|
||||
5. Create HTTP Request "Edit Container List":
|
||||
- editMessageText with updated page
|
||||
|
||||
6. Wire flow:
|
||||
- Route Callback (list output) -> Answer List Callback -> Get Containers -> Build Paginated List -> Edit Container List
|
||||
|
||||
Note: May need to reuse "Get Containers" node or create parallel path.
|
||||
</action>
|
||||
<verify>
|
||||
1. Have more than 6 containers (or temporarily set perPage to 3 for testing)
|
||||
2. Send "/status"
|
||||
3. Tap "Next" button
|
||||
4. Verify: Message edits to show next page of containers
|
||||
5. Tap "Previous" button
|
||||
6. Verify: Returns to previous page
|
||||
7. Verify: No loading indicator hangs (callback answered)
|
||||
</verify>
|
||||
<done>
|
||||
- Pagination buttons navigate between pages
|
||||
- Message edits in-place (no new messages)
|
||||
- Callback answered immediately (no loading indicator)
|
||||
- Page numbers correct in header text
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
After completing all tasks:
|
||||
1. `/status` shows inline keyboard with container list
|
||||
2. Tapping container shows submenu with action buttons
|
||||
3. "Back to List" returns to container list
|
||||
4. Pagination works (if >6 containers)
|
||||
5. `/status plex` shows that container's submenu directly
|
||||
6. All transitions are message edits, not new messages
|
||||
7. No hanging loading indicators (callbacks answered)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- KEY-01 requirement partially met: container list with inline buttons works
|
||||
- Navigation flow complete: List -> Submenu -> List
|
||||
- Foundation ready for action execution (Plan 02)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/08-inline-keyboard-infrastructure/08-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
phase: 08-inline-keyboard-infrastructure
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [08-01]
|
||||
files_modified: [n8n-workflow.json]
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Tapping Start button starts a stopped container"
|
||||
- "Tapping Stop button shows confirmation dialog"
|
||||
- "Tapping Update button shows confirmation dialog"
|
||||
- "Tapping Restart button executes restart immediately"
|
||||
- "Confirming Stop/Update executes the action"
|
||||
- "Cancelling returns to container submenu"
|
||||
- "Confirmation expires after 30 seconds"
|
||||
artifacts:
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Action execution routing and confirmation flow"
|
||||
contains: "Route Action Type"
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Confirmation keyboard builder"
|
||||
contains: "Build Confirmation"
|
||||
key_links:
|
||||
- from: "Route Callback"
|
||||
to: "Route Action Type"
|
||||
via: "action: callback routing"
|
||||
pattern: "action:"
|
||||
- from: "Route Action Type"
|
||||
to: "existing container ops"
|
||||
via: "start/stop/restart/update wiring"
|
||||
pattern: "containers/.*/start"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire action buttons to container operations and add confirmation flow for dangerous actions.
|
||||
|
||||
Purpose: When users tap action buttons in the container submenu, the corresponding action executes. Stop and Update require confirmation (per user decision). Start and Restart execute immediately.
|
||||
|
||||
Output: Updated n8n-workflow.json with action routing, confirmation flow, and wiring to existing container operations.
|
||||
</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/08-inline-keyboard-infrastructure/08-CONTEXT.md
|
||||
@.planning/phases/08-inline-keyboard-infrastructure/08-RESEARCH.md
|
||||
@.planning/phases/08-inline-keyboard-infrastructure/08-01-SUMMARY.md
|
||||
@n8n-workflow.json
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Route Action Callbacks to Container Operations</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add routing to handle `action:{type}:{name}` callbacks and wire to existing container operations.
|
||||
|
||||
1. Update "Parse Callback Data" to recognize `action:` prefix:
|
||||
```javascript
|
||||
// Add to existing parsing logic
|
||||
if (data.startsWith('action:')) {
|
||||
const parts = data.split(':');
|
||||
return {
|
||||
json: {
|
||||
isAction: true,
|
||||
actionType: parts[1], // start, stop, restart, update, logs
|
||||
containerName: parts[2],
|
||||
queryId: callbackQuery.id,
|
||||
chatId: callbackQuery.message.chat.id,
|
||||
messageId: callbackQuery.message.message_id
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. Add "isAction" output to "Route Callback" switch node:
|
||||
- Rule: `isAction === true` -> output "action"
|
||||
- This catches all action callbacks before routing by type
|
||||
|
||||
3. Create Switch node "Route Action Type":
|
||||
- Input: from Route Callback "action" output
|
||||
- Outputs:
|
||||
- "start": `actionType === 'start'`
|
||||
- "restart": `actionType === 'restart'`
|
||||
- "logs": `actionType === 'logs'`
|
||||
- "stop": `actionType === 'stop'` (needs confirmation)
|
||||
- "update": `actionType === 'update'` (needs confirmation)
|
||||
|
||||
4. For immediate actions (start, restart, logs), wire to existing container operation nodes:
|
||||
|
||||
**Start flow:**
|
||||
- Create Code node "Prepare Start Action":
|
||||
```javascript
|
||||
const { queryId, chatId, messageId, containerName } = $json;
|
||||
return {
|
||||
json: {
|
||||
queryId,
|
||||
chatId,
|
||||
messageId,
|
||||
containerName,
|
||||
// Format for existing Start Container node
|
||||
container: containerName
|
||||
}
|
||||
};
|
||||
```
|
||||
- Answer callback query immediately
|
||||
- Wire to existing "Start Container" HTTP Request node
|
||||
- After start completes, show success message (handled in Plan 03)
|
||||
|
||||
**Restart flow:**
|
||||
- Similar to start, wire to existing "Restart Container" node
|
||||
|
||||
**Logs flow:**
|
||||
- Wire to existing "Get Logs" flow
|
||||
- Logs may need special handling (send as new message, not edit)
|
||||
|
||||
5. For dangerous actions (stop, update), route to confirmation builder (Task 2)
|
||||
|
||||
6. Wire flows - ensuring callback is answered FIRST:
|
||||
- Route Action Type (start) -> Answer Start Callback -> Prepare Start Action -> Start Container -> (completion handling in Plan 03)
|
||||
- Route Action Type (restart) -> Answer Restart Callback -> Prepare Restart Action -> Restart Container -> (completion handling)
|
||||
- Route Action Type (logs) -> Answer Logs Callback -> existing logs flow
|
||||
- Route Action Type (stop) -> Build Stop Confirmation (Task 2)
|
||||
- Route Action Type (update) -> Build Update Confirmation (Task 2)
|
||||
</action>
|
||||
<verify>
|
||||
1. From container submenu, tap "Start" on a stopped container
|
||||
2. Verify: Container starts (check n8n execution or docker ps)
|
||||
3. Tap "Restart" on a running container
|
||||
4. Verify: Container restarts
|
||||
5. Tap "Logs"
|
||||
6. Verify: Logs returned (may be separate message for now)
|
||||
7. Tap "Stop" on running container
|
||||
8. Verify: Shows confirmation (not executed yet - Task 2)
|
||||
</verify>
|
||||
<done>
|
||||
- Start button starts containers immediately
|
||||
- Restart button restarts containers immediately
|
||||
- Logs button triggers log retrieval
|
||||
- Stop/Update route to confirmation flow
|
||||
- All callbacks answered (no loading indicator)
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add Confirmation Flow for Dangerous Actions</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add confirmation dialog for Stop and Update actions with 30-second timeout.
|
||||
|
||||
1. Create Code node "Build Stop Confirmation":
|
||||
```javascript
|
||||
const { queryId, chatId, messageId, containerName } = $json;
|
||||
const timestamp = Math.floor(Date.now() / 1000); // Unix seconds
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },
|
||||
{ text: '❌ Cancel', callback_data: `cancel:${containerName}` }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
json: {
|
||||
queryId,
|
||||
chatId,
|
||||
messageId,
|
||||
text: `⚠️ <b>Stop ${containerName}?</b>\n\nThis will stop the container immediately.\n\n<i>Expires in 30 seconds</i>`,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Create Code node "Build Update Confirmation":
|
||||
```javascript
|
||||
const { queryId, chatId, messageId, containerName } = $json;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Yes, Update', callback_data: `confirm:update:${containerName}:${timestamp}` },
|
||||
{ text: '❌ Cancel', callback_data: `cancel:${containerName}` }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
json: {
|
||||
queryId,
|
||||
chatId,
|
||||
messageId,
|
||||
text: `⬆️ <b>Update ${containerName}?</b>\n\nThis will pull the latest image and recreate the container.\n\n<i>Expires in 30 seconds</i>`,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. Create HTTP Request nodes to answer callback and show confirmation:
|
||||
- "Answer Stop Callback" -> answerCallbackQuery
|
||||
- "Show Stop Confirmation" -> editMessageText with confirmation keyboard
|
||||
- Same pattern for Update
|
||||
|
||||
4. Wire confirmation display:
|
||||
- Route Action Type (stop) -> Answer Stop Callback -> Build Stop Confirmation -> Show Stop Confirmation
|
||||
- Route Action Type (update) -> Answer Update Callback -> Build Update Confirmation -> Show Update Confirmation
|
||||
|
||||
5. Update "Parse Callback Data" to handle `confirm:` and `cancel:` callbacks:
|
||||
```javascript
|
||||
// confirm:stop:plex:1738595200
|
||||
if (data.startsWith('confirm:')) {
|
||||
const parts = data.split(':');
|
||||
const timestamp = parseInt(parts[3]);
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const expired = (currentTime - timestamp) > 30;
|
||||
|
||||
return {
|
||||
json: {
|
||||
isConfirm: true,
|
||||
expired: expired,
|
||||
actionType: parts[1], // stop or update
|
||||
containerName: parts[2],
|
||||
queryId: callbackQuery.id,
|
||||
chatId: callbackQuery.message.chat.id,
|
||||
messageId: callbackQuery.message.message_id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// cancel:plex
|
||||
if (data.startsWith('cancel:')) {
|
||||
const containerName = data.split(':')[1];
|
||||
return {
|
||||
json: {
|
||||
isCancel: true,
|
||||
containerName: containerName,
|
||||
queryId: callbackQuery.id,
|
||||
chatId: callbackQuery.message.chat.id,
|
||||
messageId: callbackQuery.message.message_id
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
6. Add "isConfirm" output to "Route Callback":
|
||||
- Rule: `isConfirm === true && !expired` -> output "confirm"
|
||||
- The existing "expired" output handles expired confirmations
|
||||
|
||||
7. Create Switch node "Route Confirmed Action":
|
||||
- Input: from Route Callback "confirm" output
|
||||
- Outputs: "stop", "update" based on actionType
|
||||
|
||||
8. Wire confirmed actions to actual operations:
|
||||
- Route Confirmed Action (stop) -> Answer Confirm Callback -> Stop Container -> (completion in Plan 03)
|
||||
- Route Confirmed Action (update) -> Answer Confirm Callback -> existing Update flow -> (completion)
|
||||
|
||||
9. Handle cancel callback:
|
||||
- isCancel should route back to container submenu
|
||||
- Reuse/extend existing cancel handling
|
||||
- On cancel: fetch container details again -> show submenu
|
||||
</action>
|
||||
<verify>
|
||||
1. Tap "Stop" on a running container
|
||||
2. Verify: Confirmation dialog appears with Yes/Cancel buttons
|
||||
3. Wait 35 seconds, then tap "Yes"
|
||||
4. Verify: Shows "Confirmation expired" message
|
||||
5. Tap "Stop" again, immediately tap "Yes"
|
||||
6. Verify: Container stops
|
||||
7. Start a container, tap "Stop", tap "Cancel"
|
||||
8. Verify: Returns to container submenu (not list)
|
||||
9. Test same flow for "Update" button
|
||||
</verify>
|
||||
<done>
|
||||
- Stop shows confirmation dialog
|
||||
- Update shows confirmation dialog
|
||||
- Confirmation includes 30-second timeout warning
|
||||
- Tapping "Yes" within 30s executes action
|
||||
- Tapping "Yes" after 30s shows expired message
|
||||
- Tapping "Cancel" returns to container submenu
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
After completing all tasks:
|
||||
1. Start button works immediately (no confirmation)
|
||||
2. Restart button works immediately (no confirmation)
|
||||
3. Stop button shows confirmation, then executes on confirm
|
||||
4. Update button shows confirmation, then executes on confirm
|
||||
5. Cancel returns to container submenu
|
||||
6. Expired confirmations are rejected with message
|
||||
7. Logs button retrieves container logs
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- KEY-02 requirement met: Action buttons perform operations
|
||||
- KEY-03 requirement met: Dangerous actions show confirmation
|
||||
- Actions wire correctly to existing container operation nodes
|
||||
- Confirmation timeout enforced at 30 seconds
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/08-inline-keyboard-infrastructure/08-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
phase: 08-inline-keyboard-infrastructure
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [08-02]
|
||||
files_modified: [n8n-workflow.json]
|
||||
autonomous: false
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "After action completes, message shows result with 'Back to menu' button"
|
||||
- "Buttons are removed from completed action messages"
|
||||
- "Update operations show progress message during execution"
|
||||
- "Full keyboard flow works end-to-end"
|
||||
artifacts:
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Completion message handlers"
|
||||
contains: "Show Action Result"
|
||||
- path: "n8n-workflow.json"
|
||||
provides: "Progress feedback for updates"
|
||||
contains: "Show Update Progress"
|
||||
key_links:
|
||||
- from: "container operation nodes"
|
||||
to: "Show Action Result"
|
||||
via: "completion flow"
|
||||
pattern: "editMessageText"
|
||||
- from: "Update Container flow"
|
||||
to: "Show Update Progress"
|
||||
via: "progress message"
|
||||
pattern: "Updating"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add progress feedback during operations and completion messages after actions finish.
|
||||
|
||||
Purpose: Users see visual feedback during operations ("Updating plex...") and final results ("plex updated") with a button to return to the menu. This completes the inline keyboard UX.
|
||||
|
||||
Output: Updated n8n-workflow.json with progress and completion handlers, plus full end-to-end verification.
|
||||
</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/08-inline-keyboard-infrastructure/08-CONTEXT.md
|
||||
@.planning/phases/08-inline-keyboard-infrastructure/08-RESEARCH.md
|
||||
@.planning/phases/08-inline-keyboard-infrastructure/08-01-SUMMARY.md
|
||||
@.planning/phases/08-inline-keyboard-infrastructure/08-02-SUMMARY.md
|
||||
@n8n-workflow.json
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add Completion Messages for Quick Actions</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add completion handlers that show results and "Back to menu" button after actions.
|
||||
|
||||
Per user decision: Quick actions (start, stop, restart) show final result only, not progress.
|
||||
|
||||
1. Create Code node "Build Action Success" that:
|
||||
- Takes action result and context (chatId, messageId, containerName, actionType)
|
||||
- Builds completion message based on action type
|
||||
- Includes "Back to menu" button
|
||||
- Removes action buttons (keyboard has only navigation)
|
||||
|
||||
```javascript
|
||||
const { chatId, messageId, containerName, actionType } = $json;
|
||||
|
||||
// Build success message based on action
|
||||
const messages = {
|
||||
start: `▶️ <b>${containerName}</b> started`,
|
||||
stop: `⏹️ <b>${containerName}</b> stopped`,
|
||||
restart: `🔄 <b>${containerName}</b> restarted`
|
||||
};
|
||||
|
||||
const text = messages[actionType] || `Action completed on ${containerName}`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '◀️ Back to Containers', callback_data: 'list:0' }]
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
json: {
|
||||
chatId,
|
||||
messageId,
|
||||
text,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Create HTTP Request node "Show Action Result":
|
||||
- Method: POST
|
||||
- URL: editMessageText endpoint
|
||||
- Body: chat_id, message_id, text, parse_mode, reply_markup
|
||||
|
||||
3. Wire completion after each action:
|
||||
- Start Container -> Build Action Success (with actionType='start') -> Show Action Result
|
||||
- Stop Container -> Build Action Success (with actionType='stop') -> Show Action Result
|
||||
- Restart Container -> Build Action Success (with actionType='restart') -> Show Action Result
|
||||
|
||||
4. Handle action failures:
|
||||
- Create Code node "Build Action Error":
|
||||
```javascript
|
||||
const { chatId, messageId, containerName, actionType, error } = $json;
|
||||
|
||||
const text = `❌ Failed to ${actionType} <b>${containerName}</b>\n\n${error || 'Unknown error'}`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '🔄 Try Again', callback_data: `action:${actionType}:${containerName}` }],
|
||||
[{ text: '◀️ Back to Containers', callback_data: 'list:0' }]
|
||||
]
|
||||
};
|
||||
|
||||
return { json: { chatId, messageId, text, reply_markup: keyboard } };
|
||||
```
|
||||
- Wire error outputs from container operations to error handler
|
||||
</action>
|
||||
<verify>
|
||||
1. Start a stopped container via button
|
||||
2. Verify: After start completes, message shows "plex started" with "Back to Containers" button
|
||||
3. Stop a running container (confirm when prompted)
|
||||
4. Verify: Shows "plex stopped" with back button
|
||||
5. Restart a container
|
||||
6. Verify: Shows "plex restarted" with back button
|
||||
7. Tap "Back to Containers"
|
||||
8. Verify: Returns to container list
|
||||
</verify>
|
||||
<done>
|
||||
- Start shows completion message with back button
|
||||
- Stop shows completion message with back button
|
||||
- Restart shows completion message with back button
|
||||
- Errors show retry and back buttons
|
||||
- Action buttons removed after completion (only back button remains)
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add Progress Feedback for Update Operations</name>
|
||||
<files>n8n-workflow.json</files>
|
||||
<action>
|
||||
Add progress message for update operations (longer running than start/stop/restart).
|
||||
|
||||
Per user decision: Updates show progress (simple status, not detailed steps).
|
||||
|
||||
1. Create Code node "Build Update Progress" that shows in-progress state:
|
||||
```javascript
|
||||
const { chatId, messageId, containerName } = $json;
|
||||
|
||||
return {
|
||||
json: {
|
||||
chatId,
|
||||
messageId,
|
||||
text: `⬆️ <b>Updating ${containerName}...</b>\n\nPulling latest image and recreating container.\nThis may take a few minutes.`,
|
||||
reply_markup: { inline_keyboard: [] } // Remove buttons during update
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Create HTTP Request "Show Update Progress":
|
||||
- editMessageText with progress message
|
||||
- Removes all buttons during operation
|
||||
|
||||
3. Wire update flow:
|
||||
- Route Confirmed Action (update) -> Answer Callback -> Build Update Progress -> Show Update Progress -> existing Update Container flow
|
||||
|
||||
4. Create Code node "Build Update Success":
|
||||
```javascript
|
||||
const { chatId, messageId, containerName } = $json;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '◀️ Back to Containers', callback_data: 'list:0' }]
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
json: {
|
||||
chatId,
|
||||
messageId,
|
||||
text: `✅ <b>${containerName}</b> updated successfully`,
|
||||
reply_markup: keyboard
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
5. Wire update completion:
|
||||
- After Update Container completes -> Build Update Success -> Show Action Result
|
||||
|
||||
6. Handle update errors:
|
||||
- Wire error path to Build Action Error with appropriate context
|
||||
</action>
|
||||
<verify>
|
||||
1. Start update on a container (confirm when prompted)
|
||||
2. Verify: Message immediately changes to "Updating plex..." with no buttons
|
||||
3. Wait for update to complete
|
||||
4. Verify: Message changes to "plex updated successfully" with back button
|
||||
5. If update fails, verify error message appears with retry option
|
||||
</verify>
|
||||
<done>
|
||||
- Update shows progress message during execution
|
||||
- Buttons removed during update (prevents duplicate actions)
|
||||
- Success message shown after update completes
|
||||
- Error message with retry button if update fails
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Full End-to-End Verification</name>
|
||||
<what-built>
|
||||
Complete inline keyboard infrastructure:
|
||||
- Container list with tappable buttons
|
||||
- Container submenu with action buttons
|
||||
- Confirmation dialogs for dangerous actions
|
||||
- Progress feedback for updates
|
||||
- Completion messages with navigation
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
**Flow 1: Basic Navigation**
|
||||
1. Send "/status" to bot
|
||||
2. Verify: Inline keyboard appears with container list
|
||||
3. Tap a container name
|
||||
4. Verify: Message edits to show container details and action buttons
|
||||
5. Tap "Back to List"
|
||||
6. Verify: Returns to container list
|
||||
|
||||
**Flow 2: Start Container**
|
||||
1. From container list, tap a STOPPED container
|
||||
2. Tap "Start" button
|
||||
3. Verify: Container starts, message shows "started" with back button
|
||||
4. Tap "Back to Containers"
|
||||
5. Verify: Returns to list, container now shows as Running
|
||||
|
||||
**Flow 3: Stop Container (with confirmation)**
|
||||
1. Tap a RUNNING container
|
||||
2. Tap "Stop" button
|
||||
3. Verify: Confirmation dialog appears "Stop container? Yes / No"
|
||||
4. Tap "Cancel"
|
||||
5. Verify: Returns to container submenu (not list)
|
||||
6. Tap "Stop" again
|
||||
7. Tap "Yes, Stop"
|
||||
8. Verify: Container stops, message shows "stopped" with back button
|
||||
|
||||
**Flow 4: Update Container (with progress)**
|
||||
1. Tap a container
|
||||
2. Tap "Update" button
|
||||
3. Verify: Confirmation dialog appears
|
||||
4. Tap "Yes, Update"
|
||||
5. Verify: Message shows "Updating..." with no buttons
|
||||
6. Wait for completion
|
||||
7. Verify: Message shows "updated successfully" with back button
|
||||
|
||||
**Flow 5: Confirmation Timeout**
|
||||
1. Tap a container, tap "Stop"
|
||||
2. Wait 35 seconds
|
||||
3. Tap "Yes, Stop"
|
||||
4. Verify: Shows "Confirmation expired" message
|
||||
|
||||
**Flow 6: Direct Access**
|
||||
1. Send "/status plex" (or another container name)
|
||||
2. Verify: Jumps directly to that container's submenu
|
||||
|
||||
**Flow 7: Pagination (if applicable)**
|
||||
1. If you have >6 containers, verify pagination buttons work
|
||||
2. If not, verify no pagination buttons appear
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to mark Phase 8 complete, or describe any issues found</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
After completing all tasks:
|
||||
1. All KEY-01 through KEY-05 requirements met
|
||||
2. Full navigation flow works (list -> submenu -> action -> result -> list)
|
||||
3. Confirmations work with timeout
|
||||
4. Progress shown for updates
|
||||
5. Buttons removed after action completion
|
||||
6. No hanging loading indicators anywhere
|
||||
7. Direct access (/status name) works
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- KEY-01: Status command shows container list with inline action buttons
|
||||
- KEY-02: Tapping action button performs start/stop/restart on container
|
||||
- KEY-03: Dangerous actions (stop, update) show confirmation dialog
|
||||
- KEY-04: Progress shown via message edit during operations
|
||||
- KEY-05: Buttons removed after action completes (only back button remains)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/08-inline-keyboard-infrastructure/08-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user