diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..59ba236 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/codingjoe/esupgrade + rev: 2025.9.0 + hooks: + - id: esupgrade + diff --git a/chrome-extension/content.js b/chrome-extension/content.js index a2df91b..c6bc515 100644 --- a/chrome-extension/content.js +++ b/chrome-extension/content.js @@ -1,43 +1,35 @@ -(function() { - 'use strict'; +const STORAGE_KEY = 'django-devbar-show-bar'; +let currentShowState = true; +let styleElement = null; - if (typeof chrome === 'undefined' || !chrome.storage) { - return; +function injectHideCSS() { + if (!styleElement) { + styleElement = document.createElement('style'); + styleElement.id = 'devbar-visibility-control'; + styleElement.textContent = '#django-devbar { display: none !important; }'; + (document.head || document.documentElement).appendChild(styleElement); } +} - const STORAGE_KEY = 'django-devbar-show-bar'; - let currentShowState = true; - let styleElement = null; - - function injectHideCSS() { - if (!styleElement) { - styleElement = document.createElement('style'); - styleElement.id = 'devbar-visibility-control'; - styleElement.textContent = '#django-devbar { display: none !important; }'; - (document.head || document.documentElement).appendChild(styleElement); - } - } - - function removeHideCSS() { - if (styleElement && styleElement.parentNode) { - styleElement.parentNode.removeChild(styleElement); - styleElement = null; - } +function removeHideCSS() { + if (styleElement?.parentNode) { + styleElement.parentNode.removeChild(styleElement); + styleElement = null; } +} - function checkAndApply() { - chrome.storage.local.get([STORAGE_KEY], (result) => { - currentShowState = result[STORAGE_KEY] !== false; - currentShowState ? removeHideCSS() : injectHideCSS(); - }); - } +function checkAndApply() { + chrome.storage.local.get([STORAGE_KEY], (result) => { + currentShowState = result[STORAGE_KEY] !== false; + currentShowState ? removeHideCSS() : injectHideCSS(); + }); +} - checkAndApply(); +checkAndApply(); - chrome.storage.onChanged.addListener((changes, area) => { - if (area === 'local' && changes[STORAGE_KEY]) { - currentShowState = changes[STORAGE_KEY].newValue; - currentShowState ? removeHideCSS() : injectHideCSS(); - } - }); -})(); +chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && changes[STORAGE_KEY]) { + currentShowState = changes[STORAGE_KEY].newValue; + currentShowState ? removeHideCSS() : injectHideCSS(); + } +}); diff --git a/chrome-extension/devtools.js b/chrome-extension/devtools.js index 71906c6..23c0092 100644 --- a/chrome-extension/devtools.js +++ b/chrome-extension/devtools.js @@ -2,5 +2,5 @@ chrome.devtools.panels.create( 'Django DevBar', 'icons/icon16.png', 'panel.html', - function(panel) {} + panel => {} ); diff --git a/chrome-extension/panel.html b/chrome-extension/panel.html index 269974d..7054c5f 100644 --- a/chrome-extension/panel.html +++ b/chrome-extension/panel.html @@ -79,6 +79,6 @@
- + diff --git a/chrome-extension/panel.js b/chrome-extension/panel.js index 4db1096..dd3b141 100644 --- a/chrome-extension/panel.js +++ b/chrome-extension/panel.js @@ -1,292 +1,288 @@ -(function() { - 'use strict'; +const MAX_HISTORY = 50; +const STORAGE_KEY = 'django-devbar-show-bar'; - const MAX_HISTORY = 50; - const STORAGE_KEY = 'django-devbar-show-bar'; - - const checkbox = document.getElementById('show-bar-toggle'); - if (checkbox && chrome && chrome.storage) { - chrome.storage.local.get([STORAGE_KEY], (result) => { - checkbox.checked = result[STORAGE_KEY] !== false; - }); - - checkbox.addEventListener('change', () => { - chrome.storage.local.set({ [STORAGE_KEY]: checkbox.checked }); - }); - } - - let requestHistory = []; - let currentRequest = null; - let pageUrl = null; - let pageUrlReady = false; - let pendingHarLog = null; - - chrome.devtools.inspectedWindow.eval('location.href', (result, error) => { - if (error || !result) return; - pageUrl = result; - pageUrlReady = true; - - if (pendingHarLog) { - processHarLog(pendingHarLog); - pendingHarLog = null; - } +const checkbox = document.getElementById('show-bar-toggle'); +if (checkbox && chrome && chrome.storage) { + chrome.storage.local.get([STORAGE_KEY], (result) => { + checkbox.checked = result[STORAGE_KEY] !== false; }); - chrome.devtools.network.onNavigated.addListener((url) => { - pageUrl = url; - requestHistory = []; - currentRequest = null; - renderUI(); + checkbox.addEventListener('change', () => { + chrome.storage.local.set({ [STORAGE_KEY]: checkbox.checked }); }); - - const formatMs = (value) => value?.toFixed(0) ?? '0'; - const formatTime = (date) => { - const h = date.getHours(), m = date.getMinutes(), s = date.getSeconds(), ms = date.getMilliseconds(); - return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}.${String(ms).padStart(3,'0')}`; - }; - - function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; +} + +let requestHistory = []; +let currentRequest = null; +let pageUrl = null; +let pageUrlReady = false; +let pendingHarLog = null; + +chrome.devtools.inspectedWindow.eval('location.href', (result, error) => { + if (error || !result) return; + pageUrl = result; + pageUrlReady = true; + + if (pendingHarLog) { + processHarLog(pendingHarLog); + pendingHarLog = null; } +}); + +chrome.devtools.network.onNavigated.addListener((url) => { + pageUrl = url; + requestHistory = []; + currentRequest = null; + renderUI(); +}); + +const formatMs = (value) => value?.toFixed(0) ?? '0'; +const formatTime = (date) => { + const h = date.getHours(), m = date.getMinutes(), s = date.getSeconds(), ms = date.getMilliseconds(); + return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}.${String(ms).padStart(3,'0')}`; +}; + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function getPathFromUrl(url) { + try { + const parsed = new URL(url); + return parsed.pathname + parsed.search; + } catch (e) { + return url; + } +} + +function parseDevBarHeaders(headers) { + const devbarHeaders = {}; + for (const { name, value } of headers) { + const lowerName = name.toLowerCase(); + if (lowerName.startsWith('devbar-')) { + devbarHeaders[lowerName] = value; + } + } + + if (Object.keys(devbarHeaders).length === 0) return null; - function getPathFromUrl(url) { + if (devbarHeaders['devbar-data']) { try { - const parsed = new URL(url); - return parsed.pathname + parsed.search; + return JSON.parse(devbarHeaders['devbar-data']); } catch (e) { - return url; + console.error('Failed to parse DevBar-Data header:', e); } } - function parseDevBarHeaders(headers) { - const devbarHeaders = {}; - for (const { name, value } of headers) { - const lowerName = name.toLowerCase(); - if (lowerName.startsWith('devbar-')) { - devbarHeaders[lowerName] = value; - } - } + if (devbarHeaders['devbar-query-count']) { + const dbTime = parseFloat(devbarHeaders['devbar-db-time']) || 0; + const appTime = parseFloat(devbarHeaders['devbar-app-time']) || 0; + return { + count: parseInt(devbarHeaders['devbar-query-count'], 10), + db_time: dbTime, + app_time: appTime, + total_time: dbTime + appTime, + duplicates: [] + }; + } - if (Object.keys(devbarHeaders).length === 0) return null; + return null; +} + +function isDocumentRequest(request) { + if (request._resourceType === 'document') return true; + const contentType = request.response.headers.find(h => h.name.toLowerCase() === 'content-type'); + return contentType?.value.includes('text/html') ?? false; +} + +function isMainPageRequest(url) { + if (!pageUrl) return false; + const normalize = (u) => u.split('?')[0].replace(/\/$/, ''); + return normalize(url) === normalize(pageUrl); +} + +function processRequest(request, options = {}) { + const data = parseDevBarHeaders(request.response.headers); + if (!data) return; + + const isDocument = isDocumentRequest(request); + const isMainPage = isMainPageRequest(request.request.url); + + const requestData = { + url: request.request.url, + method: request.request.method, + timestamp: new Date(request.startedDateTime || Date.now()), + data, + isDocument, + isMainPage + }; - if (devbarHeaders['devbar-data']) { - try { - return JSON.parse(devbarHeaders['devbar-data']); - } catch (e) { - console.error('Failed to parse DevBar-Data header:', e); - } - } + if (isMainPage) { + currentRequest = requestData; + } else if (isDocument && !currentRequest?.isMainPage) { + currentRequest = requestData; + } else if (!currentRequest) { + currentRequest = requestData; + } - if (devbarHeaders['devbar-query-count']) { - const dbTime = parseFloat(devbarHeaders['devbar-db-time']) || 0; - const appTime = parseFloat(devbarHeaders['devbar-app-time']) || 0; - return { - count: parseInt(devbarHeaders['devbar-query-count'], 10), - db_time: dbTime, - app_time: appTime, - total_time: dbTime + appTime, - duplicates: [] - }; + const isDuplicate = requestHistory.some( + r => r.url === requestData.url && r.timestamp.getTime() === requestData.timestamp.getTime() + ); + if (!isDuplicate) { + requestHistory.unshift(requestData); + if (requestHistory.length > MAX_HISTORY) { + requestHistory.pop(); } - - return null; } - function isDocumentRequest(request) { - if (request._resourceType === 'document') return true; - const contentType = request.response.headers.find(h => h.name.toLowerCase() === 'content-type'); - return contentType?.value.includes('text/html') ?? false; + if (!options.skipRender) { + renderUI(); } - - function isMainPageRequest(url) { - if (!pageUrl) return false; - const normalize = (u) => u.split('?')[0].replace(/\/$/, ''); - return normalize(url) === normalize(pageUrl); +} + +function renderMetric(label, value, unit = '') { + return `${label} ${value}${unit ? `${unit}` : ''}`; +} + +function getRequestType(req) { + if (req.isMainPage) return { class: 'type-page', label: 'PAGE' }; + if (req.isDocument) return { class: 'type-doc', label: 'DOC' }; + return { class: 'type-xhr', label: 'XHR' }; +} + +function renderEmptyState() { + const app = document.getElementById('app'); + const isLocalDomain = pageUrl && ( + pageUrl.includes('localhost') || + pageUrl.includes('127.0.0.1') || + pageUrl.includes('.local') || + pageUrl.includes('.test') + ); + + let html = ` +
+

Django DevBar

`; + + if (!isLocalDomain && pageUrl) { + html += ` +

⚠️ Not on a local development domain

+

This extension only works on localhost and local development domains.

+

+ django-devbar +

`; + } else { + html += ` +

No requests captured yet.

+

Navigate to a page with Django DevBar enabled.

+
+ Troubleshooting:
+ • Make sure Django DevBar middleware is installed
+ • Set DEVBAR = {'ENABLE_DEVTOOLS_DATA': True} in settings
+ • Reload the page after enabling headers
+ • Check that you're on a localhost or .local/.test domain +
+

+ django-devbar +

`; } - function processRequest(request, options = {}) { - const data = parseDevBarHeaders(request.response.headers); - if (!data) return; - - const isDocument = isDocumentRequest(request); - const isMainPage = isMainPageRequest(request.request.url); - - const requestData = { - url: request.request.url, - method: request.request.method, - timestamp: new Date(request.startedDateTime || Date.now()), - data, - isDocument, - isMainPage - }; - - if (isMainPage) { - currentRequest = requestData; - } else if (isDocument && !currentRequest?.isMainPage) { - currentRequest = requestData; - } else if (!currentRequest) { - currentRequest = requestData; - } + html += `
`; + app.innerHTML = html; +} - const isDuplicate = requestHistory.some( - r => r.url === requestData.url && r.timestamp.getTime() === requestData.timestamp.getTime() - ); - if (!isDuplicate) { - requestHistory.unshift(requestData); - if (requestHistory.length > MAX_HISTORY) { - requestHistory.pop(); - } - } - - if (!options.skipRender) { - renderUI(); - } +function renderUI() { + const app = document.getElementById('app'); + if (!currentRequest) { + renderEmptyState(); + return; } - function renderMetric(label, value, unit = '') { - return `${label} ${value}${unit ? `${unit}` : ''}`; + const { data, method, url } = currentRequest; + const type = getRequestType(currentRequest); + + let html = ` +
+
+ ${type.label} + ${escapeHtml(method)} + ${escapeHtml(getPathFromUrl(url))} + +
+
+ ${renderMetric('queries', data.count ?? 0)} + ${renderMetric('db', formatMs(data.db_time), 'ms')} + ${renderMetric('app', formatMs(data.app_time), 'ms')} + ${data.duplicates?.length ? `⚠ ${data.duplicates.length} dup` : ''} + ${formatTime(currentRequest.timestamp)} +
+
`; + + if (data.duplicates?.length > 0) { + html += `
${data.duplicates.map(dup => + `
${escapeHtml(dup.sql)} ${(dup.duration ?? 0).toFixed(1)}ms
` + ).join('')}
`; } - function getRequestType(req) { - if (req.isMainPage) return { class: 'type-page', label: 'PAGE' }; - if (req.isDocument) return { class: 'type-doc', label: 'DOC' }; - return { class: 'type-xhr', label: 'XHR' }; - } + const hasPageOrDoc = requestHistory.some(r => r.isMainPage || r.isDocument); - function renderEmptyState() { - const app = document.getElementById('app'); - const isLocalDomain = pageUrl && ( - pageUrl.includes('localhost') || - pageUrl.includes('127.0.0.1') || - pageUrl.includes('.local') || - pageUrl.includes('.test') - ); - - let html = ` -
-

Django DevBar

`; - - if (!isLocalDomain && pageUrl) { - html += ` -

⚠️ Not on a local development domain

-

This extension only works on localhost and local development domains.

-

- django-devbar -

`; - } else { - html += ` -

No requests captured yet.

-

Navigate to a page with Django DevBar enabled.

-
- Troubleshooting:
- • Make sure Django DevBar middleware is installed
- • Set DEVBAR = {'ENABLE_DEVTOOLS_DATA': True} in settings
- • Reload the page after enabling headers
- • Check that you're on a localhost or .local/.test domain -
-

- django-devbar -

`; - } + const otherRequests = requestHistory + .filter(r => r !== currentRequest) + .sort((a, b) => { + if (hasPageOrDoc) { + if (a.isMainPage !== b.isMainPage) return a.isMainPage ? -1 : 1; + } + return b.timestamp - a.timestamp; + }); - html += `
`; - app.innerHTML = html; + if (otherRequests.length > 0) { + const sectionTitle = hasPageOrDoc ? 'Other' : 'Recent Requests'; + html += `
${sectionTitle} (${otherRequests.length})
+ ${otherRequests.map(req => { + const t = getRequestType(req); + return `
+
+ ${t.label} + ${escapeHtml(req.method)} + ${escapeHtml(getPathFromUrl(req.url))} + +
+
+ ${renderMetric('queries', req.data.count ?? 0)} + ${renderMetric('db', formatMs(req.data.db_time), 'ms')} + ${renderMetric('app', formatMs(req.data.app_time), 'ms')} + ${req.data.duplicates?.length ? `` : ''} + ${formatTime(req.timestamp)} +
+
`; + }).join('')} +
`; } - function renderUI() { - const app = document.getElementById('app'); - if (!currentRequest) { - renderEmptyState(); - return; - } - - const { data, method, url } = currentRequest; - const type = getRequestType(currentRequest); - - let html = ` -
-
- ${type.label} - ${escapeHtml(method)} - ${escapeHtml(getPathFromUrl(url))} - -
-
- ${renderMetric('queries', data.count ?? 0)} - ${renderMetric('db', formatMs(data.db_time), 'ms')} - ${renderMetric('app', formatMs(data.app_time), 'ms')} - ${data.duplicates?.length ? `⚠ ${data.duplicates.length} dup` : ''} - ${formatTime(currentRequest.timestamp)} -
-
`; - - if (data.duplicates?.length > 0) { - html += `
${data.duplicates.map(dup => - `
${escapeHtml(dup.sql)} ${(dup.duration ?? 0).toFixed(1)}ms
` - ).join('')}
`; - } - - const hasPageOrDoc = requestHistory.some(r => r.isMainPage || r.isDocument); - - const otherRequests = requestHistory - .filter(r => r !== currentRequest) - .sort((a, b) => { - if (hasPageOrDoc) { - if (a.isMainPage !== b.isMainPage) return a.isMainPage ? -1 : 1; - } - return b.timestamp - a.timestamp; - }); - - if (otherRequests.length > 0) { - const sectionTitle = hasPageOrDoc ? 'Other' : 'Recent Requests'; - html += `
${sectionTitle} (${otherRequests.length})
- ${otherRequests.map(req => { - const t = getRequestType(req); - return `
-
- ${t.label} - ${escapeHtml(req.method)} - ${escapeHtml(getPathFromUrl(req.url))} - -
-
- ${renderMetric('queries', req.data.count ?? 0)} - ${renderMetric('db', formatMs(req.data.db_time), 'ms')} - ${renderMetric('app', formatMs(req.data.app_time), 'ms')} - ${req.data.duplicates?.length ? `` : ''} - ${formatTime(req.timestamp)} -
-
`; - }).join('')} -
`; - } + app.innerHTML = html; +} - app.innerHTML = html; - } +function processHarLog(harLog) { + if (!harLog?.entries) return; - function processHarLog(harLog) { - if (!harLog?.entries) return; + harLog.entries.forEach(entry => processRequest(entry, { skipRender: true })); - harLog.entries.forEach(entry => processRequest(entry, { skipRender: true })); + currentRequest = requestHistory.find(r => r.isMainPage) + || requestHistory.filter(r => r.isDocument).pop(); - currentRequest = requestHistory.find(r => r.isMainPage) - || requestHistory.filter(r => r.isDocument).pop(); + renderUI(); +} - renderUI(); +chrome.devtools.network.getHAR((harLog) => { + if (pageUrlReady) { + processHarLog(harLog); + } else { + pendingHarLog = harLog; } +}); - chrome.devtools.network.getHAR((harLog) => { - if (pageUrlReady) { - processHarLog(harLog); - } else { - pendingHarLog = harLog; - } - }); - - chrome.devtools.network.onRequestFinished.addListener(processRequest); +chrome.devtools.network.onRequestFinished.addListener(processRequest); - renderEmptyState(); -})(); +renderEmptyState(); diff --git a/chrome-extension/scripts/build.mjs b/chrome-extension/scripts/build.mjs index 9a6850b..d4a5c86 100644 --- a/chrome-extension/scripts/build.mjs +++ b/chrome-extension/scripts/build.mjs @@ -20,7 +20,7 @@ function build() { "'Django DevBar (dev)'" ); writeFileSync(devtoolsPath, content); - console.log('✓ Dev mode enabled'); + console.info('✓ Dev mode enabled'); } } diff --git a/chrome-extension/scripts/generate-icons.mjs b/chrome-extension/scripts/generate-icons.mjs index a1ac56b..08955d9 100644 --- a/chrome-extension/scripts/generate-icons.mjs +++ b/chrome-extension/scripts/generate-icons.mjs @@ -18,7 +18,7 @@ for (const size of sizes) { .resize(size, size) .png() .toFile(join(distPath, `icon${size}.png`)); - console.log(`Generated icon${size}.png`); + console.info(`Generated icon${size}.png`); } -console.log('Done!'); +console.info('Done!');