From 64b99eed068f3ed5b3874dd8dafa4741043412f5 Mon Sep 17 00:00:00 2001 From: erdemerikci Date: Fri, 22 May 2026 15:03:03 +0300 Subject: [PATCH] Remove Lightning_Report_Automatic.json file, eliminating the n8n workflow and its components for report generation and token management. --- Lightning_Report_Automatic.json | 2497 ------------------------------- README.md | 217 +++ 2 files changed, 217 insertions(+), 2497 deletions(-) delete mode 100644 Lightning_Report_Automatic.json create mode 100644 README.md diff --git a/Lightning_Report_Automatic.json b/Lightning_Report_Automatic.json deleted file mode 100644 index 3747df7..0000000 --- a/Lightning_Report_Automatic.json +++ /dev/null @@ -1,2497 +0,0 @@ -{ - "name": "Lightning_Report_Automatic", - "nodes": [ - { - "parameters": { - "content": "export REPORT_SERVICE_TOKEN=\"your-long-random-secret\"\nTo run uvicorn report_service.main:app --host 0.0.0.0 --port 8000\n\nOn another terminal: ngrok http 8000\n", - "height": 128, - "width": 512 - }, - "type": "n8n-nodes-base.stickyNote", - "position": [ - 144864, - 61392 - ], - "typeVersion": 1, - "id": "7421dafd-30d6-4b5a-8515-5b8dc7de8a7a", - "name": "Sticky Note1" - }, - { - "parameters": { - "method": "POST", - "url": "https://api-test.iklim.co/v1/auth/login", - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "username", - "value": "info@iklim.co" - }, - { - "name": "password", - "value": "gn6aV01Z759HTw9I" - } - ] - }, - "options": {} - }, - "id": "20d63bf7-de85-4f71-94e3-5c479dd2991f", - "name": "Login to iklim.co", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 143664, - 60480 - ], - "onError": "continueErrorOutput" - }, - { - "parameters": { - "assignments": { - "assignments": [ - { - "id": "5c1668fb-f97b-4ec8-80f9-61bfb0b8274e", - "name": "username", - "value": "={{ $json.username }}", - "type": "string" - }, - { - "id": "97c76bac-aa08-437f-ac48-a0cad9b93526", - "name": "type", - "value": "={{ $json.type }}", - "type": "string" - }, - { - "id": "fc59d51f-b225-4abb-9f17-93173e650652", - "name": "accessToken", - "value": "={{ $json.accessToken }}", - "type": "string" - }, - { - "id": "95e5ec41-2369-492b-9950-fc230a47f7fa", - "name": "accessTokenExpiration", - "value": "={{ $now.plus({ hours: 1 }).toMillis() }}", - "type": "string" - }, - { - "id": "3d79d06a-ed55-48af-b4bc-b7fd67d80be1", - "name": "refreshToken", - "value": "={{ $json.refreshToken }}", - "type": "string" - }, - { - "id": "aedf0ef4-9971-42e8-8b32-7d039342dd88", - "name": "refreshTokenExpiration", - "value": "={{ $now.plus({ days: 1 }).toMillis() }}", - "type": "string" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - 143904, - 60464 - ], - "id": "40fc21b3-1a4c-44a0-9831-fa34b6730e33", - "name": "CalculateExpirations" - }, - { - "parameters": { - "assignments": { - "assignments": [ - { - "id": "5c1668fb-f97b-4ec8-80f9-61bfb0b8274e", - "name": "username", - "value": "={{ $json.username }}", - "type": "string" - }, - { - "id": "97c76bac-aa08-437f-ac48-a0cad9b93526", - "name": "type", - "value": "={{ $json.type }}", - "type": "string" - }, - { - "id": "fc59d51f-b225-4abb-9f17-93173e650652", - "name": "accessToken", - "value": "={{ $json.accessToken }}", - "type": "string" - }, - { - "id": "95e5ec41-2369-492b-9950-fc230a47f7fa", - "name": "accessTokenExpiration", - "value": "={{ $now.plus({ hours: 1 }).toMillis() }}", - "type": "string" - }, - { - "id": "3d79d06a-ed55-48af-b4bc-b7fd67d80be1", - "name": "refreshToken", - "value": "={{ $json.refreshToken }}", - "type": "string" - }, - { - "id": "aedf0ef4-9971-42e8-8b32-7d039342dd88", - "name": "refreshTokenExpiration", - "value": "={{ $now.plus({ days: 1 }).toMillis() }}", - "type": "string" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - 143968, - 60720 - ], - "id": "6e9dd3f4-ffb7-46dc-b6b8-8439698012e6", - "name": "CalculateExpirations1" - }, - { - "parameters": { - "method": "POST", - "url": "https://api-test.iklim.co/v1/auth/refresh", - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "refreshToken", - "value": "={{ $input.auth..refreshToken }}" - } - ] - }, - "options": {} - }, - "id": "4fbac84b-6069-4be5-b6d0-cca265f2364b", - "name": "Refresh Token", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 143744, - 60736 - ], - "onError": "continueErrorOutput" - }, - { - "parameters": { - "rules": { - "values": [ - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "leftValue": "={{ DateTime.fromMillis($json.refreshTokenExpiration - 300000) }}", - "rightValue": "={{ $('Daily Trigger').item.json.timestamp }}", - "operator": { - "type": "dateTime", - "operation": "before" - }, - "id": "2e2eea90-cc23-412f-a0f3-c8b9747d0fae" - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Relogin" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "2c01f003-c0d1-4ed7-b41b-5e281b7353bb", - "leftValue": "={{ DateTime.fromMillis($json.accessTokenExpiration - 600000)}}", - "rightValue": "={{ $('Daily Trigger').item.json.timestamp }}", - "operator": { - "type": "dateTime", - "operation": "before" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Refresh" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "590bec27-6e6f-4f0b-a701-3a359032c4ed", - "leftValue": "={{ $('Daily Trigger').item.json.timestamp }}", - "rightValue": "", - "operator": { - "type": "dateTime", - "operation": "exists", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "Valid" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.switch", - "typeVersion": 3.4, - "position": [ - 143440, - 60704 - ], - "id": "f62f304e-72dd-4c55-834f-1d0569f0f7e3", - "name": "Switch" - }, - { - "parameters": { - "jsCode": "const staticData = $getWorkflowStaticData('global');\n\n// initialize accessToken on staticData if it desn't exist yet\nif (!staticData.hasOwnProperty('user')) {\n staticData.user = null;\n}\n\nif (!staticData.hasOwnProperty('authType')) {\n staticData.authType = 'Bearer';\n}\n\nif (!staticData.hasOwnProperty('accessToken')) {\n staticData.accessToken = null;\n}\n\nif (!staticData.hasOwnProperty('accessTokenExpiration')) {\n staticData.accessTokenExpiration = 0;\n}\n\nif (!staticData.hasOwnProperty('refreshToken')) {\n staticData.refreshToken = null;\n}\n\nif (!staticData.hasOwnProperty('refreshTokenExpiration')) {\n staticData.refreshTokenExpiration = 0;\n}\n\nconst currentTime = $input.first().json.timestamp;\n\n\nreturn [\n {\n user: staticData.user,\n bearer: staticData.bearer,\n accessToken: staticData.accessToken,\n accessTokenExpiration: staticData.accessTokenExpiration,\n refreshToken: staticData.refreshToken,\n refreshTokenExpiration: staticData.refreshTokenExpiration,\n timestamp: currentTime\n }\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 143248, - 60720 - ], - "id": "023b107a-6e4b-464c-8c96-62e8fcb929af", - "name": "Restore Credentials", - "retryOnFail": false - }, - { - "parameters": { - "jsCode": "const staticData = $getWorkflowStaticData('global');\n// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n staticData.user = item.json.username;\n staticData.authType = item.json.type;\n staticData.accessToken = item.json.accessToken;\n staticData.accessTokenExpiration = item.json.accessTokenExpiration;\n staticData.refreshToken = item.json.refreshToken;\n staticData.refreshTokenExpiration = item.json.refreshTokenExpiration;\n}\n\n//return [{ json: $getWorkflowStaticData('global') }];\nreturn $input.all();" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 144192, - 60720 - ], - "id": "f2e3ca7a-eb6b-48d7-bc69-83755dea9ee6", - "name": "Store Refresh Credentials" - }, - { - "parameters": { - "jsCode": "const staticData = $getWorkflowStaticData('global');\n// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n staticData.user = item.json.username;\n staticData.authType = item.json.type;\n staticData.accessToken = item.json.accessToken;\n staticData.accessTokenExpiration = item.json.accessTokenExpiration;\n staticData.refreshToken = item.json.refreshToken;\n staticData.refreshTokenExpiration = item.json.refreshTokenExpiration;\n}\n\n//return [{ json: $getWorkflowStaticData('global') }];\nreturn $input.all();" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 144112, - 60464 - ], - "id": "5ed52fa8-32be-4612-b1a7-1adfb61acabf", - "name": "Store Login Credentials" - }, - { - "parameters": { - "select": "channel", - "channelId": { - "__rl": true, - "value": "C0A9K1AC7SN", - "mode": "list", - "cachedResultName": "n8n-events" - }, - "text": "=⚠️ {{ $workflow.name }} isimli iş akışı çalıştırılırken bir yetkilendirme hatası oluştu \n{{ $json.error?.message || '' }}", - "otherOptions": { - "includeLinkToWorkflow": false - } - }, - "type": "n8n-nodes-base.slack", - "typeVersion": 2.4, - "position": [ - 143904, - 60288 - ], - "id": "3fcb6f64-0273-4fce-a4d3-0ed465a65d7c", - "name": "Send Error Notification", - "webhookId": "0b3582cf-042e-43d6-bc02-4c9debab0566", - "credentials": { - "slackApi": { - "id": "OKgM8VkM05pJl9kU", - "name": "Tarla Slack Account" - } - } - }, - { - "parameters": { - "rule": { - "interval": [ - { - "field": "hours", - "hoursInterval": 12 - } - ] - } - }, - "id": "9294eedf-baee-4020-8489-b7c10589a37b", - "name": "Daily Trigger", - "type": "n8n-nodes-base.scheduleTrigger", - "typeVersion": 1, - "position": [ - 142704, - 60704 - ] - }, - { - "parameters": { - "method": "POST", - "url": "https://api-test.iklim.co/v1/lightnings/within", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Authorization", - "value": "={{ $json.headers.Authorization }}" - }, - { - "name": "X-Signature", - "value": "={{ $json.headers['X-Signature'] }}" - }, - { - "name": "X-Timestamp", - "value": "={{ $json.headers['X-Timestamp'] }}" - }, - { - "name": "X-Nonce", - "value": "={{ $json.headers['X-Nonce'] }}" - }, - { - "name": "X-Idempotency-Key", - "value": "={{ $json.headers['X-Idempotency-Key'] }}" - }, - { - "name": "Content-Type", - "value": "={{ $json.headers['Content-Type'] }}" - }, - { - "name": "Accept", - "value": "={{ $json.headers['Content-Type'] }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.requestBody }}", - "options": {} - }, - "id": "a9d7863d-9929-4b22-a013-22cefb1939cc", - "name": "Lightning Request", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 144768, - 60992 - ], - "onError": "continueErrorOutput" - }, - { - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "value": "bMJhm1lTDbCj9eY1", - "mode": "list", - "cachedResultName": "customers", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/bMJhm1lTDbCj9eY1" - } - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 143248, - 60976 - ], - "id": "4322bbc8-14a9-4107-9b2e-330ba07a3b31", - "name": "Iterate Customers" - }, - { - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "value": "e2gQ68IGiYmCZkES", - "mode": "list", - "cachedResultName": "wind_turbine_farm", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/e2gQ68IGiYmCZkES" - }, - "filters": { - "conditions": [ - { - "keyName": "customer_id", - "keyValue": "={{ $json.id }}" - } - ] - } - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 143984, - 60992 - ], - "id": "22fdfc9c-9aae-4192-84ce-53b0cb05e452", - "name": "Get Customer Wind Turbines" - }, - { - "parameters": { - "select": "channel", - "channelId": { - "__rl": true, - "value": "C0A9K1AC7SN", - "mode": "list", - "cachedResultName": "n8n-events" - }, - "text": "=⚠️ {{ $workflow.name }} isimli iş akışı çalıştırılırken bir hata oluştu. \n{{ $('Iterate Customers').item.json.customer_name }} müşterisine ait {{ $('Get Customer Wind Turbines').item.json.name }} isimli türbin için yıldırımlar sorgulanırken şu hata oluştu: {{ $json.error.message }}", - "otherOptions": { - "includeLinkToWorkflow": false - } - }, - "type": "n8n-nodes-base.slack", - "typeVersion": 2.4, - "position": [ - 144896, - 60816 - ], - "id": "87edff46-b231-4e47-ab26-38d3d19bd3d0", - "name": "Request Error Notification", - "webhookId": "38b3ebc5-a136-4b78-9c7a-98f40e92a9d5", - "notesInFlow": false, - "executeOnce": true, - "credentials": { - "slackApi": { - "id": "OKgM8VkM05pJl9kU", - "name": "Tarla Slack Account" - } - } - }, - { - "parameters": { - "jsCode": "const turbines = $input.all();\n\n// 1. Safeguard: Skip if no turbines are found\nif (turbines.length === 0) {\n return { skip_customer: true };\n}\n\nconst lats = turbines.map(t => Number(t.json.latitude));\nconst lons = turbines.map(t => Number(t.json.longitude));\n\n// 2. Calculate Centroid\nconst n = lats.length;\nconst centroid_lat = lats.reduce((a, b) => a + b, 0) / n;\nconst centroid_lon = lons.reduce((a, b) => a + b, 0) / n;\n\n// 3. Haversine Distance Helper\nconst getDist = (lat1, lon1, lat2, lon2) => {\n const R = 6371e3; // Earth radius in meters\n const dLat = (lat2 - lat1) * Math.PI / 180;\n const dLon = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLon/2)**2;\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n};\n\n// 4. Find Farthest Turbine\nlet maxDist = 0;\nfor (let i = 0; i < n; i++) {\n const d = getDist(centroid_lat, centroid_lon, lats[i], lons[i]);\n if (d > maxDist) maxDist = d;\n}\n\n// 5. Calculate Rings\n// Updated: Ensures a minimum 1000m radius even for a single turbine point\nconst r1 = Math.max(1000, Math.ceil(maxDist / 1000) * 1000);\nconst r4 = r1 + 6000;\nconst boundary = Math.min(r4 + 2000, 49999); \n\nreturn {\n skip_customer: false,\n farm_id: turbines[0].json.customer_id,\n customer_name: $node[\"Loop Over Items\"].json.customer_name,\n centroid_latitude: centroid_lat,\n centroid_longitude: centroid_lon,\n monitoring_boundary_m: boundary,\n rings: { r1, r2: r1+2000, r3: r1+4000, r4 }\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 144192, - 60992 - ], - "id": "db2cd01a-fedb-4cae-ad69-9f6ba6ea659a", - "name": "Centroid & Distance Ring calculation" - }, - { - "parameters": { - "jsCode": "// Merge the centroid/rings data with the new loop variables\nreturn {\n ...$input.item.json, \n pageNumber: 0,\n stopLoop: false,\n allStrikes: [],\n tStart: null,\n tLast: null\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 144384, - 60992 - ], - "id": "282c62de-b9f8-4aed-9908-a8b8e153363e", - "name": "Loop init" - }, - { - "parameters": { - "jsCode": "const SILENCE_MS = 12 * 60 * 60 * 1000;\nconst response = $input.first().json; \nconst batchStrikes = response.lightnings || []; \nconst currentPage = response.pageNumber || 0; \n\nconst farm = $('Loop init').all().pop().json;\nlet allStrikes = [];\nlet tLast = null;\nlet tStart = null;\nlet stopLoop = false;\n\n// FIX: Clone the array to avoid 'Read Only' errors\nif (currentPage > 0) {\n try {\n const prevState = $('Logic Gate').all().pop().json;\n allStrikes = prevState.allStrikes ? [...prevState.allStrikes] : [];\n tLast = prevState.tLast;\n tStart = prevState.tStart;\n } catch (e) { }\n}\n\nif (batchStrikes.length === 0 && currentPage === 0) {\n return { stopLoop: true, allStrikes: [], pageNumber: 0, status: \"EMPTY\", customer_name: farm.customer_name };\n}\n\nif (currentPage === 0 && batchStrikes.length > 0) {\n tLast = new Date(batchStrikes[0].captured).getTime();\n}\n\nfor (let i = 0; i < batchStrikes.length; i++) {\n const currentTs = new Date(batchStrikes[i].captured).getTime();\n batchStrikes[i].timestamp = currentTs; \n allStrikes.push(batchStrikes[i]);\n\n if (batchStrikes[i + 1]) {\n const nextTs = new Date(batchStrikes[i + 1].captured).getTime();\n if (currentTs - nextTs > SILENCE_MS) {\n tStart = currentTs;\n stopLoop = true;\n break;\n }\n }\n}\n\nif (batchStrikes.length < 100) {\n stopLoop = true;\n if (!tStart && allStrikes.length > 0) {\n tStart = allStrikes[allStrikes.length - 1].timestamp;\n }\n}\n\n// Ensure End is later than Start\nconst timestamps = allStrikes.map(s => s.timestamp);\nconst final_tLast = timestamps.length > 0 ? Math.max(...timestamps) : tLast;\nconst final_tStart = tStart || (timestamps.length > 0 ? Math.min(...timestamps) : tLast);\n\nconst roundToMinute = (ts) => Math.floor(Number(ts || 0) / 60000);\nconst stormKey = [\n String(farm.customer_name || ''),\n roundToMinute(final_tStart),\n roundToMinute(final_tLast),\n].join('_');\n\nreturn { \n stopLoop, \n pageNumber: currentPage + 1,\n allStrikes, \n tStart: final_tStart, \n tLast: final_tLast,\n storm_start_min: roundToMinute(final_tStart),\n storm_end_min: roundToMinute(final_tLast),\n storm_key: stormKey,\n customer_name: farm.customer_name,\n status: stopLoop ? \"FINISHED\" : \"PAGING\"\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 145008, - 60992 - ], - "id": "9dd01358-362f-4676-bf35-af32739c4cc1", - "name": "Logic Gate" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "68ba538e-8070-4470-b0fa-30106a1f65b5", - "leftValue": "={{ $node[\"Logic Gate\"].json.stopLoop }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 145200, - 60960 - ], - "id": "2fa3ce7d-fec7-49fe-bd69-099d3fd11c61", - "name": "Stop Loop" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "8b6c4fdd-0f94-4bc8-82ac-0996af68c534", - "leftValue": "={{ $node[\"Logic Gate\"].json.allStrikes.length }}", - "rightValue": 0, - "operator": { - "type": "number", - "operation": "gt" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 145392, - 60976 - ], - "id": "0e8dfc78-4fe0-4eb1-9649-f173c50ee884", - "name": "Lightning found?" - }, - { - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "matchType": "allConditions", - "filters": { - "conditions": [ - { - "keyName": "storm_key", - "keyValue": "={{ $json.storm_key }}" - } - ] - }, - "limit": 1 - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 145632, - 61008 - ], - "id": "2d8a4c7d-7feb-4d0f-9ef5-0db2e92325ed", - "name": "Get row(s)", - "alwaysOutputData": true - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "02a4a5c6-2645-4d72-9952-9a62c69fa19f", - "leftValue": "={{ $json.skip_insert }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "false", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 146016, - 61008 - ], - "id": "ae67e14a-6d20-4300-9095-a31dafc43b44", - "name": "If" - }, - { - "parameters": { - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "columns": { - "mappingMode": "defineBelow", - "value": { - "storm_start": "={{ $json.tStart }}", - "storm_end": "={{ $json.tLast }}", - "total_strikes": "={{ $json.allStrikes.length }}", - "customer_name": "={{ $json.customer_name }}", - "status": "waiting for confirmation", - "storm_key": "={{ $json.storm_key }}", - "storm_start_min": "={{ $json.storm_start_min }}", - "storm_end_min": "={{ $json.storm_end_min }}" - }, - "matchingColumns": [], - "schema": [ - { - "id": "customer_name", - "displayName": "customer_name", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "total_strikes", - "displayName": "total_strikes", - "required": false, - "defaultMatch": false, - "display": true, - "type": "number", - "readOnly": false, - "removed": false - }, - { - "id": "storm_start", - "displayName": "storm_start", - "required": false, - "defaultMatch": false, - "display": true, - "type": "number", - "readOnly": false, - "removed": false - }, - { - "id": "storm_end", - "displayName": "storm_end", - "required": false, - "defaultMatch": false, - "display": true, - "type": "number", - "readOnly": false, - "removed": false - }, - { - "id": "status", - "displayName": "status", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "storm_key", - "displayName": "storm_key", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "storm_start_min", - "displayName": "storm_start_min", - "required": false, - "defaultMatch": false, - "display": true, - "type": "number", - "readOnly": false, - "removed": false - }, - { - "id": "storm_end_min", - "displayName": "storm_end_min", - "required": false, - "defaultMatch": false, - "display": true, - "type": "number", - "readOnly": false, - "removed": false - } - ], - "attemptToConvertTypes": true, - "convertFieldsToString": false - }, - "options": {} - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 146256, - 61008 - ], - "id": "8f2ab12b-8a8e-4616-a4ad-2238bf45a168", - "name": "Insert row" - }, - { - "parameters": { - "options": {} - }, - "type": "n8n-nodes-base.splitInBatches", - "typeVersion": 3, - "position": [ - 143744, - 60976 - ], - "id": "fe493f2d-72e3-40c0-9a02-d4b1e1499676", - "name": "Loop Over Items" - }, - { - "parameters": { - "jsCode": "const crypto = require('crypto');\nconst HMAC_SECRET = 'c88f845bd6d520ded507ef6b02efc223019ccf68f41d9070705712d480ba5166';\nconst URI = '/v1/thunderstorms/within';\n\nconst storm = $('Logic Gate').all().pop().json;\nconst farm = $('Loop init').all().pop().json;\nconst auth = $('Restore Credentials').all().pop().json;\n\nif (!storm.tLast || !storm.tStart) {\n throw new Error(\"Missing storm timestamps. Skipping Thunderstorm query.\");\n}\n\nconst durationSeconds = Math.floor((storm.tLast - storm.tStart) / 1000);\nconst timestamp = Date.now().toString();\n\nconst bodyPayload = {\n latitude: Number(Number(farm.centroid_latitude).toFixed(6)),\n longitude: Number(Number(farm.centroid_longitude).toFixed(6)),\n radius: Math.max(50000, farm.monitoring_boundary_m), // Ensure coverage[cite: 2]\n backwardInterval: Math.max(3600, durationSeconds + 600), // Dynamic padding[cite: 2]\n endTimeEpoch: Number(storm.tLast), // Use actual storm end[cite: 2]\n intersectsWith: \"THREAT_POLYGON\",\n pageNumber: 0,\n pageSize: 10\n};\n\nconst bodyString = JSON.stringify(bodyPayload);\nconst dataToSign = `POST|${URI}|${timestamp}|${bodyString}`;\nconst signature = crypto.createHmac('sha256', HMAC_SECRET).update(dataToSign).digest('hex').toLowerCase();\n\nreturn {\n requestBody: bodyPayload,\n headers: {\n \"X-Signature\": signature,\n \"X-Timestamp\": timestamp,\n \"X-Nonce\": crypto.randomUUID(),\n \"X-Idempotency-Key\": crypto.randomUUID(),\n \"Authorization\": \"Bearer \" + auth.accessToken,\n \"Content-Type\": \"application/json\"\n }\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 146480, - 61008 - ], - "id": "63828ae4-ff78-4c6e-b81c-88b4a9575eb0", - "name": "Calculate Thunderstorm Headers" - }, - { - "parameters": { - "method": "POST", - "url": "https://api-test.iklim.co/v1/thunderstorms/within", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Authorization", - "value": "={{ $json.headers.Authorization }}" - }, - { - "name": "X-Signature", - "value": "={{ $json.headers['X-Signature'] }}" - }, - { - "name": "X-Timestamp", - "value": "={{ $json.headers['X-Timestamp'] }}" - }, - { - "name": "X-Nonce", - "value": "={{ $json.headers['X-Nonce'] }}" - }, - { - "name": "X-Idempotency-Key", - "value": "={{ $json.headers['X-Idempotency-Key'] }}" - }, - { - "name": "Content-Type", - "value": "={{ $json.headers['Content-Type'] }}" - }, - { - "name": "Accept", - "value": "={{ $json.headers['Content-Type'] }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.requestBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.3, - "position": [ - 146688, - 61008 - ], - "id": "4107110b-79b2-4e53-ae98-cb4a39ce8631", - "name": "HTTP Request" - }, - { - "parameters": { - "jsCode": "const crypto = require('crypto');\nconst HMAC_SECRET = 'c88f845bd6d520ded507ef6b02efc223019ccf68f41d9070705712d480ba5166';\nconst METHOD = 'POST'; \nconst URI = '/v1/lightnings/within';\n\n// Get the override timestamp\nconst testConfig = $('Test Configuration').first().json;\nconst endTime = Number(testConfig.testTimestamp);\n\nconst allItems = $input.all();\nconst processedItems = [];\n\nfor (const item of allItems) {\n const farmData = $node[\"Loop init\"].json;\n const auth = $node[\"Restore Credentials\"].json;\n const pageNumber = item.json.pageNumber;\n const timestamp = Date.now().toString();\n \n const bodyPayload = {\n latitude: farmData.centroid_latitude,\n longitude: farmData.centroid_longitude,\n radius: farmData.monitoring_boundary_m, // Use dynamic radius[cite: 2]\n backwardInterval: 43200, // Fixed 12-hour window[cite: 2]\n endTimeEpoch: endTime, // Dynamic override[cite: 2]\n pageNumber: pageNumber,\n pageSize: 100\n };\n\n const bodyString = JSON.stringify(bodyPayload);\n const dataToSign = `${METHOD.toUpperCase()}|${URI}|${timestamp}|${bodyString}`;\n const signature = crypto.createHmac('sha256', HMAC_SECRET).update(dataToSign).digest('hex').toLowerCase();\n\n processedItems.push({\n json: {\n requestBody: bodyPayload,\n headers: {\n \"X-Signature\": signature,\n \"X-Timestamp\": timestamp,\n \"X-Nonce\": crypto.randomUUID(),\n \"X-Idempotency-Key\": crypto.randomUUID(),\n \"Authorization\": \"Bearer \" + auth.accessToken,\n \"Content-Type\": \"application/json\"\n }\n }\n });\n}\n\nreturn processedItems;" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 144576, - 60992 - ], - "id": "c7b6f9e1-561e-4a39-9cbe-d5978d3ddbdc", - "name": "Calculate Lightning Headers" - }, - { - "parameters": { - "jsCode": "const turbines = $('Get Customer Wind Turbines').all().map((t) => t.json);\nconst storm = $('Logic Gate').all().pop().json;\nconst farm = $('Loop init').all().pop().json;\nconst thunder = $input.all().map((t) => t.json);\n\nconst getNodeRowsSafely = (nodeName) => {\n try {\n return $(nodeName)\n .all()\n .map((i) => i.json)\n .filter((r) => r && r.id !== undefined && r.id !== null);\n } catch (error) {\n return [];\n }\n};\n\nconst existingRows = getNodeRowsSafely('Get row(s)');\nconst insertedRows = getNodeRowsSafely('Insert row');\nconst stormLogId = insertedRows[0]?.id ?? existingRows[0]?.id ?? null;\n\nreturn {\n customer_name: storm.customer_name,\n storm_log_id: stormLogId,\n centroid_lat: farm.centroid_latitude,\n centroid_lon: farm.centroid_longitude,\n boundary_m: farm.monitoring_boundary_m,\n rings: farm.rings,\n timezone: 'Europe/Istanbul',\n t_start: storm.tStart,\n t_end: storm.tLast,\n n_strikes: (storm.allStrikes || []).length,\n turbines: turbines,\n strikes: storm.allStrikes || [],\n storm_records: thunder,\n};" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 146896, - 61008 - ], - "id": "122509a3-5b70-4dbf-ac9e-f02fff0a1daa", - "name": "Build Report Payload" - }, - { - "parameters": { - "method": "POST", - "url": "https://patrol-plural-gooey.ngrok-free.dev/generate", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "X-Report-Token", - "value": "test-secret-123" - }, - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "ngrok-skip-browser-warning", - "value": "1" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json }}", - "options": { - "response": { - "response": { - "responseFormat": "file" - } - } - } - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.3, - "position": [ - 147104, - 61008 - ], - "id": "75501180-13a3-4022-8033-03c803ef808a", - "name": "Generate Report", - "onError": "continueErrorOutput" - }, - { - "parameters": { - "jsCode": "const customerName = String($node['Build Report Payload'].json.customer_name || 'customer').trim().replace(/\\s+/g, '_');\nconst now = new Date();\nconst year = String(now.getFullYear()).slice(-2);\nconst month = String(now.getMonth() + 1).padStart(2, '0');\nconst day = String(now.getDate()).padStart(2, '0');\nconst hour = String(now.getHours()).padStart(2, '0');\nconst minute = String(now.getMinutes()).padStart(2, '0');\nconst generatedAt = `${year}-${month}-${day}_${hour}-${minute}`;\nconst fileName = `${generatedAt}_${customerName}_iklimco_lightning_report.docx`;\n\nfor (const item of $input.all()) {\n if (!item.binary || !item.binary.data) {\n continue;\n }\n\n item.binary.data.fileName = fileName;\n item.binary.data.fileExtension = 'docx';\n item.binary.data.mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';\n}\n\nreturn $input.all();" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 147376, - 61008 - ], - "id": "da5ce86c-b0a2-4cb1-a552-71fdba0e82d5", - "name": "Prepare DOCX Metadata" - }, - { - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "matchType": "allConditions", - "filters": { - "conditions": [] - }, - "limit": 1000 - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 147584, - 61008 - ], - "id": "71310c32-b4bf-466f-9bf0-c8b2be392b67", - "name": "Get Storm Logs For Next Id", - "alwaysOutputData": true - }, - { - "parameters": { - "jsCode": "const payload = $node['Build Report Payload'].json || {};\nconst reportFileItem = $('Prepare DOCX Metadata').all()[0] || {};\nconst rows = $input.all().map((item) => item.json || {});\n\nconst normalizeId = (row) => {\n const raw = row?.id ?? row?.ID ?? row?._id ?? null;\n\n if (raw === null || raw === undefined) {\n return null;\n }\n\n if (typeof raw === 'string' && raw.trim() === '') {\n return null;\n }\n\n const num = Number(raw);\n if (Number.isFinite(num) && num > 0) {\n return num;\n }\n\n return raw;\n};\n\nconst hasValue = (v) => !(v === null || v === undefined || v === '');\n\nlet resolvedStormLogId = hasValue(payload.storm_log_id) ? payload.storm_log_id : null;\n\nif (!hasValue(resolvedStormLogId)) {\n const byKey = rows.find((row) => String(row.storm_key || '') === String(payload.storm_key || ''));\n if (byKey) {\n resolvedStormLogId = normalizeId(byKey);\n }\n}\n\nif (!hasValue(resolvedStormLogId)) {\n const ids = rows\n .map(normalizeId)\n .map((id) => Number(id))\n .filter((id) => Number.isFinite(id) && id > 0);\n\n if (ids.length > 0) {\n resolvedStormLogId = Math.max(...ids);\n }\n}\n\nreturn [{\n json: {\n ...(reportFileItem.json || {}),\n ...payload,\n storm_log_id: hasValue(resolvedStormLogId) ? resolvedStormLogId : 'unknown',\n },\n binary: reportFileItem.binary,\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 147776, - 61008 - ], - "id": "55fd799a-5c5e-4127-86be-23ed202c9569", - "name": "Resolve Storm Log Id" - }, - { - "parameters": { - "resource": "file", - "options": { - "channelId": "C0A9K1AC7SN", - "initialComment": "=✅ Lightning report uploaded for *{{ $json.customer_name }}* (log id: *{{ $json.storm_log_id }}*)." - } - }, - "type": "n8n-nodes-base.slack", - "typeVersion": 2.4, - "position": [ - 148432, - 61024 - ], - "id": "7f552eec-e913-4259-981c-36c533980918", - "name": "Upload Report to Slack", - "webhookId": "c4d5e6f7-a8b9-4012-cdef-345678901234", - "credentials": { - "slackApi": { - "id": "S5M0i1zlyebqU6oP", - "name": "n8n_lightning_report_bot" - } - } - }, - { - "parameters": { - "select": "channel", - "channelId": { - "__rl": true, - "value": "C0A9K1AC7SN", - "mode": "list", - "cachedResultName": "n8n-events" - }, - "messageType": "block", - "blocksUi": "={{ {\n \"blocks\": [\n {\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": `Approve or reject this report for *${$('Resolve Storm Log Id').item.json.customer_name ?? ''}* (log id: \\`${$('Resolve Storm Log Id').item.json.storm_log_id}\\`).`\n }\n },\n {\n \"type\": \"actions\",\n \"elements\": [\n {\n \"type\": \"button\",\n \"text\": { \"type\": \"plain_text\", \"text\": \"Approve\", \"emoji\": true },\n \"style\": \"primary\",\n \"action_id\": \"approve_storm_log\",\n \"value\": String($('Resolve Storm Log Id').item.json.storm_log_id)\n },\n {\n \"type\": \"button\",\n \"text\": { \"type\": \"plain_text\", \"text\": \"Reject\", \"emoji\": true },\n \"style\": \"danger\",\n \"action_id\": \"reject_storm_log\",\n \"value\": String($('Resolve Storm Log Id').item.json.storm_log_id)\n }\n ]\n }\n ]\n} }}", - "text": "Lightning storm report approval", - "otherOptions": {} - }, - "type": "n8n-nodes-base.slack", - "typeVersion": 2.4, - "position": [ - 149072, - 61184 - ], - "id": "44af280f-9c40-4380-bccf-40a7102a9e71", - "name": "Send Approval Buttons", - "webhookId": "d4e5f6a7-b8c9-0123-def0-456789abcdef", - "credentials": { - "slackApi": { - "id": "S5M0i1zlyebqU6oP", - "name": "n8n_lightning_report_bot" - } - } - }, - { - "parameters": { - "httpMethod": "POST", - "path": "slack-storm-log-approval", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2, - "position": [ - 142912, - 61440 - ], - "id": "d251f73d-92e7-4969-9363-0a49ec586a51", - "name": "Slack Interaction Webhook", - "webhookId": "c9d8e7f6-a5b4-4321-fedc-ba9876543211" - }, - { - "parameters": { - "jsCode": "const raw = $input.first().json;\nlet payloadRaw = raw.payload;\nif (payloadRaw === undefined && raw.body !== undefined) {\n const b = raw.body;\n if (typeof b === 'string' && b.includes('payload=')) {\n try {\n payloadRaw = new URLSearchParams(b).get('payload');\n } catch {\n payloadRaw = undefined;\n }\n } else if (b && typeof b === 'object' && b.payload !== undefined) {\n payloadRaw = b.payload;\n }\n}\nlet payload;\nif (payloadRaw !== null && payloadRaw !== undefined && typeof payloadRaw === 'object') {\n payload = payloadRaw;\n} else if (typeof payloadRaw === 'string') {\n try {\n payload = JSON.parse(payloadRaw);\n } catch {\n return [{ json: { valid: false } }];\n }\n} else {\n return [{ json: { valid: false } }];\n}\nif (payload.type !== 'block_actions') {\n return [{ json: { valid: false } }];\n}\nconst action = payload.actions?.[0];\nconst rawId = String(action?.value ?? '').trim();\nif (!rawId || !action?.action_id) {\n return [{ json: { valid: false } }];\n}\nlet status = null;\nif (action.action_id === 'approve_storm_log') {\n status = 'confirmed';\n} else if (action.action_id === 'reject_storm_log') {\n status = 'rejected';\n}\nif (!status) {\n return [{ json: { valid: false } }];\n}\n\nconst sectionText = payload?.message?.blocks?.find((block) => block?.type === 'section')?.text?.text || '';\nconst fallbackText = payload?.message?.text || '';\nconst sourceText = sectionText || fallbackText;\nconst customerMatch = sourceText.match(/for \\*(.*?)\\* \\(log id:/i) || sourceText.match(/for (.*?) \\(log id:/i);\nconst customer_name = customerMatch?.[1]?.trim() || 'unknown customer';\n\nconst numId = Number(rawId);\nconst id = Number.isFinite(numId) ? numId : rawId;\nconst slackResponseUrl = typeof payload.response_url === 'string' ? payload.response_url.trim() : '';\nreturn [{ json: { valid: true, id, status, customer_name, slack_response_url: slackResponseUrl } }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 143152, - 61440 - ], - "id": "4fac27df-10cd-4088-b263-43cf89965f14", - "name": "Parse Slack Button Payload" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "b8c9d0e1-f2a3-4455-b667-c78890123456", - "leftValue": "={{ $json.valid }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 143392, - 61440 - ], - "id": "fe06e747-6740-4ef3-b546-a41d3c20e227", - "name": "Valid Storm Approval Button?" - }, - { - "parameters": { - "operation": "update", - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "matchType": "allConditions", - "filters": { - "conditions": [ - { - "keyValue": "={{ $json.id }}" - } - ] - }, - "columns": { - "mappingMode": "defineBelow", - "value": { - "status": "={{ $json.status }}" - }, - "matchingColumns": [], - "schema": [ - { - "id": "status", - "displayName": "status", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - } - ], - "attemptToConvertTypes": false, - "convertFieldsToString": false - }, - "options": {} - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 144144, - 61440 - ], - "id": "ef6d5666-fe14-43cf-96d5-d4b374bf2aa5", - "name": "Approve Pending Row" - }, - { - "parameters": { - "method": "POST", - "url": "={{ $('Still Valid After Ack?').first().json.slack_response_url }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ (() => {\n const ack = $('Still Valid After Ack?').first().json;\n const isConfirmed = ack.status === 'confirmed';\n const id = String(ack.id ?? '');\n const customer = String(ack.customer_name || 'unknown customer');\n const verb = isConfirmed ? 'approved' : 'rejected';\n return {\n response_type: 'in_channel',\n replace_original: false,\n text: `Storm log #${id} for ${customer} ${verb} and recorded in storm_logs.`,\n };\n})() }}", - "options": {} - }, - "id": "90d9a1c5-10b9-4d9a-baa9-3a2462631e84", - "name": "Notify Slack Recording Result", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [ - 146784, - 61824 - ] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "{}", - "options": {} - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.1, - "position": [ - 143648, - 61440 - ], - "id": "e1e695ba-c94e-4e5b-bb3d-f9d7614035a6", - "name": "Respond Slack Interaction" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "f7e8d9c0-a1b2-4333-c445-d56677889901", - "leftValue": "={{ $json.valid }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 143840, - 61440 - ], - "id": "f9ceff24-2b15-4778-b278-7786ead00f53", - "name": "Still Valid After Ack?" - }, - { - "parameters": { - "assignments": { - "assignments": [ - { - "id": "8a6510f7-a33d-4c87-9472-44329c24e08e", - "name": "testTimestamp", - "value": "=1777824000000", - "type": "number" - } - ] - }, - "options": {} - }, - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - 142976, - 60704 - ], - "id": "542e9778-11be-440d-83f3-ee94d902d6fc", - "name": "Test Configuration" - }, - { - "parameters": { - "jsCode": "\n\n// Grab the data from the Logic Gate node explicitly\nconst stormData = $('Logic Gate').item.json;\n\n// Check if \"Get row(s)\" found anything\nconst existing = $input.all().filter(i => i.json && i.json.id !== undefined && i.json.id !== null);\n\nif (existing.length > 0) {\n // If found, merge and skip\n return [{ json: { ...stormData, skip_insert: true } }];\n}\n\n// If not found, merge and allow insert\nreturn [{ json: { ...stormData, skip_insert: false } }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 145808, - 61008 - ], - "id": "8744897d-3ea2-4cd8-b4eb-a0495f2e7640", - "name": "Prepare Insert Decision" - }, - { - "parameters": { - "jsCode": "const reportItem = $input.first();\nconst customer = $('Loop Over Items').item.json || {};\nconst contactEmail = String(customer.contact_email || '').trim();\nconst fileName = reportItem.binary?.data?.fileName || 'lightning_report.docx';\nconst customerName = reportItem.json.customer_name || customer.customer_name || 'Customer';\n\nreturn [{\n json: {\n ...reportItem.json,\n contact_email: contactEmail,\n customer_id: customer.id,\n email_file_name: fileName,\n customer_name: customerName,\n },\n binary: reportItem.binary,\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 147984, - 61024 - ], - "id": "b50ace64-6a0d-44a6-9803-b4d6e58e6375", - "name": "Prepare Customer Email Context" - }, - { - "parameters": { - "content": "Add these columns to the storm_logs datatable:\n- pending_contact_email (string)\n- pending_email_file_name (string)\n- pending_report_base64 (long text)\n- pending_report_mime_type (string)\n\nReports persist here until approval email is sent or rejected.", - "width": 420 - }, - "type": "n8n-nodes-base.stickyNote", - "position": [ - 147936, - 60672 - ], - "typeVersion": 1, - "id": "cde69b18-d0f9-4a07-b901-17df8a2651a3", - "name": "Sticky Note Storm Logs Email Columns" - }, - { - "parameters": { - "jsCode": "const item = $input.first();\nconst stormLogId = String(item.json.storm_log_id ?? '').trim();\nconst binaryMeta = item.binary?.data || {};\nlet reportBase64 = '';\n\ntry {\n const buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n reportBase64 = buffer.toString('base64');\n} catch {\n const raw = binaryMeta.data;\n if (typeof raw === 'string') {\n reportBase64 = raw.replace(/^data:[^;]+;base64,/, '').replace(/\\s+/g, '');\n } else if (raw) {\n reportBase64 = Buffer.from(raw).toString('base64');\n }\n}\n\nconst persistSkipped = !stormLogId || stormLogId === 'unknown' || !reportBase64;\n\nreturn [{\n json: {\n ...item.json,\n storm_log_id: stormLogId,\n pending_contact_email: item.json.contact_email || '',\n pending_email_file_name: item.json.email_file_name || 'lightning_report.docx',\n pending_report_base64: reportBase64,\n pending_report_mime_type: binaryMeta.mimeType || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n persist_skipped: persistSkipped,\n },\n binary: item.binary,\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 148192, - 61024 - ], - "id": "c6baaa7b-b348-49b6-a854-fb3adfb4c561", - "name": "Build Pending Email Record" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "a9b8c7d6-e5f4-3210-abcd-ef0987654321", - "leftValue": "={{ $('Build Pending Email Record').first().json.persist_skipped }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "false", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 148624, - 61024 - ], - "id": "a6aad493-3757-47c8-bd41-46d38c6fb1cc", - "name": "Can Persist Report?" - }, - { - "parameters": { - "operation": "update", - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "matchType": "allConditions", - "filters": { - "conditions": [ - { - "keyValue": "={{ $('Build Pending Email Record').first().json.storm_log_id }}" - } - ] - }, - "columns": { - "mappingMode": "defineBelow", - "value": { - "pending_contact_email": "={{ $('Build Pending Email Record').first().json.pending_contact_email }}", - "pending_email_file_name": "={{ $('Build Pending Email Record').first().json.pending_email_file_name }}", - "pending_report_base64": "={{ $('Build Pending Email Record').first().json.pending_report_base64 }}", - "pending_report_mime_type": "={{ $('Build Pending Email Record').first().json.pending_report_mime_type }}" - }, - "matchingColumns": [], - "schema": [ - { - "id": "pending_contact_email", - "displayName": "pending_contact_email", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "pending_email_file_name", - "displayName": "pending_email_file_name", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "pending_report_base64", - "displayName": "pending_report_base64", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "pending_report_mime_type", - "displayName": "pending_report_mime_type", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - } - ], - "attemptToConvertTypes": false, - "convertFieldsToString": false - }, - "options": {} - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 148864, - 61024 - ], - "id": "2031d43b-471e-4e91-b639-b826b0de78b7", - "name": "Persist Report To Storm Logs" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", - "leftValue": "={{ $('Still Valid After Ack?').first().json.status }}", - "rightValue": "confirmed", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 144336, - 61712 - ], - "id": "54d38d5a-6cb3-4502-97c0-f9fa38983769", - "name": "Report Approved?" - }, - { - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "matchType": "allConditions", - "filters": { - "conditions": [ - { - "keyValue": "={{ $('Still Valid After Ack?').first().json.id }}" - } - ] - }, - "limit": 1 - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 144608, - 61632 - ], - "id": "61138681-af3b-4c69-9daf-cc4e8778de24", - "name": "Get Pending Report From Storm Logs", - "alwaysOutputData": true - }, - { - "parameters": { - "jsCode": "const ack = $('Still Valid After Ack?').first().json;\nconst stormLogId = String(ack.id ?? '').trim();\nconst rows = $('Get Pending Report From Storm Logs').all()\n .map((item) => item.json)\n .filter((row) => row && row.id !== undefined && row.id !== null);\nconst row = rows[0] || {};\nlet reportBase64 = String(row.pending_report_base64 || '').trim();\nreportBase64 = reportBase64.replace(/^data:[^;]+;base64,/, '').replace(/\\s+/g, '');\nconst hasQueued = reportBase64.length > 0;\n\nreturn [{\n json: {\n ...ack,\n has_queued_report: hasQueued,\n contact_email: String(row.pending_contact_email || '').trim(),\n customer_name: row.customer_name || ack.customer_name,\n email_file_name: row.pending_email_file_name || 'lightning_report.docx',\n pending_report_base64: reportBase64,\n pending_report_mime_type: row.pending_report_mime_type || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n storm_log_id: stormLogId,\n },\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 144832, - 61632 - ], - "id": "51237c29-607f-4965-b955-f9eca7c1cfab", - "name": "Load Queued Customer Report" - }, - { - "parameters": { - "operation": "get", - "dataTableId": { - "__rl": true, - "value": "bMJhm1lTDbCj9eY1", - "mode": "list", - "cachedResultName": "customers", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/bMJhm1lTDbCj9eY1" - }, - "matchType": "allConditions", - "filters": { - "conditions": [ - { - "keyName": "customer_name", - "keyValue": "={{ $('Load Queued Customer Report').item.json.customer_name }}" - } - ] - }, - "limit": 1 - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 145072, - 61632 - ], - "id": "054cb939-b765-4dd6-8bda-89ddbe71ba07", - "name": "Lookup Customer Contact Email", - "alwaysOutputData": true - }, - { - "parameters": { - "jsCode": "const ack = $('Load Queued Customer Report').first().json;\nconst lookupRows = $input.all().map((i) => i.json).filter((r) => r && (r.contact_email || r.id));\nconst lookupEmail = lookupRows.length > 0 ? String(lookupRows[0].contact_email || '').trim() : '';\nconst contactEmail = String(ack.contact_email || '').trim() || lookupEmail;\n\nreturn [{\n json: {\n ...ack,\n contact_email: contactEmail,\n },\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 145280, - 61632 - ], - "id": "909bdb7d-cdad-4ea9-bc47-e52d7de9dbe1", - "name": "Merge Customer Contact Email" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "e5f6a7b8-c9d0-1234-ef01-345678901234", - "leftValue": "={{ $json.contact_email }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - }, - { - "id": "f6a7b8c9-d0e1-2345-f012-456789012346", - "leftValue": "={{ $json.has_queued_report }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 145488, - 61632 - ], - "id": "0bfb34a1-725e-4390-9537-a458ad9e8b19", - "name": "Ready To Email Customer?" - }, - { - "parameters": { - "jsCode": "const item = $input.first();\nconst toEmail = String(item.json.contact_email || '').trim();\nconst fromEmail = 'info@iklim.co';\nconst customerName = String(item.json.customer_name || 'Customer');\nconst fileName = String(item.json.email_file_name || 'lightning_report.docx');\nconst mimeType = item.json.pending_report_mime_type || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';\n\nlet content = String(item.json.pending_report_base64 || '').trim();\ncontent = content.replace(/^data:[^;]+;base64,/, '').replace(/\\s+/g, '');\n\nif (!toEmail) {\n throw new Error('Missing contact_email for SendGrid');\n}\nif (!content) {\n throw new Error('Missing pending_report_base64 for SendGrid attachment');\n}\n\ntry {\n Buffer.from(content, 'base64');\n} catch {\n throw new Error('pending_report_base64 is not valid base64');\n}\n\nreturn [{\n json: {\n ...item.json,\n sendgrid_payload: {\n personalizations: [{ to: [{ email: toEmail }] }],\n from: { email: fromEmail, name: 'iklim.co' },\n subject: `Yıldırım Aktivite Raporu – ${customerName}`,\n content: [{\n type: 'text/html',\n value: `

Sayın yetkili,

${customerName} için yıldırım aktivite raporunuz ektedir.

İyi çalışmalar,
İklim.co

`,\n }],\n attachments: [{\n content,\n filename: fileName,\n type: mimeType,\n disposition: 'attachment',\n }],\n },\n },\n}];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 145600, - 61616 - ], - "id": "f3a4b5c6-d7e8-9012-3456-7890abcdef12", - "name": "Prepare SendGrid Email Payload" - }, - { - "parameters": { - "method": "POST", - "url": "https://api.sendgrid.com/v3/mail/send", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "sendGridApi", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.sendgrid_payload }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.3, - "position": [ - 145712, - 61616 - ], - "id": "df693a02-eda1-4a05-b801-9db5c35ab936", - "name": "Send Customer Report Email", - "credentials": { - "sendGridApi": { - "id": "fhinNHPH2Wnm5fUh", - "name": "SendGrid account" - } - }, - "onError": "continueErrorOutput" - }, - { - "parameters": { - "operation": "update", - "dataTableId": { - "__rl": true, - "value": "PCYhmruG5hu52Alf", - "mode": "list", - "cachedResultName": "storm_logs", - "cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/PCYhmruG5hu52Alf" - }, - "matchType": "allConditions", - "filters": { - "conditions": [ - { - "keyValue": "={{ $('Still Valid After Ack?').first().json.id }}" - } - ] - }, - "columns": { - "mappingMode": "defineBelow", - "value": { - "pending_contact_email": "", - "pending_email_file_name": "", - "pending_report_base64": "", - "pending_report_mime_type": "" - }, - "matchingColumns": [], - "schema": [ - { - "id": "pending_contact_email", - "displayName": "pending_contact_email", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "pending_email_file_name", - "displayName": "pending_email_file_name", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "pending_report_base64", - "displayName": "pending_report_base64", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - }, - { - "id": "pending_report_mime_type", - "displayName": "pending_report_mime_type", - "required": false, - "defaultMatch": false, - "display": true, - "type": "string", - "readOnly": false, - "removed": false - } - ], - "attemptToConvertTypes": false, - "convertFieldsToString": false - }, - "options": {} - }, - "type": "n8n-nodes-base.dataTable", - "typeVersion": 1.1, - "position": [ - 146176, - 61824 - ], - "id": "518b94f2-5d48-4776-9fa9-7236dbfd8eb4", - "name": "Clear Pending Report In Storm Logs" - }, - { - "parameters": { - "jsCode": "const ack = $('Still Valid After Ack?').first().json;\nconst stormLogId = String($json.storm_log_id ?? $json.id ?? ack.id ?? '').trim();\nreturn [{ json: { ...ack, ...$json, storm_log_id: stormLogId || ack.id } }];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 146368, - 61824 - ], - "id": "66146634-8596-4392-ad9a-86cfb489e291", - "name": "Restore Approval Context" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "d0e1f2a3-b4c5-6789-abcd-ef0123456780", - "leftValue": "={{ $('Still Valid After Ack?').first().json.slack_response_url }}", - "rightValue": "", - "operator": { - "type": "string", - "operation": "notEmpty" - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - 146560, - 61824 - ], - "id": "a98e4c0d-2651-4841-86bf-e17a4fb7b73f", - "name": "Has Slack Response URL?" - }, - { - "parameters": { - "select": "channel", - "channelId": { - "__rl": true, - "value": "C0A9K1AC7SN", - "mode": "list", - "cachedResultName": "n8n-events" - }, - "text": "=📧 Lightning report emailed to {{ $('Merge Customer Contact Email').item.json.contact_email }} for *{{ $('Merge Customer Contact Email').item.json.customer_name }}*.", - "otherOptions": { - "includeLinkToWorkflow": false - } - }, - "type": "n8n-nodes-base.slack", - "typeVersion": 2.4, - "position": [ - 146000, - 61600 - ], - "id": "ed9d9a60-4b83-4ed6-bbf5-c2d1bb872f9a", - "name": "Email Sent Notification", - "webhookId": "e1f2a3b4-c5d6-7890-abcd-ef0123456789", - "credentials": { - "slackApi": { - "id": "OKgM8VkM05pJl9kU", - "name": "Tarla Slack Account" - } - } - } - ], - "pinData": {}, - "connections": { - "Login to iklim.co": { - "main": [ - [ - { - "node": "CalculateExpirations", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Error Notification", - "type": "main", - "index": 0 - } - ] - ] - }, - "CalculateExpirations": { - "main": [ - [ - { - "node": "Store Login Credentials", - "type": "main", - "index": 0 - } - ] - ] - }, - "CalculateExpirations1": { - "main": [ - [ - { - "node": "Store Refresh Credentials", - "type": "main", - "index": 0 - } - ] - ] - }, - "Refresh Token": { - "main": [ - [ - { - "node": "CalculateExpirations1", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Error Notification", - "type": "main", - "index": 0 - } - ] - ] - }, - "Restore Credentials": { - "main": [ - [ - { - "node": "Switch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Switch": { - "main": [ - [ - { - "node": "Login to iklim.co", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Refresh Token", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Iterate Customers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Store Login Credentials": { - "main": [ - [ - { - "node": "Restore Credentials", - "type": "main", - "index": 0 - } - ] - ] - }, - "Store Refresh Credentials": { - "main": [ - [ - { - "node": "Restore Credentials", - "type": "main", - "index": 0 - } - ] - ] - }, - "Daily Trigger": { - "main": [ - [ - { - "node": "Test Configuration", - "type": "main", - "index": 0 - } - ] - ] - }, - "Lightning Request": { - "main": [ - [ - { - "node": "Logic Gate", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Request Error Notification", - "type": "main", - "index": 0 - } - ] - ] - }, - "Iterate Customers": { - "main": [ - [ - { - "node": "Loop Over Items", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Customer Wind Turbines": { - "main": [ - [ - { - "node": "Centroid & Distance Ring calculation", - "type": "main", - "index": 0 - } - ] - ] - }, - "Centroid & Distance Ring calculation": { - "main": [ - [ - { - "node": "Loop init", - "type": "main", - "index": 0 - } - ] - ] - }, - "Loop init": { - "main": [ - [ - { - "node": "Calculate Lightning Headers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Logic Gate": { - "main": [ - [ - { - "node": "Stop Loop", - "type": "main", - "index": 0 - } - ] - ] - }, - "Stop Loop": { - "main": [ - [ - { - "node": "Lightning found?", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Calculate Lightning Headers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Lightning found?": { - "main": [ - [ - { - "node": "Get row(s)", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Loop Over Items", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get row(s)": { - "main": [ - [ - { - "node": "Prepare Insert Decision", - "type": "main", - "index": 0 - } - ] - ] - }, - "If": { - "main": [ - [ - { - "node": "Insert row", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Loop Over Items", - "type": "main", - "index": 0 - } - ] - ] - }, - "Insert row": { - "main": [ - [ - { - "node": "Calculate Thunderstorm Headers", - "type": "main", - "index": 0 - } - ] - ] - }, - "Loop Over Items": { - "main": [ - [], - [ - { - "node": "Get Customer Wind Turbines", - "type": "main", - "index": 0 - } - ] - ] - }, - "Calculate Thunderstorm Headers": { - "main": [ - [ - { - "node": "HTTP Request", - "type": "main", - "index": 0 - } - ] - ] - }, - "Calculate Lightning Headers": { - "main": [ - [ - { - "node": "Lightning Request", - "type": "main", - "index": 0 - } - ] - ] - }, - "HTTP Request": { - "main": [ - [ - { - "node": "Build Report Payload", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Report Payload": { - "main": [ - [ - { - "node": "Generate Report", - "type": "main", - "index": 0 - } - ] - ] - }, - "Generate Report": { - "main": [ - [ - { - "node": "Prepare DOCX Metadata", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare DOCX Metadata": { - "main": [ - [ - { - "node": "Get Storm Logs For Next Id", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Storm Logs For Next Id": { - "main": [ - [ - { - "node": "Resolve Storm Log Id", - "type": "main", - "index": 0 - } - ] - ] - }, - "Resolve Storm Log Id": { - "main": [ - [ - { - "node": "Prepare Customer Email Context", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Customer Email Context": { - "main": [ - [ - { - "node": "Build Pending Email Record", - "type": "main", - "index": 0 - } - ] - ] - }, - "Build Pending Email Record": { - "main": [ - [ - { - "node": "Upload Report to Slack", - "type": "main", - "index": 0 - } - ] - ] - }, - "Upload Report to Slack": { - "main": [ - [ - { - "node": "Can Persist Report?", - "type": "main", - "index": 0 - } - ] - ] - }, - "Can Persist Report?": { - "main": [ - [ - { - "node": "Persist Report To Storm Logs", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Approval Buttons", - "type": "main", - "index": 0 - } - ] - ] - }, - "Persist Report To Storm Logs": { - "main": [ - [ - { - "node": "Send Approval Buttons", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send Approval Buttons": { - "main": [ - [ - { - "node": "Loop Over Items", - "type": "main", - "index": 0 - } - ] - ] - }, - "Slack Interaction Webhook": { - "main": [ - [ - { - "node": "Parse Slack Button Payload", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Slack Button Payload": { - "main": [ - [ - { - "node": "Valid Storm Approval Button?", - "type": "main", - "index": 0 - } - ] - ] - }, - "Valid Storm Approval Button?": { - "main": [ - [ - { - "node": "Respond Slack Interaction", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Respond Slack Interaction", - "type": "main", - "index": 0 - } - ] - ] - }, - "Respond Slack Interaction": { - "main": [ - [ - { - "node": "Still Valid After Ack?", - "type": "main", - "index": 0 - } - ] - ] - }, - "Still Valid After Ack?": { - "main": [ - [ - { - "node": "Approve Pending Row", - "type": "main", - "index": 0 - } - ] - ] - }, - "Approve Pending Row": { - "main": [ - [ - { - "node": "Report Approved?", - "type": "main", - "index": 0 - } - ] - ] - }, - "Report Approved?": { - "main": [ - [ - { - "node": "Get Pending Report From Storm Logs", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Clear Pending Report In Storm Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Pending Report From Storm Logs": { - "main": [ - [ - { - "node": "Load Queued Customer Report", - "type": "main", - "index": 0 - } - ] - ] - }, - "Load Queued Customer Report": { - "main": [ - [ - { - "node": "Lookup Customer Contact Email", - "type": "main", - "index": 0 - } - ] - ] - }, - "Lookup Customer Contact Email": { - "main": [ - [ - { - "node": "Merge Customer Contact Email", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Customer Contact Email": { - "main": [ - [ - { - "node": "Ready To Email Customer?", - "type": "main", - "index": 0 - } - ] - ] - }, - "Ready To Email Customer?": { - "main": [ - [ - { - "node": "Prepare SendGrid Email Payload", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Clear Pending Report In Storm Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare SendGrid Email Payload": { - "main": [ - [ - { - "node": "Send Customer Report Email", - "type": "main", - "index": 0 - } - ] - ] - }, - "Send Customer Report Email": { - "main": [ - [ - { - "node": "Email Sent Notification", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Clear Pending Report In Storm Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Email Sent Notification": { - "main": [ - [ - { - "node": "Clear Pending Report In Storm Logs", - "type": "main", - "index": 0 - } - ] - ] - }, - "Clear Pending Report In Storm Logs": { - "main": [ - [ - { - "node": "Restore Approval Context", - "type": "main", - "index": 0 - } - ] - ] - }, - "Restore Approval Context": { - "main": [ - [ - { - "node": "Has Slack Response URL?", - "type": "main", - "index": 0 - } - ] - ] - }, - "Has Slack Response URL?": { - "main": [ - [ - { - "node": "Notify Slack Recording Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Test Configuration": { - "main": [ - [ - { - "node": "Restore Credentials", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Insert Decision": { - "main": [ - [ - { - "node": "If", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": false, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate" - }, - "versionId": "c793524f-e29c-4297-887b-2c791353f895", - "meta": { - "instanceId": "15c4ff3a74619031c77894fe5fb8c0fd585362ef637b1873abd56a139f543e12" - }, - "id": "0GaXSvJtxW6JR6hv", - "tags": [] -} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..04c525b --- /dev/null +++ b/README.md @@ -0,0 +1,217 @@ +# Lightning Report (n8n) + +Automated lightning activity reports for wind farms. An **n8n** workflow detects strikes via the [iklim.co](https://iklim.co) API, generates a Word report through a **Python report service**, routes it through **Slack** for human approval, and emails the approved report to customers via **SendGrid**. + +## Architecture + +```text +┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ +│ n8n workflow│────▶│ iklim.co API │ │ Report service │ +│ (scheduled) │ │ lightnings + │ │ FastAPI /generate │ +│ │ │ thunderstorms │ │ → DOCX │ +└──────┬──────┘ └──────────────────┘ └──────────▲──────────┘ + │ │ + │ Slack upload + approve/reject buttons │ + ▼ │ +┌─────────────┐ storm_logs datatable (pending report) │ +│ Slack │───────────────────────────────────────────┘ +└──────┬──────┘ + │ on approve + ▼ +┌─────────────┐ +│ SendGrid │──▶ customer contact_email +└─────────────┘ +``` + +## Repository layout + +| Path | Description | +|------|-------------| +| `Lightning_Report_Automatic.json` | n8n workflow export (import in n8n) | +| `report_service/` | FastAPI wrapper around report generation | +| `report_service/adapter.py` | Maps n8n JSON payload → pandas DataFrames | +| `src/reporting/docx.py` | Main DOCX builder | +| `src/analysis/` | Risk, histogram, geospatial, statistics | +| `src/visualization/` | Maps and storm-cell plots | +| `src/config.py` | Default analysis parameters | + +## Prerequisites + +- **Python 3.12+** (local or Docker) +- **n8n** (self-hosted, with Data Tables enabled) +- **iklim.co API** credentials (test: `https://api-test.iklim.co`) +- **Slack** app/bot for uploads and approval buttons +- **SendGrid** API key with Mail Send permission +- Optional: **Gemini API key** if commentary is generated inside the report service (otherwise pass `gemini_text` from n8n) + +## Report service (local) + +### 1. Install dependencies + +```bash +cd Lightning_Report_n8n +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt -r report_service/requirements.txt +``` + +### 2. Environment variables + +Create a `.env` file in the project root (never commit it): + +```env +REPORT_SERVICE_TOKEN=your-long-random-secret +GEMINI_API_KEY=optional-if-not-supplied-by-n8n +GEMINI_MODEL=gemini-2.5-flash-lite +LOG_LEVEL=INFO +``` + +`REPORT_SERVICE_TOKEN` is required. The service refuses `/generate` if it is unset. + +### 3. Run the server + +```bash +export REPORT_SERVICE_TOKEN="your-long-random-secret" +uvicorn report_service.main:app --host 0.0.0.0 --port 8000 +``` + +Health check: `GET http://localhost:8000/health` + +### 4. Expose to n8n (development) + +If n8n runs elsewhere, tunnel the service: + +```bash +ngrok http 8000 +``` + +Point the n8n **Generate Report** HTTP Request node at `https:///generate` and set header `X-Report-Token` to the same value as `REPORT_SERVICE_TOKEN`. + +## Report service (Docker) + +```bash +cd Lightning_Report_n8n +export REPORT_SERVICE_TOKEN=your-long-random-secret +docker compose -f report_service/docker-compose.yml up --build -d +``` + +From n8n on the same Docker network: `http://report-service:8000/generate` + +## API + +### `GET /health` + +Liveness probe. Returns `{"ok": true, ...}`. + +### `POST /generate` + +| Header | Required | Description | +|--------|----------|-------------| +| `X-Report-Token` | Yes | Must match `REPORT_SERVICE_TOKEN` | +| `Content-Type` | Yes | `application/json` | + +**Body** (built by n8n **Build Report Payload** node): + +| Field | Description | +|-------|-------------| +| `customer_name` | Wind farm / customer label | +| `t_start`, `t_end` | Storm window (epoch ms) | +| `centroid_lat`, `centroid_lon` | Farm centroid | +| `boundary_m` | Monitoring radius (m) | +| `rings` | `{ r1, r2, r3, r4 }` distance rings | +| `timezone` | e.g. `Europe/Istanbul` | +| `turbines` | Array of turbine objects | +| `strikes` | Lightning strike records (`captured` ISO time preferred) | +| `storm_records` | Thunderstorm polygons from `/v1/thunderstorms/within` | +| `gemini_text` | Optional pre-generated commentary | + +**Response:** DOCX file (`application/vnd.openxmlformats-officedocument.wordprocessingml.document`). + +## n8n workflow + +Import `Lightning_Report_Automatic.json` into n8n and configure: + +### Credentials + +| Integration | Used for | +|-------------|----------| +| iklim.co login | `Login to iklim.co` / token refresh | +| Slack (`n8n_lightning_report_bot`) | Report upload, approval buttons | +| Slack (`Tarla Slack Account`) | Error / email-sent notifications | +| SendGrid | Customer email after approval | + +### Data tables + +**`customers`** + +| Column | Purpose | +|--------|---------| +| `customer_name` | Display name | +| `contact_email` | SendGrid recipient | +| `id` | Linked from wind turbines | + +**`wind_turbine_farm`** + +| Column | Purpose | +|--------|---------| +| `customer_id` | FK to customers | +| `latitude`, `longitude` | Turbine positions | +| `name` | Turbine label | + +**`storm_logs`** + +| Column | Purpose | +|--------|---------| +| `customer_name`, `storm_key`, `storm_start`, `storm_end` | Storm metadata | +| `total_strikes`, `status` | Count; `waiting for confirmation` → `confirmed` / `rejected` | +| `pending_contact_email` | Email snapshot at report time | +| `pending_email_file_name` | DOCX filename | +| `pending_report_base64` | Queued report (survives n8n restarts) | +| `pending_report_mime_type` | MIME type for attachment | + +### Main flow (simplified) + +1. Daily trigger → authenticate → loop customers. +2. Query lightnings within farm boundary; detect storm window. +3. Insert/update `storm_logs`, fetch thunderstorms, build payload. +4. **Generate Report** → upload DOCX to Slack → approval buttons. +5. Persist pending report fields on `storm_logs`. +6. On Slack **Approve** → load queued report → **SendGrid** email → clear pending fields. + +Email is sent only after approval, not when the report is first generated. + +## Troubleshooting + +### `channel_not_found` (Slack) + +The Slack channel ID in the node does not exist or the bot is not invited. Re-select the channel in n8n and run `/invite @YourBot` in that channel. + +### `Account Is not Found For User …` (`/v1/thunderstorms/within`) + +Login succeeded but the iklim user has no **account** on the API environment (e.g. test). Ask iklim.co to provision the user, or use a service account that has thunderstorm access. Lightning-only reports can continue if the workflow skips failed thunderstorm calls (optional branch). + +### Histogram shows `01-01-1970` / wrong period + +Strike timestamps were parsed incorrectly when `timestamp` (epoch ms) was preferred over `captured` (ISO). Fixed in `report_service/adapter.py` — restart the report service after pulling updates. + +### SendGrid: attachment must be base64 encoded + +Use the **Prepare SendGrid Email Payload** node path; ensure `pending_report_base64` in `storm_logs` is populated and not truncated by datatable size limits. + +### Binary missing at **Upload Report to Slack** + +The datatable persist step must run **after** Slack upload, not before (persist nodes drop binary data). + +### Slack `invalid_payload` on approval follow-up + +Do not double-`JSON.stringify` the body when posting to `response_url`. Slack response URLs expire after ~30 minutes (optional confirmation message only). + +## Development notes + +- Per-farm settings (`rings`, centroid, dates) come from the n8n payload via `apply_farm_config()`, not hardcoded in `src/config.py`. +- Prefer strike field **`captured`** (ISO) for `local_time`; numeric **`timestamp`** is supported as epoch milliseconds. +- HMAC signing for iklim API calls is implemented in n8n Code nodes (`Calculate Lightning Headers`, `Calculate Thunderstorm Headers`). + +## License + +Internal use — iklim.co lightning reporting pipeline.