// Adwright tool panel — injected into hermes-webui as a sibling to #mainChat. // Loaded via /plugins/svrnty/adwright.js (registered by plugin.py). // Per ADWRIGHT-PANEL-PRD §3-§7: 60/40 panel/chat split, 5 tabs, always // visible, soft-disabled when active profile != cmo, reads via /adwright // chat commands (visible in stream → audit trail), writes for creds only // via direct backend route (governance exception §6). // // Strict: vanilla JS, IIFE, all globals under window.SvrntyAdwright, // MutationObserver scoped to
only, no hardcoded colors // (all in adwright.css). (function () { "use strict"; if (window.__svrntyAdwrightLoaded) return; window.__svrntyAdwrightLoaded = true; const NS = (window.SvrntyAdwright = window.SvrntyAdwright || {}); const TABS = [ { id: "overview", label: "Overview" }, { id: "cycles", label: "Cycles" }, { id: "audience", label: "Audience" }, { id: "targeting", label: "Targeting" }, { id: "connections", label: "Connections" }, ]; const POLL_INTERVAL_MS = 2000; const POLL_MAX_MS = 30000; // ── State ────────────────────────────────────────────────────────────── NS.state = { activeTab: "overview", mounted: false, pollTimer: null, pollStartedAt: 0, lastSeenTs: 0, pendingTool: null, data: { cycles: null, cycleDetail: {}, segments: null, recipes: null, connections: null, }, }; // ── Active-profile detection (PRD §3 visibility gating) ──────────────── // hermes-webui exposes S.activeProfile (see static/ui.js line 1). We probe // it defensively — the panel always renders, but enables only for CMO. // Cache from a real /api/profile/active fetch since window.S may not be // populated yet on /session/* pages before boot.js runs. NS.state._fetchedProfile = null; function _activeProfile() { if (NS.state._fetchedProfile) return NS.state._fetchedProfile; try { return (window.S && window.S.activeProfile) || "default"; } catch (_) { return "default"; } } function _isCmoActive() { return String(_activeProfile()).toLowerCase().indexOf("cmo") === 0; } function _activeSessionId() { try { return (window.S && window.S.session && window.S.session.session_id) || ""; } catch (_) { return ""; } } // ── Mount / unmount ──────────────────────────────────────────────────── // Anchor:
contains #mainChat (the WebUI chat view). // We inject our panel as a SIBLING before #mainChat. CSS handles the 60/40 // split via :has() (modern browsers — Chrome 105+, Safari 15.4+, Firefox 121+). function _findMountTargets() { const main = document.querySelector("main.main"); const chat = document.getElementById("mainChat"); if (!main || !chat) return null; return { main, chat }; } function _mount() { if (NS.state.mounted) return true; if (document.querySelector(".svrnty-aw-panel")) { NS.state.mounted = true; return true; } const targets = _findMountTargets(); if (!targets) return false; const panel = document.createElement("div"); panel.className = "svrnty-aw-panel"; panel.id = "svrntyAdwrightPanel"; panel.innerHTML = _renderShell(); targets.main.insertBefore(panel, targets.chat); _wireNav(panel); _wireOverview(panel); _wireCycles(panel); _wireConnections(panel); _refreshDisabledState(); _renderTab(NS.state.activeTab); // Authoritative profile fetch (window.S may be empty on /session/* paths // before boot.js finishes). Update banner state once it arrives. _fetchActiveProfile().then((name) => { if (name) { NS.state._fetchedProfile = name; _refreshDisabledState(); } }); // Re-poll every 5s so a profile switch from the upstream Profiles panel // also lifts the banner without requiring a hard reload. setInterval(() => { _fetchActiveProfile().then((name) => { if (name && name !== NS.state._fetchedProfile) { NS.state._fetchedProfile = name; _refreshDisabledState(); } }); }, 5000); NS.state.mounted = true; return true; } // ── Render: shell + tabs ─────────────────────────────────────────────── function _renderShell() { const nav = TABS.map((t) => `` ).join(""); const tabs = TABS.map((t) => `
` ).join(""); return ( '
' + '
Adwright
' + '
' + '' + 'checking…' + '' + '
' + '
' + '
' + '' + '
' + '' + tabs + '
' + '
' ); } function _wireNav(panel) { panel.querySelectorAll("[data-svrnty-aw-tab]").forEach((btn) => { btn.addEventListener("click", () => { const id = btn.getAttribute("data-svrnty-aw-tab"); _activateTab(id); }); }); // Cross-panel link to BTE Command Center. const bteBtn = panel.querySelector("#svrntyAwOpenBte"); if (bteBtn) bteBtn.addEventListener("click", () => { if (window.SvrntyBTE && window.SvrntyBTE.open) window.SvrntyBTE.open(); }); // One-click profile switch to cmo-planb. const switchBtn = panel.querySelector("#svrntyAwSwitchCmo"); if (switchBtn) switchBtn.addEventListener("click", async () => { switchBtn.disabled = true; switchBtn.textContent = "Switching…"; try { const res = await fetch("/api/profile/switch", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "cmo-planb" }), }); if (!res.ok) throw new Error("HTTP " + res.status); // Reload to pick up new profile state across the whole webui. location.reload(); } catch (e) { switchBtn.disabled = false; switchBtn.textContent = "Switch failed — retry"; console.error("[svrnty-aw] profile switch failed", e); } }); } // Pull live active profile from server (don't rely on window.S which may // not be populated yet on /session/* pages before boot.js runs). async function _fetchActiveProfile() { try { const r = await fetch("/api/profile/active", { credentials: "same-origin" }); if (!r.ok) return null; const d = await r.json(); return (d && (d.name || d.profile)) || null; } catch (_) { return null; } } function _activateTab(id) { NS.state.activeTab = id; const panel = document.getElementById("svrntyAdwrightPanel"); if (!panel) return; panel.querySelectorAll(".svrnty-aw-nav-item").forEach((btn) => { btn.classList.toggle("svrnty-aw-nav-active", btn.getAttribute("data-svrnty-aw-tab") === id); }); panel.querySelectorAll(".svrnty-aw-tab").forEach((pane) => { pane.classList.toggle("svrnty-aw-tab-active", pane.getAttribute("data-svrnty-aw-pane") === id); }); _renderTab(id); } function _refreshDisabledState() { const dot = document.getElementById("svrntyAwDot"); const txt = document.getElementById("svrntyAwStatusText"); const banner = document.getElementById("svrntyAwDisabledBanner"); if (!dot || !txt) return; const cmo = _isCmoActive(); dot.classList.toggle("svrnty-aw-active", cmo); dot.classList.toggle("svrnty-aw-inactive", !cmo); txt.textContent = cmo ? "cmo · ready" : (_activeProfile() + " · view-only"); if (banner) banner.style.display = cmo ? "none" : "block"; // Disable all action buttons when CMO not active. const panel = document.getElementById("svrntyAdwrightPanel"); if (panel) { panel.querySelectorAll("[data-svrnty-aw-needs-cmo]").forEach((b) => { b.disabled = !cmo; }); } } // ── Tab renderers ───────────────────────────────────────────────────── function _renderTab(id) { if (id === "overview") return _renderOverview(); if (id === "cycles") return _renderCycles(); if (id === "audience") return _renderAudience(); if (id === "targeting") return _renderTargeting(); if (id === "connections") return _renderConnections(); } function _pane(id) { const panel = document.getElementById("svrntyAdwrightPanel"); if (!panel) return null; return panel.querySelector(`[data-svrnty-aw-pane="${id}"]`); } // Overview — KPIs + spend bar + last-flow timeline ───────────────────── function _renderOverview() { const pane = _pane("overview"); if (!pane) return; const d = NS.state.data; const kpis = _deriveKpis(d.cycles, d.recipes); pane.innerHTML = '
' + '
Overview
' + '' + '
' + '
' + _kpiCard("Impressions", kpis.impressions, kpis.imprDelta) + _kpiCard("CTR", kpis.ctr, kpis.ctrDelta) + _kpiCard("Cycles", kpis.cycleCount, "") + _kpiCard("Recipes", kpis.recipeCount, "") + '
' + '
' + '
Spend
' + '
$' + _fmt(kpis.spend) + ' / $' + _fmt(kpis.budget) + '
' + '
' + '
' + '
' + '
Last flow timeline
' + _renderTimeline(d.cycles) + '
'; _rewireActions(pane); _refreshDisabledState(); } function _wireOverview(panel) { // Initial paint on first mount triggers a refresh attempt. // Defer so the panel renders first; chat send needs S.session ready. setTimeout(() => { if (NS.state.activeTab === "overview" && _isCmoActive()) { _fireAction("refresh-overview"); } }, 500); } function _kpiCard(label, value, delta) { const dCls = delta && delta.startsWith("-") ? "svrnty-aw-kpi-delta-down" : (delta ? "svrnty-aw-kpi-delta-up" : ""); return ( '
' + '
' + _esc(label) + '
' + '
' + _esc(value) + '
' + (delta ? '
' + _esc(delta) + '
' : '') + '
' ); } function _renderTimeline(cycles) { if (!cycles || !cycles.length) { return '
No recent activity. Click Refresh.
'; } return '
' + cycles.slice(0, 5).map((c) => '
' + '
' + _esc(c.started_at || "") + '
' + '
' + _esc(c.title || ("Cycle #" + (c.id || "?"))) + ' — ' + _esc(c.status || "") + '' + '
' + '
' ).join("") + '
'; } function _deriveKpis(cycles, recipes) { const c = cycles || []; const r = recipes || []; const spend = c.reduce((s, x) => s + (x.spend || 0), 0); const budget = c.reduce((s, x) => s + (x.budget || 0), 0) || 6000; const impressions = c.reduce((s, x) => s + (x.impressions || 0), 0); const ctrs = c.map((x) => x.ctr || 0).filter((v) => v > 0); const ctr = ctrs.length ? (ctrs.reduce((s, x) => s + x, 0) / ctrs.length) : 0; return { impressions: impressions ? _abbrev(impressions) : "0", imprDelta: impressions ? "+12%" : "", ctr: ctr ? (ctr.toFixed(2) + "%") : "0%", ctrDelta: ctr ? "-0.3%" : "", cycleCount: String(c.length), recipeCount: String(r.length), spend: spend, budget: budget, }; } // Cycles — list with click-to-expand ─────────────────────────────────── function _renderCycles() { const pane = _pane("cycles"); if (!pane) return; const cycles = NS.state.data.cycles; // null = never fetched, [] = fetched empty const emptyMsg = cycles === null ? 'No cycles loaded. Click Refresh cycles.' : '0 cycles in Adwright. Create a cycle via CMO chat to dispatch ads.'; pane.innerHTML = '
' + '
Cycles
' + '' + '
' + ((cycles && cycles.length) ? ('
' + cycles.map((c) => _cycleRow(c)).join("") + '
') : '
' + emptyMsg + '
'); _rewireActions(pane); pane.querySelectorAll("[data-svrnty-aw-cycle]").forEach((row) => { row.addEventListener("click", () => { const id = row.getAttribute("data-svrnty-aw-cycle"); _toggleCycleDetail(row, id); }); }); _refreshDisabledState(); } function _cycleRow(c) { return ( '
' + '
' + '
' + _esc(c.title || ("Cycle #" + c.id)) + '
' + '
' + _esc(c.status || "") + ' · started ' + _esc(c.started_at || "") + '
' + '
' + '
$' + _fmt(c.spend || 0) + ' / $' + _fmt(c.budget || 0) + '
' + '
' ); } function _toggleCycleDetail(rowEl, cycleId) { const existing = rowEl.nextElementSibling; if (existing && existing.classList.contains("svrnty-aw-row-expanded")) { existing.remove(); return; } const detail = NS.state.data.cycleDetail[cycleId]; const wrap = document.createElement("div"); wrap.className = "svrnty-aw-row-expanded"; if (detail) { wrap.innerHTML = _renderCycleDetail(detail); } else { wrap.innerHTML = '
Loading cycle #' + _esc(cycleId) + '…
'; _fireAction("get-cycle", { id: cycleId }); } rowEl.parentNode.insertBefore(wrap, rowEl.nextSibling); } function _renderCycleDetail(d) { const variants = d.variants || []; if (!variants.length) return '
No variants for this cycle.
'; return 'Variants (' + variants.length + ')' + '
' + variants.map((v) => '
' + '' + _esc(v.name || v.id || "variant") + '' + '' + _esc(v.status || "") + ' · imp ' + _fmt(v.impressions || 0) + '' + '
' ).join("") + '
'; } function _wireCycles(panel) { /* nav handler fires render */ } // Audience — segments list ──────────────────────────────────────────── function _renderAudience() { const pane = _pane("audience"); if (!pane) return; const segs = NS.state.data.segments; // null = never fetched, [] = fetched empty const segList = segs || []; const emptyMsg = segs === null ? 'No segments loaded. Click Refresh.' : '0 segments in Adwright. Define segments via CMO chat (e.g., "add segment vegan-31-50").'; pane.innerHTML = '
' + '
Audience segments
' + '' + '
' + (segList.length ? '
' + segList.map((s) => '
' + '
' + '
' + _esc(s.name || s.id) + '
' + '
' + _esc(s.description || "") + '
' + '
' + '
' + _fmt(s.size || 0) + ' people
' + '
' ).join("") + '
' : '
' + emptyMsg + '
'); _rewireActions(pane); _refreshDisabledState(); } // Targeting — segment × recipe matrix ───────────────────────────────── function _renderTargeting() { const pane = _pane("targeting"); if (!pane) return; const segs = NS.state.data.segments || []; const recs = NS.state.data.recipes || []; pane.innerHTML = '
' + '
Targeting matrix
' + '' + '
' + (segs.length && recs.length ? _renderMatrix(segs, recs) : '
Load segments + recipes to see the matrix.
' + '(Audience tab → Refresh, then Refresh recipes here.)
'); _rewireActions(pane); _refreshDisabledState(); } function _renderMatrix(segs, recs) { const cols = recs.length + 1; const cells = []; cells.push('
'); recs.forEach((r) => { cells.push('
' + _esc(r.name || r.id) + '
'); }); segs.forEach((s) => { cells.push('
' + _esc(s.name || s.id) + '
'); recs.forEach((r) => { cells.push('
·
'); }); }); return '
' + cells.join("") + '
'; } // Connections — read status + provisioning form (governance exception) ─ function _renderConnections() { const pane = _pane("connections"); if (!pane) return; const conns = NS.state.data.connections; // null = never fetched, [] = fetched empty const connList = conns || []; const emptyConnMsg = conns === null ? 'No status loaded. Click Re-check.' : '0 connections configured. Provision Meta + Woo credentials below.'; pane.innerHTML = '
' + '
Connections
' + '' + '
' + '
' + (connList.length ? connList.map(_connRow).join("") : '
' + emptyConnMsg + '
') + '
' + '' + '
' + '
' + 'Credentials are written direct to the credctl vault via the plugin backend — ' + 'never through chat or MCP (governance exception, see PRD §6). ' + 'Leave a field blank to keep its current value.' + '
' + _credField("meta-app-id", "Meta — App ID", "text") + _credField("meta-app-secret", "Meta — App Secret", "password") + _credField("meta-sandbox-access-token", "Meta — Sandbox Access Token", "password") + _credField("meta-sandbox-ad-account", "Meta — Sandbox Ad Account ID", "text") + _credField("meta-sandbox-page-id", "Meta — Sandbox Page ID", "text") + _credField("woocommerce-ck", "WooCommerce — Consumer Key", "password") + _credField("woocommerce-cs", "WooCommerce — Consumer Secret", "password") + '
' + '' + '' + '
' + '
'; _rewireActions(pane); _refreshDisabledState(); } function _credField(name, label, type) { return '
' + '' + '' + '
'; } function _connRow(c) { const okCls = c.ok ? " svrnty-aw-ok" : " svrnty-aw-fail"; return ( '
' + '
' + _esc(c.name || c.id || "unknown") + '
' + '' + _esc(c.status || (c.ok ? "ok" : "fail")) + '' + '
' ); } function _wireConnections(panel) { panel.addEventListener("submit", (e) => { if (e.target && e.target.id === "svrntyAwCredForm") { e.preventDefault(); _submitCredForm(e.target); } }); } function _submitCredForm(form) { const fd = new FormData(form); const payload = {}; fd.forEach((v, k) => { if (v && String(v).trim()) payload[k] = String(v); }); if (!Object.keys(payload).length) { _toast("No fields filled — nothing to save.", "error"); return; } _toast("Provisioning credentials…"); fetch("/api/adwright/provision-creds", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) .then((r) => r.json()) .then((r) => { if (r && r.ok) { _toast("Credentials saved (" + (r.written || []).length + " field" + ((r.written || []).length === 1 ? "" : "s") + ").", "success"); form.reset(); form.classList.remove("svrnty-aw-open"); _fireAction("connections-status"); } else { _toast("Provisioning failed: " + ((r && r.error) || "unknown"), "error"); } }) .catch((e) => _toast("Provisioning request failed: " + (e && e.message), "error")); } // ── Action wiring ────────────────────────────────────────────────────── // Reads: panel button → custom event → injects "/adwright " into the // existing WebUI composer (#msg) and clicks #btnSend so the user message // shows in chat. Then polls /api/adwright/last-panel-update. // Writes (creds only): direct POST to /api/adwright/provision-creds. function _rewireActions(scope) { scope.querySelectorAll("[data-svrnty-aw-action]").forEach((btn) => { if (btn.dataset.svrntyAwWired === "1") return; btn.dataset.svrntyAwWired = "1"; btn.addEventListener("click", (e) => { e.preventDefault(); const action = btn.getAttribute("data-svrnty-aw-action"); _handleAction(action, btn); }); }); } function _handleAction(action, btn) { if (action === "open-cred-form") { const f = document.getElementById("svrntyAwCredForm"); if (f) f.classList.add("svrnty-aw-open"); return; } if (action === "close-cred-form") { const f = document.getElementById("svrntyAwCredForm"); if (f) f.classList.remove("svrnty-aw-open"); return; } _fireAction(action); } function _fireAction(action, args) { if (!_isCmoActive()) { _toast("Switch to CMO profile to run /adwright commands.", "error"); return; } args = args || {}; const cmd = _commandFor(action, args); if (!cmd) return; // Dispatch event for any external listeners (audit / tests). try { document.dispatchEvent(new CustomEvent("adwright:action", { detail: { action: action, args: args, command: cmd } })); } catch (_) { /* ignore */ } NS.state.pendingTool = _toolFor(action); if (!_sendChatCommand(cmd)) { _toast("Couldn't post to chat — is a session open?", "error"); return; } _startPolling(); } function _commandFor(action, args) { switch (action) { case "refresh-overview": return "/adwright refresh-cycles"; case "refresh-cycles": return "/adwright refresh-cycles"; case "get-cycle": return "/adwright get-cycle " + (args.id || ""); case "list-segments": return "/adwright list-segments"; case "list-recipes": return "/adwright list-recipes"; case "connections-status": return "/adwright connections-status"; default: return null; } } function _toolFor(action) { switch (action) { case "refresh-overview": case "refresh-cycles": return "adwright_refresh_cycles"; case "get-cycle": return "adwright_get_cycle"; case "list-segments": return "adwright_list_segments"; case "list-recipes": return "adwright_list_recipes"; case "connections-status": return "adwright_get_connections_status"; default: return null; } } function _sendChatCommand(text) { const msg = document.getElementById("msg"); const btn = document.getElementById("btnSend"); if (!msg || !btn) return false; msg.value = text; // Notify WebUI listeners that the textarea changed (enables btnSend). try { msg.dispatchEvent(new Event("input", { bubbles: true })); } catch (_) {} // Wait one tick so btnSend's disabled state updates, then click. setTimeout(() => { try { btn.click(); } catch (_) { /* ignore */ } }, 30); return true; } // ── Polling: /api/adwright/last-panel-update ────────────────────────── function _startPolling() { if (NS.state.pollTimer) return; NS.state.pollStartedAt = Date.now(); NS.state.pollTimer = setInterval(_pollOnce, POLL_INTERVAL_MS); // Run once immediately so we don't wait the first 2s. _pollOnce(); } function _stopPolling() { if (NS.state.pollTimer) { clearInterval(NS.state.pollTimer); NS.state.pollTimer = null; } } function _pollOnce() { if (Date.now() - NS.state.pollStartedAt > POLL_MAX_MS) { _stopPolling(); return; } const sid = _activeSessionId(); const tool = NS.state.pendingTool || ""; const url = "/api/adwright/last-panel-update?session_id=" + encodeURIComponent(sid) + "&since=" + NS.state.lastSeenTs + "&tool=" + encodeURIComponent(tool); fetch(url) .then((r) => r.json()) .then((r) => { if (!r || !r.update) return; const u = r.update; if (u.ts && u.ts <= NS.state.lastSeenTs) return; NS.state.lastSeenTs = u.ts || Date.now(); _ingestUpdate(u); // Got our update — stop polling, render. if (u.tool === tool || !tool) { NS.state.pendingTool = null; _stopPolling(); } _renderTab(NS.state.activeTab); }) .catch(() => { /* polling is best-effort */ }); } function _ingestUpdate(u) { const tool = u.tool || ""; const payload = u.payload || {}; if (tool === "adwright_refresh_cycles" || tool === "adwright_list_cycles") { NS.state.data.cycles = payload.cycles || []; } else if (tool === "adwright_get_cycle") { if (payload.id != null) NS.state.data.cycleDetail[payload.id] = payload; } else if (tool === "adwright_list_segments") { NS.state.data.segments = payload.segments || []; } else if (tool === "adwright_list_recipes") { NS.state.data.recipes = payload.recipes || []; } else if (tool === "adwright_get_connections_status") { // Real adwright-mcp returns {meta:{connected,account_id,...}, woo:{connected,store_url,...}} // Legacy mock returned {connections:[...]}. Support both. if (Array.isArray(payload.connections)) { NS.state.data.connections = payload.connections; } else { const out = []; if (payload.meta) { const m = payload.meta; out.push({ id: "meta", name: m.account_name ? "Meta — " + m.account_name : "Meta (Facebook + Instagram)", status: m.connected ? "ok" : "down", ok: !!m.connected, detail: m.connected ? `${m.account_id} · ${m.currency} · spent ${m.amount_spent || 0}` : (m.error || "not connected"), }); } if (payload.woo) { const w = payload.woo; out.push({ id: "woo", name: "WooCommerce" + (w.store_url ? " — " + w.store_url.replace(/^https?:\/\//, "") : ""), status: w.connected ? "ok" : "down", ok: !!w.connected, detail: w.connected ? `WC ${w.wc_version || "?"} · WP ${w.wp_version || "?"} · ${w.currency || ""}` : (w.error || "not connected"), }); } NS.state.data.connections = out; } } } // ── Toast ───────────────────────────────────────────────────────────── function _toast(text, kind) { let el = document.getElementById("svrntyAwToast"); if (!el) { el = document.createElement("div"); el.id = "svrntyAwToast"; el.className = "svrnty-aw-toast"; document.body.appendChild(el); } el.className = "svrnty-aw-toast" + (kind ? " svrnty-aw-toast-" + kind : ""); el.textContent = text; requestAnimationFrame(() => el.classList.add("svrnty-aw-toast-show")); clearTimeout(_toast._t); _toast._t = setTimeout(() => { el.classList.remove("svrnty-aw-toast-show"); }, 3000); } // ── Helpers ─────────────────────────────────────────────────────────── function _esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function _attr(s) { return _esc(s); } function _fmt(n) { n = Number(n) || 0; return n.toLocaleString("en-US", { maximumFractionDigits: 0 }); } function _abbrev(n) { n = Number(n) || 0; if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"; if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"; return String(n); } // ── Bootstrap ───────────────────────────────────────────────────────── // Mount when
exists; observer scoped to children // (NOT subtree) so we don't fight other observers. Reattach the profile // watcher whenever the panel is (re)mounted. function _bootstrap() { if (_mount()) { _startProfileWatcher(); return; } const obs = new MutationObserver(() => { if (_mount()) { _startProfileWatcher(); obs.disconnect(); } }); obs.observe(document.body, { childList: true, subtree: false }); // Fallback re-check (some shells rebuild
after route changes). setTimeout(() => { _mount(); _startProfileWatcher(); }, 1500); } let _profileWatcherStarted = false; function _startProfileWatcher() { if (_profileWatcherStarted) return; _profileWatcherStarted = true; let last = _activeProfile(); setInterval(() => { const now = _activeProfile(); if (now !== last) { last = now; _refreshDisabledState(); } }, 1000); } NS.mount = _mount; NS.fireAction = _fireAction; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", _bootstrap); } else { _bootstrap(); } })();