// 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 += 'Clear ';
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 += '
' +
f.toUpperCase() + ' ';
});
html += '
';
html += '
Clear ';
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();
}
})();