diff --git a/n8n-workflow.json b/n8n-workflow.json
index 1ea57dc..624d30d 100644
--- a/n8n-workflow.json
+++ b/n8n-workflow.json
@@ -348,7 +348,7 @@
},
{
"parameters": {
- "jsCode": "// Get Docker API response and message\nconst dockerOutput = $input.item.json.stdout;\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\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 chatId: chatId,\n error: true,\n text: \"Can't reach Docker - check if n8n has socket access\"\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\n// Check if user specified a container name in message (e.g., \"status plex\")\nconst text = (message.text || '').toLowerCase().trim();\nlet requestedName = null;\n\n// Extract container name if message is more than just \"status\"\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n // Join all parts except \"status\" as the container query\n requestedName = parts.filter(p => p !== 'status').join(' ');\n}\n\n// If no container name specified, return summary\nif (!requestedName) {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n \n const total = containers.length;\n const running = counts['running'] || 0;\n const stopped = counts['exited'] || 0;\n const other = total - running - stopped;\n \n let summary = 'Docker Containers\\n\\n';\n summary += 'Running: ' + running + '/' + total + '\\n';\n if (stopped > 0) summary += 'Stopped: ' + stopped + '\\n';\n if (other > 0) summary += 'Other: ' + other + '\\n';\n \n // List running containers\n const runningContainers = containers\n .filter(c => c.State === 'running')\n .map(c => {\n const name = normalizeName(c.Names[0]);\n const uptime = c.Status;\n return ' \\u2022 ' + name + ' - ' + uptime;\n });\n \n if (runningContainers.length > 0) {\n summary += '\\nRunning:\\n' + runningContainers.join('\\n');\n }\n \n return [{\n json: {\n chatId: chatId,\n text: summary\n }\n }];\n}\n\n// Find matching containers\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n});\n\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: 'No container found matching \"' + requestedName + '\"'\n }\n }];\n}\n\nif (matches.length === 1) {\n const container = matches[0];\n const name = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n let text = '' + name + '\\n';\n text += 'State: ' + state + '\\n';\n text += 'Status: ' + status + '\\n';\n text += 'Image: ' + image;\n \n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Multiple matches - list them\nconst matchList = matches.map(c => {\n const name = normalizeName(c.Names[0]);\n return ' \\u2022 ' + name + ' (' + c.State + ')';\n}).join('\\n');\n\nreturn [{\n json: {\n chatId: chatId,\n text: 'Multiple matches for \"' + requestedName + '\":\\n' + matchList\n }\n}];"
+ "jsCode": "// Get Docker API response and message\nconst dockerOutput = $input.item.json.stdout;\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\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 chatId: chatId,\n error: true,\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\n// Check if user specified a container name in message (e.g., \"status plex\")\nconst text = (message.text || '').toLowerCase().trim();\nlet requestedName = null;\n\n// Extract container name if message is more than just \"status\"\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n // Join all parts except \"status\" as the container query\n requestedName = parts.filter(p => p !== 'status').join(' ');\n}\n\n// If no container name specified, return summary\nif (!requestedName) {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n \n const total = containers.length;\n const running = counts['running'] || 0;\n const stopped = counts['exited'] || 0;\n const other = total - running - stopped;\n \n let summary = 'Docker Containers\\n\\n';\n summary += 'Running: ' + running + '/' + total + '\\n';\n if (stopped > 0) summary += 'Stopped: ' + stopped + '\\n';\n if (other > 0) summary += 'Other: ' + other + '\\n';\n \n // List running containers\n const runningContainers = containers\n .filter(c => c.State === 'running')\n .map(c => {\n const name = normalizeName(c.Names[0]);\n const uptime = c.Status;\n return ' \\u2022 ' + name + ' - ' + uptime;\n });\n \n if (runningContainers.length > 0) {\n summary += '\\nRunning:\\n' + runningContainers.join('\\n');\n }\n \n return [{\n json: {\n chatId: chatId,\n text: summary\n }\n }];\n}\n\n// Find matching containers\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n});\n\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: 'No container found matching \"' + requestedName + '\"'\n }\n }];\n}\n\nif (matches.length === 1) {\n const container = matches[0];\n const name = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n let text = '' + name + '\\n';\n text += 'State: ' + state + '\\n';\n text += 'Status: ' + status + '\\n';\n text += 'Image: ' + image;\n \n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Multiple matches - list them\nconst matchList = matches.map(c => {\n const name = normalizeName(c.Names[0]);\n return ' \\u2022 ' + name + ' (' + c.State + ')';\n}).join('\\n');\n\nreturn [{\n json: {\n chatId: chatId,\n text: 'Multiple matches for \"' + requestedName + '\":\\n' + matchList\n }\n}];"
},
"id": "code-parse-match",
"name": "Parse and Match",
@@ -426,7 +426,7 @@
},
{
"parameters": {
- "jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.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: \"Can't reach Docker - check if n8n has socket access\"\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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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 action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
+ "jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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 action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
},
"id": "code-match-container",
"name": "Match Container",
@@ -573,7 +573,7 @@
},
{
"parameters": {
- "jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${stderr.trim()}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${errorMsg}`\n }\n};"
+ "jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};"
},
"id": "code-parse-action-result",
"name": "Parse Action Result",
@@ -1035,7 +1035,7 @@
},
{
"parameters": {
- "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${stderr.trim()}`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${errorMsg}`,\n answerText: 'Action failed'\n }\n};"
+ "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\n answerText: 'Action failed'\n }\n};"
},
"id": "code-parse-callback-result",
"name": "Parse Callback Result",
@@ -1287,7 +1287,7 @@
},
{
"parameters": {
- "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.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: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\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 matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results\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 chatId: chatId,\n allContainers: containers\n }\n}];"
+ "jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.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(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results\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 chatId: chatId,\n allContainers: containers\n }\n}];"
},
"id": "code-match-update-container",
"name": "Match Update Container",
@@ -1772,7 +1772,7 @@
},
{
"parameters": {
- "jsCode": "// Parse start result and format success message\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Start Command').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n chatId,\n text: `Container created but failed to start: HTTP ${statusCode}`\n }\n };\n}\n\nconst message = `${containerName} updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message\n }\n};"
+ "jsCode": "// Parse start result and format success message\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Start Command').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n chatId,\n text: `Failed to update ${containerName}`\n }\n };\n}\n\nconst message = `${containerName} updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message\n }\n};"
},
"id": "code-format-update-result",
"name": "Format Update Result",
@@ -1837,7 +1837,7 @@
},
{
"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: \"Can't reach Docker - check if n8n has socket access\"\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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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}];"
+ "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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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",