feat: add SQL query tool, platform debug, loyalty settings, and multi-module improvements
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Failing after 50m12s
CI / validate (push) Successful in 25s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- 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:
2026-03-10 20:08:07 +01:00
parent a77a8a3a98
commit 319900623a
77 changed files with 5341 additions and 401 deletions

View 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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();
}
})();