@matware/e2e-runner 1.3.1 → 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 (47) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +110 -21
  4. package/agents/test-creator.md +4 -2
  5. package/agents/test-improver.md +5 -3
  6. package/bin/cli.js +80 -17
  7. package/package.json +3 -2
  8. package/skills/e2e-testing/SKILL.md +3 -2
  9. package/skills/e2e-testing/references/action-types.md +22 -4
  10. package/skills/e2e-testing/references/test-json-format.md +23 -0
  11. package/src/actions.js +170 -14
  12. package/src/config.js +6 -0
  13. package/src/dashboard.js +135 -4
  14. package/src/db.js +11 -0
  15. package/src/mcp-tools.js +8 -2
  16. package/src/module-analysis.js +247 -0
  17. package/src/module-resolver.js +35 -2
  18. package/src/narrate.js +14 -1
  19. package/src/pool-manager.js +46 -1
  20. package/src/pool.js +177 -20
  21. package/src/runner.js +77 -10
  22. package/src/visual-diff.js +69 -0
  23. package/src/websocket.js +14 -3
  24. package/src/wizard.js +184 -0
  25. package/templates/build-dashboard.js +3 -0
  26. package/templates/dashboard/js/api.js +60 -3
  27. package/templates/dashboard/js/init.js +46 -0
  28. package/templates/dashboard/js/keyboard.js +8 -7
  29. package/templates/dashboard/js/quicksearch.js +277 -0
  30. package/templates/dashboard/js/state.js +61 -7
  31. package/templates/dashboard/js/toast.js +1 -1
  32. package/templates/dashboard/js/view-live.js +235 -42
  33. package/templates/dashboard/js/view-runs.js +379 -37
  34. package/templates/dashboard/js/view-tests.js +157 -16
  35. package/templates/dashboard/js/view-tools.js +234 -0
  36. package/templates/dashboard/js/view-watch.js +2 -2
  37. package/templates/dashboard/js/websocket.js +33 -3
  38. package/templates/dashboard/styles/base.css +489 -53
  39. package/templates/dashboard/styles/components.css +719 -84
  40. package/templates/dashboard/styles/view-live.css +459 -78
  41. package/templates/dashboard/styles/view-runs.css +779 -177
  42. package/templates/dashboard/styles/view-tests.css +440 -77
  43. package/templates/dashboard/styles/view-tools.css +206 -0
  44. package/templates/dashboard/styles/view-watch.css +198 -41
  45. package/templates/dashboard/template.html +354 -56
  46. package/templates/dashboard.html +5173 -711
  47. 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
  }
@@ -275,53 +275,121 @@ function loadDetailInline(id,detailTr){
275
275
  body.appendChild(errDiv);
276
276
  }
277
277
 
278
- // Actions panel
278
+ // Storyline \u2014 unified per-step cards with thumbnails, narrative, duration bar
279
279
  if(r.actions&&r.actions.length){
280
280
  var passCount=r.actions.filter(function(a){return a.success}).length;
281
281
  var failCount=r.actions.length-passCount;
282
- 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'},[
283
293
  el('span',{className:'net-arrow'},'\u25B6'),
284
- el('span',{className:'net-title'},'Actions'),
294
+ el('span',{className:'net-title'},'Storyline'),
285
295
  el('div',{className:'net-stats'},[
286
296
  el('span',{className:'net-stat'},[document.createTextNode('Steps: '),el('strong',null,String(r.actions.length))]),
287
- failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null
288
- ])
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
289
301
  ]);
290
- var actBody=el('div',{className:'rd-net-body',style:'padding:8px 14px'});
291
- 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');
292
306
  var label=a.narrative||a.type;
293
307
  var durText=a.duration!=null?dur(a.duration):'';
294
- var retryBadge=null;
295
- if(a.actionRetries&&a.actionRetries>0){
296
- 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))})})}
297
336
  }
298
- actBody.appendChild(el('div',{className:'lt-step'},[
299
- el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
300
- 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),
301
343
  retryBadge,
302
- el('span',{className:'step-dur'},durText)
303
- ]));
304
- });
305
- actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
306
- body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
307
- }
344
+ el('span',{className:'sl-dur'},durText)
345
+ ]);
308
346
 
309
- // Screenshots
310
- var shots=[];
311
- var hashes=r.screenshotHashes||{};
312
- (r.screenshots||[]).forEach(function(p){shots.push({path:p,label:p.split('/').pop(),type:'screenshot',hash:hashes[p]||null})});
313
- if(r.errorScreenshot){shots.push({path:r.errorScreenshot,label:r.errorScreenshot.split('/').pop(),type:'error',hash:hashes[r.errorScreenshot]||null})}
314
- if(shots.length){
315
- var shotsWrap=el('div',{className:'rd-shots'});
316
- shots.forEach(function(s){
317
- var src='/api/image?path='+encodeURIComponent(s.path);
318
- var img=document.createElement('img');img.src=src;img.alt=s.label;img.loading='lazy';
319
- var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
320
- if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
321
- else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
322
- shotsWrap.appendChild(el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[img,capEl]));
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
+ ]);
354
+
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);
323
366
  });
324
- 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]));
325
393
  }
326
394
 
327
395
  // Console logs
@@ -401,11 +469,63 @@ function refreshScreenshots(){
401
469
  var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
402
470
  var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
403
471
  (function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
404
- 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]));
405
473
  });
474
+ resetBlankBar();
406
475
  }).catch(function(){});
407
476
  }
408
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
+
409
529
  function searchByHash(){
410
530
  var container=$('#ssSearchResult');
411
531
  container.textContent='';
@@ -674,3 +794,225 @@ $('#btnExportLearnings').addEventListener('click',function(){
674
794
  /* ── Modal ── */
675
795
  function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
676
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
+ })();