@matware/e2e-runner 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.claude-plugin/marketplace.json +37 -6
  2. package/.claude-plugin/plugin.json +17 -3
  3. package/LICENSE +190 -0
  4. package/README.md +151 -527
  5. package/agents/test-creator.md +4 -2
  6. package/agents/test-improver.md +5 -3
  7. package/bin/cli.js +84 -20
  8. package/commands/capture.md +45 -0
  9. package/package.json +3 -2
  10. package/skills/e2e-testing/SKILL.md +3 -2
  11. package/skills/e2e-testing/references/action-types.md +22 -4
  12. package/skills/e2e-testing/references/test-json-format.md +23 -0
  13. package/src/actions.js +321 -14
  14. package/src/ai-generate.js +81 -0
  15. package/src/app-pool.js +339 -0
  16. package/src/config.js +131 -7
  17. package/src/dashboard.js +209 -11
  18. package/src/db.js +74 -7
  19. package/src/index.js +6 -4
  20. package/src/learner-sqlite.js +154 -0
  21. package/src/learner.js +70 -3
  22. package/src/mcp-tools.js +259 -34
  23. package/src/module-analysis.js +247 -0
  24. package/src/module-resolver.js +35 -2
  25. package/src/narrate.js +42 -1
  26. package/src/pool-manager.js +68 -17
  27. package/src/pool.js +464 -37
  28. package/src/reporter.js +4 -1
  29. package/src/runner.js +410 -63
  30. package/src/visual-diff.js +515 -0
  31. package/src/websocket.js +14 -3
  32. package/src/wizard.js +184 -0
  33. package/templates/build-dashboard.js +3 -0
  34. package/templates/dashboard/js/api.js +62 -3
  35. package/templates/dashboard/js/init.js +46 -0
  36. package/templates/dashboard/js/keyboard.js +8 -7
  37. package/templates/dashboard/js/quicksearch.js +277 -0
  38. package/templates/dashboard/js/state.js +61 -7
  39. package/templates/dashboard/js/toast.js +1 -1
  40. package/templates/dashboard/js/utils.js +20 -0
  41. package/templates/dashboard/js/view-live.js +240 -9
  42. package/templates/dashboard/js/view-runs.js +540 -94
  43. package/templates/dashboard/js/view-tests.js +157 -16
  44. package/templates/dashboard/js/view-tools.js +234 -0
  45. package/templates/dashboard/js/view-watch.js +2 -2
  46. package/templates/dashboard/js/websocket.js +36 -0
  47. package/templates/dashboard/styles/base.css +489 -53
  48. package/templates/dashboard/styles/components.css +719 -77
  49. package/templates/dashboard/styles/view-live.css +463 -59
  50. package/templates/dashboard/styles/view-runs.css +793 -155
  51. package/templates/dashboard/styles/view-tests.css +440 -77
  52. package/templates/dashboard/styles/view-tools.css +206 -0
  53. package/templates/dashboard/styles/view-watch.css +198 -41
  54. package/templates/dashboard/template.html +369 -56
  55. package/templates/dashboard.html +5375 -901
  56. package/templates/docker-compose-lightpanda.yml +7 -0
@@ -67,8 +67,8 @@ function renderRunsHealthBanner(){
67
67
  el('div',{className:'hb-lbl'},'Top Error ('+h.topErrorPattern.count+'x)')
68
68
  ]));
69
69
  }
70
- banner.appendChild(el('div',{className:'hb-link',onclick:function(){var lb=$('#runsTabLearnings');if(lb)lb.click()}},[
71
- el('span',null,'\u2192 View Learnings')
70
+ banner.appendChild(el('div',{className:'hb-link',onclick:function(){showView('insights')}},[
71
+ el('span',null,'\u2192 View Insights')
72
72
  ]));
73
73
  }).catch(function(){});
74
74
  }
@@ -88,7 +88,7 @@ function refreshRuns(){
88
88
  var htr=document.createElement('tr');
89
89
  var cols=[];
90
90
  if(!S.project)cols.push('Project');
91
- cols=cols.concat(['Suite','Source','Date','Total','Pass','Fail','Rate','Time']);
91
+ cols=cols.concat(['Suite','Driver','Source','Date','Total','Pass','Fail','Rate','Time']);
92
92
  cols.forEach(function(c){htr.appendChild(el('th',null,c))});
93
93
  head.textContent='';head.appendChild(htr);
94
94
  var colSpan=cols.length;
@@ -107,6 +107,7 @@ function refreshRuns(){
107
107
  if(r.id===S.selectedRun)tr.classList.add('expanded');
108
108
  if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
109
109
  tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
110
+ var driverTd=document.createElement('td');driverTd.appendChild(createDriverBadge(r.pool_driver));tr.appendChild(driverTd);
110
111
  var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
111
112
  tr.appendChild(el('td',null,fdate(r.generated_at)));
112
113
  tr.appendChild(el('td',null,String(r.total||0)));
@@ -203,8 +204,10 @@ function loadDetailInline(id,detailTr){
203
204
  ])
204
205
  ]);
205
206
  var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
207
+ var drvBlock=el('div',null,[el('div',{className:'rd-s-label'},'Driver'),el('div',{style:'margin-top:4px'},[createDriverBadge(d.poolDriver)])]);
206
208
  var summ=el('div',{className:'rd-summary'},[
207
209
  el('div',null,[el('div',{className:'rd-s-label'},'Suite'),el('div',{className:'rd-s-val',style:'font-size:14px;color:var(--accent)'},d.suiteName||'all')]),
210
+ drvBlock,
208
211
  srcBlock,
209
212
  el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
210
213
  el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
@@ -272,53 +275,121 @@ function loadDetailInline(id,detailTr){
272
275
  body.appendChild(errDiv);
273
276
  }
274
277
 
275
- // Actions panel
278
+ // Storyline \u2014 unified per-step cards with thumbnails, narrative, duration bar
276
279
  if(r.actions&&r.actions.length){
277
280
  var passCount=r.actions.filter(function(a){return a.success}).length;
278
281
  var failCount=r.actions.length-passCount;
279
- var actHead=el('div',{className:'rd-net-head'},[
282
+ var maxDur=Math.max.apply(null,r.actions.map(function(a){return a.duration||0}).concat([1]));
283
+ var hashes=r.screenshotHashes||{};
284
+
285
+ // "Play replay" button \u2014 only shown if at least one step has a screenshot
286
+ var hasFrames=r.actions.some(function(a){return a.autoScreenshot||a.screenshot})||(!r.success&&r.errorScreenshot);
287
+ var replayBtn=hasFrames?el('button',{
288
+ className:'rd-replay-btn',
289
+ title:'Replay step-by-step',
290
+ onclick:function(e){e.stopPropagation();openReplay(r.actions,r)}
291
+ },'\u25B6 Replay'):null;
292
+ var slHead=el('div',{className:'rd-net-head open'},[
280
293
  el('span',{className:'net-arrow'},'\u25B6'),
281
- el('span',{className:'net-title'},'Actions'),
294
+ el('span',{className:'net-title'},'Storyline'),
282
295
  el('div',{className:'net-stats'},[
283
296
  el('span',{className:'net-stat'},[document.createTextNode('Steps: '),el('strong',null,String(r.actions.length))]),
284
- failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null
285
- ])
297
+ failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null,
298
+ el('span',{className:'net-stat'},[document.createTextNode('Passed: '),el('strong',null,String(passCount))])
299
+ ]),
300
+ replayBtn
286
301
  ]);
287
- var actBody=el('div',{className:'rd-net-body',style:'padding:8px 14px'});
288
- r.actions.forEach(function(a){
302
+ var slBody=el('div',{className:'storyline'});
303
+
304
+ r.actions.forEach(function(a,idx){
305
+ var stepNum=String(idx+1).padStart(2,'0');
289
306
  var label=a.narrative||a.type;
290
307
  var durText=a.duration!=null?dur(a.duration):'';
291
- var retryBadge=null;
292
- if(a.actionRetries&&a.actionRetries>0){
293
- retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px'},'\u21BB x'+a.actionRetries);
308
+ var durPct=a.duration?Math.max(2,Math.round((a.duration/maxDur)*100)):0;
309
+ var stateCls=a.success?'pass':'fail';
310
+ var icon=a.success?'\u2714':'\u2718';
311
+
312
+ // Thumbnail: prefer autoScreenshot, fall back to action's own screenshot
313
+ var thumbPath=a.autoScreenshot||a.screenshot||null;
314
+ var thumb;
315
+ if(thumbPath){
316
+ var src='/api/image?path='+encodeURIComponent(thumbPath);
317
+ var img=document.createElement('img');
318
+ img.src=src;img.alt=label;img.loading='lazy';
319
+ thumb=el('div',{className:'sl-thumb',onclick:function(e){e.stopPropagation();openModal(src)}},[img]);
320
+ }else{
321
+ thumb=el('div',{className:'sl-thumb sl-thumb-empty',title:'No screenshot for this step'},[el('span',null,'\u25A1')]);
322
+ }
323
+
324
+ // Param chips (selector / text / value)
325
+ var chips=el('div',{className:'sl-chips'});
326
+ if(a.selector)chips.appendChild(el('span',{className:'sl-chip sl-chip-sel'},[el('span',{className:'sl-chip-k'},'sel'),el('span',{className:'sl-chip-v'},a.selector)]));
327
+ if(a.text)chips.appendChild(el('span',{className:'sl-chip'},[el('span',{className:'sl-chip-k'},'text'),el('span',{className:'sl-chip-v'},String(a.text))]));
328
+ if(a.value!=null&&a.value!=='')chips.appendChild(el('span',{className:'sl-chip'},[el('span',{className:'sl-chip-k'},'val'),el('span',{className:'sl-chip-v'},String(a.value))]));
329
+
330
+ var retryBadge=(a.actionRetries&&a.actionRetries>0)?el('span',{className:'sl-retry'},'\u21BB '+a.actionRetries):null;
331
+
332
+ var hashBadge=null;
333
+ if(thumbPath){
334
+ if(hashes[thumbPath]){hashBadge=createHashBadge(hashes[thumbPath])}
335
+ else{(function(holder,fp){ssHash(fp).then(function(h){if(h)holder.appendChild(createHashBadge(h))})})}
294
336
  }
295
- actBody.appendChild(el('div',{className:'lt-step'},[
296
- el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
297
- el('span',{className:'step-detail',style:'flex:1'},label),
337
+
338
+ var titleRow=el('div',{className:'sl-title'},[
339
+ el('span',{className:'sl-num'},stepNum),
340
+ el('span',{className:'sl-icon '+stateCls},icon),
341
+ el('span',{className:'sl-type'},a.type),
342
+ el('span',{className:'sl-narr'},label),
298
343
  retryBadge,
299
- el('span',{className:'step-dur'},durText)
300
- ]));
301
- });
302
- actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
303
- body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
304
- }
344
+ el('span',{className:'sl-dur'},durText)
345
+ ]);
346
+
347
+ var durBar=el('div',{className:'sl-bar'},[el('div',{className:'sl-bar-fill '+stateCls,style:'width:'+durPct+'%'})]);
348
+
349
+ var info=el('div',{className:'sl-info'},[
350
+ titleRow,
351
+ chips.children.length?chips:null,
352
+ durBar
353
+ ]);
305
354
 
306
- // Screenshots
307
- var shots=[];
308
- var hashes=r.screenshotHashes||{};
309
- (r.screenshots||[]).forEach(function(p){shots.push({path:p,label:p.split('/').pop(),type:'screenshot',hash:hashes[p]||null})});
310
- if(r.errorScreenshot){shots.push({path:r.errorScreenshot,label:r.errorScreenshot.split('/').pop(),type:'error',hash:hashes[r.errorScreenshot]||null})}
311
- if(shots.length){
312
- var shotsWrap=el('div',{className:'rd-shots'});
313
- shots.forEach(function(s){
314
- var src='/api/image?path='+encodeURIComponent(s.path);
315
- var img=document.createElement('img');img.src=src;img.alt=s.label;img.loading='lazy';
316
- var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
317
- if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
318
- else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
319
- shotsWrap.appendChild(el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[img,capEl]));
355
+ var card=el('div',{className:'sl-step '+stateCls},[thumb,info]);
356
+
357
+ if(!a.success&&a.error){
358
+ var errBlock=el('div',{className:'sl-err'},[
359
+ el('span',{className:'sl-err-tag'},'ERROR'),
360
+ el('span',{className:'sl-err-msg'},String(a.error))
361
+ ]);
362
+ card.appendChild(errBlock);
363
+ }
364
+
365
+ slBody.appendChild(card);
320
366
  });
321
- body.appendChild(shotsWrap);
367
+
368
+ // Final failure context card (uses test-level errorScreenshot if present)
369
+ if(!r.success&&r.errorScreenshot){
370
+ var errSrc='/api/image?path='+encodeURIComponent(r.errorScreenshot);
371
+ var errImg=document.createElement('img');errImg.src=errSrc;errImg.alt='Failure context';errImg.loading='lazy';
372
+ var errCard=el('div',{className:'sl-step sl-final-err'},[
373
+ el('div',{className:'sl-thumb sl-thumb-err',onclick:function(e){e.stopPropagation();openModal(errSrc)}},[errImg]),
374
+ el('div',{className:'sl-info'},[
375
+ el('div',{className:'sl-title'},[
376
+ el('span',{className:'sl-num'},'\u26A0'),
377
+ el('span',{className:'sl-icon fail'},'\u2718'),
378
+ el('span',{className:'sl-narr sl-final-msg'},'Page state at failure')
379
+ ]),
380
+ r.error?el('div',{className:'sl-err'},[el('span',{className:'sl-err-tag'},'TEST FAILED'),el('span',{className:'sl-err-msg'},String(r.error))]):null
381
+ ])
382
+ ]);
383
+ slBody.appendChild(errCard);
384
+ }
385
+
386
+ slHead.addEventListener('click',function(){slHead.classList.toggle('open');slBody.classList.toggle('hidden')});
387
+ body.appendChild(el('div',{className:'rd-net-panel rd-storyline-panel'},[slHead,slBody]));
388
+ } else if(r.errorScreenshot){
389
+ // No actions but we have an error screenshot \u2014 show it standalone
390
+ var errSrcOnly='/api/image?path='+encodeURIComponent(r.errorScreenshot);
391
+ var errImgOnly=document.createElement('img');errImgOnly.src=errSrcOnly;errImgOnly.loading='lazy';
392
+ body.appendChild(el('div',{className:'sl-final-err-standalone',onclick:function(e){e.stopPropagation();openModal(errSrcOnly)}},[errImgOnly]));
322
393
  }
323
394
 
324
395
  // Console logs
@@ -398,11 +469,63 @@ function refreshScreenshots(){
398
469
  var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
399
470
  var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
400
471
  (function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
401
- gal.appendChild(el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[img,capEl]));
472
+ gal.appendChild(el('div',{className:'gallery-item','data-path':f.path,onclick:function(){openModal(src)}},[img,capEl]));
402
473
  });
474
+ resetBlankBar();
403
475
  }).catch(function(){});
404
476
  }
405
477
 
478
+ /* ── Blank screenshot scan / delete ── */
479
+ function resetBlankBar(){
480
+ var bar=$('#ssBlankBar');if(bar)bar.hidden=true;
481
+ $$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
482
+ S.blankPaths=null;
483
+ }
484
+ function scanBlankScreenshots(){
485
+ if(!S.project){showToast('Select a project first','error');return}
486
+ var btn=$('#ssScanBlankBtn');btn.disabled=true;var prev=btn.textContent;btn.textContent='Scanning…';
487
+ api('/api/db/projects/'+S.project+'/screenshots/blank-scan').then(function(data){
488
+ btn.disabled=false;btn.textContent=prev;
489
+ var blanks=(data&&data.blanks)||[];
490
+ $$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
491
+ if(!blanks.length){
492
+ S.blankPaths=null;$('#ssBlankBar').hidden=true;
493
+ showToast('No blank images found ('+((data&&data.scanned)||0)+' scanned)','info');
494
+ return;
495
+ }
496
+ S.blankPaths=blanks.map(function(b){return b.path});
497
+ var found=0;
498
+ S.blankPaths.forEach(function(p){
499
+ var item=$('#screenshotGallery .gallery-item[data-path="'+(window.CSS&&CSS.escape?CSS.escape(p):p)+'"]');
500
+ if(item){item.classList.add('blank-flagged');found++}
501
+ });
502
+ $('#ssBlankMsg').textContent=blanks.length+' blank image'+(blanks.length===1?'':'s')+' of '+data.scanned+' scanned';
503
+ $('#ssBlankBar').hidden=false;
504
+ }).catch(function(){
505
+ btn.disabled=false;btn.textContent=prev;
506
+ showToast('Blank scan failed','error');
507
+ });
508
+ }
509
+ function deleteBlankScreenshots(){
510
+ if(!S.blankPaths||!S.blankPaths.length)return;
511
+ var btn=$('#ssBlankDeleteBtn');btn.disabled=true;var prev=btn.textContent;btn.textContent='Deleting…';
512
+ fetch('/api/screenshots/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({paths:S.blankPaths})})
513
+ .then(function(r){return r.json()}).then(function(res){
514
+ btn.disabled=false;btn.textContent=prev;
515
+ if(res&&res.error){showToast('Delete failed: '+res.error,'error');return}
516
+ var n=res.deleted||0,failed=(res.failed&&res.failed.length)||0;
517
+ showToast('Deleted '+n+' blank image'+(n===1?'':'s')+(failed?(' · '+failed+' failed'):''),failed?'error':'success');
518
+ resetBlankBar();
519
+ refreshScreenshots();
520
+ }).catch(function(){
521
+ btn.disabled=false;btn.textContent=prev;
522
+ showToast('Delete failed','error');
523
+ });
524
+ }
525
+ $('#ssScanBlankBtn').addEventListener('click',scanBlankScreenshots);
526
+ $('#ssBlankDeleteBtn').addEventListener('click',deleteBlankScreenshots);
527
+ $('#ssBlankCancelBtn').addEventListener('click',resetBlankBar);
528
+
406
529
  function searchByHash(){
407
530
  var container=$('#ssSearchResult');
408
531
  container.textContent='';
@@ -436,10 +559,8 @@ function refreshLearnings(){
436
559
  fetch(url).then(function(r){return r.json()}).then(function(data){
437
560
  if(!data||data.totalRuns===0){
438
561
  $('#learningsEmpty').style.display='block';
439
- $('#learningsOverview').textContent='';$('#learningsTrend').textContent='';
440
- $('#learningsFlaky').textContent='';$('#learningsSelectors').textContent='';
441
- $('#learningsPages').textContent='';$('#learningsApis').textContent='';
442
- $('#learningsErrors').textContent='';
562
+ $('#learnHero').textContent='';$('#learnCards').textContent='';
563
+ $('#learnTrend').textContent='';$('#learnBottom').textContent='';
443
564
  $('#badgeLearnings').textContent='-';
444
565
  return;
445
566
  }
@@ -464,48 +585,139 @@ function refreshLearnings(){
464
585
  $('#badgeLearnings').textContent='\u2714';
465
586
  $('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
466
587
  }
467
- renderLearnOverview(data);
588
+ renderLearnHero(data);
589
+ renderLearnCards(data);
468
590
  renderLearnTrend(data.recentTrend||[]);
469
- renderLearnFlaky(data.flakyTests||[]);
470
- renderLearnSelectors(data.unstableSelectors||[]);
471
- renderLearnPages(data.failingPages||[]);
472
- renderLearnApis(data.apiIssues||[]);
473
- renderLearnErrors(data.topErrors||[]);
591
+ renderLearnBottomRow(data);
474
592
  }).catch(function(){$('#learningsEmpty').style.display='block'});
475
593
  }
476
594
 
477
- function renderLearnOverview(d){
478
- var container=$('#learningsOverview');container.textContent='';
479
- var grid=document.createElement('div');grid.className='learn-grid';
480
- [{val:d.totalRuns,lbl:'Runs',cls:'accent'},{val:d.totalTests,lbl:'Tests',cls:'accent'},
481
- {val:d.overallPassRate+'%',lbl:'Pass Rate',cls:d.overallPassRate>=90?'green':d.overallPassRate>=70?'':'red'},
482
- {val:d.avgDurationMs<1000?d.avgDurationMs+'ms':(d.avgDurationMs/1000).toFixed(1)+'s',lbl:'Avg Duration',cls:'purple'},
483
- {val:(d.flakyTests?d.flakyTests.length:0),lbl:'Flaky Tests',cls:d.flakyTests&&d.flakyTests.length>0?'red':'green'},
484
- {val:(d.unstableSelectors?d.unstableSelectors.length:0),lbl:'Unstable Selectors',cls:d.unstableSelectors&&d.unstableSelectors.length>0?'red':'green'}
485
- ].forEach(function(item){
486
- var stat=document.createElement('div');stat.className='learn-stat';
487
- var valEl=document.createElement('div');valEl.className='learn-stat-val '+item.cls;valEl.textContent=item.val;
488
- var lblEl=document.createElement('div');lblEl.className='learn-stat-lbl';lblEl.textContent=item.lbl;
489
- stat.appendChild(valEl);stat.appendChild(lblEl);grid.appendChild(stat);
595
+ function rateColor(v){return v>=90?'var(--green)':v>=70?'var(--amber)':'var(--red)'}
596
+ function rateClass(v){return v>=90?'good':v>=70?'warn':'bad'}
597
+ function durFmt(ms){return ms<1000?Math.round(ms)+'ms':(ms/1000).toFixed(1)+'s'}
598
+
599
+ function renderLearnHero(d){
600
+ var c=$('#learnHero');c.textContent='';
601
+ var wrap=document.createElement('div');wrap.className='learn-hero';
602
+ var passRate=d.overallPassRate||0;
603
+ var ns='http://www.w3.org/2000/svg';
604
+ var ringWrap=document.createElement('div');ringWrap.className='learn-hero-ring';
605
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 36 36');
606
+ var bgCircle=document.createElementNS(ns,'circle');bgCircle.setAttribute('cx','18');bgCircle.setAttribute('cy','18');bgCircle.setAttribute('r','15.9');bgCircle.className.baseVal='learn-hero-ring-bg';svg.appendChild(bgCircle);
607
+ var fgCircle=document.createElementNS(ns,'circle');fgCircle.setAttribute('cx','18');fgCircle.setAttribute('cy','18');fgCircle.setAttribute('r','15.9');fgCircle.className.baseVal='learn-hero-ring-fg';
608
+ var circ=2*Math.PI*15.9;fgCircle.setAttribute('stroke-dasharray',circ.toFixed(1));fgCircle.setAttribute('stroke-dashoffset',(circ*(1-passRate/100)).toFixed(1));fgCircle.setAttribute('stroke',rateColor(passRate));
609
+ svg.appendChild(fgCircle);ringWrap.appendChild(svg);
610
+ var pctEl=document.createElement('div');pctEl.className='learn-hero-pct';pctEl.style.color=rateColor(passRate);pctEl.textContent=passRate+'%';
611
+ ringWrap.appendChild(pctEl);wrap.appendChild(ringWrap);
612
+
613
+ var stats=document.createElement('div');stats.className='learn-hero-stats';
614
+ var badSels=d.unstableSelectors?d.unstableSelectors.length:0;
615
+ var slowTests=d.failingPages?d.failingPages.length:0;
616
+ var apiIssues=d.apiIssues?d.apiIssues.length:0;
617
+ var topErr=d.topErrors&&d.topErrors.length>0?d.topErrors[0].occurrence_count:0;
618
+ var flakyCount=d.flakyTests?d.flakyTests.length:0;
619
+ var items=[
620
+ {val:String(d.totalRuns),lbl:'Runs',color:'var(--accent)'},
621
+ {val:String(d.totalTests),lbl:'Tests',color:'var(--accent)'},
622
+ {val:durFmt(d.avgDurationMs||0),lbl:'Avg Duration',color:'var(--purple)'},
623
+ {val:String(flakyCount),lbl:'Flaky',color:flakyCount>0?'var(--amber)':'var(--green)'},
624
+ {val:String(badSels),lbl:'Bad Selectors',color:badSels>0?'var(--red)':'var(--green)'},
625
+ {val:String(slowTests),lbl:'Slow Pages',color:slowTests>0?'var(--amber)':'var(--green)'},
626
+ {val:String(apiIssues),lbl:'API Issues',color:apiIssues>0?'var(--red)':'var(--green)'},
627
+ {val:String(topErr),lbl:'Top Error Hits',color:topErr>0?'var(--red)':'var(--green)'}
628
+ ];
629
+ items.forEach(function(it){
630
+ var statEl=document.createElement('div');statEl.className='learn-hero-stat';
631
+ var valEl=document.createElement('div');valEl.className='learn-hero-stat-val';valEl.style.color=it.color;valEl.textContent=it.val;
632
+ var lblEl=document.createElement('div');lblEl.className='learn-hero-stat-lbl';lblEl.textContent=it.lbl;
633
+ statEl.appendChild(valEl);statEl.appendChild(lblEl);stats.appendChild(statEl);
490
634
  });
491
- container.appendChild(grid);
635
+ wrap.appendChild(stats);c.appendChild(wrap);
636
+ }
637
+
638
+ function makeLearnItem(label,sub,pct,valText,color){
639
+ var item=document.createElement('div');item.className='learn-item';
640
+ var barWrap=document.createElement('div');barWrap.className='learn-item-bar';
641
+ var lblEl=document.createElement('div');lblEl.className='learn-item-label';
642
+ var codeEl=document.createElement('code');codeEl.textContent=label;lblEl.appendChild(codeEl);
643
+ barWrap.appendChild(lblEl);
644
+ if(sub){var subEl=document.createElement('div');subEl.className='learn-item-sub';subEl.textContent=sub;barWrap.appendChild(subEl)}
645
+ var bar=document.createElement('div');bar.className='learn-bar';
646
+ var fill=document.createElement('div');fill.className='learn-bar-fill';fill.style.width=Math.min(pct,100)+'%';fill.style.background=color;
647
+ bar.appendChild(fill);barWrap.appendChild(bar);
648
+ item.appendChild(barWrap);
649
+ var valEl=document.createElement('div');valEl.className='learn-item-val';valEl.style.color=color;valEl.textContent=valText;
650
+ item.appendChild(valEl);
651
+ return item;
652
+ }
653
+
654
+ function makeLearnCard(icon,title,emptyMsg){
655
+ var card=document.createElement('div');card.className='learn-card';
656
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
657
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent=icon;
658
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode(title));
659
+ card.appendChild(titleEl);
660
+ card._empty=emptyMsg;
661
+ return card;
662
+ }
663
+
664
+ function renderLearnCards(d){
665
+ var c=$('#learnCards');c.textContent='';
666
+
667
+ var selCard=makeLearnCard('\u26A0','Risky Selectors','No unstable selectors');
668
+ var sels=d.unstableSelectors||[];
669
+ if(!sels.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=selCard._empty;selCard.appendChild(e1)}
670
+ else{sels.slice(0,5).forEach(function(s){
671
+ var sel=s.selector.length>40?s.selector.slice(0,37)+'...':s.selector;
672
+ selCard.appendChild(makeLearnItem(sel,s.action_type+' \u00B7 '+s.total_uses+' uses',parseFloat(s.fail_rate),s.fail_rate+'%',parseFloat(s.fail_rate)>30?'var(--red)':'var(--amber)'));
673
+ })}
674
+ c.appendChild(selCard);
675
+
676
+ var pageCard=makeLearnCard('\u23F1','Problem Pages','No failing pages');
677
+ var pages=d.failingPages||[];
678
+ if(!pages.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=pageCard._empty;pageCard.appendChild(e2)}
679
+ else{pages.slice(0,5).forEach(function(p){
680
+ pageCard.appendChild(makeLearnItem(p.url_path,p.total_visits+' visits \u00B7 '+p.console_errors+' console errs',parseFloat(p.fail_rate),p.fail_rate+'%',parseFloat(p.fail_rate)>30?'var(--red)':'var(--amber)'));
681
+ })}
682
+ c.appendChild(pageCard);
683
+
684
+ var flakyCard=makeLearnCard('\u223C','Flaky Tests','No flaky tests detected');
685
+ var flaky=d.flakyTests||[];
686
+ if(!flaky.length){var e3=document.createElement('div');e3.className='learn-card-empty';e3.textContent=flakyCard._empty;flakyCard.appendChild(e3)}
687
+ else{flaky.slice(0,5).forEach(function(f){
688
+ flakyCard.appendChild(makeLearnItem(f.test_name,'Attempt avg '+f.avg_attempts+' \u00B7 '+f.total_runs+' runs',parseFloat(f.flaky_rate),f.flaky_rate+'%',parseFloat(f.flaky_rate)>30?'var(--red)':'var(--amber)'));
689
+ })}
690
+ c.appendChild(flakyCard);
691
+
692
+ var apiCard=makeLearnCard('\u21C4','API Issues','No API issues');
693
+ var apis=d.apiIssues||[];
694
+ if(!apis.length){var e4=document.createElement('div');e4.className='learn-card-empty';e4.textContent=apiCard._empty;apiCard.appendChild(e4)}
695
+ else{apis.slice(0,5).forEach(function(a){
696
+ var ep=a.endpoint.length>40?a.endpoint.slice(0,37)+'...':a.endpoint;
697
+ apiCard.appendChild(makeLearnItem(ep,a.total_calls+' calls \u00B7 '+durFmt(a.avg_duration_ms),parseFloat(a.error_rate),a.error_rate+'%',parseFloat(a.error_rate)>20?'var(--red)':'var(--amber)'));
698
+ })}
699
+ c.appendChild(apiCard);
492
700
  }
493
701
 
494
702
  function renderLearnTrend(trend){
495
- var container=$('#learningsTrend');container.textContent='';
703
+ var container=$('#learnTrend');container.textContent='';
496
704
  if(!trend.length)return;
497
- var card=document.createElement('div');card.className='card';
498
- var label=document.createElement('div');label.className='card-label';label.textContent='Pass Rate Trend (7 days)';card.appendChild(label);
499
- var chartDiv=document.createElement('div');chartDiv.className='learn-trend-chart';
705
+ var card=document.createElement('div');card.className='learn-card';
706
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
707
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent='\u2197';
708
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode('Pass Rate Trend'));
709
+ card.appendChild(titleEl);
710
+ var chartDiv=document.createElement('div');chartDiv.style.cssText='height:80px;width:100%';
500
711
  var w=100/trend.length;var ns='http://www.w3.org/2000/svg';
501
- var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');
712
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');svg.style.cssText='width:100%;height:100%';
502
713
  var bg=document.createElementNS(ns,'rect');bg.setAttribute('x','0');bg.setAttribute('y','0');bg.setAttribute('width','100');bg.setAttribute('height','100');bg.setAttribute('fill','var(--surface2)');bg.setAttribute('rx','2');svg.appendChild(bg);
503
714
  var gridLine=document.createElementNS(ns,'line');gridLine.setAttribute('x1','0');gridLine.setAttribute('y1','50');gridLine.setAttribute('x2','100');gridLine.setAttribute('y2','50');gridLine.setAttribute('stroke','var(--border)');gridLine.setAttribute('stroke-width','0.3');gridLine.setAttribute('stroke-dasharray','2,2');svg.appendChild(gridLine);
504
715
  var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
505
716
  var poly=document.createElementNS(ns,'polygon');poly.setAttribute('points',(0*w+w/2)+',100 '+pts+' '+((trend.length-1)*w+w/2)+',100');poly.setAttribute('fill','var(--accent-dim)');svg.appendChild(poly);
506
717
  var pl=document.createElementNS(ns,'polyline');pl.setAttribute('points',pts);pl.setAttribute('fill','none');pl.setAttribute('stroke','var(--accent)');pl.setAttribute('stroke-width','1.5');svg.appendChild(pl);
507
718
  trend.forEach(function(t,i){
508
- var circle=document.createElementNS(ns,'circle');circle.setAttribute('cx',''+(i*w+w/2));circle.setAttribute('cy',''+(100-t.pass_rate));circle.setAttribute('r','2');circle.setAttribute('fill','var(--accent)');
719
+ var color=rateColor(t.pass_rate);
720
+ var circle=document.createElementNS(ns,'circle');circle.setAttribute('cx',''+(i*w+w/2));circle.setAttribute('cy',''+(100-t.pass_rate));circle.setAttribute('r','2.5');circle.setAttribute('fill',color);
509
721
  var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
510
722
  });
511
723
  chartDiv.appendChild(svg);card.appendChild(chartDiv);
@@ -514,35 +726,47 @@ function renderLearnTrend(trend){
514
726
  card.appendChild(dates);container.appendChild(card);
515
727
  }
516
728
 
517
- function buildLearnTable(title,headers,rows){
518
- var card=document.createElement('div');card.className='card learn-section';
519
- var h=document.createElement('div');h.className='learn-section-title';h.textContent=title;card.appendChild(h);
520
- var wrap=document.createElement('div');wrap.className='tbl-wrap';
521
- var tbl=document.createElement('table');tbl.className='learn-table';
522
- var thead=document.createElement('thead');var hr=document.createElement('tr');
523
- headers.forEach(function(hdr){var th=document.createElement('th');th.textContent=hdr;hr.appendChild(th)});
524
- thead.appendChild(hr);tbl.appendChild(thead);
525
- var tbody=document.createElement('tbody');
526
- rows.forEach(function(cells){
527
- var tr=document.createElement('tr');
528
- cells.forEach(function(cell){
529
- var td=document.createElement('td');
530
- if(cell.code){var code=document.createElement('code');code.textContent=cell.code;td.appendChild(code)}
531
- else if(cell.badge){var span=document.createElement('span');span.className='badge '+cell.cls;span.textContent=cell.badge;td.appendChild(span)}
532
- else{td.textContent=cell.text!==undefined&&cell.text!==null?cell.text:(typeof cell==='object'?'-':cell)}
533
- tr.appendChild(td);
729
+ function renderLearnBottomRow(d){
730
+ var c=$('#learnBottom');c.textContent='';
731
+
732
+ var errCard=makeLearnCard('\u2718','Most Common Errors','No errors recorded');
733
+ var errors=d.topErrors||[];
734
+ if(!errors.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=errCard._empty;errCard.appendChild(e1)}
735
+ else{errors.slice(0,5).forEach(function(e){
736
+ var pat=e.pattern.length>45?e.pattern.slice(0,42)+'...':e.pattern;
737
+ var maxCount=errors[0].occurrence_count||1;
738
+ var pct=(e.occurrence_count/maxCount)*100;
739
+ var verdictEl=document.createElement('div');verdictEl.className='learn-verdict '+rateClass(100-(pct));verdictEl.textContent=e.category.replace(/-/g,' ');
740
+ var item=makeLearnItem(pat,(e.last_seen||'').split('T')[0]+' \u00B7 '+e.occurrence_count+'x',pct,e.occurrence_count+'x','var(--red)');
741
+ item.insertBefore(verdictEl,item.lastChild);
742
+ errCard.appendChild(item);
743
+ })}
744
+ c.appendChild(errCard);
745
+
746
+ var slowCard=makeLearnCard('\u23F3','Slowest Tests','No slow test data');
747
+ var trend=d.recentTrend||[];
748
+ var slowTests=[];
749
+ if(d.flakyTests){
750
+ d.flakyTests.forEach(function(f){
751
+ if(f.avg_duration_ms&&f.avg_duration_ms>2000){slowTests.push({name:f.test_name,dur:f.avg_duration_ms})}
534
752
  });
535
- tbody.appendChild(tr);
536
- });
537
- tbl.appendChild(tbody);wrap.appendChild(tbl);card.appendChild(wrap);return card;
753
+ }
754
+ if(d.failingPages){
755
+ d.failingPages.forEach(function(p){
756
+ if(p.avg_load_time_ms&&p.avg_load_time_ms>3000){slowTests.push({name:p.url_path,dur:p.avg_load_time_ms})}
757
+ });
758
+ }
759
+ slowTests.sort(function(a,b){return b.dur-a.dur});
760
+ if(!slowTests.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=slowCard._empty;slowCard.appendChild(e2)}
761
+ else{
762
+ var maxDur=slowTests[0].dur;
763
+ slowTests.slice(0,5).forEach(function(t){
764
+ slowCard.appendChild(makeLearnItem(t.name,'','',durFmt(t.dur),(t.dur/maxDur)*100,t.dur>5000?'var(--red)':'var(--amber)'));
765
+ });
766
+ }
767
+ c.appendChild(slowCard);
538
768
  }
539
769
 
540
- function renderLearnFlaky(flaky){var c=$('#learningsFlaky');c.textContent='';if(!flaky.length)return;c.appendChild(buildLearnTable('Flaky Tests',['Test','Flaky Rate','Occurrences','Total Runs','Last Flaky','Avg Attempts'],flaky.map(function(f){return[{code:f.test_name},{badge:f.flaky_rate+'%',cls:f.flaky_rate>30?'fail':'flaky'},{text:f.flaky_count},{text:f.total_runs},{text:(f.last_flaky||'-').split('T')[0]},{text:f.avg_attempts}]})))}
541
- function renderLearnSelectors(sels){var c=$('#learningsSelectors');c.textContent='';if(!sels.length)return;c.appendChild(buildLearnTable('Unstable Selectors',['Selector','Action','Fail Rate','Uses','Tests','Page'],sels.map(function(s){var sel=s.selector.length>45?s.selector.slice(0,42)+'...':s.selector;return[{code:sel},{text:s.action_type},{badge:s.fail_rate+'%',cls:s.fail_rate>30?'fail':'flaky'},{text:s.total_uses},{text:s.used_by_tests},{text:s.page_url||'-'}]})))}
542
- function renderLearnPages(pages){var c=$('#learningsPages');c.textContent='';if(!pages.length)return;c.appendChild(buildLearnTable('Failing Pages',['Page','Fail Rate','Visits','Console Errors','Network Errors'],pages.map(function(p){return[{code:p.url_path},{badge:p.fail_rate+'%',cls:p.fail_rate>30?'fail':'flaky'},{text:p.total_visits},{text:p.console_errors},{text:p.network_errors}]})))}
543
- function renderLearnApis(apis){var c=$('#learningsApis');c.textContent='';if(!apis.length)return;c.appendChild(buildLearnTable('API Issues',['Endpoint','Error Rate','Calls','Avg Duration','Status Codes'],apis.map(function(a){var ep=a.endpoint.length>45?a.endpoint.slice(0,42)+'...':a.endpoint;var d=a.avg_duration_ms<1000?Math.round(a.avg_duration_ms)+'ms':(a.avg_duration_ms/1000).toFixed(1)+'s';return[{code:ep},{badge:a.error_rate+'%',cls:a.error_rate>20?'fail':'flaky'},{text:a.total_calls},{text:d},{text:a.status_codes||'-'}]})))}
544
- function renderLearnErrors(errors){var c=$('#learningsErrors');c.textContent='';if(!errors.length)return;c.appendChild(buildLearnTable('Error Patterns',['Pattern','Category','Count','First Seen','Last Seen','Example Test'],errors.map(function(e){var pat=e.pattern.length>50?e.pattern.slice(0,47)+'...':e.pattern;return[{text:pat},{badge:e.category,cls:'run'},{text:e.occurrence_count},{text:(e.first_seen||'-').split('T')[0]},{text:(e.last_seen||'-').split('T')[0]},{code:e.example_test||'-'}]})))}
545
-
546
770
  $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
547
771
  $('#learningsDays').addEventListener('change',refreshLearnings);
548
772
 
@@ -570,3 +794,225 @@ $('#btnExportLearnings').addEventListener('click',function(){
570
794
  /* ── Modal ── */
571
795
  function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
572
796
  $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
797
+
798
+ /* ══════════════════════════════════════════════════════════════════
799
+ Screenshot Replay Player — plays a run's per-step screenshots
800
+ as a video. Pulls frames from r.actions[].autoScreenshot|screenshot.
801
+ ══════════════════════════════════════════════════════════════════ */
802
+ var REPLAY={frames:[],idx:0,playing:false,speed:1,timer:null,frameMs:1000};
803
+
804
+ function buildReplayFrames(actions,run){
805
+ var frames=[];
806
+ (actions||[]).forEach(function(a,i){
807
+ var path=a.autoScreenshot||a.screenshot||null;
808
+ frames.push({
809
+ idx:i,
810
+ path:path,
811
+ src:path?'/api/image?path='+encodeURIComponent(path):null,
812
+ type:a.type||'',
813
+ narr:a.narrative||a.type||'',
814
+ duration:a.duration||0,
815
+ success:!!a.success,
816
+ });
817
+ });
818
+ if(run&&!run.success&&run.errorScreenshot){
819
+ frames.push({
820
+ idx:frames.length,
821
+ path:run.errorScreenshot,
822
+ src:'/api/image?path='+encodeURIComponent(run.errorScreenshot),
823
+ type:'failure',
824
+ narr:'Page state at failure',
825
+ duration:0,
826
+ success:false,
827
+ });
828
+ }
829
+ return frames;
830
+ }
831
+
832
+ function openReplay(actions,run){
833
+ REPLAY.frames=buildReplayFrames(actions,run);
834
+ if(!REPLAY.frames.length){showToast&&showToast('No screenshots to replay','warn');return}
835
+ REPLAY.idx=0;REPLAY.playing=false;
836
+ $('#replayModal').classList.add('open');
837
+ $('#replayModal').setAttribute('aria-hidden','false');
838
+ renderReplayFrame();
839
+ // Auto-start playback for the "video" feel
840
+ toggleReplayPlay(true);
841
+ }
842
+
843
+ function closeReplay(){
844
+ stopReplayTimer();
845
+ $('#replayModal').classList.remove('open');
846
+ $('#replayModal').setAttribute('aria-hidden','true');
847
+ // Free image src to avoid lingering downloads
848
+ $('#replayImg').src='';
849
+ }
850
+
851
+ function renderReplayFrame(){
852
+ var f=REPLAY.frames[REPLAY.idx];if(!f)return;
853
+ var modal=$('#replayModal');
854
+ var img=$('#replayImg');
855
+ if(f.src){
856
+ modal.classList.remove('empty');
857
+ if(img.src!==location.origin+f.src&&img.src!==f.src)img.src=f.src;
858
+ }else{
859
+ modal.classList.add('empty');
860
+ img.src='';
861
+ }
862
+ $('#replayStepNum').textContent=(REPLAY.idx+1)+' / '+REPLAY.frames.length;
863
+ $('#replayStepType').textContent=f.type;
864
+ $('#replayStepNarr').textContent=f.narr;
865
+ var pct=REPLAY.frames.length>1?(REPLAY.idx/(REPLAY.frames.length-1))*100:100;
866
+ $('#replayProgressFill').style.width=pct+'%';
867
+ }
868
+
869
+ function scheduleNextReplayFrame(){
870
+ stopReplayTimer();
871
+ if(!REPLAY.playing)return;
872
+ if(REPLAY.idx>=REPLAY.frames.length-1){toggleReplayPlay(false);return}
873
+ // Uniform pacing: 1 frame per second at 1x, scaled by speed.
874
+ var ms=REPLAY.frameMs/REPLAY.speed;
875
+ REPLAY.timer=setTimeout(function(){
876
+ REPLAY.idx++;
877
+ renderReplayFrame();
878
+ scheduleNextReplayFrame();
879
+ },ms);
880
+ }
881
+
882
+ function stopReplayTimer(){if(REPLAY.timer){clearTimeout(REPLAY.timer);REPLAY.timer=null}}
883
+
884
+ function toggleReplayPlay(forceState){
885
+ REPLAY.playing=typeof forceState==='boolean'?forceState:!REPLAY.playing;
886
+ // If we're at the last frame and user hits play, restart from 0
887
+ if(REPLAY.playing&&REPLAY.idx>=REPLAY.frames.length-1){REPLAY.idx=0;renderReplayFrame()}
888
+ var btn=$('#replayPlay');if(btn)btn.innerHTML=REPLAY.playing?'❙❙':'▶';
889
+ if(REPLAY.playing)scheduleNextReplayFrame();else stopReplayTimer();
890
+ }
891
+
892
+ function stepReplay(delta){
893
+ stopReplayTimer();
894
+ REPLAY.idx=Math.max(0,Math.min(REPLAY.frames.length-1,REPLAY.idx+delta));
895
+ renderReplayFrame();
896
+ if(REPLAY.playing)scheduleNextReplayFrame();
897
+ }
898
+
899
+ function setReplaySpeed(s){
900
+ REPLAY.speed=s;
901
+ document.querySelectorAll('.replay-speed-btn').forEach(function(b){
902
+ b.classList.toggle('active',parseFloat(b.dataset.speed)===s);
903
+ });
904
+ if(REPLAY.playing){stopReplayTimer();scheduleNextReplayFrame()}
905
+ }
906
+
907
+ // Wire up controls
908
+ $('#replayPlay').addEventListener('click',function(){toggleReplayPlay()});
909
+ $('#replayPrev').addEventListener('click',function(){stepReplay(-1)});
910
+ $('#replayNext').addEventListener('click',function(){stepReplay(1)});
911
+ $('#replayClose').addEventListener('click',closeReplay);
912
+ document.querySelectorAll('.replay-speed-btn').forEach(function(b){
913
+ b.addEventListener('click',function(){setReplaySpeed(parseFloat(b.dataset.speed))});
914
+ });
915
+ document.addEventListener('keydown',function(e){
916
+ if(!$('#replayModal').classList.contains('open'))return;
917
+ if(e.key==='Escape'){closeReplay()}
918
+ else if(e.key===' '){e.preventDefault();toggleReplayPlay()}
919
+ else if(e.key==='ArrowLeft'){stepReplay(-1)}
920
+ else if(e.key==='ArrowRight'){stepReplay(1)}
921
+ });
922
+ // Click on stage advances; click outside the image closes
923
+ $('#replayModal').addEventListener('click',function(e){
924
+ if(e.target&&e.target.id==='replayModal')closeReplay();
925
+ });
926
+
927
+ // Expose to the renderer below so the storyline header can wire its button
928
+ window.openReplay=openReplay;
929
+
930
+ /* ══════════════════════════════════════════════════════════════════
931
+ Network tab — cross-run network query (Investigate › Network)
932
+ ══════════════════════════════════════════════════════════════════ */
933
+ function refreshNetwork(){
934
+ var box=$('#netResults');var empty=$('#networkEmpty');
935
+ if(!box)return;
936
+ box.textContent='';
937
+ box.appendChild(el('div',{style:'padding:20px;text-align:center;color:var(--text3);font-size:11px'},'Loading...'));
938
+ var runsUrl=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
939
+ api(runsUrl).then(function(runs){
940
+ if(!Array.isArray(runs)||runs.length===0){
941
+ box.textContent='';if(empty)empty.style.display='block';return;
942
+ }
943
+ if(empty)empty.style.display='none';
944
+ var top=runs.slice(0,30);
945
+ var promises=top.map(function(r){
946
+ return api('/api/db/runs/'+r.id+'/network-logs').catch(function(){return []}).then(function(logs){
947
+ return {run:r,logs:Array.isArray(logs)?logs:[]};
948
+ });
949
+ });
950
+ Promise.all(promises).then(function(results){renderNetworkResults(box,results)});
951
+ }).catch(function(){box.textContent='';if(empty)empty.style.display='block'});
952
+ }
953
+
954
+ function renderNetworkResults(box,results){
955
+ var statusFilter=$('#netStatusFilter')?$('#netStatusFilter').value:'errors';
956
+ var urlFilter=($('#netUrlFilter')?$('#netUrlFilter').value:'').toLowerCase().trim();
957
+ var rows=[];
958
+ results.forEach(function(r){
959
+ r.logs.forEach(function(n){
960
+ var s=n.status||0;
961
+ var keep=true;
962
+ if(statusFilter==='errors')keep=s>=400||s===0;
963
+ else if(statusFilter==='slow')keep=(n.duration||0)>=1000;
964
+ if(keep&&urlFilter&&(n.url||'').toLowerCase().indexOf(urlFilter)<0)keep=false;
965
+ if(keep)rows.push({run:r.run,n:n});
966
+ });
967
+ });
968
+ box.textContent='';
969
+ if(rows.length===0){
970
+ box.appendChild(el('div',{style:'padding:30px;text-align:center;color:var(--text3);font-size:11px'},'No matching network records.'));
971
+ return;
972
+ }
973
+ rows.sort(function(a,b){return (b.n.duration||0)-(a.n.duration||0)});
974
+ var head=el('div',{className:'net-row net-head'},[
975
+ el('span',{className:'net-col-run'},'Run'),
976
+ el('span',{className:'net-col-method'},'Method'),
977
+ el('span',{className:'net-col-status'},'Status'),
978
+ el('span',{className:'net-col-url'},'URL'),
979
+ el('span',{className:'net-col-dur'},'Duration')
980
+ ]);
981
+ box.appendChild(head);
982
+ rows.slice(0,200).forEach(function(rr){
983
+ var n=rr.n;var s=n.status||0;
984
+ var sCls=s===0?'s5xx':s<300?'s2xx':s<400?'s3xx':s<500?'s4xx':'s5xx';
985
+ var mCls=(n.method||'GET').toLowerCase();
986
+ var row=el('div',{className:'net-row clickable',onclick:function(){
987
+ showView('investigate');
988
+ var btn=document.querySelector('.tab-btn[data-tab="runsTabHistory"]');if(btn)btn.click();
989
+ }},[
990
+ el('span',{className:'net-col-run'},'#'+rr.run.id),
991
+ el('span',{className:'net-col-method m-'+mCls},n.method||'GET'),
992
+ el('span',{className:'net-col-status st-'+sCls},String(s||'ERR')),
993
+ el('span',{className:'net-col-url',title:n.url||''},n.url||''),
994
+ el('span',{className:'net-col-dur'},dur(n.duration||0))
995
+ ]);
996
+ box.appendChild(row);
997
+ });
998
+ if(rows.length>200){
999
+ box.appendChild(el('div',{style:'padding:10px;text-align:center;color:var(--text3);font-size:11px'},'Showing 200 of '+rows.length+' results'));
1000
+ }
1001
+ }
1002
+
1003
+ (function(){
1004
+ var btn=$('#btnRefreshNetwork');if(btn)btn.addEventListener('click',refreshNetwork);
1005
+ var sel=$('#netStatusFilter');if(sel)sel.addEventListener('change',refreshNetwork);
1006
+ var inp=$('#netUrlFilter');
1007
+ if(inp){
1008
+ var deb;
1009
+ inp.addEventListener('input',function(){clearTimeout(deb);deb=setTimeout(refreshNetwork,200)});
1010
+ }
1011
+ // Lazy: only fetch first time the Network tab is clicked
1012
+ var netTabBtn=document.querySelector('.tab-btn[data-tab="investigateTabNetwork"]');
1013
+ if(netTabBtn){
1014
+ netTabBtn.addEventListener('click',function(){
1015
+ if(!netTabBtn.dataset.loaded){netTabBtn.dataset.loaded='1';refreshNetwork()}
1016
+ });
1017
+ }
1018
+ })();