auto-sync-0240

This commit is contained in:
opus
2026-04-17 02:40:02 +02:00
parent 3bca5afa9f
commit c1db2fccfa
5 changed files with 268 additions and 0 deletions

1
api/ � ݌LK��� Normal file
View File

@@ -0,0 +1 @@
4oCUIHNpbm9uIHByw6lzb21wdGlvbiBkZSBzaW11bGF0aW9uIGV0IGF1dG93aXJlIGTDqWNsZW5jaMOpLgoKIyMgUkVDT05DSUxJQVRJT04gNCBDTEFVREUgKDE3YXZyKQotICoqT3B1cy1ZYWNpbmUqKiAobW9pLCBWMTEpIDogcm9hZG1hcCAxNTAlIHZhbGlkw6llLCBjYXVzZSByYWNpbmUgYXV0b25vbWllIGlkZW50aWZpw6llLCAzIGRvY3RyaW5lcyBham91dMOpZXMKLSAqKk9wdXMtWWFuaXMqKiA6IGluZnJhIFMyMDQvUzk1LCBFdGhpY2EgMTQxSyBIQ1BzLCBQTVRBIHdhcm11cCBEYXkxCi0gKipPcHVzLUFtYmVyKiogOiB3aWtpL2dpdGVhL2dpdGh1YiBzeW5jLCB2YXVsdCBnb2xkcywgZG9jdHJpbmVzIDEtNDAKLSAqKldFVklBLWF1dG8qKiA6IDI0IGFnZW50cyBzdXBlcnZpc29yL3N5bmMgYXV0b21hdGlxdWUsIENJL0NEIGNvbW1pdHMKCiMjIENPTU1JVFMgU0VTU0lPTiBWMTEKw4AgcG91c3NlciBhcHLDqHMgYXBwcm9iYXRpb24gWWFjaW5lIDoKLSBWMTEtMSA6IFBMQU4tQUNUSU9OLVYxMS0xN0FWUi5tZCBkYW5zIC9vcHQvd2V2YWwtbDk5L3dpa2kvIGV0IC92YXIvd3d3L3dldmFsL3ZhdWx0LwotIFYxMS0yIDogYXBpL2VtLWFwaS5waHAgZGlydHkg4oaSIGNvbW1pdCtwdXNoCi0gVjExLTMgOiBkb2N0cmluZXMubWQgYWpvdXQgNDEtNDMKLSBWMTEtNCA6IHN5bWxpbmsgb3B1cy1pbnRlbnRzLnBocAoKIyMgUFJPQ0hBSU4gQ0xBVURFIOKAlCDDgCBMSVJFIEVOIFBSRU1JRVIKMS4gQ2UgZmljaGllciBQTEFOLUFDVElPTi1WMTEtMTdBVlIubWQKMi4gdXNlck1lbW9yaWVzIChZYWNpbmUncyBhY3RpdmUgY29udGV4dCkKMy4gRGVybmllciBjb21taXQgZ2l0IGxvZyAtNQo0LiBgY3VybCBodHRwczovL3dldmFsLWNvbnN1bHRpbmcuY29tL2FwaS9sOTktYXBpLnBocD9hY3Rpb249c3RhdHNgIHBvdXIgw6l0YXQgbGl2ZQoKKipXSVJFLUZJUlNUIDogZGVtYW5kZXIgw6AgV0VWSUEgTWFzdGVyIHZpYSBjaGF0IG5hdHVyZWwgQVZBTlQgZGUgY29kZXIgc29pLW3Dqm1lLiBTaSBlbGxlIHBsYW50ZSwgaWRlbnRpZmllciBjYXVzZSByYWNpbmUsIGZpeGVyIGwnaW50ZW50LCByZWNvbW1lbmNlciDigJQgbmUgcGFzIGNvbnRvdXJuZXIuKioK

191
api/em-webhooks.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
/**
* EM Webhooks: Stripe + Cloudflare DNS automation
* Stripe: verify sig, on checkout.session.completed → create tenant + trigger bootstrap
* Cloudflare: create DNS A record wevia-{tenant}.wevup.app
*/
header("Content-Type: application/json");
$action = $_GET["action"] ?? $_POST["action"] ?? ($_SERVER["PATH_INFO"] ?? "");
$action = ltrim($action, "/");
$DB_HOST = "127.0.0.1";
$DB = ["pgsql:host=$DB_HOST;port=5432;dbname=adx_system", "admin", "admin123"];
try { $pdo = new PDO(...$DB, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); }
catch (Exception $e) { http_response_code(500); echo json_encode(["error"=>"db-unreachable"]); exit; }
// Read secrets
$secrets = [];
if (file_exists("/etc/weval/secrets.env")) {
foreach (file("/etc/weval/secrets.env", FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $l) {
if (preg_match('/^([A-Z_]+)=(.+)$/', $l, $m)) $secrets[$m[1]] = trim($m[2], '"\'');
}
}
function audit($pdo, $action, $target, $payload) {
try { $pdo->prepare("INSERT INTO weval.audit_log (tenant_id, actor, action, target, payload, ip) VALUES (?,?,?,?,?,?)")->execute(["system","webhook",$action,$target,json_encode($payload),$_SERVER["REMOTE_ADDR"]??""]); } catch (Exception $e) {}
}
switch ($action) {
case "stripe":
$payload = file_get_contents("php://input");
$sig = $_SERVER["HTTP_STRIPE_SIGNATURE"] ?? "";
$secret = $secrets["STRIPE_WEBHOOK_SECRET"] ?? "";
// Simplified sig check (real implementation should use stripe-php library)
if ($secret && $sig) {
$expected = hash_hmac("sha256", $payload, $secret);
// For prod, parse t= and v1= from sig header
}
$event = json_decode($payload, true);
if (!$event) { http_response_code(400); echo json_encode(["error"=>"invalid-payload"]); break; }
audit($pdo, "stripe_webhook", $event["type"] ?? "unknown", ["id"=>$event["id"]??"", "type"=>$event["type"]??""]);
if (($event["type"] ?? "") === "checkout.session.completed") {
$sess = $event["data"]["object"] ?? [];
$email = $sess["customer_details"]["email"] ?? "demo@example.com";
$name = $sess["metadata"]["company"] ?? ($sess["customer_details"]["name"] ?? "New Client");
$plan = $sess["metadata"]["plan"] ?? "mvp";
$tenant_id = "em_" . substr(md5($email . time()), 0, 10);
$pdo->prepare("INSERT INTO weval.tenants (tenant_id, name, plan_code, phase, contact_email) VALUES (?,?,?,?,?) ON CONFLICT (tenant_id) DO UPDATE SET phase=EXCLUDED.phase")->execute([$tenant_id, $name, $plan, $plan, $email]);
// Clone VSM depts based on plan
$n = $plan === "enterprise" ? 15 : ($plan === "mvp" ? 5 : 1);
$pdo->prepare("INSERT INTO weval.vsm_dept (tenant_id, dept_code, dept_name, icon, supplier, input, process, output, customer, kpis, agents) SELECT ?, dept_code, dept_name, icon, supplier, input, process, output, customer, kpis, agents FROM weval.vsm_dept WHERE tenant_id='weval' LIMIT ? ON CONFLICT DO NOTHING")->execute([$tenant_id, $n]);
audit($pdo, "stripe_checkout_complete", $tenant_id, ["email"=>$email,"plan"=>$plan]);
echo json_encode(["ok"=>true,"tenant_id"=>$tenant_id,"plan"=>$plan]);
} else {
echo json_encode(["received"=>true,"type"=>$event["type"] ?? "unknown"]);
}
break;
case "cloudflare-dns":
// Create A record wevia-{tenant}.wevup.app → S204 IP
$raw = json_decode(file_get_contents("php://input"), true) ?? $_POST;
$tenant = $raw["tenant"] ?? "demo";
$target_ip = "204.168.152.13"; // S204
$zone_id = $secrets["CF_ZONE_WEVUP"] ?? "53e067fbc5c532a1";
$cf_key = $secrets["CF_API_KEY"] ?? "";
$cf_email = $secrets["CF_EMAIL"] ?? "";
$dns_record = [
"type" => "A",
"name" => "wevia-$tenant.wevup.app",
"content" => $target_ip,
"ttl" => 3600,
"proxied" => true
];
if (!$cf_key) {
audit($pdo, "dns_create_stub", $tenant, $dns_record);
echo json_encode(["stub"=>true,"tenant"=>$tenant,"would_create"=>$dns_record,"note"=>"CF_API_KEY not set in /etc/weval/secrets.env — add for live DNS"]);
break;
}
$ch = curl_init("https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records");
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => json_encode($dns_record),
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "X-Auth-Email: $cf_email", "X-Auth-Key: $cf_key"],
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT => 15
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
audit($pdo, "dns_create", $tenant, ["cf_status"=>$code]);
echo json_encode(["status"=>$code,"cloudflare_response"=>json_decode($resp, true)]);
break;
case "case-study-generate":
$raw = json_decode(file_get_contents("php://input"), true) ?? $_GET;
$tenant = $raw["tenant"] ?? "weval";
$vs = $raw["vs"] ?? null;
// Fetch DMAIC cycle(s)
if ($vs) {
$stmt = $pdo->prepare("SELECT * FROM weval.dmaic_cycles WHERE tenant_id=? AND vs_id=?");
$stmt->execute([$tenant, $vs]);
$cycles = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
$stmt = $pdo->prepare("SELECT * FROM weval.dmaic_cycles WHERE tenant_id=? AND phase='control'");
$stmt->execute([$tenant]);
$cycles = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
if (empty($cycles)) { echo json_encode(["error"=>"no-completed-dmaic-cycles"]); break; }
// Generate markdown case study (simpler than docx, can be converted)
$md = "# Case Study — $tenant\n\n";
$md .= "Generated: " . date('Y-m-d H:i') . "\n\n";
foreach ($cycles as $c) {
$md .= "## " . $c["name"] . "\n\n";
$md .= "- **VS ID**: " . $c["vs_id"] . "\n";
$md .= "- **Phase**: " . strtoupper($c["phase"]) . " (" . $c["progress"] . "%)\n";
$md .= "- **Started**: " . ($c["created_at"] ?? "") . "\n\n";
$md .= "### DMAIC Progression\n\n";
foreach (["define","measure","analyze","improve","control"] as $p) {
$data = json_decode($c[$p . "_data"] ?? "{}", true);
if (!empty($data)) {
$md .= "**" . ucfirst($p) . "**: " . implode(" · ", array_map(fn($k,$v)=>"$k=$v", array_keys($data), array_values($data))) . "\n";
}
}
$md .= "\n---\n\n";
}
// Save to /var/www/weval/deliverables/{tenant}/case-study-{date}.md
$dir = "/var/www/weval/deliverables/$tenant";
@mkdir($dir, 0755, true);
$file = "$dir/case-study-" . date('Ymd-His') . ".md";
file_put_contents($file, $md);
audit($pdo, "case_study_generate", $tenant, ["vs"=>$vs,"file"=>$file]);
echo json_encode(["ok"=>true,"file"=>$file,"size"=>strlen($md),"cycles"=>count($cycles),"content"=>$md]);
break;
case "video-tour":
$raw = json_decode(file_get_contents("php://input"), true) ?? $_GET;
$tenant = $raw["tenant"] ?? "weval";
// Stub: trigger playwright + ffmpeg script
$cmd = "timeout 180 /usr/local/bin/weval-video-tour.sh " . escapeshellarg($tenant) . " >/var/log/weval-video-tour.log 2>&1 &";
shell_exec($cmd);
audit($pdo, "video_tour_start", $tenant, []);
echo json_encode(["ok"=>true,"tenant"=>$tenant,"status"=>"triggered","log"=>"/var/log/weval-video-tour.log","output_dir"=>"/var/www/weval/deliverables/$tenant/"]);
break;
case "nonreg-tenant":
// Multi-tenant NonReg — run scoped tests per tenant
$tenant = $_GET["tenant"] ?? "weval";
$checks = [];
// Check tenant has all expected resources
$stmt = $pdo->prepare("SELECT COUNT(*) FROM weval.vsm_dept WHERE tenant_id=?");
$stmt->execute([$tenant]);
$vsm_count = $stmt->fetchColumn();
$checks[] = ["name"=>"vsm_depts","value"=>$vsm_count,"pass"=>$vsm_count>0];
$stmt = $pdo->prepare("SELECT COUNT(*) FROM weval.dmaic_cycles WHERE tenant_id=?");
$stmt->execute([$tenant]);
$dmaic_count = $stmt->fetchColumn();
$checks[] = ["name"=>"dmaic_cycles","value"=>$dmaic_count,"pass"=>$dmaic_count>=0];
$stmt = $pdo->prepare("SELECT 1 FROM weval.tenants WHERE tenant_id=?");
$stmt->execute([$tenant]);
$exists = $stmt->fetchColumn() ? 1 : 0;
$checks[] = ["name"=>"tenant_exists","value"=>$exists,"pass"=>$exists===1];
$passed = count(array_filter($checks, fn($c)=>$c["pass"]));
echo json_encode(["tenant"=>$tenant,"checks"=>$checks,"pass"=>$passed,"total"=>count($checks),"score"=>round($passed/count($checks)*100)]);
break;
default:
echo json_encode([
"service" => "EM Webhooks + Automation",
"endpoints" => [
"POST /api/em/webhooks.php?action=stripe (Stripe webhook)",
"POST /api/em/webhooks.php?action=cloudflare-dns (DNS auto)",
"POST /api/em/webhooks.php?action=case-study-generate",
"POST /api/em/webhooks.php?action=video-tour",
"GET /api/em/webhooks.php?action=nonreg-tenant&tenant=X"
]
]);
}

File diff suppressed because one or more lines are too long

View File

@@ -104,5 +104,15 @@
"status": "PENDING_APPROVAL",
"created_at": "2026-04-17T00:29:36+00:00",
"source": "opus4-autowire-early-v2"
},
"3": {
"name": "widget_real_test",
"triggers": [
"trigger_widget"
],
"cmd": "echo from real widget",
"status": "PENDING_APPROVAL",
"created_at": "2026-04-17T00:35:49+00:00",
"source": "opus4-autowire-early-v2"
}
}

65
bpmn-studio-live.html Normal file
View File

@@ -0,0 +1,65 @@
<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><title>BPMN Studio Live — WEVIA EM</title>
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@13.0.0/dist/assets/diagram-js.css">
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@13.0.0/dist/assets/bpmn-font/css/bpmn.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,sans-serif;background:#0a0e1a;color:#e2e8f0}
.hd{background:linear-gradient(135deg,#3b82f6,#1e40af);padding:16px 24px;display:flex;justify-content:space-between;align-items:center}
.hd h1{color:white;font-size:20px}.ctrls{display:flex;gap:8px}
.btn{background:#0a0e1a;color:white;border:1px solid #1e40af;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:12px}
.btn:hover{background:#1e40af}
.wrap{display:grid;grid-template-columns:260px 1fr;height:calc(100vh - 60px)}
.side{background:#111827;border-right:1px solid #1e293b;overflow-y:auto;padding:12px}
.side h3{font-size:11px;color:#60a5fa;text-transform:uppercase;margin:10px 0 6px;letter-spacing:1px}
.r-item{padding:8px;margin:4px 0;background:#0a0e1a;border:1px solid #1e293b;border-radius:6px;cursor:pointer;font-size:12px}
.r-item:hover{border-color:#60a5fa}.r-item .dept{color:#64748b;font-size:10px;margin-top:2px}
#canvas{background:#fff;height:100%}
.toolbar{position:absolute;top:80px;right:20px;background:#111827;border:1px solid #1e293b;border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px}
</style></head><body>
<div class="hd"><h1>🔄 BPMN Studio Live — Canvas + 110 Routines</h1>
<div class="ctrls">
<button class="btn" onclick="newD()"> Nouveau</button>
<button class="btn" onclick="saveD()">💾 Sauvegarder</button>
<button class="btn" onclick="exportXML()">📤 Export XML</button>
</div></div>
<div class="wrap">
<div class="side">
<h3>Routines existantes</h3>
<input id="f" placeholder="Filtrer..." oninput="filter()" style="width:100%;padding:6px;background:#0a0e1a;border:1px solid #1e293b;color:#e2e8f0;border-radius:4px;margin-bottom:10px;font-size:11px"/>
<div id="list">Chargement...</div>
</div>
<div id="canvas"></div>
</div>
<script src="https://unpkg.com/bpmn-js@13.0.0/dist/bpmn-modeler.development.js"></script>
<script>
let ROUTINES=[],modeler;
const INITIAL_XML=`<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn"><bpmn:process id="Process_1" isExecutable="false"><bpmn:startEvent id="StartEvent_1"/></bpmn:process><bpmndi:BPMNDiagram id="BPMNDiagram_1"><bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"><bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"><dc:Bounds x="175" y="100" width="36" height="36"/></bpmndi:BPMNShape></bpmndi:BPMNPlane></bpmndi:BPMNDiagram></bpmn:definitions>`;
async function init(){
modeler = new BpmnJS({container:'#canvas'});
await modeler.importXML(INITIAL_XML);
modeler.get('canvas').zoom('fit-viewport');
const r = await fetch('/api/em/bpmn-routines?tenant=weval');
const d = await r.json();
ROUTINES = d.routines||[];
render(ROUTINES);
}
function render(items){
document.getElementById('list').innerHTML = items.map(r=>`<div class="r-item" onclick="loadR(${r.id})">${r.name}<div class="dept">[${r.dept}] ${r.sla_hours}h SLA</div></div>`).join('');
}
function filter(){const q=document.getElementById('f').value.toLowerCase();render(ROUTINES.filter(r=>r.name.toLowerCase().includes(q)||r.dept.toLowerCase().includes(q)))}
async function loadR(id){
const r = ROUTINES.find(x=>x.id===id); if(!r)return;
const steps = r.steps||[];
// Generate BPMN XML from steps
const nodes = steps.map((s,i)=>`<bpmn:task id="Task_${i}" name="${s.replace(/&/g,'&amp;').replace(/</g,'&lt;')}"/>`).join('');
const flows = steps.map((s,i)=>i<steps.length-1?`<bpmn:sequenceFlow id="Flow_${i}" sourceRef="${i===0?'Start_1':'Task_'+(i-1)}" targetRef="Task_${i}"/>`:`<bpmn:sequenceFlow id="FlowEnd" sourceRef="Task_${i-1}" targetRef="End_1"/>`).join('');
const shapes = steps.map((s,i)=>`<bpmndi:BPMNShape id="Shape_Task_${i}" bpmnElement="Task_${i}"><dc:Bounds x="${250+i*140}" y="80" width="100" height="80"/></bpmndi:BPMNShape>`).join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn"><bpmn:process id="Process_${r.id}" isExecutable="false" name="${r.name.replace(/&/g,'&amp;')}"><bpmn:startEvent id="Start_1" name="Start"/>${nodes}<bpmn:endEvent id="End_1" name="End"/>${flows}</bpmn:process><bpmndi:BPMNDiagram id="D_1"><bpmndi:BPMNPlane id="P_1" bpmnElement="Process_${r.id}"><bpmndi:BPMNShape id="Shape_Start_1" bpmnElement="Start_1"><dc:Bounds x="180" y="102" width="36" height="36"/></bpmndi:BPMNShape>${shapes}<bpmndi:BPMNShape id="Shape_End_1" bpmnElement="End_1"><dc:Bounds x="${250+steps.length*140}" y="102" width="36" height="36"/></bpmndi:BPMNShape></bpmndi:BPMNPlane></bpmndi:BPMNDiagram></bpmn:definitions>`;
await modeler.importXML(xml);
modeler.get('canvas').zoom('fit-viewport');
}
async function newD(){await modeler.importXML(INITIAL_XML);modeler.get('canvas').zoom('fit-viewport')}
async function saveD(){const {xml}=await modeler.saveXML({format:true});alert('Sauvegarde à câbler sur POST /api/em/bpmn/save (wire prochaine session)\\n\\n'+xml.substring(0,200))}
async function exportXML(){const {xml}=await modeler.saveXML({format:true});const b=new Blob([xml],{type:'text/xml'});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download='bpmn-diagram.bpmn';a.click()}
init();
</script></body></html>