""" alert.py — Shared alerting module for Performance West monitor scripts. Creates an ERPNext Issue when a posting account is broken or automation fails. Import and call: alert_account_broken(monitor, platform, error) """ import json import urllib.request import urllib.parse import os from pathlib import Path # ERPNext config — reads from env ERPNEXT_URL = os.environ.get("ERPNEXT_URL", "http://erpnext:8080") ERPNEXT_API_KEY = os.environ.get("ERPNEXT_API_KEY", "") ERPNEXT_API_SECRET = os.environ.get("ERPNEXT_API_SECRET", "") # Fallback: use Express API to create issues if ERPNext is unreachable API_URL = os.environ.get("PW_API_URL", "http://api:3001") WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "") # Only create one alert per monitor+platform per day _ALERT_STATE_FILE = Path.home() / ".monitor-alert-state.json" def _load_alert_state(): if _ALERT_STATE_FILE.exists(): try: return json.loads(_ALERT_STATE_FILE.read_text()) except Exception: pass return {} def _save_alert_state(state): _ALERT_STATE_FILE.write_text(json.dumps(state, indent=2)) def alert_account_broken(monitor: str, platform: str, error: str, detail: str = ""): """ Create an ERPNext Issue alerting that a posting account is broken. Args: monitor: Script name e.g. "reddit-monitor", "formation-worker" platform: Platform name e.g. "Reddit", "Wyoming SOS Portal" error: Short error description detail: Optional longer detail / stack trace """ from datetime import datetime, timezone today = datetime.now(timezone.utc).strftime("%Y-%m-%d") key = f"{monitor}:{platform}:{today}" state = _load_alert_state() if state.get(key): return subject = f"[Monitor Alert] {platform} — {monitor}: {error[:80]}" description = ( f"The **{monitor}** script detected a failure on **{platform}**.\n\n" f"**Error:** {error}\n\n" f"**Detail:**\n```\n{detail or 'No additional detail'}\n```\n\n" f"**Action required:** Check the account credentials / API key for {platform} " f"and update the configuration.\n\n" f"**Date:** {today}" ) issue_name = None # Try ERPNext first if ERPNEXT_API_KEY and ERPNEXT_API_SECRET: issue_name = _create_erpnext_issue(subject, description) # Fallback: Express API internal endpoint if not issue_name: issue_name = _create_api_issue(subject, description) if issue_name: print(f"[alert] ERPNext Issue '{issue_name}' created for {platform} failure") state[key] = {"issue_name": issue_name, "error": error} _save_alert_state(state) else: print(f"[alert] Failed to create alert for {platform} failure: {error}") def _create_erpnext_issue(subject: str, description: str) -> str | None: """Create an Issue in ERPNext directly via REST API.""" payload = json.dumps({ "data": json.dumps({ "doctype": "Issue", "subject": subject, "description": description, "issue_type": "Bug", "priority": "High", }) }).encode() req = urllib.request.Request( f"{ERPNEXT_URL}/api/resource/Issue", data=payload, headers={ "Authorization": f"token {ERPNEXT_API_KEY}:{ERPNEXT_API_SECRET}", "Content-Type": "application/json", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=10) as r: resp = json.loads(r.read()) return resp.get("data", {}).get("name", "") except Exception as e: print(f"[alert] ERPNext Issue creation failed: {e}") return None def _create_api_issue(subject: str, description: str) -> str | None: """Fallback: create a ticket via our Express API.""" payload = json.dumps({ "category": "issue", "subject": subject, "message": description, "email": "alerts@performancewest.net", "name": "System Monitor", }).encode() req = urllib.request.Request( f"{API_URL}/api/v1/tickets", data=payload, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=10) as r: resp = json.loads(r.read()) return resp.get("ticket_id", "") except Exception as e: print(f"[alert] Express API ticket creation failed: {e}") return None