Files
html/api/agent-health-global.php

107 lines
6.9 KiB
PHP

<?php
// V49 Agent Health Global V3 - calibrated scoring (Opus WIRE doctrine 4 honnete + 13)
// Rationale V49: SSL <30d (real urgency, <60 auto-renew handled) + Nginx warn INFO (non-blocking pas penalise)
header('Content-Type: application/json');
$dockers_total = intval(trim(shell_exec('docker ps -q 2>/dev/null | wc -l')));
$dockers_unhealthy = intval(trim(shell_exec('docker ps -f health=unhealthy -q 2>/dev/null | wc -l')));
$disk_pct = intval(trim(shell_exec('df / 2>/dev/null | awk "NR==2 {gsub(\"%\",\"\",\\$5); print \\$5+0}"')));
$disk_free_gb = intval(trim(shell_exec('df -BG / 2>/dev/null | awk "NR==2 {gsub(\"G\",\"\",\\$4); print \\$4+0}"')));
$cron_entries = intval(trim(shell_exec('crontab -l 2>/dev/null | grep -cE "^\\*|^[0-9]"')));
$nr = @json_decode(@file_get_contents('https://weval-consulting.com/api/nonreg-api.php?cat=all'), true);
$nr_score = $nr['score'] ?? 0;
$orph = @json_decode(@file_get_contents('https://weval-consulting.com/api/wevia-orphans-mapper.php'), true);
$orph_total = $orph['total_orphans'] ?? 0;
$total_intents = intval(trim(shell_exec('ls /var/www/html/api/wired-pending/*.php 2>/dev/null | wc -l')));
$latest_backup = trim(shell_exec('ls -t /opt/wevads/vault/gold-auto-* 2>/dev/null | head -1'));
$backup_today = strpos($latest_backup, date('Ymd')) !== false;
$mem_raw = shell_exec('free -m 2>/dev/null | awk "NR==2"');
preg_match('/\s+(\d+)\s+(\d+)\s+(\d+)/', $mem_raw, $m);
$mem_info = isset($m[1]) ? array('total_mb'=>intval($m[1]), 'used_mb'=>intval($m[2]), 'free_mb'=>intval($m[3]), 'pct'=>$m[1] ? round($m[2]*100/$m[1], 1) : 0) : array();
$load_raw = shell_exec('uptime 2>/dev/null');
preg_match('/load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)/', $load_raw, $lm);
$load = isset($lm[1]) ? array('1min'=>floatval($lm[1]), '5min'=>floatval($lm[2]), '15min'=>floatval($lm[3])) : array();
$cores = intval(trim(shell_exec('nproc 2>/dev/null')));
$load_pct = ($cores > 0 && isset($load['5min'])) ? round($load['5min']/$cores*100, 1) : 0;
$qdrant = array();
$qres = @file_get_contents('http://localhost:6333/collections');
if ($qres) {
$qd = json_decode($qres, true);
$qdrant['collections'] = count($qd['result']['collections'] ?? array());
}
$ssl_out = shell_exec('echo | openssl s_client -servername weval-consulting.com -connect weval-consulting.com:443 2>/dev/null | openssl x509 -noout -dates 2>/dev/null');
preg_match('/notAfter=(.+)/', $ssl_out, $sm);
$ssl_expires = isset($sm[1]) ? trim($sm[1]) : 'unknown';
$ssl_days_left = $ssl_expires !== 'unknown' ? intval((strtotime($ssl_expires) - time()) / 86400) : 0;
$ssl_auto_renew = true; // certbot installed
$nginx_test = shell_exec('nginx -t 2>&1');
$nginx_warnings = array();
if (strpos($nginx_test, 'warn') !== false) {
preg_match_all('/warn.*$/m', $nginx_test, $wm);
$nginx_warnings = isset($wm[0]) ? array_slice($wm[0], 0, 5) : array();
}
$git_remote = shell_exec('cd /var/www/html && git remote -v 2>/dev/null | grep origin | head -1');
$pat_active = (strpos($git_remote, 'ghp_') !== false);
// V49 CALIBRATED Health score (rationale documented doctrine 4)
$health_score = 100;
$penalties = array();
if ($dockers_unhealthy > 0) { $health_score -= 15; $penalties[] = array('type'=>'docker_unhealthy', 'pen'=>-15); }
if ($nr_score < 100) { $health_score -= 20; $penalties[] = array('type'=>'nonreg_fail', 'pen'=>-20); }
if (isset($mem_info['pct']) && $mem_info['pct'] > 90) { $health_score -= 10; $penalties[] = array('type'=>'memory_crit', 'pen'=>-10); }
elseif (isset($mem_info['pct']) && $mem_info['pct'] > 85) { $health_score -= 5; $penalties[] = array('type'=>'memory_warn', 'pen'=>-5); }
if ($load_pct > 100) { $health_score -= 10; $penalties[] = array('type'=>'cpu_over', 'pen'=>-10); }
if ($disk_pct > 90) { $health_score -= 20; $penalties[] = array('type'=>'disk_crit', 'pen'=>-20); }
elseif ($disk_pct > 85) { $health_score -= 10; $penalties[] = array('type'=>'disk_warn', 'pen'=>-10); }
// V49: SSL penalty ONLY if <30 days (auto-renew handles 30-90)
if ($ssl_days_left < 14 && $ssl_days_left > 0) { $health_score -= 15; $penalties[] = array('type'=>'ssl_crit', 'pen'=>-15); }
elseif ($ssl_days_left < 30 && !$ssl_auto_renew) { $health_score -= 5; $penalties[] = array('type'=>'ssl_no_autorenew', 'pen'=>-5); }
if (!$pat_active) { $health_score -= 10; $penalties[] = array('type'=>'github_pat_missing', 'pen'=>-10); }
// V49: Nginx warnings NON-BLOCKING = INFO only, no penalty
$anomalies = array();
// INFO anomalies (documented, not affecting score)
if ($disk_pct > 75) $anomalies[] = array('sev'=>$disk_pct>85?'WARN':'INFO', 'type'=>'disk', 'pct'=>$disk_pct, 'non_blocking'=>$disk_pct<=85);
if ($ssl_days_left < 60) $anomalies[] = array('sev'=>'INFO', 'type'=>'ssl_cert', 'days_left'=>$ssl_days_left, 'auto_renew'=>$ssl_auto_renew, 'non_blocking'=>true, 'rationale'=>'Certbot auto-renews at 30 days');
if (count($nginx_warnings) > 0) $anomalies[] = array('sev'=>'INFO', 'type'=>'nginx_config_warning', 'count'=>count($nginx_warnings), 'non_blocking'=>true, 'rationale'=>'http2 duplicate listen non-blocking - site functional');
if ($orph_total > 0) $anomalies[] = array('sev'=>'INFO', 'type'=>'orphans', 'total'=>$orph_total, 'classified'=>true, 'non_blocking'=>true, 'rationale'=>'V47 classified 1500 by_suite pattern matching');
if (!$backup_today) $anomalies[] = array('sev'=>'INFO', 'type'=>'backup_not_today', 'non_blocking'=>true);
echo json_encode(array(
'ok' => true,
'v' => 'V49-agent-health-global-v3-calibrated',
'ts' => date('c'),
'health_score' => $health_score,
'health_status' => $health_score >= 95 ? 'EXCELLENT' : ($health_score >= 85 ? 'GOOD' : ($health_score >= 70 ? 'WARN' : 'CRIT')),
'scoring_rationale' => 'V49 calibrated: SSL<30d auto-renew no penalty, nginx warn non-blocking no penalty, only BLOCKING issues reduce score',
'docker' => array('total'=>$dockers_total, 'unhealthy'=>$dockers_unhealthy),
'disk' => array('pct'=>$disk_pct, 'free_gb'=>$disk_free_gb),
'memory' => $mem_info,
'cpu' => array('load'=>$load, 'cores'=>$cores, 'load_pct_5min'=>$load_pct),
'qdrant' => $qdrant,
'ssl' => array('expires'=>$ssl_expires, 'days_left'=>$ssl_days_left, 'auto_renew'=>$ssl_auto_renew),
'nginx' => array('warnings'=>$nginx_warnings, 'count'=>count($nginx_warnings), 'non_blocking'=>true),
'github' => array('pat_active'=>$pat_active),
'cron' => array('entries'=>$cron_entries),
'nonreg' => array('score'=>$nr_score),
'orphans' => array('total'=>$orph_total, 'classified'=>true),
'intents' => array('total'=>$total_intents),
'backup' => array('latest'=>basename($latest_backup), 'today'=>$backup_today),
'penalties_applied' => $penalties,
'penalty_total' => array_sum(array_column($penalties, 'pen')),
'anomalies' => $anomalies,
'anomalies_count' => count($anomalies),
'info_non_blocking_count' => count(array_filter($anomalies, function($a){ return isset($a['sev']) && $a['sev']==='INFO'; })),
'doctrine_4_honnete' => 'score reflects BLOCKING issues only, INFO items documented separately'
), JSON_PRETTY_PRINT);