/* Data layer — LIVE =================================================== No synthetic numbers. All account/KPI/campaign/creative data is read from RelayLive (populated by /api/data/bootstrap). When nothing has been ingested yet the accessors return zeros / empty arrays, so the UI shows honest empty states. Pure math helpers (funnel, CI, projections) operate on the real data. =================================================================== */ /* ---------- Accounts (loaded from the API at sign-in) -------------- */ let ACCOUNTS = []; function setAccounts(list){ ACCOUNTS = Array.isArray(list) ? list : []; window.ACCOUNTS = ACCOUNTS; } /* ---------- Platforms (presentation metadata only) ---------------- */ const PLATFORMS = [ { id: 'overview', label: 'Overview', short: 'Blended', glyph: '✦', icon:'relay', tone: '#8351ff' }, { id: 'meta', label: 'Meta', short: 'Meta', glyph: 'M', icon:'meta', tone: '#2f7bff' }, { id: 'facebook', label: 'Facebook', short: 'FB', glyph: 'f', icon:'facebook', tone: '#1877F2' }, { id: 'instagram', label: 'Instagram', short: 'IG', glyph: '◉', icon:'instagram', tone: '#E1306C' }, { id: 'google', label: 'Google Ads', short: 'Google', glyph: 'G', icon:'google', tone: '#34A853' }, { id: 'google_analytics', label: 'Analytics', short: 'GA4', glyph: '▲', icon:'ga', tone: '#E37400' }, { id: 'tiktok', label: 'TikTok', short: 'TikTok', glyph: 'T', icon:'tiktok', tone: '#111111' }, { id: 'snapchat', label: 'Snapchat', short: 'Snap', glyph: 'S', icon:'snapchat', tone: '#F7C700' }, { id: 'x', label: 'X', short: 'X', glyph: '𝕏', icon:'x', tone: '#111111' }, { id: 'linkedin', label: 'LinkedIn', short: 'LinkedIn', glyph: 'in',icon:'linkedin', tone: '#0A66C2' }, ]; /* ---------- Deterministic PRNG (used only for sparkline shaping) --- */ function mulberry(seed){ return function(){ let t = (seed += 0x6D2B79F5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function strSeed(s){ let h = 5381; for (let i=0;i=6){ const r=parseInt(h.slice(0,2),16), g=parseInt(h.slice(2,4),16), b=parseInt(h.slice(4,6),16); const lum = 0.299*r+0.587*g+0.114*b; if (lum < 40) return '#d4d4d4'; } return tone; } /* ---------- Platform-derived helpers (operate on real data) ------- */ /* Per-source freshness — real sync lag from connected integrations. */ function platformFreshness(){ const f = (typeof RelayLive !== 'undefined') ? RelayLive.freshness() : {}; return f || {}; } /* Conversion funnel — ONLY values algebraically derived from reported metrics (impressions/clicks reconstructed from spend·cpc·ctr, conversions as reported). No fabricated intermediate stages. */ function campaignFunnel(c){ const cpm = (c.cpc / Math.max(c.ctr,0.01)) * 100; const impressions = Math.round((c.spend / Math.max(cpm,1)) * 1000); const clicks = Math.round(c.spend / Math.max(c.cpc, 0.01)); return [ { stage:'Impressions', short:'Impr', value: impressions }, { stage:'Clicks', short:'Clicks',value: clicks }, { stage:'Conversions', short:'Conv', value: c.conv }, ]; } /* Project next n days of spend/roas as the flat trailing-7-day mean of real data. Deterministic — no synthetic noise; always flagged `projected`. */ function projectedDays(accountId, platformId, n=7){ const ts = timeSeriesFor(accountId, platformId); if (!ts.length) return []; const last7 = ts.slice(-7); const avgSpend = last7.reduce((s,d)=>s+d.spend,0)/last7.length; const avgRoas = last7.reduce((s,d)=>s+d.roas, 0)/last7.length; const lastDate = new Date(ts[ts.length-1].date+'T00:00:00Z'); return Array.from({length:n}, (_,i)=>{ const d = new Date(lastDate); d.setUTCDate(lastDate.getUTCDate() + (i+1)); return { date: d.toISOString().slice(0,10), label: d.toLocaleDateString('en-US',{month:'short',day:'numeric'}), spend: +avgSpend.toFixed(0), roas: +avgRoas.toFixed(2), projected: true, }; }); } /* 95% CI on ROAS — proxy from conversion volume; widens for tiny samples. */ function roasCI(roas, conv){ const n = Math.max(conv, 1); const margin = roas * (0.45 / Math.sqrt(n)); return { lo: Math.max(0, +(roas - margin).toFixed(2)), hi: +(roas + margin).toFixed(2), margin: +margin.toFixed(2), wide: margin > roas * 0.25, }; } /* Diminishing-returns projection from a real base spend/roas. */ function projectedRevenueFrom(baseSpend, baseRoas, newSpend){ if (baseSpend <= 0) return 0; const currentRev = baseSpend * baseRoas; return currentRev * Math.pow(newSpend / baseSpend, 0.78); } function projectedRevenue(accountId, platformId, spendUSD){ const k = kpiSnapshot(accountId, platformId); return projectedRevenueFrom(k.spend, k.roas, spendUSD); } /* ---------- Export to window --------------------------------------- */ Object.assign(window, { ACCOUNTS, setAccounts, PLATFORMS, KPI_DEFS, kpiSnapshot, timeSeriesFor, getCampaigns, getCreatives, platformBreakdown, getAlerts, DEFAULT_RULES, DEFAULT_CUSTOM_COLS, fmt, curPrefix, setActiveCurrency, mulberry, strSeed, displayTone, platformFreshness, campaignFunnel, projectedDays, roasCI, projectedRevenue, projectedRevenueFrom, });