philipp-zettl commited on
Commit
2350eb7
·
verified ·
1 Parent(s): 766d7ab

Update index.html

Browse files
Files changed (1) hide show
  1. 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
- </div>
 
55
  <div id="tabContents" class="mt-0">
56
- </div>
 
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
- <h3 class="text-sm text-gray-500">Latency over time (attempt index)</h3>
 
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
- <input class="searchInput p-2 border rounded w-full text-sm" placeholder="search status / error / attempt">
 
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">timestamp</th>
 
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
- chartInstances[index].latTime = new Chart(timeCanvas, { type:'line', data:{ labels: attempts.map((_,i)=>i+1), datasets:[{label:'latency_ms', data: attempts.map(a=>a.latency_ms || null), spanGaps:true, tension:0.2}] }, options:{responsive:true, plugins:{legend:{display:false}}, scales:{y:{title:{display:true,text:'ms'}}}} });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- tr.innerHTML = `<td class="p-2">${i+1}</td>
206
- <td class="p-2">${a.timestamp || ''}</td>
 
 
207
  <td class="p-2">${a.status ?? ''}</td>
208
- <td class="p-2">${a.latency_ms ?? ''}</td>
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
- const rows = [['timestamp','status','latency_ms','attempt','ok','error']];
234
- (report.results || []).forEach(a=> rows.push([a.timestamp||'', a.status||'', a.latency_ms||'', a.attempt||'', a.ok||'', a.error||'']));
 
 
 
 
 
 
 
 
 
 
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)=>({timestamp:new Date(Date.now()- (20-i)*1000).toISOString(), status: i<15?200:500, latency_ms: Math.round(50 + Math.random()*600), attempt:1, ok: i<15, body: i<15?{data:'ok'}:{error:'server'}}))
 
 
 
 
 
 
 
 
 
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
  });