From 369eb2a87e42b055a0b509bfccf0fada3df59526 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 4 Feb 2026 21:24:46 -0500 Subject: [PATCH] feat(10.1-03): create container status sub-workflow - Created n8n-status.json with 11 nodes - Handles list, status, and paginate actions - Input: chatId, messageId, action, containerName, page, queryId, searchTerm - Output: success, action, text, reply_markup, container data - Docker queries via HTTP request to docker-socket-proxy --- n8n-status.json | 384 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 n8n-status.json diff --git a/n8n-status.json b/n8n-status.json new file mode 100644 index 0000000..cc52ad9 --- /dev/null +++ b/n8n-status.json @@ -0,0 +1,384 @@ +{ + "name": "Container Status", + "nodes": [ + { + "parameters": { + "inputSource": "passthrough", + "schema": { + "schemaType": "fromFields", + "fields": [ + { + "fieldName": "chatId", + "fieldType": "number" + }, + { + "fieldName": "messageId", + "fieldType": "number" + }, + { + "fieldName": "action", + "fieldType": "string" + }, + { + "fieldName": "containerId", + "fieldType": "string" + }, + { + "fieldName": "containerName", + "fieldType": "string" + }, + { + "fieldName": "page", + "fieldType": "number" + }, + { + "fieldName": "queryId", + "fieldType": "string" + }, + { + "fieldName": "searchTerm", + "fieldType": "string" + } + ] + } + }, + "id": "status-trigger", + "name": "When executed by another workflow", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + 240, + 300 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "id": "route-list", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-list", + "leftValue": "={{ $json.action }}", + "rightValue": "list", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "list" + }, + { + "id": "route-status", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-status", + "leftValue": "={{ $json.action }}", + "rightValue": "status", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "status" + }, + { + "id": "route-paginate", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-paginate", + "leftValue": "={{ $json.action }}", + "rightValue": "paginate", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "paginate" + } + ] + }, + "options": { + "fallbackOutput": "none" + } + }, + "id": "status-route-action", + "name": "Route Action", + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, + "position": [ + 460, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Prepare list request - pass through input data\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId || 0,\n page: data.page || 0,\n searchTerm: data.searchTerm || null,\n queryId: data.queryId || null\n }\n};" + }, + "id": "status-prepare-list", + "name": "Prepare List Request", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 680, + 200 + ] + }, + { + "parameters": { + "method": "GET", + "url": "=http://docker-socket-proxy:2375/containers/json?all=true", + "options": {} + }, + "id": "status-docker-list", + "name": "Docker List Containers", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 900, + 200 + ] + }, + { + "parameters": { + "jsCode": "// Build Paginated Container List Keyboard\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare List Request\").item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst page = prevData.page || 0;\nconst searchTerm = prevData.searchTerm;\n\n// Function to normalize container names (strip prefixes)\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// If specific container requested via search term, check for exact/single match\nif (searchTerm) {\n const matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(searchTerm.toLowerCase());\n });\n \n if (matches.length === 1) {\n const container = matches[0];\n const containerName = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n // Build action keyboard based on container state\n const keyboard = [];\n if (state === 'running') {\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n } else {\n keyboard.push([\n { text: '\\u25B6\\uFE0F Start', callback_data: `action:start:${containerName}` }\n ]);\n }\n keyboard.push([\n { text: '\\u{1F4CB} Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n ]);\n keyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]);\n \n // Build status text\n const stateIcon = state === 'running' ? '\\u{1F7E2}' : '\\u26AA';\n let text = `${stateIcon} ${containerName}\\n\\n`;\n text += `State: ${state}\\n`;\n text += `Status: ${status}\\n`;\n text += `Image: ${image}`;\n \n return [{\n json: {\n success: true,\n action: 'status_direct',\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard },\n container: { id: container.Id, name: containerName, state, status, image }\n }\n }];\n } else if (matches.length === 0) {\n return [{\n json: {\n success: false,\n action: 'list',\n chatId,\n messageId,\n error: `No container found matching \"${searchTerm}\"`,\n text: `No container found matching \"${searchTerm}\"`\n }\n }];\n }\n // Multiple matches - continue to show list\n}\n\nconst containersPerPage = 6;\n\n// Group by state (running first)\nconst running = containers.filter(c => c.State === 'running');\nconst stopped = containers.filter(c => c.State !== 'running');\nconst sortedContainers = [...running, ...stopped];\n\nconst totalPages = Math.ceil(sortedContainers.length / containersPerPage);\nconst start = page * containersPerPage;\nconst pageContainers = sortedContainers.slice(start, start + containersPerPage);\n\n// Build keyboard rows\nconst keyboard = [];\n\npageContainers.forEach(container => {\n const name = normalizeName(container.Names[0]);\n const stateIcon = container.State === 'running' ? '\\u{1F7E2}' : '\\u26AA'; // Green circle or white circle\n const stateText = container.State === 'running' ? 'Running' : 'Stopped';\n keyboard.push([{\n text: `${stateIcon} ${name} - ${stateText}`,\n callback_data: `select:${name}`\n }]);\n});\n\n// Add navigation row if needed\nif (totalPages > 1) {\n const navRow = [];\n if (page > 0) {\n navRow.push({ text: '\\u25C0\\uFE0F Previous', callback_data: `list:${page - 1}` });\n }\n navRow.push({ text: `${page + 1}/${totalPages}`, callback_data: 'noop' });\n if (page < totalPages - 1) {\n navRow.push({ text: 'Next \\u25B6\\uFE0F', callback_data: `list:${page + 1}` });\n }\n keyboard.push(navRow);\n}\n\n// Add Select Multiple button for batch operations\nkeyboard.push([{ text: '\\u2611\\ufe0f Select Multiple', callback_data: 'batch:mode' }]);\n\n// Build header text\nconst runningCount = running.length;\nconst totalCount = sortedContainers.length;\nlet headerText = `\\u{1F5C2} Containers (${runningCount}/${totalCount} running)`;\nif (totalPages > 1) {\n headerText += `\\n\\nPage ${page + 1} of ${totalPages}`;\n}\nheaderText += '\\n\\nTap a container to manage it:';\n\nreturn [{\n json: {\n success: true,\n action: 'list',\n chatId,\n messageId,\n text: headerText,\n reply_markup: { inline_keyboard: keyboard },\n totalContainers: totalCount,\n currentPage: page,\n totalPages\n }\n}];" + }, + "id": "status-build-list", + "name": "Build Container List", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 200 + ] + }, + { + "parameters": { + "jsCode": "// Prepare status request - pass through input data\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId || 0,\n containerName: data.containerName,\n containerId: data.containerId || null,\n queryId: data.queryId || null\n }\n};" + }, + "id": "status-prepare-status", + "name": "Prepare Status Request", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 680, + 300 + ] + }, + { + "parameters": { + "method": "GET", + "url": "=http://docker-socket-proxy:2375/containers/json?all=true", + "options": {} + }, + "id": "status-docker-single", + "name": "Docker Get Container", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 900, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Build Container Submenu for callback selection\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Status Request\").item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst searchName = prevData.containerName.toLowerCase();\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === searchName);\n\nif (!container) {\n return [{\n json: {\n success: false,\n action: 'status',\n chatId,\n messageId,\n error: `Container \"${searchName}\" not found`,\n text: `Container \"${searchName}\" not found`,\n reply_markup: { inline_keyboard: [[{ text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === 'running') {\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: '\\u25B6\\uFE0F Start', callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: '\\u{1F4CB} Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n]);\n\n// Build status text\nconst stateIcon = state === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `State: ${state}\\n`;\ntext += `Status: ${status}\\n`;\ntext += `Image: ${image}`;\n\nreturn [{\n json: {\n success: true,\n action: 'status',\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard },\n container: { id: container.Id, name: containerName, state, status, image }\n }\n}];" + }, + "id": "status-build-submenu", + "name": "Build Container Submenu", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Prepare paginate request - pass through input data\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId || 0,\n page: data.page || 0,\n queryId: data.queryId || null\n }\n};" + }, + "id": "status-prepare-paginate", + "name": "Prepare Paginate Request", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 680, + 400 + ] + }, + { + "parameters": { + "method": "GET", + "url": "=http://docker-socket-proxy:2375/containers/json?all=true", + "options": {} + }, + "id": "status-docker-paginate", + "name": "Docker List For Paginate", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 900, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Build Paginated Container List Keyboard for pagination callbacks\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Paginate Request\").item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst page = prevData.page || 0;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst containersPerPage = 6;\n\n// Group by state (running first)\nconst running = containers.filter(c => c.State === 'running');\nconst stopped = containers.filter(c => c.State !== 'running');\nconst sortedContainers = [...running, ...stopped];\n\nconst totalPages = Math.ceil(sortedContainers.length / containersPerPage);\nconst start = page * containersPerPage;\nconst pageContainers = sortedContainers.slice(start, start + containersPerPage);\n\n// Build keyboard rows\nconst keyboard = [];\n\npageContainers.forEach(container => {\n const name = normalizeName(container.Names[0]);\n const stateIcon = container.State === 'running' ? '\\u{1F7E2}' : '\\u26AA';\n const stateText = container.State === 'running' ? 'Running' : 'Stopped';\n keyboard.push([{\n text: `${stateIcon} ${name} - ${stateText}`,\n callback_data: `select:${name}`\n }]);\n});\n\n// Add navigation row if needed\nif (totalPages > 1) {\n const navRow = [];\n if (page > 0) {\n navRow.push({ text: '\\u25C0\\uFE0F Previous', callback_data: `list:${page - 1}` });\n }\n navRow.push({ text: `${page + 1}/${totalPages}`, callback_data: 'noop' });\n if (page < totalPages - 1) {\n navRow.push({ text: 'Next \\u25B6\\uFE0F', callback_data: `list:${page + 1}` });\n }\n keyboard.push(navRow);\n}\n\n// Add Select Multiple button for batch operations\nkeyboard.push([{ text: '\\u2611\\ufe0f Select Multiple', callback_data: 'batch:mode' }]);\n\n// Build header text\nconst runningCount = running.length;\nconst totalCount = sortedContainers.length;\nlet headerText = `\\u{1F5C2} Containers (${runningCount}/${totalCount} running)`;\nif (totalPages > 1) {\n headerText += `\\n\\nPage ${page + 1} of ${totalPages}`;\n}\nheaderText += '\\n\\nTap a container to manage it:';\n\nreturn [{\n json: {\n success: true,\n action: 'paginate',\n chatId,\n messageId,\n text: headerText,\n reply_markup: { inline_keyboard: keyboard },\n totalContainers: totalCount,\n currentPage: page,\n totalPages\n }\n}];" + }, + "id": "status-build-paginated", + "name": "Build Paginated List", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 400 + ] + } + ], + "connections": { + "When executed by another workflow": { + "main": [ + [ + { + "node": "Route Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route Action": { + "list": [ + [ + { + "node": "Prepare List Request", + "type": "main", + "index": 0 + } + ] + ], + "status": [ + [ + { + "node": "Prepare Status Request", + "type": "main", + "index": 0 + } + ] + ], + "paginate": [ + [ + { + "node": "Prepare Paginate Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare List Request": { + "main": [ + [ + { + "node": "Docker List Containers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Docker List Containers": { + "main": [ + [ + { + "node": "Build Container List", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Status Request": { + "main": [ + [ + { + "node": "Docker Get Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Docker Get Container": { + "main": [ + [ + { + "node": "Build Container Submenu", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Paginate Request": { + "main": [ + [ + { + "node": "Docker List For Paginate", + "type": "main", + "index": 0 + } + ] + ] + }, + "Docker List For Paginate": { + "main": [ + [ + { + "node": "Build Paginated List", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "placeholder" + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [] +}