- API-004: Add noqa for factory-pattern auth in user_account routes and payments admin - MDL-003: Add from_attributes to MerchantStoreDetailResponse schema - EXC-003: Suppress broad except in merchant_store_service and admin_subscription_service (intentional fallbacks for optional billing module) - NAM-002: Rename onboarding files to *_service.py suffix and update all imports - JS-001: Add file-level noqa for dev-toolbar.js (console interceptor by design) - JS-005: Add init guards to dashboard.js and customer-detail.js - IMPORT-004: Break circular deps by removing orders from inventory requires and marketplace from orders requires; add IMPORT-002 suppression for lazy cross-imports - MOD-025: Remove unused OnboardingAlreadyCompletedException Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
998 lines
42 KiB
JavaScript
998 lines
42 KiB
JavaScript
// 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, '>').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 '<span style="display:inline-block;padding:0 4px;border-radius:3px;font-size:9px;font-weight:bold;' +
|
|
'background:' + levelColor(level) + ';color:' + C.base + '">' + level.toUpperCase() + '</span>';
|
|
}
|
|
|
|
// ── HTML Helpers ──
|
|
function sectionHeader(title) {
|
|
return '<div style="margin-top:10px;margin-bottom:4px;color:' + C.blue +
|
|
';font-weight:bold;font-size:11px;border-bottom:1px solid ' + C.surface0 +
|
|
';padding-bottom:2px">' + escapeHtml(title) + '</div>';
|
|
}
|
|
|
|
function row(label, value, highlightFn) {
|
|
var color = highlightFn ? highlightFn(value) : C.text;
|
|
return '<div class="_dev_row" style="display:flex;justify-content:space-between;padding:1px 0">' +
|
|
'<span style="color:' + C.subtext + '">' + escapeHtml(label) + '</span>' +
|
|
'<span style="color:' + color + ';font-weight:bold">' + escapeHtml(String(value)) + '</span></div>';
|
|
}
|
|
|
|
// ── 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);
|
|
};
|
|
});
|
|
|
|
// ── 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() {
|
|
var path = window.location.pathname;
|
|
if (path.startsWith('/store/') || path === '/store') return 'store';
|
|
if (path.startsWith('/admin/') || path === '/admin') return 'admin';
|
|
if (path.indexOf('/merchants/') !== -1) return 'merchant';
|
|
if (path.indexOf('/storefront/') !== -1) return 'storefront';
|
|
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 = '<div style="width:40px;height:2px;background:' + C.surface1 + // noqa: SEC015
|
|
';border-radius:1px"></div>';
|
|
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 + '<span id="_dev_badge_' + tab.id + '" style="margin-left:4px;font-size:9px;color:' + C.subtext + '"></span>'; // 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('<span style="color:' + C.red + '">' + errCount + 'E</span>');
|
|
if (warnCount > 0) parts.push('<span style="color:' + C.yellow + '">' + warnCount + 'W</span>');
|
|
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 += '<div style="margin:2px 0"><span style="display:inline-block;padding:2px 10px;border-radius:3px;' +
|
|
'font-weight:bold;font-size:11px;background:' + badgeColor + ';color:' + C.base + '">' +
|
|
escapeHtml(frontend) + '</span></div>';
|
|
|
|
// 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 += '<div style="color:' + C.subtext + '">No JWT to decode</div>';
|
|
}
|
|
|
|
// 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 += '<div style="color:' + C.subtext + '">No exp claim</div>';
|
|
}
|
|
|
|
// 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 += '<div id="_dev_auth_me_section"></div>';
|
|
|
|
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
|
|
'<div style="color:' + C.subtext + '">Loading...</div>';
|
|
|
|
fetchFrontendAuthMe(frontend).then(function (me) {
|
|
if (activeTab !== 'auth') return;
|
|
var meHtml = sectionHeader('Server Auth (' + getAuthMeEndpoint(frontend) + ')');
|
|
if (me.error) {
|
|
meHtml += '<div style="color:' + C.red + '">' + escapeHtml(me.error) + '</div>';
|
|
} 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 += '<div style="color:' + C.red + ';font-weight:bold">MISMATCH: ' + escapeHtml(msg) + '</div>';
|
|
});
|
|
} else {
|
|
meHtml += '<div style="color:' + C.green + '">All checked fields consistent</div>';
|
|
}
|
|
}
|
|
}
|
|
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 += '<div style="color:' + C.subtext + ';margin-top:2px;font-size:10px">' +
|
|
'STORE_PLATFORM_CODE || localStorage.store_platform || null</div>';
|
|
|
|
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 += '<div style="color:' + C.red + ';font-weight:bold">MISMATCH: JWT platform_code=' +
|
|
escapeHtml(jwtPlatform) + ' but localStorage.store_platform=' + escapeHtml(lsPlatform) + '</div>';
|
|
} else if (jwtPlatform !== '(none)' && windowPlatform !== undefined && windowPlatform !== null &&
|
|
String(windowPlatform) !== '(undefined)' && jwtPlatform !== windowPlatform) {
|
|
html += '<div style="color:' + C.peach + ';font-weight:bold">WARNING: JWT platform_code=' +
|
|
escapeHtml(jwtPlatform) + ' but window.STORE_PLATFORM_CODE=' + escapeHtml(String(windowPlatform)) + '</div>';
|
|
} else {
|
|
html += '<div style="color:' + C.green + '">All sources consistent</div>';
|
|
}
|
|
|
|
contentEl.innerHTML = html; // noqa: SEC015
|
|
|
|
// Async /auth/me
|
|
fetchAuthMe().then(function (me) {
|
|
var meHtml = sectionHeader('/auth/me (server)');
|
|
if (me.error) {
|
|
meHtml += '<div style="color:' + C.red + '">' + escapeHtml(me.error) + '</div>';
|
|
} 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 += '<div style="color:' + C.red + ';font-weight:bold">MISMATCH: /me platform_code=' +
|
|
escapeHtml(me.platform_code) + ' but JWT=' + escapeHtml(jwtPlatform) + '</div>';
|
|
}
|
|
}
|
|
if (contentEl && activeTab === 'platform') {
|
|
contentEl.innerHTML += meHtml;
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderApiCallsTab() {
|
|
var html = '';
|
|
|
|
// Toolbar row
|
|
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">';
|
|
html += '<span style="color:' + C.subtext + '">' + apiCalls.length + ' requests captured</span>';
|
|
html += '<button id="_dev_api_clear" style="padding:2px 8px;font-size:10px;border-radius:3px;' +
|
|
'background:' + C.surface1 + ';color:' + C.text + ';border:none;cursor:pointer;font-family:inherit">Clear</button>';
|
|
html += '</div>';
|
|
|
|
if (apiCalls.length === 0) {
|
|
html += '<div style="color:' + C.subtext + ';text-align:center;padding:20px">No API calls captured yet.</div>';
|
|
} else {
|
|
// Table header
|
|
html += '<div style="display:flex;padding:2px 4px;color:' + C.subtext + ';font-size:10px;font-weight:bold;' +
|
|
'border-bottom:1px solid ' + C.surface1 + '">';
|
|
html += '<span style="width:50px">Method</span>';
|
|
html += '<span style="flex:1;padding:0 4px">URL</span>';
|
|
html += '<span style="width:50px;text-align:right">Status</span>';
|
|
html += '<span style="width:60px;text-align:right">Time</span>';
|
|
html += '<span style="width:70px;text-align:right">When</span>';
|
|
html += '</div>';
|
|
|
|
// 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 += '<div class="_dev_row" style="border-bottom:1px solid ' + C.surface0 + ';cursor:pointer" data-api-row="' + call.id + '">';
|
|
html += '<div style="display:flex;padding:2px 4px;align-items:center" data-api-toggle="' + call.id + '">';
|
|
html += '<span style="width:50px;color:' + methodColor + ';font-weight:bold;font-size:10px">' + call.method + '</span>';
|
|
html += '<span style="flex:1;padding:0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(call.url) + '</span>';
|
|
html += '<span style="width:50px;text-align:right;color:' + statusColor(call.status) + ';font-weight:bold">' +
|
|
(call.status !== null ? call.status : '\u2022\u2022\u2022') + '</span>';
|
|
html += '<span style="width:60px;text-align:right">' +
|
|
(call.duration !== null ? formatDuration(call.duration) : '\u2022\u2022\u2022') + '</span>';
|
|
html += '<span style="width:70px;text-align:right;color:' + C.subtext + ';font-size:10px">' + formatTime(call.timestamp) + '</span>';
|
|
html += '</div>';
|
|
|
|
// Expanded detail
|
|
if (isExpanded_) {
|
|
html += '<div style="padding:4px 8px 8px;background:' + C.surface0 + ';font-size:10px">';
|
|
if (call.requestHeaders) {
|
|
html += '<div style="color:' + C.blue + ';font-weight:bold;margin-top:4px">Request Headers</div>';
|
|
html += '<pre style="margin:2px 0;white-space:pre-wrap;color:' + C.text + '">' +
|
|
escapeHtml(JSON.stringify(call.requestHeaders, null, 2)) + '</pre>';
|
|
}
|
|
if (call.requestBody) {
|
|
html += '<div style="color:' + C.blue + ';font-weight:bold;margin-top:4px">Request Body</div>';
|
|
html += '<pre style="margin:2px 0;white-space:pre-wrap;color:' + C.text + '">' + escapeHtml(formatJsonSafe(call.requestBody)) + '</pre>';
|
|
}
|
|
if (call.responseBody) {
|
|
html += '<div style="color:' + C.green + ';font-weight:bold;margin-top:4px">Response Body</div>';
|
|
html += '<pre style="margin:2px 0;white-space:pre-wrap;color:' + C.text + ';max-height:200px;overflow-y:auto">' +
|
|
escapeHtml(formatJsonSafe(call.responseBody)) + '</pre>';
|
|
}
|
|
if (call.error) {
|
|
html += '<div style="color:' + C.red + ';font-weight:bold;margin-top:4px">Error: ' + escapeHtml(call.error) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
}
|
|
|
|
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 += '<div style="color:' + C.subtext + '">STORE_CONFIG not defined</div>';
|
|
}
|
|
|
|
html += sectionHeader('User Permissions');
|
|
if (info.userPermissions && Array.isArray(info.userPermissions)) {
|
|
if (info.userPermissions.length === 0) {
|
|
html += '<div style="color:' + C.subtext + '">(empty array)</div>';
|
|
} else {
|
|
info.userPermissions.forEach(function (perm) {
|
|
html += '<div style="padding:1px 0;color:' + C.green + '">\u2022 ' + escapeHtml(String(perm)) + '</div>';
|
|
});
|
|
}
|
|
} else {
|
|
html += '<div style="color:' + C.subtext + '">USER_PERMISSIONS not defined</div>';
|
|
}
|
|
|
|
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 += '<div style="color:' + C.subtext + '">LogConfig not defined</div>';
|
|
}
|
|
|
|
contentEl.innerHTML = html; // noqa: SEC015
|
|
}
|
|
|
|
function renderConsoleTab() {
|
|
var html = '';
|
|
|
|
// Filter bar
|
|
html += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">';
|
|
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 += '<button class="_dev_console_filter" data-filter="' + f + '" style="padding:2px 8px;font-size:10px;' +
|
|
'border-radius:3px;background:' + bg + ';color:' + fg + ';border:none;cursor:pointer;font-family:inherit;font-weight:bold">' +
|
|
f.toUpperCase() + '</button>';
|
|
});
|
|
html += '<div style="flex:1"></div>';
|
|
html += '<button id="_dev_console_clear" style="padding:2px 8px;font-size:10px;border-radius:3px;' +
|
|
'background:' + C.surface1 + ';color:' + C.text + ';border:none;cursor:pointer;font-family:inherit">Clear</button>';
|
|
html += '</div>';
|
|
|
|
// Filtered logs
|
|
var filtered = consoleLogs;
|
|
if (consoleFilter !== 'all') {
|
|
filtered = consoleLogs.filter(function (l) { return l.level === consoleFilter; });
|
|
}
|
|
|
|
if (filtered.length === 0) {
|
|
html += '<div style="color:' + C.subtext + ';text-align:center;padding:20px">No console output captured.</div>';
|
|
} else {
|
|
for (var i = filtered.length - 1; i >= 0; i--) {
|
|
var entry = filtered[i];
|
|
html += '<div class="_dev_row" style="display:flex;gap:6px;padding:2px 0;border-bottom:1px solid ' + C.surface0 + ';align-items:flex-start">';
|
|
html += '<span style="flex-shrink:0;color:' + C.subtext + ';font-size:10px;width:75px">' + formatTime(entry.timestamp) + '</span>';
|
|
html += '<span style="flex-shrink:0">' + levelBadge(entry.level) + '</span>';
|
|
html += '<span style="flex:1;white-space:pre-wrap;word-break:break-all;color:' + levelColor(entry.level) + '">';
|
|
html += escapeHtml(entry.args.join(' '));
|
|
html += '</span></div>';
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
})();
|