Healthy
Sync activo

LEADSCORING

ManyChat + Meta — Lead scoring y analytics en tiempo real

👥
Infoproductores
🚀
Lanzamientos activos
💬
Leads totales
📊
Lanzamientos total
Infoproductores

Tareas esbirrito

Cola de tareas autónomas del agente

Log del orquestador

Auto-refresco cada 10s

Memoria del agente

Conocimiento persistente de esbirrito

Largo plazo
(cargando...)
Notas diarias
(cargando...)
'; } var srcdoc = rendered.replace(/&/g,'&').replace(/"/g,'"'); // Stat con estética dashboard var statBlock = function(label, value, sub, color){ return '
' + '
' + label + '
' + '
' + value + '
' + (sub ? '
' + sub + '
' : '') + '
'; }; return '' // Toolbar — back button + stats + '
' + ' ' + '
' + statBlock('Lanzados', e.delivered_count.toLocaleString('es-ES'), 'disparados', '25,91,255') + statBlock('Entregados', e.delivered_count.toLocaleString('es-ES'), '100% éxito', '0,200,150') + statBlock('Aperturas', (e.estimated_opens || 0).toLocaleString('es-ES'), (e.estimated_open_rate || 0) + '% open', '234,179,8') + statBlock('Clicks', (e.estimated_clicks || 0).toLocaleString('es-ES'), (e.estimated_click_rate || 0) + '% CTR', '139,92,246') + '
' + '
' // Email card (paleta dashboard) + '
' + '
' + '
' + escapeHtml(e.subject) + '
' + '
' + '
M
' + '
' + '
MKT Hackers <info@mkthackers.com>
' + '
para tu lista del LZ del 5 de mayo
' + '
' + '
' + (firstS ? firstS.toLocaleDateString('es-ES', {day:'numeric', month:'long', year:'numeric', hour:'2-digit', minute:'2-digit'}) : '—') + '
' + '
' + '
' + ' ' + '
'; } // === Fila visual rica: borde lateral colorizado por avatar + barrita open rate === var _AVATAR_META = { contratado: {n:1, label:'Contratado', rgb:'59,130,246', emoji:'💼'}, empresario: {n:2, label:'Empresario', rgb:'16,185,129', emoji:'🏢'}, freelancer: {n:3, label:'Freelancer', rgb:'245,158,11', emoji:'🚀'}, desempleado: {n:4, label:'Desempleado', rgb:'239,68,68', emoji:'🔍'}, sin_clasificar: {n:'?', label:'Sin clasificar', rgb:'148,163,184', emoji:'❓'}, }; function _gmailRow(o) { var pending = !o.isSent; var op = pending ? '.5' : '1'; var avMeta = (o.avatar && _AVATAR_META[o.avatar]) || _AVATAR_META.sin_clasificar; var openRate = o.openRate || 0; // Mini barra visual de % apertura var openBar = pending ? '
' : '' + '
' + '
' + '
' + '
' + ' ' + openRate + '%' + '
'; var stat = function(value, width){ width = width || 54; return '
' + value + '
'; }; var eyeSvg = ''; return '' + '
' // Avatar circle visual (con emoji + número) + '
' + ' ' + avMeta.emoji + '' + '
' + avMeta.n + '
' + '
' // Avatar label + Subject + '
' + '
' + ' Av' + avMeta.n + ' · ' + avMeta.label + '' + (pending ? '' : 'ENVIADO') + '
' + '
' + escapeHtml(o.subject) + (o.subjectSub ? '— ' + escapeHtml(o.subjectSub) + '' : '') + '
' + '
' // Stats con números + barra open rate + '
' + stat(pending ? '—' : (o.launched||0).toLocaleString('es-ES')) + stat(pending ? '—' : (o.delivered||0).toLocaleString('es-ES')) + stat(pending ? '—' : (o.opened||0).toLocaleString('es-ES')) + openBar + stat(pending ? '—' : (o.clicked||0).toLocaleString('es-ES'), 44) + '
' + '
' + escapeHtml(o.date) + '
' + '
' + eyeSvg + '
' + '
'; } // Header de columnas function _gmailColHeader() { var col = function(label, width){ width = width || 54; return '
' + label + '
'; }; return '' + '
' + '
' + '
' + '
' + col('Lanzados') + col('Entregados') + col('Aperturas') + '
% Apertura
' + col('Clicks', 44) + '
' + '
Fecha
' + '
' + '
'; } function _emailFmtDateGmail(d) { var now = new Date(); var sameDay = d.toDateString() === now.toDateString(); if (sameDay) { return d.toLocaleTimeString('es-ES', {hour:'2-digit', minute:'2-digit'}); } var sameYear = d.getFullYear() === now.getFullYear(); if (sameYear) { return d.toLocaleDateString('es-ES', {day:'numeric', month:'short'}); } return d.toLocaleDateString('es-ES', {day:'2-digit', month:'2-digit', year:'2-digit'}); } function _emailFmtDate(d) { var now = new Date(); var diff = (now.getTime() - d.getTime()) / 1000; if (diff < 60) return 'ahora'; if (diff < 3600) return Math.round(diff/60) + ' min'; if (diff < 86400) return Math.round(diff/3600) + ' h'; if (diff < 86400*7) return Math.round(diff/86400) + ' d'; return d.toLocaleDateString('es-ES', {day:'2-digit', month:'short'}); } function renderHistComparison() { if(!_curLanz || !_allLanz.length) return; if(_curLanz.tipo === 'vsl') return; // VSL no compara con LZs var wrap = document.getElementById('histComparison'); if(!wrap) return; // Get all closed launches + current, sorted chronologically var launches = _allLanz.filter(function(x){ return x.estado === 'cerrado' || x.id === _curLanz.id; }).sort(function(a,b){ // Try to parse fecha_inicio or use id order var da = a.fecha_inicio || a.id; var db = b.fecha_inicio || b.id; return da.localeCompare(db); }); if(launches.length < 2) { wrap.innerHTML = ''; return; } // Compute metrics for each launch (real for closed, projected for active) var is1337 = _curInfopro === '1337'; var isIAH = _curInfopro === 'ia-hackers'; var rA, rG, rV, prDefault, rCash; if (isIAH) { rA=0.293; rG=0.205; rV=0.230; prDefault=1900; rCash=0.81; } else if (is1337) { rA=0.20; rG=0.25; rV=0.298; prDefault=2997; rCash=0.80; } else { rA=0.272; rG=0.361; rV=0.299; prDefault=1997; rCash=0.80; } function metricsFor(l) { // 18/05/26 — ticket dinámico por LZ: si tiene ventas reales, usar el promedio. // Sin esto, 1337 con 5 ventas premium 4500€ + 29 default 2997€ se proyectaba // como si todas fueran 2997€ → infraestima proyección y ROAS futuro. var pr = (l.ticket_medio_real && l.ticket_medio_real > 0) ? l.ticket_medio_real : prDefault; var leads = (l.manychat||{}).leads_totales || 0; var spend = l.gasto_total_real || 0; var cpl = leads > 0 ? (spend/leads) : 0; // Confirmación de asistencia: tag ManyChat (real-time, único campo). var confAsist = (l.manychat||{}).confirman_asistencia || 0; // Real si hay resultados_*, sino proyectado. Track de qué cell es real. var rAsist = l.resultados_asistencia; var rAgendas = l.resultados_agendas; var rVentas = l.resultados_ventas; var rFact = l.resultados_facturacion; var rCash2 = l.resultados_cash; var asist = (rAsist != null && rAsist > 0) ? rAsist : Math.round(leads*rA); // Agendas: SIEMPRE el valor real de Calendly (rAgendas) si existe el campo, // aunque sea 0. Solo proyectar si el campo no existe (LZ sin Calendly). var agendas = (rAgendas != null) ? rAgendas : Math.round(asist*rG); var ventas = (rVentas != null && rVentas > 0) ? rVentas : Math.round(agendas*rV); var fact = (rFact != null && rFact > 0) ? rFact : (ventas*pr); var cash = (rCash2 != null && rCash2 > 0) ? rCash2 : Math.round(fact*rCash); var roas = spend>0 && fact>0 ? fact/spend : 0; // Flags: ¿el valor está SELLADO? // Reglas según fase del LZ activo: // - LZ cerrado: TODO sellado (no parpadea nada). // - LZ activo ANTES del directo: nada sellado (todo proyección). // - LZ activo CON directo ya pasado: leads/spend/asist se sellan // (peak alcanzado el día del directo). Agendas/ventas/fact/cash/roas // SIGUEN abiertos hasta el día antes del próximo LZ → parpadean. var lzClosed = l.estado !== 'activo'; var directoDate = l.fecha_inicio ? new Date(l.fecha_inicio) : null; var directoPasado = directoDate && (new Date() >= directoDate); var preDirecto = !lzClosed && !directoPasado; // Leads/spend/asist se sellan al pasar el directo (o si está cerrado). var capSellada = lzClosed || directoPasado; var isReal = { leads: capSellada && leads > 0, spend: capSellada && spend > 0, confAsist: capSellada && confAsist > 0, asist: capSellada && rAsist != null && rAsist > 0, // Agendas/ventas/fact/cash/roas: solo sellados cuando el LZ cierra. agendas: lzClosed && rAgendas != null && rAgendas > 0, ventas: lzClosed && rVentas != null && rVentas > 0, fact: lzClosed && rFact != null && rFact > 0, cash: lzClosed && rCash2 != null && rCash2 > 0, roas: lzClosed && rFact != null && rFact > 0 && spend > 0, }; return {leads, cpl, confAsist, asist, agendas, ventas, fact, cash, roas, spend, isActive: l.estado==='activo', isReal}; } var allMetrics = launches.map(function(l){ return Object.assign({label: shortLabel(l.id), id: l.id}, metricsFor(l)); }); var current = allMetrics[allMetrics.length-1]; var previous = allMetrics.length >= 2 ? allMetrics[allMetrics.length-2] : null; function shortLabel(id) { // LZ-MKT-7-ABRIL-2026 → 7 ABR var m = id.match(/(\d{1,2})[\s-]+([A-Z]+)/i); if(m) return m[1]+' '+m[2].substring(0,3).toUpperCase(); return id; } // Sparkline SVG generator function sparkline(values, color, width, height) { if(!values.length || values.every(function(v){return v===0;})) return ''; var max = Math.max.apply(null, values); var min = Math.min.apply(null, values); var range = max - min || 1; var w = width || 100; var h = height || 30; var pts = values.map(function(v, i) { var x = (i/(values.length-1)) * w; var y = h - ((v-min)/range) * h; return x.toFixed(1)+','+y.toFixed(1); }).join(' '); var lastX = w; var lastY = h - ((values[values.length-1]-min)/range) * h; return '' +'' +'' +''; } function pctChange(curr, prev) { if(!prev || prev === 0) return null; return ((curr - prev) / prev) * 100; } function arrow(pct, inverse) { if(pct === null || pct === undefined) return ''; var goodIfUp = !inverse; var isGood = goodIfUp ? pct >= 0 : pct < 0; var color = isGood ? 'var(--success)' : 'var(--danger)'; var icon = pct >= 0 ? '↑' : '↓'; return ''+icon+' '+Math.abs(pct).toFixed(0)+'%'; } function rank(values, currentIdx) { var sorted = values.slice().sort(function(a,b){return b-a;}); var pos = sorted.indexOf(values[currentIdx]) + 1; return pos + 'º de ' + values.length; } // Build the cards (5 metrics) var color = isIAH ? '#F97316' : (is1337 ? '#EAB308' : '#7C3AED'); // Same colors as the forecast cards above // ratioOf: la métrica sobre la que calcular el ratio % (ej: agendas sobre asistencia) var metrics = [ {key:'leads', label:'Leads', icon:'👥', fmt:function(v){return fmtN(v);}, inverse:false, color:'#0EA5E9', rgb:'14,165,233'}, {key:'spend', label:'Inversión', icon:'💸', fmt:function(v){return v>=1000?Math.round(v/1000)+'k€':Math.round(v)+'€';}, inverse:false, color:'#EAB308', rgb:'234,179,8'}, {key:'confAsist', label:'Confirman asistencia', icon:'✅', fmt:function(v){return fmtN(v);}, inverse:false, color:'#10B981', rgb:'16,185,129', ratioOf:'leads', ratioLabel:'leads'}, {key:'asist', label:'Asistencia', icon:'🎯', fmt:function(v){return fmtN(v);}, inverse:false, color:'var(--success)', rgb:'var(--success-rgb)', ratioOf:'leads', ratioLabel:'leads'}, {key:'agendas', label:'Agendas', icon:'📞', fmt:function(v){return fmtN(v);}, inverse:false, color:'var(--warn)', rgb:'var(--warn-rgb)', ratioOf:'asist', ratioLabel:'asistencia'}, {key:'ventas', label:'Compras', icon:'🏆', fmt:function(v){return fmtN(v);}, inverse:false, color:'var(--purple)', rgb:'var(--purple-rgb)', ratioOf:'agendas', ratioLabel:'agendas'}, {key:'fact', label:'Facturación', icon:'💰', fmt:function(v){return v>=1000?Math.round(v/1000)+'k€':Math.round(v)+'€';}, inverse:false, color:'#06B6D4', rgb:'6,182,212'}, {key:'cash', label:'Cash', icon:'💎', fmt:function(v){return v>=1000?Math.round(v/1000)+'k€':Math.round(v)+'€';}, inverse:false, color:'var(--primary)', rgb:'var(--primary-rgb)', ratioOf:'fact', ratioLabel:'facturación'}, {key:'roas', label:'ROAS', icon:'📈', fmt:function(v){return v.toFixed(2)+'x';}, inverse:false, color:'#EC4899', rgb:'236,72,153'}, ]; // All launches chronologically (oldest LEFT → current RIGHT). No slicing — scroll horizontal if many. var ordered = allMetrics.slice(); function pct(curr, prev) { if(!prev || prev === 0 || curr === 0) return null; return ((curr - prev) / prev) * 100; } function deltaBadge(p, inverse) { if(p === null) return ''; var goodIfUp = !inverse; var isGood = goodIfUp ? p >= 0 : p < 0; var col = isGood ? 'var(--success)' : 'var(--danger)'; var bg = isGood ? 'rgba(var(--success-rgb),.10)' : 'rgba(var(--danger-rgb),.10)'; var arr = p >= 0 ? '↑' : '↓'; return '
'+arr+Math.abs(p).toFixed(0)+'%
'; } var lastIdx = ordered.length - 1; var nCols = ordered.length; // Get the rgb of infoproductor color for gradient var ipRgb = isIAH ? '249,115,22' : (is1337 ? '234,179,8' : '124,58,237'); // ─── Build TABLE — versión compacta y elegante ─── var headerHtml = '' +''; headerHtml += ordered.map(function(x, idx) { var isCurrent = idx === lastIdx; var col = isCurrent ? color : 'var(--text2)'; var dot = isCurrent ? '' : ''; var bg = isCurrent ? 'background:linear-gradient(135deg,rgba('+ipRgb+',.07),rgba('+ipRgb+',.02));border-bottom:1px solid rgba('+ipRgb+',.25);' : 'background:rgba(255,255,255,.012);border-bottom:1px solid var(--border);'; return '' +'
'+dot+'📅 '+x.label+'
' +'
'+(isCurrent?'en curso':'cerrado')+'
' +''; }).join(''); headerHtml += ''; // Metric rows var bodyHtml = metrics.map(function(m, mi) { var isLast = mi === metrics.length - 1; var rowHtml = '' +'' +'
' +'' +''+m.icon+' '+m.label+'' +'
' +''; rowHtml += ordered.map(function(x, idx) { var v = x[m.key]; var prev = idx > 0 ? ordered[idx - 1] : null; var pv = prev ? prev[m.key] : null; // Modo % o absoluto var displayVal; if (_histMode === 'pct' && m.ratioOf) { var refV = x[m.ratioOf]; displayVal = (v > 0 && refV > 0) ? ((v/refV)*100).toFixed(1)+'%' : '—'; } else { displayVal = v > 0 ? m.fmt(v) : '—'; } var isCurrent = idx === lastIdx; var valColor = isCurrent ? 'var(--text)' : 'var(--text2)'; var valOpacity = isCurrent ? '1' : '0.55'; var bg; if (isCurrent) { bg = 'background:linear-gradient(135deg,rgba('+ipRgb+',.06),rgba('+ipRgb+',.015));'; } else { bg = 'background:rgba('+m.rgb+',.018);'; } var borderBottom = isLast && isCurrent ? 'border-bottom:1px solid rgba('+ipRgb+',.20)' : 'border-bottom:1px solid var(--border)'; // Sellado: si la celda tiene dato REAL no parpadea (cierra). Solo previsiones parpadean. var cellIsReal = x.isReal && x.isReal[m.key]; var blinkClass = (isCurrent && !cellIsReal) ? 'prev-blink' : ''; var sealedBadge = (isCurrent && cellIsReal) ? '
' : ''; var pctNum = (isCurrent && v > 0 && pv) ? pct(v, pv) : null; var deltaB = ''; if (pctNum !== null) { var goodIfUp = !m.inverse; var isGood = goodIfUp ? pctNum >= 0 : pctNum < 0; var col2 = isGood ? 'var(--success)' : 'var(--danger)'; var bg2 = isGood ? 'rgba(var(--success-rgb),.10)' : 'rgba(var(--danger-rgb),.10)'; var arr = pctNum >= 0 ? '↑' : '↓'; deltaB = '
'+arr+Math.abs(pctNum).toFixed(0)+'%
'; } var ratioHtml = ''; return '' +sealedBadge +deltaB +'
'+displayVal+'
' +ratioHtml +''; }).join(''); rowHtml += ''; return rowHtml; }).join(''); // No scroll — table always fits the container var hasOverflow = false; wrap.innerHTML = '' +'
' +ljSectionHeader('histCompare', '📊', 'Comparativa lanzamientos', ordered.length+' lanzamientos', color, ipRgb) +(_ljOpen !== 'histCompare' ? '
' : '
' +'
' +'
' +'' +'' +'
' +'
' +'
' +'' +'' +ordered.map(function(){return '';}).join('') +''+headerHtml+''+bodyHtml+'
' +'
' +'
'); // Auto-scroll to the right (current launch) on render setTimeout(function(){ var t = document.querySelector('.hist-table'); if(t) t.scrollLeft = t.scrollWidth; }, 100); } function renderAdsHighlights() { if(!_curLanz) return; var wrap = $('adsHighlightsWrap'); if(!wrap) return; var allAds = (_curLanz.lead_scoring_ads||{}).ads||[]; // VSL: aplicar filtro de fecha de la sección "ads" — sustituir leads del ad // por los leads del periodo (vía leads_by_week). spend/CPL siguen lifetime // del meta_map (no hay distribución temporal por ad). var _adsRange = (_curLanz.tipo === 'vsl' && typeof vslGetRange === 'function') ? vslGetRange('ads') : null; if (_adsRange && _adsRange.from) { allAds = allAds.map(function(a){ var inRange = _vslAdLeadsInRange(a, _adsRange.from, _adsRange.to); var copy = Object.assign({}, a); copy._period_leads = inRange.leads; copy._period_active = true; copy.leads = inRange.leads; // para que los filtros (>=3 leads) funcionen sobre periodo return copy; }).filter(function(a){ return (a.leads||0) > 0; }); } var goodAds = allAds.filter(function(a){return a.score>=3.5 && a.leads>=3 && (_adMeta[a.utm_content||a.ad_id]||{}).status === 'ACTIVE';}); var badAds = allAds.filter(function(a){return a.score<2.0 && a.leads>=3 && (_adMeta[a.utm_content||a.ad_id]||{}).status === 'ACTIVE';}); var dangerAds = allAds.filter(function(a){ return isAdDanger(a) && (_adMeta[a.utm_content||a.ad_id]||{}).status === 'ACTIVE'; }); function emptyState(msg) { return '
'+msg+'
'; } // Ads que vuelan (col1: izquierda) var col1 = '
' +'
🚀
Ads que vuelan
Pushear presupuesto
'+goodAds.length+'
'; if(goodAds.length) { col1 += '
'+goodAds.sort(function(a,b){return b.score-a.score;}).slice(0,5).map(function(a){return renderAdHighlightRow(a,'success');}).join('')+'
'; } else { col1 += emptyState('Aún no hay ads volando'); } col1 += '
'; // Ads quemando (col2: centro) var col2 = '
' +'
🚨
Ads quemando presupuesto
Apagar o revisar
'+badAds.length+'
'; if(badAds.length) { col2 += '
'+badAds.sort(function(a,b){return a.score-b.score;}).slice(0,5).map(function(a){return renderAdHighlightRow(a,'danger');}).join('')+'
'; } else { col2 += emptyState('Ningún ad quemando — bien'); } col2 += '
'; // Ads en peligro (col3: derecha) var col3 = '
' +'
⚠️
Ads en peligro
CPL >40€ o >60€ sin leads
'+dangerAds.length+'
'; if(dangerAds.length) { dangerAds.sort(function(a,b){var ma=_adMeta[a.utm_content||a.ad_id]||{};var mb=_adMeta[b.utm_content||b.ad_id]||{};return (mb.spend||0)-(ma.spend||0);}); col3 += '
'+dangerAds.slice(0,5).map(function(a){ var adId=a.utm_content||a.ad_id;var m=_adMeta[adId]||{}; var info=m.cpl>40?'CPL: '+m.cpl.toFixed(0)+'€':''+Math.round(m.spend)+'€ sin leads'; return '
' +'' +(m.thumbnail?'':'') +'
'+(m.name||adId)+'
' +''+info+'' +'
'; }).join('')+'
'; } else { col3 += emptyState('Sin ads en peligro'); } col3 += '
'; wrap.innerHTML = '
'+col1+col2+col3+'
'; } function toggleAdsSort() { sortAdsBy('score'); } // ── Ad Preview System ── // Cache de URLs iframe Meta (caducan, TTL 8 min en frontend) const _pvCache = {}; // {adId: src} const _pvCacheTs = {}; // {adId: timestamp} const PV_CACHE_TTL = 8 * 60 * 1000; function _pvGetCache(adId) { const ts = _pvCacheTs[adId]; if (!ts) return null; if (Date.now() - ts > PV_CACHE_TTL) { delete _pvCache[adId]; delete _pvCacheTs[adId]; return null; } return _pvCache[adId]; } function _pvSetCache(adId, src) { _pvCache[adId] = src; _pvCacheTs[adId] = Date.now(); } let _pvTimer = null; let _pvCurrentId = null; function pvShow(thumbWrap, adId, name, leads, spend, cpl) { pvCancelHide(); _pvCurrentId = adId; const pv = $('adPreviewFloat'); // Position next to thumbnail const r = thumbWrap.getBoundingClientRect(); const vh = window.innerHeight; const vw = window.innerWidth; let top = r.top - 80; if (top + 400 > vh) top = vh - 410; if (top < 8) top = 8; let left = r.right + 14; if (left + 296 > vw) left = r.left - 296; pv.style.top = top + 'px'; pv.style.left = left + 'px'; pv.classList.add('visible'); const bar = `
${name}${leads} leads · ${spend?Math.round(spend)+'€':'-'} · CPL ${cpl?cpl.toFixed(1)+'€':'-'}
`; const thumbImg = thumbWrap.querySelector('img'); const thumbSrc = thumbImg ? thumbImg.src : ''; pv.innerHTML = `
Cargando ad...
${bar}`; const iframeSrc = _pvGetCache(adId); if (iframeSrc) { loadPvIframe(adId, iframeSrc); } else { fetch('/api/ad-preview/' + adId) .then(r => r.json().then(d => ({status: r.status, d}))) .then(({status, d}) => { if (_pvCurrentId !== adId) return; if (d && d.iframe) { _pvSetCache(adId, d.iframe); loadPvIframe(adId, d.iframe); } else { showPvError(adId, d && d.error ? d.error : ('HTTP ' + status)); } }) .catch(err => { if (_pvCurrentId === adId) showPvError(adId, 'fetch fail: ' + err.message); }); } } function showPvError(adId, msg) { const wrap = document.getElementById('pvIframe-' + adId); if (!wrap) return; const ph = wrap.parentElement.querySelector('.pv-placeholder-wrap'); if (ph) { const sp = ph.querySelector('.pv-spinner-box'); if (sp) sp.innerHTML = '
No se pudo cargar el preview
'+msg+'
'; } } function loadPvIframe(adId, src) { const wrap = document.getElementById('pvIframe-' + adId); if (!wrap) return; const iframe = document.createElement('iframe'); iframe.src = src; iframe.allow = 'autoplay'; iframe.scrolling = 'no'; let loaded = false; iframe.onload = function() { loaded = true; wrap.style.opacity = '1'; const ph = wrap.parentElement.querySelector('.pv-placeholder-wrap'); if (ph) ph.style.display = 'none'; }; iframe.onerror = function() { if (!loaded) showPvError(adId, 'iframe error'); }; wrap.appendChild(iframe); // Si Meta tarda >8s en renderizar, mostrar mensaje setTimeout(function() { if (!loaded) showPvError(adId, 'Meta tardó demasiado'); }, 8000); } function pvScheduleHide() { _pvTimer = setTimeout(() => { $('adPreviewFloat').classList.remove('visible'); _pvCurrentId = null; }, 400); } function pvCancelHide() { if (_pvTimer) { clearTimeout(_pvTimer); _pvTimer = null; } } async function toggleAdStatus(adId) { // Find all toggle buttons for this ad (desktop + mobile) const btns = document.querySelectorAll(`#toggle-${adId}, #toggle-m-${adId}`); btns.forEach(b => b.classList.add('ad-toggle-loading')); try { const resp = await fetch('/api/ad-toggle/' + adId, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({slug: _curInfopro}) }); const data = await resp.json(); if (data.ok) { // Update local meta if (_adMeta[adId]) _adMeta[adId].status = data.new; // Update all toggle buttons btns.forEach(b => { b.classList.remove('ad-toggle-loading', 'ad-toggle-on', 'ad-toggle-off'); b.classList.add(data.new === 'ACTIVE' ? 'ad-toggle-on' : 'ad-toggle-off'); b.title = data.new === 'ACTIVE' ? 'Pausar anuncio' : 'Activar anuncio'; }); // Update the status dot and row opacity const row = btns[0]?.closest('tr') || btns[0]?.closest('.ads-card'); if (row) { row.style.opacity = data.new === 'ACTIVE' ? '1' : '0.5'; const dot = row.querySelector('span[style*="border-radius:50%"]'); if (dot) { if (data.new === 'ACTIVE') { dot.style.cssText = 'width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--success);box-shadow:0 0 3px var(--success);animation:pulse 2s infinite'; } else { dot.style.cssText = 'width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--muted)'; } } } toast(data.new === 'ACTIVE' ? '▶️ Ad activado' : '⏸️ Ad pausado'); } else if (data.error === 'rate_limited') { // Meta API saturada (típico en cuentas chicas tipo Constelaciones cuando // varios usuarios + audit pegan a la vez). Mensaje útil en lugar de "bad request". toast('⏳ Meta saturada en esta cuenta. Espera ~1 min y vuelve a intentarlo.', 'warn'); } else { toast('Error: ' + (data.detail || data.error || 'desconocido'), 'danger'); } } catch(e) { toast('Error de conexión', 'danger'); } btns.forEach(b => b.classList.remove('ad-toggle-loading')); } async function toggleAdLeads(adId, row) { const leadsRow = document.getElementById('ad-leads-' + adId); if(!leadsRow) return; // Toggle visibility if(leadsRow.style.display !== 'none') { leadsRow.style.display = 'none'; return; } leadsRow.style.display = ''; const wrap = leadsRow.querySelector('.ad-leads-wrap'); // Don't reload if already loaded if(wrap.dataset.loaded) return; wrap.innerHTML = '
'; try { const slug = _curInfopro; const lanzId = _curLanz.id; // fast=1 → respuesta instantánea sin esperar lookups ManyChat (los enlaces a MC se cargan después si es necesario) // since=start_date → cuadra con el contador del ad (que filtra desde esa fecha) // _t = cache buster (evita que CDN/navegador cachee respuestas viejas) const params = new URLSearchParams({fast: '1', _t: Date.now()}); if (_curLanz.start_date) params.set('since', _curLanz.start_date); const resp = await fetch('/api/lanzamientos/'+slug+'/'+lanzId+'/ad-leads/'+encodeURIComponent(adId)+'?'+params.toString(), { headers: {'Cache-Control': 'no-cache'}, cache: 'no-store', }); if (!resp.ok) { const txt = await resp.text(); throw new Error('HTTP '+resp.status+': '+txt.slice(0,150)); } const leads = await resp.json(); if(!leads.length) { const m = _adMeta[adId]||{}; const sp = m.spend||0; const imp = m.impressions||0; const lpv = m.landing_views||0; wrap.innerHTML = `
0 leads — solo gasto
${sp?''+fmtN(Math.round(sp))+'€ gastado':''} ${imp?''+fmtN(imp)+' impresiones':''} ${lpv?''+fmtN(lpv)+' visitas landing':''}
`; wrap.dataset.loaded = '1'; return; } // Build leads table var cols, colLabels; if(_curInfopro === '1337') { cols = ['NOMBRE','EMAIL','TELÉFONO','EDAD','SITUACIÓN','CAPITAL','EXPERIENCIA','FECHA']; colLabels = ['Nombre','Email','Teléfono','Edad','Situación','Capital','Experiencia','Fecha']; } else if(_curInfopro === 'constelaciones') { cols = ['NOMBRE','EMAIL','TELÉFONO','CONSTELACION','PROBLEM','CONFLICTO','CONCIENCIA','RELACIÓN','FECHA']; colLabels = ['Nombre','Email','Teléfono','Constelación','Problema','Conflicto','Conciencia','Terapia','Fecha']; } else if(_curInfopro === 'vibex') { cols = ['NOMBRE','EMAIL','TELÉFONO','EDAD','FACTURACION','INMUEBLES','PATRIMONIO','CUELLO BOTELLA','FECHA']; colLabels = ['Nombre','Email','Teléfono','Edad','Facturación','Inmuebles','Patrimonio','Cuello botella','Fecha']; } else { cols = ['NOMBRE','EMAIL','TELÉFONO','EDAD','¿TRABAJA ACTUALMENTE?','INGRESOS','DESEO','FECHA']; colLabels = ['Nombre','Email','Teléfono','Edad','Trabajo','Ingresos','Deseo','Fecha']; } // Filter to cols that have data var activeCols = []; cols.forEach(function(c,i){ if(leads.some(function(l){return l[c]})) activeCols.push({key:c,label:colLabels[i]}); }); var hasScore = leads.some(function(l){return l._score != null;}); var tbl = ''; tbl += ''; activeCols.forEach(function(c){ tbl += ''; }); if (hasScore) tbl += ''; tbl += ''; tbl += ''; leads.forEach(function(l){ var mcUrl = l._mc_url || ''; var emailKey = (l.EMAIL || '').toLowerCase(); tbl += ''; activeCols.forEach(function(c){ var val = l[c.key] || '-'; tbl += ''; }); // Score individual del lead (color del bucket exacto) if (hasScore) { var sc = l._score; tbl += ''; } // ManyChat link (placeholder — se rellena en background con fast=0) tbl += ''; tbl += ''; }); tbl += '
'+c.label+'Score
'+esc(val)+''+ljScorePill(sc,'9px')+''; if(mcUrl) { tbl += 'ManyChat ↗'; } tbl += '
'; tbl += '
'+leads.length+' leads de este anuncio
'; wrap.innerHTML = tbl; wrap.dataset.loaded = '1'; // Background: pedir _mc_url (lookup ManyChat) y rellenar las celdas vacías. // El primer render con fast=1 es instantáneo. Esta segunda fetch con fast=0 // hace el lookup ManyChat (~0.5-3s por lead, paralelizado) y enriquece. var alreadyHasMc = leads.some(function(l){return l._mc_url;}); if (!alreadyHasMc) { var bgParams = new URLSearchParams({fast: '0', _t: Date.now()}); if (_curLanz.start_date) bgParams.set('since', _curLanz.start_date); fetch('/api/lanzamientos/'+slug+'/'+lanzId+'/ad-leads/'+encodeURIComponent(adId)+'?'+bgParams.toString(), { headers: {'Cache-Control': 'no-cache'}, cache: 'no-store', }).then(function(r){return r.ok ? r.json() : null;}).then(function(enriched){ if (!enriched) return; var byEmail = {}; enriched.forEach(function(l){ if (l._mc_url && l.EMAIL) byEmail[l.EMAIL.toLowerCase()] = l._mc_url; }); var rows = wrap.querySelectorAll('tr[data-mc-email]'); rows.forEach(function(tr){ var em = tr.getAttribute('data-mc-email'); var url = byEmail[em]; if (!url) return; var cell = tr.querySelector('.ad-mc-cell'); if (cell && !cell.querySelector('a')) { cell.innerHTML = 'ManyChat ↗'; } }); }).catch(function(){}); } } catch(e) { console.error('ad-leads error', e); wrap.innerHTML = '
Error cargando leads
'+(e && e.message ? e.message : String(e))+'
'; } } async function init() { startPresence(); const skills=await (await fetch('/api/skills')).json(); const sel=$('nt-skill'); skills.forEach(s=>{ const o=document.createElement('option'); o.value=s.id; o.textContent=s.name||s.id; sel.appendChild(o); }); $('nt-time').value=new Date(Date.now()+5*60000).toISOString().slice(0,16); loadGlobalStats(); loadInfoproGrid(); } setInterval(()=>{ if($('sec-log').classList.contains('active')) loadLog(); },10000); setInterval(()=>{ if($('sec-home').classList.contains('active')) loadGlobalStats(); },30000); // Auto-refresh DESACTIVADO. Recarga manual con el botón ↻ del header. // === Health badge — auto refresh cada 30s === async function refreshHealth() { try { // Combinamos /api/health (chequeos lógicos) y /api/health/freshness // (heartbeats por loop + Meta failures + LZ stale). El badge muestra // el PEOR estado: si freshness=critical, badge=critical aunque health=OK. const [r, fr] = await Promise.all([ fetch('/api/health').then(x=>x.json()).catch(()=>({status:'UNKNOWN'})), fetch('/api/health/freshness?_t='+Date.now()).then(x=>x.json()).catch(()=>({overall:'unknown'})), ]); const dot = document.getElementById('healthDot'); const lab = document.getElementById('healthLabel'); const badge = document.getElementById('healthBadge'); if(!dot || !lab || !badge) return; window._lastHealth = r; window._lastFreshness = fr; // Decisión combinada: peor de los dos var status = r.status || 'UNKNOWN'; var alerts = (r.active_alerts||[]).length; var freshOverall = fr.overall || 'unknown'; // ok / stale / critical var label = ''; var isCritical = freshOverall === 'critical'; var isStale = (status === 'DEGRADED' || alerts > 0 || freshOverall === 'stale'); var isOk = (status === 'OK' && alerts === 0 && freshOverall === 'ok'); if (isCritical) { // Loop muerto o Meta fallando reiteradamente dot.style.background='var(--danger)'; dot.style.boxShadow='0 0 6px var(--danger)'; badge.style.background='rgba(239,68,68,.10)'; badge.style.borderColor='rgba(239,68,68,.30)'; badge.style.color='var(--danger)'; // Identificar qué loop está muerto para el label var deadLoop = ''; if (fr.loops) { for (const [k,v] of Object.entries(fr.loops)) { if (v.status === 'critical') { deadLoop = k.toUpperCase()+' caído'; break; } } } label = deadLoop || 'Error'; } else if (isOk) { dot.style.background='var(--success)'; dot.style.boxShadow='0 0 6px var(--success)'; badge.style.background='rgba(34,197,94,.10)'; badge.style.borderColor='rgba(34,197,94,.30)'; badge.style.color='var(--success)'; // Mostrar la fuente más fresca para feedback positivo if (fr.loops && fr.loops.csv && fr.loops.csv.age_sec != null) { label = 'Sync hace '+fr.loops.csv.age_sec+'s'; } else { label = 'Healthy'; } } else { // stale dot.style.background='var(--warn)'; dot.style.boxShadow='0 0 6px var(--warn)'; badge.style.background='rgba(234,179,8,.10)'; badge.style.borderColor='rgba(234,179,8,.30)'; badge.style.color='var(--warn)'; var nMeta = (fr.meta_failures||[]).length; var nIssues = alerts || r.issues_count || 0; if (nMeta > 0) label = 'Meta '+nMeta+' fallos'; else if (nIssues > 0) label = 'Issues '+nIssues; else label = 'Sync lento'; } lab.textContent = label; } catch(e) {} } // Modal split limpio (solo iframes — sin sidebar). Se abre desde el modal de insights. async function openLandingMockupClean(lzId, slug, originalUrl) { var mod = document.createElement('div'); mod.style.cssText = 'position:fixed;inset:0;z-index:99999999;background:rgba(0,0,0,.85);display:flex;flex-direction:column;padding:14px'; mod.onclick = function(e){ if(e.target===mod) mod.remove(); }; // Cargar variantes pre-generadas var allVariants = [{id:0, label:'Propuesta v1'}]; try { var resp = await fetch('/api/landing-variants/'+encodeURIComponent(lzId)+'/'+encodeURIComponent(slug)+'?_t='+Date.now()); if (resp.ok) { var data = await resp.json(); if (data.variants && data.variants.length) allVariants = data.variants; } } catch(e) {} var visitedIds = [allVariants[0].id]; // primera tab = default var activeTabIdx = 0; function buildMockupUrl(variantId) { return '/api/landing-mockup/'+encodeURIComponent(lzId)+'/'+encodeURIComponent(slug)+'?variant='+variantId+'&_t='+Date.now(); } function renderTabsBar() { // Una sola barra de tabs ARRIBA — controla qué versión se ve a la derecha. // El lado izquierdo tiene una pestaña fija "Original" para mantener simetría visual. var tabs = ''; tabs += '
'; // separador entre lados tabs += visitedIds.map(function(vid, i) { var v = allVariants.find(function(x){return x.id===vid;}) || {label:'Versión '+(i+1)}; var active = i === activeTabIdx; var bg = active ? '#fff' : 'rgba(255,255,255,.55)'; var col = active ? '#6366f1' : '#555'; var weight = active ? '800' : '600'; var border = active ? '2px solid #6366f1' : '2px solid transparent'; return ''; }).join(''); var allDone = visitedIds.length >= allVariants.length; var btnLabel = allDone ? '✓ Las has visto todas' : '🔄 Otra versión'; var btnDisabled = allDone ? 'disabled' : ''; var btnBg = allDone ? 'rgba(34,197,94,.4)' : 'linear-gradient(90deg,#6366f1,#ec4899)'; var btnCursor = allDone ? 'default' : 'pointer'; var genBtn = ''; return '
'+tabs+genBtn+'
'; } function renderAll() { var currentVariantId = visitedIds[activeTabIdx]; mod.innerHTML = '
' + '
📊 Original vs Mockup
' + '
'+esc(slug)+'
' + '' + '
' + renderTabsBar() + '
' + '' + '' + '
'; // Botón "Otra versión" — solo cicla entre las 4 pre-generadas var bg = mod.querySelector('#mockupGenBtn'); if (bg && !bg.disabled) bg.onclick = function() { var nextId = null; for (var i = 0; i < allVariants.length; i++) { if (visitedIds.indexOf(allVariants[i].id) === -1) { nextId = allVariants[i].id; break; } } if (nextId === null) return; // todas visitadas visitedIds.push(nextId); activeTabIdx = visitedIds.length - 1; renderAll(); }; // Tabs click mod.querySelectorAll('button[data-tab-idx]').forEach(function(b) { b.onclick = function() { activeTabIdx = parseInt(b.getAttribute('data-tab-idx'), 10); renderAll(); }; }); } renderAll(); document.body.appendChild(mod); } // === Landing insights modal compacto === async function showLandingInsights(lzId, slug, displayName) { var m = document.getElementById('landingInsightsModal'); if (!m) { m = document.createElement('div'); m.id = 'landingInsightsModal'; m.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:20px'; m.onclick = function(e){ if(e.target===m) m.remove(); }; document.body.appendChild(m); } m.innerHTML = '
' + '
' + '
💡
' + '
Análisis IA — '+esc(displayName)+'
' + '
Landing '+slug.slice(-1).toUpperCase()+' · '+lzId+'
' + '' + '
' + '
Cargando análisis…
' + '
'; try { // Fetch insights estáticos. Plugin WP DEPRECATED — visitas se derivan de Meta + CSV. var resp = await fetch('/api/landing-insights/'+encodeURIComponent(lzId)+'/'+encodeURIComponent(slug)+'?_t='+Date.now(), {cache:'no-store'}); if (!resp.ok) { var t = await resp.text(); throw new Error('HTTP '+resp.status+': '+t.slice(0,200)); } var data = await resp.json(); // FUENTE CANÓNICA HÍBRIDA: registros CSV / (visitas Meta × weight_plugin). // Si plugin no disponible → fallback equitativo entre variantes A/B principales. var lzCur = _curLanz || {}; var landsCur = ((lzCur.captacion || {}).landings) || []; var totRegsLz = landsCur.reduce(function(s,l){ return s + (l.registros || 0); }, 0); var thisLand = landsCur.find(function(l){ var k = (l.url || '').replace(/\/$/, '').split('/').pop(); return k === slug || (l.name === slug); }) || {}; var registrosReal = thisLand.registros || data.registros || 0; var landViewsLzTotal = lzCur.landing_views_total || 0; var wpStats = window._wpLandingStats || {}; var thisRs = wpStats[slug]; var visitasReal; if (thisRs && typeof thisRs.weight === 'number' && thisRs.weight > 0 && landViewsLzTotal > 0) { visitasReal = Math.round(landViewsLzTotal * thisRs.weight); } else { var nMainIns = landsCur.filter(function(ld){ return (ld.registros || 0) > totRegsLz * 0.05; }).length || landsCur.length || 1; var thisIsMain = (registrosReal > totRegsLz * 0.05); visitasReal = thisIsMain ? (landViewsLzTotal > 0 ? Math.round(landViewsLzTotal / nMainIns) : (data.visitas || 0)) : (totRegsLz > 0 && landViewsLzTotal > 0 ? Math.round(landViewsLzTotal * (registrosReal / totRegsLz)) : (data.visitas || 0)); } var convReal = visitasReal > 0 ? (registrosReal / visitasReal * 100) : (data.conversion_pct || 0); var sinceTxt = lzCur.fecha_inicio ? ' · desde '+lzCur.fecha_inicio : ''; var body = document.getElementById('landingInsightsBody'); if (!body) return; var ins = data.insights || {}; var tips = ins.tips || []; var reps = ins.text_replacements || []; // ── Calcular uplift optimista a partir de los `impacto` de cada tip ── // Sumamos la cota ALTA del rango (+X-Y%) y aplicamos diminishing returns // (no se acumula 1:1 — cada tip añade menos que el anterior) var sumUpliftHigh = 0; tips.forEach(function(t){ var im = (t.impacto || '').toString(); var nums = (im.match(/(\d+(?:\.\d+)?)/g) || []).map(parseFloat); if (nums.length >= 2) sumUpliftHigh += Math.max.apply(null, nums); else if (nums.length === 1) sumUpliftHigh += nums[0]; }); // Diminishing returns: combinación realista, no suma cruda var combinedFactor = 1; var sortedNums = []; tips.forEach(function(t){ var im = (t.impacto || '').toString(); var nums = (im.match(/(\d+(?:\.\d+)?)/g) || []).map(parseFloat); if (nums.length) sortedNums.push(Math.max.apply(null, nums)); }); sortedNums.sort(function(a,b){return b-a;}); sortedNums.forEach(function(u, idx){ // Tip 1: 100% del impacto, tip 2: 60%, tip 3: 35%, tip 4: 20%, tip 5+: 10% var weight = idx === 0 ? 1 : idx === 1 ? 0.6 : idx === 2 ? 0.35 : idx === 3 ? 0.2 : 0.1; combinedFactor *= (1 + (u/100) * weight); }); var convOptimista = convReal * combinedFactor; var deltaPct = convOptimista - convReal; var registrosExtra = Math.round((deltaPct/100) * visitasReal); // BOTÓN GRANDE arriba con animación var html = ''; // ── HERO: % conversión optimista estimado como PROTAGONISTA ── if (convReal > 0 && convOptimista > convReal) { html += '
' + '
⚡ Conversión estimada con cambios
' + '
' + '
'+convReal.toFixed(2)+'%
' + '
' + '
'+convOptimista.toFixed(2)+'%
' + '
' + '
+'+deltaPct.toFixed(2)+' pp · ≈ +'+fmtN(registrosExtra)+' registros más con las mismas visitas
' + '
Estimación combinada de los '+tips.length+' tips (con diminishing returns)
' + '
'; } // 3 KPIs reales filtrados (debajo del hero) html += '
' + '
'+fmtN(visitasReal)+'
Visitas'+sinceTxt+'
' + '
'+fmtN(registrosReal)+'
Registros'+sinceTxt+'
' + '
'+convReal.toFixed(2)+'%
Conversión actual
' + '
'; if (ins.diagnostico) { html += '
Diagnóstico
'+esc(ins.diagnostico)+'
'; } if (ins.publico) { html += '
Match Público
'+esc(ins.publico)+'
'; } if (tips.length) { html += '
'+tips.length+' tips para mejorar conversión
'; tips.forEach(function(t,i){ html += '
' +'
' +'
'+(i+1)+'
' +'
' +'
'+esc(t.titulo||'')+'
' +'
'+esc(t.accion||'')+'
' +(t.ejemplo?'
"'+esc(t.ejemplo)+'"
':'') +(t.impacto?'
'+esc(t.impacto)+'
':'') +'
'; }); } if (reps.length) { html += '
' + '
' + '
Cambios de copy ('+reps.length+')
' + '' + '
'; reps.forEach(function(r){ html += '
' + '
'+esc(r.antes||'')+'
' + '
→ '+esc(r.despues||'')+'
' + (r.razon?'
'+esc(r.razon)+'
':'') + '
'; }); html += '
'; } if (data.url) { html += '
Ver landing original ↗
'; } body.innerHTML = html; } catch(e) { var body = document.getElementById('landingInsightsBody'); if (body) body.innerHTML = '
Error analizando landing
'+esc(e.message||String(e))+'
'; } } async function showHealthDetail() { var r = window._lastHealth || {checks:{}}; var lines = []; Object.entries(r.checks||{}).forEach(([k,v])=>{ var icon = v.ok ? '✅' : '⚠️'; var msg = v.ok ? 'OK' : (v.issues||[]).join(', '); lines.push(`${icon} ${k}: ${msg}`); }); if((r.active_alerts||[]).length) lines.push('','🚨 Alerts: '+r.active_alerts.join(', ')); // Estado por fuente de datos try { const fr = await fetch('/api/health/freshness?_t='+Date.now()).then(x=>x.json()); // ── Loops paralelos (CSV / Sales / Medium) ── const overallIcon = fr.overall === 'critical' ? '🔴' : (fr.overall === 'stale' ? '🟡' : '🟢'); lines.push('','── Sistema: '+overallIcon+' '+(fr.overall||'?').toUpperCase()+' ──'); if (fr.loops) { const loopNames = {csv:'CSVs', sales:'Sales/Calendly', medium:'Scoring/Meta/MC'}; Object.entries(fr.loops).forEach(([k, v]) => { const icon = v.status === 'critical' ? '🔴' : (v.status === 'stale' ? '🟡' : (v.status === 'ok' ? '🟢' : '⚪')); const ageStr = v.age_sec == null ? 'sin heartbeat' : v.age_sec < 60 ? `${v.age_sec}s` : v.age_sec < 3600 ? `${Math.floor(v.age_sec/60)}min` : `${Math.floor(v.age_sec/3600)}h`; lines.push(`${icon} ${loopNames[k]||k}: hace ${ageStr} (esperado <${v.expected_sec}s)`); }); } // ── Meta API failures (no silenciosas) ── if ((fr.meta_failures||[]).length) { lines.push('','── Meta API fallos ('+fr.meta_failures.length+') ──'); fr.meta_failures.slice(0, 8).forEach(f => { const retryStr = f.next_retry_in_sec < 60 ? `${f.next_retry_in_sec}s` : f.next_retry_in_sec < 3600 ? `${Math.floor(f.next_retry_in_sec/60)}min` : `${Math.floor(f.next_retry_in_sec/3600)}h`; lines.push(`⚠ x${f.count} retry en ${retryStr} — ${f.last_error}`); }); } // ── Smoke tests (invariantes de negocio) ── if (fr.smoke) { const smokeFails = fr.smoke.failures || []; if (smokeFails.length) { lines.push('','── Validación: '+smokeFails.length+' problemas detectados ──'); smokeFails.slice(0, 10).forEach(f => lines.push(`❌ ${f}`)); } else if (fr.smoke.checks) { const okCount = Object.values(fr.smoke.checks).filter(c => c.ok).length; const total = Object.keys(fr.smoke.checks).length; lines.push('','── Validación: '+okCount+'/'+total+' invariantes OK ──'); } } // ── Frescura por LZ ── lines.push('','── LZ activos ──'); (fr.lz||[]).forEach(lz => { const icon = lz.stale ? '🟠' : '🟢'; const age = lz.age_min != null ? `${lz.age_min} min` : '?'; lines.push(`${icon} ${lz.id}: hace ${age}`); }); if (fr.watchdog && (fr.watchdog.crit || fr.watchdog.warn)) { lines.push('','── Watchdog ──'); lines.push(`Crit: ${fr.watchdog.crit} · Warn: ${fr.watchdog.warn}`); (fr.watchdog.last_lines||[]).slice(-3).forEach(l => lines.push(' ' + l.slice(22))); } } catch(e) { lines.push('Error fetching freshness: '+e); } alert((r.status||'?')+'\n\n'+lines.join('\n')); } refreshHealth(); setInterval(refreshHealth, 30000); function handleHash() { var h = window.location.hash.replace('#/',''); if(!h) { showHome(); return; } var parts = h.split('/'); if(parts.length === 2) { showLanzamiento(parts[0], parts[1]); } else if(parts.length === 1) { showInfopro(parts[0]); } else { showHome(); } } window.addEventListener('hashchange', handleHash); init().then(function(){ if(window.location.hash && window.location.hash !== '#/') handleHash(); }); function showScoreTooltip(e, text, label, color) { let tip = document.getElementById('scoreTooltip'); if (!tip) { tip = document.createElement('div'); tip.id = 'scoreTooltip'; document.body.appendChild(tip); } tip.innerHTML = `
${label}
${text}
`; tip.style.display = 'block'; const r = e.target.getBoundingClientRect(); tip.style.left = r.left + 'px'; tip.style.top = (r.bottom + 8) + 'px'; } function hideScoreTooltip() { const tip = document.getElementById('scoreTooltip'); if (tip) tip.style.display = 'none'; } // ═══════════════════════════════════════════════════════════ // VSL EVERGREEN — Dedicated Panel // ═══════════════════════════════════════════════════════════ var _vslPeriod = '30d'; var _vslCompare = false; function _vslDateRange(period) { var now = new Date(); var today = now.toISOString().slice(0,10); var start = null; if (period === 'all') start = null; else if (period === '7d') { var d=new Date(now);d.setDate(d.getDate()-6);start=d.toISOString().slice(0,10); } else if (period === '14d') { var d=new Date(now);d.setDate(d.getDate()-13);start=d.toISOString().slice(0,10); } else if (period === '30d') { var d=new Date(now);d.setDate(d.getDate()-29);start=d.toISOString().slice(0,10); } else if (period === 'week') { var d=new Date(now);var dow=d.getDay()||7;d.setDate(d.getDate()-dow+1);start=d.toISOString().slice(0,10); } else if (period === 'month') { start=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-01'; } return {start:start, end:today}; } function _vslQuickSelect(sectionId, from, to) { // Clamp from to first date with data var _dr = _vslDataRange(); if (_dr.first && from < _dr.first) from = _dr.first; _vslFilters[sectionId] = {mode:'custom', from:from, to:to}; if (_vslPickers[sectionId]) { _vslPickers[sectionId].setDate([from, to], false); // false = no trigger onChange (avoid double refresh) setTimeout(function(){ try{_vslPickers[sectionId].close();}catch(e){} }, 50); } var bar = document.querySelector('.vsl-section-filter[data-section="'+sectionId+'"]'); if(bar) { bar.querySelectorAll('.vsl-f-btn').forEach(function(b){b.classList.remove('vsl-f-active')}); var calBtn = bar.querySelector('.vsl-f-btn[data-mode="custom"]'); if(calBtn) calBtn.classList.add('vsl-f-active'); } vslRefreshSection(sectionId); } function _vslAggregate(ds, start, end) { var r = {leads:0,paid_leads:0,ventas:0,fact:0,cash:0,spend:0,landing_views:0,agendas:0,canceladas:0,realizadas:0,days:0,attr_dir:0,attr_ind:0,attr_fact_dir:0,attr_fact_ind:0}; for (var day in ds) { if (start && day < start) continue; if (day > end) continue; r.leads += ds[day].leads||0; r.paid_leads += ds[day].paid_leads||0; r.ventas += ds[day].ventas||0; r.fact += ds[day].fact||0; r.cash += ds[day].cash||0; r.spend += ds[day].spend||0; r.landing_views += ds[day].landing_views||0; r.agendas += ds[day].agendas||0; r.canceladas += ds[day].canceladas||0; r.realizadas += ds[day].realizadas||0; r.attr_dir += ds[day].attr_dir||0; r.attr_ind += ds[day].attr_ind||0; r.attr_fact_dir += ds[day].attr_fact_dir||0; r.attr_fact_ind += ds[day].attr_fact_ind||0; r.days++; } return r; } function _vslPrevRange(start, end) { if (!start) return {start:null,end:null}; var s = new Date(start+'T00:00:00'); var e = new Date(end+'T00:00:00'); var span = Math.round((e-s)/(86400000))+1; var ps = new Date(s); ps.setDate(ps.getDate()-span); var pe = new Date(s); pe.setDate(pe.getDate()-1); return {start:ps.toISOString().slice(0,10), end:pe.toISOString().slice(0,10)}; } function _vslDelta(cur, prev) { if (!prev || prev===0) return {pct:0,txt:'—',cls:''}; var pct = ((cur-prev)/prev*100); var arrow = pct>0?'▲':pct<0?'▼':''; var cls = pct>0?'color:var(--success)':pct<0?'color:var(--danger)':'color:var(--muted)'; return {pct:pct, txt:arrow+Math.abs(pct).toFixed(1)+'%', cls:cls}; } function _vslDeltaInv(cur, prev) { // Inverted: lower is better (CPL) var d = _vslDelta(cur,prev); if (d.pct>0) d.cls='color:var(--danger)'; else if (d.pct<0) d.cls='color:var(--success)'; return d; } // ─── VSL Section Date Filter Widget (Flatpickr calendar) ─── var _vslFilters = {}; var _vslPickers = {}; function vslFilterBar(sectionId, defaultMode) { // Read current mode from state (survives re-renders), fallback to defaultMode var curMode = (_vslFilters[sectionId] && _vslFilters[sectionId].mode) ? _vslFilters[sectionId].mode : (defaultMode || 'week'); var uid = 'vsl-cal-'+sectionId; function ac(m){ return curMode===m?' vsl-f-active':''; } return '
' +'' +'' +'' +'' +'' +'
'; } function vslInitPicker(sectionId) { var uid = 'vsl-cal-'+sectionId; var el = document.getElementById(uid); if (!el) return; // Destroy old picker if the DOM element changed (re-render) if (_vslPickers[sectionId]) { try { _vslPickers[sectionId].destroy(); } catch(e) {} _vslPickers[sectionId] = null; } // Hidden input for flatpickr (button triggers it) — clean up old one first var oldHidden = document.getElementById(uid + '-hidden'); if (oldHidden) oldHidden.remove(); var hiddenInput = document.createElement('input'); hiddenInput.style.display = 'none'; hiddenInput.style.position = 'absolute'; hiddenInput.style.visibility = 'hidden'; hiddenInput.style.width = '0'; hiddenInput.style.height = '0'; hiddenInput.id = uid + '-hidden'; el.parentNode.insertBefore(hiddenInput, el.nextSibling); // Get data range to block dates with no data var _dr = _vslDataRange(); var _minDate = _dr.first || null; var _maxDate = _dr.last || null; var picker = flatpickr(hiddenInput, { mode: 'range', locale: 'es', dateFormat: 'j M Y', defaultDate: null, minDate: _minDate || undefined, disableMobile: true, animate: true, positionElement: el, onReady: function(sel, dateStr, instance) { // Inject quick-select panel into flatpickr calendar var cal = instance.calendarContainer; if (!cal) return; var panel = document.createElement('div'); panel.className = 'vsl-quick-panel'; var yr = new Date().getFullYear(); var sid = sectionId; // capture for inline handlers var minD = _minDate || '1970-01-01'; var maxD = _maxDate || '2099-12-31'; // Quarters — disable if entirely before data range var qRow = '
T'; var quarters = [ {l:'1\u00baT',f:yr+'-01-01',t:yr+'-03-31'}, {l:'2\u00baT',f:yr+'-04-01',t:yr+'-06-30'}, {l:'3\u00baT',f:yr+'-07-01',t:yr+'-09-30'}, {l:'4\u00baT',f:yr+'-10-01',t:yr+'-12-31'} ]; quarters.forEach(function(q){ var disabled = q.t < minD || q.f > maxD; if (disabled) { qRow+=''; } else { // Clamp from to minDate var cf = q.f < minD ? minD : q.f; qRow+=''; } }); qRow+='
'; // Months — disable if entirely before data range var mRow = '
M'; var mNames = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; mNames.forEach(function(m, i){ var mi = String(i+1).padStart(2,'0'); var lastDay = new Date(yr, i+1, 0).getDate(); var mFrom = yr+'-'+mi+'-01'; var mTo = yr+'-'+mi+'-'+String(lastDay).padStart(2,'0'); var disabled = mTo < minD || mFrom > maxD; if (disabled) { mRow+=''; } else { var cf = mFrom < minD ? minD : mFrom; mRow+=''; } }); mRow+='
'; panel.innerHTML = qRow + mRow; cal.insertBefore(panel, cal.firstChild); }, onChange: function(dates) { if (dates.length === 2) { var from = dates[0].toISOString().slice(0,10); var to = dates[1].toISOString().slice(0,10); _vslFilters[sectionId] = {mode:'custom', from:from, to:to}; var bar = document.querySelector('.vsl-section-filter[data-section="'+sectionId+'"]'); if(bar) { bar.querySelectorAll('.vsl-f-btn').forEach(function(b){b.classList.remove('vsl-f-active')}); var calBtn = bar.querySelector('.vsl-f-btn[data-mode="custom"]'); if(calBtn) calBtn.classList.add('vsl-f-active'); } vslRefreshSection(sectionId); } } }); _vslPickers[sectionId] = picker; // Button click opens the flatpickr el.addEventListener('click', function(){ picker.open(); }); // Restore current filter dates on the picker (survives re-renders) var curF = _vslFilters[sectionId]; if (curF && curF.from) { picker.setDate([curF.from, curF.to], false); } // Update range badge after init _vslUpdateRangeLabel(sectionId); } function vslSetFilter(sectionId, mode) { var now = new Date(); var today = now.toISOString().slice(0,10); var from = null; if (mode === 'today') { from = today; } else if (mode === 'week') { var d=new Date(now);d.setDate(d.getDate()-6);from=d.toISOString().slice(0,10); } else if (mode === 'month') { var d=new Date(now);d.setDate(d.getDate()-29);from=d.toISOString().slice(0,10); } else if (mode === 'all') { var _dr0 = _vslDataRange(); from = _dr0.first || null; } // Clamp from to first date with data if (from) { var _dr = _vslDataRange(); if (_dr.first && from < _dr.first) from = _dr.first; } _vslFilters[sectionId] = {mode:mode, from:from, to:today}; // Update buttons via data-mode var bar = document.querySelector('.vsl-section-filter[data-section="'+sectionId+'"]'); if(bar) { bar.querySelectorAll('.vsl-f-btn').forEach(function(b){b.classList.remove('vsl-f-active')}); var hit = bar.querySelector('.vsl-f-btn[data-mode="'+mode+'"]'); if(hit) hit.classList.add('vsl-f-active'); } // Sync flatpickr calendar to show selected dates (false = don't trigger onChange) if (_vslPickers[sectionId]) { if (from) { _vslPickers[sectionId].setDate([from, today], false); } else { _vslPickers[sectionId].clear(); } } vslRefreshSection(sectionId); } function vslGetRange(sectionId) { var f = _vslFilters[sectionId]; if (!f) { var now=new Date();var d=new Date(now);d.setDate(d.getDate()-6); return {from:d.toISOString().slice(0,10),to:now.toISOString().slice(0,10)}; } return {from:f.from, to:f.to}; } function _vslFmtDateShort(iso) { if (!iso) return ''; var d = new Date(iso+'T12:00:00'); var meses = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic']; return d.getDate()+' '+meses[d.getMonth()]; } function _vslDataRange() { // Find first and last date with leads > 0 in daily_stats var ds = _curLanz ? _curLanz.daily_stats||{} : {}; var first = null, last = null; Object.keys(ds).sort().forEach(function(d) { if ((ds[d].leads||0) > 0) { if (!first) first = d; last = d; } }); return {first: first, last: last}; } function _vslUpdateRangeLabel(sectionId) { var r = vslGetRange(sectionId); var label = document.getElementById('vsl-range-'+sectionId); if (!label) return; if (!r.from) { label.innerHTML = 'Todo el periodo'; } else { var ds = _curLanz ? _curLanz.daily_stats||{} : {}; var tot=0; Object.keys(ds).forEach(function(d){ if((!r.from||d>=r.from)&&d<=r.to) tot+=(ds[d].leads||0); }); var fromStr = _vslFmtDateShort(r.from); var toStr = _vslFmtDateShort(r.to); // Check if selected range has any lead data var dataRange = _vslDataRange(); var noData = false; var partialData = false; if (dataRange.first) { if (r.to < dataRange.first || r.from > dataRange.last) { noData = true; } else if (r.from < dataRange.first || r.to > dataRange.last) { partialData = true; } } else { noData = true; } var warning = ''; if (noData) { warning = 'Sin datos de leads en este periodo \u2014 CSV desde '+_vslFmtDateShort(dataRange.first||'')+''; } else if (partialData && tot === 0) { warning = 'Periodo parcialmente fuera del rango de datos'; } label.innerHTML = '' +''+fromStr+' \u2192 '+toStr+'' +'\u00b7' +''+fmtN(tot)+' leads' +'' +warning; } } function vslRefreshSection(sectionId) { _vslUpdateRangeLabel(sectionId); if (sectionId === 'kpi' || sectionId === 'kpi-vsl1') { // Re-render full page to recalc KPIs with new filter if (_curLanz && _curInfopro) showLanzamiento(_curInfopro, _curLanz.id); return; } if (sectionId === 'agendas') renderAgendasChart(); if (sectionId === 'ads') { renderFilteredAds(); renderAdsHighlights(); } if (sectionId === 'score') { renderLeadJourney(); } if (sectionId === 'mc') { renderMcFunnel(); } } // Init flatpickr pickers after DOM render function vslInitAllPickers() { ['kpi','kpi-vsl1','agendas','ads','score','mc'].forEach(function(s){ vslInitPicker(s); }); } function renderVslAgendas() { var wrap = document.getElementById('vslAgendasSection'); if (!wrap || !_curLanz || _curLanz.tipo !== 'vsl') return; var ds = _curLanz.daily_stats || {}; var r = vslGetRange('agendas'); // Filter daily stats var days = Object.keys(ds).filter(function(d) { if (r.from && d < r.from) return false; if (r.to && d > r.to) return false; return true; }).sort(); var totalLeads = 0, totalVentas = 0, totalFact = 0; days.forEach(function(d) { totalLeads += ds[d].leads||0; totalVentas += ds[d].ventas||0; totalFact += ds[d].fact||0; }); var maxDay = Math.max.apply(null, days.map(function(d){return ds[d].leads||0}).concat([1])); var barsHtml = days.map(function(d) { var ld = ds[d].leads || 0; var vt = ds[d].ventas || 0; var h = Math.max(3, Math.round(ld/maxDay*70)); var dayName = new Date(d+'T12:00:00').toLocaleDateString('es',{weekday:'short'}).slice(0,2); var dayNum = d.slice(8); return '
' +'
' +(vt>0?'
'+vt+'
':'') +'
' +'
' +'
'+dayName+'
' +'
'+dayNum+'
' +'
'; }).join(''); var avgLeads = days.length > 0 ? (totalLeads/days.length).toFixed(0) : 0; var avgVentas = days.length > 0 ? (totalVentas/days.length).toFixed(1) : 0; wrap.innerHTML = '' +'
' +'Actividad diaria' +''+fmtN(totalLeads)+' leads · '+fmtN(totalVentas)+' ventas · '+fmtN(Math.round(totalFact))+'€ · '+avgLeads+' leads/d' +'
' +vslFilterBar('agendas', _vslFilters.agendas?.mode || 'week') +'
'+barsHtml+'
'; // Init default if not set if (!_vslFilters.agendas) vslSetFilter('agendas', 'week'); } function setVslPeriod(period) { _vslPeriod = period; if (_curLanz && _curLanz.tipo === 'vsl' && _curInfopro) { showLanzamiento(_curInfopro, _curLanz.id); } } function renderVslPanel(p, l) { var ds = l.daily_stats || {}; var ws = l.weekly_stats || {}; var mc = l.manychat || {}; var lsa = l.lead_scoring_avatar || {}; var ads = (l.lead_scoring_ads||{}).ads || []; var sbw = l.score_by_week || {}; setBreadcrumb([ {label:'Home',onclick:'showHome()'}, {label:p.nombre,onclick:"showInfopro('"+p.slug+"')"}, {label:'VSL Evergreen'} ]); $('topbar-infopro').style.display='flex'; $('topbar-infopro').innerHTML='
'+(p.contacto||p.nombre)+'
'+p.nombre+'
'; // Load ad meta for thumbnails var adIds = ads.map(function(a){return a.utm_content||a.ad_id}).filter(Boolean); if(adIds.length) fetch('/api/ad-meta-batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:adIds,slug:p.slug,lanz_id:l.id})}).then(function(r){return r.json()}).then(function(d){_adMeta=d;_renderVslAds();try{var _cb=document.getElementById('adsCountryBar');if(_cb)_cb.innerHTML=_renderAdsCountryBarInner();}catch(e){}renderLeadJourney()}).catch(function(){}); _renderVslBody(p, l); } // (attribution is in showLanzamiento rowBlock grid, not here) function _renderVslBody(p, l) { var ds = l.daily_stats || {}; var ws = l.weekly_stats || {}; var mc = l.manychat || {}; var ads = (l.lead_scoring_ads||{}).ads || []; var range = _vslDateRange(_vslPeriod); var cur = _vslAggregate(ds, range.start, range.end); var prev = {leads:0,ventas:0,fact:0,days:0}; var prevRange = {start:null,end:null}; if (_vslCompare && range.start) { prevRange = _vslPrevRange(range.start, range.end); prev = _vslAggregate(ds, prevRange.start, prevRange.end); } var leadsPerDay = cur.days>0 ? (cur.leads/cur.days) : 0; var ventasPerDay = cur.days>0 ? (cur.ventas/cur.days) : 0; var cpl = (l.gasto_total_real||0)>0 && (l.csv_leads_unique||0)>0 ? ((l.gasto_total_real)/(l.csv_leads_unique)) : 0; var roas = (l.gasto_total_real||0)>0 ? ((l.resultados_facturacion||0)/(l.gasto_total_real)) : 0; var convPct = cur.leads>0 ? (cur.ventas/cur.leads*100) : 0; // Comparison deltas var dLeads = _vslDelta(cur.leads, prev.leads); var dVentas = _vslDelta(cur.ventas, prev.ventas); var dFact = _vslDelta(cur.fact, prev.fact); var dLpd = _vslDelta(leadsPerDay, prev.days>0?prev.leads/prev.days:0); var dVpd = _vslDelta(ventasPerDay, prev.days>0?prev.ventas/prev.days:0); var showComp = _vslCompare && range.start; function deltaHtml(d) { if (!showComp) return ''; return '
'+d.txt+'
'; } function kpiCard(value, label, accent, delta) { return '
' +'
'+label+'
' +'
'+value+'
' +delta +'
'; } // ── Trend bars ── var allDays = Object.keys(ds).filter(function(d){return !range.start||d>=range.start}).filter(function(d){return d<=range.end}).sort(); var maxL = Math.max.apply(null, allDays.map(function(d){return ds[d].leads||0}).concat([1])); var barsHtml = allDays.map(function(d){ var ld = ds[d].leads||0; var vt = ds[d].ventas||0; var h = Math.max(2, Math.round(ld/maxL*80)); var dayLabel = d.slice(5); // MM-DD return '
' +'
' +(vt>0?'
':'') +'
'+(allDays.length<=14?dayLabel:'')+'
' +'
'; }).join(''); // ── Funnel rates ── var funnelSteps = [ {key:'leads_totales', label:'Leads totales'}, {key:'inician_primer_boton', label:'Primer botón'}, {key:'ven_vsl', label:'Ven el vídeo'}, {key:'lead_caliente', label:'Lead caliente'}, {key:'click_agendar', label:'Click agendar'}, {key:'nueva_entrevista', label:'Nueva entrevista'}, {key:'cita_confirmada', label:'Cita confirmada'}, ]; var fBase = mc.leads_totales || 1; var funnelHtml = funnelSteps.map(function(f, i) { var val = mc[f.key] || 0; var pct = (val/fBase*100).toFixed(1); var stepRate = ''; if (i > 0) { var prevVal = mc[funnelSteps[i-1].key] || 1; stepRate = prevVal > 0 ? '' + (val/prevVal*100).toFixed(1) + '% del paso anterior' : ''; } var w = Math.max(8, val/fBase*100); return '
' +'
' +''+f.label+'' +''+fmtN(val)+' ('+pct+'%)' +'
' +'
' +'
' +'
' +(stepRate?'
'+stepRate+'
':'') +'
'; }).join(''); // ── Score trend (weekly) ── var scoreWeeks = Object.keys(l.score_by_week||{}).sort(); var scoreTrendHtml = ''; if (scoreWeeks.length >= 3) { var lastW = scoreWeeks.slice(-6); var first = (l.score_by_week[lastW[0]]||{}).avg||0; var last = (l.score_by_week[lastW[lastW.length-1]]||{}).avg||0; var trend = last - first; var trendTxt = trend > 0.1 ? '▲ Subiendo' : trend < -0.1 ? '▼ Bajando' : '— Estable'; var trendColor = trend > 0.1 ? 'var(--success)' : trend < -0.1 ? 'var(--danger)' : 'var(--muted)'; scoreTrendHtml = '
' + lastW.map(function(w){ var sc = (l.score_by_week[w]||{}).avg||0; var h = Math.max(8, Math.round(sc/5*40)); return '
'+w.slice(5)+'
'+sc+'
'; }).join('') + '
'+trendTxt+'
' + '
'; } // ── Period label ── var periodLabel = _vslPeriod === 'all' ? 'Todo ('+cur.days+' días)' : range.start+' → '+range.end+' ('+cur.days+'d)'; $('lanzamientoDetail').innerHTML = '' // Hero +'
' +'
' +'
🟢
' +'

VSL Evergreen

'+p.nombre+' · Siempre activo

' +'
' +renderSyncPill(l) +'activo' +'
' // Period control bar +'
' +'Periodo' +'
' +['7d','14d','30d','week','month','all'].map(function(p){ var lbl = p==='week'?'7 d\u00edas':p==='month'?'Mes':p==='all'?'Todo':p; return ''; }).join('') +'
' +'' +'
'+periodLabel+'
' +'
' // KPI Pulse Strip +'
' +kpiCard(fmtN(cur.leads), 'Leads', 'var(--primary)', deltaHtml(dLeads)) +kpiCard(fmtN(cur.ventas)+' ('+convPct.toFixed(1)+'%)', 'Ventas', 'var(--success)', deltaHtml(dVentas)) +kpiCard(fmtN(Math.round(cur.fact))+'€', 'Facturación', 'var(--success)', deltaHtml(dFact)) +kpiCard(leadsPerDay.toFixed(0)+'/d', 'Leads/día', 'var(--primary)', deltaHtml(dLpd)) +kpiCard(ventasPerDay.toFixed(1)+'/d', 'Ventas/día', '#7C3AED', deltaHtml(dVpd)) +'
' // Trend chart +'
' +'
Tendencia diaria — leads (barras) + ventas (puntos verdes)
' +'
'+barsHtml+'
' +'
' // Two columns: Funnel + Lifetime +'
' // Funnel +'
' +'
Embudo VSL Acumulado
' +funnelHtml +'
' // Lifetime stats + score trend +'
' +'
' +'
Lifetime
' +'
' +kpiCard(fmtN(l.csv_leads_unique||0), 'Leads totales', 'var(--primary)', '') +kpiCard(fmtN(l.resultados_ventas||0), 'Ventas totales', 'var(--success)', '') +kpiCard(fmtN(l.gasto_total_real||0)+'€', 'Gasto total', 'var(--danger)', '') +kpiCard('x'+roas.toFixed(2), 'ROAS', roas>=1?'var(--success)':'var(--danger)', '') +kpiCard(cpl>0?cpl.toFixed(1)+'€':'—', 'CPL medio', 'var(--warn)', '') +kpiCard(fmtN(Math.round((l.resultados_facturacion||0)))+'€', 'Facturación total', 'var(--success)', '') +'
' +'
' +(scoreTrendHtml ? '
' +'
Calidad de leads por semana
' +scoreTrendHtml+'
' : '') +'
' +'
' // Ads table — etiqueta dinámica según _vslPeriod +'
' +'
Ads '+(_vslPeriod==='all'?'Acumulado · '+ads.length+' anuncios':'Periodo activo · escalado por leads_by_week')+'
' +'
' +'
'; // Cache p for re-renders window._vslCachedP = p; // Render ads table _renderVslAds(); } function _renderVslAds() { var container = document.getElementById('vslAdsContainer'); if (!container || !_curLanz) return; var ads = (_curLanz.lead_scoring_ads||{}).ads || []; var meta = _adMeta || {}; // Aplicar filtro principal de _vslPeriod a la tabla pequeña Ads del cuerpo VSL var _bodyRange = (typeof _vslDateRange === 'function') ? _vslDateRange(_vslPeriod) : {start:null,end:null}; var _bodyFiltered = !!_bodyRange.start; if (_bodyFiltered) { ads = ads.map(function(a){ var inRange = _vslAdLeadsInRange(a, _bodyRange.start, _bodyRange.end); var copy = Object.assign({}, a); copy._period_leads = inRange.leads; copy._period_ratio = inRange.ratio; copy._period_active = true; return copy; }).filter(function(a){ return (a._period_leads||0) > 0; }) .sort(function(a,b){ return (b._period_leads||0) - (a._period_leads||0); }); } var top20 = ads.slice(0, 30); container.innerHTML = top20.map(function(a, i) { var aid = a.utm_content || a.ad_id; var m = meta[aid] || {}; var name = (m.name || 'Ad '+aid).substring(0, 50); var thumb = m.thumbnail || ''; var periodMode = !!a._period_active; var adLeadsDisplay = periodMode ? (a._period_leads||0) : (a.leads||0); var spend = m.spend || 0; var cpl = m.cpl || 0; var status = m.status || ''; var statusDot = status==='ACTIVE'?'var(--success)':status==='PAUSED'?'var(--warn)':'var(--muted)'; // Fatigue sparkline from leads_by_week var lbw = a.leads_by_week || {}; var weeks = Object.keys(lbw).sort().slice(-4); var sparkHtml = ''; if (weeks.length >= 2) { var maxW = Math.max.apply(null, weeks.map(function(w){return lbw[w]})); sparkHtml = '
' + weeks.map(function(w){ var h = maxW>0 ? Math.max(2, Math.round(lbw[w]/maxW*16)) : 2; return '
'; }).join('') + '
'; // Fatigue detection if (weeks.length >= 3) { var first = lbw[weeks[0]]||0; var last = lbw[weeks[weeks.length-1]]||0; if (first > 5 && last < first * 0.5) { sparkHtml += '
fatiga
'; } } } return '
' +'
#'+(i+1)+'
' +(thumb?'':'
') +'
'+name+'
' +'
'+status+'
' +'
'+fmtN(adLeadsDisplay)+'
leads
' +'
'+( cpl>0?cpl.toFixed(1)+'€':'—')+'
CPL
' +'
'+(a.score||0)+'
score
' +'
'+sparkHtml+'
' +'
'; }).join(''); }