diff --git a/n8n-confirmation.json b/n8n-confirmation.json new file mode 100644 index 0000000..811851b --- /dev/null +++ b/n8n-confirmation.json @@ -0,0 +1,603 @@ +{ + "name": "Confirmation Dialogs", + "nodes": [ + { + "parameters": { + "inputSource": "passthrough", + "schema": { + "schemaType": "fromFields", + "inputFieldName": "", + "fields": [ + { + "fieldName": "chatId", + "fieldType": "number" + }, + { + "fieldName": "messageId", + "fieldType": "number" + }, + { + "fieldName": "action", + "fieldType": "string" + }, + { + "fieldName": "containerId", + "fieldType": "string" + }, + { + "fieldName": "containerName", + "fieldType": "string" + }, + { + "fieldName": "confirmAction", + "fieldType": "string" + }, + { + "fieldName": "confirmationToken", + "fieldType": "string" + }, + { + "fieldName": "expired", + "fieldType": "boolean" + }, + { + "fieldName": "responseMode", + "fieldType": "string" + } + ] + } + }, + "id": "trigger-confirm-workflow", + "name": "When executed by another workflow", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + 240, + 300 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "id": "show-stop", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-show-stop", + "leftValue": "={{ $json.action }}", + "rightValue": "show_stop", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "show_stop" + }, + { + "id": "show-update", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-show-update", + "leftValue": "={{ $json.action }}", + "rightValue": "show_update", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "show_update" + }, + { + "id": "confirm-stop", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-confirm", + "leftValue": "={{ $json.action }}", + "rightValue": "confirm", + "operator": { + "type": "string", + "operation": "equals" + } + }, + { + "id": "is-stop-action", + "leftValue": "={{ $json.confirmAction }}", + "rightValue": "stop", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "confirm_stop" + }, + { + "id": "confirm-update", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-confirm", + "leftValue": "={{ $json.action }}", + "rightValue": "confirm", + "operator": { + "type": "string", + "operation": "equals" + } + }, + { + "id": "is-update-action", + "leftValue": "={{ $json.confirmAction }}", + "rightValue": "update", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "confirm_update" + }, + { + "id": "cancel", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-cancel", + "leftValue": "={{ $json.action }}", + "rightValue": "cancel", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "cancel" + }, + { + "id": "expired", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-expired", + "leftValue": "={{ $json.expired }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "expired" + } + ] + }, + "options": { + "fallbackOutput": "none" + } + }, + "id": "switch-confirm-action", + "name": "Route Confirmation Action", + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, + "position": [ + 460, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Build Stop Confirmation dialog with timestamp for 30-second timeout\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Stop ${containerName}?\\n\\nThis will stop the container immediately.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n action: 'show_stop',\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-build-stop-confirm", + "name": "Build Stop Confirmation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 100 + ] + }, + { + "parameters": { + "jsCode": "// Build Update Confirmation dialog with timestamp for 30-second timeout\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Update', callback_data: `confirm:update:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Update ${containerName}?\\n\\nThis will pull the latest image and recreate the container.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n action: 'show_update',\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-build-update-confirm", + "name": "Build Update Confirmation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 200 + ] + }, + { + "parameters": { + "jsCode": "// Confirmation expired - return expired response\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Build keyboard for expired message\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n action: 'expired',\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for ${containerName} has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-handle-expired", + "name": "Handle Expired", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 600 + ] + }, + { + "parameters": { + "jsCode": "// Cancel confirmation - return to container submenu\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n action: 'cancel',\n containerName,\n chatId,\n messageId\n }\n};" + }, + "id": "code-handle-cancel", + "name": "Handle Cancel", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 500 + ] + }, + { + "parameters": { + "jsCode": "// Check if expired BEFORE executing confirm action\nconst data = $('When executed by another workflow').item.json;\nconst expired = data.expired === true;\n\nif (expired) {\n // Return expired action - main workflow will handle display\n return {\n json: {\n ...data,\n action: 'expired',\n expired: true\n }\n };\n}\n\n// Not expired, proceed with stop confirmation\nreturn {\n json: {\n ...data,\n action: 'confirm_stop',\n expired: false\n }\n};" + }, + "id": "code-check-stop-expired", + "name": "Check Stop Expired", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 300 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "is-expired", + "leftValue": "={{ $json.expired }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + "id": "if-stop-expired", + "name": "Is Stop Expired?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 920, + 300 + ] + }, + { + "parameters": { + "jsCode": "// Prepare input for container actions sub-workflow (stop action)\nconst data = $('When executed by another workflow').item.json;\n\nreturn {\n json: {\n containerId: data.containerId || '',\n containerName: data.containerName,\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode || 'inline'\n }\n};" + }, + "id": "code-prepare-stop-action", + "name": "Prepare Stop Action", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1140, + 350 + ] + }, + { + "parameters": { + "source": "database", + "workflowId": { + "__rl": true, + "mode": "list", + "value": "fYSZS5PkH0VSEaT5" + }, + "mode": "once", + "options": { + "waitForSubWorkflow": true + } + }, + "id": "execute-actions-stop", + "name": "Execute Stop Action", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [ + 1360, + 350 + ] + }, + { + "parameters": { + "jsCode": "// Format stop result with keyboard\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:stop:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n action: 'confirm_stop_result',\n success,\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-format-stop-result", + "name": "Format Stop Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1580, + 350 + ] + }, + { + "parameters": { + "jsCode": "// Build expired response for stop confirmation\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n action: 'expired',\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for ${containerName} has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-stop-expired-response", + "name": "Stop Expired Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1140, + 250 + ] + }, + { + "parameters": { + "jsCode": "// Check if expired BEFORE executing update confirm action\nconst data = $('When executed by another workflow').item.json;\nconst expired = data.expired === true;\n\nif (expired) {\n // Return expired action - main workflow will handle display\n return {\n json: {\n ...data,\n action: 'expired',\n expired: true\n }\n };\n}\n\n// Not expired, proceed with update confirmation\nreturn {\n json: {\n ...data,\n action: 'confirm_update',\n expired: false\n }\n};" + }, + "id": "code-check-update-expired", + "name": "Check Update Expired", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 400 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "is-expired", + "leftValue": "={{ $json.expired }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + "id": "if-update-expired", + "name": "Is Update Expired?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 920, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Build expired response for update confirmation\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n action: 'expired',\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for ${containerName} has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-update-expired-response", + "name": "Update Expired Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1140, + 350 + ] + }, + { + "parameters": { + "jsCode": "// Return confirm_update action for main workflow to handle\n// Main workflow will call the update sub-workflow\nconst data = $('When executed by another workflow').item.json;\n\nreturn {\n json: {\n action: 'confirm_update',\n containerId: data.containerId || '',\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode || 'inline'\n }\n};" + }, + "id": "code-return-update-action", + "name": "Return Update Action", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1140, + 450 + ] + } + ], + "connections": { + "When executed by another workflow": { + "main": [ + [ + { + "node": "Route Confirmation Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route Confirmation Action": { + "main": [ + [ + { + "node": "Build Stop Confirmation", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build Update Confirmation", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Check Stop Expired", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Check Update Expired", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Handle Cancel", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Handle Expired", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Stop Expired": { + "main": [ + [ + { + "node": "Is Stop Expired?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Is Stop Expired?": { + "main": [ + [ + { + "node": "Stop Expired Response", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Stop Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Stop Action": { + "main": [ + [ + { + "node": "Execute Stop Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Stop Action": { + "main": [ + [ + { + "node": "Format Stop Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Update Expired": { + "main": [ + [ + { + "node": "Is Update Expired?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Is Update Expired?": { + "main": [ + [ + { + "node": "Update Expired Response", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Return Update Action", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "any" + } +}