This commit is contained in:
@@ -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=[
|
||||
|
||||
176
api/blade-tasks/wevia-agent-v4.ps1
Normal file
176
api/blade-tasks/wevia-agent-v4.ps1
Normal file
@@ -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++
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
124
api/wevia-services-live.php
Normal file
124
api/wevia-services-live.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* WEVAL Services Live Status API V96.20
|
||||
*
|
||||
* User WTP admin-v2: Loki affiché ROUGE 18021ms mais alert dit LIVE 3 days
|
||||
* Root cause: admin-v2.html SERVICES hardcoded line 102 avec s:'up' statique
|
||||
* Many services afficherait `---` sans vraie donnée
|
||||
*
|
||||
* This API returns TRUE live status for 23 services by probing each one.
|
||||
*/
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
function probe_port($host, $port, $timeout = 2) {
|
||||
$start = microtime(true);
|
||||
$conn = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||
$ms = round((microtime(true) - $start) * 1000);
|
||||
if ($conn) { fclose($conn); return ['up' => 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);
|
||||
85
api/wevia-vault.php.GOLD-V79-20260420-030934
Normal file
85
api/wevia-vault.php.GOLD-V79-20260420-030934
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* WEVIA Obsidian Vault API — Sovereign Memory Layer
|
||||
* Replaces 3000+ tokens of userMemories with on-demand search
|
||||
* Endpoint: /api/wevia-vault.php
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
$VAULT = '/opt/obsidian-vault';
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? 'search';
|
||||
$q = $_GET['q'] ?? $_POST['q'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'search':
|
||||
if (!$q) { echo json_encode(['error' => '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']]);
|
||||
}
|
||||
@@ -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}
|
||||
</style>
|
||||
</head>
|
||||
<body style="padding-top:60px"><div style="position:fixed;top:0;left:0;right:0;height:28px;background:#ffffffee;z-index:100;display:flex;align-items:center;padding:0 14px;font-family:Nunito,sans-serif;font-size:.65rem;gap:12px;border-bottom:1px solid #e2e8f0;backdrop-filter:blur(8px)"><b style="color:#059669">WEVIA</b></div>
|
||||
<body style="padding-top:60px;padding-bottom:64px"><div style="position:fixed;top:0;left:0;right:0;height:28px;background:#ffffffee;z-index:100;display:flex;align-items:center;padding:0 14px;font-family:Nunito,sans-serif;font-size:.65rem;gap:12px;border-bottom:1px solid #e2e8f0;backdrop-filter:blur(8px)"><b style="color:#059669">WEVIA</b></div>
|
||||
<div style="position:fixed;top:30px;left:0;right:0;display:flex;justify-content:center;gap:5px;padding:4px;z-index:100;background:#f8fafcee;backdrop-filter:blur(8px);font-family:Nunito,sans-serif">
|
||||
<a href="/agents-archi.html" style="padding:2px 8px;border-radius:4px;font:700 8px Nunito;text-decoration:none;color:#5a6a80;border:1px solid #c8d8e8">Architecture</a>
|
||||
<a href="/director-center.html" style="padding:2px 8px;border-radius:4px;font:700 8px Nunito;text-decoration:none;color:#5a6a80;border:1px solid #c8d8e8">Director</a>
|
||||
@@ -376,7 +376,7 @@ h+='<section><h2>Recommendations</h2>';recs.forEach(r=>{h+=recoH(r)});h+='</sect
|
||||
if(decs.length){h+='<section><h2>Architecture Decisions</h2>';decs.forEach(d=>{h+=`<div class="decision">${d.fact}<div style="font-size:.6rem;color:var(--dim);margin-top:2px">${d.created_at}</div></div>`});h+='</section>'}return h}
|
||||
|
||||
function rpC(){const cx=D.cortex||{};let h=`<div class="g g4">${kp(cx.fast_lines,'CORTEX','lines','bl')}${kp(cx.router_lines,'Router','lines','p')}${kp(cx.router_functions,'Functions','routing','g')}${kp(D.ai_providers.length,'Providers','0€','o')}</div>`;
|
||||
h+='<section><h2>Smart Router</h2><div class="g g4">';[['T0: Local','weval-brain-v3','~200ms','p'],['T1: Free','Cerebras · Groq · SambaNova','~1.3s','g'],['T2: Fallback','Mistral · Cohere · Gemini','~2s','b'],['T3: Emergency','HuggingFace · Replicate','~3s','d']].forEach(([n,d,l,c])=>{h+=`<div class="cd" style="border-top:2px solid var(--${c})"><h3>${n}</h3><div style="font-size:.78rem;color:var(--t);margin:3px 0">${d}</div><div class="kpi-sub">${l}</div></div>`});
|
||||
h+='<section><h2>Smart Router</h2><div class="g g4">';[['T0: Local','weval-brain-v4','~200ms','p'],['T1: Free','Cerebras · Groq · SambaNova','~1.3s','g'],['T2: Fallback','Mistral · Cohere · Gemini','~2s','b'],['T3: Emergency','HuggingFace · Replicate','~3s','d']].forEach(([n,d,l,c])=>{h+=`<div class="cd" style="border-top:2px solid var(--${c})"><h3>${n}</h3><div style="font-size:.78rem;color:var(--t);margin:3px 0">${d}</div><div class="kpi-sub">${l}</div></div>`});
|
||||
h+='</div></section><section><h2>Providers</h2><table><thead><tr><th>Provider</th><th>Model</th><th>Tier</th></tr></thead><tbody>';
|
||||
D.ai_providers.forEach(p=>{h+=`<tr><td style="font-weight:600">${p.name}</td><td>${p.model}</td><td>${tg(p.tier,p.tier==='T0'?'p':p.tier==='T1'?'g':p.tier==='T2'?'b':'d')}</td></tr>`});h+='</tbody></table></section>';return h}
|
||||
|
||||
|
||||
593
dg-command-center.html.GOLD-V79-20260420-030934
Normal file
593
dg-command-center.html.GOLD-V79-20260420-030934
Normal file
@@ -0,0 +1,593 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WEVAL · DG Command Center — Real-time Pilotage</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-0:#05060a; --bg-1:#0b0d15; --bg-2:#11141f; --bg-3:#171b2a;
|
||||
--border:rgba(99,102,241,0.15); --border-h:rgba(99,102,241,0.35);
|
||||
--text:#e2e8f0; --dim:#94a3b8; --mute:#64748b;
|
||||
--accent:#14b8a6; --accent2:#6366f1; --purple:#a855f7; --cyan:#06b6d4;
|
||||
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444; --rose:#f43f5e; --gold:#eab308;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:'Inter',system-ui,sans-serif;background:radial-gradient(ellipse at top,#0f1420,#05060a 60%);color:var(--text);min-height:100vh;font-size:13px;line-height:1.5}
|
||||
.container{max-width:1760px;margin:0 auto;padding:24px 28px 80px}
|
||||
|
||||
/* HEADER */
|
||||
header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}
|
||||
header h1{font-size:26px;font-weight:800;background:linear-gradient(90deg,#22d3ee,#a855f7,#eab308);-webkit-background-clip:text;background-clip:text;color:transparent;letter-spacing:-0.4px;display:flex;align-items:center;gap:10px}
|
||||
header .sub{color:var(--dim);font-size:12.5px;margin-top:5px}
|
||||
header .clock{font-family:'JetBrains Mono',monospace;color:var(--accent);font-size:11px;margin-top:4px}
|
||||
.actions{display:flex;gap:8px}
|
||||
.btn{padding:7px 13px;background:var(--bg-2);border:1px solid var(--border);color:var(--text);border-radius:8px;font-size:11.5px;cursor:pointer;text-decoration:none;font-family:inherit;transition:all .2s}
|
||||
.btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(34,197,94,.7);animation:pulse 2s infinite}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,.7)}70%{box-shadow:0 0 0 8px rgba(34,197,94,0)}}
|
||||
|
||||
/* LAYOUT GRID */
|
||||
.row{display:grid;gap:14px;margin-bottom:14px}
|
||||
.row-4{grid-template-columns:repeat(4,1fr)}
|
||||
.row-3{grid-template-columns:repeat(3,1fr)}
|
||||
.row-2{grid-template-columns:2fr 1fr}
|
||||
.row-2e{grid-template-columns:1fr 1fr}
|
||||
@media(max-width:1200px){.row-4,.row-3,.row-2,.row-2e{grid-template-columns:1fr 1fr}}
|
||||
@media(max-width:720px){.row-4,.row-3,.row-2,.row-2e{grid-template-columns:1fr}}
|
||||
|
||||
/* CARDS */
|
||||
.card{background:var(--bg-1);border:1px solid var(--border);border-radius:12px;padding:16px;position:relative;overflow:hidden}
|
||||
.card.span-2{grid-column:span 2}
|
||||
.card.span-3{grid-column:span 3}
|
||||
.card.span-4{grid-column:span 4}
|
||||
.card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
|
||||
.card-title{font-size:11.5px;color:var(--dim);text-transform:uppercase;letter-spacing:0.6px;font-weight:700;display:flex;align-items:center;gap:6px}
|
||||
.card-badge{font-size:9.5px;padding:2px 7px;border-radius:8px;font-weight:700;letter-spacing:0.3px;background:rgba(20,184,166,0.15);color:#5eead4}
|
||||
.card-badge.warn{background:rgba(245,158,11,.18);color:#fbbf24}
|
||||
.card-badge.danger{background:rgba(239,68,68,.18);color:#fca5a5}
|
||||
.card-badge.info{background:rgba(99,102,241,.18);color:#a5b4fc}
|
||||
|
||||
/* KPI big */
|
||||
.kpi-big{font-size:32px;font-weight:800;letter-spacing:-0.5px;line-height:1}
|
||||
.kpi-big.gold{background:linear-gradient(135deg,var(--gold),var(--warn));-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.kpi-big.ok{color:var(--ok)}
|
||||
.kpi-big.warn{color:var(--warn)}
|
||||
.kpi-big.danger{color:var(--err)}
|
||||
.kpi-sub{color:var(--dim);font-size:11px;margin-top:4px}
|
||||
|
||||
/* ALERTS DG STRIP */
|
||||
.alerts-strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;margin-bottom:20px}
|
||||
.alert-card{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:14px 16px;border-left:4px solid var(--warn);position:relative;transition:all .2s}
|
||||
.alert-card:hover{border-color:var(--border-h);transform:translateY(-2px)}
|
||||
.alert-card.critical{border-left-color:var(--err);background:linear-gradient(135deg,rgba(239,68,68,0.06),var(--bg-1))}
|
||||
.alert-card.high{border-left-color:var(--warn)}
|
||||
.alert-card.medium{border-left-color:var(--cyan)}
|
||||
.alert-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px}
|
||||
.alert-title{font-size:13px;font-weight:700;color:var(--text);display:flex;gap:7px;align-items:flex-start}
|
||||
.alert-lvl{font-size:9px;padding:2px 6px;border-radius:6px;font-weight:700;letter-spacing:0.3px;text-transform:uppercase;flex-shrink:0}
|
||||
.alert-lvl.critical{background:rgba(239,68,68,0.2);color:#fca5a5}
|
||||
.alert-lvl.high{background:rgba(245,158,11,0.2);color:#fbbf24}
|
||||
.alert-lvl.medium{background:rgba(6,182,212,0.18);color:#7dd3fc}
|
||||
.alert-detail{font-size:11.5px;color:var(--dim);margin:6px 0;line-height:1.45}
|
||||
.alert-foot{display:flex;justify-content:space-between;align-items:center;margin-top:8px;font-size:10.5px}
|
||||
.alert-foot .deadline{color:var(--warn);font-weight:600}
|
||||
.alert-foot a{color:var(--accent);text-decoration:none;font-weight:600}
|
||||
|
||||
/* TOC streams */
|
||||
.toc-wrap{display:flex;flex-direction:column;gap:8px}
|
||||
.toc-stream{display:grid;grid-template-columns:28px 1fr 60px 80px 1fr;gap:10px;align-items:center;padding:8px 10px;background:var(--bg-2);border-radius:8px;border-left:3px solid var(--dim)}
|
||||
.toc-stream.bottleneck{border-left-color:var(--err);background:linear-gradient(135deg,rgba(239,68,68,0.06),var(--bg-2));box-shadow:0 0 0 1px rgba(239,68,68,0.25)}
|
||||
.toc-stream.flow{border-left-color:var(--ok)}
|
||||
.toc-stream.starved{border-left-color:var(--cyan)}
|
||||
.toc-icon{font-size:18px;text-align:center}
|
||||
.toc-label{font-size:12px;font-weight:600;color:var(--text)}
|
||||
.toc-label .small{color:var(--dim);font-size:10px;font-weight:400;margin-top:2px}
|
||||
.toc-throughput{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:800;color:var(--text);text-align:center}
|
||||
.toc-bar-wrap{background:var(--bg-3);height:10px;border-radius:5px;overflow:hidden}
|
||||
.toc-bar-fill{height:100%;background:linear-gradient(90deg,#14b8a6,#6366f1);transition:width 1.2s cubic-bezier(.4,0,.2,1)}
|
||||
.toc-bar-fill.bot{background:linear-gradient(90deg,#ef4444,#f59e0b)}
|
||||
.toc-util{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);text-align:right;font-weight:600}
|
||||
.toc-constraint{font-size:10px;color:var(--mute);font-style:italic;grid-column:1/-1;padding-left:38px;margin-top:-3px}
|
||||
|
||||
/* Funnel */
|
||||
.funnel-wrap{display:flex;flex-direction:column;gap:6px;align-items:center;padding:10px 0}
|
||||
.funnel-row{display:grid;grid-template-columns:160px 1fr 70px 50px;gap:10px;align-items:center;width:100%;font-size:12px}
|
||||
.funnel-label{color:var(--text);font-weight:500;font-size:11.5px}
|
||||
.funnel-bar-wrap{background:var(--bg-3);height:28px;border-radius:4px;overflow:hidden;position:relative}
|
||||
.funnel-bar{height:100%;transition:width 1.2s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;padding-left:10px;font-size:11.5px;font-weight:700;color:white}
|
||||
.funnel-count{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;color:var(--text);text-align:right}
|
||||
.funnel-conv{font-family:'JetBrains Mono',monospace;font-size:10.5px;color:var(--dim);text-align:right}
|
||||
.funnel-conv.warn{color:var(--warn)}
|
||||
.funnel-conv.danger{color:var(--err)}
|
||||
|
||||
/* Data pipelines */
|
||||
.dp-wrap{display:grid;grid-template-columns:1fr;gap:6px}
|
||||
.dp-row{display:grid;grid-template-columns:1fr 80px 1fr 60px;gap:10px;align-items:center;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11.5px}
|
||||
.dp-name{color:var(--text);font-weight:500}
|
||||
.dp-vol{font-family:'JetBrains Mono',monospace;font-weight:700;text-align:right;color:var(--text)}
|
||||
.dp-bar-wrap{background:var(--bg-3);height:8px;border-radius:4px;overflow:hidden}
|
||||
.dp-bar-fill{height:100%;background:linear-gradient(90deg,var(--ok),var(--cyan));transition:width 1.2s}
|
||||
.dp-status{font-size:10.5px;text-align:right;font-family:'JetBrains Mono',monospace}
|
||||
.dp-status.ok{color:var(--ok)} .dp-status.warn{color:var(--warn)} .dp-status.danger{color:var(--err)}
|
||||
|
||||
/* Marketing grid */
|
||||
.mkt-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||
@media(max-width:900px){.mkt-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.mkt-cell{background:var(--bg-2);border-radius:6px;padding:10px;border-left:2px solid var(--purple)}
|
||||
.mkt-cell .l{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:0.4px;font-weight:600}
|
||||
.mkt-cell .v{font-size:17px;font-weight:800;color:var(--text);font-family:'JetBrains Mono',monospace;margin-top:3px;line-height:1}
|
||||
.mkt-cell .u{font-size:10.5px;color:var(--dim);margin-left:2px}
|
||||
|
||||
/* CRM view */
|
||||
.crm-stage-row{display:grid;grid-template-columns:110px 50px 1fr 80px;gap:10px;align-items:center;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11.5px;margin-bottom:5px}
|
||||
.stage-label{font-weight:600;color:var(--text)}
|
||||
.stage-count{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--accent);text-align:center}
|
||||
.stage-bar-wrap{background:var(--bg-3);height:10px;border-radius:4px;overflow:hidden}
|
||||
.stage-bar-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--purple));transition:width 1.2s}
|
||||
.stage-val{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--gold);text-align:right;font-size:11px}
|
||||
|
||||
.accounts-wrap{display:flex;flex-direction:column;gap:5px}
|
||||
.acc-row{display:grid;grid-template-columns:1fr 110px 60px;gap:10px;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11px;align-items:center;border-left:2px solid var(--accent2)}
|
||||
.acc-row:hover{background:var(--bg-3)}
|
||||
.acc-name{font-weight:600;color:var(--text)}
|
||||
.acc-name .step{display:block;font-size:10px;color:var(--dim);font-weight:400;margin-top:2px}
|
||||
.acc-stage{font-size:10px;color:var(--dim);font-family:'JetBrains Mono',monospace}
|
||||
.acc-val{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--gold);text-align:right}
|
||||
|
||||
/* Risk matrix 5x5 */
|
||||
.rm-wrap{display:grid;grid-template-columns:80px 1fr;gap:10px}
|
||||
.rm-grid-5x5{display:grid;grid-template-columns:20px repeat(5,1fr);gap:3px}
|
||||
.rm-cell{aspect-ratio:1.4;display:flex;align-items:center;justify-content:center;border-radius:4px;font-weight:800;font-size:14px;cursor:help;position:relative}
|
||||
.rm-header{font-size:9px;color:var(--dim);text-align:center;display:flex;align-items:center;justify-content:center}
|
||||
.rm-sev1{background:rgba(34,197,94,0.15);color:#86efac}
|
||||
.rm-sev2{background:rgba(132,204,22,0.18);color:#d9f99d}
|
||||
.rm-sev3{background:rgba(234,179,8,0.2);color:#fef08a}
|
||||
.rm-sev4{background:rgba(249,115,22,0.22);color:#fed7aa}
|
||||
.rm-sev5{background:rgba(239,68,68,0.3);color:#fca5a5}
|
||||
.rm-sev-empty{background:var(--bg-3);color:var(--mute);font-weight:400;font-size:10px}
|
||||
|
||||
.risk-list{display:flex;flex-direction:column;gap:5px;margin-top:10px}
|
||||
.risk-row{display:grid;grid-template-columns:40px 1fr auto;gap:8px;align-items:center;padding:6px 10px;background:var(--bg-2);border-radius:5px;font-size:11px;border-left:2px solid var(--warn)}
|
||||
.risk-row.critical{border-left-color:var(--err)}
|
||||
.risk-row.high{border-left-color:var(--warn)}
|
||||
.risk-row.medium{border-left-color:var(--cyan)}
|
||||
.risk-id{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--mute)}
|
||||
.risk-title{color:var(--text);font-weight:500}
|
||||
.risk-title .mit{display:block;font-size:10px;color:var(--dim);font-style:italic;margin-top:2px}
|
||||
.risk-score{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:800;color:var(--text);text-align:right}
|
||||
|
||||
.loading{text-align:center;padding:50px;color:var(--dim)}
|
||||
.spinner{width:38px;height:38px;border:3px solid var(--bg-3);border-top-color:var(--accent);border-radius:50%;margin:0 auto 14px;animation:spin 1s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* === OPUS RESPONSIVE FIX v2 19avr — append-only, doctrine #14 === */
|
||||
@media(max-width: 480px) {
|
||||
html, body { overflow-x: hidden !important; max-width: 100vw; }
|
||||
body, main, section, article { word-break: break-word; overflow-wrap: anywhere; }
|
||||
img, video, iframe, canvas, svg, table, pre, code { max-width: 100% !important; }
|
||||
pre, code { white-space: pre-wrap; word-break: break-all; }
|
||||
table { display: block; overflow-x: auto; }
|
||||
.container, [class*="container"], [class*="wrapper"] { max-width: 100vw !important; padding-left: 12px !important; padding-right: 12px !important; }
|
||||
[class*="grid"], [class*="-grid"] { grid-template-columns: 1fr !important; gap: 10px !important; }
|
||||
[class*="kpi"], [class*="stats"], [class*="-cards"] { grid-template-columns: 1fr !important; }
|
||||
header, nav, footer { flex-wrap: wrap !important; }
|
||||
header > *, nav > *, footer > * { max-width: 100%; }
|
||||
h1 { font-size: 22px !important; word-break: break-word; }
|
||||
h2 { font-size: 18px !important; }
|
||||
.pitch, [class*="pitch"], [class*="hero"] { word-break: break-word; overflow-wrap: anywhere; }
|
||||
}
|
||||
/* === OPUS RESPONSIVE FIX v2 END === */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<h1><span>🎖️</span>DG Command Center <span class="pulse"></span></h1>
|
||||
<div class="sub">Real-time pilotage — TOC · Conversion · Data · Marketing · CRM · Risk · Alertes</div>
|
||||
<div class="clock" id="clock">—</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/weval-technology-platform.html" class="btn">🏠 WTP</a>
|
||||
<a href="/agent-roi-simulator.html" class="btn">🧮 ROI Sim</a>
|
||||
<a href="/crm.html" class="btn">💼 CRM</a>
|
||||
<button class="btn" id="btn-refresh" onclick="load()">↻ Refresh</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ALERTS DG TOP -->
|
||||
<div class="card" style="margin-bottom:20px;border-left:4px solid var(--err)">
|
||||
<div class="card-head">
|
||||
<div class="card-title">🚨 Alertes DG — à traiter maintenant <span class="card-badge danger" id="alerts-count">— alertes</span></div>
|
||||
<div class="card-badge" id="alerts-critical">—</div>
|
||||
</div>
|
||||
<div class="alerts-strip" id="alerts-strip"><div class="loading"><div class="spinner"></div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- ROW: TOC + Conversion Funnel -->
|
||||
<div class="row row-2">
|
||||
<div class="card" id="toc-card">
|
||||
<div class="card-head">
|
||||
<div class="card-title">🎯 TOC Theory of Constraints — Goldratt</div>
|
||||
<div class="card-badge danger" id="toc-bot-badge">— bottleneck</div>
|
||||
</div>
|
||||
<div class="toc-wrap" id="toc-streams"><div class="loading"><div class="spinner"></div></div></div>
|
||||
<div style="margin-top:12px;padding:10px 12px;background:var(--bg-2);border-radius:6px;font-size:10.5px;color:var(--dim);line-height:1.5">
|
||||
<strong style="color:var(--accent)">5 Focusing Steps (Goldratt):</strong>
|
||||
1. Identifier la contrainte · 2. Exploiter (max) · 3. Subordonner tout le reste · 4. Élever la contrainte · 5. Si brisée → reprendre au 1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title">🎚️ Conversion Funnel</div>
|
||||
<div class="card-badge info" id="conv-overall">— %</div>
|
||||
</div>
|
||||
<div class="funnel-wrap" id="funnel-wrap"><div class="loading"><div class="spinner"></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW: Data pipelines + Marketing KPIs -->
|
||||
<div class="row row-2e">
|
||||
<div class="card">
|
||||
<div class="card-head"><div class="card-title">🔌 Data Pipelines Health</div><div class="card-badge" id="dp-badge">live</div></div>
|
||||
<div class="dp-wrap" id="dp-wrap"><div class="loading"><div class="spinner"></div></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><div class="card-title">📣 Marketing KPIs</div><div class="card-badge info">WEVADS + Ethica</div></div>
|
||||
<div class="mkt-grid" id="mkt-grid"><div class="loading"><div class="spinner"></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW: CRM pipeline + Top accounts -->
|
||||
<div class="row row-2e">
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title">💼 CRM Pipeline by Stage</div>
|
||||
<div class="card-badge info" id="pipe-val">— k€</div>
|
||||
</div>
|
||||
<div id="crm-stages"><div class="loading"><div class="spinner"></div></div></div>
|
||||
<div style="margin-top:12px;padding-top:10px;border-top:1px dashed var(--border);display:grid;grid-template-columns:repeat(4,1fr);gap:8px;font-size:10.5px;color:var(--dim)">
|
||||
<div><strong style="color:var(--text);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-opps">—</strong>Opps actives</div>
|
||||
<div><strong style="color:var(--ok);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-won">—</strong>Won ce mois</div>
|
||||
<div><strong style="color:var(--err);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-lost">—</strong>Lost</div>
|
||||
<div><strong style="color:var(--purple);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-cycle">—</strong>Cycle (j)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><div class="card-title">🎯 Top Accounts & Next Steps</div><div class="card-badge info" id="acc-badge">—</div></div>
|
||||
<div class="accounts-wrap" id="accounts-wrap"><div class="loading"><div class="spinner"></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW: Risk Management 5x5 + Risk list -->
|
||||
<div class="row row-2e">
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title">⚠️ Risk Management WEVAL — Matrice 5×5</div>
|
||||
<div class="card-badge danger" id="risk-count">—</div>
|
||||
</div>
|
||||
<div class="rm-wrap">
|
||||
<div style="display:flex;flex-direction:column;justify-content:space-around;font-size:10px;color:var(--dim);text-align:right;font-weight:600">
|
||||
<div>Likelihood</div>
|
||||
<div>L=5</div><div>L=4</div><div>L=3</div><div>L=2</div><div>L=1</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="rm-grid-5x5" id="risk-matrix"></div>
|
||||
<div style="display:grid;grid-template-columns:20px repeat(5,1fr);gap:3px;margin-top:4px">
|
||||
<div></div>
|
||||
<div class="rm-header">Impact 1</div><div class="rm-header">2</div><div class="rm-header">3</div><div class="rm-header">4</div><div class="rm-header">5</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><div class="card-title">📋 Top 8 Risques à traiter</div><div class="card-badge danger" id="risks-prio">—</div></div>
|
||||
<div class="risk-list" id="risk-list"><div class="loading"><div class="spinner"></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/api/wevia-v69-dg-command-center.php';
|
||||
let DATA = null;
|
||||
|
||||
function clockTick(){
|
||||
const d = new Date();
|
||||
document.getElementById('clock').textContent = d.toLocaleDateString('fr-FR') + ' · ' + d.toLocaleTimeString('fr-FR') + ' · auto-refresh 20s';
|
||||
}
|
||||
setInterval(clockTick, 1000); clockTick();
|
||||
|
||||
async function load(){
|
||||
try {
|
||||
const r = await fetch(API + '?t=' + Date.now());
|
||||
DATA = await r.json();
|
||||
render();
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function fmt(n){
|
||||
if (!n && n !== 0) return '—';
|
||||
if (Math.abs(n) >= 1000000) return (n/1000000).toFixed(2)+'M';
|
||||
if (Math.abs(n) >= 1000) return (n/1000).toFixed(1)+'k';
|
||||
return Math.round(n);
|
||||
}
|
||||
|
||||
function render(){
|
||||
if (!DATA) return;
|
||||
const s = DATA.summary;
|
||||
|
||||
// Alerts
|
||||
const alerts = DATA.alerts_dg || [];
|
||||
document.getElementById('alerts-count').textContent = alerts.length + ' alertes';
|
||||
document.getElementById('alerts-critical').textContent = s.alerts_critical + ' critical';
|
||||
document.getElementById('alerts-critical').className = 'card-badge ' + (s.alerts_critical > 0 ? 'danger' : 'info');
|
||||
document.getElementById('alerts-strip').innerHTML = alerts.map(a => `
|
||||
<div class="alert-card ${a.level}">
|
||||
<div class="alert-head">
|
||||
<div class="alert-title"><span>${a.icon}</span>${a.title}</div>
|
||||
<div class="alert-lvl ${a.level}">${a.level}</div>
|
||||
</div>
|
||||
<div class="alert-detail">${a.detail}</div>
|
||||
<div class="alert-foot">
|
||||
<span class="deadline">⏱ ${a.deadline}</span>
|
||||
<a href="${a.action_link}">→ Action</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// TOC
|
||||
const streams = (DATA.toc && DATA.toc.streams) || [];
|
||||
document.getElementById('toc-bot-badge').textContent = '🔴 ' + (s.toc_bottleneck_label || '—');
|
||||
document.getElementById('toc-streams').innerHTML = streams.map(st => {
|
||||
const isBot = st.id === DATA.toc.bottleneck;
|
||||
const pct = Math.min(100, st.utilization_pct);
|
||||
return `<div class="toc-stream ${st.status} ${isBot?'bottleneck':''}">
|
||||
<div class="toc-icon">${st.icon}</div>
|
||||
<div class="toc-label">${st.label}${isBot?' <span class="card-badge danger" style="margin-left:4px">GOULET</span>':''}<div class="small">${st.constraint}</div></div>
|
||||
<div class="toc-throughput">${st.throughput}<div style="font-size:9px;color:var(--mute);font-weight:400">${st.unit}</div></div>
|
||||
<div><div class="toc-bar-wrap"><div class="toc-bar-fill ${isBot?'bot':''}" style="width:0%" data-w="${pct}"></div></div></div>
|
||||
<div class="toc-util">${pct.toFixed(0)}%<div style="font-size:9px;color:var(--mute)">cap ${st.capacity}</div></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
setTimeout(()=>document.querySelectorAll('.toc-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 80);
|
||||
|
||||
// Funnel
|
||||
const funnel = DATA.conversion_funnel || [];
|
||||
document.getElementById('conv-overall').textContent = s.conversion_overall_pct.toFixed(3) + '% overall';
|
||||
const maxCount = Math.max(...funnel.map(f=>f.count), 1);
|
||||
document.getElementById('funnel-wrap').innerHTML = funnel.map((f,i) => {
|
||||
const w = (f.count/maxCount)*100;
|
||||
const cls = (f.conv_pct||100) < 15 ? 'danger' : (f.conv_pct||100) < 35 ? 'warn' : '';
|
||||
return `<div class="funnel-row">
|
||||
<div class="funnel-label">${f.step}</div>
|
||||
<div class="funnel-bar-wrap"><div class="funnel-bar" style="width:0%;background:${f.color}" data-w="${w}">${f.count}</div></div>
|
||||
<div class="funnel-count">${fmt(f.count)}</div>
|
||||
<div class="funnel-conv ${cls}">${f.conv_pct||100}%</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
setTimeout(()=>document.querySelectorAll('.funnel-bar').forEach(el=>el.style.width=el.dataset.w+'%'), 100);
|
||||
|
||||
// Data pipelines
|
||||
const dp = DATA.data_pipeline || [];
|
||||
document.getElementById('dp-wrap').innerHTML = dp.map(d => {
|
||||
const pct = d.target ? Math.min(100, (d.volume/d.target)*100) : 100;
|
||||
return `<div class="dp-row">
|
||||
<div class="dp-name">${d.name}</div>
|
||||
<div class="dp-vol">${fmt(d.volume)} ${d.unit||''}</div>
|
||||
<div class="dp-bar-wrap"><div class="dp-bar-fill" style="width:0%" data-w="${pct}"></div></div>
|
||||
<div class="dp-status ${d.status}">${d.status}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
setTimeout(()=>document.querySelectorAll('.dp-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 120);
|
||||
|
||||
// Marketing
|
||||
const m = DATA.marketing || {};
|
||||
const mktCells = [
|
||||
{l:'HCPs Maghreb', v:fmt(m.ethica_hcps), u:''},
|
||||
{l:'Emails valides', v:fmt(m.emails_validated), u:''},
|
||||
{l:'Warmup accts', v:fmt(m.warmup_accounts), u:''},
|
||||
{l:'Seeds actifs', v:fmt(m.seed_accounts), u:''},
|
||||
{l:'Inbox rate', v:m.inbox_rate_pct, u:'%'},
|
||||
{l:'Open rate', v:m.open_rate_pct, u:'%'},
|
||||
{l:'Click rate', v:m.click_rate_pct, u:'%'},
|
||||
{l:'Conversions', v:m.conversions_month, u:'/mois'},
|
||||
{l:'CAC', v:m.cac_eur, u:'€'},
|
||||
{l:'LTV', v:m.ltv_eur, u:'€'},
|
||||
{l:'Deliver. mean wk', v:m.email_deliverability_mean_week, u:'%'},
|
||||
{l:'Campaigns live', v:2, u:''}
|
||||
];
|
||||
document.getElementById('mkt-grid').innerHTML = mktCells.map(c => `<div class="mkt-cell"><div class="l">${c.l}</div><div class="v">${c.v}<span class="u">${c.u}</span></div></div>`).join('');
|
||||
|
||||
// CRM pipeline by stage
|
||||
const crm = DATA.crm || {};
|
||||
const stages = crm.pipeline_by_stage || [];
|
||||
document.getElementById('pipe-val').textContent = fmt(crm.pipeline_value_keur) + ' k€';
|
||||
document.getElementById('crm-opps').textContent = crm.opportunities_active;
|
||||
document.getElementById('crm-won').textContent = crm.deals_won_month;
|
||||
document.getElementById('crm-lost').textContent = crm.deals_lost_month;
|
||||
document.getElementById('crm-cycle').textContent = crm.avg_cycle_days;
|
||||
const maxVal = Math.max(...stages.map(s=>s.value_keur), 1);
|
||||
document.getElementById('crm-stages').innerHTML = stages.map(st => {
|
||||
const w = (st.value_keur/maxVal)*100;
|
||||
return `<div class="crm-stage-row">
|
||||
<div class="stage-label">${st.stage}</div>
|
||||
<div class="stage-count">${st.count}</div>
|
||||
<div><div class="stage-bar-wrap"><div class="stage-bar-fill" style="width:0%" data-w="${w}"></div></div></div>
|
||||
<div class="stage-val">${fmt(st.value_keur)} k€</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
setTimeout(()=>document.querySelectorAll('.stage-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 130);
|
||||
|
||||
// Top accounts
|
||||
const accs = crm.top_accounts || [];
|
||||
document.getElementById('acc-badge').textContent = accs.length + ' accounts';
|
||||
document.getElementById('accounts-wrap').innerHTML = accs.map(a => `
|
||||
<div class="acc-row">
|
||||
<div class="acc-name">${a.name}<span class="step">→ ${a.next_step}</span></div>
|
||||
<div class="acc-stage">${a.stage}</div>
|
||||
<div class="acc-val">${a.value_keur ? fmt(a.value_keur)+'k€' : '—'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Risk matrix 5x5
|
||||
const risks = DATA.risks || [];
|
||||
document.getElementById('risk-count').textContent = s.risks_critical + ' critical · ' + s.risks_high + ' high';
|
||||
const grid = {};
|
||||
risks.forEach(r => { const k=r.likelihood+'_'+r.impact; grid[k]=(grid[k]||[]); grid[k].push(r); });
|
||||
let rmHtml = '<div></div>'; // corner top-left
|
||||
for (let imp=1; imp<=5; imp++) rmHtml += `<div class="rm-header" style="height:14px"></div>`; // column headers actually placed below
|
||||
// Rows L=5 → L=1 (high likelihood at top)
|
||||
rmHtml = '';
|
||||
for (let l=5; l>=1; l--) {
|
||||
rmHtml += `<div class="rm-header" style="font-size:10px">L=${l}</div>`;
|
||||
for (let i=1; i<=5; i++) {
|
||||
const cells = grid[l+'_'+i] || [];
|
||||
const sev = l*i;
|
||||
let cls = 'rm-sev-empty';
|
||||
if (sev >= 20) cls = 'rm-sev5';
|
||||
else if (sev >= 15) cls = 'rm-sev5';
|
||||
else if (sev >= 10) cls = 'rm-sev4';
|
||||
else if (sev >= 6) cls = 'rm-sev3';
|
||||
else if (sev >= 3) cls = 'rm-sev2';
|
||||
else cls = 'rm-sev1';
|
||||
rmHtml += `<div class="rm-cell ${cls}" title="${cells.map(c=>c.id+': '+c.title).join(' · ')}">${cells.length || '·'}</div>`;
|
||||
}
|
||||
}
|
||||
document.getElementById('risk-matrix').innerHTML = rmHtml;
|
||||
|
||||
// Risk list (top 8 by severity)
|
||||
const sorted = [...risks].sort((a,b) => (b.likelihood*b.impact) - (a.likelihood*a.impact)).slice(0, 8);
|
||||
document.getElementById('risks-prio').textContent = risks.length + ' risques total';
|
||||
document.getElementById('risk-list').innerHTML = sorted.map(r => `
|
||||
<div class="risk-row ${r.priority}">
|
||||
<div class="risk-id">${r.id}</div>
|
||||
<div class="risk-title">${r.title}<span class="mit">🛡 ${r.mitigation}</span></div>
|
||||
<div class="risk-score">${r.likelihood}×${r.impact}=<strong>${r.likelihood*r.impact}</strong></div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
load();
|
||||
setInterval(load, 20000);
|
||||
</script>
|
||||
<script>
|
||||
/* V75 AVATAR UNIFIER — Meeting-rooms emoji style (Opus 19avr) */
|
||||
(function() {
|
||||
if (window.__WEVAL_AVATAR_V75) return;
|
||||
window.__WEVAL_AVATAR_V75 = true;
|
||||
const REG_URL = '/api/agent-avatars-v75.json';
|
||||
const SVG_EP = '/api/agent-avatar-svg.php';
|
||||
function emojiSVGUrl(name, emoji) {
|
||||
return SVG_EP + '?n=' + encodeURIComponent(name) + '&e=' + encodeURIComponent(emoji);
|
||||
}
|
||||
fetch(REG_URL + '?t=' + Date.now()).then(r => r.json()).then(REG => {
|
||||
function getAvatarUrl(name) {
|
||||
const rec = REG[name];
|
||||
if (!rec) return null;
|
||||
if (typeof rec === 'object' && rec.svg) return rec.svg;
|
||||
if (typeof rec === 'object' && rec.emoji) return emojiSVGUrl(name, rec.emoji);
|
||||
return typeof rec === 'string' ? rec : null;
|
||||
}
|
||||
function findCI(key) {
|
||||
const lower = key.toLowerCase();
|
||||
for (const k of Object.keys(REG)) if (k.toLowerCase() === lower) return k;
|
||||
return null;
|
||||
}
|
||||
function apply() {
|
||||
document.querySelectorAll('img').forEach(img => {
|
||||
const key = img.alt || img.dataset.agent || img.dataset.name || img.title || '';
|
||||
if (!key) return;
|
||||
let url = getAvatarUrl(key);
|
||||
if (!url) { const alt = findCI(key); if (alt) url = getAvatarUrl(alt); }
|
||||
if (url && img.src !== url && !img.src.endsWith(url)) {
|
||||
img.src = url;
|
||||
img.setAttribute('data-weval-v75', '1');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('[data-agent]:not([data-weval-v75-applied])').forEach(el => {
|
||||
const name = el.dataset.agent;
|
||||
const url = getAvatarUrl(name);
|
||||
if (!url) return;
|
||||
const img = document.createElement('img');
|
||||
img.src = url; img.alt = name; img.title = name;
|
||||
img.className = 'v75-avatar';
|
||||
img.style.cssText = 'width:32px;height:32px;border-radius:50%;object-fit:cover;vertical-align:middle;background:transparent';
|
||||
el.setAttribute('data-weval-v75-applied', '1');
|
||||
el.prepend(img);
|
||||
});
|
||||
}
|
||||
apply();
|
||||
setTimeout(apply, 400); setTimeout(apply, 1200); setTimeout(apply, 3000);
|
||||
const mo = new MutationObserver(() => apply());
|
||||
mo.observe(document.body, {childList: true, subtree: true});
|
||||
setTimeout(() => mo.disconnect(), 20000);
|
||||
console.log('[V75 AvatarUnifier] applied from', Object.keys(REG).length, 'agents');
|
||||
}).catch(e => console.warn('[V75] fetch failed', e));
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
|
||||
<script>
|
||||
(function(){
|
||||
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
|
||||
var d = document;
|
||||
var m = d.createElement('div');
|
||||
m.id = 'opus-udrill';
|
||||
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
|
||||
var inner = d.createElement('div');
|
||||
inner.id = 'opus-udrill-in';
|
||||
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
|
||||
inner.addEventListener('click', function(e){ e.stopPropagation(); });
|
||||
m.appendChild(inner);
|
||||
m.addEventListener('click', function(){ m.style.display='none'; });
|
||||
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
|
||||
(d.body || d.documentElement).appendChild(m);
|
||||
function openCard(card) {
|
||||
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
|
||||
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
|
||||
inner.innerHTML = html;
|
||||
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
|
||||
m.style.display = 'flex';
|
||||
}
|
||||
function wire(root) {
|
||||
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
|
||||
var cards = root.querySelectorAll(sels);
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var c = cards[i];
|
||||
if (c.__opusWired) continue;
|
||||
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
|
||||
var r = c.getBoundingClientRect();
|
||||
if (r.width < 60 || r.height < 40) continue;
|
||||
c.__opusWired = true;
|
||||
c.style.cursor = 'pointer';
|
||||
c.setAttribute('role','button');
|
||||
c.setAttribute('tabindex','0');
|
||||
c.addEventListener('click', function(ev){
|
||||
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
|
||||
if (ev.target.closest('a,button,input,select')) return;
|
||||
ev.preventDefault(); ev.stopPropagation();
|
||||
openCard(this);
|
||||
});
|
||||
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
|
||||
}
|
||||
}
|
||||
var initRun = function(){ wire(d.body || d.documentElement); };
|
||||
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
|
||||
else initRun();
|
||||
var mo = new MutationObserver(function(muts){
|
||||
var newCard = false;
|
||||
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
|
||||
if (newCard) initRun();
|
||||
});
|
||||
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
|
||||
})();
|
||||
</script>
|
||||
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
112
downloads/blade-install.ps1
Normal file
112
downloads/blade-install.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
# WEVIA Blade Install v4.0 — DEFINITIVE persistent install
|
||||
# Run as Administrator in PowerShell:
|
||||
# irm https://weval-consulting.com/downloads/blade-install.ps1 | iex
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
$BASE = "https://weval-consulting.com"
|
||||
$INSTALL_DIR = "C:\ProgramData\WEVAL"
|
||||
New-Item -ItemType Directory -Path $INSTALL_DIR -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Write-Host " WEVIA Blade Agent v4.0 - DEFINITIVE INSTALL" -ForegroundColor Cyan
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
|
||||
# STEP 1: Kill all existing WEVIA/Sentinel agents
|
||||
Write-Host "`n[1/6] Killing existing agents..." -ForegroundColor Yellow
|
||||
Get-Process -Name powershell, pwsh -ErrorAction SilentlyContinue | Where-Object {
|
||||
try { $_.CommandLine -match "sentinel-agent|blade-agent|wevia-agent" } catch { $false }
|
||||
} | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Get-ScheduledTask -TaskName "WEVIA*", "Sentinel*", "Blade*" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Write-Host " Done." -ForegroundColor Green
|
||||
|
||||
# STEP 2: Download agent v4
|
||||
Write-Host "`n[2/6] Downloading agent v4..." -ForegroundColor Yellow
|
||||
$agentPath = "$INSTALL_DIR\wevia-agent-v4.ps1"
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$BASE/api/blade-tasks/wevia-agent-v4.ps1" -OutFile $agentPath -UseBasicParsing
|
||||
Write-Host " Downloaded: $agentPath ($((Get-Item $agentPath).Length) bytes)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " FAILED download: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# STEP 3: Register as scheduled task (runs at logon + every 5 min watchdog check)
|
||||
Write-Host "`n[3/6] Registering scheduled task (auto-start at logon + 5min watchdog)..." -ForegroundColor Yellow
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$agentPath`""
|
||||
$trigger1 = New-ScheduledTaskTrigger -AtLogOn
|
||||
$trigger2 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5)
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Highest
|
||||
|
||||
Register-ScheduledTask -TaskName "WEVIA-Agent-v4" -Action $action -Trigger @($trigger1, $trigger2) -Settings $settings -Principal $principal -Force | Out-Null
|
||||
Write-Host " Scheduled task registered (AtLogon + 5min watchdog)." -ForegroundColor Green
|
||||
|
||||
# STEP 4: Create watchdog script that relaunches agent if dead
|
||||
Write-Host "`n[4/6] Installing watchdog..." -ForegroundColor Yellow
|
||||
$watchdogPath = "$INSTALL_DIR\watchdog.ps1"
|
||||
$watchdogScript = @'
|
||||
# Relaunches agent if not heartbeating
|
||||
$ErrorActionPreference = "Continue"
|
||||
$wdFile = "C:\ProgramData\WEVAL\agent-v4.watchdog"
|
||||
$agentPath = "C:\ProgramData\WEVAL\wevia-agent-v4.ps1"
|
||||
|
||||
if (Test-Path $wdFile) {
|
||||
$lastTicks = [int64](Get-Content $wdFile -Raw).Trim()
|
||||
$lastBeat = [DateTime]::new($lastTicks)
|
||||
$delta = ((Get-Date) - $lastBeat).TotalSeconds
|
||||
if ($delta -lt 120) {
|
||||
# Agent alive, nothing to do
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
# Agent dead or never started - launch it
|
||||
$existing = Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {
|
||||
try { $_.CommandLine -match "wevia-agent-v4" } catch { $false }
|
||||
}
|
||||
if (!$existing) {
|
||||
Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$agentPath`"" -WindowStyle Hidden
|
||||
Add-Content -Path "C:\ProgramData\WEVAL\watchdog.log" -Value "[$(Get-Date -Format 'o')] Relaunched agent"
|
||||
}
|
||||
'@
|
||||
Set-Content -Path $watchdogPath -Value $watchdogScript -Force
|
||||
Write-Host " Watchdog installed: $watchdogPath" -ForegroundColor Green
|
||||
|
||||
# STEP 5: Start the agent immediately
|
||||
Write-Host "`n[5/6] Starting agent v4 NOW..." -ForegroundColor Yellow
|
||||
Start-ScheduledTask -TaskName "WEVIA-Agent-v4" -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 3
|
||||
$running = Get-Process powershell -ErrorAction SilentlyContinue | Where-Object {
|
||||
try { $_.CommandLine -match "wevia-agent-v4" } catch { $false }
|
||||
}
|
||||
if ($running) {
|
||||
Write-Host " Agent running (PID=$($running.Id))." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Agent did not start via task, launching direct..." -ForegroundColor Yellow
|
||||
Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$agentPath`"" -WindowStyle Hidden
|
||||
}
|
||||
|
||||
# STEP 6: Verify heartbeat reaches server
|
||||
Write-Host "`n[6/6] Verifying heartbeat to server..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 10
|
||||
try {
|
||||
$status = Invoke-RestMethod -Uri "$BASE/api/blade-api.php?k=BLADE2026&action=status" -TimeoutSec 10
|
||||
$reportedVersion = $status.blade.heartbeat.agent_version
|
||||
if ($reportedVersion -eq "4.0") {
|
||||
Write-Host " SUCCESS: Server reports agent_version=4.0" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " WARNING: Server reports agent_version=$reportedVersion (expected 4.0)" -ForegroundColor Yellow
|
||||
Write-Host " Wait 30s and re-run status check" -ForegroundColor Yellow
|
||||
}
|
||||
} catch {
|
||||
Write-Host " Status check failed: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "`n============================================================" -ForegroundColor Cyan
|
||||
Write-Host " INSTALL COMPLETE" -ForegroundColor Cyan
|
||||
Write-Host " Logs: $INSTALL_DIR\agent-v4.log" -ForegroundColor Gray
|
||||
Write-Host " Watchdog: runs every 5 min via scheduler" -ForegroundColor Gray
|
||||
Write-Host " Agent restarts automatically on logoff/reboot" -ForegroundColor Gray
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Reference in New Issue
Block a user