Fixed version with better memory management and storage
// ==UserScript==
// @name API Interceptor + DOM Inspector
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description Fixed version with better memory management and storage
// @author gl4.manu
// @match *://*/*
// @grant GM_log
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_listValues
// @run-at document-start
// @noframes
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Configuration
const CONFIG = {
enabled: true,
maxLogEntries: 50, // Reduced for performance
maxBodySize: 1000, // Smaller body size
maxStorageSize: 500000, // ~500KB max storage
autoSaveInterval: 5000,
persistData: true,
persistKey: 'devtools_pro_v4',
logToConsole: false, // Disable console logging for performance
compressData: true, // Compress stored data
cleanupInterval: 60000, // Cleanup every minute
maxTooltipAge: 3000 // Auto-hide tooltip after 3 seconds
};
// Storage
let interceptedRequests = [];
let requestId = 0;
let expandedItems = new Set();
let inspectorActive = false;
let currentTooltip = null;
let tooltipTimeout = null;
let currentHoverElement = null;
let originalFetch = null;
let saveTimeout = null;
let updateQueue = [];
let updateTimer = null;
// ==================== MEMORY MANAGEMENT ====================
function cleanupOldData() {
// Remove old entries beyond limit
if (interceptedRequests.length > CONFIG.maxLogEntries) {
const removed = interceptedRequests.splice(CONFIG.maxLogEntries);
removed.forEach(r => expandedItems.delete(r.id));
}
// Clear old tooltips
if (currentTooltip && currentTooltip.parentNode) {
clearTooltip();
}
// Force garbage collection hint
if (interceptedRequests.length > CONFIG.maxLogEntries * 0.8) {
console.log('[Cleanup] Reduced to', interceptedRequests.length, 'entries');
}
}
function compressData(data) {
if (!CONFIG.compressData) return JSON.stringify(data);
// Simple compression: remove unnecessary whitespace and truncate long strings
const compressed = JSON.stringify(data, (key, value) => {
if (typeof value === 'string' && value.length > 500) {
return value.substring(0, 500) + '...[truncated]';
}
return value;
});
return compressed;
}
function decompressData(compressed) {
try {
return JSON.parse(compressed);
} catch(e) {
return null;
}
}
function getStorageSize() {
let total = 0;
const keys = GM_listValues();
for (let key of keys) {
const value = GM_getValue(key, '');
total += value.length;
}
return total;
}
function saveData() {
if (!CONFIG.persistData) return;
try {
// Check storage size
if (getStorageSize() > CONFIG.maxStorageSize) {
console.warn('[Storage] Size limit reached, clearing old data');
const oldData = GM_getValue(CONFIG.persistKey, '');
if (oldData.length > CONFIG.maxStorageSize * 0.8) {
// Keep only last 25 entries
const trimmed = interceptedRequests.slice(0, 25);
const compressed = compressData({
version: '4.1.0',
timestamp: Date.now(),
requests: trimmed,
requestId: requestId
});
GM_setValue(CONFIG.persistKey, compressed);
return;
}
}
const dataToSave = {
version: '4.1.0',
timestamp: Date.now(),
requests: interceptedRequests.slice(0, CONFIG.maxLogEntries),
requestId: requestId,
expandedIds: Array.from(expandedItems)
};
const compressed = compressData(dataToSave);
GM_setValue(CONFIG.persistKey, compressed);
} catch(e) {
console.error('[Storage] Save failed:', e);
}
}
function loadData() {
if (!CONFIG.persistData) return false;
try {
const saved = GM_getValue(CONFIG.persistKey, null);
if (!saved) return false;
const data = decompressData(saved);
if (!data) return false;
// Check age (max 2 hours)
const maxAge = 7200000;
if (Date.now() - data.timestamp > maxAge) {
console.log('[Storage] Data too old, clearing');
GM_deleteValue(CONFIG.persistKey);
return false;
}
interceptedRequests = data.requests || [];
requestId = data.requestId || 0;
expandedItems = new Set(data.expandedIds || []);
console.log(`[Storage] Loaded ${interceptedRequests.length} requests`);
return true;
} catch(e) {
console.error('[Storage] Load failed:', e);
return false;
}
}
// ==================== OPTIMIZED UI UPDATES ====================
function queueUpdate() {
if (updateTimer) clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
updateLogsDisplay();
updateTimer = null;
}, 300);
}
function updateLogsDisplay() {
const container = document.getElementById('logs-container');
if (!container) return;
const searchTerm = document.querySelector('.search-box')?.value.toLowerCase() || '';
const filtered = searchTerm ?
interceptedRequests.filter(log =>
log.url.toLowerCase().includes(searchTerm) ||
log.method.toLowerCase().includes(searchTerm)
) : interceptedRequests;
// Virtual scrolling: only render first 20 for performance
const renderLimit = 20;
const toRender = filtered.slice(0, renderLimit);
const hasMore = filtered.length > renderLimit;
container.innerHTML = toRender.map(log => {
const isExpanded = expandedItems.has(log.id);
const hasJWT = !!log.jwt;
return `
<div class="log-entry" data-id="${log.id}">
<div class="log-header" onclick="window.toggleLog(${log.id})">
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
<span class="log-method">${escapeHtml(log.method)}</span>
<span class="log-url" title="${escapeHtml(log.url)}">${escapeHtml(truncate(log.url, 50))}</span>
${log.status ? `<span class="log-status">${log.status}</span>` : ''}
${hasJWT ? '<span class="badge badge-jwt">🔑</span>' : ''}
</div>
<div class="log-details ${isExpanded ? 'expanded' : ''}">
${renderDetails(log)}
</div>
</div>
`;
}).join('');
if (hasMore) {
container.innerHTML += `<div style="text-align:center;padding:5px;color:#888;">+ ${filtered.length - renderLimit} more requests (search to see all)</div>`;
}
updateStats(filtered.length);
}
function renderDetails(log) {
let html = '';
if (log.jwt) {
html += `<div class="detail-section">
<div class="detail-title">🔑 JWT</div>
<div class="detail-content jwt-token">${escapeHtml(truncate(log.jwt, 100))}</div>
</div>`;
}
if (log.requestHeaders) {
html += `<div class="detail-section">
<div class="detail-title">📤 Headers</div>
<div class="detail-content"><pre>${escapeHtml(truncate(JSON.stringify(log.requestHeaders, null, 2), 300))}</pre></div>
</div>`;
}
if (log.responseBody) {
html += `<div class="detail-section">
<div class="detail-title">📥 Body</div>
<div class="detail-content"><pre>${escapeHtml(truncate(log.responseBody, 300))}</pre></div>
</div>`;
}
if (log.error) {
html += `<div class="detail-section">
<div class="detail-title">❌ Error</div>
<div class="detail-content">${escapeHtml(log.error)}</div>
</div>`;
}
html += `<div class="detail-section">
<div class="detail-title">⏱️ ${new Date(log.timestamp).toLocaleTimeString()}</div>
</div>`;
return html;
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
function truncate(str, len) {
if (!str) return '';
return str.length > len ? str.substring(0, len) + '...' : str;
}
function updateStats(filteredCount) {
const statsBar = document.getElementById('stats-bar');
if (statsBar) {
const countSpan = document.getElementById('request-count');
if (countSpan) countSpan.textContent = interceptedRequests.length;
}
}
// ==================== FIXED DOM INSPECTOR ====================
function clearTooltip() {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
if (currentTooltip && currentTooltip.parentNode) {
currentTooltip.parentNode.removeChild(currentTooltip);
}
currentTooltip = null;
}
function showTooltipFixed(element, x, y) {
clearTooltip();
const tooltip = document.createElement('div');
tooltip.className = 'dom-inspector-tooltip';
const tagName = element.tagName.toLowerCase();
const id = element.id ? `#${element.id}` : '';
const classes = element.className ? `.${element.className.split(' ')[0]}` : '';
tooltip.innerHTML = `
<div><strong>${tagName}${id}${classes}</strong></div>
<div style="font-size:9px;margin-top:4px;">
<button onclick="window.copySelector('${escapeHtml(getElementSelector(element))}')">Copy Selector</button>
<button onclick="window.copyXPath('${escapeHtml(getElementXPath(element))}')">Copy XPath</button>
<button onclick="window.editElement()">Edit</button>
</div>
`;
document.body.appendChild(tooltip);
currentTooltip = tooltip;
// Position
let left = x + 15;
let top = y - 30;
const rect = tooltip.getBoundingClientRect();
if (left + rect.width > window.innerWidth) left = x - rect.width - 15;
if (top + rect.height > window.innerHeight) top = y - rect.height - 15;
if (top < 0) top = 10;
if (left < 0) left = 10;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
// Auto-hide after 3 seconds
tooltipTimeout = setTimeout(clearTooltip, CONFIG.maxTooltipAge);
window.copySelector = (selector) => {
navigator.clipboard.writeText(selector);
showNotification('✅ Selector copied');
clearTooltip();
};
window.copyXPath = (xpath) => {
navigator.clipboard.writeText(xpath);
showNotification('✅ XPath copied');
clearTooltip();
};
window.editElement = () => {
const newHTML = prompt('Edit HTML:', element.outerHTML);
if (newHTML && newHTML !== element.outerHTML) {
try {
const temp = document.createElement('div');
temp.innerHTML = newHTML;
element.parentNode.replaceChild(temp.firstChild, element);
showNotification('✅ Element updated');
} catch(e) {
showNotification('❌ Invalid HTML');
}
}
clearTooltip();
};
}
function getElementSelector(element) {
if (element.id) return `#${element.id}`;
if (element.className && typeof element.className === 'string') {
const cls = element.className.split(' ')[0];
if (cls) return `${element.tagName.toLowerCase()}.${cls}`;
}
return element.tagName.toLowerCase();
}
function getElementXPath(element) {
if (element.id) return `//*[@id="${element.id}"]`;
if (element === document.body) return '/html/body';
let ix = 0;
const siblings = element.parentNode.childNodes;
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i];
if (sibling === element) {
const parentPath = element.parentNode === document.body ? '/html/body' : getElementXPath(element.parentNode);
return parentPath + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++;
}
return '';
}
function showNotification(msg) {
const notif = document.createElement('div');
notif.textContent = msg;
notif.style.cssText = `
position: fixed;
bottom: 80px;
right: 20px;
background: #00ff00;
color: #000;
padding: 5px 10px;
border-radius: 4px;
font-size: 11px;
z-index: 1000000;
animation: fadeOut 1.5s ease-out;
`;
document.body.appendChild(notif);
setTimeout(() => notif.remove(), 1500);
}
// ==================== FIXED EVENT HANDLERS ====================
function initDOMInspectorFixed() {
let inspectorBtn = null;
const addControls = () => {
const container = document.getElementById('logs-container');
if (container && !document.getElementById('inspector-toggle')) {
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:5px;margin-bottom:10px;';
controls.innerHTML = `
<button id="inspector-toggle" style="background:#333;color:#0f0;border:1px solid #0f0;padding:3px 8px;border-radius:3px;cursor:pointer;">🔍 DOM Inspector OFF</button>
<button id="inspector-clear-highlight" style="background:#333;color:#0f0;border:1px solid #0f0;padding:3px 8px;border-radius:3px;cursor:pointer;">✨ Clear</button>
`;
const statsBar = document.getElementById('stats-bar');
if (statsBar) {
statsBar.parentNode.insertBefore(controls, statsBar.nextSibling);
}
inspectorBtn = document.getElementById('inspector-toggle');
const clearBtn = document.getElementById('inspector-clear-highlight');
if (inspectorBtn) {
inspectorBtn.addEventListener('click', toggleInspector);
}
if (clearBtn) {
clearBtn.addEventListener('click', () => {
if (currentHoverElement) {
currentHoverElement.classList.remove('dom-inspector-highlight');
currentHoverElement = null;
}
clearTooltip();
});
}
} else {
setTimeout(addControls, 200);
}
};
function toggleInspector() {
inspectorActive = !inspectorActive;
if (inspectorActive) {
inspectorBtn.textContent = '🔍 DOM Inspector ON';
inspectorBtn.style.background = '#0f0';
inspectorBtn.style.color = '#000';
document.body.style.cursor = 'crosshair';
document.addEventListener('mouseover', onMouseOver);
document.addEventListener('click', onClick, true);
} else {
inspectorBtn.textContent = '🔍 DOM Inspector OFF';
inspectorBtn.style.background = '#333';
inspectorBtn.style.color = '#0f0';
document.body.style.cursor = '';
document.removeEventListener('mouseover', onMouseOver);
document.removeEventListener('click', onClick, true);
if (currentHoverElement) {
currentHoverElement.classList.remove('dom-inspector-highlight');
currentHoverElement = null;
}
clearTooltip();
}
}
function onMouseOver(e) {
if (!inspectorActive) return;
// Ignore inspector buttons
if (e.target.closest('#inspector-toggle') || e.target.closest('#inspector-clear-highlight')) {
return;
}
if (currentHoverElement) {
currentHoverElement.classList.remove('dom-inspector-highlight');
}
currentHoverElement = e.target;
currentHoverElement.classList.add('dom-inspector-highlight');
showTooltipFixed(currentHoverElement, e.clientX, e.clientY);
}
function onClick(e) {
if (!inspectorActive) return;
if (e.target.closest('#inspector-toggle') || e.target.closest('#inspector-clear-highlight')) {
return;
}
e.preventDefault();
e.stopPropagation();
const element = e.target;
const action = prompt(
'DOM Actions:\n' +
'1 - Copy Selector\n' +
'2 - Copy XPath\n' +
'3 - Edit HTML\n' +
'4 - Log to Console\n' +
'5 - Hide Element'
);
switch(action) {
case '1':
navigator.clipboard.writeText(getElementSelector(element));
showNotification('✅ Selector copied');
break;
case '2':
navigator.clipboard.writeText(getElementXPath(element));
showNotification('✅ XPath copied');
break;
case '3':
const newHTML = prompt('Edit HTML:', element.outerHTML);
if (newHTML && newHTML !== element.outerHTML) {
const temp = document.createElement('div');
temp.innerHTML = newHTML;
element.parentNode.replaceChild(temp.firstChild, element);
showNotification('✅ Updated');
}
break;
case '4':
console.log('[DOM]', element);
showNotification('✅ Check console (F12)');
break;
case '5':
element.style.display = 'none';
showNotification('✅ Hidden');
break;
}
clearTooltip();
return false;
}
addControls();
}
// ==================== API INTERCEPTORS (Optimized) ====================
function extractJWT(text) {
if (!text || typeof text !== 'string') return null;
const match = text.match(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/);
return match ? match[0] : null;
}
function addLogEntry(entry) {
if (!CONFIG.enabled) return;
// Remove duplicate consecutive requests (optional)
const lastEntry = interceptedRequests[0];
if (lastEntry && lastEntry.url === entry.url && lastEntry.method === entry.method && Date.now() - lastEntry.timestamp < 100) {
return; // Skip duplicate within 100ms
}
entry.id = requestId++;
entry.timestamp = Date.now();
interceptedRequests.unshift(entry);
// Cleanup old entries
if (interceptedRequests.length > CONFIG.maxLogEntries) {
const removed = interceptedRequests.pop();
expandedItems.delete(removed.id);
}
// Queue UI update
queueUpdate();
// Queue save
if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => saveData(), CONFIG.autoSaveInterval);
}
function interceptFetch() {
if (!window.fetch) return;
originalFetch = window.fetch;
window.fetch = async function(...args) {
const [resource, config = {}] = args;
const url = resource instanceof Request ? resource.url : resource.toString();
const method = config.method || (resource instanceof Request ? resource.method : 'GET');
const startTime = Date.now();
// Extract JWT from headers
let headers = config.headers || {};
if (resource instanceof Request) {
headers = Object.fromEntries(resource.headers.entries());
}
const authHeader = headers['authorization'] || headers['Authorization'];
const jwt = authHeader ? extractJWT(authHeader) : null;
addLogEntry({ type: 'fetch', method, url, jwt });
try {
const response = await originalFetch.apply(this, args);
addLogEntry({
type: 'fetch', method, url,
status: response.status,
duration: Date.now() - startTime
});
return response;
} catch(error) {
addLogEntry({ type: 'fetch', method, url, error: error.message });
throw error;
}
};
}
function interceptXHR() {
if (!window.XMLHttpRequest) return;
const XHR = window.XMLHttpRequest;
const originalOpen = XHR.prototype.open;
const originalSend = XHR.prototype.send;
XHR.prototype.open = function(method, url) {
this._data = { method, url, startTime: Date.now() };
return originalOpen.apply(this, arguments);
};
XHR.prototype.send = function(body) {
if (this._data) {
// Extract JWT from headers if any
let jwt = null;
if (this._data.requestHeaders) {
const authHeader = this._data.requestHeaders['Authorization'] || this._data.requestHeaders['authorization'];
if (authHeader) jwt = extractJWT(authHeader);
}
addLogEntry({ type: 'xhr', ...this._data, jwt });
}
this.addEventListener('loadend', function() {
if (this._data) {
addLogEntry({
type: 'xhr',
method: this._data.method,
url: this._data.url,
status: this.status,
duration: Date.now() - this._data.startTime
});
}
});
return originalSend.apply(this, arguments);
};
}
// ==================== UI CREATION ====================
function createFloatingPanel() {
GM_addStyle(`
#api-interceptor-panel {
position: fixed;
bottom: 10px;
right: 10px;
width: 500px;
max-height: 450px;
background: #1e1e1e;
color: #e0e0e0;
border-radius: 6px;
z-index: 999999;
font-family: monospace;
font-size: 11px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
display: none;
flex-direction: column;
border: 1px solid #0f0;
}
#api-interceptor-panel.active { display: flex; }
.panel-header {
padding: 6px 10px;
background: #2a2a2a;
border-bottom: 1px solid #0f0;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 {
margin: 0;
font-size: 11px;
color: #0f0;
}
.panel-controls button {
background: #333;
color: #0f0;
border: 1px solid #0f0;
padding: 2px 5px;
cursor: pointer;
font-size: 9px;
border-radius: 3px;
}
.panel-controls button:hover {
background: #0f0;
color: #000;
}
.panel-content {
overflow-y: auto;
padding: 8px;
max-height: 400px;
}
.log-entry {
background: #252525;
border-left: 2px solid #0f0;
margin-bottom: 4px;
padding: 4px 6px;
font-size: 10px;
}
.log-header {
cursor: pointer;
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.log-method {
color: #ff6b6b;
font-weight: bold;
}
.log-url {
color: #4ecdc4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.log-status {
color: #ffe66d;
}
.badge-jwt {
background: #ffd700;
color: #000;
padding: 0 3px;
border-radius: 2px;
}
.log-details {
display: none;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #444;
}
.log-details.expanded { display: block; }
.detail-section {
margin-bottom: 5px;
}
.detail-title {
color: #0f0;
font-size: 9px;
margin-bottom: 2px;
}
.detail-content {
background: #1a1a1a;
padding: 4px;
border-radius: 3px;
font-size: 9px;
overflow-x: auto;
max-height: 80px;
}
.jwt-token {
color: #ffd700;
word-break: break-all;
}
.dom-inspector-highlight {
outline: 2px solid #0f0 !important;
background-color: rgba(0,255,0,0.1) !important;
cursor: crosshair !important;
}
.dom-inspector-tooltip {
position: fixed;
background: #1e1e1e;
color: #0f0;
padding: 6px 10px;
border-radius: 4px;
font-size: 10px;
z-index: 1000000;
border: 1px solid #0f0;
pointer-events: auto;
}
.dom-inspector-tooltip button {
background: #333;
color: #0f0;
border: 1px solid #0f0;
padding: 2px 6px;
margin: 0 2px;
cursor: pointer;
font-size: 9px;
border-radius: 3px;
}
.toggle-panel-btn {
position: fixed;
bottom: 10px;
right: 10px;
width: 32px;
height: 32px;
background: #0f0;
color: #000;
border: none;
border-radius: 50%;
cursor: pointer;
z-index: 999998;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.search-box {
width: 100%;
padding: 4px 6px;
margin-bottom: 8px;
background: #2a2a2a;
border: 1px solid #0f0;
color: #0f0;
border-radius: 3px;
font-size: 10px;
}
.stats-bar {
background: #2a2a2a;
padding: 3px 6px;
font-size: 9px;
margin-bottom: 8px;
border-radius: 3px;
display: flex;
justify-content: space-between;
}
@keyframes fadeOut {
0% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
}
`);
// Toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'toggle-panel-btn';
toggleBtn.innerHTML = '🛠️';
document.body.appendChild(toggleBtn);
// Panel
const panel = document.createElement('div');
panel.id = 'api-interceptor-panel';
panel.innerHTML = `
<div class="panel-header">
<h3>🛠️ DevTools Pro</h3>
<div class="panel-controls">
<button id="clear-logs">Clear</button>
<button id="export-logs">Export</button>
<button id="close-panel">×</button>
</div>
</div>
<div class="panel-content">
<input type="text" class="search-box" placeholder="Filter...">
<div class="stats-bar">
<span>📡 <span id="request-count">0</span></span>
<span>💾 Auto-save</span>
</div>
<div id="logs-container"></div>
</div>
`;
document.body.appendChild(panel);
// Draggable
let dragData = { active: false };
const header = panel.querySelector('.panel-header');
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
dragData = {
active: true,
startX: e.clientX,
startY: e.clientY,
startLeft: panel.offsetLeft,
startTop: panel.offsetTop
};
panel.style.left = dragData.startLeft + 'px';
panel.style.top = dragData.startTop + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragData.active) return;
panel.style.left = (dragData.startLeft + e.clientX - dragData.startX) + 'px';
panel.style.top = (dragData.startTop + e.clientY - dragData.startY) + 'px';
});
document.addEventListener('mouseup', () => {
dragData.active = false;
});
// Event handlers
toggleBtn.onclick = () => panel.classList.toggle('active');
document.getElementById('close-panel').onclick = () => panel.classList.remove('active');
document.getElementById('clear-logs').onclick = () => {
if (confirm('Clear all?')) {
interceptedRequests = [];
expandedItems.clear();
requestId = 0;
updateLogsDisplay();
saveData();
}
};
document.getElementById('export-logs').onclick = () => {
const data = JSON.stringify(interceptedRequests, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `devtools-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
document.querySelector('.search-box').addEventListener('input', () => updateLogsDisplay());
return panel;
}
// ==================== INIT ====================
function init() {
console.log('[DevTools Pro] v4.1 - Optimized');
// Load saved data
loadData();
// Setup interceptors
interceptFetch();
interceptXHR();
// Create UI
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
createFloatingPanel();
initDOMInspectorFixed();
updateLogsDisplay();
// Periodic cleanup
setInterval(() => {
cleanupOldData();
if (CONFIG.persistData) saveData();
}, CONFIG.cleanupInterval);
});
} else {
createFloatingPanel();
initDOMInspectorFixed();
updateLogsDisplay();
setInterval(() => {
cleanupOldData();
if (CONFIG.persistData) saveData();
}, CONFIG.cleanupInterval);
}
// Save on unload
window.addEventListener('beforeunload', () => saveData());
}
init();
})();