setMobileNav(false)}/>}
>
);
}
/* Small per-platform spend scorecards for the Overview — real ingested spend
per platform for the selected account, in the account's currency. Each card
deep-links into that platform's tab. Honest data only (spend > 0). */
function PlatformSpendCards({ accountId }){
const { setScope } = useGlobal();
const dataVersion = useDataVersion();
const cards = React.useMemo(()=>{
const rows = platformBreakdown(accountId)
.filter(m => !['overview','google_analytics'].includes(m.platform) && Number(m.spend) > 0)
.map(m => { const p = PLATFORMS.find(x=>x.id===m.platform) || {};
return { id:m.platform, label:p.short || p.label || m.platform, spend:Number(m.spend), tone:p.tone || '#8351ff' }; })
.sort((a,b)=> b.spend - a.spend);
const total = rows.reduce((s,r)=>s+r.spend, 0) || 1;
return rows.map(r => ({ ...r, pct: r.spend/total*100 }));
}, [accountId, dataVersion]);
if (cards.length === 0) return null;
return (
Spend by platform
{cards.length} {cards.length===1?'platform':'platforms'}
{cards.map(c => (
))}
);
}
/* ===================== Overview view ============================= */
function OverviewView({ accountId, range, dateRange, dark, alerts, canEdit, moduleSettings, onOpenColumns }){
const w = moduleSettings.widgets;
const on = (id)=> w[id]!==false;
const platformId = 'overview';
const liveCount = useMemo(()=>getCampaigns(accountId,'overview').filter(c=>c.status==='active').length, [accountId]);
const anyKpi = KPI_WIDGET_IDS.some(id=> w[id]!==false);
const anyChartRow = on('chart') || on('anomalies') || on('platform_mix');
return (
{anyKpi &&
}
{anyChartRow && (
{on('chart') && (
{metricHiddenFor(accountId,'roas') ? 'Spend trend' : 'Spend × ROAS'}
Blended
{metricHiddenFor(accountId,'roas') ? 'Daily ad spend.' : 'Daily ad spend vs return on ad spend.'}
Spend
{!metricHiddenFor(accountId,'roas') &&
ROAS
}
)}
{(on('anomalies')||on('platform_mix')) && (
{on('anomalies') && (
{alerts.slice(0,3).map(a => { const tone = SEV_TONES[a.sev]; return (
); })}
)}
{on('platform_mix') && (
Platform mix
{formatRange(dateRange)}
)}
)}
)}
{on('campaign_table') && }
{on('creative') && }
{liveCount} active campaigns · {alerts.length} recent events
Auto-refresh every 30s
);
}
/* ===================== Workspace ================================= */
function Workspace({ currentUser, integrations, onOpenAdmin, onSignOut, dark, setDark, pushAudit }){
const g = useGlobal();
const { scope, accountId, range, search } = g;
const setMobileNav = g.setMobileNav;
const canEdit = can(currentUser,'campaigns.edit');
const canReallocate = can(currentUser,'reallocate.apply');
const canAutomate = can(currentUser,'automations.manage');
const canAdmin = can(currentUser,'users.manage') || can(currentUser,'integrations.manage');
const canMediaplan = can(currentUser,'mediaplan.manage');
const canSettings = can(currentUser,'settings.manage');
const [showColumns, setShowColumns] = useState(false);
const [showAutomations, setShowAutomations] = useState(false);
const [showPalette, setShowPalette] = useState(false);
const [showReallocator, setShowReallocator] = useState(false);
const [customCols, setCustomCols] = useState(DEFAULT_CUSTOM_COLS);
const [rules, setRules] = useState(DEFAULT_RULES);
useEffect(()=>{ const onKey=(e)=>{ if((e.metaKey||e.ctrlKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); setShowPalette(p=>!p);} }; window.addEventListener('keydown',onKey); return ()=>window.removeEventListener('keydown',onKey); }, []);
/* alerts (live from the warehouse) + toasts */
const dataVersion = useDataVersion();
const [allRead, setAllRead] = useState(false);
useEffect(()=>{ setAllRead(false); }, [accountId]);
const alerts = useMemo(()=>{
const a = getAlerts(accountId);
return allRead ? a.map(x=>({ ...x, read:true })) : a;
}, [accountId, dataVersion, allRead]);
const [toasts, setToasts] = useState([]);
const dismissToast = (id)=>setToasts(arr=>arr.filter(t=>t.id!==id));
const markAllRead = ()=>setAllRead(true);
/* account-switch toast */
const first = useRef(true);
useEffect(()=>{ if(first.current){ first.current=false; return; } const id='t'+Date.now(); const acc=ACCOUNTS.find(a=>a.id===accountId);
setToasts(arr=>[...arr,{id,sev:'info',title:'Switched account',msg:`Loaded ${acc?acc.name:'account'} — refreshing data.`}]);
setTimeout(()=>setToasts(arr=>arr.filter(x=>x.id!==id)),4000); }, [accountId]);
/* Auto-refresh: every 10 minutes re-fetch the current account's bundles so
the view tracks the server-side cron sync without manual clicks. */
useEffect(()=>{
const t = setInterval(()=>{
try {
RelayLive.clearAll();
RelayLive.ensure(accountId, 'overview', g.dateRange);
if (scope.startsWith('platform:')) RelayLive.ensure(accountId, scope.split(':')[1], g.dateRange);
} catch(e){ console.warn('[relay] auto-refresh failed:', e); }
}, 10 * 60 * 1000);
return ()=>clearInterval(t);
}, [accountId, scope, g.dateRange && g.dateRange.start, g.dateRange && g.dateRange.end]);
/* manual Meta data refresh — server pulls fresh insights from the Graph API */
const [syncing, setSyncing] = useState(false);
const doRefresh = async ()=>{
if (syncing) return;
setSyncing(true);
const tid = 't'+Date.now();
try {
const r = await RelayAPI.post('/sync/meta', { days: 7 });
RelayLive.clearAll();
await RelayLive.ensure(accountId, 'overview', g.dateRange);
if (scope.startsWith('platform:')) await RelayLive.ensure(accountId, scope.split(':')[1], g.dateRange);
const done = r && r.result && r.result.complete;
setToasts(arr=>[...arr,{ id:tid, sev:'info', title:'Data refreshed',
msg: done ? 'Meta insights re-pulled for the last 7 days.' : 'Partial sync — click Refresh again to continue.' }]);
} catch(e) {
setToasts(arr=>[...arr,{ id:tid, sev:'error', title:'Refresh failed', msg: e.message || 'Sync error' }]);
}
setTimeout(()=>setToasts(arr=>arr.filter(x=>x.id!==tid)), 5000);
setSyncing(false);
};
/* title per scope */
const scopeTitle = scope==='overview'?'Overview'
: scope.startsWith('platform:')?(PLATFORMS.find(p=>p.id===scope.split(':')[1])||{}).label
: scope==='creative'?'Creative Preview' : scope==='ga'?'Google Analytics' : scope==='media'?'Media Plan' : scope==='reports'?'Report Builder' : scope==='settings'?'Settings':'';
return (
{/* top master controls */}
{scopeTitle}
{!g.isReporter &&
}
{!g.isReporter &&
}
{!g.isReporter && (
)}
setDark(d=>!d)}>
{/* contextual action row for platform/overview */}
{(scope==='overview'||scope.startsWith('platform:')) && (
{canEdit &&
setShowColumns(true)} className="hidden sm:inline-flex">Columns}
{canReallocate &&
setShowReallocator(true)} className="hidden sm:inline-flex">Reallocate}
{canAutomate &&
setShowAutomations(true)} className="hidden sm:inline-flex">Automations {rules.filter(r=>r.enabled).length}}
)}
{/* routed content */}
{(scope==='overview') && <>
setShowColumns(true)}/>
>}
{scope.startsWith('platform:') && }
{scope==='creative' && Creative Preview
Top creatives across platforms for this account
}
{scope==='reports' && }
{scope==='ga' && }
{scope==='media' && }
{scope==='settings' && }
{/* overlays */}
setShowColumns(false)} onSave={(col)=>setCustomCols(a=>[...a,col])}/>
setShowAutomations(false)} rules={rules} setRules={setRules}/>
setShowPalette(false)} accountId={accountId} platformId={scope.startsWith('platform:')?scope.split(':')[1]:'overview'}
onAction={(it)=>{ switch(it.action){
case 'go-platform': g.setScope(it.payload==='overview'?'overview':`platform:${it.payload}`); break;
case 'toggle-dark': setDark(d=>!d); break;
case 'open-reallocator': setShowReallocator(true); break;
case 'open-automations': setShowAutomations(true); break;
case 'open-columns': setShowColumns(true); break;
case 'mark-read': markAllRead(); break;
case 'view-campaign': g.setScope(`platform:${it.payload.platform}`); g.setSearch(it.payload.name); break;
case 'view-adset': g.setScope(`platform:${it.payload.campaign.platform}`); g.setSearch(it.payload.adset.name); break;
default: break;
} }}/>
setShowReallocator(false)} accountId={accountId}/>
);
}
/* FreshnessChips lives here (uses platform feeds) */
function FreshnessChips(){
const { feeds } = useGlobal();
useDataVersion(); // re-render when freshness loads
const [, tick] = useState(0);
useEffect(()=>{ const t=setInterval(()=>tick(x=>x+1),3500); return ()=>clearInterval(t); }, []);
const fresh = platformFreshness();
const fmtLag = (s)=> s<60?`${s}s`:`${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`;
const plats = PLATFORMS.filter(p=>p.id!=='overview' && p.id!=='google_analytics' && feeds[p.id]);
return (
Feeds
·
{plats.length===0 && no live feeds — connect a source in Admin}
{plats.map(p=>{
const f=fresh[p.id]||{lag:0,status:'ok'}; const delayed=f.status==='delayed';
return (
{fmtLag(f.lag)}{delayed&&late});
})}
);
}
Object.assign(window, { GlobalContext, useGlobal, GlobalProvider, Workspace, Sidebar, OverviewView, FreshnessChips, AccountSwitcher, DateRangePicker, DATE_PRESETS });
/* v4 — manual Meta refresh button added 2026-07-02 */