index.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Harshark - Parv's HAR Parser</title>
  7. <link rel="icon" type="image/png" href="./favicon.png" />
  8. <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
  9. <meta name="theme-color" content="#3b82f6">
  10. <meta name="description" content="Simple, Online HTTP Archive (HAR) parser">
  11. <!-- PWA Manifest Placeholder (Data URI) -->
  12. <link rel="manifest" id="manifest-placeholder">
  13. <style>
  14. body {
  15. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  16. }
  17. .drag-active {
  18. border-color: #3b82f6;
  19. background-color: #eff6ff;
  20. }
  21. /* Custom scrollbar for details panel */
  22. .details-panel::-webkit-scrollbar {
  23. width: 8px;
  24. }
  25. .details-panel::-webkit-scrollbar-track {
  26. background: #f1f1f1;
  27. }
  28. .details-panel::-webkit-scrollbar-thumb {
  29. background: #cbd5e1;
  30. border-radius: 4px;
  31. }
  32. .details-panel::-webkit-scrollbar-thumb:hover {
  33. background: #94a3b8;
  34. }
  35. .truncate-text {
  36. max-width: 200px;
  37. white-space: nowrap;
  38. overflow: hidden;
  39. text-overflow: ellipsis;
  40. }
  41. </style>
  42. </head>
  43. <body class="bg-gray-50 h-screen flex flex-col overflow-hidden">
  44. <!-- Header -->
  45. <header class="bg-blue-600 text-white shadow-md z-10">
  46. <div class="container mx-auto px-4 py-3 flex justify-between items-center">
  47. <div class="flex items-center space-x-2">
  48. <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  49. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
  50. </svg>
  51. <h1 class="text-xl font-bold">Harshark</h1>
  52. </div>
  53. <div class="flex items-center space-x-4">
  54. <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">
  55. <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  56. <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" />
  57. </svg>
  58. Install
  59. </button>
  60. <button id="resetBtn" class="px-3 py-1 bg-blue-700 hover:bg-blue-800 rounded text-sm hidden">Reset</button>
  61. <div class="text-xs opacity-75">Build By Parv Ashwani</div>
  62. </div>
  63. </div>
  64. </header>
  65. <!-- Main Content -->
  66. <main class="flex-1 flex overflow-hidden relative">
  67. <!-- Dropzone (Initial State) -->
  68. <div id="dropzone" class="absolute inset-0 flex flex-col items-center justify-center bg-white z-20 transition-all duration-300">
  69. <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">
  70. <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">
  71. <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" />
  72. </svg>
  73. <h2 class="text-2xl font-bold text-gray-700 mb-2">Drop HAR or XML file here</h2>
  74. <p class="text-gray-500 mb-6">or click to browse</p>
  75. <input type="file" id="fileInput" class="hidden" accept=".har,.json,.xml">
  76. <button class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition">Select File</button>
  77. <p class="mt-4 text-xs text-gray-400">Supports Chrome, Firefox, Safari, Edge, and legacy IE XML logs</p>
  78. </div>
  79. </div>
  80. <!-- Split View (Hidden initially) -->
  81. <div id="app-view" class="w-full h-full flex hidden">
  82. <!-- List View -->
  83. <div class="flex-1 flex flex-col min-w-0 border-r border-gray-200 bg-white">
  84. <!-- Search/Filter Bar -->
  85. <div class="p-2 border-b border-gray-200 flex space-x-2 bg-gray-50">
  86. <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">
  87. <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">
  88. <option value="ALL">All Methods</option>
  89. <option value="GET">GET</option>
  90. <option value="POST">POST</option>
  91. <option value="PUT">PUT</option>
  92. <option value="DELETE">DELETE</option>
  93. <option value="OPTIONS">OPTIONS</option>
  94. </select>
  95. </div>
  96. <!-- Table Header -->
  97. <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">
  98. <div class="col-span-1">Status</div>
  99. <div class="col-span-1">Method</div>
  100. <div class="col-span-6">URL</div>
  101. <div class="col-span-2 text-right">Size</div>
  102. <div class="col-span-2 text-right">Time</div>
  103. </div>
  104. <!-- Table Body -->
  105. <div id="entriesList" class="flex-1 overflow-y-auto divide-y divide-gray-100">
  106. <!-- Entries will be injected here -->
  107. </div>
  108. <!-- Footer Status -->
  109. <div class="p-2 border-t border-gray-200 text-xs text-gray-500 bg-gray-50 flex justify-between">
  110. <span id="entryCount">0 entries</span>
  111. <span id="totalSize">0 KB</span>
  112. <span id="totalTime">0 ms</span>
  113. </div>
  114. </div>
  115. <!-- Detail View -->
  116. <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">
  117. <div class="sticky top-0 bg-white border-b border-gray-200 p-4 flex justify-between items-start shadow-sm">
  118. <h3 class="font-bold text-gray-800 text-lg">Details</h3>
  119. <button id="closeDetails" class="text-gray-400 hover:text-gray-600">
  120. <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  121. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
  122. </svg>
  123. </button>
  124. </div>
  125. <div id="detailsContent" class="p-4 space-y-6">
  126. <div class="text-center text-gray-500 mt-10">Select an entry to view details</div>
  127. </div>
  128. </div>
  129. </div>
  130. </main>
  131. <!-- XML Parser Helper (Invisible) -->
  132. <div id="xml-helper" style="display:none;"></div>
  133. <script>
  134. // --- PWA Setup via Data URI ---
  135. const manifest = {
  136. "name": "Harshark",
  137. "short_name": "Harshark",
  138. "start_url": ".",
  139. "display": "standalone",
  140. "background_color": "#ffffff",
  141. "theme_color": "#3b82f6",
  142. "icons": [
  143. {
  144. "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",
  145. "sizes": "192x192",
  146. "type": "image/svg+xml"
  147. }
  148. ]
  149. };
  150. const manifestBlob = new Blob([JSON.stringify(manifest)], {type: 'application/manifest+json'});
  151. document.getElementById('manifest-placeholder').href = URL.createObjectURL(manifestBlob);
  152. // --- Application State ---
  153. let entries = [];
  154. let filteredEntries = [];
  155. let isXmlMode = false;
  156. // --- DOM Elements ---
  157. const dropzone = document.getElementById('dropzone');
  158. const dropzoneBorder = document.getElementById('dropzone-border');
  159. const fileInput = document.getElementById('fileInput');
  160. const appView = document.getElementById('app-view');
  161. const entriesList = document.getElementById('entriesList');
  162. const detailsPanel = document.getElementById('detailsPanel');
  163. const detailsContent = document.getElementById('detailsContent');
  164. const closeDetailsBtn = document.getElementById('closeDetails');
  165. const searchInput = document.getElementById('searchInput');
  166. const methodFilter = document.getElementById('methodFilter');
  167. const resetBtn = document.getElementById('resetBtn');
  168. const entryCountEl = document.getElementById('entryCount');
  169. const totalSizeEl = document.getElementById('totalSize');
  170. const totalTimeEl = document.getElementById('totalTime');
  171. const installBtn = document.getElementById('installBtn');
  172. // --- PWA Install Logic ---
  173. let deferredPrompt;
  174. window.addEventListener('beforeinstallprompt', (e) => {
  175. // Prevent Chrome 67 and earlier from automatically showing the prompt
  176. e.preventDefault();
  177. // Stash the event so it can be triggered later.
  178. deferredPrompt = e;
  179. // Update UI to notify the user they can add to home screen
  180. installBtn.classList.remove('hidden');
  181. });
  182. installBtn.addEventListener('click', (e) => {
  183. // Hide our user interface that shows our A2HS button
  184. installBtn.classList.add('hidden');
  185. // Show the prompt
  186. deferredPrompt.prompt();
  187. // Wait for the user to respond to the prompt
  188. deferredPrompt.userChoice.then((choiceResult) => {
  189. if (choiceResult.outcome === 'accepted') {
  190. console.log('User accepted the A2HS prompt');
  191. } else {
  192. console.log('User dismissed the A2HS prompt');
  193. }
  194. deferredPrompt = null;
  195. });
  196. });
  197. window.addEventListener('appinstalled', () => {
  198. // Hide the app-provided install promotion
  199. installBtn.classList.add('hidden');
  200. deferredPrompt = null;
  201. console.log('PWA was installed');
  202. });
  203. // --- Event Listeners ---
  204. // Drag & Drop
  205. dropzone.addEventListener('click', () => fileInput.click());
  206. dropzone.addEventListener('dragover', (e) => {
  207. e.preventDefault();
  208. dropzoneBorder.classList.add('drag-active');
  209. });
  210. dropzone.addEventListener('dragleave', () => {
  211. dropzoneBorder.classList.remove('drag-active');
  212. });
  213. dropzone.addEventListener('drop', (e) => {
  214. e.preventDefault();
  215. dropzoneBorder.classList.remove('drag-active');
  216. if (e.dataTransfer.files.length) {
  217. handleFile(e.dataTransfer.files[0]);
  218. }
  219. });
  220. fileInput.addEventListener('change', (e) => {
  221. if (e.target.files.length) {
  222. handleFile(e.target.files[0]);
  223. }
  224. });
  225. resetBtn.addEventListener('click', () => {
  226. entries = [];
  227. filteredEntries = [];
  228. renderEntries();
  229. dropzone.classList.remove('hidden');
  230. appView.classList.add('hidden');
  231. resetBtn.classList.add('hidden');
  232. detailsPanel.classList.add('hidden');
  233. });
  234. closeDetailsBtn.addEventListener('click', () => {
  235. detailsPanel.classList.add('hidden');
  236. });
  237. searchInput.addEventListener('input', filterEntries);
  238. methodFilter.addEventListener('change', filterEntries);
  239. // --- File Handling Logic ---
  240. function handleFile(file) {
  241. const reader = new FileReader();
  242. reader.onload = (e) => {
  243. const content = e.target.result;
  244. try {
  245. // Try JSON first
  246. const json = JSON.parse(content);
  247. if (json.log && json.log.entries) {
  248. isXmlMode = false;
  249. entries = json.log.entries.map((entry, index) => ({
  250. id: index,
  251. method: entry.request.method,
  252. url: entry.request.url,
  253. status: entry.response.status,
  254. statusText: entry.response.statusText,
  255. mimeType: entry.response.content.mimeType,
  256. size: entry.response.content.size,
  257. time: entry.time,
  258. request: entry.request,
  259. response: entry.response,
  260. startedDateTime: entry.startedDateTime
  261. }));
  262. showApp();
  263. } else {
  264. // Not a standard HAR, maybe different JSON structure?
  265. throw new Error("Invalid HAR structure");
  266. }
  267. } catch (err) {
  268. // If JSON parsing fails, try XML
  269. try {
  270. const parser = new DOMParser();
  271. const xmlDoc = parser.parseFromString(content, "text/xml");
  272. if (xmlDoc.getElementsByTagName("parsererror").length > 0) {
  273. throw new Error("Invalid XML");
  274. }
  275. parseXML(xmlDoc);
  276. showApp();
  277. } catch (xmlErr) {
  278. alert("Could not parse file. It must be a valid HAR (JSON) or supported XML network log.");
  279. console.error(xmlErr);
  280. }
  281. }
  282. };
  283. reader.readAsText(file);
  284. }
  285. function parseXML(xmlDoc) {
  286. // Basic XML parsing logic trying to find common network log patterns
  287. // IE Developer Tools Network Data often exports as XML
  288. // We look for common tags
  289. isXmlMode = true;
  290. entries = [];
  291. // Try to find list of requests
  292. // Common structure in some IE exports: <xml><entry>...</entry></xml> or similar
  293. // This is a heuristic approach
  294. const xmlEntries = xmlDoc.querySelectorAll("entry, request, item, log > *");
  295. if (xmlEntries.length === 0) {
  296. // Fallback: just dump the top level children
  297. const children = xmlDoc.documentElement.children;
  298. for (let i = 0; i < children.length; i++) {
  299. processXmlNode(children[i], i);
  300. }
  301. } else {
  302. xmlEntries.forEach((node, index) => {
  303. processXmlNode(node, index);
  304. });
  305. }
  306. if (entries.length === 0) {
  307. alert("XML parsed but no recognizable entries found.");
  308. }
  309. }
  310. function processXmlNode(node, index) {
  311. // Attempt to extract method, url, status from attributes or child tags
  312. const url = node.getAttribute("url") || node.querySelector("url")?.textContent || "Unknown URL";
  313. const method = node.getAttribute("method") || node.querySelector("method")?.textContent || "GET"; // Default to GET if missing
  314. const status = node.getAttribute("status") || node.querySelector("status")?.textContent || "0";
  315. const size = node.getAttribute("size") || node.querySelector("size")?.textContent || "0";
  316. const time = node.getAttribute("time") || node.querySelector("time")?.textContent || "0";
  317. // Store raw node for details
  318. entries.push({
  319. id: index,
  320. method: method,
  321. url: url,
  322. status: parseInt(status),
  323. statusText: "",
  324. size: parseInt(size),
  325. time: parseFloat(time),
  326. rawXml: new XMLSerializer().serializeToString(node),
  327. originalNode: node
  328. });
  329. }
  330. function showApp() {
  331. dropzone.classList.add('hidden');
  332. appView.classList.remove('hidden');
  333. resetBtn.classList.remove('hidden');
  334. filterEntries();
  335. }
  336. // --- Rendering Logic ---
  337. function filterEntries() {
  338. const term = searchInput.value.toLowerCase();
  339. const method = methodFilter.value;
  340. filteredEntries = entries.filter(e => {
  341. const matchesTerm = e.url.toLowerCase().includes(term);
  342. const matchesMethod = method === "ALL" || e.method === method;
  343. return matchesTerm && matchesMethod;
  344. });
  345. renderEntries();
  346. updateStats();
  347. }
  348. function renderEntries() {
  349. entriesList.innerHTML = '';
  350. filteredEntries.forEach(entry => {
  351. const row = document.createElement('div');
  352. 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';
  353. row.onclick = () => showDetails(entry);
  354. // Status Color
  355. let statusColor = 'text-gray-600';
  356. if (entry.status >= 200 && entry.status < 300) statusColor = 'text-green-600';
  357. else if (entry.status >= 300 && entry.status < 400) statusColor = 'text-yellow-600';
  358. else if (entry.status >= 400) statusColor = 'text-red-600';
  359. // Format Size
  360. const sizeStr = formatBytes(entry.size);
  361. // Format Time
  362. const timeStr = Math.round(entry.time) + ' ms';
  363. row.innerHTML = `
  364. <div class="col-span-1 ${statusColor} font-mono">${entry.status || '-'}</div>
  365. <div class="col-span-1 font-bold text-gray-700 text-xs">${entry.method}</div>
  366. <div class="col-span-6 truncate-text text-gray-800" title="${entry.url}">${entry.url}</div>
  367. <div class="col-span-2 text-right text-gray-500 text-xs">${sizeStr}</div>
  368. <div class="col-span-2 text-right text-gray-500 text-xs">${timeStr}</div>
  369. `;
  370. entriesList.appendChild(row);
  371. });
  372. }
  373. function showDetails(entry) {
  374. detailsPanel.classList.remove('hidden');
  375. if (isXmlMode) {
  376. renderXmlDetails(entry);
  377. } else {
  378. renderHarDetails(entry);
  379. }
  380. }
  381. function renderHarDetails(entry) {
  382. // General Info
  383. let html = `
  384. <div class="bg-white p-3 rounded shadow-sm border border-gray-100">
  385. <div class="text-xs font-bold text-gray-500 uppercase mb-1">General</div>
  386. <div class="mb-1"><span class="font-semibold">Request URL:</span> <span class="break-all text-sm">${entry.url}</span></div>
  387. <div class="mb-1"><span class="font-semibold">Request Method:</span> <span class="text-sm">${entry.method}</span></div>
  388. <div class="mb-1"><span class="font-semibold">Status Code:</span> <span class="text-sm">${entry.status} ${entry.statusText}</span></div>
  389. <div class="mb-1"><span class="font-semibold">Time:</span> <span class="text-sm">${entry.time.toFixed(2)} ms</span></div>
  390. <div class="mb-1"><span class="font-semibold">Started At:</span> <span class="text-sm">${new Date(entry.startedDateTime).toLocaleString()}</span></div>
  391. </div>
  392. `;
  393. // Request Headers
  394. html += renderHeadersSection("Request Headers", entry.request.headers);
  395. // Response Headers
  396. html += renderHeadersSection("Response Headers", entry.response.headers);
  397. // Request Content (if any)
  398. if (entry.request.postData && entry.request.postData.text) {
  399. const contentText = entry.request.postData.text;
  400. html += createContentSection("Request Body", contentText, entry.request.postData.mimeType);
  401. }
  402. // Response Content
  403. if (entry.response.content && entry.response.content.text) {
  404. const contentText = entry.response.content.text;
  405. html += createContentSection(`Response Body (${formatBytes(entry.response.content.size)})`, contentText, entry.response.content.mimeType);
  406. } else if (entry.response.content && entry.response.content.size > 0) {
  407. html += createContentSection(`Response Body (${formatBytes(entry.response.content.size)})`, "(Binary data or content not available)", entry.response.content.mimeType);
  408. }
  409. detailsContent.innerHTML = html;
  410. // Re-attach event listeners for copy buttons if any
  411. document.querySelectorAll('.copy-btn').forEach(btn => {
  412. btn.addEventListener('click', (e) => {
  413. const text = decodeURIComponent(e.target.dataset.content);
  414. navigator.clipboard.writeText(text).then(() => {
  415. const originalText = e.target.textContent;
  416. e.target.textContent = 'Copied!';
  417. setTimeout(() => e.target.textContent = originalText, 1500);
  418. });
  419. });
  420. });
  421. }
  422. function createContentSection(title, content, mimeType) {
  423. let displayContent = escapeHtml(content);
  424. let isJson = false;
  425. // Try to pretty print JSON
  426. if (mimeType && (mimeType.includes('json') || mimeType.includes('javascript'))) {
  427. try {
  428. const obj = JSON.parse(content);
  429. displayContent = escapeHtml(JSON.stringify(obj, null, 2));
  430. isJson = true;
  431. } catch(e) {}
  432. }
  433. return `
  434. <div class="bg-white p-3 rounded shadow-sm border border-gray-100 mt-4 group">
  435. <div class="flex justify-between items-center mb-2">
  436. <div class="text-xs font-bold text-gray-500 uppercase">${title}</div>
  437. <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>
  438. </div>
  439. ${mimeType ? `<div class="text-xs text-gray-500 mb-2">Mime-Type: ${mimeType}</div>` : ''}
  440. <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>
  441. </div>
  442. `;
  443. }
  444. function renderXmlDetails(entry) {
  445. // Pretty print XML
  446. let formattedXml = entry.rawXml;
  447. try {
  448. // Simple indentation for XML
  449. const PADDING = ' ';
  450. const reg = /(>)(<)(\/*)/g;
  451. let pad = 0;
  452. formattedXml = formattedXml.replace(reg, '$1\r\n$2$3');
  453. formattedXml = formattedXml.split('\r\n').map(node => {
  454. let indent = 0;
  455. if (node.match(/.+<\/\w[^>]*>$/)) {
  456. indent = 0;
  457. } else if (node.match(/^<\/\w/)) {
  458. if (pad !== 0) pad -= 1;
  459. } else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
  460. indent = 1;
  461. } else {
  462. indent = 0;
  463. }
  464. const padding = new Array(pad + 1).join(PADDING);
  465. if (indent > 0) pad += 1;
  466. return padding + node;
  467. }).join('\r\n');
  468. } catch(e) {
  469. // fallback to raw if formatting fails
  470. formattedXml = entry.rawXml;
  471. }
  472. detailsContent.innerHTML = `
  473. <div class="bg-white p-3 rounded shadow-sm border border-gray-100">
  474. <div class="text-xs font-bold text-gray-500 uppercase mb-1">General Info</div>
  475. <div class="mb-1"><span class="font-semibold">URL:</span> <span class="break-all text-sm">${entry.url}</span></div>
  476. <div class="mb-1"><span class="font-semibold">Method:</span> <span class="text-sm">${entry.method}</span></div>
  477. <div class="mb-1"><span class="font-semibold">Status:</span> <span class="text-sm">${entry.status}</span></div>
  478. <div class="mb-1"><span class="font-semibold">Size:</span> <span class="text-sm">${formatBytes(entry.size)}</span></div>
  479. <div class="mb-1"><span class="font-semibold">Time:</span> <span class="text-sm">${entry.time} ms</span></div>
  480. </div>
  481. <div class="bg-white p-3 rounded shadow-sm border border-gray-100 mt-4 group">
  482. <div class="flex justify-between items-center mb-2">
  483. <div class="text-xs font-bold text-gray-500 uppercase">Raw XML Element</div>
  484. <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>
  485. </div>
  486. <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>
  487. </div>
  488. `;
  489. document.querySelectorAll('.copy-btn').forEach(btn => {
  490. btn.addEventListener('click', (e) => {
  491. const text = decodeURIComponent(e.target.dataset.content);
  492. navigator.clipboard.writeText(text).then(() => {
  493. const originalText = e.target.textContent;
  494. e.target.textContent = 'Copied!';
  495. setTimeout(() => e.target.textContent = originalText, 1500);
  496. });
  497. });
  498. });
  499. }
  500. function renderHeadersSection(title, headers) {
  501. if (!headers || headers.length === 0) return '';
  502. let items = headers.map(h => `
  503. <div class="grid grid-cols-3 gap-2 py-1 border-b border-gray-50 last:border-0">
  504. <div class="col-span-1 font-semibold text-gray-600 truncate text-xs" title="${h.name}">${h.name}</div>
  505. <div class="col-span-2 text-gray-800 break-all text-xs">${escapeHtml(h.value)}</div>
  506. </div>
  507. `).join('');
  508. return `
  509. <div class="bg-white p-3 rounded shadow-sm border border-gray-100 mt-4">
  510. <div class="text-xs font-bold text-gray-500 uppercase mb-2">${title}</div>
  511. ${items}
  512. </div>
  513. `;
  514. }
  515. // --- Helpers ---
  516. function updateStats() {
  517. entryCountEl.textContent = `${filteredEntries.length} entries`;
  518. let size = filteredEntries.reduce((acc, curr) => acc + (curr.size || 0), 0);
  519. totalSizeEl.textContent = formatBytes(size);
  520. let time = filteredEntries.reduce((acc, curr) => acc + (curr.time || 0), 0);
  521. totalTimeEl.textContent = Math.round(time) + ' ms';
  522. }
  523. function formatBytes(bytes, decimals = 2) {
  524. if (!+bytes) return '0 Bytes';
  525. const k = 1024;
  526. const dm = decimals < 0 ? 0 : decimals;
  527. const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  528. const i = Math.floor(Math.log(bytes) / Math.log(k));
  529. return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
  530. }
  531. function escapeHtml(text) {
  532. if (!text) return '';
  533. return text
  534. .replace(/&/g, "&amp;")
  535. .replace(/</g, "&lt;")
  536. .replace(/>/g, "&gt;")
  537. .replace(/"/g, "&quot;")
  538. .replace(/'/g, "&#039;");
  539. }
  540. </script>
  541. </body>
  542. </html>