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:
Lucas Berger
2026-02-03 11:44:46 -05:00
parent fe4c19c7c6
commit ecd02a4b0e
4 changed files with 1013 additions and 2 deletions
@@ -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>