-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworkflows_export.json
More file actions
1 lines (1 loc) · 205 KB
/
Copy pathworkflows_export.json
File metadata and controls
1 lines (1 loc) · 205 KB
1
[{"updatedAt":"2026-03-11T14:24:28.200Z","createdAt":"2026-02-04T22:18:00.467Z","id":"F6-LX1h1pe6d9ko-YpEiX","name":"Initial Bot","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{"updates":["message"],"additionalFields":{}},"type":"n8n-nodes-base.telegramTrigger","typeVersion":1.2,"position":[-2304,592],"id":"3eff5869-bada-4e8b-8b11-e9e35633b8af","name":"Telegram Trigger","webhookId":"954427f9-0722-4eb6-b151-9abd97acaf38","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{"promptType":"define","text":"={{ $json.message }}","options":{"systemMessage":"Eres **Sara**, la asistente virtual de la empresa. Atiendes consultas comerciales y técnicas básicas.\n\nTu tono debe ser:\n\n* profesional\n* claro\n* breve\n* empático\n\nResponde siempre en español.\n\n---\n\n# HERRAMIENTAS DISPONIBLES\n\n1. **busqueda_productos**\n Base de datos oficial con precios, planes, servicios y características técnicas.\n\n2. **guardar_lead**\n Guarda los datos de contacto de un cliente interesado.\n\n3. **registrar_duda_tecnica**\n Registra un problema técnico para que el equipo lo revise.\n\n---\n\n# REGLAS PRINCIPALES\n\n## 1. CONSULTAR PRODUCTOS\n\nSi el usuario pregunta sobre:\n\n* precios\n* costos\n* planes\n* servicios\n* características técnicas\n* qué productos ofrece la empresa\n\nDEBES usar la herramienta **busqueda_productos** antes de responder.\n\nNunca inventes información.\n\n---\n\n## 2. RESPUESTAS\n\nLas respuestas deben ser:\n\n* máximo **2 párrafos**\n* claras y directas\n* basadas en la información obtenida con la herramienta\n\nSi la herramienta devuelve mucha información:\n\n* resume lo más importante\n* destaca precio y qué incluye.\n\n---\n\n## 3. CUANDO NO HAY INFORMACIÓN\n\nSi la herramienta no devuelve resultados:\n\n1. pide disculpas\n2. solicita el correo del usuario\n3. explica que un especialista lo contactará.\n\n---\n\n## 4. REGISTRAR LEADS\n\nSi el usuario:\n\n* muestra interés real en contratar\n* pide información para comprar\n* proporciona datos de contacto\n\nDEBES usar inmediatamente **guardar_lead**.\n\nLuego confirma al usuario que:\n\n* sus datos fueron registrados\n* un asesor lo contactará pronto.\n\n---\n\n## 5. SOPORTE TÉCNICO\n\nSi el usuario indica que algo:\n\n* no funciona\n* tiene un error\n* presenta un problema técnico\n\nPrimero intenta consultar **busqueda_productos**.\n\nSi no encuentras solución:\nusa **registrar_duda_tecnica**.\n\nLuego informa al usuario que:\nun técnico revisará el caso y se contactará pronto.\n\n---\n\n# COMPORTAMIENTO IMPORTANTE\n\n* No inventes precios ni información técnica.\n* Siempre usa las herramientas cuando corresponda.\n* Mantén respuestas cortas y útiles.\n* Prioriza ayudar al usuario y guiarlo hacia una solución.\n\nAl finalizar cada respuesta, ofrece siempre una acción siguiente lógica en una sola frase breve.\n\nEjemplo: \"¿Te gustaría ver nuestros planes de precios o prefieres hablar con un técnico?\""}},"type":"@n8n/n8n-nodes-langchain.agent","typeVersion":3.1,"position":[-592,944],"id":"3079c93b-9ffa-4ce8-b4a8-bc425720fa57","name":"AI Agent"},{"parameters":{"modelName":"models/gemini-2.5-flash-lite","options":{"temperature":0.4,"topP":1}},"type":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","typeVersion":1,"position":[-896,1264],"id":"a0edede8-1d07-41fe-b027-018b09d7aa2e","name":"Google Gemini Chat Model","credentials":{"googlePalmApi":{"id":"aFcqI71CduhYfRTh","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"chatId":"={{ $('Telegram Trigger').first().json.message.chat.id }}","text":"={{$json.output.replace(/([_*[\\]()~`>#+=|{}.!-])/g, '\\\\$1')}}","additionalFields":{"appendAttribution":false,"parse_mode":"=MarkdownV2","message_thread_id":0}},"type":"n8n-nodes-base.telegram","typeVersion":1.2,"position":[-16,1072],"id":"40ed40ac-638b-4b16-98e3-3d333dbba38c","name":"Send a text message","webhookId":"2976bfbb-0c20-48d9-b960-0b3371e38523","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{"sessionIdType":"customKey","sessionKey":"id"},"type":"@n8n/n8n-nodes-langchain.memoryBufferWindow","typeVersion":1.3,"position":[-1776,1824],"id":"9c9dc619-0d61-4ead-92fe-a071caa72145","name":"Simple Memory","disabled":true},{"parameters":{"sessionIdType":"customKey","sessionKey":"={{ $('Telegram Trigger').item.json.message.chat.id }}"},"type":"@n8n/n8n-nodes-langchain.memoryPostgresChat","typeVersion":1.3,"position":[-1968,1808],"id":"9aa8a13d-0e94-40ad-a52a-5616b82fab8f","name":"Postgres Chat Memory","credentials":{"postgres":{"id":"PpeWzoXZlzXJOsDX","name":"chatwoot"}},"disabled":true},{"parameters":{"sessionIdType":"customKey","sessionKey":"={{ $json.chat_id }}","sessionTTL":100},"type":"@n8n/n8n-nodes-langchain.memoryRedisChat","typeVersion":1.5,"position":[-752,1280],"id":"c09233f8-a98b-494c-8ec2-2be1989a4900","name":"Redis Chat Memory","credentials":{"redis":{"id":"jAyEo3o5v0RaQJoI","name":"Redis account"}}},{"parameters":{"amount":2},"type":"n8n-nodes-base.wait","typeVersion":1.1,"position":[-1984,592],"id":"64daac70-ac6d-457a-b4f5-82d84219db19","name":"Wait","webhookId":"59d5c574-c5cc-4e65-9417-37b99b15fdd3"},{"parameters":{"operation":"get","key":"={{ $('Telegram Trigger').item.json.message.chat.id.toString() }}","options":{}},"type":"n8n-nodes-base.redis","typeVersion":1,"position":[-1824,592],"id":"242162ff-5560-4633-9f9e-865fee49914f","name":"Get Last messages","credentials":{"redis":{"id":"jAyEo3o5v0RaQJoI","name":"Redis account"}}},{"parameters":{"operation":"set","key":"={{ $json.message.chat.id.toString() }}","value":"={{ $json.message.text || \"\"}}"},"type":"n8n-nodes-base.redis","typeVersion":1,"position":[-2128,592],"id":"10e36dfe-c928-4866-a87a-773dba435240","name":"Store messages","credentials":{"redis":{"id":"jAyEo3o5v0RaQJoI","name":"Redis account"}}},{"parameters":{"content":"Tomar mensaje y decidir accion","height":80},"type":"n8n-nodes-base.stickyNote","position":[-2032,464],"typeVersion":1,"id":"8f1f3fe2-ea89-477c-90a6-872737f715c0","name":"Sticky Note"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":3},"conditions":[{"id":"f65beecc-1a5c-4eb1-b5c5-01b48cc0a143","leftValue":"={{ $('Edit Fields2').item.json.message }}","rightValue":"^.{0,2}$","operator":{"type":"string","operation":"regex"}}],"combinator":"and"},"renameOutput":true,"outputKey":"Trash"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":3},"conditions":[{"id":"dfb1da51-d785-43e7-b0b9-ba080d2474a3","leftValue":"={{ $('Edit Fields2').item.json.message }}","rightValue":"^/","operator":{"type":"string","operation":"regex"}}],"combinator":"and"},"renameOutput":true,"outputKey":"Commands"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":3},"conditions":[{"id":"a453c820-ff0b-406b-86da-4dd17e5371b6","leftValue":"={{ $json.message }}","rightValue":"^(Ver Precios|Hablar con Humano|Soporte Técnico)$","operator":{"type":"string","operation":"regex"}}],"combinator":"and"},"renameOutput":true,"outputKey":"Telegram Buttons"}]},"options":{"fallbackOutput":"extra"}},"type":"n8n-nodes-base.switch","typeVersion":3.4,"position":[-1456,576],"id":"28d5bec5-1d55-43fe-b2a9-cb18d9b39608","name":"Switch"},{"parameters":{"chatId":"={{ $('Telegram Trigger').item.json.message.chat.id }}","text":"¡Hola! Soy tu asistente. Puedes preguntarme sobre nuestros servicios o precios.","replyMarkup":"replyKeyboard","replyKeyboard":{"rows":[{"row":{"buttons":[{"text":"Ver Precios","additionalFields":{}},{"text":"Hablar con Humano","additionalFields":{}},{"text":"Soporte Técnico","additionalFields":{}}]}}]},"replyKeyboardOptions":{"resize_keyboard":true,"one_time_keyboard":true},"additionalFields":{"appendAttribution":false}},"type":"n8n-nodes-base.telegram","typeVersion":1.2,"position":[-1104,368],"id":"c8a353d3-2c33-408a-be33-44203d614456","name":"Send greeting","webhookId":"a9474a33-6f72-4e0c-bcf6-1d3772c393c0","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{},"type":"n8n-nodes-base.noOp","typeVersion":1,"position":[-1104,176],"id":"229bba7d-0cb6-47e5-a6cb-c741384df119","name":"Do nothing"},{"parameters":{"operation":"delete","key":"={{ $('Telegram Trigger').item.json.message.chat.id.toString() }}"},"type":"n8n-nodes-base.redis","typeVersion":1,"position":[-1632,352],"id":"afc5e828-9c81-40fc-aaba-2632bb736cf6","name":"Clean buffer","credentials":{"redis":{"id":"jAyEo3o5v0RaQJoI","name":"Redis account"}}},{"parameters":{"tableName":"products_vectors","options":{}},"type":"@n8n/n8n-nodes-langchain.vectorStorePGVector","typeVersion":1.3,"position":[-144,1520],"id":"a8bb5953-3adc-4a5b-b477-7fbd2ee19369","name":"Postgres PGVector Store","credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"modelName":"models/gemini-embedding-001"},"type":"@n8n/n8n-nodes-langchain.embeddingsGoogleGemini","typeVersion":1,"position":[-160,1680],"id":"0edb3bf3-7408-458b-9b20-d85df4f24e12","name":"Embeddings Google Gemini","credentials":{"googlePalmApi":{"id":"aFcqI71CduhYfRTh","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"options":{"temperature":0.4,"topP":1}},"type":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","typeVersion":1,"position":[208,1504],"id":"ed630a15-bb01-4c62-9b26-7edeab3055f2","name":"Google Gemini Chat Model1","credentials":{"googlePalmApi":{"id":"aFcqI71CduhYfRTh","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"description":"Útil para buscar precios, características y detalles técnicos de los productos y planes. Úsala siempre que el usuario pregunte por costos o información de servicios."},"type":"@n8n/n8n-nodes-langchain.toolVectorStore","typeVersion":1.1,"position":[-16,1328],"id":"f513eb54-8833-4ecb-89ff-76a0a3702268","name":"busqueda_productos"},{"parameters":{"assignments":{"assignments":[{"id":"c4eff363-570f-4ef3-86a8-20bd00d7cd26","name":"chat_id","value":"={{ $('Telegram Trigger').item.json.message.chat.id }}","type":"string"},{"id":"6db5f221-77b2-4bf9-b429-01c4d9dbd4bb","name":"message","value":"={{ $('Get Last messages').item.json.propertyName }}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-1632,592],"id":"506aba18-de69-485e-a41a-43db2b4413a4","name":"Edit Fields2"},{"parameters":{"description":"Usa esta herramienta CUANDO el usuario te proporcione su nombre y forma de contacto (correo o teléfono) para registrarlo como interesado. Extrae el nombre, el dato de contacto y en qué producto mostró interés.","workflowId":{"__rl":true,"value":"5Uk6tIIr_4MZ4KqPeVlGb","mode":"list","cachedResultUrl":"/workflow/5Uk6tIIr_4MZ4KqPeVlGb","cachedResultName":"Tool - Guardar Lead"},"workflowInputs":{"mappingMode":"defineBelow","value":{},"matchingColumns":[],"schema":[],"attemptToConvertTypes":false,"convertFieldsToString":false}},"type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":2.2,"position":[-544,1296],"id":"6f21400e-fa55-4372-8397-4654336e1952","name":"guardar_lead"},{"parameters":{"description":"Registra una incidencia técnica para que el equipo de soporte la revise.\n\nUsa esta herramienta cuando:\n\nel usuario diga que algo no funciona\n\nexista un error técnico\n\nel usuario tenga una duda técnica que no puedes resolver con la base de datos\n\nel problema requiera intervención humana\n\nDespués de usarla, informa al usuario que un técnico revisará su caso pronto.","workflowId":{"__rl":true,"value":"ytj4-pDyJo4FePk1KMOhy","mode":"list","cachedResultUrl":"/workflow/ytj4-pDyJo4FePk1KMOhy","cachedResultName":"soporte_pendientes"},"workflowInputs":{"mappingMode":"defineBelow","value":{},"matchingColumns":[],"schema":[],"attemptToConvertTypes":false,"convertFieldsToString":false}},"type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":2.2,"position":[-320,1280],"id":"8c466b2d-c709-4ff5-9311-2b0d2b6bffb2","name":"registrar_duda_tecnica"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":3},"conditions":[{"leftValue":"={{ $json.message }}","rightValue":"Ver Precios","operator":{"type":"string","operation":"equals"},"id":"f3b1809c-8187-4ae7-8d8f-fa0ece27c10a"}],"combinator":"and"},"renameOutput":true,"outputKey":"See prices"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":3},"conditions":[{"id":"0f01cb79-74a5-45f8-ba16-3f07eca8748b","leftValue":"={{ $json.message }}","rightValue":"Hablar con Humano","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"Talk with human"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":3},"conditions":[{"id":"dad997c9-4a7a-40f7-b3bd-f87ca5328370","leftValue":"={{ $json.message }}","rightValue":"Soporte Técnico","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"Tech support"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.4,"position":[-1104,576],"id":"d4773ef4-3c89-41b9-a832-db6f6c2a5131","name":"check button answer"},{"parameters":{"assignments":{"assignments":[{"id":"3e5b4b5f-2240-4724-ab63-753425ccc4fc","name":"message","value":"Muestra los precios y servicios disponibles","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-864,448],"id":"21718fd8-97d6-4532-b562-d7fab2016a48","name":"Edit Fields"},{"parameters":{"assignments":{"assignments":[{"id":"2c84df8d-dbd2-4467-8c84-d527ef658552","name":"message","value":"Tengo un problema técnico, ayúdame o registrar mi caso","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-864,592],"id":"f235fd7d-3869-422b-a45a-808af9a202a1","name":"Edit Fields1"},{"parameters":{"assignments":{"assignments":[{"id":"5ac2d74a-8e97-45a4-bcfa-85190a239151","name":"message","value":"Quiero hablar con una persona","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-864,736],"id":"8e582224-12d9-4b17-896e-8e6f9ada3b48","name":"Edit Fields3"}],"connections":{"Google Gemini Chat Model":{"ai_languageModel":[[{"node":"AI Agent","type":"ai_languageModel","index":0}]]},"Telegram Trigger":{"main":[[{"node":"Store messages","type":"main","index":0}]]},"AI Agent":{"main":[[{"node":"Send a text message","type":"main","index":0}]]},"Simple Memory":{"ai_memory":[[]]},"Postgres Chat Memory":{"ai_memory":[[]]},"Redis Chat Memory":{"ai_memory":[[{"node":"AI Agent","type":"ai_memory","index":0}]]},"Wait":{"main":[[{"node":"Get Last messages","type":"main","index":0}]]},"Get Last messages":{"main":[[{"node":"Clean buffer","type":"main","index":0},{"node":"Edit Fields2","type":"main","index":0}]]},"Store messages":{"main":[[{"node":"Wait","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Do nothing","type":"main","index":0}],[{"node":"Send greeting","type":"main","index":0}],[{"node":"check button answer","type":"main","index":0}],[{"node":"AI Agent","type":"main","index":0}]]},"Clean buffer":{"main":[[]]},"Postgres PGVector Store":{"ai_vectorStore":[[{"node":"busqueda_productos","type":"ai_vectorStore","index":0}]]},"Embeddings Google Gemini":{"ai_embedding":[[{"node":"Postgres PGVector Store","type":"ai_embedding","index":0}]]},"Google Gemini Chat Model1":{"ai_languageModel":[[{"node":"busqueda_productos","type":"ai_languageModel","index":0}]]},"busqueda_productos":{"ai_tool":[[{"node":"AI Agent","type":"ai_tool","index":0}]]},"Edit Fields2":{"main":[[{"node":"Switch","type":"main","index":0}]]},"guardar_lead":{"ai_tool":[[{"node":"AI Agent","type":"ai_tool","index":0}]]},"registrar_duda_tecnica":{"ai_tool":[[{"node":"AI Agent","type":"ai_tool","index":0}]]},"check button answer":{"main":[[{"node":"Edit Fields","type":"main","index":0}],[{"node":"Edit Fields1","type":"main","index":0}],[{"node":"Edit Fields3","type":"main","index":0}]]},"Edit Fields":{"main":[[{"node":"AI Agent","type":"main","index":0}]]},"Edit Fields1":{"main":[[{"node":"AI Agent","type":"main","index":0}]]},"Edit Fields3":{"main":[[{"node":"AI Agent","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{"Telegram Trigger":[{"json":{"update_id":766412297,"message":{"message_id":12,"from":{"id":8482639810,"is_bot":false,"first_name":"Joe","language_code":"en"},"chat":{"id":8482639810,"first_name":"Joe","type":"private"},"date":1770569893,"text":"message from drtx"}}}]},"versionId":"917e2432-a985-4d98-b961-9c5f3f60a9d7","activeVersionId":"765b8273-1a78-4370-83a9-6adeafe43ee2","versionCounter":659,"triggerCount":1,"tags":[{"updatedAt":"2026-02-04T22:18:16.935Z","createdAt":"2026-02-04T22:18:16.935Z","id":"xtKfAF6slGzG18ni","name":"Orangutan"}],"shared":[{"updatedAt":"2026-02-04T22:18:00.475Z","createdAt":"2026-02-04T22:18:00.475Z","role":"workflow:owner","workflowId":"F6-LX1h1pe6d9ko-YpEiX","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-02-07T20:52:16.000Z","createdAt":"2026-02-07T20:52:00.229Z","id":"kBSFSpve0bo0pNNHc9Dka","name":"My workflow","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"updates":["message"],"additionalFields":{}},"type":"n8n-nodes-base.telegramTrigger","typeVersion":1.2,"position":[0,0],"id":"33bbf316-2df9-40ee-aeec-343ae056cb5b","name":"Telegram Trigger","webhookId":"954427f9-0722-4eb6-b151-9abd97acaf38","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}}],"connections":{},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":null,"pinData":{},"versionId":"e773dff8-faeb-4ab2-8251-e48f1649701f","activeVersionId":null,"versionCounter":4,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-02-07T20:52:00.242Z","createdAt":"2026-02-07T20:52:00.242Z","role":"workflow:owner","workflowId":"kBSFSpve0bo0pNNHc9Dka","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-03-07T22:15:03.127Z","createdAt":"2026-02-09T23:48:32.040Z","id":"AzXttskG2Ktfh8Cy5ODoX","name":"Carga de Productos","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0],"id":"685f7a41-d824-40c1-807a-92456218738f","name":"When clicking ‘Execute workflow’"},{"parameters":{"jsCode":"return [\n { json: { text: \"Producto: Plan Básico. Precio: $10/mes. Incluye: Acceso web y soporte por email.\" } },\n { json: { text: \"Producto: Plan Pro. Precio: $25/mes. Incluye: App móvil, web y soporte 24/7.\" } },\n { json: { text: \"Producto: Consultoría. Precio: $50/hora. Detalle: Reunión con experto.\" } }\n];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[208,0],"id":"756a90f2-87c6-4d68-a101-f14b76d92146","name":"Code in JavaScript"},{"parameters":{"mode":"insert","tableName":"products_vectors","options":{}},"type":"@n8n/n8n-nodes-langchain.vectorStorePGVector","typeVersion":1.3,"position":[432,0],"id":"a521ba3b-47eb-4df2-95bc-3927a954fe7f","name":"Postgres PGVector Store","credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"modelName":"models/gemini-embedding-001"},"type":"@n8n/n8n-nodes-langchain.embeddingsGoogleGemini","typeVersion":1,"position":[432,224],"id":"b79ad941-17be-4f9c-bae4-d7bfc3ff5f1e","name":"Embeddings Google Gemini","credentials":{"googlePalmApi":{"id":"aFcqI71CduhYfRTh","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"options":{}},"type":"@n8n/n8n-nodes-langchain.documentDefaultDataLoader","typeVersion":1.1,"position":[752,208],"id":"62be82ac-aae8-4d38-a601-f503a83088a9","name":"Default Data Loader"}],"connections":{"When clicking ‘Execute workflow’":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Code in JavaScript":{"main":[[{"node":"Postgres PGVector Store","type":"main","index":0}]]},"Embeddings Google Gemini":{"ai_embedding":[[{"node":"Postgres PGVector Store","type":"ai_embedding","index":0}]]},"Default Data Loader":{"ai_document":[[{"node":"Postgres PGVector Store","type":"ai_document","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"f51d4e21-0776-4cc4-8892-f00bda3fe1f4","activeVersionId":null,"versionCounter":21,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-02-09T23:48:32.049Z","createdAt":"2026-02-09T23:48:32.049Z","role":"workflow:owner","workflowId":"AzXttskG2Ktfh8Cy5ODoX","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-02-12T23:24:25.218Z","createdAt":"2026-02-12T19:48:50.795Z","id":"ludp_AOizJJSiY8QjXqx0","name":"🤖🧠 AI Agent Chatbot + LONG TERM Memory + Note Storage + Telegram","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"updates":["message"],"additionalFields":{"download":true}},"id":"82faa456-0add-4f4d-a845-fb87a1933776","name":"Get the Image","type":"n8n-nodes-base.telegramTrigger","position":[1264,528],"webhookId":"8404b32c-14bd-428e-88a6-560755f0f7ba","typeVersion":1.1,"credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{"chatId":"={{ $('Get the Image').item.json.message.chat.id }}","text":"={{ $json.content }}","additionalFields":{"appendAttribution":false}},"id":"a75323eb-2407-45f6-8cf6-c567717c78ce","name":"Send Content for the Analyzed image","type":"n8n-nodes-base.telegram","position":[2000,400],"typeVersion":1.1,"webhookId":"145d20f3-86b1-4bda-9e09-1c4232d4e984","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{"content":"# Automated Image Analysis and Response via Telegram\n\n## Example: @SubAlertMe_Bot\n\n## Summary:\nThe automated image analysis and response workflow using n8n is a sophisticated solution designed to streamline the process of analyzing images sent via Telegram and delivering insightful responses based on the analysis outcomes. This cutting-edge workflow employs a series of meticulously orchestrated nodes to ensure seamless automation and efficiency in image processing tasks.\n\n## Use Cases:\nThis advanced workflow caters to a myriad of scenarios where real-time image analysis and response mechanisms are paramount. The use cases include:\n- Providing immediate feedback on images shared within Telegram groups.\n- Enabling automated content moderation based on the analysis of image content.\n- Facilitating rapid categorization and tagging of images based on the results of the analysis.\n\n## Detailed Workflow Setup:\nTo effectively implement this workflow, users must adhere to a meticulous setup process, which includes:\n- Access to the versatile n8n platform, ensuring seamless workflow orchestration.\n- Integration of a Telegram account to facilitate image reception and communication.\n- Utilization of an OpenAI account for sophisticated image analysis capabilities.\n- Configuration of Telegram and OpenAI credentials within the n8n environment for seamless integration.\n- Proficiency in creating and interconnecting nodes within the n8n workflow for optimal functionality.\n\n## Detailed Node Description:\n1. **Get the Image (Telegram Trigger):**\n - Actively triggers upon receipt of an image via Telegram, ensuring prompt processing.\n - Extracts essential information from the received image message to initiate further actions.\n\n2. **Merge all fields To get data from trigger:**\n - Seamlessly amalgamates all relevant data fields extracted from the trigger node for comprehensive data consolidation.\n\n3. **Analyze Image (OpenAI):**\n - Harnesses the powerful capabilities of OpenAI services to conduct in-depth analysis of the received image.\n - Processes the image data in base64 format to derive meaningful insights from the visual content.\n\n4. **Aggregate all fields:**\n - Compiles and consolidates all data items for subsequent processing and analysis, ensuring comprehensive data aggregation.\n\n5. **Send Content for the Analyzed Image (Telegram):**\n - Transmits the analyzed content back to the Telegram chat interface for seamless communication.\n - Delivers the analyzed information in textual format, enhancing user understanding and interaction.\n\n6. **Switch Node:**\n - The Switch node is pivotal for decision-making based on predefined conditions within the workflow.\n - It evaluates incoming data to determine the existence or absence of specific elements, such as images in this context.\n - Utilizes a set of rules to assess the presence of image data in the message payload and distinguishes between cases where images are detected and when they are not.\n - This crucial node plays a pivotal role in directing the flow of the workflow based on the outcomes of its evaluations.\n\n\n\n## Conclusion:\nThe automation of image analysis processes through this sophisticated workflow not only enhances operational efficiency but also revolutionizes communication dynamics within Telegram interactions. By incorporating this advanced workflow solution, users can optimize their image analysis workflows, bolster communication efficacy, and unlock new levels of automation in image processing tasks.\n","height":1361.9843517631348,"width":1235.4238259410247},"id":"8fd94463-adb7-4c61-8afa-0318baa14401","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-144,-16],"typeVersion":1},{"parameters":{"chatId":"={{ $json.message.chat.id }}","text":"Please Upload an Image ....","additionalFields":{"appendAttribution":false}},"id":"c5f846d6-8147-4c32-b387-f494480faea8","name":"Update Telegram Error Message","type":"n8n-nodes-base.telegram","position":[2000,640],"typeVersion":1.1,"webhookId":"1dbb57c8-b1f6-44fd-b710-18ebe23ae339","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{"amount":3},"id":"14bf4d8d-f534-4be2-a1c6-26e3b7200403","name":"Wait","type":"n8n-nodes-base.wait","position":[1808,640],"webhookId":"d4d6fc13-d8ad-42b6-b4dd-e922b5534282","typeVersion":1.1},{"parameters":{"content":"","height":1359.6943046286056,"width":1139.7707284714515,"color":4},"id":"b72694cc-3520-4bab-830a-4f9eb98b2bec","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[1248,0],"typeVersion":1},{"parameters":{"resource":"image","operation":"analyze","inputType":"base64","options":{}},"id":"e30521e0-ac93-47d0-9138-be6fc60cadaf","name":"Analyze image","type":"@n8n/n8n-nodes-langchain.openAi","position":[1808,400],"typeVersion":1.3},{"parameters":{"rules":{"values":[{"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"operator":{"type":"array","operation":"exists","singleValue":true},"leftValue":"={{ $json.message.photo }}","rightValue":""}]},"renameOutput":true,"outputKey":"Image"},{"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"3fe3a96d-6ee9-4f12-a32c-f5f5b729e257","operator":{"type":"array","operation":"notExists","singleValue":true},"leftValue":"={{ $json.message.photo }}","rightValue":""}]},"renameOutput":true,"outputKey":"Empty"}]},"options":{}},"id":"e0e012fa-f6ab-4865-b435-77b522810e85","name":"Switch ( image or not )","type":"n8n-nodes-base.switch","position":[1440,528],"typeVersion":3},{"parameters":{"options":{}},"id":"accc45a0-dd03-4c63-b9b4-1e1b2ca094a0","name":"When chat message received","type":"@n8n/n8n-nodes-langchain.chatTrigger","position":[0,2128],"webhookId":"8ba8fa53-2c24-47a8-b4dd-67b88c106e3d","typeVersion":1.1},{"parameters":{"content":"## Retrieve Long Term Memories\nGoogle Docs","height":380,"width":340,"color":4},"id":"ca53ad09-309a-488f-adcc-a300c4606345","name":"Sticky Note7","type":"n8n-nodes-base.stickyNote","position":[224,1808],"typeVersion":1},{"parameters":{"content":"## Save To Current Chat Memory (Optional)","height":380,"width":280},"id":"03eec37d-9cce-422f-9075-3e319671cb27","name":"Sticky Note8","type":"n8n-nodes-base.stickyNote","position":[1360,2368],"typeVersion":1},{"parameters":{"content":"## Save Long Term Memories\nGoogle Docs","height":380,"width":280,"color":4},"id":"9fe49cd3-6940-49a4-bb17-4a96a85dea61","name":"Sticky Note9","type":"n8n-nodes-base.stickyNote","position":[1680,2368],"typeVersion":1},{"parameters":{"options":{}},"id":"b2e95ce8-4030-467f-9e9d-13aad0133f12","name":"gpt-4o-mini","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[1152,2432],"typeVersion":1.1},{"parameters":{"assignments":{"assignments":[{"id":"d6f68b1c-a6a6-44d4-8686-dc4dcdde4767","name":"output","type":"string","value":"={{ $json.output }}"}]},"options":{}},"id":"1dd84ecf-f643-41f6-b239-4555df0d9855","name":"Chat Response","type":"n8n-nodes-base.set","position":[1760,2128],"typeVersion":3.4},{"parameters":{"sessionIdType":"customKey","sessionKey":"={{ $('When chat message received').item.json.sessionId }}","contextWindowLength":50},"id":"afcf0b54-19f4-4d9a-a6e1-a1ae5d20001a","name":"Window Buffer Memory","type":"@n8n/n8n-nodes-langchain.memoryBufferWindow","position":[1472,2528],"typeVersion":1.3},{"parameters":{"descriptionType":"manual","toolDescription":"Save Memory","operation":"update","documentURL":"[Google Doc ID]","actionsUi":{"actionFields":[{"action":"insert","text":"={ \n \"memory\": \"{{ $fromAI('memory') }}\",\n \"date\": \"{{ $now }}\"\n}"}]}},"id":"d20d3e9a-9d74-4c9c-b07d-dac9e4b12707","name":"Save Long Term Memories","type":"n8n-nodes-base.googleDocsTool","position":[1792,2528],"typeVersion":2},{"parameters":{"operation":"get","documentURL":"[Google Doc ID]"},"id":"0ce2f469-0918-462f-8c4c-fb17decaf506","name":"Retrieve Long Term Memories","type":"n8n-nodes-base.googleDocs","position":[352,1952],"typeVersion":2,"alwaysOutputData":true},{"parameters":{"chatId":"=1234567891","text":"={{ $json.output }}","additionalFields":{"appendAttribution":false,"parse_mode":"HTML"}},"id":"25410197-36b4-4864-8535-0878a3b3a687","name":"Telegram Response","type":"n8n-nodes-base.telegram","position":[1760,1792],"typeVersion":1.2,"webhookId":"b6bcd0f8-2506-4eac-a22f-91417db4ab72","credentials":{"telegramApi":{"id":"utd0LpFieiak4Opj","name":"drtx_socierty_bot"}}},{"parameters":{"content":"## AI AGENT with Long Term Memory & Note Storage","height":780,"width":1320,"color":5},"id":"0927c530-038d-45ec-86ff-183f21ffada2","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[1008,2016],"typeVersion":1},{"parameters":{"model":"=deepseek-chat","options":{}},"id":"7cba49cd-07b0-48f8-b670-92bc8befb9be","name":"DeepSeek-V3 Chat","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[1152,2592],"typeVersion":1.1},{"parameters":{"promptType":"define","text":"={{ $('When chat message received').item.json.chatInput }}","options":{"systemMessage":"=## ROLE \nYou are a friendly, attentive, and helpful AI assistant. Your primary goal is to assist the user while maintaining a personalized and engaging interaction. \n\n---\n\n## RULES \n\n1. **Memory Management**: \n - When the user sends a new message, evaluate whether it contains noteworthy or personal information (e.g., preferences, habits, goals, or important events). \n - If such information is identified, use the **Save Memory** tool to store this data in memory. \n - Always send a meaningful response back to the user, even if your primary action was saving information. This response should not reveal that information was stored but should acknowledge or engage with the user’s input naturally. \n\n2. **Note Management**: \n - If the user provides information that is intended to be stored as a note (e.g., specific instructions, reminders, or standalone pieces of information), use the **Save Note** tool. \n - Notes should not be stored in memory using the **Save Memory** tool. \n - Ensure that notes are clear, concise, and accurately reflect the user’s input. \n\n3. **Context Awareness**: \n - Use stored memories and notes to provide contextually relevant and personalized responses. \n - Always consider the **date and time** when a memory or note was collected to ensure your responses are up-to-date and accurate.\n\n4. **User-Centric Responses**: \n - Tailor your responses based on the user's preferences and past interactions. \n - Be proactive in recalling relevant details from memory or notes when appropriate but avoid overwhelming the user with unnecessary information.\n\n5. **Privacy and Sensitivity**: \n - Handle all user data with care and sensitivity. Avoid making assumptions or sharing stored information unless it directly enhances the conversation or task at hand.\n - Never store passwords or usernames.\n\n6. **Fallback Responses**: \n - **IMPORTANT** If no specific task or question arises from the user’s message (e.g., when only saving information), respond in a way that keeps the conversation flowing naturally. For example: \n - Acknowledge their input: “Thanks for sharing that!” \n - Provide a friendly follow-up: “Is there anything else I can help you with today?” \n - DO NOT tell jokes as a fallback response.\n\n---\n\n## TOOLS \n\n### Save Memory \n- Use this tool to store summarized, concise, and meaningful information about the user. \n- Extract key details from user messages that could enhance future interactions (e.g., likes/dislikes, important dates, hobbies). \n- Ensure that the summary is clear and devoid of unnecessary details.\n\n### Save Note \n- Use this tool to store specific instructions, reminders, or standalone pieces of information provided by the user. \n- Notes should not include general personal preferences or habits meant for long-term memory storage. \n- Ensure that notes are concise and accurately reflect what the user wants to store.\n\n---\n\n## MEMORIES \n\n### Recent Noteworthy Memories \nHere are the most recent memories collected from the user, including their date and time of collection: \n\n**{{ $json.data[0].content }}**\n\n### Guidelines for Using Memories: \n- Prioritize recent memories but do not disregard older ones if they remain relevant. \n- Cross-reference memories to maintain consistency in your responses. For example, if a user shares conflicting preferences over time, clarify or adapt accordingly.\n\n---\n\n## NOTES \n\n### Recent Notes Collected from User: \nHere are the most recent notes collected from the user: \n\n**{{ $json.data[1].content }}**\n\n### Guidelines for Using Notes: \n- Use notes for tasks requiring specific instructions or reminders.\n- Do not mix note content with general memory content; keep them distinct.\n\n---\n\n## ADDITIONAL INSTRUCTIONS \n\n- Think critically before responding to ensure your answers are thoughtful and accurate. \n- Strive to build trust with the user by being consistent, reliable, and personable in your interactions. \n- Avoid robotic or overly formal language; aim for a conversational tone that aligns with being \"friendly and helpful.\" \n"}},"id":"1bc2f008-a9f9-455b-a4b0-b09d46381158","name":"AI Tools Agent","type":"@n8n/n8n-nodes-langchain.agent","position":[1392,2128],"typeVersion":1.7,"alwaysOutputData":false},{"parameters":{"content":"## Save Notes\nGoogle Docs","height":380,"width":280,"color":4},"id":"c5378000-73d4-4be5-ac26-2f4acc4cd192","name":"Sticky Note10","type":"n8n-nodes-base.stickyNote","position":[2000,2368],"typeVersion":1},{"parameters":{"descriptionType":"manual","toolDescription":"Save Notes","operation":"update","documentURL":"[Google Doc ID]","actionsUi":{"actionFields":[{"action":"insert","text":"={ \n \"note\": \"{{ $fromAI('memory') }}\",\n \"date\": \"{{ $now }}\"\n}"}]}},"id":"1cceacf5-1a91-477d-bd04-e7d39094121f","name":"Save Notes","type":"n8n-nodes-base.googleDocsTool","position":[2112,2528],"typeVersion":2},{"parameters":{"content":"## Retrieve Notes\nGoogle Docs","height":380,"width":340,"color":4},"id":"46265db0-559c-40b5-9efb-9c502da05e6d","name":"Sticky Note11","type":"n8n-nodes-base.stickyNote","position":[224,2208],"typeVersion":1},{"parameters":{"operation":"get","documentURL":"[Google Doc ID]"},"id":"871c854f-2492-4fa9-b80e-eea4b7378dac","name":"Retrieve Notes","type":"n8n-nodes-base.googleDocs","position":[352,2352],"typeVersion":2,"alwaysOutputData":true,"onError":"continueRegularOutput"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"id":"54cbd8c6-9470-466c-a608-1d05fd503386","name":"Aggregate","type":"n8n-nodes-base.aggregate","position":[848,2128],"typeVersion":1},{"parameters":{},"id":"8c53368e-033e-4a2d-b9fd-c8ed6e238bdb","name":"Merge","type":"n8n-nodes-base.merge","position":[672,2128],"typeVersion":3},{"parameters":{"content":"## LLM","height":380,"width":280,"color":3},"id":"3e09fe38-f34a-44c5-a37e-d54b8421b1d9","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[1040,2368],"typeVersion":1},{"parameters":{"content":"## Telegram \n(Optional)","height":300,"width":260},"id":"4ca36777-8d98-4af6-9a8d-1219307bb4c9","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[1680,1680],"typeVersion":1},{"parameters":{"operation":"getAll","calendar":{"__rl":true,"mode":"list","value":"derekcheungsa@gmail.com","cachedResultName":"derekcheungsa@gmail.com"},"options":{"timeMin":"={{$fromAI(\"date\",\"the date after which to fetch the messages in format YYYY-MM-DDTHH:MM:SS\")}}","fields":"=items(summary, start(dateTime))"}},"id":"7e63b3a2-8fd6-4361-8f03-7cc05cd53753","name":"Google Calendar","type":"n8n-nodes-base.googleCalendarTool","position":[3936,1904],"typeVersion":1.1},{"parameters":{"authentication":"serviceAccount","operation":"getAll","filters":{"labelIds":["INBOX","UNREAD"],"readStatus":"unread","receivedAfter":"={{$fromAI(\"date\",\"the date after which to fetch the messages in format YYYY-MM-DDTHH:MM:SS\")}}"}},"id":"5a21b84d-a9cd-41cf-aad3-31c4fbdd10fe","name":"Get Email","type":"n8n-nodes-base.gmailTool","position":[3808,1904],"typeVersion":2.1,"webhookId":"0113b940-9542-420a-8bd0-281943823225","credentials":{"googleApi":{"id":"ko2iHlnyUqInc173","name":"Gmail account"}}},{"parameters":{"model":"gpt-4o-mini","options":{}},"id":"9f12ec6c-6f9d-4335-beac-746cb1cc2829","name":"OpenAI Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[3568,1904],"typeVersion":1},{"parameters":{"updates":["message"],"additionalFields":{}},"id":"480abb9a-ddf1-4920-aba6-b962329cb875","name":"Listen for incoming events","type":"n8n-nodes-base.telegramTrigger","position":[2768,1584],"webhookId":"322dce18-f93e-4f86-b9b1-3305519b7834","typeVersion":1},{"parameters":{"chatId":"={{ $('Listen for incoming events').first().json.message.from.id }}","text":"={{ $json.output }}","additionalFields":{"appendAttribution":false,"parse_mode":"Markdown"}},"id":"143be0b3-d39a-45b5-9438-49011cbc859b","name":"Telegram","type":"n8n-nodes-base.telegram","position":[4208,1584],"typeVersion":1.1,"webhookId":"be2edf60-e1cc-45d6-b42e-4c3496351a9c","onError":"continueErrorOutput"},{"parameters":{"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"a0bf9719-4272-46f6-ab3b-eda6f7b44fd8","operator":{"type":"string","operation":"empty","singleValue":true},"leftValue":"={{ $json.message.text }}","rightValue":""}]},"options":{}},"id":"f649122a-ba0f-4fec-89b9-b90cc7ebd2b3","name":"If","type":"n8n-nodes-base.if","position":[3136,1584],"typeVersion":2.2},{"parameters":{"resource":"audio","operation":"transcribe","options":{}},"id":"04282846-4bfb-43e3-b144-592a22b7f0fd","name":"Speech to Text","type":"@n8n/n8n-nodes-langchain.openAi","position":[3552,1584],"typeVersion":1.3},{"parameters":{"fields":{"values":[{"name":"text","stringValue":"={{ $json?.message?.text || \"\" }}"}]},"options":{}},"id":"a6f64954-cfae-47df-88b5-8722644945fc","name":"Voice or Text","type":"n8n-nodes-base.set","position":[2976,1584],"typeVersion":3.2},{"parameters":{"resource":"file","fileId":"={{ $('Listen for incoming events').item.json.message.voice.file_id }}","additionalFields":{}},"id":"f83c30e4-59e1-4545-9e44-165e31792e6d","name":"Get Voice File","type":"n8n-nodes-base.telegram","position":[3312,1584],"typeVersion":1.1,"webhookId":"8ed040d3-ea52-4aa4-86c0-480ef35b0710"},{"parameters":{"promptType":"define","text":"={{ $json.text }}","options":{"systemMessage":"=You are a helpful assistant.\n\nToday's date is {{ $now }}.\n\nGuidelines:\n- When fetching emails, filter out any promotional emails. \n- When summarizing emails, include Sender, Message date, subject, and brief summary of email.\n- if the user did not specify a date in the request assume they are asking for today\n- Use baserow tool to answer questions about tasks\n- When answering questions about calendar events, filter out events that don't apply to the question. For example, the question is about events for today, only reply with events for today. Don't mention future events if it's more than 1 week away"}},"id":"caf9d632-fdf9-4938-9ff4-c8e4d2cb3243","name":"Angie, AI Assistant 👩🏻🏫","type":"@n8n/n8n-nodes-langchain.agent","position":[3712,1584],"typeVersion":1.6},{"parameters":{"databaseId":146496,"tableId":372174,"additionalOptions":{}},"id":"4a39101f-92eb-40b4-9bb8-e7d757c8dd32","name":"Tasks","type":"n8n-nodes-base.baserowTool","position":[4048,1904],"typeVersion":1},{"parameters":{"descriptionType":"manual","toolDescription":"Useful for getting contact information. For example emails or phone numbers.","databaseId":146496,"tableId":372177,"additionalOptions":{}},"id":"85185d76-0500-4dc8-a7da-3bb3d928b4b9","name":"Contacts","type":"n8n-nodes-base.baserowTool","position":[4176,1904],"typeVersion":1},{"parameters":{"content":"## Start here: Step-by Step Youtube Tutorial :star:\n\n[](https://youtu.be/pXjowPc6V2s)\n","height":389.78268107403096,"width":386.9292441979969,"color":5},"id":"b0fb9b06-504f-45c5-8776-3da12de724e7","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[2304,1456],"typeVersion":1},{"parameters":{"sessionIdType":"customKey","sessionKey":"={{ $('Listen for incoming events').first().json.message.from.id }}"},"id":"73a47fe1-31b5-4b25-90cf-6308e602b821","name":"Window Buffer Memory1","type":"@n8n/n8n-nodes-langchain.memoryBufferWindow","position":[3696,1904],"typeVersion":1.2},{"parameters":{"content":"## Process Telegram Request\n","height":278.75,"width":496.25,"color":5},"id":"e3a4d36f-602a-4111-8e8d-0005f31abe9b","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[2944,1488],"typeVersion":1}],"connections":{"Wait":{"main":[[{"node":"Update Telegram Error Message","type":"main","index":0}]]},"Analyze image":{"main":[[{"node":"Send Content for the Analyzed image","type":"main","index":0}]]},"Get the Image":{"main":[[{"node":"Switch ( image or not )","type":"main","index":0}]]},"Switch ( image or not )":{"main":[[{"node":"Analyze image","type":"main","index":0}],[{"node":"Wait","type":"main","index":0}]]},"Merge":{"main":[[{"node":"Aggregate","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"AI Tools Agent","type":"main","index":0}]]},"Save Notes":{"ai_tool":[[{"node":"AI Tools Agent","type":"ai_tool","index":0}]]},"gpt-4o-mini":{"ai_languageModel":[[{"node":"AI Tools Agent","type":"ai_languageModel","index":0}]]},"AI Tools Agent":{"main":[[{"node":"Telegram Response","type":"main","index":0},{"node":"Chat Response","type":"main","index":0}]]},"Retrieve Notes":{"main":[[{"node":"Merge","type":"main","index":1}]]},"Window Buffer Memory":{"ai_memory":[[{"node":"AI Tools Agent","type":"ai_memory","index":0}]]},"Save Long Term Memories":{"ai_tool":[[{"node":"AI Tools Agent","type":"ai_tool","index":0}]]},"When chat message received":{"main":[[{"node":"Retrieve Long Term Memories","type":"main","index":0},{"node":"Retrieve Notes","type":"main","index":0}]]},"Retrieve Long Term Memories":{"main":[[{"node":"Merge","type":"main","index":0}]]},"If":{"main":[[{"node":"Get Voice File","type":"main","index":0}],[{"node":"Angie, AI Assistant 👩🏻🏫","type":"main","index":0}]]},"Tasks":{"ai_tool":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"ai_tool","index":0}]]},"Contacts":{"ai_tool":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"ai_tool","index":0}]]},"Get Email":{"ai_tool":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"ai_tool","index":0}]]},"Voice or Text":{"main":[[{"node":"If","type":"main","index":0}]]},"Get Voice File":{"main":[[{"node":"Speech to Text","type":"main","index":0}]]},"Speech to Text":{"main":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"main","index":0}]]},"Google Calendar":{"ai_tool":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"ai_tool","index":0}]]},"OpenAI Chat Model":{"ai_languageModel":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"ai_languageModel","index":0}]]},"Listen for incoming events":{"main":[[{"node":"Voice or Text","type":"main","index":0}]]},"Angie, AI Assistant 👩🏻🏫":{"main":[[{"node":"Telegram","type":"main","index":0}]]},"Window Buffer Memory1":{"ai_memory":[[{"node":"Angie, AI Assistant 👩🏻🏫","type":"ai_memory","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":null,"pinData":{},"versionId":"942a40d6-976c-4259-baa7-5a8c6c1b2d70","activeVersionId":null,"versionCounter":9,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-02-12T19:48:50.801Z","createdAt":"2026-02-12T19:48:50.801Z","role":"workflow:owner","workflowId":"ludp_AOizJJSiY8QjXqx0","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-02-12T22:17:28.218Z","createdAt":"2026-02-12T21:22:54.554Z","id":"5Uk6tIIr_4MZ4KqPeVlGb","name":"Tool - Guardar Lead","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"workflowInputs":{"values":[{"name":"nombre"},{"name":"contacto"},{"name":"interes"}]}},"type":"n8n-nodes-base.executeWorkflowTrigger","typeVersion":1.1,"position":[0,0],"id":"bf6bf742-e502-4f8d-8985-dc12d9452077","name":"When Executed by Another Workflow"},{"parameters":{"schema":{"__rl":true,"mode":"list","value":"public"},"table":{"__rl":true,"value":"leads","mode":"list"},"columns":{"mappingMode":"defineBelow","value":{"nombre":"={{ $json.nombre }}","contacto":"={{ $json.contacto }}","interes":"={{ $json.interes }}"},"matchingColumns":["id"],"schema":[{"id":"id","displayName":"id","required":false,"defaultMatch":true,"display":true,"type":"number","canBeUsedToMatch":true,"removed":true},{"id":"nombre","displayName":"nombre","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"contacto","displayName":"contacto","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"interes","displayName":"interes","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"fecha","displayName":"fecha","required":false,"defaultMatch":false,"display":true,"type":"dateTime","canBeUsedToMatch":true,"removed":true}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{"replaceEmptyStrings":true}},"type":"n8n-nodes-base.postgres","typeVersion":2.6,"position":[208,0],"id":"52c5febc-c9e5-42d6-be51-6c01b26a8f89","name":"Insert rows in a table","credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}}],"connections":{"When Executed by Another Workflow":{"main":[[{"node":"Insert rows in a table","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{"When Executed by Another Workflow":[{"json":{"nombre":"David M.","contacto":"davidmanjarres2004@gmail.com","interes":"Plan pro"}}]},"versionId":"fef1012f-aef4-484c-9024-fcab6cc86d69","activeVersionId":null,"versionCounter":35,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-02-12T21:22:54.562Z","createdAt":"2026-02-12T21:22:54.562Z","role":"workflow:owner","workflowId":"5Uk6tIIr_4MZ4KqPeVlGb","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-03-07T22:58:12.454Z","createdAt":"2026-03-07T22:40:59.314Z","id":"ytj4-pDyJo4FePk1KMOhy","name":"soporte_pendientes","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"schema":{"__rl":true,"mode":"list","value":"public"},"table":{"__rl":true,"value":"soporte_pendientes","mode":"list","cachedResultName":"soporte_pendientes"},"columns":{"mappingMode":"defineBelow","value":{"telegram_id":"={{ $json.chatId }}","nombre_usuario":"={{ $json.username }}","duda_tecnica":"={{ $json.doubt }}"},"matchingColumns":["id"],"schema":[{"id":"id","displayName":"id","required":false,"defaultMatch":true,"display":true,"type":"number","canBeUsedToMatch":true,"removed":true},{"id":"telegram_id","displayName":"telegram_id","required":false,"defaultMatch":false,"display":true,"type":"number","canBeUsedToMatch":true},{"id":"nombre_usuario","displayName":"nombre_usuario","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"duda_tecnica","displayName":"duda_tecnica","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"fecha_registro","displayName":"fecha_registro","required":false,"defaultMatch":false,"display":true,"type":"dateTime","canBeUsedToMatch":true,"removed":true},{"id":"estado","displayName":"estado","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":true}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.postgres","typeVersion":2.6,"position":[-160,-80],"id":"4242151e-64f2-431f-9157-a1fecebfc932","name":"Insert rows in a table","credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"workflowInputs":{"values":[{"name":"chatId"},{"name":"username"},{"name":"doubt"}]}},"type":"n8n-nodes-base.executeWorkflowTrigger","typeVersion":1.1,"position":[-400,-80],"id":"a6a8ff30-06ee-4682-91d9-1fed5fbef51f","name":"When Executed by Another Workflow"}],"connections":{"When Executed by Another Workflow":{"main":[[{"node":"Insert rows in a table","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{"When Executed by Another Workflow":[{"json":{"chatId":188,"username":"DRTX2","doubt":"Cuanto vale la morzilla"}}]},"versionId":"1b68be22-61ce-448b-b8b5-a83a5f242605","activeVersionId":null,"versionCounter":25,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-03-07T22:40:59.321Z","createdAt":"2026-03-07T22:40:59.321Z","role":"workflow:owner","workflowId":"ytj4-pDyJo4FePk1KMOhy","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-04-18T17:46:16.430Z","createdAt":"2026-04-18T01:01:26.256Z","id":"YYJRilJQWoMbRVwB7_tP8","name":"🔍 Workflow 1 — Monitor PetSafe (30 min)","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":30}]}},"id":"87d2090e-79ca-447b-ba9a-503ef3f2cacf","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.1,"position":[-464,352]},{"parameters":{"assignments":{"assignments":[{"id":"w1c01","name":"TRELLO_API_KEY","value":"={{ $env.TRELLO_API_KEY }}","type":"string"},{"id":"w1c02","name":"TRELLO_TOKEN","value":"={{ $env.TRELLO_TOKEN }}","type":"string"},{"id":"w1c03","name":"TRELLO_BOARD_ID","value":"={{ $env.TRELLO_BOARD_ID }}","type":"string"},{"id":"w1c04","name":"GITHUB_TOKEN","value":"={{ $env.GITHUB_TOKEN }}","type":"string"},{"id":"w1c04b","name":"GITHUB_TOKEN_BACKEND","value":"={{ $env.GITHUB_TOKEN_BACKEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w1c04f","name":"GITHUB_TOKEN_FRONTEND","value":"={{ $env.GITHUB_TOKEN_FRONTEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w1c05","name":"GITHUB_OWNER_BACKEND","value":"={{ $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w1c05f","name":"GITHUB_OWNER_FRONTEND","value":"={{ $env.GITHUB_OWNER_FRONTEND || $env.GITHUB_OWNER || $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w1c06","name":"GITHUB_REPO_BACKEND","value":"={{ $env.GITHUB_REPO_BACKEND }}","type":"string"},{"id":"w1c07","name":"GITHUB_REPO_FRONTEND","value":"={{ $env.GITHUB_REPO_FRONTEND }}","type":"string"},{"id":"w1c08","name":"GEMINI_API_KEY","value":"={{ $env.GEMINI_API_KEY }}","type":"string"},{"id":"w1c09","name":"DISCORD_WEBHOOK_URL","value":"={{ $env.DISCORD_WEBHOOK_URL }}","type":"string"}]},"options":{}},"id":"712c643f-be2c-476a-8faf-57a9d5508f73","name":"Configuración","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-240,352]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/cards","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"name,desc,due,dueComplete,idMembers,idList,dateLastActivity,labels"},{"name":"attachments","value":"true"},{"name":"attachment_fields","value":"name,url,mimeType,isUpload,date"}]},"options":{}},"id":"8dd97636-4c6c-4899-97d6-c20bebb622cf","name":"Trello Cards","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,0]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/lists","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"}]},"options":{}},"id":"f273e197-d2fc-472f-a0de-dacdce76f457","name":"Trello Lists","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,112]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/members","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"fullName,username"}]},"options":{}},"id":"d8d971e6-1ec8-4430-a7cd-365731464d21","name":"Trello Members","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,208]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/checklists","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"name,idCard"},{"name":"checkItem_fields","value":"name,state,due"}]},"options":{}},"id":"bf1b3955-db54-4b34-975d-c1b8937a4f75","name":"Trello Checklists","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,304]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/commits?per_page=30","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"b9f88c68-8a82-4323-b773-cd87176fdcd8","name":"GitHub Back Commits","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,400]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/commits?per_page=30","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"f7f0085a-3a3e-4eb3-9bc2-9b5f178998b6","name":"GitHub Front Commits","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,512]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/pulls?state=all&per_page=20","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"52a2448d-9501-41c7-9660-d355018478c8","name":"GitHub Back PRs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,608]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/pulls?state=all&per_page=20","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"86af3d95-772c-4e93-8a32-c52d9358044f","name":"GitHub Front PRs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,704]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/actions/runs?per_page=5","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"2f15b88f-8f17-4b81-9b3f-97f4cb367ed0","name":"GitHub Back CI Runs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,800]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/actions/runs?per_page=5","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"46696b96-dbe1-4e7a-aa27-fcd9397096cf","name":"GitHub Front CI Runs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,912]},{"parameters":{"operation":"executeQuery","query":"SELECT id, fecha_ejecucion, hay_problemas, total_tareas, tareas_vencidas, progreso_pct, kpis_json FROM analisis_proyecto ORDER BY fecha_ejecucion DESC LIMIT 10;","options":{}},"id":"a211901e-06a5-4c2f-aa1e-9b8a9a178af4","name":"Postgres Historial","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[0,1008],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT nombre, tipo, fecha_inicio, fecha_fin, responsable, completado FROM hitos_proyecto ORDER BY fecha_fin ASC;","options":{}},"id":"a341775d-033c-4bca-8e41-aadef6df1ce0","name":"Postgres Hitos","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[0,1104],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"numberInputs":12},"id":"12a421d2-f340-451c-85d8-cc659983ef6b","name":"Merge Datos","type":"n8n-nodes-base.merge","typeVersion":3,"position":[288,512]},{"parameters":{"jsCode":"const now = new Date();\nconst items = $input.all().map(i => i.json);\n\nconst MEMBER_MAP = {\n 'Fernando Joel Bonilla Guerrero': { github: 'JoelBonillaG', initials: 'FG' },\n 'Joel Bonilla': { github: 'JoelBonillaG', initials: 'FG' },\n 'David Manjarres': { github: 'DRTX2', initials: 'DM' },\n 'David Oswaldo Manjarres Quintero': { github: 'DRTX2', initials: 'DM' },\n 'David Josue Barragan Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'David Josué Barragán Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'David Barragán Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'David Barragan Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'Josue Joel Garcia Abata': { github: 'JosueGarciaAbata', initials: 'JA' },\n 'Josué Joel García Abata': { github: 'JosueGarciaAbata', initials: 'JA' },\n 'Josué García Abata': { github: 'JosueGarciaAbata', initials: 'JA' },\n 'Josue Garcia Abata': { github: 'JosueGarciaAbata', initials: 'JA' }\n};\n\nfunction resolveMember(fullName) {\n if (!fullName) return { github: null, initials: '??' };\n if (MEMBER_MAP[fullName]) return MEMBER_MAP[fullName];\n const norm = fullName.toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '');\n for (const [k, v] of Object.entries(MEMBER_MAP)) {\n if (k.toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '') === norm) return v;\n }\n const parts = fullName.split(' ').filter(Boolean);\n const initials = (parts[0]?.[0] || '?') + (parts[parts.length - 1]?.[0] || '');\n return { github: null, initials: initials.toUpperCase() };\n}\n\nconst backRepo = $node['Configuración'].json.GITHUB_REPO_BACKEND;\nconst frontRepo = $node['Configuración'].json.GITHUB_REPO_FRONTEND;\n\nlet cards = [], lists = [], members = [], historial = [], hitos = [], checklists = [];\nlet bRuns = [], fRuns = [];\n\nitems.forEach(d => {\n if (!d || typeof d !== 'object') return;\n if (d.workflow_runs !== undefined && Array.isArray(d.workflow_runs)) {\n const first = d.workflow_runs[0];\n const repoName = first?.repository?.name || first?.head_repository?.name || '';\n if (repoName === backRepo) bRuns = d.workflow_runs;\n else if (repoName === frontRepo) fRuns = d.workflow_runs;\n else if (!bRuns.length) bRuns = d.workflow_runs;\n else fRuns = d.workflow_runs;\n return;\n }\n if (d.idCard && Array.isArray(d.checkItems)) { checklists.push(d); return; }\n if (d.nombre && d.tipo && d.fecha_fin !== undefined) { hitos.push(d); return; }\n if (d.fecha_ejecucion !== undefined) { historial.push(d); return; }\n if (d.id && d.name && d.idList !== undefined && d.due !== undefined) { cards.push(d); return; }\n if (d.id && d.name && d.closed !== undefined && !d.idList) { lists.push(d); return; }\n if (d.id && (d.fullName || d.username) && !d.idList && !d.name) { members.push(d); return; }\n});\n\nconst allCommits = items.filter(d => d && d.sha && d.commit);\nconst bCommits = allCommits.filter(c => c.url && c.url.includes('/' + backRepo + '/'));\nconst fCommits = allCommits.filter(c => c.url && c.url.includes('/' + frontRepo + '/'));\n\nconst allPRs = items.filter(d => d && d.number && d.state && d.pull_request !== undefined);\nconst bPRs = allPRs.filter(p => p.base?.repo?.name === backRepo);\nconst fPRs = allPRs.filter(p => p.base?.repo?.name === frontRepo);\n\nconst listaById = {};\nlists.forEach(l => { listaById[l.id] = l.name; });\nconst miembroById = {};\nmembers.forEach(m => { miembroById[m.id] = m.fullName || m.username; });\n\nconst checklistsByCard = {};\nchecklists.forEach(ch => {\n if (!checklistsByCard[ch.idCard]) checklistsByCard[ch.idCard] = [];\n checklistsByCard[ch.idCard].push(ch);\n});\n\nfunction normFase(s) {\n return (s || '').toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').trim();\n}\n\nconst tareasRaw = cards.map(c => {\n const due = c.due ? new Date(c.due) : null;\n const vencida = due && !c.dueComplete && due < now;\n const diasRestantes = due ? Math.floor((due - now) / 86400000) : null;\n const atts = c.attachments || [];\n const enlaces = atts.filter(a => a.url && a.isUpload === false).map(a => ({ name: a.name || a.url, url: a.url }));\n const archivos = atts.filter(a => a.isUpload === true).map(a => ({ name: a.name, url: a.url, mimeType: a.mimeType || '' }));\n const pdfs = archivos.filter(a => (a.mimeType || '').includes('pdf') || (a.name || '').toLowerCase().endsWith('.pdf'));\n const chArr = checklistsByCard[c.id] || [];\n const chTotal = chArr.reduce((s, ch) => s + (ch.checkItems?.length || 0), 0);\n const chDone = chArr.reduce((s, ch) => s + (ch.checkItems?.filter(i => i.state === 'complete').length || 0), 0);\n const daysSince = c.dateLastActivity ? Math.floor((now - new Date(c.dateLastActivity)) / 86400000) : null;\n const responsables = (c.idMembers || []).map(id => miembroById[id] || id);\n return {\n id: c.id,\n nombre: c.name,\n lista: listaById[c.idList] || 'desconocida',\n completada: !!c.dueComplete,\n fecha_limite: c.due,\n dias_restantes: diasRestantes,\n vencida: !!vencida,\n responsables,\n responsables_ids: c.idMembers || [],\n adjuntos_count: atts.length,\n enlaces,\n archivos,\n pdfs_count: pdfs.length,\n checklist_total: chTotal,\n checklist_done: chDone,\n checklist_pct: chTotal > 0 ? Math.round(chDone / chTotal * 100) : null,\n dias_sin_actividad: daysSince,\n dateLastActivity: c.dateLastActivity,\n stale: daysSince !== null && daysSince > 5 && !c.dueComplete,\n sin_asignar: (c.idMembers || []).length === 0,\n sin_fecha: !c.due\n };\n});\n\nconst tareas = tareasRaw.filter(t => t.nombre && t.nombre.trim() !== '.');\n\nconst fases = {};\nfor (const t of tareas) {\n const key = normFase(t.lista);\n if (!fases[key]) fases[key] = { nombre: t.lista, tareas: [], completadas: 0, vencidas: 0, pendientes: 0 };\n fases[key].tareas.push(t);\n if (t.completada) fases[key].completadas++;\n else if (t.vencida) fases[key].vencidas++;\n else fases[key].pendientes++;\n}\nfor (const f of Object.values(fases)) {\n f.total = f.tareas.length;\n f.pct = f.total > 0 ? Math.round(f.completadas / f.total * 100) : 0;\n if (f.vencidas > 0) f.semaforo = 'rojo';\n else if (f.pct >= 80) f.semaforo = 'verde';\n else if (f.pct >= 40) f.semaforo = 'amarillo';\n else f.semaforo = 'gris';\n}\n\nconst vencidas = tareas.filter(t => t.vencida);\nconst completadas = tareas.filter(t => t.completada);\nconst pendientes = tareas.filter(t => !t.completada && !t.vencida);\nconst stales = tareas.filter(t => t.stale);\nconst sinAsignar = tareas.filter(t => t.sin_asignar);\nconst sinFecha = tareas.filter(t => t.sin_fecha);\nconst progreso = tareas.length > 0 ? Math.round(completadas.length / tareas.length * 100) : 0;\n\nconst sevenDaysAgo = new Date(now - 7 * 86400000);\nconst fourteenDaysAgo = new Date(now - 14 * 86400000);\nconst thirtyDaysAgo = new Date(now - 30 * 86400000);\n\nconst completadas7d = completadas.filter(t => t.dateLastActivity && new Date(t.dateLastActivity) >= sevenDaysAgo).length;\nconst velocidad = (completadas7d / 7).toFixed(2);\nconst aTiempo = completadas.filter(t => !t.fecha_limite || new Date(t.fecha_limite) >= new Date(t.dateLastActivity || 0)).length;\nconst pctATiempo = completadas.length > 0 ? Math.round(aTiempo / completadas.length * 100) : 100;\n\nconst commitsRecent = [...bCommits, ...fCommits].filter(c => c.commit?.author?.date && new Date(c.commit.author.date) >= fourteenDaysAgo);\nconst commitsPorDia = (commitsRecent.length / 14).toFixed(2);\n\nconst prsAbiertos = [...bPRs, ...fPRs].filter(p => p.state === 'open');\nconst prOpenTimes = prsAbiertos.map(p => (now - new Date(p.created_at)) / 86400000);\nconst avgPROpenDays = prOpenTimes.length > 0 ? (prOpenTimes.reduce((a, b) => a + b, 0) / prOpenTimes.length).toFixed(1) : '0';\nconst prsRecientes = [...bPRs, ...fPRs].filter(p => new Date(p.created_at) >= thirtyDaysAgo);\nconst mergeados = prsRecientes.filter(p => p.merged_at).length;\nconst cerrados = prsRecientes.filter(p => p.state === 'closed').length;\nconst mergeRatio = cerrados > 0 ? Math.round(mergeados / cerrados * 100) : 0;\n\nconst convRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\\(.+\\))?:/i;\nconst convCommits = commitsRecent.filter(c => convRegex.test((c.commit?.message || '').split('\\n')[0]));\nconst pctConv = commitsRecent.length > 0 ? Math.round(convCommits.length / commitsRecent.length * 100) : 0;\n\nconst authorsRecent = new Set(commitsRecent.map(c => c.author?.login || c.commit?.author?.email || '?').filter(x => x !== '?'));\nconst busFactor = authorsRecent.size;\n\nconst lastBackCommit = bCommits[0]?.commit?.author?.date;\nconst lastFrontCommit = fCommits[0]?.commit?.author?.date;\nconst silenceBack = lastBackCommit ? Math.floor((now - new Date(lastBackCommit)) / 86400000) : null;\nconst silenceFront = lastFrontCommit ? Math.floor((now - new Date(lastFrontCommit)) / 86400000) : null;\n\nconst bLastRun = bRuns[0];\nconst fLastRun = fRuns[0];\nconst ciBack = bLastRun ? { status: bLastRun.conclusion || bLastRun.status, date: bLastRun.updated_at, name: bLastRun.name, url: bLastRun.html_url, branch: bLastRun.head_branch, actor: bLastRun.actor?.login } : null;\nconst ciFront = fLastRun ? { status: fLastRun.conclusion || fLastRun.status, date: fLastRun.updated_at, name: fLastRun.name, url: fLastRun.html_url, branch: fLastRun.head_branch, actor: fLastRun.actor?.login } : null;\n\nconst hoyStr = now.toISOString().slice(0, 10);\nconst proximos = hitos.filter(h => h.fecha_fin >= hoyStr && !h.completado).slice(0, 5).map(h => ({\n nombre: h.nombre, tipo: h.tipo, fecha_fin: h.fecha_fin,\n dias_faltan: Math.floor((new Date(h.fecha_fin) - now) / 86400000),\n responsable: h.responsable\n}));\nconst fin = hitos.find(h => h.tipo === 'entrega_final');\nconst diasHastaFin = fin ? Math.floor((new Date(fin.fecha_fin) - now) / 86400000) : null;\n\nlet proyeccion = 'sin datos';\nif (diasHastaFin !== null && pendientes.length > 0) {\n const necesaria = pendientes.length / Math.max(diasHastaFin, 1);\n if (parseFloat(velocidad) >= necesaria) proyeccion = 'on-track';\n else if (parseFloat(velocidad) >= necesaria * 0.7) proyeccion = 'riesgo';\n else proyeccion = 'retrasado';\n}\n\nconst workPerPerson = {};\nfor (const t of tareas) {\n for (const r of t.responsables) {\n const m = resolveMember(r);\n if (!workPerPerson[r]) workPerPerson[r] = { trello_name: r, initials: m.initials, github: m.github, tareas_total: 0, tareas_done: 0, tareas_pending: 0, tareas_overdue: 0, commits_14d: 0 };\n workPerPerson[r].tareas_total++;\n if (t.completada) workPerPerson[r].tareas_done++;\n else if (t.vencida) workPerPerson[r].tareas_overdue++;\n else workPerPerson[r].tareas_pending++;\n }\n}\nfor (const c of commitsRecent) {\n const login = c.author?.login;\n if (!login) continue;\n for (const p of Object.values(workPerPerson)) {\n if (p.github === login) p.commits_14d++;\n }\n}\nconst ranking = Object.values(workPerPerson).sort((a, b) => (b.tareas_done + b.commits_14d) - (a.tareas_done + a.commits_14d));\n\nlet tendencia = 'sin historial suficiente';\nif (historial.length >= 3) {\n const conProblemas = historial.filter(h => h.hay_problemas).length;\n tendencia = conProblemas >= 3 ? conProblemas + '/' + historial.length + ' con problemas' : 'estable';\n}\n\nlet diffPrev = null;\nif (historial.length > 0) {\n const prev = historial[0];\n const prevKpis = (typeof prev.kpis_json === 'string' ? JSON.parse(prev.kpis_json) : prev.kpis_json) || {};\n diffPrev = {\n progreso: progreso - (prev.progreso_pct || 0),\n vencidas: vencidas.length - (prev.tareas_vencidas || 0),\n commits_14d: commitsRecent.length - (prevKpis.commits_14d || 0)\n };\n}\n\nfunction formatCommits(list, limit) {\n return list.slice(0, limit).map(c => ({\n sha: (c.sha || '').slice(0, 7),\n autor: c.commit?.author?.name || c.author?.login || '?',\n login: c.author?.login || null,\n mensaje: (c.commit?.message || '').split('\\n')[0].slice(0, 120),\n fecha: c.commit?.author?.date,\n url: c.html_url\n }));\n}\nconst topBack = formatCommits(bCommits, 5);\nconst topFront = formatCommits(fCommits, 5);\n\nconst kpis = {\n velocidad_tareas_por_dia: parseFloat(velocidad),\n completadas_7d: completadas7d,\n pct_a_tiempo: pctATiempo,\n progreso_sprint_pct: progreso,\n commits_por_dia: parseFloat(commitsPorDia),\n commits_14d: commitsRecent.length,\n pct_commits_convencionales: pctConv,\n bus_factor_14d: busFactor,\n silencio_back_dias: silenceBack,\n silencio_front_dias: silenceFront,\n pr_open_time_dias: parseFloat(avgPROpenDays),\n pr_merge_ratio_30d: mergeRatio,\n dias_hasta_fin: diasHastaFin,\n proyeccion\n};\n\nconst problemas = [];\nif (vencidas.length > 0) problemas.push({ tipo: 'tareas_vencidas', severidad: 'alta', count: vencidas.length });\nif (proyeccion === 'retrasado') problemas.push({ tipo: 'proyeccion_retraso', severidad: 'alta' });\nif (proyeccion === 'riesgo') problemas.push({ tipo: 'proyeccion_riesgo', severidad: 'media' });\nif (parseFloat(avgPROpenDays) > 7 && prsAbiertos.length > 0) problemas.push({ tipo: 'pr_stale', severidad: 'media', count: prsAbiertos.filter(p => (now - new Date(p.updated_at)) / 86400000 > 7).length });\nif (parseFloat(commitsPorDia) < 0.5 && diasHastaFin !== null && diasHastaFin < 45) problemas.push({ tipo: 'baja_actividad', severidad: 'media' });\nif (ciBack && ciBack.status === 'failure') problemas.push({ tipo: 'ci_back_roto', severidad: 'alta' });\nif (ciFront && ciFront.status === 'failure') problemas.push({ tipo: 'ci_front_roto', severidad: 'alta' });\nif (stales.length > 3) problemas.push({ tipo: 'cards_stale', severidad: 'media', count: stales.length });\nif (busFactor < 2 && commitsRecent.length > 0 && diasHastaFin !== null && diasHastaFin < 45) problemas.push({ tipo: 'bus_factor_bajo', severidad: 'media' });\nif (sinAsignar.length > 0) problemas.push({ tipo: 'cards_sin_asignar', severidad: 'baja', count: sinAsignar.length });\n\nconst sev = problemas.map(p => p.severidad);\nconst severidadMax = sev.includes('alta') ? 'alta' : sev.includes('media') ? 'media' : 'baja';\n\nreturn [{\n json: {\n fecha: now.toISOString(),\n hay_problemas: problemas.length > 0,\n total_tarjetas: tareas.length,\n total_completadas: completadas.length,\n total_pendientes: pendientes.length,\n tareas_vencidas_list: vencidas,\n tareas_vencidas_count: vencidas.length,\n progreso_pct: progreso,\n fases,\n ranking,\n cards_stale: stales,\n cards_sin_asignar: sinAsignar,\n cards_sin_fecha: sinFecha,\n kpis,\n hitos_proximos: proximos,\n tendencia_historial: tendencia,\n diff_previa: diffPrev,\n problemas_detectados: problemas,\n severidad_max: severidadMax,\n cantidad_problemas: problemas.length,\n github: {\n commits_back: bCommits.length,\n commits_front: fCommits.length,\n prs_abiertos_back: bPRs.filter(p => p.state === 'open').length,\n prs_abiertos_front: fPRs.filter(p => p.state === 'open').length,\n prs_detalle_abiertos: prsAbiertos.map(p => ({ numero: p.number, titulo: p.title, autor: p.user?.login, dias_abierto: Math.floor((now - new Date(p.created_at)) / 86400000), repo: p.base?.repo?.name, url: p.html_url })),\n top_commits_back: topBack,\n top_commits_front: topFront,\n ci_back: ciBack,\n ci_front: ciFront,\n deploy_front_url: 'https://pet-safe-six.vercel.app',\n back_repo_url: 'https://github.com/' + $node['Configuración'].json.GITHUB_OWNER_BACKEND + '/' + backRepo,\n front_repo_url: 'https://github.com/' + $node['Configuración'].json.GITHUB_OWNER_FRONTEND + '/' + frontRepo\n },\n snapshot_tareas: tareas,\n gemini_key: $node['Configuración'].json.GEMINI_API_KEY,\n discord_url: $node['Configuración'].json.DISCORD_WEBHOOK_URL\n }\n}];"},"id":"46b4201c-b1e6-4c98-81a8-6969658dae49","name":"Análisis + KPIs","type":"n8n-nodes-base.code","typeVersion":2,"position":[528,512]},{"parameters":{"operation":"executeQuery","query":"=INSERT INTO analisis_proyecto (fecha_ejecucion, hay_problemas, total_tareas, tareas_completadas, tareas_vencidas, progreso_pct, commits_backend, commits_frontend, prs_abiertos_backend, prs_abiertos_frontend, problemas_detectados, cantidad_problemas, severidad_maxima, kpis_json) VALUES (NOW(), {{ $json.hay_problemas }}, {{ $json.total_tarjetas }}, {{ $json.total_completadas }}, {{ $json.tareas_vencidas_count }}, {{ $json.progreso_pct }}, {{ $json.github.commits_back }}, {{ $json.github.commits_front }}, {{ $json.github.prs_abiertos_back }}, {{ $json.github.prs_abiertos_front }}, '{{ JSON.stringify($json.problemas_detectados).replace(/'/g, \"''\") }}'::jsonb, {{ $json.cantidad_problemas }}, '{{ $json.severidad_max }}', '{{ JSON.stringify($json.kpis).replace(/'/g, \"''\") }}'::jsonb) RETURNING id;","options":{}},"id":"db612035-9b21-4839-9d5d-d9061171e03f","name":"Guardar Análisis","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[768,512],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"jsCode":"const analisisId = $json.id;\nconst snapshot = $node['Análisis + KPIs'].json.snapshot_tareas;\n\nconst rows = snapshot.map(t => {\n const memId = (t.responsables_ids || [])[0] || null;\n const memName = (t.responsables || [])[0] || null;\n return {\n analisis_id: analisisId,\n card_id: t.id,\n card_nombre: (t.nombre || '').slice(0, 500),\n lista_nombre: t.lista,\n miembro_id: memId,\n miembro_nombre: memName,\n fecha_vencimiento: t.fecha_limite,\n completada: t.completada,\n tiene_adjuntos: t.adjuntos_count > 0,\n adjuntos_count: t.adjuntos_count\n };\n});\n\nreturn rows.map(r => ({ json: r }));"},"id":"ff57f414-96a2-45b0-a904-49c98a233a80","name":"Guardar Snapshot Tareas","type":"n8n-nodes-base.code","typeVersion":2,"position":[1008,512]},{"parameters":{"schema":{"__rl":true,"mode":"list","value":"public"},"table":{"__rl":true,"mode":"list","value":"estado_tareas"},"columns":{"mappingMode":"autoMapInputData","value":{}},"options":{}},"id":"b693a511-20ae-4e49-9505-d102741f79bc","name":"Insert Snapshot","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1248,512],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"c1","leftValue":"={{ $node['Análisis + KPIs'].json.hay_problemas }}","rightValue":"","operator":{"type":"boolean","operation":"true"}}],"combinator":"and"},"options":{}},"id":"fa9c0b5a-5d75-4647-8139-b698f4607f12","name":"IF Hay Problemas","type":"n8n-nodes-base.if","typeVersion":2,"position":[768,304]},{"parameters":{"jsCode":"const d = $node['Análisis + KPIs'].json;\n\nfunction semEmoji(s) { return ({ verde: '🟢', amarillo: '🟡', rojo: '🔴', gris: '⚪' })[s] || '⚪'; }\n\nfunction renderFase(f) {\n const lines = [semEmoji(f.semaforo) + ' ' + f.nombre + ' (' + f.completadas + '/' + f.total + ' = ' + f.pct + '%)'];\n for (const t of f.tareas) {\n const status = t.completada ? '[✓]' : (t.vencida ? '[✗ VENCIDA]' : '[⏳]');\n const resp = t.responsables.length > 0 ? t.responsables.join(', ') : 'SIN ASIGNAR';\n const due = t.fecha_limite ? ' due=' + t.fecha_limite.slice(0, 10) : ' (sin fecha)';\n const chk = t.checklist_total > 0 ? ' checklist=' + t.checklist_done + '/' + t.checklist_total : '';\n const adj = t.adjuntos_count > 0 ? ' adj=' + t.adjuntos_count : '';\n lines.push(' ' + status + ' ' + t.nombre + ' -> ' + resp + due + chk + adj);\n }\n return lines.join('\\n');\n}\n\nconst fasesStr = Object.values(d.fases).map(renderFase).join('\\n\\n');\n\nconst commitsBackStr = d.github.top_commits_back.map(c => ' ' + c.sha + ' ' + c.autor + ': ' + c.mensaje + ' (' + (c.fecha || '').slice(0, 10) + ')').join('\\n') || ' (sin commits)';\nconst commitsFrontStr = d.github.top_commits_front.map(c => ' ' + c.sha + ' ' + c.autor + ': ' + c.mensaje + ' (' + (c.fecha || '').slice(0, 10) + ')').join('\\n') || ' (sin commits)';\n\nconst entregables = d.snapshot_tareas\n .filter(t => (t.archivos.length > 0 || t.enlaces.length > 0) && t.nombre && t.nombre.trim() !== '.')\n .sort((a, b) => new Date(b.dateLastActivity || 0) - new Date(a.dateLastActivity || 0))\n .slice(0, 15)\n .map(t => {\n const files = t.archivos.map(a => a.name).join(', ');\n const links = t.enlaces.map(l => (l.name || l.url)).join(', ');\n const parts = [files, links].filter(Boolean).join(' | ');\n return ' [' + t.lista + '] ' + t.nombre + ': ' + parts;\n }).join('\\n') || ' (ninguno)';\n\nconst rankingStr = d.ranking.map(r => ' ' + r.initials + ' ' + r.trello_name + ': ' + r.tareas_done + '✓ / ' + r.tareas_pending + '⏳ / ' + r.tareas_overdue + '✗ commits14d=' + r.commits_14d).join('\\n') || ' (sin datos)';\n\nconst ciBack = d.github.ci_back ? d.github.ci_back.status + ' en ' + d.github.ci_back.branch + ' (' + (d.github.ci_back.date || '').slice(0, 10) + ')' : 'sin datos';\nconst ciFront = d.github.ci_front ? d.github.ci_front.status + ' en ' + d.github.ci_front.branch + ' (' + (d.github.ci_front.date || '').slice(0, 10) + ')' : 'sin datos';\n\nfunction sign(n) { return n >= 0 ? '+' + n : String(n); }\nconst diffStr = d.diff_previa ? 'progreso ' + sign(d.diff_previa.progreso) + '% | vencidas ' + sign(d.diff_previa.vencidas) + ' | commits14d ' + sign(d.diff_previa.commits_14d) : 'sin previa';\n\nconst staleStr = d.cards_stale.slice(0, 5).map(t => t.nombre + '(' + t.dias_sin_actividad + 'd, ' + (t.responsables[0] || 'sin asignar') + ')').join(', ') || 'ninguna';\nconst sinAsignarStr = d.cards_sin_asignar.slice(0, 5).map(t => t.nombre).join(', ') || 'ninguna';\nconst sinFechaStr = d.cards_sin_fecha.slice(0, 5).map(t => t.nombre).join(', ') || 'ninguna';\n\nconst prompt = 'Eres gestor PM del proyecto PetSafe (clínica veterinaria, NestJS backend + Angular frontend, fin programado 2026-06-10). Analiza estado real y da diagnóstico accionable en español, texto plano, SIN markdown, SIN asteriscos, SIN emojis.\\n\\n' +\n'Estructura OBLIGATORIA, cada sección máx 3 oraciones:\\n' +\n'RESUMEN: estado global (verde/amarillo/rojo) con motivo principal en una línea.\\n' +\n'POR FASE: comenta SOLO las fases con problemas o riesgo, menciona tareas específicas por nombre.\\n' +\n'CARGA EQUIPO: observación sobre distribución de trabajo entre FG, DM, DP, JA.\\n' +\n'RIESGOS: top 3 con severidad y causa (no inventes, usa los datos).\\n' +\n'ACCIONES: 3 acciones concretas asignadas por nombre real (ej: Josué revisar PR X, David Barragán desbloquear tarea Y).\\n' +\n'PROYECCION: llegamos al 10-jun-2026? sí/no/riesgo con justificación numérica.\\n\\n' +\n'=== CONTEXTO REAL ===\\n' +\n'Fecha: ' + d.fecha + '\\n' +\n'Progreso global: ' + d.progreso_pct + '% (' + d.total_completadas + '/' + d.total_tarjetas + ')\\n' +\n'Días hasta fin proyecto: ' + d.kpis.dias_hasta_fin + '\\n' +\n'Proyección calculada: ' + d.kpis.proyeccion + '\\n' +\n'Velocidad: ' + d.kpis.velocidad_tareas_por_dia + ' tareas/día (' + d.kpis.completadas_7d + ' completadas últimos 7d)\\n' +\n'Diff vs análisis previo: ' + diffStr + '\\n' +\n'Tendencia histórica: ' + d.tendencia_historial + '\\n\\n' +\n'=== TAREAS POR FASE (completo) ===\\n' +\nfasesStr + '\\n\\n' +\n'=== CARDS PROBLEMÁTICAS ===\\n' +\n'Stale (sin actividad >5d): ' + d.cards_stale.length + ' -> ' + staleStr + '\\n' +\n'Sin asignar: ' + d.cards_sin_asignar.length + ' -> ' + sinAsignarStr + '\\n' +\n'Sin fecha: ' + d.cards_sin_fecha.length + ' -> ' + sinFechaStr + '\\n\\n' +\n'=== GITHUB ===\\n' +\n'Commits 14d back=' + d.github.commits_back + ' front=' + d.github.commits_front + '\\n' +\n'Silencio back=' + d.kpis.silencio_back_dias + 'd front=' + d.kpis.silencio_front_dias + 'd\\n' +\n'% commits convencionales (feat/fix/etc): ' + d.kpis.pct_commits_convencionales + '%\\n' +\n'Bus factor 14d (autores distintos): ' + d.kpis.bus_factor_14d + '\\n' +\n'PRs abiertos: ' + (d.github.prs_abiertos_back + d.github.prs_abiertos_front) + ' (avg ' + d.kpis.pr_open_time_dias + 'd open)\\n' +\n'CI backend: ' + ciBack + '\\n' +\n'CI frontend: ' + ciFront + '\\n' +\n'Deploy front: ' + d.github.deploy_front_url + '\\n\\n' +\n'Top 5 commits backend:\\n' + commitsBackStr + '\\n\\n' +\n'Top 5 commits frontend:\\n' + commitsFrontStr + '\\n\\n' +\n'=== CARGA POR PERSONA ===\\n' +\nrankingStr + '\\n\\n' +\n'=== ENTREGABLES (archivos + enlaces) ===\\n' +\nentregables + '\\n\\n' +\n'=== HITOS PRÓXIMOS ===\\n' +\n(d.hitos_proximos.map(h => h.nombre + ' (' + h.dias_faltan + 'd, resp=' + h.responsable + ')').join('\\n') || '(ninguno)') + '\\n\\n' +\n'Problemas auto-detectados: ' + JSON.stringify(d.problemas_detectados) + '\\n' +\n'Severidad máxima: ' + d.severidad_max;\n\nreturn [{ json: { prompt, gemini_key: d.gemini_key, discord_url: d.discord_url } }];"},"id":"af1114ef-ef4b-41d6-b57e-7ffefcac71d2","name":"Preparar Prompt","type":"n8n-nodes-base.code","typeVersion":2,"position":[1008,208]},{"parameters":{"method":"POST","url":"=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $json.gemini_key }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ ({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { temperature: 0.2, maxOutputTokens: 2048 } }) }}","options":{}},"id":"63671008-c20c-4360-8d23-b355af133fc2","name":"Gemini Análisis","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1248,208]},{"parameters":{"jsCode":"const d = $node['Análisis + KPIs'].json;\nconst geminiResp = $node['Gemini Análisis'].json;\nconst iaText = geminiResp?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '(sin respuesta IA)';\n\nconst colorMap = { alta: 15158332, media: 16761095, baja: 3066993 };\nconst color = colorMap[d.severidad_max] || 3066993;\n\nfunction semEmoji(s) { return ({ verde: '🟢', amarillo: '🟡', rojo: '🔴', gris: '⚪' })[s] || '⚪'; }\n\nconst resumen1 = semEmoji(d.severidad_max === 'alta' ? 'rojo' : d.severidad_max === 'media' ? 'amarillo' : 'verde') + ' **' + d.progreso_pct + '%** progreso · ' + d.total_completadas + '/' + d.total_tarjetas + ' tareas · **' + d.kpis.dias_hasta_fin + 'd** hasta fin';\nconst resumen2 = '📈 Proyección: **' + d.kpis.proyeccion + '** · Velocidad ' + d.kpis.velocidad_tareas_por_dia + '/día · ' + d.kpis.completadas_7d + ' completadas 7d';\nfunction sign(n) { return n >= 0 ? '+' + n : String(n); }\nconst resumen3 = d.diff_previa ? '🔁 vs previa: ' + sign(d.diff_previa.progreso) + '% progreso, ' + sign(d.diff_previa.vencidas) + ' vencidas, ' + sign(d.diff_previa.commits_14d) + ' commits' : '🔁 primera ejecución';\n\nconst descripcion = [resumen1, resumen2, resumen3].join('\\n');\n\nconst fields = [];\n\nconst fasesTxt = Object.values(d.fases).map(f => {\n const head = semEmoji(f.semaforo) + ' **' + f.nombre + '** ' + f.pct + '% (✓' + f.completadas + ' ⏳' + f.pendientes + ' ✗' + f.vencidas + ')';\n const tareasFiltered = f.tareas.filter(t => t.nombre && t.nombre.trim() !== '.');\n const details = tareasFiltered.slice(0, 8).map(t => {\n const ic = t.completada ? '✓' : (t.vencida ? '🔴' : '⏳');\n const r = t.responsables[0] ? ' · ' + t.responsables[0].split(' ').slice(0, 2).join(' ') : ' · sin asignar';\n return ic + ' ' + t.nombre.slice(0, 55) + r;\n }).join('\\n');\n return head + '\\n' + details;\n});\nconst fasesFull = fasesTxt.join('\\n\\n');\nfields.push({ name: '📋 Tareas por fase', value: fasesFull.slice(0, 1024), inline: false });\n\nif (d.cards_stale.length > 0 || d.cards_sin_asignar.length > 0 || d.cards_sin_fecha.length > 0) {\n const lines = [];\n if (d.cards_stale.length > 0) lines.push('🕸️ **Stale**: ' + d.cards_stale.slice(0, 3).map(t => t.nombre + '(' + t.dias_sin_actividad + 'd)').join(', '));\n if (d.cards_sin_asignar.length > 0) lines.push('❓ **Sin asignar**: ' + d.cards_sin_asignar.slice(0, 3).map(t => t.nombre).join(', '));\n if (d.cards_sin_fecha.length > 0) lines.push('📅 **Sin fecha**: ' + d.cards_sin_fecha.slice(0, 3).map(t => t.nombre).join(', '));\n fields.push({ name: '⚠️ Cards problemáticas', value: lines.join('\\n').slice(0, 1024), inline: false });\n}\n\nconst commitBack = d.github.top_commits_back.slice(0, 5).map(c => '`' + c.sha + '` ' + c.autor.split(' ')[0] + ': ' + c.mensaje.slice(0, 60) + ' · ' + (c.fecha || '').slice(0, 10)).join('\\n') || '(sin commits)';\nconst commitFront = d.github.top_commits_front.slice(0, 5).map(c => '`' + c.sha + '` ' + c.autor.split(' ')[0] + ': ' + c.mensaje.slice(0, 60) + ' · ' + (c.fecha || '').slice(0, 10)).join('\\n') || '(sin commits)';\nfields.push({ name: '📝 Commits backend (top 5)', value: commitBack.slice(0, 1024), inline: false });\nfields.push({ name: '📝 Commits frontend (top 5)', value: commitFront.slice(0, 1024), inline: false });\n\nconst entregables = d.snapshot_tareas\n .filter(t => (t.archivos.length > 0 || t.enlaces.length > 0) && t.nombre && t.nombre.trim() !== '.')\n .sort((a, b) => new Date(b.dateLastActivity || 0) - new Date(a.dateLastActivity || 0))\n .slice(0, 6);\nif (entregables.length > 0) {\n const entTxt = entregables.map(t => {\n const files = t.archivos.slice(0, 2).map(a => '[📄 ' + a.name + '](' + a.url + ')').join(' ');\n const links = t.enlaces.slice(0, 2).map(l => '[🔗 ' + (l.name || 'link') + '](' + l.url + ')').join(' ');\n return '**' + t.nombre.slice(0, 40) + '**: ' + [files, links].filter(Boolean).join(' ');\n }).join('\\n');\n fields.push({ name: '📎 Entregables recientes', value: entTxt.slice(0, 1024), inline: false });\n}\n\n\n\nconst kpiTxt = '⚡ Velocidad: ' + d.kpis.velocidad_tareas_por_dia + '/día · Bus factor: ' + d.kpis.bus_factor_14d + ' · Conv: ' + d.kpis.pct_commits_convencionales + '%\\n' +\n '🔇 Silencio back: ' + d.kpis.silencio_back_dias + 'd · front: ' + d.kpis.silencio_front_dias + 'd\\n' +\n '🔀 PRs abiertos: ' + (d.github.prs_abiertos_back + d.github.prs_abiertos_front) + ' · avg open ' + d.kpis.pr_open_time_dias + 'd · merge ratio ' + d.kpis.pr_merge_ratio_30d + '%';\nfields.push({ name: '📊 KPIs', value: kpiTxt.slice(0, 1024), inline: false });\n\nconst ciStatusEmoji = s => s === 'success' ? '✅' : s === 'failure' ? '❌' : s === 'in_progress' ? '🟡' : '⚪';\nconst ciBackTxt = d.github.ci_back ? ciStatusEmoji(d.github.ci_back.status) + ' ' + d.github.ci_back.status + ' · `' + d.github.ci_back.branch + '` · [run](' + d.github.ci_back.url + ')' : '(sin datos)';\nconst ciFrontTxt = d.github.ci_front ? ciStatusEmoji(d.github.ci_front.status) + ' ' + d.github.ci_front.status + ' · `' + d.github.ci_front.branch + '` · [run](' + d.github.ci_front.url + ')' : '(sin datos)';\nfields.push({ name: '🔧 CI', value: 'Backend: ' + ciBackTxt + '\\nFrontend: ' + ciFrontTxt + '\\n🌐 Deploy front: ' + d.github.deploy_front_url, inline: false });\n\nif (d.hitos_proximos.length > 0) {\n const hitTxt = d.hitos_proximos.slice(0, 4).map(h => '📅 **' + h.nombre + '** en ' + h.dias_faltan + 'd (' + h.responsable + ')').join('\\n');\n fields.push({ name: '🎯 Hitos próximos', value: hitTxt.slice(0, 1024), inline: false });\n}\n\nconst iaResumen = iaText.length > 1000 ? iaText.slice(0, 1000) + '…' : iaText;\nfields.push({ name: '🤖 Diagnóstico IA (Gemini)', value: iaResumen, inline: false });\n\nconst payload = {\n username: 'Monitor PetSafe',\n embeds: [{\n title: '📊 Estado proyecto PetSafe — ' + new Date().toLocaleDateString('es-ES'),\n description: descripcion.slice(0, 4000),\n color,\n fields,\n footer: { text: 'Monitor automático · ' + d.severidad_max.toUpperCase() + ' · ' + d.cantidad_problemas + ' problemas' },\n timestamp: d.fecha\n }]\n};\n\nreturn [{ json: { discord_payload: payload, ia_text: iaText } }];"},"id":"2fd70a72-5353-4893-a861-f67827b950c1","name":"Build Discord Payload","type":"n8n-nodes-base.code","typeVersion":2,"position":[1488,208]},{"parameters":{"operation":"executeQuery","query":"=UPDATE analisis_proyecto SET recomendaciones_ia = '{{ $json.ia_text.replace(/'/g, \"''\") }}' WHERE id = (SELECT MAX(id) FROM analisis_proyecto);","options":{}},"id":"3a04b156-ea08-451d-b3e5-7e89431c4904","name":"Guardar Recomendación IA","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1728,352],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"method":"POST","url":"={{ $node[\"Configuración\"].json.DISCORD_WEBHOOK_URL }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify($json.discord_payload) }}","options":{}},"id":"a8ae5802-9637-4600-9ce9-1b97a307b99e","name":"Discord Alerta","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1728,128]},{"parameters":{"jsCode":"const d = $node['Analisis + KPIs'].json;\nconst fases = d.fases || {};\nconst tareas = d.snapshot_tareas || [];\nconst kpis = d.kpis || {};\n\n// Cuellos de botella\nconst bottlenecks = [];\nfor (const [fname, f] of Object.entries(fases)) {\n if (f.pct < 50 && f.total > 2) {\n bottlenecks.push({\n fase: fname,\n bloqueadas: f.pendientes,\n porcentaje: f.pct,\n severidad: f.pct < 20 ? 'CRITICO' : 'ALTO'\n });\n }\n}\n\n// Proyección mejorada\nconst diasFin = d.kpis.dias_hasta_fin || 30;\nconst porcentajeFin = d.progreso_pct || 0;\nconst requiredBurnRate = (100 - porcentajeFin) / diasFin;\nconst burnRate = d.kpis.velocidad_tareas_por_dia || 0;\n\nconst riesgo = {\n proyeccion: d.kpis.proyeccion,\n dias_restantes: diasFin,\n progreso_actual: porcentajeFin,\n burn_rate_actual: burnRate,\n burn_rate_requerido: parseFloat(requiredBurnRate.toFixed(2)),\n riesgo_nivel: burnRate < requiredBurnRate * 0.7 ? 'CRITICO' : burnRate < requiredBurnRate ? 'ALTO' : 'BAJO'\n};\n\n// Personas críticas\nconst rankingCritico = d.ranking.slice(0, 3).map(r => ({\n initials: r.initials,\n nombre: r.trello_name,\n carga: r.tareas_total,\n completadas: r.tareas_done,\n activo: r.commits_14d > 0\n}));\n\nreturn [{ json: {\n cuellos_botella: bottlenecks,\n riesgo,\n personas_criticas: rankingCritico\n}}];"},"id":"94b7e764-a6c6-4d46-a89d-41c44a364bf8","name":"Analisis Impacto","type":"n8n-nodes-base.code","typeVersion":2,"position":[528,304]}],"connections":{"Schedule Trigger":{"main":[[{"node":"Configuración","type":"main","index":0}]]},"Configuración":{"main":[[{"node":"Trello Cards","type":"main","index":0},{"node":"Trello Lists","type":"main","index":0},{"node":"Trello Members","type":"main","index":0},{"node":"Trello Checklists","type":"main","index":0},{"node":"GitHub Back Commits","type":"main","index":0},{"node":"GitHub Front Commits","type":"main","index":0},{"node":"GitHub Back PRs","type":"main","index":0},{"node":"GitHub Front PRs","type":"main","index":0},{"node":"GitHub Back CI Runs","type":"main","index":0},{"node":"GitHub Front CI Runs","type":"main","index":0},{"node":"Postgres Historial","type":"main","index":0},{"node":"Postgres Hitos","type":"main","index":0}]]},"Trello Cards":{"main":[[{"node":"Merge Datos","type":"main","index":0}]]},"Trello Lists":{"main":[[{"node":"Merge Datos","type":"main","index":1}]]},"Trello Members":{"main":[[{"node":"Merge Datos","type":"main","index":2}]]},"Trello Checklists":{"main":[[{"node":"Merge Datos","type":"main","index":3}]]},"GitHub Back Commits":{"main":[[{"node":"Merge Datos","type":"main","index":4}]]},"GitHub Front Commits":{"main":[[{"node":"Merge Datos","type":"main","index":5}]]},"GitHub Back PRs":{"main":[[{"node":"Merge Datos","type":"main","index":6}]]},"GitHub Front PRs":{"main":[[{"node":"Merge Datos","type":"main","index":7}]]},"GitHub Back CI Runs":{"main":[[{"node":"Merge Datos","type":"main","index":8}]]},"GitHub Front CI Runs":{"main":[[{"node":"Merge Datos","type":"main","index":9}]]},"Postgres Historial":{"main":[[{"node":"Merge Datos","type":"main","index":10}]]},"Postgres Hitos":{"main":[[{"node":"Merge Datos","type":"main","index":11}]]},"Merge Datos":{"main":[[{"node":"Análisis + KPIs","type":"main","index":0}]]},"Análisis + KPIs":{"main":[[{"node":"Guardar Análisis","type":"main","index":0},{"node":"IF Hay Problemas","type":"main","index":0}]]},"Guardar Análisis":{"main":[[{"node":"Guardar Snapshot Tareas","type":"main","index":0}]]},"Guardar Snapshot Tareas":{"main":[[{"node":"Insert Snapshot","type":"main","index":0}]]},"IF Hay Problemas":{"main":[[{"node":"Preparar Prompt","type":"main","index":0}]]},"Preparar Prompt":{"main":[[{"node":"Gemini Análisis","type":"main","index":0}]]},"Gemini Análisis":{"main":[[{"node":"Build Discord Payload","type":"main","index":0}]]},"Build Discord Payload":{"main":[[{"node":"Discord Alerta","type":"main","index":0},{"node":"Guardar Recomendación IA","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"d7f3faf9-bfa4-463b-97c7-4b7f267b1039","activeVersionId":null,"versionCounter":3,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-04-18T01:01:26.263Z","createdAt":"2026-04-18T01:01:26.263Z","role":"workflow:owner","workflowId":"YYJRilJQWoMbRVwB7_tP8","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-04-18T17:47:06.609Z","createdAt":"2026-04-18T01:01:42.056Z","id":"nJpjpbQLqnHS7RhwJOE9t","name":"Workflow2","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"httpMethod":"POST","path":"discord-chat","responseMode":"responseNode","options":{}},"id":"10f0cd8e-9918-41d8-b915-c344a7558780","name":"Discord Webhook","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[-896,240],"webhookId":"5eae7a28-5ca6-4f0e-a612-0902d724ae81"},{"parameters":{"assignments":{"assignments":[{"id":"w2c01","name":"TRELLO_API_KEY","value":"={{ $env.TRELLO_API_KEY }}","type":"string"},{"id":"w2c02","name":"TRELLO_TOKEN","value":"={{ $env.TRELLO_TOKEN }}","type":"string"},{"id":"w2c03","name":"TRELLO_BOARD_ID","value":"={{ $env.TRELLO_BOARD_ID }}","type":"string"},{"id":"w2c04","name":"GITHUB_TOKEN","value":"={{ $env.GITHUB_TOKEN }}","type":"string"},{"id":"w2c04b","name":"GITHUB_TOKEN_BACKEND","value":"={{ $env.GITHUB_TOKEN_BACKEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w2c04f","name":"GITHUB_TOKEN_FRONTEND","value":"={{ $env.GITHUB_TOKEN_FRONTEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w2c05","name":"GITHUB_OWNER_BACKEND","value":"={{ $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w2c05f","name":"GITHUB_OWNER_FRONTEND","value":"={{ $env.GITHUB_OWNER_FRONTEND || $env.GITHUB_OWNER || $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w2c06","name":"GITHUB_REPO_BACKEND","value":"={{ $env.GITHUB_REPO_BACKEND }}","type":"string"},{"id":"w2c07","name":"GITHUB_REPO_FRONTEND","value":"={{ $env.GITHUB_REPO_FRONTEND }}","type":"string"},{"id":"w2c08","name":"GEMINI_API_KEY","value":"={{ $env.GEMINI_API_KEY }}","type":"string"},{"id":"w2c09","name":"pregunta_usuario","value":"={{ $json.body.content || 'Dame un resumen' }}","type":"string"},{"id":"w2c10","name":"usuario_discord","value":"={{ $json.body.user || 'desconocido' }}","type":"string"}]},"options":{}},"id":"8b2a22fe-c7fa-4438-be60-f06e96374dff","name":"Config","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-688,240]},{"parameters":{"jsCode":"const pregunta = ($node['Config'].json.pregunta_usuario || '').trim();\nconst low = pregunta.toLowerCase();\n\nlet tipo = 'chat';\nif (low.startsWith('!reporte')) tipo = 'reporte';\nelse if (low.startsWith('!kpis')) tipo = 'kpis';\n\nconst necesita_diff = /(codig|commit|diff|revisa|calidad|pull request|\\bpr\\b|merge)/.test(low);\n\nreturn [{ json: { pregunta, tipo, necesita_diff, usuario: $node['Config'].json.usuario_discord } }];"},"id":"6d1cd1cc-3414-4f58-83e9-88b27ccf5b3d","name":"Router Comandos","type":"n8n-nodes-base.code","typeVersion":2,"position":[-464,240]},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"leftValue":"={{ $json.tipo }}","rightValue":"reporte","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}},{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"leftValue":"={{ $json.tipo }}","rightValue":"kpis","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}},{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"leftValue":"={{ $json.tipo }}","rightValue":"chat","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}}]},"options":{}},"id":"5761e1e1-5eb6-43e1-aa3d-be7463f948e6","name":"Switch Tipo","type":"n8n-nodes-base.switch","typeVersion":3.2,"position":[-240,240]},{"parameters":{"workflowId":{"__rl":true,"value":"WORKFLOW4_ID","mode":"id"},"options":{}},"id":"aed595f0-0cf9-4090-86b6-120b50422b56","name":"Trigger Reporte PDF","type":"n8n-nodes-base.executeWorkflow","typeVersion":1.2,"position":[0,0]},{"parameters":{"respondWith":"text","responseBody":"Reporte PDF generado y enviado al canal + email.","options":{}},"id":"5e485703-383a-4b16-9460-73b0ad023317","name":"Resp Reporte","type":"n8n-nodes-base.respondToWebhook","typeVersion":1,"position":[224,0]},{"parameters":{"operation":"executeQuery","query":"SELECT fecha_ejecucion, progreso_pct, tareas_vencidas, kpis_json FROM analisis_proyecto ORDER BY fecha_ejecucion DESC LIMIT 1;","options":{}},"id":"026b7f9f-a7ad-4c91-b62f-2247396ae19c","name":"Postgres Ultimo KPI","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[0,160],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"jsCode":"const row = $json;\nconst k = row.kpis_json || {};\nconst lines = [\n 'KPIs PetSafe (' + new Date(row.fecha_ejecucion).toLocaleString('es-EC') + ')',\n '',\n 'Progreso sprint: ' + row.progreso_pct + '%',\n 'Tareas vencidas: ' + row.tareas_vencidas,\n 'Velocidad: ' + k.velocidad_tareas_por_dia + ' tareas/dÃÂÂa (completadas 7d: ' + k.completadas_7d + ')',\n '% a tiempo: ' + k.pct_a_tiempo + '%',\n 'Commits 14d: ' + k.commits_14d + ' (' + k.commits_por_dia + '/dÃÂÂa)',\n 'PR avg open: ' + k.pr_open_time_dias + ' dÃÂÂas',\n 'PR merge ratio 30d: ' + k.pr_merge_ratio_30d + '%',\n 'DÃÂÂas hasta fin: ' + k.dias_hasta_fin,\n 'Proyección: ' + k.proyeccion\n];\nreturn [{ json: { text: lines.join('\\n') } }];"},"id":"d9488c7d-0571-48a2-9076-d614cc945350","name":"Formatear KPIs","type":"n8n-nodes-base.code","typeVersion":2,"position":[224,160]},{"parameters":{"respondWith":"text","responseBody":"={{ $json.text }}","options":{}},"id":"e2663a7a-f8ce-4e6d-ae16-2fda7722eae9","name":"Resp KPIs","type":"n8n-nodes-base.respondToWebhook","typeVersion":1,"position":[448,160]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Config\"].json.TRELLO_BOARD_ID }}/cards","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Config\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Config\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"name,desc,due,dueComplete,idMembers,idList,dateLastActivity,labels"}]},"options":{}},"id":"2a66c351-718d-4858-93eb-aca734eaf2ee","name":"Trello Cards","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,352]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Config\"].json.TRELLO_BOARD_ID }}/lists","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Config\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Config\"].json.TRELLO_TOKEN }}"}]},"options":{}},"id":"f79c702b-0efa-4a5e-a84b-258ac42730d9","name":"Trello Lists","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,432]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Config\"].json.TRELLO_BOARD_ID }}/members","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Config\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Config\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"fullName,username"}]},"options":{}},"id":"079ad8e3-27bf-4c7a-b623-2c48eedc9cc5","name":"Trello Members","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,512]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Config\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Config\"].json.GITHUB_REPO_BACKEND }}/commits?per_page=15","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Config\"].json.GITHUB_TOKEN_BACKEND || $node[\"Config\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"a555672c-e69f-4196-bdbc-ad65ba72e171","name":"GH Back Commits","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,592]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Config\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Config\"].json.GITHUB_REPO_FRONTEND }}/commits?per_page=15","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Config\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Config\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"f663fb1e-f1f5-441d-b4f6-c0ef60d9d7ed","name":"GH Front Commits","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,672]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Config\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Config\"].json.GITHUB_REPO_BACKEND }}/pulls?state=all&per_page=15","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Config\"].json.GITHUB_TOKEN_BACKEND || $node[\"Config\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"64f2b0c9-026a-41e7-bee2-47f1fdbf5b21","name":"GH Back PRs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,752]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Config\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Config\"].json.GITHUB_REPO_FRONTEND }}/pulls?state=all&per_page=15","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Config\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Config\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"1c725ce2-0060-4cd6-abd5-ffad3135f7a3","name":"GH Front PRs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,832]},{"parameters":{"operation":"executeQuery","query":"SELECT fecha_ejecucion, hay_problemas, tareas_vencidas, progreso_pct, kpis_json, recomendaciones_ia FROM analisis_proyecto ORDER BY fecha_ejecucion DESC LIMIT 5;","options":{}},"id":"3b7a1287-ab6a-47a1-b010-6c3859b0f441","name":"PG Historial","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[0,912],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT pregunta, respuesta, fecha, aplicada, usuario FROM recomendaciones WHERE pregunta IS NOT NULL ORDER BY fecha DESC LIMIT 5;","options":{}},"id":"72077000-eae5-449f-a87e-a3de1b42f9d0","name":"PG Recomendaciones","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[0,992],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT nombre, tipo, fecha_inicio, fecha_fin, responsable, completado FROM hitos_proyecto WHERE fecha_fin >= CURRENT_DATE - INTERVAL '7 days' ORDER BY fecha_fin ASC LIMIT 10;","options":{}},"id":"3590d0d4-a959-45d6-b9e5-d3d57205874c","name":"PG Hitos","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[0,1072],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"numberInputs":10},"id":"377717c4-9203-4253-a02e-76e12ef92cba","name":"Merge Chat","type":"n8n-nodes-base.merge","typeVersion":3,"position":[288,640]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"d1","leftValue":"={{ $('Router Comandos').first().json.necesita_diff }}","rightValue":"","operator":{"type":"boolean","operation":"true"}}],"combinator":"and"},"options":{}},"id":"d7e96528-b76b-4f2e-afcb-e6f2faef4758","name":"IF Necesita Diff","type":"n8n-nodes-base.if","typeVersion":2,"position":[528,640]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Config\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Config\"].json.GITHUB_REPO_BACKEND }}/commits/{{ $node[\"GH Back Commits\"].first().json.sha }}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Config\"].json.GITHUB_TOKEN_BACKEND || $node[\"Config\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"d8825a5d-98ab-4bf6-a010-71886ded33c6","name":"Diff Backend","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[736,416]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Config\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Config\"].json.GITHUB_REPO_FRONTEND }}/commits/{{ $node[\"GH Front Commits\"].first().json.sha }}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Config\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Config\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"c7047028-90d4-4f1f-8ab0-63183d26ae88","name":"Diff Frontend","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[784,784]},{"parameters":{"jsCode":"const now = new Date();\nconst pregunta = $('Router Comandos').first().json.pregunta;\nconst usuario = $('Router Comandos').first().json.usuario;\nconst needDiff = $('Router Comandos').first().json.necesita_diff;\n\nconst cards = $('Trello Cards').all().map(i => i.json);\nconst lists = $('Trello Lists').all().map(i => i.json);\nconst members = $('Trello Members').all().map(i => i.json);\nconst bCommits = $('GH Back Commits').all().map(i => i.json);\nconst fCommits = $('GH Front Commits').all().map(i => i.json);\nconst bPRs = $('GH Back PRs').all().map(i => i.json);\nconst fPRs = $('GH Front PRs').all().map(i => i.json);\nconst historial = $('PG Historial').all().map(i => i.json);\nconst recos = $('PG Recomendaciones').all().map(i => i.json);\nconst hitos = $('PG Hitos').all().map(i => i.json);\n\nlet diffBack = null, diffFront = null;\nif (needDiff) {\n try { diffBack = $('Diff Backend').first().json; } catch (e) {}\n try { diffFront = $('Diff Frontend').first().json; } catch (e) {}\n}\n\nconst listaById = {};\nlists.forEach(l => { if (l?.id) listaById[l.id] = l.name; });\nconst memberById = {};\nmembers.forEach(m => { if (m?.id) memberById[m.id] = m.fullName || m.username; });\n\nconst tareas = cards.map(c => {\n const due = c.due ? new Date(c.due) : null;\n const vencida = due && !c.dueComplete && due < now;\n const diasRest = due ? Math.floor((due - now) / 86400000) : null;\n return {\n nombre: c.name,\n lista: listaById[c.idList] || 'desconocida',\n completada: !!c.dueComplete,\n fecha_limite: c.due,\n dias_restantes: diasRest,\n vencida: !!vencida,\n responsables: (c.idMembers || []).map(id => memberById[id] || id),\n };\n});\n\nconst vencidas = tareas.filter(t => t.vencida);\nconst porVencer = tareas.filter(t => !t.completada && !t.vencida && t.dias_restantes !== null && t.dias_restantes <= 3);\n\nconst ultimoAnalisis = historial[0] || null;\nconst kpis = ultimoAnalisis?.kpis_json || null;\n\nconst simplifyPR = p => ({ numero: p.number, titulo: p.title, estado: p.state, autor: p.user?.login, creado: p.created_at, dias_abierto: Math.floor((now - new Date(p.created_at)) / 86400000) });\nconst prsAbiertos = [...bPRs.map(simplifyPR), ...fPRs.map(simplifyPR)].filter(p => p.estado === 'open');\nconst prsStale = prsAbiertos.filter(p => p.dias_abierto > 7);\n\nconst simplifyCommit = c => ({ sha: (c.sha || '').slice(0, 7), mensaje: (c.commit?.message || '').split('\\n')[0], autor: c.commit?.author?.name, fecha: c.commit?.author?.date });\n\nlet diffInfo = null;\nif (diffBack || diffFront) {\n const shapeDiff = d => d ? {\n sha: d.sha?.slice(0, 7), mensaje: d.commit?.message, autor: d.commit?.author?.name,\n archivos: (d.files || []).map(f => ({ archivo: f.filename, cambios: f.changes, patch: (f.patch || '').slice(0, 1200) }))\n } : null;\n diffInfo = { backend: shapeDiff(diffBack), frontend: shapeDiff(diffFront) };\n}\n\nconst hoy = now.toISOString().slice(0, 10);\nconst hitosActivos = hitos.filter(h => h.fecha_fin >= hoy).map(h => ({\n nombre: h.nombre, tipo: h.tipo, fecha_fin: h.fecha_fin,\n dias_faltan: Math.floor((new Date(h.fecha_fin) - now) / 86400000),\n responsable: h.responsable\n}));\n\nconst ctx = {\n fecha_actual: now.toISOString(), usuario,\n tareas: {\n total: tareas.length, vencidas, por_vencer_3dias: porVencer,\n completadas_count: tareas.filter(t => t.completada).length,\n distribucion: tareas.reduce((a, t) => { a[t.lista] = (a[t.lista] || 0) + 1; return a; }, {})\n },\n github: {\n commits_back_recientes: bCommits.slice(0, 8).map(simplifyCommit),\n commits_front_recientes: fCommits.slice(0, 8).map(simplifyCommit),\n prs_abiertos: prsAbiertos, prs_stale_mas_7d: prsStale\n },\n kpis_ultimo_analisis: kpis,\n hitos_proximos: hitosActivos.slice(0, 5),\n historial_resumen: historial.map(h => ({ fecha: h.fecha_ejecucion, problemas: h.hay_problemas, vencidas: h.tareas_vencidas, progreso: h.progreso_pct })),\n recomendaciones_previas: recos.map(r => ({ pregunta: (r.pregunta || '').slice(0, 120), respuesta: (r.respuesta || '').slice(0, 250), aplicada: r.aplicada, fecha: r.fecha })),\n diff_commits: diffInfo\n};\n\nconst prompt = `Eres asistente experto de gestion del proyecto PetSafe. Copiloto del gestor para decisiones.\n\nREGLAS:\n1. SOLO respondes sobre el proyecto (Trello, GitHub, tareas, commits, PRs, equipo, riesgos, hitos, KPIs). Si no: responde EXACTAMENTE \"No es una pregunta relacionada con el proyecto.\"\n2. Espanol, texto plano, sin markdown, sin asteriscos, sin emojis, maximo 1800 caracteres.\n3. Siempre basado en datos reales del contexto. Si falta dato, dilo.\n4. Si preguntan que hacer, riesgos, decisiones: usa DIAGNOSTICO / RIESGOS / ACCIONES RECOMENDADAS.\n5. Si piden review de codigo y hay diff_commits, analiza calidad, bugs potenciales, buenas practicas.\n6. Si la pregunta es simple (ej \"hay vencidas?\"), responde directo.\n7. Considera hitos proximos y dias hasta fin para contextualizar urgencia.\n8. Menciona recomendaciones previas no aplicadas si son relevantes.\n\nCONTEXTO (fecha ${now.toISOString()}):\n${JSON.stringify(ctx, null, 2)}\n\nPREGUNTA DE ${usuario}:\n${pregunta}`;\n\nreturn [{ json: { prompt, gemini_key: $('Config').first().json.GEMINI_API_KEY, pregunta, usuario } }];"},"id":"1f169446-9a2c-44fe-99ef-99df338367b9","name":"Construir Contexto","type":"n8n-nodes-base.code","typeVersion":2,"position":[1008,640]},{"parameters":{"method":"POST","url":"=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $json.gemini_key }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ ({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { temperature: 0.3, maxOutputTokens: 2048 } }) }}","options":{}},"id":"293a0ed4-160e-4736-ac04-de0081ad85eb","name":"Gemini Chat","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1248,640]},{"parameters":{"operation":"executeQuery","query":"=INSERT INTO recomendaciones (fecha, tipo, pregunta, respuesta, usuario, aplicada) VALUES (NOW(), 'chat', '{{ $node[\"Construir Contexto\"].json.pregunta.replace(/'/g, \"''\") }}', '{{ $node[\"Gemini Chat\"].json.candidates[0].content.parts[0].text.replace(/'/g, \"''\") }}', '{{ $node[\"Construir Contexto\"].json.usuario.replace(/'/g, \"''\") }}', false);","options":{}},"id":"c612e02e-6d41-4ac7-9d8e-2e3cb459f8f1","name":"Guardar Q&A","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1488,640],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}},"continueOnFail":true},{"parameters":{"respondWith":"text","responseBody":"={{ $node['Gemini Chat'].json.candidates[0].content.parts[0].text }}","options":{}},"id":"55a93e52-c52c-41c0-a4e4-368835f63fed","name":"Resp Chat","type":"n8n-nodes-base.respondToWebhook","typeVersion":1,"position":[1728,640]},{"parameters":{"resource":"message","operation":"get","guildId":{"__rl":true,"mode":"list","value":""},"channelId":{"__rl":true,"mode":"list","value":""},"options":{}},"type":"n8n-nodes-base.discord","typeVersion":2,"position":[-608,464],"id":"ec92cc77-f571-480a-8d16-85136d233330","name":"Get a message","webhookId":"1506ff84-7cf8-4008-ab49-1deaa2b874ba","credentials":{"discordBotApi":{"id":"jdDQyM0kYTMXjxR4","name":"Discord Bot account"}}}],"connections":{"Discord Webhook":{"main":[[{"node":"Config","type":"main","index":0}]]},"Router Comandos":{"main":[[{"node":"Switch Tipo","type":"main","index":0}]]},"Switch Tipo":{"main":[[{"node":"Trigger Reporte PDF","type":"main","index":0}],[{"node":"Postgres Ultimo KPI","type":"main","index":0}],[{"node":"Trello Cards","type":"main","index":0},{"node":"Trello Lists","type":"main","index":0},{"node":"Trello Members","type":"main","index":0},{"node":"GH Back Commits","type":"main","index":0},{"node":"GH Front Commits","type":"main","index":0},{"node":"GH Back PRs","type":"main","index":0},{"node":"GH Front PRs","type":"main","index":0},{"node":"PG Historial","type":"main","index":0},{"node":"PG Recomendaciones","type":"main","index":0},{"node":"PG Hitos","type":"main","index":0}]]},"Trigger Reporte PDF":{"main":[[{"node":"Resp Reporte","type":"main","index":0}]]},"Formatear KPIs":{"main":[[{"node":"Resp KPIs","type":"main","index":0}]]},"Trello Cards":{"main":[[{"node":"Merge Chat","type":"main","index":0}]]},"Trello Lists":{"main":[[{"node":"Merge Chat","type":"main","index":1}]]},"Trello Members":{"main":[[{"node":"Merge Chat","type":"main","index":2}]]},"GH Back Commits":{"main":[[{"node":"Merge Chat","type":"main","index":3}]]},"GH Front Commits":{"main":[[{"node":"Merge Chat","type":"main","index":4}]]},"GH Back PRs":{"main":[[{"node":"Merge Chat","type":"main","index":5}]]},"GH Front PRs":{"main":[[{"node":"Merge Chat","type":"main","index":6}]]},"PG Historial":{"main":[[{"node":"Merge Chat","type":"main","index":7}]]},"PG Recomendaciones":{"main":[[{"node":"Merge Chat","type":"main","index":8}]]},"PG Hitos":{"main":[[{"node":"Merge Chat","type":"main","index":9}]]},"Merge Chat":{"main":[[{"node":"IF Necesita Diff","type":"main","index":0}]]},"IF Necesita Diff":{"main":[[{"node":"Diff Backend","type":"main","index":0},{"node":"Diff Frontend","type":"main","index":0}],[{"node":"Construir Contexto","type":"main","index":0}]]},"Diff Backend":{"main":[[{"node":"Construir Contexto","type":"main","index":0}]]},"Diff Frontend":{"main":[[{"node":"Construir Contexto","type":"main","index":0}]]},"Construir Contexto":{"main":[[{"node":"Gemini Chat","type":"main","index":0}]]},"Gemini Chat":{"main":[[{"node":"Guardar Q&A","type":"main","index":0}]]},"Guardar Q&A":{"main":[[{"node":"Resp Chat","type":"main","index":0}]]},"Config":{"main":[[{"node":"Router Comandos","type":"main","index":0}]]},"Postgres Ultimo KPI":{"main":[[{"node":"Formatear KPIs","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"490056d4-3fb0-42b2-993f-3f5c4878a671","activeVersionId":null,"versionCounter":11,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-04-18T01:01:42.061Z","createdAt":"2026-04-18T01:01:42.061Z","role":"workflow:owner","workflowId":"nJpjpbQLqnHS7RhwJOE9t","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-04-18T17:50:08.317Z","createdAt":"2026-04-18T01:03:36.901Z","id":"OXZ50xed7WPZBplTyj6fX","name":"📄 Workflow 4 — Reporte PDF Semanal","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 18 * * 5"}]}},"id":"2a1ab314-8cb2-4327-8244-92d09434e79e","name":"Schedule Viernes 18:00","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.1,"position":[-480,208]},{"parameters":{},"id":"5e849500-39f2-4e78-bf55-46d3e635c769","name":"On-Demand Trigger","type":"n8n-nodes-base.executeWorkflowTrigger","typeVersion":1,"position":[-480,432]},{"parameters":{"assignments":{"assignments":[{"id":"w4c01","name":"GITHUB_TOKEN","value":"={{ $env.GITHUB_TOKEN }}","type":"string"},{"id":"w4c01b","name":"GITHUB_TOKEN_BACKEND","value":"={{ $env.GITHUB_TOKEN_BACKEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w4c01f","name":"GITHUB_TOKEN_FRONTEND","value":"={{ $env.GITHUB_TOKEN_FRONTEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w4c02","name":"GITHUB_OWNER_BACKEND","value":"={{ $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w4c02f","name":"GITHUB_OWNER_FRONTEND","value":"={{ $env.GITHUB_OWNER_FRONTEND || $env.GITHUB_OWNER || $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w4c03","name":"GITHUB_REPO_BACKEND","value":"={{ $env.GITHUB_REPO_BACKEND }}","type":"string"},{"id":"w4c04","name":"GITHUB_REPO_FRONTEND","value":"={{ $env.GITHUB_REPO_FRONTEND }}","type":"string"},{"id":"w4c05","name":"GEMINI_API_KEY","value":"={{ $env.GEMINI_API_KEY }}","type":"string"},{"id":"w4c06","name":"DISCORD_WEBHOOK_URL","value":"={{ $env.DISCORD_WEBHOOK_URL }}","type":"string"},{"id":"w4c07","name":"GOTENBERG_URL","value":"={{ $env.GOTENBERG_URL || 'http://gotenberg:3000' }}","type":"string"},{"id":"w4c08","name":"SMTP_FROM_NAME","value":"={{ $env.SMTP_FROM_NAME }}","type":"string"},{"id":"w4c09","name":"SMTP_FROM_EMAIL","value":"={{ $env.SMTP_FROM_EMAIL }}","type":"string"},{"id":"w4c10","name":"rango_dias","value":"={{ $json.rango_dias || 7 }}","type":"number"},{"id":"w4c11","name":"fecha_desde","value":"={{ new Date(Date.now() - ($json.rango_dias || 7) * 86400000).toISOString() }}","type":"string"},{"id":"w4c12","name":"fecha_hoy","value":"={{ new Date().toISOString() }}","type":"string"}]},"options":{}},"id":"3fdf0b38-3194-4d61-a26e-10c2bf113561","name":"Configuración","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-240,304]},{"parameters":{"operation":"executeQuery","query":"SELECT fecha_ejecucion, hay_problemas, total_tareas, tareas_completadas, tareas_vencidas, progreso_pct, cantidad_problemas, severidad_maxima, kpis_json FROM analisis_proyecto WHERE fecha_ejecucion >= NOW() - ($1 || ' days')::interval ORDER BY fecha_ejecucion DESC;","options":{"queryReplacement":"={{ $node[\"Configuración\"].json.rango_dias }}"}},"id":"ca8fa219-8c55-402f-b22f-d39a5fa089ed","name":"PG Análisis 7d","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,0],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT * FROM analisis_proyecto ORDER BY fecha_ejecucion DESC LIMIT 1;","options":{}},"id":"e726b56b-6631-4ceb-b036-081be3ac508c","name":"PG Último Análisis","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,128],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT DISTINCT ON (card_id) card_id, card_nombre, lista_nombre, miembro_nombre, fecha_vencimiento FROM estado_tareas WHERE completada = FALSE AND fecha_vencimiento < NOW() ORDER BY card_id, fecha DESC;","options":{}},"id":"9d902731-8dd3-4a24-89f5-4a9d5b5211e8","name":"PG Tareas Vencidas","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,240],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT nombre, tipo, fecha_inicio, fecha_fin, responsable, completado FROM hitos_proyecto WHERE fecha_fin >= CURRENT_DATE AND fecha_fin <= CURRENT_DATE + INTERVAL '30 days' ORDER BY fecha_fin ASC;","options":{}},"id":"0009c0fe-e373-4622-b8a2-2bf2421e6fd9","name":"PG Hitos Próximos","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,368],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT fecha, tipo, descripcion FROM eventos_detectados WHERE fecha >= NOW() - ($1 || ' days')::interval ORDER BY fecha DESC LIMIT 100;","options":{"queryReplacement":"={{ $node[\"Configuración\"].json.rango_dias }}"}},"id":"f83712b9-e09d-4d46-aa1b-9b60a8088dad","name":"PG Eventos 7d","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,480],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT fecha, tipo, descripcion FROM recomendaciones WHERE aplicada = FALSE AND tipo = 'alerta_ia' AND fecha >= NOW() - ($1 || ' days')::interval ORDER BY fecha DESC LIMIT 20;","options":{"queryReplacement":"={{ $node[\"Configuración\"].json.rango_dias }}"}},"id":"aad47761-c706-447c-9124-21677db5a95d","name":"PG Recomendaciones","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,608],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"operation":"executeQuery","query":"SELECT email, nombre FROM destinatarios_reporte WHERE activo = TRUE;","options":{}},"id":"c206ab52-ab9b-45fb-8277-86f6f7ef8626","name":"PG Destinatarios","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,720],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/commits","sendQuery":true,"queryParameters":{"parameters":[{"name":"since","value":"={{ $node[\"Configuración\"].json.fecha_desde }}"},{"name":"per_page","value":"100"}]},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"}]},"options":{}},"id":"45153463-8079-47b0-8b45-4d8b4973b731","name":"GitHub Back 7d","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,848]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/commits","sendQuery":true,"queryParameters":{"parameters":[{"name":"since","value":"={{ $node[\"Configuración\"].json.fecha_desde }}"},{"name":"per_page","value":"100"}]},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"}]},"options":{}},"id":"9d3c5b81-f805-48a0-83a3-7c43505dd663","name":"GitHub Front 7d","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,960]},{"parameters":{"numberInputs":9},"id":"f21e08d1-3bba-4c75-827f-418bfb451b34","name":"Merge Reporte","type":"n8n-nodes-base.merge","typeVersion":3,"position":[288,448]},{"parameters":{"jsCode":"// Process heterogeneous array from Merge node\nconst items = $input.all().map(i => i.json);\n\nlet analisis7d = [], ultimo = null, vencidas = [], hitos = [], eventos = [], recos = [], destinatarios = [], backCommits = [], frontCommits = [];\n\n// Helper: detect object type by structure\nconst detectType = (obj) => {\n if (!obj || typeof obj !== 'object') return 'unknown';\n \n // Análisis histórico: has fecha_ejecucion + progreso_pct\n if (obj.fecha_ejecucion && (obj.progreso_pct !== undefined || obj.kpis_json)) {\n return 'analisis';\n }\n // Tareas vencidas: has card_id + card_nombre\n if (obj.card_id && obj.card_nombre) {\n return 'tarea';\n }\n // Destinatarios: has email\n if (obj.email && obj.nombre) {\n return 'destinatario';\n }\n // Commits GitHub: has sha + commit\n if (obj.sha && obj.commit) {\n return 'commit';\n }\n // Hitos: has fecha_vencimiento pero no card_id\n if (obj.fecha_vencimiento && !obj.card_id) {\n return 'hito';\n }\n // Recomendaciones: has recomendacion o contenido IA\n if (obj.recomendacion || obj.contenido_ia) {\n return 'recomendacion';\n }\n // Eventos: has evento o evento_tipo\n if (obj.evento || obj.evento_tipo) {\n return 'evento';\n }\n \n return 'unknown';\n};\n\n// Segregate: Merge data is heterogeneous array\nfor (const obj of items) {\n const type = detectType(obj);\n \n if (type === 'analisis') {\n analisis7d.push(obj);\n } else if (type === 'tarea') {\n vencidas.push(obj);\n } else if (type === 'destinatario') {\n destinatarios.push(obj);\n } else if (type === 'commit') {\n // Guess backend vs frontend by repo in commit message or branch hint\n const msg = obj.commit?.message || '';\n if (msg.toLowerCase().includes('front') || msg.toLowerCase().includes('ui') || msg.toLowerCase().includes('component')) {\n frontCommits.push(obj);\n } else {\n backCommits.push(obj);\n }\n } else if (type === 'hito') {\n hitos.push(obj);\n } else if (type === 'recomendacion') {\n recos.push(obj);\n } else if (type === 'evento') {\n eventos.push(obj);\n }\n}\n\n// Sort análisis by date DESC to get latest first\nanalisis7d.sort((a, b) => new Date(b.fecha_ejecucion) - new Date(a.fecha_ejecucion));\nultimo = analisis7d[0] || {};\n\n// Parse KPIs if string\nconst k = ultimo.kpis_json || {};\nconst kpis = typeof k === 'string' ? JSON.parse(k) : k;\n\n// Calculate trend\nconst tendencia = analisis7d.length > 1 ? {\n progreso_inicial: analisis7d[analisis7d.length - 1]?.progreso_pct || 0,\n progreso_final: analisis7d[0]?.progreso_pct || 0,\n delta: (analisis7d[0]?.progreso_pct || 0) - (analisis7d[analisis7d.length - 1]?.progreso_pct || 0),\n ejecuciones: analisis7d.length\n} : null;\n\nconst prompt = `Genera un RESUMEN EJECUTIVO del reporte semanal del proyecto PetSafe (sistema veterinario). Usa tono profesional en español. Estructura:\n\n1. ESTADO GENERAL (1 párrafo, 3-4 frases)\n2. LOGROS DE LA SEMANA (bullets, máx 4)\n3. RIESGOS CRÍTICOS (bullets con prioridad A/M/B, máx 4)\n4. PRÓXIMOS PASOS (bullets accionables, máx 4)\n\nDatos:\n${JSON.stringify({\n kpis,\n tendencia,\n ultimo: {\n progreso_pct: ultimo.progreso_pct,\n total_tareas: ultimo.total_tareas,\n tareas_completadas: ultimo.tareas_completadas,\n tareas_vencidas: ultimo.tareas_vencidas,\n cantidad_problemas: ultimo.cantidad_problemas\n },\n vencidas_count: vencidas.length,\n hitos_proximos: hitos.slice(0, 3),\n commits: { back: backCommits.length, front: frontCommits.length },\n eventos_count: eventos.length\n})}\n\nResponde SOLO con HTML válido usando <h3>, <p>, <ul>, <li>, <strong>. Sin backticks ni markdown.`;\n\nconst data = {\n fecha_reporte: new Date().toISOString(),\n fecha_reporte_local: new Date().toLocaleString('es-EC', { timeZone: 'America/Guayaquil' }),\n rango_dias: $node['Configuración'].json.rango_dias,\n ultimo_analisis: ultimo,\n kpis,\n tendencia,\n analisis_historia: analisis7d,\n tareas_vencidas: vencidas,\n hitos_proximos: hitos,\n eventos_recientes: eventos,\n recomendaciones_pendientes: recos,\n destinatarios: destinatarios.map(d => d.email),\n destinatarios_info: destinatarios,\n commits_back: backCommits.map(c => ({ sha: (c.sha || '').substring(0,7), autor: c.commit?.author?.name, fecha: c.commit?.author?.date, mensaje: (c.commit?.message || '').split('\\n')[0] })),\n commits_front: frontCommits.map(c => ({ sha: (c.sha || '').substring(0,7), autor: c.commit?.author?.name, fecha: c.commit?.author?.date, mensaje: (c.commit?.message || '').split('\\n')[0] })),\n total_commits_back: backCommits.length,\n total_commits_front: frontCommits.length,\n gemini_key: $env.GEMINI_API_KEY,\n prompt\n};\n\nreturn [{ json: data }];\n"},"id":"3a5484e6-4b2e-44df-baf8-887655aeaf14","name":"Build Report Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[512,448]},{"parameters":{"method":"POST","url":"=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $json.gemini_key }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ ({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { temperature: 0.4, maxOutputTokens: 1200 } }) }}","options":{}},"id":"6eebde2a-0881-4970-bcdc-6fd332ece511","name":"Gemini Executive Summary","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[720,448]},{"parameters":{"jsCode":"const gem = $json;\nconst data = $node['Build Report Data'].json;\nconst summaryHtml = gem?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || \n `<p><strong>Estado del proyecto:</strong> Progreso ${data.ultimo_analisis.progreso_pct || 0}% · Equipo activo · ${data.tareas_vencidas.length} tareas vencidas · Proyección: ${data.kpis.proyeccion_a_tiempo ? 'A tiempo' : 'En riesgo'}</p>\n <p><strong>Focus:</strong> ${data.kpis.dias_hasta_fin} días hasta fin. Velocidad ${data.kpis.velocidad_tareas_por_dia || 0}/día.</p>`; \n\nconst fmt = (d) => d ? new Date(d).toLocaleDateString('es-EC', { timeZone: 'America/Guayaquil' }) : '-';\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, c => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\n\nconst k = data.kpis || {};\nconst rowsKpis = [\n ['Progreso general', `${data.ultimo_analisis.progreso_pct || 0}%`],\n ['Velocidad (tareas/sem)', k.velocidad_semana ?? '-'],\n ['% Tareas a tiempo', (k.pct_a_tiempo != null ? k.pct_a_tiempo + '%' : '-')],\n ['Commits/día (back)', k.commits_por_dia_back ?? '-'],\n ['Commits/día (front)', k.commits_por_dia_front ?? '-'],\n ['PR open time (h) back', k.pr_open_time_back ?? '-'],\n ['PR open time (h) front', k.pr_open_time_front ?? '-'],\n ['PR merge ratio', k.pr_merge_ratio ?? '-'],\n ['Días restantes', k.dias_hasta_fin ?? '-'],\n ['Proyección a tiempo', k.proyeccion_a_tiempo ? 'Sí' : 'En riesgo']\n];\n\nconst trHtml = (arr) => arr.map(r => `<tr><td>${esc(r[0])}</td><td><strong>${esc(r[1])}</strong></td></tr>`).join('');\n\nconst vencidasHtml = data.tareas_vencidas.length ? `<table><thead><tr><th>Tarea</th><th>Lista</th><th>Responsable</th><th>Vencida el</th></tr></thead><tbody>${data.tareas_vencidas.map(v => `<tr><td>${esc(v.card_nombre)}</td><td>${esc(v.lista_nombre)}</td><td>${esc(v.miembro_nombre || '-')}</td><td>${fmt(v.fecha_vencimiento)}</td></tr>`).join('')}</tbody></table>` : '<p><em>Sin tareas vencidas.</em></p>';\n\nconst hitosHtml = data.hitos_proximos.length ? `<table><thead><tr><th>Hito</th><th>Tipo</th><th>Fecha</th><th>Responsable</th></tr></thead><tbody>${data.hitos_proximos.map(h => `<tr><td>${esc(h.nombre)}</td><td>${esc(h.tipo)}</td><td>${fmt(h.fecha_fin)}</td><td>${esc(h.responsable || '-')}</td></tr>`).join('')}</tbody></table>` : '';\n\nconst commitsHtml = (arr, titulo) => {\n if (!arr.length) return '';\n const lines = arr.slice(0, 10).map(c => {\n const author = esc(c.autor).split(' ')[0];\n const msg = esc(c.mensaje).slice(0, 70);\n const date = fmt(c.fecha);\n return `<div style=\"margin-bottom:3mm; font-size:9.5pt;\"><code>${esc(c.sha)}</code> ${author}: ${msg} · ${date}</div>`;\n }).join('');\n return `<h4>${titulo}</h4>${lines}`;\n};\n\nconst eventosHtml = data.eventos_recientes.length ? `<ul>${data.eventos_recientes.slice(0, 15).map(e => `<li>${fmt(e.fecha)} — <strong>${esc(e.tipo)}</strong>: ${esc(e.descripcion)}</li>`).join('')}</ul>` : '';\n\nconst recosHtml = data.recomendaciones_pendientes.length ? `<ul>${data.recomendaciones_pendientes.slice(0, 10).map(r => `<li>${fmt(r.fecha)} — ${esc((r.descripcion || '').substring(0, 300))}</li>`).join('')}</ul>` : '';\n\nconst tendenciaHtml = data.tendencia ? `<p>Progreso: <strong>${data.tendencia.progreso_inicial}%</strong> → <strong>${data.tendencia.progreso_final}%</strong> (Δ ${data.tendencia.delta >= 0 ? '+' : ''}${data.tendencia.delta} pts · ${data.tendencia.ejecuciones} ejecuciones)</p>` : '<p><em>Datos insuficientes para tendencia.</em></p>';\n\nconst html = `<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n<meta charset=\"utf-8\"/>\n<title>Reporte Semanal PetSafe</title>\n<style>\n @page { margin: 22mm 18mm; }\n body { font-family: 'Helvetica', Arial, sans-serif; color: #222; font-size: 11pt; line-height: 1.4; }\n .portada { text-align: center; padding: 40mm 0 30mm; border-bottom: 3px solid #1e40af; margin-bottom: 20mm; }\n .portada h1 { color: #1e40af; font-size: 28pt; margin: 0 0 6mm; }\n .portada h2 { color: #555; font-size: 14pt; font-weight: normal; margin: 0 0 10mm; }\n .portada .meta { color: #777; font-size: 10pt; }\n h2 { color: #1e40af; border-bottom: 2px solid #1e40af; padding-bottom: 2mm; margin-top: 12mm; font-size: 14pt; }\n h3 { color: #333; font-size: 12pt; margin-top: 6mm; }\n h4 { color: #555; font-size: 11pt; margin-top: 4mm; }\n table { width: 100%; border-collapse: collapse; margin: 4mm 0; font-size: 10pt; }\n th { background: #1e40af; color: white; padding: 2mm; text-align: left; }\n td { padding: 2mm; border-bottom: 1px solid #ddd; }\n tr:nth-child(even) td { background: #f7f7fb; }\n ul { margin: 2mm 0; padding-left: 6mm; }\n li { margin-bottom: 1mm; }\n code { background: #eef; padding: 0 1mm; border-radius: 2px; font-size: 9pt; }\n .footer { margin-top: 20mm; padding-top: 3mm; border-top: 1px solid #ccc; color: #888; font-size: 9pt; text-align: center; }\n .badge { display: inline-block; padding: 1mm 3mm; border-radius: 3px; font-size: 9pt; font-weight: bold; }\n .badge-ok { background: #16a34a; color: white; }\n .badge-warn { background: #eab308; color: white; }\n .badge-err { background: #dc2626; color: white; }\n</style>\n</head>\n<body>\n<div class=\"portada\">\n <h1>🐾 PetSafe</h1>\n <h2>Reporte Semanal de Gestión</h2>\n <div class=\"meta\">Generado: ${esc(data.fecha_reporte_local)}<br/>Rango: últimos ${data.rango_dias} días</div>\n</div>\n\n<h2>1. Resumen Ejecutivo</h2>\n${summaryHtml}\n\n<h2>2. KPIs del Proyecto</h2>\n<table><thead><tr><th>Indicador</th><th>Valor</th></tr></thead><tbody>${trHtml(rowsKpis)}</tbody></table>\n\n<h2>3. Tendencia Semanal</h2>\n${tendenciaHtml}\n\n<h2>4. Tareas Vencidas</h2>\n${vencidasHtml}\n\n${data.hitos_proximos.length ? '<h2>5. Hitos Próximos (30 días)</h2>' : ''}\n${hitosHtml}\n\n<h2>6. Actividad Git</h2>\n${commitsHtml(data.commits_back, 'Backend')}\n${commitsHtml(data.commits_front, 'Frontend')}\n\n${data.eventos_recientes.length ? '<h2>7. Eventos Detectados</h2>' : ''}\n${eventosHtml}\n\n${data.recomendaciones_pendientes.length ? '<h2>8. Recomendaciones Pendientes (IA)</h2>' : ''}\n${recosHtml}\n\n<div class=\"footer\">🤖 Reporte generado automáticamente por n8n + Gemini · PetSafe PM Assistant</div>\n</body>\n</html>`;\n\nconst fileName = `reporte-petsafe-${new Date().toISOString().slice(0,10)}.html`;\nconst summaryText = `Reporte semanal PetSafe: progreso ${data.ultimo_analisis.progreso_pct || 0}%, ${data.tareas_vencidas.length} tareas vencidas, ${data.total_commits_back + data.total_commits_front} commits, ${data.eventos_recientes.length} eventos.`;\n\nreturn [{\n json: {\n summaryText,\n destinatarios: data.destinatarios,\n destinatarios_info: data.destinatarios_info,\n rango_dias: data.rango_dias,\n fecha_reporte_local: data.fecha_reporte_local\n },\n binary: {\n data: {\n data: Buffer.from(html).toString('base64'),\n mimeType: 'text/html',\n fileName: 'index.html'\n }\n }\n}];\n"},"id":"df0042e4-fea5-4077-9a92-44e5e3a35220","name":"Build HTML + Binary","type":"n8n-nodes-base.code","typeVersion":2,"position":[944,448]},{"parameters":{"method":"POST","url":"={{ $node[\"Configuración\"].json.GOTENBERG_URL }}/forms/chromium/convert/html","sendBody":true,"contentType":"multipart-form-data","bodyParameters":{"parameters":[{"parameterType":"formBinaryData","name":"files","inputDataFieldName":"data"}]},"options":{"response":{"response":{"responseFormat":"file","outputPropertyName":"pdf"}}}},"id":"9651a355-5932-4a6e-bca7-941cce377518","name":"Gotenberg HTML→PDF","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1168,448]},{"parameters":{"jsCode":"const meta = $node['Build HTML + Binary'].json;\nconst fileName = `reporte-petsafe-${new Date().toISOString().slice(0,10)}.pdf`;\nconst pdfBin = $binary.pdf;\nreturn [{\n json: { ...meta, fileName },\n binary: { pdf: { ...pdfBin, fileName, mimeType: 'application/pdf' } }\n}];\n"},"id":"1b57d090-9439-4cfb-bbb8-9771d2ccf61b","name":"Rename PDF","type":"n8n-nodes-base.code","typeVersion":2,"position":[1392,448]},{"parameters":{"method":"POST","url":"={{ $node[\"Configuración\"].json.DISCORD_WEBHOOK_URL }}","sendBody":true,"contentType":"multipart-form-data","bodyParameters":{"parameters":[{"name":"payload_json","value":"={{ JSON.stringify({ content: '📄 **Reporte semanal PetSafe**\\n' + $json.summaryText, username: 'PetSafe Reports' }) }}"},{"parameterType":"formBinaryData","name":"file","inputDataFieldName":"pdf"}]},"options":{}},"id":"c3afca16-dac9-4432-ac29-ca2cfb5c83a7","name":"Discord Upload PDF","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1632,320]},{"parameters":{"fromEmail":"={{ $node[\"Configuración\"].json.SMTP_FROM_NAME }} <{{ $node[\"Configuración\"].json.SMTP_FROM_EMAIL }}>","toEmail":"={{ $json.destinatarios.join(',') }}","subject":"=📊 Reporte Semanal PetSafe — {{ $json.fecha_reporte_local }}","html":"=<p>Hola,</p><p>Adjunto el reporte semanal de gestión del proyecto <strong>PetSafe</strong>.</p><p><em>{{ $json.summaryText }}</em></p><p>Rango: últimos {{ $json.rango_dias }} días.</p><p>— PetSafe PM Assistant</p>","options":{"attachments":"pdf"}},"id":"6c07ef74-3b17-4303-908c-f5cc61cea250","name":"SMTP Enviar Email","type":"n8n-nodes-base.emailSend","typeVersion":2.1,"position":[1632,480],"webhookId":"c5016110-4376-426d-b794-b3163f0da85b","credentials":{"smtp":{"id":"Lm2lXXQEu2hLS8TJ","name":"SMTP account"}}},{"parameters":{"operation":"executeQuery","query":"INSERT INTO reportes_generados (tipo, rango_dias, enviado_discord, enviado_email, destinatarios, resumen) VALUES ($1, $2, TRUE, TRUE, string_to_array($3, '|'), $4);","options":{"queryReplacement":"={{ ['semanal', Number($json.rango_dias || 7), ($json.destinatarios || []).join('|'), String($json.summaryText || '').replace(/,/g, ';')].join(',') }}"}},"id":"e8f89c85-804b-44a0-9425-27878a591848","name":"PG Log Reporte","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[1632,640],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}}],"connections":{"Schedule Viernes 18:00":{"main":[[{"node":"Configuración","type":"main","index":0}]]},"On-Demand Trigger":{"main":[[{"node":"Configuración","type":"main","index":0}]]},"Configuración":{"main":[[{"node":"PG Análisis 7d","type":"main","index":0},{"node":"PG Último Análisis","type":"main","index":0},{"node":"PG Tareas Vencidas","type":"main","index":0},{"node":"PG Hitos Próximos","type":"main","index":0},{"node":"PG Eventos 7d","type":"main","index":0},{"node":"PG Recomendaciones","type":"main","index":0},{"node":"PG Destinatarios","type":"main","index":0},{"node":"GitHub Back 7d","type":"main","index":0},{"node":"GitHub Front 7d","type":"main","index":0}]]},"PG Análisis 7d":{"main":[[{"node":"Merge Reporte","type":"main","index":0}]]},"PG Último Análisis":{"main":[[{"node":"Merge Reporte","type":"main","index":1}]]},"PG Tareas Vencidas":{"main":[[{"node":"Merge Reporte","type":"main","index":2}]]},"PG Hitos Próximos":{"main":[[{"node":"Merge Reporte","type":"main","index":3}]]},"PG Eventos 7d":{"main":[[{"node":"Merge Reporte","type":"main","index":4}]]},"PG Recomendaciones":{"main":[[{"node":"Merge Reporte","type":"main","index":5}]]},"PG Destinatarios":{"main":[[{"node":"Merge Reporte","type":"main","index":6}]]},"GitHub Back 7d":{"main":[[{"node":"Merge Reporte","type":"main","index":7}]]},"GitHub Front 7d":{"main":[[{"node":"Merge Reporte","type":"main","index":8}]]},"Merge Reporte":{"main":[[{"node":"Build Report Data","type":"main","index":0}]]},"Build Report Data":{"main":[[{"node":"Gemini Executive Summary","type":"main","index":0}]]},"Gemini Executive Summary":{"main":[[{"node":"Build HTML + Binary","type":"main","index":0}]]},"Build HTML + Binary":{"main":[[{"node":"Gotenberg HTML→PDF","type":"main","index":0}]]},"Gotenberg HTML→PDF":{"main":[[{"node":"Rename PDF","type":"main","index":0}]]},"Rename PDF":{"main":[[{"node":"Discord Upload PDF","type":"main","index":0},{"node":"SMTP Enviar Email","type":"main","index":0},{"node":"PG Log Reporte","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"d10c1c6f-e10c-4679-885f-07364889df48","activeVersionId":null,"versionCounter":4,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-04-18T01:03:36.908Z","createdAt":"2026-04-18T01:03:36.908Z","role":"workflow:owner","workflowId":"OXZ50xed7WPZBplTyj6fX","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-04-18T17:48:31.477Z","createdAt":"2026-04-18T01:34:59.070Z","id":"gsF53_9r2Q8jZBG6sI1pd","name":"⚡ Workflow 3 — Delta Detector (5 min)","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes"}]}},"id":"35a782ca-1f37-4453-82af-496b69a34e63","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.1,"position":[-2048,224]},{"parameters":{"assignments":{"assignments":[{"id":"w3c01","name":"TRELLO_API_KEY","value":"={{ $env.TRELLO_API_KEY }}","type":"string"},{"id":"w3c02","name":"TRELLO_TOKEN","value":"={{ $env.TRELLO_TOKEN }}","type":"string"},{"id":"w3c03","name":"TRELLO_BOARD_ID","value":"={{ $env.TRELLO_BOARD_ID }}","type":"string"},{"id":"w3c04","name":"GITHUB_TOKEN","value":"={{ $env.GITHUB_TOKEN }}","type":"string"},{"id":"w3c04b","name":"GITHUB_TOKEN_BACKEND","value":"={{ $env.GITHUB_TOKEN_BACKEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w3c04f","name":"GITHUB_TOKEN_FRONTEND","value":"={{ $env.GITHUB_TOKEN_FRONTEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w3c05","name":"GITHUB_OWNER_BACKEND","value":"={{ $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w3c05f","name":"GITHUB_OWNER_FRONTEND","value":"={{ $env.GITHUB_OWNER_FRONTEND || $env.GITHUB_OWNER || $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w3c06","name":"GITHUB_REPO_BACKEND","value":"={{ $env.GITHUB_REPO_BACKEND }}","type":"string"},{"id":"w3c07","name":"GITHUB_REPO_FRONTEND","value":"={{ $env.GITHUB_REPO_FRONTEND }}","type":"string"},{"id":"w3c08","name":"GEMINI_API_KEY","value":"={{ $env.GEMINI_API_KEY }}","type":"string"},{"id":"w3c09","name":"DISCORD_WEBHOOK_URL","value":"={{ $env.DISCORD_WEBHOOK_URL }}","type":"string"}]},"options":{}},"id":"b539e9ff-b4bf-4bd6-a38e-166ec73fbcab","name":"Configuración","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-1824,224]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/cards","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"name,dueComplete,due,idList,dateLastActivity"},{"name":"attachments","value":"true"},{"name":"attachment_fields","value":"name,url,mimeType,date"}]},"options":{}},"id":"12345724-d6de-445d-acc3-f84d467a6e23","name":"Trello Cards Delta","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-1584,32]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/commits","sendQuery":true,"queryParameters":{"parameters":[{"name":"sha","value":"main"},{"name":"per_page","value":"5"}]},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"}]},"options":{}},"id":"b90dc949-6975-46da-9879-4f09902cefe3","name":"GitHub Back Main","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-1584,160]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/commits","sendQuery":true,"queryParameters":{"parameters":[{"name":"sha","value":"main"},{"name":"per_page","value":"5"}]},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"}]},"options":{}},"id":"0bb2f9bd-72c1-4dc5-a924-f0cb815be9b2","name":"GitHub Front Main","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-1584,304]},{"parameters":{"operation":"executeQuery","query":"SELECT snapshot FROM estado_conocido WHERE id = 1;","options":{}},"id":"bb2b1c4f-237b-4b0f-9292-820b0bda9c30","name":"PG Estado Conocido","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[-1584,448],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"numberInputs":4},"id":"25aa8373-c8dc-4988-9a92-0d41232c8bab","name":"Merge Delta","type":"n8n-nodes-base.merge","typeVersion":3,"position":[-1328,224]},{"parameters":{"jsCode":"const items = $input.all().map(i => i.json);\n\nlet cards = [], backCommits = [], frontCommits = [], prevSnapshot = {};\n\nfor (const it of items) {\n if (Array.isArray(it) && it[0]?.id?.length === 24 && 'dueComplete' in it[0]) {\n cards = it;\n } else if (Array.isArray(it) && it[0]?.sha && it[0]?.commit) {\n // Distinguish back vs front by url\n const url = it[0].url || '';\n if (url.includes($node['Configuración'].json.GITHUB_REPO_BACKEND)) backCommits = it;\n else if (url.includes($node['Configuración'].json.GITHUB_REPO_FRONTEND)) frontCommits = it;\n } else if (it.snapshot) {\n prevSnapshot = typeof it.snapshot === 'string' ? JSON.parse(it.snapshot) : it.snapshot;\n }\n}\n\nconst prev = {\n main_commit_back_sha: prevSnapshot.main_commit_back_sha || null,\n main_commit_front_sha: prevSnapshot.main_commit_front_sha || null,\n card_states: prevSnapshot.card_states || {}\n};\n\nconst events = [];\nconst newCardStates = {};\n\nfor (const c of cards) {\n const attCount = (c.attachments || []).length;\n const pdfAtts = (c.attachments || []).filter(a => (a.mimeType || '').includes('pdf') || (a.name || '').toLowerCase().endsWith('.pdf'));\n newCardStates[c.id] = {\n dueComplete: !!c.dueComplete,\n attCount,\n pdfCount: pdfAtts.length\n };\n const prevCard = prev.card_states[c.id];\n if (prevCard) {\n if (!prevCard.dueComplete && c.dueComplete) {\n events.push({\n tipo: 'tarea_completada',\n emoji: '✅',\n titulo: 'Tarea completada',\n descripcion: `La tarjeta \"${c.name}\" fue marcada como completada.`,\n detalle: { card_id: c.id, card_name: c.name, due: c.due }\n });\n }\n if ((prevCard.pdfCount || 0) < pdfAtts.length) {\n const newPdfs = pdfAtts.slice(-(pdfAtts.length - (prevCard.pdfCount || 0)));\n events.push({\n tipo: 'entregable_subido',\n emoji: '📎',\n titulo: 'Nuevo entregable PDF',\n descripcion: `Se subió un PDF a \"${c.name}\": ${newPdfs.map(p => p.name).join(', ')}`,\n detalle: { card_id: c.id, card_name: c.name, pdfs: newPdfs.map(p => ({ name: p.name, url: p.url })) }\n });\n }\n }\n}\n\nconst latestBack = backCommits[0];\nif (latestBack && latestBack.sha !== prev.main_commit_back_sha) {\n if (prev.main_commit_back_sha) {\n events.push({\n tipo: 'commit_main_backend',\n emoji: '🔀',\n titulo: 'Commit a main (backend)',\n descripcion: `${latestBack.commit?.author?.name || '?'}: ${(latestBack.commit?.message || '').split('\\n')[0]}`,\n detalle: { repo: 'backend', sha: latestBack.sha, url: latestBack.html_url, autor: latestBack.commit?.author?.name, mensaje: latestBack.commit?.message }\n });\n }\n}\n\nconst latestFront = frontCommits[0];\nif (latestFront && latestFront.sha !== prev.main_commit_front_sha) {\n if (prev.main_commit_front_sha) {\n events.push({\n tipo: 'commit_main_frontend',\n emoji: '🔀',\n titulo: 'Commit a main (frontend)',\n descripcion: `${latestFront.commit?.author?.name || '?'}: ${(latestFront.commit?.message || '').split('\\n')[0]}`,\n detalle: { repo: 'frontend', sha: latestFront.sha, url: latestFront.html_url, autor: latestFront.commit?.author?.name, mensaje: latestFront.commit?.message }\n });\n }\n}\n\nconst newSnapshot = {\n main_commit_back_sha: latestBack?.sha || prev.main_commit_back_sha,\n main_commit_front_sha: latestFront?.sha || prev.main_commit_front_sha,\n card_states: newCardStates\n};\n\nreturn [{\n json: {\n events,\n event_count: events.length,\n new_snapshot: newSnapshot,\n new_snapshot_json: JSON.stringify(newSnapshot)\n }\n}];\n"},"id":"0b0b2a58-7b4c-4c99-b174-1ac94b3f7056","name":"Delta Detector","type":"n8n-nodes-base.code","typeVersion":2,"position":[-1104,224]},{"parameters":{"operation":"executeQuery","query":"UPDATE estado_conocido SET snapshot = $1::jsonb, actualizado = NOW() WHERE id = 1;","options":{"queryReplacement":"={{ $json.new_snapshot_json }}"}},"id":"4a488875-5b78-4ed0-8692-8d29ef3b7ea9","name":"PG Update Estado","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[-880,432],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}},{"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"combinator":"and","conditions":[{"id":"ev-gt-0","leftValue":"={{ $json.event_count }}","rightValue":0,"operator":{"type":"number","operation":"gt"}}]},"options":{}},"id":"de811501-b999-40a2-b609-f48f1806e765","name":"IF Hay Eventos","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[-880,128]},{"parameters":{"jsCode":"const events = $json.events || [];\nreturn events.map(e => ({ json: e }));\n"},"id":"be725e12-4f5a-4b07-91cc-ad3250885691","name":"Split Events","type":"n8n-nodes-base.code","typeVersion":2,"position":[-656,80]},{"parameters":{"method":"POST","url":"=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $node[\"Configuración\"].json.GEMINI_API_KEY }}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"bodyParameters":{"parameters":[{}]},"options":{}},"id":"53334a95-a1a2-46d5-a2f4-25af7a74afd1","name":"Gemini Event Analysis","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-448,80]},{"parameters":{"jsCode":"const ev = $node['Split Events'].json;\nconst gem = $json;\nconst analysis = gem?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';\n\nlet extra = '';\nif (ev.detalle?.url) extra = `\\n🔗 ${ev.detalle.url}`;\nif (ev.detalle?.pdfs?.length) extra = '\\n' + ev.detalle.pdfs.map(p => `📄 [${p.name}](${p.url})`).join('\\n');\n\nconst content = `${ev.emoji} **${ev.titulo}**\\n${ev.descripcion}${extra}\\n\\n🤖 ${analysis}`;\n\nreturn [{ json: {\n ...ev,\n analysis,\n content,\n detalle_json: JSON.stringify(ev.detalle || {})\n}}];\n"},"id":"37ff03cd-583f-42f4-9e5f-b9ef15f0e999","name":"Build Discord Msg","type":"n8n-nodes-base.code","typeVersion":2,"position":[-224,80]},{"parameters":{"method":"POST","url":"={{ $node[\"Configuración\"].json.DISCORD_WEBHOOK_URL }}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"bodyParameters":{"parameters":[{}]},"options":{}},"id":"925c765c-cbab-4512-90b3-851485692892","name":"Discord Event","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,0]},{"parameters":{"operation":"executeQuery","query":"INSERT INTO eventos_detectados (tipo, descripcion, detalle, notificado) VALUES ($1, $2, $3::jsonb, TRUE);","options":{"queryReplacement":"={{ $json.tipo }},={{ $json.descripcion }},={{ $json.detalle_json }}"}},"id":"21f959d9-9f85-4e48-b424-73b6b356642f","name":"PG Insert Evento","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[0,160],"credentials":{"postgres":{"id":"ZTOMqed0IOgzim2H","name":"Postgres account 2"}}}],"connections":{"Schedule Trigger":{"main":[[{"node":"Configuración","type":"main","index":0}]]},"Configuración":{"main":[[{"node":"Trello Cards Delta","type":"main","index":0},{"node":"GitHub Back Main","type":"main","index":0},{"node":"GitHub Front Main","type":"main","index":0},{"node":"PG Estado Conocido","type":"main","index":0}]]},"Trello Cards Delta":{"main":[[{"node":"Merge Delta","type":"main","index":0}]]},"GitHub Back Main":{"main":[[{"node":"Merge Delta","type":"main","index":1}]]},"GitHub Front Main":{"main":[[{"node":"Merge Delta","type":"main","index":2}]]},"PG Estado Conocido":{"main":[[{"node":"Merge Delta","type":"main","index":3}]]},"Merge Delta":{"main":[[{"node":"Delta Detector","type":"main","index":0}]]},"Delta Detector":{"main":[[{"node":"IF Hay Eventos","type":"main","index":0},{"node":"PG Update Estado","type":"main","index":0}]]},"IF Hay Eventos":{"main":[[{"node":"Split Events","type":"main","index":0}]]},"Split Events":{"main":[[{"node":"Gemini Event Analysis","type":"main","index":0}]]},"Gemini Event Analysis":{"main":[[{"node":"Build Discord Msg","type":"main","index":0}]]},"Build Discord Msg":{"main":[[{"node":"Discord Event","type":"main","index":0},{"node":"PG Insert Evento","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate","availableInMCP":false},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"b2b433b4-c7f9-45c2-ad7b-1b606ad2d34b","activeVersionId":null,"versionCounter":3,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-04-18T01:34:59.086Z","createdAt":"2026-04-18T01:34:59.086Z","role":"workflow:owner","workflowId":"gsF53_9r2Q8jZBG6sI1pd","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]},{"updatedAt":"2026-04-23T06:08:15.759Z","createdAt":"2026-04-23T06:08:15.759Z","id":"Q4GWX9tgUGXv8rek","name":"Monitor PetSafe","description":null,"active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"mode":"cron","cron":"0 9 * * *"}},"id":"36c0b0b0-f4df-4876-afda-a223500bc816","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.1,"position":[-288,272]},{"parameters":{"assignments":{"assignments":[{"id":"w1c01","name":"TRELLO_API_KEY","value":"={{ $env.TRELLO_API_KEY }}","type":"string"},{"id":"w1c02","name":"TRELLO_TOKEN","value":"={{ $env.TRELLO_TOKEN }}","type":"string"},{"id":"w1c03","name":"TRELLO_BOARD_ID","value":"={{ $env.TRELLO_BOARD_ID }}","type":"string"},{"id":"w1c04","name":"GITHUB_TOKEN","value":"={{ $env.GITHUB_TOKEN }}","type":"string"},{"id":"w1c04b","name":"GITHUB_TOKEN_BACKEND","value":"={{ $env.GITHUB_TOKEN_BACKEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w1c04f","name":"GITHUB_TOKEN_FRONTEND","value":"={{ $env.GITHUB_TOKEN_FRONTEND || $env.GITHUB_TOKEN }}","type":"string"},{"id":"w1c05","name":"GITHUB_OWNER_BACKEND","value":"={{ $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w1c05f","name":"GITHUB_OWNER_FRONTEND","value":"={{ $env.GITHUB_OWNER_FRONTEND || $env.GITHUB_OWNER || $env.GITHUB_OWNER_BACKEND }}","type":"string"},{"id":"w1c06","name":"GITHUB_REPO_BACKEND","value":"={{ $env.GITHUB_REPO_BACKEND }}","type":"string"},{"id":"w1c07","name":"GITHUB_REPO_FRONTEND","value":"={{ $env.GITHUB_REPO_FRONTEND }}","type":"string"},{"id":"w1c08","name":"GEMINI_API_KEY","value":"={{ $env.GEMINI_API_KEY }}","type":"string"},{"id":"w1c09","name":"DISCORD_WEBHOOK_URL","value":"={{ $env.DISCORD_WEBHOOK_URL }}","type":"string"}]},"options":{}},"id":"4f38fc37-d630-41a3-97b6-f1898b6248cd","name":"Configuración","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-64,272]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/cards","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"name,desc,due,dueComplete,idMembers,idList,dateLastActivity,labels"},{"name":"attachments","value":"true"},{"name":"attachment_fields","value":"name,url,mimeType,isUpload,date"}]},"options":{}},"id":"2e1a0c10-9747-4c2c-b55e-dcbd522aec47","name":"Trello Cards","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,-368]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/lists","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"}]},"options":{}},"id":"c9c1fee8-50ec-4f5b-880e-ab815b3acc94","name":"Trello Lists","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,-256]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/members","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"fullName,username"}]},"options":{}},"id":"c1dda079-34e7-41e6-9dc0-54355ec37cee","name":"Trello Members","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,-160]},{"parameters":{"url":"=https://api.trello.com/1/boards/{{ $node[\"Configuración\"].json.TRELLO_BOARD_ID }}/checklists","sendQuery":true,"queryParameters":{"parameters":[{"name":"key","value":"={{ $node[\"Configuración\"].json.TRELLO_API_KEY }}"},{"name":"token","value":"={{ $node[\"Configuración\"].json.TRELLO_TOKEN }}"},{"name":"fields","value":"name,idCard"},{"name":"checkItem_fields","value":"name,state,due"}]},"options":{}},"id":"c5ab5ac2-509c-4f87-a3fe-4c5f58bfcd2c","name":"Trello Checklists","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,-64]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/commits?per_page=30","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"7c1d6cb9-6e6d-4329-b1c9-cf0031731211","name":"GitHub Back Commits","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,32]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/commits?per_page=30","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"787c8172-3f76-46b3-8a28-4d48cf6a73c7","name":"GitHub Front Commits","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,144]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/pulls?state=all&per_page=20","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"1ff43c2f-f9ad-43ad-9990-3a93a8398c1f","name":"GitHub Back PRs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,240]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/pulls?state=all&per_page=20","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"8b1eb2e2-551f-4044-b9cf-52143b017bc9","name":"GitHub Front PRs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,336]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_BACKEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_BACKEND }}/actions/runs?per_page=5","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_BACKEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"700d2011-8497-44eb-8162-d1f8edcbe6d1","name":"GitHub Back CI Runs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,432]},{"parameters":{"url":"=https://api.github.com/repos/{{ $node[\"Configuración\"].json.GITHUB_OWNER_FRONTEND }}/{{ $node[\"Configuración\"].json.GITHUB_REPO_FRONTEND }}/actions/runs?per_page=5","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"=Bearer {{ $node[\"Configuración\"].json.GITHUB_TOKEN_FRONTEND || $node[\"Configuración\"].json.GITHUB_TOKEN }}"},{"name":"Accept","value":"application/vnd.github+json"},{"name":"User-Agent","value":"n8n-agent"}]},"options":{}},"id":"86c636c2-68c5-4a36-aa23-e71ef9559dc7","name":"GitHub Front CI Runs","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,544]},{"parameters":{"operation":"executeQuery","query":"SELECT id, fecha_ejecucion, hay_problemas, total_tareas, tareas_vencidas, progreso_pct, kpis_json FROM analisis_proyecto ORDER BY fecha_ejecucion DESC LIMIT 10;","options":{}},"id":"173e4b01-f8c7-4b93-a044-25c6f773c03a","name":"Postgres Historial","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[192,640],"credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"operation":"executeQuery","query":"SELECT nombre, tipo, fecha_inicio, fecha_fin, responsable, completado FROM hitos_proyecto ORDER BY fecha_fin ASC;","options":{}},"id":"9106c55d-541e-47f3-87ab-edbce8d27292","name":"Postgres Hitos","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[192,736],"credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"numberInputs":12},"id":"5d9cc377-a952-4128-9c35-b672dc4b1a68","name":"Merge Datos","type":"n8n-nodes-base.merge","typeVersion":3,"position":[480,144]},{"parameters":{"jsCode":"const now = new Date();\nconst items = $input.all().map(i => i.json);\n\nconst MEMBER_MAP = {\n 'Fernando Joel Bonilla Guerrero': { github: 'JoelBonillaG', initials: 'FG' },\n 'Joel Bonilla': { github: 'JoelBonillaG', initials: 'FG' },\n 'David Manjarres': { github: 'DRTX2', initials: 'DM' },\n 'David Oswaldo Manjarres Quintero': { github: 'DRTX2', initials: 'DM' },\n 'David Josue Barragan Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'David Josué Barragán Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'David Barragán Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'David Barragan Pozo': { github: 'DavidJosueP2', initials: 'DP' },\n 'Josue Joel Garcia Abata': { github: 'JosueGarciaAbata', initials: 'JA' },\n 'Josué Joel García Abata': { github: 'JosueGarciaAbata', initials: 'JA' },\n 'Josué García Abata': { github: 'JosueGarciaAbata', initials: 'JA' },\n 'Josue Garcia Abata': { github: 'JosueGarciaAbata', initials: 'JA' }\n};\n\nfunction resolveMember(fullName) {\n if (!fullName) return { github: null, initials: '??' };\n if (MEMBER_MAP[fullName]) return MEMBER_MAP[fullName];\n const norm = fullName.toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '');\n for (const [k, v] of Object.entries(MEMBER_MAP)) {\n if (k.toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '') === norm) return v;\n }\n const parts = fullName.split(' ').filter(Boolean);\n const initials = (parts[0]?.[0] || '?') + (parts[parts.length - 1]?.[0] || '');\n return { github: null, initials: initials.toUpperCase() };\n}\n\nconst backRepo = $node['Configuración'].json.GITHUB_REPO_BACKEND;\nconst frontRepo = $node['Configuración'].json.GITHUB_REPO_FRONTEND;\n\nlet cards = [], lists = [], members = [], historial = [], hitos = [], checklists = [];\nlet bRuns = [], fRuns = [];\n\nitems.forEach(d => {\n if (!d || typeof d !== 'object') return;\n if (d.workflow_runs !== undefined && Array.isArray(d.workflow_runs)) {\n const first = d.workflow_runs[0];\n const repoName = first?.repository?.name || first?.head_repository?.name || '';\n if (repoName === backRepo) bRuns = d.workflow_runs;\n else if (repoName === frontRepo) fRuns = d.workflow_runs;\n else if (!bRuns.length) bRuns = d.workflow_runs;\n else fRuns = d.workflow_runs;\n return;\n }\n if (d.idCard && Array.isArray(d.checkItems)) { checklists.push(d); return; }\n if (d.nombre && d.tipo && d.fecha_fin !== undefined) { hitos.push(d); return; }\n if (d.fecha_ejecucion !== undefined) { historial.push(d); return; }\n if (d.id && d.name && d.idList !== undefined && d.due !== undefined) { cards.push(d); return; }\n if (d.id && d.name && d.closed !== undefined && !d.idList) { lists.push(d); return; }\n if (d.id && (d.fullName || d.username) && !d.idList && !d.name) { members.push(d); return; }\n});\n\nconst allCommits = items.filter(d => d && d.sha && d.commit);\nconst bCommits = allCommits.filter(c => c.url && c.url.includes('/' + backRepo + '/'));\nconst fCommits = allCommits.filter(c => c.url && c.url.includes('/' + frontRepo + '/'));\n\nconst allPRs = items.filter(d => d && d.number && d.state && d.html_url?.includes('/pull/'));\nconst bPRs = allPRs.filter(p => p.base?.repo?.name === backRepo);\nconst fPRs = allPRs.filter(p => p.base?.repo?.name === frontRepo);\n\nconst listaById = {};\nlists.forEach(l => { listaById[l.id] = l.name; });\nconst miembroById = {};\nmembers.forEach(m => { miembroById[m.id] = m.fullName || m.username; });\n\nconst checklistsByCard = {};\nchecklists.forEach(ch => {\n if (!checklistsByCard[ch.idCard]) checklistsByCard[ch.idCard] = [];\n checklistsByCard[ch.idCard].push(ch);\n});\n\nfunction normFase(s) {\n return (s || '').toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').trim();\n}\n\nconst tareasRaw = cards.map(c => {\n const due = c.due ? new Date(c.due) : null;\n const vencida = due && !c.dueComplete && due < now;\n const diasRestantes = due ? Math.floor((due - now) / 86400000) : null;\n const atts = c.attachments || [];\n const enlaces = atts.filter(a => a.url && a.isUpload === false).map(a => ({ name: a.name || a.url, url: a.url }));\n const archivos = atts.filter(a => a.isUpload === true).map(a => ({ name: a.name, url: a.url, mimeType: a.mimeType || '' }));\n const pdfs = archivos.filter(a => (a.mimeType || '').includes('pdf') || (a.name || '').toLowerCase().endsWith('.pdf'));\n const chArr = checklistsByCard[c.id] || [];\n const chTotal = chArr.reduce((s, ch) => s + (ch.checkItems?.length || 0), 0);\n const chDone = chArr.reduce((s, ch) => s + (ch.checkItems?.filter(i => i.state === 'complete').length || 0), 0);\n const daysSince = c.dateLastActivity ? Math.floor((now - new Date(c.dateLastActivity)) / 86400000) : null;\n const responsables = (c.idMembers || []).map(id => miembroById[id] || id);\n return {\n id: c.id,\n nombre: c.name,\n lista: listaById[c.idList] || 'desconocida',\n completada: !!c.dueComplete,\n fecha_limite: c.due,\n dias_restantes: diasRestantes,\n vencida: !!vencida,\n responsables,\n responsables_ids: c.idMembers || [],\n adjuntos_count: atts.length,\n enlaces,\n archivos,\n pdfs_count: pdfs.length,\n checklist_total: chTotal,\n checklist_done: chDone,\n checklist_pct: chTotal > 0 ? Math.round(chDone / chTotal * 100) : null,\n dias_sin_actividad: daysSince,\n dateLastActivity: c.dateLastActivity,\n stale: daysSince !== null && daysSince > 5 && !c.dueComplete,\n sin_asignar: (c.idMembers || []).length === 0,\n sin_fecha: !c.due\n };\n});\n\nconst tareas = tareasRaw.filter(t => t.nombre && t.nombre.trim() !== '.');\n\nconst fases = {};\nfor (const t of tareas) {\n const key = normFase(t.lista);\n if (!fases[key]) fases[key] = { nombre: t.lista, tareas: [], completadas: 0, vencidas: 0, pendientes: 0 };\n fases[key].tareas.push(t);\n if (t.completada) fases[key].completadas++;\n else if (t.vencida) fases[key].vencidas++;\n else fases[key].pendientes++;\n}\nfor (const f of Object.values(fases)) {\n f.total = f.tareas.length;\n f.pct = f.total > 0 ? Math.round(f.completadas / f.total * 100) : 0;\n if (f.vencidas > 0) f.semaforo = 'rojo';\n else if (f.pct >= 80) f.semaforo = 'verde';\n else if (f.pct >= 40) f.semaforo = 'amarillo';\n else f.semaforo = 'gris';\n}\n\nconst vencidas = tareas.filter(t => t.vencida);\nconst completadas = tareas.filter(t => t.completada);\nconst pendientes = tareas.filter(t => !t.completada && !t.vencida);\nconst stales = tareas.filter(t => t.stale);\nconst sinAsignar = tareas.filter(t => t.sin_asignar);\nconst sinFecha = tareas.filter(t => t.sin_fecha);\nconst progreso = tareas.length > 0 ? Math.round(completadas.length / tareas.length * 100) : 0;\n\nconst sevenDaysAgo = new Date(now - 7 * 86400000);\nconst fourteenDaysAgo = new Date(now - 14 * 86400000);\nconst thirtyDaysAgo = new Date(now - 30 * 86400000);\n\nconst completadas7d = completadas.filter(t => t.dateLastActivity && new Date(t.dateLastActivity) >= sevenDaysAgo).length;\nconst velocidad = (completadas7d / 7).toFixed(2);\nconst aTiempo = completadas.filter(t => !t.fecha_limite || new Date(t.fecha_limite) >= new Date(t.dateLastActivity || 0)).length;\nconst pctATiempo = completadas.length > 0 ? Math.round(aTiempo / completadas.length * 100) : 100;\n\nconst commitsRecent = [...bCommits, ...fCommits].filter(c => c.commit?.author?.date && new Date(c.commit.author.date) >= fourteenDaysAgo);\nconst commitsPorDia = (commitsRecent.length / 14).toFixed(2);\n\nconst prsAbiertos = [...bPRs, ...fPRs].filter(p => p.state === 'open');\nconst prOpenTimes = prsAbiertos.map(p => (now - new Date(p.created_at)) / 86400000);\nconst avgPROpenDays = prOpenTimes.length > 0 ? (prOpenTimes.reduce((a, b) => a + b, 0) / prOpenTimes.length).toFixed(1) : '0';\nconst prsRecientes = [...bPRs, ...fPRs].filter(p => new Date(p.created_at) >= thirtyDaysAgo);\nconst mergeados = prsRecientes.filter(p => p.merged_at).length;\nconst cerrados = prsRecientes.filter(p => p.state === 'closed').length;\nconst mergeRatio = cerrados > 0 ? Math.round(mergeados / cerrados * 100) : 0;\n\nconst convRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\\(.+\\))?:/i;\nconst convCommits = commitsRecent.filter(c => convRegex.test((c.commit?.message || '').split('\\n')[0]));\nconst pctConv = commitsRecent.length > 0 ? Math.round(convCommits.length / commitsRecent.length * 100) : 0;\n\nconst authorsRecent = new Set(commitsRecent.map(c => c.author?.login || c.commit?.author?.email || '?').filter(x => x !== '?'));\nconst busFactor = authorsRecent.size;\n\nconst lastBackCommit = bCommits[0]?.commit?.author?.date;\nconst lastFrontCommit = fCommits[0]?.commit?.author?.date;\nconst silenceBack = lastBackCommit ? Math.floor((now - new Date(lastBackCommit)) / 86400000) : null;\nconst silenceFront = lastFrontCommit ? Math.floor((now - new Date(lastFrontCommit)) / 86400000) : null;\n\nconst bLastRun = bRuns[0];\nconst fLastRun = fRuns[0];\nconst ciBack = bLastRun ? { status: bLastRun.conclusion || bLastRun.status, date: bLastRun.updated_at, name: bLastRun.name, url: bLastRun.html_url, branch: bLastRun.head_branch, actor: bLastRun.actor?.login } : null;\nconst ciFront = fLastRun ? { status: fLastRun.conclusion || fLastRun.status, date: fLastRun.updated_at, name: fLastRun.name, url: fLastRun.html_url, branch: fLastRun.head_branch, actor: fLastRun.actor?.login } : null;\n\nconst hoyStr = now.toISOString().slice(0, 10);\nconst proximos = hitos.filter(h => h.fecha_fin >= hoyStr && !h.completado).slice(0, 5).map(h => ({\n nombre: h.nombre, tipo: h.tipo, fecha_fin: h.fecha_fin,\n dias_faltan: Math.floor((new Date(h.fecha_fin) - now) / 86400000),\n responsable: h.responsable\n}));\nconst fin = hitos.find(h => h.tipo === 'entrega_final');\nconst diasHastaFin = fin ? Math.floor((new Date(fin.fecha_fin) - now) / 86400000) : null;\n\nlet proyeccion = 'sin datos';\nif (diasHastaFin !== null && pendientes.length > 0) {\n const necesaria = pendientes.length / Math.max(diasHastaFin, 1);\n if (parseFloat(velocidad) >= necesaria) proyeccion = 'on-track';\n else if (parseFloat(velocidad) >= necesaria * 0.7) proyeccion = 'riesgo';\n else proyeccion = 'retrasado';\n}\n\nconst workPerPerson = {};\nfor (const t of tareas) {\n for (const r of t.responsables) {\n const m = resolveMember(r);\n if (!workPerPerson[r]) workPerPerson[r] = { trello_name: r, initials: m.initials, github: m.github, tareas_total: 0, tareas_done: 0, tareas_pending: 0, tareas_overdue: 0, commits_14d: 0 };\n workPerPerson[r].tareas_total++;\n if (t.completada) workPerPerson[r].tareas_done++;\n else if (t.vencida) workPerPerson[r].tareas_overdue++;\n else workPerPerson[r].tareas_pending++;\n }\n}\nfor (const c of commitsRecent) {\n const login = c.author?.login;\n if (!login) continue;\n for (const p of Object.values(workPerPerson)) {\n if (p.github === login) p.commits_14d++;\n }\n}\nconst ranking = Object.values(workPerPerson).sort((a, b) => (b.tareas_done + b.commits_14d) - (a.tareas_done + a.commits_14d));\n\nlet tendencia = 'sin historial suficiente';\nif (historial.length >= 3) {\n const conProblemas = historial.filter(h => h.hay_problemas).length;\n tendencia = conProblemas >= 3 ? conProblemas + '/' + historial.length + ' con problemas' : 'estable';\n}\n\nlet diffPrev = null;\nif (historial.length > 0) {\n const prev = historial[0];\n const prevKpis = (typeof prev.kpis_json === 'string' ? JSON.parse(prev.kpis_json) : prev.kpis_json) || {};\n diffPrev = {\n progreso: progreso - (prev.progreso_pct || 0),\n vencidas: vencidas.length - (prev.tareas_vencidas || 0),\n commits_14d: commitsRecent.length - (prevKpis.commits_14d || 0)\n };\n}\n\nfunction formatCommits(list, limit) {\n return list.slice(0, limit).map(c => ({\n sha: (c.sha || '').slice(0, 7),\n autor: c.commit?.author?.name || c.author?.login || '?',\n login: c.author?.login || null,\n mensaje: (c.commit?.message || '').split('\\n')[0].slice(0, 120),\n fecha: c.commit?.author?.date,\n url: c.html_url\n }));\n}\nconst topBack = formatCommits(bCommits, 5);\nconst topFront = formatCommits(fCommits, 5);\n\nconst kpis = {\n velocidad_tareas_por_dia: parseFloat(velocidad),\n completadas_7d: completadas7d,\n pct_a_tiempo: pctATiempo,\n progreso_sprint_pct: progreso,\n commits_por_dia: parseFloat(commitsPorDia),\n commits_14d: commitsRecent.length,\n pct_commits_convencionales: pctConv,\n bus_factor_14d: busFactor,\n silencio_back_dias: silenceBack,\n silencio_front_dias: silenceFront,\n pr_open_time_dias: parseFloat(avgPROpenDays),\n pr_merge_ratio_30d: mergeRatio,\n dias_hasta_fin: diasHastaFin,\n proyeccion\n};\n\nconst problemas = [];\nif (vencidas.length > 0) problemas.push({ tipo: 'tareas_vencidas', severidad: 'alta', count: vencidas.length });\nif (proyeccion === 'retrasado') problemas.push({ tipo: 'proyeccion_retraso', severidad: 'alta' });\nif (proyeccion === 'riesgo') problemas.push({ tipo: 'proyeccion_riesgo', severidad: 'media' });\nif (parseFloat(avgPROpenDays) > 7 && prsAbiertos.length > 0) problemas.push({ tipo: 'pr_stale', severidad: 'media', count: prsAbiertos.filter(p => (now - new Date(p.updated_at)) / 86400000 > 7).length });\nif (parseFloat(commitsPorDia) < 0.5 && diasHastaFin !== null && diasHastaFin < 45) problemas.push({ tipo: 'baja_actividad', severidad: 'media' });\nif (ciBack && ciBack.status === 'failure') problemas.push({ tipo: 'ci_back_roto', severidad: 'alta' });\nif (ciFront && ciFront.status === 'failure') problemas.push({ tipo: 'ci_front_roto', severidad: 'alta' });\nif (stales.length > 3) problemas.push({ tipo: 'cards_stale', severidad: 'media', count: stales.length });\nif (busFactor < 2 && commitsRecent.length > 0 && diasHastaFin !== null && diasHastaFin < 45) problemas.push({ tipo: 'bus_factor_bajo', severidad: 'media' });\nif (sinAsignar.length > 0) problemas.push({ tipo: 'cards_sin_asignar', severidad: 'baja', count: sinAsignar.length });\n\nconst sev = problemas.map(p => p.severidad);\nconst severidadMax = sev.includes('alta') ? 'alta' : sev.includes('media') ? 'media' : 'baja';\n\nreturn [{\n json: {\n fecha: now.toISOString(),\n hay_problemas: problemas.length > 0,\n total_tarjetas: tareas.length,\n total_completadas: completadas.length,\n total_pendientes: pendientes.length,\n tareas_vencidas_list: vencidas,\n tareas_vencidas_count: vencidas.length,\n progreso_pct: progreso,\n fases,\n ranking,\n cards_stale: stales,\n cards_sin_asignar: sinAsignar,\n cards_sin_fecha: sinFecha,\n kpis,\n hitos_proximos: proximos,\n tendencia_historial: tendencia,\n diff_previa: diffPrev,\n problemas_detectados: problemas,\n severidad_max: severidadMax,\n cantidad_problemas: problemas.length,\n github: {\n commits_back: bCommits.length,\n commits_front: fCommits.length,\n prs_abiertos_back: bPRs.filter(p => p.state === 'open').length,\n prs_abiertos_front: fPRs.filter(p => p.state === 'open').length,\n prs_detalle_abiertos: prsAbiertos.map(p => ({ numero: p.number, titulo: p.title, autor: p.user?.login, dias_abierto: Math.floor((now - new Date(p.created_at)) / 86400000), repo: p.base?.repo?.name, url: p.html_url })),\n top_commits_back: topBack,\n top_commits_front: topFront,\n ci_back: ciBack,\n ci_front: ciFront,\n deploy_front_url: 'https://pet-safe-six.vercel.app',\n back_repo_url: 'https://github.com/' + $node['Configuración'].json.GITHUB_OWNER_BACKEND + '/' + backRepo,\n front_repo_url: 'https://github.com/' + $node['Configuración'].json.GITHUB_OWNER_FRONTEND + '/' + frontRepo\n },\n snapshot_tareas: tareas,\n gemini_key: $node['Configuración'].json.GEMINI_API_KEY,\n discord_url: $node['Configuración'].json.DISCORD_WEBHOOK_URL\n }\n}];"},"id":"1ca0aad3-ff88-42d3-9370-1427a44beb0e","name":"Análisis + KPIs","type":"n8n-nodes-base.code","typeVersion":2,"position":[720,144]},{"parameters":{"operation":"executeQuery","query":"INSERT INTO analisis_proyecto (fecha_ejecucion, hay_problemas, total_tareas, tareas_completadas, tareas_vencidas, progreso_pct, commits_backend, commits_frontend, prs_abiertos_backend, prs_abiertos_frontend, problemas_detectados, cantidad_problemas, severidad_maxima, kpis_json) VALUES (NOW(), {{ $json.hay_problemas }}, {{ $json.total_tarjetas }}, {{ $json.total_completadas }}, {{ $json.tareas_vencidas_count }}, {{ $json.progreso_pct }}, {{ $json.github.commits_back }}, {{ $json.github.commits_front }}, {{ $json.github.prs_abiertos_back }}, {{ $json.github.prs_abiertos_front }}, '{{ JSON.stringify($json.problemas_detectados).replace(/'/g, \"''\") }}'::jsonb, {{ $json.cantidad_problemas }}, '{{ $json.severidad_max }}', '{{ JSON.stringify($json.kpis).replace(/'/g, \"''\") }}'::jsonb) RETURNING id;","options":{}},"id":"3c183f88-31cd-40cc-ac9c-e655feb2c916","name":"Guardar Análisis","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[960,144],"credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"jsCode":"const analisisId = $json.id;\nconst snapshot = $node['Análisis + KPIs'].json.snapshot_tareas;\n\nconst rows = snapshot.map(t => {\n const memId = (t.responsables_ids || [])[0] || null;\n const memName = (t.responsables || [])[0] || null;\n return {\n analisis_id: analisisId,\n card_id: t.id,\n card_nombre: (t.nombre || '').slice(0, 500),\n lista_nombre: t.lista,\n miembro_id: memId,\n miembro_nombre: memName,\n fecha_vencimiento: t.fecha_limite,\n completada: t.completada,\n tiene_adjuntos: t.adjuntos_count > 0,\n adjuntos_count: t.adjuntos_count\n };\n});\n\nreturn rows.map(r => ({ json: r }));"},"id":"8ad44a15-46d4-45c8-8a9c-b3b428c3053f","name":"Guardar Snapshot Tareas","type":"n8n-nodes-base.code","typeVersion":2,"position":[1200,144]},{"parameters":{"schema":{"__rl":true,"mode":"list","value":"public"},"table":{"__rl":true,"mode":"list","value":"estado_tareas"},"columns":{"mappingMode":"autoMapInputData","value":{}},"options":{}},"id":"bcf07083-b413-443d-9a7d-50d6b82bfcd3","name":"Insert Snapshot","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1440,144],"credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"conditions":{"options":{"caseSensitive":true,"typeValidation":"loose"},"conditions":[{"id":"c1","leftValue":"={{ $node['Análisis + KPIs'].json.hay_problemas }}","rightValue":"","operator":{"type":"boolean","operation":"true"}}],"combinator":"and"},"options":{}},"id":"b9e0af42-9241-488e-b023-a64925c1992e","name":"IF Hay Problemas","type":"n8n-nodes-base.if","typeVersion":2,"position":[960,-64]},{"parameters":{"jsCode":"const d = $node['Análisis + KPIs'].json;\n\nfunction semEmoji(s) { return ({ verde: '🟢', amarillo: '🟡', rojo: '🔴', gris: '⚪' })[s] || '⚪'; }\n\nfunction renderFase(f) {\n const lines = [semEmoji(f.semaforo) + ' ' + f.nombre + ' (' + f.completadas + '/' + f.total + ' = ' + f.pct + '%)'];\n for (const t of f.tareas) {\n const status = t.completada ? '[✓]' : (t.vencida ? '[✗ VENCIDA]' : '[⏳]');\n const resp = t.responsables.length > 0 ? t.responsables.join(', ') : 'SIN ASIGNAR';\n const due = t.fecha_limite ? ' due=' + t.fecha_limite.slice(0, 10) : ' (sin fecha)';\n const chk = t.checklist_total > 0 ? ' checklist=' + t.checklist_done + '/' + t.checklist_total : '';\n const adj = t.adjuntos_count > 0 ? ' adj=' + t.adjuntos_count : '';\n lines.push(' ' + status + ' ' + t.nombre + ' -> ' + resp + due + chk + adj);\n }\n return lines.join('\\n');\n}\n\nconst fasesStr = Object.values(d.fases).map(renderFase).join('\\n\\n');\n\nconst commitsBackStr = d.github.top_commits_back.map(c => ' ' + c.sha + ' ' + c.autor + ': ' + c.mensaje + ' (' + (c.fecha || '').slice(0, 10) + ')').join('\\n') || ' (sin commits)';\nconst commitsFrontStr = d.github.top_commits_front.map(c => ' ' + c.sha + ' ' + c.autor + ': ' + c.mensaje + ' (' + (c.fecha || '').slice(0, 10) + ')').join('\\n') || ' (sin commits)';\n\nconst entregables = d.snapshot_tareas\n .filter(t => (t.archivos.length > 0 || t.enlaces.length > 0) && t.nombre && t.nombre.trim() !== '.')\n .sort((a, b) => new Date(b.dateLastActivity || 0) - new Date(a.dateLastActivity || 0))\n .slice(0, 15)\n .map(t => {\n const files = t.archivos.map(a => a.name).join(', ');\n const links = t.enlaces.map(l => (l.name || l.url)).join(', ');\n const parts = [files, links].filter(Boolean).join(' | ');\n return ' [' + t.lista + '] ' + t.nombre + ': ' + parts;\n }).join('\\n') || ' (ninguno)';\n\nconst rankingStr = d.ranking.map(r => ' ' + r.initials + ' ' + r.trello_name + ': ' + r.tareas_done + '✓ / ' + r.tareas_pending + '⏳ / ' + r.tareas_overdue + '✗ commits14d=' + r.commits_14d).join('\\n') || ' (sin datos)';\n\nconst ciBack = d.github.ci_back ? d.github.ci_back.status + ' en ' + d.github.ci_back.branch + ' (' + (d.github.ci_back.date || '').slice(0, 10) + ')' : 'sin datos';\nconst ciFront = d.github.ci_front ? d.github.ci_front.status + ' en ' + d.github.ci_front.branch + ' (' + (d.github.ci_front.date || '').slice(0, 10) + ')' : 'sin datos';\n\nfunction sign(n) { return n >= 0 ? '+' + n : String(n); }\nconst diffStr = d.diff_previa ? 'progreso ' + sign(d.diff_previa.progreso) + '% | vencidas ' + sign(d.diff_previa.vencidas) + ' | commits14d ' + sign(d.diff_previa.commits_14d) : 'sin previa';\n\nconst staleStr = d.cards_stale.slice(0, 5).map(t => t.nombre + '(' + t.dias_sin_actividad + 'd, ' + (t.responsables[0] || 'sin asignar') + ')').join(', ') || 'ninguna';\nconst sinAsignarStr = d.cards_sin_asignar.slice(0, 5).map(t => t.nombre).join(', ') || 'ninguna';\nconst sinFechaStr = d.cards_sin_fecha.slice(0, 5).map(t => t.nombre).join(', ') || 'ninguna';\n\nconst prompt = 'Eres gestor PM del proyecto PetSafe (clínica veterinaria, NestJS backend + Angular frontend, fin programado 2026-06-10). Analiza estado real y da diagnóstico accionable en español, texto plano, SIN markdown, SIN asteriscos, SIN emojis.\\n\\n' +\n'Estructura OBLIGATORIA, cada sección máx 3 oraciones:\\n' +\n'RESUMEN: estado global (verde/amarillo/rojo) con motivo principal en una línea.\\n' +\n'POR FASE: comenta SOLO las fases con problemas o riesgo, menciona tareas específicas por nombre.\\n' +\n'CARGA EQUIPO: observación sobre distribución de trabajo entre FG, DM, DP, JA.\\n' +\n'RIESGOS: top 3 con severidad y causa (no inventes, usa los datos).\\n' +\n'ACCIONES: 3 acciones concretas asignadas por nombre real (ej: Josué revisar PR X, David Barragán desbloquear tarea Y).\\n' +\n'PROYECCION: llegamos al 10-jun-2026? sí/no/riesgo con justificación numérica.\\n\\n' +\n'=== CONTEXTO REAL ===\\n' +\n'Fecha: ' + d.fecha + '\\n' +\n'Progreso global: ' + d.progreso_pct + '% (' + d.total_completadas + '/' + d.total_tarjetas + ')\\n' +\n'Días hasta fin proyecto: ' + d.kpis.dias_hasta_fin + '\\n' +\n'Proyección calculada: ' + d.kpis.proyeccion + '\\n' +\n'Velocidad: ' + d.kpis.velocidad_tareas_por_dia + ' tareas/día (' + d.kpis.completadas_7d + ' completadas últimos 7d)\\n' +\n'Diff vs análisis previo: ' + diffStr + '\\n' +\n'Tendencia histórica: ' + d.tendencia_historial + '\\n\\n' +\n'=== TAREAS POR FASE (completo) ===\\n' +\nfasesStr + '\\n\\n' +\n'=== CARDS PROBLEMÁTICAS ===\\n' +\n'Stale (sin actividad >5d): ' + d.cards_stale.length + ' -> ' + staleStr + '\\n' +\n'Sin asignar: ' + d.cards_sin_asignar.length + ' -> ' + sinAsignarStr + '\\n' +\n'Sin fecha: ' + d.cards_sin_fecha.length + ' -> ' + sinFechaStr + '\\n\\n' +\n'=== GITHUB ===\\n' +\n'Commits 14d back=' + d.github.commits_back + ' front=' + d.github.commits_front + '\\n' +\n'Silencio back=' + d.kpis.silencio_back_dias + 'd front=' + d.kpis.silencio_front_dias + 'd\\n' +\n'% commits convencionales (feat/fix/etc): ' + d.kpis.pct_commits_convencionales + '%\\n' +\n'Bus factor 14d (autores distintos): ' + d.kpis.bus_factor_14d + '\\n' +\n'PRs abiertos: ' + (d.github.prs_abiertos_back + d.github.prs_abiertos_front) + ' (avg ' + d.kpis.pr_open_time_dias + 'd open)\\n' +\n'CI backend: ' + ciBack + '\\n' +\n'CI frontend: ' + ciFront + '\\n' +\n'Deploy front: ' + d.github.deploy_front_url + '\\n\\n' +\n'Top 5 commits backend:\\n' + commitsBackStr + '\\n\\n' +\n'Top 5 commits frontend:\\n' + commitsFrontStr + '\\n\\n' +\n'=== CARGA POR PERSONA ===\\n' +\nrankingStr + '\\n\\n' +\n'=== ENTREGABLES (archivos + enlaces) ===\\n' +\nentregables + '\\n\\n' +\n'=== HITOS PRÓXIMOS ===\\n' +\n(d.hitos_proximos.map(h => h.nombre + ' (' + h.dias_faltan + 'd, resp=' + h.responsable + ')').join('\\n') || '(ninguno)') + '\\n\\n' +\n'Problemas auto-detectados: ' + JSON.stringify(d.problemas_detectados) + '\\n' +\n'Severidad máxima: ' + d.severidad_max;\n\nreturn [{ json: { prompt, gemini_key: d.gemini_key, discord_url: d.discord_url } }];"},"id":"46d25fec-8cab-4a12-8d92-67898640076a","name":"Preparar Prompt","type":"n8n-nodes-base.code","typeVersion":2,"position":[1200,-160]},{"parameters":{"method":"POST","url":"=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={{ $json.gemini_key }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ ({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { temperature: 0.2, maxOutputTokens: 2048 } }) }}","options":{}},"id":"17e89ac2-a0b3-40ce-b781-9d7d37de207d","name":"Gemini Análisis","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1440,-160]},{"parameters":{"jsCode":"const d = $node['Análisis + KPIs'].json;\nconst geminiResp = $node['Gemini Análisis'].json;\nconst iaText = geminiResp?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '(sin respuesta IA)';\n\nconst colorMap = { alta: 15158332, media: 16761095, baja: 3066993 };\nconst color = colorMap[d.severidad_max] || 3066993;\n\nfunction semEmoji(s) { return ({ verde: '🟢', amarillo: '🟡', rojo: '🔴', gris: '⚪' })[s] || '⚪'; }\n\nconst resumen1 = semEmoji(d.severidad_max === 'alta' ? 'rojo' : d.severidad_max === 'media' ? 'amarillo' : 'verde') + ' **' + d.progreso_pct + '%** progreso · ' + d.total_completadas + '/' + d.total_tarjetas + ' tareas · **' + d.kpis.dias_hasta_fin + 'd** hasta fin';\nconst resumen2 = '📈 Proyección: **' + d.kpis.proyeccion + '** · Velocidad ' + d.kpis.velocidad_tareas_por_dia + '/día · ' + d.kpis.completadas_7d + ' completadas últimos 7d';\nfunction sign(n) { return n >= 0 ? '+' + n : String(n); }\nconst resumen3 = d.diff_previa ? '🔁 vs previa: ' + sign(d.diff_previa.progreso) + '% progreso, ' + sign(d.diff_previa.vencidas) + ' vencidas, ' + sign(d.diff_previa.commits_14d) + ' commits' : '🔁 primera ejecución';\n\nconst descripcion = [resumen1, resumen2, resumen3].join('\\n');\n\nconst fields = [];\n\nconst fasesTxt = Object.values(d.fases).map(f => {\n const head = semEmoji(f.semaforo) + ' **' + f.nombre + '** ' + f.pct + '% (✓' + f.completadas + ' ⏳' + f.pendientes + ' ✗' + f.vencidas + ')';\n const tareasFiltered = f.tareas.filter(t => t.nombre && t.nombre.trim() !== '.');\n const details = tareasFiltered.slice(0, 8).map(t => {\n const ic = t.completada ? '✓' : (t.vencida ? '🔴' : '⏳');\n const r = t.responsables[0] ? ' · ' + t.responsables[0].split(' ').slice(0, 2).join(' ') : ' · sin asignar';\n return ic + ' ' + t.nombre.slice(0, 55) + r;\n }).join('\\n');\n return head + '\\n' + details;\n});\nconst fasesFull = fasesTxt.join('\\n\\n');\nfields.push({ name: '📋 Tareas por fase', value: fasesFull.slice(0, 1024), inline: false });\n\nif (d.cards_stale.length > 0 || d.cards_sin_asignar.length > 0 || d.cards_sin_fecha.length > 0) {\n const lines = [];\n if (d.cards_stale.length > 0) lines.push('🕸️ **Stale**: ' + d.cards_stale.slice(0, 3).map(t => t.nombre + '(' + t.dias_sin_actividad + 'd)').join(', '));\n if (d.cards_sin_asignar.length > 0) lines.push('❓ **Sin asignar**: ' + d.cards_sin_asignar.slice(0, 3).map(t => t.nombre).join(', '));\n if (d.cards_sin_fecha.length > 0) lines.push('📅 **Sin fecha**: ' + d.cards_sin_fecha.slice(0, 3).map(t => t.nombre).join(', '));\n fields.push({ name: '⚠️ Cards problemáticas', value: lines.join('\\n').slice(0, 1024), inline: false });\n}\n\nconst commitBack = d.github.top_commits_back.slice(0, 5).map(c => '`' + c.sha + '` ' + c.autor.split(' ')[0] + ': ' + c.mensaje.slice(0, 60) + ' · ' + (c.fecha || '').slice(0, 10)).join('\\n') || '(sin commits)';\nconst commitFront = d.github.top_commits_front.slice(0, 5).map(c => '`' + c.sha + '` ' + c.autor.split(' ')[0] + ': ' + c.mensaje.slice(0, 60) + ' · ' + (c.fecha || '').slice(0, 10)).join('\\n') || '(sin commits)';\nfields.push({ name: '📝 Commits backend (top 5)', value: commitBack.slice(0, 1024), inline: false });\nfields.push({ name: '📝 Commits frontend (top 5)', value: commitFront.slice(0, 1024), inline: false });\n\nconst entregables = d.snapshot_tareas\n .filter(t => (t.archivos.length > 0 || t.enlaces.length > 0) && t.nombre && t.nombre.trim() !== '.')\n .sort((a, b) => new Date(b.dateLastActivity || 0) - new Date(a.dateLastActivity || 0))\n .slice(0, 6);\nif (entregables.length > 0) {\n const entTxt = entregables.map(t => {\n const files = t.archivos.slice(0, 2).map(a => '[📄 ' + a.name + '](' + a.url + ')').join(' ');\n const links = t.enlaces.slice(0, 2).map(l => '[🔗 ' + (l.name || 'link') + '](' + l.url + ')').join(' ');\n return '**' + t.nombre.slice(0, 40) + '**: ' + [files, links].filter(Boolean).join(' ');\n }).join('\\n');\n fields.push({ name: '📎 Entregables recientes', value: entTxt.slice(0, 1024), inline: false });\n}\n\n\n\nconst kpiTxt = '⚡ Velocidad: ' + d.kpis.velocidad_tareas_por_dia + '/día · Bus factor: ' + d.kpis.bus_factor_14d + ' · Conv: ' + d.kpis.pct_commits_convencionales + '%\\n' +\n '🔇 Silencio back: ' + d.kpis.silencio_back_dias + 'd · front: ' + d.kpis.silencio_front_dias + 'd\\n' +\n '🔀 PRs abiertos: ' + (d.github.prs_abiertos_back + d.github.prs_abiertos_front) + ' · avg open ' + d.kpis.pr_open_time_dias + 'd · merge ratio ' + d.kpis.pr_merge_ratio_30d + '%';\nfields.push({ name: '📊 KPIs', value: kpiTxt.slice(0, 1024), inline: false });\n\nconst ciStatusEmoji = s => s === 'success' ? '✅' : s === 'failure' ? '❌' : s === 'in_progress' ? '🟡' : '⚪';\nconst ciBackTxt = d.github.ci_back ? ciStatusEmoji(d.github.ci_back.status) + ' ' + d.github.ci_back.status + ' · `' + d.github.ci_back.branch + '` · [run](' + d.github.ci_back.url + ')' : '(sin datos)';\nconst ciFrontTxt = d.github.ci_front ? ciStatusEmoji(d.github.ci_front.status) + ' ' + d.github.ci_front.status + ' · `' + d.github.ci_front.branch + '` · [run](' + d.github.ci_front.url + ')' : '(sin datos)';\nfields.push({ name: '🔧 CI', value: 'Backend: ' + ciBackTxt + '\\nFrontend: ' + ciFrontTxt + '\\n🌐 Deploy front: ' + d.github.deploy_front_url, inline: false });\n\nif (d.hitos_proximos.length > 0) {\n const hitTxt = d.hitos_proximos.slice(0, 4).map(h => '📅 **' + h.nombre + '** en ' + h.dias_faltan + 'd (' + h.responsable + ')').join('\\n');\n fields.push({ name: '🎯 Hitos próximos', value: hitTxt.slice(0, 1024), inline: false });\n}\n\nconst iaResumen = iaText.length > 1000 ? iaText.slice(0, 1000) + '…' : iaText;\nfields.push({ name: '🤖 Diagnóstico IA (Gemini)', value: iaResumen, inline: false });\n\nconst payload = {\n username: 'Monitor PetSafe',\n embeds: [{\n title: '📊 Estado proyecto PetSafe — ' + new Date().toLocaleDateString('es-ES'),\n description: descripcion.slice(0, 4000),\n color,\n fields,\n footer: { text: 'Monitor automático · ' + d.severidad_max.toUpperCase() + ' · ' + d.cantidad_problemas + ' problemas' },\n timestamp: d.fecha\n }]\n};\n\nreturn [{ json: { discord_payload: payload, ia_text: iaText } }];"},"id":"e01235e0-fb31-4c47-99c5-67e9435958ce","name":"Build Discord Payload","type":"n8n-nodes-base.code","typeVersion":2,"position":[1680,-160]},{"parameters":{"operation":"executeQuery","query":"UPDATE analisis_proyecto SET recomendaciones_ia = '{{ $json.ia_text.replace(/'/g, \"''\") }}' WHERE id = (SELECT MAX(id) FROM analisis_proyecto);","options":{}},"id":"c1f15751-b93b-43e4-a0d1-766ad441ca1e","name":"Guardar Recomendación IA","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1920,-16],"credentials":{"postgres":{"id":"jNkFhyCAmWj8r3Qh","name":"Postgres account"}}},{"parameters":{"method":"POST","url":"={{ $node[\"Configuración\"].json.DISCORD_WEBHOOK_URL }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify($json.discord_payload) }}","options":{}},"id":"e29c2642-3f64-408f-b9fc-e617de140526","name":"Discord Alerta","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1920,-240]},{"parameters":{"jsCode":"const d = $node['Analisis + KPIs'].json;\nconst fases = d.fases || {};\nconst tareas = d.snapshot_tareas || [];\nconst kpis = d.kpis || {};\n\n// Cuellos de botella\nconst bottlenecks = [];\nfor (const [fname, f] of Object.entries(fases)) {\n if (f.pct < 50 && f.total > 2) {\n bottlenecks.push({\n fase: fname,\n bloqueadas: f.pendientes,\n porcentaje: f.pct,\n severidad: f.pct < 20 ? 'CRITICO' : 'ALTO'\n });\n }\n}\n\n// Proyección mejorada\nconst diasFin = d.kpis.dias_hasta_fin || 30;\nconst porcentajeFin = d.progreso_pct || 0;\nconst requiredBurnRate = (100 - porcentajeFin) / diasFin;\nconst burnRate = d.kpis.velocidad_tareas_por_dia || 0;\n\nconst riesgo = {\n proyeccion: d.kpis.proyeccion,\n dias_restantes: diasFin,\n progreso_actual: porcentajeFin,\n burn_rate_actual: burnRate,\n burn_rate_requerido: parseFloat(requiredBurnRate.toFixed(2)),\n riesgo_nivel: burnRate < requiredBurnRate * 0.7 ? 'CRITICO' : burnRate < requiredBurnRate ? 'ALTO' : 'BAJO'\n};\n\n// Personas críticas\nconst rankingCritico = d.ranking.slice(0, 3).map(r => ({\n initials: r.initials,\n nombre: r.trello_name,\n carga: r.tareas_total,\n completadas: r.tareas_done,\n activo: r.commits_14d > 0\n}));\n\nreturn [{ json: {\n cuellos_botella: bottlenecks,\n riesgo,\n personas_criticas: rankingCritico\n}}];"},"id":"2f39d715-e048-41cc-b50b-43928b9fe337","name":"Analisis Impacto","type":"n8n-nodes-base.code","typeVersion":2,"position":[720,-64]},{"parameters":{},"id":"e9e6a33e-a352-4236-9d4f-437849f5019e","name":"Error Trigger","type":"n8n-nodes-base.errorTrigger","typeVersion":1,"position":[-288,-16]},{"parameters":{"assignments":{"assignments":[{"id":"e1s1","name":"mensaje_error","value":"={{ `🚨 **Error en Workflow de Monitoreo** 🚨\\n\\n**Workflow:** ${$workflow.name}\\n**Nodo:** ${$json.error.node.name}\\n**Error:** ${$json.error.message}` }}","type":"string"}]},"options":{}},"id":"a1b2c3d4-e5f6-7890-1234-567890abcdef","name":"Formatear Error","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[-64,-16]},{"parameters":{"method":"POST","url":"={{ $env.DISCORD_WEBHOOK_URL }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ ({ content: $json.mensaje_error }) }}","options":{}},"id":"b2c3d4e5-f6a7-8901-2345-67890abcdef1","name":"Alerta a Discord","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[192,-16]}],"connections":{"Schedule Trigger":{"main":[[{"node":"Configuración","type":"main","index":0}]]},"Configuración":{"main":[[{"node":"Trello Cards","type":"main","index":0},{"node":"Trello Lists","type":"main","index":0},{"node":"Trello Members","type":"main","index":0},{"node":"Trello Checklists","type":"main","index":0},{"node":"GitHub Back Commits","type":"main","index":0},{"node":"GitHub Front Commits","type":"main","index":0},{"node":"GitHub Back PRs","type":"main","index":0},{"node":"GitHub Front PRs","type":"main","index":0},{"node":"GitHub Back CI Runs","type":"main","index":0},{"node":"GitHub Front CI Runs","type":"main","index":0},{"node":"Postgres Historial","type":"main","index":0},{"node":"Postgres Hitos","type":"main","index":0}]]},"Trello Cards":{"main":[[{"node":"Merge Datos","type":"main","index":0}]]},"Trello Lists":{"main":[[{"node":"Merge Datos","type":"main","index":1}]]},"Trello Members":{"main":[[{"node":"Merge Datos","type":"main","index":2}]]},"Trello Checklists":{"main":[[{"node":"Merge Datos","type":"main","index":3}]]},"GitHub Back Commits":{"main":[[{"node":"Merge Datos","type":"main","index":4}]]},"GitHub Front Commits":{"main":[[{"node":"Merge Datos","type":"main","index":5}]]},"GitHub Back PRs":{"main":[[{"node":"Merge Datos","type":"main","index":6}]]},"GitHub Front PRs":{"main":[[{"node":"Merge Datos","type":"main","index":7}]]},"GitHub Back CI Runs":{"main":[[{"node":"Merge Datos","type":"main","index":8}]]},"GitHub Front CI Runs":{"main":[[{"node":"Merge Datos","type":"main","index":9}]]},"Postgres Historial":{"main":[[{"node":"Merge Datos","type":"main","index":10}]]},"Postgres Hitos":{"main":[[{"node":"Merge Datos","type":"main","index":11}]]},"Merge Datos":{"main":[[{"node":"Análisis + KPIs","type":"main","index":0}]]},"Análisis + KPIs":{"main":[[{"node":"Guardar Análisis","type":"main","index":0},{"node":"IF Hay Problemas","type":"main","index":0}]]},"Guardar Análisis":{"main":[[{"node":"Guardar Snapshot Tareas","type":"main","index":0}]]},"Guardar Snapshot Tareas":{"main":[[{"node":"Insert Snapshot","type":"main","index":0}]]},"IF Hay Problemas":{"main":[[{"node":"Preparar Prompt","type":"main","index":0}]]},"Preparar Prompt":{"main":[[{"node":"Gemini Análisis","type":"main","index":0}]]},"Gemini Análisis":{"main":[[{"node":"Build Discord Payload","type":"main","index":0}]]},"Build Discord Payload":{"main":[[{"node":"Discord Alerta","type":"main","index":0},{"node":"Guardar Recomendación IA","type":"main","index":0}]]},"Error Trigger":{"main":[[{"node":"Formatear Error","type":"main","index":0}]]},"Formatear Error":{"main":[[{"node":"Alerta a Discord","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","binaryMode":"separate"},"staticData":null,"meta":{"templateCredsSetupCompleted":true,"instanceId":"2ea39fa17dcdc4ec838b704e984f07415fea66105c8eb910cc7de85c3d5eeea5"},"pinData":{},"versionId":"f625fe3c-61aa-4dde-b65f-1d35f5117728","activeVersionId":null,"versionCounter":1,"triggerCount":0,"tags":[],"shared":[{"updatedAt":"2026-04-23T06:08:15.778Z","createdAt":"2026-04-23T06:08:15.778Z","role":"workflow:owner","workflowId":"Q4GWX9tgUGXv8rek","projectId":"ixuM5WTjejXtid6r","project":{"updatedAt":"2026-02-04T22:01:50.457Z","createdAt":"2026-02-04T21:54:06.387Z","id":"ixuM5WTjejXtid6r","name":"David Manjarres <davidmanjarres2004@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"bb4dbe4f-b694-4789-be9a-263426cd8714"}}]}]