Files
html/weval-enterprise-management.html

879 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WEVAL Enterprise Management — Hub Unifié</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-main: #0a0e1a;
--bg-card: rgba(30, 41, 59, 0.65);
--bg-card-hover: rgba(51, 65, 85, 0.85);
--border: rgba(148, 163, 184, 0.15);
--border-h: rgba(99, 102, 241, 0.5);
--text: #e2e8f0;
--text-dim: #94a3b8;
--text-muted: #64748b;
--accent: #6366f1;
--accent2: #ec4899;
--success: #10b981;
--warn: #f59e0b;
--danger: #dc2626;
--gradient-1: linear-gradient(135deg, #6366f1 0%, #ec4899 100%);
--gradient-2: linear-gradient(135deg, #10b981 0%, #06b6d4 100%);
--gradient-3: linear-gradient(135deg, #f59e0b 0%, #dc2626 100%);
--gradient-4: linear-gradient(135deg, #8b5cf6 0%, #3b82f6 100%);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
background: var(--bg-main);
color: var(--text);
min-height: 100vh;
background-image:
radial-gradient(at 15% 10%, rgba(99, 102, 241, 0.12) 0px, transparent 50%),
radial-gradient(at 85% 90%, rgba(236, 72, 153, 0.08) 0px, transparent 50%),
radial-gradient(at 50% 50%, rgba(16, 185, 129, 0.04) 0px, transparent 70%);
background-attachment: fixed;
}
.container { max-width: 1600px; margin: 0 auto; padding: 24px; }
/* HERO */
.hero {
background: var(--gradient-1);
border-radius: 20px;
padding: 36px 40px;
margin-bottom: 28px;
position: relative;
overflow: hidden;
box-shadow: 0 20px 60px rgba(99, 102, 241, 0.25);
}
.hero::before {
content: '';
position: absolute;
top: -50%; right: -10%;
width: 400px; height: 400px;
background: radial-gradient(circle, rgba(255,255,255,0.15), transparent 70%);
border-radius: 50%;
}
.hero h1 {
font-size: 36px;
font-weight: 800;
color: white;
letter-spacing: -1px;
margin-bottom: 10px;
position: relative;
z-index: 2;
}
.hero .subtitle {
color: rgba(255,255,255,0.85);
font-size: 15px;
margin-bottom: 22px;
position: relative;
z-index: 2;
}
.hero-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
position: relative;
z-index: 2;
}
.stat-card {
background: rgba(255,255,255,0.13);
backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 14px;
padding: 16px;
}
.stat-val { font-size: 26px; font-weight: 800; color: white; }
.stat-lbl { font-size: 11px; color: rgba(255,255,255,0.8); text-transform: uppercase; letter-spacing: 1px; margin-top: 4px; }
/* SEARCH */
.search-bar {
position: sticky;
top: 0;
z-index: 100;
background: rgba(10, 14, 26, 0.95);
backdrop-filter: blur(20px);
margin: 0 -24px 24px;
padding: 16px 24px;
border-bottom: 1px solid var(--border);
}
.search-bar input {
width: 100%;
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: 14px;
padding: 14px 20px 14px 52px;
color: var(--text);
font-size: 15px;
outline: none;
transition: all .2s;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'><circle cx='11' cy='11' r='8'/><line x1='21' y1='21' x2='16.65' y2='16.65'/></svg>");
background-repeat: no-repeat;
background-position: 18px center;
}
.search-bar input:focus { border-color: var(--accent); }
/* QUICK ACTIONS */
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 12px;
margin-bottom: 32px;
}
.qa-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
cursor: pointer;
transition: all .2s;
display: flex;
align-items: center;
gap: 14px;
}
.qa-card:hover { border-color: var(--border-h); background: var(--bg-card-hover); transform: translateY(-2px); }
.qa-icon { font-size: 24px; }
.qa-label { font-weight: 600; font-size: 14px; }
.qa-chat { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
/* CATEGORIES TABS */
.tabs {
display: flex;
gap: 8px;
overflow-x: auto;
margin-bottom: 20px;
padding-bottom: 8px;
}
.tab {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 16px;
cursor: pointer;
white-space: nowrap;
font-size: 13px;
font-weight: 500;
transition: all .15s;
}
.tab:hover { border-color: var(--border-h); }
.tab.active { background: var(--gradient-1); border-color: transparent; color: white; }
/* SECTION GRID */
.section { margin-bottom: 40px; }
.section-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.section-count {
font-size: 12px;
background: rgba(99, 102, 241, 0.2);
color: #a5b4fc;
padding: 3px 10px;
border-radius: 10px;
font-weight: 600;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
}
.page-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 16px;
cursor: pointer;
transition: all .15s;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
text-decoration: none;
color: var(--text);
}
.page-card:hover { border-color: var(--border-h); background: var(--bg-card-hover); transform: translateX(4px); }
.page-card .name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.page-card .arrow { color: var(--text-muted); font-size: 18px; flex-shrink: 0; }
/* SERVICES */
.services {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
margin-bottom: 32px;
}
.service-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 18px;
transition: all .2s;
position: relative;
overflow: hidden;
}
.service-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 3px;
background: var(--gradient-2);
}
.service-card:hover { border-color: var(--border-h); transform: translateY(-3px); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.service-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.service-name { font-size: 16px; font-weight: 700; }
.service-status {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 10px var(--success);
}
.service-status.down { background: var(--danger); box-shadow: 0 0 10px var(--danger); }
.service-status.unknown { background: var(--text-muted); }
.service-desc { font-size: 13px; color: var(--text-dim); margin-bottom: 10px; }
.service-url {
font-family: 'SF Mono', 'Courier New', monospace;
font-size: 11px;
color: var(--text-muted);
background: rgba(0,0,0,0.25);
padding: 4px 8px;
border-radius: 6px;
display: inline-block;
margin-bottom: 10px;
}
.service-btn {
display: inline-block;
background: var(--gradient-4);
padding: 8px 14px;
border-radius: 8px;
color: white;
font-size: 12px;
font-weight: 600;
text-decoration: none;
margin-top: 4px;
}
/* WEVIA */
.wevia-card {
background: var(--gradient-4);
border-radius: 18px;
padding: 28px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
box-shadow: 0 15px 40px rgba(139, 92, 246, 0.3);
}
.wevia-card::before {
content: '🧠';
position: absolute;
font-size: 200px;
top: -40px; right: -40px;
opacity: 0.08;
}
.wevia-card h2 {
color: white;
font-size: 24px;
margin-bottom: 8px;
position: relative;
z-index: 2;
}
.wevia-sub { color: rgba(255,255,255,0.85); margin-bottom: 18px; position: relative; z-index: 2; }
.intent-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 10px;
position: relative;
z-index: 2;
}
.intent-card {
background: rgba(255,255,255,0.12);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
padding: 12px 14px;
cursor: pointer;
transition: all .15s;
}
.intent-card:hover { background: rgba(255,255,255,0.22); transform: translateX(3px); }
.intent-trigger { color: white; font-weight: 600; font-size: 13px; }
.intent-tool { color: rgba(255,255,255,0.7); font-size: 11px; margin-top: 3px; font-family: monospace; }
/* CHAT WIDGET */
.wevia-chat-bar {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--gradient-1);
color: white;
padding: 14px 24px;
border-radius: 50px;
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.5);
cursor: pointer;
font-weight: 600;
z-index: 1000;
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
transition: all .2s;
}
.wevia-chat-bar:hover { transform: translateY(-3px); box-shadow: 0 15px 40px rgba(99, 102, 241, 0.7); }
.hide { display: none !important; }
footer {
text-align: center;
color: var(--text-muted);
padding: 30px;
font-size: 12px;
border-top: 1px solid var(--border);
margin-top: 40px;
}
.breadcrumbs { color: var(--text-dim); font-size: 13px; margin-bottom: 20px; }
.breadcrumbs a { color: var(--accent); text-decoration: none; }
/* === Doctrine 84 — Premium page tiles (thumb + title + HTTP badge) === */
.page-tile { display:flex; flex-direction:column; background:var(--bg-card,#13182a); border:1px solid var(--border,#22283d); border-radius:12px; overflow:hidden; text-decoration:none; transition:all .2s ease; cursor:pointer; }
.page-tile:hover { transform:translateY(-2px); border-color:#4f46e5; box-shadow:0 8px 24px rgba(79,70,229,.18); }
.page-tile .thumb-wrap { position:relative; width:100%; aspect-ratio:16/10; background:#0a0f1e; overflow:hidden; }
.page-tile .thumb-img { width:100%; height:100%; object-fit:cover; display:block; background:#0a0f1e; }
.page-tile .thumb-img.thumb-missing { opacity:.2; }
.page-tile .thumb-ext { width:100%; height:100%; display:flex; align-items:center; justify-content:center; color:#4f46e5; font-size:14px; font-weight:600; letter-spacing:1px; background:linear-gradient(135deg,#1a1f35,#0a0f1e); }
.page-tile .http-badge { position:absolute; top:8px; right:8px; padding:3px 8px; border-radius:6px; font-size:11px; font-weight:600; font-family:monospace; backdrop-filter:blur(6px); }
.page-tile .http-badge.b-ok { background:rgba(16,185,129,.85); color:#fff; }
.page-tile .http-badge.b-redir { background:rgba(251,191,36,.85); color:#1f2937; }
.page-tile .http-badge.b-err { background:rgba(239,68,68,.85); color:#fff; }
.page-tile .http-badge.b-unk { background:rgba(100,116,139,.75); color:#fff; }
.page-tile .tile-meta { padding:10px 12px; }
.page-tile .tile-title { font-size:13px; font-weight:500; color:var(--text,#e2e8f0); margin-bottom:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.page-tile .tile-path { font-size:11px; color:var(--text-muted,#94a3b8); font-family:monospace; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:14px; }
/* Skeleton loader while meta fetch in progress */
.page-tile.loading .thumb-img { background:linear-gradient(90deg,#0a0f1e 0%,#1a1f35 50%,#0a0f1e 100%); background-size:200% 100%; animation:shimmer 1.5s infinite; }
@keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
</style>
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143847 -->
<style id="doctrine60-ux-direct">
/* DOCTRINE-60-UX-ENRICH injected-direct */
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
pointer-events: none;
z-index: -1;
}
.card, .kpi, .panel, .btn {
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
}
.card:hover, .kpi:hover, .panel:hover {
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
border-color: rgba(100,180,255,0.5);
}
@keyframes pulseD60 {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.pulse, .live-indicator, .active, .online {
animation: pulseD60 3s ease-in-out infinite;
}
.modal, .chat, .speech, .overlay {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.enter-stagger {
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
}
@keyframes enterStagD60 {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div class="container">
<div class="breadcrumbs">WEVAL &rsaquo; Enterprise Management Hub</div>
<div class="hero">
<h1>🏛️ WEVAL Enterprise Management</h1>
<div class="subtitle">Hub unifié — toute notre architecture, accessible en un clic. Zero régression, enrichissement continu.</div>
<div class="hero-stats">
<div class="stat-card"><div class="stat-val" id="statPages"></div><div class="stat-lbl">Screens</div></div>
<div class="stat-card"><div class="stat-val" id="statApis"></div><div class="stat-lbl">APIs REST</div></div>
<div class="stat-card"><div class="stat-val" id="statWiki"></div><div class="stat-lbl">Wiki Entries</div></div>
<div class="stat-card"><div class="stat-val" id="statVault"></div><div class="stat-lbl">Vault Docs</div></div>
<div class="stat-card"><div class="stat-val" id="statSvc"></div><div class="stat-lbl">Services</div></div>
<div class="stat-card"><div class="stat-val" id="statIntents"></div><div class="stat-lbl">WEVIA Intents</div></div>
<div class="stat-card"><div class="stat-val" id="statGrand"></div><div class="stat-lbl">GRAND TOTAL</div></div>
<div class="stat-card"><div class="stat-val"><span id="statHealth"></span>%</div><div class="stat-lbl">Health</div></div>
</div>
</div>
<div class="search-bar">
<input id="search" type="text" placeholder="🔎 Chercher une page, API, service, ou intent..." autocomplete="off">
</div>
<!-- QUICK ACTIONS -->
<div class="section">
<div class="section-title">⚡ Quick Actions <span class="section-count" id="qaCount">0</span></div>
<div class="quick-actions" id="quickActions"></div>
</div>
<!-- WEVIA MASTER -->
<div class="wevia-card">
<h2>🧠 WEVIA Master — Intents via Chat NL</h2>
<div class="wevia-sub">Pilote toute la plateforme en langage naturel. Clique un intent pour l'exécuter, ou ouvre le chat.</div>
<div class="intent-grid" id="intentsGrid"></div>
</div>
<!-- SERVICES -->
<div class="section">
<div class="section-title">🧬 Services & Capabilities <span class="section-count" id="svcCount">0</span></div>
<div class="services" id="servicesGrid"></div>
</div>
<!-- CATEGORIES FILTER -->
<div class="tabs" id="tabs"></div>
<!-- PAGES BY CATEGORY -->
<div id="categoriesContainer"></div>
<!-- WIKI -->
<div class="section">
<div class="section-title">📚 Wiki Entries <span class="section-count" id="wikiCount">0</span></div>
<input id="wikiSearch" type="text" placeholder="🔎 Chercher dans le wiki..." style="width:100%;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;color:var(--text);margin-bottom:12px;font-size:13px">
<div class="grid" id="wikiGrid"></div>
</div>
<!-- VAULT -->
<div class="section">
<div class="section-title">🔒 Vault Docs <span class="section-count" id="vaultCount">0</span></div>
<div class="grid" id="vaultGrid"></div>
</div>
<footer>
Généré via <a href="/wevia-master.html" style="color: var(--accent);">WEVIA Master</a>
Plan action V23+ —
<a href="/api/plan-action-dp.md" style="color: var(--accent);">plan-action-dp.md</a>
<a href="/wiki/P0-BRIEF-DECISIONNEL-17avr.md" style="color: var(--accent);">P0 Brief</a>
</footer>
</div>
<a href="/wevia-master.html" class="wevia-chat-bar">💬 Chat WEVIA Master</a>
<script>
let INVENTORY = null;
let CURRENT_CAT = 'all';
let SEARCH = '';
async function loadData() {
try {
// Doctrine 84 : preload page meta (titles + HTTP status) in parallel with inventory
const [rInv, rMeta] = await Promise.all([
fetch('/api/wem-inventory.json'),
fetch('/api/wem-page-meta.json').catch(() => null)
]);
INVENTORY = await rInv.json();
if (rMeta && rMeta.ok) {
try { window.WEM_META = await rMeta.json(); } catch(e) { window.WEM_META = {}; }
} else { window.WEM_META = {}; }
render();
checkHealth();
checkServices();
} catch(e) {
console.error('load err', e);
document.body.innerHTML = '<div style="padding:40px;text-align:center;color:#dc2626">Erreur chargement inventaire. Vérifiez /api/wem-inventory.json</div>';
}
}
function checkHealth() {
fetch('/api/screens-health.json').then(r => r.json()).then(d => {
const tot = d.total || 0;
const hth = (d.counts?.UP||0) + (d.counts?.SLOW||0) + (d.counts?.PROTECTED||0);
const pct = tot > 0 ? Math.round(hth/tot*100) : 0;
document.getElementById('statHealth').textContent = pct;
}).catch(()=>{});
}
async function checkServices() {
// Probe WEVIA Master for capabilities inventory
try {
const r = await fetch('/api/wevia-master-api.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({message: 'capabilities inventory', session: 'wem', history: []})
});
const d = await r.json();
const content = d.content || '';
// Parse "✅ Service (HTTP 200)" lines
document.querySelectorAll('.service-card').forEach(card => {
const name = card.dataset.name;
if (content.includes('✅ ' + name)) {
card.querySelector('.service-status').classList.remove('down','unknown');
} else if (content.includes('❌ ' + name)) {
card.querySelector('.service-status').classList.add('down');
}
});
} catch(e) {}
}
function render() {
// Stats — WEM_V2_WIKI_VAULT
document.getElementById('statPages').textContent = INVENTORY.total_screens || INVENTORY.total_pages;
document.getElementById('statApis').textContent = INVENTORY.total_apis;
if (document.getElementById('statWiki')) document.getElementById('statWiki').textContent = INVENTORY.total_wiki || 0;
if (document.getElementById('statVault')) document.getElementById('statVault').textContent = INVENTORY.total_vault || 0;
document.getElementById('statSvc').textContent = INVENTORY.total_services;
document.getElementById('statIntents').textContent = INVENTORY.total_intents + '+';
if (document.getElementById('statGrand')) document.getElementById('statGrand').textContent = INVENTORY.grand_total || INVENTORY.total_pages;
// Quick actions
const qa = document.getElementById('quickActions');
qa.innerHTML = INVENTORY.quick_actions.map(a => `
<div class="qa-card" onclick="weviaChat('${a.chat.replace(/'/g,"\\'")}')">
<div class="qa-icon">${a.icon}</div>
<div style="flex:1;min-width:0">
<div class="qa-label">${a.label}</div>
<div class="qa-chat">${a.chat}</div>
</div>
</div>
`).join('');
document.getElementById('qaCount').textContent = INVENTORY.quick_actions.length;
// Intents
const ig = document.getElementById('intentsGrid');
ig.innerHTML = INVENTORY.wevia_intents.map(i => `
<div class="intent-card" onclick="weviaChat('${i.trigger.split('/')[0].trim().replace(/'/g,"\\'")}')">
<div class="intent-trigger">${i.trigger}</div>
<div class="intent-tool">→ ${i.tool} · ${i.cat}</div>
</div>
`).join('');
// Services
const sg = document.getElementById('servicesGrid');
sg.innerHTML = Object.entries(INVENTORY.external_services).map(([name, svc]) => `
<div class="service-card" data-name="${name}">
<div class="service-head">
<div class="service-name">${name}</div>
<div class="service-status unknown" title="Status"></div>
</div>
<div class="service-desc">${svc.desc}</div>
<div class="service-url">${svc.url}</div>
<br>
<a href="${svc.public}" class="service-btn">Ouvrir Hub</a>
<a href="${svc.url}" target="_blank" class="service-btn" style="background:rgba(255,255,255,0.15);margin-left:6px">Direct</a>
</div>
`).join('');
document.getElementById('svcCount').textContent = Object.keys(INVENTORY.external_services).length;
// Tabs + categories
const tabs = document.getElementById('tabs');
const cats = Object.keys(INVENTORY.categorized_pages).sort((a,b) => INVENTORY.categorized_pages[b].length - INVENTORY.categorized_pages[a].length);
tabs.innerHTML = `<div class="tab active" onclick="filterCat('all')">Toutes (${INVENTORY.total_pages})</div>` +
cats.map(c => `<div class="tab" onclick="filterCat('${c}')">${c} (${INVENTORY.categorized_pages[c].length})</div>`).join('');
renderCategories();
}
function renderCategories() {
const container = document.getElementById('categoriesContainer');
const cats = Object.keys(INVENTORY.categorized_pages).sort((a,b) => INVENTORY.categorized_pages[b].length - INVENTORY.categorized_pages[a].length);
container.innerHTML = cats.filter(c => CURRENT_CAT === 'all' || c === CURRENT_CAT).map(c => {
const pages = INVENTORY.categorized_pages[c].filter(p => !SEARCH || p.toLowerCase().includes(SEARCH.toLowerCase()));
if (pages.length === 0 && SEARCH) return '';
return `
<div class="section">
<div class="section-title">📂 ${c} <span class="section-count">${pages.length}</span></div>
<div class="grid">
${pages.map(p => {
const url = p.startsWith('http') ? p : (p.startsWith('/') ? p : '/' + p);
// Build meta lookup key matching worker's key convention
let pathKey = null;
let displayPath = p;
const mW = p.match(/^https?:\/\/wevads\.weval-consulting\.com\/(.+)$/);
const mE = p.match(/^https?:\/\/ethica\.wevup\.app\/(.+)$/);
const mM = p.match(/^https?:\/\/(?:www\.)?weval-consulting\.com\/(.+)$/);
if (mW) { pathKey = 'wevads/' + mW[1]; displayPath = mW[1]; }
else if (mE) { pathKey = 'ethica/' + mE[1]; displayPath = mE[1]; }
else if (mM) { pathKey = mM[1]; displayPath = mM[1]; }
else if (!p.startsWith('http')) { pathKey = p.replace(/^\//, ''); displayPath = pathKey; }
// else: external 3rd party → pathKey stays null
const isExt = !pathKey;
const meta = (window.WEM_META && pathKey && window.WEM_META[pathKey]) || {};
const title = meta.title || displayPath.replace(/\.(html|php|md|json)$/,'').replace(/[\/\-_]/g, ' ').substring(0, 60);
const http = meta.http || 0;
const mtime = meta.mtime || '';
const thumbUrl = isExt ? '' : ('/api/wem-screen-thumb.php?path=' + encodeURIComponent(pathKey));
const badgeClass = http >= 200 && http < 300 ? 'b-ok' : (http >= 300 && http < 400 ? 'b-redir' : (http >= 400 ? 'b-err' : 'b-unk'));
const badgeText = http > 0 ? http : '...';
const thumbHtml = isExt
? '<div class="thumb-ext"><span>EXT</span></div>'
: `<img class="thumb-img" loading="lazy" src="${thumbUrl}" alt="" onerror="this.classList.add('thumb-missing')">`;
return `<a class="page-tile" href="${url}" target="_blank" title="${title} (${p})">
<div class="thumb-wrap">
${thumbHtml}
<span class="http-badge ${badgeClass}">${badgeText}</span>
</div>
<div class="tile-meta">
<div class="tile-title">${title}</div>
<div class="tile-path">${displayPath.length > 42 ? displayPath.substring(0,40)+'…' : displayPath}${mtime ? ' · ' + mtime : ''}</div>
</div>
</a>`;
}).join('')}
</div>
</div>`;
}).join('');
}
function filterCat(c) {
CURRENT_CAT = c;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
renderCategories();
}
function weviaChat(msg) {
window.open('/wevia-master.html?q=' + encodeURIComponent(msg), '_blank');
}
document.getElementById('search').addEventListener('input', (e) => {
SEARCH = e.target.value;
renderCategories();
});
function renderWiki(filter = '') {
const wg = document.getElementById('wikiGrid');
if (!wg || !INVENTORY.wiki_pages) return;
const pages = INVENTORY.wiki_pages.filter(p => !filter || p.toLowerCase().includes(filter.toLowerCase()));
wg.innerHTML = pages.slice(0, 300).map(p => {
const name = p.replace(/\.json$/,'').replace(/^-/,'').replace(/-/g,' / ').substring(0, 70);
const url = p.endsWith('.md') ? '/wiki/' + p : '/wiki/view/' + encodeURIComponent(p);
return `<a class="page-card" href="${url}" target="_blank"><span class="name">${name}</span><span class="arrow"></span></a>`;
}).join('');
document.getElementById('wikiCount').textContent = pages.length + ' / ' + INVENTORY.total_wiki;
}
function renderVault() {
const vg = document.getElementById('vaultGrid');
if (!vg || !INVENTORY.vault_pages) return;
vg.innerHTML = INVENTORY.vault_pages.map(p => {
const name = p.replace(/\.(md|json)$/,'').substring(0, 70);
return `<div class="page-card" onclick="weviaChat('show vault ${p}')"><span class="name">${name}</span><span class="arrow"></span></div>`;
}).join('');
document.getElementById('vaultCount').textContent = INVENTORY.vault_pages.length + ' / ' + INVENTORY.total_vault;
}
// Wire wiki search
setTimeout(() => {
const ws = document.getElementById('wikiSearch');
if (ws) ws.addEventListener('input', e => renderWiki(e.target.value));
renderWiki();
renderVault();
}, 500);
loadData();
// Refresh health every 60s
setInterval(() => { checkHealth(); }, 60000);
</script>
<!-- LIVE KPI INJECTION — 17avr2026 — AJOUT ONLY -->
<script>
(function liveKPI(){
fetch('/api/em-live-kpi.php').then(r=>r.json()).then(d=>{
const el=id=>document.getElementById(id);
if(el('statPages'))el('statPages').textContent=d.assets.html_pages;
if(el('statApis'))el('statApis').textContent=d.assets.php_apis;
if(el('statWiki'))el('statWiki').textContent=d.assets.wiki_entries;
if(el('statVault'))el('statVault').textContent=(d.assets.vault_doctrines||0)+(d.assets.vault_sessions||0)+(d.assets.vault_decisions||0);
if(el('statSvc'))el('statSvc').textContent=d.services.filter(s=>s.status==='UP').length;
if(el('statIntents'))el('statIntents').textContent=d.tools.total;
if(el('statGrand'))el('statGrand').textContent=d.grand_total;
if(el('statHealth')){el('statHealth').textContent=d.health.pct+'%';el('statHealth').style.color=d.health.pct===100?'#4ade80':'#f59e0b';}
if(document.getElementById('liveKpiPanel'))return;
var p=document.createElement('div');p.id='liveKpiPanel';
p.style.cssText='margin:12px 24px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;';
var cards=[
{i:'\u2699',t:'S204 Server',v:d.s204.load+' load | '+(d.s204.ram_free_mb/1024).toFixed(1)+'GB free',s:d.s204.disk_pct+' disk | '+d.s204.fpm_workers+' FPM | '+d.s204.docker_containers+' Docker | '+d.s204.cpu_cores+' CPU',c:'#3b82f6'},
{i:'\ud83d\udda5',t:'S95 WEVADS',v:d.s95.status+(d.s95.load?' | Load '+d.s95.load:''),s:d.s95.disk_pct?'Disk '+d.s95.disk_pct+(d.s95.ram_free_mb?' | '+Math.round(d.s95.ram_free_mb/1024)+'GB free':''):'',c:d.s95.status==='UP'?'#10b981':'#ef4444'},
{i:'\ud83e\udde0',t:'Sovereign IA',v:d.sovereign.active+'/'+d.sovereign.total+' providers | '+d.sovereign.cost,s:'Primary: '+d.sovereign.primary,c:'#8b5cf6'},
{i:'\ud83c\udfe5',t:'Ethica HCP',v:d.ethica.total_hcps.toLocaleString()+' HCPs',s:d.ethica.with_email.toLocaleString()+' emails ('+d.ethica.pct_email+'%) | Gap: '+d.ethica.gap_email.toLocaleString(),c:'#ec4899'},
{i:'\ud83d\udd0a',t:'Whisper STT',v:d.whisper.binary,s:'Model: '+d.whisper.model,c:d.whisper.binary==='COMPILED'?'#10b981':'#6b7280'},
{i:'\ud83d\udcbe',t:'Git',v:d.git.status,s:(d.git.head||'').substring(0,35),c:d.git.status==='CLEAN'?'#10b981':'#f59e0b'},
{i:'\u2705',t:'NonReg Playwright',v:d.nonreg.passed+'/'+d.nonreg.total,s:d.nonreg.score+' — 6\u03c3',c:'#10b981'},
{i:'\ud83d\udd17',t:'Services Live',v:d.services.filter(s=>s.status==='UP').length+'/'+d.services.length+' UP',s:d.services.map(s=>s.name.split(' ')[0]+':'+(s.status==='UP'?'\u2705':'\u274c')).join(' '),c:'#06b6d4'}
];
cards.forEach(function(c){
var dv=document.createElement('div');
dv.style.cssText='background:rgba(15,20,40,0.7);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:14px;border-left:3px solid '+c.c+';cursor:pointer;transition:transform .2s;';
dv.onmouseenter=function(){this.style.transform='translateY(-2px)';};
dv.onmouseleave=function(){this.style.transform='none';};
dv.innerHTML='<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><span style="font-size:18px">'+c.i+'</span><span style="color:'+c.c+';font-weight:600;font-size:13px">'+c.t+'</span></div><div style="color:#e2e8f0;font-size:15px;font-weight:600">'+c.v+'</div><div style="color:#94a3b8;font-size:11px;margin-top:4px">'+c.s+'</div>';
p.appendChild(dv);
});
var pmta=document.createElement('div');
pmta.style.cssText='background:rgba(15,20,40,0.7);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:14px;border-left:3px solid #f97316;';
pmta.innerHTML='<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><span style="font-size:18px">\ud83d\udce7</span><span style="color:#f97316;font-weight:600;font-size:13px">PMTA ECS</span></div><div style="color:#e2e8f0;font-size:13px">'+d.pmta.map(function(x){return x.name+': '+(x.status==='UP'?'\u2705':'\u26a0\ufe0f');}).join(' | ')+'</div>';
p.appendChild(pmta);
var dk=document.createElement('div');
dk.style.cssText='background:rgba(15,20,40,0.7);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:14px;border-left:3px solid #06b6d4;grid-column:span 2;';
dk.innerHTML='<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><span style="font-size:18px">\ud83d\udc33</span><span style="color:#06b6d4;font-weight:600;font-size:13px">Docker ('+d.docker.length+')</span></div><div style="display:flex;flex-wrap:wrap;gap:4px">'+d.docker.map(function(c){return '<span style="background:rgba(6,182,212,0.15);color:#67e8f9;padding:2px 8px;border-radius:6px;font-size:11px">'+c.name+'</span>';}).join('')+'</div>';
p.appendChild(dk);
if(d.ethica.by_country&&d.ethica.by_country.length){
var eth=document.createElement('div');
eth.style.cssText='background:rgba(15,20,40,0.7);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:14px;border-left:3px solid #ec4899;grid-column:span 2;';
eth.innerHTML='<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span style="font-size:18px">\ud83c\udf0d</span><span style="color:#ec4899;font-weight:600;font-size:13px">Ethica par pays</span></div><div style="display:flex;gap:20px;flex-wrap:wrap">'+d.ethica.by_country.map(function(c){return '<div style="text-align:center"><div style="color:#e2e8f0;font-size:18px;font-weight:700">'+parseInt(c.t).toLocaleString()+'</div><div style="color:#94a3b8;font-size:11px">'+c.country+' ('+parseInt(c.e).toLocaleString()+' emails)</div></div>';}).join('')+'</div>';
p.appendChild(eth);
}
var ts=document.createElement('div');
ts.style.cssText='grid-column:1/-1;text-align:right;color:#475569;font-size:10px;padding:4px 0;';
ts.textContent='Live KPI \u2014 '+d.ts+' \u2014 '+d.elapsed_ms+'ms';
p.appendChild(ts);
var h=document.querySelector('.stat-bar')||document.querySelector('.hero-stats');
if(h&&h.parentNode)h.parentNode.insertBefore(p,h.nextSibling);
else{var sb=document.querySelector('.search-bar')||document.querySelector('input[type=search]');if(sb)sb.parentNode.insertBefore(p,sb);else document.body.appendChild(p);}
}).catch(function(e){console.warn('LiveKPI:',e);});
setTimeout(liveKPI,60000);
})();
</script>
<script src="/api/em-live-inject.js"></script>
<!-- WTP-GAP-FILL-V1 (doctrine 90-v2 gap-fill showcase, 18avr 2026) -->
<style>
.wtp-gapfill-banner{position:fixed;bottom:0;left:0;right:0;z-index:99999;background:linear-gradient(90deg,#05060a,#0b0d15 20%,#181d2e 50%,#0b0d15 80%,#05060a);border-top:2px solid #14b8a6;color:#e2e8f0;padding:10px 16px;font-family:Inter,system-ui,-apple-system,sans-serif;font-size:11.5px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;box-shadow:0 -10px 30px rgba(20,184,166,.28)}
.wtp-gapfill-banner a{color:#5eead4;text-decoration:none;font-weight:600;transition:color .15s}
.wtp-gapfill-banner a:hover{color:#22d3ee}
.wtp-gapfill-banner .pill{padding:2px 9px;background:rgba(99,102,241,.14);color:#a5b4fc;border-radius:10px;font-size:10.5px;font-family:JetBrains Mono,monospace;font-weight:600}
.wtp-gapfill-banner .pill.new{background:rgba(20,184,166,.22);color:#5eead4}
.wtp-gapfill-banner .pill.hot{background:rgba(236,72,153,.22);color:#f472b6}
.wtp-gapfill-banner .close{margin-left:auto;cursor:pointer;color:#64748b;padding:0 8px;font-size:16px;line-height:1;border:1px solid #334155;border-radius:4px}
.wtp-gapfill-banner .close:hover{color:#e2e8f0;border-color:#64748b}
.wtp-gapfill-banner.hidden{display:none}
@media(max-width:768px){.wtp-gapfill-banner{font-size:10px;padding:7px 10px;gap:8px}}
</style>
<div class="wtp-gapfill-banner" id="wtpGapFillBanner">
<span>🎯 <strong>WEVAL Agents Gap-Fill ERP</strong></span>
<span class="pill hot">45 gaps</span>
<span class="pill">SAP · Oracle · NetSuite · Dynamics</span>
<span class="pill new">🆕 Meeting Rooms</span>
<span class="pill new">🆕 Lean 6 Sigma</span>
<span id="wtp-gfb-metrics" class="pill">— chargement —</span>
<a href="/weval-technology-platform.html">→ WTP Portal (16 mod)</a>
<a href="/enterprise-model.html">Enterprise Model</a>
<a href="/api/weval-agents-gap-fill-manifest.json" target="_blank">📋 Manifest</a>
<span class="close" onclick="document.getElementById("wtpGapFillBanner").classList.add("hidden");localStorage.setItem("wtpGapFillHidden","1")">×</span>
</div>
<script>
(async()=>{
if(localStorage.getItem("wtpGapFillHidden")==="1"){document.getElementById("wtpGapFillBanner").classList.add("hidden");return;}
try{
const r=await fetch("/api/source-of-truth.json?t="+Date.now());
const d=await r.json();
const el=document.getElementById("wtp-gfb-metrics");
if(el)el.textContent=(d.ethica_total||"?")+" HCPs · "+(d.nonreg||"?")+" · "+(d.providers_count||"?")+" IA · "+(d.agents_count||"?")+" agents · "+(d.docker_running||"?")+" 🐳";
}catch(e){}
})();
</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/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t32b4) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
// DOCTRINE-60-UX-JS staggered entrance
(function(){
if (!('IntersectionObserver' in window)) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting) {
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
obs.unobserve(e.target);
}
});
});
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
})();
</script>
</body>
</html>