# tools/send_summary_email.rb # # Sends an HTML summary email via SMTP when scraper errors are present. # Called by run_all.sh; reads summary data from stdin in the format: # # Line 1: finish timestamp (UTC) # Line 2: total scraper count # Line 3: total saved # Line 4: total warns # Lines 5+: pipe-delimited summary rows — name|saved|warns|status # # Required env vars (set via docker-compose.yml from .env): # SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, # SMTP_SMTPSecure, SMTP_SENTFROM, SMTP_ADDADDRESS require "net/smtp" require "openssl" # ── Read SMTP config ──────────────────────────────────────────────────────── host = ENV.fetch("SMTP_HOST", "") port = ENV.fetch("SMTP_PORT", "587").to_i username = ENV.fetch("SMTP_USERNAME", "") password = ENV.fetch("SMTP_PASSWORD", "") secure = ENV.fetch("SMTP_SMTPSecure", "tls").downcase # "tls" or "ssl" from = ENV.fetch("SMTP_SENTFROM", "") to = ENV.fetch("SMTP_ADDADDRESS", "") if host.empty? || from.empty? || to.empty? warn "[send_summary_email] SMTP not configured — skipping email" exit 0 end # ── Read stdin ─────────────────────────────────────────────────────────────── lines = $stdin.read.split("\n") finish_time = lines[0].to_s.strip total_count = lines[1].to_s.strip total_saved = lines[2].to_s.strip total_warns = lines[3].to_s.strip entries = lines[4..] || [] rows = entries.map do |e| parts = e.split("|") { name: parts[0].to_s, saved: parts[1].to_s, warns: parts[2].to_s, status: parts[3].to_s.strip } end error_rows = rows.select { |r| r[:status] == "ERROR" } blocked_rows = rows.select { |r| r[:status] == "blocked" } warn_rows = rows.select { |r| r[:status] == "warn" } # ── Build subject ──────────────────────────────────────────────────────────── error_count = error_rows.size subject = if error_count > 0 "TAS Councils Scraper — #{error_count} error(s) — #{finish_time} UTC" else "TAS Councils Scraper — completed with warnings — #{finish_time} UTC" end # ── Build HTML body ────────────────────────────────────────────────────────── STATUS_COLOUR = { "ok" => "#198754", "warn" => "#856404", "blocked" => "#856404", "ERROR" => "#dc3545" }.freeze STATUS_BG = { "ok" => "#d1e7dd", "warn" => "#fff3cd", "blocked" => "#fff3cd", "ERROR" => "#f8d7da" }.freeze row_html = rows.map do |r| colour = STATUS_COLOUR.fetch(r[:status], "#333") bg = STATUS_BG.fetch(r[:status], "#fff") <<~TR
Finished #{finish_time} UTC
#{error_rows.any? ? "⚠ #{error_rows.size} scraper(s) exited with errors: #{error_rows.map { |r| r[:name] }.join(', ')}
" : ""} #{blocked_rows.any? ? "⚠ #{blocked_rows.size} scraper(s) blocked by WAF/Cloudflare: #{blocked_rows.map { |r| r[:name] }.join(', ')}
" : ""}| Council | Saved | Warns | Status |
|---|---|---|---|
| TOTAL (#{total_count} scrapers) | #{total_saved} | #{total_warns} |