Files
html/api/visual-management-live.php
2026-04-20 15:47:46 +02:00

160 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Visual Management Live API
* Doctrine 65: VISUAL_MANAGEMENT - Lean Six Sigma KPI aggregation
* 0 hardcode · live fetch · UX premium
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache, max-age=30');
function q($sql) {
$cmd = 'PGPASSWORD=admin123 psql -h 10.1.0.3 -U admin -d adx_system -t -A -F "|" -c ' . escapeshellarg($sql) . ' 2>&1';
$raw = @shell_exec($cmd);
$rows = [];
foreach (array_filter(array_map('trim', explode("\n", $raw ?? ''))) as $line) {
// Support single-column AND multi-column rows
if (strpos($line, '|') !== false) {
$rows[] = array_map('trim', explode('|', $line));
} else {
$rows[] = [$line];
}
}
return $rows;
}
function qone($sql) {
$r = q($sql);
return $r && isset($r[0][0]) ? $r[0][0] : null;
}
$out = ['ts' => date('c'), 'generator' => 'visual-management-live.php v1 doctrine 65'];
// 1. BUSINESS KPIs (CRM pipeline state)
$out['business'] = [
'crm_deals' => (int)qone("SELECT count(*) FROM admin.pipeline_deals"),
'crm_deals_amount_eur' => (int)qone("SELECT COALESCE(SUM(amount),0) FROM admin.pipeline_deals WHERE currency='EUR'"),
'crm_companies' => (int)qone("SELECT count(*) FROM admin.pipeline_companies"),
'crm_contacts' => (int)qone("SELECT count(*) FROM admin.pipeline_contacts"),
'crm_contacts_b2b' => (int)qone("SELECT count(*) FROM admin.pipeline_contacts WHERE segment_type='B2B'"),
'crm_contacts_linked' => (int)qone("SELECT count(*) FROM admin.pipeline_contacts WHERE company_id IS NOT NULL"),
'crm_activities' => (int)qone("SELECT count(*) FROM admin.pipeline_activities"),
'crm_activities_last_7d' => (int)qone("SELECT count(*) FROM admin.pipeline_activities WHERE created_at > NOW() - interval '7 days'"),
'ethica_hcps' => (int)qone("SELECT count(*) FROM ethica.medecins_real"),
'ethica_hcps_dz' => (int)qone("SELECT count(*) FROM ethica.medecins_real WHERE pays='DZ'"),
'ethica_hcps_ma' => (int)qone("SELECT count(*) FROM ethica.medecins_real WHERE pays='MA'"),
'ethica_hcps_tn' => (int)qone("SELECT count(*) FROM ethica.medecins_real WHERE pays='TN'"),
'office_accounts' => (int)qone("SELECT count(*) FROM admin.office_accounts"),
'office_active' => (int)qone("SELECT count(*) FROM admin.office_accounts WHERE status='active'"),
'office_warming' => (int)qone("SELECT count(*) FROM admin.office_accounts WHERE status='warming'"),
];
// 2. FLUX KPIs (flow detection - doctrine 55 anti-staleness)
$out['flux'] = [
'send_contacts_last_7d' => (int)qone("SELECT count(*) FROM admin.send_contacts WHERE created_at > NOW() - interval '7 days'"),
'send_contacts_last_30d' => (int)qone("SELECT count(*) FROM admin.send_contacts WHERE created_at > NOW() - interval '30 days'"),
'send_contacts_last_created' => qone("SELECT MAX(created_at)::text FROM admin.send_contacts"),
'graph_send_last_7d' => (int)qone("SELECT count(*) FROM admin.graph_send_log WHERE created_at > NOW() - interval '7 days'"),
'graph_send_last_created' => qone("SELECT MAX(created_at)::text FROM admin.graph_send_log"),
'leads_last_7d' => (int)qone("SELECT count(*) FROM admin.leads WHERE created_at > NOW() - interval '7 days'"),
'weval_leads_last_7d' => (int)qone("SELECT count(*) FROM admin.weval_leads WHERE created_at > NOW() - interval '7 days'"),
'pipeline_deals_last_30d' => (int)qone("SELECT count(*) FROM admin.pipeline_deals WHERE created_at > NOW() - interval '30 days'"),
'pipeline_contacts_last_24h' => (int)qone("SELECT count(*) FROM admin.pipeline_contacts WHERE created_at > NOW() - interval '24 hours'"),
];
// 3. QUALITY KPIs (Lean 6σ)
// NonReg + L99
$nr = json_decode(@file_get_contents('http://127.0.0.1/api/nonreg-api.php?cat=all'), true);
$l99 = json_decode(@file_get_contents('http://127.0.0.1/api/l99-api.php?action=stats'), true);
$out['quality'] = [
'nonreg_pass' => (int)($nr['pass'] ?? 0),
'nonreg_total' => (int)($nr['total'] ?? 0),
'nonreg_score' => (float)($nr['score'] ?? 0),
'l99_pass' => (int)($l99['pass'] ?? 0),
'l99_total' => (int)($l99['total'] ?? 0),
'l99_score' => (float)($l99['score'] ?? 0),
];
// 4. ANDON ALERTS (flux stagnation detection - doctrine 55 KPI gap fix)
$andons = [];
$last_send = strtotime($out['flux']['send_contacts_last_created'] ?? 'now');
$days_since_send = ($last_send && $last_send > 0) ? floor((time() - $last_send) / 86400) : 999;
if ($days_since_send > 7) {
$andons[] = ['severity' => 'RED', 'kpi' => 'send_contacts flux', 'message' => "send_contacts aucun ajout depuis $days_since_send jours"];
}
// 20avr fix doctrine #17 SEND MANUAL:
// Low email volume is EXPECTED when Yacine hasn't launched campaign.
// Andon reste INFO pour transparence, pas ORANGE (pas un bug technique)
if ($out['flux']['graph_send_last_7d'] < 100) {
$andons[] = ['severity' => 'INFO', 'kpi' => 'graph_send_log flux', 'message' => "Emails envoyés 7j: " . $out['flux']['graph_send_last_7d'] . " (doctrine #17 SEND MANUAL — en attente lancement campagne par Yacine)"];
}
if ($out['flux']['pipeline_deals_last_30d'] == 0) {
$andons[] = ['severity' => 'RED', 'kpi' => 'crm_deals flux', 'message' => "Aucun nouveau deal en 30 jours (pipeline commercial stale)"];
}
if ($out['business']['crm_contacts_b2b'] > 0 && $out['business']['crm_activities'] < 100) {
$andons[] = ['severity' => 'ORANGE', 'kpi' => 'crm_activities', 'message' => "Activities = " . $out['business']['crm_activities'] . " vs " . $out['business']['crm_contacts_b2b'] . " contacts B2B — sous-exploité"];
}
if ($out['quality']['nonreg_score'] < 100) {
$andons[] = ['severity' => 'ORANGE', 'kpi' => 'nonreg', 'message' => "NonReg score " . $out['quality']['nonreg_score'] . "% — régressions détectées"];
}
$out['andons'] = $andons;
$out['andons_count'] = count($andons);
// 5. INFRA KPIs
$load = trim(@shell_exec('cat /proc/loadavg 2>&1') ?? '');
$load_parts = explode(' ', $load);
$mem = trim(@shell_exec("free -m | awk 'NR==2{printf \"%.1f\", \$3*100/\$2}'") ?? '0');
$disk = trim(@shell_exec("df / | awk 'NR==2{print \$5}' | tr -d '%'") ?? '0');
$out['infra'] = [
'load_1m' => (float)($load_parts[0] ?? 0),
'load_5m' => (float)($load_parts[1] ?? 0),
'mem_pct' => (float)$mem,
'disk_pct' => (int)$disk,
'uptime_hours' => (int)(trim(@shell_exec("awk '{print int(\$1/3600)}' /proc/uptime") ?? 0)),
'docker_containers' => (int)trim(@shell_exec("docker ps -q 2>/dev/null | wc -l") ?? 0),
'fpm_workers' => (int)trim(@shell_exec("pgrep -c 'php-fpm: pool www' 2>/dev/null") ?? 0),
];
// 6. CLASSIFICATION KPIs (doctrine 63)
$out['classification'] = [
'leads_classified_pct' => 100.0 * (int)qone("SELECT count(*) FROM admin.leads WHERE segment_type IS NOT NULL") / max(1, (int)qone("SELECT count(*) FROM admin.leads")),
'send_contacts_classified_pct' => 100.0 * (int)qone("SELECT count(*) FROM admin.send_contacts WHERE segment_type IS NOT NULL") / max(1, (int)qone("SELECT count(*) FROM admin.send_contacts")),
'leads_b2b' => (int)qone("SELECT count(*) FROM admin.leads WHERE segment_type='B2B'"),
'send_contacts_b2b' => (int)qone("SELECT count(*) FROM admin.send_contacts WHERE segment_type='B2B'"),
];
// Global health score (weighted)
$health = 0;
$health += min(25, $out['quality']['nonreg_score'] / 4);
$health += min(25, $out['quality']['l99_score'] / 4);
$health += $andons ? max(0, 25 - count($andons) * 5) : 25;
$health += ($out['business']['crm_contacts_b2b'] > 1000) ? 25 : min(25, $out['business']['crm_contacts_b2b'] / 40);
$out['health_score'] = round($health, 1);
$out['health_status'] = $health >= 85 ? 'GREEN' : ($health >= 60 ? 'AMBER' : 'RED');
// 7. KPI HISTORY (doctrine 65 - 30 derniers jours)
$hist_rows = q("SELECT snap_date::text, crm_deals, crm_companies, crm_contacts, crm_contacts_b2b, crm_activities, ethica_hcps, health_score, andons_count FROM admin.kpi_history_daily WHERE snap_date > CURRENT_DATE - interval '30 days' ORDER BY snap_date ASC LIMIT 31");
$out['history'] = [];
foreach ($hist_rows as $h) {
if (count($h) >= 9) {
$out['history'][] = [
'date' => $h[0],
'deals' => (int)$h[1],
'companies' => (int)$h[2],
'contacts' => (int)$h[3],
'contacts_b2b' => (int)$h[4],
'activities' => (int)$h[5],
'hcps' => (int)$h[6],
'health' => (float)$h[7],
'andons' => (int)$h[8],
];
}
}
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);