crm-observation.py: script Python Playwright pour phase 2 observation 7j (doctrine 62) · API fetch + screenshot + report JSON + Blade alert si delta<500 apres J+2 · daily 9h cron installé · intent WEVIA observe_crm_pipeline wire
This commit is contained in:
158
crm-observation.py
Executable file
158
crm-observation.py
Executable file
@@ -0,0 +1,158 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user