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
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled

This commit is contained in:
opus
2026-04-21 22:54:56 +02:00
parent 6b25030a3c
commit a705e42253
4 changed files with 187 additions and 4 deletions

55
api/csat-api.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/**
* WEVAL CSAT - Customer Satisfaction Score sovereign 21avr2026
* Rating 1-5 after resolved ticket/interaction.
* Storage: /opt/weval-l99/data/csat-responses.jsonl
*/
header('Content-Type: application/json');
$STORAGE = '/opt/weval-l99/data/csat-responses.jsonl';
@mkdir(dirname($STORAGE), 0755, true);
$action = $_GET['action'] ?? ($_POST['action'] ?? 'stats');
if ($action === 'submit' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$rating = intval($_POST['rating'] ?? -1);
$context = substr(trim($_POST['context'] ?? ''), 0, 200);
$user = substr(trim($_POST['user'] ?? 'anonymous'), 0, 60);
if ($rating < 1 || $rating > 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",
]);

65
api/nps-collector.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
/**
* WEVAL NPS Collector - sovereign 21avr2026
* Zero external tool (Typeform/etc). Local JSONL storage.
* GET /api/nps-collector.php?action=stats -> 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),
]);

63
api/tickets-api.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
/**
* WEVAL Support Tickets sovereign 21avr2026
* Local JSONL + status tracking (open/resolved/closed)
* Storage: /opt/weval-l99/data/tickets.jsonl
*/
header('Content-Type: application/json');
$STORAGE = '/opt/weval-l99/data/tickets.jsonl';
@mkdir(dirname($STORAGE), 0755, true);
$action = $_GET['action'] ?? ($_POST['action'] ?? 'stats');
if ($action === 'create' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$subject = substr(trim($_POST['subject'] ?? ''), 0, 200);
$body = substr(trim($_POST['body'] ?? ''), 0, 2000);
$user = substr(trim($_POST['user'] ?? 'anonymous'), 0, 60);
$priority = in_array($_POST['priority'] ?? '', ['low','medium','high','critical']) ? $_POST['priority'] : 'medium';
if (!$subject) {
echo json_encode(['ok'=>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',
]);

View File

@@ -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"]
]