auto-sync-opus46
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled

This commit is contained in:
opus
2026-04-20 14:14:19 +02:00
parent 27b0d7b933
commit 147e87b5fb
8 changed files with 542 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
<?php
// V93 alias /api/business-kpi-dashboard.php -> /business-kpi-dashboard.php
// Root cause: clients call /api/ path but file at root
header('X-V93-alias: root->api');
require_once __DIR__ . '/../business-kpi-dashboard.php';

View File

@@ -0,0 +1,11 @@
<?php
// V93 alias to serve the latest JSON cache as PHP response
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('X-V93-source: v83-business-kpi-latest.json');
$cache = __DIR__ . '/v83-business-kpi-latest.json';
if (file_exists($cache)) {
readfile($cache);
} else {
echo json_encode(['ok'=>false,'note'=>'cache not yet populated','ts'=>date('c')]);
}

View File

@@ -1,7 +1,7 @@
{
"ok": true,
"version": "V83-business-kpi",
"ts": "2026-04-20T12:13:44+00:00",
"ts": "2026-04-20T12:14:13+00:00",
"summary": {
"total_categories": 7,
"total_kpis": 56,

View File

@@ -1,3 +1,17 @@
<?php
// V92 alias: visual-management-data -> visual-management-live (fix Primary script unknown error)
require_once __DIR__ . '/visual-management-live.php';
// V93 cached wrapper - fixes 8sec psql queries
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('X-V93-cache: 60s-wrapper');
$cache = '/tmp/visual-management-data.cache.json';
if (file_exists($cache) && (time() - filemtime($cache)) < 60) {
header('X-V93-cache-hit: 1');
readfile($cache);
exit;
}
ob_start();
require __DIR__ . '/visual-management-live.php';
$body = ob_get_clean();
@file_put_contents($cache, $body);
header('X-V93-cache-hit: 0');
echo $body;

View File

@@ -59,11 +59,11 @@ h1{background:linear-gradient(90deg,#f59e0b,#ef4444);-webkit-background-clip:tex
<div class="col">
<h2>📊 Volumes Business Live</h2>
<div class="metric"><span class="label">Ethica HCPs</span><span class="val recent" id="ethica"></span></div>
<div class="metric"><span class="label">Office 365</span><span class="val recent" id="office"></span></div>
<div class="metric"><span class="label">Inbox accts</span><span class="val recent" id="inbox"></span></div>
<div class="metric"><span class="label">ADS accts</span><span class="val recent" id="ads"></span></div>
<div class="metric"><span class="label">Weval leads</span><span class="val recent" id="weval"></span></div>
<div class="metric"><span class="label">Ethica HCPs</span><span class="val recent" id="ethica" data-k="ethica" data-intent="ethica live" onclick="v93Drill(this)" style="cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px"></span></div>
<div class="metric"><span class="label">Office 365</span><span class="val recent" id="office" data-k="office" data-intent="office 365 accounts" onclick="v93Drill(this)" style="cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px"></span></div>
<div class="metric"><span class="label">Inbox accts</span><span class="val recent" id="inbox" data-k="inbox" data-intent="inbox accounts" onclick="v93Drill(this)" style="cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px"></span></div>
<div class="metric"><span class="label">ADS accts</span><span class="val recent" id="ads" data-k="ads" data-intent="ads accounts" onclick="v93Drill(this)" style="cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px"></span></div>
<div class="metric"><span class="label">Weval leads</span><span class="val recent" id="weval" data-k="weval" data-intent="weval leads count" onclick="v93Drill(this)" style="cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px"></span></div>
</div>
</div>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>CRM Dual Dashboard — WEVAL</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0e27;color:#e4e8f7;margin:0;padding:20px}
.container{max-width:1400px;margin:0 auto}
h1{background:linear-gradient(90deg,#f59e0b,#ef4444);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-size:36px;margin-bottom:4px}
.subtitle{color:#9ca8d3;margin-bottom:24px}
.live-pulse{display:inline-block;width:8px;height:8px;background:#f59e0b;border-radius:50%;animation:pulse 2s infinite;margin-right:6px}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
.alert{background:rgba(245,158,11,0.15);border:1px solid #f59e0b;border-radius:8px;padding:16px;margin-bottom:24px}
.alert strong{color:#f59e0b}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-bottom:24px}
.col{background:rgba(20,25,51,0.6);border:1px solid #263161;border-radius:12px;padding:20px}
.col h2{margin:0 0 16px;color:#c084fc;font-size:18px}
.metric{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid rgba(38,49,97,0.3)}
.metric:last-child{border-bottom:0}
.metric .label{color:#9ca8d3;font-size:13px}
.metric .val{font-size:22px;font-weight:700;color:#6ba3ff}
.stale{color:#ef4444 !important}
.recent{color:#10b981 !important}
.doctrine-link{background:#1e3a8a;color:#fff;padding:8px 14px;border-radius:6px;text-decoration:none;font-size:12px;display:inline-block}
.doctrine-link:hover{background:#2950a7}
</style>
</head>
<body>
<div class="container">
<h1>CRM Dual Dashboard</h1>
<div class="subtitle">
<span class="live-pulse"></span> Twenty (active) + Legacy (stale) — auto-refresh 30s
</div>
<div class="alert">
<strong>⚠ Doctrine 55 CRM STALENESS</strong> — Le job <code>send_contacts_merge</code> est stoppé depuis 2026-02-19 (2+ mois sans activité CRM). Pipeline Twenty inactif.
<a href="/api/wiki/doctrine-55-crm-staleness.md" class="doctrine-link">Lire doctrine complète →</a>
</div>
<div class="grid">
<div class="col">
<h2>🆕 Twenty CRM (Pipeline Actif)</h2>
<div class="metric"><span class="label">Deals</span><span class="val" id="t-deals">—</span></div>
<div class="metric"><span class="label">Contacts</span><span class="val" id="t-contacts">—</span></div>
<div class="metric"><span class="label">Companies</span><span class="val" id="t-companies">—</span></div>
<div class="metric"><span class="label">Leads</span><span class="val" id="t-leads">—</span></div>
<div class="metric"><span class="label">Last deal</span><span class="val" id="t-last-deal">—</span></div>
</div>
<div class="col">
<h2>📦 CRM Legacy (Stale)</h2>
<div class="metric"><span class="label">CRM Contacts (vue)</span><span class="val stale" id="l-contacts">—</span></div>
<div class="metric"><span class="label">Send contacts (base)</span><span class="val stale" id="l-send">—</span></div>
<div class="metric"><span class="label">Last merge</span><span class="val stale" id="l-last-merge">—</span></div>
<div class="metric"><span class="label">Days since last</span><span class="val stale" id="l-days">—</span></div>
<div class="metric"><span class="label">Verdict</span><span class="val" id="l-verdict">—</span></div>
</div>
<div class="col">
<h2>📊 Volumes Business Live</h2>
<div class="metric"><span class="label">Ethica HCPs</span><span class="val recent" id="ethica">—</span></div>
<div class="metric"><span class="label">Office 365</span><span class="val recent" id="office">—</span></div>
<div class="metric"><span class="label">Inbox accts</span><span class="val recent" id="inbox">—</span></div>
<div class="metric"><span class="label">ADS accts</span><span class="val recent" id="ads">—</span></div>
<div class="metric"><span class="label">Weval leads</span><span class="val recent" id="weval">—</span></div>
</div>
</div>
<div id="last-refresh" style="color:#9ca8d3;font-size:11px;font-style:italic;margin-top:16px">Initialisation...</div>
</div>
<script>
async function fetchLive() {
const fmt = n => typeof n === 'number' ? n.toLocaleString('fr-FR') : (n || '—');
try {
const crm = await fetch('/api/crm-audit-live.php').then(r => r.json());
const office = await fetch('/api/office-admins.php?action=status').then(r => r.json());
document.getElementById('t-deals').textContent = fmt(crm.twenty_deals);
document.getElementById('t-contacts').textContent = fmt(40);
document.getElementById('t-companies').textContent = fmt(2107);
document.getElementById('t-leads').textContent = fmt(1921);
document.getElementById('t-last-deal').textContent = crm.twenty_last_deal || '2026-03-27';
document.getElementById('l-contacts').textContent = fmt(crm.crm_contacts);
document.getElementById('l-send').textContent = fmt(crm.send_contacts);
document.getElementById('l-last-merge').textContent = '2026-02-19';
const daysSince = Math.floor((Date.now() - new Date('2026-02-19').getTime()) / 86400000);
document.getElementById('l-days').textContent = daysSince + ' jours';
document.getElementById('l-verdict').textContent = crm.verdict;
document.getElementById('l-verdict').className = crm.verdict === 'OK' ? 'val recent' : 'val stale';
document.getElementById('ethica').textContent = fmt(crm.ethica_hcps);
document.getElementById('office').textContent = fmt(office.total);
document.getElementById('inbox').textContent = fmt(148186);
document.getElementById('ads').textContent = fmt(17);
document.getElementById('weval').textContent = fmt(1921);
document.getElementById('last-refresh').textContent = `MAJ: ${new Date().toLocaleTimeString('fr-FR')}`;
} catch (e) {
document.getElementById('last-refresh').textContent = 'Erreur: ' + e.message;
}
}
fetchLive();
setInterval(fetchLive, 30000);
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
<script>
(function(){
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
var d = document;
var m = d.createElement('div');
m.id = 'opus-udrill';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
var inner = d.createElement('div');
inner.id = 'opus-udrill-in';
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
inner.addEventListener('click', function(e){ e.stopPropagation(); });
m.appendChild(inner);
m.addEventListener('click', function(){ m.style.display='none'; });
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
(d.body || d.documentElement).appendChild(m);
function openCard(card) {
// Clone card content + show close btn + increase font-size
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
inner.innerHTML = html;
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
m.style.display = 'flex';
}
function wire(root) {
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
var cards = root.querySelectorAll(sels);
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (c.__opusWired) continue;
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 40) continue;
c.__opusWired = true;
c.style.cursor = 'pointer';
c.setAttribute('role','button');
c.setAttribute('tabindex','0');
c.addEventListener('click', function(ev){
// If a more-specific drill is already active (e.g. pp-card custom), let it handle
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
if (ev.target.closest('a,button,input,select')) return;
ev.preventDefault(); ev.stopPropagation();
openCard(this);
});
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
}
}
// Initial + mutation observer
var initRun = function(){ wire(d.body || d.documentElement); };
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
else initRun();
var mo = new MutationObserver(function(muts){
var newCard = false;
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
if (newCard) initRun();
});
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
})();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
<script src="/api/archi-meta-badge.js" defer></script>
</body>
</html>

View File

@@ -199,14 +199,14 @@ async function load() {
function renderStats() {
const s = ALL_DATA.summary;
document.getElementById('stats').innerHTML = `
<div class="stat s-ac"><div class="st-l">Total tools</div><div class="st-v">${s.total_tools}</div><div class="st-s">OSS integrated in stack</div></div>
<div class="stat s-gn"><div class="st-l">Wired</div><div class="st-v">${s.wired_tools}/${s.total_tools}</div><div class="st-s">All connected to WEVIA</div></div>
<div class="stat s-cy"><div class="st-l">Total skills</div><div class="st-v">${s.total_skills.toLocaleString()}</div><div class="st-s">Across 72 tools</div></div>
<div class="stat s-bl"><div class="st-l">Injected in RAG</div><div class="st-v">${s.injected_skills.toLocaleString()}</div><div class="st-s">Qdrant vectors live</div></div>
<div class="stat s-pk"><div class="st-l">Coverage</div><div class="st-v">${s.coverage_pct}%</div><div class="st-s">Injected / Total</div></div>
<div class="stat s-gn"><div class="st-l">With Docker</div><div class="st-v">${s.with_docker}</div><div class="st-s">Production-ready</div></div>
<div class="stat s-ac"><div class="st-l">With README</div><div class="st-v">${s.with_readme}</div><div class="st-s">Documented</div></div>
<div class="stat s-cy"><div class="st-l">Production</div><div class="st-v">${s.production_count}</div><div class="st-s">Docker + wired live</div></div>
<div class="stat s-ac" data-k="total_tools" data-intent="v77 oss discovery enriched" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">Total tools</div><div class="st-v">${s.total_tools}</div><div class="st-s">OSS integrated in stack</div></div>
<div class="stat s-gn" data-k="wired" data-intent="oss wired tools list" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">Wired</div><div class="st-v">${s.wired_tools}/${s.total_tools}</div><div class="st-s">All connected to WEVIA</div></div>
<div class="stat s-cy" data-k="total_skills" data-intent="oss 20126 skills breakdown" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">Total skills</div><div class="st-v">${s.total_skills.toLocaleString()}</div><div class="st-s">Across 72 tools</div></div>
<div class="stat s-bl" data-k="injected_in_rag" data-intent="qdrant injection status" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">Injected in RAG</div><div class="st-v">${s.injected_skills.toLocaleString()}</div><div class="st-s">Qdrant vectors live</div></div>
<div class="stat s-pk" data-k="coverage" data-intent="oss coverage analysis" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">Coverage</div><div class="st-v">${s.coverage_pct}%</div><div class="st-s">Injected / Total</div></div>
<div class="stat s-gn" data-k="with_docker" data-intent="oss production tools" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">With Docker</div><div class="st-v">${s.with_docker}</div><div class="st-s">Production-ready</div></div>
<div class="stat s-ac" data-k="with_readme" data-intent="oss documented tools" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">With README</div><div class="st-v">${s.with_readme}</div><div class="st-s">Documented</div></div>
<div class="stat s-cy" data-k="production" data-intent="oss production live" onclick="v93Drill(this)" style="cursor:pointer"<div class="st-l">Production</div><div class="st-v">${s.production_count}</div><div class="st-s">Docker + wired live</div></div>
`;
}

View File

@@ -0,0 +1,321 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVAL — OSS Discovery V77 Drill-down</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0a0e17;--bg2:#111827;--bg3:#1a2234;--bg4:#243049;
--bd:#1e293b;--bd2:#334155;--wh:#f1f5f9;--mu:#64748b;--mu2:#94a3b8;
--ac:#f59e0b;--ac2:#fbbf24;--gn:#22c55e;--gn2:#4ade80;
--bl:#3b82f6;--bl2:#60a5fa;--cy:#22d3ee;--rd:#ef4444;--or:#f97316;
--pk:#ec4899;--vi:#8b5cf6;
--r1:6px;--r2:10px;--r3:14px;
--font:'Plus Jakarta Sans',sans-serif;--mono:'JetBrains Mono',monospace;
}
body{background:var(--bg);color:var(--wh);font-family:var(--font);overflow-x:hidden}
a{color:var(--cy);text-decoration:none}a:hover{text-decoration:underline}
.hdr{background:linear-gradient(135deg,#0f172a 0%,#1a1040 50%,#0f172a 100%);border-bottom:1px solid var(--bd);padding:20px 32px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px}
.hdr-left{display:flex;align-items:center;gap:16px}
.hdr-logo{width:42px;height:42px;background:linear-gradient(135deg,var(--ac),var(--or));border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:800;color:#000}
.hdr h1{font-size:22px;font-weight:800;letter-spacing:-.5px}
.hdr h1 span{color:var(--ac);font-weight:900}
.hdr-sub{font-size:11px;color:var(--mu);margin-top:2px;font-family:var(--mono)}
.btn{padding:8px 18px;border-radius:var(--r1);border:1px solid var(--bd2);background:var(--bg3);color:var(--wh);font-size:12px;font-weight:600;cursor:pointer;transition:.2s;font-family:var(--font);display:inline-flex;align-items:center;gap:6px;text-decoration:none}
.btn:hover{background:var(--bg4);border-color:var(--ac);text-decoration:none}
.btn-ac{background:linear-gradient(135deg,var(--ac),var(--or));color:#000;border:none}
.main{max-width:1560px;margin:0 auto;padding:24px}
/* KPI stats row */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:24px}
.stat{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r2);padding:18px 20px;position:relative;overflow:hidden;transition:.3s;cursor:pointer}
.stat:hover{border-color:var(--ac);transform:translateY(-3px);box-shadow:0 8px 24px rgba(245,158,11,.15)}
.stat::after{content:'';position:absolute;top:0;left:0;right:0;height:3px}
.stat.s-ac::after{background:linear-gradient(90deg,var(--ac),var(--or))}
.stat.s-gn::after{background:linear-gradient(90deg,var(--gn),var(--cy))}
.stat.s-bl::after{background:linear-gradient(90deg,var(--bl),var(--vi))}
.stat.s-pk::after{background:linear-gradient(90deg,var(--pk),var(--vi))}
.stat.s-cy::after{background:linear-gradient(90deg,var(--cy),var(--bl))}
.st-l{font-size:10px;color:var(--mu);text-transform:uppercase;letter-spacing:.7px;font-weight:700}
.st-v{font-size:28px;font-weight:800;margin:6px 0;font-family:var(--mono);line-height:1}
.st-s{font-size:10px;color:var(--mu2);margin-top:4px}
/* Categories drill-down */
.section-title{font-size:16px;font-weight:800;margin:28px 0 14px;display:flex;align-items:center;gap:10px;color:var(--ac2)}
.section-title::before{content:'';display:inline-block;width:4px;height:18px;background:linear-gradient(180deg,var(--ac),var(--or));border-radius:2px}
.cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px}
.cat{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r2);padding:16px 18px;cursor:pointer;transition:.25s;position:relative;overflow:hidden}
.cat:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.4)}
.cat-strip{position:absolute;top:0;left:0;right:0;height:3px}
.cat-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.cat-emoji{font-size:22px}
.cat-name{font-size:14px;font-weight:700}
.cat-count{margin-left:auto;font-family:var(--mono);font-weight:700;font-size:13px}
.cat-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px;padding-top:10px;border-top:1px solid var(--bd)}
.cat-stat{font-size:10px;color:var(--mu2)}
.cat-stat b{display:block;font-family:var(--mono);font-size:14px;color:var(--wh);margin-bottom:2px}
.cat-cov{margin-top:10px}
.cat-cov-bar{height:6px;background:var(--bg);border-radius:3px;overflow:hidden}
.cat-cov-fill{height:100%;background:linear-gradient(90deg,var(--gn),var(--cy));transition:width .8s ease}
.cat-cov-label{display:flex;justify-content:space-between;font-size:10px;color:var(--mu);margin-top:4px}
/* Tool drill-down detail */
.drill{margin-top:14px;padding-top:14px;border-top:1px dashed var(--bd);display:none}
.cat.open .drill{display:block}
.tool-row{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:var(--r1);transition:.2s;font-size:11px}
.tool-row:hover{background:var(--bg3)}
.tool-name{flex:1;font-family:var(--mono);font-weight:600}
.tool-badges{display:flex;gap:4px;flex-shrink:0}
.tool-badge{font-size:9px;padding:2px 6px;border-radius:10px;font-family:var(--mono);font-weight:600}
.tb-files{background:rgba(59,130,246,.15);color:var(--bl2)}
.tb-docker{background:rgba(34,211,238,.15);color:var(--cy)}
.tb-readme{background:rgba(34,197,94,.15);color:var(--gn2)}
.tb-wired{background:rgba(245,158,11,.15);color:var(--ac2)}
/* Skills explorer */
.explorer{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r2);margin-top:14px;padding:18px 20px}
.explorer-head{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:14px}
.search-wrap{display:flex;align-items:center;gap:8px;background:var(--bg);border:1px solid var(--bd2);border-radius:var(--r1);padding:8px 12px;flex:1;max-width:420px}
.search-wrap input{flex:1;background:none;border:none;color:var(--wh);font-family:var(--mono);font-size:12px;outline:none}
.filter-pills{display:flex;gap:6px;flex-wrap:wrap}
.pill{padding:5px 12px;border-radius:20px;border:1px solid var(--bd2);background:var(--bg3);color:var(--mu2);font-size:10px;font-weight:700;cursor:pointer;transition:.2s}
.pill:hover{border-color:var(--ac);color:var(--wh)}
.pill.active{background:var(--ac);color:#000;border-color:var(--ac)}
.skills-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;max-height:400px;overflow-y:auto;padding-right:8px}
.skill-item{background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r1);padding:10px 12px;font-size:11px;transition:.2s;cursor:pointer}
.skill-item:hover{border-color:var(--cy);background:var(--bg4);transform:translateY(-1px)}
.skill-name{font-family:var(--mono);font-weight:600;margin-bottom:3px}
.skill-cat{font-size:9px;color:var(--mu);text-transform:uppercase}
/* Production OSS */
.prod-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;margin-top:10px}
.prod-item{background:var(--bg2);border:1px solid var(--gn);border-radius:var(--r1);padding:12px 14px;position:relative}
.prod-item::before{content:'';position:absolute;top:6px;right:6px;width:8px;height:8px;background:var(--gn);border-radius:50%;box-shadow:0 0 8px var(--gn);animation:pulse 2s ease-in-out infinite}
@keyframes pulse{50%{opacity:.4}}
.prod-name{font-family:var(--mono);font-weight:700;font-size:12px;margin-bottom:2px}
.prod-info{font-size:10px;color:var(--mu)}
.loading{text-align:center;padding:40px;color:var(--mu)}
.footer-n{font-size:10px;color:var(--mu);text-align:center;margin-top:32px;padding-top:14px;border-top:1px solid var(--bd)}
@media(max-width:768px){
.stats{grid-template-columns:repeat(2,1fr)}
.main{padding:14px}
.hdr{padding:14px}
}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-left">
<div class="hdr-logo">OSS</div>
<div>
<h1>WEVAL <span>OSS Discovery</span> V77</h1>
<div class="hdr-sub">72 tools · 6 178 skills · drill-down par catégorie · Enterprise Model UX</div>
</div>
</div>
<div style="display:flex;gap:8px">
<a class="btn" href="/weval-technology-platform.html">← WTP</a>
<a class="btn" href="/enterprise-model.html">🏢 Enterprise Model</a>
<a class="btn btn-ac" href="/agents-archi.html">🤖 Agents Archi</a>
</div>
</header>
<main class="main">
<!-- KPI Summary -->
<div class="stats" id="stats"><div class="loading">Loading...</div></div>
<!-- Categories drill-down -->
<div class="section-title">📦 Catégories (cliquer pour drill-down)</div>
<div class="cats" id="cats"></div>
<!-- Skills Explorer -->
<div class="section-title" style="margin-top:32px">🔎 Skills Explorer · 6 178 skills</div>
<div class="explorer">
<div class="explorer-head">
<div class="search-wrap">
<span>🔍</span>
<input type="text" id="search" placeholder="Search skills by name or category...">
</div>
<div class="filter-pills" id="filter-pills">
<div class="pill active" data-cat="all">All</div>
</div>
</div>
<div class="skills-grid" id="skills-grid">
<div class="loading">Loading skills...</div>
</div>
<div style="font-size:10px;color:var(--mu);margin-top:12px;text-align:center">
Affichage de <b id="skills-count">0</b> skills · 694 injected in Qdrant vectors (coverage 11.2%)
</div>
</div>
<!-- Production OSS -->
<div class="section-title" style="margin-top:32px">🟢 Production OSS (Docker déployé live)</div>
<div class="prod-grid" id="prod-grid"></div>
<div class="footer-n">
V77 OSS Discovery enriched · Style Enterprise Model · Doctrine #60 UX premium · Cross-linked with WTP+Enterprise+Archi
</div>
</main>
<script>
// Sample skills list from category names (real skills stored in Qdrant, sample here for UX)
const SAMPLE_SKILLS_PER_CAT = {
llm_core: ['gpt-wrapper','claude-bridge','mistral-adapter','ollama-local','vllm-server','text-generation-webui','prompt-optimizer','token-counter','llm-proxy','fallback-cascade'],
agents: ['dspy-agent','crewai-swarm','langgraph-flow','autogen-team','superclaude-framework','claude-skills','agent-router','mcp-server','tool-dispatcher','skill-injector','task-planner','reasoning-agent','retrieval-agent','code-agent'],
automation: ['n8n-workflow','activepieces-flow','temporal-pipeline','cron-scheduler','webhook-dispatch','event-router'],
observability: ['grafana-dash','prometheus-scrape','loki-logs','langfuse-trace','uptime-kuma','opentelemetry-tracer'],
dev_tools: ['claude-code','vscode-ext','docker-compose','git-hooks','jetbrains-plugin','eslint-config','prettier-setup','typescript-types','pytest-runner','jest-suite'],
rag_vector: ['qdrant-client','milvus-index','pinecone-wrapper','weaviate-client','chroma-store','embeddings-batch','semantic-search','hybrid-rerank'],
security: ['crowdsec-rules','nuclei-templates','keycloak-sso','vault-secrets','fail2ban-config','auth-middleware'],
weval_own: ['wevia-master','wevia-brain','weval-l99','weval-nonreg','wevads-arsenal','ethica-pipeline','paperclip-db','deerflow','mirofish','blade-ops','sentinel-lite','dynamic-resolver']
};
let ALL_DATA = null;
let FILTER_CAT = 'all';
let SEARCH = '';
async function load() {
try {
const r = await fetch('/api/v77-oss-discovery-enriched.php?t=' + Date.now());
ALL_DATA = await r.json();
renderStats();
renderCategories();
renderSkills();
renderProduction();
} catch(e) {
document.getElementById('stats').innerHTML = '<div class="loading" style="color:#ef4444">Error loading: ' + e.message + '</div>';
}
}
function renderStats() {
const s = ALL_DATA.summary;
document.getElementById('stats').innerHTML = `
<div class="stat s-ac"><div class="st-l">Total tools</div><div class="st-v">${s.total_tools}</div><div class="st-s">OSS integrated in stack</div></div>
<div class="stat s-gn"><div class="st-l">Wired</div><div class="st-v">${s.wired_tools}/${s.total_tools}</div><div class="st-s">All connected to WEVIA</div></div>
<div class="stat s-cy"><div class="st-l">Total skills</div><div class="st-v">${s.total_skills.toLocaleString()}</div><div class="st-s">Across 72 tools</div></div>
<div class="stat s-bl"><div class="st-l">Injected in RAG</div><div class="st-v">${s.injected_skills.toLocaleString()}</div><div class="st-s">Qdrant vectors live</div></div>
<div class="stat s-pk"><div class="st-l">Coverage</div><div class="st-v">${s.coverage_pct}%</div><div class="st-s">Injected / Total</div></div>
<div class="stat s-gn"><div class="st-l">With Docker</div><div class="st-v">${s.with_docker}</div><div class="st-s">Production-ready</div></div>
<div class="stat s-ac"><div class="st-l">With README</div><div class="st-v">${s.with_readme}</div><div class="st-s">Documented</div></div>
<div class="stat s-cy"><div class="st-l">Production</div><div class="st-v">${s.production_count}</div><div class="st-s">Docker + wired live</div></div>
`;
}
function renderCategories() {
const pills = document.getElementById('filter-pills');
const cats = document.getElementById('cats');
cats.innerHTML = '';
// Add filter pills
const existingPills = pills.querySelectorAll('.pill:not([data-cat="all"])');
existingPills.forEach(p => p.remove());
ALL_DATA.categories.forEach((c, i) => {
// Category card
const el = document.createElement('div');
el.className = 'cat';
el.dataset.catIdx = i;
el.innerHTML = `
<div class="cat-strip" style="background:${c.color}"></div>
<div class="cat-head">
<div class="cat-emoji">${c.emoji}</div>
<div class="cat-name">${c.label}</div>
<div class="cat-count" style="color:${c.color}">${c.count}</div>
</div>
<div class="cat-stats">
<div class="cat-stat"><b>${c.total_files}</b>files</div>
<div class="cat-stat"><b>${c.est_skills.toLocaleString()}</b>skills est.</div>
<div class="cat-stat"><b>${c.est_injected}</b>injected</div>
</div>
<div class="cat-cov">
<div class="cat-cov-bar"><div class="cat-cov-fill" style="width:${c.coverage_pct}%;background:linear-gradient(90deg,${c.color},var(--cy))"></div></div>
<div class="cat-cov-label"><span>Coverage</span><span>${c.coverage_pct}%</span></div>
</div>
<div class="drill">
<div style="font-size:10px;color:var(--mu);text-transform:uppercase;margin-bottom:6px;font-weight:700">Tools (${c.tools.length})</div>
${c.tools.map(t => `
<div class="tool-row">
<div class="tool-name">${t.name}</div>
<div class="tool-badges">
<span class="tool-badge tb-files">${t.files}f</span>
${t.has_docker ? '<span class="tool-badge tb-docker">🐳</span>' : ''}
${t.has_readme ? '<span class="tool-badge tb-readme">📖</span>' : ''}
${t.wired ? '<span class="tool-badge tb-wired">⚡</span>' : ''}
</div>
</div>
`).join('')}
</div>
`;
el.onclick = (ev) => {
if (ev.target.closest('.tool-row')) return;
el.classList.toggle('open');
};
cats.appendChild(el);
// Filter pill
const p = document.createElement('div');
p.className = 'pill';
p.dataset.cat = Object.keys({llm_core:1,agents:1,automation:1,observability:1,dev_tools:1,rag_vector:1,security:1,weval_own:1})[i];
p.textContent = c.emoji + ' ' + c.label;
p.onclick = () => {
document.querySelectorAll('.pill').forEach(x => x.classList.remove('active'));
p.classList.add('active');
FILTER_CAT = p.dataset.cat;
renderSkills();
};
pills.appendChild(p);
});
}
function renderSkills() {
const grid = document.getElementById('skills-grid');
const catKeys = ['llm_core','agents','automation','observability','dev_tools','rag_vector','security','weval_own'];
let items = [];
catKeys.forEach((k,i) => {
(SAMPLE_SKILLS_PER_CAT[k]||[]).forEach(s => {
items.push({ name: s, cat: k, catLabel: ALL_DATA.categories[i]?.label || k, color: ALL_DATA.categories[i]?.color || '#64748b' });
});
});
// Filter
if (FILTER_CAT !== 'all') items = items.filter(x => x.cat === FILTER_CAT);
if (SEARCH) items = items.filter(x => x.name.toLowerCase().includes(SEARCH) || x.catLabel.toLowerCase().includes(SEARCH));
document.getElementById('skills-count').textContent = items.length;
grid.innerHTML = items.map(x => `
<div class="skill-item" style="border-left:3px solid ${x.color}">
<div class="skill-name">${x.name}</div>
<div class="skill-cat">${x.catLabel}</div>
</div>
`).join('') || '<div class="loading">No skills match filter</div>';
}
function renderProduction() {
const grid = document.getElementById('prod-grid');
const CAT_EMOJI = {llm_core:'🧠',agents:'🤖',automation:'⚡',observability:'👁️',dev_tools:'🛠️',rag_vector:'🔍',security:'🛡️',weval_own:'👑'};
grid.innerHTML = ALL_DATA.production_tools.map(t => `
<div class="prod-item">
<div class="prod-name">${CAT_EMOJI[t.category]||'📦'} ${t.name}</div>
<div class="prod-info">${t.files} files · ${t.category.replace('_',' ')}</div>
</div>
`).join('') || '<div class="loading">No production tools</div>';
}
document.getElementById('search').addEventListener('input', e => {
SEARCH = e.target.value.toLowerCase();
renderSkills();
});
load();
setInterval(load, 60000);
</script>
</body>
</html>