// App shell — nav, header, routing, context menu, drill flow
const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA, useRef: useRefA, useCallback: useCallbackA } = React;
function Sidebar({ route, onNav }) {
const items = [
{ id:"overview", label:"Overview", icon:"dashboard" },
{ id:"accounts", label:"Accounts", icon:"accounts" },
{ id:"changes", label:"Changes feed", icon:"feed" },
{ id:"alerts", label:"Alerts", icon:"alerts", badge: window.FABCOM_DATA.alerts.filter(a=>a.severity==="red").length },
];
return (
{items.map(it => (
onNav(it.id)}
className={`flex items-center gap-2.5 h-8 px-2 rounded-md text-[12.5px] ${route === it.id ? "bg-[var(--surface-2)] text-1" : "text-2 hover:bg-[var(--surface-2)]"}`}>
{it.label}
{it.badge > 0 && (
{it.badge}
)}
))}
Workspaces
{["HKT Group","Swire Properties","Cathay Pacific","HSBC","Lane Crawford"].map((w,i) => (
{w.split(" ").map(x=>x[0]).slice(0,2).join("")}
{w}
))}
VT
Vincent Tam
Media Ops Lead
);
}
function TopBar({ onToggleTheme, theme }) {
return (
);
}
// Alerts standalone page
function AlertsPage({ onInvestigate }) {
const data = window.FABCOM_DATA;
const red = data.alerts.filter(a => a.severity === "red");
const amber = data.alerts.filter(a => a.severity === "amber");
const Section = ({title, items, tone}) => (
{items.map(al => {
const acc = data.accounts.find(a => a.id === al.account_id);
return (
{al.title}
{al.kind.replace("_"," ")}
{al.detail}
{al.kind === "big_edit"
?
: (<>Current {window.fmtHKD(window.toHKD(al.value, acc.ccy))} · Baseline {window.fmtHKD(window.toHKD(al.baseline, acc.ccy))} >)}
onInvestigate(al)}>Investigate
Snooze
);
})}
{items.length === 0 &&
No {tone} alerts.
}
);
return (
Alerts · thresholds configured in ops/budget_rules.yaml
Budget misconfig alerts
);
}
function BQModal({ account, onClose }) {
const sql = `-- Daily settings for account ${account.id} (${account.name})
SELECT a.account_id, a.account_name, a.currency, a.balance, a.spend_cap,
c.campaign_id, c.name AS campaign, c.status, c.objective, c.daily_budget,
s.adset_id, s.name AS adset, s.daily_budget AS adset_daily, s.bid_strategy, s.optimization_goal
FROM \`fabcom-etl-core.meta_ads_settings.accounts\` a
JOIN \`fabcom-etl-core.meta_ads_settings.campaigns\` c USING (account_id)
JOIN \`fabcom-etl-core.meta_ads_settings.adsets\` s USING (campaign_id)
WHERE a.account_id = '${account.id}'
AND c.status = 'ACTIVE'
ORDER BY c.daily_budget DESC;`;
return (
e.stopPropagation()} style={{boxShadow:"var(--shadow-lg)"}}>
BigQuery snippet · {account.name}
{ navigator.clipboard && navigator.clipboard.writeText(sql); onClose(); }}>Copy
Open in BigQuery
⌘C also copies
);
}
function App(){
// Read route from hash
const readHash = () => {
const h = (location.hash || "#overview").replace(/^#/,"");
const [p, qs] = h.split("?");
const params = new URLSearchParams(qs || "");
return { page: p || "overview", params };
};
const [route, setRoute] = useStateA(readHash());
useEffectA(() => {
const onHash = () => setRoute(readHash());
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
const go = (page, params = {}) => {
const p = new URLSearchParams(params).toString();
location.hash = "#" + page + (p ? "?" + p : "");
};
const [filters, setFilters] = useStateA(() => ({
query: route.params.get("q") || "",
business: route.params.get("biz") || "",
currency: route.params.get("ccy") || "",
status: route.params.get("s") || "",
}));
// keep URL in sync for filters on overview
useEffectA(() => {
if (route.page !== "overview" && route.page !== "accounts") return;
const p = new URLSearchParams();
if (filters.query) p.set("q", filters.query);
if (filters.business) p.set("biz", filters.business);
if (filters.currency) p.set("ccy", filters.currency);
if (filters.status) p.set("s", filters.status);
if (route.params.get("acc")) p.set("acc", route.params.get("acc"));
const nh = "#" + route.page + (p.toString() ? "?"+p.toString() : "");
if (location.hash !== nh) history.replaceState(null, "", nh);
}, [filters, route.page]);
// Drill stack in right pane: [{kind: 'account'|'campaign'|'adset', id}, ...]
const [stack, setStack] = useStateA(() => {
const acc = route.params.get("acc");
return acc ? [{kind:"account", id: acc}] : [];
});
const current = stack[stack.length - 1];
const openAccount = (id) => setStack([{kind:"account", id}]);
const openCampaign = (id) => setStack(s => [...s, {kind:"campaign", id}]);
const openAdset = (id) => setStack(s => [...s, {kind:"adset", id}]);
const popDetail = () => setStack([]);
const back = (targetKind, id) => {
if (!targetKind) { setStack(s => s.slice(0, -1)); return; }
setStack(s => {
const i = s.findIndex(x => x.kind === targetKind && x.id === id);
return i >= 0 ? s.slice(0, i+1) : s;
});
};
useEffectA(() => {
// persist selected account in URL
if ((route.page === "overview" || route.page === "accounts") && stack[0]?.kind === "account") {
const p = new URLSearchParams(location.hash.split("?")[1] || "");
p.set("acc", stack[0].id);
history.replaceState(null, "", "#" + route.page + "?" + p.toString());
}
}, [stack, route.page]);
// Context menu
const [ctx, setCtx] = useStateA(null); // {x,y,account}
const [toast, setToast] = useStateA(null);
const [bqModal, setBqModal] = useStateA(null);
const onContextMenu = (e, account) => {
setCtx({x: e.clientX, y: e.clientY, account});
};
// Cmd-K to focus search (simplified: just shows a toast)
useEffectA(() => {
const onKey = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
const el = document.querySelector('input[placeholder="Search accounts…"]');
if (el) el.focus();
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
// Theme toggle (via Tweaks protocol)
const toggleTheme = () => {
const cur = document.documentElement.classList.contains("dark") ? "dark" : "light";
const nxt = cur === "dark" ? "light" : "dark";
document.documentElement.classList.remove("dark","light");
document.documentElement.classList.add(nxt);
try { window.parent.postMessage({ type:"__edit_mode_set_keys", edits: { theme: nxt } }, "*"); } catch(e){}
};
const theme = document.documentElement.classList.contains("dark") ? "dark" : "light";
// Main layout: sidebar | workarea
const showRightPane = stack.length > 0 && (route.page === "overview" || route.page === "accounts");
return (
{ setStack([]); go(p); }}/>
{(route.page === "overview" || route.page === "accounts") && (
openAccount(al.account_id)}
/>
{showRightPane && (
{current.kind === "account" && (
)}
{current.kind === "campaign" && (
)}
{current.kind === "adset" && (
)}
)}
)}
{route.page === "changes" && { go("overview"); openAccount(id); }}/>}
{route.page === "alerts" && { go("overview"); openAccount(al.account_id); }}/>}
{ctx && (
setCtx(null)} items={[
{ icon:"arrow-right", label:"Open account detail", onClick: () => openAccount(ctx.account.id), hint:"Enter" },
{ sep:true },
{ icon:"copy", label:"Copy BQ query for this account", onClick: () => setBqModal(ctx.account), hint:"⌘⇧C" },
{ icon:"copy", label:"Copy account ID", onClick: () => { navigator.clipboard && navigator.clipboard.writeText(ctx.account.id); setToast("Copied " + ctx.account.id); } },
{ icon:"link", label:"Copy shareable URL", onClick: () => { navigator.clipboard && navigator.clipboard.writeText(location.origin + location.pathname + "#overview?acc=" + ctx.account.id); setToast("URL copied"); } },
{ sep:true },
{ icon:"sheet", label:"Export this account to Sheets", onClick: () => setToast("Exporting " + ctx.account.name + "…") },
{ icon:"external", label:"Open in Meta Ads Manager", onClick: () => setToast("Opening Ads Manager…") },
]}/>
)}
{bqModal && setBqModal(null)}/>}
{toast && setToast(null)}/>}
);
}
// Mount only after data is loaded (handled by data_loader.jsx)
const __mount = () => ReactDOM.createRoot(document.getElementById("root")).render( );
if (window.FABCOM_DATA) __mount(); else window.__fabcomMount = __mount;