232 lines
12 KiB
HTML
232 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr"><head>
|
|
<meta charset="UTF-8">
|
|
<title>Visual Management · WEVAL Consulting</title>
|
|
<style>
|
|
:root {
|
|
--bg:#0a0e27; --panel:#141933; --border:#263161; --text:#e4e8f7; --muted:#9ca8d3;
|
|
--green:#10b981; --amber:#f59e0b; --red:#ef4444; --blue:#6ba3ff; --purple:#c084fc;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); padding: 20px; min-height: 100vh; line-height: 1.4; }
|
|
.header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 16px; border-bottom: 2px solid #1e3a8a; margin-bottom: 20px; }
|
|
.header h1 { color: var(--blue); font-size: 26px; }
|
|
.header .sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
.health-badge { padding: 14px 28px; border-radius: 12px; font-size: 28px; font-weight: bold; text-align: center; min-width: 180px; }
|
|
.health-badge .label { font-size: 11px; display: block; text-transform: uppercase; opacity: 0.8; margin-bottom: 4px; }
|
|
.health-green { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #fff; }
|
|
.health-amber { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #fff; }
|
|
.health-red { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: #fff; }
|
|
.section { margin: 24px 0; }
|
|
.section h2 { color: var(--purple); font-size: 18px; margin-bottom: 12px; padding: 8px 0; border-bottom: 1px dashed var(--border); display: flex; align-items: center; gap: 8px; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
|
.kpi { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px; position: relative; overflow: hidden; }
|
|
.kpi::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--blue); }
|
|
.kpi.green::before { background: var(--green); }
|
|
.kpi.amber::before { background: var(--amber); }
|
|
.kpi.red::before { background: var(--red); }
|
|
.kpi .label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
.kpi .value { font-size: 28px; font-weight: bold; color: var(--text); font-family: 'SF Mono', Monaco, monospace; }
|
|
.kpi .unit { font-size: 13px; color: var(--muted); margin-left: 4px; font-weight: normal; }
|
|
.kpi .sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
|
|
.andon { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--panel); border: 1px solid var(--border); border-left-width: 5px; border-radius: 6px; margin-bottom: 8px; }
|
|
.andon.RED { border-left-color: var(--red); background: linear-gradient(90deg, rgba(239,68,68,0.1) 0%, var(--panel) 100%); }
|
|
.andon.ORANGE { border-left-color: var(--amber); background: linear-gradient(90deg, rgba(245,158,11,0.1) 0%, var(--panel) 100%); }
|
|
.andon .sev { font-weight: bold; padding: 3px 10px; border-radius: 4px; font-size: 11px; }
|
|
.andon.RED .sev { background: var(--red); color: #fff; }
|
|
.andon.ORANGE .sev { background: var(--amber); color: #000; }
|
|
.andon .msg { flex: 1; margin-left: 16px; font-size: 13px; }
|
|
.andon .kpi-name { color: var(--muted); font-size: 11px; font-family: 'SF Mono', monospace; }
|
|
.footer { color: var(--muted); font-size: 11px; margin-top: 28px; text-align: right; padding-top: 16px; border-top: 1px dashed var(--border); }
|
|
.links { display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0; }
|
|
.links a { color: var(--blue); text-decoration: none; font-size: 12px; padding: 4px 10px; background: var(--panel); border-radius: 4px; border: 1px solid var(--border); }
|
|
.links a:hover { background: var(--border); }
|
|
.skeleton { display: inline-block; width: 60px; height: 24px; background: linear-gradient(90deg, var(--border), #1e2549, var(--border)); background-size: 200% 100%; animation: skel 1.5s infinite; border-radius: 4px; }
|
|
@keyframes skel { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
#toast { position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: var(--red); color: #fff; border-radius: 6px; display: none; z-index: 1000; font-size: 13px; }
|
|
.no-andon { color: var(--green); padding: 20px; text-align: center; font-size: 14px; background: var(--panel); border-radius: 6px; border: 1px dashed var(--green); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<div>
|
|
<h1>📊 Visual Management · WEVAL</h1>
|
|
<div class="sub">Doctrine 65 — Lean Six Sigma · Andon alerts · Auto-refresh 30s · 0 hardcode</div>
|
|
</div>
|
|
<div class="health-badge health-amber" id="health-badge">
|
|
<span class="label">Health Score</span>
|
|
<span id="health-value">—</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="links">
|
|
<a href="/dashboards-hub.html">🏠 Hub</a>
|
|
<a href="/crm-dashboard-live.html">💼 CRM</a>
|
|
<a href="/contacts-segmentation-dashboard.html">🎯 B2B/B2C</a>
|
|
<a href="/ethica-dashboard-live.html">🏥 Ethica</a>
|
|
<a href="/office-365-dashboard-live.html">📮 O365</a>
|
|
<a href="/infra-dashboard-live.html">⚙️ Infra</a>
|
|
<a href="/database-dashboard-live.html">🗄️ DB</a>
|
|
<a href="/wevia-master.html">🤖 WEVIA Chat</a>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🚨 Andon Alerts</h2>
|
|
<div id="andon-list"><div class="skeleton" style="width:100%;height:60px"></div></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>💼 Business KPIs — CRM Pipeline</h2>
|
|
<div class="grid" id="kpi-business"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🌊 Flux KPIs — Flow Detection (doctrine 55 anti-staleness)</h2>
|
|
<div class="grid" id="kpi-flux"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🏥 Ethica HCPs</h2>
|
|
<div class="grid" id="kpi-ethica"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📮 Office 365 Email Infrastructure</h2>
|
|
<div class="grid" id="kpi-office"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>✅ Quality — Lean Six Sigma</h2>
|
|
<div class="grid" id="kpi-quality"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🎯 B2B/B2C Classification (doctrine 63)</h2>
|
|
<div class="grid" id="kpi-classif"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>⚙️ Infra Live</h2>
|
|
<div class="grid" id="kpi-infra"></div>
|
|
</div>
|
|
|
|
<div class="footer" id="footer">Connecting...</div>
|
|
<div id="toast"></div>
|
|
|
|
<script>
|
|
const fmt = n => (n == null) ? '—' : Number(n).toLocaleString('fr-FR');
|
|
const fmtPct = n => n == null ? '—' : Number(n).toFixed(1) + '%';
|
|
const fmtMoney = n => n == null ? '—' : (Math.round(n/1000) + 'k€');
|
|
|
|
function showToast(msg) {
|
|
const t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.style.display = 'block';
|
|
setTimeout(() => t.style.display = 'none', 5000);
|
|
}
|
|
|
|
function kpiCard(label, value, unit='', sub='', status='') {
|
|
return `<div class="kpi ${status}">
|
|
<div class="label">${label}</div>
|
|
<div class="value">${value}<span class="unit">${unit}</span></div>
|
|
${sub ? `<div class="sub">${sub}</div>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
async function loadData() {
|
|
try {
|
|
const r = await fetch('/api/visual-management-live.php');
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
const d = await r.json();
|
|
|
|
// Health badge
|
|
const badge = document.getElementById('health-badge');
|
|
badge.className = 'health-badge health-' + (d.health_status || 'amber').toLowerCase();
|
|
document.getElementById('health-value').textContent = (d.health_score || 0) + '/100';
|
|
|
|
// Andons
|
|
const alist = document.getElementById('andon-list');
|
|
alist.innerHTML = '';
|
|
if (!d.andons || d.andons.length === 0) {
|
|
alist.innerHTML = '<div class="no-andon">✅ Aucune alerte Andon — système healthy</div>';
|
|
} else {
|
|
d.andons.forEach(a => {
|
|
alist.innerHTML += `<div class="andon ${a.severity}">
|
|
<span class="sev">${a.severity}</span>
|
|
<span class="msg">${a.message}</span>
|
|
<span class="kpi-name">${a.kpi}</span>
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
// Business
|
|
const b = d.business || {};
|
|
const statDeals = b.crm_deals < 5 ? 'red' : 'green';
|
|
document.getElementById('kpi-business').innerHTML =
|
|
kpiCard('Deals', fmt(b.crm_deals), '', `Valeur: ${fmtMoney(b.crm_deals_amount_eur)}`, statDeals) +
|
|
kpiCard('Companies', fmt(b.crm_companies), '', 'Pipeline B2B', 'green') +
|
|
kpiCard('Contacts B2B', fmt(b.crm_contacts_b2b), '', `${fmt(b.crm_contacts_linked)} linked`, 'green') +
|
|
kpiCard('Activities', fmt(b.crm_activities), '', `${fmt(b.crm_activities_last_7d)} (7j)`, b.crm_activities < 100 ? 'amber' : 'green') +
|
|
kpiCard('Leads (7.3M)', fmt(b.crm_contacts + 7354710), '', 'leads + contacts pool');
|
|
|
|
// Flux
|
|
const f = d.flux || {};
|
|
const dateLastSend = f.send_contacts_last_created ? f.send_contacts_last_created.split(' ')[0] : '—';
|
|
const dateLastGraph = f.graph_send_last_created ? f.graph_send_last_created.split(' ')[0] : '—';
|
|
document.getElementById('kpi-flux').innerHTML =
|
|
kpiCard('send_contacts 7j', fmt(f.send_contacts_last_7d), '', `Dernière: ${dateLastSend}`, f.send_contacts_last_7d === 0 ? 'red' : 'green') +
|
|
kpiCard('send_contacts 30j', fmt(f.send_contacts_last_30d), '', 'Flux legacy CRM', 'amber') +
|
|
kpiCard('graph_send 7j', fmt(f.graph_send_last_7d), '', `Dernière: ${dateLastGraph}`, f.graph_send_last_7d < 100 ? 'amber' : 'green') +
|
|
kpiCard('weval_leads 7j', fmt(f.weval_leads_last_7d), '', 'B2B scraper flux', f.weval_leads_last_7d > 100 ? 'green' : 'amber') +
|
|
kpiCard('pipeline_deals 30j', fmt(f.pipeline_deals_last_30d), '', 'Nouveaux deals', f.pipeline_deals_last_30d === 0 ? 'red' : 'green') +
|
|
kpiCard('pipeline_contacts 24h', fmt(f.pipeline_contacts_last_24h), '', 'Import récent', 'green');
|
|
|
|
// Ethica
|
|
document.getElementById('kpi-ethica').innerHTML =
|
|
kpiCard('HCPs Total', fmt(b.ethica_hcps), '', '3 pays Maghreb+INT') +
|
|
kpiCard('DZ Algérie', fmt(b.ethica_hcps_dz), '', 'Campaign ready', 'green') +
|
|
kpiCard('MA Maroc', fmt(b.ethica_hcps_ma), '', 'Active market', 'green') +
|
|
kpiCard('TN Tunisie', fmt(b.ethica_hcps_tn), '', 'Secondary market', 'green');
|
|
|
|
// Office
|
|
document.getElementById('kpi-office').innerHTML =
|
|
kpiCard('Accounts', fmt(b.office_accounts), '', '9 tenants') +
|
|
kpiCard('Actifs', fmt(b.office_active), '', 'Ready to send', 'green') +
|
|
kpiCard('Warming', fmt(b.office_warming), '', 'En préchauffe', 'amber');
|
|
|
|
// Quality
|
|
const qu = d.quality || {};
|
|
document.getElementById('kpi-quality').innerHTML =
|
|
kpiCard('NonReg', fmt(qu.nonreg_pass) + '/' + fmt(qu.nonreg_total), '', fmtPct(qu.nonreg_score) + ' score', qu.nonreg_score === 100 ? 'green' : 'red') +
|
|
kpiCard('L99', fmt(qu.l99_pass) + '/' + fmt(qu.l99_total), '', fmtPct(qu.l99_score) + ' score', qu.l99_score === 100 ? 'green' : 'red');
|
|
|
|
// Classification
|
|
const c = d.classification || {};
|
|
document.getElementById('kpi-classif').innerHTML =
|
|
kpiCard('Leads classified', fmtPct(c.leads_classified_pct), '', `B2B: ${fmt(c.leads_b2b)}`, c.leads_classified_pct === 100 ? 'green' : 'amber') +
|
|
kpiCard('Send contacts classified', fmtPct(c.send_contacts_classified_pct), '', `B2B: ${fmt(c.send_contacts_b2b)}`, c.send_contacts_classified_pct === 100 ? 'green' : 'amber');
|
|
|
|
// Infra
|
|
const i = d.infra || {};
|
|
document.getElementById('kpi-infra').innerHTML =
|
|
kpiCard('Load 1m', i.load_1m, '', `5m: ${i.load_5m}`, i.load_1m < 4 ? 'green' : 'amber') +
|
|
kpiCard('Memory', fmtPct(i.mem_pct), '', 'S204', i.mem_pct < 70 ? 'green' : 'amber') +
|
|
kpiCard('Disk', fmtPct(i.disk_pct), '', '150G total', i.disk_pct < 80 ? 'green' : 'amber') +
|
|
kpiCard('Docker', fmt(i.docker_containers), '', 'Containers UP', 'green') +
|
|
kpiCard('Uptime', fmt(i.uptime_hours), 'h', 'Since last reboot');
|
|
|
|
// Footer
|
|
document.getElementById('footer').textContent =
|
|
`Last refresh: ${new Date(d.ts).toLocaleString('fr-FR')} · Andons: ${d.andons_count || 0} · API: /api/visual-management-live.php · Auto-refresh 30s`;
|
|
} catch (e) {
|
|
showToast('Erreur: ' + e.message);
|
|
document.getElementById('footer').textContent = 'Error: ' + e.message;
|
|
}
|
|
}
|
|
loadData();
|
|
setInterval(loadData, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|