diff --git a/api/ambre-pw-tests/output/results.json b/api/ambre-pw-tests/output/results.json index 86a25b94f..3bc1578c5 100644 --- a/api/ambre-pw-tests/output/results.json +++ b/api/ambre-pw-tests/output/results.json @@ -80,11 +80,11 @@ "workerIndex": 0, "parallelIndex": 0, "status": "passed", - "duration": 20618, + "duration": 20510, "errors": [], "stdout": [ { - "text": "Mermaid found · 1 divs · 1 SVG rendered · badges: [\"♻️ KB Reused (1 uses)\",\"flowchart\",\"2ms\"]\n" + "text": "Mermaid found · 1 divs · 1 SVG rendered · badges: [\"♻️ KB Reused (2 uses)\",\"flowchart\",\"3ms\"]\n" }, { "text": "Total: 1.5s · mermaid found: true\n" @@ -95,7 +95,7 @@ ], "stderr": [], "retry": 0, - "startTime": "2026-04-22T01:24:19.355Z", + "startTime": "2026-04-22T01:33:41.411Z", "annotations": [], "attachments": [ { @@ -124,8 +124,8 @@ ], "errors": [], "stats": { - "startTime": "2026-04-22T01:24:18.728Z", - "duration": 21481.236, + "startTime": "2026-04-22T01:33:40.350Z", + "duration": 21970.469, "expected": 1, "skipped": 0, "unexpected": 0, diff --git a/api/ambre-pw-tests/output/v37-01-mermaid.png b/api/ambre-pw-tests/output/v37-01-mermaid.png index 382023834..a7a46212a 100644 Binary files a/api/ambre-pw-tests/output/v37-01-mermaid.png and b/api/ambre-pw-tests/output/v37-01-mermaid.png differ diff --git a/api/ambre-pw-tests/output/v37-02-reuse.png b/api/ambre-pw-tests/output/v37-02-reuse.png index b317ff988..fc20b0274 100644 Binary files a/api/ambre-pw-tests/output/v37-02-reuse.png and b/api/ambre-pw-tests/output/v37-02-reuse.png differ diff --git a/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/test-finished-1.png b/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/test-finished-1.png index 5b68bf6ab..fb954e27f 100644 Binary files a/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/test-finished-1.png and b/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/test-finished-1.png differ diff --git a/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/video.webm b/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/video.webm index 35a3bc87e..1346bed10 100644 Binary files a/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/video.webm and b/api/ambre-pw-tests/output/v37-mermaid-V37-·-mermaid-inline-render-artifact-chromium/video.webm differ diff --git a/api/ambre-scan-230b.php b/api/ambre-scan-230b.php new file mode 100644 index 000000000..5fe21d57d --- /dev/null +++ b/api/ambre-scan-230b.php @@ -0,0 +1,51 @@ +&1 | head -5")))); +$out["latest_commit"] = trim(@shell_exec("git log -1 --oneline 2>&1")); + +// Check my wave-229 tools + wave-230 state +$reg = @file_get_contents("/var/www/html/api/wevia-tool-registry.json"); +$data = @json_decode($reg, true); +if ($data) { + $out["wave_229_tools"] = array_map(function($t){return $t["id"];}, + array_filter($data["tools"] ?? [], function($t){return ($t["wave"] ?? 0) == 229;})); + $out["total_tools"] = count($data["tools"] ?? []); +} + +// Ethica state +$ethica = []; +$ethica["ecm_py_exists"] = file_exists("/var/www/html/ethica/ecm.py") || file_exists("/opt/ethica/ecm.py") || file_exists("/var/www/weval/ecm.py"); +$ethica["find_ecm"] = trim(@shell_exec("find /var/www /opt -name 'ecm.py' 2>/dev/null | head -5")); +$ethica["consent_urls"] = [ + "consent.wevup.app" => @shell_exec("curl -sI --max-time 3 https://consent.wevup.app/ 2>&1 | head -1"), +]; +$out["ethica"] = $ethica; + +// Mermaid V10 state in wevia.html +$w = @file_get_contents("/var/www/html/wevia.html"); +$out["wevia"] = [ + "size" => strlen($w), + "v10_mermaid" => strpos($w, "AMBRE-V10-MERMAID") !== false, + "v10_css_minheight" => strpos($w, "min-height:200px") !== false, +]; + +// Check i18n helpers in wevia.html +$out["i18n"] = [ + "detectLang" => strpos($w, "function detectLang") !== false, + "lang_var" => strpos($w, "var lang =") !== false, +]; + +// Mermaid KB state +$mkb = @file_get_contents("/var/www/html/generated/mermaid-learn-kb.json"); +if ($mkb) { + $kb_data = @json_decode($mkb, true); + $out["mermaid_kb_entries"] = count($kb_data ?: []); +} + +// Load current +$out["load"] = trim(shell_exec("uptime")); + +echo json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); diff --git a/api/ambre-v10-render.php b/api/ambre-v10-render.php new file mode 100644 index 000000000..a117ab587 --- /dev/null +++ b/api/ambre-v10-render.php @@ -0,0 +1,78 @@ +\" + + \"
Erreur rendu: \" + String(err).substring(0, 200) + \"\"; + }); + } else if (window.mermaid && typeof window.mermaid.init === \"function\") { + target.className = \"mermaid\"; + target.textContent = mcode; + window.mermaid.init(undefined, target); + } + } catch(e) { console.warn(\"mermaid render fail\", e); } + }, 500);"; + +if (strpos($c, $old_render) === false) { + echo json_encode(["error"=>"render pattern not found", "orig_changed" => strlen($c) != $orig]); + exit; +} +$c = str_replace($old_render, $new_render, $c); + +$backup = "/opt/wevads/vault/wevia.html.GOLD-" . date("Ymd-His") . "-v10-render"; +@copy($path, $backup); +$wrote = @file_put_contents($path, $c); + +echo json_encode([ + "delta" => strlen($c) - $orig, + "wrote" => $wrote, + "backup" => basename($backup), +]); diff --git a/api/social-signals-hub.php b/api/social-signals-hub.php index cd3e3544e..4ee32138b 100644 --- a/api/social-signals-hub.php +++ b/api/social-signals-hub.php @@ -1,5 +1,5 @@ 3 && strpos($opp_lower, $co_first) !== false) { $match = $row; break; } + } + pg_close($pg); + return $match; +} + +// === POST create_task with lead linking (WAVE 233) === 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"; + + // WAVE 233: Auto-link lead + $lead_match = link_lead($body['opportunity'] ?? ''); + $lead_id = $lead_match ? (int)$lead_match['id'] : null; + + $q = "INSERT INTO weval_tasks (title, source, source_ref, category, opportunity, tools_used, first_steps, kpi, estimated_mad, inspired_by, status, wave, lead_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING id, created_at"; $r = @pg_query_params($pg, $q, [ - $body['title']??'?', $body['source']??'advisor-wave232', $body['source_ref']??'', + $body['title']??'?', $body['source']??'advisor-wave233', $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 + $body['kpi']??'', (int)($body['estimated_mad']??0), $body['inspired_by']??'', 'proposed', 233, $lead_id ]); - 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'], 'lead_linked'=>$lead_match]); + } else { pg_close($pg); http_response_code(500); echo json_encode(['error'=>'insert failed', 'details'=>pg_last_error()]); } exit; } -// === PATCH update_task_status === +// === PATCH update_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; - } + $allowed = ['proposed','in_progress','done','cancelled','blocked']; + if (!$task_id || !in_array($new_status, $allowed)) { http_response_code(400); echo json_encode(['error'=>'invalid']); 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']); } + else { pg_close($pg); http_response_code(404); echo json_encode(['error'=>'not found']); } exit; } -// === GET list_tasks === +// === GET list_tasks with lead JOIN (WAVE 233) === 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'); + // LEFT JOIN with weval_leads + $r = @pg_query($pg, ' + SELECT t.*, l.slug AS lead_slug, l.company AS lead_company, l.mql_score AS lead_mql, l.sql_qualified AS lead_sql + FROM weval_tasks t + LEFT JOIN weval_leads l ON t.lead_id = l.id + ORDER BY t.created_at DESC LIMIT 20 + '); $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; } + $linked = count(array_filter($tasks, function($t){return !empty($t['lead_id']);})); pg_close($pg); - echo json_encode(['ok'=>true, 'count'=>count($tasks), 'by_status'=>$agg, 'tasks'=>$tasks]); + echo json_encode(['ok'=>true, 'count'=>count($tasks), 'by_status'=>$agg, 'linked_count'=>$linked, 'tasks'=>$tasks]); exit; } -// === SSE streaming endpoint === +// === GET export_tasks_csv (WAVE 233) === +if (($_GET['action'] ?? '') === 'export_csv') { + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename="weval_tasks_' . date('Ymd') . '.csv"'); + $pg = @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=3'); + if (!$pg) { echo "error,no pg\n"; exit; } + $r = @pg_query($pg, 'SELECT t.id, t.title, t.status, t.opportunity, t.estimated_mad, t.tools_used, t.kpi, t.created_at, l.company AS linked_lead, l.mql_score AS lead_mql FROM weval_tasks t LEFT JOIN weval_leads l ON t.lead_id = l.id ORDER BY t.created_at DESC'); + $out = fopen('php://output', 'w'); + fputcsv($out, ['id','title','status','opportunity','estimated_mad','tools_used','kpi','created_at','linked_lead','lead_mql']); + while ($row = pg_fetch_assoc($r)) fputcsv($out, $row); + fclose($out); + pg_close($pg); + exit; +} + +// === POST ask_wevia (WAVE 233) === +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'ask_wevia') { + $body = json_decode(file_get_contents('php://input'), true) ?: []; + $idea = $body['idea'] ?? []; + $q = "Contextualisé par cette idea de conversion:\n" . + "Titre: " . ($idea['title'] ?? '?') . "\n" . + "Opportunité: " . ($idea['opportunity'] ?? '?') . "\n" . + "Tools: " . (is_array($idea['tools_used']??null) ? implode(', ', $idea['tools_used']) : ($idea['tools_used'] ?? '?')) . "\n" . + "KPI: " . ($idea['kpi'] ?? '?') . "\n" . + "MAD est: " . ($idea['estimated_mad'] ?? 0) . "\n" . + "Détaille un plan exécutable 14j step-by-step avec multi-agents WEVAL."; + + // Query WEVIA Master via saas-chat.php + $ch = curl_init('http://127.0.0.1/api/saas-chat.php'); + $payload = json_encode(['message' => $q]); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>30, CURLOPT_HTTPHEADER=>['Content-Type: application/json'], CURLOPT_POSTFIELDS=>$payload]); + $r = curl_exec($ch); $c = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + + if ($c === 200) { + echo json_encode(['ok'=>true, 'wevia_response'=>json_decode($r, true) ?: $r, 'query'=>$q]); + } else { + // Fallback: direct LLM query + $secrets = load_secrets(); + $payload2 = json_encode(['model'=>'mistral-small-latest','messages'=>[['role'=>'system','content'=>'Tu es WEVIA Master, assistant multi-agents WEVAL Consulting. Réponds en FR compact.'],['role'=>'user','content'=>$q]],'max_tokens'=>1500,'temperature'=>0.3]); + $ch = curl_init('https://api.mistral.ai/v1/chat/completions'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>25, CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.($secrets['MISTRAL_KEY']??'')], CURLOPT_POSTFIELDS=>$payload2]); + $rf = curl_exec($ch); $cf = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + $text = ''; + if ($cf === 200) { $d = json_decode($rf, true); $text = $d['choices'][0]['message']['content'] ?? ''; } + echo json_encode(['ok'=>(bool)$text, 'fallback'=>'Mistral direct', 'wevia_response'=>$text, 'query'=>$q]); + } + exit; +} + +// === SSE stream === 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', - ]; + $send = function($event, $data) { echo "event: $event\n"; echo "data: " . json_encode($data) . "\n\n"; @ob_flush(); flush(); }; + $send('hello', ['wave'=>233, 'msg'=>'SSE social stream live', 'ts'=>date('c')]); + $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); + $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 : ''; } } + 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'); @@ -119,29 +188,26 @@ if (($_GET['action'] ?? '') === 'stream') { exit; } -// === Default: aggregation with snscrape Twitter + 5 Mastodon === +// === Default aggregation (Reddit 5 subs WAVE 233) === $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' => 232, 'version' => 'social-signals-hub-v5', - 'topics' => $topic_list, 'channels' => [], 'aggregated_ideas' => [], -]; +$signals = ['ts'=>date('c'), 'wave'=>233, 'version'=>'social-signals-hub-v6', 'topics'=>$topic_list, 'channels'=>[], 'aggregated_ideas'=>[]]; -// Parallel fetch $urls = []; $urls['linkedin'] = 'http://127.0.0.1/api/linkedin-posts.php'; foreach (['SaaS conversion', 'B2B sales outbound'] as $i => $q) { $urls['hn_'.$i] = 'https://hn.algolia.com/api/v1/search?query=' . urlencode($q) . '&tags=story&hitsPerPage=6'; } -foreach (['SaaS', 'Entrepreneur', 'B2BSales'] as $i => $s) { - $urls['rd_'.$i] = 'https://old.reddit.com/r/' . $s . '/.rss?limit=5'; +// WAVE 233: 5 Reddit subs (was 3) +$reddit_subs = ['SaaS', 'Entrepreneur', 'B2BSales', 'startups', 'marketing']; +foreach ($reddit_subs as $i => $s) { + $urls['rd_'.$i] = 'https://old.reddit.com/r/' . $s . '/.rss?limit=4'; } $urls['hn_yt'] = 'https://hn.algolia.com/api/v1/search?query=' . urlencode('youtube.com SaaS') . '&tags=story&hitsPerPage=10'; -// Mastodon 5 instances -$mast_hosts = ['mastodon.social', 'mstdn.social', 'fosstodon.org', 'hachyderm.io', 'techhub.social']; +// Mastodon 5 instances (kept from wave 232) +$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'; } @@ -161,7 +227,7 @@ if (!empty($results['linkedin'])) { $ln['count'] = count($ln['items']); $signals['channels']['linkedin'] = $ln; -// HackerNews +// HN $hn = ['channel'=>'hackernews','source'=>'Algolia API','items'=>[]]; foreach ([0,1] as $i) { if (empty($results['hn_'.$i])) continue; @@ -175,22 +241,22 @@ $hn['items'] = array_slice($hn['items'], 0, 10); $hn['count'] = count($hn['items']); $signals['channels']['hackernews'] = $hn; -// Reddit RSS -$rd = ['channel'=>'reddit','source'=>'old.reddit RSS','items'=>[]]; -foreach ([0,1,2] as $i) { +// Reddit 5 subs (WAVE 233) +$rd = ['channel'=>'reddit','source'=>'5 subs RSS (SaaS+Entr+B2B+startups+marketing)','items'=>[]]; +foreach (range(0,4) as $i) { if (empty($results['rd_'.$i])) continue; $xml = @simplexml_load_string($results['rd_'.$i]); if (!$xml) continue; - $sub = ['SaaS','Entrepreneur','B2BSales'][$i]; + $sub = $reddit_subs[$i]; foreach ($xml->entry ?? [] as $entry) { $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['items'] = array_slice($rd['items'], 0, 20); $rd['count'] = count($rd['items']); $signals['channels']['reddit'] = $rd; -// YouTube via HN-filtered +// YouTube $yt = ['channel'=>'youtube','source'=>'HackerNews YT-filtered','items'=>[]]; if (!empty($results['hn_yt'])) { $hd = @json_decode($results['hn_yt'], true); @@ -206,67 +272,31 @@ $yt['items'] = array_slice($yt['items'], 0, 8); $yt['count'] = count($yt['items']); $signals['channels']['youtube'] = $yt; -// 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($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, 8); -$tw['count'] = count($tw['items']); -$signals['channels']['twitter'] = $tw; - -// Mastodon 5 instances merged -$ma = ['channel'=>'mastodon','source'=>'5 instances (social/mstdn/fosstodon/hachyderm/techhub)','items'=>[]]; +// Mastodon +$ma = ['channel'=>'mastodon','source'=>'5 instances merged','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) { $content = trim(strip_tags($s['content'] ?? '')); if (strlen($content) > 20) { - $ma['items'][] = [ - '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), - 'date' => substr($s['created_at'] ?? '', 0, 10), - ]; + $ma['items'][] = ['title'=>substr($content,0,180),'url'=>$s['url']??'','user'=>'@'.($s['account']['acct']??'?'),'instance'=>$mast_hosts[$i]??'?','favorites'=>(int)($s['favourites_count']??0)]; } } } -usort($ma['items'], function($a,$b){return ($b['favorites']??0)-($a['favorites']??0);}); $ma['items'] = array_slice($ma['items'], 0, 10); $ma['count'] = count($ma['items']); $signals['channels']['mastodon'] = $ma; -// Dark Scout async +// Dark Scout opt-in 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]); - $ds_raw = curl_exec($ds_ch); - curl_close($ds_ch); - $sc = ['channel'=>'dark_scout','source'=>'google+bing+ddg','items'=>[]]; + $ds_raw = curl_exec($ds_ch); curl_close($ds_ch); + $sc = ['channel'=>'dark_scout','source'=>'multi-engine','items'=>[]]; if ($ds_raw) { $sd = @json_decode($ds_raw, true); - foreach (array_slice(($sd['results']??[]), 0, 6) as $r) { + foreach (array_slice(($sd['results']??[]),0,6) as $r) { $sc['items'][] = ['title'=>substr($r['title']??'',0,140),'snippet'=>substr($r['snippet']??'',0,150),'url'=>$r['url']??'','category'=>$r['category']??'']; } } @@ -279,14 +309,14 @@ foreach ($signals['channels'] as $c) foreach ($c['items'] as $i) if (!empty($i[' $signals['aggregated_ideas'] = array_slice(array_unique($all), 0, 30); $signals['total_items'] = array_sum(array_map(function($c){return $c['count']??0;}, $signals['channels'])); -// LLM cascade +// LLM 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), 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."; + $weval_ctx = "WEVAL Consulting: 48 leads Paperclip · Vistex MQL95 450K MAD · Ethica MQL100 200K MAD · Huawei MQL90. Products: SAP, API HCP Maghreb 157K HCPs, Pharma Cloud, WEVAL SaaS Freemium. Tools: WEVIA Master 269 tools, Dark Scout, WePredict, WEVADS Brain, Blade AI, DocuSeal. Pipeline 2.9M MAD."; $summary = ""; - foreach ($signals['channels'] as $k => $c) { $summary .= "- $k ({$c['count']}): ".substr($c['items'][0]['title']??'(none)',0,60)."\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 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}]}"; + $prompt = "$weval_ctx\n\nSignals 7 channels:\n$summary\nHeadlines:\n - $headlines\n\n5 CONCRETE conversion ideas for WEVAL MENA. Each: opp, tools, 14d exec, KPI, MAD, 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'], @@ -297,7 +327,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); diff --git a/api/v83-business-kpi-latest.json b/api/v83-business-kpi-latest.json index 4a0a12b4a..16b626fc4 100644 --- a/api/v83-business-kpi-latest.json +++ b/api/v83-business-kpi-latest.json @@ -1,7 +1,7 @@ { "ok": true, "version": "V83-business-kpi", - "ts": "2026-04-22T01:30:29+00:00", + "ts": "2026-04-22T01:34:42+00:00", "summary": { "total_categories": 8, "total_kpis": 64, diff --git a/generated/mermaid-learn-kb.json b/generated/mermaid-learn-kb.json index ffdaecc89..fd0391d9c 100644 --- a/generated/mermaid-learn-kb.json +++ b/generated/mermaid-learn-kb.json @@ -6,7 +6,7 @@ "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": 3 + "use_count": 4 }, { "id": "39559de03fd9", @@ -15,7 +15,7 @@ "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": 2 + "use_count": 3 }, { "id": "bf87b2067bbd",