From a705e4225393c7629c2454e447980bbc13ae4dba Mon Sep 17 00:00:00 2001 From: opus Date: Tue, 21 Apr 2026 22:54:56 +0200 Subject: [PATCH] feat(cs-sovereign-wire): 3 new endpoints sovereign (NPS CSAT Tickets) zero external tool zero cost JSONL storage + wired in v83 KPI (nps_score csat mttr tickets_open) - 4 KPIs hardcoded now LIVE wire - doctrine souverainete + honnetete --- api/csat-api.php | 55 ++++++++++++++++++++++++++++ api/nps-collector.php | 65 ++++++++++++++++++++++++++++++++++ api/tickets-api.php | 63 ++++++++++++++++++++++++++++++++ api/wevia-v83-business-kpi.php | 8 ++--- 4 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 api/csat-api.php create mode 100644 api/nps-collector.php create mode 100644 api/tickets-api.php diff --git a/api/csat-api.php b/api/csat-api.php new file mode 100644 index 000000000..1f7747219 --- /dev/null +++ b/api/csat-api.php @@ -0,0 +1,55 @@ + 5) { + echo json_encode(['ok'=>false,'error'=>'invalid_rating','expected'=>'1-5']); + exit; + } + @file_put_contents($STORAGE, json_encode(['ts'=>date('c'),'rating'=>$rating,'context'=>$context,'user'=>$user])."\n", FILE_APPEND | LOCK_EX); + echo json_encode(['ok'=>true,'recorded'=>true]); + exit; +} + +$responses = []; +if (is_readable($STORAGE)) { + foreach (file($STORAGE) as $line) { + $r = @json_decode(trim($line), true); + if ($r && isset($r['rating'])) $responses[] = $r; + } +} + +$n = count($responses); +if ($n === 0) { + echo json_encode(['ok'=>true,'source'=>'sovereign_jsonl','ts'=>date('c'),'csat_score_pct'=>0,'responses_total'=>0,'status'=>'wire_needed','drill'=>'No ratings yet. POST /api/csat-api.php?action=submit']); + exit; +} + +// CSAT = % ratings >= 4 (out of 5) +$satisfied = 0; +foreach ($responses as $r) if ($r['rating'] >= 4) $satisfied++; +$pct = round(($satisfied / $n) * 100); + +echo json_encode([ + 'ok'=>true, + 'source'=>'sovereign_jsonl', + 'ts'=>date('c'), + 'csat_score_pct'=>$pct, + 'responses_total'=>$n, + 'satisfied_count'=>$satisfied, + 'status'=>$pct >= 85 ? 'ok' : ($pct > 0 ? 'warn' : 'wire_needed'), + 'drill'=>"% ratings >=4 out of 5", +]); diff --git a/api/nps-collector.php b/api/nps-collector.php new file mode 100644 index 000000000..351bd46a7 --- /dev/null +++ b/api/nps-collector.php @@ -0,0 +1,65 @@ + KPI ready + * POST /api/nps-collector.php?action=submit -> save response (score 0-10, comment) + */ +header('Content-Type: application/json'); + +$STORAGE = '/opt/weval-l99/data/nps-responses.jsonl'; +@mkdir(dirname($STORAGE), 0755, true); + +$action = $_GET['action'] ?? ($_POST['action'] ?? 'stats'); + +if ($action === 'submit' && $_SERVER['REQUEST_METHOD'] === 'POST') { + $score = intval($_POST['score'] ?? -1); + $comment = substr(trim($_POST['comment'] ?? ''), 0, 500); + $user = substr(trim($_POST['user'] ?? 'anonymous'), 0, 60); + if ($score < 0 || $score > 10) { + echo json_encode(['ok'=>false,'error'=>'invalid_score','expected'=>'0-10']); + exit; + } + $record = ['ts'=>date('c'),'score'=>$score,'comment'=>$comment,'user'=>$user,'ip'=>$_SERVER['REMOTE_ADDR']??'']; + @file_put_contents($STORAGE, json_encode($record)."\n", FILE_APPEND | LOCK_EX); + echo json_encode(['ok'=>true,'recorded'=>$record]); + exit; +} + +// stats = NPS score aggregation +$responses = []; +if (is_readable($STORAGE)) { + foreach (file($STORAGE) as $line) { + $r = @json_decode(trim($line), true); + if ($r && isset($r['score'])) $responses[] = $r; + } +} + +$n = count($responses); +if ($n === 0) { + echo json_encode(['ok'=>true,'source'=>'sovereign_jsonl','ts'=>date('c'),'nps_score'=>0,'responses_total'=>0,'promoters'=>0,'passives'=>0,'detractors'=>0,'status'=>'wire_needed','drill'=>'No responses yet. Post to this endpoint with score+comment.','endpoint_submit'=>'/api/nps-collector.php?action=submit']); + exit; +} + +$promoters = 0; $passives = 0; $detractors = 0; +foreach ($responses as $r) { + $s = $r['score']; + if ($s >= 9) $promoters++; + elseif ($s >= 7) $passives++; + else $detractors++; +} +$nps = round((($promoters - $detractors) / $n) * 100); + +echo json_encode([ + 'ok'=>true, + 'source'=>'sovereign_jsonl', + 'ts'=>date('c'), + 'nps_score'=>$nps, + 'responses_total'=>$n, + 'promoters'=>$promoters, + 'passives'=>$passives, + 'detractors'=>$detractors, + 'status'=>$nps >= 50 ? 'ok' : ($nps >= 0 ? 'warn' : 'fail'), + 'drill'=>"NPS = ((promoters - detractors) / total) * 100", + 'recent_comments'=>array_slice(array_reverse(array_column($responses, 'comment')), 0, 5), +]); diff --git a/api/tickets-api.php b/api/tickets-api.php new file mode 100644 index 000000000..c10bd0545 --- /dev/null +++ b/api/tickets-api.php @@ -0,0 +1,63 @@ +false,'error'=>'subject_required']); + exit; + } + $id = 'TKT-' . date('Ymd') . '-' . substr(md5($subject.microtime()), 0, 6); + $record = ['ts'=>date('c'),'id'=>$id,'status'=>'open','subject'=>$subject,'body'=>$body,'user'=>$user,'priority'=>$priority,'resolved_at'=>null]; + @file_put_contents($STORAGE, json_encode($record)."\n", FILE_APPEND | LOCK_EX); + echo json_encode(['ok'=>true,'ticket_id'=>$id]); + exit; +} + +$tickets = []; +if (is_readable($STORAGE)) { + foreach (file($STORAGE) as $line) { + $r = @json_decode(trim($line), true); + if ($r) $tickets[] = $r; + } +} + +// Stats +$total = count($tickets); +$open = 0; $resolved = 0; $mttr_hours = 0; $mttr_count = 0; +foreach ($tickets as $t) { + if ($t['status'] === 'open') $open++; + elseif ($t['status'] === 'resolved' || $t['status'] === 'closed') { + $resolved++; + if (!empty($t['resolved_at'])) { + $delta = (strtotime($t['resolved_at']) - strtotime($t['ts'])) / 3600; + if ($delta > 0) { $mttr_hours += $delta; $mttr_count++; } + } + } +} +$mttr = $mttr_count > 0 ? round($mttr_hours / $mttr_count, 1) : 0; + +echo json_encode([ + 'ok'=>true, + 'source'=>'sovereign_jsonl', + 'ts'=>date('c'), + 'tickets_total'=>$total, + 'tickets_open'=>$open, + 'tickets_resolved'=>$resolved, + 'mttr_hours'=>$mttr, + 'status'=>$open === 0 && $total === 0 ? 'wire_needed' : ($open <= 5 ? 'ok' : 'warn'), + 'endpoint_create'=>'/api/tickets-api.php?action=create', +]); diff --git a/api/wevia-v83-business-kpi.php b/api/wevia-v83-business-kpi.php index 5334187e8..a5d4fe8cf 100644 --- a/api/wevia-v83-business-kpi.php +++ b/api/wevia-v83-business-kpi.php @@ -133,10 +133,10 @@ $kpis = [ "kpis" => [ ["id" => "customer_churn_monthly", "label" => "Monthly churn", "value" => $v50["churn_monthly"], "unit" => "%", "target" => 5, "trend" => "live", "status" => "ok", "source" => "CRM", "drill" => "Target < 5%/month"], ["id" => "net_revenue_retention", "label" => "Net Revenue Retention", "value" => $v50["nrr"], "unit" => "%", "target" => 110, "trend" => "live", "status" => $v50["nrr"] >= 110 ? "ok" : "warn", "source" => "Stripe", "drill" => "Target > 100% = expansion > churn"], - ["id" => "nps_score", "label" => "NPS score", "value" => 0, "unit" => "pts", "target" => 50, "trend" => "wire_survey", "status" => "warn", "source" => "Customer survey tool", "drill" => "Send NPS campaign via Pharma Cloud"], - ["id" => "csat_score", "label" => "CSAT (CSAT)", "value" => 0, "unit" => "%", "target" => 85, "trend" => "wire_survey", "status" => "warn", "source" => "Support tickets rating", "drill" => "Post-ticket rating avg"], - ["id" => "support_tickets_open", "label" => "Support tickets open", "value" => (int)trim(@shell_exec('grep -c "" /var/log/support-tickets.log 2>/dev/null || echo 0')), "unit" => "tickets", "target" => 5, "trend" => "wire_support", "status" => "live", "source" => "Zendesk/Intercom", "drill" => "Low = healthy"], - ["id" => "mean_time_to_resolution", "label" => "MTTR support", "value" => 0, "unit" => "hours", "target" => 24, "trend" => "wire_support", "status" => "warn", "source" => "Support system", "drill" => "First response to close"], + ["id" => "nps_score", "label" => "NPS score", "value" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/nps-collector.php"),true); return intval($r["nps_score"]??0);})(), "unit" => "pts", "target" => 50, "trend" => "live", "status" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/nps-collector.php"),true); return $r["status"]??"wire_needed";})(), "source" => "sovereign NPS collector /api/nps-collector.php", "drill" => "POST score 0-10 + comment · NPS = (promoters-detractors)/total*100"], + ["id" => "csat_score", "label" => "CSAT (Customer Satisfaction)", "value" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/csat-api.php"),true); return intval($r["csat_score_pct"]??0);})(), "unit" => "%", "target" => 85, "trend" => "live", "status" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/csat-api.php"),true); return $r["status"]??"wire_needed";})(), "source" => "sovereign CSAT /api/csat-api.php", "drill" => "POST rating 1-5 after ticket · CSAT = % ratings >=4"], + ["id" => "support_tickets_open", "label" => "Support tickets open", "value" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/tickets-api.php"),true); return intval($r["tickets_open"]??0);})(), "unit" => "tickets", "target" => 5, "trend" => "live", "status" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/tickets-api.php"),true); $o=intval($r["tickets_open"]??0); $t=intval($r["tickets_total"]??0); if($t===0) return "wire_needed"; return $o<=5?"ok":"warn";})(), "source" => "sovereign tickets /api/tickets-api.php", "drill" => "POST subject+body · statuses: open/resolved/closed"], + ["id" => "mean_time_to_resolution", "label" => "MTTR support", "value" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/tickets-api.php"),true); return floatval($r["mttr_hours"]??0);})(), "unit" => "hours", "target" => 24, "trend" => "live", "status" => (function(){$r=@json_decode(@file_get_contents("http://localhost/api/tickets-api.php"),true); $m=floatval($r["mttr_hours"]??0); $t=intval($r["tickets_total"]??0); if($t===0) return "wire_needed"; return $m<=24?"ok":"warn";})(), "source" => "sovereign tickets MTTR", "drill" => "avg(resolved_at - ts) in hours on resolved tickets"], ["id" => "customer_health_score", "label" => "Customer health score avg", "value" => 75, "unit" => "/100", "target" => 80, "trend" => "computed", "status" => "ok", "source" => "WePredict model", "drill" => "Composite: usage + tickets + payments"], ["id" => "feature_adoption_rate", "label" => "Feature adoption", "value" => $v50["feature_adoption"], "unit" => "%", "target" => 70, "trend" => "live", "status" => $v50["feature_adoption"] >= 70 ? "ok" : "warn", "source" => "Platform telemetry", "drill" => "Features used / features available"] ]