feat: add SQL query tool, platform debug, loyalty settings, and multi-module improvements
Some checks failed
Some checks failed
- Add admin SQL query tool with saved queries, schema explorer presets, and collapsible category sections (dev_tools module) - Add platform debug tool for admin diagnostics - Add loyalty settings page with owner-only access control - Fix loyalty settings owner check (use currentUser instead of window.__userData) - Replace HTTPException with AuthorizationException in loyalty routes - Expand loyalty module with PIN service, Apple Wallet, program management - Improve store login with platform detection and multi-platform support - Update billing feature gates and subscription services - Add store platform sync improvements and remove is_primary column - Add unit tests for loyalty (PIN, points, stamps, program services) - Update i18n translations across dev_tools locales Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
781
static/shared/js/dev-toolbar.js
Normal file
781
static/shared/js/dev-toolbar.js
Normal file
@@ -0,0 +1,781 @@
|
||||
// static/shared/js/dev-toolbar.js
|
||||
/**
|
||||
* 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 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'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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.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 +
|
||||
';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: '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>';
|
||||
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 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);
|
||||
|
||||
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 switchTab(tabId) {
|
||||
activeTab = tabId;
|
||||
localStorage.setItem(STORAGE_TAB_KEY, tabId);
|
||||
updateTabStyles();
|
||||
renderTab();
|
||||
}
|
||||
|
||||
function updateTabStyles() {
|
||||
var tabs = ['platform', '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('/') + ')' : '';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tab Renderers ──
|
||||
function renderTab() {
|
||||
if (!contentEl) return;
|
||||
switch (activeTab) {
|
||||
case 'platform': renderPlatformTab(); 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;
|
||||
|
||||
// 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 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;
|
||||
|
||||
// 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 += 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;
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user