159 lines
5.7 KiB
Python
Executable File
159 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
CRM Pipeline Daily Observer — doctrine 62 Phase 2 (observation J+1 to J+7)
|
|
Runs daily at 9am via cron. Uses Playwright to screenshot + fetch API + validate delta.
|
|
Alerts via Blade queue if delta < 500/day.
|
|
"""
|
|
import json, os, sys, time, urllib.request, urllib.parse, datetime
|
|
from pathlib import Path
|
|
|
|
LOG_FILE = "/var/log/weval/crm-observation.log"
|
|
SCREENSHOTS_DIR = "/var/www/html/api/crm-observation-screenshots"
|
|
BLADE_KEY = "BLADE2026"
|
|
DELTA_ALERT_THRESHOLD = 500
|
|
API_STATUS = "https://weval-consulting.com/api/crm-pipeline-live.php?action=status"
|
|
PAGE_URL = "https://weval-consulting.com/crm-pipeline-live.html"
|
|
|
|
os.makedirs(SCREENSHOTS_DIR, exist_ok=True)
|
|
Path(LOG_FILE).parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
def log(msg):
|
|
line = f"[{datetime.datetime.now().isoformat()}] {msg}\n"
|
|
print(line, end="")
|
|
try:
|
|
with open(LOG_FILE, "a") as f: f.write(line)
|
|
except Exception as e:
|
|
print(f"log write err: {e}")
|
|
|
|
def fetch_api():
|
|
"""Fetch API status + parse delta"""
|
|
try:
|
|
r = urllib.request.urlopen(API_STATUS, timeout=10)
|
|
d = json.loads(r.read().decode())
|
|
return d
|
|
except Exception as e:
|
|
log(f"API_FETCH_ERR {e}")
|
|
return None
|
|
|
|
def playwright_screenshot():
|
|
"""Take screenshot of dashboard via playwright"""
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
except ImportError:
|
|
log("PLAYWRIGHT_NOT_INSTALLED")
|
|
return None
|
|
|
|
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
screenshot = f"{SCREENSHOTS_DIR}/crm-pipeline-{ts}.png"
|
|
try:
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
|
|
ctx = browser.new_context(viewport={"width": 1400, "height": 900}, ignore_https_errors=True)
|
|
page = ctx.new_page()
|
|
page.goto(PAGE_URL, wait_until="networkidle", timeout=30000)
|
|
time.sleep(2) # let JS render
|
|
page.screenshot(path=screenshot, full_page=True)
|
|
browser.close()
|
|
log(f"SCREENSHOT_OK {screenshot}")
|
|
return screenshot
|
|
except Exception as e:
|
|
log(f"PLAYWRIGHT_ERR {e}")
|
|
return None
|
|
|
|
def send_blade_alert(msg, cmd=None):
|
|
"""Push alert to Blade queue for Windows-side notification"""
|
|
try:
|
|
data = urllib.parse.urlencode({
|
|
"k": BLADE_KEY,
|
|
"name": "CRM Observation Alert",
|
|
"cmd": cmd or f"Write-Host 'CRM ALERT: {msg}'; New-BurntToastNotification -Text 'WEVAL CRM', '{msg}' -ErrorAction SilentlyContinue",
|
|
"type": "powershell",
|
|
"priority": "high"
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
"https://weval-consulting.com/api/blade-task-queue.php?k=BLADE2026&action=add",
|
|
data=data, method="POST"
|
|
)
|
|
r = urllib.request.urlopen(req, timeout=10)
|
|
d = json.loads(r.read().decode())
|
|
log(f"BLADE_ALERT_SENT {d.get('task_id', '?')}")
|
|
return True
|
|
except Exception as e:
|
|
log(f"BLADE_ALERT_ERR {e}")
|
|
return False
|
|
|
|
def main():
|
|
log("=== DAILY OBSERVATION START ===")
|
|
|
|
# 1. Fetch API
|
|
data = fetch_api()
|
|
if not data or not data.get("ok"):
|
|
log("API_UNAVAILABLE")
|
|
send_blade_alert("API crm-pipeline-live DOWN")
|
|
return 1
|
|
|
|
total = data.get("send_contacts_total", 0)
|
|
delta_today = data.get("delta_today", 0)
|
|
runs_ok = data.get("runs_ok_24h", 0)
|
|
runs_err = data.get("runs_err_24h", 0)
|
|
last_age = data.get("last_run_age", "?")
|
|
cron_status = data.get("cron_status", "?")
|
|
|
|
log(f"STATS total={total} delta_today={delta_today} ok_24h={runs_ok} err_24h={runs_err} last={last_age} cron={cron_status}")
|
|
|
|
# 2. Playwright screenshot
|
|
screenshot = playwright_screenshot()
|
|
|
|
# 3. Decision: alert or pass
|
|
alert_triggered = False
|
|
alert_reasons = []
|
|
|
|
if cron_status != "active":
|
|
alert_reasons.append(f"cron_status={cron_status} (not active)")
|
|
alert_triggered = True
|
|
|
|
if runs_err > runs_ok and runs_ok + runs_err > 5:
|
|
alert_reasons.append(f"err>ok ({runs_err}>{runs_ok})")
|
|
alert_triggered = True
|
|
|
|
# Day-N check: after J+2, if delta still 0 → alert
|
|
day_since_reactivation = (datetime.datetime.now() - datetime.datetime(2026, 4, 17, 14, 23)).days
|
|
if day_since_reactivation >= 2 and delta_today < DELTA_ALERT_THRESHOLD:
|
|
alert_reasons.append(f"delta_today={delta_today} < {DELTA_ALERT_THRESHOLD} (day {day_since_reactivation} after reactivation)")
|
|
alert_triggered = True
|
|
|
|
if alert_triggered:
|
|
msg = f"Day{day_since_reactivation}: {' | '.join(alert_reasons)}"
|
|
log(f"ALERT {msg}")
|
|
send_blade_alert(msg)
|
|
else:
|
|
log(f"OK day{day_since_reactivation}: delta={delta_today}, runs ok/err={runs_ok}/{runs_err}")
|
|
|
|
# 4. Write daily report JSON
|
|
report = {
|
|
"ts": datetime.datetime.now().isoformat(),
|
|
"day_since_reactivation": day_since_reactivation,
|
|
"total": total,
|
|
"delta_today": delta_today,
|
|
"runs_ok_24h": runs_ok,
|
|
"runs_err_24h": runs_err,
|
|
"last_run_age": last_age,
|
|
"cron_status": cron_status,
|
|
"alert_triggered": alert_triggered,
|
|
"alert_reasons": alert_reasons,
|
|
"screenshot": screenshot,
|
|
}
|
|
report_file = f"/var/www/html/api/crm-observation-latest.json"
|
|
try:
|
|
with open(report_file, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
log(f"REPORT_SAVED {report_file}")
|
|
except Exception as e:
|
|
log(f"REPORT_ERR {e}")
|
|
|
|
log("=== END ===\n")
|
|
return 0 if not alert_triggered else 2
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|