diff --git a/api/l99-state.json b/api/l99-state.json index 297912c07..0da88d885 100644 --- a/api/l99-state.json +++ b/api/l99-state.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-04-16T17:00:12.845088", + "timestamp": "2026-04-16T17:04:52.895997", "layers": { "DOCKER": { "pass": 19, diff --git a/api/screenshots/growth-tab-actions.png b/api/screenshots/growth-tab-actions.png index 1243419b8..0c20530be 100644 Binary files a/api/screenshots/growth-tab-actions.png and b/api/screenshots/growth-tab-actions.png differ diff --git a/api/screenshots/growth-tab-assets.png b/api/screenshots/growth-tab-assets.png index b6277834f..f6320ee4f 100644 Binary files a/api/screenshots/growth-tab-assets.png and b/api/screenshots/growth-tab-assets.png differ diff --git a/api/screenshots/growth-tab-intel.png b/api/screenshots/growth-tab-intel.png index 684b1fbec..db732bf65 100644 Binary files a/api/screenshots/growth-tab-intel.png and b/api/screenshots/growth-tab-intel.png differ diff --git a/api/screenshots/growth-tab-pipeline.png b/api/screenshots/growth-tab-pipeline.png index 3d6b414be..0d34e9d1a 100644 Binary files a/api/screenshots/growth-tab-pipeline.png and b/api/screenshots/growth-tab-pipeline.png differ diff --git a/api/wevia-blade-status.json b/api/wevia-blade-status.json index 6dc2b7c02..9f8a7c6e5 100644 --- a/api/wevia-blade-status.json +++ b/api/wevia-blade-status.json @@ -1 +1 @@ -{"ts":"17:02","status":"offline"} +{"ts":"17:04","status":"offline"} diff --git a/api/wevia-ops.php b/api/wevia-ops.php index 692d5c464..d41cb91f1 100644 --- a/api/wevia-ops.php +++ b/api/wevia-ops.php @@ -1486,6 +1486,39 @@ switch($action) { ]; break; + + case "batch_html_guard": + // Batch HTML_GUARD: protect all .html from displaying raw 0,"patched"=>[],"already"=>[],"failed"=>[]]; + $files = array_filter(array_map("trim", explode("\n", + shell_exec("grep -rln 'await res.json()\|r\.json()' /var/www/html/*.html 2>/dev/null") ?: ""))); + foreach ($files as $fp) { + if (!file_exists($fp)) continue; + $results["scanned"]++; + $ct = file_get_contents($fp); + if (strpos($ct,"HTML_GUARD")!==false) { $results["already"][]=basename($fp); continue; } + $orig = $ct; + // Pattern A: "await X.json()" → text() + check + $ct = preg_replace( + '/(\s*)(var|let|const)\s+(\w+)\s*=\s*await\s+(\w+)\.json\(\)\s*;/', + '$1/* HTML_GUARD_V2_BATCH */ $2 _t_$3=await $4.text(); $2 $3; {var _q=(_t_$3||\'\').trim();if(_q.startsWith(\'r.json())" + $ct = preg_replace( + '/\.then\(\s*(\w+)\s*=>\s*\1\.json\(\)\s*\)/', + '.then($1=>$1.text().then(t=>{var q=(t||\'\').trim();if(q.startsWith(\'(@file_put_contents($fp,$ct)!==false)]; + if ($r["ok"]??false) { $results["patched"][]=basename($fp); } + else { $results["failed"][]=basename($fp); } + } + } + $results["summary"]=["scanned"=>$results["scanned"],"patched"=>count($results["patched"]),"already"=>count($results["already"])]; + break; + default: $results=["actions"=>["test_providers","webchat","nonreg","reconcile","git_push","ethica","docker_list","git_log","disk","ports","crons","services","playwright_scan","paperclip","slack"]]; } diff --git a/api/wevia-ops.php.pre-bhg-20260416_150436 b/api/wevia-ops.php.pre-bhg-20260416_150436 new file mode 100644 index 000000000..692d5c464 --- /dev/null +++ b/api/wevia-ops.php.pre-bhg-20260416_150436 @@ -0,0 +1,1492 @@ +"auth"]);exit;} + +// OPUS v3 HELPER — écriture fichier robuste (fixe silent-fail récurrent sur fichiers root-owned) +// Retourne ['ok'=>bool, 'bytes'=>int|false, 'md5'=>string|null, 'via'=>'direct'|'sudo_cp'|'fail', 'marker_ok'=>bool] +if (!function_exists('opus_write_safe')) { + function opus_write_safe($target, $content, $verify_marker = null, $exec_bit = false) { + if (!$target || !is_string($content)) return ['ok'=>false,'err'=>'bad_args']; + $dir = dirname($target); + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + if (!is_dir($dir)) @shell_exec("sudo mkdir -p " . escapeshellarg($dir)); + } + // chattr -i si present + $attr = @shell_exec("lsattr " . escapeshellarg($target) . " 2>/dev/null"); + $had_immut = ($attr && strpos($attr, 'i') !== false && strpos($attr, '-i-') === false); + if ($had_immut) @shell_exec("sudo chattr -i " . escapeshellarg($target)); + + $via = 'direct'; + $n = @file_put_contents($target, $content); + if ($n === false) { + $via = 'sudo_cp'; + $tmpw = tempnam('/tmp', 'ows'); + @file_put_contents($tmpw, $content); + @shell_exec("sudo cp " . escapeshellarg($tmpw) . " " . escapeshellarg($target)); + @shell_exec("sudo chown www-data:www-data " . escapeshellarg($target)); + @unlink($tmpw); + clearstatcache(); + $n = file_exists($target) ? filesize($target) : false; + } + if ($exec_bit) { @chmod($target, 0755); @shell_exec("sudo chmod +x " . escapeshellarg($target)); } + if ($had_immut) @shell_exec("sudo chattr +i " . escapeshellarg($target)); + + $marker_ok = true; + if ($verify_marker !== null) { + $final = @file_get_contents($target); + $marker_ok = ($final && strpos($final, $verify_marker) !== false); + } + return [ + 'ok' => ($n !== false && $marker_ok), + 'bytes' => $n, + 'md5' => ($n !== false) ? @md5_file($target) : null, + 'via' => ($n === false ? 'fail' : $via), + 'marker_ok' => $marker_ok, + ]; + } +} + +$action=$_REQUEST["action"]??""; +$results=[]; + +switch($action) { + case "test_providers": + $svcs=["deepseek","copilot","meta","qwen","perplexity","duckduckgo","lechat","huggingchat"]; + // Parallel test with multi_curl + $mh = curl_multi_init(); + $handles = []; + foreach($svcs as $s) { + $ch = curl_init("http://localhost/api/wevia-webchat-direct.php"); + curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>15,CURLOPT_HTTPHEADER=>["Content-Type: application/json"],CURLOPT_POSTFIELDS=>json_encode(["service"=>$s,"message"=>"bonjour"])]); + curl_multi_add_handle($mh, $ch); + $handles[$s] = $ch; + } + $running = null; + do { curl_multi_exec($mh, $running); usleep(100000); } while ($running > 0); + foreach($handles as $s=>$ch) { + $r = json_decode(curl_multi_getcontent($ch), true); + $results[$s] = $r["provider"] ?? "FAIL"; + curl_multi_remove_handle($mh, $ch); + curl_close($ch); + } + curl_multi_close($mh); + break; + // OLD sequential code below (disabled) + foreach($svcs as $DISABLED) { + $ch=curl_init("http://localhost/api/wevia-webchat-direct.php"); + curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>12,CURLOPT_HTTPHEADER=>["Content-Type: application/json"],CURLOPT_POSTFIELDS=>json_encode(["service"=>$s,"message"=>"bonjour"])]); + $r=json_decode(curl_exec($ch),true);curl_close($ch); + $results[$s]=$r["provider"]??"FAIL"; + } + break; + case "webchat": + $ch=curl_init("http://localhost:8902/health"); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]); + $r=json_decode(curl_exec($ch),true);curl_close($ch); + $results=$r; + break; + case "nonreg": + $ch=curl_init("http://127.0.0.1/api/nonreg-api.php?cat=all"); + curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>30]); + $r=json_decode(curl_exec($ch),true);curl_close($ch); + $results=["pass"=>$r["pass"]??0,"total"=>$r["total"]??0]; + break; + case "reconcile": + $results["ports"]=trim(shell_exec('ss -tlnp | grep LISTEN | wc -l')); + $results["pages"]=trim(shell_exec('ls /var/www/html/*.html 2>/dev/null | wc -l')); + $results["git_dirty"]=trim(shell_exec('cd /var/www/html && git status -s | wc -l')); + $results["crons"]=trim(shell_exec('crontab -l 2>/dev/null | grep -v "^#" | grep -v "^$" | wc -l')); + $results["docker"]=trim(shell_exec('docker ps --format "{{.Names}}" 2>/dev/null | wc -l')); + $results["webchat"]=trim(shell_exec('curl -s http://localhost:8902/health 2>/dev/null | python3 -c "import sys,json;d=json.loads(sys.stdin.read());print(len(d.get(\"services\",[])))" 2>/dev/null')); + break; + case "git_push": + $results["html"]=trim(shell_exec('cd /var/www/html && git add -A && git commit -m "auto-push" 2>&1 | tail -n 2 && git push 2>&1 | tail -n 2')); + break; + case "gen_pdf": + $title = $_REQUEST["title"] ?? "WEVAL Report"; + $body = $_REQUEST["body"] ?? "Rapport genere par WEVIA"; + $fname = "report_" . date("Ymd_His") . ".pdf"; + $path = "/var/www/html/downloads/$fname"; + $py = "from fpdf import FPDF; p=FPDF(); p.add_page(); p.set_font(\"Helvetica\",\"B\",16); p.cell(0,10,\"" . addslashes($title) . "\",ln=True); p.set_font(\"Helvetica\",\"\",11); p.multi_cell(0,7,\"" . addslashes(substr($body,0,2000)) . "\"); p.output(\"$path\")"; + shell_exec("python3 -c " . escapeshellarg($py) . " 2>&1"); + $results["pdf"] = file_exists($path) ? "https://weval-consulting.com/downloads/$fname" : "FAIL"; + $results["size"] = file_exists($path) ? filesize($path) : 0; + break; + case "gen_mermaid": + $code = $_REQUEST["code"] ?? "graph TD; A-->B; B-->C;"; + $fname = "diagram_" . date("Ymd_His") . ".png"; + $mmd = "/tmp/merm_" . uniqid() . ".mmd"; + $png = "/var/www/html/downloads/$fname"; + file_put_contents($mmd, $code); + shell_exec("/usr/bin/mmdc -i " . escapeshellarg($mmd) . " -o " . escapeshellarg($png) . " -w 1400 2>&1"); + @unlink($mmd); + $results["image"] = file_exists($png) ? "https://weval-consulting.com/downloads/$fname" : "FAIL"; + $results["size"] = file_exists($png) ? filesize($png) : 0; + break; + case "gen_swot": + $subject = $_REQUEST["subject"] ?? "WEVAL Consulting"; + $fname = "swot_" . date("Ymd_His") . ".png"; + $mmd = "/tmp/swot_" . uniqid() . ".mmd"; + $png = "/var/www/html/downloads/$fname"; + $code = "graph TD\n subgraph Forces\n F1[12 providers IA 0EUR]\n F2[382 tools]\n F3[131K HCPs Ethica]\n end\n subgraph Faiblesses\n W1[Equipe reduite]\n W2[Dependance providers gratuits]\n end\n subgraph Opportunites\n O1[Marche pharma Maghreb]\n O2[IA souveraine en croissance]\n end\n subgraph Menaces\n T1[Concurrence SaaS]\n T2[Regulation IA]\n end"; + file_put_contents($mmd, $code); + shell_exec("/usr/bin/mmdc -i " . escapeshellarg($mmd) . " -o " . escapeshellarg($png) . " -w 1400 2>&1"); + @unlink($mmd); + $results["image"] = file_exists($png) ? "https://weval-consulting.com/downloads/$fname" : "FAIL"; + break; + case "gen_ishikawa": + $fname = "ishikawa_" . date("Ymd_His") . ".png"; + $mmd = "/tmp/ishi_" . uniqid() . ".mmd"; + $png = "/var/www/html/downloads/$fname"; + $code = "graph LR\n A[Probleme] --> B[Methode]\n A --> C[Main-doeuvre]\n A --> D[Machine]\n A --> E[Matiere]\n A --> F[Milieu]\n A --> G[Mesure]\n B --> B1[Process non documente]\n C --> C1[Formation IA]\n D --> D1[Serveur S95 legacy]\n E --> E1[Data HCP incomplete]\n F --> F1[Reglementation pharma]\n G --> G1[KPI pas en temps reel]"; + file_put_contents($mmd, $code); + shell_exec("/usr/bin/mmdc -i " . escapeshellarg($mmd) . " -o " . escapeshellarg($png) . " -w 1400 2>&1"); + @unlink($mmd); + $results["image"] = file_exists($png) ? "https://weval-consulting.com/downloads/$fname" : "FAIL"; + break; + case "docker_list": + $results["containers"] = trim(shell_exec("docker ps --format \"{{.Names}}: {{.Status}} ({{.Image}})\" 2>/dev/null")); + $results["count"] = trim(shell_exec("docker ps -q 2>/dev/null | wc -l")); + break; + case "git_log": + $results["log"] = trim(shell_exec("cd /var/www/html && git log --oneline -5 2>/dev/null")); + $results["dirty"] = trim(shell_exec("cd /var/www/html && git status -s 2>/dev/null | head -10")); + $results["branch"] = trim(shell_exec("cd /var/www/html && git branch --show-current 2>/dev/null")); + break; + case "disk": + $results["usage"] = trim(shell_exec("df -h / /var/www 2>/dev/null | tail -2")); + $results["big_files"] = trim(shell_exec("du -sh /var/www/html /var/log /opt 2>/dev/null")); + break; + case "ports": + $results["listening"] = trim(shell_exec("ss -tlnp | grep LISTEN | awk \"{print \$4}\" | sort -t: -k2 -n 2>/dev/null | head -20")); + break; + case "crons": + $results["crontab"] = trim(shell_exec("crontab -l 2>/dev/null | grep -v \"^#\" | grep -v \"^$\"")); + break; + case "services": + $results["nginx"] = trim(shell_exec("nginx -t 2>&1 | tail -1")); + $results["php"] = trim(shell_exec("php -v 2>/dev/null | head -1")); + $results["pg"] = trim(shell_exec("pg_isready 2>/dev/null")); + $results["redis"] = trim(shell_exec("redis-cli ping 2>/dev/null")); + $results["ollama"] = trim(shell_exec("curl -s http://localhost:11434/api/tags 2>/dev/null | python3 -c \"import sys,json;d=json.loads(sys.stdin.read());print(len(d.get('models',[])),'models')\" 2>/dev/null")); + break; + case "undefined_scan": + // Scan pages for "undefined" strings rendered in HTML (JS bugs) + $pages = explode(" ", "index agents-archi wevia-meeting-rooms enterprise-model director-center l99-brain wevia-master paperclip wevia wevads-ia/index ethica weval-arena weval-arena-v2 deepseek admin-saas monitoring command-center"); + $results["scanned"] = 0; + $results["issues"] = []; + foreach ($pages as $p) { + $path = "/var/www/html/$p.html"; + if (!file_exists($path)) continue; + $html = @file_get_contents($path); + if ($html === false) continue; + $results["scanned"]++; + // Count rendered "undefined" (not in comments/strings) + $count = preg_match_all("/>\s*undefined\s* 0) $results["issues"][$p] = $count; + } + $results["total_issues"] = array_sum($results["issues"]); + $results["status"] = $results["total_issues"] === 0 ? "GODMODE_CLEAN" : "issues_found"; + break; + case "self_heal": + // AUTONOMOUS SELF-HEAL: check + fix everything broken + $fixed = []; + $checked = []; + + // 1. nginx pid - check only, NO auto-reload (was causing downtime) + $pid_owner = trim(shell_exec("stat -c %U /run/nginx.pid 2>/dev/null")); + $checked["nginx_pid_owner"] = $pid_owner ?: "missing"; + // Reload only on first run per day, logged elsewhere + + // 2. nginx config valid + $conf = trim(shell_exec("sudo nginx -t 2>&1 | tail -1")); + $checked["nginx_test"] = $conf; + if (strpos($conf, "successful") === false) { + $fixed[] = "nginx_config_invalid_NO_ACTION"; + } + + // 3. DeerFlow port 2024 + $df = trim(shell_exec("ss -tln 2>&1 | grep 2024 | wc -l")); + $checked["deerflow_port"] = ($df > 0) ? "LISTEN" : "DOWN"; + if ($df == 0) { + shell_exec("sudo nohup bash -c \"cd /opt/deer-flow && make dev-daemon\" > /tmp/df.log 2>&1 &"); + $fixed[] = "deerflow_skipped_decommissioned"; + } + + // 4. Webchat port 8902 + $wc = trim(shell_exec("ss -tln 2>&1 | grep 8902 | wc -l")); + $checked["webchat_port"] = ($wc > 0) ? "LISTEN" : "DOWN"; + if ($wc == 0) { + shell_exec("nohup python3 /opt/weval-l99/wevia-webchat-api.py > /tmp/wc.log 2>&1 &"); + $fixed[] = "webchat_restart"; + } + + // 5. Redis + $redis = trim(shell_exec("redis-cli ping 2>&1")); + $checked["redis"] = $redis; + if ($redis !== "PONG") { + shell_exec("sudo systemctl restart redis 2>&1"); + $fixed[] = "redis_restart"; + } + + // 6. PostgreSQL + $pg = trim(shell_exec("pg_isready 2>&1 | tail -1")); + $checked["postgres"] = $pg; + + // 7. Ollama + $ol = trim(shell_exec("curl -s --max-time 2 http://localhost:11434/api/tags 2>&1 | head -c 10")); + $checked["ollama"] = $ol ? "UP" : "DOWN"; + if (!$ol) { + shell_exec("sudo systemctl restart ollama 2>&1 &"); + $fixed[] = "ollama_restart"; + } + + // 8. Docker containers health + $dead = trim(shell_exec("docker ps -a --filter 'status=exited' --format '{{.Names}}' 2>/dev/null | wc -l")); + $checked["dead_containers"] = $dead; + + // 9. Disk space + $disk = trim(shell_exec("df -h / | tail -1 | awk '{print \$5}'")); + $checked["disk_usage"] = $disk; + + // 10. Git dirty + $dirty = trim(shell_exec("cd /var/www/html && git status -s 2>&1 | wc -l")); + $checked["git_dirty"] = $dirty; + if ($dirty > 20) { + shell_exec("cd /var/www/html && git add -A && git commit -m 'self-heal-auto-push' 2>&1 && git push 2>&1"); + $fixed[] = "git_auto_push"; + } + + // 11. Cloudflare origin reachable + $cf = trim(shell_exec("curl -so /dev/null -w '%{http_code}' --max-time 5 http://localhost/ 2>&1")); + $checked["local_http"] = $cf; + + $results["checked"] = $checked; + $results["fixed"] = $fixed; + $results["health_score"] = count($fixed) === 0 ? "PERFECT_6SIGMA" : count($fixed) . "_issues_fixed"; + break; + case "git_reconcile_all": + // Reconcile ALL git repos: push dirty, pull remote, report + $repos_opt = glob("/opt/*/.git"); + $repos_www = glob("/var/www/*/.git"); + $all_repos = array_map(fn($p)=>dirname($p), array_merge($repos_opt, $repos_www)); + + $summary = ["total"=>count($all_repos), "clean"=>0, "dirty"=>0, "pushed"=>0, "no_remote"=>0, "details"=>[]]; + + foreach ($all_repos as $repo) { + $name = basename($repo); + $dirty = trim(shell_exec("cd $repo && git status -s 2>&1 | wc -l")); + $remote = trim(shell_exec("cd $repo && git remote 2>&1 | head -1")); + + if (!$remote) { + $summary["no_remote"]++; + $summary["details"][$name] = "no_remote"; + continue; + } + + if ($dirty > 0) { + $summary["dirty"]++; + // Only auto-push our own repos + if (in_array($name, ["html","weval","weval-l99","paperclip-weval","wevia-brain"])) { + shell_exec("cd $repo && git add -A 2>&1 && git commit -m \"auto-reconcile-16avr\" 2>&1"); + $push = trim(shell_exec("cd $repo && git push 2>&1 | tail -1")); + $summary["pushed"]++; + $summary["details"][$name] = "pushed: $push"; + } else { + $summary["details"][$name] = "dirty_skipped (external repo)"; + } + } else { + $summary["clean"]++; + } + } + + $results = $summary; + break; + case "ethica_pilot": + // Real ethica pilot readiness count via S95 PG + $sql = "SELECT + COUNT(*) FILTER (WHERE country='DZ' AND email IS NOT NULL AND email != '') as dz_ready, + COUNT(*) FILTER (WHERE country='MA' AND email IS NOT NULL AND email != '') as ma_ready, + COUNT(*) FILTER (WHERE country='TN' AND email IS NOT NULL AND email != '') as tn_ready, + COUNT(*) FILTER (WHERE email IS NOT NULL AND email != '') as total_with_email + FROM ethica.medecins_real"; + $res = trim(shell_exec("curl -sk -u weval:W3valAdmin2026 --max-time 8 'https://10.1.0.3:8443/api/sentinel-brain.php?action=psql' -d " . escapeshellarg(urlencode($sql)) . " 2>&1 | head -c 500")); + $results["query_result"] = $res; + // Fallback: local API + $api = @json_decode(@file_get_contents("http://localhost/api/ethica-stats.php"), true); + if ($api) { + $results["dz_total"] = $api["dz"] ?? "?"; + $results["ma_total"] = $api["ma"] ?? "?"; + $results["tn_total"] = $api["tn"] ?? "?"; + $results["total"] = $api["total"] ?? "?"; + } else { + // Use ethica action data + $ec = @json_decode(shell_exec("curl -s --max-time 5 http://localhost/api/wevia-ops.php?k=BLADE2026&action=ethica"), true); + if ($ec) { + $results["ethica_data"] = $ec["results"] ?? $ec; + } + } + break; + case "dns_check": + // Check SPF/DKIM/DMARC for wevup.app + weval-consulting.com + $domains = ["wevup.app", "weval-consulting.com"]; + foreach ($domains as $dom) { + $spf = trim(shell_exec("dig +short TXT $dom 2>&1 | grep -i spf | head -1")); + $dmarc = trim(shell_exec("dig +short TXT _dmarc.$dom 2>&1 | head -1")); + $dkim = trim(shell_exec("dig +short TXT weval._domainkey.$dom 2>&1 | head -1")); + $results[$dom] = [ + "spf" => $spf ?: "none", + "dmarc" => $dmarc ?: "none", + "dkim_weval" => $dkim ?: "none" + ]; + } + break; + case "ssl_check": + // SSL cert expiration + $domains = ["weval-consulting.com", "git.weval-consulting.com", "wevup.app"]; + foreach ($domains as $dom) { + $cmd = "echo | openssl s_client -servername $dom -connect $dom:443 2>/dev/null | openssl x509 -noout -dates 2>&1 | head -2"; + $res = trim(shell_exec($cmd)); + $results[$dom] = str_replace(["\n","\t"], " | ", $res) ?: "unreachable"; + } + break; + case "ollama_models": + // Real Ollama model list + $out = trim(shell_exec("curl -s --max-time 5 http://localhost:11434/api/tags 2>&1")); + $d = @json_decode($out, true); + $models = $d["models"] ?? []; + $results["count"] = count($models); + $results["models"] = array_map(fn($m)=>[ + "name" => $m["name"] ?? "?", + "size" => round(($m["size"] ?? 0)/1024/1024) . "MB" + ], $models); + // Check GPU/CPU + $results["ollama_http"] = trim(shell_exec("curl -s -o /dev/null -w %{http_code} http://localhost:11434/")); + break; + case "l99_autofix": + // Autofix missing L99 tests + refresh state + shell_exec("nohup timeout 120 python3 /opt/weval-l99/wevia-l99-autofix.py > /tmp/l99fix.log 2>&1 &"); + // Immediately get current state + $state = @json_decode(@file_get_contents("/var/www/html/api/l99-state.json"),true); + $layers = $state["layers"] ?? []; + $failing = []; + foreach ($layers as $n => $l) { + if (($l["pass"] ?? 0) < ($l["total"] ?? 0)) { + $failing[$n] = ($l["pass"] ?? 0) . "/" . ($l["total"] ?? 0); + } + } + $results["status"] = "autofix_running_bg"; + $results["failing_layers"] = $failing; + $results["total_tests"] = array_sum(array_column($layers,"total")); + $results["passing_tests"] = array_sum(array_column($layers,"pass")); + $results["check_log"] = "/tmp/l99fix.log"; + break; + case "l99_update": + // Force L99 refresh (runs scanner + writes state) + $out = shell_exec("python3 /opt/weval-l99/l99-state-updater.py 2>&1 | tail -3"); + shell_exec("cp /opt/weval-l99/l99-state.json /var/www/html/api/l99-state.json 2>&1"); + $state = @json_decode(@file_get_contents("/var/www/html/api/l99-state.json"),true); + $layers = $state["layers"] ?? []; + $total = array_sum(array_column($layers, "total")); + $pass = array_sum(array_column($layers, "pass")); + $results["updater_output"] = trim($out); + $results["layers"] = count($layers); + $results["tests"] = "$pass/$total"; + $results["timestamp"] = $state["timestamp"] ?? "?"; + break; + case "wiki_update": + // Generate session summary in wiki + $date = date("Y-m-d-H:i"); + $wiki_file = "/var/www/html/wiki/session-$date.md"; + $summary = "# Session WEVIA Master $date\n\n"; + $summary .= "## Actions Exécutées\n\n"; + // Get recent git commits + $commits = trim(shell_exec("cd /var/www/html && git log --oneline -20 2>&1")); + $summary .= "### Commits récents\n```\n$commits\n```\n\n"; + // Get WEVIA Master state + $reconcile = @json_decode(trim(shell_exec("curl -s --max-time 5 http://localhost/api/wevia-ops.php?k=BLADE2026&action=reconcile")), true); + $summary .= "### Reconcile\n```\n" . json_encode($reconcile["results"] ?? [], JSON_PRETTY_PRINT) . "\n```\n\n"; + // Doctrine + $doctrine = @json_decode(trim(shell_exec("curl -s --max-time 5 http://localhost/api/wevia-ops.php?k=BLADE2026&action=doctrine_check")), true); + $summary .= "### Doctrine\n```\n" . json_encode($doctrine["results"] ?? [], JSON_PRETTY_PRINT) . "\n```\n"; + + file_put_contents($wiki_file, $summary); + $results["wiki_file"] = $wiki_file; + $results["size"] = filesize($wiki_file); + $results["sections"] = 3; + break; + case "sync_all": + // L99 + Git + Gitea + Wiki + Vault all in one (background) + $script = "/tmp/sync_all.sh"; + $cmd = "#!/bin/bash\n"; + $cmd .= "echo SYNC_ALL_START_$(date +%s) >> /tmp/sync_all.log\n"; + $cmd .= "# 1. L99 refresh\n"; + $cmd .= "python3 /opt/weval-l99/l99-state-updater.py 2>&1 >> /tmp/sync_all.log\n"; + $cmd .= "cp /opt/weval-l99/l99-state.json /var/www/html/api/l99-state.json 2>&1\n"; + $cmd .= "# 2. Git push GitHub\n"; + $cmd .= "cd /var/www/html && git add -A && git commit -m auto-sync-all 2>&1 | tail -1 >> /tmp/sync_all.log && git push 2>&1 | tail -1 >> /tmp/sync_all.log\n"; + $cmd .= "# 3. Git push Gitea\n"; + $cmd .= "git push gitea --all 2>&1 | tail -1 >> /tmp/sync_all.log\n"; + $cmd .= "# 4. Vault gold backup\n"; + $cmd .= "mkdir -p /opt/wevads/vault/gold-auto-$(date +%Y%m%d-%H%M%S)\n"; + $cmd .= "cp -r /var/www/html/api /opt/wevads/vault/gold-auto-$(date +%Y%m%d-%H%M%S)/ 2>&1\n"; + $cmd .= "echo SYNC_ALL_DONE_$(date +%s) >> /tmp/sync_all.log\n"; + file_put_contents($script, $cmd); + chmod($script, 0755); + shell_exec("nohup bash $script > /tmp/sync_all.out 2>&1 &"); + $results["status"] = "background_sync_started"; + $results["script"] = $script; + $results["log"] = "/tmp/sync_all.log"; + $results["components"] = ["L99","GitHub","Gitea","Vault"]; + break; + case "cloudflare_status": + // Cloudflare DNS + tunnel status + $ip_dns = trim(shell_exec("dig +short weval-consulting.com @1.1.1.1 2>/dev/null | head -1")); + $cf_ray = trim(shell_exec("curl -sI --max-time 5 https://weval-consulting.com/ 2>&1 | grep -i cf-ray | head -1")); + $http = trim(shell_exec("curl -s -o /dev/null -w %{http_code} --max-time 5 https://weval-consulting.com/")); + $zone = "1488bbba251c6fa282999fcc09aac9fe"; + $results["dns_ip"] = $ip_dns ?: "no_resolve"; + $results["cf_ray"] = $cf_ray ?: "no_ray"; + $results["site_http"] = $http; + $results["zone_id"] = $zone; + $results["domain"] = "weval-consulting.com"; + break; + case "redis_status": + // Redis memory + key counts + $ping = trim(shell_exec("redis-cli ping 2>&1")); + $info = trim(shell_exec("redis-cli info memory 2>&1 | grep -E 'used_memory_human|maxmemory_human' | head -4")); + $mem_count = trim(shell_exec("redis-cli -n 2 LLEN wevia:memory:global 2>&1")); + $dbs = trim(shell_exec("redis-cli info keyspace 2>&1")); + $results["ping"] = $ping; + $results["memory_info"] = $info; + $results["wevia_memory_count"] = $mem_count; + $results["all_dbs"] = $dbs; + break; + case "screenshots_list": + // Recent playwright screenshots + $dirs = glob("/var/www/html/screenshots/l99-pw-*"); + usort($dirs, fn($a,$b)=>filemtime($b)-filemtime($a)); + $latest = $dirs[0] ?? null; + $results["total_dirs"] = count($dirs); + if ($latest) { + $results["latest_dir"] = basename($latest); + $results["latest_date"] = date("Y-m-d H:i:s", filemtime($latest)); + $results["images"] = array_map("basename", glob("$latest/*.png")); + } + break; + case "disk_cleanup": + // Clean up vault old backups + apt cache + journal + $before = trim(shell_exec("df -h / 2>&1 | tail -1 | awk '{print \$5}'")); + // Clean journal logs >7days + shell_exec("sudo journalctl --vacuum-time=3d 2>&1"); + // Clean apt cache + shell_exec("sudo apt-get clean 2>&1"); + // Clean old /tmp + shell_exec("sudo find /tmp -type f -mtime +2 -delete 2>&1"); + $after = trim(shell_exec("df -h / 2>&1 | tail -1 | awk '{print \$5}'")); + $results["before"] = $before; + $results["after"] = $after; + $results["freed"] = ($before != $after) ? "yes" : "no"; + break; + case "s95_status": + // Check S95 (95.216.167.89 via WireGuard 10.1.0.3) + $s95_api = trim(shell_exec("curl -sk -u weval:W3valAdmin2026 --max-time 5 https://10.1.0.3:8443/api/sentinel-brain.php?action=status 2>&1 | head -c 500")); + $s95_ping = trim(shell_exec("ping -c 1 -W 2 10.1.0.3 2>&1 | grep -oE 'time=[0-9.]+' | head -1")); + $s95_http = trim(shell_exec("curl -s -o /dev/null -w %{http_code} --max-time 5 http://10.1.0.3:5890/api/sentinel-brain.php?action=status 2>&1")); + + $results["wireguard_ping"] = $s95_ping ?: "no_response"; + $results["sentinel_api"] = $s95_http ?: "no_response"; + $results["api_response"] = substr($s95_api, 0, 200); + + // PMTA check + $pmta = trim(shell_exec("curl -sk -u weval:W3valAdmin2026 --max-time 5 https://10.1.0.3:8443/api/sentinel-brain.php?action=exec&cmd=pgrep%20-c%20pmta 2>&1 | head -c 100")); + $results["pmta_processes"] = $pmta; + break; + case "sentinel_status": + // Razer Blade Windows Sentinel agent + $last_call = trim(shell_exec("grep -h 'sentinel' /var/log/nginx/access.log 2>/dev/null | tail -1 | awk '{print \$4,\$7}' | head -c 200")); + $registry = @json_decode(@file_get_contents("/var/www/html/api/sentinel-registry.json"), true); + $results["last_call"] = $last_call ?: "no_recent"; + $results["registered_tasks"] = count($registry["tasks"] ?? []); + $results["agent_version"] = "v2.3.3"; + $results["install_url"] = "https://weval-consulting.com/api/sentinel-agent-v2.3.ps1"; + break; + case "memory_status": + // RAM + CPU + $mem = trim(shell_exec("free -h 2>&1 | grep Mem")); + $cpu = trim(shell_exec("top -bn1 2>&1 | grep 'Cpu(s)' | head -1")); + $load = trim(shell_exec("uptime 2>&1 | grep -oE 'load average:.*'")); + $results["memory"] = $mem; + $results["cpu"] = substr($cpu, 0, 100); + $results["load"] = $load; + $results["disk"] = trim(shell_exec("df -h / 2>&1 | tail -1")); + break; + case "gitea_push_all": + // Launch background sync - don't block + $script = "/tmp/gitea_sync.sh"; + file_put_contents($script, "#!/bin/bash\nfor d in /var/www/html /var/www/weval /opt/weval-l99 /opt/paperclip-weval /opt/wevia-brain; do\n n=$(basename $d)\n cd $d 2>/dev/null || continue\n git remote | grep -q gitea || git remote add gitea http://9ce6ca77bbfb7b9e669d659de441e4c648879d25@127.0.0.1:3300/yanis/$n.git 2>/dev/null\n git push gitea --all 2>&1 | tail -1 >> /tmp/gitea_sync.log\ndone\necho DONE_$(date +%s) >> /tmp/gitea_sync.log\n"); + chmod($script, 0755); + shell_exec("nohup bash $script > /tmp/gitea_sync.out 2>&1 &"); + + $results["status"] = "background_started"; + $results["script"] = $script; + $results["check_log"] = "/tmp/gitea_sync.log"; + $results["current_gitea_repos"] = 0; + $r = @json_decode(shell_exec("curl -s --max-time 3 http://localhost:3300/api/v1/repos/search?limit=100"), true); + $results["current_gitea_repos"] = count($r["data"] ?? []); + $results["repos_names"] = array_slice(array_map(fn($rp)=>$rp["name"], $r["data"] ?? []), 0, 10); + + break; + case "gitea_status": + // Check Gitea instance status + $up = trim(shell_exec("curl -s -o /dev/null -w %{http_code} --max-time 3 http://localhost:3300/")); + $results["gitea_http"] = $up; + $results["gitea_url"] = "https://git.weval-consulting.com"; + $results["container"] = trim(shell_exec("docker ps --filter name=gitea --format \"{{.Status}}\"")); + + // Get repos via API + $repos = shell_exec("curl -s --max-time 5 http://localhost:3300/api/v1/repos/search?limit=50"); + $d = @json_decode($repos, true); + $results["total_repos"] = $d["total_count"] ?? 0; + $results["repos_sample"] = array_slice(array_map(fn($r)=>$r["full_name"], $d["data"] ?? []), 0, 5); + break; + case "paperclip_register": + // Register deerflow skills into paperclip + $skills_dir = "/opt/deer-flow/skills/weval"; + $paperclip_registry = "/var/www/html/api/paperclip-skills.json"; + + $skills = []; + if (is_dir($skills_dir)) { + foreach (scandir($skills_dir) as $s) { + if ($s[0] !== "." && $s !== "-1" && is_dir("$skills_dir/$s")) { + $skills[] = ["name" => $s, "source" => "deerflow_migration", "path" => "$skills_dir/$s"]; + } + } + } + + $current = @json_decode(@file_get_contents($paperclip_registry), true) ?: ["agents"=>[],"skills"=>[]]; + $current["skills"] = $skills; + $current["last_sync"] = date("Y-m-d H:i:s"); + $current["total_skills"] = count($skills); + + file_put_contents($paperclip_registry, json_encode($current, JSON_PRETTY_PRINT)); + + $results["registered"] = count($skills); + $results["registry"] = $paperclip_registry; + $results["sample"] = array_slice($skills, 0, 5); + break; + case "l99_status": + $state = @json_decode(@file_get_contents("/var/www/html/api/l99-state.json"),true); + $layers = $state["layers"] ?? []; + $total = array_sum(array_column($layers, "total")); + $pass = array_sum(array_column($layers, "pass")); + $results["layers_count"] = count($layers); + $results["tests_pass"] = "$pass/$total"; + $results["pct"] = $total > 0 ? round(100*$pass/$total) . "%" : "0%"; + $results["timestamp"] = $state["timestamp"] ?? "?"; + $results["top_layers"] = []; + foreach ($layers as $name => $l) { + if (($l["total"] ?? 0) >= 10) { + $results["top_layers"][$name] = ($l["pass"] ?? 0) . "/" . ($l["total"] ?? 0); + } + } + break; + case "wiki_status": + $wiki_dir = "/var/www/html/wiki"; + $md_files = glob("$wiki_dir/*.md"); + $results["wiki_md_count"] = count($md_files); + $results["wiki_md_sample"] = array_slice(array_map("basename", $md_files), 0, 10); + $claude_md = glob("/var/www/html/CLAUDE-*.md"); + $results["claude_md_count"] = count($claude_md); + $results["vault_obsidian"] = file_exists("/var/www/html/wiki") ? "exists" : "missing"; + break; + case "vault_status": + $vault = "/opt/wevads/vault"; + $gold_count = 0; + if (is_dir($vault)) { + foreach (scandir($vault) as $d) { + if (strpos($d, "gold") === 0) $gold_count++; + } + } + $results["vault_path"] = $vault; + $results["gold_backups"] = $gold_count; + $results["last_backup"] = trim(shell_exec("ls -t $vault 2>/dev/null | grep gold | head -1")); + $results["total_size"] = trim(shell_exec("du -sh $vault 2>/dev/null | awk '{print \$1}'")); + break; + case "cron_status": + $results["crons_active"] = trim(shell_exec("crontab -l 2>/dev/null | grep -v '^#' | grep -c '\S'")); + $results["crons_sample"] = trim(shell_exec("crontab -l 2>/dev/null | grep -v '^#' | head -5")); + break; + case "full_audit": + // ALL-IN-ONE audit: execute many actions in parallel + $actions = ["reconcile","test_providers","ethica","doctrine_check","port_check","self_heal","paperclip","playwright_scan"]; + foreach ($actions as $a) { + $ch = curl_init("http://localhost/api/wevia-ops.php?k=BLADE2026&action=$a"); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>15]); + $r = json_decode(curl_exec($ch), true); + curl_close($ch); + $results[$a] = $r["results"] ?? "error"; + } + $results["_summary"] = [ + "audit_date" => date("Y-m-d H:i:s"), + "actions_run" => count($actions), + "god_mode" => "6SIGMA" + ]; + break; + case "doctrine_check": + // Check doctrine violations: port conflicts, missing chattr, regressions + $dup = trim(shell_exec("ss -tln 2>&1 | awk '\''{print \$4}'\'' | sort -u | awk -F: '\''{print \$NF}'\'' | sort | uniq -d | head -5")); + $results["ports_duplicated"] = $dup ?: "NONE"; + $results["unlocked_critical"] = trim(shell_exec("for f in /var/www/html/api/wevia-dispatcher.php /var/www/html/api/wevia-ops.php /var/www/html/wevia-ia/wevia.html; do lsattr \$f 2>/dev/null | grep -v \"^----i\" | head -1; done")); + $results["git_dirty"] = trim(shell_exec("cd /var/www/html && git status -s 2>&1 | wc -l")); + $results["nginx_test"] = strpos(shell_exec("sudo nginx -t 2>&1"), "successful") !== false ? "OK" : "FAIL"; + $results["docker_dead"] = trim(shell_exec("docker ps -a --filter status=exited --format '{{.Names}}' 2>/dev/null | wc -l")); + $results["pmta_running"] = trim(shell_exec("pgrep -c pmta 2>/dev/null")); + $results["critical_apis"] = []; + foreach (["nonreg-api.php","wevia-ops.php","wevia-dispatcher.php","wevia-webchat-direct.php"] as $api) { + $results["critical_apis"][$api] = file_exists("/var/www/html/api/$api") ? "exists" : "MISSING"; + } + $results["status"] = ($results["ports_duplicated"] == "NONE") ? "NO_CONFLICTS" : "PORT_CONFLICTS"; + break; + case "port_check": + $results["listening"] = trim(shell_exec("ss -tln 2>&1 | grep LISTEN | wc -l")); + $results["ports_used"] = trim(shell_exec("ss -tln 2>&1 | awk '{print \$4}' | awk -F: '{print \$NF}' | sort -n | uniq | tr '\n' ',' | head -c 500")); + $results["key_ports"] = []; + foreach ([80,443,5432,6333,8902,11434,2526,5890] as $p) { + $open = trim(shell_exec("ss -tln 2>&1 | grep \":$p \" | wc -l")); + $results["key_ports"]["port_$p"] = $open > 0 ? "LISTEN" : "FREE"; + } + break; + case "deerflow_purge": + foreach (["deerflow.service","deerflow-web.service","deerflow-langgraph.service"] as $s) { + shell_exec("sudo systemctl stop $s 2>&1"); + shell_exec("sudo systemctl disable $s 2>&1"); + shell_exec("sudo systemctl mask $s 2>&1"); + } + shell_exec("sudo pkill -9 -f langgraph 2>&1"); + shell_exec("sudo pkill -9 -f deer-flow 2>&1"); + sleep(2); + $results["deerflow_service"] = trim(shell_exec("systemctl is-active deerflow.service 2>&1")); + $results["port_2024"] = trim(shell_exec("ss -tln 2>&1 | grep 2024 | wc -l")); + $results["port_3000"] = trim(shell_exec("ss -tln 2>&1 | grep 3000 | wc -l")); + $results["status"] = "PURGED"; + break; + case "deerflow_start": + // Start DeerFlow via sudo (www-data has NOPASSWD ALL) + $running = trim(shell_exec("ss -tln 2>&1 | grep 2024 | wc -l")); + if ($running > 0) { + $results["status"] = "already_running"; + } else { + shell_exec("cd /opt/deer-flow && sudo nohup make dev-daemon > /tmp/df.log 2>&1 &"); + sleep(5); + $port = trim(shell_exec("ss -tln 2>&1 | grep 2024 | wc -l")); + $results["status"] = ($port > 0) ? "started" : "starting"; + $results["port_2024"] = ($port > 0) ? "LISTEN" : "NOT_YET"; + $results["log"] = trim(shell_exec("tail -5 /tmp/df.log 2>/dev/null")); + } + break; + case "nginx_fix": + // Fix nginx.pid permission + $r1 = trim(shell_exec("sudo chown www-data:www-data /run/nginx.pid 2>&1")); + $r2 = trim(shell_exec("sudo nginx -t 2>&1 | tail -1")); + $r3 = trim(shell_exec("sudo systemctl reload nginx 2>&1")); + $results["chown"] = $r1 ?: "OK"; + $results["test"] = $r2; + $results["reload"] = $r3 ?: "OK"; + break; + case "slack_config": + // Add slack webhook to secrets (only if url provided) + $url = $_GET["url"] ?? $_POST["url"] ?? ""; + if ($url && preg_match("/^https:\/\/hooks\.slack\.com/", $url)) { + shell_exec("sudo sed -i '/^SLACK_WEBHOOK=/d' /etc/weval/secrets.env 2>/dev/null"); + shell_exec("echo 'SLACK_WEBHOOK=" . escapeshellarg($url) . "' | sudo tee -a /etc/weval/secrets.env > /dev/null"); + $results["configured"] = true; + $results["url"] = substr($url, 0, 50) . "..."; + } else { + $results["configured"] = false; + $results["hint"] = "Pass ?url=https://hooks.slack.com/services/..."; + $env = @file_get_contents("/etc/weval/secrets.env"); + $results["current"] = $env && strpos($env, "SLACK_WEBHOOK") !== false ? "exists" : "not_set"; + } + break; + case "playwright_scan": + shell_exec("nohup timeout 180 python3 /opt/weval-l99/l99-playwright-visual.py > /tmp/pwv.log 2>&1 &"); + $state = @json_decode(@file_get_contents("/var/www/html/api/l99-state.json"),true); + $layers = $state["layers"] ?? []; + $visual = $layers["PLAYWRIGHT-VISUAL"] ?? []; + $results["status"] = "Scan lance"; + $results["last_scan"] = $state["timestamp"] ?? "?"; + $results["visual"] = ($visual["pass"] ?? 0) . "/" . ($visual["total"] ?? 0); + break; + case "paperclip": + $f = glob("/var/www/html/api/paperclip*.json")[0] ?? glob("/opt/paperclip/agents.json")[0] ?? null; + if ($f) { + $d = @json_decode(file_get_contents($f), true); + $results["agents"] = count($d["agents"] ?? $d); + $results["file"] = $f; + } else { + $results["agents"] = 0; + $results["status"] = "no_registry_found"; + } + break; + case "slack": + $env = @file_get_contents("/etc/weval/secrets.env"); + if ($env && preg_match("/SLACK_WEBHOOK=(\S+)/", $env, $m)) { + $url = trim($m[1], '"\''); + $ch = curl_init($url); + curl_setopt_array($ch, [CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5, + CURLOPT_HTTPHEADER=>["Content-Type: application/json"], + CURLOPT_POSTFIELDS=>json_encode(["text"=>"WEVIA Master wire test 16avr"])]); + $r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + $results["webhook"] = "configured"; + $results["test"] = ($code == 200) ? "OK" : "FAIL:$code"; + } else { + $results["webhook"] = "not_set"; + } + break; + case "wevads_check": + // Real test of WEVADS pages + $pages = [ + "wevads-ia/index.html" => "WEVADS IA", + "wevads-hub.html" => "WEVADS Hub", + "wevads-performance.html" => "WEVADS Performance", + ]; + foreach ($pages as $p => $name) { + $url = "https://weval-consulting.com/$p"; + $code = trim(shell_exec("curl -so /dev/null -w '%{http_code}' --max-time 5 $url 2>&1")); + $exists = file_exists("/var/www/html/$p") ? "OK" : "MISS"; + $results[$name] = "$code/$exists"; + } + // APIs + $apis = ["wevads-modules.php","wevads-v2-api.php","wevads-p1-api.php","wevads-p2-api.php","wevads-p3-api.php","wevads-p4-api.php","wevads-p5-api.php"]; + $ok = 0; + foreach ($apis as $a) { + $code = trim(shell_exec("curl -so /dev/null -w '%{http_code}' --max-time 3 https://weval-consulting.com/api/$a 2>&1")); + if ($code == "200") $ok++; + } + $results["apis"] = "$ok/" . count($apis) . " OK"; + // Count accounts + $dbc = @shell_exec("PGPASSWORD=admin123 psql -h 10.1.0.3 -U admin -d adx_system -tAc 'SELECT COUNT(*) FROM send_contacts LIMIT 1' 2>/dev/null"); + $results["send_contacts"] = trim($dbc) ?: "?"; + break; + case "ethica": + // Real Ethica stats + pages + APIs test + $dbc = @shell_exec("PGPASSWORD=admin123 psql -h 10.1.0.3 -U admin -d adx_system -tAc \"SELECT pays, COUNT(*) FROM ethica.medecins_real GROUP BY pays ORDER BY pays\" 2>/dev/null"); + $counts = []; + foreach (explode("\n", trim($dbc)) as $line) { + $p = explode("|", $line); + if (count($p) == 2) $counts[$p[0]] = $p[1]; + } + $results["hcps"] = $counts ?: "DB_unreachable"; + $results["total"] = array_sum($counts); + + // Emails coverage + $em = @shell_exec("PGPASSWORD=admin123 psql -h 10.1.0.3 -U admin -d adx_system -tAc \"SELECT COUNT(*) FROM ethica.medecins_real WHERE email IS NOT NULL AND email != ''\" 2>/dev/null"); + $results["emails"] = trim($em) ?: "?"; + + // Pages + $pages = ["ethica-hub.html","ethica-hcp-manager.html","ethica-monitor.html","ethica-pipeline.html","ethica-chatbot.html","ethica-drill.html","ethica-sms.html","ethica-login.html"]; + $ok = 0; + foreach ($pages as $p) { + $code = trim(shell_exec("curl -so /dev/null -w '%{http_code}' --max-time 3 https://weval-consulting.com/$p 2>&1")); + if ($code == "200" || $code == "302") $ok++; + } + $results["pages"] = "$ok/" . count($pages); + + // APIs + $apis = ["ethica-api.php","ethica-collector-api.php","ethica-consent-api.php","ethica-data-api.php","ethica-enrich-api.php"]; + $aok = 0; + foreach ($apis as $a) { + $code = trim(shell_exec("curl -so /dev/null -w '%{http_code}' --max-time 3 https://weval-consulting.com/api/$a 2>&1")); + if (in_array($code, ["200","302","401","405"])) $aok++; + } + $results["apis"] = "$aok/" . count($apis); + + // Crons + $crons = trim(shell_exec("crontab -l 2>/dev/null | grep -ic ethica")); + $results["crons"] = $crons; + + // Consent + $cons = trim(shell_exec("PGPASSWORD=admin123 psql -h 10.1.0.3 -U admin -d adx_system -tAc \"SELECT COUNT(*) FROM ethica.consent_log\" 2>/dev/null")); + $results["consent_optins"] = $cons ?: "?"; + break; + + case "cartography_status": + $h = @file_get_contents("/var/www/html/api/screens-health.json"); + if(!$h){ $results=["error"=>"no health data"]; break; } + $j = json_decode($h, true); + $c = $j["counts"] ?? []; + $defective = ($c["DOWN"]??0)+($c["BROKEN"]??0)+($c["NOT_FOUND"]??0)+($c["ERROR"]??0); + // Sample 10 defective URLs + $samples = []; + if(isset($j["by_url"])){ + foreach($j["by_url"] as $url=>$d){ + $st = $d["status"] ?? ""; + if(in_array($st, ["DOWN","BROKEN","NOT_FOUND","ERROR"])){ + $samples[] = ["url"=>$url, "status"=>$st, "code"=>$d["code"]??"?"]; + if(count($samples)>=10) break; + } + } + } + $results = [ + "total" => $j["total"] ?? 0, + "generated_at" => $j["generated_at"] ?? "?", + "elapsed_sec" => $j["elapsed_sec"] ?? 0, + "counts" => $c, + "defective_total" => $defective, + "sample_defective" => $samples, + "filter_url" => "https://weval-consulting.com/cartographie-screens.html (clic sur DEFECTUEUX)" + ]; + break; + + + case "smart_classify": + // Run smart classifier (re-probe with POST) + $log = "/tmp/smart_classify.log"; + $cmd = "nohup python3 /var/www/html/api/screens-health-smart.py > $log 2>&1 &"; + exec($cmd); + // If smart json already exists, return summary + $sf = "/var/www/html/api/screens-health-smart.json"; + if (file_exists($sf)) { + $sj = json_decode(file_get_contents($sf), true); + $results = [ + "status" => "smart_classifier_started_background", + "previous_smart_run" => $sj["generated_at"] ?? "none", + "smart_counts" => $sj["smart_counts"] ?? [], + "rechecked" => $sj["rechecked"] ?? 0, + "real_problems_count" => count($sj["real_problems"] ?? []), + "sample_real_problems" => array_slice($sj["real_problems"] ?? [], 0, 8), + "log" => $log + ]; + } else { + $results = ["status" => "smart_classifier_started_first_time", "log" => $log, "wait_seconds" => 30]; + } + break; + + + case "find_watchdog": + $r = shell_exec("python3 /var/www/html/api/find-watchdog.py 2>&1 | head -c 5000"); + $results = json_decode(trim($r), true) ?: ["raw"=>$r, "parse_error"=>true]; + break; + case "smart_classify_safe": + $log = "/tmp/smart_safe.log"; + exec("nohup python3 /var/www/html/api/screens-health-smart-safe.py > $log 2>&1 &"); + $sf = "/var/www/html/api/screens-health-smart.json"; + if (file_exists($sf)) { + $sj = json_decode(file_get_contents($sf), true); + $results = ["status"=>"safe_classifier_running_bg", "previous_run"=>$sj["generated_at"]??"none", + "smart_counts"=>$sj["smart_counts"]??[], "rechecked"=>$sj["rechecked"]??0, + "false_positives_count"=>count($sj["false_positives"]??[]), + "real_problems_count"=>count($sj["real_problems"]??[]), + "log"=>$log, "workers"=>"5 (safe)"]; + } else { + $results = ["status"=>"safe_classifier_first_run","wait_seconds"=>120, "log"=>$log]; + } + break; + case "investigate_500": + $r = shell_exec("python3 /var/www/html/api/investigate-500.py 2>&1 | head -c 5000"); + $results = json_decode(trim($r), true) ?: ["raw"=>$r, "parse_error"=>true]; + break; + case "reclassify_health": + $r = shell_exec("sudo python3 /var/www/html/api/reclassify-health.py 2>&1 | head -c 3000"); + $results = json_decode(trim($r), true) ?: ["raw"=>$r, "parse_error"=>true]; + break; + + + case "watchdog_find": + // Find everything that restarts nginx + $results = ["active_crons" => [], "systemd_units" => [], "scripts_with_restart_nginx" => [], "log_sources" => []]; + // 1. Root crons + $crons = @shell_exec("sudo crontab -u root -l 2>/dev/null | grep -v '^#' | grep -i 'watchdog\|nginx\|restart'"); + $results["active_crons"] = array_filter(array_map("trim", explode("\n", $crons ?: ""))); + // 2. Cron.d + $crond = @shell_exec("sudo grep -l 'nginx\|watchdog' /etc/cron.d/* 2>/dev/null | head -5"); + $results["cron_d_files"] = array_filter(array_map("trim", explode("\n", $crond ?: ""))); + // 3. Systemd timers + $timers = @shell_exec("sudo systemctl list-timers 2>/dev/null | grep -i 'nginx\|watchdog' | head -10"); + $results["systemd_units"] = trim($timers ?: "none"); + // 4. Scripts containing 'systemctl restart nginx' + $scripts = @shell_exec("sudo grep -rl 'systemctl restart nginx\|service nginx restart' /opt /usr/local/bin /usr/local/sbin /etc/cron* 2>/dev/null | head -10"); + $results["scripts_with_restart_nginx"] = array_filter(array_map("trim", explode("\n", $scripts ?: ""))); + // 5. Who wrote to the watchdog log recently + $log_writer = @shell_exec("sudo lsof /var/log/phpfpm-watchdog.log 2>/dev/null | head -5"); + $results["current_log_writer"] = trim($log_writer ?: "none"); + // 6. Last 3 entries of the log + $last = @shell_exec("sudo tail -3 /var/log/phpfpm-watchdog.log 2>/dev/null"); + $results["last_log_entries"] = trim($last ?: ""); + break; + + case "watchdog_disable": + // Disable nginx-restart watchdog safely (comment out in cron) + $disabled = []; + // Find cron lines with restart nginx AND comment them + $crons = @shell_exec("sudo crontab -u root -l 2>/dev/null"); + if ($crons) { + $lines = explode("\n", $crons); + $new_lines = []; + foreach ($lines as $l) { + if (preg_match("/watchdog/i", $l) && strpos($l, "#") !== 0 && trim($l) !== "") { + $new_lines[] = "# DISABLED_BY_WEVIA_" . date("Ymd") . " " . $l; + $disabled[] = $l; + } else { + $new_lines[] = $l; + } + } + if (!empty($disabled)) { + $tmp = tempnam("/tmp", "cron_"); + file_put_contents($tmp, implode("\n", $new_lines)); + @shell_exec("sudo crontab -u root $tmp 2>&1"); + @unlink($tmp); + } + } + $results = ["disabled_lines" => $disabled, "count" => count($disabled)]; + break; + + case "investigate_500": + // Sample TRULY_BROKEN files and check their first 20 lines for common includes + $sf = "/var/www/html/api/screens-health-smart.json"; + $results = ["broken_sampled" => [], "common_includes" => [], "pattern_analysis" => ""]; + if (!file_exists($sf)) { $results["error"] = "no smart data"; break; } + $sj = json_decode(file_get_contents($sf), true); + $broken = array_filter($sj["real_problems"] ?? [], fn($p) => $p["status"] === "TRULY_BROKEN"); + $sample = array_slice($broken, 0, 30); + $include_counts = []; + foreach ($sample as $p) { + $url = $p["url"]; + $local = str_replace("https://weval-consulting.com/", "/var/www/html/", $url); + if (file_exists($local)) { + $head = @file_get_contents($local, false, null, 0, 3000); + preg_match_all("/(?:require_once|include_once|require|include)\s*[(\"\'].*?[\"\']/", $head, $m); + foreach ($m[0] as $inc) { + $include_counts[$inc] = ($include_counts[$inc] ?? 0) + 1; + } + $results["broken_sampled"][] = ["url" => $url, "file_size" => filesize($local), "code" => $p["post_code"]]; + } else { + $results["broken_sampled"][] = ["url" => $url, "file_missing" => true]; + } + } + arsort($include_counts); + $results["common_includes"] = array_slice($include_counts, 0, 10, true); + // Now check if the most-common include itself has a problem + if (!empty($include_counts)) { + $top = array_keys($include_counts)[0]; + preg_match("/[\"\']([^\"\']+)[\"\']/", $top, $pm); + if ($pm) { + $inc_path = $pm[1]; + if ($inc_path[0] !== "/") $inc_path = "/var/www/html/api/" . basename($inc_path); + if (file_exists($inc_path)) { + $lint = @shell_exec("php -l $inc_path 2>&1"); + $results["top_include_lint"] = ["path" => $inc_path, "result" => trim($lint)]; + } else { + $results["top_include_missing"] = $inc_path; + } + } + } + break; + + case "reclass_health": + // Merge smart classifier results back into screens-health.json + $hf = "/var/www/html/api/screens-health.json"; + $sf = "/var/www/html/api/screens-health-smart.json"; + if (!file_exists($hf) || !file_exists($sf)) { $results = ["error"=>"missing files"]; break; } + $h = json_decode(file_get_contents($hf), true); + $s = json_decode(file_get_contents($sf), true); + // Build reclass map + $reclass = []; + foreach (($s["details_by_status"] ?? []) as $smart_st => $urls) { + foreach ($urls as $u) { + if ($u === "...") continue; + // Reclassify: POST_OK/POST_BAD_REQUEST/POST_302/POST_NOT_ALLOWED → UP, AUTH_REQUIRED → PROTECTED + if (in_array($smart_st, ["POST_OK","POST_BAD_REQUEST","POST_302","POST_NOT_ALLOWED"])) $reclass[$u] = "UP"; + elseif ($smart_st === "AUTH_REQUIRED") $reclass[$u] = "PROTECTED"; + elseif ($smart_st === "RATE_LIMITED") $reclass[$u] = "SLOW"; // rate limit = visible but slow + } + } + // Apply reclass + $changed = 0; + foreach ($h["by_url"] as $u => &$d) { + if (isset($reclass[$u]) && $d["status"] !== $reclass[$u]) { + $d["smart_reclass"] = $d["status"]; // keep history + $d["status"] = $reclass[$u]; + $changed++; + } + } + unset($d); + // Recount + $counts = []; + foreach ($h["by_url"] as $d) { $counts[$d["status"]] = ($counts[$d["status"]] ?? 0) + 1; } + $h["counts"] = $counts; + $h["reclassified_at"] = date("c"); + $h["reclassified_count"] = $changed; + // Backup + write + copy($hf, $hf . ".pre-reclass-" . date("Ymd-His")); + file_put_contents($hf, json_encode($h, JSON_PRETTY_PRINT)); + $results = ["reclassified" => $changed, "new_counts" => $counts]; + break; + + + case "l99_full_scan": + // Trigger FULL 74-layer scan (background, CPU heavy - on-demand only) + $log = "/opt/weval-l99/logs/last-run.log"; + $pid_file = "/tmp/l99-fullscan.pid"; + // Check if already running + if (file_exists($pid_file)) { + $pid = trim(@file_get_contents($pid_file)); + if ($pid && file_exists("/proc/$pid")) { + $results = ["status"=>"already_running", "pid"=>$pid, "log"=>$log, "tail"=>trim(@shell_exec("tail -10 $log"))]; + break; + } + } + // Launch background + $cmd = "cd /opt/weval-l99 && nohup bash run-full.sh > /tmp/l99-fullscan.log 2>&1 & echo $! > $pid_file"; + @shell_exec($cmd); + sleep(1); + $new_pid = trim(@file_get_contents($pid_file)); + $results = [ + "status" => "full_scan_started_background", + "pid" => $new_pid, + "log" => $log, + "estimated_duration" => "30-60 minutes (74 layers)", + "note" => "CPU heavy - use only on-demand. Check later with same action.", + "previous_state" => @json_decode(@file_get_contents("/opt/weval-l99/l99-state.json"), true) + ]; + break; + + case "l99_full_status": + // Check full-scan progress + $log = "/opt/weval-l99/logs/last-run.log"; + $pid_file = "/tmp/l99-fullscan.pid"; + $running = false; + if (file_exists($pid_file)) { + $pid = trim(@file_get_contents($pid_file)); + if ($pid && file_exists("/proc/$pid")) $running = true; + } + // Count layer progress in log + $layers_done = 0; + if (file_exists($log)) { + $layers_done = (int) trim(@shell_exec("grep -c '^\s*OK\s\[' $log 2>/dev/null")); + } + $results = [ + "running" => $running, + "layers_seen_ok" => $layers_done, + "log_file" => $log, + "last_lines" => trim(@shell_exec("tail -8 $log 2>/dev/null")), + "current_state" => @json_decode(@file_get_contents("/opt/weval-l99/l99-state.json"), true)["layers_count"] ?? "?" + ]; + break; + + case "file_create": + // OPUS WIRE 16avr — permet à WEVIA de créer des scripts/fichiers via chat NL + // Allowlist stricte + syntax check + GOLD auto si override + $target = $_GET['path'] ?? $_POST['path'] ?? ''; + $content_b64 = $_GET['content_b64'] ?? $_POST['content_b64'] ?? ''; + $allowed_prefixes = ['/tmp/', '/opt/weval-ops/', '/var/log/wevia/', '/var/www/html/wiki/', '/opt/obsidian-vault/sessions/', '/opt/obsidian-vault/learnings/']; + $ok = false; + foreach ($allowed_prefixes as $p) if (strpos($target, $p) === 0) $ok = true; + if (!$ok || !$content_b64) { $results = ["error"=>"path_not_allowed_or_empty","allowed"=>$allowed_prefixes]; break; } + $bin = base64_decode($content_b64, true); + if ($bin === false) { $results = ["error"=>"b64_decode_fail"]; break; } + // Syntax check selon extension + $ext = strtolower(pathinfo($target, PATHINFO_EXTENSION)); + $tmp = tempnam('/tmp', 'fc'); + file_put_contents($tmp, $bin); + $lint = ''; + if ($ext === 'php') $lint = shell_exec("php8.4 -l $tmp 2>&1"); + elseif ($ext === 'sh') $lint = shell_exec("bash -n $tmp 2>&1"); + elseif ($ext === 'py') $lint = shell_exec("python3 -c \"import py_compile,sys; py_compile.compile(sys.argv[1])\" $tmp 2>&1"); + if ($lint && $ext === 'php' && strpos($lint,'No syntax errors')===false) { @unlink($tmp); $results=["error"=>"php_lint_fail","detail"=>$lint]; break; } + if ($lint && $ext === 'sh' && trim($lint) !== '') { @unlink($tmp); $results=["error"=>"bash_lint_fail","detail"=>$lint]; break; } + if ($lint && $ext === 'py' && trim($lint) !== '') { @unlink($tmp); $results=["error"=>"python_lint_fail","detail"=>$lint]; break; } + // GOLD auto si override + $existed = file_exists($target); + if ($existed) { + $gdir = '/opt/wevads/vault/auto-gold-' . date('Ymd'); + @mkdir($gdir, 0755, true); + @copy($target, $gdir . '/' . basename($target) . '.' . time() . '.GOLD'); + } + // Write + chmod exec if script — sudo fallback pour /opt/* + $dir = dirname($target); + if (!is_dir($dir)) { @mkdir($dir, 0755, true); if (!is_dir($dir)) shell_exec("sudo mkdir -p " . escapeshellarg($dir)); } + $n = @file_put_contents($target, $bin); + if ($n === false && strpos($target, "/opt/") === 0) { + $tmpw = tempnam("/tmp", "fcw"); + file_put_contents($tmpw, $bin); + shell_exec("sudo cp " . escapeshellarg($tmpw) . " " . escapeshellarg($target)); + shell_exec("sudo chown www-data:www-data " . escapeshellarg($target)); + @unlink($tmpw); + clearstatcache(); + $n = file_exists($target) ? filesize($target) : false; + } + if (in_array($ext, ['sh','py'])) { @chmod($target, 0755); shell_exec("sudo chmod +x " . escapeshellarg($target)); } + @unlink($tmp); + $results = [ + "wrote" => $n, + "path" => $target, + "md5" => md5_file($target), + "existed" => $existed, + "lint_ext" => $ext, + "lint_ok" => true, + "gold_backup" => $existed, + ]; + break; + + case "generate_script": + // OPUS v2 — ferme la boucle autonomie: NL → LLM souverain → file_create + // Usage: ?action=generate_script&description=...&target=/tmp/foo.sh&lang=bash|php|python + $desc = $_REQUEST["description"] ?? ""; + $target = $_REQUEST["target"] ?? ""; + $lang = strtolower($_REQUEST["lang"] ?? "bash"); + $lang_hints = [ + "bash" => "shebang #!/bin/bash, set -u, PAS de log fichier sauf si demande explicite, exit codes propres, pas d'interactif, sortie stdout pour la reponse", + "php" => "tag "python3 shebang, try/except globaux, json.dump utf-8, no external deps sauf stdlib", + ]; + if (!$desc || !$target) { $results=["error"=>"missing description or target"]; break; } + $hint = $lang_hints[$lang] ?? ""; + $system = "Tu generes UNIQUEMENT du code $lang sans markdown, sans backticks, sans explication. Regles: $hint. Le code doit etre directement executable. Zero commentaire superflu."; + $user = "Genere un script $lang qui fait: $desc\n\nUNIQUEMENT le code brut, rien d'autre."; + + $ch = curl_init("http://127.0.0.1:4000/v1/chat/completions"); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => ["Content-Type: application/json"], + CURLOPT_POSTFIELDS => json_encode([ + "messages" => [ + ["role"=>"system","content"=>$system], + ["role"=>"user","content"=>$user], + ], + "max_tokens" => 1500, + "stream" => false, + "temperature" => 0.1, + ]), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + $raw = curl_exec($ch); curl_close($ch); + $d = @json_decode($raw, true); + $code = $d["choices"][0]["message"]["content"] ?? ""; + // Strip markdown si LLM a mis quand même + $code = preg_replace('/^```[a-z]*\n?/m', '', $code); + $code = preg_replace('/\n?```\s*$/m', '', $code); + $code = trim($code); + if (!$code) { $results=["error"=>"llm_empty","raw"=>substr($raw,0,400)]; break; } + + // Delegate to file_create internally via re-assign + $_REQUEST["path"] = $target; + $_REQUEST["content_b64"] = base64_encode($code); + // Simule le switch file_create en recapturant sa logique + $target2 = $_REQUEST["path"]; + $b64 = $_REQUEST["content_b64"]; + $allowed = ["/tmp/", "/opt/weval-ops/", "/var/log/wevia/", "/var/www/html/wiki/", "/opt/obsidian-vault/sessions/", "/opt/obsidian-vault/learnings/"]; + $ok = false; foreach($allowed as $p) if (strpos($target2, $p) === 0) $ok = true; + if (!$ok) { $results=["error"=>"path_not_allowed","generated_code"=>substr($code,0,500)]; break; } + + $bin = base64_decode($b64, true); + $ext = strtolower(pathinfo($target2, PATHINFO_EXTENSION)); + $tmpf = tempnam("/tmp","gs"); + file_put_contents($tmpf, $bin); + $lint = ""; + if ($ext === "php") $lint = shell_exec("php8.4 -l $tmpf 2>&1"); + elseif ($ext === "sh") $lint = shell_exec("bash -n $tmpf 2>&1"); + elseif ($ext === "py") $lint = shell_exec("python3 -c \"import py_compile,sys; py_compile.compile(sys.argv[1])\" $tmpf 2>&1"); + $lint_fail = false; + if ($ext==="php" && $lint && strpos($lint,"No syntax errors")===false) $lint_fail = true; + if ($ext==="sh" && $lint && trim($lint)!=="") $lint_fail = true; + if ($ext==="py" && $lint && trim($lint)!=="") $lint_fail = true; + if ($lint_fail) { @unlink($tmpf); $results=["error"=>"llm_generated_bad_syntax","lint"=>$lint,"code_preview"=>substr($code,0,500)]; break; } + + // Write with sudo fallback si /opt + $n = @file_put_contents($target2, $bin); + if ($n === false && strpos($target2, "/opt/") === 0) { + shell_exec("sudo cp " . escapeshellarg($tmpf) . " " . escapeshellarg($target2)); + shell_exec("sudo chown www-data:www-data " . escapeshellarg($target2)); + clearstatcache(); + $n = file_exists($target2) ? filesize($target2) : false; + } + if (in_array($ext,["sh","py"])) { @chmod($target2,0755); shell_exec("sudo chmod +x " . escapeshellarg($target2)); } + @unlink($tmpf); + + $results = [ + "ok" => (bool)$n, + "path" => $target2, + "lang" => $lang, + "bytes" => $n, + "md5" => $n ? md5_file($target2) : null, + "lint_passed" => !$lint_fail, + "llm_model" => $d["model"] ?? "sovereign-cascade", + "code_preview" => substr($code, 0, 300), + ]; + break; + + + case "deep_diagnostic_modules": + // Deep diagnose cartographie + L99 modules + $results = ["cartographie" => [], "l99_module" => [], "l99_saas" => [], "issues" => []]; + + // === Cartographie === + $hf = "/var/www/html/api/screens-health.json"; + if (file_exists($hf)) { + $h = json_decode(file_get_contents($hf), true); + $total_404 = 0; $total_500 = 0; $sample_404 = []; $sample_500 = []; + foreach (($h["by_url"] ?? []) as $u => $d) { + $code = $d["code"] ?? 0; + if ($code == 404) { $total_404++; if (count($sample_404) < 5) $sample_404[] = $u; } + elseif ($code >= 500 && $code < 600) { $total_500++; if (count($sample_500) < 5) $sample_500[] = $u; } + } + $results["cartographie"] = [ + "total_screens" => $h["total"] ?? 0, + "counts" => $h["counts"] ?? [], + "real_404_count" => $total_404, + "real_500_count" => $total_500, + "sample_404" => $sample_404, + "sample_500" => $sample_500, + "reclassified" => $h["reclassified_count"] ?? 0, + "last_scan" => $h["generated_at"] ?? "unknown" + ]; + if ($total_404 > 0) $results["issues"][] = "Cartographie: $total_404 vraies pages 404 detectees (iframes seraient blanches)"; + } else { + $results["issues"][] = "screens-health.json MISSING"; + } + + // === L99 main === + $l99 = "/opt/weval-l99/l99-state.json"; + if (file_exists($l99)) { + $j = json_decode(file_get_contents($l99), true); + $results["l99_module"] = [ + "total" => $j["total"] ?? 0, + "pass" => $j["pass"] ?? 0, + "fail" => $j["fail"] ?? 0, + "warn" => $j["warn"] ?? 0, + "layers_count" => $j["layers_count"] ?? 0, + "timestamp" => $j["timestamp"] ?? "unknown" + ]; + if (($j["layers_count"] ?? 0) < 20) $results["issues"][] = "L99: seulement ".($j["layers_count"])." layers (doctrine attend 74+). Relancer full_scan."; + } + + // === L99 SAAS visual === + $vis = "/opt/weval-l99/l99-visual-state.json"; + if (file_exists($vis)) { + $v = json_decode(file_get_contents($vis), true); + $results["l99_saas"] = [ + "targets" => $v["targets"] ?? $v["total"] ?? 0, + "pass" => $v["pass"] ?? 0, + "fail" => $v["fail"] ?? 0, + "warn_count" => 0, + "warn_details" => [] + ]; + // Count warns + if (isset($v["results"])) { + foreach ($v["results"] as $rn) { + if (($rn["status"] ?? "") === "WARN") { + $results["l99_saas"]["warn_count"]++; + if (count($results["l99_saas"]["warn_details"]) < 10) { + $results["l99_saas"]["warn_details"][] = ($rn["name"] ?? "?") . " (" . ($rn["pct"] ?? 0) . "% diff)"; + } + } + } + } + } + + // === Check l99-master.py patch applied === + $l99_master = "/opt/weval-l99/l99-master.py"; + if (file_exists($l99_master)) { + $mc = @file_get_contents($l99_master); + $results["l99_master_patched"] = strpos($mc, "OPUS_FIX_MINB_500") !== false; + if (!$results["l99_master_patched"]) $results["issues"][] = "l99-master.py threshold NOT patched (404 still passes as OK)"; + } + + // === Check cartographie-screens.html v2 patch === + $carto = "/var/www/html/cartographie-screens.html"; + if (file_exists($carto)) { + $cc = @file_get_contents($carto); + $results["carto_v2_patched"] = strpos($cc, "PREVIEW_V2_HTTPCHECK") !== false; + if (!$results["carto_v2_patched"]) $results["issues"][] = "cartographie preview v2 not patched"; + } + + // Summary + $results["summary"] = [ + "total_issues" => count($results["issues"]), + "carto_404_in_previews" => $total_404 ?? 0, + "l99_missing_layers" => max(0, 74 - ($j["layers_count"] ?? 0)), + "l99_visual_warns" => $results["l99_saas"]["warn_count"] ?? 0 + ]; + break; + + + case "coverage_audit": + // Deep coverage audit: detect gaps in screen scanning + $results = [ + "total_scanned" => 0, + "by_domain" => [], + "by_category" => [], + "gaps" => [], + "spa_sub_routes_missing" => [], + "suggested_urls" => [] + ]; + $hf = "/var/www/html/api/screens-health.json"; + if (!file_exists($hf)) { $results["error"] = "no screens-health"; break; } + $h = json_decode(file_get_contents($hf), true); + $urls = array_keys($h["by_url"] ?? []); + $results["total_scanned"] = count($urls); + + // Group by domain + $by_dom = []; + foreach ($urls as $u) { + preg_match("#https?://([^/]+)#", $u, $m); + $d = $m[1] ?? "unknown"; + $by_dom[$d] = ($by_dom[$d] ?? 0) + 1; + } + arsort($by_dom); + $results["by_domain"] = array_slice($by_dom, 0, 20); + + // Test known subdomains not already fully scanned + $test_doms = [ + "https://app.weval-consulting.com" => "WEVAL_app", + "https://weval.ma" => "WEVAL_life_ma", + "https://git.weval-consulting.com" => "Gitea", + "https://api.weval-consulting.com" => "API_subdomain", + "https://admin.weval-consulting.com" => "Admin", + "https://chat.weval-consulting.com" => "Chat", + "https://saas.weval-consulting.com" => "SaaS", + "https://office.weval-consulting.com" => "Office", + "https://docs.weval-consulting.com" => "Docs", + "https://status.weval-consulting.com" => "Status" + ]; + foreach ($test_doms as $url => $label) { + $ch = curl_init($url); + curl_setopt_array($ch, [CURLOPT_NOBODY => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 4, CURLOPT_SSL_VERIFYPEER => false]); + curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + $exists = isset($by_dom[parse_url($url, PHP_URL_HOST)]); + $results["suggested_urls"][] = ["url" => $url, "label" => $label, "http_code" => $code, "in_scan" => $exists]; + if ($code && $code != 404 && !$exists) { + $results["gaps"][] = "SUBDOMAIN $label ($url) = HTTP $code but NOT in scan"; + } + } + + // Detect SPA files with data-page/data-tab NOT scanned as sub-routes + $spa_files = [ + "/var/www/html/wevads-ia/index.html" => "WEVADS IA SPA", + "/var/www/html/l99.html" => "L99 Command Center", + "/var/www/html/l99-saas.html" => "L99 Mission Control", + "/var/www/html/wevia-master.html" => "WEVIA Master", + "/var/www/html/growth-engine-v2.html" => "Growth Engine", + "/var/www/html/agents-archi.html" => "Agents Archi" + ]; + foreach ($spa_files as $path => $label) { + if (file_exists($path)) { + $content = file_get_contents($path); + preg_match_all('/data-(page|tab|route|section)="([^"]+)"/', $content, $m); + $routes = array_unique($m[2]); + preg_match_all('/(?:showSection|showTab|openPage|navigateTo)\([\"\']([^\"\']+)[\"\']/', $content, $m2); + $routes = array_unique(array_merge($routes, $m2[1])); + if (count($routes) > 0) { + $results["spa_sub_routes_missing"][] = [ + "file" => basename($path), + "label" => $label, + "sub_routes_detected" => count($routes), + "sample" => array_slice($routes, 0, 10) + ]; + } + } + } + + // Categorize URLs + $cats = ["API" => 0, "HTML" => 0, "chatbot_generated" => 0, "products" => 0, "blog" => 0, "wevads_sub" => 0, "other" => 0]; + foreach ($urls as $u) { + if (strpos($u, "wevads.weval-consulting.com") !== false) $cats["wevads_sub"]++; + elseif (strpos($u, "/api/") !== false) $cats["API"]++; + elseif (strpos($u, "/generated/") !== false) $cats["chatbot_generated"]++; + elseif (strpos($u, "/products/") !== false) $cats["products"]++; + elseif (strpos($u, "/blog/") !== false) $cats["blog"]++; + elseif (strpos($u, ".html") !== false) $cats["HTML"]++; + else $cats["other"]++; + } + arsort($cats); + $results["by_category"] = $cats; + + $results["summary"] = [ + "total" => count($urls), + "gaps_count" => count($results["gaps"]), + "spa_with_subroutes" => count($results["spa_sub_routes_missing"]), + "recommendation" => count($results["gaps"]) > 0 ? "Add detected subdomains to screens-autodiscovery.py sources" : "Coverage complete for top-level" + ]; + break; + + case "run_fast_tests": + $batteries = [ + "nonreg" => ["type"=>"http","url"=>"http://127.0.0.1/api/nonreg-api.php?cat=summary","json"=>1], + "l99" => ["type"=>"http","url"=>"http://127.0.0.1/api/l99-api.php?action=stats","json"=>1], + "qa_hub" => ["type"=>"http","url"=>"http://127.0.0.1/api/qa-hub-api.php?mode=summary","json"=>1], + "test_llm" => ["type"=>"http","url"=>"http://127.0.0.1/api/test-llm.php","keyword"=>"ok"], + "test_groq" => ["type"=>"http","url"=>"http://127.0.0.1/api/test-groq.php","keyword"=>"content"], + "test_redis" => ["type"=>"http","url"=>"http://127.0.0.1/api/test-redis.php","keyword"=>"ok"], + "wevia_chat" => ["type"=>"http","url"=>"http://127.0.0.1/api/wevia-chat-test.php","keyword"=>"ok"], + "functional" => ["type"=>"shell","cmd"=>"timeout 30 python3 /opt/weval-l99/functional-tests.py 2>&1 | tail -15"], + "ethica_chat" => ["type"=>"shell","cmd"=>"timeout 30 python3 /opt/weval-l99/test-ethica-chatbot.py 2>&1 | tail -10"], + "wevads_endpoints" => ["type"=>"shell","cmd"=>"timeout 25 bash /opt/weval-l99/test-wevads-endpoints.sh 2>&1 | tail -10"], + "partners" => ["type"=>"shell","cmd"=>"timeout 20 python3 /opt/weval-l99/test_partners.py 2>&1 | tail -10"], + "playwright_state" => ["type"=>"file","path"=>"/opt/weval-l99/playwright-visual-state.json","keyword"=>"PASS"], + ]; + $start = microtime(true); + $mh = curl_multi_init(); + $hdls = []; + foreach ($batteries as $n => $b) { + if ($b["type"] !== "http") continue; + $ch = curl_init($b["url"]); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>20, CURLOPT_FOLLOWLOCATION=>true]); + curl_multi_add_handle($mh, $ch); + $hdls[$n] = $ch; + } + $running = null; + do { curl_multi_exec($mh, $running); usleep(50000); } while ($running > 0); + $out = []; + foreach ($hdls as $n => $ch) { + $resp = curl_multi_getcontent($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_multi_remove_handle($mh, $ch); + curl_close($ch); + $b = $batteries[$n]; + $pass = false; $detail = "http$code"; + if ($code >= 200 && $code < 400) { + if (isset($b["keyword"])) { + $pass = (stripos($resp, $b["keyword"]) !== false); + $detail = $pass ? "kw_ok" : "kw_miss:" . $b["keyword"]; + } else { + $j = @json_decode($resp, true); + $p = $j["pass"] ?? $j["score"] ?? null; + $f = $j["fail"] ?? null; + $pass = ($p !== null && ($f === null || $f === 0 || $f === "0")); + $detail = "pass=$p fail=$f"; + } + } + $out[$n] = ["pass"=>$pass, "http"=>$code, "detail"=>$detail]; + } + curl_multi_close($mh); + + foreach ($batteries as $n => $b) { + if ($b["type"] === "shell") { + $r = @shell_exec($b["cmd"]); + $pass = ($r && stripos($r, "error") === false && stripos($r, "fail") === false && strpos($r, "Traceback") === false); + $out[$n] = ["pass"=>$pass, "type"=>"shell", "output"=>substr(trim($r ?? ''), 0, 280)]; + } elseif ($b["type"] === "file") { + $content = @file_get_contents($b["path"]); + $pass = ($content && stripos($content, $b["keyword"] ?? "PASS") !== false); + $out[$n] = ["pass"=>$pass, "type"=>"file", "detail"=>$pass ? "found" : "missing"]; + } + } + + $passed = count(array_filter($out, fn($v) => $v["pass"] === true)); + $failed = count(array_filter($out, fn($v) => $v["pass"] === false)); + $results = [ + "summary" => ["total"=>count($out), "passed"=>$passed, "failed"=>$failed, "elapsed_s"=>round(microtime(true)-$start,2)], + "batteries" => $out, + ]; + break; + + default: + $results=["actions"=>["test_providers","webchat","nonreg","reconcile","git_push","ethica","docker_list","git_log","disk","ports","crons","services","playwright_scan","paperclip","slack"]]; +} +echo json_encode(["ok"=>true,"action"=>$action,"results"=>$results]); diff --git a/nl-autowire-status.html b/nl-autowire-status.html index 3a5f253ac..812ecc311 100644 --- a/nl-autowire-status.html +++ b/nl-autowire-status.html @@ -114,7 +114,7 @@ async function load() { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({message:'verifyfp'}) }); - const j = await r.json(); + /* HTML_GUARD_V2_BATCH */ const _t_j=await r.text(); const j; {var _q=(_t_j||'').trim();if(_q.startsWith('🏠 All${d.count}`; (d.files||[]).filter(f=>f.type==='dir').forEach(f => { sb.innerHTML += `
${typeof d.content==='string'?d.content:JSON.stringify(d.content,null,2)}`;
} catch(e) {
res.innerHTML = `Chaque couche pilotee par des agents specialises
-