Files
html/dsh-predict-widget.js
opus 47d3f3f618
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-commit via WEVIA vault_git intent 2026-04-18T13:23:22+00:00
2026-04-18 15:23:22 +02:00

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);
})();