Compare commits

...

3 Commits

Author SHA1 Message Date
Opus
c67ba9c962 V157 Opus WTP banner orphans link consolidation - Yacine demande WEVAL Technology Platform point entree de tout architecture - sitemap api avant 6 orphans dont droid e2e-dashboard et 4 duplicates accents - V157 ajout 2 liens additifs droid.html WEDROID Terminal 28KB et e2e-dashboard.html Playwright 8 screenshots dans banner WTP apres Arsenal History - resultat 6 sur 6 orphans devient 4 sur 6 orphans 4 restants sont duplicates accents harmless - GOLD backup vault v157-wtp-orphans-link - chattr discipline -i +i - WTP file size 360717 vers 361275 bytes additif 558 bytes - HTTP 200 OK - NR 153 sur 153 preserved - L99 153 sur 153 6sigma DPMO 0 preserved - doctrines 1 scan exhaustif autres claudes 4-actions wave-222 e2e-tests scenario business 12 etapes - 3 GOLD - 4 honnete - 14 zero ecrasement additif uniquement - 16 zero regression NR L99 maintenus
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
2026-04-22 03:09:56 +02:00
Opus V158
54c7e3ec4d V157 V158 E2E tests REVEAL 6 critical surprises before Kaouther GO
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
TESTS PASSED:
T1 Data quality 14/14 pilot view 3542 HIGH quality
T2 Consent flow 5/5 500 tokens unique 100pct coverage
T3 Template file exists 2187 bytes 3 placeholders
T4 PMTA Direct send SMTP 250 OK
T5 SPF weval-consulting.com includes S204 PMTA

SURPRISES CRITIQUES for Kaouther readiness:

S1 creative_html=filename only
  Campaign 2 stores ethica-pilot-template.html not inline HTML
  Pipeline must file_get_contents at send time

S2 Graph API all disabled
  197 graph_accounts all can_send=false status=disabled
  OAuth tokens expired/revoked
  Only PMTA_Direct path works

S3 ethica.senders SPF hardfail
  raphaelafortin deloisnegron allonzomichel .onmicrosoft.com
  SPF v=spf1 include:spf.protection.outlook.com -all
  HARDFAIL when sent via our PMTA

S4 Campaign 2 from_email will fail SPF
  raphaelafortin.onmicrosoft.com cannot use our PMTA
  Must change to ethica@weval-consulting.com

S5 Pipeline SAFETY MODE
  auto_mode=false dangerous_crons_disabled=true
  24 campaigns paused 0 active
  send_queue 0 last_send 2026-04-16

S6 DKIM MISSING
  No DKIM selector found (tested google default selector1 2 mta s1 s2 k1)
  DMARC p=quarantine pct=100 = spam folder without DKIM

FIX PRIORITIES:
  P1 Change Campaign 2 from_email
  P2 Setup DKIM weval-consulting.com
  P3 Seed placement test before pilot
  P4 IP warmup 3 days
  P5 Activate campaign + disable safety

Verdict: Data ready. Email auth NOT ready for 3 days.

L99 153/153 PASS (25 consecutive versions V125-V158)

Chain V131-V158 complete

Doctrines 0+1+2+4+13+14+95+100 applied
Tests revealed truth that simulations saved us from surprising Kaouther
2026-04-22 03:09:08 +02:00
opus
39904106c9 AUTO-BACKUP 20260422-0305
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
2026-04-22 03:05:03 +02:00
34 changed files with 3194 additions and 157 deletions

32
api/ambre-export-v30.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
header("Content-Type: application/json");
$src_dir = "/var/www/html/api/ambre-pw-tests/output";
$dest_dir = "/var/www/html/generated";
if (!is_dir($dest_dir)) @mkdir($dest_dir, 0777, true);
// Copy video
$video_src = glob("$src_dir/v30-final-showcase-*/video.webm")[0] ?? null;
$out = [];
if ($video_src) {
$dest = "$dest_dir/wevia-v30-showcase-" . date("Ymd-His") . ".webm";
@copy($video_src, $dest);
@chmod($dest, 0644);
$out["video"] = [
"url" => "/generated/" . basename($dest),
"size_mb" => round(filesize($dest)/1024/1024, 2),
];
}
// Copy all V30 screenshots
$shots = glob("$src_dir/v30-*.png");
$out["screenshots"] = [];
foreach ($shots as $s) {
$bn = basename($s);
$d = "$dest_dir/$bn";
@copy($s, $d);
$out["screenshots"][] = "/generated/$bn";
}
$out["shots_count"] = count($out["screenshots"]);
echo json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);

30
api/ambre-git-commit.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
header("Content-Type: text/plain");
chdir("/var/www/html");
echo "=== git status ===\n";
echo shell_exec("git status --short 2>&1 | head -30");
echo "\n=== git add ===\n";
echo shell_exec("git add wevia.html js/wevia-sse-override.js api/ambre-tool-pdf-premium.php api/ambre-llm-semaphore.php api/ambre-session-chat.php 2>&1 | head -20");
echo "\n=== git commit ===\n";
$msg = "wave-229 6sigma stability · SSE fix · PDF Premium circuit · semaphore LLM\n\n" .
"- Fix CRITICAL: /js/wevia-sse-override.js regex /n/g split by literal newline (line 48)\n" .
"- Fix CRITICAL: _ambre_gen_pat ReferenceError · hoist declaration before first usage (line 1318)\n" .
"- Fix: /mermaid/i.test → indexOf (safer, no regex ambiguity)\n" .
"- Fix: new RegExp(finalFileUrl) → split/join (no regex escape needed)\n" .
"- Add: server-side LLM semaphore /api/ambre-llm-semaphore.php (max 5 concurrent)\n" .
"- Add: PDF Premium circuit /api/ambre-tool-pdf-premium.php (12KB, Chart.js + google-chrome)\n" .
"- Add: V9-PDF-PREMIUM router in wevia.html\n" .
"- Result: load avg 17 → 9 · V30 12-turn showcase all screenshots substantial · video 10.36MB";
echo shell_exec("git -c user.email='ambre@weval.com' -c user.name='Ambre WEVIA' commit -m " . escapeshellarg($msg) . " 2>&1 | head -20");
echo "\n=== git tag ===\n";
echo shell_exec("git tag -a wave-229-6sigma-stability-sse-fixed -m " . escapeshellarg("wave-229 · SSE+regex fix · PDF Premium · LLM semaphore · V30 showcase") . " 2>&1");
echo "\n=== push ===\n";
// Use the token credentials (may timeout but will show)
echo shell_exec("timeout 60 git push origin main 2>&1 | tail -5");
echo "\n=== push tag ===\n";
echo shell_exec("timeout 30 git push origin wave-229-6sigma-stability-sse-fixed 2>&1 | tail -5");
echo "\n=== final log ===\n";
echo shell_exec("git log --oneline -5");
echo "\n=== recent tags ===\n";
echo shell_exec("git tag -l 'wave-*' --sort=-creatordate | head -5");

16
api/ambre-list-videos.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
header("Content-Type: application/json");
$dir = "/var/www/html/api/ambre-pw-tests/output";
$vids = [];
foreach (glob("$dir/*/video.webm") as $v) {
$vids[] = ["path"=>$v, "size"=>filesize($v), "mtime"=>date("Y-m-d H:i", filemtime($v))];
}
foreach (glob("$dir/*/*.webm") as $v) {
$vids[] = ["path"=>$v, "size"=>filesize($v), "mtime"=>date("Y-m-d H:i", filemtime($v))];
}
// Dedup
$out = [];
foreach ($vids as $v) {
if (!isset($out[$v["path"]])) $out[$v["path"]] = $v;
}
echo json_encode(array_values($out), JSON_PRETTY_PRINT);

108
api/ambre-mermaid-learn.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
/**
* ambre-mermaid-learn.php · Mermaid schema learning system
* Every mermaid diagram generated is saved with context + tags for reuse
* Uses Qdrant KB + local JSON fallback
*/
header("Content-Type: application/json; charset=utf-8");
$raw = file_get_contents("php://input");
$in = json_decode($raw, true) ?: $_POST;
$action = $in["action"] ?? "list";
$store_file = "/var/www/html/generated/mermaid-learn-kb.json";
if (!is_dir(dirname($store_file))) @mkdir(dirname($store_file), 0777, true);
$kb = file_exists($store_file) ? (json_decode(@file_get_contents($store_file), true) ?: []) : [];
if ($action === "save") {
$topic = trim($in["topic"] ?? "");
$code = trim($in["code"] ?? "");
$kind = $in["kind"] ?? "flowchart"; // flowchart, sequence, gantt, pie, etc.
$context = $in["context"] ?? "";
if (!$topic || !$code) {
echo json_encode(["error"=>"topic and code required"]);
exit;
}
$id = bin2hex(random_bytes(6));
$entry = [
"id" => $id,
"topic" => $topic,
"kind" => $kind,
"context" => $context,
"code" => $code,
"created_at" => date("c"),
"use_count" => 0,
];
$kb[] = $entry;
// Cap at 500 entries (keep most recent + most used)
if (count($kb) > 500) {
usort($kb, function($a,$b){ return ($b["use_count"] - $a["use_count"]) ?: strcmp($b["created_at"], $a["created_at"]); });
$kb = array_slice($kb, 0, 500);
}
@file_put_contents($store_file, json_encode($kb, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE));
echo json_encode(["ok"=>true, "id"=>$id, "total"=>count($kb)]);
exit;
}
if ($action === "search") {
$q = trim($in["query"] ?? "");
if (!$q) { echo json_encode([]); exit; }
$q_lower = mb_strtolower($q);
$hits = [];
foreach ($kb as &$entry) {
$topic_lower = mb_strtolower($entry["topic"]);
$ctx_lower = mb_strtolower($entry["context"]);
$score = 0;
// Split query into words, count matches
$words = preg_split('/\s+/', $q_lower);
foreach ($words as $w) {
if (strlen($w) < 2) continue;
if (strpos($topic_lower, $w) !== false) $score += 2;
if (strpos($ctx_lower, $w) !== false) $score += 1;
}
if ($score > 0) {
$entry["score"] = $score + ($entry["use_count"] * 0.1);
$hits[] = $entry;
}
}
usort($hits, function($a,$b){ return $b["score"] <=> $a["score"]; });
$top = array_slice($hits, 0, 5);
echo json_encode($top, JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === "use") {
$id = $in["id"] ?? "";
foreach ($kb as &$entry) {
if ($entry["id"] === $id) {
$entry["use_count"] = ($entry["use_count"] ?? 0) + 1;
@file_put_contents($store_file, json_encode($kb, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE));
echo json_encode(["ok"=>true, "use_count"=>$entry["use_count"]]);
exit;
}
}
echo json_encode(["error"=>"not found"]);
exit;
}
if ($action === "stats") {
$kinds = [];
$total_uses = 0;
foreach ($kb as $e) {
$k = $e["kind"] ?? "flowchart";
$kinds[$k] = ($kinds[$k] ?? 0) + 1;
$total_uses += ($e["use_count"] ?? 0);
}
echo json_encode([
"total_diagrams" => count($kb),
"by_kind" => $kinds,
"total_uses" => $total_uses,
]);
exit;
}
// default: list all
echo json_encode([
"total" => count($kb),
"items" => array_slice(array_reverse($kb), 0, 20),
], JSON_UNESCAPED_UNICODE);

79
api/ambre-pdf-enh.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
header("Content-Type: application/json");
$path = "/var/www/html/api/ambre-tool-pdf-premium.php";
$c = @file_get_contents($path);
// Enhance the system prompt to suggest best chart type based on topic
$old_sys = '"chart_data\": {\n \"type\": \"bar\",';
$new_sys = '"chart_data\": {\n \"type\": \"bar\", // or \"pie\", \"line\", \"doughnut\", \"radar\", \"polarArea\" selon le sujet',
if (strpos($c, $old_sys) !== false) {
$c = str_replace($old_sys, $new_sys, $c);
}
// Enhance the rendered Chart.js config to handle different types with proper options
$old_js_chart = 'new Chart(ctx, {
type: cd.type || "$chart_type",
data: {
labels: cd.labels || [],
datasets: [{
label: cd.title || "Données",
data: cd.values || [],
backgroundColor: ["#6366f1","#8b5cf6","#3b82f6","#06b6d4","#10b981","#f59e0b","#ef4444","#ec4899"],
borderColor: "#4338ca",
borderWidth: 2,
borderRadius: 6,
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: { legend: { display: false }, title: { display: true, text: cd.title, color: "#334155", font:{size:14}}},
scales: { y: { beginAtZero: true, grid:{color:"#f1f5f9"}}, x: {grid:{display:false}}},
}
});';
$new_js_chart = 'var _chartType = cd.type || "bar";
var _palette = ["#6366f1","#8b5cf6","#3b82f6","#06b6d4","#10b981","#f59e0b","#ef4444","#ec4899","#84cc16","#f97316"];
var _dataset = {
label: cd.title || "Données",
data: cd.values || [],
backgroundColor: (["pie","doughnut","polarArea"].indexOf(_chartType) >= 0) ? _palette : _palette.slice(0, (cd.values||[]).length),
borderColor: (["line","radar"].indexOf(_chartType) >= 0) ? "#6366f1" : "#4338ca",
borderWidth: (["pie","doughnut","polarArea"].indexOf(_chartType) >= 0) ? 2 : (["line","radar"].indexOf(_chartType) >= 0 ? 3 : 2),
borderRadius: (_chartType === "bar") ? 6 : 0,
tension: (_chartType === "line") ? 0.35 : 0,
fill: (_chartType === "line") ? false : true,
pointBackgroundColor: "#6366f1",
pointRadius: (["line","radar"].indexOf(_chartType) >= 0) ? 5 : 0,
};
var _showLegend = (["pie","doughnut","polarArea","radar"].indexOf(_chartType) >= 0);
var _showScales = (["pie","doughnut","polarArea","radar"].indexOf(_chartType) < 0);
new Chart(ctx, {
type: _chartType,
data: { labels: cd.labels || [], datasets: [_dataset] },
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: _showLegend, position: "bottom", labels: {boxWidth: 12, font: {size: 11}}},
title: { display: true, text: cd.title || "Données", color: "#334155", font: {size: 14, weight: "600"}, padding: {top: 4, bottom: 14}}
},
scales: _showScales ? { y: { beginAtZero: true, grid: {color: "#f1f5f9"}}, x: {grid: {display: false}}} : {},
}
});';
if (strpos($c, $old_js_chart) !== false) {
$c = str_replace($old_js_chart, $new_js_chart, $c);
}
// Save
$backup = "/opt/wevads/vault/pdf-premium.GOLD-" . date("Ymd-His") . "-chart-types";
@copy($path, $backup);
$wrote = @file_put_contents($path, $c);
echo json_encode([
"wrote" => $wrote,
"size" => strlen($c),
"backup" => basename($backup),
]);

56
api/ambre-scan-v30.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
header("Content-Type: application/json");
$out = [];
// Recent git activity
chdir("/var/www/html");
$out["git_commits_last_30m"] = array_filter(array_map("trim", explode("\n", @shell_exec("git log --since='30 minutes ago' --oneline 2>&1 | head -20"))));
$out["git_tags_today"] = array_filter(array_map("trim", explode("\n", @shell_exec("git tag -l | while read t; do d=$(git log -1 --format=%at \"$t\" 2>/dev/null); if [ -n \"$d\" ] && [ \"$d\" -gt $(($(date +%s)-86400)) ]; then echo \"$t\"; fi; done 2>&1 | head -20"))));
// Recent ambre-* files
$recent_ambre = array_map("basename", array_filter(glob("/var/www/html/api/ambre-*.php"), function($f){ return filemtime($f) > (time()-3600); }));
$out["ambre_files_last_hour"] = $recent_ambre;
// oss-catalog state
$oss = "/var/www/html/oss-catalog.html";
$out["oss_catalog"] = file_exists($oss) ? [
"size" => filesize($oss),
"mtime" => date("Y-m-d H:i", filemtime($oss)),
"tool_count_preg" => preg_match_all("/data-cat=/", @file_get_contents($oss) ?: ""),
] : "NOT FOUND";
// Wiki/vault doctrines
$out["doctrines"] = array_map("basename", glob("/opt/obsidian-vault/doctrines/*.md") ?: []);
$out["doctrines_count"] = count(glob("/opt/obsidian-vault/doctrines/*.md") ?: []);
// Recent wave-* tags
$out["recent_wave_tags"] = array_filter(array_map("trim", explode("\n", @shell_exec("git tag -l 'wave-*' --sort=-creatordate 2>&1 | head -10"))));
// V30 video + screenshots still live
$out["v30_artifacts"] = [
"video" => glob("/var/www/html/generated/wevia-v30-showcase*.webm"),
"screenshots" => count(glob("/var/www/html/generated/v30-*.png") ?: []),
];
// Mermaid KB
$mkb = "/var/www/html/generated/mermaid-learn-kb.json";
$out["mermaid_kb"] = file_exists($mkb) ? [
"size" => filesize($mkb),
"entries" => count(json_decode(@file_get_contents($mkb), true) ?: []),
] : "NOT FOUND";
// PDF Premium endpoint
$out["pdf_premium"] = file_exists("/var/www/html/api/ambre-tool-pdf-premium.php") ? "LIVE" : "MISSING";
// What's in wevia.html now
$w = @file_get_contents("/var/www/html/wevia.html");
$out["wevia_state"] = [
"size" => strlen($w),
"v5_memory" => strpos($w, "AMBRE-V5-MEMORY") !== false,
"v6_tools" => strpos($w, "AMBRE-V6-TOOLS") !== false,
"v7_premium" => strpos($w, "AMBRE-V7-PREMIUM") !== false,
"v9_pdf_premium" => strpos($w, "AMBRE-V9-PDF-PREMIUM") !== false,
"ambre_gen_pat_hoisted" => strpos($w, "HOISTED") !== false,
];
echo json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);

141
api/ambre-tool-mermaid.php Normal file
View File

@@ -0,0 +1,141 @@
<?php
/**
* ambre-tool-mermaid.php · Mermaid generation with learning KB (RAG-enabled)
* Flow:
* 1. Search KB for similar schema (score > 5)
* 2. If found: reuse + mark used
* 3. Else: LLM generates + auto-save to KB
*/
header("Content-Type: application/json; charset=utf-8");
set_time_limit(60);
require_once __DIR__ . "/ambre-llm-semaphore.php";
$raw = file_get_contents("php://input");
$in = json_decode($raw, true) ?: $_POST;
$topic = trim($in["topic"] ?? $in["message"] ?? "");
if (!$topic) { echo json_encode(["error"=>"topic required"]); exit; }
$t0 = microtime(true);
// Step 1: Search KB
$kb_resp = @file_get_contents("http://127.0.0.1/api/ambre-mermaid-learn.php", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Content-Type: application/json\r\n",
"content" => json_encode(["action"=>"search", "query"=>$topic]),
"timeout" => 5,
],
]));
$kb_hits = @json_decode($kb_resp, true) ?: [];
$reused = null;
if (!empty($kb_hits) && ($kb_hits[0]["score"] ?? 0) >= 5) {
$reused = $kb_hits[0];
// Mark used
@file_get_contents("http://127.0.0.1/api/ambre-mermaid-learn.php", false, stream_context_create([
"http" => ["method"=>"POST","header"=>"Content-Type: application/json\r\n",
"content"=>json_encode(["action"=>"use","id"=>$reused["id"]]),"timeout"=>3]
]));
}
if ($reused) {
echo json_encode([
"ok" => true,
"mermaid_code" => $reused["code"],
"topic" => $reused["topic"],
"kind" => $reused["kind"] ?? "flowchart",
"source" => "kb_reused",
"kb_id" => $reused["id"],
"kb_score" => $reused["score"],
"use_count" => $reused["use_count"] ?? 0,
"elapsed_ms" => round((microtime(true)-$t0)*1000),
"provider" => "WEVIA Mermaid Learning KB",
], JSON_UNESCAPED_UNICODE);
exit;
}
// Step 2: No match → LLM generate
$sys = "Tu es un expert en diagrammes Mermaid. Pour le sujet donné, génère UNIQUEMENT le code Mermaid valide (sans markdown wrapper ```).\n" .
"Règles strictes :\n" .
"- Utiliser UNIQUEMENT des crochets [texte] pour les noeuds, pas de {accolades} ni ((parenthèses))\n" .
"- Pas d'accents (é→e, à→a, etc.)\n" .
"- Pas d'emojis\n" .
"- Max 12 noeuds\n" .
"- Syntaxe : flowchart LR, flowchart TD, sequenceDiagram, gantt, pie, mindmap selon le besoin\n" .
"- Labels courts (< 30 chars)\n" .
"- Arrows : --> ou --|label|-->\n" .
"Réponds STRICTEMENT avec le code Mermaid, rien d'autre.";
$sem_id = AmbreLLMSemaphore::acquire();
if (!$sem_id) {
echo json_encode(["error"=>"service busy"]);
exit;
}
try {
$llm_t = microtime(true);
$llm = @file_get_contents("http://127.0.0.1:4000/v1/chat/completions", false, stream_context_create([
"http" => [
"method"=>"POST",
"header"=>"Content-Type: application/json\r\n",
"content"=>json_encode([
"model"=>"fast",
"messages"=>[["role"=>"system","content"=>$sys],["role"=>"user","content"=>$topic]],
"max_tokens"=>800,
"temperature"=>0.3,
]),
"timeout"=>25,
],
]));
$llm_ms = round((microtime(true)-$llm_t)*1000);
} finally {
AmbreLLMSemaphore::release($sem_id);
}
$d = @json_decode($llm, true);
$code = $d["choices"][0]["message"]["content"] ?? "";
// Sanitize
$code = preg_replace('/^```(?:mermaid)?\s*/m', '', $code);
$code = preg_replace('/\s*```\s*$/m', '', $code);
$code = trim($code);
if (!$code) {
echo json_encode(["error"=>"LLM returned empty code", "llm_ms"=>$llm_ms]);
exit;
}
// Detect kind
$kind = "flowchart";
if (stripos($code, "sequenceDiagram") !== false) $kind = "sequence";
elseif (stripos($code, "gantt") === 0) $kind = "gantt";
elseif (stripos($code, "pie") === 0) $kind = "pie";
elseif (stripos($code, "mindmap") !== false) $kind = "mindmap";
elseif (stripos($code, "classDiagram") !== false) $kind = "class";
elseif (stripos($code, "erDiagram") !== false) $kind = "er";
// Step 3: Save to KB
$save_resp = @file_get_contents("http://127.0.0.1/api/ambre-mermaid-learn.php", false, stream_context_create([
"http" => ["method"=>"POST","header"=>"Content-Type: application/json\r\n",
"content"=>json_encode([
"action"=>"save", "topic"=>$topic, "kind"=>$kind,
"context"=>"Auto-generated from user query",
"code"=>$code,
]),
"timeout"=>5]
]));
$saved = @json_decode($save_resp, true);
echo json_encode([
"ok" => true,
"mermaid_code" => $code,
"topic" => $topic,
"kind" => $kind,
"source" => "llm_generated_saved",
"kb_id" => $saved["id"] ?? null,
"kb_total" => $saved["total"] ?? null,
"llm_ms" => $llm_ms,
"elapsed_ms" => round((microtime(true)-$t0)*1000),
"provider" => "WEVIA Mermaid + KB Learning",
], JSON_UNESCAPED_UNICODE);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"generated_at": "2026-04-22T03:00:02.461157",
"generated_at": "2026-04-22T03:05:02.210565",
"stats": {
"total": 48,
"pending": 31,

View File

@@ -0,0 +1,286 @@
{
"ts": "2026-04-22T01:05:02+00:00",
"server": "s204",
"s204": {
"load": 7.46,
"uptime": "2026-04-14 11:51:24",
"ram_total_mb": 31335,
"ram_used_mb": 13291,
"ram_free_mb": 18043,
"disk_total": "150G",
"disk_used": "122G",
"disk_free": "22G",
"disk_pct": "85%",
"fpm_workers": 140,
"docker_containers": 19,
"cpu_cores": 8
},
"s95": {
"load": 1.45,
"disk_pct": "82%",
"status": "UP",
"ram_total_mb": 15610,
"ram_free_mb": 11944
},
"pmta": [
{
"name": "SER6",
"ip": "110.239.84.121",
"status": "DOWN"
},
{
"name": "SER7",
"ip": "110.239.65.64",
"status": "DOWN"
},
{
"name": "SER8",
"ip": "182.160.55.107",
"status": "DOWN"
},
{
"name": "SER9",
"ip": "110.239.86.68",
"status": "DOWN"
}
],
"assets": {
"html_pages": 323,
"php_apis": 961,
"wiki_entries": 2123,
"vault_doctrines": 97,
"vault_sessions": 104,
"vault_decisions": 12
},
"tools": {
"total": 638,
"registry_version": "?"
},
"sovereign": {
"status": "UP",
"providers": [
"Cerebras-fast",
"Cerebras-think",
"Groq",
"Cloudflare-AI",
"Gemini",
"SambaNova",
"NVIDIA-NIM",
"Mistral",
"Groq-OSS",
"HF-Space",
"HF-Router",
"OpenRouter",
"GitHub-Models"
],
"active": 13,
"total": 13,
"primary": "Cerebras-fast",
"cost": "0€"
},
"ethica": {
"total_hcps": 161733,
"with_email": 110660,
"with_phone": 155151,
"gap_email": 51073,
"pct_email": 68.4,
"pct_phone": 95.9,
"by_country": [
{
"country": "DZ",
"hcps": 122337,
"with_email": 78549,
"with_tel": 119396,
"pct_email": 64.2,
"pct_tel": 97.6
},
{
"country": "MA",
"hcps": 19723,
"with_email": 15081,
"with_tel": 18737,
"pct_email": 76.5,
"pct_tel": 95
},
{
"country": "TN",
"hcps": 17794,
"with_email": 15151,
"with_tel": 17018,
"pct_email": 85.1,
"pct_tel": 95.6
},
{
"country": "INTL",
"hcps": 1879,
"with_email": 1879,
"with_tel": 0,
"pct_email": 100,
"pct_tel": 0
}
]
},
"docker": [
{
"name": "weval-docuseal",
"status": "Up 10 seconds",
"ports": ""
},
{
"name": "loki",
"status": "Up 5 days",
"ports": ""
},
{
"name": "listmonk",
"status": "Up 5 days",
"ports": ""
},
{
"name": "plausible-plausible-1",
"status": "Up 4 days",
"ports": ""
},
{
"name": "plausible-plausible-db-1",
"status": "Up 4 days",
"ports": ""
},
{
"name": "plausible-plausible-events-db-1",
"status": "Up 4 days",
"ports": ""
},
{
"name": "n8n-docker-n8n-1",
"status": "Up 5 days",
"ports": ""
},
{
"name": "mattermost-docker-mm-db-1",
"status": "Up 5 days",
"ports": ""
},
{
"name": "mattermost-docker-mattermost-1",
"status": "Up 5 days (healthy)",
"ports": ""
},
{
"name": "twenty",
"status": "Up 5 days",
"ports": ""
},
{
"name": "twenty-redis",
"status": "Up 5 days",
"ports": ""
},
{
"name": "langfuse",
"status": "Up 6 days",
"ports": ""
},
{
"name": "redis-weval",
"status": "Up 7 days",
"ports": ""
},
{
"name": "gitea",
"status": "Up 7 days",
"ports": ""
},
{
"name": "node-exporter",
"status": "Up 7 days",
"ports": ""
},
{
"name": "prometheus",
"status": "Up 7 days",
"ports": ""
},
{
"name": "searxng",
"status": "Up 7 days",
"ports": ""
},
{
"name": "uptime-kuma",
"status": "Up 2 days (healthy)",
"ports": ""
},
{
"name": "vaultwarden",
"status": "Up 7 days (healthy)",
"ports": ""
},
{
"name": "qdrant",
"status": "Up 7 days",
"ports": ""
}
],
"crons": {
"active": 35
},
"git": {
"head": "39904106c AUTO-BACKUP 20260422-0305",
"dirty": 1,
"status": "DIRTY"
},
"nonreg": {
"total": 153,
"passed": 153,
"score": "100%"
},
"services": [
{
"name": "DeerFlow",
"port": 3002,
"status": "UP"
},
{
"name": "DeerFlow API",
"port": 8001,
"status": "UP"
},
{
"name": "Qdrant",
"port": 6333,
"status": "UP"
},
{
"name": "Ollama",
"port": 11434,
"status": "UP"
},
{
"name": "Redis",
"port": 6379,
"status": "UP"
},
{
"name": "Sovereign",
"port": 4000,
"status": "UP"
},
{
"name": "SearXNG",
"port": 8080,
"status": "UP"
}
],
"whisper": {
"binary": "COMPILED",
"model": "142MB"
},
"grand_total": 4162,
"health": {
"score": 4,
"max": 6,
"pct": 67
},
"elapsed_ms": 11977
}

View File

@@ -1,28 +1,28 @@
{
"version": "1.0",
"scanned_at": "2026-04-16T16:59:03.806435",
"scanned_at": "2026-04-22T03:03:01.674431",
"server": "S95",
"infra": {
"disk": {
"total": "150G",
"used": "119G",
"avail": "26G",
"pct": "83%"
"used": "122G",
"avail": "23G",
"pct": "85%"
},
"memory": {
"total": "30Gi",
"used": "8.0Gi",
"free": "3.3Gi"
"used": "12Gi",
"free": "459Mi"
},
"ports_count": 42,
"ports_count": 43,
"docker_count": 19,
"nginx_sites": 14,
"html_pages": 505,
"api_files": 478,
"crons": 3,
"html_pages": 718,
"api_files": 3152,
"crons": 35,
"tool_registry": {
"count": 421,
"version": "7.4"
"count": 638,
"version": "?"
}
},
"assets": [
@@ -176,7 +176,7 @@
"type": "api",
"status": "live",
"port": 8001,
"process": "uvicorn",
"process": "python",
"maturity": 50,
"source": "port_scan"
},
@@ -195,7 +195,7 @@
"name": "loki",
"type": "docker",
"status": "up",
"docker_status": "Up 3 hours",
"docker_status": "Up 5 days",
"source": "docker"
},
{
@@ -203,7 +203,7 @@
"name": "listmonk",
"type": "docker",
"status": "up",
"docker_status": "Up 6 hours",
"docker_status": "Up 5 days",
"source": "docker"
},
{
@@ -211,7 +211,7 @@
"name": "plausible-plausible-1",
"type": "docker",
"status": "up",
"docker_status": "Up 6 hours",
"docker_status": "Up 4 days",
"source": "docker"
},
{
@@ -219,7 +219,7 @@
"name": "plausible-plausible-db-1",
"type": "docker",
"status": "up",
"docker_status": "Up 6 hours",
"docker_status": "Up 4 days",
"source": "docker"
},
{
@@ -227,7 +227,7 @@
"name": "plausible-plausible-events-db-1",
"type": "docker",
"status": "up",
"docker_status": "Up 6 hours",
"docker_status": "Up 4 days",
"source": "docker"
},
{
@@ -235,7 +235,7 @@
"name": "n8n-docker-n8n-1",
"type": "docker",
"status": "up",
"docker_status": "Up 11 hours",
"docker_status": "Up 5 days",
"source": "docker"
},
{
@@ -243,7 +243,7 @@
"name": "mattermost-docker-mm-db-1",
"type": "docker",
"status": "up",
"docker_status": "Up 11 hours",
"docker_status": "Up 5 days",
"source": "docker"
},
{
@@ -251,7 +251,7 @@
"name": "mattermost-docker-mattermost-1",
"type": "docker",
"status": "up",
"docker_status": "Up 11 hours (healthy)",
"docker_status": "Up 5 days (healthy)",
"source": "docker"
},
{
@@ -259,7 +259,7 @@
"name": "twenty",
"type": "docker",
"status": "up",
"docker_status": "Up 5 hours",
"docker_status": "Up 5 days",
"source": "docker"
},
{
@@ -267,7 +267,7 @@
"name": "twenty-redis",
"type": "docker",
"status": "up",
"docker_status": "Up 12 hours",
"docker_status": "Up 5 days",
"source": "docker"
},
{
@@ -275,7 +275,7 @@
"name": "redis-weval",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days",
"docker_status": "Up 7 days",
"source": "docker"
},
{
@@ -283,7 +283,7 @@
"name": "gitea",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days",
"docker_status": "Up 7 days",
"source": "docker"
},
{
@@ -291,7 +291,7 @@
"name": "node-exporter",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days",
"docker_status": "Up 7 days",
"source": "docker"
},
{
@@ -299,7 +299,7 @@
"name": "prometheus",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days",
"docker_status": "Up 7 days",
"source": "docker"
},
{
@@ -307,7 +307,7 @@
"name": "searxng",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days",
"docker_status": "Up 7 days",
"source": "docker"
},
{
@@ -323,7 +323,7 @@
"name": "vaultwarden",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days (healthy)",
"docker_status": "Up 7 days (healthy)",
"source": "docker"
},
{
@@ -331,7 +331,7 @@
"name": "qdrant",
"type": "docker",
"status": "up",
"docker_status": "Up 2 days",
"docker_status": "Up 7 days",
"source": "docker"
},
{

View File

@@ -1,5 +1,5 @@
<?php
// WAVE 231 v4 · Social Signals Hub · YouTube (HN filter) + Twitter (Nitter) + Mastodon + Paperclip tasks
// WAVE 232 v5 · Twitter snscrape OSS + 5 Mastodon instances + task PATCH + SSE
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
set_time_limit(25);
@@ -9,8 +9,7 @@ function load_secrets() {
if (!is_readable('/etc/weval/secrets.env')) return $s;
foreach (file('/etc/weval/secrets.env', FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $l) {
if (empty(trim($l))||$l[0]==='#') continue;
$p = strpos($l,'=');
if ($p) $s[trim(substr($l,0,$p))] = trim(substr($l,$p+1)," \t\"'");
$p = strpos($l,'='); if ($p) $s[trim(substr($l,0,$p))] = trim(substr($l,$p+1)," \t\"'");
}
return $s;
}
@@ -27,70 +26,111 @@ function multi_fetch($urls, $timeout=7) {
$running = null;
do { curl_multi_exec($mh, $running); curl_multi_select($mh, 0.1); } while ($running > 0);
$out = [];
foreach ($handles as $k => $ch) {
$out[$k] = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
foreach ($handles as $k => $ch) { $out[$k] = curl_multi_getcontent($ch); curl_multi_remove_handle($mh, $ch); curl_close($ch); }
curl_multi_close($mh);
return $out;
}
// === POST endpoint: auto-create Paperclip task from LLM idea ===
// === POST create_task ===
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'create_task') {
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$pg = @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=3');
if (!$pg) { http_response_code(500); echo json_encode(['error'=>'no pg']); exit; }
$q = "INSERT INTO weval_tasks (title, source, source_ref, category, opportunity, tools_used, first_steps, kpi, estimated_mad, inspired_by, status, wave) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) RETURNING id, created_at";
$r = @pg_query_params($pg, $q, [
$body['title'] ?? '?',
$body['source'] ?? 'advisor-wave231',
$body['source_ref'] ?? '',
$body['category'] ?? 'conversion',
$body['opportunity'] ?? '',
is_array($body['tools_used']??null) ? implode('|', $body['tools_used']) : ($body['tools_used'] ?? ''),
is_array($body['first_steps']??null) ? implode("\n- ", $body['first_steps']) : ($body['first_steps'] ?? ''),
$body['kpi'] ?? '',
(int)($body['estimated_mad'] ?? 0),
$body['inspired_by'] ?? '',
'proposed',
231
$body['title']??'?', $body['source']??'advisor-wave232', $body['source_ref']??'',
$body['category']??'conversion', $body['opportunity']??'',
is_array($body['tools_used']??null)?implode('|',$body['tools_used']):($body['tools_used']??''),
is_array($body['first_steps']??null)?implode("\n- ",$body['first_steps']):($body['first_steps']??''),
$body['kpi']??'', (int)($body['estimated_mad']??0), $body['inspired_by']??'', 'proposed', 232
]);
if ($r) {
$row = pg_fetch_assoc($r);
pg_close($pg);
echo json_encode(['ok'=>true, 'task_id'=>(int)$row['id'], 'created_at'=>$row['created_at']]);
} else {
pg_close($pg);
http_response_code(500);
echo json_encode(['error'=>'insert failed', 'details'=>pg_last_error()]);
}
if ($r) { $row = pg_fetch_assoc($r); pg_close($pg); echo json_encode(['ok'=>true, 'task_id'=>(int)$row['id'], 'created_at'=>$row['created_at']]); }
else { pg_close($pg); http_response_code(500); echo json_encode(['error'=>'insert failed', 'details'=>pg_last_error()]); }
exit;
}
// === GET endpoint: list existing tasks ===
// === PATCH update_task_status ===
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'update_status') {
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$task_id = (int)($body['task_id'] ?? 0);
$new_status = $body['status'] ?? '';
$allowed = ['proposed', 'in_progress', 'done', 'cancelled', 'blocked'];
if (!$task_id || !in_array($new_status, $allowed)) {
http_response_code(400); echo json_encode(['error'=>'invalid', 'allowed'=>$allowed]); exit;
}
$pg = @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=3');
if (!$pg) { http_response_code(500); echo json_encode(['error'=>'no pg']); exit; }
$r = @pg_query_params($pg, 'UPDATE weval_tasks SET status=$1 WHERE id=$2 RETURNING id, status', [$new_status, $task_id]);
if ($r && ($row = pg_fetch_assoc($r))) { pg_close($pg); echo json_encode(['ok'=>true, 'task_id'=>(int)$row['id'], 'new_status'=>$row['status']]); }
else { pg_close($pg); http_response_code(404); echo json_encode(['error'=>'task not found']); }
exit;
}
// === GET list_tasks ===
if (($_GET['action'] ?? '') === 'list_tasks') {
$pg = @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=3');
if (!$pg) { echo json_encode(['error'=>'no pg', 'tasks'=>[]]); exit; }
$r = @pg_query($pg, 'SELECT * FROM weval_tasks ORDER BY created_at DESC LIMIT 20');
$tasks = [];
if ($r) while ($row = pg_fetch_assoc($r)) $tasks[] = $row;
$tasks = []; if ($r) while ($row = pg_fetch_assoc($r)) $tasks[] = $row;
$agg = []; foreach ($tasks as $t) { $s = $t['status']??'?'; $agg[$s] = ($agg[$s]??0)+1; }
pg_close($pg);
echo json_encode(['ok'=>true, 'count'=>count($tasks), 'tasks'=>$tasks]);
echo json_encode(['ok'=>true, 'count'=>count($tasks), 'by_status'=>$agg, 'tasks'=>$tasks]);
exit;
}
// === Default: aggregation ===
// === SSE streaming endpoint ===
if (($_GET['action'] ?? '') === 'stream') {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
@ob_end_flush();
$send = function($event, $data) {
echo "event: $event\n";
echo "data: " . json_encode($data) . "\n\n";
@ob_flush(); flush();
};
$send('hello', ['wave'=>232, 'msg'=>'SSE social stream live', 'ts'=>date('c')]);
// Stream channels one by one
$channels = [
'linkedin' => 'http://127.0.0.1/api/linkedin-posts.php',
'hackernews' => 'https://hn.algolia.com/api/v1/search?query=' . urlencode('SaaS conversion') . '&tags=story&hitsPerPage=5',
'reddit' => 'https://old.reddit.com/r/SaaS/.rss?limit=5',
];
foreach ($channels as $name => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_USERAGENT=>'weval-bot']);
$raw = curl_exec($ch);
curl_close($ch);
$count = 0; $top = '';
if ($name === 'linkedin') { $d = @json_decode($raw, true); if (isset($d['posts'])) { $count = count($d['posts']); $top = $d['posts'][0]['title'] ?? ''; } }
elseif ($name === 'hackernews') { $d = @json_decode($raw, true); if (isset($d['hits'])) { $count = count($d['hits']); $top = $d['hits'][0]['title'] ?? ''; } }
elseif ($name === 'reddit') { $xml = @simplexml_load_string($raw); if ($xml) { $entries = $xml->entry??[]; $count = count($entries); $top = $count ? (string)$entries[0]->title : ''; } }
$send('channel', ['name'=>$name, 'count'=>$count, 'top'=>$top, 'ts'=>date('c')]);
}
// Tasks count
$pg = @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=3');
if ($pg) {
$r = @pg_query($pg, 'SELECT status, COUNT(*) AS n FROM weval_tasks GROUP BY status');
$agg = []; if ($r) while ($row = pg_fetch_assoc($r)) $agg[$row['status']] = (int)$row['n'];
pg_close($pg);
$send('tasks', ['by_status'=>$agg, 'ts'=>date('c')]);
}
$send('done', ['total_channels'=>count($channels), 'ts'=>date('c')]);
exit;
}
// === Default: aggregation with snscrape Twitter + 5 Mastodon ===
$topics = $_GET['topics'] ?? 'B2B SaaS conversion,LinkedIn outbound,pharma digital';
$topic_list = array_slice(array_map('trim', explode(',', $topics)), 0, 3);
$with_scout = ($_GET['scout'] ?? '') === '1';
$with_twitter = ($_GET['twitter'] ?? '1') === '1'; // default ON
$signals = [
'ts' => date('c'), 'wave' => 231, 'version' => 'social-signals-hub-v4',
'ts' => date('c'), 'wave' => 232, 'version' => 'social-signals-hub-v5',
'topics' => $topic_list, 'channels' => [], 'aggregated_ideas' => [],
];
// Parallel URLs
// Parallel fetch
$urls = [];
$urls['linkedin'] = 'http://127.0.0.1/api/linkedin-posts.php';
foreach (['SaaS conversion', 'B2B sales outbound'] as $i => $q) {
@@ -99,15 +139,11 @@ foreach (['SaaS conversion', 'B2B sales outbound'] as $i => $q) {
foreach (['SaaS', 'Entrepreneur', 'B2BSales'] as $i => $s) {
$urls['rd_'.$i] = 'https://old.reddit.com/r/' . $s . '/.rss?limit=5';
}
// YouTube via HN filtered (HN stories that link to youtube.com)
$urls['hn_yt'] = 'https://hn.algolia.com/api/v1/search?query=' . urlencode('youtube.com SaaS') . '&tags=story&hitsPerPage=10';
// Twitter via Nitter.net
foreach (array_slice($topic_list, 0, 2) as $i => $t) {
$urls['tw_'.$i] = 'https://nitter.net/search?q=' . urlencode($t) . '&f=tweets';
}
// Mastodon public search
foreach (array_slice($topic_list, 0, 2) as $i => $t) {
$urls['ma_'.$i] = 'https://mastodon.social/api/v2/search?q=' . urlencode($t) . '&type=statuses&limit=5';
// Mastodon 5 instances
$mast_hosts = ['mastodon.social', 'mstdn.social', 'fosstodon.org', 'hachyderm.io', 'techhub.social'];
foreach ($mast_hosts as $i => $h) {
$urls['ma_'.$i] = 'https://' . $h . '/api/v2/search?q=' . urlencode($topic_list[0] ?? 'SaaS') . '&type=statuses&limit=3';
}
$t0 = microtime(true);
@@ -118,17 +154,8 @@ $signals['fetch_duration_s'] = round(microtime(true) - $t0, 2);
$ln = ['channel'=>'linkedin','source'=>'internal-db','items'=>[]];
if (!empty($results['linkedin'])) {
$ld = @json_decode($results['linkedin'], true);
if (isset($ld['posts'])) {
foreach (array_slice($ld['posts'], 0, 8) as $p) {
$ln['items'][] = [
'title' => $p['title'] ?? '',
'excerpt' => substr($p['excerpt'] ?? '', 0, 150),
'likes' => (int)($p['likes'] ?? 0),
'views' => (int)($p['views'] ?? 0),
'url' => $p['linkedin_url'] ?? '',
'date' => $p['post_date'] ?? '',
];
}
if (isset($ld['posts'])) foreach (array_slice($ld['posts'], 0, 8) as $p) {
$ln['items'][] = ['title'=>$p['title']??'','excerpt'=>substr($p['excerpt']??'',0,150),'likes'=>(int)($p['likes']??0),'views'=>(int)($p['views']??0),'url'=>$p['linkedin_url']??'','date'=>$p['post_date']??''];
}
}
$ln['count'] = count($ln['items']);
@@ -140,13 +167,7 @@ foreach ([0,1] as $i) {
if (empty($results['hn_'.$i])) continue;
$hd = @json_decode($results['hn_'.$i], true);
foreach (($hd['hits'] ?? []) as $h) {
$hn['items'][] = [
'title' => substr($h['title'] ?? '', 0, 140),
'points' => (int)($h['points'] ?? 0),
'comments' => (int)($h['num_comments'] ?? 0),
'url' => $h['url'] ?? ('https://news.ycombinator.com/item?id=' . ($h['objectID'] ?? '')),
'date' => substr($h['created_at'] ?? '', 0, 10),
];
$hn['items'][] = ['title'=>substr($h['title']??'',0,140),'points'=>(int)($h['points']??0),'comments'=>(int)($h['num_comments']??0),'url'=>$h['url']??('https://news.ycombinator.com/item?id='.($h['objectID']??'')),'date'=>substr($h['created_at']??'',0,10)];
}
}
usort($hn['items'], function($a,$b){return ($b['points']??0)-($a['points']??0);});
@@ -162,36 +183,21 @@ foreach ([0,1,2] as $i) {
if (!$xml) continue;
$sub = ['SaaS','Entrepreneur','B2BSales'][$i];
foreach ($xml->entry ?? [] as $entry) {
$title = (string)$entry->title;
$link = (string)$entry->link['href'];
if ($title && $link) {
$rd['items'][] = [
'title' => substr($title, 0, 140),
'subreddit' => 'r/' . $sub,
'url' => $link,
'date' => substr((string)$entry->updated, 0, 10),
];
}
$rd['items'][] = ['title'=>substr((string)$entry->title,0,140),'subreddit'=>'r/'.$sub,'url'=>(string)$entry->link['href'],'date'=>substr((string)$entry->updated,0,10)];
}
}
$rd['items'] = array_slice($rd['items'], 0, 15);
$rd['count'] = count($rd['items']);
$signals['channels']['reddit'] = $rd;
// YouTube via HN filter (HN stories linking to youtube.com)
// YouTube via HN-filtered
$yt = ['channel'=>'youtube','source'=>'HackerNews YT-filtered','items'=>[]];
if (!empty($results['hn_yt'])) {
$hd = @json_decode($results['hn_yt'], true);
foreach (($hd['hits'] ?? []) as $h) {
$url = $h['url'] ?? '';
if (strpos($url, 'youtube.com') !== false || strpos($url, 'youtu.be') !== false) {
$yt['items'][] = [
'title' => substr($h['title'] ?? '', 0, 140),
'url' => $url,
'points' => (int)($h['points'] ?? 0),
'hn_discussion' => 'https://news.ycombinator.com/item?id=' . ($h['objectID'] ?? ''),
'date' => substr($h['created_at'] ?? '', 0, 10),
];
$yt['items'][] = ['title'=>substr($h['title']??'',0,140),'url'=>$url,'points'=>(int)($h['points']??0),'hn_discussion'=>'https://news.ycombinator.com/item?id='.($h['objectID']??''),'date'=>substr($h['created_at']??'',0,10)];
}
}
}
@@ -200,37 +206,35 @@ $yt['items'] = array_slice($yt['items'], 0, 8);
$yt['count'] = count($yt['items']);
$signals['channels']['youtube'] = $yt;
// Twitter via Nitter.net
$tw = ['channel'=>'twitter','source'=>'nitter.net','items'=>[]];
foreach ([0,1] as $i) {
if (empty($results['tw_'.$i])) continue;
$html = $results['tw_'.$i];
// Extract tweets: <div class="tweet-content media-body">...</div>
preg_match_all('~<div class="tweet-content[^"]*"[^>]*>(.*?)</div>~s', $html, $contents, PREG_SET_ORDER);
preg_match_all('~<a class="tweet-link"[^>]*href="([^"]+)"~', $html, $links, PREG_SET_ORDER);
preg_match_all('~<a class="username"[^>]*>([^<]+)</a>~', $html, $users, PREG_SET_ORDER);
$topic = $topic_list[$i] ?? '';
for ($j = 0; $j < min(5, count($contents), count($links)); $j++) {
$text = trim(strip_tags(html_entity_decode($contents[$j][1] ?? '')));
$link = $links[$j][1] ?? '';
$user = trim($users[$j][1] ?? '');
if (strlen($text) > 20) {
// Twitter via snscrape OSS
$tw = ['channel'=>'twitter','source'=>'snscrape (OSS)','items'=>[]];
if ($with_twitter) {
$tw_query = $topic_list[0] ?? 'SaaS';
$tw_cmd = '/opt/oss/pandas-ai/venv/bin/snscrape --jsonl --max-results 6 twitter-search ' . escapeshellarg($tw_query) . ' 2>/dev/null';
$tw_raw = @shell_exec('timeout 8 ' . $tw_cmd);
if ($tw_raw) {
$lines = explode("\n", trim($tw_raw));
foreach ($lines as $line) {
$l = @json_decode($line, true);
if (!$l) continue;
$tw['items'][] = [
'title' => substr($text, 0, 180),
'user' => $user,
'url' => 'https://twitter.com' . str_replace('#m', '', $link),
'topic' => $topic,
'title' => substr($l['rawContent'] ?? $l['content'] ?? '', 0, 180),
'user' => '@' . ($l['user']['username'] ?? '?'),
'url' => $l['url'] ?? '',
'likes' => (int)($l['likeCount'] ?? 0),
'retweets' => (int)($l['retweetCount'] ?? 0),
'date' => substr($l['date'] ?? '', 0, 10),
];
}
}
}
$tw['items'] = array_slice($tw['items'], 0, 10);
$tw['items'] = array_slice($tw['items'], 0, 8);
$tw['count'] = count($tw['items']);
$signals['channels']['twitter'] = $tw;
// Mastodon
$ma = ['channel'=>'mastodon','source'=>'mastodon.social API','items'=>[]];
foreach ([0,1] as $i) {
// Mastodon 5 instances merged
$ma = ['channel'=>'mastodon','source'=>'5 instances (social/mstdn/fosstodon/hachyderm/techhub)','items'=>[]];
foreach (range(0,4) as $i) {
if (empty($results['ma_'.$i])) continue;
$md = @json_decode($results['ma_'.$i], true);
foreach (($md['statuses'] ?? []) as $s) {
@@ -240,19 +244,20 @@ foreach ([0,1] as $i) {
'title' => substr($content, 0, 180),
'url' => $s['url'] ?? '',
'user' => '@' . ($s['account']['acct'] ?? '?'),
'instance' => $mast_hosts[$i] ?? '?',
'favorites' => (int)($s['favourites_count'] ?? 0),
'reblogs' => (int)($s['reblogs_count'] ?? 0),
'topic' => $topic_list[$i] ?? '',
'date' => substr($s['created_at'] ?? '', 0, 10),
];
}
}
}
usort($ma['items'], function($a,$b){return ($b['favorites']??0)-($a['favorites']??0);});
$ma['items'] = array_slice($ma['items'], 0, 8);
$ma['items'] = array_slice($ma['items'], 0, 10);
$ma['count'] = count($ma['items']);
$signals['channels']['mastodon'] = $ma;
// Dark Scout async (only if explicit ?scout=1)
// Dark Scout async
if ($with_scout) {
$ds_ch = curl_init('http://127.0.0.1/api/v83-dark-scout-enriched.php?q=' . urlencode($topic_list[0] ?? 'SaaS'));
curl_setopt_array($ds_ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>12]);
@@ -269,7 +274,6 @@ if ($with_scout) {
$signals['channels']['dark_scout'] = $sc;
}
// Aggregate
$all = [];
foreach ($signals['channels'] as $c) foreach ($c['items'] as $i) if (!empty($i['title'])) $all[] = $i['title'];
$signals['aggregated_ideas'] = array_slice(array_unique($all), 0, 30);
@@ -278,14 +282,11 @@ $signals['total_items'] = array_sum(array_map(function($c){return $c['count']??0
// LLM cascade
if (($_GET['llm'] ?? '') === '1') {
$secrets = load_secrets();
$weval_ctx = "WEVAL Consulting (Casablanca/Paris · SAP Ecosystem Partner).\nLive: 48 leads Paperclip, Vistex MQL 95 (450K MAD), Ethica MQL 100 (200K MAD signing), Huawei MQL 90.\nProducts: SAP consulting, API HCP Maghreb 157K HCPs, Pharma Cloud, WEVAL SaaS Freemium.\nSovereign tools: WEVIA Master (269 tools), Dark Scout, WePredict, WEVADS Brain (9 winners), Blade AI, DocuSeal live 3050, pandasai+Ollama, WeasyPrint.\nPipeline 2.9M MAD.";
$weval_ctx = "WEVAL Consulting (Casablanca/Paris · SAP Ecosystem Partner).\nLive: 48 leads Paperclip, Vistex MQL 95 (450K MAD), Ethica MQL 100 (200K MAD), Huawei MQL 90.\nProducts: SAP, API HCP Maghreb 157K HCPs, Pharma Cloud, WEVAL SaaS Freemium.\nTools: WEVIA Master 269 tools, Dark Scout, WePredict, WEVADS Brain 9 winners, Blade AI, DocuSeal 3050, pandasai+Ollama.\nPipeline 2.9M MAD.";
$summary = "";
foreach ($signals['channels'] as $k => $c) {
$top = $c['items'][0]['title'] ?? '(none)';
$summary .= "- $k ({$c['count']}): $top\n";
}
foreach ($signals['channels'] as $k => $c) { $summary .= "- $k ({$c['count']}): ".substr($c['items'][0]['title']??'(none)',0,60)."\n"; }
$headlines = implode("\n - ", array_slice($signals['aggregated_ideas'], 0, 15));
$prompt = "$weval_ctx\n\nSignals from LinkedIn + HN + Reddit + YouTube + Twitter + Mastodon:\n$summary\nTop headlines:\n - $headlines\n\nProvide 5 CONCRETE conversion ideas ADAPTED to WEVAL sovereign stack + MENA. Each must:\n1. Target one real opportunity\n2. Use only existing WEVAL tools\n3. Executable in 14 days\n4. Measurable KPI + estimated MAD revenue\n5. Cite the social signal that inspired it\n\nReply ONLY JSON: {ideas:[{rank:N, title:str, channel:str, opportunity:str, tools_used:[str], first_steps:[str,str,str], kpi:str, estimated_mad:N, inspired_by:str}]}";
$prompt = "$weval_ctx\n\nSignals from 7 channels:\n$summary\nTop headlines:\n - $headlines\n\nProvide 5 CONCRETE conversion ideas for WEVAL MENA market. Each: opp, tools, 14d exec, KPI, MAD est, inspired_by.\nJSON: {ideas:[{rank:N, title:str, channel:str, opportunity:str, tools_used:[str], first_steps:[str,str,str], kpi:str, estimated_mad:N, inspired_by:str}]}";
$payload = json_encode(['model'=>'llama-3.3-70b','messages'=>[['role'=>'user','content'=>$prompt]],'max_tokens'=>2200,'temperature'=>0.4]);
$provs = [
['url'=>'https://api.cerebras.ai/v1/chat/completions','key'=>$secrets['CEREBRAS_API_KEY']??'','name'=>'Cerebras'],
@@ -296,9 +297,7 @@ if (($_GET['llm'] ?? '') === '1') {
if (empty($p['key'])) continue;
$pp = isset($p['override']) ? preg_replace('/"model":"[^"]+"/','"model":"'.$p['override'].'"',$payload,1) : $payload;
$ch = curl_init($p['url']);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>20,
CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.$p['key']],
CURLOPT_POSTFIELDS=>$pp]);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>20, CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.$p['key']], CURLOPT_POSTFIELDS=>$pp]);
$r = curl_exec($ch); $c = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($c === 200) {
$d = json_decode($r, true);

View File

@@ -1,5 +1,5 @@
{
"timestamp": "2026-04-22T02:30:15",
"timestamp": "2026-04-22T03:00:24",
"features": {
"total": 36,
"pass": 35
@@ -13,7 +13,7 @@
"score": 97.2,
"log": [
"=== UX AGENT v1.0 ===",
"Time: 2026-04-22 02:30:02",
"Time: 2026-04-22 03:00:01",
" core: 4/4",
" layout: 3/4",
" interaction: 6/6",

View File

@@ -1,12 +1,12 @@
{
"ok": true,
"version": "V83-business-kpi",
"ts": "2026-04-22T00:59:41+00:00",
"ts": "2026-04-22T01:08:05+00:00",
"summary": {
"total_categories": 8,
"total_kpis": 64,
"ok": 64,
"warn": 0,
"ok": 62,
"warn": 2,
"fail": 0,
"wire_needed": 0,
"data_completeness_pct": 100

View File

@@ -0,0 +1,56 @@
[
{
"id": "b319b6c6772a",
"topic": "parcours client retail omnicanal",
"kind": "flowchart",
"context": "Customer journey retail e-commerce physical store loyalty",
"code": "flowchart LR\n A[Découverte] --> B[Recherche]\n B --> C[App Mobile]\n C --> D[Click & Collect]\n D --> E[Magasin]\n E --> F[Fidélité]",
"created_at": "2026-04-22T01:06:12+00:00",
"use_count": 1
},
{
"id": "39559de03fd9",
"topic": "architecture IA souveraine WEVIA",
"kind": "flowchart",
"context": "Architecture cascade LLM multi-provider sovereign",
"code": "flowchart TD\n U[Utilisateur] --> R[Routeur]\n R --> C[Cerebras]\n R --> G[Groq]\n R --> S[SambaNova]\n C --> O[Orchestrateur]\n G --> O\n S --> O\n O --> U",
"created_at": "2026-04-22T01:06:12+00:00",
"use_count": 0
},
{
"id": "bf87b2067bbd",
"topic": "pipeline CI\/CD devops",
"kind": "flowchart",
"context": "DevOps pipeline deployment continuous integration",
"code": "flowchart LR\n D[Dev] --> G[Git]\n G --> B[Build]\n B --> T[Tests]\n T --> S[Staging]\n S --> P[Prod]",
"created_at": "2026-04-22T01:06:13+00:00",
"use_count": 0
},
{
"id": "a123bad6be8b",
"topic": "cycle de vie client SaaS",
"kind": "flowchart",
"context": "Customer lifecycle onboarding retention churn",
"code": "flowchart LR\n A[Acquisition] --> O[Onboarding]\n O --> E[Engagement]\n E --> R[Rétention]\n R --> U[Upsell]\n U --> E",
"created_at": "2026-04-22T01:06:13+00:00",
"use_count": 0
},
{
"id": "39c5e4cd7dd7",
"topic": "analyse SWOT entreprise",
"kind": "quadrant",
"context": "Strategic analysis SWOT matrix",
"code": "flowchart TB\n S[Forces] --- W[Faiblesses]\n O[Opportunités] --- T[Menaces]",
"created_at": "2026-04-22T01:06:13+00:00",
"use_count": 0
},
{
"id": "efe6a331c528",
"topic": "processus achat B2B entreprise",
"kind": "flowchart",
"context": "Auto-generated from user query",
"code": "flowchart LR\n A[Requête de devis] -->|Demande de devis|> B[Création du devis]\n B -->|Envoi du devis|> C[Analyse du devis]\n C -->|Acceptation du devis|> D[Création de la commande]\n D -->|Envoi de la commande|> E[Validation de la commande]\n E -->|Validation OK|> F[Livraison du produit]\n F -->|Livraison OK|> G[Facturation]\n G -->|Facturation OK|> H[Suivi de la commande]\n H -->|Suivi OK|> I[Clôture de la commande]\n I -->|Clôture OK|> J[Analyse de la commande]",
"created_at": "2026-04-22T01:08:53+00:00",
"use_count": 0
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
generated/v30-01-01-hi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
generated/v30-04-04-qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
generated/v30-07-07-hd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
generated/v30-99-final.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

View File

@@ -784,15 +784,30 @@ document.addEventListener('DOMContentLoaded',()=>{const s=document.createElement
box.innerHTML = '<div style="color:#64748b;font-size:11px;padding:8px">No tasks yet · click + Task on any LLM idea above to create</div>';
return;
}
var html = '<div style="font-size:11px;color:#6ee7b7;margin-bottom:8px;font-weight:700">📋 '+d.count+' tasks in Paperclip DB</div>';
var html = '<div style="font-size:11px;color:#6ee7b7;margin-bottom:8px;font-weight:700">📋 '+d.count+' tasks · ';
if (d.by_status) {
Object.keys(d.by_status).forEach(function(s){
var c = {proposed:'#fbbf24',in_progress:'#22d3ee',done:'#10b981',cancelled:'#94a3b8',blocked:'#ef4444'}[s] || '#94a3b8';
html += '<span style="padding:1px 6px;margin-right:4px;border-radius:6px;background:'+c+'22;color:'+c+';font-size:10px">'+s+': '+d.by_status[s]+'</span>';
});
}
html += '</div>';
d.tasks.forEach(function(t){
var colStatus = {proposed:'#fbbf24',in_progress:'#22d3ee',done:'#10b981'}[t.status] || '#94a3b8';
html += '<div style="padding:8px 10px;margin-bottom:4px;background:rgba(0,0,0,.2);border:1px solid rgba(16,185,129,.15);border-left:3px solid '+colStatus+';border-radius:6px">';
html += '<div style="display:flex;align-items:center;gap:6px"><b style="color:#e0e7ff;font-size:11.5px">#'+t.id+' '+t.title+'</b>';
var colStatus = {proposed:'#fbbf24',in_progress:'#22d3ee',done:'#10b981',cancelled:'#94a3b8',blocked:'#ef4444'}[t.status] || '#94a3b8';
html += '<div class="wave232StatusBtns" style="padding:8px 10px;margin-bottom:4px;background:rgba(0,0,0,.2);border:1px solid rgba(16,185,129,.15);border-left:3px solid '+colStatus+';border-radius:6px">';
html += '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"><b style="color:#e0e7ff;font-size:11.5px">#'+t.id+' '+t.title+'</b>';
html += '<span style="margin-left:auto;padding:1px 6px;border-radius:6px;background:'+colStatus+'22;color:'+colStatus+';font-size:9px;font-weight:700">'+t.status+'</span></div>';
html += '<div style="font-size:10px;color:#94a3b8;margin-top:3px">🎯 '+(t.opportunity||'')+' · '+(t.estimated_mad?Math.round(t.estimated_mad/1000)+'K MAD':'')+' · '+((t.created_at||'').slice(0,16))+'</div>';
if (t.tools_used) html += '<div style="font-size:10px;color:#64748b;margin-top:2px">🔧 '+t.tools_used.replace(/\|/g,' · ')+'</div>';
if (t.kpi) html += '<div style="font-size:10px;color:#6ee7b7;margin-top:2px">📊 '+t.kpi+'</div>';
// Wave 232: status workflow buttons
html += '<div style="display:flex;gap:4px;margin-top:5px;flex-wrap:wrap">';
['proposed','in_progress','done','cancelled','blocked'].forEach(function(s){
if (s === t.status) return;
var sc = {proposed:'#fbbf24',in_progress:'#22d3ee',done:'#10b981',cancelled:'#94a3b8',blocked:'#ef4444'}[s];
html += '<button onclick="updateTaskStatus('+t.id+', \''+s+'\')" style="padding:2px 7px;border-radius:5px;background:'+sc+'15;color:'+sc+';border:1px solid '+sc+'44;font-size:9px;cursor:pointer;font-weight:600">→ '+s+'</button>';
});
html += '</div>';
html += '</div>';
});
box.innerHTML = html;
@@ -802,6 +817,53 @@ document.addEventListener('DOMContentLoaded',()=>{const s=document.createElement
});
};
// Wave 232: Update task status workflow
window.updateTaskStatus = function(taskId, newStatus) {
fetch('/api/social-signals-hub.php?action=update_status', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({task_id: taskId, status: newStatus})
})
.then(function(r){return r.json();})
.then(function(d){
if (d.ok) { setTimeout(loadWave231Tasks, 300); }
});
};
// Wave 232: SSE stream visualization
window.startWave232SSE = function() {
var sseBox = document.getElementById('wave232SSE');
if (!sseBox) return;
sseBox.innerHTML = '<div style="color:#a855f7;font-size:11px">🔴 SSE stream connecting…</div>';
try {
var es = new EventSource('/api/social-signals-hub.php?action=stream&cb='+Date.now());
var events = [];
function render() {
var html = '<div style="font-size:11px;color:#c4b5fd;margin-bottom:6px">🔴 Live stream · '+events.length+' events</div>';
events.slice(-8).forEach(function(e){
var col = {hello:'#10b981',channel:'#22d3ee',tasks:'#fbbf24',done:'#a855f7'}[e.event] || '#94a3b8';
html += '<div style="padding:4px 8px;margin-bottom:3px;border-left:2px solid '+col+';background:rgba(0,0,0,.2);font-size:10px;color:#e0e7ff"><span style="color:'+col+';font-weight:700">'+e.event+'</span> · ';
if (e.data.name) html += '<b>'+e.data.name+'</b> count='+(e.data.count||0)+' · '+(e.data.top||'').slice(0,50);
else if (e.data.by_status) html += 'tasks: '+JSON.stringify(e.data.by_status);
else if (e.data.msg) html += e.data.msg;
else if (e.data.total_channels) html += 'done · '+e.data.total_channels+' channels streamed';
html += '</div>';
});
sseBox.innerHTML = html;
}
['hello','channel','tasks','done'].forEach(function(evName){
es.addEventListener(evName, function(e){
events.push({event: evName, data: JSON.parse(e.data)});
render();
if (evName === 'done') setTimeout(function(){es.close();}, 1000);
});
});
es.onerror = function(){ sseBox.innerHTML += '<div style="color:#94a3b8;font-size:10px">stream closed</div>'; es.close(); };
} catch(e) {
sseBox.innerHTML = '<div style="color:#ef4444">SSE err: '+e.message+'</div>';
}
};
// Inject tasks section after advisor renders
var origBuildSocial = buildSocialHub;
buildSocialHub = function(d) {
@@ -812,6 +874,13 @@ document.addEventListener('DOMContentLoaded',()=>{const s=document.createElement
h += '<button onclick="loadWave231Tasks()" style="margin-left:auto;padding:3px 8px;border-radius:6px;background:rgba(16,185,129,.15);color:#6ee7b7;border:1px solid rgba(16,185,129,.3);font-size:10px;cursor:pointer">🔄 Refresh</button></div>';
h += '<div id="wave231Tasks">loading…</div>';
h += '</div>';
// Wave 232: SSE live stream panel
h += '<div style="margin-top:12px;padding:12px;background:rgba(168,85,247,.06);border:1px solid rgba(168,85,247,.25);border-radius:8px">';
h += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><b style="color:#a855f7;font-size:11px;text-transform:uppercase">🔴 Live SSE Stream · channels + tasks</b>';
h += '<span style="padding:2px 6px;border-radius:6px;background:rgba(168,85,247,.2);color:#c4b5fd;font-size:9px;font-weight:700">WAVE 232</span>';
h += '<button onclick="startWave232SSE()" style="margin-left:auto;padding:3px 8px;border-radius:6px;background:rgba(168,85,247,.15);color:#c4b5fd;border:1px solid rgba(168,85,247,.3);font-size:10px;cursor:pointer">▶ Start stream</button></div>';
h += '<div id="wave232SSE"><div style="color:#64748b;font-size:10px">Click ▶ Start stream to connect SSE endpoint</div></div>';
h += '</div>';
setTimeout(loadWave231Tasks, 600);
return h;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

View File

@@ -5236,6 +5236,9 @@ if (typeof window.navigateTo === 'function'){
<a href="/weval-mega-master.html" style="padding:9px 16px;background:rgba(34,211,238,.15);border:1px solid rgba(34,211,238,.4);border-radius:8px;color:#22d3ee;text-decoration:none;font-size:11px;font-weight:600">🌐 Mega Master (606)</a>
<a href="/arsenal-master.html" style="padding:9px 16px;background:rgba(52,211,153,.15);border:1px solid rgba(52,211,153,.4);border-radius:8px;color:#34d399;text-decoration:none;font-size:11px;font-weight:600">🎯 Arsenal Master (183)</a>
<a href="/arsenal-history/" style="padding:9px 16px;background:rgba(167,139,250,.15);border:1px solid rgba(167,139,250,.4);border-radius:8px;color:#a78bfa;text-decoration:none;font-size:11px;font-weight:600">📚 Arsenal History (6)</a>
<!-- V157 Opus orphans link · droid + e2e-dashboard added to ecosystem index -->
<a href="/droid.html" style="padding:9px 16px;background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.4);border-radius:8px;color:#10b981;text-decoration:none;font-size:11px;font-weight:600">🤖 WEDROID Terminal (28KB)</a>
<a href="/e2e-dashboard.html" style="padding:9px 16px;background:rgba(251,191,36,.15);border:1px solid rgba(251,191,36,.4);border-radius:8px;color:#fbbf24;text-decoration:none;font-size:11px;font-weight:600">🎭 E2E Tests (8 screenshots)</a>
</div>
</div>

View File

@@ -0,0 +1,201 @@
# V157 + V158 - Tests E2E + SURPRISES CRITIQUES identifiées - 2026-04-22
## Objectif Yacine
"DEROULE TOUS LES TESTS DE SIMULATION ON VEUT PAS AVOIR DES SURPRISES"
Mission: Tests exhaustifs avant GO Kaouther pour zero surprise.
## Résultats Tests E2E
### ✅ TESTS PASSÉS
**TEST 1 - Data Quality: 14/14 PASS**
- Pilot view 3810 total, 3542 HIGH quality
- 0 NULL/duplicate/invalid emails
- 0 cross-contamination (amazon, avocat, .se, .dk)
- 0 fake 4-digit pattern
- 0 entity names
- 0 bounced
- 100% DZ generaliste coherence
**TEST 2 - Consent Flow: 5/5 PASS**
- 500 tokens pending, 100% unique
- 100% coverage tokens ↔ HCPs
- Format valide 32 chars alphanum
- consent.wevup.app HTTP 200
**TEST 3a - Template file: PASS**
- /var/www/html/api/ethica-pilot-template.html exists (2187 bytes)
- 3 placeholders: {{NOM}} {{TOKEN}} {{TRACKING_ID}}
- Branded Ethica header, CTA, unsubscribe, tracking pixel
**TEST 4 - PMTA_Direct send: PASS**
- send-controller.php seed_test works
- SMTP response 250-8BITMIME (accepted)
- Rate limit functional (1/100)
- tracking_id généré
**TEST 5 - SPF weval-consulting.com: PASS**
- SPF inclut S204 PMTA IP (204.168.152.13) ✅
## 🚨 SURPRISES CRITIQUES POUR KAOUTHER
### SURPRISE #1: creative_html = filename only
`ethica.campaigns[id=2].creative_html` contient juste `"ethica-pilot-template.html"` (26 chars) au lieu du HTML inline.
**Impact**: Pipeline de send DOIT faire `file_get_contents($creative_html)` au moment du send. À vérifier que la logic existe.
### SURPRISE #2: Tous les graph_accounts disabled
```
admin.graph_accounts:
Total: 197 accounts
can_send=true: 0 (!)
can_send=false: 197
Status: "disabled" partout
```
Microsoft Graph API = DOWN pour send. Tous les tokens OAuth ont expiré ou été révoqués.
**Impact**: Seul PMTA_Direct fonctionne. PMTA_Direct suffit mais needs proper DNS.
### SURPRISE #3: ethica.senders SPF HARDFAIL
```
raphaelafortin.onmicrosoft.com: v=spf1 include:spf.protection.outlook.com -all
deloisnegron.onmicrosoft.com: v=spf1 include:spf.protection.outlook.com -all
allonzomichel.onmicrosoft.com: v=spf1 include:spf.protection.outlook.com -all
```
`-all` = HARDFAIL toute IP non Microsoft.
Notre PMTA (S204 204.168.152.13) n'est PAS dans SPF de ces domaines.
→ Emails envoyés avec FROM ethica.senders via PMTA = SPF FAIL
→ Gmail/Yahoo/Outlook = rejet ou spam
### SURPRISE #4: Campaign #2 from_email
```
Campaign #2: Pilot Consent DZ - 500 MG
from_email: raphaelafortin@raphaelafortin.onmicrosoft.com
→ FAIL SPF when sent via our PMTA
```
**FIX REQUIS**: Changer from_email vers `ethica@weval-consulting.com` (SPF PASS)
### SURPRISE #5: Send pipeline en SAFETY MODE
```
send-controller status:
auto_mode: false
dangerous_crons_disabled: true
campaigns active: 0
campaigns paused: 24
send_queue: 0
sent_today: 0
last_send: 2026-04-16 (6 jours ago)
```
Pipeline est volontairement en mode safe. Ne s'auto-déclenche pas.
### SURPRISE #6: DKIM MISSING sur weval-consulting.com
```
Testé selectors: google, default, selector1, selector2, mta, s1, s2, k1
Résultat: AUCUN DKIM record trouvé
DMARC: v=DMARC1; p=quarantine; pct=100
```
DMARC p=quarantine + DKIM missing = emails iront en spam (même avec SPF PASS).
## ✅ Solutions recommandées V159+
### Priority 1: Fix from_email Campaign #2
```sql
UPDATE ethica.campaigns
SET from_email = 'ethica@weval-consulting.com',
from_name = 'Ethica Pharma - Pilot Consent'
WHERE id = 2;
```
### Priority 2: Setup DKIM weval-consulting.com
Configure DNS Cloudflare:
```
default._domainkey.weval-consulting.com TXT "v=DKIM1; k=rsa; p=<public_key>"
```
Configure PMTA signing config pour sign outgoing emails.
### Priority 3: Seed placement test
Avant lancement Kaouther, test réel send:
```bash
curl "https://weval-consulting.com/api/send-controller.php?action=seed_test&token=WEVADS2026&to=yacine.mahboub@gmail.com&subject=TEST+CAMPAIGN+2+Ethica&html=<file>&from_name=Ethica+Pharma&method=PMTA_Direct&limit=1"
```
Vérifier: reçu en inbox ou spam?
### Priority 4: IP Warmup 3 jours
Cron gradual: 10 → 50 → 200 → 500 emails/jour sur seeds.
Check reputation MxToolbox avant pilot real.
### Priority 5: Activate campaign + disable safety
Quand SPF+DKIM+DMARC alignés:
```sql
UPDATE ethica.campaigns SET status = 'scheduled' WHERE id = 2;
```
Remove `/tmp/wevads_crons_disabled` flag.
## État POST-V158
| Test | Status | Surprise |
|---|---|---|
| Data quality | ✅ 14/14 | - |
| Consent flow | ✅ 5/5 | - |
| Template file | ✅ exists | creative_html = filename |
| PMTA send | ✅ 250 OK | - |
| SPF weval-consulting | ✅ | - |
| Graph API | ❌ all disabled | - |
| SPF ethica senders | ❌ hardfail | from_email needs change |
| Pipeline mode | ⚠ safety | auto_mode=false |
| DKIM | ❌ missing | DMARC quarantine |
## Verdict Kaouther GO
**🟡 READY MAIS NÉCESSITE 2 FIX CRITIQUES AVANT** :
1. **Change from_email** (2 secondes SQL)
2. **Setup DKIM** (30 min DNS + PMTA config)
Sans ces 2 fix:
- Emails envoyés iront en SPAM (DMARC quarantine)
- Pilot = échec prévisible
Avec ces 2 fix:
- SPF PASS + DKIM PASS + DMARC aligned = inbox placement
- 3,542 HIGH quality DZ MG ready
- 500 consent tokens ready
- 230k/jour capacity (largement suffisant)
## L99 153/153 PASS ✅ (25 versions consécutives V125-V158)
## Chain V131 → V158
```
V131-V152 Routing + Playwright + Admin + Ethica pipeline
V153 Send infra audit
V154 Consent tokens (autre Claude)
V155 Pilot views quality scoring
V156 Pipeline health monitoring
V157 Tests 1-3 E2E (DATA + CONSENT + TEMPLATE)
V158 Tests 4+ SURPRISES identifiées (Graph disabled, SPF fails, DKIM missing)
```
## Doctrines V157-V158
- 0 Root cause (identification SPF/DKIM/Graph issues)
- 1 GOLD (views are non destructives)
- 2 Zero écrasement (READ-ONLY tests + documentation)
- 4 Zero régression L99 stable
- 13 Cause racine (DMARC quarantine reason)
- 14 Test-driven (6 categories tested live)
- 95 Traçabilité wiki complète
- 100 Train release
Tests E2E révèlent une vérité qu'on n'aurait PAS su sans ces simulations:
**L'infrastructure data est PRÊTE mais l'infrastructure DNS/auth email
NE l'est PAS encore pour un pilot Kaouther réussi.**
Better to know NOW than after first campaign flops to Kaouther doctors.