@matware/e2e-runner 1.3.0 → 1.3.1

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.
@@ -59,19 +59,6 @@ a{color:var(--accent);text-decoration:none}
59
59
  .pool-item strong{color:var(--text);font-weight:500}
60
60
  .pool-item .pool-sessions{margin-left:auto;color:var(--text3);font-size:9px}
61
61
 
62
- /* ── Sync Status ── */
63
- .sync-status{padding:12px 16px;border-top:1px solid var(--border)}
64
- .sync-status .sync-header{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text2)}
65
- .sync-status .sync-mode{font-size:9px;padding:2px 6px;border-radius:3px;text-transform:uppercase;font-weight:600}
66
- .sync-status .sync-mode.hub{background:var(--purple-dim);color:var(--purple)}
67
- .sync-status .sync-mode.agent{background:var(--accent-dim);color:var(--accent)}
68
- .sync-status .sync-mode.standalone{background:var(--surface3);color:var(--text3)}
69
- .sync-status .sync-details{margin-top:6px;font-size:10px;color:var(--text3)}
70
- .sync-status .sync-instances{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px}
71
- .sync-status .sync-inst{display:inline-flex;align-items:center;gap:3px;padding:2px 6px;border-radius:10px;font-size:9px;background:var(--surface3);border:1px solid var(--border)}
72
- .sync-status .sync-inst.online{border-color:var(--green);color:var(--green)}
73
- .sync-status .sync-inst.offline{color:var(--text3)}
74
-
75
62
  /* ── Main ── */
76
63
  .main{margin-left:232px;flex:1;min-height:100vh;display:flex;flex-direction:column}
77
64
  .main-header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--surface);position:sticky;top:0;z-index:40}
@@ -172,6 +159,13 @@ tbody tr.selected td{background:var(--accent-dim)}
172
159
  .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
173
160
  .trigger-badge .trig-icon{font-size:11px;line-height:1}
174
161
 
162
+ /* ── Driver Badges ── */
163
+ .driver-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;font-family:var(--mono);white-space:nowrap}
164
+ .driver-badge.drv-browserless{background:var(--accent-dim)}
165
+ .driver-badge.drv-cdp{background:var(--purple-dim)}
166
+ .driver-badge.drv-steel{background:var(--amber-dim)}
167
+ .driver-badge .drv-icon{font-size:11px;line-height:1}
168
+
175
169
  /* ── Filter Bar ── */
176
170
  .filter-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
177
171
  .filter-btn{padding:5px 12px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text2);font-family:var(--mono);font-size:11px;cursor:pointer;transition:all .15s}
@@ -563,6 +557,42 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
563
557
  .learn-trend-chart{width:100%;height:100px;margin-bottom:20px}
564
558
  .learn-trend-chart svg{width:100%;height:100%}
565
559
 
560
+ /* ── Learnings Dashboard (visual cards) ── */
561
+ .learn-hero{display:flex;align-items:center;gap:24px;margin-bottom:20px;padding:20px 24px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
562
+ .learn-hero-ring{position:relative;width:100px;height:100px;flex-shrink:0}
563
+ .learn-hero-ring svg{width:100%;height:100%;transform:rotate(-90deg)}
564
+ .learn-hero-ring-bg{fill:none;stroke:var(--surface3);stroke-width:8}
565
+ .learn-hero-ring-fg{fill:none;stroke-width:8;stroke-linecap:round;transition:stroke-dashoffset .6s ease}
566
+ .learn-hero-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;font-family:var(--mono)}
567
+ .learn-hero-stats{flex:1;display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
568
+ .learn-hero-stat{text-align:center}
569
+ .learn-hero-stat-val{font-size:18px;font-weight:700;font-family:var(--mono)}
570
+ .learn-hero-stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}
571
+
572
+ .learn-cols{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
573
+ @media(max-width:900px){.learn-cols{grid-template-columns:1fr}}
574
+
575
+ .learn-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:14px 16px}
576
+ .learn-card-title{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px;display:flex;align-items:center;gap:6px}
577
+ .learn-card-title .lc-icon{font-size:13px}
578
+ .learn-card-empty{font-size:11px;color:var(--text3);font-style:italic}
579
+
580
+ .learn-item{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
581
+ .learn-item:last-child{border-bottom:none}
582
+ .learn-item-bar{flex:1;min-width:0}
583
+ .learn-item-label{font-size:11px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px}
584
+ .learn-item-label code{background:var(--surface3);padding:1px 4px;border-radius:3px;font-size:10px}
585
+ .learn-item-sub{font-size:9px;color:var(--text3)}
586
+ .learn-item-val{font-size:13px;font-weight:700;font-family:var(--mono);flex-shrink:0;min-width:44px;text-align:right}
587
+
588
+ .learn-bar{height:4px;border-radius:2px;background:var(--surface3);overflow:hidden;margin-top:3px}
589
+ .learn-bar-fill{height:100%;border-radius:2px;transition:width .4s ease}
590
+
591
+ .learn-verdict{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:10px;font-size:10px;font-weight:600}
592
+ .learn-verdict.good{background:var(--green-dim);color:var(--green)}
593
+ .learn-verdict.warn{background:var(--amber-dim);color:var(--amber)}
594
+ .learn-verdict.bad{background:var(--red-dim);color:var(--red)}
595
+
566
596
  /* ── Pool Distribution ── */
567
597
  .pool-dist{display:flex;align-items:stretch;gap:0;border-radius:6px;overflow:hidden;height:22px;margin:8px 0;font-size:10px;font-weight:600;font-family:var(--mono)}
568
598
  .pool-dist-seg{display:flex;align-items:center;justify-content:center;gap:4px;padding:0 8px;color:#fff;white-space:nowrap;min-width:40px;transition:flex .3s}
@@ -584,7 +614,7 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
584
614
  .live-stats span strong{color:var(--text)}
585
615
  .live-progress{height:3px;background:var(--surface3)}
586
616
  .live-progress-fill{height:100%;background:var(--purple);transition:width .4s;border-radius:0 2px 2px 0}
587
- .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto;min-height:0}
617
+ .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;min-height:0;flex:1}
588
618
  .live-test{padding:10px 12px;border-radius:var(--r);border-left:3px solid var(--text3);background:var(--surface2);font-size:11px;transition:border-color .2s,padding .25s,max-height .35s cubic-bezier(.4,0,.2,1)}
589
619
  .live-test.running{border-left-color:var(--purple)}
590
620
  .live-test.passed{border-left-color:var(--green)}
@@ -641,6 +671,29 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
641
671
  .lr-dismiss{padding:2px 6px;font-size:9px;font-family:var(--mono);background:transparent;border:1px solid transparent;border-radius:4px;color:var(--text3);cursor:pointer;transition:all .15s;margin-left:auto}
642
672
  .lr-dismiss:hover{color:var(--red);border-color:rgba(239,68,68,.3);background:var(--red-dim)}
643
673
 
674
+ /* ── Screencast Panel ── */
675
+ .live-body{display:flex;flex:1;min-height:0;overflow:hidden}
676
+ .live-body .live-tests{flex:1;min-width:0}
677
+ .screencast-panel{width:420px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--border);background:var(--surface2)}
678
+ .screencast-header{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:var(--surface3)}
679
+ .screencast-label{font-size:11px;font-weight:600;color:var(--purple);white-space:nowrap}
680
+ .screencast-select{flex:1;padding:4px 8px;font-size:10px;font-family:var(--mono);background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);outline:none;cursor:pointer}
681
+ .screencast-select:focus{border-color:var(--purple)}
682
+ .screencast-viewport{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#000;position:relative}
683
+ .screencast-viewport img{max-width:100%;max-height:100%;object-fit:contain;display:none}
684
+ .screencast-placeholder{display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--text3);font-size:12px;font-family:var(--mono)}
685
+
686
+ /* ── Screencast focus badge on test cards ── */
687
+ .sc-focus-badge{cursor:pointer;font-size:10px;padding:1px 4px;border-radius:3px;opacity:.4;transition:all .15s}
688
+ .sc-focus-badge:hover{opacity:.8}
689
+ .sc-focus-badge.active{opacity:1;background:var(--purple-dim);border-radius:3px}
690
+
691
+ /* ── Screencast toggle in Tests view ── */
692
+ .screencast-toggle-label{display:flex;align-items:center;gap:4px;cursor:pointer;font-size:14px;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--surface2);transition:all .15s;user-select:none}
693
+ .screencast-toggle-label:hover{border-color:var(--purple);background:var(--surface3)}
694
+ .screencast-toggle-label input{display:none}
695
+ .screencast-toggle-label:has(input:checked){border-color:var(--purple);background:var(--purple-dim);color:var(--purple)}
696
+
644
697
  .live-nav-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
645
698
  .spinner{display:inline-block;width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
646
699
  .spinner-small{display:inline-block;width:8px;height:8px;border:1.5px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
@@ -678,9 +731,6 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
678
731
  <div class="nav-item" data-view="live" id="navLive" style="display:none">
679
732
  <i class="icon"><span class="live-nav-dot"></span></i><span>Live</span><span class="badge" id="liveBadge" style="background:var(--purple-dim);color:var(--purple)">0</span>
680
733
  </div>
681
- <div class="nav-item" data-view="instances" id="navInstances" style="display:none">
682
- <i class="icon">&#9673;</i><span>Instances</span><span class="badge" id="badgeInstances">-</span>
683
- </div>
684
734
 
685
735
  <div class="pool-status" id="poolStatus">
686
736
  <div class="pool-info">
@@ -694,17 +744,6 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
694
744
  <span id="wsLabel" style="font-size:10px;color:var(--text3)">ws: connecting</span>
695
745
  </div>
696
746
  </div>
697
-
698
- <!-- Sync Status -->
699
- <div class="sync-status" id="syncStatus" style="display:none">
700
- <div class="sync-header">
701
- <span class="pool-dot" id="syncDot"></span>
702
- <strong>Sync</strong>
703
- <span class="sync-mode" id="syncMode">--</span>
704
- </div>
705
- <div class="sync-details" id="syncDetails"></div>
706
- <div class="sync-instances" id="syncInstances"></div>
707
- </div>
708
747
  </aside>
709
748
 
710
749
  <div class="main">
@@ -731,7 +770,11 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
731
770
  <div class="view" id="view-tests">
732
771
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
733
772
  <div style="font-family:var(--sans);font-size:16px;font-weight:600">Tests</div>
734
- <div style="display:flex;gap:8px">
773
+ <div style="display:flex;gap:8px;align-items:center">
774
+ <label class="screencast-toggle-label" title="Enable live browser screencast during test runs">
775
+ <input type="checkbox" id="screencastToggle" />
776
+ <span>&#128249;</span>
777
+ </label>
735
778
  <button class="btn sm primary" id="btnRunAll">&#9655; Run All</button>
736
779
  </div>
737
780
  </div>
@@ -833,13 +876,12 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
833
876
  <button class="btn sm" id="btnRefreshLearnings">Refresh</button>
834
877
  </div>
835
878
  </div>
836
- <div id="learningsOverview"></div>
837
- <div id="learningsTrend"></div>
838
- <div id="learningsFlaky"></div>
839
- <div id="learningsSelectors"></div>
840
- <div id="learningsPages"></div>
841
- <div id="learningsApis"></div>
842
- <div id="learningsErrors"></div>
879
+ <div id="learnDash">
880
+ <div id="learnHero"></div>
881
+ <div id="learnCards" class="learn-cols"></div>
882
+ <div id="learnTrend"></div>
883
+ <div id="learnBottom" class="learn-cols"></div>
884
+ </div>
843
885
  <div class="empty" id="learningsEmpty" style="display:none">
844
886
  <div class="empty-icon">&#9733;</div>
845
887
  <p>No learnings data yet. Run some tests to start building knowledge.</p>
@@ -847,54 +889,6 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
847
889
  </div>
848
890
  </div>
849
891
 
850
- <!-- ════════════════ Instances View (Hub Mode) ════════════════ -->
851
- <div class="view" id="view-instances">
852
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
853
- <div style="font-family:var(--sans);font-size:16px;font-weight:600">Connected Instances</div>
854
- <div style="display:flex;gap:8px">
855
- <select id="instanceFilter" style="padding:6px 10px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:11px">
856
- <option value="all">All Status</option>
857
- <option value="active">Active</option>
858
- <option value="pending">Pending</option>
859
- <option value="suspended">Suspended</option>
860
- </select>
861
- <button class="btn sm" id="refreshInstances">Refresh</button>
862
- </div>
863
- </div>
864
-
865
- <div class="stats" id="instanceStats">
866
- <div class="stat-block"><div class="stat-val accent" id="statInstancesTotal">-</div><div class="stat-lbl">Total</div></div>
867
- <div class="stat-block"><div class="stat-val green" id="statInstancesOnline">-</div><div class="stat-lbl">Online</div></div>
868
- <div class="stat-block"><div class="stat-val purple" id="statInstancesActive">-</div><div class="stat-lbl">Active</div></div>
869
- <div class="stat-block"><div class="stat-val" id="statInstancesPending" style="color:var(--amber)">-</div><div class="stat-lbl">Pending</div></div>
870
- </div>
871
-
872
- <div class="card" style="padding:0">
873
- <div class="tbl-wrap">
874
- <table>
875
- <thead>
876
- <tr>
877
- <th>Status</th>
878
- <th>Instance ID</th>
879
- <th>Display Name</th>
880
- <th>Role</th>
881
- <th>Environment</th>
882
- <th>Last Seen</th>
883
- <th>Actions</th>
884
- </tr>
885
- </thead>
886
- <tbody id="instancesBody"></tbody>
887
- </table>
888
- </div>
889
- </div>
890
-
891
- <div class="empty" id="instancesEmpty" style="display:none">
892
- <div class="empty-icon">&#9673;</div>
893
- <p>No instances registered yet.</p>
894
- <p style="margin-top:8px;font-size:11px;color:var(--text3)">Use <code>npx e2e-runner sync add-instance</code> to register agents.</p>
895
- </div>
896
- </div>
897
-
898
892
  <!-- ════════════════ Live View ════════════════ -->
899
893
  <div class="view" id="view-live">
900
894
  <div class="live-panel active" id="livePanel">
@@ -914,7 +908,19 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
914
908
  <button class="live-clear-btn" id="liveClearBtn">Clear All</button>
915
909
  </div>
916
910
  <div class="live-progress"><div class="live-progress-fill" id="liveProgressFill" style="width:0"></div></div>
917
- <div class="live-tests" id="liveTests"></div>
911
+ <div class="live-body">
912
+ <div class="live-tests" id="liveTests"></div>
913
+ <div class="screencast-panel" id="screencastPanel" style="display:none">
914
+ <div class="screencast-header">
915
+ <span class="screencast-label">&#128249; Screencast</span>
916
+ <select id="screencastSelect" class="screencast-select"><option value="">Select test...</option></select>
917
+ </div>
918
+ <div class="screencast-viewport">
919
+ <img id="screencastImg" alt="Browser screencast" />
920
+ <div class="screencast-placeholder" id="screencastPlaceholder">Select a running test to watch</div>
921
+ </div>
922
+ </div>
923
+ </div>
918
924
  </div>
919
925
  <div class="empty" id="liveEmpty">
920
926
  <div class="empty-icon" style="font-size:48px;opacity:.3">&#9679;</div>
@@ -1128,6 +1134,26 @@ function createTriggerBadge(source){
1128
1134
  return badge;
1129
1135
  }
1130
1136
 
1137
+ function createDriverBadge(driver){
1138
+ if(!driver)return document.createTextNode('--');
1139
+ var labels={browserless:'Browserless',cdp:'CDP',steel:'Steel',auto:'Auto'};
1140
+ var colors={browserless:'var(--accent)',cdp:'var(--purple)',steel:'var(--amber)'};
1141
+ var icons={browserless:'\u{1F310}',cdp:'\u{1F50C}',steel:'\u{1F6E1}'};
1142
+ // Handle multi-driver (e.g. "browserless,steel")
1143
+ var parts=driver.split(',');
1144
+ if(parts.length>1){
1145
+ var wrap=el('span',{style:'display:inline-flex;gap:4px'});
1146
+ parts.forEach(function(d){wrap.appendChild(createDriverBadge(d.trim()))});
1147
+ return wrap;
1148
+ }
1149
+ var d=driver.trim();
1150
+ var badge=el('span',{className:'driver-badge drv-'+d,style:'color:'+(colors[d]||'var(--text3)')},[
1151
+ el('span',{className:'drv-icon'},icons[d]||'\u2699'),
1152
+ document.createTextNode(labels[d]||d)
1153
+ ]);
1154
+ return badge;
1155
+ }
1156
+
1131
1157
  /* ── Pool Distribution Summary ── */
1132
1158
  var POOL_COLORS=['#6366f1','#22d3ee','#f59e0b','#10b981','#ef4444','#8b5cf6','#ec4899','#14b8a6'];
1133
1159
  function buildPoolDistribution(tests){
@@ -1186,7 +1212,6 @@ function showView(v){
1186
1212
  if(viewEl)viewEl.classList.add('active');
1187
1213
  if(v==='watch'&&typeof startWatchPolling==='function')startWatchPolling();
1188
1214
  else if(typeof stopWatchPolling==='function')stopWatchPolling();
1189
- if(v==='instances'&&typeof refreshInstances==='function')refreshInstances();
1190
1215
  }
1191
1216
 
1192
1217
  /* ── Inner Tabs ── */
@@ -1259,6 +1284,8 @@ function triggerRun(suite,projectId){
1259
1284
  if(suite)body.suite=suite;
1260
1285
  if(projectId)body.projectId=projectId;
1261
1286
  else if(S.project)body.projectId=S.project;
1287
+ var scToggle=$('#screencastToggle');
1288
+ if(scToggle&&scToggle.checked)body.screencast=true;
1262
1289
  fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1263
1290
  }
1264
1291
 
@@ -1293,170 +1320,7 @@ function renderPool(d){
1293
1320
  poolList.style.display='none';
1294
1321
  }
1295
1322
  }
1296
- function refreshStatus(){
1297
- api('/api/status').then(function(d){
1298
- renderPool(d.pool);
1299
- // Check if sync is enabled and update UI
1300
- if(d.config && d.config.sync){
1301
- renderSyncStatus(d.config.sync);
1302
- }
1303
- }).catch(function(){});
1304
- }
1305
-
1306
- /* ── Sync ── */
1307
- var syncMode = null;
1308
-
1309
- function renderSyncStatus(sync){
1310
- var status=$('#syncStatus');
1311
- var dot=$('#syncDot');
1312
- var mode=$('#syncMode');
1313
- var details=$('#syncDetails');
1314
- var instances=$('#syncInstances');
1315
-
1316
- if(!sync || sync.mode === 'standalone'){
1317
- status.style.display='none';
1318
- $('#navInstances').style.display='none';
1319
- syncMode = null;
1320
- return;
1321
- }
1322
-
1323
- status.style.display='block';
1324
- syncMode = sync.mode;
1325
-
1326
- mode.textContent = sync.mode;
1327
- mode.className = 'sync-mode ' + sync.mode;
1328
-
1329
- if(sync.mode === 'hub'){
1330
- dot.className = 'pool-dot on';
1331
- dot.style.background = 'var(--purple)';
1332
- details.textContent = 'Accepting agent connections';
1333
- $('#navInstances').style.display = 'flex';
1334
- refreshInstances();
1335
- } else if(sync.mode === 'agent'){
1336
- var hubUrl = sync.agent && sync.agent.hubUrl;
1337
- if(hubUrl){
1338
- dot.className = 'pool-dot on';
1339
- dot.style.background = 'var(--accent)';
1340
- details.innerHTML = 'Hub: <strong>' + hubUrl + '</strong>';
1341
- } else {
1342
- dot.className = 'pool-dot off';
1343
- details.textContent = 'Not connected';
1344
- }
1345
- $('#navInstances').style.display = 'none';
1346
- }
1347
- }
1348
-
1349
- function refreshInstances(){
1350
- if(syncMode !== 'hub') return;
1351
-
1352
- api('/api/sync/instances').then(function(d){
1353
- var instances = d.instances || [];
1354
- var online = 0;
1355
- var active = 0;
1356
- var pending = 0;
1357
- var now = Date.now();
1358
-
1359
- instances.forEach(function(inst){
1360
- if(inst.status === 'active') active++;
1361
- if(inst.status === 'pending') pending++;
1362
- if(inst.lastSeen){
1363
- var lastSeen = new Date(inst.lastSeen + 'Z').getTime();
1364
- if(now - lastSeen < 5 * 60 * 1000) online++;
1365
- }
1366
- });
1367
-
1368
- $('#statInstancesTotal').textContent = instances.length;
1369
- $('#statInstancesOnline').textContent = online;
1370
- $('#statInstancesActive').textContent = active;
1371
- $('#statInstancesPending').textContent = pending;
1372
- $('#badgeInstances').textContent = online + '/' + instances.length;
1373
-
1374
- var tbody = $('#instancesBody');
1375
- tbody.innerHTML = '';
1376
-
1377
- if(instances.length === 0){
1378
- $('#instancesEmpty').style.display = 'block';
1379
- return;
1380
- }
1381
- $('#instancesEmpty').style.display = 'none';
1382
-
1383
- instances.forEach(function(inst){
1384
- var isOnline = inst.lastSeen && (now - new Date(inst.lastSeen + 'Z').getTime() < 5 * 60 * 1000);
1385
- var statusClass = inst.status === 'active' ? 'pass' : inst.status === 'pending' ? 'flaky' : 'fail';
1386
-
1387
- var tr = el('tr', null, [
1388
- el('td', null, [
1389
- el('span', {className: 'pool-dot ' + (isOnline ? 'on' : 'off'), style: 'margin-right:6px'}),
1390
- el('span', {className: 'badge ' + statusClass}, inst.status)
1391
- ]),
1392
- el('td', {style: 'font-family:var(--mono)'}, inst.instanceId),
1393
- el('td', null, inst.displayName),
1394
- el('td', null, inst.role),
1395
- el('td', null, inst.environment || '-'),
1396
- el('td', {style: 'color:var(--text3);font-size:11px'}, inst.lastSeen ? fdate(inst.lastSeen) : 'never'),
1397
- el('td', null, [
1398
- inst.status === 'pending' ? el('button', {className: 'btn sm', onclick: function(e){
1399
- e.stopPropagation();
1400
- approveInstance(inst.instanceId);
1401
- }}, 'Approve') : null,
1402
- inst.status === 'active' ? el('button', {className: 'btn sm danger', onclick: function(e){
1403
- e.stopPropagation();
1404
- revokeInstance(inst.instanceId);
1405
- }}, 'Suspend') : null
1406
- ])
1407
- ]);
1408
- tbody.appendChild(tr);
1409
- });
1410
-
1411
- // Update sync status sidebar with online instances
1412
- var syncInst = $('#syncInstances');
1413
- syncInst.innerHTML = '';
1414
- instances.slice(0, 5).forEach(function(inst){
1415
- var isOnline = inst.lastSeen && (now - new Date(inst.lastSeen + 'Z').getTime() < 5 * 60 * 1000);
1416
- var span = el('span', {className: 'sync-inst ' + (isOnline ? 'online' : 'offline')}, [
1417
- el('span', {className: 'pool-dot ' + (isOnline ? 'on' : 'off'), style: 'width:5px;height:5px'}),
1418
- document.createTextNode(inst.instanceId.slice(0, 12))
1419
- ]);
1420
- syncInst.appendChild(span);
1421
- });
1422
- if(instances.length > 5){
1423
- syncInst.appendChild(el('span', {style: 'font-size:9px;color:var(--text3)'}, '+' + (instances.length - 5) + ' more'));
1424
- }
1425
-
1426
- }).catch(function(err){
1427
- console.error('Failed to load instances:', err);
1428
- });
1429
- }
1430
-
1431
- function approveInstance(instanceId){
1432
- api('/api/sync/instances/' + instanceId, {
1433
- method: 'PATCH',
1434
- body: JSON.stringify({status: 'active'})
1435
- }).then(function(){
1436
- showToast('Instance approved', 'success');
1437
- refreshInstances();
1438
- }).catch(function(err){
1439
- showToast('Failed to approve: ' + err.message, 'error');
1440
- });
1441
- }
1442
-
1443
- function revokeInstance(instanceId){
1444
- api('/api/sync/instances/' + instanceId, {
1445
- method: 'PATCH',
1446
- body: JSON.stringify({status: 'suspended'})
1447
- }).then(function(){
1448
- showToast('Instance suspended', 'success');
1449
- refreshInstances();
1450
- }).catch(function(err){
1451
- showToast('Failed to suspend: ' + err.message, 'error');
1452
- });
1453
- }
1454
-
1455
- $('#refreshInstances').addEventListener('click', refreshInstances);
1456
- $('#instanceFilter').addEventListener('change', function(){
1457
- // For now just refresh - could add client-side filtering
1458
- refreshInstances();
1459
- });
1323
+ function refreshStatus(){api('/api/status').then(function(d){renderPool(d.pool)}).catch(function(){})}
1460
1324
 
1461
1325
  /* ── Projects ── */
1462
1326
  function refreshProjects(){
@@ -1583,6 +1447,12 @@ function handleWS(m){
1583
1447
  var r7=getLiveRun(m);if(r7){r7.on=false;r7.done=true;r7.tests.__error={status:'failed',error:m.error}}
1584
1448
  showToast('Run error: '+m.error,'error');
1585
1449
  renderLive();break;
1450
+ case 'test:frame':
1451
+ if(S.screencastTest===m.name&&m.data){
1452
+ var img=$('#screencastImg');
1453
+ if(img)img.src='data:image/jpeg;base64,'+m.data;
1454
+ }
1455
+ break;
1586
1456
  case 'db:updated':
1587
1457
  refreshRuns();refreshProjects();refreshScreenshots();refreshLearnings();refreshWatch();break;
1588
1458
  }
@@ -2222,7 +2092,7 @@ function refreshRuns(){
2222
2092
  var htr=document.createElement('tr');
2223
2093
  var cols=[];
2224
2094
  if(!S.project)cols.push('Project');
2225
- cols=cols.concat(['Suite','Source','Date','Total','Pass','Fail','Rate','Time']);
2095
+ cols=cols.concat(['Suite','Driver','Source','Date','Total','Pass','Fail','Rate','Time']);
2226
2096
  cols.forEach(function(c){htr.appendChild(el('th',null,c))});
2227
2097
  head.textContent='';head.appendChild(htr);
2228
2098
  var colSpan=cols.length;
@@ -2241,6 +2111,7 @@ function refreshRuns(){
2241
2111
  if(r.id===S.selectedRun)tr.classList.add('expanded');
2242
2112
  if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
2243
2113
  tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
2114
+ var driverTd=document.createElement('td');driverTd.appendChild(createDriverBadge(r.pool_driver));tr.appendChild(driverTd);
2244
2115
  var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
2245
2116
  tr.appendChild(el('td',null,fdate(r.generated_at)));
2246
2117
  tr.appendChild(el('td',null,String(r.total||0)));
@@ -2337,8 +2208,10 @@ function loadDetailInline(id,detailTr){
2337
2208
  ])
2338
2209
  ]);
2339
2210
  var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
2211
+ var drvBlock=el('div',null,[el('div',{className:'rd-s-label'},'Driver'),el('div',{style:'margin-top:4px'},[createDriverBadge(d.poolDriver)])]);
2340
2212
  var summ=el('div',{className:'rd-summary'},[
2341
2213
  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')]),
2214
+ drvBlock,
2342
2215
  srcBlock,
2343
2216
  el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
2344
2217
  el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
@@ -2570,10 +2443,8 @@ function refreshLearnings(){
2570
2443
  fetch(url).then(function(r){return r.json()}).then(function(data){
2571
2444
  if(!data||data.totalRuns===0){
2572
2445
  $('#learningsEmpty').style.display='block';
2573
- $('#learningsOverview').textContent='';$('#learningsTrend').textContent='';
2574
- $('#learningsFlaky').textContent='';$('#learningsSelectors').textContent='';
2575
- $('#learningsPages').textContent='';$('#learningsApis').textContent='';
2576
- $('#learningsErrors').textContent='';
2446
+ $('#learnHero').textContent='';$('#learnCards').textContent='';
2447
+ $('#learnTrend').textContent='';$('#learnBottom').textContent='';
2577
2448
  $('#badgeLearnings').textContent='-';
2578
2449
  return;
2579
2450
  }
@@ -2598,48 +2469,139 @@ function refreshLearnings(){
2598
2469
  $('#badgeLearnings').textContent='\u2714';
2599
2470
  $('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
2600
2471
  }
2601
- renderLearnOverview(data);
2472
+ renderLearnHero(data);
2473
+ renderLearnCards(data);
2602
2474
  renderLearnTrend(data.recentTrend||[]);
2603
- renderLearnFlaky(data.flakyTests||[]);
2604
- renderLearnSelectors(data.unstableSelectors||[]);
2605
- renderLearnPages(data.failingPages||[]);
2606
- renderLearnApis(data.apiIssues||[]);
2607
- renderLearnErrors(data.topErrors||[]);
2475
+ renderLearnBottomRow(data);
2608
2476
  }).catch(function(){$('#learningsEmpty').style.display='block'});
2609
2477
  }
2610
2478
 
2611
- function renderLearnOverview(d){
2612
- var container=$('#learningsOverview');container.textContent='';
2613
- var grid=document.createElement('div');grid.className='learn-grid';
2614
- [{val:d.totalRuns,lbl:'Runs',cls:'accent'},{val:d.totalTests,lbl:'Tests',cls:'accent'},
2615
- {val:d.overallPassRate+'%',lbl:'Pass Rate',cls:d.overallPassRate>=90?'green':d.overallPassRate>=70?'':'red'},
2616
- {val:d.avgDurationMs<1000?d.avgDurationMs+'ms':(d.avgDurationMs/1000).toFixed(1)+'s',lbl:'Avg Duration',cls:'purple'},
2617
- {val:(d.flakyTests?d.flakyTests.length:0),lbl:'Flaky Tests',cls:d.flakyTests&&d.flakyTests.length>0?'red':'green'},
2618
- {val:(d.unstableSelectors?d.unstableSelectors.length:0),lbl:'Unstable Selectors',cls:d.unstableSelectors&&d.unstableSelectors.length>0?'red':'green'}
2619
- ].forEach(function(item){
2620
- var stat=document.createElement('div');stat.className='learn-stat';
2621
- var valEl=document.createElement('div');valEl.className='learn-stat-val '+item.cls;valEl.textContent=item.val;
2622
- var lblEl=document.createElement('div');lblEl.className='learn-stat-lbl';lblEl.textContent=item.lbl;
2623
- stat.appendChild(valEl);stat.appendChild(lblEl);grid.appendChild(stat);
2479
+ function rateColor(v){return v>=90?'var(--green)':v>=70?'var(--amber)':'var(--red)'}
2480
+ function rateClass(v){return v>=90?'good':v>=70?'warn':'bad'}
2481
+ function durFmt(ms){return ms<1000?Math.round(ms)+'ms':(ms/1000).toFixed(1)+'s'}
2482
+
2483
+ function renderLearnHero(d){
2484
+ var c=$('#learnHero');c.textContent='';
2485
+ var wrap=document.createElement('div');wrap.className='learn-hero';
2486
+ var passRate=d.overallPassRate||0;
2487
+ var ns='http://www.w3.org/2000/svg';
2488
+ var ringWrap=document.createElement('div');ringWrap.className='learn-hero-ring';
2489
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 36 36');
2490
+ 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);
2491
+ 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';
2492
+ 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));
2493
+ svg.appendChild(fgCircle);ringWrap.appendChild(svg);
2494
+ var pctEl=document.createElement('div');pctEl.className='learn-hero-pct';pctEl.style.color=rateColor(passRate);pctEl.textContent=passRate+'%';
2495
+ ringWrap.appendChild(pctEl);wrap.appendChild(ringWrap);
2496
+
2497
+ var stats=document.createElement('div');stats.className='learn-hero-stats';
2498
+ var badSels=d.unstableSelectors?d.unstableSelectors.length:0;
2499
+ var slowTests=d.failingPages?d.failingPages.length:0;
2500
+ var apiIssues=d.apiIssues?d.apiIssues.length:0;
2501
+ var topErr=d.topErrors&&d.topErrors.length>0?d.topErrors[0].occurrence_count:0;
2502
+ var flakyCount=d.flakyTests?d.flakyTests.length:0;
2503
+ var items=[
2504
+ {val:String(d.totalRuns),lbl:'Runs',color:'var(--accent)'},
2505
+ {val:String(d.totalTests),lbl:'Tests',color:'var(--accent)'},
2506
+ {val:durFmt(d.avgDurationMs||0),lbl:'Avg Duration',color:'var(--purple)'},
2507
+ {val:String(flakyCount),lbl:'Flaky',color:flakyCount>0?'var(--amber)':'var(--green)'},
2508
+ {val:String(badSels),lbl:'Bad Selectors',color:badSels>0?'var(--red)':'var(--green)'},
2509
+ {val:String(slowTests),lbl:'Slow Pages',color:slowTests>0?'var(--amber)':'var(--green)'},
2510
+ {val:String(apiIssues),lbl:'API Issues',color:apiIssues>0?'var(--red)':'var(--green)'},
2511
+ {val:String(topErr),lbl:'Top Error Hits',color:topErr>0?'var(--red)':'var(--green)'}
2512
+ ];
2513
+ items.forEach(function(it){
2514
+ var statEl=document.createElement('div');statEl.className='learn-hero-stat';
2515
+ var valEl=document.createElement('div');valEl.className='learn-hero-stat-val';valEl.style.color=it.color;valEl.textContent=it.val;
2516
+ var lblEl=document.createElement('div');lblEl.className='learn-hero-stat-lbl';lblEl.textContent=it.lbl;
2517
+ statEl.appendChild(valEl);statEl.appendChild(lblEl);stats.appendChild(statEl);
2624
2518
  });
2625
- container.appendChild(grid);
2519
+ wrap.appendChild(stats);c.appendChild(wrap);
2520
+ }
2521
+
2522
+ function makeLearnItem(label,sub,pct,valText,color){
2523
+ var item=document.createElement('div');item.className='learn-item';
2524
+ var barWrap=document.createElement('div');barWrap.className='learn-item-bar';
2525
+ var lblEl=document.createElement('div');lblEl.className='learn-item-label';
2526
+ var codeEl=document.createElement('code');codeEl.textContent=label;lblEl.appendChild(codeEl);
2527
+ barWrap.appendChild(lblEl);
2528
+ if(sub){var subEl=document.createElement('div');subEl.className='learn-item-sub';subEl.textContent=sub;barWrap.appendChild(subEl)}
2529
+ var bar=document.createElement('div');bar.className='learn-bar';
2530
+ var fill=document.createElement('div');fill.className='learn-bar-fill';fill.style.width=Math.min(pct,100)+'%';fill.style.background=color;
2531
+ bar.appendChild(fill);barWrap.appendChild(bar);
2532
+ item.appendChild(barWrap);
2533
+ var valEl=document.createElement('div');valEl.className='learn-item-val';valEl.style.color=color;valEl.textContent=valText;
2534
+ item.appendChild(valEl);
2535
+ return item;
2536
+ }
2537
+
2538
+ function makeLearnCard(icon,title,emptyMsg){
2539
+ var card=document.createElement('div');card.className='learn-card';
2540
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
2541
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent=icon;
2542
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode(title));
2543
+ card.appendChild(titleEl);
2544
+ card._empty=emptyMsg;
2545
+ return card;
2546
+ }
2547
+
2548
+ function renderLearnCards(d){
2549
+ var c=$('#learnCards');c.textContent='';
2550
+
2551
+ var selCard=makeLearnCard('\u26A0','Risky Selectors','No unstable selectors');
2552
+ var sels=d.unstableSelectors||[];
2553
+ if(!sels.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=selCard._empty;selCard.appendChild(e1)}
2554
+ else{sels.slice(0,5).forEach(function(s){
2555
+ var sel=s.selector.length>40?s.selector.slice(0,37)+'...':s.selector;
2556
+ 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)'));
2557
+ })}
2558
+ c.appendChild(selCard);
2559
+
2560
+ var pageCard=makeLearnCard('\u23F1','Problem Pages','No failing pages');
2561
+ var pages=d.failingPages||[];
2562
+ if(!pages.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=pageCard._empty;pageCard.appendChild(e2)}
2563
+ else{pages.slice(0,5).forEach(function(p){
2564
+ 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)'));
2565
+ })}
2566
+ c.appendChild(pageCard);
2567
+
2568
+ var flakyCard=makeLearnCard('\u223C','Flaky Tests','No flaky tests detected');
2569
+ var flaky=d.flakyTests||[];
2570
+ if(!flaky.length){var e3=document.createElement('div');e3.className='learn-card-empty';e3.textContent=flakyCard._empty;flakyCard.appendChild(e3)}
2571
+ else{flaky.slice(0,5).forEach(function(f){
2572
+ 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)'));
2573
+ })}
2574
+ c.appendChild(flakyCard);
2575
+
2576
+ var apiCard=makeLearnCard('\u21C4','API Issues','No API issues');
2577
+ var apis=d.apiIssues||[];
2578
+ if(!apis.length){var e4=document.createElement('div');e4.className='learn-card-empty';e4.textContent=apiCard._empty;apiCard.appendChild(e4)}
2579
+ else{apis.slice(0,5).forEach(function(a){
2580
+ var ep=a.endpoint.length>40?a.endpoint.slice(0,37)+'...':a.endpoint;
2581
+ 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)'));
2582
+ })}
2583
+ c.appendChild(apiCard);
2626
2584
  }
2627
2585
 
2628
2586
  function renderLearnTrend(trend){
2629
- var container=$('#learningsTrend');container.textContent='';
2587
+ var container=$('#learnTrend');container.textContent='';
2630
2588
  if(!trend.length)return;
2631
- var card=document.createElement('div');card.className='card';
2632
- var label=document.createElement('div');label.className='card-label';label.textContent='Pass Rate Trend (7 days)';card.appendChild(label);
2633
- var chartDiv=document.createElement('div');chartDiv.className='learn-trend-chart';
2589
+ var card=document.createElement('div');card.className='learn-card';
2590
+ var titleEl=document.createElement('div');titleEl.className='learn-card-title';
2591
+ var iconEl=document.createElement('span');iconEl.className='lc-icon';iconEl.textContent='\u2197';
2592
+ titleEl.appendChild(iconEl);titleEl.appendChild(document.createTextNode('Pass Rate Trend'));
2593
+ card.appendChild(titleEl);
2594
+ var chartDiv=document.createElement('div');chartDiv.style.cssText='height:80px;width:100%';
2634
2595
  var w=100/trend.length;var ns='http://www.w3.org/2000/svg';
2635
- var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');
2596
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');svg.style.cssText='width:100%;height:100%';
2636
2597
  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);
2637
2598
  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);
2638
2599
  var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
2639
2600
  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);
2640
2601
  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);
2641
2602
  trend.forEach(function(t,i){
2642
- 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)');
2603
+ var color=rateColor(t.pass_rate);
2604
+ 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);
2643
2605
  var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
2644
2606
  });
2645
2607
  chartDiv.appendChild(svg);card.appendChild(chartDiv);
@@ -2648,35 +2610,47 @@ function renderLearnTrend(trend){
2648
2610
  card.appendChild(dates);container.appendChild(card);
2649
2611
  }
2650
2612
 
2651
- function buildLearnTable(title,headers,rows){
2652
- var card=document.createElement('div');card.className='card learn-section';
2653
- var h=document.createElement('div');h.className='learn-section-title';h.textContent=title;card.appendChild(h);
2654
- var wrap=document.createElement('div');wrap.className='tbl-wrap';
2655
- var tbl=document.createElement('table');tbl.className='learn-table';
2656
- var thead=document.createElement('thead');var hr=document.createElement('tr');
2657
- headers.forEach(function(hdr){var th=document.createElement('th');th.textContent=hdr;hr.appendChild(th)});
2658
- thead.appendChild(hr);tbl.appendChild(thead);
2659
- var tbody=document.createElement('tbody');
2660
- rows.forEach(function(cells){
2661
- var tr=document.createElement('tr');
2662
- cells.forEach(function(cell){
2663
- var td=document.createElement('td');
2664
- if(cell.code){var code=document.createElement('code');code.textContent=cell.code;td.appendChild(code)}
2665
- else if(cell.badge){var span=document.createElement('span');span.className='badge '+cell.cls;span.textContent=cell.badge;td.appendChild(span)}
2666
- else{td.textContent=cell.text!==undefined&&cell.text!==null?cell.text:(typeof cell==='object'?'-':cell)}
2667
- tr.appendChild(td);
2613
+ function renderLearnBottomRow(d){
2614
+ var c=$('#learnBottom');c.textContent='';
2615
+
2616
+ var errCard=makeLearnCard('\u2718','Most Common Errors','No errors recorded');
2617
+ var errors=d.topErrors||[];
2618
+ if(!errors.length){var e1=document.createElement('div');e1.className='learn-card-empty';e1.textContent=errCard._empty;errCard.appendChild(e1)}
2619
+ else{errors.slice(0,5).forEach(function(e){
2620
+ var pat=e.pattern.length>45?e.pattern.slice(0,42)+'...':e.pattern;
2621
+ var maxCount=errors[0].occurrence_count||1;
2622
+ var pct=(e.occurrence_count/maxCount)*100;
2623
+ var verdictEl=document.createElement('div');verdictEl.className='learn-verdict '+rateClass(100-(pct));verdictEl.textContent=e.category.replace(/-/g,' ');
2624
+ var item=makeLearnItem(pat,(e.last_seen||'').split('T')[0]+' \u00B7 '+e.occurrence_count+'x',pct,e.occurrence_count+'x','var(--red)');
2625
+ item.insertBefore(verdictEl,item.lastChild);
2626
+ errCard.appendChild(item);
2627
+ })}
2628
+ c.appendChild(errCard);
2629
+
2630
+ var slowCard=makeLearnCard('\u23F3','Slowest Tests','No slow test data');
2631
+ var trend=d.recentTrend||[];
2632
+ var slowTests=[];
2633
+ if(d.flakyTests){
2634
+ d.flakyTests.forEach(function(f){
2635
+ if(f.avg_duration_ms&&f.avg_duration_ms>2000){slowTests.push({name:f.test_name,dur:f.avg_duration_ms})}
2668
2636
  });
2669
- tbody.appendChild(tr);
2670
- });
2671
- tbl.appendChild(tbody);wrap.appendChild(tbl);card.appendChild(wrap);return card;
2637
+ }
2638
+ if(d.failingPages){
2639
+ d.failingPages.forEach(function(p){
2640
+ if(p.avg_load_time_ms&&p.avg_load_time_ms>3000){slowTests.push({name:p.url_path,dur:p.avg_load_time_ms})}
2641
+ });
2642
+ }
2643
+ slowTests.sort(function(a,b){return b.dur-a.dur});
2644
+ if(!slowTests.length){var e2=document.createElement('div');e2.className='learn-card-empty';e2.textContent=slowCard._empty;slowCard.appendChild(e2)}
2645
+ else{
2646
+ var maxDur=slowTests[0].dur;
2647
+ slowTests.slice(0,5).forEach(function(t){
2648
+ slowCard.appendChild(makeLearnItem(t.name,'','',durFmt(t.dur),(t.dur/maxDur)*100,t.dur>5000?'var(--red)':'var(--amber)'));
2649
+ });
2650
+ }
2651
+ c.appendChild(slowCard);
2672
2652
  }
2673
2653
 
2674
- 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}]})))}
2675
- 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||'-'}]})))}
2676
- 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}]})))}
2677
- 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||'-'}]})))}
2678
- 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||'-'}]})))}
2679
-
2680
2654
  $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
2681
2655
  $('#learningsDays').addEventListener('change',refreshLearnings);
2682
2656
 
@@ -2710,10 +2684,41 @@ $('#modal').addEventListener('click',function(){$('#modal').classList.remove('op
2710
2684
  /* ══════════════════════════════════════════════════════════════════
2711
2685
  Live Execution View
2712
2686
  ══════════════════════════════════════════════════════════════════ */
2713
- function clearFinishedLiveRuns(){for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}renderLive()}
2687
+ function clearFinishedLiveRuns(){for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}S.screencastTest=null;renderLive()}
2714
2688
  function dismissLiveRun(rid){delete S.liveRuns[rid];renderLive()}
2715
2689
  $('#liveClearBtn').addEventListener('click',clearFinishedLiveRuns);
2716
2690
 
2691
+ // Screencast state
2692
+ S.screencastTest=null;
2693
+
2694
+ $('#screencastSelect').addEventListener('change',function(){
2695
+ S.screencastTest=this.value||null;
2696
+ var img=$('#screencastImg'),ph=$('#screencastPlaceholder');
2697
+ if(S.screencastTest){img.style.display='block';ph.style.display='none';img.src=''}
2698
+ else{img.style.display='none';ph.style.display='flex'}
2699
+ });
2700
+
2701
+ function updateScreencastSelect(){
2702
+ var sel=$('#screencastSelect'),panel=$('#screencastPanel');
2703
+ var runningTests=[];
2704
+ for(var k in S.liveRuns){var r=S.liveRuns[k];for(var n in r.tests){if(n!=='__error'&&r.tests[n].status==='running')runningTests.push(n)}}
2705
+ // Show panel if any run is active
2706
+ var anyActive=false;for(var k2 in S.liveRuns)if(S.liveRuns[k2].on)anyActive=true;
2707
+ panel.style.display=anyActive?'':'none';
2708
+ // Rebuild options
2709
+ var prev=sel.value;
2710
+ while(sel.options.length>1)sel.remove(1);
2711
+ runningTests.forEach(function(n){var o=document.createElement('option');o.value=n;o.textContent=n;sel.appendChild(o)});
2712
+ // Auto-select first running test if nothing selected
2713
+ if(!S.screencastTest&&runningTests.length>0){S.screencastTest=runningTests[0];sel.value=S.screencastTest;$('#screencastImg').style.display='block';$('#screencastPlaceholder').style.display='none'}
2714
+ else if(S.screencastTest&&runningTests.indexOf(S.screencastTest)===-1){
2715
+ // Current test finished — pick next running or clear
2716
+ if(runningTests.length>0){S.screencastTest=runningTests[0];sel.value=S.screencastTest}
2717
+ else{S.screencastTest=null;sel.value='';$('#screencastImg').style.display='none';$('#screencastPlaceholder').style.display='flex';$('#screencastPlaceholder').textContent='No running tests'}
2718
+ }
2719
+ else{sel.value=S.screencastTest||''}
2720
+ }
2721
+
2717
2722
  function renderLive(){
2718
2723
  var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
2719
2724
  var runs=S.liveRuns;var runIds=Object.keys(runs);
@@ -2824,12 +2829,18 @@ function renderLive(){
2824
2829
  ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
2825
2830
  }
2826
2831
 
2832
+ // Screencast focus indicator
2833
+ var scFocusBadge=null;
2834
+ if(t.status==='running'){
2835
+ var isFocused=S.screencastTest===name;
2836
+ scFocusBadge=el('span',{className:'sc-focus-badge'+(isFocused?' active':''),title:'Watch this test',onclick:function(e){e.stopPropagation();S.screencastTest=name;$('#screencastSelect').value=name;$('#screencastImg').style.display='block';$('#screencastPlaceholder').style.display='none';renderLive()}},'\uD83C\uDFA5');
2837
+ }
2827
2838
  var serialBadge=t.serial?el('span',{className:'serial-badge'},'Serial'):null;
2828
2839
  var poolBadge=t.poolUrl?el('span',{className:'pool-badge'},t.poolUrl.replace('ws://','').replace('wss://','')):null;
2829
2840
  var card=el('div',{className:'live-test '+t.status+(isCollapsed?' collapsed':'')},[
2830
2841
  el('div',{className:'lt-name'},[
2831
2842
  t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
2832
- document.createTextNode(' '+name),serialBadge,poolBadge,summaryEl
2843
+ document.createTextNode(' '+name),scFocusBadge,serialBadge,poolBadge,summaryEl
2833
2844
  ]),
2834
2845
  el('div',{className:'lt-meta'},meta),stepsEl
2835
2846
  ]);
@@ -2849,6 +2860,7 @@ function renderLive(){
2849
2860
  });
2850
2861
  grid.appendChild(testGrid);
2851
2862
  });
2863
+ updateScreencastSelect();
2852
2864
  }
2853
2865
 
2854
2866
 
@@ -2875,7 +2887,7 @@ document.addEventListener('keydown',function(e){
2875
2887
  return;
2876
2888
  }
2877
2889
  if(e.key==='?'){$('#kbModal').classList.toggle('open');return}
2878
- var viewMap={'1':'watch','2':'tests','3':'runs','4':'live','5':'instances'};
2890
+ var viewMap={'1':'watch','2':'tests','3':'runs','4':'live'};
2879
2891
  if(viewMap[e.key]){showView(viewMap[e.key]);return}
2880
2892
  if(e.key==='r'){
2881
2893
  if(S.view==='watch')refreshWatch();