187 lines
11 KiB
JavaScript
187 lines
11 KiB
JavaScript
/**
|
|
* DSH PREDICT Widget · WePredict v1.0 · 18avr2026
|
|
* Self-contained · zero external deps · safe to include anywhere.
|
|
* Fetches live data from WePredict endpoints and renders a glass-card panel.
|
|
* Yacine Mahboub · WEVAL Consulting
|
|
*/
|
|
(function(){
|
|
if (window.__DSH_PREDICT_LOADED__) return;
|
|
window.__DSH_PREDICT_LOADED__ = true;
|
|
|
|
const API_V71 = '/api/wevia-v71-intelligence-growth.php';
|
|
const API_HEAL = '/api/opus-arch-predictive-heal.php';
|
|
const API_CACHE = '/api/opus5-predictive-cache.php';
|
|
const API_GROWTH = '/api/growth-engine-api.php';
|
|
|
|
// ── Styles injection (scoped by .dsh-predict prefix) ──
|
|
const css = `
|
|
.dsh-predict-wrap{position:relative;margin:20px 0;font-family:-apple-system,Segoe UI,Roboto,sans-serif;color:#e6e9f0}
|
|
.dsh-predict-card{
|
|
background:linear-gradient(135deg,rgba(20,25,40,.72),rgba(35,20,60,.58));
|
|
backdrop-filter:blur(18px) saturate(1.2);-webkit-backdrop-filter:blur(18px) saturate(1.2);
|
|
border:1px solid rgba(120,140,255,.18);border-radius:20px;padding:22px 26px;
|
|
box-shadow:0 8px 32px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.05);
|
|
transition:transform .25s ease, box-shadow .25s ease, border-color .25s ease;
|
|
overflow:hidden
|
|
}
|
|
.dsh-predict-card:hover{transform:translateY(-3px);box-shadow:0 14px 42px rgba(80,100,255,.28);border-color:rgba(140,170,255,.38)}
|
|
.dsh-predict-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;gap:12px;flex-wrap:wrap}
|
|
.dsh-predict-title{font-size:15px;font-weight:600;letter-spacing:.5px;display:flex;align-items:center;gap:10px;margin:0}
|
|
.dsh-predict-title .emoji{font-size:22px}
|
|
.dsh-predict-title small{display:block;font-size:10px;font-weight:400;color:#8892b4;letter-spacing:1px;margin-top:2px}
|
|
.dsh-predict-badge{
|
|
display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:999px;font-size:10px;
|
|
font-weight:700;letter-spacing:1.2px;text-transform:uppercase
|
|
}
|
|
.dsh-predict-badge.live{background:rgba(52,211,153,.15);color:#34d399;border:1px solid rgba(52,211,153,.35)}
|
|
.dsh-predict-badge.warn{background:rgba(250,204,21,.15);color:#facc15;border:1px solid rgba(250,204,21,.35)}
|
|
.dsh-predict-badge.alert{background:rgba(248,113,113,.15);color:#f87171;border:1px solid rgba(248,113,113,.45)}
|
|
.dsh-predict-badge .dot{width:7px;height:7px;border-radius:50%;background:currentColor;animation:dshPulse 1.8s ease-in-out infinite}
|
|
@keyframes dshPulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}}
|
|
.dsh-predict-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}
|
|
.dsh-predict-tile{
|
|
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:14px;
|
|
padding:14px 16px;transition:all .2s ease;cursor:default
|
|
}
|
|
.dsh-predict-tile:hover{background:rgba(255,255,255,.06);border-color:rgba(140,170,255,.25)}
|
|
.dsh-predict-tile .label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#8892b4;margin-bottom:6px;font-weight:600}
|
|
.dsh-predict-tile .value{font-size:26px;font-weight:700;line-height:1;letter-spacing:-.5px}
|
|
.dsh-predict-tile .value.g{background:linear-gradient(135deg,#34d399,#60a5fa);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
|
.dsh-predict-tile .value.w{color:#facc15}
|
|
.dsh-predict-tile .value.r{color:#f87171}
|
|
.dsh-predict-tile .sub{font-size:11px;color:#8892b4;margin-top:6px}
|
|
.dsh-predict-actions{margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,.07)}
|
|
.dsh-predict-actions .label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#8892b4;font-weight:600;margin-bottom:8px}
|
|
.dsh-predict-actions ul{margin:0;padding:0 0 0 18px;font-size:12px;color:#c5cbe0;line-height:1.7}
|
|
.dsh-predict-foot{margin-top:14px;font-size:10px;color:#5f6787;display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap}
|
|
.dsh-predict-foot a{color:#60a5fa;text-decoration:none}
|
|
.dsh-predict-foot a:hover{text-decoration:underline}
|
|
.dsh-predict-err{color:#f87171;font-size:12px;padding:10px;text-align:center}
|
|
.dsh-predict-skel{display:inline-block;width:60%;height:22px;background:linear-gradient(90deg,rgba(255,255,255,.05),rgba(255,255,255,.12),rgba(255,255,255,.05));background-size:200% 100%;animation:dshShim 1.5s ease-in-out infinite;border-radius:4px}
|
|
@keyframes dshShim{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
|
`;
|
|
const st = document.createElement('style');
|
|
st.id='dsh-predict-style';st.textContent=css;
|
|
document.head.appendChild(st);
|
|
|
|
// ── Mount point: use #dsh-predict-mount if present, else inject before </body> ──
|
|
let mount = document.getElementById('dsh-predict-mount');
|
|
if (!mount) {
|
|
mount = document.createElement('div');
|
|
mount.id = 'dsh-predict-mount';
|
|
mount.className = 'dsh-predict-wrap';
|
|
// Insert inside main/container if available, else body end
|
|
const parent = document.querySelector('main, .main, .content, .container, #app') || document.body;
|
|
parent.appendChild(mount);
|
|
}
|
|
mount.classList.add('dsh-predict-wrap');
|
|
mount.innerHTML = `
|
|
<div class="dsh-predict-card" role="region" aria-label="DSH Predict live">
|
|
<div class="dsh-predict-head">
|
|
<div>
|
|
<p class="dsh-predict-title"><span class="emoji">🔮</span>DSH PREDICT <small>WePredict · growth · spy · heal</small></p>
|
|
</div>
|
|
<span id="dshp-badge" class="dsh-predict-badge live"><span class="dot"></span>LIVE</span>
|
|
</div>
|
|
<div class="dsh-predict-grid" id="dshp-grid">
|
|
<div class="dsh-predict-tile"><div class="label">Loading…</div><div class="value"><span class="dsh-predict-skel"></span></div></div>
|
|
</div>
|
|
<div class="dsh-predict-actions" id="dshp-actions" style="display:none">
|
|
<div class="label">Recommended actions</div>
|
|
<ul id="dshp-reco"></ul>
|
|
</div>
|
|
<div class="dsh-predict-foot">
|
|
<span id="dshp-ts">—</span>
|
|
<span>Sources: <a href="${API_V71}" target="_blank">v71 Growth</a> · <a href="${API_HEAL}" target="_blank">Heal</a> · <a href="${API_CACHE}" target="_blank">Cache</a></span>
|
|
</div>
|
|
</div>`;
|
|
|
|
// ── Fetch helpers ──
|
|
const fetchJSON = (url, timeout=8000) => new Promise((res)=>{
|
|
const ctrl = new AbortController();
|
|
const to = setTimeout(()=>ctrl.abort(), timeout);
|
|
fetch(url,{signal:ctrl.signal,cache:'no-store'})
|
|
.then(r=>r.ok?r.json():Promise.reject(r.status))
|
|
.then(d=>{clearTimeout(to);res({ok:true,data:d});})
|
|
.catch(e=>{clearTimeout(to);res({ok:false,err:String(e)});});
|
|
});
|
|
|
|
const fmt = (n) => {
|
|
if (n===null||n===undefined) return '—';
|
|
if (typeof n==='number'){
|
|
if (Math.abs(n)>=1e6) return (n/1e6).toFixed(1)+'M';
|
|
if (Math.abs(n)>=1e3) return (n/1e3).toFixed(1)+'k';
|
|
return Number.isInteger(n)?n.toString():n.toFixed(2);
|
|
}
|
|
return String(n);
|
|
};
|
|
|
|
// ── Render ──
|
|
async function render(){
|
|
const [r71, rheal, rcache] = await Promise.all([
|
|
fetchJSON(API_V71), fetchJSON(API_HEAL), fetchJSON(API_CACHE)
|
|
]);
|
|
|
|
const grid = document.getElementById('dshp-grid');
|
|
const badge = document.getElementById('dshp-badge');
|
|
const ts = document.getElementById('dshp-ts');
|
|
const actionsBox = document.getElementById('dshp-actions');
|
|
const reco = document.getElementById('dshp-reco');
|
|
|
|
if (!r71.ok && !rheal.ok && !rcache.ok) {
|
|
grid.innerHTML = `<div class="dsh-predict-err">⚠ Predict APIs unreachable. Retry soon.</div>`;
|
|
badge.className='dsh-predict-badge alert';badge.innerHTML='<span class="dot"></span>ALERT';
|
|
return;
|
|
}
|
|
|
|
const s = (r71.ok && r71.data && r71.data.summary) ? r71.data.summary : {};
|
|
const heal = rheal.ok ? rheal.data : {};
|
|
const cache = rcache.ok && rcache.data && rcache.data.stats ? rcache.data.stats : {};
|
|
|
|
// Tiles
|
|
const tiles = [
|
|
{ lbl:'Load · next hour', val:fmt(heal.predicted_next_hour), sub:(heal.alert?'⚠ over threshold':'safe · threshold '+(heal.threshold||5)), klass:heal.alert?'w':'g' },
|
|
{ lbl:'Competitors tracked', val:s.competitors_tracked||0, sub:`${s.competitors_high_threat||0} high threat`, klass:(s.competitors_high_threat>=3?'w':'g') },
|
|
{ lbl:'Opportunities · k€', val:fmt(s.opportunities_total_value_keur), sub:`${s.opportunities_high_urgency||0} high urgency`, klass:'g' },
|
|
{ lbl:'Innovations · 24h', val:s.innovations_last_24h||0, sub:`total ${s.innovations_total||0}`, klass:'g' },
|
|
{ lbl:'Chatbots deployed', val:`${s.chatbots_deployed||0}/${s.chatbots_total||0}`, sub:'lead capture hub', klass:'g' },
|
|
{ lbl:'Leads · 7d', val:s.leads_captured_7d||0, sub:(s.leads_captured_7d===0?'🔴 capture à relancer':'ok'), klass:(s.leads_captured_7d===0?'r':'g') },
|
|
{ lbl:'Agility agents · gap', val:s.agility_agents_gap||0, sub:`FTE savings ${s.agility_fte_savings_year||0}/an`, klass:(s.agility_agents_gap>=5?'w':'g') },
|
|
{ lbl:'Cache predict · hit%', val:(cache.hit_rate_pct!==undefined?cache.hit_rate_pct+'%':'—'), sub:`${cache.gets||0} gets · ${cache.hits||0} hits`, klass:'g' }
|
|
];
|
|
grid.innerHTML = tiles.map(t => `
|
|
<div class="dsh-predict-tile">
|
|
<div class="label">${t.lbl}</div>
|
|
<div class="value ${t.klass}">${t.val}</div>
|
|
<div class="sub">${t.sub}</div>
|
|
</div>`).join('');
|
|
|
|
// Badge status
|
|
const hasAlert = heal.alert || (s.leads_captured_7d===0) || (s.competitors_high_threat>=3);
|
|
const hasWarn = (s.opportunities_high_urgency>=3) || (s.agility_agents_gap>=5);
|
|
if (hasAlert) { badge.className='dsh-predict-badge alert'; badge.innerHTML='<span class="dot"></span>ALERT'; }
|
|
else if (hasWarn) { badge.className='dsh-predict-badge warn'; badge.innerHTML='<span class="dot"></span>WARN'; }
|
|
else { badge.className='dsh-predict-badge live'; badge.innerHTML='<span class="dot"></span>LIVE'; }
|
|
|
|
// Recommendations
|
|
const rec = [];
|
|
if (heal.alert && Array.isArray(heal.recommended_actions)) rec.push(...heal.recommended_actions);
|
|
if (s.leads_captured_7d===0) rec.push('Relancer campagne lead capture (0 leads 7j)');
|
|
if (s.competitors_high_threat>=3) rec.push(`${s.competitors_high_threat} concurrents high-threat · revoir positionnement`);
|
|
if (s.opportunities_high_urgency>=3) rec.push(`${s.opportunities_high_urgency} opportunités urgentes · prioriser`);
|
|
if (s.agility_agents_gap>=5) rec.push(`${s.agility_agents_gap} agents agility à créer · potentiel FTE ${s.agility_fte_savings_year||0}/an`);
|
|
if (rec.length) {
|
|
actionsBox.style.display='block';
|
|
reco.innerHTML = rec.slice(0,4).map(x=>`<li>${x}</li>`).join('');
|
|
} else {
|
|
actionsBox.style.display='none';
|
|
}
|
|
|
|
ts.textContent = 'Updated ' + new Date().toLocaleTimeString('fr-FR');
|
|
}
|
|
|
|
render();
|
|
// Refresh every 60s
|
|
setInterval(render, 60000);
|
|
})();
|