Parv Ashwani 5 mēneši atpakaļ
revīzija
cdaf45333a
2 mainītis faili ar 613 papildinājumiem un 0 dzēšanām
  1. BIN
      favicon.png
  2. 613 0
      index.html

BIN
favicon.png


+ 613 - 0
index.html

@@ -0,0 +1,613 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Harshark - Parv's HAR Parser</title>
+	<link rel="icon" type="image/png" href="./favicon.png" />
+
+    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
+    <meta name="theme-color" content="#3b82f6">
+    <meta name="description" content="Simple, Online HTTP Archive (HAR) parser">
+    
+    <!-- PWA Manifest Placeholder (Data URI) -->
+    <link rel="manifest" id="manifest-placeholder">
+
+    <style>
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+        }
+        .drag-active {
+            border-color: #3b82f6;
+            background-color: #eff6ff;
+        }
+        /* Custom scrollbar for details panel */
+        .details-panel::-webkit-scrollbar {
+            width: 8px;
+        }
+        .details-panel::-webkit-scrollbar-track {
+            background: #f1f1f1;
+        }
+        .details-panel::-webkit-scrollbar-thumb {
+            background: #cbd5e1;
+            border-radius: 4px;
+        }
+        .details-panel::-webkit-scrollbar-thumb:hover {
+            background: #94a3b8;
+        }
+        .truncate-text {
+            max-width: 200px;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    </style>
+</head>
+<body class="bg-gray-50 h-screen flex flex-col overflow-hidden">
+
+    <!-- Header -->
+    <header class="bg-blue-600 text-white shadow-md z-10">
+        <div class="container mx-auto px-4 py-3 flex justify-between items-center">
+            <div class="flex items-center space-x-2">
+                <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
+                </svg>
+                <h1 class="text-xl font-bold">Harshark</h1>
+            </div>
+            <div class="flex items-center space-x-4">
+                <button id="installBtn" class="px-3 py-1 bg-white text-blue-600 hover:bg-blue-50 rounded text-sm font-bold hidden shadow-sm transition-colors flex items-center gap-1">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
+                    </svg>
+                    Install
+                </button>
+                <button id="resetBtn" class="px-3 py-1 bg-blue-700 hover:bg-blue-800 rounded text-sm hidden">Reset</button>
+                <div class="text-xs opacity-75">Build By Parv Ashwani</div>
+            </div>
+        </div>
+    </header>
+
+    <!-- Main Content -->
+    <main class="flex-1 flex overflow-hidden relative">
+        
+        <!-- Dropzone (Initial State) -->
+        <div id="dropzone" class="absolute inset-0 flex flex-col items-center justify-center bg-white z-20 transition-all duration-300">
+            <div class="border-4 border-dashed border-gray-300 rounded-xl p-12 text-center max-w-lg mx-4 hover:border-blue-400 transition-colors cursor-pointer" id="dropzone-border">
+                <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
+                </svg>
+                <h2 class="text-2xl font-bold text-gray-700 mb-2">Drop HAR or XML file here</h2>
+                <p class="text-gray-500 mb-6">or click to browse</p>
+                <input type="file" id="fileInput" class="hidden" accept=".har,.json,.xml">
+                <button class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition">Select File</button>
+                <p class="mt-4 text-xs text-gray-400">Supports Chrome, Firefox, Safari, Edge, and legacy IE XML logs</p>
+            </div>
+        </div>
+
+        <!-- Split View (Hidden initially) -->
+        <div id="app-view" class="w-full h-full flex hidden">
+            
+            <!-- List View -->
+            <div class="flex-1 flex flex-col min-w-0 border-r border-gray-200 bg-white">
+                <!-- Search/Filter Bar -->
+                <div class="p-2 border-b border-gray-200 flex space-x-2 bg-gray-50">
+                    <input type="text" id="searchInput" placeholder="Filter by URL..." class="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:border-blue-500">
+                    <select id="methodFilter" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:border-blue-500 bg-white">
+                        <option value="ALL">All Methods</option>
+                        <option value="GET">GET</option>
+                        <option value="POST">POST</option>
+                        <option value="PUT">PUT</option>
+                        <option value="DELETE">DELETE</option>
+                        <option value="OPTIONS">OPTIONS</option>
+                    </select>
+                </div>
+
+                <!-- Table Header -->
+                <div class="grid grid-cols-12 bg-gray-100 border-b border-gray-200 text-xs font-semibold text-gray-600 py-2 px-4">
+                    <div class="col-span-1">Status</div>
+                    <div class="col-span-1">Method</div>
+                    <div class="col-span-6">URL</div>
+                    <div class="col-span-2 text-right">Size</div>
+                    <div class="col-span-2 text-right">Time</div>
+                </div>
+
+                <!-- Table Body -->
+                <div id="entriesList" class="flex-1 overflow-y-auto divide-y divide-gray-100">
+                    <!-- Entries will be injected here -->
+                </div>
+                
+                <!-- Footer Status -->
+                <div class="p-2 border-t border-gray-200 text-xs text-gray-500 bg-gray-50 flex justify-between">
+                    <span id="entryCount">0 entries</span>
+                    <span id="totalSize">0 KB</span>
+                    <span id="totalTime">0 ms</span>
+                </div>
+            </div>
+
+            <!-- Detail View -->
+            <div id="detailsPanel" class="w-1/3 bg-gray-50 border-l border-gray-200 flex flex-col hidden details-panel overflow-y-auto shadow-xl z-30">
+                <div class="sticky top-0 bg-white border-b border-gray-200 p-4 flex justify-between items-start shadow-sm">
+                    <h3 class="font-bold text-gray-800 text-lg">Details</h3>
+                    <button id="closeDetails" class="text-gray-400 hover:text-gray-600">
+                        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
+                        </svg>
+                    </button>
+                </div>
+                
+                <div id="detailsContent" class="p-4 space-y-6">
+                    <div class="text-center text-gray-500 mt-10">Select an entry to view details</div>
+                </div>
+            </div>
+
+        </div>
+    </main>
+
+    <!-- XML Parser Helper (Invisible) -->
+    <div id="xml-helper" style="display:none;"></div>
+
+    <script>
+        // --- PWA Setup via Data URI ---
+        const manifest = {
+            "name": "Harshark",
+            "short_name": "Harshark",
+            "start_url": ".",
+            "display": "standalone",
+            "background_color": "#ffffff",
+            "theme_color": "#3b82f6",
+            "icons": [
+                {
+                    "src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%233b82f6'%3E%3Cpath d='M13 10V3L4 14h7v7l9-11h-7z'/%3E%3C/svg%3E",
+                    "sizes": "192x192",
+                    "type": "image/svg+xml"
+                }
+            ]
+        };
+        const manifestBlob = new Blob([JSON.stringify(manifest)], {type: 'application/manifest+json'});
+        document.getElementById('manifest-placeholder').href = URL.createObjectURL(manifestBlob);
+
+        // --- Application State ---
+        let entries = [];
+        let filteredEntries = [];
+        let isXmlMode = false;
+
+        // --- DOM Elements ---
+        const dropzone = document.getElementById('dropzone');
+        const dropzoneBorder = document.getElementById('dropzone-border');
+        const fileInput = document.getElementById('fileInput');
+        const appView = document.getElementById('app-view');
+        const entriesList = document.getElementById('entriesList');
+        const detailsPanel = document.getElementById('detailsPanel');
+        const detailsContent = document.getElementById('detailsContent');
+        const closeDetailsBtn = document.getElementById('closeDetails');
+        const searchInput = document.getElementById('searchInput');
+        const methodFilter = document.getElementById('methodFilter');
+        const resetBtn = document.getElementById('resetBtn');
+        const entryCountEl = document.getElementById('entryCount');
+        const totalSizeEl = document.getElementById('totalSize');
+        const totalTimeEl = document.getElementById('totalTime');
+        const installBtn = document.getElementById('installBtn');
+
+        // --- PWA Install Logic ---
+        let deferredPrompt;
+
+        window.addEventListener('beforeinstallprompt', (e) => {
+            // Prevent Chrome 67 and earlier from automatically showing the prompt
+            e.preventDefault();
+            // Stash the event so it can be triggered later.
+            deferredPrompt = e;
+            // Update UI to notify the user they can add to home screen
+            installBtn.classList.remove('hidden');
+        });
+
+        installBtn.addEventListener('click', (e) => {
+            // Hide our user interface that shows our A2HS button
+            installBtn.classList.add('hidden');
+            // Show the prompt
+            deferredPrompt.prompt();
+            // Wait for the user to respond to the prompt
+            deferredPrompt.userChoice.then((choiceResult) => {
+                if (choiceResult.outcome === 'accepted') {
+                    console.log('User accepted the A2HS prompt');
+                } else {
+                    console.log('User dismissed the A2HS prompt');
+                }
+                deferredPrompt = null;
+            });
+        });
+
+        window.addEventListener('appinstalled', () => {
+            // Hide the app-provided install promotion
+            installBtn.classList.add('hidden');
+            deferredPrompt = null;
+            console.log('PWA was installed');
+        });
+
+        // --- Event Listeners ---
+        
+        // Drag & Drop
+        dropzone.addEventListener('click', () => fileInput.click());
+        dropzone.addEventListener('dragover', (e) => {
+            e.preventDefault();
+            dropzoneBorder.classList.add('drag-active');
+        });
+        dropzone.addEventListener('dragleave', () => {
+            dropzoneBorder.classList.remove('drag-active');
+        });
+        dropzone.addEventListener('drop', (e) => {
+            e.preventDefault();
+            dropzoneBorder.classList.remove('drag-active');
+            if (e.dataTransfer.files.length) {
+                handleFile(e.dataTransfer.files[0]);
+            }
+        });
+
+        fileInput.addEventListener('change', (e) => {
+            if (e.target.files.length) {
+                handleFile(e.target.files[0]);
+            }
+        });
+
+        resetBtn.addEventListener('click', () => {
+            entries = [];
+            filteredEntries = [];
+            renderEntries();
+            dropzone.classList.remove('hidden');
+            appView.classList.add('hidden');
+            resetBtn.classList.add('hidden');
+            detailsPanel.classList.add('hidden');
+        });
+
+        closeDetailsBtn.addEventListener('click', () => {
+            detailsPanel.classList.add('hidden');
+        });
+
+        searchInput.addEventListener('input', filterEntries);
+        methodFilter.addEventListener('change', filterEntries);
+
+        // --- File Handling Logic ---
+
+        function handleFile(file) {
+            const reader = new FileReader();
+            reader.onload = (e) => {
+                const content = e.target.result;
+                try {
+                    // Try JSON first
+                    const json = JSON.parse(content);
+                    if (json.log && json.log.entries) {
+                        isXmlMode = false;
+                        entries = json.log.entries.map((entry, index) => ({
+                            id: index,
+                            method: entry.request.method,
+                            url: entry.request.url,
+                            status: entry.response.status,
+                            statusText: entry.response.statusText,
+                            mimeType: entry.response.content.mimeType,
+                            size: entry.response.content.size,
+                            time: entry.time,
+                            request: entry.request,
+                            response: entry.response,
+                            startedDateTime: entry.startedDateTime
+                        }));
+                        showApp();
+                    } else {
+                        // Not a standard HAR, maybe different JSON structure?
+                        throw new Error("Invalid HAR structure");
+                    }
+                } catch (err) {
+                    // If JSON parsing fails, try XML
+                    try {
+                        const parser = new DOMParser();
+                        const xmlDoc = parser.parseFromString(content, "text/xml");
+                        if (xmlDoc.getElementsByTagName("parsererror").length > 0) {
+                            throw new Error("Invalid XML");
+                        }
+                        parseXML(xmlDoc);
+                        showApp();
+                    } catch (xmlErr) {
+                        alert("Could not parse file. It must be a valid HAR (JSON) or supported XML network log.");
+                        console.error(xmlErr);
+                    }
+                }
+            };
+            reader.readAsText(file);
+        }
+
+        function parseXML(xmlDoc) {
+            // Basic XML parsing logic trying to find common network log patterns
+            // IE Developer Tools Network Data often exports as XML
+            // We look for common tags
+            isXmlMode = true;
+            entries = [];
+            
+            // Try to find list of requests
+            // Common structure in some IE exports: <xml><entry>...</entry></xml> or similar
+            // This is a heuristic approach
+            const xmlEntries = xmlDoc.querySelectorAll("entry, request, item, log > *"); 
+            
+            if (xmlEntries.length === 0) {
+                 // Fallback: just dump the top level children
+                 const children = xmlDoc.documentElement.children;
+                 for (let i = 0; i < children.length; i++) {
+                     processXmlNode(children[i], i);
+                 }
+            } else {
+                xmlEntries.forEach((node, index) => {
+                    processXmlNode(node, index);
+                });
+            }
+
+            if (entries.length === 0) {
+                alert("XML parsed but no recognizable entries found.");
+            }
+        }
+
+        function processXmlNode(node, index) {
+            // Attempt to extract method, url, status from attributes or child tags
+            const url = node.getAttribute("url") || node.querySelector("url")?.textContent || "Unknown URL";
+            const method = node.getAttribute("method") || node.querySelector("method")?.textContent || "GET"; // Default to GET if missing
+            const status = node.getAttribute("status") || node.querySelector("status")?.textContent || "0";
+            const size = node.getAttribute("size") || node.querySelector("size")?.textContent || "0";
+            const time = node.getAttribute("time") || node.querySelector("time")?.textContent || "0";
+            
+            // Store raw node for details
+            entries.push({
+                id: index,
+                method: method,
+                url: url,
+                status: parseInt(status),
+                statusText: "",
+                size: parseInt(size),
+                time: parseFloat(time),
+                rawXml: new XMLSerializer().serializeToString(node),
+                originalNode: node
+            });
+        }
+
+        function showApp() {
+            dropzone.classList.add('hidden');
+            appView.classList.remove('hidden');
+            resetBtn.classList.remove('hidden');
+            filterEntries();
+        }
+
+        // --- Rendering Logic ---
+
+        function filterEntries() {
+            const term = searchInput.value.toLowerCase();
+            const method = methodFilter.value;
+
+            filteredEntries = entries.filter(e => {
+                const matchesTerm = e.url.toLowerCase().includes(term);
+                const matchesMethod = method === "ALL" || e.method === method;
+                return matchesTerm && matchesMethod;
+            });
+
+            renderEntries();
+            updateStats();
+        }
+
+        function renderEntries() {
+            entriesList.innerHTML = '';
+            filteredEntries.forEach(entry => {
+                const row = document.createElement('div');
+                row.className = 'grid grid-cols-12 border-b border-gray-100 py-2 px-4 hover:bg-blue-50 cursor-pointer items-center text-sm';
+                row.onclick = () => showDetails(entry);
+
+                // Status Color
+                let statusColor = 'text-gray-600';
+                if (entry.status >= 200 && entry.status < 300) statusColor = 'text-green-600';
+                else if (entry.status >= 300 && entry.status < 400) statusColor = 'text-yellow-600';
+                else if (entry.status >= 400) statusColor = 'text-red-600';
+
+                // Format Size
+                const sizeStr = formatBytes(entry.size);
+
+                // Format Time
+                const timeStr = Math.round(entry.time) + ' ms';
+
+                row.innerHTML = `
+                    <div class="col-span-1 ${statusColor} font-mono">${entry.status || '-'}</div>
+                    <div class="col-span-1 font-bold text-gray-700 text-xs">${entry.method}</div>
+                    <div class="col-span-6 truncate-text text-gray-800" title="${entry.url}">${entry.url}</div>
+                    <div class="col-span-2 text-right text-gray-500 text-xs">${sizeStr}</div>
+                    <div class="col-span-2 text-right text-gray-500 text-xs">${timeStr}</div>
+                `;
+                entriesList.appendChild(row);
+            });
+        }
+
+        function showDetails(entry) {
+            detailsPanel.classList.remove('hidden');
+            
+            if (isXmlMode) {
+                renderXmlDetails(entry);
+            } else {
+                renderHarDetails(entry);
+            }
+        }
+
+        function renderHarDetails(entry) {
+            // General Info
+            let html = `
+                <div class="bg-white p-3 rounded shadow-sm border border-gray-100">
+                    <div class="text-xs font-bold text-gray-500 uppercase mb-1">General</div>
+                    <div class="mb-1"><span class="font-semibold">Request URL:</span> <span class="break-all text-sm">${entry.url}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Request Method:</span> <span class="text-sm">${entry.method}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Status Code:</span> <span class="text-sm">${entry.status} ${entry.statusText}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Time:</span> <span class="text-sm">${entry.time.toFixed(2)} ms</span></div>
+                    <div class="mb-1"><span class="font-semibold">Started At:</span> <span class="text-sm">${new Date(entry.startedDateTime).toLocaleString()}</span></div>
+                </div>
+            `;
+
+            // Request Headers
+            html += renderHeadersSection("Request Headers", entry.request.headers);
+            
+            // Response Headers
+            html += renderHeadersSection("Response Headers", entry.response.headers);
+
+            // Request Content (if any)
+            if (entry.request.postData && entry.request.postData.text) {
+                const contentText = entry.request.postData.text;
+                html += createContentSection("Request Body", contentText, entry.request.postData.mimeType);
+            }
+
+            // Response Content
+            if (entry.response.content && entry.response.content.text) {
+                const contentText = entry.response.content.text;
+                html += createContentSection(`Response Body (${formatBytes(entry.response.content.size)})`, contentText, entry.response.content.mimeType);
+            } else if (entry.response.content && entry.response.content.size > 0) {
+                 html += createContentSection(`Response Body (${formatBytes(entry.response.content.size)})`, "(Binary data or content not available)", entry.response.content.mimeType);
+            }
+
+            detailsContent.innerHTML = html;
+            
+            // Re-attach event listeners for copy buttons if any
+            document.querySelectorAll('.copy-btn').forEach(btn => {
+                btn.addEventListener('click', (e) => {
+                    const text = decodeURIComponent(e.target.dataset.content);
+                    navigator.clipboard.writeText(text).then(() => {
+                        const originalText = e.target.textContent;
+                        e.target.textContent = 'Copied!';
+                        setTimeout(() => e.target.textContent = originalText, 1500);
+                    });
+                });
+            });
+        }
+
+        function createContentSection(title, content, mimeType) {
+            let displayContent = escapeHtml(content);
+            let isJson = false;
+            
+            // Try to pretty print JSON
+            if (mimeType && (mimeType.includes('json') || mimeType.includes('javascript'))) {
+                try {
+                    const obj = JSON.parse(content);
+                    displayContent = escapeHtml(JSON.stringify(obj, null, 2));
+                    isJson = true;
+                } catch(e) {}
+            }
+
+            return `
+                <div class="bg-white p-3 rounded shadow-sm border border-gray-100 mt-4 group">
+                    <div class="flex justify-between items-center mb-2">
+                        <div class="text-xs font-bold text-gray-500 uppercase">${title}</div>
+                        <button class="copy-btn text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100 transition opacity-0 group-hover:opacity-100" data-content="${encodeURIComponent(content)}">Copy</button>
+                    </div>
+                    ${mimeType ? `<div class="text-xs text-gray-500 mb-2">Mime-Type: ${mimeType}</div>` : ''}
+                    <div class="bg-gray-50 p-2 rounded text-xs font-mono overflow-auto max-h-60 whitespace-pre-wrap break-all ${isJson ? 'text-green-700' : ''}">${displayContent}</div>
+                </div>
+            `;
+        }
+
+        function renderXmlDetails(entry) {
+            // Pretty print XML
+            let formattedXml = entry.rawXml;
+            try {
+                // Simple indentation for XML
+                const PADDING = '  ';
+                const reg = /(>)(<)(\/*)/g;
+                let pad = 0;
+                formattedXml = formattedXml.replace(reg, '$1\r\n$2$3');
+                formattedXml = formattedXml.split('\r\n').map(node => {
+                    let indent = 0;
+                    if (node.match(/.+<\/\w[^>]*>$/)) {
+                        indent = 0;
+                    } else if (node.match(/^<\/\w/)) {
+                        if (pad !== 0) pad -= 1;
+                    } else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
+                        indent = 1;
+                    } else {
+                        indent = 0;
+                    }
+                    const padding = new Array(pad + 1).join(PADDING);
+                    if (indent > 0) pad += 1;
+                    return padding + node;
+                }).join('\r\n');
+            } catch(e) {
+                // fallback to raw if formatting fails
+                formattedXml = entry.rawXml;
+            }
+
+            detailsContent.innerHTML = `
+                <div class="bg-white p-3 rounded shadow-sm border border-gray-100">
+                    <div class="text-xs font-bold text-gray-500 uppercase mb-1">General Info</div>
+                    <div class="mb-1"><span class="font-semibold">URL:</span> <span class="break-all text-sm">${entry.url}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Method:</span> <span class="text-sm">${entry.method}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Status:</span> <span class="text-sm">${entry.status}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Size:</span> <span class="text-sm">${formatBytes(entry.size)}</span></div>
+                    <div class="mb-1"><span class="font-semibold">Time:</span> <span class="text-sm">${entry.time} ms</span></div>
+                </div>
+                <div class="bg-white p-3 rounded shadow-sm border border-gray-100 mt-4 group">
+                    <div class="flex justify-between items-center mb-2">
+                        <div class="text-xs font-bold text-gray-500 uppercase">Raw XML Element</div>
+                        <button class="copy-btn text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100 transition opacity-0 group-hover:opacity-100" data-content="${encodeURIComponent(formattedXml)}">Copy</button>
+                    </div>
+                    <pre class="bg-gray-50 p-2 rounded text-xs font-mono overflow-auto max-h-96 whitespace-pre-wrap text-blue-900">${escapeHtml(formattedXml)}</pre>
+                </div>
+            `;
+
+            document.querySelectorAll('.copy-btn').forEach(btn => {
+                btn.addEventListener('click', (e) => {
+                    const text = decodeURIComponent(e.target.dataset.content);
+                    navigator.clipboard.writeText(text).then(() => {
+                        const originalText = e.target.textContent;
+                        e.target.textContent = 'Copied!';
+                        setTimeout(() => e.target.textContent = originalText, 1500);
+                    });
+                });
+            });
+        }
+
+        function renderHeadersSection(title, headers) {
+            if (!headers || headers.length === 0) return '';
+            
+            let items = headers.map(h => `
+                <div class="grid grid-cols-3 gap-2 py-1 border-b border-gray-50 last:border-0">
+                    <div class="col-span-1 font-semibold text-gray-600 truncate text-xs" title="${h.name}">${h.name}</div>
+                    <div class="col-span-2 text-gray-800 break-all text-xs">${escapeHtml(h.value)}</div>
+                </div>
+            `).join('');
+
+            return `
+                <div class="bg-white p-3 rounded shadow-sm border border-gray-100 mt-4">
+                    <div class="text-xs font-bold text-gray-500 uppercase mb-2">${title}</div>
+                    ${items}
+                </div>
+            `;
+        }
+
+        // --- Helpers ---
+
+        function updateStats() {
+            entryCountEl.textContent = `${filteredEntries.length} entries`;
+            
+            let size = filteredEntries.reduce((acc, curr) => acc + (curr.size || 0), 0);
+            totalSizeEl.textContent = formatBytes(size);
+
+            let time = filteredEntries.reduce((acc, curr) => acc + (curr.time || 0), 0);
+            totalTimeEl.textContent = Math.round(time) + ' ms';
+        }
+
+        function formatBytes(bytes, decimals = 2) {
+            if (!+bytes) return '0 Bytes';
+            const k = 1024;
+            const dm = decimals < 0 ? 0 : decimals;
+            const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+            const i = Math.floor(Math.log(bytes) / Math.log(k));
+            return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
+        }
+
+        function escapeHtml(text) {
+            if (!text) return '';
+            return text
+                .replace(/&/g, "&amp;")
+                .replace(/</g, "&lt;")
+                .replace(/>/g, "&gt;")
+                .replace(/"/g, "&quot;")
+                .replace(/'/g, "&#039;");
+        }
+        
+    </script>
+</body>
+</html>