// static/shared/js/dev-toolbar.js // noqa: SEC015 - dev-only toolbar, innerHTML used with trusted/constructed content only // noqa: JS-001 - console references are intentional: this file intercepts and wraps console methods // noqa: JS-005 - init guard not needed: runs inside DOMContentLoaded IIFE /** * Dev-Mode Debug Toolbar * * Full-featured debug toolbar for multi-tenant app development. * Bottom-docked, resizable panel with 4 tabs: * - Platform: context from all sources (migrated from platform-diag.js) * - API Calls: live log of intercepted fetch requests * - Request Info: current page metadata and globals * - Console: captured console.log/warn/error/info * * Toggle: Ctrl+Alt+D * Only loads on localhost — auto-hidden in production. * Theme: Catppuccin Mocha */ (function () { 'use strict'; // ── Localhost guard ── var host = window.location.hostname; if (host !== 'localhost' && host !== '127.0.0.1') return; // ── Constants ── var STORAGE_HEIGHT_KEY = '_dev_toolbar_height'; var STORAGE_TAB_KEY = '_dev_toolbar_tab'; var DEFAULT_HEIGHT = 320; var MIN_HEIGHT = 150; var MAX_HEIGHT_RATIO = 0.8; var MAX_API_CALLS = 200; var MAX_CONSOLE_LOGS = 500; var TRUNCATE_LIMIT = 2048; // Catppuccin Mocha palette var C = { base: '#1e1e2e', surface0: '#313244', surface1: '#45475a', text: '#cdd6f4', subtext: '#a6adc8', blue: '#89b4fa', mauve: '#cba6f7', green: '#a6e3a1', red: '#f38ba8', peach: '#fab387', yellow: '#f9e2af', teal: '#94e2d5', sky: '#89dceb', }; // ── State ── var apiCalls = []; var consoleLogs = []; var activeTab = localStorage.getItem(STORAGE_TAB_KEY) || 'platform'; var panelHeight = parseInt(localStorage.getItem(STORAGE_HEIGHT_KEY), 10) || DEFAULT_HEIGHT; var toolbarEl = null; var contentEl = null; var isExpanded = false; var consoleFilter = 'all'; var expandedApiRows = {}; // ── Utilities ── function escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function truncate(str, limit) { if (typeof str !== 'string') { try { str = JSON.stringify(str); } catch (e) { str = String(str); } } if (str.length > limit) return str.slice(0, limit) + '\u2026 [truncated]'; return str; } function decodeJwtPayload(token) { try { var parts = token.split('.'); if (parts.length !== 3) return null; var payload = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')); return JSON.parse(payload); } catch (e) { return null; } } function formatDuration(ms) { if (ms < 1000) return ms + 'ms'; return (ms / 1000).toFixed(1) + 's'; } function formatTime(ts) { var d = new Date(ts); return d.toLocaleTimeString(undefined, { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0'); } function highlightPlatform(value) { if (value === 'oms') return C.green; if (value === 'loyalty') return C.mauve; if (value === 'hosting') return C.sky; if (value === 'main') return C.peach; if (value === '(none)' || value === '(empty)' || value === '(undefined)' || value === '(null — Source 3 fallback)') return C.red; return C.yellow; } function statusColor(status) { if (status === 'ERR') return C.red; if (status >= 200 && status < 300) return C.green; if (status >= 300 && status < 400) return C.blue; if (status >= 400 && status < 500) return C.yellow; if (status >= 500) return C.red; return C.subtext; } function levelColor(level) { if (level === 'error') return C.red; if (level === 'warn') return C.yellow; if (level === 'info') return C.blue; return C.subtext; } function levelBadge(level) { return '' + level.toUpperCase() + ''; } // ── HTML Helpers ── function sectionHeader(title) { return '
' + escapeHtml(title) + '
'; } function row(label, value, highlightFn) { var color = highlightFn ? highlightFn(value) : C.text; return '
' + '' + escapeHtml(label) + '' + '' + escapeHtml(String(value)) + '
'; } // ── Interceptors (installed immediately at parse time) ── // Fetch interceptor var _originalFetch = window.fetch; window.fetch = function (input, init) { init = init || {}; var entry = { id: apiCalls.length, timestamp: Date.now(), method: (init.method || 'GET').toUpperCase(), url: typeof input === 'string' ? input : (input && input.url ? input.url : String(input)), requestBody: init.body ? truncate(init.body, TRUNCATE_LIMIT) : null, requestHeaders: null, status: null, duration: null, responseBody: null, error: null, }; // Capture request headers if (init.headers) { try { if (init.headers instanceof Headers) { var h = {}; init.headers.forEach(function (v, k) { h[k] = v; }); entry.requestHeaders = h; } else { entry.requestHeaders = Object.assign({}, init.headers); } } catch (e) { /* ignore */ } } apiCalls.push(entry); if (apiCalls.length > MAX_API_CALLS) apiCalls.shift(); var start = performance.now(); return _originalFetch.call(this, input, init).then(function (response) { entry.status = response.status; entry.duration = Math.round(performance.now() - start); var clone = response.clone(); clone.text().then(function (text) { entry.responseBody = truncate(text, TRUNCATE_LIMIT); refreshIfActive('api'); }).catch(function () {}); refreshIfActive('api'); return response; }).catch(function (err) { entry.status = 'ERR'; entry.duration = Math.round(performance.now() - start); entry.error = err.message; refreshIfActive('api'); throw err; }); }; // Console interceptor var _origConsole = { log: console.log, info: console.info, warn: console.warn, error: console.error, }; ['log', 'info', 'warn', 'error'].forEach(function (level) { console[level] = function () { var args = Array.prototype.slice.call(arguments); consoleLogs.push({ timestamp: Date.now(), level: level, args: args.map(function (a) { if (typeof a === 'object') { try { return JSON.stringify(a, null, 2); } catch (e) { return String(a); } } return String(a); }), }); if (consoleLogs.length > MAX_CONSOLE_LOGS) consoleLogs.shift(); refreshIfActive('console'); _origConsole[level].apply(console, args); }; }); // Capture uncaught errors and unhandled rejections window.addEventListener('error', function (e) { var msg = e.message || 'Unknown error'; if (e.filename) msg += ' at ' + e.filename.split('/').pop() + ':' + e.lineno; consoleLogs.push({ timestamp: Date.now(), level: 'error', args: [msg], }); if (consoleLogs.length > MAX_CONSOLE_LOGS) consoleLogs.shift(); refreshIfActive('console'); }); window.addEventListener('unhandledrejection', function (e) { var msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled promise rejection'; consoleLogs.push({ timestamp: Date.now(), level: 'error', args: [msg], }); if (consoleLogs.length > MAX_CONSOLE_LOGS) consoleLogs.shift(); refreshIfActive('console'); }); // ── Refresh helper ── function refreshIfActive(tab) { if (isExpanded && activeTab === tab && contentEl) { renderTab(); // Also update tab badges updateBadges(); } } // ── Data Collectors ── function gatherPlatformContext() { var storeToken = localStorage.getItem('store_token'); var jwt = storeToken ? decodeJwtPayload(storeToken) : null; return { windowPlatformCode: window.STORE_PLATFORM_CODE, windowStoreCode: window.STORE_CODE, lsPlatform: localStorage.getItem('store_platform') || '', lsStoreCode: localStorage.getItem('storeCode') || '', lsToken: storeToken, jwt: jwt, pathname: window.location.pathname, host: window.location.host, loginWouldSend: window.STORE_PLATFORM_CODE || localStorage.getItem('store_platform') || null, }; } function fetchAuthMe() { var token = localStorage.getItem('store_token'); if (!token) return Promise.resolve({ error: 'No store_token in localStorage' }); return _originalFetch.call(window, '/api/v1/store/auth/me', { headers: { 'Authorization': 'Bearer ' + token }, }).then(function (resp) { if (!resp.ok) return { error: resp.status + ' ' + resp.statusText }; return resp.json(); }).catch(function (e) { return { error: e.message }; }); } function gatherRequestInfo() { return { url: window.location.href, pathname: window.location.pathname, host: window.location.host, protocol: window.location.protocol, storeCode: window.STORE_CODE || '(undefined)', storeConfig: window.STORE_CONFIG || null, userPermissions: window.USER_PERMISSIONS || null, storePlatformCode: window.STORE_PLATFORM_CODE || '(undefined)', lsPlatform: localStorage.getItem('store_platform') || '(empty)', logConfig: window.LogConfig || null, detectedFrontend: detectFrontend(), environment: 'localhost', tokensPresent: { store_token: !!localStorage.getItem('store_token'), admin_token: !!localStorage.getItem('admin_token'), merchant_token: !!localStorage.getItem('merchant_token'), customer_token: !!localStorage.getItem('customer_token'), }, }; } function detectFrontend() { // Prefer server-injected value (set in base templates) if (window.FRONTEND_TYPE) return window.FRONTEND_TYPE; // Fallback for pages without base template (e.g., API docs) var path = window.location.pathname; if (path.startsWith('/api/')) return 'api'; return 'unknown'; } // ── UI Builder ── function createToolbar() { // Collapse bar (always visible at bottom) var collapseBar = document.createElement('div'); collapseBar.id = '_dev_toolbar_collapse'; Object.assign(collapseBar.style, { position: 'fixed', bottom: '0', left: '0', right: '0', zIndex: '99999', height: '4px', background: C.mauve, cursor: 'pointer', transition: 'height 0.15s', }); collapseBar.title = 'Dev Toolbar (Ctrl+Alt+D)'; collapseBar.addEventListener('mouseenter', function () { collapseBar.style.height = '8px'; }); collapseBar.addEventListener('mouseleave', function () { if (!isExpanded) collapseBar.style.height = '4px'; }); collapseBar.addEventListener('click', toggleToolbar); document.body.appendChild(collapseBar); // Main toolbar panel toolbarEl = document.createElement('div'); toolbarEl.id = '_dev_toolbar'; Object.assign(toolbarEl.style, { position: 'fixed', bottom: '0', left: '0', right: '0', zIndex: '99998', height: panelHeight + 'px', background: C.base, color: C.text, fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace", fontSize: '11px', lineHeight: '1.5', borderTop: '2px solid ' + C.mauve, display: 'none', flexDirection: 'column', }); // Drag handle var dragHandle = document.createElement('div'); Object.assign(dragHandle.style, { height: '6px', cursor: 'ns-resize', background: C.surface0, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: '0', }); dragHandle.innerHTML = '
'; setupDragResize(dragHandle); toolbarEl.appendChild(dragHandle); // Tab bar var tabBar = document.createElement('div'); tabBar.id = '_dev_toolbar_tabs'; Object.assign(tabBar.style, { display: 'flex', alignItems: 'center', background: C.surface0, borderBottom: '1px solid ' + C.surface1, flexShrink: '0', padding: '0 8px', }); var tabs = [ { id: 'platform', label: 'Platform' }, { id: 'auth', label: 'Auth' }, { id: 'api', label: 'API' }, { id: 'request', label: 'Request' }, { id: 'console', label: 'Console' }, ]; tabs.forEach(function (tab) { var btn = document.createElement('button'); btn.id = '_dev_tab_' + tab.id; btn.dataset.tab = tab.id; Object.assign(btn.style, { padding: '4px 12px', border: 'none', cursor: 'pointer', fontSize: '11px', fontFamily: 'inherit', background: 'transparent', color: C.subtext, borderBottom: '2px solid transparent', transition: 'all 0.15s', }); btn.innerHTML = tab.label + ''; // noqa: SEC015 btn.addEventListener('click', function () { switchTab(tab.id); }); tabBar.appendChild(btn); }); // Close button (right-aligned) var spacer = document.createElement('div'); spacer.style.flex = '1'; tabBar.appendChild(spacer); var copyBtn = document.createElement('button'); copyBtn.id = '_dev_copy_btn'; Object.assign(copyBtn.style, { padding: '2px 8px', border: 'none', cursor: 'pointer', fontSize: '11px', fontFamily: 'inherit', background: C.teal, color: C.base, borderRadius: '3px', fontWeight: 'bold', margin: '2px 4px 2px 0', }); copyBtn.textContent = '\u2398 Copy'; copyBtn.addEventListener('click', copyTabContent); tabBar.appendChild(copyBtn); var closeBtn = document.createElement('button'); Object.assign(closeBtn.style, { padding: '2px 8px', border: 'none', cursor: 'pointer', fontSize: '11px', fontFamily: 'inherit', background: C.red, color: C.base, borderRadius: '3px', fontWeight: 'bold', margin: '2px 0', }); closeBtn.textContent = '\u2715 Close'; closeBtn.addEventListener('click', toggleToolbar); tabBar.appendChild(closeBtn); toolbarEl.appendChild(tabBar); // Content area contentEl = document.createElement('div'); contentEl.id = '_dev_toolbar_content'; Object.assign(contentEl.style, { flex: '1', overflowY: 'auto', padding: '8px 12px', }); toolbarEl.appendChild(contentEl); document.body.appendChild(toolbarEl); // Inject hover style for rows and content lines var style = document.createElement('style'); style.textContent = '._dev_row:hover, #_dev_toolbar_content > div:hover { background: ' + C.surface1 + ' }'; document.head.appendChild(style); updateTabStyles(); updateBadges(); } function setupDragResize(handle) { var startY, startHeight; handle.addEventListener('mousedown', function (e) { e.preventDefault(); startY = e.clientY; startHeight = panelHeight; document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', onDragEnd); document.body.style.userSelect = 'none'; }); function onDrag(e) { var delta = startY - e.clientY; var newHeight = Math.max(MIN_HEIGHT, Math.min(window.innerHeight * MAX_HEIGHT_RATIO, startHeight + delta)); panelHeight = Math.round(newHeight); if (toolbarEl) toolbarEl.style.height = panelHeight + 'px'; } function onDragEnd() { document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', onDragEnd); document.body.style.userSelect = ''; localStorage.setItem(STORAGE_HEIGHT_KEY, String(panelHeight)); } } function toggleToolbar() { if (!toolbarEl) createToolbar(); isExpanded = !isExpanded; toolbarEl.style.display = isExpanded ? 'flex' : 'none'; if (isExpanded) { renderTab(); updateBadges(); } } function copyTabContent() { if (!contentEl) return; var text = contentEl.innerText || contentEl.textContent || ''; navigator.clipboard.writeText(text).then(function () { var btn = document.getElementById('_dev_copy_btn'); if (btn) { var orig = btn.textContent; btn.textContent = '\u2714 Copied!'; btn.style.background = C.green; setTimeout(function () { btn.textContent = orig; btn.style.background = C.teal; }, 1500); } }).catch(function () { // Fallback for older browsers var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); var btn = document.getElementById('_dev_copy_btn'); if (btn) { var orig = btn.textContent; btn.textContent = '\u2714 Copied!'; btn.style.background = C.green; setTimeout(function () { btn.textContent = orig; btn.style.background = C.teal; }, 1500); } }); } function switchTab(tabId) { activeTab = tabId; localStorage.setItem(STORAGE_TAB_KEY, tabId); updateTabStyles(); renderTab(); } function updateTabStyles() { var tabs = ['platform', 'auth', 'api', 'request', 'console']; tabs.forEach(function (id) { var btn = document.getElementById('_dev_tab_' + id); if (!btn) return; if (id === activeTab) { btn.style.color = C.mauve; btn.style.borderBottomColor = C.mauve; btn.style.background = C.base; } else { btn.style.color = C.subtext; btn.style.borderBottomColor = 'transparent'; btn.style.background = 'transparent'; } }); } function updateBadges() { var apiBadge = document.getElementById('_dev_badge_api'); if (apiBadge) { apiBadge.textContent = apiCalls.length > 0 ? '(' + apiCalls.length + ')' : ''; } var consoleBadge = document.getElementById('_dev_badge_console'); if (consoleBadge) { var errCount = consoleLogs.filter(function (l) { return l.level === 'error'; }).length; var warnCount = consoleLogs.filter(function (l) { return l.level === 'warn'; }).length; var parts = []; if (errCount > 0) parts.push('' + errCount + 'E'); if (warnCount > 0) parts.push('' + warnCount + 'W'); if (parts.length === 0 && consoleLogs.length > 0) parts.push(consoleLogs.length.toString()); consoleBadge.innerHTML = parts.length > 0 ? '(' + parts.join('/') + ')' : ''; // noqa: SEC015 } } // ── Auth Tab Helpers ── var TOKEN_MAP = { admin: { key: 'admin_token', endpoint: '/api/v1/admin/auth/me' }, store: { key: 'store_token', endpoint: '/api/v1/store/auth/me' }, merchant: { key: 'merchant_token', endpoint: '/api/v1/merchants/auth/me' }, storefront: { key: 'customer_token', endpoint: '/api/v1/storefront/profile' }, }; function getTokenForFrontend(frontend) { var info = TOKEN_MAP[frontend]; return info ? localStorage.getItem(info.key) : null; } function getAuthMeEndpoint(frontend) { var info = TOKEN_MAP[frontend]; return info ? info.endpoint : null; } function fetchFrontendAuthMe(frontend) { var token = getTokenForFrontend(frontend); var endpoint = getAuthMeEndpoint(frontend); if (!token || !endpoint) return Promise.resolve({ error: 'No token or endpoint for ' + frontend }); return _originalFetch.call(window, endpoint, { headers: { 'Authorization': 'Bearer ' + token }, }).then(function (resp) { if (!resp.ok) return { error: resp.status + ' ' + resp.statusText }; return resp.json(); }).catch(function (e) { return { error: e.message }; }); } function renderAuthTab() { var frontend = detectFrontend(); var token = getTokenForFrontend(frontend); var jwt = token ? decodeJwtPayload(token) : null; var html = ''; // 1. Detected Frontend badge var frontendColors = { admin: C.red, store: C.green, merchant: C.peach, storefront: C.blue }; var badgeColor = frontendColors[frontend] || C.subtext; html += sectionHeader('Detected Frontend'); html += '
' + escapeHtml(frontend) + '
'; // 2. Active Token var tokenInfo = TOKEN_MAP[frontend]; html += sectionHeader('Active Token (' + (tokenInfo ? tokenInfo.key : 'n/a') + ')'); if (token) { html += row('Status', 'present', function () { return C.green; }); html += row('Last 20 chars', '...' + token.slice(-20)); } else { html += row('Status', 'absent', function () { return C.red; }); } // 3. JWT Decoded html += sectionHeader('JWT Decoded'); if (jwt) { Object.keys(jwt).forEach(function (key) { var val = jwt[key]; if (key === 'exp' || key === 'iat') { val = new Date(val * 1000).toLocaleString() + ' (' + val + ')'; } html += row(key, val ?? '(null)'); }); } else { html += '
No JWT to decode
'; } // 4. Token Expiry countdown html += sectionHeader('Token Expiry'); if (jwt && jwt.exp) { var now = Math.floor(Date.now() / 1000); var remaining = jwt.exp - now; var expiryColor; var expiryText; if (remaining <= 0) { expiryColor = C.red; expiryText = 'EXPIRED (' + Math.abs(remaining) + 's ago)'; } else if (remaining < 300) { expiryColor = C.yellow; var m = Math.floor(remaining / 60); var s = remaining % 60; expiryText = m + 'm ' + s + 's remaining'; } else { expiryColor = C.green; var m2 = Math.floor(remaining / 60); var s2 = remaining % 60; expiryText = m2 + 'm ' + s2 + 's remaining'; } html += row('Expires', expiryText, function () { return expiryColor; }); } else { html += '
No exp claim
'; } // 5. All Tokens Overview html += sectionHeader('All Tokens Overview'); Object.keys(TOKEN_MAP).forEach(function (fe) { var present = !!localStorage.getItem(TOKEN_MAP[fe].key); var color = present ? C.green : C.red; var label = present ? 'present' : 'absent'; html += row(TOKEN_MAP[fe].key, label, function () { return color; }); }); // 6. Server Auth (/auth/me) — placeholder, filled async html += '
'; contentEl.innerHTML = html; // noqa: SEC015 // noqa: SEC015 // Async fetch if (token && getAuthMeEndpoint(frontend)) { var meSection = document.getElementById('_dev_auth_me_section'); if (meSection) meSection.innerHTML = sectionHeader('Server Auth (' + getAuthMeEndpoint(frontend) + ')') + // noqa: SEC015 '
Loading...
'; fetchFrontendAuthMe(frontend).then(function (me) { if (activeTab !== 'auth') return; var meHtml = sectionHeader('Server Auth (' + getAuthMeEndpoint(frontend) + ')'); if (me.error) { meHtml += '
' + escapeHtml(me.error) + '
'; } else { Object.keys(me).forEach(function (key) { meHtml += row(key, me[key] ?? '(null)'); }); // 7. Consistency Check if (jwt) { meHtml += sectionHeader('Consistency Check (JWT vs /auth/me)'); var mismatches = []; var checkFields = ['platform_code', 'store_code', 'username', 'user_id', 'role']; checkFields.forEach(function (field) { if (jwt[field] !== undefined && me[field] !== undefined && String(jwt[field]) !== String(me[field])) { mismatches.push(field + ': JWT=' + jwt[field] + ' vs server=' + me[field]); } }); if (mismatches.length > 0) { mismatches.forEach(function (msg) { meHtml += '
MISMATCH: ' + escapeHtml(msg) + '
'; }); } else { meHtml += '
All checked fields consistent
'; } } } var section = document.getElementById('_dev_auth_me_section'); if (section) section.innerHTML = meHtml; // noqa: SEC015 }); } } // ── Tab Renderers ── function renderTab() { if (!contentEl) return; switch (activeTab) { case 'platform': renderPlatformTab(); break; case 'auth': renderAuthTab(); break; case 'api': renderApiCallsTab(); break; case 'request': renderRequestInfoTab(); break; case 'console': renderConsoleTab(); break; } } function renderPlatformTab() { var ctx = gatherPlatformContext(); var jwt = ctx.jwt; var html = ''; html += sectionHeader('Client State'); html += row('window.STORE_PLATFORM_CODE', ctx.windowPlatformCode ?? '(undefined)', highlightPlatform); html += row('window.STORE_CODE', ctx.windowStoreCode ?? '(undefined)'); html += row('localStorage.store_platform', ctx.lsPlatform || '(empty)', highlightPlatform); html += row('localStorage.storeCode', ctx.lsStoreCode || '(empty)'); html += row('localStorage.store_token', ctx.lsToken ? '...' + ctx.lsToken.slice(-20) : '(empty)'); html += sectionHeader('JWT Token (decoded)'); html += row('platform_code', jwt?.platform_code ?? '(none)', highlightPlatform); html += row('platform_id', jwt?.platform_id ?? '(none)'); html += row('store_code', jwt?.store_code ?? '(none)'); html += row('store_role', jwt?.store_role ?? '(none)'); html += row('username', jwt?.username ?? '(none)'); html += row('expires', jwt?.exp ? new Date(jwt.exp * 1000).toLocaleTimeString() : '(none)'); html += sectionHeader('Login Would Send'); var loginVal = ctx.loginWouldSend || '(null \u2014 Source 3 fallback)'; html += row('platform_code', loginVal, highlightPlatform); html += '
' + 'STORE_PLATFORM_CODE || localStorage.store_platform || null
'; html += sectionHeader('URL'); html += row('host', ctx.host); html += row('pathname', ctx.pathname); // Consistency check html += sectionHeader('Consistency Check'); var jwtPlatform = jwt?.platform_code ?? '(none)'; var lsPlatform = ctx.lsPlatform || '(empty)'; var windowPlatform = ctx.windowPlatformCode; if (jwtPlatform !== '(none)' && lsPlatform !== '(empty)' && jwtPlatform !== lsPlatform) { html += '
MISMATCH: JWT platform_code=' + escapeHtml(jwtPlatform) + ' but localStorage.store_platform=' + escapeHtml(lsPlatform) + '
'; } else if (jwtPlatform !== '(none)' && windowPlatform !== undefined && windowPlatform !== null && String(windowPlatform) !== '(undefined)' && jwtPlatform !== windowPlatform) { html += '
WARNING: JWT platform_code=' + escapeHtml(jwtPlatform) + ' but window.STORE_PLATFORM_CODE=' + escapeHtml(String(windowPlatform)) + '
'; } else { html += '
All sources consistent
'; } contentEl.innerHTML = html; // noqa: SEC015 // Async /auth/me fetchAuthMe().then(function (me) { var meHtml = sectionHeader('/auth/me (server)'); if (me.error) { meHtml += '
' + escapeHtml(me.error) + '
'; } else { meHtml += row('platform_code', me.platform_code ?? '(null)', highlightPlatform); meHtml += row('username', me.username || '(unknown)'); meHtml += row('role', me.role || '(unknown)'); if (me.platform_code && jwtPlatform !== '(none)' && me.platform_code !== jwtPlatform) { meHtml += '
MISMATCH: /me platform_code=' + escapeHtml(me.platform_code) + ' but JWT=' + escapeHtml(jwtPlatform) + '
'; } } if (contentEl && activeTab === 'platform') { contentEl.innerHTML += meHtml; } }); } function renderApiCallsTab() { var html = ''; // Toolbar row html += '
'; html += '' + apiCalls.length + ' requests captured'; html += ''; html += '
'; if (apiCalls.length === 0) { html += '
No API calls captured yet.
'; } else { // Table header html += '
'; html += 'Method'; html += 'URL'; html += 'Status'; html += 'Time'; html += 'When'; html += '
'; // Rows (newest first) for (var i = apiCalls.length - 1; i >= 0; i--) { var call = apiCalls[i]; var isExpanded_ = expandedApiRows[call.id]; var methodColor = call.method === 'GET' ? C.green : call.method === 'POST' ? C.blue : call.method === 'PUT' ? C.peach : call.method === 'DELETE' ? C.red : C.text; html += '
'; html += '
'; html += '' + call.method + ''; html += '' + escapeHtml(call.url) + ''; html += '' + (call.status !== null ? call.status : '\u2022\u2022\u2022') + ''; html += '' + (call.duration !== null ? formatDuration(call.duration) : '\u2022\u2022\u2022') + ''; html += '' + formatTime(call.timestamp) + ''; html += '
'; // Expanded detail if (isExpanded_) { html += '
'; if (call.requestHeaders) { html += '
Request Headers
'; html += '
' +
                            escapeHtml(JSON.stringify(call.requestHeaders, null, 2)) + '
'; } if (call.requestBody) { html += '
Request Body
'; html += '
' + escapeHtml(formatJsonSafe(call.requestBody)) + '
'; } if (call.responseBody) { html += '
Response Body
'; html += '
' +
                            escapeHtml(formatJsonSafe(call.responseBody)) + '
'; } if (call.error) { html += '
Error: ' + escapeHtml(call.error) + '
'; } html += '
'; } html += '
'; } } contentEl.innerHTML = html; // noqa: SEC015 // Attach event listeners var clearBtn = document.getElementById('_dev_api_clear'); if (clearBtn) { clearBtn.addEventListener('click', function () { apiCalls.length = 0; expandedApiRows = {}; renderApiCallsTab(); updateBadges(); }); } contentEl.querySelectorAll('[data-api-toggle]').forEach(function (el) { el.addEventListener('click', function () { var id = parseInt(el.dataset.apiToggle, 10); expandedApiRows[id] = !expandedApiRows[id]; renderApiCallsTab(); }); }); } function formatJsonSafe(str) { try { var obj = JSON.parse(str); return JSON.stringify(obj, null, 2); } catch (e) { return str; } } function renderRequestInfoTab() { var info = gatherRequestInfo(); var html = ''; html += sectionHeader('Page'); html += row('URL', info.url); html += row('pathname', info.pathname); html += row('host', info.host); html += row('protocol', info.protocol); html += row('detected frontend', info.detectedFrontend); html += row('environment', info.environment); html += sectionHeader('Platform Context'); html += row('STORE_CODE', info.storeCode); html += row('STORE_PLATFORM_CODE', info.storePlatformCode, highlightPlatform); html += row('localStorage.store_platform', info.lsPlatform, highlightPlatform); html += sectionHeader('Store Config'); if (info.storeConfig) { Object.keys(info.storeConfig).forEach(function (key) { html += row(key, info.storeConfig[key] ?? '(null)'); }); } else { html += '
STORE_CONFIG not defined
'; } html += sectionHeader('User Permissions'); if (info.userPermissions && Array.isArray(info.userPermissions)) { if (info.userPermissions.length === 0) { html += '
(empty array)
'; } else { info.userPermissions.forEach(function (perm) { html += '
\u2022 ' + escapeHtml(String(perm)) + '
'; }); } } else { html += '
USER_PERMISSIONS not defined
'; } html += sectionHeader('Tokens'); html += row('store_token', info.tokensPresent.store_token ? 'present' : 'absent'); html += row('admin_token', info.tokensPresent.admin_token ? 'present' : 'absent'); html += row('merchant_token', info.tokensPresent.merchant_token ? 'present' : 'absent'); html += row('customer_token', info.tokensPresent.customer_token ? 'present' : 'absent'); html += sectionHeader('Log Config'); if (info.logConfig) { Object.keys(info.logConfig).forEach(function (key) { var val = info.logConfig[key]; if (typeof val !== 'function') { html += row(key, String(val)); } }); } else { html += '
LogConfig not defined
'; } contentEl.innerHTML = html; // noqa: SEC015 } function renderConsoleTab() { var html = ''; // Filter bar html += '
'; var filters = ['all', 'log', 'info', 'warn', 'error']; filters.forEach(function (f) { var isActive = consoleFilter === f; var bg = isActive ? C.mauve : C.surface1; var fg = isActive ? C.base : C.text; html += ''; }); html += '
'; html += ''; html += '
'; // Filtered logs var filtered = consoleLogs; if (consoleFilter !== 'all') { filtered = consoleLogs.filter(function (l) { return l.level === consoleFilter; }); } if (filtered.length === 0) { html += '
No console output captured.
'; } else { for (var i = filtered.length - 1; i >= 0; i--) { var entry = filtered[i]; html += '
'; html += '' + formatTime(entry.timestamp) + ''; html += '' + levelBadge(entry.level) + ''; html += ''; html += escapeHtml(entry.args.join(' ')); html += '
'; } } contentEl.innerHTML = html; // noqa: SEC015 // Attach filter listeners contentEl.querySelectorAll('._dev_console_filter').forEach(function (btn) { btn.addEventListener('click', function () { consoleFilter = btn.dataset.filter; renderConsoleTab(); }); }); var clearBtn = document.getElementById('_dev_console_clear'); if (clearBtn) { clearBtn.addEventListener('click', function () { consoleLogs.length = 0; renderConsoleTab(); updateBadges(); }); } } // ── Keyboard shortcut: Ctrl+Shift+D ── document.addEventListener('keydown', function (e) { if (e.ctrlKey && e.altKey && (e.key === 'D' || e.key === 'd')) { e.preventDefault(); toggleToolbar(); } }); // ── Init on DOMContentLoaded ── function init() { createToolbar(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();