BE-LightningReport/Lightning_Report_Automatic.json

3217 lines
104 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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\nReport generation uses async polling nodes (Start Async Report -> Wait -> Poll -> Download). Update ngrok base URL in those HTTP nodes when the tunnel URL changes.\n",
"height": 128,
"width": 512
},
"type": "n8n-nodes-base.stickyNote",
"position": [
193024,
78016
],
"typeVersion": 1,
"id": "fabfab42-5146-4bcd-9638-14129e096bc1",
"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": "ccb6966e-72fc-4b64-abae-722f5d631953",
"name": "Login to iklim.co",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
191824,
77104
],
"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": [
192064,
77088
],
"id": "01027f48-993a-401b-80fb-4361c13938a5",
"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": [
192128,
77344
],
"id": "fa0b49ea-3b74-48c7-8a4b-3eefcfc8f028",
"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": "951348af-1bf8-4302-a9e9-dd2e25b7f21d",
"name": "Refresh Token",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
191824,
77360
],
"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": [
191600,
77328
],
"id": "0e162c28-007a-4652-8225-7d424ceee8db",
"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": [
191408,
77344
],
"id": "f4a61331-25fc-496f-8a5e-5bfcf638ee32",
"name": "Restore Credentials",
"retryOnFail": false
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nstaticData.authRetryAttempt = 0;\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\nreturn $input.all();"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
192352,
77344
],
"id": "8f464ccf-9a38-4ebc-a3d0-f5b81cee8747",
"name": "Store Refresh Credentials"
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nstaticData.authRetryAttempt = 0;\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\nreturn $input.all();"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
192272,
77088
],
"id": "b7da4601-8cab-420f-8ac2-5a9065f7f208",
"name": "Store Login Credentials"
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "C0A9K1AC7SN",
"mode": "list",
"cachedResultName": "n8n-events"
},
"text": "=🛑 {{ $workflow.name }} — yetkilendirme başarısız ({{ $json.authRetryAttempt }}/3 deneme)\n*Hata:* {{ $json.last_error_message || $json.error?.message || $json.message }}",
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"type": "n8n-nodes-base.slack",
"typeVersion": 2.4,
"position": [
192432,
76736
],
"id": "6596c8b9-dd8b-4990-b626-62e21460a5fa",
"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": "10b39b14-3bc2-447f-9c4e-98bd56cd24b6",
"name": "Daily Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
190864,
77328
]
},
{
"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": "91371278-11d3-4a5e-a3bc-6a7d72381261",
"name": "Lightning Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
192928,
77616
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "bMJhm1lTDbCj9eY1",
"mode": "list",
"cachedResultName": "customers",
"cachedResultUrl": "/projects/dWFeIPZ8ouir7Ex0/datatables/bMJhm1lTDbCj9eY1"
},
"limit": 10
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
191408,
77600
],
"id": "06b3f62c-030f-4c2b-9d30-cd0a1dba1c70",
"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 }}"
}
]
},
"returnAll": true
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
192144,
77616
],
"id": "2c7393ba-1f5b-443d-9457-37cd1b07697e",
"name": "Get Customer Wind Turbines"
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "C0A9K1AC7SN",
"mode": "list",
"cachedResultName": "n8n-events"
},
"text": "=🛑 {{ $workflow.name }} — iş akışı durduruldu ({{ $json.lightningRetryAttempt }}/3 deneme başarısız)\n*Müşteri:* {{ $json.customer_name }}\n*Türbin:* {{ $json.turbine_name }}\n*Hata:* {{ $json.last_error_message || $json.error?.message || $json.message }}",
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"type": "n8n-nodes-base.slack",
"typeVersion": 2.4,
"position": [
193568,
77184
],
"id": "286e6469-bcf1-4430-8098-680a3cf4cfa7",
"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 MAX_ATTEMPTS = 3;\nconst farm = $('Loop init').all().pop().json;\nconst errorPayload = $input.first().json || {};\nconst errorMessage = errorPayload.error?.message || errorPayload.message || JSON.stringify(errorPayload);\n\nlet turbineName = 'N/A';\ntry {\n turbineName = $('Get Customer Wind Turbines').first().json.name || turbineName;\n} catch (e) {}\n\nlet previousAttempt = 0;\ntry {\n const prev = $('Lightning Retry Handler').all().pop().json;\n if (prev?.lightningRetryAttempt != null) {\n previousAttempt = Number(prev.lightningRetryAttempt);\n }\n} catch (e) {}\n\nconst attempt = previousAttempt + 1;\n\nif (attempt < MAX_ATTEMPTS) {\n return [{\n json: {\n ...farm,\n lightningRetryAttempt: attempt,\n retry_lightning: true,\n pageNumber: farm.pageNumber ?? 0,\n last_error_message: errorMessage,\n turbine_name: turbineName,\n },\n }];\n}\n\nreturn [{\n json: {\n ...farm,\n lightningRetryAttempt: attempt,\n retry_lightning: false,\n last_error_message: errorMessage,\n turbine_name: turbineName,\n error: errorPayload.error || errorPayload,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
193152,
77184
],
"id": "16593a5b-ce44-4e21-a3c3-63f4968ea399",
"name": "Lightning Retry Handler"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "f5a6b7c8-d9e0-4123-f456-667788990011",
"leftValue": "={{ $json.retry_lightning }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
193360,
77184
],
"id": "ca073d74-cb75-4304-b5f4-f7e0dc1df666",
"name": "Retry Lightning Request?"
},
{
"parameters": {
"amount": 30
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
193568,
77360
],
"id": "d966bbdc-91a1-40d5-a302-0c90fa0ee8be",
"name": "Wait Before Retry",
"webhookId": "b7c8d9e0-f1a2-4345-b678-889900112233"
},
{
"parameters": {
"errorMessage": "=Lightning API failed after 3 attempts for {{ $json.customer_name }}: {{ $json.last_error_message || $json.error?.message || 'unknown error' }}"
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
193744,
77184
],
"id": "6c5f3fd7-ca6e-4dd1-a026-fe7ad6d4e855",
"name": "Stop Workflow"
},
{
"parameters": {
"jsCode": "const MAX_ATTEMPTS = 3;\nconst errorPayload = $input.first().json || {};\nconst errorMessage = errorPayload.error?.message || errorPayload.message || JSON.stringify(errorPayload);\nconst staticData = $getWorkflowStaticData('global');\n\nlet previousAttempt = 0;\ntry {\n const prev = $('Auth Retry Handler').all().pop().json;\n if (prev?.authRetryAttempt != null) {\n previousAttempt = Number(prev.authRetryAttempt);\n }\n} catch (e) {\n previousAttempt = Number(staticData.authRetryAttempt || 0);\n}\n\nconst attempt = previousAttempt + 1;\nstaticData.authRetryAttempt = attempt;\n\nif (attempt < MAX_ATTEMPTS) {\n return [{\n json: {\n retry_auth: true,\n authRetryAttempt: attempt,\n last_error_message: errorMessage,\n },\n }];\n}\n\nreturn [{\n json: {\n retry_auth: false,\n authRetryAttempt: attempt,\n last_error_message: errorMessage,\n error: errorPayload.error || errorPayload,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
192048,
76736
],
"id": "97c051a3-e7f9-4f55-882e-802e3c3ad936",
"name": "Auth Retry Handler"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "a9b8c7d6-e5f4-3210-abcd-auth000002",
"leftValue": "={{ $json.retry_auth }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
192240,
76736
],
"id": "125b55e9-5463-4497-876e-a6519cb752cd",
"name": "Retry Auth Request?"
},
{
"parameters": {
"amount": 30
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
192432,
76896
],
"id": "c1bc4b5b-8f19-4718-a899-d688d492bbec",
"name": "Wait Before Auth Retry",
"webhookId": "c8d9e0f1-a2b3-4345-b678-889900112244"
},
{
"parameters": {
"errorMessage": "=iklim.co authentication failed after 3 attempts: {{ $json.last_error_message || $json.error?.message || 'unknown error' }}"
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
192608,
76736
],
"id": "f8efb8f0-88bb-4b84-99b2-0a85f39262ff",
"name": "Stop Workflow Auth"
},
{
"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: ($('Loop Over Items').first().json || {}).customer_name,\n language: String(($('Loop Over Items').first().json || {}).language || 'en').trim() || 'en',\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": [
192352,
77616
],
"id": "2e1f09a2-33a8-47f4-81a7-84325996554f",
"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 lightningRetryAttempt: 0\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
192544,
77616
],
"id": "f236d46d-7e7a-47f1-8525-d3de2b4c2335",
"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 stopLoop = false;\n\nif (currentPage > 0) {\n try {\n const prevState = $('Logic Gate').all().pop().json;\n allStrikes = prevState.allStrikes ? [...prevState.allStrikes] : [];\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\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 stopLoop = true;\n break;\n }\n }\n}\n\nif (batchStrikes.length < 100) {\n stopLoop = true;\n}\n\nconst timestamps = allStrikes.map((s) => s.timestamp).filter((ts) => Number.isFinite(ts));\nconst final_tStart = timestamps.length > 0 ? Math.min(...timestamps) : null;\nconst final_tLast = timestamps.length > 0 ? Math.max(...timestamps) : null;\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": [
193168,
77616
],
"id": "52d6204a-a965-45dd-b789-fca4189d153f",
"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": [
193360,
77568
],
"id": "34b7b435-2c56-4e47-a62c-e4841ebaf7db",
"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": [
193552,
77600
],
"id": "0c2fd4a8-325a-43d1-b496-0a93e67d1052",
"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": [
193792,
77632
],
"id": "a0014ff6-7642-4a30-b55b-90393ff111e7",
"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": [
194176,
77632
],
"id": "84d5aefa-ff56-411e-88d8-03dbb1db625a",
"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": [
194416,
77632
],
"id": "12c7498f-0f3e-4aab-b4c9-ac464044e988",
"name": "Insert row"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
191904,
77600
],
"id": "f63756e6-684e-47a1-871b-0edcbb873cf9",
"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": [
194640,
77632
],
"id": "e0338883-8932-461a-b46e-e987deadade6",
"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": [
194832,
77632
],
"id": "bc871f9d-3b59-43d3-b97c-7fec3e6c8b3d",
"name": "Thunderstorm Request",
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const item = $input.first();\nconst error = item.json.error || {};\n\nconst extractErrorMessage = (payload) => {\n const err = payload.error || {};\n const candidates = [\n payload.last_error_message,\n payload.errorMessage,\n err.message,\n payload.message,\n ];\n for (const candidate of candidates) {\n if (!candidate) continue;\n const text = String(candidate);\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}$/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]);\n if (parsed.message) return String(parsed.message);\n } catch (e) {}\n }\n return text;\n }\n return 'Thunderstorm API request failed';\n};\n\nconst rawMessage = extractErrorMessage(item.json);\nconst isAccountMissing = /account is not found/i.test(String(rawMessage));\nconst reason = isAccountMissing\n ? 'Thunderstorm API account is not provisioned for this user on api-test.iklim.co'\n : String(rawMessage);\n\nreturn [{\n json: {\n thunderstorms: [],\n thunderstorm_fetch_skipped: true,\n thunderstorm_fetch_error: reason,\n thunderstorm_retry_attempts: item.json.thunderstormRetryAttempt || null,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
195456,
77664
],
"id": "edeff220-abc6-4194-ab43-2341a48294fc",
"name": "Thunderstorm Error Fallback"
},
{
"parameters": {
"jsCode": "const MAX_ATTEMPTS = 3;\nconst errorPayload = $input.first().json || {};\nconst error = errorPayload.error || {};\n\nconst extractErrorMessage = (payload) => {\n const err = payload.error || {};\n const candidates = [\n payload.last_error_message,\n payload.errorMessage,\n err.message,\n payload.message,\n ];\n for (const candidate of candidates) {\n if (!candidate) continue;\n const text = String(candidate);\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}$/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]);\n if (parsed.message) return String(parsed.message);\n } catch (e) {}\n }\n return text;\n }\n return 'Thunderstorm API request failed';\n};\n\nconst rawMessage = extractErrorMessage(errorPayload);\n\nlet previousAttempt = 0;\ntry {\n const prev = $('Thunderstorm Retry Handler').all().pop().json;\n if (prev?.thunderstormRetryAttempt != null) {\n previousAttempt = Number(prev.thunderstormRetryAttempt);\n }\n} catch (e) {}\n\nconst attempt = previousAttempt + 1;\n\nif (attempt < MAX_ATTEMPTS) {\n return [{\n json: {\n thunderstormRetryAttempt: attempt,\n retry_thunderstorm: true,\n last_error_message: rawMessage,\n },\n }];\n}\n\nreturn [{\n json: {\n thunderstormRetryAttempt: attempt,\n retry_thunderstorm: false,\n last_error_message: rawMessage,\n error: errorPayload.error || errorPayload,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
195056,
77440
],
"id": "ef38ff2f-9fb5-4c6d-8a17-f4cf7b466b9b",
"name": "Thunderstorm Retry Handler"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "e5f6a7b8-c9d0-4123-e456-7890abcdef01",
"leftValue": "={{ $json.retry_thunderstorm }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
195248,
77440
],
"id": "6d09cfcc-5a15-4c84-b08e-5415e0635458",
"name": "Retry Thunderstorm Request?"
},
{
"parameters": {
"amount": 30
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
195472,
77440
],
"id": "6f1a53f9-ed19-449a-8f52-fa2545a5f3dd",
"name": "Wait Before Thunderstorm Retry",
"webhookId": "c8d9e0f1-a2b3-4456-c789-0123456789ab"
},
{
"parameters": {
"jsCode": "const crypto = require('crypto');\nconst HMAC_SECRET = 'c88f845bd6d520ded507ef6b02efc223019ccf68f41d9070705712d480ba5166';\nconst METHOD = 'POST';\nconst URI = '/v1/lightnings/within';\n\nconst testConfig = $('Test Configuration').first().json;\nconst endTime = Number(testConfig.testTimestamp);\n\nconst staticData = $getWorkflowStaticData('global');\nconst accessToken = staticData.accessToken;\nif (!accessToken) {\n throw new Error('Missing accessToken in workflow static data');\n}\n\nconst farmBase = $('Loop init').all().pop().json;\nconst allItems = $input.all();\nconst processedItems = [];\n\nfor (const item of allItems) {\n const farmData = { ...farmBase, ...item.json };\n const pageNumber = farmData.pageNumber ?? 0;\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,\n backwardInterval: 43200,\n endTimeEpoch: endTime,\n 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 ' + accessToken,\n 'Content-Type': 'application/json',\n },\n },\n });\n}\n\nreturn processedItems;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
192736,
77616
],
"id": "15e7469c-1d42-4fee-8a7e-22df6093b7b3",
"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;\n\nconst unwrapThunderstorms = (items) => {\n const records = [];\n for (const item of items) {\n if (!item || item.thunderstorm_fetch_skipped) {\n continue;\n }\n if (Array.isArray(item.thunderstorms)) {\n records.push(...item.thunderstorms);\n continue;\n }\n if (Array.isArray(item.cells)) {\n records.push(...item.cells);\n continue;\n }\n if (Array.isArray(item.data)) {\n records.push(...item.data);\n continue;\n }\n if (Array.isArray(item.items)) {\n records.push(...item.items);\n continue;\n }\n records.push(item);\n }\n return records;\n};\n\nconst thunder = unwrapThunderstorms($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\nlet customer = {};\ntry {\n customer = $('Loop Over Items').first().json || {};\n} catch (e) {\n customer = {};\n}\nconst language = String(customer.language || farm.language || 'en').trim() || 'en';\n\nreturn {\n customer_name: storm.customer_name,\n storm_log_id: stormLogId,\n language,\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": [
195648,
77776
],
"id": "7ce4bd24-6888-41c8-9822-132b43843d6d",
"name": "Build Report Payload"
},
{
"parameters": {
"method": "POST",
"url": "https://patrol-plural-gooey.ngrok-free.dev/generate/async",
"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": {
"timeout": 120000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
195856,
77632
],
"id": "a9405d4d-c78c-4b13-8d0c-b8abe9d1bf1c",
"name": "Start Async Report",
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const payload = $('Build Report Payload').first().json;\nconst start = $input.first().json || {};\nif (!start.job_id) {\n throw new Error('Async report start did not return job_id');\n}\nreturn [{\n json: {\n ...payload,\n report_job_id: start.job_id,\n poll_attempt: 0,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
196064,
77632
],
"id": "a7c59ab1-f1ef-48aa-b289-3ea32e576a46",
"name": "Attach Report Job"
},
{
"parameters": {
"amount": 15
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
196272,
77632
],
"id": "5722c248-b22d-4468-aa82-a153196623cb",
"name": "Wait for Report",
"webhookId": "d4e5f6a7-b8c9-4012-d345-6789abcdef01"
},
{
"parameters": {
"url": "=https://patrol-plural-gooey.ngrok-free.dev/generate/async/{{ $json.report_job_id }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Report-Token",
"value": "test-secret-123"
},
{
"name": "ngrok-skip-browser-warning",
"value": "1"
}
]
},
"options": {
"timeout": 120000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
196480,
77632
],
"id": "fa4a2f64-0eca-4e37-9b99-f9a6a0d70c95",
"name": "Poll Report Status",
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const ctx = $('Wait for Report').item.json;\nconst poll = $input.first().json || {};\nreturn [{\n json: {\n ...ctx,\n poll_status: poll.status,\n poll_error: poll.error || null,\n poll_filename: poll.filename || null,\n poll_attempt: Number(ctx.poll_attempt || 0) + 1,\n },\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
196688,
77632
],
"id": "27eaf27f-b51e-49fe-a6a1-95be4aacca39",
"name": "Merge Poll Status"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "f5a6b7c8-d9e0-4123-a456-6677889900aa",
"leftValue": "={{ $json.poll_status }}",
"rightValue": "complete",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
196896,
77632
],
"id": "fd307ad6-a3bd-4bfe-b7ca-3fb35edb1597",
"name": "Report Complete?"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "f5a6b7c8-d9e0-4123-a456-6677889900ac",
"leftValue": "={{ $json.poll_status }}",
"rightValue": "failed",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
197088,
77296
],
"id": "b982bc43-73b3-4072-9482-97fb07782f0e",
"name": "Report Failed?"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "f5a6b7c8-d9e0-4123-a456-6677889900ae",
"leftValue": "={{ $json.poll_attempt }}",
"rightValue": 60,
"operator": {
"type": "number",
"operation": "lt"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
197312,
77344
],
"id": "f0cdea17-dc81-424e-88c8-4824ea444ba5",
"name": "Continue Polling?"
},
{
"parameters": {
"errorMessage": "=Report generation failed: {{ $json.poll_error || 'unknown error' }}"
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
197296,
77152
],
"id": "a0a1f9ee-b5de-4421-8aab-cca896ab68e8",
"name": "Stop Report Failed"
},
{
"parameters": {
"errorMessage": "=Report generation timed out after {{ $json.poll_attempt || 60 }} poll attempts"
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
197520,
77360
],
"id": "57914e12-4e59-4f70-abbf-fc4aa3d6296b",
"name": "Stop Report Timeout"
},
{
"parameters": {
"url": "=https://patrol-plural-gooey.ngrok-free.dev/generate/async/{{ $json.report_job_id }}/download",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Report-Token",
"value": "test-secret-123"
},
{
"name": "ngrok-skip-browser-warning",
"value": "1"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "file"
}
},
"timeout": 180000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
197120,
77616
],
"id": "3d18ff22-b4c8-4929-bd4b-99959efb22fc",
"name": "Download 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": [
197360,
77600
],
"id": "d88489d3-d04c-4dff-bdbe-c56d4a013324",
"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": [
197568,
77600
],
"id": "3992f130-8176-4823-861f-7d8502bd14b0",
"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": [
197776,
77600
],
"id": "43bc1189-a887-4714-a901-86ac901ab575",
"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": [
198352,
77600
],
"id": "f60d271c-bc14-4582-9952-258c1b1ef90f",
"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": [
198928,
77744
],
"id": "5b019c4e-be55-497c-9faa-00fee5a6da55",
"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": [
191072,
78064
],
"id": "93d76d11-0765-4ee4-b6fa-26395ba494b4",
"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": [
191312,
78064
],
"id": "bc708cdf-95ca-4267-8317-2e2ed47e4560",
"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": [
191552,
78064
],
"id": "b7de0d16-5523-4739-9460-94049d67d027",
"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": [
192304,
78064
],
"id": "2419265f-4bef-4188-9f7a-3ac8ed5db1b5",
"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": "d6d0ea81-9340-49f6-b4e9-69d01f7fb809",
"name": "Notify Slack Recording Result",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
195360,
78448
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
191808,
78064
],
"id": "463d8f11-0df9-437d-af8e-2801e6bf0d45",
"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": [
192000,
78064
],
"id": "e7f3c857-f069-4671-82e7-f7305d0c7d8f",
"name": "Still Valid After Ack?"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "8a6510f7-a33d-4c87-9472-44329c24e08e",
"name": "testTimestamp",
"value": "=1779899738000",
"type": "number"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
191136,
77328
],
"id": "fa1a4e65-dcfe-422a-afe9-099f05c5fbd9",
"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": [
193968,
77632
],
"id": "359d57ae-d661-4062-a43c-628e3df7e084",
"name": "Prepare Insert Decision"
},
{
"parameters": {
"jsCode": "const reportItem = $input.first();\nlet customer = {};\ntry {\n customer = $('Loop Over Items').first().json || {};\n} catch (e) {\n customer = {};\n}\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": [
197968,
77600
],
"id": "701bb193-2c94-42b4-8b39-8b9ae4845ac0",
"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": [
192256,
78640
],
"typeVersion": 1,
"id": "78313ea9-b9b8-4b95-9a7e-69ab97401db6",
"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": [
198160,
77600
],
"id": "917032b0-eb08-41c1-abca-4ab2c4ec2638",
"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": [
198544,
77600
],
"id": "bd377b9d-6867-4940-abe3-f0f0bf07c80d",
"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": [
198784,
77584
],
"id": "6d9be5f6-488a-4c36-8707-6eb19710b9c7",
"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": [
192496,
78400
],
"id": "43a12b59-0bcf-4a77-9be6-0c748a05679e",
"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": [
192768,
78256
],
"id": "94c73441-3c4f-4ab7-aed5-97ebaccd1dbd",
"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": [
192992,
78256
],
"id": "b56db58b-07f6-49b1-b98f-63f6a1dc865c",
"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": [
193232,
78256
],
"id": "560f795a-6879-4fb9-a5fa-83529679663a",
"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": [
193440,
78256
],
"id": "6526f28e-d6ea-4556-9482-057f11d8b443",
"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": [
193648,
78256
],
"id": "89da9171-185e-4bd2-bddf-df1a3254a71b",
"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: `<p>Sayın yetkili,</p><p><strong>${customerName}</strong> için yıldırım aktivite raporunuz ektedir.</p><p>İyi çalışmalar,<br>İklim.co</p>`,\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": [
193888,
78240
],
"id": "37e3b05a-a1b8-4f6f-90af-dad1574e713e",
"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": [
194128,
78240
],
"id": "fb46dc09-9b35-4e41-afc0-026209d9559c",
"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": [
194752,
78448
],
"id": "c88ded04-d632-4485-b0ab-46ecf922184d",
"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": [
194944,
78448
],
"id": "4a5b2068-8d0d-4c90-8270-04384d209777",
"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": [
195136,
78448
],
"id": "4de950cb-6b8c-4c47-ab36-52a593dc820a",
"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": [
194400,
78240
],
"id": "88bea895-5c1d-4da4-bdb8-fdaa2f0125d9",
"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": "Auth Retry Handler",
"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": "Auth Retry Handler",
"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": "Lightning Retry Handler",
"type": "main",
"index": 0
}
]
]
},
"Lightning Retry Handler": {
"main": [
[
{
"node": "Retry Lightning Request?",
"type": "main",
"index": 0
}
]
]
},
"Retry Lightning Request?": {
"main": [
[
{
"node": "Wait Before Retry",
"type": "main",
"index": 0
}
],
[
{
"node": "Request Error Notification",
"type": "main",
"index": 0
}
]
]
},
"Wait Before Retry": {
"main": [
[
{
"node": "Calculate Lightning Headers",
"type": "main",
"index": 0
}
]
]
},
"Request Error Notification": {
"main": [
[
{
"node": "Stop Workflow",
"type": "main",
"index": 0
}
]
]
},
"Auth Retry Handler": {
"main": [
[
{
"node": "Retry Auth Request?",
"type": "main",
"index": 0
}
]
]
},
"Retry Auth Request?": {
"main": [
[
{
"node": "Wait Before Auth Retry",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Error Notification",
"type": "main",
"index": 0
}
]
]
},
"Wait Before Auth Retry": {
"main": [
[
{
"node": "Login to iklim.co",
"type": "main",
"index": 0
}
]
]
},
"Send Error Notification": {
"main": [
[
{
"node": "Stop Workflow Auth",
"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": "Thunderstorm Request",
"type": "main",
"index": 0
}
]
]
},
"Calculate Lightning Headers": {
"main": [
[
{
"node": "Lightning Request",
"type": "main",
"index": 0
}
]
]
},
"Thunderstorm Request": {
"main": [
[
{
"node": "Build Report Payload",
"type": "main",
"index": 0
}
],
[
{
"node": "Thunderstorm Retry Handler",
"type": "main",
"index": 0
}
]
]
},
"Thunderstorm Retry Handler": {
"main": [
[
{
"node": "Retry Thunderstorm Request?",
"type": "main",
"index": 0
}
]
]
},
"Retry Thunderstorm Request?": {
"main": [
[
{
"node": "Wait Before Thunderstorm Retry",
"type": "main",
"index": 0
}
],
[
{
"node": "Thunderstorm Error Fallback",
"type": "main",
"index": 0
}
]
]
},
"Wait Before Thunderstorm Retry": {
"main": [
[
{
"node": "Calculate Thunderstorm Headers",
"type": "main",
"index": 0
}
]
]
},
"Thunderstorm Error Fallback": {
"main": [
[
{
"node": "Build Report Payload",
"type": "main",
"index": 0
}
]
]
},
"Build Report Payload": {
"main": [
[
{
"node": "Start Async Report",
"type": "main",
"index": 0
}
]
]
},
"Start Async Report": {
"main": [
[
{
"node": "Attach Report Job",
"type": "main",
"index": 0
}
]
]
},
"Attach Report Job": {
"main": [
[
{
"node": "Wait for Report",
"type": "main",
"index": 0
}
]
]
},
"Wait for Report": {
"main": [
[
{
"node": "Poll Report Status",
"type": "main",
"index": 0
}
]
]
},
"Poll Report Status": {
"main": [
[
{
"node": "Merge Poll Status",
"type": "main",
"index": 0
}
]
]
},
"Merge Poll Status": {
"main": [
[
{
"node": "Report Complete?",
"type": "main",
"index": 0
}
]
]
},
"Report Complete?": {
"main": [
[
{
"node": "Download Report",
"type": "main",
"index": 0
}
],
[
{
"node": "Report Failed?",
"type": "main",
"index": 0
}
]
]
},
"Report Failed?": {
"main": [
[
{
"node": "Stop Report Failed",
"type": "main",
"index": 0
}
],
[
{
"node": "Continue Polling?",
"type": "main",
"index": 0
}
]
]
},
"Continue Polling?": {
"main": [
[
{
"node": "Wait for Report",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop Report Timeout",
"type": "main",
"index": 0
}
]
]
},
"Download 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": "4b32c35a-7a40-4cdc-97e9-8c714ca93bf9",
"meta": {
"instanceId": "15c4ff3a74619031c77894fe5fb8c0fd585362ef637b1873abd56a139f543e12"
},
"id": "0GaXSvJtxW6JR6hv",
"tags": []
}