auto-sync-0335

This commit is contained in:
opus
2026-04-22 03:35:02 +02:00
parent 59c686e975
commit 5f2f7612ee
10 changed files with 268 additions and 109 deletions

View File

@@ -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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 80 KiB

51
api/ambre-scan-230b.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
header("Content-Type: application/json");
$out = [];
chdir("/var/www/html");
$out["recent_tags"] = array_filter(array_map("trim", explode("\n", @shell_exec("git tag -l 'wave-*' --sort=-creatordate 2>&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);

78
api/ambre-v10-render.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
header("Content-Type: application/json");
$path = "/var/www/html/wevia.html";
$c = @file_get_contents($path);
$orig = strlen($c);
// The V10 block uses class="mermaid" which triggers the problematic CSS
// Keep class="mermaid" (so mermaid.run picks it up) BUT add an override style in-line
// The issue is font-size:0 !important - we can't override easily
// Better: after mermaid.run, the data-processed=true attribute is set, which UNSETS font-size:0
// So the issue must be that mermaid.run() isn't triggering or the SVG has issues
// Let me use a different approach: use mermaid.render() directly to get SVG as string, insert directly
$old = "var uniqId = \"mmd-\" + Date.now();
var inlineBlock = \"<div style=\\\"margin:12px 0;padding:14px;background:#fafafa;border:1px solid #e5e7eb;border-radius:12px\\\">\" +
\"<div style=\\\"font-weight:600;font-size:13px;color:#6b7280;margin-bottom:10px\\\">📊 \" + topic + \"</div>\" +
\"<div class=\\\"mermaid\\\" id=\\\"\" + uniqId + \"\\\" style=\\\"text-align:center;min-height:200px;font-size:14px;color:#333\\\">\" + mcode + \"</div>\"";
$new = "var uniqId = \"mmd-\" + Date.now();
// Pre-render SVG via mermaid.render() to avoid CSS font-size:0 !important issue
var inlineBlock = \"<div style=\\\"margin:12px 0;padding:14px;background:#fafafa;border:1px solid #e5e7eb;border-radius:12px\\\">\" +
\"<div style=\\\"font-weight:600;font-size:13px;color:#6b7280;margin-bottom:10px\\\">📊 \" + topic + \"</div>\" +
\"<div id=\\\"\" + uniqId + \"\\\" style=\\\"text-align:center;min-height:150px;padding:10px;background:#fff;border-radius:8px\\\">Rendu en cours...</div>\"";
if (strpos($c, $old) === false) {
echo json_encode(["error"=>"V10 pattern not found for CSS fix"]);
exit;
}
$c = str_replace($old, $new, $c);
// And update the render call to use mermaid.render(id, code) API
$old_render = "setTimeout(function(){
try {
if (window.mermaid && typeof window.mermaid.run === \"function\") {
window.mermaid.run({ nodes: [document.getElementById(uniqId)] });
}
} catch(e) { console.warn(\"mermaid render fail\", e); }
}, 300);";
$new_render = "setTimeout(function(){
try {
var target = document.getElementById(uniqId);
if (!target) return;
if (window.mermaid && typeof window.mermaid.render === \"function\") {
// Use render() to get SVG string directly
window.mermaid.render(\"svg-\" + uniqId, mcode).then(function(result) {
if (result && result.svg) {
target.innerHTML = result.svg;
target.style.minHeight = \"auto\";
}
}).catch(function(err){
console.warn(\"mermaid.render error\", err);
target.innerHTML = \"<pre style=\\\"font-size:11px;color:#b00;padding:10px;background:#fee;border-radius:6px\\\">Erreur rendu: \" + String(err).substring(0, 200) + \"</pre>\";
});
} 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),
]);

View File

@@ -1,5 +1,5 @@
<?php
// WAVE 232 v5 · Twitter snscrape OSS + 5 Mastodon instances + task PATCH + SSE
// WAVE 233 v6 · Ask WEVIA + 5 Reddit subs + lead linking + CSV export
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
set_time_limit(25);
@@ -31,83 +31,152 @@ function multi_fetch($urls, $timeout=7) {
return $out;
}
// === POST create_task ===
function link_lead($opportunity) {
// WAVE 233: match opportunity string to weval_leads.id via slug
if (empty($opportunity)) return null;
$pg = @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=2');
if (!$pg) return null;
$opp_lower = strtolower($opportunity);
$r = @pg_query($pg, 'SELECT id, slug, company, mql_score FROM weval_leads ORDER BY mql_score DESC LIMIT 50');
$match = null;
if ($r) while ($row = pg_fetch_assoc($r)) {
$co = strtolower($row['company'] ?? '');
$sl = strtolower($row['slug'] ?? '');
$co_first = explode(' ', $co)[0] ?? '';
if ($sl && strpos($opp_lower, $sl) !== false) { $match = $row; break; }
if ($co_first && strlen($co_first) > 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);

View File

@@ -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,

View File

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