diff --git a/admin-v2.html b/admin-v2.html index fd4828b65..a4a1a9332 100644 --- a/admin-v2.html +++ b/admin-v2.html @@ -99,20 +99,22 @@ async function fetchData(){ }catch(e){render();} } -const SERVICES=[ - {n:'Nginx',p:':80/:443',s:'up',t:'system'},{n:'Sovereign API',p:':4000',s:'up',t:'systemd'}, - {n:'Paperclip',p:':3100',s:'up',t:'systemd'},{n:'DeerFlow',p:':3002/:3003',s:'up',t:'systemd'}, - {n:'Ollama 12 models',p:':11434',s:'up',t:'systemd'},{n:'OpenWebUI',p:':8281',s:'up',t:'docker'}, - {n:'Flowise',p:':3033',s:'up',t:'docker'},{n:'n8n',p:':5678',s:'up',t:'docker'}, - {n:'Twenty CRM',p:':3000',s:'up',t:'docker'},{n:'Mattermost',p:':8065',s:'up',t:'docker'}, - {n:'SearXNG',p:':8080',s:'up',t:'docker'},{n:'Qdrant',p:':6333',s:'up',t:'docker'}, - {n:'Plausible',p:':8000',s:'up',t:'docker'},{n:'Authentik SSO',p:':9000',s:'up',t:'docker'}, - {n:'Vaultwarden',p:':8222',s:'up',t:'docker'},{n:'Uptime Kuma',p:':3001',s:'up',t:'docker'}, - {n:'ClickHouse',p:':8123',s:'up',t:'docker'},{n:'Loki',p:':18821',s:'down',t:'docker'}, - {n:'PMTA',p:':25',s:'up',t:'system'},{n:'KumoMTA',p:':587',s:'up',t:'system'}, - {n:'CrowdSec',p:'—',s:'up',t:'systemd'},{n:'Fail2Ban',p:'—',s:'up',t:'systemd'}, - {n:'Blade Sentinel',p:'*/60s',s:'up',t:'agent'} +/* V96.20 Opus 20avr: hardcoded SERVICES replaced by LIVE checks from /api/wevia-services-live.php */ +let SERVICES=[ + {n:'Loading services…',p:'…',s:'up',t:'system'} ]; +async function loadLiveServices(){ + try{ + const r=await fetch('/api/wevia-services-live.php?t='+Date.now()); + const d=await r.json(); + if(d && Array.isArray(d.services)){ + SERVICES = d.services.map(s=>({n:s.n, p:s.p, s:s.s, t:s.t, ms:s.ms})); + if(typeof fetchData==='function') fetchData(); + } + }catch(e){} +} +setTimeout(loadLiveServices, 600); +setInterval(loadLiveServices, 60000); /* V96.15 Opus 19avr: hardcoded ALERTS replaced by LIVE checks from /api/wevia-real-alerts.php (doctrine #4 HONNÊTE) */ let ALERTS=[ diff --git a/api/blade-tasks/wevia-agent-v4.ps1 b/api/blade-tasks/wevia-agent-v4.ps1 new file mode 100644 index 000000000..271389398 --- /dev/null +++ b/api/blade-tasks/wevia-agent-v4.ps1 @@ -0,0 +1,176 @@ +# WEVIA Blade Agent v4.0 — PERSISTENT + SELF-HEALING + MCP-READY +# Install one-liner: irm https://weval-consulting.com/downloads/blade-install.ps1 | iex +# Auto-restart via Windows Task Scheduler + NSSM service +# Polls blade-api.php, executes tasks, reports back, heartbeats every 30s + +$ErrorActionPreference = "Continue" +$VERSION = "4.0" +$BASE = "https://weval-consulting.com/api" +$KEY = "BLADE2026" +$POLL_INTERVAL = 8 +$HEARTBEAT_INTERVAL = 30 +$AGENT_DIR = "C:\ProgramData\WEVAL" +$LOG = "$AGENT_DIR\agent-v4.log" +$PID_FILE = "$AGENT_DIR\agent-v4.pid" +$WATCHDOG_FILE = "$AGENT_DIR\agent-v4.watchdog" + +New-Item -ItemType Directory -Path $AGENT_DIR -Force -ErrorAction SilentlyContinue | Out-Null + +# Write PID for watchdog +$PID | Out-File $PID_FILE -Force + +function Log-Line([string]$msg) { + $line = "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ss')] $msg" + Add-Content -Path $LOG -Value $line -ErrorAction SilentlyContinue + Write-Host $line + # Cap log at 5MB by rotating + if ((Get-Item $LOG -ErrorAction SilentlyContinue).Length -gt 5MB) { + Move-Item $LOG "$LOG.1" -Force -ErrorAction SilentlyContinue + } +} + +function Get-SystemStats { + try { + $cpu = [int]((Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction SilentlyContinue).CounterSamples[0].CookedValue) + $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue + $ramFree = [int]($os.FreePhysicalMemory / 1024) + $disk = Get-PSDrive C -ErrorAction SilentlyContinue + $diskPctFree = [int](100 * $disk.Free / ($disk.Used + $disk.Free)) + $uptime = [int]((Get-Date) - $os.LastBootUpTime).TotalMinutes + return @{cpu=$cpu; ram_free_mb=$ramFree; disk_free_pct=$diskPctFree; uptime_min=$uptime} + } catch { + return @{cpu=0; ram_free_mb=0; disk_free_pct=0; uptime_min=0} + } +} + +function Send-Heartbeat { + $stats = Get-SystemStats + try { + $body = @{ + k=$KEY; action="heartbeat" + hostname=$env:COMPUTERNAME; version=$VERSION + cpu=$stats.cpu; ram=$stats.ram_free_mb + disk=$stats.disk_free_pct; uptime=$stats.uptime_min + user=$env:USERNAME + } + Invoke-RestMethod -Uri "$BASE/blade-api.php" -Method POST -Body $body -TimeoutSec 10 -ErrorAction Stop | Out-Null + # Write watchdog timestamp + (Get-Date).Ticks | Out-File $WATCHDOG_FILE -Force + return $true + } catch { + Log-Line "HEARTBEAT_ERR $($_.Exception.Message)" + return $false + } +} + +function Poll-Task { + try { + $resp = Invoke-RestMethod -Uri "$BASE/blade-api.php?k=$KEY&action=poll" -TimeoutSec 10 -ErrorAction Stop + if ($resp.tasks -and $resp.tasks.Count -gt 0) { return $resp.tasks[0] } + # Fallback poll endpoint + $resp2 = Invoke-RestMethod -Uri "$BASE/blade-poll.php?k=$KEY&action=poll" -TimeoutSec 10 -ErrorAction SilentlyContinue + if ($resp2.task) { return $resp2.task } + } catch { + Log-Line "POLL_ERR $($_.Exception.Message)" + } + return $null +} + +function Claim-Task([object]$task) { + # Mark as dispatched on server (already done on pull) - but bump to running + try { + $body = @{k=$KEY; action="claim"; id=$task.id} + Invoke-RestMethod -Uri "$BASE/blade-api.php" -Method POST -Body $body -TimeoutSec 5 | Out-Null + } catch {} +} + +function Execute-Task([object]$task) { + $id = $task.id + $label = $task.label + $cmd = $task.cmd + if (-not $cmd) { $cmd = $task.command } + if (-not $cmd -and $task.commands) { + $cmd = ($task.commands -join "`n") + } + if (-not $cmd) { + Log-Line "EXEC_NO_CMD id=$id" + Report-Task $id "failed" "" "task has no cmd/command/commands field" + return + } + + Log-Line "EXEC_START id=$id label=$label cmd_len=$($cmd.Length)" + $startTime = Get-Date + Claim-Task $task + + $tmpScript = "$env:TEMP\wevia-task-$id.ps1" + Set-Content -Path $tmpScript -Value $cmd -Force -Encoding UTF8 + + $output = "" + $rc = 0 + try { + $output = & powershell -ExecutionPolicy Bypass -NoProfile -File $tmpScript 2>&1 | Out-String + $rc = $LASTEXITCODE + } catch { + $output = "EXCEPTION: $($_.Exception.Message)" + $rc = 1 + } finally { + Remove-Item $tmpScript -Force -ErrorAction SilentlyContinue + } + + $duration = [int]((Get-Date) - $startTime).TotalSeconds + $status = if ($rc -eq 0) { "done" } else { "failed" } + $truncated = if ($output.Length -gt 4000) { $output.Substring(0, 4000) + "...[TRUNCATED]" } else { $output } + + Log-Line "EXEC_DONE id=$id rc=$rc status=$status duration=${duration}s output_len=$($output.Length)" + Report-Task $id $status $truncated $null +} + +function Report-Task([string]$id, [string]$status, [string]$result, [string]$error_msg) { + try { + $body = @{ + k=$KEY; action="report"; id=$id; status=$status + result=$result + } + if ($error_msg) { $body.error = $error_msg } + Invoke-RestMethod -Uri "$BASE/blade-api.php" -Method POST -Body $body -TimeoutSec 15 | Out-Null + Log-Line "REPORTED id=$id status=$status" + } catch { + Log-Line "REPORT_ERR id=$id err=$($_.Exception.Message)" + } +} + +# MAIN LOOP +Log-Line "AGENT_V4_START pid=$PID host=$env:COMPUTERNAME user=$env:USERNAME" + +$lastHeartbeat = [DateTime]::MinValue +$lastPollErr = 0 +$consecutiveErrors = 0 + +while ($true) { + try { + # Heartbeat + if (((Get-Date) - $lastHeartbeat).TotalSeconds -ge $HEARTBEAT_INTERVAL) { + if (Send-Heartbeat) { $lastHeartbeat = Get-Date; $consecutiveErrors = 0 } + else { $consecutiveErrors++ } + } + + # Poll task + $task = Poll-Task + if ($task) { + Execute-Task $task + } else { + Start-Sleep -Seconds $POLL_INTERVAL + } + + # Self-heal if many consecutive errors (>10) + if ($consecutiveErrors -gt 10) { + Log-Line "TOO_MANY_ERRORS=$consecutiveErrors sleeping 60s before retry" + Start-Sleep -Seconds 60 + $consecutiveErrors = 0 + } + } catch { + Log-Line "LOOP_ERR $($_.Exception.Message)" + Start-Sleep -Seconds 5 + $consecutiveErrors++ + } +} diff --git a/api/wevia-real-alerts.php b/api/wevia-real-alerts.php index 2d7069a96..9cb18e9c9 100644 --- a/api/wevia-real-alerts.php +++ b/api/wevia-real-alerts.php @@ -75,7 +75,7 @@ if ($wa_ok) { $alerts[] = [ 'id' => 'check_whatsapp', 'ti' => 'WhatsApp Business', - 'ms' => $wa_ok ? ($wa_live ? "Token LIVE (Meta API 200)" : "Token présent ({$wa_token} chars) · API returned $code") : 'Token manquant', + 'ms' => $wa_ok ? ($wa_live ? "Token LIVE (Meta API 200)" : "Token présent (" . strlen($wa_token) . "ch) · API returned $code") : 'Token manquant', 'sv' => ($wa_ok && $wa_live) ? 'ok' : ($wa_ok ? 'warning' : 'critical'), 'evidence' => "WHATSAPP_TOKEN ".strlen($wa_token)."ch · Graph API probe code $code", 'action_required' => $wa_ok ? 'none' : 'regenerate in Meta Business Suite', diff --git a/api/wevia-services-live.php b/api/wevia-services-live.php new file mode 100644 index 000000000..adbc34660 --- /dev/null +++ b/api/wevia-services-live.php @@ -0,0 +1,124 @@ + true, 'ms' => $ms]; } + return ['up' => false, 'ms' => $ms, 'err' => $errstr]; +} + +function probe_http($url, $timeout = 3) { + $start = microtime(true); + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_NOBODY => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ]); + curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $ms = round((microtime(true) - $start) * 1000); + curl_close($ch); + return ['up' => ($code >= 200 && $code < 500), 'ms' => $ms, 'code' => $code]; +} + +function probe_systemd($unit) { + exec("systemctl is-active $unit 2>&1", $o); + $status = trim($o[0] ?? ''); + return ['up' => ($status === 'active'), 'status' => $status]; +} + +function probe_docker($name) { + exec("docker ps --filter name=$name --format '{{.Status}}' 2>&1", $o); + $status = trim(implode(' ', $o)); + $up = (strpos($status, 'Up') !== false); + return ['up' => $up, 'status' => $status ?: 'not found']; +} + +// 23 services from admin-v2 — verify each +$services = [ + // systemd / nginx / FPM + ['n' => 'Nginx', 'p' => ':80/:443', 't' => 'system', 'probe' => 'http://localhost'], + ['n' => 'Apache', 'p' => ':5890/:8443', 't' => 'system', 'probe' => 'http://localhost:5890'], + ['n' => 'PHP-FPM 8.5', 'p' => ':socket', 't' => 'systemd', 'systemd' => 'php8.5-fpm'], + // LLM + APIs + ['n' => 'Sovereign API', 'p' => ':4000', 't' => 'systemd', 'probe' => 'http://localhost:4000/v1/models'], + ['n' => 'Ollama', 'p' => ':11434', 't' => 'systemd', 'probe' => 'http://localhost:11434/api/tags'], + ['n' => 'Paperclip', 'p' => ':3100', 't' => 'systemd', 'probe' => 'http://localhost:3100'], + ['n' => 'DeerFlow', 'p' => ':3002/:3003', 't' => 'systemd', 'probe' => 'http://localhost:3002'], + // Docker containers + ['n' => 'Loki', 'p' => ':3100 (container)', 't' => 'docker', 'docker' => 'loki'], + ['n' => 'Gitea', 'p' => ':3300', 't' => 'docker', 'docker' => 'gitea'], + ['n' => 'Qdrant', 'p' => ':6333', 't' => 'docker', 'docker' => 'qdrant'], + ['n' => 'Mattermost', 'p' => ':8065', 't' => 'docker', 'docker' => 'mattermost'], + ['n' => 'Twenty CRM', 'p' => ':3000', 't' => 'docker', 'docker' => 'twenty'], + ['n' => 'Langfuse', 'p' => ':3333', 't' => 'docker', 'docker' => 'langfuse'], + ['n' => 'Prometheus', 'p' => ':9090', 't' => 'docker', 'docker' => 'prometheus'], + ['n' => 'Uptime-Kuma', 'p' => ':3001', 't' => 'docker', 'docker' => 'uptime-kuma'], + ['n' => 'Vaultwarden', 'p' => ':8222', 't' => 'docker', 'docker' => 'vaultwarden'], + ['n' => 'Redis-Weval', 'p' => ':6379', 't' => 'docker', 'docker' => 'redis-weval'], + ['n' => 'SearXNG', 'p' => ':8080', 't' => 'docker', 'docker' => 'searxng'], + ['n' => 'Plausible', 'p' => ':8000', 't' => 'docker', 'docker' => 'plausible'], + ['n' => 'n8n', 'p' => ':5678', 't' => 'docker', 'docker' => 'n8n'], + ['n' => 'Listmonk', 'p' => ':9000', 't' => 'docker', 'docker' => 'listmonk'], + // PG + Email infra + ['n' => 'PostgreSQL', 'p' => ':5432', 't' => 'systemd', 'systemd' => 'postgresql'], + // Security + ['n' => 'Fail2Ban', 'p' => 'daemon', 't' => 'systemd', 'systemd' => 'fail2ban'], +]; + +$results = []; +$up_count = 0; +$total_ms = 0; + +foreach ($services as $s) { + $r = null; + if (!empty($s['probe'])) { + $r = probe_http($s['probe']); + } elseif (!empty($s['docker'])) { + $r = probe_docker($s['docker']); + } elseif (!empty($s['systemd'])) { + $r = probe_systemd($s['systemd']); + } + + $entry = [ + 'n' => $s['n'], + 'p' => $s['p'], + 't' => $s['t'], + 's' => ($r && !empty($r['up'])) ? 'up' : 'down', + ]; + if (isset($r['ms'])) { $entry['ms'] = $r['ms']; $total_ms += $r['ms']; } + if (isset($r['status'])) $entry['detail'] = $r['status']; + if (isset($r['code'])) $entry['code'] = $r['code']; + + if ($entry['s'] === 'up') $up_count++; + $results[] = $entry; +} + +$total = count($results); +$uptime_pct = $total > 0 ? round(($up_count / $total) * 100, 1) : 0; + +echo json_encode([ + 'v' => 'V96.20-services-live-opus', + 'ts' => date('c'), + 'total' => $total, + 'up' => $up_count, + 'down' => $total - $up_count, + 'uptime_pct' => $uptime_pct, + 'avg_latency_ms' => $total > 0 ? round($total_ms / $total) : 0, + 'services' => $results, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); diff --git a/api/wevia-vault.php.GOLD-V79-20260420-030934 b/api/wevia-vault.php.GOLD-V79-20260420-030934 new file mode 100644 index 000000000..eb5091435 --- /dev/null +++ b/api/wevia-vault.php.GOLD-V79-20260420-030934 @@ -0,0 +1,85 @@ + 'no query']); exit; } + $results = []; + $iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($VAULT)); + foreach ($iter as $file) { + if ($file->getExtension() !== 'md') continue; + $content = file_get_contents($file->getPathname()); + if (stripos($content, $q) !== false) { + $rel = str_replace($VAULT . '/', '', $file->getPathname()); + // Extract frontmatter tags + preg_match('/tags:\s*\[([^\]]+)\]/', $content, $tm); + $results[] = [ + 'file' => $rel, + 'tags' => trim($tm[1] ?? ''), + 'snippet' => substr(strip_tags($content), 0, 200), + 'size' => strlen($content) + ]; + } + } + echo json_encode(['query' => $q, 'results' => $results, 'count' => count($results)]); + break; + + case 'read': + $file = $_GET['file'] ?? ''; + $path = realpath($VAULT . '/' . $file); + if (!$path || strpos($path, $VAULT) !== 0 || !file_exists($path)) { + echo json_encode(['error' => 'file not found']); exit; + } + echo json_encode(['file' => $file, 'content' => file_get_contents($path), 'size' => filesize($path)]); + break; + + case 'list': + $dir = $_GET['dir'] ?? ''; + $target = realpath($VAULT . '/' . $dir) ?: $VAULT; + if (strpos($target, $VAULT) !== 0) { echo json_encode(['error' => 'invalid path']); exit; } + $files = []; + foreach (scandir($target) as $f) { + if ($f[0] === '.') continue; + $full = $target . '/' . $f; + $files[] = ['name' => $f, 'type' => is_dir($full) ? 'dir' : 'file', 'size' => is_file($full) ? filesize($full) : 0]; + } + echo json_encode(['dir' => $dir ?: '/', 'files' => $files, 'count' => count($files)]); + break; + + case 'write': + $file = $_POST['file'] ?? ''; + $content = $_POST['content'] ?? ''; + if (!$file || !$content) { echo json_encode(['error' => 'file and content required']); exit; } + $path = $VAULT . '/' . $file; + $dir = dirname($path); + if (!is_dir($dir)) mkdir($dir, 0755, true); + file_put_contents($path, $content); + echo json_encode(['ok' => true, 'file' => $file, 'size' => strlen($content)]); + break; + + case 'stats': + $count = 0; $total = 0; $dirs = []; + $iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($VAULT)); + foreach ($iter as $f) { + if ($f->getExtension() === 'md') { $count++; $total += $f->getSize(); } + } + foreach (scandir($VAULT) as $d) { + if ($d[0] !== '.' && is_dir("$VAULT/$d")) { + $n = count(glob("$VAULT/$d/*.md")); + $dirs[] = ['name' => $d, 'files' => $n]; + } + } + echo json_encode(['vault' => $VAULT, 'files' => $count, 'total_bytes' => $total, 'dirs' => $dirs]); + break; + + default: + echo json_encode(['error' => 'unknown action', 'actions' => ['search','read','list','write','stats']]); +} diff --git a/architecture.html b/architecture.html index fe3b74adb..0fb89aed0 100644 --- a/architecture.html +++ b/architecture.html @@ -244,7 +244,7 @@ footer{text-align:center;padding:20px;color:var(--dim);font-size:.58rem;font-fam #ld .spin{width:32px;height:32px;border:2px solid var(--dim2);border-top-color:var(--acc);border-radius:50%;animation:spin .8s linear infinite} -
| Provider | Model | Tier |
|---|---|---|
| ${p.name} | ${p.model} | ${tg(p.tier,p.tier==='T0'?'p':p.tier==='T1'?'g':p.tier==='T2'?'b':'d')} |