Update index.html
Browse files- index.html +103 -25
index.html
CHANGED
|
@@ -51,9 +51,11 @@
|
|
| 51 |
|
| 52 |
<section id="tabsContainer" class="hidden">
|
| 53 |
<div id="tabButtons" class="flex border-b border-gray-300">
|
| 54 |
-
|
|
|
|
| 55 |
<div id="tabContents" class="mt-0">
|
| 56 |
-
|
|
|
|
| 57 |
</section>
|
| 58 |
|
| 59 |
<footer class="mt-6 text-xs text-gray-500">Concurrency Report • Generated by concurrency_tester</footer>
|
|
@@ -92,14 +94,16 @@
|
|
| 92 |
</div>
|
| 93 |
|
| 94 |
<div class="card p-4 bg-white border mb-6">
|
| 95 |
-
|
|
|
|
| 96 |
<canvas class="latencyTime" height="80"></canvas>
|
| 97 |
</div>
|
| 98 |
|
| 99 |
<div class="card p-4 bg-white border">
|
| 100 |
<h3 class="text-sm text-gray-500 mb-2">Attempts (table) — filter/search</h3>
|
| 101 |
<div class="flex gap-2 mb-2">
|
| 102 |
-
|
|
|
|
| 103 |
<select class="filterOk p-2 border rounded text-sm">
|
| 104 |
<option value="all">All</option>
|
| 105 |
<option value="ok">Only OK</option>
|
|
@@ -111,7 +115,8 @@
|
|
| 111 |
<thead class="bg-gray-50 sticky top-0">
|
| 112 |
<tr>
|
| 113 |
<th class="p-2 text-left">#</th>
|
| 114 |
-
<th class="p-2 text-left">
|
|
|
|
| 115 |
<th class="p-2 text-left">status</th>
|
| 116 |
<th class="p-2 text-left">latency_ms</th>
|
| 117 |
<th class="p-2 text-left">attempt</th>
|
|
@@ -192,20 +197,74 @@ function renderReportIntoPanel(panel, report, index){
|
|
| 192 |
const pieCanvas = panel.querySelector('.statusPie').getContext('2d');
|
| 193 |
chartInstances[index].statusPie = new Chart(pieCanvas, { type:'pie', data:{ labels:Object.keys(statuses), datasets:[{data:Object.values(statuses)}] }, options:{responsive:true} });
|
| 194 |
|
| 195 |
-
// Latency over time
|
| 196 |
const timeCanvas = panel.querySelector('.latencyTime').getContext('2d');
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
// --- Table ---
|
| 200 |
-
const tbody = panel.querySelector('.attemptsBody');
|
| 201 |
tbody.innerHTML=''; // Clear old data
|
| 202 |
-
attempts.forEach((a,i)=>{
|
| 203 |
const tr = document.createElement('tr');
|
| 204 |
tr.className = "border-b border-gray-100 hover:bg-gray-50";
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
| 207 |
<td class="p-2">${a.status ?? ''}</td>
|
| 208 |
-
<td class="p-2">${a.latency_ms
|
| 209 |
<td class="p-2">${a.attempt ?? ''}</td>
|
| 210 |
<td class="p-2">${a.ok ? 'yes' : 'no'}</td>
|
| 211 |
<td class="p-2"><pre class="text-xs">${typeof a.body === 'string' ? a.body : JSON.stringify(a.body || a.error || '', null, 0)}</pre></td>`;
|
|
@@ -228,10 +287,20 @@ function renderReportIntoPanel(panel, report, index){
|
|
| 228 |
panel.querySelector('.filterOk').onchange = () => applyFilter();
|
| 229 |
|
| 230 |
|
| 231 |
-
// --- Download Buttons ---
|
| 232 |
panel.querySelector('.downloadCsvBtn').onclick = () => {
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
const csv = rows.map(r=> r.map(c=> '"'+String(c).replace(/"/g,'""')+'"').join(',')).join('\n');
|
| 236 |
const blob = new Blob([csv],{type:'text/csv'});
|
| 237 |
const url = URL.createObjectURL(blob);
|
|
@@ -378,11 +447,11 @@ document.getElementById('fileInput').addEventListener('change', async (ev)=>{
|
|
| 378 |
|
| 379 |
// Paste/Render
|
| 380 |
document.getElementById('renderBtn').addEventListener('click', ()=>{
|
| 381 |
-
const txt = document.getElementById('jsonPaste').value.trim();
|
| 382 |
if(!txt) { alert('Paste JSON first'); return; }
|
| 383 |
|
| 384 |
-
try {
|
| 385 |
-
const j = JSON.parse(txt);
|
| 386 |
if (Array.isArray(j)) {
|
| 387 |
// It's an array of reports
|
| 388 |
j.forEach((report, i) => {
|
|
@@ -392,14 +461,14 @@ document.getElementById('renderBtn').addEventListener('click', ()=>{
|
|
| 392 |
// It's a single report
|
| 393 |
addReport(j, `Pasted Report ${allReports.length + 1}`);
|
| 394 |
}
|
| 395 |
-
} catch(e) {
|
| 396 |
-
alert('Invalid JSON: ' + e.message);
|
| 397 |
}
|
| 398 |
});
|
| 399 |
|
| 400 |
// Clear Paste Area
|
| 401 |
-
document.getElementById('clearBtn').addEventListener('click', ()=>{
|
| 402 |
-
document.getElementById('jsonPaste').value='';
|
| 403 |
});
|
| 404 |
|
| 405 |
// Clear All
|
|
@@ -431,12 +500,21 @@ document.getElementById('clearAllBtn').addEventListener('click', () => {
|
|
| 431 |
});
|
| 432 |
|
| 433 |
|
| 434 |
-
// Load sample
|
| 435 |
document.getElementById('loadSample').addEventListener('click', ()=>{
|
| 436 |
const sample = {
|
| 437 |
config:{total:20, concurrency:5, timeout:10, retries:1, service_url:'https://example.com'},
|
| 438 |
summary:{requested:20, completed_attempts:20, successful_responses:15, failed_attempts:5, success_rate:75, latency_ms:{min:50, max:1200, mean:210, median:180, p90:480, p99:900}},
|
| 439 |
-
results: Array.from({length:20}, (_,i)=>({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
};
|
| 441 |
addReport(sample, `Sample Report ${allReports.length + 1}`);
|
| 442 |
});
|
|
|
|
| 51 |
|
| 52 |
<section id="tabsContainer" class="hidden">
|
| 53 |
<div id="tabButtons" class="flex border-b border-gray-300">
|
| 54 |
+
<!-- Tab buttons will be injected here -->
|
| 55 |
+
</div>
|
| 56 |
<div id="tabContents" class="mt-0">
|
| 57 |
+
<!-- Tab contents will be injected here -->
|
| 58 |
+
</div>
|
| 59 |
</section>
|
| 60 |
|
| 61 |
<footer class="mt-6 text-xs text-gray-500">Concurrency Report • Generated by concurrency_tester</footer>
|
|
|
|
| 94 |
</div>
|
| 95 |
|
| 96 |
<div class="card p-4 bg-white border mb-6">
|
| 97 |
+
<!-- UPDATED Chart Title -->
|
| 98 |
+
<h3 class="text-sm text-gray-500">Latency over time (by Start Time)</h3>
|
| 99 |
<canvas class="latencyTime" height="80"></canvas>
|
| 100 |
</div>
|
| 101 |
|
| 102 |
<div class="card p-4 bg-white border">
|
| 103 |
<h3 class="text-sm text-gray-500 mb-2">Attempts (table) — filter/search</h3>
|
| 104 |
<div class="flex gap-2 mb-2">
|
| 105 |
+
<!-- UPDATED Placeholder Text -->
|
| 106 |
+
<input class="searchInput p-2 border rounded w-full text-sm" placeholder="search req_id / status / error / attempt">
|
| 107 |
<select class="filterOk p-2 border rounded text-sm">
|
| 108 |
<option value="all">All</option>
|
| 109 |
<option value="ok">Only OK</option>
|
|
|
|
| 115 |
<thead class="bg-gray-50 sticky top-0">
|
| 116 |
<tr>
|
| 117 |
<th class="p-2 text-left">#</th>
|
| 118 |
+
<th class="p-2 text-left">req_id</th> <!-- ADDED -->
|
| 119 |
+
<th class="p-2 text-left">Start Time</th> <!-- RENAMED -->
|
| 120 |
<th class="p-2 text-left">status</th>
|
| 121 |
<th class="p-2 text-left">latency_ms</th>
|
| 122 |
<th class="p-2 text-left">attempt</th>
|
|
|
|
| 197 |
const pieCanvas = panel.querySelector('.statusPie').getContext('2d');
|
| 198 |
chartInstances[index].statusPie = new Chart(pieCanvas, { type:'pie', data:{ labels:Object.keys(statuses), datasets:[{data:Object.values(statuses)}] }, options:{responsive:true} });
|
| 199 |
|
| 200 |
+
// --- MODIFIED: Latency over time ---
|
| 201 |
const timeCanvas = panel.querySelector('.latencyTime').getContext('2d');
|
| 202 |
+
// Sort attempts by start time to make the chart coherent
|
| 203 |
+
const sortedAttempts = [...attempts].sort((a, b) => (a.start_timestamp || 0) - (b.start_timestamp || 0));
|
| 204 |
+
|
| 205 |
+
chartInstances[index].latTime = new Chart(timeCanvas, {
|
| 206 |
+
type:'line',
|
| 207 |
+
data:{
|
| 208 |
+
datasets:[{
|
| 209 |
+
label:'latency_ms',
|
| 210 |
+
// Use {x, y} data format
|
| 211 |
+
data: sortedAttempts.map(a => ({
|
| 212 |
+
x: a.start_timestamp, // Use timestamp for x
|
| 213 |
+
y: a.latency_ms || null
|
| 214 |
+
})),
|
| 215 |
+
spanGaps:true,
|
| 216 |
+
tension:0.2,
|
| 217 |
+
pointRadius: 1, // Smaller points
|
| 218 |
+
borderWidth: 1.5 // Thinner line
|
| 219 |
+
}]
|
| 220 |
+
},
|
| 221 |
+
options:{
|
| 222 |
+
responsive:true,
|
| 223 |
+
plugins:{
|
| 224 |
+
legend:{display:false},
|
| 225 |
+
tooltip: {
|
| 226 |
+
callbacks: {
|
| 227 |
+
title: (tooltipItems) => {
|
| 228 |
+
// Show readable date in tooltip
|
| 229 |
+
const ts = tooltipItems[0].parsed.x;
|
| 230 |
+
return new Date(ts * 1000).toLocaleString();
|
| 231 |
+
},
|
| 232 |
+
label: (tooltipItem) => {
|
| 233 |
+
return ` Latency: ${tooltipItem.parsed.y.toFixed(2)} ms`;
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
},
|
| 238 |
+
scales:{
|
| 239 |
+
x: {
|
| 240 |
+
type: 'linear', // Use linear scale for timestamps
|
| 241 |
+
title:{display:true,text:'Start Time'},
|
| 242 |
+
ticks: {
|
| 243 |
+
// Format ticks to be readable times
|
| 244 |
+
callback: function(value) {
|
| 245 |
+
// Multiply by 1000 because JS Date uses milliseconds
|
| 246 |
+
return new Date(value * 1000).toLocaleTimeString();
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
},
|
| 250 |
+
y:{title:{display:true,text:'ms'}}
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
});
|
| 254 |
+
// --- END MODIFIED ---
|
| 255 |
|
| 256 |
+
// --- MODIFIED: Table ---
|
| 257 |
+
const tbody = panel.querySelector('.attemptsBody');
|
| 258 |
tbody.innerHTML=''; // Clear old data
|
| 259 |
+
attempts.sort((a, b) => a.req_id > b.req_id).forEach((a,i)=>{
|
| 260 |
const tr = document.createElement('tr');
|
| 261 |
tr.className = "border-b border-gray-100 hover:bg-gray-50";
|
| 262 |
+
// UPDATED innerHTML to include req_id and use start_timestamp
|
| 263 |
+
tr.innerHTML = `<td class="p-2">${a.req_id ?? i+1}</td>
|
| 264 |
+
<td class="p-2">${a.req_id ?? ''}</td>
|
| 265 |
+
<td class="p-2">${a.start_timestamp ? new Date(a.start_timestamp * 1000).toLocaleString() : ''}</td>
|
| 266 |
<td class="p-2">${a.status ?? ''}</td>
|
| 267 |
+
<td class="p-2">${a.latency_ms ? a.latency_ms.toFixed(2) : ''}</td>
|
| 268 |
<td class="p-2">${a.attempt ?? ''}</td>
|
| 269 |
<td class="p-2">${a.ok ? 'yes' : 'no'}</td>
|
| 270 |
<td class="p-2"><pre class="text-xs">${typeof a.body === 'string' ? a.body : JSON.stringify(a.body || a.error || '', null, 0)}</pre></td>`;
|
|
|
|
| 287 |
panel.querySelector('.filterOk').onchange = () => applyFilter();
|
| 288 |
|
| 289 |
|
| 290 |
+
// --- MODIFIED: Download Buttons ---
|
| 291 |
panel.querySelector('.downloadCsvBtn').onclick = () => {
|
| 292 |
+
// UPDATED CSV headers and data rows
|
| 293 |
+
const rows = [['req_id','start_timestamp','status','latency_ms','attempt','ok','error','body']];
|
| 294 |
+
(report.results || []).forEach(a=> rows.push([
|
| 295 |
+
a.req_id||'',
|
| 296 |
+
a.start_timestamp ? new Date(a.start_timestamp * 1000).toISOString() : '', // Use ISOString for CSV
|
| 297 |
+
a.status||'',
|
| 298 |
+
a.latency_ms||'',
|
| 299 |
+
a.attempt||'',
|
| 300 |
+
a.ok||'',
|
| 301 |
+
a.error||'',
|
| 302 |
+
typeof a.body === 'string' ? a.body : JSON.stringify(a.body || '')
|
| 303 |
+
]));
|
| 304 |
const csv = rows.map(r=> r.map(c=> '"'+String(c).replace(/"/g,'""')+'"').join(',')).join('\n');
|
| 305 |
const blob = new Blob([csv],{type:'text/csv'});
|
| 306 |
const url = URL.createObjectURL(blob);
|
|
|
|
| 447 |
|
| 448 |
// Paste/Render
|
| 449 |
document.getElementById('renderBtn').addEventListener('click', ()=>{
|
| 450 |
+
const txt = document.getElementById('jsonPaste').value.trim();
|
| 451 |
if(!txt) { alert('Paste JSON first'); return; }
|
| 452 |
|
| 453 |
+
try {
|
| 454 |
+
const j = JSON.parse(txt);
|
| 455 |
if (Array.isArray(j)) {
|
| 456 |
// It's an array of reports
|
| 457 |
j.forEach((report, i) => {
|
|
|
|
| 461 |
// It's a single report
|
| 462 |
addReport(j, `Pasted Report ${allReports.length + 1}`);
|
| 463 |
}
|
| 464 |
+
} catch(e) {
|
| 465 |
+
alert('Invalid JSON: ' + e.message);
|
| 466 |
}
|
| 467 |
});
|
| 468 |
|
| 469 |
// Clear Paste Area
|
| 470 |
+
document.getElementById('clearBtn').addEventListener('click', ()=>{
|
| 471 |
+
document.getElementById('jsonPaste').value='';
|
| 472 |
});
|
| 473 |
|
| 474 |
// Clear All
|
|
|
|
| 500 |
});
|
| 501 |
|
| 502 |
|
| 503 |
+
// --- MODIFIED: Load sample ---
|
| 504 |
document.getElementById('loadSample').addEventListener('click', ()=>{
|
| 505 |
const sample = {
|
| 506 |
config:{total:20, concurrency:5, timeout:10, retries:1, service_url:'https://example.com'},
|
| 507 |
summary:{requested:20, completed_attempts:20, successful_responses:15, failed_attempts:5, success_rate:75, latency_ms:{min:50, max:1200, mean:210, median:180, p90:480, p99:900}},
|
| 508 |
+
results: Array.from({length:20}, (_,i)=>({
|
| 509 |
+
req_id: i, // ADDED
|
| 510 |
+
start_timestamp: (Date.now() - (20-i)*1000 - Math.random()*1000) / 1000, // ADDED (as unix timestamp)
|
| 511 |
+
status: i<15?200:500,
|
| 512 |
+
latency_ms: Math.round(50 + Math.random()*600 + (i%5)*50), // Add some variance
|
| 513 |
+
attempt:1,
|
| 514 |
+
ok: i<15,
|
| 515 |
+
body: i<15?{data:'ok'}:{error:'server'}
|
| 516 |
+
// 'timestamp' field removed
|
| 517 |
+
}))
|
| 518 |
};
|
| 519 |
addReport(sample, `Sample Report ${allReports.length + 1}`);
|
| 520 |
});
|