diff --git a/Import_Wind_Turbine_Farm.json b/Import_Wind_Turbine_Farm.json new file mode 100644 index 0000000..eaafa64 --- /dev/null +++ b/Import_Wind_Turbine_Farm.json @@ -0,0 +1,410 @@ +{ + "name": "Import_Wind_Turbine_Farm", + "nodes": [ + { + "parameters": { + "content": "Upload turbine CSV through the n8n form (no server file access needed).\n\n1. Import this workflow and **Activate** it.\n2. Open **Import Wind Turbines Form** → copy the **Production URL**.\n3. Submit the form with customer_id + CSV file.\n\nCSV format (comma or semicolon):\nname,latitude,longitude,farm_name\nWTG-01,39.854321,32.765432,My Wind Farm\n\nAlso supported headers:\n- name / turbin / wtg / turbin adi\n- latitude / lat / enlem\n- longitude / lon / boylam\n\nOptional column: customer_id\n\nIf import fails, the error shows the column names n8n detected.", + "height": 340, + "width": 560 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + -560, + -160 + ], + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "Sticky Note" + }, + { + "parameters": { + "formTitle": "Import Wind Turbines", + "formDescription": "Upload a CSV with turbine names and coordinates for one wind farm.\nRequired columns: name, latitude, longitude", + "formPath": "import-wind-turbines", + "responseMode": "lastNode", + "authentication": "none", + "formFields": { + "values": [ + { + "fieldLabel": "customer_id", + "fieldType": "text", + "fieldName": "customer_id", + "requiredField": true, + "placeholder": "Must match customers.id in datatable" + }, + { + "fieldLabel": "csv_file", + "fieldType": "file", + "fieldName": "csv_file", + "requiredField": true, + "acceptFileTypes": ".csv", + "multipleFiles": false + }, + { + "fieldLabel": "replace_existing", + "fieldType": "dropdown", + "fieldName": "replace_existing", + "requiredField": true, + "fieldOptions": { + "values": [ + { + "option": "yes" + }, + { + "option": "no" + } + ] + } + } + ] + }, + "options": { + "buttonLabel": "Import", + "appendAttribution": false + } + }, + "type": "n8n-nodes-base.formTrigger", + "typeVersion": 2.4, + "position": [ + 0, + 0 + ], + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "name": "Import Wind Turbines Form", + "webhookId": "f1e2d3c4-b5a6-7890-abcd-ef1234567890" + }, + { + "parameters": { + "jsCode": "const item = $input.first();\nconst json = item.json ?? {};\n\nconst normalizeKey = (value) => String(value).toLowerCase().replace(/[\\W_]+/g, '');\n\nconst pickJson = (aliases) => {\n for (const alias of aliases) {\n if (json[alias] != null && String(json[alias]).trim() !== '') {\n return String(json[alias]).trim();\n }\n }\n const wanted = new Set(aliases.map(normalizeKey));\n for (const [key, value] of Object.entries(json)) {\n if (!wanted.has(normalizeKey(key))) continue;\n if (value != null && String(value).trim() !== '') {\n return String(value).trim();\n }\n }\n return '';\n};\n\nconst customerId = pickJson([\n 'customer_id',\n 'Customer ID',\n 'Customer_ID',\n 'customer id',\n]);\nif (!customerId) {\n const available = Object.keys(json).filter((key) => !['submittedAt', 'formMode', 'formQueryParameters'].includes(key));\n throw new Error(`customer_id is required. Form returned keys: ${available.join(', ') || '(none)'}`);\n}\n\nconst replaceRaw = pickJson([\n 'replace_existing',\n 'Replace existing turbines for this customer?',\n]) || 'no';\nconst replaceExisting = ['yes', 'true', '1'].includes(replaceRaw.toLowerCase());\n\nconst binary = item.binary ?? {};\nconst csvKey = Object.keys(binary).find((key) => {\n const norm = normalizeKey(key);\n return norm === 'csvfile' || norm.startsWith('csvfile') || norm.includes('turbinecsv') || norm.includes('csv');\n}) || (Object.keys(binary).length === 1 ? Object.keys(binary)[0] : '');\nif (!csvKey) {\n throw new Error(`Upload a CSV file. Binary keys: ${Object.keys(binary).join(', ') || '(none)'}`);\n}\n\nreturn [{\n json: {\n customer_id: customerId,\n replace_existing: replaceExisting,\n },\n binary: {\n csv_file: binary[csvKey],\n },\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 240, + 0 + ], + "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "name": "Normalize Form Input" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "44444444-4444-4444-8444-444444444444", + "leftValue": "={{ $json.replace_existing }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 480, + 0 + ], + "id": "d4e5f6a7-b8c9-0123-def0-234567890123", + "name": "Replace Existing?" + }, + { + "parameters": { + "resource": "row", + "operation": "deleteRows", + "dataTableId": { + "__rl": true, + "value": "wind_turbine_farm", + "mode": "name", + "cachedResultName": "wind_turbine_farm" + }, + "matchType": "allConditions", + "filters": { + "conditions": [ + { + "keyName": "customer_id", + "condition": "eq", + "keyValue": "={{ $('Normalize Form Input').first().json.customer_id }}" + } + ] + } + }, + "type": "n8n-nodes-base.dataTable", + "typeVersion": 1.1, + "position": [ + 720, + -96 + ], + "id": "e5f6a7b8-c9d0-1234-ef01-345678901234", + "name": "Delete Existing Turbines", + "alwaysOutputData": true + }, + { + "parameters": { + "jsCode": "const normalized = $('Normalize Form Input').first();\nreturn [{\n json: normalized.json,\n binary: normalized.binary,\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 960, + 0 + ], + "id": "f6a7b8c9-d0e1-2345-f012-456789012345", + "name": "Reattach CSV Binary" + }, + { + "parameters": { + "operation": "csv", + "binaryPropertyName": "csv_file", + "options": { + "headerRow": true + } + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1200, + 0 + ], + "id": "a7b8c9d0-e1f2-3456-0123-567890123456", + "name": "Extract From CSV" + }, + { + "parameters": { + "jsCode": "const config = $('Normalize Form Input').first().json;\nconst defaultCustomerId = String(config.customer_id ?? '').trim();\nif (!defaultCustomerId) {\n throw new Error('customer_id is required (must match customers.id).');\n}\n\nconst cleanKey = (key) => String(key).replace(/^\\uFEFF/, '').trim();\nconst normalizeKey = (key) => cleanKey(key).toLowerCase().replace(/[\\s_\\-]+/g, '');\n\nconst NAME_ALIASES = [\n 'name', 'turbine', 'turbinename', 'turbin', 'turbinadi', 'turbinadı', 'turbine_name',\n 'wtg', 'label', 'id', 'unit', 'generator', 'turbineid', 'turbinid', 'ad', 'isim',\n];\nconst LAT_ALIASES = ['latitude', 'lat', 'enlem', 'y', 'northing'];\nconst LON_ALIASES = ['longitude', 'lon', 'lng', 'boylam', 'long', 'x', 'easting'];\nconst CUSTOMER_ALIASES = ['customerid', 'customer_id', 'customer'];\nconst FARM_ALIASES = ['farmname', 'farm_name', 'farm', 'windfarm', 'wind_farm', 'santral', 'res', 'projeadi', 'projectname'];\n\nconst isFarmKey = (key) => {\n const normalized = normalizeKey(key);\n return normalized.includes('farm') || normalized.includes('santral') || FARM_ALIASES.some((alias) => normalizeKey(alias) === normalized);\n};\n\nconst parseNumber = (value) => {\n const text = String(value ?? '').trim().replace(/\\s+/g, '');\n if (!text) return NaN;\n if (text.includes(',') && !text.includes('.')) {\n return Number(text.replace(',', '.'));\n }\n return Number(text);\n};\n\nconst expandDelimitedRow = (row) => {\n const keys = Object.keys(row);\n if (keys.length !== 1) return row;\n\n const onlyKey = cleanKey(keys[0]);\n const onlyValue = row[keys[0]];\n\n if (onlyKey.includes(';') && (onlyValue == null || String(onlyValue).trim() === '')) {\n const headers = onlyKey.split(';').map(cleanKey);\n return Object.fromEntries(headers.map((header) => [header, '']));\n }\n\n const delimiter = onlyKey.includes(';') ? ';' : (onlyKey.includes('\\t') ? '\\t' : null);\n if (!delimiter) return row;\n\n const headers = onlyKey.split(delimiter).map(cleanKey);\n const values = String(onlyValue ?? '').split(delimiter).map((part) => cleanKey(part));\n if (headers.length < 2 || values.length < 2) return row;\n\n const expanded = {};\n for (let i = 0; i < headers.length; i += 1) {\n expanded[headers[i]] = values[i] ?? '';\n }\n return expanded;\n};\n\nconst buildNormalizedRow = (row) => {\n const expanded = expandDelimitedRow(row);\n const normalized = {};\n for (const [key, value] of Object.entries(expanded)) {\n normalized[normalizeKey(key)] = value;\n }\n return normalized;\n};\n\nconst pickValue = (normRow, aliases, fuzzyTokens = []) => {\n for (const alias of aliases) {\n const value = normRow[normalizeKey(alias)];\n if (value != null && String(value).trim() !== '') {\n return String(value).trim();\n }\n }\n\n for (const token of fuzzyTokens) {\n for (const [key, value] of Object.entries(normRow)) {\n if (!key.includes(token)) continue;\n if (value != null && String(value).trim() !== '') {\n return String(value).trim();\n }\n }\n }\n\n return '';\n};\n\nconst inferRow = (normRow) => {\n const entries = Object.entries(normRow)\n .filter(([key, value]) => key && value != null && String(value).trim() !== '' && !CUSTOMER_ALIASES.includes(key) && !isFarmKey(key));\n\n const numeric = [];\n const text = [];\n\n for (const [key, value] of entries) {\n const parsed = parseNumber(value);\n const raw = String(value).trim();\n if (Number.isFinite(parsed) && /^-?\\d/.test(raw)) {\n numeric.push(parsed);\n } else {\n text.push(String(value).trim());\n }\n }\n\n if (text.length >= 1 && numeric.length >= 2) {\n return {\n name: text[0],\n latitude: numeric[0],\n longitude: numeric[1],\n };\n }\n\n if (entries.length === 3) {\n const values = entries.map(([, value]) => String(value).trim());\n const nums = values.map(parseNumber);\n const allNumeric = nums.every(Number.isFinite);\n if (allNumeric) {\n return { name: `T${values[0]}`, latitude: nums[0], longitude: nums[1] };\n }\n const numericIndexes = nums.map((num, idx) => (Number.isFinite(num) ? idx : -1)).filter((idx) => idx >= 0);\n if (numericIndexes.length >= 2) {\n const nameIndex = values.findIndex((value, idx) => !Number.isFinite(nums[idx]));\n return {\n name: nameIndex >= 0 ? values[nameIndex] : values[0],\n latitude: nums[numericIndexes[0]],\n longitude: nums[numericIndexes[1]],\n };\n }\n }\n\n return null;\n};\n\nconst rows = $input.all().map((item) => item.json);\nif (!rows.length) {\n throw new Error('CSV file is empty or could not be parsed.');\n}\n\nreturn rows.map((rawRow, index) => {\n const normRow = buildNormalizedRow(rawRow);\n\n let name = pickValue(normRow, NAME_ALIASES, ['name', 'turbin', 'wtg', 'turbine', 'label', 'ad', 'isim']);\n let latRaw = pickValue(normRow, LAT_ALIASES, ['lat', 'enlem']);\n let lonRaw = pickValue(normRow, LON_ALIASES, ['lon', 'lng', 'boylam', 'long']);\n const rowCustomerId = pickValue(normRow, CUSTOMER_ALIASES, ['customer']) || defaultCustomerId;\n const farmName = pickValue(normRow, FARM_ALIASES, ['farm', 'santral', 'res']);\n\n let latitude = parseNumber(latRaw);\n let longitude = parseNumber(lonRaw);\n\n if (!name || !Number.isFinite(latitude) || !Number.isFinite(longitude)) {\n const inferred = inferRow(normRow);\n if (inferred) {\n name = name || inferred.name;\n latitude = Number.isFinite(latitude) ? latitude : inferred.latitude;\n longitude = Number.isFinite(longitude) ? longitude : inferred.longitude;\n }\n }\n\n if (!name) {\n const keys = Object.keys(rawRow).map(cleanKey);\n throw new Error(\n `Missing turbine name in CSV row ${index + 1}. Found columns: ${keys.join(', ') || '(none)'}. ` +\n 'Expected headers like name, latitude, longitude (also supports turbin, enlem, boylam).',\n );\n }\n if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {\n throw new Error(\n `Invalid coordinates for turbine \"${name}\" in CSV row ${index + 1}. ` +\n `latitude=\"${latRaw}\" longitude=\"${lonRaw}\"`,\n );\n }\n\n return {\n json: {\n customer_id: rowCustomerId,\n farm_name: farmName,\n name,\n latitude,\n longitude,\n },\n };\n});" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1440, + 0 + ], + "id": "b8c9d0e1-f234-5678-1234-678901234567", + "name": "Map Turbine Rows" + }, + { + "parameters": { + "resource": "row", + "operation": "insert", + "dataTableId": { + "__rl": true, + "value": "wind_turbine_farm", + "mode": "name", + "cachedResultName": "wind_turbine_farm" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "customer_id": "={{ $json.customer_id }}", + "farm_name": "={{ $json.farm_name }}", + "name": "={{ $json.name }}", + "latitude": "={{ $json.latitude }}", + "longitude": "={{ $json.longitude }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "customer_id", + "displayName": "customer_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "readOnly": false, + "removed": false + }, + { + "id": "farm_name", + "displayName": "farm_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "readOnly": false, + "removed": false + }, + { + "id": "name", + "displayName": "name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "readOnly": false, + "removed": false + }, + { + "id": "latitude", + "displayName": "latitude", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "readOnly": false, + "removed": false + }, + { + "id": "longitude", + "displayName": "longitude", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "readOnly": false, + "removed": false + } + ], + "attemptToConvertTypes": true, + "convertFieldsToString": false + }, + "options": { + "optimizeBulk": true + } + }, + "type": "n8n-nodes-base.dataTable", + "typeVersion": 1.1, + "position": [ + 1680, + 0 + ], + "id": "c9d0e1f2-3456-7890-2345-789012345678", + "name": "Insert Turbine Rows" + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Import complete", + "completionMessage": "Turbine rows were written to the wind_turbine_farm datatable.", + "options": {} + }, + "type": "n8n-nodes-base.form", + "typeVersion": 1, + "position": [ + 1920, + 0 + ], + "id": "d0e1f2a3-4567-8901-2345-678901234567", + "name": "Import Complete" + } + ], + "pinData": {}, + "connections": { + "Import Wind Turbines Form": { + "main": [ + [ + { + "node": "Normalize Form Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Form Input": { + "main": [ + [ + { + "node": "Replace Existing?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Replace Existing?": { + "main": [ + [ + { + "node": "Delete Existing Turbines", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reattach CSV Binary", + "type": "main", + "index": 0 + } + ] + ] + }, + "Delete Existing Turbines": { + "main": [ + [ + { + "node": "Reattach CSV Binary", + "type": "main", + "index": 0 + } + ] + ] + }, + "Reattach CSV Binary": { + "main": [ + [ + { + "node": "Extract From CSV", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract From CSV": { + "main": [ + [ + { + "node": "Map Turbine Rows", + "type": "main", + "index": 0 + } + ] + ] + }, + "Map Turbine Rows": { + "main": [ + [ + { + "node": "Insert Turbine Rows", + "type": "main", + "index": 0 + } + ] + ] + }, + "Insert Turbine Rows": { + "main": [ + [ + { + "node": "Import Complete", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "00000000-0000-4000-8000-000000000002", + "meta": { + "templateCredsSetupCompleted": false + }, + "tags": [] +} diff --git a/Lightning_Report_Automatic.json b/Lightning_Report_Automatic.json deleted file mode 100644 index fd717c4..0000000 --- a/Lightning_Report_Automatic.json +++ /dev/null @@ -1,3217 +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\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: `

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": [ - 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": [] -} \ No newline at end of file diff --git a/Lightning_Report_Manual.json b/Lightning_Report_Manual.json index 68b94cc..fa8e635 100644 --- a/Lightning_Report_Manual.json +++ b/Lightning_Report_Manual.json @@ -9,11 +9,11 @@ }, "type": "n8n-nodes-base.stickyNote", "position": [ - 200944, - 80864 + 198256, + 84640 ], "typeVersion": 1, - "id": "ec8b33d2-a74a-42dc-aae5-458ecab0d0a3", + "id": "d3e74f9c-5ff4-4cdf-b189-038aa7b37b63", "name": "Sticky Note1" }, { @@ -35,13 +35,13 @@ }, "options": {} }, - "id": "27edeb8d-4885-440a-8c12-3f52b5e0fd48", + "id": "652fbcfb-cbb2-4fd9-a0da-fac80224bb3d", "name": "Login to iklim.co", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ - 199744, - 79952 + 197056, + 83728 ], "onError": "continueErrorOutput" }, @@ -92,10 +92,10 @@ "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ - 199984, - 79936 + 197296, + 83712 ], - "id": "8f423a2f-0766-4cfe-ba11-779722e2f895", + "id": "abe10af8-3702-4df7-b114-bd3141fd2a1d", "name": "CalculateExpirations" }, { @@ -145,10 +145,10 @@ "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ - 200048, - 80192 + 197360, + 83968 ], - "id": "6e0be6c6-9509-43fc-bd0e-63958759230f", + "id": "283b025a-0cd4-4bdb-95e6-a44ccd3f9aa1", "name": "CalculateExpirations1" }, { @@ -166,13 +166,13 @@ }, "options": {} }, - "id": "e37a94b7-cdb8-4ef9-9f4e-f6334d35e1d2", + "id": "7121fe11-5dd1-495b-8d75-0cf564e67710", "name": "Refresh Token", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ - 199744, - 80208 + 197056, + 83984 ], "onError": "continueErrorOutput" }, @@ -260,10 +260,10 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ - 199520, - 80176 + 196832, + 83952 ], - "id": "0c2c1b9d-a222-4505-8814-736d5fad5a06", + "id": "5f998d9b-0af9-46d2-9ace-9a7fa926ef7b", "name": "Switch" }, { @@ -273,10 +273,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 199328, - 80192 + 196640, + 83968 ], - "id": "f3cc5f51-1896-48cf-af79-d930302b13c8", + "id": "8b56df42-1a88-4d08-baaf-2d1ed6887348", "name": "Restore Credentials", "retryOnFail": false }, @@ -287,10 +287,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 200272, - 80192 + 197584, + 83968 ], - "id": "932e6d12-b693-42f8-a746-90e14c646495", + "id": "d58a574d-9712-4ebe-b941-9efa291ba21f", "name": "Store Refresh Credentials" }, { @@ -300,10 +300,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 200192, - 79936 + 197504, + 83712 ], - "id": "9de02592-f925-4e8d-b757-a49b47dffb82", + "id": "b8077750-76db-49f7-a371-4e839e126a0b", "name": "Store Login Credentials" }, { @@ -323,10 +323,10 @@ "type": "n8n-nodes-base.slack", "typeVersion": 2.4, "position": [ - 200352, - 79584 + 197664, + 83360 ], - "id": "5f0581b3-0cd3-40b4-a759-facd1de685ed", + "id": "353eaf10-e27a-4bb9-a8f2-91787ea7027d", "name": "Send Error Notification", "webhookId": "0b3582cf-042e-43d6-bc02-4c9debab0566", "credentials": { @@ -338,13 +338,13 @@ }, { "parameters": {}, - "id": "9a59d0fe-ce46-4c1c-bd24-ea15c277d68d", + "id": "027e6023-51a1-402f-99f7-a79b272d8e6e", "name": "Daily Trigger", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ - 198624, - 80192 + 195936, + 83968 ] }, { @@ -389,13 +389,13 @@ "jsonBody": "={{ $json.requestBody }}", "options": {} }, - "id": "219f9dcb-9ee6-4384-9dde-6ce32df51391", + "id": "3d480a8d-13ed-422b-9757-66c4574b6073", "name": "Lightning Request", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ - 200848, - 80464 + 198160, + 84240 ], "onError": "continueErrorOutput" }, @@ -420,10 +420,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 199328, - 80448 + 196640, + 84224 ], - "id": "3f7fc370-13f1-4fdc-b333-46f5f4533c33", + "id": "3d86be42-36ab-46ce-b731-26eb59aed434", "name": "Iterate Customers" }, { @@ -449,10 +449,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 200064, - 80464 + 197376, + 84240 ], - "id": "fac9d472-e86e-4969-aae2-2cde2d32fa86", + "id": "9e1353e6-c8a2-4be2-8ead-22869274137b", "name": "Get Customer Wind Turbines" }, { @@ -472,10 +472,10 @@ "type": "n8n-nodes-base.slack", "typeVersion": 2.4, "position": [ - 201488, - 80032 + 198800, + 83808 ], - "id": "3b7413b7-9b23-4e07-a363-d47ac5c6ee31", + "id": "43d48e1c-e05b-4f8d-a38b-9137dc77eaf2", "name": "Request Error Notification", "webhookId": "38b3ebc5-a136-4b78-9c7a-98f40e92a9d5", "notesInFlow": false, @@ -494,10 +494,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 201072, - 80032 + 198384, + 83808 ], - "id": "10a56f8f-3034-4653-9a63-998852abae72", + "id": "ee2eeba4-3669-46e5-acd6-798e54d65818", "name": "Lightning Retry Handler" }, { @@ -528,10 +528,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 201280, - 80032 + 198592, + 83808 ], - "id": "0acc1f28-4854-49db-a738-8b83b6f98dc6", + "id": "07f95ba2-437b-45ae-9620-0fd52a7b885a", "name": "Retry Lightning Request?" }, { @@ -541,10 +541,10 @@ "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ - 201488, - 80208 + 198800, + 83984 ], - "id": "c2684274-7bb6-4b2a-9ef6-de385c6445b0", + "id": "24254a45-5809-4f5b-856b-94e95aac7807", "name": "Wait Before Retry", "webhookId": "b7c8d9e0-f1a2-4345-b678-889900112233" }, @@ -555,10 +555,10 @@ "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ - 201664, - 80032 + 198976, + 83808 ], - "id": "d3078187-3fcd-4254-86cf-c9b54b2a74e9", + "id": "253ed90d-ab8f-400c-a8c6-edbe6b3c08a9", "name": "Stop Workflow" }, { @@ -568,10 +568,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 199968, - 79584 + 197280, + 83360 ], - "id": "e11e74b9-b5da-4554-b749-dcfe945ab19a", + "id": "95ede16f-dcaa-4371-bd72-86f258b68d42", "name": "Auth Retry Handler" }, { @@ -602,10 +602,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 200160, - 79584 + 197472, + 83360 ], - "id": "7c21738a-3964-49c9-821e-37565ac3847e", + "id": "fae58887-d64d-4b80-b26d-c5247bad23cd", "name": "Retry Auth Request?" }, { @@ -615,10 +615,10 @@ "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ - 200352, - 79744 + 197664, + 83520 ], - "id": "fad025fa-21b6-49c4-a561-c718fcd633cf", + "id": "37f1340c-3234-46e1-b4f0-b6696c5b3ee9", "name": "Wait Before Auth Retry", "webhookId": "c8d9e0f1-a2b3-4345-b678-889900112244" }, @@ -629,10 +629,10 @@ "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ - 200528, - 79584 + 197840, + 83360 ], - "id": "9f4bcb30-b4ae-4f27-bcf0-f2dd440f2c27", + "id": "6a088c93-0f6e-4d5f-84d3-fa03da9e3914", "name": "Stop Workflow Auth" }, { @@ -642,10 +642,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 200272, - 80464 + 197584, + 84240 ], - "id": "454daa20-4d93-4522-bc00-56fa8a842f5a", + "id": "2238396a-397b-4c85-865f-3c7dec711501", "name": "Centroid & Distance Ring calculation" }, { @@ -655,10 +655,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 200464, - 80464 + 197776, + 84240 ], - "id": "687960ad-f669-4286-9403-6f8cccfa4742", + "id": "d0c98ffc-0742-46cf-afb5-933208ee748e", "name": "Loop init" }, { @@ -668,10 +668,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 201088, - 80464 + 198400, + 84240 ], - "id": "8e5a335a-e1b9-475a-9e93-3987fd079271", + "id": "01389127-d9dd-4113-b979-e651099bed2a", "name": "Logic Gate" }, { @@ -702,10 +702,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 201280, - 80416 + 198592, + 84192 ], - "id": "1ac5d05f-8299-439a-ad71-565d749b1951", + "id": "d42949ed-a501-4e0f-97c4-c755f154ea42", "name": "Stop Loop" }, { @@ -735,10 +735,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 201472, - 80448 + 198784, + 84224 ], - "id": "c0547d10-b5ae-4431-b396-a3df79782a80", + "id": "ce0b591c-5dd7-427c-a0ff-1511bacb35d6", "name": "Lightning found?" }, { @@ -765,10 +765,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 201712, - 80480 + 199024, + 84256 ], - "id": "c260df11-6b96-4528-9511-1764bf2646e7", + "id": "0e157cc7-a646-4cf6-a5a2-6fb842b43af8", "name": "Get row(s)", "alwaysOutputData": true }, @@ -800,10 +800,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 202096, - 80480 + 199408, + 84256 ], - "id": "e6f58776-6818-4d00-bbed-96860d555c1c", + "id": "017fb1b5-d99f-4456-bb1d-cefd5bd43879", "name": "If" }, { @@ -918,10 +918,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 202336, - 80480 + 199648, + 84256 ], - "id": "66d45469-2129-4c8c-a512-42ce291a9f67", + "id": "56e977ca-9aad-450a-8e34-e0c52d1039d5", "name": "Insert row" }, { @@ -931,10 +931,10 @@ "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [ - 199824, - 80448 + 197136, + 84224 ], - "id": "1557d7b5-4207-4516-ad56-bf68e0693be7", + "id": "7d12e1cf-c145-4b9a-a24b-9ab5e91eb906", "name": "Loop Over Items" }, { @@ -944,10 +944,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 202560, - 80480 + 199872, + 84256 ], - "id": "31a343f7-1724-4177-aea4-73aa2ccc3b58", + "id": "62e5f434-bf10-459e-ae44-b82524238e01", "name": "Calculate Thunderstorm Headers" }, { @@ -995,10 +995,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 202752, - 80480 + 200064, + 84256 ], - "id": "89c7675a-641e-4002-8e21-7f51be063b4b", + "id": "f3c657e2-b4ab-44c7-a98e-d8c5c427e017", "name": "Thunderstorm Request", "onError": "continueErrorOutput" }, @@ -1009,10 +1009,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 203376, - 80512 + 200688, + 84288 ], - "id": "00c3cf00-9f21-4d5e-94e6-bcef39375d9d", + "id": "5d797ff1-cee4-45cc-ad7e-5b49d6528d04", "name": "Thunderstorm Error Fallback" }, { @@ -1022,10 +1022,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 202976, - 80288 + 200288, + 84064 ], - "id": "09ac6f1e-7bd8-49d8-8353-fc409d905f3a", + "id": "2638b5c6-7957-4f09-b9e8-27c156c6f138", "name": "Thunderstorm Retry Handler" }, { @@ -1056,10 +1056,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 203168, - 80288 + 200480, + 84064 ], - "id": "3510b96e-2785-4202-bcdd-3c39ab725b01", + "id": "af6372af-3e4e-4607-9b8e-5a203ffe60ac", "name": "Retry Thunderstorm Request?" }, { @@ -1069,10 +1069,10 @@ "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ - 203392, - 80288 + 200704, + 84064 ], - "id": "d1975a07-4f25-42c9-aa3b-d3300eca91fc", + "id": "f01c06e0-5681-45db-be09-fe62a7b6cf91", "name": "Wait Before Thunderstorm Retry", "webhookId": "c8d9e0f1-a2b3-4456-c789-0123456789ab" }, @@ -1083,10 +1083,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 200656, - 80464 + 197968, + 84240 ], - "id": "fea0c908-869d-4676-a1ed-285e1eeb06b9", + "id": "acf71540-a1ca-44e0-a404-379236841b92", "name": "Calculate Lightning Headers" }, { @@ -1096,10 +1096,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 203568, - 80624 + 200880, + 84400 ], - "id": "0f824542-6b2c-406b-a49e-94def23459f1", + "id": "a3a4f196-882c-408b-8c82-dd110a0116dc", "name": "Build Report Payload" }, { @@ -1133,10 +1133,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 203776, - 80480 + 201088, + 84256 ], - "id": "034b0df6-609b-46bc-a8dd-eb6908dcdea6", + "id": "8b003fb1-7ce3-4b55-a18f-9e121cca7f41", "name": "Start Async Report", "onError": "continueErrorOutput" }, @@ -1147,10 +1147,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 203984, - 80480 + 201296, + 84256 ], - "id": "2f632822-7e25-4a29-9989-1da22278bd97", + "id": "47a8b777-5552-405f-a54b-f82448478aeb", "name": "Attach Report Job" }, { @@ -1160,10 +1160,10 @@ "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ - 204192, - 80480 + 201504, + 84256 ], - "id": "8216a8fb-3cc5-4593-880d-afde4f354eb4", + "id": "ac498279-b84f-45b2-9ed7-a0b504524d88", "name": "Wait for Report", "webhookId": "d4e5f6a7-b8c9-4012-d345-6789abcdef01" }, @@ -1190,10 +1190,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 204400, - 80480 + 201712, + 84256 ], - "id": "f5d6a077-4d67-4013-98bc-66e5ca70ba37", + "id": "cfee8f9a-fdbf-45b6-a6cc-fb5eca777a8a", "name": "Poll Report Status", "onError": "continueErrorOutput" }, @@ -1204,10 +1204,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 204608, - 80480 + 201920, + 84256 ], - "id": "ec5beb20-a1a6-4c42-8887-d28c0a251fc5", + "id": "0826f896-13f5-47a0-a451-9bbf5a2e2666", "name": "Merge Poll Status" }, { @@ -1237,10 +1237,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 204816, - 80480 + 202128, + 84256 ], - "id": "7d0e5e6a-0a35-4284-b900-e854e4dea9b2", + "id": "bd3d377e-3252-42b5-b1cf-3c137168fab8", "name": "Report Complete?" }, { @@ -1270,10 +1270,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 205008, - 80144 + 202320, + 83920 ], - "id": "fb6b4c25-3976-4596-9e7e-980af24fd6c1", + "id": "91b3aabb-4a7d-458f-9d56-d1229f729bc3", "name": "Report Failed?" }, { @@ -1303,10 +1303,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 205232, - 80192 + 202544, + 83968 ], - "id": "1bc1aaf0-1c31-44d6-9865-aa83b1e107a9", + "id": "52ed1273-0a93-4fdf-ab70-f33f9cd70462", "name": "Continue Polling?" }, { @@ -1316,10 +1316,10 @@ "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ - 205216, - 80000 + 202528, + 83776 ], - "id": "7078547d-f2f5-4675-89b4-b9e7087adea5", + "id": "916b6e49-e8ee-4319-b9b1-54624e743922", "name": "Stop Report Failed" }, { @@ -1329,10 +1329,10 @@ "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ - 205440, - 80208 + 202752, + 83984 ], - "id": "3b8b9430-a03f-47fd-b87b-cdcb8b8faedb", + "id": "927e0d98-2985-4dde-be31-872ffe72ebd5", "name": "Stop Report Timeout" }, { @@ -1363,10 +1363,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 205040, - 80464 + 202352, + 84240 ], - "id": "b6e5831e-19ed-4388-8ad1-bb7ad6cbc9c9", + "id": "99a22feb-89a2-483a-af5b-eaeda7811954", "name": "Download Report", "onError": "continueErrorOutput" }, @@ -1377,10 +1377,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 205280, - 80448 + 202592, + 84224 ], - "id": "032cc551-9e82-44d5-8ddb-9385c796277c", + "id": "0a28b731-3d0d-4b2c-b745-4c9d44c4c301", "name": "Prepare DOCX Metadata" }, { @@ -1402,10 +1402,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 205488, - 80448 + 202800, + 84224 ], - "id": "0d8b9a7a-7cf1-4137-a338-5bc026429ac9", + "id": "e9dff831-748d-403a-ad98-7988a3b0222c", "name": "Get Storm Logs For Next Id", "alwaysOutputData": true }, @@ -1416,10 +1416,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 205696, - 80448 + 203008, + 84224 ], - "id": "d5a5ca1f-70c2-4310-b9cc-ffd9ccdcd656", + "id": "f54e8e2d-a107-449e-9959-59febc8b9c97", "name": "Resolve Storm Log Id" }, { @@ -1433,10 +1433,10 @@ "type": "n8n-nodes-base.slack", "typeVersion": 2.4, "position": [ - 206272, - 80448 + 203584, + 84224 ], - "id": "9c01e454-8f6c-47bc-a705-84d0a6dbb6da", + "id": "f5beaab8-d524-4072-87bb-156b0d5b5ff3", "name": "Upload Report to Slack", "webhookId": "c4d5e6f7-a8b9-4012-cdef-345678901234", "credentials": { @@ -1463,10 +1463,10 @@ "type": "n8n-nodes-base.slack", "typeVersion": 2.4, "position": [ - 206848, - 80592 + 204160, + 84368 ], - "id": "a665bfe7-2e8b-4c0e-b10f-bbf0a39bea81", + "id": "d0222810-1f47-41a5-9af2-31f14fdee53a", "name": "Send Approval Buttons", "webhookId": "d4e5f6a7-b8c9-0123-def0-456789abcdef", "credentials": { @@ -1486,10 +1486,10 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ - 198992, - 80912 + 196304, + 84688 ], - "id": "7707402e-8f62-4065-a57d-7eb8006ac166", + "id": "797d3223-c057-4c4d-b806-1b3bf5c451ec", "name": "Slack Interaction Webhook", "webhookId": "c9d8e7f6-a5b4-4321-fedc-ba9876543211" }, @@ -1500,10 +1500,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 199232, - 80912 + 196544, + 84688 ], - "id": "bcd115e6-f807-4468-a7dd-7f9e84823b77", + "id": "8af87bcb-8da1-4951-a01f-3dc66cd812ec", "name": "Parse Slack Button Payload" }, { @@ -1534,10 +1534,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 199472, - 80912 + 196784, + 84688 ], - "id": "23f1a622-c3ec-41ab-9984-f39cc4a4380e", + "id": "b79d2a86-1145-4722-8743-df5e695a821a", "name": "Valid Storm Approval Button?" }, { @@ -1584,10 +1584,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 200224, - 80912 + 197536, + 84688 ], - "id": "77bc5739-9d4f-4640-ad1d-585e7edf10af", + "id": "d38a6984-6fdc-4134-b82d-581280b2f5a2", "name": "Approve Pending Row" }, { @@ -1608,13 +1608,13 @@ "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": "f7e97be7-f790-40ff-85e0-621f838f692f", + "id": "f44c0b2e-6b33-40ca-8208-447f8a7b6039", "name": "Notify Slack Recording Result", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ - 203280, - 81296 + 200592, + 85072 ] }, { @@ -1626,10 +1626,10 @@ "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ - 199728, - 80912 + 197040, + 84688 ], - "id": "686c8a3d-8e76-4081-99b2-67bb70f505a2", + "id": "4455b8ce-d7d7-4ea7-9291-eb889f6b11a3", "name": "Respond Slack Interaction" }, { @@ -1660,10 +1660,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 199920, - 80912 + 197232, + 84688 ], - "id": "14d670e2-c7ce-486e-b018-14c36c6e5160", + "id": "35b27958-e840-4ae8-9b3f-bde79485000a", "name": "Still Valid After Ack?" }, { @@ -1673,10 +1673,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 201888, - 80480 + 199200, + 84256 ], - "id": "cace5f97-bb92-4e60-a78a-1eff57fabf57", + "id": "f9ac0c55-3e90-44da-99f1-3e409edf4673", "name": "Prepare Insert Decision" }, { @@ -1686,10 +1686,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 205888, - 80448 + 203200, + 84224 ], - "id": "5af333d1-b692-4c2d-b721-9a144f06dca6", + "id": "ce5cf428-f1b5-48de-bf2d-4d69901d319c", "name": "Prepare Customer Email Context" }, { @@ -1699,11 +1699,11 @@ }, "type": "n8n-nodes-base.stickyNote", "position": [ - 200176, - 81488 + 197488, + 85264 ], "typeVersion": 1, - "id": "02a93a79-3473-49e3-b412-4f880a34c75c", + "id": "7c034fb3-300b-43ea-9e4b-d84541ac2978", "name": "Sticky Note Storm Logs Email Columns" }, { @@ -1713,10 +1713,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 206080, - 80448 + 203392, + 84224 ], - "id": "ff06bf5e-4755-424b-a3c3-80250259de2d", + "id": "8f370884-72f6-444e-a429-7229eff9d0ca", "name": "Build Pending Email Record" }, { @@ -1747,10 +1747,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 206464, - 80448 + 203776, + 84224 ], - "id": "01709423-0268-4d04-ab93-1fd35c005e3f", + "id": "9b070b06-30e7-4d6d-97c1-1b178f70c9e8", "name": "Can Persist Report?" }, { @@ -1830,10 +1830,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 206704, - 80432 + 204016, + 84208 ], - "id": "9085f244-e475-477c-9168-f020628af606", + "id": "c5477916-2b80-48bc-af90-8516c6990513", "name": "Persist Report To Storm Logs" }, { @@ -1863,10 +1863,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 200416, - 81248 + 197728, + 85024 ], - "id": "8239a011-b5cf-4a6d-8fbd-df6655b4a0da", + "id": "49819232-e8f6-4ecd-9119-6627c0708b4d", "name": "Report Approved?" }, { @@ -1892,10 +1892,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 200688, - 81104 + 198000, + 84880 ], - "id": "196a2d9e-dc7d-4a28-bdf1-49969010e3ac", + "id": "4b2668bf-983c-49f4-9082-7ea90fafde9e", "name": "Get Pending Report From Storm Logs", "alwaysOutputData": true }, @@ -1906,10 +1906,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 200912, - 81104 + 198224, + 84880 ], - "id": "9ea4bf46-ff7b-4db3-8fc7-bc101a75190d", + "id": "e55bf292-c738-4213-a5c7-746f44edd468", "name": "Load Queued Customer Report" }, { @@ -1936,10 +1936,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 201152, - 81104 + 198464, + 84880 ], - "id": "f4923090-6139-4fe1-b657-add072662217", + "id": "bef1a639-c07e-4211-86b0-8cb2ffedc70f", "name": "Lookup Customer Contact Email", "alwaysOutputData": true }, @@ -1950,10 +1950,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 201360, - 81104 + 198672, + 84880 ], - "id": "81bf0ff7-7145-460f-a977-cd811ac0a2c8", + "id": "8cf75ae0-b9b8-480d-b7ae-ce1fc1c79672", "name": "Merge Customer Contact Email" }, { @@ -1993,10 +1993,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 201568, - 81104 + 198880, + 84880 ], - "id": "73370502-a532-4990-84ef-1532668dbfc1", + "id": "586e567f-9127-4f2c-9ab7-4049c58ec36d", "name": "Ready To Email Customer?" }, { @@ -2006,10 +2006,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 201808, - 81088 + 199120, + 84864 ], - "id": "b21127fd-490f-436f-8b47-2786cb43c109", + "id": "5d6c2086-811e-4df7-89a4-9bb40619bd50", "name": "Prepare SendGrid Email Payload" }, { @@ -2035,10 +2035,10 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ - 202048, - 81088 + 199360, + 84864 ], - "id": "e2ee647b-e24d-426a-8f4b-406254706767", + "id": "62f7d108-d3fd-48ba-b338-e3339746c30e", "name": "Send Customer Report Email", "credentials": { "sendGridApi": { @@ -2125,10 +2125,10 @@ "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ - 202672, - 81296 + 199984, + 85072 ], - "id": "4d0f4e9d-a21d-4efd-bcd3-386fb51126d7", + "id": "24720d02-c609-43ae-87d7-d9ec62b3f351", "name": "Clear Pending Report In Storm Logs" }, { @@ -2138,10 +2138,10 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 202864, - 81296 + 200176, + 85072 ], - "id": "57680066-4217-4378-a02f-0fcb6ae43a19", + "id": "34c47201-3f8a-449b-a327-7c6c76f6aec8", "name": "Restore Approval Context" }, { @@ -2171,10 +2171,10 @@ "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ - 203056, - 81296 + 200368, + 85072 ], - "id": "f0c2cc10-617c-4c9b-a10f-2fdc5d8fccfe", + "id": "e54d3ac1-57da-4ca7-a7fb-cc3e90367178", "name": "Has Slack Response URL?" }, { @@ -2194,10 +2194,10 @@ "type": "n8n-nodes-base.slack", "typeVersion": 2.4, "position": [ - 202320, - 81088 + 199632, + 84864 ], - "id": "baab5afc-4f89-4fb8-aa9d-dc2893d1ec29", + "id": "8389e862-384a-40ff-ae64-40b9adc777df", "name": "Email Sent Notification", "webhookId": "e1f2a3b4-c5d6-7890-abcd-ef0123456789", "credentials": { @@ -2214,19 +2214,19 @@ { "id": "8a6510f7-a33d-4c87-9472-44329c24e08e", "name": "customer_id", - "value": "=8", + "value": "=9", "type": "number" }, { "id": "e3af6b6b-2360-4900-8d39-8a5fe0fd2f3e", "name": "start_time_epoch_ms", - "value": "=1777819500000", + "value": "=1781315646000", "type": "number" }, { "id": "164778e6-cb52-4f1f-8207-4ec37d404a8f", "name": "end_time_epoch_ms", - "value": "=1777823940000", + "value": "=1781363826000", "type": "number" } ] @@ -2236,10 +2236,10 @@ "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ - 198896, - 80192 + 196208, + 83968 ], - "id": "6ab821eb-53a2-484e-9714-16ec28eaaf21", + "id": "252ea1d9-f46d-4b05-b894-6b637547122a", "name": "Manual Input" }, { @@ -2250,11 +2250,11 @@ }, "type": "n8n-nodes-base.stickyNote", "position": [ - 198176, - 79984 + 195488, + 83760 ], "typeVersion": 1, - "id": "9c1a475c-d01f-48dd-9798-c78d5c316aa4", + "id": "6e2958a5-80c6-4c8a-9011-ab882073c897", "name": "Sticky Note Manual Input" } ], @@ -3232,10 +3232,10 @@ "executionOrder": "v1", "binaryMode": "separate" }, - "versionId": "eadea801-5596-4fa5-b2ee-4c3cee31d48f", + "versionId": "6f66fdd8-206e-4243-a9b2-fd962cecfbf6", "meta": { "instanceId": "15c4ff3a74619031c77894fe5fb8c0fd585362ef637b1873abd56a139f543e12" }, "id": "cEYNZsrEiJjLLBtD", "tags": [] -} +} \ No newline at end of file diff --git a/examples/wind_turbines_sample.csv b/examples/wind_turbines_sample.csv new file mode 100644 index 0000000..e155bfa --- /dev/null +++ b/examples/wind_turbines_sample.csv @@ -0,0 +1,4 @@ +name,latitude,longitude,farm_name +WTG-01,39.854321,32.765432,Sample Wind Farm +WTG-02,39.855100,32.766200,Sample Wind Farm +WTG-03,39.855880,32.766968,Sample Wind Farm diff --git a/report_service/adapter.py b/report_service/adapter.py index e754fa3..4fc4ebc 100644 --- a/report_service/adapter.py +++ b/report_service/adapter.py @@ -31,6 +31,7 @@ _LIGHTNING_COLUMN_ALIASES: dict[str, list[str]] = { "time", "timestamp", ], + "ic_height": ["ic_height", "inCloudHeight", "in_cloud_height", "InCloudHeight"], } _TURBINE_COLUMN_ALIASES: dict[str, list[str]] = { @@ -158,6 +159,9 @@ def _build_lightning_df( if "current_abs" not in df.columns: df["current_abs"] = df["current"].abs() + if "ic_height" in df.columns: + df["ic_height"] = pd.to_numeric(df["ic_height"], errors="coerce") + return df diff --git a/src/reporting/docx.py b/src/reporting/docx.py index 33c1556..99a6984 100644 --- a/src/reporting/docx.py +++ b/src/reporting/docx.py @@ -4,7 +4,7 @@ import io import os from dataclasses import dataclass from datetime import datetime -from typing import Any, Iterable +from typing import Any, Iterable, Literal import pandas as pd import matplotlib @@ -163,6 +163,24 @@ def _add_paragraph(doc: Document, text: str, size_pt: int = 11, align: WD_ALIGN_ r.font.size = Pt(size_pt) +def _add_multiline_paragraph( + doc: Document, + text: str, + size_pt: int = 11, + align: WD_ALIGN_PARAGRAPH = WD_ALIGN_PARAGRAPH.LEFT, +) -> None: + lines = [line.strip() for line in text.splitlines() if line.strip()] + if not lines: + return + p = doc.add_paragraph() + p.alignment = align + for index, line in enumerate(lines): + if index > 0: + p.add_run().add_break() + run = p.add_run(line) + run.font.size = Pt(size_pt) + + def _add_bullets(doc: Document, items: Iterable[str], size_pt: int = 10) -> None: for item in items: p = doc.add_paragraph(style="List Bullet") @@ -499,6 +517,56 @@ def _fit_one_line_table_layout( return font_pt, [w * scale for w in required] +def _add_detailed_strike_list_section( + doc: Document, + title: str, + description: str, + empty_message: str, + centroid_lat: float, + centroid_lng: float, + lightning_df: pd.DataFrame, + strike_type: Literal["cg", "ic"], + content_width: float, + lang, +) -> None: + _add_title(doc, title, size_pt=14) + _add_multiline_paragraph(doc, description, size_pt=10) + table_data, row_colors = build_lightning_event_table_data( + centroid_lat, + centroid_lng, + lightning_df, + lang, + strike_type=strike_type, + ) + if len(table_data) <= 1: + _add_paragraph(doc, empty_message, size_pt=10) + return + + table_data[0] = [ + str(h).replace(" ", "\u00A0", 1) if " " in str(h) else str(h) for h in table_data[0] + ] + available_width_cm = float(content_width) * 2.54 + if strike_type == "ic": + min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.4, 1.8] + else: + min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.8] + font_pt, col_widths_cm = _fit_one_line_table_layout( + table_data=table_data, + available_width_cm=available_width_cm, + min_col_widths_cm=min_col_widths_cm, + max_font_pt=15, + min_font_pt=8, + ) + _add_table( + doc, + table_data, + row_colors=row_colors, + column_widths_cm=col_widths_cm, + font_size_pt=float(font_pt), + autofit=False, + ) + + def _add_table( doc: Document, table_data: list[list[str]], @@ -813,7 +881,7 @@ def create_docx_report( doc.add_page_break() - # Farm-wide maps + charts + risk table, anchored at the n8n-supplied centroid. + # Risk table, then farm-wide maps and charts anchored at the n8n-supplied centroid. centroid_row = pd.Series({ "lat": centroid_lat, "lng": centroid_lng, @@ -825,6 +893,13 @@ def create_docx_report( has_cg = bool(((type_values == "0") & mask_within).any()) has_ic = bool(((type_values != "0") & mask_within).any()) + risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang) + if risk_table_data and len(risk_table_data) > 1: + _add_title(doc, s.turbine_risk_assessment, size_pt=14) + _add_paragraph(doc, s.turbine_risk_assessment_desc, size_pt=10) + _add_table(doc, risk_table_data, row_colors=risk_row_colors) + doc.add_page_break() + if has_cg: _add_title(doc, s.cloud_to_ground_lightnings, size_pt=14) if start_date and end_date: @@ -849,7 +924,19 @@ def create_docx_report( ) if cg_chart is not None: _add_image_from_bytes(doc, cg_chart.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width) - doc.add_page_break() + _add_detailed_strike_list_section( + doc, + s.detailed_cg_events, + s.detailed_cg_events_desc, + s.detailed_cg_events_empty, + centroid_lat, + centroid_lng, + lightning_df, + "cg", + content_width, + lang, + ) + doc.add_page_break() if has_ic: _add_title(doc, s.intercloud_lightnings, size_pt=14) @@ -875,13 +962,18 @@ def create_docx_report( ) if ic_chart is not None: _add_image_from_bytes(doc, ic_chart.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width) - doc.add_page_break() - - risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang) - if risk_table_data and len(risk_table_data) > 1: - _add_title(doc, s.turbine_risk_assessment, size_pt=14) - _add_paragraph(doc, s.turbine_risk_assessment_desc, size_pt=10) - _add_table(doc, risk_table_data, row_colors=risk_row_colors) + _add_detailed_strike_list_section( + doc, + s.detailed_ic_events, + s.detailed_ic_events_desc, + s.detailed_ic_events_empty, + centroid_lat, + centroid_lng, + lightning_df, + "ic", + content_width, + lang, + ) doc.add_page_break() # Daily breakdown @@ -1081,30 +1173,6 @@ def create_docx_report( ) doc.add_page_break() - # Farm-wide lightning event table, anchored at the n8n-supplied centroid. - _add_title(doc, s.detailed_lightning_data, size_pt=14) - table_data, row_colors = build_lightning_event_table_data(centroid_lat, centroid_lng, lightning_df, lang) - if table_data and table_data[0]: - table_data[0] = [str(h).replace(" ", "\u00A0", 1) if " " in str(h) else str(h) for h in table_data[0]] - available_width_cm = float(content_width) * 2.54 - min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.4, 2.0, 1.8] - font_pt, col_widths_cm = _fit_one_line_table_layout( - table_data=table_data, - available_width_cm=available_width_cm, - min_col_widths_cm=min_col_widths_cm, - max_font_pt=15, - min_font_pt=8, - ) - _add_table( - doc, - table_data, - row_colors=row_colors, - column_widths_cm=col_widths_cm, - font_size_pt=float(font_pt), - autofit=False, - ) - doc.add_page_break() - # Appendix _add_title(doc, s.appendix, size_pt=16) _add_title(doc, s.risk_calc_method, size_pt=14) diff --git a/src/reporting/docx_sections.py b/src/reporting/docx_sections.py index e804d16..eee7d47 100644 --- a/src/reporting/docx_sections.py +++ b/src/reporting/docx_sections.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Callable +from typing import Any, Callable, Literal import numpy as np import pandas as pd @@ -280,17 +280,45 @@ def build_lightning_event_table_data( centroid_lng: float, lightning_df: pd.DataFrame, lang: ReportLanguage | None = None, + strike_type: Literal["cg", "ic", "all"] = "all", ) -> tuple[list[list[str]], list[str]]: s = get_strings(lang or get_report_language()) + include_type_column = strike_type == "all" + include_height_column = strike_type != "cg" + + if strike_type == "cg": + source_df = lightning_df[lightning_df["p_type"].astype(str) == "0"].copy() + elif strike_type == "ic": + source_df = lightning_df[lightning_df["p_type"].astype(str) != "0"].copy() + else: + source_df = lightning_df.copy() + + header = [ + s.col_no, + s.col_time_local, + s.col_lat, + s.col_lng, + s.col_current_amps, + ] + if include_height_column: + header.append(s.col_height_m) + if include_type_column: + header.append(s.col_lightning_type) + header.append(s.col_proximity_km) + + if source_df.empty: + return [header], ["lightgrey"] + pre = precompute_distances_and_rings( - centroid_lat, centroid_lng, lightning_df, config.distance_rings + centroid_lat, centroid_lng, source_df, config.distance_rings ) rows: list[list[str]] = [] row_colors: list[str] = [] outermost_km = max(config.distance_rings) / 1000.0 rings_km = [r / 1000.0 for r in config.distance_rings] + proximity_index = len(header) - 1 - for i, rec in enumerate(lightning_df.itertuples(index=False)): + for i, rec in enumerate(source_df.itertuples(index=False)): proximity = float(pre["dists_km"][i]) if proximity > outermost_km: continue @@ -310,48 +338,43 @@ def build_lightning_event_table_data( except Exception: local_time = str(getattr(rec, "local_time", ""))[:19] - lightning_type = s.lightning_type_cg if str(rec.p_type) == "0" else s.lightning_type_ic - height_val = getattr(rec, "ic_height", "") - if height_val == "": - height_val = getattr(rec, "height", "") - - rows.append( - [ - "", - local_time, - f"{rec.lat:.5f}", - f"{rec.lng:.5f}", - str(rec.current), - str(height_val), - lightning_type, - f"{proximity:.2f}", - ] - ) + row = [ + "", + local_time, + f"{rec.lat:.5f}", + f"{rec.lng:.5f}", + str(rec.current), + ] + if include_height_column: + height_val = getattr(rec, "ic_height", "") + if height_val == "" or pd.isna(height_val): + height_val = getattr(rec, "inCloudHeight", "") + row.append("" if height_val == "" or pd.isna(height_val) else str(height_val)) + if include_type_column: + lightning_type = s.lightning_type_cg if str(rec.p_type) == "0" else s.lightning_type_ic + row.append(lightning_type) + row.append(f"{proximity:.2f}") + rows.append(row) row_colors.append(color) - sorted_data = sorted( - zip(rows, row_colors), - key=lambda x: (0 if x[0][6] == s.lightning_type_cg else 1, float(x[0][7])), - ) - if sorted_data: + if rows: + if include_type_column: + type_index = proximity_index - 1 + sorted_data = sorted( + zip(rows, row_colors), + key=lambda x: (0 if x[0][type_index] == s.lightning_type_cg else 1, float(x[0][proximity_index])), + ) + else: + sorted_data = sorted( + zip(rows, row_colors), + key=lambda x: float(x[0][proximity_index]), + ) rows, row_colors = zip(*sorted_data) rows = list(rows) row_colors = list(row_colors) for idx, row in enumerate(rows): row[0] = str(idx + 1) - else: - rows, row_colors = [], [] - header = [ - s.col_no, - s.col_time_local, - s.col_lat, - s.col_lng, - s.col_current_amps, - s.col_height_m, - s.col_lightning_type, - s.col_proximity_km, - ] return [header] + rows, ["lightgrey"] + row_colors diff --git a/src/reporting/strings.py b/src/reporting/strings.py index df98510..9eadb68 100644 --- a/src/reporting/strings.py +++ b/src/reporting/strings.py @@ -98,6 +98,12 @@ class ReportStrings: storm_cells: str storm_cells_daily_viz: str detailed_lightning_data: str + detailed_cg_events: str + detailed_ic_events: str + detailed_cg_events_desc: str + detailed_ic_events_desc: str + detailed_cg_events_empty: str + detailed_ic_events_empty: str appendix: str risk_calc_method: str how_risk_determined: str @@ -308,6 +314,19 @@ _STRINGS_EN = ReportStrings( storm_cells="Storm Cells", storm_cells_daily_viz="Daily storm cells visualization.", detailed_lightning_data="Detailed Lightning Event Data", + detailed_cg_events="Detailed Cloud-to-Ground Lightning List", + detailed_ic_events="Detailed Intercloud Lightning List", + detailed_cg_events_desc=( + "Current (amps): peak current of the cloud-to-ground strike.\n" + "Proximity (km): air distance from the strike to the wind farm centroid." + ), + detailed_ic_events_desc=( + "Current (amps): peak current of the in-cloud strike.\n" + "Height (m): in-cloud flash height.\n" + "Proximity (km): air distance from the strike to the wind farm centroid." + ), + detailed_cg_events_empty="No cloud-to-ground lightning events were recorded within the analysis area during this period.", + detailed_ic_events_empty="No intercloud lightning events were recorded within the analysis area during this period.", appendix="Appendix", risk_calc_method="1. Risk Calculation Method", how_risk_determined="How Risk Scores Are Determined:", @@ -328,14 +347,14 @@ _STRINGS_EN = ReportStrings( max_risk_score="Maximum Risk Score: ~2.500 (close distance, high current)", typical_range="Typical Range: 0.010 - 1.000 for most lightning events", risk_categories="Risk Score Categories (Fixed Color Intervals):", - risk_cat_very_low="Very Low Risk (<0.1): Blue - Distant lightning with low current", - risk_cat_low="Low Risk (0.1-0.2): Teal - Moderate distance lightning", - risk_cat_med_low="Med-Low Risk (0.2-0.4): Green - Closer lightning", - risk_cat_medium="Medium Risk (0.4-0.6): Yellow - Moderate risk lightning", - risk_cat_med_high="Med-High Risk (0.6-0.8): Orange - High risk lightning", - risk_cat_high="High Risk (0.8-1.0): Dark Orange - Very high risk lightning", - risk_cat_very_high="Very High Risk (1.0-1.2): Red - Extreme risk lightning", - risk_cat_critical="Critical Risk (>1.2): Dark Red - Critical risk lightning", + risk_cat_very_low="Very Low Risk (<0.1): Blue", + risk_cat_low="Low Risk (0.1-0.2): Teal", + risk_cat_med_low="Med-Low Risk (0.2-0.4): Green", + risk_cat_medium="Medium Risk (0.4-0.6): Yellow", + risk_cat_med_high="Med-High Risk (0.6-0.8): Orange", + risk_cat_high="High Risk (0.8-1.0): Dark Orange", + risk_cat_very_high="Very High Risk (1.0-1.2): Red", + risk_cat_critical="Critical Risk (>1.2): Dark Red", risk_chart="3. Risk Score Calculation Chart", chart_reference="Chart Reference Guide:", risk_chart_desc_1="The following chart shows how distance and current magnitude affect risk scores.", @@ -527,6 +546,19 @@ _STRINGS_TR = ReportStrings( storm_cells="Fırtına Hücreleri", storm_cells_daily_viz="Fırtına hücrelerinin dağılımı", detailed_lightning_data="Ayrıntılı Yıldırım Olay Verileri", + detailed_cg_events="Ayrıntılı Yıldırım Listesi", + detailed_ic_events="Ayrıntılı Şimşek Listesi", + detailed_cg_events_desc=( + "Akım (amper): yıldırımın akım büyüklüğü.\n" + "Yakınlık (km): yıldırımın rüzgar çiftliğinin merkez noktasına (centroid) olan kuş uçuşu mesafesi." + ), + detailed_ic_events_desc=( + "Akım (amper): şimşeğin akım büyüklüğü.\n" + "Yükseklik (m): şimşeğin oluştuğu yükseklik.\n" + "Yakınlık (km): şimşeğin rüzgar çiftliğinin merkez noktasına olan kuş uçuşu mesafesi." + ), + detailed_cg_events_empty="Bu dönemde analiz alanında yıldırım olayı kaydedilmemiştir.", + detailed_ic_events_empty="Bu dönemde analiz alanında şimşek olayı kaydedilmemiştir.", appendix="Ek", risk_calc_method="1. Risk Hesaplama Yöntemi", how_risk_determined="Risk Skorları Nasıl Belirlenir:", @@ -547,14 +579,14 @@ _STRINGS_TR = ReportStrings( max_risk_score="Maksimum Risk Skoru: ~2.500 (yakın mesafe, yüksek akım)", typical_range="Tipik Aralık: çoğu yıldırım olayı için 0.010 - 1.000", risk_categories="Risk Skoru Kategorileri (Sabit Renk Aralıkları):", - risk_cat_very_low="Çok Düşük Risk (<0.1): Mavi - Uzak, düşük akımlı yıldırım", - risk_cat_low="Düşük Risk (0.1-0.2): Turkuaz - Orta mesafeli yıldırım", - risk_cat_med_low="Orta-Düşük Risk (0.2-0.4): Yeşil - Daha yakın yıldırım", - risk_cat_medium="Orta Risk (0.4-0.6): Sarı - Orta riskli yıldırım", - risk_cat_med_high="Orta-Yüksek Risk (0.6-0.8): Turuncu - Yüksek riskli yıldırım", - risk_cat_high="Yüksek Risk (0.8-1.0): Koyu Turuncu - Çok yüksek riskli yıldırım", - risk_cat_very_high="Çok Yüksek Risk (1.0-1.2): Kırmızı - Aşırı riskli yıldırım", - risk_cat_critical="Kritik Risk (>1.2): Koyu Kırmızı - Kritik riskli yıldırım", + risk_cat_very_low="Çok düşük risk (<0.1): Mavi", + risk_cat_low="Düşük Risk (0.1-0.2): Turkuaz", + risk_cat_med_low="Orta-Düşük Risk (0.2-0.4): Yeşil", + risk_cat_medium="Orta Risk (0.4-0.6): Sarı", + risk_cat_med_high="Orta-Yüksek Risk (0.6-0.8): Turuncu", + risk_cat_high="Yüksek Risk (0.8-1.0): Koyu Turuncu", + risk_cat_very_high="Çok Yüksek Risk (1.0-1.2): Kırmızı", + risk_cat_critical="Kritik Risk (>1.2): Koyu Kırmızı", risk_chart="3. Risk Skoru Hesaplama Grafiği", chart_reference="Grafik Referans Kılavuzu:", risk_chart_desc_1="Aşağıdaki grafik, mesafe ve akım büyüklüğünün risk skorlarını nasıl etkilediğini gösterir.", diff --git a/src/visualization/maps.py b/src/visualization/maps.py index b13fb92..8cdfb0c 100644 --- a/src/visualization/maps.py +++ b/src/visualization/maps.py @@ -11,6 +11,8 @@ COORDINATE_PLANE_RING_LINE_WIDTH = 4 COORDINATE_PLANE_LIGHTNING_SIZE_MIN = 10 COORDINATE_PLANE_LIGHTNING_SIZE_MAX = 24 COORDINATE_PLANE_LIGHTNING_CURRENT_SCALE = 800 +COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE = 9 +COORDINATE_PLANE_TURBINE_TEXT_POSITION = 'middle center' def plot_turbine_map(turbine_row: pd.Series, lightning_df: pd.DataFrame, turbine_df: pd.DataFrame) -> go.Figure: turbine_lat = turbine_row['lat'] @@ -371,8 +373,8 @@ def plot_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.DataFrame, tu line=dict(color='black', width=1) ), text=turbine_df['name'].tolist(), - textfont=dict(size=18, color='black'), # turbine name label font size - textposition='middle center', + textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'), + textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION, name='Wind Turbines', showlegend=True )) @@ -394,7 +396,7 @@ def plot_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.DataFrame, tu line=dict(color='black', width=1) ), text=['S'] * len(storm_df), - textfont=dict(size=18, color='white'), # storm "S" label font size + textfont=dict(size=18, color='white'), textposition='middle center', name='Storm Cells', showlegend=True @@ -521,8 +523,8 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da line=dict(color='black', width=1) ), text=turbine_df['name'].tolist(), - textfont=dict(size=24, color='black'), - textposition='middle center', + textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'), + textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION, name=s.map_wind_turbines, showlegend=True )) @@ -694,8 +696,8 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df: line=dict(color='black', width=1) ), text=turbine_df['name'].tolist(), - textfont=dict(size=18, color='black'), # turbine name label font size - textposition='middle center', + textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'), + textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION, name=s.map_wind_turbines, showlegend=True )) @@ -717,7 +719,7 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df: line=dict(color='black', width=1) ), text=['S'] * len(storm_df), - textfont=dict(size=18, color='white'), # storm "S" label font size + textfont=dict(size=18, color='white'), textposition='middle center', name=s.map_storm_cells, showlegend=True diff --git a/src/visualization/storm_cells.py b/src/visualization/storm_cells.py index 177ed6b..0f7729c 100644 --- a/src/visualization/storm_cells.py +++ b/src/visualization/storm_cells.py @@ -13,6 +13,10 @@ from src.config import config from src.reporting.strings import ReportLanguage, get_report_language, get_strings from src.utils import parse_period_string_to_datetime from src.visualization.basemap import add_satellite_basemap +from src.visualization.maps import ( + COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, + COORDINATE_PLANE_TURBINE_TEXT_POSITION, +) def format_datetime_for_display(datetime_str: str) -> str: """ @@ -177,8 +181,8 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D line=dict(color='black', width=1) ), text=turbine_df['name'].tolist(), - textfont=dict(size=12, color='black'), - textposition='middle center', + textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'), + textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION, name=s.map_wind_turbines, showlegend=True, hovertemplate=(