@matware/e2e-runner 1.1.1 → 1.3.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 (89) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/.mcp.json +9 -0
  4. package/.opencode/commands/create-test.md +63 -0
  5. package/.opencode/commands/run.md +50 -0
  6. package/.opencode/commands/verify-issue.md +62 -0
  7. package/.opencode/skills/e2e-testing/SKILL.md +181 -0
  8. package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
  9. package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
  10. package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
  11. package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
  12. package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
  13. package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
  14. package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
  15. package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
  16. package/.opencode/skills/e2e-testing/references/variables.md +41 -0
  17. package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
  18. package/OPENCODE.md +166 -0
  19. package/README.md +990 -296
  20. package/agents/test-analyzer.md +81 -0
  21. package/agents/test-creator.md +155 -0
  22. package/agents/test-improver.md +177 -0
  23. package/bin/cli.js +602 -22
  24. package/commands/create-test.md +65 -0
  25. package/commands/run.md +49 -0
  26. package/commands/verify-issue.md +63 -0
  27. package/opencode.json +11 -0
  28. package/package.json +15 -2
  29. package/scripts/setup-opencode.sh +113 -0
  30. package/skills/e2e-testing/SKILL.md +173 -0
  31. package/skills/e2e-testing/references/action-types.md +143 -0
  32. package/skills/e2e-testing/references/auth-strategies.md +91 -0
  33. package/skills/e2e-testing/references/graphql.md +59 -0
  34. package/skills/e2e-testing/references/issue-verification.md +59 -0
  35. package/skills/e2e-testing/references/multi-pool.md +60 -0
  36. package/skills/e2e-testing/references/network-debugging.md +62 -0
  37. package/skills/e2e-testing/references/test-json-format.md +163 -0
  38. package/skills/e2e-testing/references/troubleshooting.md +224 -0
  39. package/skills/e2e-testing/references/variables.md +41 -0
  40. package/skills/e2e-testing/references/visual-verification.md +89 -0
  41. package/src/actions.js +597 -20
  42. package/src/ai-generate.js +142 -12
  43. package/src/config.js +171 -0
  44. package/src/dashboard.js +299 -17
  45. package/src/db.js +335 -13
  46. package/src/index.js +15 -8
  47. package/src/learner-markdown.js +177 -0
  48. package/src/learner-neo4j.js +255 -0
  49. package/src/learner-sqlite.js +658 -0
  50. package/src/learner.js +418 -0
  51. package/src/mcp-tools.js +1558 -50
  52. package/src/module-resolver.js +310 -0
  53. package/src/narrate.js +262 -0
  54. package/src/neo4j-pool.js +124 -0
  55. package/src/pool-manager.js +223 -0
  56. package/src/reporter.js +117 -3
  57. package/src/runner.js +274 -71
  58. package/src/sync/auth.js +354 -0
  59. package/src/sync/client.js +572 -0
  60. package/src/sync/hub-routes.js +816 -0
  61. package/src/sync/index.js +68 -0
  62. package/src/sync/middleware.js +347 -0
  63. package/src/sync/queue.js +209 -0
  64. package/src/sync/schema.js +540 -0
  65. package/src/verify.js +14 -9
  66. package/src/watch.js +384 -0
  67. package/templates/build-dashboard.js +69 -0
  68. package/templates/dashboard/js/api.js +60 -0
  69. package/templates/dashboard/js/init.js +13 -0
  70. package/templates/dashboard/js/keyboard.js +46 -0
  71. package/templates/dashboard/js/state.js +40 -0
  72. package/templates/dashboard/js/toast.js +41 -0
  73. package/templates/dashboard/js/utils.js +196 -0
  74. package/templates/dashboard/js/view-live.js +143 -0
  75. package/templates/dashboard/js/view-runs.js +572 -0
  76. package/templates/dashboard/js/view-tests.js +294 -0
  77. package/templates/dashboard/js/view-watch.js +242 -0
  78. package/templates/dashboard/js/websocket.js +110 -0
  79. package/templates/dashboard/styles/base.css +69 -0
  80. package/templates/dashboard/styles/components.css +110 -0
  81. package/templates/dashboard/styles/view-live.css +74 -0
  82. package/templates/dashboard/styles/view-runs.css +207 -0
  83. package/templates/dashboard/styles/view-tests.css +96 -0
  84. package/templates/dashboard/styles/view-watch.css +53 -0
  85. package/templates/dashboard/template.html +267 -0
  86. package/templates/dashboard.html +2171 -530
  87. package/templates/docker-compose-neo4j.yml +19 -0
  88. package/templates/e2e.config.js +3 -0
  89. package/templates/sample-test.json +0 -8
@@ -7,6 +7,8 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
9
9
  <style>
10
+ /* ── base.css ── */
11
+ /* ── Reset & Variables ── */
10
12
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
11
13
  :root{
12
14
  --bg:#090a10;--surface:#11131b;--surface2:#181b26;--surface3:#1f2333;
@@ -51,6 +53,24 @@ a{color:var(--accent);text-decoration:none}
51
53
  .pool-info{font-size:11px;color:var(--text2);line-height:1.7}
52
54
  .pool-info strong{color:var(--text)}
53
55
  .ws-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:4px}
56
+ .pool-list{margin-top:6px;display:flex;flex-direction:column;gap:3px}
57
+ .pool-item{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--text2);font-family:var(--mono);padding:2px 0}
58
+ .pool-item .pool-dot{width:6px;height:6px;margin-right:0}
59
+ .pool-item strong{color:var(--text);font-weight:500}
60
+ .pool-item .pool-sessions{margin-left:auto;color:var(--text3);font-size:9px}
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)}
54
74
 
55
75
  /* ── Main ── */
56
76
  .main{margin-left:232px;flex:1;min-height:100vh;display:flex;flex-direction:column}
@@ -61,6 +81,18 @@ a{color:var(--accent);text-decoration:none}
61
81
  .view.active{display:block}
62
82
  #view-live.active{display:flex;flex-direction:column;flex:1;min-height:calc(100vh - 0px);padding:16px}
63
83
 
84
+ /* ── Responsive ── */
85
+ @media(max-width:768px){
86
+ .sidebar{width:60px}.sidebar-logo h1,.sidebar-section-label,.nav-item span:not(.icon):not(.badge),.pool-info,.sidebar select,.sidebar-logo .ver{display:none}
87
+ .nav-item{justify-content:center;padding:12px}.nav-item .icon{width:auto}
88
+ .main{margin-left:60px}
89
+ .suite-grid,.gallery,.module-grid{grid-template-columns:1fr}
90
+ .lr-test-grid{grid-template-columns:1fr}
91
+ .toast-container{right:12px;bottom:12px}
92
+ }
93
+
94
+
95
+ /* ── components.css ── */
64
96
  /* ── Buttons ── */
65
97
  .btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--r);font-family:var(--mono);font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text);transition:all .15s;white-space:nowrap}
66
98
  .btn:hover{background:var(--surface3);border-color:var(--border-hi)}
@@ -92,12 +124,243 @@ td{padding:8px 12px;border-bottom:1px solid var(--border)}
92
124
  tbody tr{cursor:pointer;transition:background .1s}
93
125
  tbody tr:hover td{background:var(--surface2)}
94
126
  tbody tr.selected td{background:var(--accent-dim)}
127
+
128
+ /* ── Badges ── */
95
129
  .badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600}
96
130
  .badge.pass{background:var(--green-dim);color:var(--green)}
97
131
  .badge.fail{background:var(--red-dim);color:var(--red)}
98
132
  .badge.flaky{background:var(--amber-dim);color:var(--amber)}
99
133
  .badge.run{background:var(--purple-dim);color:var(--purple)}
100
134
 
135
+ /* ── Empty ── */
136
+ .empty{text-align:center;padding:48px 24px;color:var(--text3)}
137
+ .empty-icon{font-size:36px;margin-bottom:8px;opacity:.5}
138
+
139
+ /* ── Modal ── */
140
+ .modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;display:none;align-items:center;justify-content:center;padding:24px;cursor:pointer}
141
+ .modal.open{display:flex}
142
+ .modal img{max-width:100%;max-height:90vh;border-radius:var(--r);cursor:default}
143
+
144
+ /* ── Toast Notifications ── */
145
+ .toast-container{position:fixed;bottom:24px;right:24px;z-index:300;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none}
146
+ .toast{padding:10px 16px;border-radius:var(--r);font-family:var(--mono);font-size:11px;font-weight:500;color:#fff;pointer-events:auto;animation:toastIn .3s ease;min-width:200px;max-width:380px;box-shadow:0 8px 24px rgba(0,0,0,.4);display:flex;align-items:center;gap:8px}
147
+ .toast.success{background:var(--green);border:1px solid rgba(255,255,255,.15)}
148
+ .toast.error{background:var(--red);border:1px solid rgba(255,255,255,.15)}
149
+ .toast.info{background:var(--accent);border:1px solid rgba(255,255,255,.15)}
150
+ .toast.fade-out{animation:toastOut .3s ease forwards}
151
+ @keyframes toastIn{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}
152
+ @keyframes toastOut{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(24px)}}
153
+ .toast.clickable{cursor:pointer}
154
+ .toast.clickable:hover{filter:brightness(1.1)}
155
+
156
+ /* ── Copy Button ── */
157
+ .copy-btn{display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:4px;font-size:9px;font-family:var(--mono);font-weight:500;color:var(--text3);background:transparent;border:1px solid transparent;cursor:pointer;transition:all .15s;user-select:none;white-space:nowrap;flex-shrink:0}
158
+ .copy-btn:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-dim)}
159
+ .copy-btn.copied{color:var(--green);border-color:var(--green);background:var(--green-dim)}
160
+
161
+ /* ── Screenshot Hash Badge ── */
162
+ .ss-hash{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;border-radius:10px;font-family:var(--mono);font-size:9px;font-weight:500;background:var(--surface3);border:1px solid var(--border);color:var(--text2);cursor:pointer;transition:all .15s;user-select:none;white-space:nowrap;vertical-align:middle}
163
+ .ss-hash:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
164
+ .ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
165
+ .ss-hash .ss-icon{font-size:10px;line-height:1}
166
+
167
+ /* ── Trigger Source Badges ── */
168
+ .trigger-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}
169
+ .trigger-badge.src-dashboard{background:rgba(127,140,162,.10);color:var(--text2)}
170
+ .trigger-badge.src-mcp{background:var(--purple-dim);color:var(--purple)}
171
+ .trigger-badge.src-cli{background:var(--accent-dim);color:var(--accent)}
172
+ .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
173
+ .trigger-badge .trig-icon{font-size:11px;line-height:1}
174
+
175
+ /* ── Filter Bar ── */
176
+ .filter-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
177
+ .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}
178
+ .filter-btn:hover{background:var(--surface3);border-color:var(--border-hi)}
179
+ .filter-btn.active{background:var(--accent-dim);border-color:var(--accent);color:var(--accent)}
180
+ .filter-bar input{padding:5px 10px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:11px;max-width:200px}
181
+ .filter-bar input:focus{outline:none;border-color:var(--accent)}
182
+ .filter-bar input::placeholder{color:var(--text3)}
183
+
184
+ /* ── Inner Tabs ── */
185
+ .tab-bar{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:20px}
186
+ .tab-btn{padding:8px 16px;font-family:var(--mono);font-size:11px;font-weight:500;color:var(--text3);cursor:pointer;border:none;background:transparent;border-bottom:2px solid transparent;transition:all .15s}
187
+ .tab-btn:hover{color:var(--text2);background:var(--surface2)}
188
+ .tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
189
+ .tab-pane{display:none}
190
+ .tab-pane.active{display:block}
191
+
192
+ /* ── Keyboard Shortcuts Modal ── */
193
+ .kb-modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:250;display:none;align-items:center;justify-content:center;padding:24px}
194
+ .kb-modal.open{display:flex}
195
+ .kb-modal-content{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;max-width:420px;width:100%;max-height:80vh;overflow-y:auto}
196
+ .kb-modal-content h2{font-family:var(--sans);font-size:16px;font-weight:700;margin-bottom:16px;color:var(--text)}
197
+ .kb-row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border)}
198
+ .kb-row:last-child{border-bottom:none}
199
+ .kb-key{display:inline-flex;align-items:center;justify-content:center;min-width:24px;padding:2px 8px;border-radius:4px;background:var(--surface3);border:1px solid var(--border);font-family:var(--mono);font-size:11px;font-weight:600;color:var(--accent)}
200
+ .kb-desc{font-size:12px;color:var(--text2)}
201
+
202
+ /* ── Serial / Pool Badges ── */
203
+ .serial-badge{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:8px;font-size:9px;font-weight:600;background:var(--amber-dim);color:var(--amber);vertical-align:middle;margin-left:4px}
204
+ .pool-badge{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:8px;font-size:9px;font-weight:600;background:rgba(99,102,241,.15);color:#818cf8;vertical-align:middle;margin-left:4px;font-family:var(--mono);letter-spacing:-.3px}
205
+ .pool-badge::before{content:'\1F517';font-size:8px}
206
+
207
+
208
+ /* ── view-watch.css ── */
209
+ /* ── Watch View: Project Cards ── */
210
+ .watch-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:24px}
211
+ .watch-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .2s,box-shadow .2s}
212
+ .watch-card:hover{border-color:var(--border-hi);box-shadow:0 2px 12px rgba(0,0,0,.25)}
213
+ .watch-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
214
+ .watch-card-name{font-family:var(--sans);font-size:14px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
215
+ .watch-card-icons{display:flex;gap:6px;flex-shrink:0;margin-left:8px}
216
+ .watch-card-icons .btn{padding:3px 8px;font-size:10px}
217
+
218
+ /* ── Sparkline ── */
219
+ .watch-sparkline{height:40px;margin-bottom:10px}
220
+ .watch-sparkline svg{width:100%;height:100%;display:block}
221
+
222
+ /* ── Card Footer ── */
223
+ .watch-card-footer{display:flex;align-items:center;justify-content:space-between;font-size:11px}
224
+ .watch-card-status{display:flex;align-items:center;gap:6px}
225
+ .watch-card-status .status-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
226
+ .watch-card-status .status-dot.green{background:var(--green);box-shadow:0 0 6px var(--green)}
227
+ .watch-card-status .status-dot.red{background:var(--red);box-shadow:0 0 6px var(--red)}
228
+ .watch-card-status .status-dot.amber{background:var(--amber);box-shadow:0 0 6px var(--amber)}
229
+ .watch-card-status .status-dot.dim{background:var(--text3)}
230
+ .watch-card-rate{font-weight:600}
231
+ .watch-card-rate.green{color:var(--green)}
232
+ .watch-card-rate.amber{color:var(--amber)}
233
+ .watch-card-rate.red{color:var(--red)}
234
+
235
+ .watch-card-meta{display:flex;flex-direction:column;gap:4px;margin-top:10px;font-size:10px;color:var(--text3)}
236
+ .watch-card-meta span{display:flex;align-items:center;gap:6px}
237
+ .watch-card-countdown{color:var(--accent);font-weight:500;font-variant-numeric:tabular-nums}
238
+ .watch-card-commit{font-family:var(--mono);color:var(--text3)}
239
+
240
+ /* ── Event Log ── */
241
+ .watch-event-log{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
242
+ .watch-event-log-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);background:var(--surface2)}
243
+ .watch-event-log-header .title{font-family:var(--sans);font-size:13px;font-weight:600}
244
+ .watch-event-log-body{max-height:400px;overflow-y:auto}
245
+ .watch-event-row{display:grid;grid-template-columns:120px 130px minmax(80px,1fr) 46px 74px 40px 52px 60px;align-items:center;gap:0;padding:7px 16px;border-bottom:1px solid var(--border);font-size:11px;transition:background .1s}
246
+ .watch-event-row:last-child{border-bottom:none}
247
+ .watch-event-row:hover{background:var(--surface2)}
248
+ .watch-event-row.we-header{font-size:9px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;padding:6px 16px;background:var(--surface2);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:1}
249
+ .watch-event-row.we-header:hover{background:var(--surface2)}
250
+ .watch-event-time{color:var(--text3);font-variant-numeric:tabular-nums;font-family:var(--mono);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
251
+ .watch-event-project{color:var(--text);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px}
252
+ .watch-event-suite{color:var(--accent);font-family:var(--mono);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px}
253
+ .watch-event-result{justify-self:center}
254
+ .watch-event-counts{font-family:var(--mono);font-size:10px;color:var(--text2);white-space:nowrap;text-align:center}
255
+ .watch-event-counts .we-counts-ok{color:var(--green)}
256
+ .watch-event-rate{font-weight:600;color:var(--text2);font-variant-numeric:tabular-nums;text-align:right}
257
+ .watch-event-duration{color:var(--text3);font-family:var(--mono);font-size:10px;text-align:right}
258
+ .we-trigger{color:var(--text3);font-size:9px;padding:1px 6px;border-radius:8px;background:var(--surface3);white-space:nowrap;text-align:center;justify-self:end}
259
+
260
+ /* ── Watch Table (legacy) ── */
261
+ .watch-jobs-table{width:100%;border-collapse:collapse;font-size:11px}
262
+
263
+
264
+ /* ── view-tests.css ── */
265
+ /* ── Suite Cards ── */
266
+ .suite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;padding:16px}
267
+ .suite-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;transition:border-color .2s,box-shadow .2s}
268
+ .suite-card:hover{border-color:var(--border-hi);box-shadow:0 2px 12px rgba(0,0,0,.25)}
269
+ .suite-card-head{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border);background:var(--surface2)}
270
+ .suite-card-icon{width:32px;height:32px;border-radius:6px;background:var(--accent-dim);display:flex;align-items:center;justify-content:center;font-size:14px;color:var(--accent);flex-shrink:0}
271
+ .suite-card-info{flex:1;min-width:0}
272
+ .suite-card-name{font-family:var(--sans);font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
273
+ .suite-card-file{font-size:10px;color:var(--text3);margin-top:1px;font-family:var(--mono)}
274
+ .suite-card-count{flex-shrink:0;display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:40px;height:40px;border-radius:var(--r);background:var(--surface3);border:1px solid var(--border)}
275
+ .suite-card-count-num{font-size:16px;font-weight:700;color:var(--text);line-height:1}
276
+ .suite-card-count-lbl{font-size:8px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;line-height:1;margin-top:2px}
277
+ .suite-card-body{padding:0}
278
+ .suite-card-tests{list-style:none;max-height:180px;overflow-y:auto}
279
+ .suite-card-tests::-webkit-scrollbar{width:4px}
280
+ .suite-card-tests::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
281
+ .suite-card-tests li{font-size:11px;color:var(--text2);padding:6px 16px 6px 30px;position:relative;cursor:pointer;transition:all .15s;border-bottom:1px solid rgba(35,39,56,.5)}
282
+ .suite-card-tests li:last-child{border-bottom:none}
283
+ .suite-card-tests li:hover{color:var(--text);background:var(--surface2)}
284
+ .suite-card-tests li::before{content:"\25B8";position:absolute;left:14px;color:var(--text3);font-size:9px;top:7px;transition:transform .15s}
285
+ .suite-card-tests li.expanded{color:var(--accent);background:var(--accent-dim)}
286
+ .suite-card-tests li.expanded::before{content:"\25BE";color:var(--accent)}
287
+ .suite-test-steps{padding:8px 0 8px 14px;border-left:2px solid var(--accent-dim);margin:6px 0 4px 6px}
288
+ .suite-card-footer{padding:10px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;background:var(--surface)}
289
+
290
+ /* ── Suite Detail Modal ── */
291
+ .suite-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:200;display:none;align-items:flex-start;justify-content:center;padding:32px 24px;overflow-y:auto;backdrop-filter:blur(4px)}
292
+ .suite-modal-overlay.open{display:flex}
293
+ .suite-modal{width:100%;max-width:960px;background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;animation:suiteModalIn .2s ease}
294
+ @keyframes suiteModalIn{from{opacity:0;transform:translateY(-12px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
295
+ .suite-modal-header{display:flex;align-items:center;gap:14px;padding:18px 24px;background:var(--surface2);border-bottom:1px solid var(--border)}
296
+ .suite-modal-header .suite-card-icon{width:36px;height:36px;font-size:16px}
297
+ .suite-modal-title{flex:1;min-width:0}
298
+ .suite-modal-title h2{font-family:var(--sans);font-size:16px;font-weight:700;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
299
+ .suite-modal-title span{font-size:10px;color:var(--text3);font-family:var(--mono)}
300
+ .suite-modal-actions{display:flex;gap:8px;align-items:center;flex-shrink:0}
301
+ .suite-modal-close{width:32px;height:32px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface3);color:var(--text2);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;line-height:1}
302
+ .suite-modal-close:hover{background:var(--red-dim);border-color:rgba(239,68,68,.3);color:var(--red)}
303
+ .suite-modal-body{padding:0}
304
+ .suite-modal-loading{padding:32px;text-align:center;color:var(--text3);font-size:12px}
305
+ .suite-modal-test{border-bottom:1px solid var(--border)}
306
+ .suite-modal-test:last-child{border-bottom:none}
307
+ .suite-modal-test-header{display:flex;align-items:center;gap:10px;padding:12px 24px;cursor:pointer;transition:background .15s;user-select:none}
308
+ .suite-modal-test-header:hover{background:var(--surface2)}
309
+ .suite-modal-test.open>.suite-modal-test-header{background:var(--surface2)}
310
+ .suite-modal-test-chevron{font-size:9px;color:var(--text3);transition:transform .15s;width:14px;text-align:center;flex-shrink:0}
311
+ .suite-modal-test.open>.suite-modal-test-header .suite-modal-test-chevron{transform:rotate(90deg);color:var(--accent)}
312
+ .suite-modal-test-name{font-size:12px;font-weight:500;color:var(--text);flex:1}
313
+ .suite-modal-test-badge{font-size:9px;padding:2px 7px;border-radius:8px;background:var(--surface3);color:var(--text3);flex-shrink:0}
314
+ .suite-modal-test-actions{display:none;padding:0 24px 16px 48px}
315
+ .suite-modal-test.open>.suite-modal-test-actions{display:block}
316
+ .suite-modal-step{display:flex;align-items:flex-start;gap:8px;padding:5px 0;font-size:11px;font-family:var(--mono);border-bottom:1px solid rgba(35,39,56,.3)}
317
+ .suite-modal-step:last-child{border-bottom:none}
318
+ .suite-modal-step-num{width:22px;text-align:right;color:var(--text3);flex-shrink:0;font-size:10px;padding-top:1px}
319
+ .suite-modal-step-type{color:var(--purple);font-weight:600;flex-shrink:0;min-width:120px}
320
+ .suite-modal-step-detail{color:var(--text2);flex:1;min-width:0;word-break:break-word}
321
+ .suite-modal-step-detail .step-sel{color:var(--accent)}
322
+ .suite-modal-step-detail .step-arrow{color:var(--text3);margin:0 4px}
323
+ .suite-modal-step-detail .step-val{color:var(--text)}
324
+ .suite-modal-expect{padding:10px 24px 10px 48px;background:var(--green-dim);border-bottom:1px solid var(--border);font-size:11px;color:var(--green);display:flex;align-items:flex-start;gap:8px}
325
+ .suite-modal-expect-label{font-weight:600;flex-shrink:0}
326
+
327
+ /* ── Project Accordion ── */
328
+ .project-accordion{margin-bottom:2px}
329
+ .project-accordion-header{display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .15s;user-select:none}
330
+ .project-accordion-header:hover{background:var(--surface2);border-color:var(--border-hi)}
331
+ .project-accordion.open>.project-accordion-header{border-radius:var(--r) var(--r) 0 0;border-bottom-color:transparent;background:var(--surface2)}
332
+ .project-accordion-chevron{font-size:10px;color:var(--text3);transition:transform .2s ease;flex-shrink:0;width:16px;text-align:center}
333
+ .project-accordion.open>.project-accordion-header .project-accordion-chevron{transform:rotate(90deg);color:var(--accent)}
334
+ .project-accordion-name{font-family:var(--sans);font-size:13px;font-weight:600;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
335
+ .project-accordion-meta{display:flex;align-items:center;gap:10px;flex-shrink:0}
336
+ .project-accordion-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px;background:var(--surface3);color:var(--text2)}
337
+ .project-accordion-body{overflow:hidden;max-height:0;transition:max-height .3s ease;border:1px solid var(--border);border-top:none;border-radius:0 0 var(--r) var(--r);background:var(--bg)}
338
+ .project-accordion.open>.project-accordion-body{max-height:5000px}
339
+
340
+ /* ── Module Cards ── */
341
+ .module-section-title{font-family:var(--sans);font-size:14px;font-weight:600;margin:24px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);color:var(--text2);display:flex;align-items:center;gap:8px}
342
+ .module-section-title .mod-icon{color:var(--purple)}
343
+ .module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
344
+ .module-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;border-left:3px solid var(--purple);transition:border-color .15s}
345
+ .module-card:hover{border-color:var(--border-hi);border-left-color:var(--purple)}
346
+ .module-card-name{font-weight:600;font-size:13px;color:var(--purple);margin-bottom:4px}
347
+ .module-card-desc{font-size:11px;color:var(--text2);margin-bottom:8px}
348
+ .module-card-meta{font-size:10px;color:var(--text3);display:flex;gap:12px}
349
+ .module-card-params{list-style:none;font-size:10px;color:var(--text2);margin-top:6px}
350
+ .module-card-params li{padding:2px 0}
351
+ .module-card-params li::before{content:'$';color:var(--purple);margin-right:4px}
352
+
353
+ /* ── Variables ── */
354
+ .var-table{width:100%;border-collapse:collapse;font-size:12px}
355
+ .var-table th{text-align:left;font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;padding:8px 12px;border-bottom:1px solid var(--border)}
356
+ .var-table td{padding:8px 12px;border-bottom:1px solid var(--border)}
357
+ .var-table td code{background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:11px}
358
+ .var-add-form{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:16px}
359
+ .var-add-form input,.var-add-form select{padding:6px 10px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px}
360
+ .var-add-form input:focus,.var-add-form select:focus{outline:none;border-color:var(--accent)}
361
+
362
+
363
+ /* ── view-runs.css ── */
101
364
  /* ── Trend Chart ── */
102
365
  .chart{display:flex;align-items:flex-end;gap:3px;height:60px}
103
366
  .chart-bar{flex:1;min-width:3px;max-width:16px;border-radius:2px 2px 0 0;cursor:pointer;position:relative;transition:opacity .15s}
@@ -105,17 +368,209 @@ tbody tr.selected td{background:var(--accent-dim)}
105
368
  .chart-bar .tip{display:none;position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--surface3);border:1px solid var(--border);padding:4px 8px;border-radius:4px;font-size:10px;white-space:nowrap;z-index:10;pointer-events:none}
106
369
  .chart-bar:hover .tip{display:block}
107
370
 
108
- /* ── Suite Cards ── */
109
- .suite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
110
- .suite-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .15s}
111
- .suite-card:hover{border-color:var(--border-hi)}
112
- .suite-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
113
- .suite-card-name{font-weight:600;font-size:13px}
114
- .suite-card-count{font-size:10px;color:var(--text3)}
115
- .suite-card-tests{list-style:none;margin-bottom:12px}
116
- .suite-card-tests li{font-size:11px;color:var(--text2);padding:2px 0;padding-left:14px;position:relative}
117
- .suite-card-tests li::before{content:">";position:absolute;left:0;color:var(--text3)}
371
+ /* ── Inline Run Detail ── */
372
+ .run-detail-row td{padding:0!important;border-bottom:2px solid var(--purple)}
373
+ .run-detail-row:hover td{background:transparent!important}
374
+ .rd-wrap{overflow:hidden;transition:max-height .4s cubic-bezier(.4,0,.2,1);max-height:0}
375
+ .rd-wrap.open{max-height:10000px}
376
+ .rd-inner{padding:20px 24px;background:var(--bg);position:relative}
377
+ .rd-inner::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--purple),var(--accent),transparent 80%)}
378
+ .rd-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:1px;background:var(--border);border-radius:8px;overflow:hidden;margin-bottom:20px}
379
+ .rd-summary>div{background:var(--surface);padding:14px 16px;text-align:center;transition:background .15s}
380
+ .rd-summary>div:hover{background:var(--surface2)}
381
+ .rd-s-label{font-size:9px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
382
+ .rd-s-val{font-weight:700;font-size:18px;margin-top:4px}
383
+ .rd-test{margin-bottom:12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .2s}
384
+ .rd-test:hover{border-color:var(--border-hi)}
385
+ .rd-test:last-child{margin-bottom:0}
386
+ .rd-test-head{display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--surface2);border-bottom:1px solid var(--border);position:relative;padding-left:20px}
387
+ .rd-test-head::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px}
388
+ .rd-test.pass .rd-test-head::before{background:var(--green)}
389
+ .rd-test.fail .rd-test-head::before{background:var(--red)}
390
+ .rd-test.flaky .rd-test-head::before{background:var(--amber)}
391
+ .rd-test-name{font-family:var(--sans);font-weight:600;font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
392
+ .rd-test-dur{font-size:11px;color:var(--text2);flex-shrink:0;font-variant-numeric:tabular-nums}
393
+ .rd-test-body{padding:16px}
394
+ .rd-retries{font-size:11px;color:var(--amber);margin-bottom:10px;display:flex;align-items:center;gap:6px}
395
+ .rd-error-msg{font-size:12px;color:var(--red);padding:10px 14px;background:var(--red-dim);border-radius:var(--r);margin-bottom:12px;word-break:break-all;border-left:3px solid var(--red);line-height:1.5;position:relative;padding-right:60px}
396
+ .rd-error-msg .copy-btn{position:absolute;top:8px;right:8px;opacity:0}
397
+ .rd-error-msg:hover .copy-btn{opacity:1}
398
+ .rd-shots{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px}
399
+ .rd-shot{width:140px;border-radius:6px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:all .2s;background:var(--surface2)}
400
+ .rd-shot:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.35)}
401
+ .rd-shot img{width:100%;height:84px;object-fit:cover;display:block}
402
+ .rd-shot .rd-shot-cap{font-size:9px;color:var(--text3);padding:5px 8px;background:var(--surface)}
403
+ .rd-shot .rd-shot-cap .cap-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
404
+ .rd-shot .rd-shot-cap .ss-hash{margin-top:3px}
405
+ .rd-shot.err-shot{border-color:rgba(239,68,68,.4)}
406
+ .rd-shot.err-shot .rd-shot-cap{color:var(--red)}
407
+ .rd-logs{margin-bottom:12px}
408
+ .rd-log-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px}
409
+ .rd-log-item{font-size:11px;padding:4px 10px;border-left:2px solid var(--border);margin-bottom:2px;color:var(--text2);word-break:break-all;line-height:1.5}
410
+ .rd-log-item.error{border-left-color:var(--red);color:var(--red)}
411
+ .rd-log-item.warning,.rd-log-item.warn{border-left-color:var(--amber);color:var(--amber)}
412
+
413
+ /* ── Network Panel ── */
414
+ .rd-net-panel{margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--surface)}
415
+ .rd-net-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface2);cursor:pointer;user-select:none;transition:background .15s}
416
+ .rd-net-head:hover{background:var(--surface3)}
417
+ .rd-net-head .net-arrow{font-size:9px;color:var(--text3);transition:transform .2s}
418
+ .rd-net-head.open .net-arrow{transform:rotate(90deg)}
419
+ .rd-net-head .net-title{font-family:var(--sans);font-size:12px;font-weight:600;color:var(--text)}
420
+ .rd-net-head .net-stats{margin-left:auto;display:flex;gap:12px;font-size:10px}
421
+ .rd-net-head .net-stat{color:var(--text3)}
422
+ .rd-net-head .net-stat strong{color:var(--text2)}
423
+ .rd-net-head .net-stat.has-err strong{color:var(--red)}
424
+ .rd-net-body{display:none;max-height:600px;overflow-y:auto}
425
+ .rd-net-head.open~.rd-net-body{display:block}
426
+ .rd-net-cols{display:flex;align-items:center;gap:8px;padding:6px 14px;font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:1}
427
+ .rd-net-cols .col-e{width:16px;flex-shrink:0}
428
+ .rd-net-cols .col-m{width:50px;flex-shrink:0}
429
+ .rd-net-cols .col-s{width:60px;flex-shrink:0}
430
+ .rd-net-cols .col-u{flex:1;min-width:0}
431
+ .rd-net-cols .col-d{width:60px;flex-shrink:0;text-align:right}
432
+ .rd-net-row{display:flex;align-items:center;gap:8px;padding:7px 14px;font-size:11px;font-family:var(--mono);border-bottom:1px solid var(--border);cursor:pointer;transition:background .1s}
433
+ .rd-net-row:hover{background:var(--surface2)}
434
+ .rd-net-row.has-error{background:rgba(239,68,68,.03)}
435
+ .rd-net-row.has-error:hover{background:rgba(239,68,68,.06)}
436
+ .rd-net-expand{width:16px;flex-shrink:0;font-size:8px;color:var(--text3);transition:transform .2s;text-align:center}
437
+ .rd-net-row.open .rd-net-expand{transform:rotate(90deg);color:var(--accent)}
438
+ .rd-net-method{width:50px;flex-shrink:0;display:inline-flex;justify-content:center;padding:2px 6px;border-radius:3px;font-size:9px;font-weight:700}
439
+ .rd-net-method.get{background:var(--accent-dim);color:var(--accent)}
440
+ .rd-net-method.post{background:var(--green-dim);color:var(--green)}
441
+ .rd-net-method.put,.rd-net-method.patch{background:var(--amber-dim);color:var(--amber)}
442
+ .rd-net-method.delete{background:var(--red-dim);color:var(--red)}
443
+ .rd-net-status{width:60px;flex-shrink:0;font-size:10px;font-weight:600}
444
+ .rd-net-status.s2xx{color:var(--green)}
445
+ .rd-net-status.s3xx{color:var(--amber)}
446
+ .rd-net-status.s4xx,.rd-net-status.s5xx{color:var(--red)}
447
+ .rd-net-op{flex-shrink:0;padding:1px 7px;border-radius:3px;font-size:9px;font-weight:600;background:var(--purple-dim,rgba(168,85,247,.12));color:var(--purple,#a855f7);font-family:var(--sans);letter-spacing:.01em;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
448
+ .rd-net-url{flex:1;min-width:0;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
449
+ .rd-net-dur{width:60px;flex-shrink:0;text-align:right;color:var(--text3);font-variant-numeric:tabular-nums}
450
+ .rd-net-detail{display:none;border-bottom:1px solid var(--border);background:var(--bg);overflow:hidden}
451
+ .rd-net-row.open+.rd-net-detail{display:block}
452
+ .rd-net-row .copy-btn{opacity:0;padding:1px 6px}
453
+ .rd-net-row:hover .copy-btn{opacity:1}
454
+ .rd-net-body .rd-log-item{padding:6px 14px;margin-bottom:0;border-left:3px solid var(--border);border-bottom:1px solid var(--border);font-size:11px}
455
+ .rd-net-body .rd-log-item:last-child{border-bottom:none}
456
+ .rd-net-body .rd-log-item.error{border-left-color:var(--red)}
457
+ .rd-net-body .rd-log-item.warn,.rd-net-body .rd-log-item.warning{border-left-color:var(--amber)}
458
+
459
+ /* ── Network Detail Sections ── */
460
+ .rd-nd-section{border-bottom:1px solid var(--border)}
461
+ .rd-nd-section:last-child{border-bottom:none}
462
+ .rd-nd-toggle{display:flex;align-items:center;gap:8px;padding:8px 16px;cursor:pointer;user-select:none;transition:background .15s;font-size:10px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.06em}
463
+ .rd-nd-toggle:hover{background:var(--surface2);color:var(--text2)}
464
+ .rd-nd-toggle .nd-arrow{font-size:8px;transition:transform .2s}
465
+ .rd-nd-toggle.open .nd-arrow{transform:rotate(90deg)}
466
+ .rd-nd-toggle .nd-count{margin-left:auto;font-size:9px;font-weight:400;letter-spacing:0;text-transform:none}
467
+ .rd-nd-content{display:none;padding:0 16px 10px;max-height:300px;overflow-y:auto}
468
+ .rd-nd-toggle.open+.rd-nd-content{display:block}
469
+ .rd-nd-content pre{white-space:pre-wrap;word-break:break-all;color:var(--text2);font-size:11px;line-height:1.6;margin:0;font-family:var(--mono)}
470
+ .rd-hdr-table{display:grid;grid-template-columns:minmax(120px,auto) 1fr;gap:0;font-size:11px}
471
+ .rd-hdr-row{display:contents}
472
+ .rd-hdr-row:hover .rd-hdr-key,.rd-hdr-row:hover .rd-hdr-val{background:var(--surface2)}
473
+ .rd-hdr-key{padding:3px 12px 3px 0;color:var(--accent);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
474
+ .rd-hdr-val{padding:3px 0;color:var(--text2);border-bottom:1px solid var(--border);word-break:break-all}
475
+ .rd-nd-empty{color:var(--text3);font-size:11px;padding:4px 0;font-style:italic}
476
+
477
+ tr.expanded td{background:var(--surface2)!important}
478
+ tr.expanded td:first-child{position:relative}
479
+ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--purple)}
480
+
481
+ /* ── Actions Panel in Run Detail ── */
482
+ .rd-actions-panel{margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--surface)}
483
+ .rd-actions-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface2);cursor:pointer;user-select:none;transition:background .15s}
484
+ .rd-actions-head:hover{background:var(--surface3)}
485
+ .rd-actions-head .act-arrow{font-size:9px;color:var(--text3);transition:transform .2s}
486
+ .rd-actions-head.open .act-arrow{transform:rotate(90deg)}
487
+ .rd-actions-body{display:none;max-height:500px;overflow-y:auto;padding:8px 14px}
488
+ .rd-actions-head.open~.rd-actions-body{display:block}
489
+
490
+ /* ── Run Detail Insights ── */
491
+ .rd-insights{margin-bottom:16px;display:flex;flex-direction:column;gap:6px}
492
+ .rd-insights:empty{display:none}
493
+ .rd-ins-health{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);font-size:12px}
494
+ .rd-ins-rate{font-size:18px;font-weight:700}
495
+ .rd-ins-rate.green{color:var(--green)}
496
+ .rd-ins-rate.amber{color:var(--amber)}
497
+ .rd-ins-rate.red{color:var(--red)}
498
+ .rd-ins-trend{font-size:11px;color:var(--text2)}
499
+ .rd-ins-trend.green{color:var(--green)}
500
+ .rd-ins-trend.red{color:var(--red)}
501
+ .rd-ins-tag{padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600}
502
+ .rd-ins-tag.amber{background:var(--amber-dim);color:var(--amber)}
503
+ .rd-ins-tag.red{background:var(--red-dim);color:var(--red)}
504
+ .rd-ins-item{display:flex;align-items:flex-start;gap:8px;padding:6px 12px;border-radius:var(--r);font-size:11px;color:var(--text2);border-left:3px solid var(--border)}
505
+ .rd-ins-item.red{border-left-color:var(--red);background:var(--red-dim)}
506
+ .rd-ins-item.green{border-left-color:var(--green);background:var(--green-dim)}
507
+ .rd-ins-item.amber{border-left-color:var(--amber);background:var(--amber-dim)}
508
+ .rd-ins-icon{font-size:12px;flex-shrink:0;width:14px;text-align:center}
509
+
510
+ /* ── Health Banner ── */
511
+ .health-banner{display:flex;align-items:center;gap:1px;background:var(--border);border-radius:var(--r);overflow:hidden;margin-bottom:20px}
512
+ .health-banner:empty{display:none}
513
+ .hb-item{flex:1;background:var(--surface);padding:12px 16px;text-align:center;min-width:0;transition:background .15s}
514
+ .hb-item:hover{background:var(--surface2)}
515
+ .hb-val{font-size:20px;font-weight:700;margin-bottom:2px}
516
+ .hb-val.green{color:var(--green)}
517
+ .hb-val.amber{color:var(--amber)}
518
+ .hb-val.red{color:var(--red)}
519
+ .hb-val.accent{color:var(--accent)}
520
+ .hb-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
521
+ .hb-trend{font-size:10px;margin-top:2px}
522
+ .hb-trend.green{color:var(--green)}
523
+ .hb-trend.red{color:var(--red)}
524
+ .hb-trend.dim{color:var(--text3)}
525
+ .hb-link{background:var(--surface);padding:12px 16px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s;min-width:100px}
526
+ .hb-link:hover{background:var(--accent-dim)}
527
+ .hb-link span{font-size:11px;color:var(--accent);font-weight:600}
528
+
529
+ /* ── Screenshots ── */
530
+ .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
531
+ .gallery-item{background:var(--surface2);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;cursor:pointer;transition:border-color .15s}
532
+ .gallery-item:hover{border-color:var(--accent)}
533
+ .gallery-item img{width:100%;height:150px;object-fit:cover;display:block}
534
+ .gallery-item .cap{padding:6px 10px;font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
535
+ .gallery-item .cap .cap-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
536
+ .gallery-item .cap .ss-hash{flex-shrink:0}
537
+ .ss-search{display:flex;gap:8px;margin-bottom:16px;align-items:center}
538
+ .ss-search input{flex:1;max-width:320px;padding:8px 12px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px}
539
+ .ss-search input:focus{outline:none;border-color:var(--accent)}
540
+ .ss-search input::placeholder{color:var(--text3)}
541
+ .ss-search button{padding:8px 16px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px;cursor:pointer;transition:background .15s,border-color .15s}
542
+ .ss-search button:hover{background:var(--surface3);border-color:var(--accent)}
543
+ .ss-search-result{margin-bottom:20px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
544
+ .ss-search-result img{max-width:100%;max-height:500px;border-radius:var(--r);cursor:pointer;display:block;margin-top:8px}
545
+ .ss-search-result .ss-result-label{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px}
546
+ .ss-search-error{font-size:11px;color:var(--red);margin-bottom:12px}
118
547
 
548
+ /* ── Learnings ── */
549
+ .learn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-bottom:20px}
550
+ .learn-stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:14px;text-align:center}
551
+ .learn-stat-val{font-size:24px;font-weight:700;margin-bottom:2px}
552
+ .learn-stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
553
+ .learn-section{margin-bottom:20px}
554
+ .learn-section-title{font-family:var(--sans);font-size:13px;font-weight:600;margin-bottom:10px;color:var(--text)}
555
+ .learn-table{width:100%;border-collapse:collapse;font-size:11px}
556
+ .learn-table th{text-align:left;font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;padding:6px 10px;border-bottom:1px solid var(--border);cursor:pointer;user-select:none}
557
+ .learn-table th:hover{color:var(--text2)}
558
+ .learn-table th.sorted::after{content:' \25B2';font-size:8px}
559
+ .learn-table th.sorted.desc::after{content:' \25BC'}
560
+ .learn-table td{padding:6px 10px;border-bottom:1px solid var(--border);color:var(--text2)}
561
+ .learn-table td code{background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:10px;color:var(--text)}
562
+ .learn-table tbody tr:hover td{background:var(--surface2);color:var(--text)}
563
+ .learn-trend-chart{width:100%;height:100px;margin-bottom:20px}
564
+ .learn-trend-chart svg{width:100%;height:100%}
565
+
566
+ /* ── Pool Distribution ── */
567
+ .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
+ .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}
569
+ .pool-dist-legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin:4px 0 8px;font-size:10px;font-family:var(--mono);color:var(--text2)}
570
+ .pool-dist-legend span::before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:4px;vertical-align:middle}
571
+
572
+
573
+ /* ── view-live.css ── */
119
574
  /* ── Live Execution ── */
120
575
  .live-panel{display:none;background:var(--surface);border:1px solid var(--purple);border-radius:var(--r);overflow:hidden;animation:fadeSlide .3s ease;flex-direction:column}
121
576
  .live-panel.active{display:flex;flex:1;min-height:0}
@@ -145,15 +600,18 @@ tbody tr.selected td{background:var(--accent-dim)}
145
600
  .live-test .lt-summary .lt-expand{color:var(--purple);font-size:9px;opacity:.6}
146
601
  .live-test .lt-meta{color:var(--text2);font-size:10px;margin-bottom:6px}
147
602
  .live-test .lt-icon{font-size:12px}
148
- .lt-actions{max-height:180px;overflow-y:auto;border-top:1px solid var(--border);padding-top:6px;margin-top:4px}
603
+ .lt-actions{overflow-y:auto;border-top:1px solid var(--border);padding-top:6px;margin-top:4px}
149
604
  .lt-step{display:flex;align-items:flex-start;gap:6px;padding:2px 0;font-size:10px;font-family:var(--mono);line-height:1.4}
150
605
  .lt-step .step-icon{flex-shrink:0;width:14px;text-align:center}
151
606
  .lt-step .step-icon.ok{color:var(--green)}
152
607
  .lt-step .step-icon.fail{color:var(--red)}
153
608
  .lt-step .step-icon.run{color:var(--purple)}
154
609
  .lt-step .step-type{color:var(--purple);font-weight:600;flex-shrink:0}
155
- .lt-step .step-detail{color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
610
+ .lt-step .step-detail{color:var(--text2);flex:1;min-width:0;white-space:pre-wrap;word-break:break-word}
156
611
  .lt-step .step-dur{color:var(--text3);flex-shrink:0;margin-left:auto}
612
+ .lt-step.pool-log{background:rgba(99,102,241,.06);border-left:2px solid rgba(99,102,241,.3);padding-left:8px}
613
+ .lt-step.pool-log .step-icon{color:#818cf8}
614
+ .lt-step.pool-log .step-type{color:#818cf8;font-weight:600}
157
615
  .lt-screenshots{border-top:1px solid var(--border);margin-top:6px;padding-top:6px}
158
616
  .lt-screenshots-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:10px;color:var(--text3);font-family:var(--mono);padding:2px 0;user-select:none}
159
617
  .lt-screenshots-toggle:hover{color:var(--text)}
@@ -165,13 +623,15 @@ tbody tr.selected td{background:var(--accent-dim)}
165
623
  .lt-ss-thumb img{width:100%;height:100%;object-fit:cover;display:block}
166
624
  .lt-ss-thumb:hover{border-color:var(--purple);box-shadow:0 0 0 1px var(--purple)}
167
625
  .lt-ss-label{font-size:8px;color:var(--text3);font-family:var(--mono);text-align:center;padding:2px 2px 0;display:flex;align-items:center;justify-content:center;gap:3px;flex-wrap:wrap}
626
+
627
+ /* ── Live Run Sections ── */
168
628
  .lr-section-header{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;margin:6px 0 2px;border-radius:6px;font-family:var(--mono);font-size:12px;font-weight:700;border-left:3px solid var(--purple)}
169
629
  .lr-section-header.running{border-color:var(--purple);background:rgba(139,92,246,.08);color:var(--purple)}
170
630
  .lr-section-header.pass{border-color:var(--green);background:rgba(52,211,153,.08);color:var(--green)}
171
631
  .lr-section-header.fail{border-color:var(--red);background:rgba(248,113,113,.08);color:var(--red)}
172
632
  .lr-section-stats{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text2);font-weight:400}
173
633
  .lr-project-name{letter-spacing:.5px}
174
- .lr-test-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px;padding:4px 0 8px}
634
+ .lr-test-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px;padding:4px 0 8px}
175
635
  .live-done{background:var(--green-dim);color:var(--green);text-align:center;padding:10px;font-weight:600;font-size:12px}
176
636
  .live-done.has-failures{background:var(--red-dim);color:var(--red)}
177
637
  .live-close{padding:4px 10px;font-size:10px;background:transparent;border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer}
@@ -186,119 +646,6 @@ tbody tr.selected td{background:var(--accent-dim)}
186
646
  .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}
187
647
  @keyframes spin{to{transform:rotate(360deg)}}
188
648
 
189
- /* ── Screenshots ── */
190
- .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
191
- .gallery-item{background:var(--surface2);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;cursor:pointer;transition:border-color .15s}
192
- .gallery-item:hover{border-color:var(--accent)}
193
- .gallery-item img{width:100%;height:150px;object-fit:cover;display:block}
194
- .gallery-item .cap{padding:6px 10px;font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
195
- .gallery-item .cap .cap-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
196
- .gallery-item .cap .ss-hash{flex-shrink:0}
197
- .ss-search{display:flex;gap:8px;margin-bottom:16px;align-items:center}
198
- .ss-search input{flex:1;max-width:320px;padding:8px 12px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px}
199
- .ss-search input:focus{outline:none;border-color:var(--accent)}
200
- .ss-search input::placeholder{color:var(--text3)}
201
- .ss-search button{padding:8px 16px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px;cursor:pointer;transition:background .15s,border-color .15s}
202
- .ss-search button:hover{background:var(--surface3);border-color:var(--accent)}
203
- .ss-search-result{margin-bottom:20px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
204
- .ss-search-result img{max-width:100%;max-height:500px;border-radius:var(--r);cursor:pointer;display:block;margin-top:8px}
205
- .ss-search-result .ss-result-label{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px}
206
- .ss-search-error{font-size:11px;color:var(--red);margin-bottom:12px}
207
-
208
- /* ── Inline Run Detail ── */
209
- .run-detail-row td{padding:0!important;border-bottom:2px solid var(--border-hi)}
210
- .run-detail-row:hover td{background:transparent!important}
211
- .rd-wrap{overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1);max-height:0}
212
- .rd-wrap.open{max-height:2000px}
213
- .rd-inner{padding:16px 20px;background:var(--bg);border-left:3px solid var(--purple);margin:0 8px 8px}
214
- .rd-test{margin-bottom:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
215
- .rd-test:last-child{margin-bottom:0}
216
- .rd-test-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:var(--surface2)}
217
- .rd-test-name{font-weight:600;font-size:12px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
218
- .rd-test-dur{font-size:11px;color:var(--text2);flex-shrink:0}
219
- .rd-test-body{padding:10px 14px}
220
- .rd-error-msg{font-size:11px;color:var(--red);padding:8px 12px;background:var(--red-dim);border-radius:var(--r);margin-bottom:10px;word-break:break-all;border-left:3px solid var(--red)}
221
- .rd-shots{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
222
- .rd-shot{width:120px;border-radius:4px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:border-color .15s,transform .15s}
223
- .rd-shot:hover{border-color:var(--accent);transform:scale(1.03)}
224
- .rd-shot img{width:100%;height:72px;object-fit:cover;display:block}
225
- .rd-shot .rd-shot-cap{font-size:9px;color:var(--text3);padding:4px 6px;background:var(--surface2)}
226
- .rd-shot .rd-shot-cap .cap-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
227
- .rd-shot .rd-shot-cap .ss-hash{margin-top:3px}
228
- .rd-shot.err-shot{border-color:rgba(239,68,68,.4)}
229
- .rd-shot.err-shot .rd-shot-cap{color:var(--red)}
230
- .rd-logs{margin-top:10px}
231
- .rd-log-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;margin-bottom:4px}
232
- .rd-log-item{font-size:10px;padding:3px 8px;border-left:2px solid var(--border);margin-bottom:2px;color:var(--text2);word-break:break-all}
233
- .rd-log-item.error{border-left-color:var(--red);color:var(--red)}
234
- .rd-log-item.warning,.rd-log-item.warn{border-left-color:var(--amber);color:var(--amber)}
235
- .rd-net-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:10px;color:var(--text3);font-family:var(--mono);padding:2px 0;user-select:none;margin-top:10px}
236
- .rd-net-toggle:hover{color:var(--text)}
237
- .rd-net-toggle .net-arrow{transition:transform .2s;font-size:8px}
238
- .rd-net-toggle.open .net-arrow{transform:rotate(90deg)}
239
- .rd-net-list{display:none;margin-top:6px}
240
- .rd-net-toggle.open+.rd-net-list{display:block}
241
- .rd-net-row{display:flex;align-items:center;gap:8px;padding:3px 8px;font-size:10px;font-family:var(--mono);border-left:2px solid var(--border);margin-bottom:0;cursor:pointer;transition:background .15s}
242
- .rd-net-row:hover{background:var(--surface2)}
243
- .rd-net-row.has-error{border-left-color:var(--red)}
244
- .rd-net-method{display:inline-block;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:700;min-width:36px;text-align:center}
245
- .rd-net-method.get{background:var(--accent-dim);color:var(--accent)}
246
- .rd-net-method.post{background:var(--green-dim);color:var(--green)}
247
- .rd-net-method.put,.rd-net-method.patch{background:var(--amber-dim);color:var(--amber)}
248
- .rd-net-method.delete{background:var(--red-dim);color:var(--red)}
249
- .rd-net-status{display:inline-block;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:600;min-width:28px;text-align:center}
250
- .rd-net-status.s2xx{background:var(--green-dim);color:var(--green)}
251
- .rd-net-status.s3xx{background:var(--amber-dim);color:var(--amber)}
252
- .rd-net-status.s4xx,.rd-net-status.s5xx{background:var(--red-dim);color:var(--red)}
253
- .rd-net-url{color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
254
- .rd-net-dur{color:var(--text3);flex-shrink:0}
255
- .rd-net-expand{font-size:8px;color:var(--text3);transition:transform .2s;flex-shrink:0}
256
- .rd-net-row.open .rd-net-expand{transform:rotate(90deg)}
257
- .rd-net-detail{display:none;margin:0 0 4px 0;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);font-size:10px;font-family:var(--mono);overflow:hidden}
258
- .rd-net-row.open+.rd-net-detail{display:block}
259
- .rd-net-detail-section{margin-bottom:8px}
260
- .rd-net-detail-section:last-child{margin-bottom:0}
261
- .rd-net-detail-title{font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
262
- .rd-net-detail-body{white-space:pre-wrap;word-break:break-all;color:var(--text2);max-height:300px;overflow-y:auto;line-height:1.5}
263
- .rd-retries{font-size:10px;color:var(--amber);margin-bottom:6px}
264
- .rd-summary{display:flex;gap:16px;margin-bottom:14px;padding:10px 14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);font-size:11px;align-items:center}
265
- .rd-summary .rd-s-label{color:var(--text3);font-size:9px;text-transform:uppercase;letter-spacing:.08em}
266
- .rd-summary .rd-s-val{font-weight:700;font-size:16px;margin-top:2px}
267
- tr.expanded td{background:var(--surface2)!important}
268
- tr.expanded td:first-child{position:relative}
269
- tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--purple)}
270
-
271
- /* ── Empty ── */
272
- .empty{text-align:center;padding:48px 24px;color:var(--text3)}
273
- .empty-icon{font-size:36px;margin-bottom:8px;opacity:.5}
274
-
275
- /* ── Modal ── */
276
- .modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;display:none;align-items:center;justify-content:center;padding:24px;cursor:pointer}
277
- .modal.open{display:flex}
278
- .modal img{max-width:100%;max-height:90vh;border-radius:var(--r);cursor:default}
279
-
280
- /* ── Screenshot Hash Badge ── */
281
- .ss-hash{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;border-radius:10px;font-family:var(--mono);font-size:9px;font-weight:500;background:var(--surface3);border:1px solid var(--border);color:var(--text2);cursor:pointer;transition:all .15s;user-select:none;white-space:nowrap;vertical-align:middle}
282
- .ss-hash:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
283
- .ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
284
- .ss-hash .ss-icon{font-size:10px;line-height:1}
285
-
286
- /* ── Trigger Source Badges ── */
287
- .trigger-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}
288
- .trigger-badge.src-dashboard{background:rgba(127,140,162,.10);color:var(--text2)}
289
- .trigger-badge.src-mcp{background:var(--purple-dim);color:var(--purple)}
290
- .trigger-badge.src-cli{background:var(--accent-dim);color:var(--accent)}
291
- .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
292
- .trigger-badge .trig-icon{font-size:11px;line-height:1}
293
-
294
- /* ── Responsive ── */
295
- @media(max-width:768px){
296
- .sidebar{width:60px}.sidebar-logo h1,.sidebar-section-label,.nav-item span:not(.icon),.pool-info,.sidebar select,.sidebar-logo .ver{display:none}
297
- .nav-item{justify-content:center;padding:12px}.nav-item .icon{width:auto}
298
- .main{margin-left:60px}
299
- .suite-grid,.gallery{grid-template-columns:1fr}
300
- .lr-test-grid{grid-template-columns:1fr}
301
- }
302
649
  </style>
303
650
  </head>
304
651
  <body>
@@ -319,17 +666,20 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
319
666
  <div class="sidebar-section">
320
667
  <div class="sidebar-section-label">Navigation</div>
321
668
  </div>
322
- <div class="nav-item" data-view="live" id="navLive" style="display:none">
323
- <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>
669
+ <div class="nav-item active" data-view="watch">
670
+ <i class="icon">&#9202;</i><span>Watch</span>
324
671
  </div>
325
- <div class="nav-item active" data-view="suites">
326
- <i class="icon">&#9655;</i><span>Suites</span>
672
+ <div class="nav-item" data-view="tests">
673
+ <i class="icon">&#9655;</i><span>Tests</span><span class="badge" id="badgeSuites">-</span>
327
674
  </div>
328
675
  <div class="nav-item" data-view="runs">
329
- <i class="icon">&#9776;</i><span>Runs</span>
676
+ <i class="icon">&#9776;</i><span>Runs</span><span class="badge" id="badgeRuns">-</span>
330
677
  </div>
331
- <div class="nav-item" data-view="screenshots">
332
- <i class="icon">&#9635;</i><span>Screenshots</span>
678
+ <div class="nav-item" data-view="live" id="navLive" style="display:none">
679
+ <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
+ </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>
333
683
  </div>
334
684
 
335
685
  <div class="pool-status" id="poolStatus">
@@ -338,16 +688,214 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
338
688
  <strong>Pool</strong> <span id="poolLabel">--</span>
339
689
  </div>
340
690
  <div class="pool-info">Sessions: <strong id="poolSessions">-/-</strong></div>
691
+ <div class="pool-list" id="poolList" style="display:none"></div>
341
692
  <div class="pool-info" style="margin-top:6px">
342
693
  <span class="ws-dot" id="wsDot" style="background:var(--red)"></span>
343
694
  <span id="wsLabel" style="font-size:10px;color:var(--text3)">ws: connecting</span>
344
695
  </div>
345
696
  </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>
346
708
  </aside>
347
709
 
348
710
  <div class="main">
349
711
 
350
- <!-- Live View -->
712
+ <!-- ════════════════ Watch View (default) ════════════════ -->
713
+ <div class="view active" id="view-watch">
714
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
715
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Watch</div>
716
+ </div>
717
+ <div class="watch-grid" id="watchCards"></div>
718
+ <div class="watch-event-log">
719
+ <div class="watch-event-log-header">
720
+ <span class="title">Recent Runs</span>
721
+ </div>
722
+ <div class="watch-event-log-body" id="watchEventLog"></div>
723
+ </div>
724
+ <div class="empty" id="watchEmpty" style="display:none">
725
+ <div class="empty-icon">&#9202;</div>
726
+ <p>No projects registered yet. Run some tests to see project cards here.</p>
727
+ </div>
728
+ </div>
729
+
730
+ <!-- ════════════════ Tests View (inner tabs: Suites / Modules / Variables) ════════════════ -->
731
+ <div class="view" id="view-tests">
732
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
733
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Tests</div>
734
+ <div style="display:flex;gap:8px">
735
+ <button class="btn sm primary" id="btnRunAll">&#9655; Run All</button>
736
+ </div>
737
+ </div>
738
+ <div class="tab-bar">
739
+ <button class="tab-btn active" data-tab="testsTabSuites">Suites</button>
740
+ <button class="tab-btn" data-tab="testsTabModules">Modules</button>
741
+ <button class="tab-btn" data-tab="testsTabVariables">Variables</button>
742
+ </div>
743
+ <!-- Suites tab -->
744
+ <div class="tab-pane active" id="testsTabSuites">
745
+ <div id="suiteAccordionContainer"></div>
746
+ <div class="suite-grid" id="suiteGrid"></div>
747
+ <div class="empty" id="suitesEmpty" style="display:none">
748
+ <div class="empty-icon">&#9655;</div>
749
+ <p>No test suites found.</p>
750
+ </div>
751
+ </div>
752
+ <!-- Modules tab -->
753
+ <div class="tab-pane" id="testsTabModules">
754
+ <div id="moduleSection"></div>
755
+ </div>
756
+ <!-- Variables tab -->
757
+ <div class="tab-pane" id="testsTabVariables">
758
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
759
+ <div style="font-family:var(--sans);font-size:14px;font-weight:600;color:var(--text2)">Variables</div>
760
+ <button class="btn sm primary" id="btnAddVar">+ Add Variable</button>
761
+ </div>
762
+ <div id="varAddForm" style="display:none"></div>
763
+ <div id="variablesContainer"></div>
764
+ <div class="empty" id="variablesEmpty" style="display:none">
765
+ <div class="empty-icon">&#9881;</div>
766
+ <p>No variables set. Add variables to use <code>{{var.KEY}}</code> in your tests.</p>
767
+ </div>
768
+ </div>
769
+ </div>
770
+
771
+ <!-- ════════════════ Runs View (inner tabs: History / Screenshots / Learnings) ════════════════ -->
772
+ <div class="view" id="view-runs">
773
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
774
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Runs</div>
775
+ </div>
776
+ <div class="tab-bar">
777
+ <button class="tab-btn active" data-tab="runsTabHistory">History</button>
778
+ <button class="tab-btn" data-tab="runsTabScreenshots">Screenshots<span class="badge" id="badgeScreenshots" style="margin-left:6px">-</span></button>
779
+ <button class="tab-btn" data-tab="runsTabLearnings" id="runsTabLearnings">Learnings<span class="badge" id="badgeLearnings" style="margin-left:6px">-</span></button>
780
+ </div>
781
+ <!-- History tab -->
782
+ <div class="tab-pane active" id="runsTabHistory">
783
+ <div class="health-banner" id="runsHealthBanner"></div>
784
+ <div class="card">
785
+ <div class="card-label">Pass Rate Trend</div>
786
+ <div class="chart" id="trendChart"></div>
787
+ </div>
788
+ <div class="filter-bar" id="filterBar">
789
+ <button class="filter-btn active" data-filter="all">All</button>
790
+ <button class="filter-btn" data-filter="pass">Pass</button>
791
+ <button class="filter-btn" data-filter="fail">Fail</button>
792
+ <button class="filter-btn" data-filter="mixed">Mixed</button>
793
+ <input type="text" id="runSearchInput" placeholder="Search suite..." spellcheck="false">
794
+ </div>
795
+ <div class="card" style="padding:0">
796
+ <div class="tbl-wrap">
797
+ <table>
798
+ <thead id="runsHead"><tr></tr></thead>
799
+ <tbody id="runsBody"></tbody>
800
+ </table>
801
+ </div>
802
+ </div>
803
+ <div class="empty" id="runsEmpty" style="display:none">
804
+ <div class="empty-icon">&#9776;</div>
805
+ <p>No runs recorded yet.</p>
806
+ </div>
807
+ </div>
808
+ <!-- Screenshots tab -->
809
+ <div class="tab-pane" id="runsTabScreenshots">
810
+ <div class="ss-search">
811
+ <input type="text" id="ssHashInput" placeholder="Search by hash (e.g. ss:a3f2b1c9)" spellcheck="false">
812
+ <button id="ssHashBtn">Search</button>
813
+ </div>
814
+ <div id="ssSearchResult"></div>
815
+ <div class="gallery" id="screenshotGallery"></div>
816
+ <div class="empty" id="screenshotsEmpty" style="display:none">
817
+ <div class="empty-icon">&#9635;</div>
818
+ <p>Select a project to view screenshots.</p>
819
+ </div>
820
+ </div>
821
+ <!-- Learnings tab -->
822
+ <div class="tab-pane" id="runsTabLearnings">
823
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
824
+ <div style="font-family:var(--sans);font-size:14px;font-weight:600;color:var(--text2)">Learnings</div>
825
+ <div style="display:flex;gap:8px;align-items:center">
826
+ <select id="learningsDays" style="padding:5px 8px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:11px">
827
+ <option value="7">7 days</option>
828
+ <option value="14">14 days</option>
829
+ <option value="30" selected>30 days</option>
830
+ <option value="90">90 days</option>
831
+ </select>
832
+ <button class="btn sm" id="btnExportLearnings">Export MD</button>
833
+ <button class="btn sm" id="btnRefreshLearnings">Refresh</button>
834
+ </div>
835
+ </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>
843
+ <div class="empty" id="learningsEmpty" style="display:none">
844
+ <div class="empty-icon">&#9733;</div>
845
+ <p>No learnings data yet. Run some tests to start building knowledge.</p>
846
+ </div>
847
+ </div>
848
+ </div>
849
+
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
+ <!-- ════════════════ Live View ════════════════ -->
351
899
  <div class="view" id="view-live">
352
900
  <div class="live-panel active" id="livePanel">
353
901
  <div class="live-header">
@@ -370,67 +918,53 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
370
918
  </div>
371
919
  <div class="empty" id="liveEmpty">
372
920
  <div class="empty-icon" style="font-size:48px;opacity:.3">&#9679;</div>
373
- <p>No tests running. Start a test from the Suites view or another console.</p>
921
+ <p>No tests running. Start a test from the Tests view or another console.</p>
374
922
  <p style="margin-top:8px;font-size:11px;color:var(--text3)">This view activates automatically when tests are detected.</p>
375
923
  </div>
376
924
  </div>
377
925
 
378
- <!-- Suites View -->
379
- <div class="view active" id="view-suites">
380
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
381
- <div style="font-family:var(--sans);font-size:16px;font-weight:600">Test Suites</div>
382
- <button class="btn primary" id="btnRunAll">Run All Tests</button>
383
- </div>
384
- <div class="suite-grid" id="suiteGrid"></div>
385
- <div class="empty" id="suitesEmpty" style="display:none">
386
- <div class="empty-icon">&#9655;</div>
387
- <p>No test suites found.</p>
388
- </div>
389
- </div>
926
+ </div>
390
927
 
391
- <!-- Runs View -->
392
- <div class="view" id="view-runs">
393
- <div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Run History</div>
394
- <div class="card">
395
- <div class="card-label">Pass Rate Trend</div>
396
- <div class="chart" id="trendChart"></div>
397
- </div>
398
- <div class="card" style="padding:0">
399
- <div class="tbl-wrap">
400
- <table>
401
- <thead id="runsHead"><tr></tr></thead>
402
- <tbody id="runsBody"></tbody>
403
- </table>
928
+ <div class="suite-modal-overlay" id="suiteModalOverlay">
929
+ <div class="suite-modal" id="suiteModal">
930
+ <div class="suite-modal-header">
931
+ <div class="suite-card-icon">&#9655;</div>
932
+ <div class="suite-modal-title">
933
+ <h2 id="suiteModalName"></h2>
934
+ <span id="suiteModalFile"></span>
935
+ </div>
936
+ <div class="suite-modal-actions">
937
+ <button class="btn sm primary" id="suiteModalRun">&#9655; Run</button>
938
+ <button class="suite-modal-close" id="suiteModalClose">&times;</button>
404
939
  </div>
405
940
  </div>
406
- <div class="empty" id="runsEmpty" style="display:none">
407
- <div class="empty-icon">&#9776;</div>
408
- <p>No runs recorded yet.</p>
409
- </div>
410
- </div>
411
-
412
- <!-- Screenshots View -->
413
- <div class="view" id="view-screenshots">
414
- <div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Screenshots</div>
415
- <div class="ss-search">
416
- <input type="text" id="ssHashInput" placeholder="Search by hash (e.g. ss:a3f2b1c9)" spellcheck="false">
417
- <button id="ssHashBtn">Search</button>
418
- </div>
419
- <div id="ssSearchResult"></div>
420
- <div class="gallery" id="screenshotGallery"></div>
421
- <div class="empty" id="screenshotsEmpty" style="display:none">
422
- <div class="empty-icon">&#9635;</div>
423
- <p>Select a project to view screenshots.</p>
941
+ <div class="suite-modal-body" id="suiteModalBody">
942
+ <div class="suite-modal-loading">Loading...</div>
424
943
  </div>
425
944
  </div>
426
-
427
945
  </div>
428
-
429
946
  <div class="modal" id="modal"><img id="modalImg" src="" alt=""></div>
947
+ <div class="toast-container" id="toastContainer"></div>
948
+ <div class="kb-modal" id="kbModal">
949
+ <div class="kb-modal-content">
950
+ <h2>Keyboard Shortcuts</h2>
951
+ <div class="kb-row"><span class="kb-key">1</span><span class="kb-desc">Watch view</span></div>
952
+ <div class="kb-row"><span class="kb-key">2</span><span class="kb-desc">Tests view</span></div>
953
+ <div class="kb-row"><span class="kb-key">3</span><span class="kb-desc">Runs view</span></div>
954
+ <div class="kb-row"><span class="kb-key">4</span><span class="kb-desc">Live view</span></div>
955
+ <div class="kb-row"><span class="kb-key">j / k</span><span class="kb-desc">Navigate runs (next / previous)</span></div>
956
+ <div class="kb-row"><span class="kb-key">Enter</span><span class="kb-desc">Expand / collapse selected run</span></div>
957
+ <div class="kb-row"><span class="kb-key">Esc</span><span class="kb-desc">Close modal / collapse run</span></div>
958
+ <div class="kb-row"><span class="kb-key">r</span><span class="kb-desc">Refresh current view</span></div>
959
+ <div class="kb-row"><span class="kb-key">?</span><span class="kb-desc">Show this help</span></div>
960
+ </div>
961
+ </div>
430
962
 
431
963
  <script>
432
964
  (function(){
433
965
  'use strict';
966
+ /* ── utils.js ── */
967
+ /* ── DOM Helpers ── */
434
968
  var $=function(s){return document.querySelector(s)};
435
969
  var $$=function(s){return document.querySelectorAll(s)};
436
970
 
@@ -450,53 +984,106 @@ function css(n){return n.replace(/[^a-zA-Z0-9\-_]/g,'_')}
450
984
  function dur(ms){return ms>=1000?(ms/1000).toFixed(1)+'s':ms+'ms'}
451
985
  function fdate(iso){return iso?new Date(iso).toLocaleString():'--'}
452
986
 
453
- /** Pretty-print a string if it's JSON, otherwise return as-is */
454
987
  function prettyJson(str){
455
988
  if(!str)return '';
456
989
  try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
457
990
  }
458
991
 
459
- /** Format headers object as readable string */
460
992
  function fmtHeaders(h){
461
993
  if(!h||typeof h!=='object')return '';
462
994
  return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
463
995
  }
464
996
 
465
- /** Build a clickable network row + expandable detail panel */
997
+ function buildHeaderKV(h){
998
+ if(!h||typeof h!=='object') return el('div',{className:'rd-nd-empty'},'No data');
999
+ var table=el('div',{className:'rd-hdr-table'});
1000
+ Object.keys(h).forEach(function(k){
1001
+ var row=el('div',{className:'rd-hdr-row'});
1002
+ row.appendChild(el('span',{className:'rd-hdr-key'},k));
1003
+ row.appendChild(el('span',{className:'rd-hdr-val'},String(h[k])));
1004
+ table.appendChild(row);
1005
+ });
1006
+ return table;
1007
+ }
1008
+
1009
+ function makeCopyBtn(getTextFn){
1010
+ var btn=el('span',{className:'copy-btn',onclick:function(e){
1011
+ e.stopPropagation();
1012
+ var text=typeof getTextFn==='function'?getTextFn():String(getTextFn);
1013
+ navigator.clipboard.writeText(text).then(function(){
1014
+ btn.textContent='\u2713 Copied';
1015
+ btn.classList.add('copied');
1016
+ setTimeout(function(){btn.textContent='\u2398 Copy';btn.classList.remove('copied')},1200);
1017
+ });
1018
+ }},'\u2398 Copy');
1019
+ return btn;
1020
+ }
1021
+
1022
+ function buildNdSection(title,contentEl,count,copyText){
1023
+ var toggle=el('div',{className:'rd-nd-toggle'},[
1024
+ el('span',{className:'nd-arrow'},'\u25B6'),
1025
+ el('span',null,title),
1026
+ count?el('span',{className:'nd-count'},count+' entries'):null,
1027
+ makeCopyBtn(copyText||function(){return contentWrap.textContent})
1028
+ ]);
1029
+ var contentWrap=el('div',{className:'rd-nd-content'});
1030
+ contentWrap.appendChild(contentEl);
1031
+ toggle.addEventListener('click',function(e){
1032
+ e.stopPropagation();
1033
+ toggle.classList.toggle('open');
1034
+ });
1035
+ return el('div',{className:'rd-nd-section'},[toggle,contentWrap]);
1036
+ }
1037
+
1038
+ function gqlOp(n){
1039
+ if(n.requestBody){
1040
+ try{
1041
+ var b=JSON.parse(n.requestBody);
1042
+ if(b.operationName)return b.operationName;
1043
+ if(b.query){var m=b.query.match(/^(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/);if(m)return m[1]}
1044
+ }catch(e){}
1045
+ }
1046
+ if(n.url){
1047
+ try{var u=new URL(n.url,location.href);var op=u.searchParams.get('operationName');if(op)return op}catch(e){}
1048
+ }
1049
+ return null;
1050
+ }
1051
+
466
1052
  function buildNetRow(n){
467
- var mCls='rd-net-method '+n.method.toLowerCase();
468
- var sCls='rd-net-status '+(n.status<300?'s2xx':n.status<400?'s3xx':n.status<500?'s4xx':'s5xx');
1053
+ var mCls='rd-net-method '+(n.method||'GET').toLowerCase();
1054
+ var sCode=n.status||0;
1055
+ var sCls='rd-net-status '+(sCode<300?'s2xx':sCode<400?'s3xx':sCode<500?'s4xx':'s5xx');
469
1056
  var hasDetail=n.requestBody||n.responseBody||n.requestHeaders||n.responseHeaders;
470
- var rowCls='rd-net-row'+(n.status>=400?' has-error':'');
471
- var row=el('div',{className:rowCls},[
472
- hasDetail?el('span',{className:'rd-net-expand'},'\u25B6'):null,
473
- el('span',{className:mCls},n.method),
474
- el('span',{className:sCls},String(n.status)+(n.statusText?' '+n.statusText:'')),
475
- el('span',{className:'rd-net-url'},n.url),
476
- el('span',{className:'rd-net-dur'},dur(n.duration))
477
- ]);
1057
+ var rowCls='rd-net-row'+(sCode>=400?' has-error':'');
1058
+ var opName=gqlOp(n);
1059
+ var children=[
1060
+ el('span',{className:'rd-net-expand'},hasDetail?'\u25B6':''),
1061
+ el('span',{className:mCls},n.method||'GET'),
1062
+ el('span',{className:sCls},String(sCode))
1063
+ ];
1064
+ if(opName)children.push(el('span',{className:'rd-net-op'},opName));
1065
+ children.push(el('span',{className:'rd-net-url'},n.url||''));
1066
+ children.push(makeCopyBtn(n.url||''));
1067
+ children.push(el('span',{className:'rd-net-dur'},dur(n.duration)));
1068
+ var row=el('div',{className:rowCls},children);
478
1069
  var detail=null;
479
1070
  if(hasDetail){
480
1071
  var sections=[];
481
1072
  if(n.requestHeaders){
482
- var s=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Request Headers')]);
483
- s.appendChild(el('div',{className:'rd-net-detail-body'},fmtHeaders(n.requestHeaders)));
484
- sections.push(s);
1073
+ var hCount=Object.keys(n.requestHeaders).length;
1074
+ sections.push(buildNdSection('Request Headers',buildHeaderKV(n.requestHeaders),hCount,fmtHeaders(n.requestHeaders)));
485
1075
  }
486
1076
  if(n.requestBody){
487
- var s2=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Request Body')]);
488
- s2.appendChild(el('div',{className:'rd-net-detail-body'},prettyJson(n.requestBody)));
489
- sections.push(s2);
1077
+ var rbText=prettyJson(n.requestBody);
1078
+ sections.push(buildNdSection('Request Body',el('pre',null,rbText),null,rbText));
490
1079
  }
491
1080
  if(n.responseHeaders){
492
- var s3=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Response Headers')]);
493
- s3.appendChild(el('div',{className:'rd-net-detail-body'},fmtHeaders(n.responseHeaders)));
494
- sections.push(s3);
1081
+ var rhCount=Object.keys(n.responseHeaders).length;
1082
+ sections.push(buildNdSection('Response Headers',buildHeaderKV(n.responseHeaders),rhCount,fmtHeaders(n.responseHeaders)));
495
1083
  }
496
1084
  if(n.responseBody){
497
- var s4=el('div',{className:'rd-net-detail-section'},[el('div',{className:'rd-net-detail-title'},'Response Body')]);
498
- s4.appendChild(el('div',{className:'rd-net-detail-body'},prettyJson(n.responseBody)));
499
- sections.push(s4);
1085
+ var respText=prettyJson(n.responseBody);
1086
+ sections.push(buildNdSection('Response Body',el('pre',null,respText),null,respText));
500
1087
  }
501
1088
  detail=el('div',{className:'rd-net-detail'},sections);
502
1089
  row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});
@@ -530,7 +1117,6 @@ function createHashBadge(hash){
530
1117
  return badge;
531
1118
  }
532
1119
 
533
- /* ── Trigger source badge helper ── */
534
1120
  function createTriggerBadge(source){
535
1121
  var s=source||'unknown';
536
1122
  var labels={dashboard:'Dashboard',mcp:'MCP',cli:'CLI',unknown:'--'};
@@ -542,35 +1128,367 @@ function createTriggerBadge(source){
542
1128
  return badge;
543
1129
  }
544
1130
 
545
- /* ── State ── */
1131
+ /* ── Pool Distribution Summary ── */
1132
+ var POOL_COLORS=['#6366f1','#22d3ee','#f59e0b','#10b981','#ef4444','#8b5cf6','#ec4899','#14b8a6'];
1133
+ function buildPoolDistribution(tests){
1134
+ var pools={};var total=0;
1135
+ Object.keys(tests).forEach(function(n){
1136
+ if(n==='__error')return;var t=tests[n];
1137
+ if(!t.poolUrl)return;
1138
+ var label=t.poolUrl.replace('ws://','').replace('wss://','');
1139
+ if(!pools[label])pools[label]={count:0,passed:0,failed:0};
1140
+ pools[label].count++;total++;
1141
+ if(t.status==='passed'||t.success)pools[label].passed++;
1142
+ if(t.status==='failed'||t.success===false)pools[label].failed++;
1143
+ });
1144
+ var keys=Object.keys(pools);
1145
+ if(keys.length<2)return null;
1146
+ var bar=el('div',{className:'pool-dist'});
1147
+ var legend=el('div',{className:'pool-dist-legend'});
1148
+ keys.forEach(function(k,i){
1149
+ var pct=Math.round(pools[k].count/total*100);
1150
+ var color=POOL_COLORS[i%POOL_COLORS.length];
1151
+ var seg=el('div',{className:'pool-dist-seg'});
1152
+ seg.style.flex=pools[k].count;seg.style.background=color;
1153
+ seg.textContent=k+' ('+pools[k].count+')';
1154
+ bar.appendChild(seg);
1155
+ var lg=el('span',{},k+': '+pools[k].count+' tests ('+pct+'%)');
1156
+ lg.style.cssText='display:inline-flex;align-items:center;gap:4px';
1157
+ var dot=el('span',{});dot.style.cssText='width:8px;height:8px;border-radius:2px;background:'+color+';flex-shrink:0';
1158
+ lg.insertBefore(dot,lg.firstChild);
1159
+ legend.appendChild(lg);
1160
+ });
1161
+ return el('div',{style:'padding:4px 12px'},[bar,legend]);
1162
+ }
1163
+
1164
+
1165
+ /* ── state.js ── */
1166
+ /* ── Global State ── */
546
1167
  var S={
547
- ws:null,project:null,view:'suites',selectedRun:null,
548
- liveRuns:{},liveExpanded:new Set(),liveSSOpen:new Set()
1168
+ ws:null,project:null,view:'watch',selectedRun:null,
1169
+ liveRuns:{},liveCollapsed:new Set(),liveSSOpen:new Set(),
1170
+ runFilter:{status:'all',search:''},
1171
+ lastLearningsData:null,
1172
+ highlightedRunIdx:-1
549
1173
  };
550
1174
 
551
1175
  /* ── Navigation ── */
552
1176
  $$('.nav-item').forEach(function(n){
553
1177
  n.addEventListener('click',function(){
554
- $$('.nav-item').forEach(function(x){x.classList.remove('active')});
555
- n.classList.add('active');
556
- S.view=n.dataset.view;
557
- $$('.view').forEach(function(v){v.classList.remove('active')});
558
- $('#view-'+S.view).classList.add('active');
1178
+ showView(n.dataset.view);
559
1179
  });
560
1180
  });
561
1181
  function showView(v){
562
1182
  S.view=v;
563
1183
  $$('.nav-item').forEach(function(n){n.classList.toggle('active',n.dataset.view===v)});
564
1184
  $$('.view').forEach(function(x){x.classList.remove('active')});
565
- $('#view-'+v).classList.add('active');
1185
+ var viewEl=$('#view-'+v);
1186
+ if(viewEl)viewEl.classList.add('active');
1187
+ if(v==='watch'&&typeof startWatchPolling==='function')startWatchPolling();
1188
+ else if(typeof stopWatchPolling==='function')stopWatchPolling();
1189
+ if(v==='instances'&&typeof refreshInstances==='function')refreshInstances();
566
1190
  }
567
1191
 
1192
+ /* ── Inner Tabs ── */
1193
+ function initTabs(){
1194
+ $$('.tab-bar').forEach(function(bar){
1195
+ var container=bar.parentElement;
1196
+ bar.querySelectorAll('.tab-btn').forEach(function(btn){
1197
+ btn.addEventListener('click',function(){
1198
+ bar.querySelectorAll('.tab-btn').forEach(function(b){b.classList.remove('active')});
1199
+ btn.classList.add('active');
1200
+ container.querySelectorAll('.tab-pane').forEach(function(p){p.classList.remove('active')});
1201
+ var pane=container.querySelector('#'+btn.dataset.tab);
1202
+ if(pane)pane.classList.add('active');
1203
+ });
1204
+ });
1205
+ });
1206
+ }
1207
+
1208
+
1209
+ /* ── toast.js ── */
1210
+ /* ── Toast Notifications ── */
1211
+ function showToast(message,type,timeout){
1212
+ type=type||'info';
1213
+ timeout=timeout||5000;
1214
+ var container=$('#toastContainer');
1215
+ var icons={success:'\u2714',error:'\u2718',info:'\u2139'};
1216
+ var t=el('div',{className:'toast '+type},[
1217
+ el('span',null,icons[type]||''),
1218
+ el('span',null,message)
1219
+ ]);
1220
+ container.appendChild(t);
1221
+ setTimeout(function(){
1222
+ t.classList.add('fade-out');
1223
+ setTimeout(function(){if(t.parentNode)t.parentNode.removeChild(t)},300);
1224
+ },timeout);
1225
+ }
1226
+
1227
+ function showEnrichedToast(message,type){
1228
+ var container=$('#toastContainer');
1229
+ var icons={success:'\u2714',error:'\u2718',info:'\u2139'};
1230
+ var t=el('div',{className:'toast clickable '+type,onclick:function(){showView('runs');var lb=$('#runsTabLearnings');if(lb)lb.click()}},[
1231
+ el('span',null,icons[type]||''),
1232
+ el('span',null,message)
1233
+ ]);
1234
+ container.appendChild(t);
1235
+ setTimeout(function(){
1236
+ t.classList.add('fade-out');
1237
+ setTimeout(function(){if(t.parentNode)t.parentNode.removeChild(t)},300);
1238
+ },7000);
1239
+ }
1240
+
1241
+ /* ── Download helper ── */
1242
+ function downloadFile(filename,content,mimeType){
1243
+ var blob=new Blob([content],{type:mimeType||'text/plain'});
1244
+ var url=URL.createObjectURL(blob);
1245
+ var a=document.createElement('a');
1246
+ a.href=url;a.download=filename;
1247
+ document.body.appendChild(a);a.click();
1248
+ document.body.removeChild(a);
1249
+ URL.revokeObjectURL(url);
1250
+ }
1251
+
1252
+
1253
+ /* ── api.js ── */
1254
+ /* ── API & Pool ── */
1255
+ function api(p){return fetch(p).then(function(r){return r.json()})}
1256
+ function triggerRun(suite,projectId){
1257
+ if(anyLiveRunning())return;
1258
+ var body={};
1259
+ if(suite)body.suite=suite;
1260
+ if(projectId)body.projectId=projectId;
1261
+ else if(S.project)body.projectId=S.project;
1262
+ fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1263
+ }
1264
+
1265
+ function renderPool(d){
1266
+ if(!d)return;
1267
+ var poolList=$('#poolList');
1268
+ if(d.pools&&d.pools.length>1){
1269
+ var anyAvail=d.availableCount>0;
1270
+ $('#poolDot').className='pool-dot '+(anyAvail?'on':'off');
1271
+ $('#poolLabel').textContent=anyAvail?d.availableCount+'/'+d.totalPools+' ready':'all busy';
1272
+ $('#poolSessions').textContent=(d.totalRunning||0)+'/'+(d.totalMaxConcurrent||0);
1273
+ poolList.textContent='';poolList.style.display='';
1274
+ d.pools.forEach(function(p){
1275
+ var label=(p.url||'').replace('ws://','').replace('wss://','');
1276
+ var ok=!p.error&&p.available;
1277
+ var dot=el('span',{className:'pool-dot '+(ok?'on':'off')});
1278
+ var name=el('strong',{},label);
1279
+ var status=el('span',{},p.error?'offline':p.available?'ready':'busy');
1280
+ var sess=el('span',{className:'pool-sessions'},(p.running||0)+'/'+(p.maxConcurrent||0));
1281
+ poolList.appendChild(el('div',{className:'pool-item'},[dot,name,status,sess]));
1282
+ });
1283
+ }else if(d.pools&&d.pools.length===1){
1284
+ var p=d.pools[0];
1285
+ $('#poolDot').className='pool-dot '+(p.error||!p.available?'off':'on');
1286
+ $('#poolLabel').textContent=p.error?'offline':p.available?'ready':'busy';
1287
+ $('#poolSessions').textContent=(p.running||0)+'/'+(p.maxConcurrent||0);
1288
+ poolList.style.display='none';
1289
+ }else{
1290
+ $('#poolDot').className='pool-dot '+(d.error||!d.available?'off':'on');
1291
+ $('#poolLabel').textContent=d.error?'offline':d.available?'ready':'busy';
1292
+ $('#poolSessions').textContent=(d.running||0)+'/'+(d.maxConcurrent||0);
1293
+ poolList.style.display='none';
1294
+ }
1295
+ }
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
+ });
1460
+
1461
+ /* ── Projects ── */
1462
+ function refreshProjects(){
1463
+ api('/api/db/projects').then(function(projects){
1464
+ var sel=$('#projectSelect'),prev=sel.value;
1465
+ while(sel.options.length>1)sel.remove(1);
1466
+ if(Array.isArray(projects))projects.forEach(function(p){
1467
+ var o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o);
1468
+ });
1469
+ sel.value=prev||'';
1470
+ }).catch(function(){});
1471
+ }
1472
+ $('#projectSelect').addEventListener('change',function(){
1473
+ S.project=this.value?parseInt(this.value,10):null;
1474
+ S.selectedRun=null;
1475
+ refreshRuns();refreshSuites();refreshScreenshots();refreshLearnings();refreshWatch();
1476
+ });
1477
+
1478
+
1479
+ /* ── websocket.js ── */
568
1480
  /* ── WebSocket ── */
569
1481
  function connectWS(){
570
1482
  var proto=location.protocol==='https:'?'wss:':'ws:';
571
1483
  S.ws=new WebSocket(proto+'//'+location.host);
572
- S.ws.onopen=function(){$('#wsDot').style.background='var(--green)';$('#wsLabel').textContent='ws: connected';$('#wsLabel').style.color='var(--green)'};
573
- S.ws.onclose=function(){$('#wsDot').style.background='var(--red)';$('#wsLabel').textContent='ws: disconnected';$('#wsLabel').style.color='var(--text3)';setTimeout(connectWS,3000)};
1484
+ S.ws.onopen=function(){
1485
+ $('#wsDot').style.background='var(--green)';$('#wsLabel').textContent='ws: connected';$('#wsLabel').style.color='var(--green)';
1486
+ showToast('WebSocket connected','info');
1487
+ };
1488
+ S.ws.onclose=function(){
1489
+ $('#wsDot').style.background='var(--red)';$('#wsLabel').textContent='ws: disconnected';$('#wsLabel').style.color='var(--text3)';
1490
+ setTimeout(connectWS,3000);
1491
+ };
574
1492
  S.ws.onerror=function(){};
575
1493
  S.ws.onmessage=function(e){try{handleWS(JSON.parse(e.data))}catch(x){}};
576
1494
  }
@@ -583,22 +1501,16 @@ function getLiveRun(m){
583
1501
  }
584
1502
  function anyLiveRunning(){for(var k in S.liveRuns)if(S.liveRuns[k].on)return true;return false}
585
1503
 
586
- /* Staleness guard: auto-finish stuck runs, garbage-collect old finished runs */
587
1504
  setInterval(function(){
588
1505
  var changed=false;
589
1506
  for(var k in S.liveRuns){
590
1507
  var r=S.liveRuns[k];
591
1508
  var age=Date.now()-r._lastEvent;
592
- /* Mark stuck runs as done */
593
1509
  if(r.on&&!r.done){
594
- /* 0/0 runs (never received any tests) — mark stale after 10s */
595
1510
  if(r.total===0&&age>10000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
596
- /* All tests completed but run:complete never arrived */
597
1511
  else if(r.completed>=r.total&&r.total>0&&age>15000){r.on=false;r.done=true;r.active=0;changed=true}
598
- /* General staleness — no events for 30s */
599
1512
  else if(age>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
600
1513
  }
601
- /* Auto-remove stale 0/0 runs after 15s, finished runs after 120s */
602
1514
  if(r.done&&r.stale&&r.total===0&&age>15000){delete S.liveRuns[k];changed=true}
603
1515
  else if(r.done&&age>120000){delete S.liveRuns[k];changed=true}
604
1516
  }
@@ -609,148 +1521,704 @@ function handleWS(m){
609
1521
  switch(m.event){
610
1522
  case 'pool:status':renderPool(m.data);break;
611
1523
  case 'run:start':
612
- /* Clear all finished/stale runs when a new one starts */
613
1524
  for(var dk in S.liveRuns){if(S.liveRuns[dk].done)delete S.liveRuns[dk]}
614
1525
  var r=getLiveRun(m);
615
1526
  r.total=m.total;r.on=true;r.done=false;
616
- S.liveExpanded=new Set();S.liveSSOpen=new Set();
1527
+ S.liveCollapsed=new Set();S.liveSSOpen=new Set();
617
1528
  showView('live');renderLive();break;
618
1529
  case 'test:start':
619
- var r=getLiveRun(m);if(!r)break;
620
- r.active=m.activeCount;
621
- r.tests[m.name]={status:'running',actions:0,totalActions:0,error:null,actionLog:[],screenshots:[]};
1530
+ var r2=getLiveRun(m);if(!r2)break;
1531
+ r2.active=m.activeCount;
1532
+ r2.tests[m.name]={status:'running',actions:0,totalActions:0,error:null,actionLog:[],screenshots:[],serial:m.serial||false};
1533
+ renderLive();break;
1534
+ case 'test:pool':
1535
+ var rp=getLiveRun(m);if(!rp||!rp.tests[m.name])break;
1536
+ rp.tests[m.name].poolUrl=m.poolUrl||null;
1537
+ rp.tests[m.name].actionLog.unshift({type:'pool',narrative:'\uD83D\uDD17 '+m.name+' \u2192 '+(m.poolUrl||'').replace('ws://','').replace('wss://',''),success:true,duration:null,isPoolLog:true});
622
1538
  renderLive();break;
623
1539
  case 'test:action':
624
- var r=getLiveRun(m);if(!r||!r.tests[m.name])break;
625
- var t=r.tests[m.name];
1540
+ var r3=getLiveRun(m);if(!r3||!r3.tests[m.name])break;
1541
+ var t=r3.tests[m.name];
626
1542
  t.actions=m.actionIndex+1;t.totalActions=m.totalActions;t.actionType=m.action.type;
627
- t.actionLog.push({type:m.action.type,selector:m.action.selector||null,value:m.action.value||null,text:m.action.text||null,success:m.success,duration:m.duration,error:m.error||null});
1543
+ t.actionLog.push({type:m.action.type,selector:m.action.selector||null,value:m.action.value||null,text:m.action.text||null,success:m.success,duration:m.duration,error:m.error||null,narrative:m.narrative||null,actionRetries:m.action.retries||0});
628
1544
  if(m.screenshotPath)t.screenshots.push(m.screenshotPath);
629
1545
  renderLive();break;
630
1546
  case 'test:retry':
631
- var r=getLiveRun(m);if(!r||!r.tests[m.name])break;
632
- r.tests[m.name].retry=m.attempt+'/'+m.maxAttempts;
1547
+ var r4=getLiveRun(m);if(!r4||!r4.tests[m.name])break;
1548
+ r4.tests[m.name].retry=m.attempt+'/'+m.maxAttempts;
633
1549
  renderLive();break;
634
1550
  case 'test:complete':
635
- var r=getLiveRun(m);if(!r)break;
636
- r.completed++;
637
- if(m.success){r.passed++;if(r.tests[m.name])r.tests[m.name].status='passed'}
638
- else{r.failed++;if(r.tests[m.name]){r.tests[m.name].status='failed';r.tests[m.name].error=m.error}}
639
- if(r.tests[m.name]){
640
- r.tests[m.name].duration=m.duration;
641
- if(m.screenshots&&m.screenshots.length)r.tests[m.name].screenshots=m.screenshots;
642
- if(m.errorScreenshot)r.tests[m.name].errorScreenshot=m.errorScreenshot;
643
- if(m.networkLogs&&m.networkLogs.length)r.tests[m.name].networkLogs=m.networkLogs;
1551
+ var r5=getLiveRun(m);if(!r5)break;
1552
+ r5.completed++;
1553
+ if(m.success){r5.passed++;if(r5.tests[m.name])r5.tests[m.name].status='passed'}
1554
+ else{r5.failed++;if(r5.tests[m.name]){r5.tests[m.name].status='failed';r5.tests[m.name].error=m.error}}
1555
+ if(r5.tests[m.name]){
1556
+ r5.tests[m.name].duration=m.duration;
1557
+ if(m.screenshots&&m.screenshots.length)r5.tests[m.name].screenshots=m.screenshots;
1558
+ if(m.errorScreenshot)r5.tests[m.name].errorScreenshot=m.errorScreenshot;
1559
+ if(m.networkLogs&&m.networkLogs.length)r5.tests[m.name].networkLogs=m.networkLogs;
1560
+ if(m.poolUrl)r5.tests[m.name].poolUrl=m.poolUrl;
644
1561
  }
645
- r.active=Math.max(0,r.active-1);
1562
+ r5.active=Math.max(0,r5.active-1);
646
1563
  renderLive();break;
647
1564
  case 'run:complete':
648
- var r=getLiveRun(m);if(r){r.on=false;r.done=true;r.active=0}
649
- renderLive();refreshRuns();refreshProjects();break;
1565
+ var r6=getLiveRun(m);if(r6){r6.on=false;r6.done=true;r6.active=0}
1566
+ var summary=m.summary||{};
1567
+ var baseMsg='Run complete: '+(summary.failed>0?summary.failed+' failed':'all '+(summary.total||0)+' passed');
1568
+ var baseType=summary.failed>0?'error':'success';
1569
+ var healthUrl=S.project?'/api/db/projects/'+S.project+'/health':'/api/db/health';
1570
+ fetch(healthUrl).then(function(r){return r.json()}).then(function(h){
1571
+ if(h&&h.passRate!==undefined){
1572
+ var extra='. Pass rate: '+h.passRate+'%';
1573
+ if(h.passRateTrend==='declining')extra+=' (declining, '+h.trendDelta+'%)';
1574
+ else if(h.passRateTrend==='improving')extra+=' (improving, +'+h.trendDelta+'%)';
1575
+ if(h.flakyCount>0)extra+='. '+h.flakyCount+' flaky test(s)';
1576
+ showEnrichedToast(baseMsg+extra,baseType);
1577
+ } else {
1578
+ showToast(baseMsg,baseType);
1579
+ }
1580
+ }).catch(function(){showToast(baseMsg,baseType)});
1581
+ renderLive();refreshRuns();refreshProjects();refreshWatch();break;
650
1582
  case 'run:error':
651
- var r=getLiveRun(m);if(r){r.on=false;r.done=true;r.tests.__error={status:'failed',error:m.error}}
1583
+ var r7=getLiveRun(m);if(r7){r7.on=false;r7.done=true;r7.tests.__error={status:'failed',error:m.error}}
1584
+ showToast('Run error: '+m.error,'error');
652
1585
  renderLive();break;
653
1586
  case 'db:updated':
654
- refreshRuns();refreshProjects();refreshScreenshots();break;
1587
+ refreshRuns();refreshProjects();refreshScreenshots();refreshLearnings();refreshWatch();break;
655
1588
  }
656
1589
  }
657
1590
 
658
- /* ── API ── */
659
- function api(p){return fetch(p).then(function(r){return r.json()})}
660
- function triggerRun(suite,projectId){
661
- if(anyLiveRunning())return;
662
- var body={};
663
- if(suite)body.suite=suite;
664
- if(projectId)body.projectId=projectId;
665
- else if(S.project)body.projectId=S.project;
666
- fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1591
+
1592
+ /* ── view-watch.js ── */
1593
+ /* ══════════════════════════════════════════════════════════════════
1594
+ Watch View — Project Cards + Sparklines + Event Log
1595
+ ══════════════════════════════════════════════════════════════════ */
1596
+ var _watchInterval=null;
1597
+ var _countdownInterval=null;
1598
+ var _watchData=null;
1599
+
1600
+ function refreshWatch(){
1601
+ // Fetch projects overview (sparklines)
1602
+ api('/api/db/projects/overview').then(function(projects){
1603
+ if(!Array.isArray(projects)||!projects.length){
1604
+ $('#watchCards').textContent='';
1605
+ $('#watchEmpty').style.display='block';
1606
+ return;
1607
+ }
1608
+ $('#watchEmpty').style.display='none';
1609
+ _watchData=projects;
1610
+ renderWatchCards(projects);
1611
+ }).catch(function(){
1612
+ // Fallback: use regular projects list
1613
+ api('/api/db/projects').then(function(projects){
1614
+ if(!Array.isArray(projects)||!projects.length){$('#watchEmpty').style.display='block';return}
1615
+ $('#watchEmpty').style.display='none';
1616
+ _watchData=projects.map(function(p){return Object.assign({},p,{sparkline:[]})});
1617
+ renderWatchCards(_watchData);
1618
+ }).catch(function(){});
1619
+ });
1620
+
1621
+ // Fetch event log (recent runs)
1622
+ var runsUrl=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
1623
+ api(runsUrl).then(function(runs){
1624
+ renderEventLog(runs);
1625
+ }).catch(function(){});
1626
+
1627
+ // Fetch watch jobs status for countdown
1628
+ fetch('/api/watch/status').then(function(r){
1629
+ if(!r.ok)throw new Error('not running');
1630
+ return r.json();
1631
+ }).then(function(jobs){
1632
+ applyWatchJobData(jobs);
1633
+ }).catch(function(){
1634
+ // Watch engine not running — that's fine, cards still show
1635
+ });
667
1636
  }
668
1637
 
669
- /* ── Pool ── */
670
- function renderPool(d){
671
- if(!d)return;
672
- $('#poolDot').className='pool-dot '+(d.error||!d.available?'off':'on');
673
- $('#poolLabel').textContent=d.error?'offline':d.available?'ready':'busy';
674
- $('#poolSessions').textContent=(d.running||0)+'/'+(d.maxConcurrent||0);
1638
+ function renderWatchCards(projects){
1639
+ var container=$('#watchCards');
1640
+ container.textContent='';
1641
+
1642
+ projects.forEach(function(p){
1643
+ var sparkline=p.sparkline||[];
1644
+ var lastRate=sparkline.length?sparkline[sparkline.length-1]:null;
1645
+ var rateColor=lastRate===null?'dim':lastRate>=90?'green':lastRate>=70?'amber':'red';
1646
+ var dotColor=rateColor;
1647
+
1648
+ var sparkEl=el('div',{className:'watch-sparkline'});
1649
+ if(sparkline.length>=2){
1650
+ sparkEl.appendChild(buildSparkline(sparkline));
1651
+ } else {
1652
+ sparkEl.style.cssText='height:40px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:10px';
1653
+ sparkEl.textContent=sparkline.length?'1 run':'No runs yet';
1654
+ }
1655
+
1656
+ var triggerBtn=el('button',{className:'btn sm',onclick:function(e){e.stopPropagation();triggerRun(null,p.id)}},'\u25B6');
1657
+ var detailBtn=el('button',{className:'btn sm',onclick:function(e){
1658
+ e.stopPropagation();
1659
+ S.project=p.id;$('#projectSelect').value=p.id;
1660
+ showView('runs');
1661
+ refreshRuns();refreshSuites();
1662
+ }},'\uD83D\uDD0D');
1663
+
1664
+ var card=el('div',{className:'watch-card',id:'watch-card-'+p.id},[
1665
+ el('div',{className:'watch-card-header'},[
1666
+ el('div',{className:'watch-card-name'},p.name),
1667
+ el('div',{className:'watch-card-icons'},[triggerBtn,detailBtn])
1668
+ ]),
1669
+ sparkEl,
1670
+ el('div',{className:'watch-card-footer'},[
1671
+ el('div',{className:'watch-card-status'},[
1672
+ el('span',{className:'status-dot '+dotColor}),
1673
+ el('span',{className:'watch-card-rate '+rateColor},lastRate!==null?lastRate+'%':'—')
1674
+ ]),
1675
+ el('span',{style:'color:var(--text3);font-size:10px'},p.runCount?p.runCount+' runs':'')
1676
+ ]),
1677
+ el('div',{className:'watch-card-meta'},[
1678
+ el('span',{className:'watch-card-countdown',id:'watch-countdown-'+p.id},''),
1679
+ p.lastCommit?el('span',{className:'watch-card-commit'},'\u{1F4CB} '+p.lastCommit.slice(0,8)):null
1680
+ ])
1681
+ ]);
1682
+
1683
+ container.appendChild(card);
1684
+ });
675
1685
  }
676
- function refreshStatus(){api('/api/status').then(function(d){renderPool(d.pool)}).catch(function(){})}
677
1686
 
678
- /* ── Projects ── */
679
- function refreshProjects(){
680
- api('/api/db/projects').then(function(projects){
681
- var sel=$('#projectSelect'),prev=sel.value;
682
- while(sel.options.length>1)sel.remove(1);
683
- if(Array.isArray(projects))projects.forEach(function(p){
684
- var o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o);
685
- });
686
- sel.value=prev||'';
687
- }).catch(function(){});
1687
+ function buildSparkline(data){
1688
+ var ns='http://www.w3.org/2000/svg';
1689
+ var svg=document.createElementNS(ns,'svg');
1690
+ svg.setAttribute('viewBox','0 0 200 40');
1691
+ svg.setAttribute('preserveAspectRatio','none');
1692
+
1693
+ var n=data.length;
1694
+ var w=200/(n-1||1);
1695
+ var pts=data.map(function(v,i){return (i*w)+','+(40-v*0.4)}).join(' ');
1696
+
1697
+ // Gradient fill
1698
+ var poly=document.createElementNS(ns,'polygon');
1699
+ poly.setAttribute('points','0,40 '+pts+' '+((n-1)*w)+',40');
1700
+ poly.setAttribute('fill','var(--accent-dim)');
1701
+ svg.appendChild(poly);
1702
+
1703
+ // Line
1704
+ var pl=document.createElementNS(ns,'polyline');
1705
+ pl.setAttribute('points',pts);
1706
+ pl.setAttribute('fill','none');
1707
+ pl.setAttribute('stroke','var(--accent)');
1708
+ pl.setAttribute('stroke-width','1.5');
1709
+ svg.appendChild(pl);
1710
+
1711
+ // End dot
1712
+ if(n>0){
1713
+ var lastVal=data[n-1];
1714
+ var dotColor=lastVal>=90?'var(--green)':lastVal>=70?'var(--amber)':'var(--red)';
1715
+ var circle=document.createElementNS(ns,'circle');
1716
+ circle.setAttribute('cx',''+(n-1)*w);
1717
+ circle.setAttribute('cy',''+(40-lastVal*0.4));
1718
+ circle.setAttribute('r','3');
1719
+ circle.setAttribute('fill',dotColor);
1720
+ svg.appendChild(circle);
1721
+ }
1722
+
1723
+ return svg;
688
1724
  }
689
- $('#projectSelect').addEventListener('change',function(){
690
- S.project=this.value?parseInt(this.value,10):null;
691
- S.selectedRun=null;
692
- refreshRuns();refreshSuites();refreshScreenshots();
693
- });
694
1725
 
695
- /* ── Suites ── */
1726
+ function applyWatchJobData(jobs){
1727
+ if(!jobs||!jobs.length)return;
1728
+ jobs.forEach(function(j){
1729
+ // Find matching card by project name
1730
+ if(!_watchData)return;
1731
+ var match=_watchData.find(function(p){return p.name===j.name||p.cwd===j.cwd});
1732
+ if(!match)return;
1733
+ var cdEl=$('#watch-countdown-'+match.id);
1734
+ if(cdEl&&j.nextRunAt){
1735
+ cdEl.dataset.nextRunAt=j.nextRunAt;
1736
+ updateCountdown(cdEl);
1737
+ }
1738
+ });
1739
+ startCountdownTimer();
1740
+ }
1741
+
1742
+ function startCountdownTimer(){
1743
+ if(_countdownInterval)return;
1744
+ _countdownInterval=setInterval(function(){
1745
+ $$('.watch-card-countdown[data-next-run-at]').forEach(updateCountdown);
1746
+ },1000);
1747
+ }
1748
+
1749
+ function updateCountdown(cdEl){
1750
+ var next=cdEl.dataset.nextRunAt;
1751
+ if(!next){cdEl.textContent='';return}
1752
+ var diff=new Date(next)-Date.now();
1753
+ if(diff<=0){cdEl.textContent='\u23F1 Running...';return}
1754
+ var m=Math.floor(diff/60000);
1755
+ var s=Math.floor((diff%60000)/1000);
1756
+ cdEl.textContent='\u23F1 Next: '+m+'m '+String(s).padStart(2,'0')+'s';
1757
+ }
1758
+
1759
+ function renderEventLog(runs){
1760
+ var container=$('#watchEventLog');
1761
+ if(!container)return;
1762
+ container.textContent='';
1763
+
1764
+ if(!Array.isArray(runs)||!runs.length){
1765
+ container.appendChild(el('div',{style:'padding:16px;text-align:center;color:var(--text3);font-size:11px'},'No runs recorded yet.'));
1766
+ return;
1767
+ }
1768
+
1769
+ // Column header row
1770
+ container.appendChild(el('div',{className:'watch-event-row we-header'},[
1771
+ el('span',null,'Time'),
1772
+ el('span',null,'Project'),
1773
+ el('span',null,'Suite'),
1774
+ el('span',{style:'justify-self:center'},'Status'),
1775
+ el('span',{style:'text-align:center'},'Tests'),
1776
+ el('span',{style:'text-align:right'},'Rate'),
1777
+ el('span',{style:'text-align:right'},'Duration'),
1778
+ el('span',{style:'text-align:right'},'Source')
1779
+ ]));
1780
+
1781
+ var recent=runs.slice(0,30);
1782
+ recent.forEach(function(r){
1783
+ var rate=parseFloat(r.pass_rate)||0;
1784
+ var badgeCls=r.failed>0?'fail':'pass';
1785
+ var badgeText=r.failed>0?'FAIL':'PASS';
1786
+
1787
+ // Test counts: "5/5" or "3/5 (2 fail)"
1788
+ var countsText=r.passed+'/'+r.total;
1789
+ var countsParts=[el('span',{className:'we-counts-ok'},String(r.passed))];
1790
+ countsParts.push(document.createTextNode('/'+r.total));
1791
+ if(r.failed>0){
1792
+ countsParts.push(document.createTextNode(' ('));
1793
+ countsParts.push(el('span',{style:'color:var(--red)'},r.failed+' fail'));
1794
+ countsParts.push(document.createTextNode(')'));
1795
+ }
1796
+
1797
+ // Trigger badge
1798
+ var triggerIcon={'cli':'\u2318','dashboard':'\uD83D\uDCBB','mcp':'\u2699','watch':'\u23F1','api':'\u26A1'};
1799
+ var trigSrc=r.triggered_by||'cli';
1800
+ var trigEl=el('span',{className:'we-trigger',title:'Triggered by: '+trigSrc},(triggerIcon[trigSrc]||'\u2318')+' '+trigSrc);
1801
+
1802
+ var row=el('div',{className:'watch-event-row',style:'cursor:pointer'},[
1803
+ el('span',{className:'watch-event-time'},fdate(r.generated_at)),
1804
+ el('span',{className:'watch-event-project'},r.project_name||'—'),
1805
+ el('span',{className:'watch-event-suite'},r.suite_name||'all'),
1806
+ el('span',{className:'watch-event-result'},[el('span',{className:'badge '+badgeCls},badgeText)]),
1807
+ el('span',{className:'watch-event-counts'},countsParts),
1808
+ el('span',{className:'watch-event-rate'},rate>0?rate.toFixed(0)+'%':'—'),
1809
+ el('span',{className:'watch-event-duration'},r.duration?dur(r.duration):'—'),
1810
+ trigEl
1811
+ ]);
1812
+
1813
+ // Click to navigate to run detail
1814
+ (function(run){
1815
+ row.addEventListener('click',function(){
1816
+ S.project=run.project_id;$('#projectSelect').value=run.project_id;
1817
+ showView('runs');
1818
+ refreshRuns();
1819
+ });
1820
+ })(r);
1821
+
1822
+ container.appendChild(row);
1823
+ });
1824
+ }
1825
+
1826
+ function startWatchPolling(){
1827
+ if(_watchInterval)return;
1828
+ refreshWatch();
1829
+ _watchInterval=setInterval(refreshWatch,10000);
1830
+ }
1831
+ function stopWatchPolling(){
1832
+ if(_watchInterval){clearInterval(_watchInterval);_watchInterval=null}
1833
+ if(_countdownInterval){clearInterval(_countdownInterval);_countdownInterval=null}
1834
+ }
1835
+
1836
+
1837
+ /* ── view-tests.js ── */
1838
+ /* ══════════════════════════════════════════════════════════════════
1839
+ Tests View — Suites + Modules + Variables (inner tabs)
1840
+ ══════════════════════════════════════════════════════════════════ */
696
1841
  function refreshSuites(){
697
- var grid=$('#suiteGrid'),empty=$('#suitesEmpty');
1842
+ var grid=$('#suiteGrid'),empty=$('#suitesEmpty'),accordion=$('#suiteAccordionContainer');
698
1843
  grid.textContent='';
1844
+ var moduleSection=$('#moduleSection');
1845
+ moduleSection.textContent='';
699
1846
 
700
1847
  if(S.project){
701
- // Single project — fetch its suites
702
1848
  api('/api/db/projects/'+S.project+'/suites').then(function(suites){
703
1849
  if(!Array.isArray(suites)||suites.length===0){empty.style.display='block';empty.querySelector('p').textContent='No test suites found for this project.';return}
704
1850
  empty.style.display='none';
1851
+ $('#badgeSuites').textContent=suites.length;
705
1852
  renderSuiteCards(grid,suites,S.project);
706
1853
  }).catch(function(){});
1854
+ api('/api/db/projects/'+S.project+'/modules').then(function(modules){
1855
+ renderModules(moduleSection,modules);
1856
+ }).catch(function(){});
707
1857
  } else {
708
- // All projects — fetch each project's suites
709
1858
  api('/api/db/projects').then(function(projects){
710
1859
  if(!Array.isArray(projects)||projects.length===0){empty.style.display='block';empty.querySelector('p').textContent='No projects registered yet.';return}
711
- var loaded=0,hasAny=false;
1860
+ var loaded=0,hasAny=false,totalSuites=0;
712
1861
  projects.forEach(function(p){
713
1862
  api('/api/db/projects/'+p.id+'/suites').then(function(suites){
714
1863
  loaded++;
715
1864
  if(Array.isArray(suites)&&suites.length>0){
716
- hasAny=true;
1865
+ hasAny=true;totalSuites+=suites.length;
717
1866
  var label=el('div',{style:'grid-column:1/-1;font-family:var(--sans);font-size:13px;font-weight:600;margin-top:'+(grid.children.length?'16':'0')+'px;padding-bottom:6px;border-bottom:1px solid var(--border);color:var(--text2)'},p.name);
718
1867
  grid.appendChild(label);
719
1868
  renderSuiteCards(grid,suites,p.id);
720
1869
  }
721
- if(loaded===projects.length&&!hasAny){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.'}
1870
+ if(loaded===projects.length){
1871
+ $('#badgeSuites').textContent=totalSuites;
1872
+ if(!hasAny){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.'}
1873
+ }
722
1874
  }).catch(function(){loaded++;});
723
1875
  });
724
1876
  }).catch(function(){});
725
1877
  }
726
1878
  }
1879
+
1880
+ function renderProjectAccordion(container,project,suites){
1881
+ var totalTests=suites.reduce(function(sum,s){return sum+(s.testCount||0)},0);
1882
+ var body=el('div',{className:'project-accordion-body'});
1883
+ var innerGrid=el('div',{className:'suite-grid'});
1884
+ renderSuiteCards(innerGrid,suites,project.id);
1885
+ body.appendChild(innerGrid);
1886
+
1887
+ var header=el('div',{className:'project-accordion-header'},[
1888
+ el('span',{className:'project-accordion-chevron'},'\u25B6'),
1889
+ el('span',{className:'project-accordion-name'},project.name),
1890
+ el('div',{className:'project-accordion-meta'},[
1891
+ el('span',{className:'project-accordion-badge'},suites.length+(suites.length===1?' suite':' suites')),
1892
+ el('span',{className:'project-accordion-badge'},totalTests+(totalTests===1?' test':' tests'))
1893
+ ])
1894
+ ]);
1895
+
1896
+ var wrapper=el('div',{className:'project-accordion'},[header,body]);
1897
+ header.addEventListener('click',function(){wrapper.classList.toggle('open')});
1898
+ container.appendChild(wrapper);
1899
+ }
1900
+
1901
+ /* ── Suite Modal ── */
1902
+ var _suiteCache={};
1903
+
1904
+ function openSuiteModal(suiteName,projectId){
1905
+ var overlay=$('#suiteModalOverlay');
1906
+ var body=$('#suiteModalBody');
1907
+ $('#suiteModalName').textContent=suiteName;
1908
+ $('#suiteModalFile').textContent=suiteName+'.json';
1909
+ body.textContent='';
1910
+ body.appendChild(el('div',{className:'suite-modal-loading'},'Loading\u2026'));
1911
+ overlay.classList.add('open');
1912
+
1913
+ $('#suiteModalRun').onclick=function(){triggerRun(suiteName,projectId)};
1914
+ $('#suiteModalClose').onclick=function(){overlay.classList.remove('open')};
1915
+ overlay.addEventListener('click',function(e){if(e.target===overlay)overlay.classList.remove('open')});
1916
+
1917
+ var cacheKey=projectId+'::'+suiteName;
1918
+ var p=_suiteCache[cacheKey]||api('/api/db/projects/'+projectId+'/suites/'+encodeURIComponent(suiteName));
1919
+ _suiteCache[cacheKey]=p;
1920
+ p.then(function(data){
1921
+ body.textContent='';
1922
+ if(!data||!data.tests||!data.tests.length){
1923
+ body.appendChild(el('div',{className:'suite-modal-loading'},'No tests found'));
1924
+ return;
1925
+ }
1926
+ data.tests.forEach(function(test){
1927
+ var actionsDiv=el('div',{className:'suite-modal-test-actions'});
1928
+ (test.actions||[]).forEach(function(a,i){
1929
+ var detailContent;
1930
+ if(a.selector&&(a.value||a.text)){
1931
+ detailContent=[el('span',{className:'step-sel'},a.selector),el('span',{className:'step-arrow'},'\u2192'),el('span',{className:'step-val'},a.text||a.value)];
1932
+ } else {
1933
+ detailContent=a.selector||a.value||a.text||'';
1934
+ }
1935
+ actionsDiv.appendChild(el('div',{className:'suite-modal-step'},[
1936
+ el('span',{className:'suite-modal-step-num'},String(i+1)),
1937
+ el('span',{className:'suite-modal-step-type'},a.type),
1938
+ el('span',{className:'suite-modal-step-detail'},detailContent)
1939
+ ]));
1940
+ });
1941
+
1942
+ var header=el('div',{className:'suite-modal-test-header'},[
1943
+ el('span',{className:'suite-modal-test-chevron'},'\u25B6'),
1944
+ el('span',{className:'suite-modal-test-name'},test.name),
1945
+ el('span',{className:'suite-modal-test-badge'},(test.actions||[]).length+' actions')
1946
+ ]);
1947
+
1948
+ var testEl=el('div',{className:'suite-modal-test'},[header,actionsDiv]);
1949
+ if(test.expect){
1950
+ var expectText=Array.isArray(test.expect)?test.expect.join(', '):test.expect;
1951
+ var expectEl=el('div',{className:'suite-modal-expect'},[
1952
+ el('span',{className:'suite-modal-expect-label'},'Expect:'),
1953
+ document.createTextNode(expectText)
1954
+ ]);
1955
+ testEl.insertBefore(expectEl,actionsDiv);
1956
+ }
1957
+ header.addEventListener('click',function(){testEl.classList.toggle('open')});
1958
+ body.appendChild(testEl);
1959
+ });
1960
+ }).catch(function(){
1961
+ body.textContent='';
1962
+ body.appendChild(el('div',{className:'suite-modal-loading',style:'color:var(--red)'},'Failed to load suite'));
1963
+ });
1964
+ }
1965
+
727
1966
  function renderSuiteCards(container,suites,projectId){
728
1967
  suites.forEach(function(s){
729
1968
  var tests=el('ul',{className:'suite-card-tests'});
730
- (s.tests||[]).forEach(function(t){tests.appendChild(el('li',null,t))});
731
1969
  var pid=projectId;
732
- var card=el('div',{className:'suite-card'},[
733
- el('div',{className:'suite-card-head'},[
1970
+ (s.tests||[]).forEach(function(t){
1971
+ var li=el('li',null,t);
1972
+ li.addEventListener('click',function(e){
1973
+ e.stopPropagation();
1974
+ var existing=li.querySelector('.suite-test-steps');
1975
+ if(existing){existing.remove();li.classList.remove('expanded');return}
1976
+ tests.querySelectorAll('.suite-test-steps').forEach(function(d){d.remove()});
1977
+ tests.querySelectorAll('li.expanded').forEach(function(l){l.classList.remove('expanded')});
1978
+ var stepsDiv=el('div',{className:'suite-test-steps'});
1979
+ stepsDiv.appendChild(el('div',{style:'color:var(--text3);font-size:10px'},'Loading...'));
1980
+ li.appendChild(stepsDiv);
1981
+ li.classList.add('expanded');
1982
+ var cacheKey=pid+'::'+s.name;
1983
+ var p=_suiteCache[cacheKey]||api('/api/db/projects/'+pid+'/suites/'+encodeURIComponent(s.name));
1984
+ _suiteCache[cacheKey]=p;
1985
+ p.then(function(data){
1986
+ stepsDiv.textContent='';
1987
+ var test=(data.tests||[]).find(function(x){return x.name===t});
1988
+ if(!test||!test.actions||!test.actions.length){
1989
+ stepsDiv.appendChild(el('div',{style:'color:var(--text3);font-size:10px'},'No actions'));
1990
+ return;
1991
+ }
1992
+ if(test.serial){
1993
+ var sb=el('span',{className:'serial-badge'},'Serial');
1994
+ li.insertBefore(sb,li.querySelector('.suite-test-steps'));
1995
+ }
1996
+ test.actions.forEach(function(a,i){
1997
+ var detail=a.selector||a.value||a.text||'';
1998
+ if(a.selector&&(a.value||a.text))detail=a.selector+' \u2192 '+(a.text||a.value);
1999
+ stepsDiv.appendChild(el('div',{className:'lt-step'},[
2000
+ el('span',{className:'step-icon',style:'color:var(--text3)'},String(i+1)),
2001
+ el('span',{className:'step-type'},a.type),
2002
+ el('span',{className:'step-detail'},detail)
2003
+ ]));
2004
+ });
2005
+ }).catch(function(){
2006
+ stepsDiv.textContent='';
2007
+ stepsDiv.appendChild(el('div',{style:'color:var(--red);font-size:10px'},'Failed to load'));
2008
+ });
2009
+ });
2010
+ tests.appendChild(li);
2011
+ });
2012
+
2013
+ var cardHead=el('div',{className:'suite-card-head',style:'cursor:pointer'},[
2014
+ el('div',{className:'suite-card-icon'},'\u25B6'),
2015
+ el('div',{className:'suite-card-info'},[
734
2016
  el('div',{className:'suite-card-name'},s.name),
735
- el('span',{className:'suite-card-count'},s.testCount+' tests')
2017
+ el('div',{className:'suite-card-file'},s.file||s.name+'.json')
736
2018
  ]),
737
- tests,
738
- el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
2019
+ el('div',{className:'suite-card-count'},[
2020
+ el('div',{className:'suite-card-count-num'},String(s.testCount||0)),
2021
+ el('div',{className:'suite-card-count-lbl'},'tests')
2022
+ ])
2023
+ ]);
2024
+ (function(name,projId){
2025
+ cardHead.addEventListener('click',function(){openSuiteModal(name,projId)});
2026
+ })(s.name,pid);
2027
+
2028
+ var card=el('div',{className:'suite-card'},[
2029
+ cardHead,
2030
+ el('div',{className:'suite-card-body'},[tests]),
2031
+ el('div',{className:'suite-card-footer'},[
2032
+ el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
2033
+ ])
739
2034
  ]);
740
2035
  container.appendChild(card);
741
2036
  });
742
2037
  }
743
2038
 
744
- /* ── Runs ── */
2039
+ function renderModules(container,modules){
2040
+ if(!Array.isArray(modules)||modules.length===0)return;
2041
+ var title=el('div',{className:'module-section-title'},[
2042
+ el('span',{className:'mod-icon'},'\u{1F9E9}'),
2043
+ document.createTextNode(' Reusable Modules ('+modules.length+')')
2044
+ ]);
2045
+ container.appendChild(title);
2046
+ var grid=el('div',{className:'module-grid'});
2047
+ modules.forEach(function(m){
2048
+ var paramsEl=null;
2049
+ if(m.params&&m.params.length){
2050
+ var items=m.params.map(function(p){return el('li',null,typeof p==='string'?p:(p.name||String(p)))});
2051
+ paramsEl=el('ul',{className:'module-card-params'},items);
2052
+ }
2053
+ var card=el('div',{className:'module-card'},[
2054
+ el('div',{className:'module-card-name'},m.name),
2055
+ m.description?el('div',{className:'module-card-desc'},m.description):null,
2056
+ el('div',{className:'module-card-meta'},[
2057
+ el('span',null,m.actionCount+' actions'),
2058
+ m.params&&m.params.length?el('span',null,m.params.length+' params'):null
2059
+ ]),
2060
+ paramsEl
2061
+ ]);
2062
+ grid.appendChild(card);
2063
+ });
2064
+ container.appendChild(grid);
2065
+ }
2066
+
2067
+ /* ── Variables ── */
2068
+ function refreshVariables(){
2069
+ var container=$('#variablesContainer'),empty=$('#variablesEmpty');
2070
+ container.textContent='';
2071
+ if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to manage variables.';return}
2072
+ api('/api/db/projects/'+S.project+'/variables').then(function(vars){
2073
+ if(!Array.isArray(vars)||!vars.length){empty.style.display='block';empty.querySelector('p').textContent='No variables set. Add variables to use {{var.KEY}} in your tests.';return}
2074
+ empty.style.display='none';
2075
+ renderVariables(vars);
2076
+ }).catch(function(){empty.style.display='block'});
2077
+ }
2078
+
2079
+ function renderVariables(vars){
2080
+ var container=$('#variablesContainer');
2081
+ var tbl=el('table',{className:'var-table'});
2082
+ var thead=document.createElement('thead');
2083
+ var hr=document.createElement('tr');
2084
+ ['Key','Value','Scope','Actions'].forEach(function(h){hr.appendChild(el('th',null,h))});
2085
+ thead.appendChild(hr);tbl.appendChild(thead);
2086
+ var tbody=document.createElement('tbody');
2087
+ vars.forEach(function(v){
2088
+ var tr=document.createElement('tr');
2089
+ tr.appendChild(el('td',null,[el('code',null,v.key)]));
2090
+ tr.appendChild(el('td',{style:'max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'},v.is_secret?'\u2022\u2022\u2022\u2022\u2022\u2022':v.value));
2091
+ tr.appendChild(el('td',{style:'color:var(--text3)'},v.scope||'project'));
2092
+ var delBtn=el('button',{className:'btn sm danger',onclick:function(){
2093
+ if(!confirm('Delete variable "'+v.key+'"?'))return;
2094
+ fetch('/api/db/projects/'+S.project+'/variables/'+encodeURIComponent(v.key),{method:'DELETE'}).then(function(){refreshVariables();showToast('Variable deleted','success')}).catch(function(){showToast('Delete failed','error')});
2095
+ }},'\u2715');
2096
+ tr.appendChild(el('td',null,[delBtn]));
2097
+ tbody.appendChild(tr);
2098
+ });
2099
+ tbl.appendChild(tbody);
2100
+ container.appendChild(tbl);
2101
+ }
2102
+
2103
+ /* ── Variable Add Form ── */
2104
+ $('#btnAddVar').addEventListener('click',function(){
2105
+ var form=$('#varAddForm');
2106
+ if(form.style.display==='none'){
2107
+ form.style.display='';
2108
+ form.textContent='';
2109
+ var keyInput=el('input',{type:'text',placeholder:'KEY',style:'margin-right:8px;width:120px'});
2110
+ var valInput=el('input',{type:'text',placeholder:'Value',style:'margin-right:8px;width:200px'});
2111
+ var secretCheck=el('input',{type:'checkbox',style:'margin-right:4px'});
2112
+ var saveBtn=el('button',{className:'btn sm primary',onclick:function(){
2113
+ var k=keyInput.value.trim(),v=valInput.value;
2114
+ if(!k){showToast('Key is required','error');return}
2115
+ if(!S.project){showToast('Select a project first','error');return}
2116
+ fetch('/api/db/projects/'+S.project+'/variables',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:k,value:v,is_secret:secretCheck.checked})}).then(function(r){return r.json()}).then(function(){
2117
+ form.style.display='none';refreshVariables();showToast('Variable saved','success');
2118
+ }).catch(function(){showToast('Save failed','error')});
2119
+ }},'Save');
2120
+ var cancelBtn=el('button',{className:'btn sm',onclick:function(){form.style.display='none'}},'Cancel');
2121
+ form.appendChild(el('div',{className:'var-add-form',style:'display:flex;align-items:center;gap:8px;flex-wrap:wrap'},[
2122
+ keyInput,valInput,
2123
+ el('label',{style:'font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px'},[secretCheck,document.createTextNode('Secret')]),
2124
+ saveBtn,cancelBtn
2125
+ ]));
2126
+ } else {
2127
+ form.style.display='none';
2128
+ }
2129
+ });
2130
+
2131
+ $('#btnRunAll').addEventListener('click',function(){triggerRun()});
2132
+
2133
+
2134
+ /* ── view-runs.js ── */
2135
+ /* ══════════════════════════════════════════════════════════════════
2136
+ Runs View — History + Screenshots + Learnings (inner tabs)
2137
+ ══════════════════════════════════════════════════════════════════ */
2138
+
2139
+ /* ── Filters ── */
2140
+ $$('.filter-btn').forEach(function(btn){
2141
+ btn.addEventListener('click',function(){
2142
+ $$('.filter-btn').forEach(function(b){b.classList.remove('active')});
2143
+ btn.classList.add('active');
2144
+ S.runFilter.status=btn.dataset.filter;
2145
+ applyRunFilters();
2146
+ });
2147
+ });
2148
+ $('#runSearchInput').addEventListener('input',function(){
2149
+ S.runFilter.search=this.value.trim().toLowerCase();
2150
+ applyRunFilters();
2151
+ });
2152
+
2153
+ var _allRunRows=[];
2154
+ function applyRunFilters(){
2155
+ _allRunRows.forEach(function(item){
2156
+ var show=true;
2157
+ var r=item.data;
2158
+ if(S.runFilter.status!=='all'){
2159
+ var total=r.total||0;var passed=r.passed||0;var failed=r.failed||0;
2160
+ if(S.runFilter.status==='pass'&&(failed>0||total===0))show=false;
2161
+ if(S.runFilter.status==='fail'&&failed===0)show=false;
2162
+ if(S.runFilter.status==='mixed'&&(failed===0||passed===0))show=false;
2163
+ }
2164
+ if(show&&S.runFilter.search){
2165
+ var suite=(r.suite_name||'all').toLowerCase();
2166
+ var proj=(r.project_name||'').toLowerCase();
2167
+ if(suite.indexOf(S.runFilter.search)===-1&&proj.indexOf(S.runFilter.search)===-1)show=false;
2168
+ }
2169
+ item.tr.style.display=show?'':'none';
2170
+ if(item.detailTr)item.detailTr.style.display=show?'':'none';
2171
+ });
2172
+ }
2173
+
2174
+ function renderRunsHealthBanner(){
2175
+ var banner=$('#runsHealthBanner');
2176
+ banner.textContent='';
2177
+ var url=S.project?'/api/db/projects/'+S.project+'/health':'/api/db/health';
2178
+ fetch(url).then(function(r){return r.json()}).then(function(h){
2179
+ if(!h||!h.passRate)return;
2180
+ var rateColor=h.passRate>=90?'green':h.passRate>=70?'amber':'red';
2181
+ var trendIcon=h.passRateTrend==='improving'?'\u25B2':h.passRateTrend==='declining'?'\u25BC':'=';
2182
+ var trendCls=h.passRateTrend==='improving'?'green':h.passRateTrend==='declining'?'red':'dim';
2183
+ var deltaStr=h.trendDelta!==0?(h.trendDelta>0?'+':'')+h.trendDelta+'%':'';
2184
+
2185
+ banner.appendChild(el('div',{className:'hb-item'},[
2186
+ el('div',{className:'hb-val '+rateColor},h.passRate+'%'),
2187
+ el('div',{className:'hb-lbl'},'Pass Rate'),
2188
+ el('div',{className:'hb-trend '+trendCls},trendIcon+' '+h.passRateTrend+(deltaStr?' ('+deltaStr+')':''))
2189
+ ]));
2190
+ if(h.flakyCount>0){
2191
+ banner.appendChild(el('div',{className:'hb-item'},[
2192
+ el('div',{className:'hb-val amber'},String(h.flakyCount)),
2193
+ el('div',{className:'hb-lbl'},'Flaky Tests')
2194
+ ]));
2195
+ }
2196
+ if(h.topErrorPattern){
2197
+ var cat=h.topErrorPattern.category||h.topErrorPattern.pattern||'unknown';
2198
+ var pat=cat.replace(/-/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase()});
2199
+ banner.appendChild(el('div',{className:'hb-item'},[
2200
+ el('div',{className:'hb-val red',style:'font-size:13px'},pat),
2201
+ el('div',{className:'hb-lbl'},'Top Error ('+h.topErrorPattern.count+'x)')
2202
+ ]));
2203
+ }
2204
+ banner.appendChild(el('div',{className:'hb-link',onclick:function(){var lb=$('#runsTabLearnings');if(lb)lb.click()}},[
2205
+ el('span',null,'\u2192 View Learnings')
2206
+ ]));
2207
+ }).catch(function(){});
2208
+ }
2209
+
745
2210
  function refreshRuns(){
2211
+ renderRunsHealthBanner();
746
2212
  var url=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
747
2213
  api(url).then(function(rows){
748
2214
  var chart=$('#trendChart'),body=$('#runsBody'),empty=$('#runsEmpty'),head=$('#runsHead');
749
2215
  chart.textContent='';body.textContent='';
750
- if(!Array.isArray(rows)||rows.length===0){empty.style.display='block';head.parentNode.parentNode.style.display='none';return}
2216
+ _allRunRows=[];
2217
+ S.highlightedRunIdx=-1;
2218
+ if(!Array.isArray(rows)||rows.length===0){empty.style.display='block';head.parentNode.parentNode.style.display='none';$('#badgeRuns').textContent='0';return}
751
2219
  empty.style.display='none';head.parentNode.parentNode.style.display='';
2220
+ $('#badgeRuns').textContent=rows.length;
752
2221
 
753
- // Thead
754
2222
  var htr=document.createElement('tr');
755
2223
  var cols=[];
756
2224
  if(!S.project)cols.push('Project');
@@ -759,7 +2227,6 @@ function refreshRuns(){
759
2227
  head.textContent='';head.appendChild(htr);
760
2228
  var colSpan=cols.length;
761
2229
 
762
- // Chart
763
2230
  rows.slice(0,40).slice().reverse().forEach(function(r){
764
2231
  var rate=parseFloat(r.pass_rate)||0;
765
2232
  var color=rate>=90?'var(--green)':rate>=70?'var(--amber)':'var(--red)';
@@ -768,7 +2235,6 @@ function refreshRuns(){
768
2235
  chart.appendChild(bar);
769
2236
  });
770
2237
 
771
- // Rows
772
2238
  rows.forEach(function(r){
773
2239
  var tr=document.createElement('tr');
774
2240
  tr.dataset.runId=r.id;
@@ -786,12 +2252,14 @@ function refreshRuns(){
786
2252
  tr.addEventListener('click',function(){toggleDetail(r.id,tr,colSpan)});
787
2253
  body.appendChild(tr);
788
2254
 
789
- // If this run was already expanded, re-expand it
2255
+ var item={tr:tr,data:r,detailTr:null};
790
2256
  if(r.id===S.selectedRun){
791
2257
  var detailTr=createDetailRow(colSpan);
792
2258
  body.appendChild(detailTr);
793
2259
  loadDetailInline(r.id,detailTr);
2260
+ item.detailTr=detailTr;
794
2261
  }
2262
+ _allRunRows.push(item);
795
2263
  });
796
2264
  }).catch(function(){});
797
2265
  }
@@ -802,8 +2270,12 @@ function createDetailRow(colSpan){
802
2270
  var td=document.createElement('td');
803
2271
  td.setAttribute('colspan',colSpan);
804
2272
  var wrap=el('div',{className:'rd-wrap'});
805
- var inner=el('div',{className:'rd-inner'});
806
- inner.innerHTML='<div style="color:var(--text3);font-size:11px"><span class="spinner-small"></span> Loading...</div>';
2273
+ var inner=el('div',{className:'rd-inner'},[
2274
+ el('div',{style:'color:var(--text3);font-size:11px'},[
2275
+ el('span',{className:'spinner-small'}),
2276
+ document.createTextNode(' Loading...')
2277
+ ])
2278
+ ]);
807
2279
  wrap.appendChild(inner);
808
2280
  td.appendChild(wrap);
809
2281
  detailTr.appendChild(td);
@@ -811,7 +2283,6 @@ function createDetailRow(colSpan){
811
2283
  }
812
2284
 
813
2285
  function toggleDetail(id,clickedTr,colSpan){
814
- // If already expanded, collapse
815
2286
  if(S.selectedRun===id){
816
2287
  var existing=clickedTr.nextElementSibling;
817
2288
  if(existing&&existing.classList.contains('run-detail-row')){
@@ -824,7 +2295,6 @@ function toggleDetail(id,clickedTr,colSpan){
824
2295
  return;
825
2296
  }
826
2297
 
827
- // Collapse any other open detail
828
2298
  var prevTr=document.querySelector('#runsBody tr.expanded');
829
2299
  if(prevTr){
830
2300
  prevTr.classList.remove('expanded');
@@ -836,83 +2306,142 @@ function toggleDetail(id,clickedTr,colSpan){
836
2306
  }
837
2307
  }
838
2308
 
839
- // Expand new
840
2309
  S.selectedRun=id;
841
2310
  clickedTr.classList.add('expanded');
842
2311
  var detailTr=createDetailRow(colSpan);
843
2312
  clickedTr.parentNode.insertBefore(detailTr,clickedTr.nextSibling);
844
-
845
- // Animate open
846
2313
  requestAnimationFrame(function(){
847
2314
  requestAnimationFrame(function(){
848
2315
  var w2=detailTr.querySelector('.rd-wrap');
849
2316
  if(w2)w2.classList.add('open');
850
2317
  });
851
2318
  });
852
-
853
2319
  loadDetailInline(id,detailTr);
854
2320
  }
855
2321
 
2322
+ /* ── Run Detail ── */
856
2323
  function loadDetailInline(id,detailTr){
857
2324
  api('/api/db/runs/'+id).then(function(d){
858
2325
  if(d.error)return;
859
2326
  var inner=detailTr.querySelector('.rd-inner');
860
2327
  inner.textContent='';
861
-
862
2328
  var results=d.results||[];
863
2329
 
864
- // Summary bar
2330
+ var exportBtn=el('div',null,[
2331
+ el('div',{className:'rd-s-label'},'Export'),
2332
+ el('div',{style:'margin-top:4px'},[
2333
+ el('button',{className:'btn sm',onclick:function(e){
2334
+ e.stopPropagation();
2335
+ downloadFile('run-'+id+'.json',JSON.stringify(d,null,2),'application/json');
2336
+ }},'JSON')
2337
+ ])
2338
+ ]);
865
2339
  var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
866
2340
  var summ=el('div',{className:'rd-summary'},[
867
- el('div',null,[el('div',{className:'rd-s-label'},'Suite'),el('div',{className:'rd-s-val',style:'font-size:13px;color:var(--accent)'},d.suiteName||'all')]),
2341
+ 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')]),
868
2342
  srcBlock,
869
2343
  el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
870
2344
  el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
871
- el('div',null,[el('div',{className:'rd-s-label'},'Failed'),el('div',{className:'rd-s-val',style:'color:var(--red)'},String(d.summary.failed))]),
872
- el('div',null,[el('div',{className:'rd-s-label'},'Duration'),el('div',{className:'rd-s-val',style:'font-size:13px;color:var(--text2)'},d.summary.duration||'-')])
2345
+ el('div',null,[el('div',{className:'rd-s-label'},'Failed'),el('div',{className:'rd-s-val',style:'color:'+(d.summary.failed>0?'var(--red)':'var(--text3)')},String(d.summary.failed))]),
2346
+ el('div',null,[el('div',{className:'rd-s-label'},'Duration'),el('div',{className:'rd-s-val',style:'font-size:14px;color:var(--text2)'},d.summary.duration||'-')]),
2347
+ exportBtn
873
2348
  ]);
874
2349
  inner.appendChild(summ);
875
2350
 
876
- // Test result cards
2351
+ // Insights
2352
+ var insightsContainer=el('div',{className:'rd-insights'});
2353
+ inner.appendChild(insightsContainer);
2354
+ fetch('/api/db/runs/'+id+'/insights').then(function(r){return r.json()}).then(function(ins){
2355
+ if(!ins||ins.error)return;
2356
+ var items=[];
2357
+ var h=ins.health;
2358
+ if(h){
2359
+ var rateColor=h.passRate>=90?'green':h.passRate>=70?'amber':'red';
2360
+ var trendIcon=h.passRateTrend==='improving'?'\u25B2':h.passRateTrend==='declining'?'\u25BC':'=';
2361
+ var trendCls=h.passRateTrend==='improving'?'green':h.passRateTrend==='declining'?'red':'';
2362
+ items.push(el('div',{className:'rd-ins-health'},[
2363
+ el('span',{className:'rd-ins-rate '+rateColor},h.passRate+'%'),
2364
+ el('span',{className:'rd-ins-trend '+trendCls},trendIcon+' '+h.passRateTrend),
2365
+ h.flakyCount>0?el('span',{className:'rd-ins-tag amber'},h.flakyCount+' flaky'):null,
2366
+ h.unstableSelectorCount>0?el('span',{className:'rd-ins-tag red'},h.unstableSelectorCount+' unstable sel.'):null
2367
+ ]));
2368
+ }
2369
+ var insights=ins.insights||[];
2370
+ insights.forEach(function(i){
2371
+ var icon=i.type==='new-failure'?'\u2718':i.type==='recovered'?'\u2714':i.type==='flaky'?'\u223C':'!';
2372
+ var cls=i.type==='new-failure'?'red':i.type==='recovered'?'green':i.type==='flaky'?'amber':'';
2373
+ items.push(el('div',{className:'rd-ins-item '+cls},[
2374
+ el('span',{className:'rd-ins-icon'},icon),
2375
+ el('span',null,i.message)
2376
+ ]));
2377
+ });
2378
+ if(items.length>0){items.forEach(function(it){insightsContainer.appendChild(it)})}
2379
+ else{insightsContainer.style.display='none'}
2380
+ }).catch(function(){insightsContainer.style.display='none'});
2381
+
2382
+ // Pool distribution bar
2383
+ var histPoolTests={};
2384
+ results.forEach(function(r){if(!r.poolUrl)return;histPoolTests[r.name]={poolUrl:r.poolUrl,success:r.success}});
2385
+ var histPoolDist=buildPoolDistribution(histPoolTests);
2386
+ if(histPoolDist)inner.appendChild(histPoolDist);
2387
+
877
2388
  results.forEach(function(r){
878
2389
  var d2=r.durationMs?dur(r.durationMs):r.endTime&&r.startTime?dur(new Date(r.endTime)-new Date(r.startTime)):'-';
879
2390
  var flaky=r.success&&r.attempt>1;
2391
+ var state=flaky?'flaky':(r.success?'pass':'fail');
880
2392
 
881
- // Header: badge + name + duration
882
2393
  var badges=el('div',{style:'display:flex;gap:6px;align-items:center;flex-shrink:0'});
883
2394
  badges.appendChild(el('span',{className:'badge '+(r.success?'pass':'fail')},r.success?'PASS':'FAIL'));
884
2395
  if(flaky)badges.appendChild(el('span',{className:'badge flaky'},'FLAKY'));
885
2396
 
886
- var head=el('div',{className:'rd-test-head'},[
887
- badges,
888
- el('div',{className:'rd-test-name'},r.name),
889
- el('div',{className:'rd-test-dur'},d2)
890
- ]);
891
-
892
- // Body content
2397
+ var poolEl=r.poolUrl?el('span',{className:'pool-badge'},r.poolUrl.replace('ws://','').replace('wss://','')) :null;
2398
+ var head=el('div',{className:'rd-test-head'},[badges,el('div',{className:'rd-test-name'},[document.createTextNode(r.name),poolEl]),el('div',{className:'rd-test-dur'},d2)]);
893
2399
  var body=el('div',{className:'rd-test-body'});
894
2400
 
895
- // Retries info
896
- if(r.maxAttempts>1){
897
- body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts));
2401
+ if(r.maxAttempts>1){body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts))}
2402
+ if(r.error){
2403
+ var errDiv=el('div',{className:'rd-error-msg'});
2404
+ errDiv.appendChild(document.createTextNode(r.error));
2405
+ errDiv.appendChild(makeCopyBtn(r.error));
2406
+ body.appendChild(errDiv);
898
2407
  }
899
2408
 
900
- // Error message
901
- if(r.error){
902
- body.appendChild(el('div',{className:'rd-error-msg'},r.error));
2409
+ // Actions panel
2410
+ if(r.actions&&r.actions.length){
2411
+ var passCount=r.actions.filter(function(a){return a.success}).length;
2412
+ var failCount=r.actions.length-passCount;
2413
+ var actHead=el('div',{className:'rd-net-head'},[
2414
+ el('span',{className:'net-arrow'},'\u25B6'),
2415
+ el('span',{className:'net-title'},'Actions'),
2416
+ el('div',{className:'net-stats'},[
2417
+ el('span',{className:'net-stat'},[document.createTextNode('Steps: '),el('strong',null,String(r.actions.length))]),
2418
+ failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null
2419
+ ])
2420
+ ]);
2421
+ var actBody=el('div',{className:'rd-net-body',style:'padding:8px 14px'});
2422
+ r.actions.forEach(function(a){
2423
+ var label=a.narrative||a.type;
2424
+ var durText=a.duration!=null?dur(a.duration):'';
2425
+ var retryBadge=null;
2426
+ if(a.actionRetries&&a.actionRetries>0){
2427
+ retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px'},'\u21BB x'+a.actionRetries);
2428
+ }
2429
+ actBody.appendChild(el('div',{className:'lt-step'},[
2430
+ el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
2431
+ el('span',{className:'step-detail',style:'flex:1'},label),
2432
+ retryBadge,
2433
+ el('span',{className:'step-dur'},durText)
2434
+ ]));
2435
+ });
2436
+ actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
2437
+ body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
903
2438
  }
904
2439
 
905
- // Screenshots per test
2440
+ // Screenshots
906
2441
  var shots=[];
907
2442
  var hashes=r.screenshotHashes||{};
908
- (r.screenshots||[]).forEach(function(p){
909
- var fname=p.split('/').pop();
910
- shots.push({path:p,label:fname,type:'screenshot',hash:hashes[p]||null});
911
- });
912
- if(r.errorScreenshot){
913
- var ename=r.errorScreenshot.split('/').pop();
914
- shots.push({path:r.errorScreenshot,label:ename,type:'error',hash:hashes[r.errorScreenshot]||null});
915
- }
2443
+ (r.screenshots||[]).forEach(function(p){shots.push({path:p,label:p.split('/').pop(),type:'screenshot',hash:hashes[p]||null})});
2444
+ if(r.errorScreenshot){shots.push({path:r.errorScreenshot,label:r.errorScreenshot.split('/').pop(),type:'error',hash:hashes[r.errorScreenshot]||null})}
916
2445
  if(shots.length){
917
2446
  var shotsWrap=el('div',{className:'rd-shots'});
918
2447
  shots.forEach(function(s){
@@ -921,66 +2450,69 @@ function loadDetailInline(id,detailTr){
921
2450
  var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
922
2451
  if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
923
2452
  else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
924
- var shotEl=el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[
925
- img,capEl
926
- ]);
927
- shotsWrap.appendChild(shotEl);
2453
+ shotsWrap.appendChild(el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[img,capEl]));
928
2454
  });
929
2455
  body.appendChild(shotsWrap);
930
2456
  }
931
2457
 
932
- // Console logs (errors and warnings only)
2458
+ // Console logs
933
2459
  var cIssues=(r.consoleLogs||[]).filter(function(l){return l.type==='error'||l.type==='warn'||l.type==='warning'});
934
2460
  if(cIssues.length){
935
- var logSec=el('div',{className:'rd-logs'});
936
- logSec.appendChild(el('div',{className:'rd-log-label'},'Console'));
937
- cIssues.forEach(function(l){
938
- logSec.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text));
939
- });
940
- body.appendChild(logSec);
2461
+ var cErrors=cIssues.filter(function(l){return l.type==='error'}).length;
2462
+ var cWarns=cIssues.length-cErrors;
2463
+ var conHead=el('div',{className:'rd-net-head'},[
2464
+ el('span',{className:'net-arrow'},'\u25B6'),
2465
+ el('span',{className:'net-title'},'Console'),
2466
+ el('div',{className:'net-stats'},[
2467
+ cErrors?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(cErrors))]):null,
2468
+ cWarns?el('span',{className:'net-stat'},[document.createTextNode('Warnings: '),el('strong',null,String(cWarns))]):null
2469
+ ]),
2470
+ makeCopyBtn(function(){return cIssues.map(function(l){return '['+l.type+'] '+l.text}).join('\n')})
2471
+ ]);
2472
+ var conBody=el('div',{className:'rd-net-body'});
2473
+ cIssues.forEach(function(l){conBody.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text))});
2474
+ conHead.addEventListener('click',function(){conHead.classList.toggle('open')});
2475
+ body.appendChild(el('div',{className:'rd-net-panel'},[conHead,conBody]));
941
2476
  }
942
2477
 
943
2478
  // Network errors
944
2479
  if(r.networkErrors&&r.networkErrors.length){
945
- var netSec=el('div',{className:'rd-logs'});
946
- netSec.appendChild(el('div',{className:'rd-log-label'},'Network Errors'));
947
- r.networkErrors.forEach(function(ne){
948
- netSec.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url));
949
- });
950
- body.appendChild(netSec);
2480
+ var neHead=el('div',{className:'rd-net-head'},[
2481
+ el('span',{className:'net-arrow'},'\u25B6'),
2482
+ el('span',{className:'net-title'},'Network Errors'),
2483
+ el('div',{className:'net-stats'},[el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(r.networkErrors.length))])]),
2484
+ makeCopyBtn(function(){return r.networkErrors.map(function(ne){return '['+ne.error+'] '+ne.url}).join('\n')})
2485
+ ]);
2486
+ var neBody=el('div',{className:'rd-net-body'});
2487
+ r.networkErrors.forEach(function(ne){neBody.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url))});
2488
+ neHead.addEventListener('click',function(){neHead.classList.toggle('open')});
2489
+ body.appendChild(el('div',{className:'rd-net-panel'},[neHead,neBody]));
951
2490
  }
952
2491
 
953
- // Network API logs (collapsible, clickable rows)
2492
+ // Network panel
954
2493
  if(r.networkLogs&&r.networkLogs.length){
955
2494
  var errCount=r.networkLogs.filter(function(n){return n.status>=400}).length;
956
- var netLabel='Network ('+r.networkLogs.length+' request'+(r.networkLogs.length!==1?'s':'')+(errCount?', '+errCount+' error'+(errCount!==1?'s':''):'')+')';
957
- var netToggle=el('div',{className:'rd-net-toggle'},[
2495
+ var netHead=el('div',{className:'rd-net-head'},[
958
2496
  el('span',{className:'net-arrow'},'\u25B6'),
959
- el('span',{},netLabel)
2497
+ el('span',{className:'net-title'},'Network Requests'),
2498
+ el('div',{className:'net-stats'},[
2499
+ el('span',{className:'net-stat'},[document.createTextNode('Total: '),el('strong',null,String(r.networkLogs.length))]),
2500
+ errCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(errCount))]):null
2501
+ ])
960
2502
  ]);
961
- var netList=el('div',{className:'rd-net-list'});
962
- r.networkLogs.forEach(function(n){
963
- var built=buildNetRow(n);
964
- netList.appendChild(built.row);
965
- if(built.detail)netList.appendChild(built.detail);
966
- });
967
- netToggle.addEventListener('click',function(){
968
- netToggle.classList.toggle('open');
969
- });
970
- body.appendChild(netToggle);
971
- body.appendChild(netList);
2503
+ var netCols=el('div',{className:'rd-net-cols'},[el('span',{className:'col-e'},''),el('span',{className:'col-m'},'Method'),el('span',{className:'col-s'},'Status'),el('span',{className:'col-u'},'URL'),el('span',{className:'col-d'},'Time')]);
2504
+ var netBody=el('div',{className:'rd-net-body'},[netCols]);
2505
+ r.networkLogs.forEach(function(n){var built=buildNetRow(n);netBody.appendChild(built.row);if(built.detail)netBody.appendChild(built.detail)});
2506
+ netHead.addEventListener('click',function(){netHead.classList.toggle('open')});
2507
+ body.appendChild(el('div',{className:'rd-net-panel'},[netHead,netBody]));
972
2508
  }
973
2509
 
974
- var testCard=el('div',{className:'rd-test'},[head,body]);
975
- inner.appendChild(testCard);
2510
+ inner.appendChild(el('div',{className:'rd-test '+state},[head,body]));
976
2511
  });
977
2512
 
978
- // Re-trigger open animation if not yet open
979
2513
  var w=detailTr.querySelector('.rd-wrap');
980
- if(w&&!w.classList.contains('open')){
981
- requestAnimationFrame(function(){w.classList.add('open')});
982
- }
983
- }).catch(function(err){
2514
+ if(w&&!w.classList.contains('open')){requestAnimationFrame(function(){w.classList.add('open')})}
2515
+ }).catch(function(){
984
2516
  var inner=detailTr.querySelector('.rd-inner');
985
2517
  if(inner)inner.textContent='Failed to load run detail';
986
2518
  });
@@ -990,25 +2522,21 @@ function loadDetailInline(id,detailTr){
990
2522
  function refreshScreenshots(){
991
2523
  var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
992
2524
  gal.textContent='';
993
- if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';return}
2525
+ if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';$('#badgeScreenshots').textContent='-';return}
994
2526
  api('/api/db/projects/'+S.project+'/screenshots').then(function(files){
995
- if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';return}
2527
+ if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
996
2528
  empty.style.display='none';
2529
+ $('#badgeScreenshots').textContent=files.length;
997
2530
  files.forEach(function(f){
998
2531
  var src='/api/image?path='+encodeURIComponent(f.path);
999
2532
  var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
1000
2533
  var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
1001
- (function(c,fp){
1002
- ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))});
1003
- })(capEl,f.path);
1004
- var item=el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[
1005
- img,capEl
1006
- ]);gal.appendChild(item);
2534
+ (function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
2535
+ gal.appendChild(el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[img,capEl]));
1007
2536
  });
1008
2537
  }).catch(function(){});
1009
2538
  }
1010
2539
 
1011
- /* ── Screenshot Hash Search ── */
1012
2540
  function searchByHash(){
1013
2541
  var container=$('#ssSearchResult');
1014
2542
  container.textContent='';
@@ -1020,67 +2548,182 @@ function searchByHash(){
1020
2548
  return;
1021
2549
  }
1022
2550
  fetch('/api/screenshot-hash/'+hash).then(function(res){
1023
- if(!res.ok){
1024
- container.appendChild(el('div',{className:'ss-search-error'},'Screenshot not found for hash: ss:'+hash));
1025
- return;
1026
- }
2551
+ if(!res.ok){container.appendChild(el('div',{className:'ss-search-error'},'Screenshot not found for hash: ss:'+hash));return}
1027
2552
  return res.blob();
1028
2553
  }).then(function(blob){
1029
2554
  if(!blob)return;
1030
2555
  var url=URL.createObjectURL(blob);
1031
- var wrap=el('div',{className:'ss-search-result'},[
1032
- el('div',{className:'ss-result-label'},[
1033
- createHashBadge(hash),
1034
- el('span',{},'Found')
1035
- ])
1036
- ]);
1037
- var img=document.createElement('img');
1038
- img.src=url;
1039
- img.alt='ss:'+hash;
2556
+ var wrap=el('div',{className:'ss-search-result'},[el('div',{className:'ss-result-label'},[createHashBadge(hash),el('span',{},'Found')])]);
2557
+ var img=document.createElement('img');img.src=url;img.alt='ss:'+hash;
1040
2558
  img.addEventListener('click',function(){openModal(url)});
1041
2559
  wrap.appendChild(img);
1042
2560
  container.appendChild(wrap);
1043
- }).catch(function(){
1044
- container.appendChild(el('div',{className:'ss-search-error'},'Error searching for screenshot.'));
1045
- });
2561
+ }).catch(function(){container.appendChild(el('div',{className:'ss-search-error'},'Error searching for screenshot.'))});
1046
2562
  }
1047
2563
  $('#ssHashBtn').addEventListener('click',searchByHash);
1048
2564
  $('#ssHashInput').addEventListener('keydown',function(e){if(e.key==='Enter')searchByHash()});
1049
2565
 
1050
- /* ── Live Execution ── */
1051
- function clearFinishedLiveRuns(){
1052
- for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}
1053
- renderLive();
2566
+ /* ── Learnings ── */
2567
+ function refreshLearnings(){
2568
+ var days=$('#learningsDays').value||30;
2569
+ var url=S.project?'/api/db/projects/'+S.project+'/learnings?days='+days:'/api/db/learnings?days='+days;
2570
+ fetch(url).then(function(r){return r.json()}).then(function(data){
2571
+ if(!data||data.totalRuns===0){
2572
+ $('#learningsEmpty').style.display='block';
2573
+ $('#learningsOverview').textContent='';$('#learningsTrend').textContent='';
2574
+ $('#learningsFlaky').textContent='';$('#learningsSelectors').textContent='';
2575
+ $('#learningsPages').textContent='';$('#learningsApis').textContent='';
2576
+ $('#learningsErrors').textContent='';
2577
+ $('#badgeLearnings').textContent='-';
2578
+ return;
2579
+ }
2580
+ $('#learningsEmpty').style.display='none';
2581
+ S.lastLearningsData=data;
2582
+ var flakyCount=data.flakyTests?data.flakyTests.length:0;
2583
+ var passRate=data.overallPassRate||0;
2584
+ var declining=data.recentTrend&&Array.isArray(data.recentTrend.data||data.recentTrend)&&(function(){
2585
+ var td=data.recentTrend.data||data.recentTrend;
2586
+ if(td.length<2)return false;
2587
+ var last=td[td.length-1].pass_rate;
2588
+ var prior=td.slice(0,-1).reduce(function(s,t){return s+t.pass_rate},0)/(td.length-1);
2589
+ return last-prior<-2;
2590
+ })();
2591
+ if(passRate<70){
2592
+ $('#badgeLearnings').textContent='\u26A0';
2593
+ $('#badgeLearnings').style.background='var(--red-dim)';$('#badgeLearnings').style.color='var(--red)';
2594
+ } else if(flakyCount>0||declining){
2595
+ $('#badgeLearnings').textContent=flakyCount>0?flakyCount:(declining?'\u25BC':'\u2714');
2596
+ $('#badgeLearnings').style.background='var(--amber-dim)';$('#badgeLearnings').style.color='var(--amber)';
2597
+ } else {
2598
+ $('#badgeLearnings').textContent='\u2714';
2599
+ $('#badgeLearnings').style.background='var(--green-dim)';$('#badgeLearnings').style.color='var(--green)';
2600
+ }
2601
+ renderLearnOverview(data);
2602
+ renderLearnTrend(data.recentTrend||[]);
2603
+ renderLearnFlaky(data.flakyTests||[]);
2604
+ renderLearnSelectors(data.unstableSelectors||[]);
2605
+ renderLearnPages(data.failingPages||[]);
2606
+ renderLearnApis(data.apiIssues||[]);
2607
+ renderLearnErrors(data.topErrors||[]);
2608
+ }).catch(function(){$('#learningsEmpty').style.display='block'});
2609
+ }
2610
+
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);
2624
+ });
2625
+ container.appendChild(grid);
1054
2626
  }
1055
- function dismissLiveRun(rid){
1056
- delete S.liveRuns[rid];
1057
- renderLive();
2627
+
2628
+ function renderLearnTrend(trend){
2629
+ var container=$('#learningsTrend');container.textContent='';
2630
+ 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';
2634
+ 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');
2636
+ 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
+ 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
+ var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
2639
+ 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
+ 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
+ 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)');
2643
+ var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
2644
+ });
2645
+ chartDiv.appendChild(svg);card.appendChild(chartDiv);
2646
+ var dates=document.createElement('div');dates.style.cssText='display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px';
2647
+ dates.appendChild(el('span',null,trend[0].date));dates.appendChild(el('span',null,trend[trend.length-1].date));
2648
+ card.appendChild(dates);container.appendChild(card);
2649
+ }
2650
+
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);
2668
+ });
2669
+ tbody.appendChild(tr);
2670
+ });
2671
+ tbl.appendChild(tbody);wrap.appendChild(tbl);card.appendChild(wrap);return card;
1058
2672
  }
2673
+
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
+ $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
2681
+ $('#learningsDays').addEventListener('change',refreshLearnings);
2682
+
2683
+ $('#btnExportLearnings').addEventListener('click',function(){
2684
+ var data=S.lastLearningsData;
2685
+ if(!data){showToast('No learnings data to export','error');return}
2686
+ var md='# E2E Learnings Report\n\n';
2687
+ md+='| Metric | Value |\n|--------|-------|\n';
2688
+ md+='| Total Runs | '+data.totalRuns+' |\n';
2689
+ md+='| Total Tests | '+data.totalTests+' |\n';
2690
+ md+='| Pass Rate | '+data.overallPassRate+'% |\n';
2691
+ md+='| Avg Duration | '+dur(data.avgDurationMs)+' |\n\n';
2692
+ if(data.flakyTests&&data.flakyTests.length){
2693
+ md+='## Flaky Tests\n\n| Test | Flaky Rate | Occurrences |\n|------|-----------|-------------|\n';
2694
+ data.flakyTests.forEach(function(f){md+='| '+f.test_name+' | '+f.flaky_rate+'% | '+f.flaky_count+' |\n'});md+='\n';
2695
+ }
2696
+ if(data.unstableSelectors&&data.unstableSelectors.length){
2697
+ md+='## Unstable Selectors\n\n| Selector | Action | Fail Rate |\n|----------|--------|-----------|\n';
2698
+ data.unstableSelectors.forEach(function(s){md+='| `'+s.selector+'` | '+s.action_type+' | '+s.fail_rate+'% |\n'});md+='\n';
2699
+ }
2700
+ downloadFile('learnings-report.md',md,'text/markdown');
2701
+ showToast('Learnings exported','success');
2702
+ });
2703
+
2704
+ /* ── Modal ── */
2705
+ function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
2706
+ $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
2707
+
2708
+
2709
+ /* ── view-live.js ── */
2710
+ /* ══════════════════════════════════════════════════════════════════
2711
+ Live Execution View
2712
+ ══════════════════════════════════════════════════════════════════ */
2713
+ function clearFinishedLiveRuns(){for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}renderLive()}
2714
+ function dismissLiveRun(rid){delete S.liveRuns[rid];renderLive()}
1059
2715
  $('#liveClearBtn').addEventListener('click',clearFinishedLiveRuns);
1060
2716
 
1061
2717
  function renderLive(){
1062
2718
  var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
1063
- var runs=S.liveRuns;
1064
- var runIds=Object.keys(runs);
1065
-
1066
- if(runIds.length===0){
1067
- panel.classList.remove('active');
1068
- navLive.style.display='none';
1069
- liveEmpty.style.display='block';
1070
- $('#liveClearBtn').style.display='none';
1071
- return;
1072
- }
2719
+ var runs=S.liveRuns;var runIds=Object.keys(runs);
1073
2720
 
1074
- navLive.style.display='';
1075
- liveEmpty.style.display='none';
1076
- panel.classList.add('active');
2721
+ if(runIds.length===0){panel.classList.remove('active');navLive.style.display='none';liveEmpty.style.display='block';$('#liveClearBtn').style.display='none';return}
2722
+
2723
+ navLive.style.display='';liveEmpty.style.display='none';panel.classList.add('active');
1077
2724
 
1078
- // Aggregate stats across all runs
1079
2725
  var gTotal=0,gCompleted=0,gPassed=0,gFailed=0,gActive=0,gRunning=false,gDone=true;
1080
- runIds.forEach(function(rid){
1081
- var r=runs[rid];gTotal+=r.total;gCompleted+=r.completed;gPassed+=r.passed;gFailed+=r.failed;gActive+=r.active;
1082
- if(r.on)gRunning=true;if(!r.done)gDone=false;
1083
- });
2726
+ runIds.forEach(function(rid){var r=runs[rid];gTotal+=r.total;gCompleted+=r.completed;gPassed+=r.passed;gFailed+=r.failed;gActive+=r.active;if(r.on)gRunning=true;if(!r.done)gDone=false});
1084
2727
 
1085
2728
  var badgeActive=0;
1086
2729
  runIds.forEach(function(rid){var r=runs[rid];Object.keys(r.tests).forEach(function(n){if(n!=='__error'&&r.tests[n].status==='running')badgeActive++})});
@@ -1088,21 +2731,13 @@ function renderLive(){
1088
2731
  $('#liveBadge').style.background=gRunning?'var(--purple-dim)':gFailed>0?'var(--red-dim)':'var(--green-dim)';
1089
2732
  $('#liveBadge').style.color=gRunning?'var(--purple)':gFailed>0?'var(--red)':'var(--green)';
1090
2733
 
1091
- $('#liveTotal').textContent=gTotal;
1092
- $('#livePass').textContent=gPassed;
1093
- $('#liveFail').textContent=gFailed;
1094
- $('#liveActive').textContent=gActive;
1095
- var pct=gTotal>0?gCompleted/gTotal*100:0;
1096
- $('#liveProgressFill').style.width=pct+'%';
1097
-
1098
- // Hide single-project info (now shown per-section)
2734
+ $('#liveTotal').textContent=gTotal;$('#livePass').textContent=gPassed;$('#liveFail').textContent=gFailed;$('#liveActive').textContent=gActive;
2735
+ $('#liveProgressFill').style.width=(gTotal>0?gCompleted/gTotal*100:0)+'%';
1099
2736
  $('#liveProject').style.display='none';
1100
2737
 
1101
- // Show "Clear All" when there are finished/stale runs
1102
2738
  var hasFinished=runIds.some(function(rid){return runs[rid].done||!runs[rid].on});
1103
2739
  $('#liveClearBtn').style.display=hasFinished?'inline-block':'none';
1104
2740
 
1105
- // Header state
1106
2741
  var lbl=panel.querySelector('.live-header .label');
1107
2742
  var anyStale=runIds.some(function(rid){return runs[rid].stale});
1108
2743
  if(!gRunning&&gDone){
@@ -1111,148 +2746,104 @@ function renderLive(){
1111
2746
  var dot=lbl.querySelector('.dot');if(dot)dot.remove();
1112
2747
  $('#liveProgressFill').style.background=anyStale?'var(--yellow)':gFailed>0?'var(--red)':'var(--green)';
1113
2748
  } else {
1114
- if(!lbl.querySelector('.dot')){
1115
- lbl.textContent='';
1116
- var d=el('span',{className:'dot'});lbl.appendChild(d);lbl.appendChild(document.createTextNode(' RUNNING'));
1117
- }
1118
- lbl.style.color='var(--purple)';
1119
- $('#liveProgressFill').style.background='var(--purple)';
2749
+ if(!lbl.querySelector('.dot')){lbl.textContent='';var d=el('span',{className:'dot'});lbl.appendChild(d);lbl.appendChild(document.createTextNode(' RUNNING'))}
2750
+ lbl.style.color='var(--purple)';$('#liveProgressFill').style.background='var(--purple)';
1120
2751
  }
1121
2752
 
1122
- // Render per-run sections
1123
2753
  grid.textContent='';
1124
2754
  runIds.forEach(function(rid){
1125
2755
  var L=runs[rid];
1126
- // Project section header
1127
2756
  var projLabel=L.project||(L.cwd?L.cwd.split('/').pop():'Run');
1128
2757
  var runStatus=L.done?(L.failed>0?'fail':'pass'):'running';
1129
2758
  var dismissBtn=null;
1130
- if(L.done||!L.on){
1131
- dismissBtn=el('button',{className:'lr-dismiss',onclick:function(e){e.stopPropagation();dismissLiveRun(rid)}},'\u2715');
1132
- }
1133
- var sectionHeader=el('div',{className:'lr-section-header '+runStatus},[
1134
- el('span',{className:'lr-project-name'},projLabel),
1135
- createTriggerBadge(L.triggeredBy),
1136
- el('span',{className:'lr-section-stats'},[
1137
- el('span',{},L.completed+'/'+L.total),
1138
- L.failed>0?el('span',{style:'color:var(--red);margin-left:6px'},L.failed+' failed'):null,
1139
- L.on?el('span',{className:'spinner-small',style:'margin-left:6px'}):null
1140
- ]),
2759
+ if(L.done||!L.on){dismissBtn=el('button',{className:'lr-dismiss',onclick:function(e){e.stopPropagation();dismissLiveRun(rid)}},'\u2715')}
2760
+ grid.appendChild(el('div',{className:'lr-section-header '+runStatus},[
2761
+ el('span',{className:'lr-project-name'},projLabel),createTriggerBadge(L.triggeredBy),
2762
+ el('span',{className:'lr-section-stats'},[el('span',{},L.completed+'/'+L.total),L.failed>0?el('span',{style:'color:var(--red);margin-left:6px'},L.failed+' failed'):null,L.on?el('span',{className:'spinner-small',style:'margin-left:6px'}):null]),
1141
2763
  dismissBtn
1142
- ]);
1143
- grid.appendChild(sectionHeader);
2764
+ ]));
2765
+
2766
+ var poolDist=buildPoolDistribution(L.tests);
2767
+ if(poolDist)grid.appendChild(poolDist);
1144
2768
 
1145
2769
  var testGrid=el('div',{className:'lr-test-grid'});
1146
- var names=Object.keys(L.tests);
1147
- names.forEach(function(name){
2770
+ Object.keys(L.tests).forEach(function(name){
1148
2771
  if(name==='__error')return;
1149
- var t=L.tests[name];
1150
- var testKey=rid+'::'+name;
2772
+ var t=L.tests[name];var testKey=rid+'::'+name;
1151
2773
  var iconText=t.status==='passed'?'\u2714':t.status==='failed'?'\u2718':'\u25CF';
1152
2774
  var iconColor=t.status==='passed'?'color:var(--green)':t.status==='failed'?'color:var(--red)':'color:var(--purple)';
1153
2775
  var meta='';
1154
- if(t.status==='running'){
1155
- meta=t.actionType?('Step '+(t.actions||0)+'/'+(t.totalActions||'?')):'starting...';
1156
- if(t.retry)meta='Retry '+t.retry;
1157
- } else if(t.status==='passed'){meta=t.duration||'done'}
2776
+ if(t.status==='running'){meta=t.actionType?('Step '+(t.actions||0)+'/'+(t.totalActions||'?')):'starting...';if(t.retry)meta='Retry '+t.retry}
2777
+ else if(t.status==='passed'){meta=t.duration||'done'}
1158
2778
  else if(t.status==='failed'){meta=t.error||'failed'}
1159
- // Action log
2779
+
1160
2780
  var stepsEl=el('div',{className:'lt-actions'});
1161
2781
  if(t.actionLog&&t.actionLog.length>0){
1162
2782
  t.actionLog.forEach(function(a){
1163
- var detail=a.selector||a.value||a.text||'';
2783
+ var detail=a.narrative||a.selector||a.value||a.text||'';
1164
2784
  var durText=a.duration!=null?(a.duration<1000?a.duration+'ms':(a.duration/1000).toFixed(1)+'s'):'';
1165
- stepsEl.appendChild(el('div',{className:'lt-step'},[
1166
- el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
1167
- el('span',{className:'step-type'},a.type),
1168
- el('span',{className:'step-detail'},detail),
1169
- el('span',{className:'step-dur'},durText)
2785
+ var retryBadge=null;
2786
+ if(a.actionRetries&&a.actionRetries>0){retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px;margin-left:4px'},'\u21BB x'+a.actionRetries)}
2787
+ var stepCls='lt-step'+(a.isPoolLog?' pool-log':'');
2788
+ stepsEl.appendChild(el('div',{className:stepCls},[
2789
+ el('span',{className:'step-icon '+(a.isPoolLog?'':a.success?'ok':'fail')},a.isPoolLog?'\uD83D\uDD17':a.success?'\u2714':'\u2718'),
2790
+ el('span',{className:'step-type'},a.isPoolLog?'pool':a.type),
2791
+ el('span',{className:'step-detail'},a.isPoolLog?a.narrative:detail),
2792
+ retryBadge,
2793
+ a.isPoolLog?null:el('span',{className:'step-dur'},durText)
1170
2794
  ]));
1171
2795
  });
1172
- if(t.status==='running'&&t.actions<t.totalActions){
1173
- stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'waiting...')]));
1174
- }
2796
+ if(t.status==='running'&&t.actions<t.totalActions){stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'waiting...')]))}
1175
2797
  } else if(t.status==='running'){
1176
2798
  stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'connecting...')]));
1177
2799
  }
1178
2800
  var isFinished=t.status==='passed'||t.status==='failed';
1179
- var isCollapsed=isFinished&&!S.liveExpanded.has(testKey);
1180
- var summaryEl=el('div',{className:'lt-summary'},[
1181
- el('span',{className:'lt-dur'},t.duration||''),
1182
- el('span',{className:'lt-expand'},isCollapsed?'\u25BC':'\u25B2')
1183
- ]);
1184
- // Screenshots section
2801
+ var isCollapsed=isFinished&&S.liveCollapsed.has(testKey);
2802
+ var summaryEl=el('div',{className:'lt-summary'},[el('span',{className:'lt-dur'},t.duration||''),el('span',{className:'lt-expand'},isCollapsed?'\u25BC':'\u25B2')]);
2803
+
1185
2804
  var ssEl=null;
1186
2805
  var allSS=(t.screenshots||[]).slice();
1187
2806
  if(t.errorScreenshot)allSS.push(t.errorScreenshot);
1188
2807
  if(allSS.length>0){
1189
2808
  var ssOpen=S.liveSSOpen&&S.liveSSOpen.has(testKey);
1190
- var toggle=el('div',{className:'lt-screenshots-toggle'+(ssOpen?' open':'')},[
1191
- el('span',{className:'ss-arrow'},'\u25B6'),
1192
- el('span',{},'Screenshots ('+allSS.length+')')
1193
- ]);
2809
+ var toggle=el('div',{className:'lt-screenshots-toggle'+(ssOpen?' open':'')},[el('span',{className:'ss-arrow'},'\u25B6'),el('span',{},'Screenshots ('+allSS.length+')')]);
1194
2810
  var ssGridEl=el('div',{className:'lt-screenshots-grid'});
1195
2811
  allSS.forEach(function(ssPath){
1196
- var fname=ssPath.split('/').pop();
1197
- var isErr=t.errorScreenshot&&ssPath===t.errorScreenshot;
2812
+ var fname=ssPath.split('/').pop();var isErr=t.errorScreenshot&&ssPath===t.errorScreenshot;
1198
2813
  var thumb=el('div',{className:'lt-ss-thumb'});
1199
- var img=document.createElement('img');
1200
- img.src='/api/image?path='+encodeURIComponent(ssPath);
1201
- img.alt=fname;img.loading='lazy';
2814
+ var img=document.createElement('img');img.src='/api/image?path='+encodeURIComponent(ssPath);img.alt=fname;img.loading='lazy';
1202
2815
  if(isErr)thumb.style.borderColor='var(--red)';
1203
2816
  thumb.appendChild(img);
1204
2817
  thumb.addEventListener('click',function(e){e.stopPropagation();openModal('/api/image?path='+encodeURIComponent(ssPath),fname)});
1205
2818
  var labelEl=el('div',{className:'lt-ss-label'},[el('span',{style:'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'},fname)]);
1206
- // Compute hash async and append badge
1207
- (function(lbl,sp){
1208
- ssHash(sp).then(function(h){lbl.appendChild(createHashBadge(h))});
1209
- })(labelEl,ssPath);
2819
+ (function(lbl,sp){ssHash(sp).then(function(h){lbl.appendChild(createHashBadge(h))})})(labelEl,ssPath);
1210
2820
  ssGridEl.appendChild(el('div',{},[thumb,labelEl]));
1211
2821
  });
1212
- toggle.addEventListener('click',function(e){
1213
- e.stopPropagation();
1214
- if(S.liveSSOpen.has(testKey))S.liveSSOpen.delete(testKey);else S.liveSSOpen.add(testKey);
1215
- toggle.classList.toggle('open');
1216
- ssGridEl.style.display=ssGridEl.style.display==='grid'?'none':'grid';
1217
- });
2822
+ toggle.addEventListener('click',function(e){e.stopPropagation();if(S.liveSSOpen.has(testKey))S.liveSSOpen.delete(testKey);else S.liveSSOpen.add(testKey);toggle.classList.toggle('open');ssGridEl.style.display=ssGridEl.style.display==='grid'?'none':'grid'});
1218
2823
  if(ssOpen)ssGridEl.style.display='grid';
1219
2824
  ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
1220
2825
  }
2826
+
2827
+ var serialBadge=t.serial?el('span',{className:'serial-badge'},'Serial'):null;
2828
+ var poolBadge=t.poolUrl?el('span',{className:'pool-badge'},t.poolUrl.replace('ws://','').replace('wss://','')):null;
1221
2829
  var card=el('div',{className:'live-test '+t.status+(isCollapsed?' collapsed':'')},[
1222
2830
  el('div',{className:'lt-name'},[
1223
2831
  t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
1224
- document.createTextNode(' '+name),
1225
- summaryEl
2832
+ document.createTextNode(' '+name),serialBadge,poolBadge,summaryEl
1226
2833
  ]),
1227
- el('div',{className:'lt-meta'},meta),
1228
- stepsEl
2834
+ el('div',{className:'lt-meta'},meta),stepsEl
1229
2835
  ]);
1230
2836
  if(ssEl)card.appendChild(ssEl);
1231
- // Network API logs in live view (clickable rows)
1232
2837
  if(t.networkLogs&&t.networkLogs.length&&!isCollapsed){
1233
2838
  var liveErrCount=t.networkLogs.filter(function(n){return n.status>=400}).length;
1234
- var liveNetLabel='Network ('+t.networkLogs.length+(liveErrCount?', '+liveErrCount+' error'+(liveErrCount!==1?'s':'')+')':')');
1235
- var liveNetToggle=el('div',{className:'rd-net-toggle',style:'margin:6px 0 0;padding-left:0'},[
1236
- el('span',{className:'net-arrow'},'\u25B6'),
1237
- el('span',{},liveNetLabel)
1238
- ]);
1239
- var liveNetList=el('div',{className:'rd-net-list'});
1240
- t.networkLogs.forEach(function(n){
1241
- var built=buildNetRow(n);
1242
- liveNetList.appendChild(built.row);
1243
- if(built.detail)liveNetList.appendChild(built.detail);
1244
- });
1245
- liveNetToggle.addEventListener('click',function(e){e.stopPropagation();liveNetToggle.classList.toggle('open')});
1246
- card.appendChild(liveNetToggle);
1247
- card.appendChild(liveNetList);
1248
- }
1249
- if(isFinished){
1250
- card.addEventListener('click',function(){
1251
- if(S.liveExpanded.has(testKey))S.liveExpanded.delete(testKey);
1252
- else S.liveExpanded.add(testKey);
1253
- renderLive();
1254
- });
2839
+ var liveNetHead=el('div',{className:'rd-net-head'},[el('span',{className:'net-arrow'},'\u25B6'),el('span',{className:'net-title'},'Network Requests'),el('div',{className:'net-stats'},[el('span',{className:'net-stat'},[document.createTextNode('Total: '),el('strong',null,String(t.networkLogs.length))]),liveErrCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(liveErrCount))]):null])]);
2840
+ var liveNetCols=el('div',{className:'rd-net-cols'},[el('span',{className:'col-e'},''),el('span',{className:'col-m'},'Method'),el('span',{className:'col-s'},'Status'),el('span',{className:'col-u'},'URL'),el('span',{className:'col-d'},'Time')]);
2841
+ var liveNetBody=el('div',{className:'rd-net-body'},[liveNetCols]);
2842
+ t.networkLogs.forEach(function(n){var built=buildNetRow(n);liveNetBody.appendChild(built.row);if(built.detail)liveNetBody.appendChild(built.detail)});
2843
+ liveNetHead.addEventListener('click',function(e){e.stopPropagation();liveNetHead.classList.toggle('open')});
2844
+ card.appendChild(el('div',{className:'rd-net-panel',style:'margin-top:6px'},[liveNetHead,liveNetBody]));
1255
2845
  }
2846
+ if(isFinished){card.addEventListener('click',function(e){if(window.getSelection().toString())return;if(S.liveCollapsed.has(testKey))S.liveCollapsed.delete(testKey);else S.liveCollapsed.add(testKey);renderLive()})}
1256
2847
  testGrid.appendChild(card);
1257
2848
  if(!isCollapsed)stepsEl.scrollTop=stepsEl.scrollHeight;
1258
2849
  });
@@ -1260,21 +2851,71 @@ function renderLive(){
1260
2851
  });
1261
2852
  }
1262
2853
 
1263
- /* ── Actions ── */
1264
- $('#btnRunAll').addEventListener('click',function(){triggerRun()});
1265
2854
 
1266
- /* ── Modal ── */
1267
- function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
1268
- $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
1269
- document.addEventListener('keydown',function(e){if(e.key==='Escape')$('#modal').classList.remove('open')});
2855
+ /* ── keyboard.js ── */
2856
+ /* ══════════════════════════════════════════════════════════════════
2857
+ Keyboard Shortcuts (Updated: 1=Watch, 2=Tests, 3=Runs, 4=Live)
2858
+ ══════════════════════════════════════════════════════════════════ */
2859
+ document.addEventListener('keydown',function(e){
2860
+ var tag=document.activeElement.tagName;
2861
+ if(tag==='INPUT'||tag==='SELECT'||tag==='TEXTAREA')return;
2862
+ if(e.key==='Escape'){
2863
+ if($('#kbModal').classList.contains('open')){$('#kbModal').classList.remove('open');return}
2864
+ if($('#modal').classList.contains('open')){$('#modal').classList.remove('open');return}
2865
+ if($('#suiteModalOverlay').classList.contains('open')){$('#suiteModalOverlay').classList.remove('open');return}
2866
+ if(S.selectedRun!==null){
2867
+ var expanded=document.querySelector('#runsBody tr.expanded');
2868
+ if(expanded){
2869
+ var next=expanded.nextElementSibling;
2870
+ if(next&&next.classList.contains('run-detail-row')){var w=next.querySelector('.rd-wrap');if(w)w.classList.remove('open');expanded.classList.remove('expanded');setTimeout(function(){if(next.parentNode)next.parentNode.removeChild(next)},350)}
2871
+ S.selectedRun=null;
2872
+ }
2873
+ return;
2874
+ }
2875
+ return;
2876
+ }
2877
+ if(e.key==='?'){$('#kbModal').classList.toggle('open');return}
2878
+ var viewMap={'1':'watch','2':'tests','3':'runs','4':'live','5':'instances'};
2879
+ if(viewMap[e.key]){showView(viewMap[e.key]);return}
2880
+ if(e.key==='r'){
2881
+ if(S.view==='watch')refreshWatch();
2882
+ else if(S.view==='tests'){refreshSuites();refreshVariables()}
2883
+ else if(S.view==='runs'){refreshRuns();refreshScreenshots();refreshLearnings()}
2884
+ else if(S.view==='live')renderLive();
2885
+ return;
2886
+ }
2887
+ if(S.view==='runs'&&(e.key==='j'||e.key==='k')){
2888
+ var visible=_allRunRows.filter(function(item){return item.tr.style.display!=='none'});
2889
+ if(!visible.length)return;
2890
+ if(e.key==='j')S.highlightedRunIdx=Math.min(S.highlightedRunIdx+1,visible.length-1);
2891
+ if(e.key==='k')S.highlightedRunIdx=Math.max(S.highlightedRunIdx-1,0);
2892
+ visible.forEach(function(item,i){if(i===S.highlightedRunIdx){item.tr.classList.add('selected');item.tr.scrollIntoView({block:'nearest'})}else item.tr.classList.remove('selected')});
2893
+ return;
2894
+ }
2895
+ if(S.view==='runs'&&e.key==='Enter'){
2896
+ var visible2=_allRunRows.filter(function(item){return item.tr.style.display!=='none'});
2897
+ if(S.highlightedRunIdx>=0&&S.highlightedRunIdx<visible2.length){visible2[S.highlightedRunIdx].tr.click()}
2898
+ return;
2899
+ }
2900
+ });
2901
+ $('#kbModal').addEventListener('click',function(e){if(e.target===$('#kbModal'))$('#kbModal').classList.remove('open')});
1270
2902
 
1271
- /* ── Init ── */
2903
+
2904
+ /* ── init.js ── */
2905
+ /* ══════════════════════════════════════════════════════════════════
2906
+ Init — startup sequence
2907
+ ══════════════════════════════════════════════════════════════════ */
2908
+ initTabs();
1272
2909
  connectWS();
1273
2910
  refreshStatus();
1274
2911
  refreshProjects();
1275
2912
  refreshSuites();
1276
2913
  refreshRuns();
1277
2914
  refreshScreenshots();
2915
+ refreshLearnings();
2916
+ refreshVariables();
2917
+ startWatchPolling();
2918
+
1278
2919
  })();
1279
2920
  </script>
1280
2921
  </body>