--- 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:" --- 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. @/home/luc/.claude/get-shit-done/workflows/execute-plan.md @/home/luc/.claude/get-shit-done/templates/summary.md @.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 Task 1: Add Container List Inline Keyboard n8n-workflow.json 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: `Containers (${start + 1}-${Math.min(start + perPage, sorted.length)} of ${sorted.length})\n\nTap a container to manage it:`, reply_markup: { inline_keyboard: keyboard } } }; ``` 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) - /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 Task 2: Add Container Submenu with Action Buttons n8n-workflow.json 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} ${containerName}\n\n` + `State: ${container.State}\n` + `Status: ${status}\n` + `Image: ${image}`, reply_markup: { inline_keyboard: keyboard } } }; ``` 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 - 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) Task 3: Handle List Pagination Callbacks n8n-workflow.json 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. 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) - Pagination buttons navigate between pages - Message edits in-place (no new messages) - Callback answered immediately (no loading indicator) - Page numbers correct in header text 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) - KEY-01 requirement partially met: container list with inline buttons works - Navigation flow complete: List -> Submenu -> List - Foundation ready for action execution (Plan 02) After completion, create `.planning/phases/08-inline-keyboard-infrastructure/08-01-SUMMARY.md`