diff --git a/api/blade-actions-surfaced.json b/api/blade-actions-surfaced.json
index ec79fabc5..51d6cfd76 100644
--- a/api/blade-actions-surfaced.json
+++ b/api/blade-actions-surfaced.json
@@ -1,15 +1,15 @@
{
- "generated_at": "2026-04-20T02:30:01.698166",
+ "generated_at": "2026-04-20T02:40:01.462772",
"stats": {
- "total": 550,
- "pending": 1061,
+ "total": 552,
+ "pending": 1065,
"kaouther_surfaced": 29,
"chrome_surfaced": 10,
"notif_only_done": 0,
"autofix_archived": 0,
"cerebras_archived": 0,
"older_3d_archived": 0,
- "unknown": 511,
+ "unknown": 513,
"errors": 0
},
"actions": [
diff --git a/api/blade-tasks/task_20260420003502_c0a58c.json b/api/blade-tasks/task_20260420003502_c0a58c.json
new file mode 100644
index 000000000..0d08b3abf
--- /dev/null
+++ b/api/blade-tasks/task_20260420003502_c0a58c.json
@@ -0,0 +1,11 @@
+{
+ "id": "task_20260420003502_c0a58c",
+ "name": "Blade self-heal 02:35",
+ "type": "powershell",
+ "command": "\n# Blade self-heal\nWrite-Host \"Self-heal triggered $(Get-Date)\"\n$agentProc = Get-Process powershell | Where-Object { $_.CommandLine -match 'sentinel-agent' }\nif (!$agentProc) {\n Write-Host \"Agent not running, starting...\"\n Start-Process powershell -ArgumentList \"-ExecutionPolicy\",\"Bypass\",\"-File\",\"C:\\ProgramData\\WEVAL\\sentinel-agent.ps1\" -WindowStyle Hidden\n}\n# Clear stale tasks > 3 days locally\n$cutoff = (Get-Date).AddDays(-3)\nGet-ChildItem \"C:\\ProgramData\\WEVAL\\tasks\\*.json\" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | Move-Item -Destination \"C:\\ProgramData\\WEVAL\\tasks\\archived\\\" -Force -ErrorAction SilentlyContinue\nWrite-Host \"Self-heal complete\"\n",
+ "cmd": "\n# Blade self-heal\nWrite-Host \"Self-heal triggered $(Get-Date)\"\n$agentProc = Get-Process powershell | Where-Object { $_.CommandLine -match 'sentinel-agent' }\nif (!$agentProc) {\n Write-Host \"Agent not running, starting...\"\n Start-Process powershell -ArgumentList \"-ExecutionPolicy\",\"Bypass\",\"-File\",\"C:\\ProgramData\\WEVAL\\sentinel-agent.ps1\" -WindowStyle Hidden\n}\n# Clear stale tasks > 3 days locally\n$cutoff = (Get-Date).AddDays(-3)\nGet-ChildItem \"C:\\ProgramData\\WEVAL\\tasks\\*.json\" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | Move-Item -Destination \"C:\\ProgramData\\WEVAL\\tasks\\archived\\\" -Force -ErrorAction SilentlyContinue\nWrite-Host \"Self-heal complete\"\n",
+ "priority": "high",
+ "status": "pending",
+ "created": "2026-04-20T00:35:02+00:00",
+ "created_by": "blade-control-ui"
+}
\ No newline at end of file
diff --git a/api/em-kpi-cache.json b/api/em-kpi-cache.json
index e69de29bb..aada94287 100644
--- a/api/em-kpi-cache.json
+++ b/api/em-kpi-cache.json
@@ -0,0 +1,7 @@
+
+
500 Internal Server Error
+
+500 Internal Server Error
+
nginx/1.24.0 (Ubuntu)
+
+
diff --git a/api/handlers/resend-playwright-create-key.py b/api/handlers/resend-playwright-create-key.py
index 93a3ab67a..4e1784f1c 100755
--- a/api/handlers/resend-playwright-create-key.py
+++ b/api/handlers/resend-playwright-create-key.py
@@ -1,186 +1,228 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-"""Opus v5.9.3: WEVIA auto Resend Full Access key creation via Playwright headless"""
-import sys, json, os, time, subprocess
+"""Opus v5.9.4: WEVIA auto Resend Full Access key via Playwright headless."""
+import sys, json, os, time, subprocess, base64
from playwright.sync_api import sync_playwright
+CREDS_FILE = "/opt/wevia-brain/email-providers/resend-creds.json"
+KEY_FILE = "/opt/wevia-brain/email-providers/resend.key"
+
def log(msg):
- print(f"[{time.strftime('%H:%M:%S')}] {msg}", file=sys.stderr)
+ sys.stderr.write("[{}] {}\n".format(time.strftime("%H:%M:%S"), msg))
+ sys.stderr.flush()
+
+def save_key(new_key):
+ with open("/tmp/new_resend_key.txt","w") as f:
+ f.write(new_key)
+ subprocess.run(["sudo","cp","/tmp/new_resend_key.txt", KEY_FILE], check=False)
+ subprocess.run(["sudo","chmod","600", KEY_FILE], check=False)
+ b64 = base64.b64encode(new_key.encode()).decode()
+ cmd = "echo " + b64 + " | base64 -d | sudo tee " + KEY_FILE + " > /dev/null && sudo chmod 600 " + KEY_FILE
+ subprocess.run(["curl","-s","-X","POST","https://wevads.weval-consulting.com/api/sentinel-brain.php",
+ "--data-urlencode","action=exec","--data-urlencode","cmd="+cmd,"--max-time","10"],
+ capture_output=True, timeout=15)
def main():
- # Get Resend credentials from vault
- email_vault = "/opt/wevia-brain/email-providers/resend-creds.json"
- if not os.path.exists(email_vault):
- return {"ok": False, "error": f"Resend creds missing at {email_vault}. Create with: echo '{{"email":"...","password":"..."}}' | sudo tee {email_vault}"}
+ if not os.path.exists(CREDS_FILE):
+ return {
+ "ok": False,
+ "error": "No creds file at " + CREDS_FILE,
+ "solution": "First type in WEVIA chat: 'set resend password: YOUR_PASSWORD'"
+ }
- with open(email_vault) as f:
+ with open(CREDS_FILE) as f:
creds = json.load(f)
-
- email = creds.get("email", "")
- password = creds.get("password", "")
+ email = creds.get("email","")
+ password = creds.get("password","")
if not email or not password:
- return {"ok": False, "error": "creds file must have email and password"}
+ return {"ok":False,"error":"creds missing fields"}
- log(f"Login as {email}")
-
- result = {"ok": False, "steps": []}
+ log("Start Chromium login as " + email)
+ steps = []
new_key = None
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
- args=["--no-sandbox","--disable-setuid-sandbox","--disable-dev-shm-usage"]
+ args=["--no-sandbox","--disable-setuid-sandbox","--disable-dev-shm-usage","--disable-blink-features=AutomationControlled"]
)
context = browser.new_context(
user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
- viewport={"width":1280,"height":800}
+ viewport={"width":1280,"height":900}
)
page = context.new_page()
try:
- # Step 1: Go to login
- log("Goto resend.com/login")
+ log("goto /login")
page.goto("https://resend.com/login", wait_until="domcontentloaded", timeout=30000)
- result["steps"].append({"step":"goto_login","url":page.url})
- page.wait_for_timeout(2000)
+ page.wait_for_timeout(2500)
+ steps.append({"s":"loaded","url":page.url})
- # Step 2: Try Google SSO (most likely since email is gmail)
- # Or email/password form
+ # Fill email
try:
- # Look for email input
- email_input = page.locator("input[type=email]").first
- if email_input.is_visible(timeout=3000):
- log("Email input found")
- email_input.fill(email)
- page.wait_for_timeout(500)
- # Click continue/next/submit
- for btn_text in ["Continue","Sign in","Log in","Next"]:
- try:
- page.get_by_role("button", name=btn_text).first.click(timeout=2000)
- log(f"Clicked {btn_text}")
- break
- except: pass
- page.wait_for_timeout(3000)
- # Password
- try:
- pwd_input = page.locator("input[type=password]").first
- if pwd_input.is_visible(timeout=3000):
- pwd_input.fill(password)
- for btn_text in ["Sign in","Log in","Continue","Submit"]:
- try:
- page.get_by_role("button", name=btn_text).first.click(timeout=2000)
- log(f"Clicked {btn_text} (pwd)")
- break
- except: pass
- except:
- log("No password field - maybe magic link?")
+ page.fill("input[type=email]", email, timeout=5000)
+ log("email filled")
except Exception as e:
- log(f"Email flow err: {e}")
+ log("no email input: " + str(e)[:100])
- page.wait_for_timeout(5000)
- result["steps"].append({"step":"after_login_attempt","url":page.url,"title":page.title()})
+ # Submit email (look for Continue button)
+ submitted = False
+ for txt in ["Continue","Log in","Sign in","Next"]:
+ try:
+ page.get_by_role("button", name=txt, exact=False).first.click(timeout=2500)
+ log("clicked " + txt)
+ submitted = True
+ break
+ except:
+ continue
+ if not submitted:
+ try:
+ page.keyboard.press("Enter")
+ log("pressed enter")
+ except:
+ pass
- # Screenshot for debug
- page.screenshot(path="/tmp/resend-login-debug.png", full_page=True)
+ page.wait_for_timeout(3500)
+ steps.append({"s":"after_email","url":page.url})
- if "login" in page.url.lower() or "signin" in page.url.lower():
+ # Try password if field appears
+ try:
+ if page.locator("input[type=password]").first.is_visible(timeout=3000):
+ page.fill("input[type=password]", password)
+ log("password filled")
+ for txt in ["Log in","Sign in","Continue","Submit"]:
+ try:
+ page.get_by_role("button", name=txt, exact=False).first.click(timeout=2500)
+ break
+ except:
+ continue
+ except:
+ log("no password field - probably magic link flow")
+ page.screenshot(path="/tmp/resend-login-no-pwd.png", full_page=True)
browser.close()
return {
"ok": False,
- "error": "Still on login page after submit - likely need magic link or 2FA",
+ "error": "Resend uses magic-link login (no password field detected)",
"url": page.url,
- "screenshot": "/tmp/resend-login-debug.png",
- "steps": result["steps"],
- "suggestion": "Use email magic link manually OR provide 2FA code OR use password manager credentials"
+ "screenshot": "/tmp/resend-login-no-pwd.png",
+ "steps": steps,
+ "workaround": "Resend sent magic link to " + email + ". Options: (a) manually open link in browser, (b) auto-read Gmail via Gmail API and follow link (requires Gmail OAuth)"
}
- # Step 3: Go to api-keys page
- log("Goto /api-keys")
+ page.wait_for_timeout(5000)
+ steps.append({"s":"after_pwd","url":page.url})
+
+ if "login" in page.url.lower() or "signin" in page.url.lower():
+ page.screenshot(path="/tmp/resend-still-login.png", full_page=True)
+ browser.close()
+ return {"ok": False, "error":"still on login", "url":page.url, "screenshot":"/tmp/resend-still-login.png", "steps": steps}
+
+ # Navigate to API keys
+ log("goto /api-keys")
page.goto("https://resend.com/api-keys", wait_until="domcontentloaded", timeout=20000)
page.wait_for_timeout(3000)
+ steps.append({"s":"apikeys_loaded","url":page.url})
- # Step 4: Click "Create API Key"
- for sel in ["text=Create API Key","text=Create API key","button:has-text(\"Create\")"]:
+ # Click Create API Key
+ clicked = False
+ for sel in ["text=Create API Key","text=Create API key","text=Create key"]:
try:
page.locator(sel).first.click(timeout=3000)
- log(f"Clicked: {sel}")
+ clicked = True
+ log("clicked create: " + sel)
break
- except: pass
+ except:
+ continue
+ if not clicked:
+ try:
+ page.get_by_role("button", name="Create", exact=False).first.click(timeout=3000)
+ clicked = True
+ except:
+ pass
+
page.wait_for_timeout(2000)
- # Step 5: Fill name
+ # Fill name
try:
- page.locator("input[placeholder*=\"ame\"], input[name=name]").first.fill("wevia-master-full-auto")
- except: pass
+ page.locator("input[name=name], input[placeholder*=ame]").first.fill("wevia-master-auto-"+str(int(time.time())))
+ log("name filled")
+ except:
+ pass
- # Step 6: Select Full access
+ # Select Full access radio
try:
- page.locator("text=Full access").first.click(timeout=3000)
+ page.get_by_text("Full access", exact=False).first.click(timeout=3000)
+ log("selected Full access")
except:
try:
- page.get_by_role("radio").nth(0).click() # first radio often = Full
- except: pass
+ radios = page.locator("input[type=radio]").all()
+ if radios:
+ radios[0].click()
+ log("clicked first radio")
+ except:
+ pass
- # Step 7: Click create
- for sel in ["text=Create","button:has-text(\"Create\")","button[type=submit]"]:
+ # Click final Add/Create button
+ for txt in ["Add","Create","Save","Generate"]:
try:
- page.locator(sel).first.click(timeout=3000)
- log(f"Clicked final create: {sel}")
+ page.get_by_role("button", name=txt, exact=False).first.click(timeout=2500)
+ log("final click: " + txt)
break
- except: pass
+ except:
+ continue
- page.wait_for_timeout(4000)
+ page.wait_for_timeout(4500)
+ page.screenshot(path="/tmp/resend-key-shown.png", full_page=True)
- # Step 8: Extract the key from page (monospace field or copy button aria)
- key_selectors = ["code","input[value^=re_]","[aria-label*=API]","pre"]
- for sel in key_selectors:
+ # Extract key
+ for sel in ["code","pre","[class*=mono]","input[readonly]"]:
try:
elts = page.locator(sel).all()
for e in elts:
- txt = (e.inner_text() or "")
- if txt.startswith("re_") and len(txt) > 20:
- new_key = txt.strip()
- break
- val = e.get_attribute("value") or ""
- if val.startswith("re_") and len(val) > 20:
- new_key = val.strip()
- break
- if new_key: break
- except: pass
-
- page.screenshot(path="/tmp/resend-key-created.png", full_page=True)
- result["steps"].append({"step":"key_extraction","found":bool(new_key)})
+ try:
+ txt = e.inner_text(timeout=1000) or ""
+ if txt.startswith("re_") and len(txt) >= 20:
+ new_key = txt.strip()
+ log("found key in " + sel)
+ break
+ val = e.get_attribute("value") or ""
+ if val.startswith("re_") and len(val) >= 20:
+ new_key = val.strip()
+ break
+ except:
+ continue
+ if new_key:
+ break
+ except:
+ continue
+ steps.append({"s":"key_extraction","found": bool(new_key)})
browser.close()
except Exception as e:
+ try:
+ page.screenshot(path="/tmp/resend-error.png", full_page=True)
+ except:
+ pass
browser.close()
- return {"ok":False,"error":str(e),"steps":result["steps"]}
+ return {"ok":False,"error":str(e)[:300],"steps":steps,"screenshot":"/tmp/resend-error.png"}
if not new_key:
- return {"ok": False, "error": "Could not extract new key", "steps": result["steps"], "screenshot": "/tmp/resend-key-created.png"}
-
- # Save key on S204 + S95
- with open("/tmp/new_resend_key.txt","w") as f:
- f.write(new_key)
- subprocess.run(["sudo","cp","/tmp/new_resend_key.txt","/opt/wevia-brain/email-providers/resend.key"], check=False)
- subprocess.run(["sudo","chmod","600","/opt/wevia-brain/email-providers/resend.key"], check=False)
-
- import base64
- b64 = base64.b64encode(new_key.encode()).decode()
- cmd = f"echo {b64} | base64 -d | sudo tee /opt/wevia-brain/email-providers/resend.key > /dev/null && sudo chmod 600 /opt/wevia-brain/email-providers/resend.key"
- subprocess.run(["curl","-s","-X","POST","https://wevads.weval-consulting.com/api/sentinel-brain.php",
- "--data-urlencode","action=exec",
- "--data-urlencode",f"cmd={cmd}","--max-time","10"], capture_output=True, timeout=15)
+ return {"ok": False, "error":"key extraction failed", "steps":steps, "screenshot":"/tmp/resend-key-shown.png"}
+ save_key(new_key)
return {
"ok": True,
- "v": "V5.9.3-playwright-auto-resend-key",
- "key_preview": new_key[:10] + "..." + new_key[-4:],
- "key_saved_s204": True,
- "key_saved_s95": True,
- "screenshot": "/tmp/resend-key-created.png",
- "next_step": "Now call resend_ultimate_setup which uses this new key automatically"
+ "v": "V5.9.4-playwright-full-auto",
+ "key_preview": new_key[:10]+"..."+new_key[-4:],
+ "saved_dual": True,
+ "screenshot": "/tmp/resend-key-shown.png",
+ "steps": steps,
+ "next_step": "Now trigger: 'resend full setup' to add wevup.app domain + DNS records"
}
if __name__ == "__main__":
- r = main()
- print(json.dumps(r, indent=2))
+ try:
+ r = main()
+ except Exception as e:
+ r = {"ok": False, "error": "fatal: " + str(e)[:300]}
+ sys.stdout.write(json.dumps(r, indent=2))
+ sys.stdout.flush()
diff --git a/api/v83-business-kpi-latest.json b/api/v83-business-kpi-latest.json
index 2527a6e7f..1b272a3fd 100644
--- a/api/v83-business-kpi-latest.json
+++ b/api/v83-business-kpi-latest.json
@@ -1,7 +1,7 @@
{
"ok": true,
"version": "V83-business-kpi",
- "ts": "2026-04-20T00:31:03+00:00",
+ "ts": "2026-04-20T00:35:14+00:00",
"summary": {
"total_categories": 7,
"total_kpis": 56,