diff --git a/n8n-container-logs.json b/n8n-container-logs.json new file mode 100644 index 0000000..34e1c3d --- /dev/null +++ b/n8n-container-logs.json @@ -0,0 +1,255 @@ +{ + "name": "Container Logs", + "nodes": [ + { + "parameters": {}, + "id": "711c56f9-46d5-41dd-aa5c-e7e4230793f3", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1, + "position": [ + 240, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Parse and validate input\nconst input = $json;\n\n// Get container identifier (ID or name)\nconst containerId = input.containerId || '';\nconst containerName = input.containerName || '';\nconst lineCount = input.lineCount || 50;\nconst chatId = input.chatId;\nconst messageId = input.messageId || 0;\nconst responseMode = input.responseMode || 'text';\n\nif (!containerId && !containerName) {\n throw new Error('Either containerId or containerName required');\n}\n\nif (!chatId) {\n throw new Error('chatId required');\n}\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n lineCount: Math.min(Math.max(parseInt(lineCount), 1), 1000),\n chatId: chatId,\n messageId: messageId,\n responseMode: responseMode\n }\n};" + }, + "id": "dac65a64-173a-4536-b3c5-0699704380fa", + "name": "Parse Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 460, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Get container ID if needed\nconst data = $json;\n\n// If we already have container ID, pass through\nif (data.containerId) {\n return {\n json: {\n ...data,\n useDirectId: true\n }\n };\n}\n\n// Otherwise, need to query Docker to find by name\nreturn {\n json: {\n ...data,\n useDirectId: false,\n dockerCommand: 'curl -s --max-time 5 \"http://docker-socket-proxy:2375/v1.47/containers/json?all=1\"'\n }\n};" + }, + "id": "d6db7666-5ecd-4200-8231-ffa6307e4c39", + "name": "Check Container ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 680, + 300 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "id": "has-id", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "check-direct", + "leftValue": "={{ $json.useDirectId }}", + "rightValue": "true", + "operator": { + "type": "boolean", + "operation": "true" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "direct" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "id": "818a4c33-e150-423e-a333-adf9843aac05", + "name": "Route ID Check", + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, + "position": [ + 900, + 300 + ] + }, + { + "parameters": { + "command": "={{ $json.dockerCommand }}" + }, + "id": "83cbcda1-68c5-46ee-bbb6-2eb5f1ae9077", + "name": "Query Docker", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 1120, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Find container by name\nconst dockerOutput = $input.item.json.stdout;\nconst data = $('Check Container ID').item.json;\nconst containerName = data.containerName.toLowerCase();\n\n// Parse Docker response\nlet containers;\ntry {\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n throw new Error('Failed to parse Docker response');\n}\n\n// Normalize name function\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find exact match\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n throw new Error(`Container \"${containerName}\" not found`);\n}\n\nreturn {\n json: {\n ...data,\n containerId: container.Id,\n containerName: normalizeName(container.Names[0])\n }\n};" + }, + "id": "52dd705b-dd3b-4fdc-8484-276845857ad0", + "name": "Find Container", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1340, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Build Docker logs command\nconst data = $json;\nconst containerId = data.containerId;\nconst lineCount = data.lineCount;\n\nconst cmd = `curl -s --max-time 10 \"http://docker-socket-proxy:2375/v1.47/containers/${containerId}/logs?stdout=1&stderr=1&tail=${lineCount}×tamps=1\"`;\n\nreturn {\n json: {\n ...data,\n logsCommand: cmd\n }\n};" + }, + "id": "86619651-82f0-459b-a969-d210d2dd6361", + "name": "Build Logs Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1560, + 300 + ] + }, + { + "parameters": { + "command": "={{ $json.logsCommand }}" + }, + "id": "b602549b-e526-4207-a41d-74a936060a25", + "name": "Execute Logs", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 1780, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Format logs output for Telegram\nconst rawOutput = $input.item.json.stdout || '';\nconst data = $('Build Logs Command').item.json;\n\n// HTML escape function\nfunction escapeHtml(text) {\n return text.replace(/&/g, '&').replace(//g, '>');\n}\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n success: true,\n message: `No logs available for ${data.containerName}`,\n containerName: data.containerName,\n lineCount: 0\n }\n };\n}\n\n// Strip Docker binary headers and process lines\nconst lines = rawOutput.split('\\n')\n .filter(line => line.length > 0)\n .map(line => {\n // Check if line starts with binary header (8-byte Docker stream header)\n if (line.length > 8 && line.charCodeAt(0) <= 2) {\n return line.substring(8);\n }\n return line;\n })\n .join('\\n');\n\n// Truncate for Telegram (4096 char limit, leave room for header)\nconst maxLen = 3800;\nconst truncated = lines.length > maxLen\n ? lines.substring(0, maxLen) + '\\n... (truncated)'\n : lines;\n\n// Escape HTML entities\nconst escaped = escapeHtml(truncated);\n\nconst lineCount = lines.split('\\n').length;\nconst header = `Logs for ${data.containerName} (last ${lineCount} lines):\\n\\n`;\nconst formatted = header + '
' + escaped + '
';\n\nreturn {\n json: {\n success: true,\n message: formatted,\n containerName: data.containerName,\n lineCount: lineCount\n }\n};" + }, + "id": "e423b026-e619-4e3a-abd9-563e935cd74d", + "name": "Format Logs", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2000, + 300 + ] + } + ], + "connections": { + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "Parse Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Input": { + "main": [ + [ + { + "node": "Check Container ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Container ID": { + "main": [ + [ + { + "node": "Route ID Check", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route ID Check": { + "main": [ + [ + { + "node": "Build Logs Command", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Query Docker", + "type": "main", + "index": 0 + } + ] + ] + }, + "Query Docker": { + "main": [ + [ + { + "node": "Find Container", + "type": "main", + "index": 0 + } + ] + ] + }, + "Find Container": { + "main": [ + [ + { + "node": "Build Logs Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Logs Command": { + "main": [ + [ + { + "node": "Execute Logs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Logs": { + "main": [ + [ + { + "node": "Format Logs", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1" + }, + "versionId": "c2a9969f-2928-41f9-be03-9692ae242751", + "meta": { + "instanceId": "unraid-docker-manager" + }, + "tags": [] +} \ No newline at end of file diff --git a/n8n-workflow.json b/n8n-workflow.json index 4dc2751..9647a4e 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -1900,184 +1900,12 @@ 600 ] }, - { - "parameters": { - "command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'", - "options": {} - }, - "id": "exec-docker-list-logs", - "name": "Docker List for Logs", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1120, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Get Docker API response and logs info from Parse Logs\nconst dockerOutput = $input.item.json.stdout;\nconst logsData = $('Parse Logs Command').item.json;\nconst containerQuery = logsData.containerQuery;\nconst lines = logsData.lines;\nconst chatId = logsData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n lines: lines,\n chatId: chatId\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n lines: lines,\n chatId: chatId\n }\n}];" - }, - "id": "code-match-logs-container", - "name": "Match Logs Container", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 600 - ] - }, - { - "parameters": { - "rules": { - "values": [ - { - "id": "logs-docker-error", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-negative", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "lt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "logs-no-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-zero", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "logs-single-match", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-one", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - }, - { - "id": "logs-multiple-matches", - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "loose" - }, - "conditions": [ - { - "id": "count-many", - "leftValue": "={{ $json.matchCount }}", - "rightValue": 1, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "renameOutput": false - } - ] - }, - "options": {} - }, - "id": "switch-logs-match-count", - "name": "Check Logs Match Count", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.2, - "position": [ - 1560, - 600 - ] - }, - { - "parameters": { - "jsCode": "// Build Docker logs API curl command\nconst matchData = $input.item.json;\nconst container = matchData.matches[0]; // Single match verified by switch\nconst lines = matchData.lines;\n\nconst cmd = `curl -s --max-time 5 \"http://docker-socket-proxy:2375/v1.47/containers/${container.Id}/logs?stdout=1&stderr=1&tail=${lines}×tamps=1\"`;\n\nreturn {\n json: {\n command: cmd,\n containerName: container.Name,\n containerId: container.Id,\n lines: lines,\n chatId: matchData.chatId\n }\n};" - }, - "id": "code-build-logs-cmd", - "name": "Build Logs Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 500 - ] - }, - { - "parameters": { - "command": "={{ $json.command }}", - "options": {} - }, - "id": "exec-logs", - "name": "Execute Logs", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2000, - 500 - ] - }, - { - "parameters": { - "jsCode": "// Format Docker logs for Telegram\nconst rawOutput = $input.item.json.stdout || '';\nconst containerName = $('Build Logs Command').item.json.containerName;\nconst requestedLines = $('Build Logs Command').item.json.lines;\nconst chatId = $('Build Logs Command').item.json.chatId;\n\n// HTML escape function for log content\nfunction escapeHtml(text) {\n return text.replace(/&/g, '&').replace(//g, '>');\n}\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n chatId: chatId,\n text: `No logs available for ${containerName}`\n }\n };\n}\n\n// Docker API with timestamps returns text lines when using tail parameter\n// But may have 8-byte binary headers we need to strip\nconst lines = rawOutput.split('\\n')\n .filter(line => line.length > 0)\n .map(line => {\n // Check if line starts with binary header (non-printable chars in first 8 bytes)\n if (line.length > 8 && line.charCodeAt(0) <= 2) {\n return line.substring(8);\n }\n return line;\n })\n .join('\\n');\n\n// Truncate for Telegram (4096 char limit, leave room for header)\nconst maxLen = 3800;\nconst truncated = lines.length > maxLen\n ? lines.substring(0, maxLen) + '\\n... (truncated)'\n : lines;\n\n// Escape HTML entities in log content to prevent parse errors\nconst escaped = escapeHtml(truncated);\n\nconst lineCount = lines.split('\\n').length;\nconst header = `Logs for ${containerName} (last ${lineCount} lines):\\n\\n`;\n\nreturn {\n json: {\n chatId: chatId,\n text: header + '
' + escaped + '
'\n }\n};" - }, - "id": "code-format-logs", - "name": "Format Logs", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 500 - ] - }, { "parameters": { "resource": "message", "operation": "sendMessage", "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text }}", + "text": "={{ $json.message }}", "additionalFields": { "parse_mode": "HTML" } @@ -2097,57 +1925,6 @@ } } }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $json.chatId }}", - "text": "={{ $json.text || 'Error retrieving logs' }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-logs-error", - "name": "Send Logs Error", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 1780, - 700 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, - { - "parameters": { - "jsCode": "const data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n text: `No container found matching \"${data.containerQuery}\".\\n\\nTry \"status\" to see all containers.`\n }\n};" - }, - "id": "code-format-logs-no-match", - "name": "Format Logs No Match", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 650 - ] - }, - { - "parameters": { - "jsCode": "const data = $input.item.json;\nconst names = data.matches.map(m => m.Name).join('\\n- ');\nreturn {\n json: {\n chatId: data.chatId,\n text: `Found ${data.matches.length} containers matching \"${data.containerQuery}\":\\n\\n- ${names}\\n\\nPlease be more specific.`\n }\n};" - }, - "id": "code-format-logs-multiple", - "name": "Format Logs Multiple", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 600 - ] - }, { "parameters": { "resource": "message", @@ -2731,74 +2508,6 @@ } } }, - { - "parameters": { - "jsCode": "// Prepare logs action\nconst data = $('Parse Callback Data').item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId,\n lines: 30\n }\n};" - }, - "id": "code-prepare-logs-action", - "name": "Prepare Logs Action", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1780, - 1300 - ] - }, - { - "parameters": { - "method": "GET", - "url": "=http://docker-socket-proxy:2375/containers/json?all=true", - "options": {} - }, - "id": "http-get-container-for-logs", - "name": "Get Container For Logs", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2000, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Find container and build logs command\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Logs Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\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]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 10 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/logs?stdout=true&stderr=true&tail=${lines}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n containerState: container.State,\n lines,\n chatId,\n messageId\n }\n};" - }, - "id": "code-build-logs-action-cmd", - "name": "Build Logs Action Command", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 1300 - ] - }, - { - "parameters": { - "command": "={{ $json.cmd }}", - "options": {} - }, - "id": "exec-logs-action", - "name": "Execute Logs Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 2440, - 1300 - ] - }, - { - "parameters": { - "jsCode": "// Parse and format logs output\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Build Logs Action Command').item.json;\nconst containerName = prevData.containerName;\nconst containerState = prevData.containerState;\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Docker logs API returns binary stream with 8-byte header per line\n// Strip non-printable characters and clean up\nlet logs = stdout\n .split('\\n')\n .map(line => {\n // Remove Docker stream header (first 8 bytes of each frame)\n // The header contains stream type and length info\n if (line.length > 8) {\n const cleaned = line.substring(8).replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n return cleaned;\n }\n return line.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n })\n .filter(line => line.trim().length > 0)\n .slice(-lines)\n .join('\\n');\n\nif (!logs || logs.trim().length === 0) {\n logs = '(no recent logs)';\n}\n\n// Truncate if too long for Telegram (max ~4096 chars)\nif (logs.length > 3800) {\n logs = '...' + logs.slice(-3800);\n}\n\n// Build keyboard for navigation\nconst keyboard = [];\nif (containerState === '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} Refresh 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\nconst stateIcon = containerState === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nconst timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\nlet text = `${stateIcon} ${containerName} - Logs (${timestamp})\\n\\n
${logs}
`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" - }, - "id": "code-format-logs-action-result", - "name": "Format Logs Action Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2660, - 1300 - ] - }, { "parameters": { "method": "POST", @@ -5168,6 +4877,81 @@ 4440, -200 ] + }, + { + "parameters": { + "jsCode": "// Prepare input for Container Logs sub-workflow (text command)\nconst data = $json;\n\n// Check if there's an error from Parse Logs Command\nif (data.error) {\n return {\n json: {\n error: true,\n chatId: data.chatId,\n text: data.text\n }\n };\n}\n\nreturn {\n json: {\n containerName: data.containerQuery,\n lineCount: data.lines,\n chatId: data.chatId,\n messageId: data.messageId || 0,\n responseMode: \"text\"\n }\n};" + }, + "id": "a895bb2d-1f61-4466-b475-b32ec5f0e83a", + "name": "Prepare Text Logs Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1120, + 600 + ] + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "mode": "list", + "value": "PLACEHOLDER_LOGS_ID" + }, + "options": {} + }, + "id": "926c7683-c0e4-41a4-a983-e3f7ecb6ff41", + "name": "Execute Text Logs", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [ + 1340, + 600 + ] + }, + { + "parameters": { + "jsCode": "// Prepare input for Container Logs sub-workflow (inline action)\nconst data = $('Parse Callback Data').item.json;\n\nreturn {\n json: {\n containerName: data.containerName,\n lineCount: 30,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: \"inline\"\n }\n};" + }, + "id": "16b24086-5b5d-4980-82c7-4fb37b4e8f6c", + "name": "Prepare Inline Logs Input", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 1300 + ] + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "mode": "list", + "value": "PLACEHOLDER_LOGS_ID" + }, + "options": {} + }, + "id": "a88974bd-45c0-401e-b50a-c6171cfe06d4", + "name": "Execute Inline Logs", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [ + 2000, + 1300 + ] + }, + { + "parameters": { + "jsCode": "// Format logs result for inline keyboard display\nconst result = $json;\nconst data = $('Prepare Inline Logs Input').item.json;\n\n// Get container state (need to fetch from Docker)\n// For now, build basic keyboard\nconst containerName = result.containerName;\n\n// Build inline keyboard\nconst keyboard = [\n [\n { text: '\ud83d\udd04 Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\u2b06\ufe0f Update', callback_data: `action:update:${containerName}` }\n ],\n [\n { text: '\u25c0\ufe0f Back to List', callback_data: 'list:0' }\n ]\n];\n\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: result.message,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" + }, + "id": "b1800598-1ff6-4da3-8506-4e4e8127f902", + "name": "Format Inline Logs Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2220, + 1300 + ] } ], "connections": { @@ -5914,116 +5698,7 @@ "main": [ [ { - "node": "Docker List for Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Docker List for Logs": { - "main": [ - [ - { - "node": "Match Logs Container", - "type": "main", - "index": 0 - } - ] - ] - }, - "Match Logs Container": { - "main": [ - [ - { - "node": "Check Logs Match Count", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Logs Match Count": { - "main": [ - [ - { - "node": "Send Logs Error", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Format Logs No Match", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Build Logs Command", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Format Logs Multiple", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Logs Command": { - "main": [ - [ - { - "node": "Execute Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Logs": { - "main": [ - [ - { - "node": "Format Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Logs": { - "main": [ - [ - { - "node": "Send Logs Response", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Logs No Match": { - "main": [ - [ - { - "node": "Send Logs Error", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Logs Multiple": { - "main": [ - [ - { - "node": "Send Logs Error", + "node": "Prepare Text Logs Input", "type": "main", "index": 0 } @@ -6372,7 +6047,7 @@ ], [ { - "node": "Prepare Logs Action", + "node": "Prepare Inline Logs Input", "type": "main", "index": 0 } @@ -6434,61 +6109,6 @@ ] ] }, - "Prepare Logs Action": { - "main": [ - [ - { - "node": "Get Container For Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Container For Logs": { - "main": [ - [ - { - "node": "Build Logs Action Command", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Logs Action Command": { - "main": [ - [ - { - "node": "Execute Logs Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Logs Action": { - "main": [ - [ - { - "node": "Format Logs Action Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Logs Action Result": { - "main": [ - [ - { - "node": "Send Logs Result", - "type": "main", - "index": 0 - } - ] - ] - }, "Build Stop Confirmation": { "main": [ [ @@ -7410,6 +7030,61 @@ } ] ] + }, + "Prepare Text Logs Input": { + "main": [ + [ + { + "node": "Execute Text Logs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Text Logs": { + "main": [ + [ + { + "node": "Send Logs Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Inline Logs Input": { + "main": [ + [ + { + "node": "Execute Inline Logs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Inline Logs": { + "main": [ + [ + { + "node": "Format Inline Logs Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Inline Logs Result": { + "main": [ + [ + { + "node": "Send Logs Result", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {},