# Lightning Report (n8n) Automated lightning activity reports for wind farms. An **n8n** workflow detects strikes via the [iklim.co](https://iklim.co) API, generates a Word report through a **Python report service**, routes it through **Slack** for human approval, and emails the approved report to customers via **SendGrid**. ## Architecture ```text ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │ n8n workflow│────▶│ iklim.co API │ │ Report service │ │ (scheduled) │ │ lightnings + │ │ FastAPI /generate │ │ │ │ thunderstorms │ │ → DOCX │ └──────┬──────┘ └──────────────────┘ └──────────▲──────────┘ │ │ │ Slack upload + approve/reject buttons │ ▼ │ ┌─────────────┐ storm_logs datatable (pending report) │ │ Slack │───────────────────────────────────────────┘ └──────┬──────┘ │ on approve ▼ ┌─────────────┐ │ SendGrid │──▶ customer contact_email └─────────────┘ ``` ## Repository layout | Path | Description | |------|-------------| | `Lightning_Report_Automatic.json` | n8n workflow export (import in n8n) | | `report_service/` | FastAPI wrapper around report generation | | `report_service/adapter.py` | Maps n8n JSON payload → pandas DataFrames | | `src/reporting/docx.py` | Main DOCX builder | | `src/analysis/` | Risk, histogram, geospatial, statistics | | `src/visualization/` | Maps and storm-cell plots | | `src/config.py` | Default analysis parameters | ## Prerequisites - **Python 3.12+** (local or Docker) - **n8n** (self-hosted, with Data Tables enabled) - **iklim.co API** credentials (test: `https://api-test.iklim.co`) - **Slack** app/bot for uploads and approval buttons - **SendGrid** API key with Mail Send permission - Optional: **Gemini API key** if commentary is generated inside the report service (otherwise pass `gemini_text` from n8n) ## Report service (local) ### 1. Install dependencies ```bash cd Lightning_Report_n8n python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt -r report_service/requirements.txt ``` ### 2. Environment variables Create a `.env` file in the project root (never commit it): ```env REPORT_SERVICE_TOKEN=your-long-random-secret GEMINI_API_KEY=optional-if-not-supplied-by-n8n GEMINI_MODEL=gemini-2.5-flash-lite LOG_LEVEL=INFO ``` `REPORT_SERVICE_TOKEN` is required. The service refuses `/generate` if it is unset. ### 3. Run the server ```bash export REPORT_SERVICE_TOKEN="your-long-random-secret" uvicorn report_service.main:app --host 0.0.0.0 --port 8000 ``` Health check: `GET http://localhost:8000/health` ### 4. Expose to n8n (development) If n8n runs elsewhere, tunnel the service: ```bash ngrok http 8000 ``` Point the n8n **Generate Report** HTTP Request node at `https:///generate` and set header `X-Report-Token` to the same value as `REPORT_SERVICE_TOKEN`. ## Report service (Docker) ```bash cd Lightning_Report_n8n export REPORT_SERVICE_TOKEN=your-long-random-secret docker compose -f report_service/docker-compose.yml up --build -d ``` From n8n on the same Docker network: `http://report-service:8000/generate` ## API ### `GET /health` Liveness probe. Returns `{"ok": true, ...}`. ### `POST /generate` | Header | Required | Description | |--------|----------|-------------| | `X-Report-Token` | Yes | Must match `REPORT_SERVICE_TOKEN` | | `Content-Type` | Yes | `application/json` | **Body** (built by n8n **Build Report Payload** node): | Field | Description | |-------|-------------| | `customer_name` | Wind farm / customer label | | `t_start`, `t_end` | Storm window (epoch ms) | | `centroid_lat`, `centroid_lon` | Farm centroid | | `boundary_m` | Monitoring radius (m) | | `rings` | `{ r1, r2, r3, r4 }` distance rings | | `timezone` | e.g. `Europe/Istanbul` | | `turbines` | Array of turbine objects | | `strikes` | Lightning strike records (`captured` ISO time preferred) | | `storm_records` | Thunderstorm polygons from `/v1/thunderstorms/within` | | `gemini_text` | Optional pre-generated commentary | | `language` | Optional report language: `en` (default) or `tr` | **Response:** DOCX file (`application/vnd.openxmlformats-officedocument.wordprocessingml.document`). ## n8n workflow Import `Lightning_Report_Automatic.json` into n8n and configure: ### Credentials | Integration | Used for | |-------------|----------| | iklim.co login | `Login to iklim.co` / token refresh | | Slack (`n8n_lightning_report_bot`) | Report upload, approval buttons | | Slack (`Tarla Slack Account`) | Error / email-sent notifications | | SendGrid | Customer email after approval | ### Data tables **`customers`** | Column | Purpose | |--------|---------| | `customer_name` | Display name | | `contact_email` | SendGrid recipient | | `language` | Report language: `en` or `tr` (also accepts `turkish`, `Türkçe`) | | `id` | Linked from wind turbines | **`wind_turbine_farm`** | Column | Purpose | |--------|---------| | `customer_id` | FK to customers | | `latitude`, `longitude` | Turbine positions | | `name` | Turbine label | **`storm_logs`** | Column | Purpose | |--------|---------| | `customer_name`, `storm_key`, `storm_start`, `storm_end` | Storm metadata | | `total_strikes`, `status` | Count; `waiting for confirmation` → `confirmed` / `rejected` | | `pending_contact_email` | Email snapshot at report time | | `pending_email_file_name` | DOCX filename | | `pending_report_base64` | Queued report (survives n8n restarts) | | `pending_report_mime_type` | MIME type for attachment | ### Main flow (simplified) 1. Daily trigger → authenticate → loop customers. 2. Query lightnings within farm boundary; detect storm window. 3. Insert/update `storm_logs`, fetch thunderstorms, build payload (include `language` from the customer row). 4. **Generate Report** → upload DOCX to Slack → approval buttons. 5. Persist pending report fields on `storm_logs`. 6. On Slack **Approve** → load queued report → **SendGrid** email → clear pending fields. Email is sent only after approval, not when the report is first generated. ## Troubleshooting ### `channel_not_found` (Slack) The Slack channel ID in the node does not exist or the bot is not invited. Re-select the channel in n8n and run `/invite @YourBot` in that channel. ### `Account Is not Found For User …` (`/v1/thunderstorms/within`) Login succeeded but the iklim user has no **account** on the API environment (e.g. test). Ask iklim.co to provision the user, or use a service account that has thunderstorm access. Lightning-only reports can continue if the workflow skips failed thunderstorm calls (optional branch). ### ngrok `503` / `ERR_NGROK_3004` on **Generate Report** ngrok closes long-lived HTTP connections when the report service takes several minutes to respond (Kaleido chart export). The HTML `503` page with `ERR_NGROK_3004` means ngrok never received a complete response from uvicorn. **Fix:** Re-import `Lightning_Report_Automatic.json`. Report generation uses separate n8n nodes (not one long Code node): 1. **Start Async Report** — `POST /generate/async` 2. **Wait for Report** — 15s pause 3. **Poll Report Status** — `GET /generate/async/{job_id}` (loops until complete) 4. **Download Report** — `GET /generate/async/{job_id}/download` Restart uvicorn after pulling report-service changes. Update the ngrok URL in the three HTTP nodes when your tunnel URL changes. ### `Task execution timed out after 60 seconds` on report generation n8n Code nodes are capped at **60 seconds**. Do not put polling loops inside a single Code node. Use the multi-node async flow above instead. **Alternative:** Skip ngrok and call the service directly (`http://host.docker.internal:8000` from Docker n8n, or `http://127.0.0.1:8000` on the same host). ### `timeout of 300000ms exceeded` on **Generate Report** The report service can take several minutes when many lightning strikes produce multiple Kaleido chart exports (maps, histograms, heatmaps). n8n’s HTTP Request node defaults to **5 minutes (300000 ms)**. **Fixes (use together):** 1. Re-import `Lightning_Report_Automatic.json` — the **Generate Report** node sets `timeout: 900000` (15 min). 2. On self-hosted n8n, raise the global ceiling if needed: ```bash N8N_DEFAULT_REQUEST_TIMEOUT=900000 EXECUTIONS_TIMEOUT=3600 EXECUTIONS_TIMEOUT_MAX=3600 ``` 3. Prefer **Docker network** (`http://report-service:8000/generate`) over ngrok when n8n and the report service run on the same host — fewer proxy timeouts. 4. Watch report-service logs: each `kaleido` export spawns Chrome briefly; very large datasets are capped by `histogram_params.max_periods` (default 8 activity periods). ### Histogram shows `01-01-1970` / wrong period Strike timestamps were parsed incorrectly when `timestamp` (epoch ms) was preferred over `captured` (ISO). Fixed in `report_service/adapter.py` — restart the report service after pulling updates. ### SendGrid: attachment must be base64 encoded Use the **Prepare SendGrid Email Payload** node path; ensure `pending_report_base64` in `storm_logs` is populated and not truncated by datatable size limits. ### Binary missing at **Upload Report to Slack** The datatable persist step must run **after** Slack upload, not before (persist nodes drop binary data). ### Slack `invalid_payload` on approval follow-up Do not double-`JSON.stringify` the body when posting to `response_url`. Slack response URLs expire after ~30 minutes (optional confirmation message only). ## Development notes - Per-farm settings (`rings`, centroid, dates, `language`) come from the n8n payload via `apply_farm_config()`, not hardcoded in `src/config.py`. - Set `customers.language` to `tr` for Turkish DOCX output; omit or use `en` for English. - Prefer strike field **`captured`** (ISO) for `local_time`; numeric **`timestamp`** is supported as epoch milliseconds. - HMAC signing for iklim API calls is implemented in n8n Code nodes (`Calculate Lightning Headers`, `Calculate Thunderstorm Headers`). ## License Internal use — iklim.co lightning reporting pipeline.