@matware/e2e-runner 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/.mcp.json +9 -0
  3. package/README.md +505 -279
  4. package/agents/test-analyzer.md +81 -0
  5. package/agents/test-creator.md +102 -0
  6. package/agents/test-improver.md +140 -0
  7. package/bin/cli.js +275 -7
  8. package/commands/create-test.md +50 -0
  9. package/commands/run.md +49 -0
  10. package/commands/verify-issue.md +63 -0
  11. package/package.json +11 -3
  12. package/skills/e2e-testing/SKILL.md +166 -0
  13. package/skills/e2e-testing/references/action-types.md +100 -0
  14. package/skills/e2e-testing/references/test-json-format.md +159 -0
  15. package/skills/e2e-testing/references/troubleshooting.md +182 -0
  16. package/src/actions.js +280 -17
  17. package/src/ai-generate.js +122 -11
  18. package/src/config.js +58 -0
  19. package/src/dashboard.js +173 -10
  20. package/src/db.js +232 -17
  21. package/src/index.js +9 -3
  22. package/src/learner-markdown.js +177 -0
  23. package/src/learner-neo4j.js +255 -0
  24. package/src/learner-sqlite.js +354 -0
  25. package/src/learner.js +413 -0
  26. package/src/mcp-tools.js +575 -16
  27. package/src/module-resolver.js +273 -0
  28. package/src/narrate.js +225 -0
  29. package/src/neo4j-pool.js +124 -0
  30. package/src/reporter.js +47 -2
  31. package/src/runner.js +180 -40
  32. package/src/verify.js +19 -5
  33. package/templates/build-dashboard.js +28 -0
  34. package/templates/dashboard/app.js +1152 -0
  35. package/templates/dashboard/styles.css +413 -0
  36. package/templates/dashboard/template.html +201 -0
  37. package/templates/dashboard.html +1091 -268
  38. package/templates/docker-compose-neo4j.yml +19 -0
  39. package/templates/e2e.config.js +3 -0
@@ -53,12 +53,13 @@ a{color:var(--accent);text-decoration:none}
53
53
  .ws-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:4px}
54
54
 
55
55
  /* ── Main ── */
56
- .main{margin-left:232px;flex:1;min-height:100vh}
56
+ .main{margin-left:232px;flex:1;min-height:100vh;display:flex;flex-direction:column}
57
57
  .main-header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--surface);position:sticky;top:0;z-index:40}
58
58
  .main-header .title{font-family:var(--sans);font-size:16px;font-weight:600}
59
59
  .main-header .actions{margin-left:auto;display:flex;gap:8px}
60
60
  .view{display:none;padding:24px}
61
61
  .view.active{display:block}
62
+ #view-live.active{display:flex;flex-direction:column;flex:1;min-height:calc(100vh - 0px);padding:16px}
62
63
 
63
64
  /* ── Buttons ── */
64
65
  .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}
@@ -83,6 +84,24 @@ a{color:var(--accent);text-decoration:none}
83
84
  .stat-val.accent{color:var(--accent)}
84
85
  .stat-val.purple{color:var(--purple)}
85
86
 
87
+ /* ── Learnings ── */
88
+ .learn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-bottom:20px}
89
+ .learn-stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:14px;text-align:center}
90
+ .learn-stat-val{font-size:24px;font-weight:700;margin-bottom:2px}
91
+ .learn-stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
92
+ .learn-section{margin-bottom:20px}
93
+ .learn-section-title{font-family:var(--sans);font-size:13px;font-weight:600;margin-bottom:10px;color:var(--text)}
94
+ .learn-table{width:100%;border-collapse:collapse;font-size:11px}
95
+ .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}
96
+ .learn-table th:hover{color:var(--text2)}
97
+ .learn-table th.sorted::after{content:' \\25B2';font-size:8px}
98
+ .learn-table th.sorted.desc::after{content:' \\25BC'}
99
+ .learn-table td{padding:6px 10px;border-bottom:1px solid var(--border);color:var(--text2)}
100
+ .learn-table td code{background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:10px;color:var(--text)}
101
+ .learn-table tbody tr:hover td{background:var(--surface2);color:var(--text)}
102
+ .learn-trend-chart{width:100%;height:100px;margin-bottom:20px}
103
+ .learn-trend-chart svg{width:100%;height:100%}
104
+
86
105
  /* ── Tables ── */
87
106
  .tbl-wrap{overflow-x:auto}
88
107
  table{width:100%;border-collapse:collapse;font-size:12px}
@@ -104,20 +123,84 @@ tbody tr.selected td{background:var(--accent-dim)}
104
123
  .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}
105
124
  .chart-bar:hover .tip{display:block}
106
125
 
126
+ /* ── Project Accordion ── */
127
+ .project-accordion{margin-bottom:2px}
128
+ .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}
129
+ .project-accordion-header:hover{background:var(--surface2);border-color:var(--border-hi)}
130
+ .project-accordion.open>.project-accordion-header{border-radius:var(--r) var(--r) 0 0;border-bottom-color:transparent;background:var(--surface2)}
131
+ .project-accordion-chevron{font-size:10px;color:var(--text3);transition:transform .2s ease;flex-shrink:0;width:16px;text-align:center}
132
+ .project-accordion.open>.project-accordion-header .project-accordion-chevron{transform:rotate(90deg);color:var(--accent)}
133
+ .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}
134
+ .project-accordion-meta{display:flex;align-items:center;gap:10px;flex-shrink:0}
135
+ .project-accordion-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px;background:var(--surface3);color:var(--text2)}
136
+ .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)}
137
+ .project-accordion.open>.project-accordion-body{max-height:5000px}
138
+
107
139
  /* ── Suite Cards ── */
108
- .suite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
109
- .suite-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .15s}
110
- .suite-card:hover{border-color:var(--border-hi)}
111
- .suite-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
112
- .suite-card-name{font-weight:600;font-size:13px}
113
- .suite-card-count{font-size:10px;color:var(--text3)}
114
- .suite-card-tests{list-style:none;margin-bottom:12px}
115
- .suite-card-tests li{font-size:11px;color:var(--text2);padding:2px 0;padding-left:14px;position:relative}
116
- .suite-card-tests li::before{content:">";position:absolute;left:0;color:var(--text3)}
140
+ .suite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;padding:16px}
141
+ .suite-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;transition:border-color .2s,box-shadow .2s}
142
+ .suite-card:hover{border-color:var(--border-hi);box-shadow:0 2px 12px rgba(0,0,0,.25)}
143
+ .suite-card-head{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border);background:var(--surface2)}
144
+ .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}
145
+ .suite-card-info{flex:1;min-width:0}
146
+ .suite-card-name{font-family:var(--sans);font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
147
+ .suite-card-file{font-size:10px;color:var(--text3);margin-top:1px;font-family:var(--mono)}
148
+ .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)}
149
+ .suite-card-count-num{font-size:16px;font-weight:700;color:var(--text);line-height:1}
150
+ .suite-card-count-lbl{font-size:8px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;line-height:1;margin-top:2px}
151
+ .suite-card-body{padding:0}
152
+ .suite-card-tests{list-style:none;max-height:180px;overflow-y:auto}
153
+ .suite-card-tests::-webkit-scrollbar{width:4px}
154
+ .suite-card-tests::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
155
+ .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)}
156
+ .suite-card-tests li:last-child{border-bottom:none}
157
+ .suite-card-tests li:hover{color:var(--text);background:var(--surface2)}
158
+ .suite-card-tests li::before{content:"\25B8";position:absolute;left:14px;color:var(--text3);font-size:9px;top:7px;transition:transform .15s}
159
+ .suite-card-tests li.expanded{color:var(--accent);background:var(--accent-dim)}
160
+ .suite-card-tests li.expanded::before{content:"\25BE";color:var(--accent)}
161
+ .suite-test-steps{padding:8px 0 8px 14px;border-left:2px solid var(--accent-dim);margin:6px 0 4px 6px}
162
+ .suite-card-footer{padding:10px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;background:var(--surface)}
163
+
164
+ /* ── Suite Detail Modal ── */
165
+ .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)}
166
+ .suite-modal-overlay.open{display:flex}
167
+ .suite-modal{width:100%;max-width:960px;background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;animation:suiteModalIn .2s ease}
168
+ @keyframes suiteModalIn{from{opacity:0;transform:translateY(-12px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
169
+ .suite-modal-header{display:flex;align-items:center;gap:14px;padding:18px 24px;background:var(--surface2);border-bottom:1px solid var(--border)}
170
+ .suite-modal-header .suite-card-icon{width:36px;height:36px;font-size:16px}
171
+ .suite-modal-title{flex:1;min-width:0}
172
+ .suite-modal-title h2{font-family:var(--sans);font-size:16px;font-weight:700;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
173
+ .suite-modal-title span{font-size:10px;color:var(--text3);font-family:var(--mono)}
174
+ .suite-modal-actions{display:flex;gap:8px;align-items:center;flex-shrink:0}
175
+ .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}
176
+ .suite-modal-close:hover{background:var(--red-dim);border-color:rgba(239,68,68,.3);color:var(--red)}
177
+ .suite-modal-body{padding:0}
178
+ .suite-modal-loading{padding:32px;text-align:center;color:var(--text3);font-size:12px}
179
+ .suite-modal-test{border-bottom:1px solid var(--border)}
180
+ .suite-modal-test:last-child{border-bottom:none}
181
+ .suite-modal-test-header{display:flex;align-items:center;gap:10px;padding:12px 24px;cursor:pointer;transition:background .15s;user-select:none}
182
+ .suite-modal-test-header:hover{background:var(--surface2)}
183
+ .suite-modal-test.open>.suite-modal-test-header{background:var(--surface2)}
184
+ .suite-modal-test-chevron{font-size:9px;color:var(--text3);transition:transform .15s;width:14px;text-align:center;flex-shrink:0}
185
+ .suite-modal-test.open>.suite-modal-test-header .suite-modal-test-chevron{transform:rotate(90deg);color:var(--accent)}
186
+ .suite-modal-test-name{font-size:12px;font-weight:500;color:var(--text);flex:1}
187
+ .suite-modal-test-badge{font-size:9px;padding:2px 7px;border-radius:8px;background:var(--surface3);color:var(--text3);flex-shrink:0}
188
+ .suite-modal-test-actions{display:none;padding:0 24px 16px 48px}
189
+ .suite-modal-test.open>.suite-modal-test-actions{display:block}
190
+ .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)}
191
+ .suite-modal-step:last-child{border-bottom:none}
192
+ .suite-modal-step-num{width:22px;text-align:right;color:var(--text3);flex-shrink:0;font-size:10px;padding-top:1px}
193
+ .suite-modal-step-type{color:var(--purple);font-weight:600;flex-shrink:0;min-width:120px}
194
+ .suite-modal-step-detail{color:var(--text2);flex:1;min-width:0;word-break:break-word}
195
+ .suite-modal-step-detail .step-sel{color:var(--accent)}
196
+ .suite-modal-step-detail .step-arrow{color:var(--text3);margin:0 4px}
197
+ .suite-modal-step-detail .step-val{color:var(--text)}
198
+ .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}
199
+ .suite-modal-expect-label{font-weight:600;flex-shrink:0}
117
200
 
118
201
  /* ── Live Execution ── */
119
- .live-panel{display:none;background:var(--surface);border:1px solid var(--purple);border-radius:var(--r);overflow:hidden;animation:fadeSlide .3s ease}
120
- .live-panel.active{display:block}
202
+ .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}
203
+ .live-panel.active{display:flex;flex:1;min-height:0}
121
204
  @keyframes fadeSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
122
205
  .live-header{padding:14px 16px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--border);background:var(--purple-dim)}
123
206
  .live-header .label{font-weight:600;color:var(--purple);font-size:12px;display:flex;align-items:center;gap:8px}
@@ -128,7 +211,7 @@ tbody tr.selected td{background:var(--accent-dim)}
128
211
  .live-stats span strong{color:var(--text)}
129
212
  .live-progress{height:3px;background:var(--surface3)}
130
213
  .live-progress-fill{height:100%;background:var(--purple);transition:width .4s;border-radius:0 2px 2px 0}
131
- .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;max-height:calc(100vh - 200px);overflow-y:auto}
214
+ .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto;min-height:0}
132
215
  .live-test{padding:10px 12px;border-radius:var(--r);border-left:3px solid var(--text3);background:var(--surface2);font-size:11px;transition:border-color .2s,padding .25s,max-height .35s cubic-bezier(.4,0,.2,1)}
133
216
  .live-test.running{border-left-color:var(--purple)}
134
217
  .live-test.passed{border-left-color:var(--green)}
@@ -144,14 +227,14 @@ tbody tr.selected td{background:var(--accent-dim)}
144
227
  .live-test .lt-summary .lt-expand{color:var(--purple);font-size:9px;opacity:.6}
145
228
  .live-test .lt-meta{color:var(--text2);font-size:10px;margin-bottom:6px}
146
229
  .live-test .lt-icon{font-size:12px}
147
- .lt-actions{max-height:180px;overflow-y:auto;border-top:1px solid var(--border);padding-top:6px;margin-top:4px}
230
+ .lt-actions{overflow-y:auto;border-top:1px solid var(--border);padding-top:6px;margin-top:4px}
148
231
  .lt-step{display:flex;align-items:flex-start;gap:6px;padding:2px 0;font-size:10px;font-family:var(--mono);line-height:1.4}
149
232
  .lt-step .step-icon{flex-shrink:0;width:14px;text-align:center}
150
233
  .lt-step .step-icon.ok{color:var(--green)}
151
234
  .lt-step .step-icon.fail{color:var(--red)}
152
235
  .lt-step .step-icon.run{color:var(--purple)}
153
236
  .lt-step .step-type{color:var(--purple);font-weight:600;flex-shrink:0}
154
- .lt-step .step-detail{color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
237
+ .lt-step .step-detail{color:var(--text2);flex:1;min-width:0;white-space:pre-wrap;word-break:break-word}
155
238
  .lt-step .step-dur{color:var(--text3);flex-shrink:0;margin-left:auto}
156
239
  .lt-screenshots{border-top:1px solid var(--border);margin-top:6px;padding-top:6px}
157
240
  .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}
@@ -170,11 +253,15 @@ tbody tr.selected td{background:var(--accent-dim)}
170
253
  .lr-section-header.fail{border-color:var(--red);background:rgba(248,113,113,.08);color:var(--red)}
171
254
  .lr-section-stats{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text2);font-weight:400}
172
255
  .lr-project-name{letter-spacing:.5px}
173
- .lr-test-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px;padding:4px 0 8px}
256
+ .lr-test-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px;padding:4px 0 8px}
174
257
  .live-done{background:var(--green-dim);color:var(--green);text-align:center;padding:10px;font-weight:600;font-size:12px}
175
258
  .live-done.has-failures{background:var(--red-dim);color:var(--red)}
176
259
  .live-close{padding:4px 10px;font-size:10px;background:transparent;border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer}
177
260
  .live-close:hover{color:var(--text);border-color:var(--border-hi)}
261
+ .live-clear-btn{padding:5px 12px;font-size:10px;font-family:var(--mono);font-weight:500;background:var(--surface2);border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer;display:none;transition:all .15s}
262
+ .live-clear-btn:hover{color:var(--text);border-color:var(--border-hi);background:var(--surface3)}
263
+ .lr-dismiss{padding:2px 6px;font-size:9px;font-family:var(--mono);background:transparent;border:1px solid transparent;border-radius:4px;color:var(--text3);cursor:pointer;transition:all .15s;margin-left:auto}
264
+ .lr-dismiss:hover{color:var(--red);border-color:rgba(239,68,68,.3);background:var(--red-dim)}
178
265
 
179
266
  .live-nav-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
180
267
  .spinner{display:inline-block;width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
@@ -189,42 +276,138 @@ tbody tr.selected td{background:var(--accent-dim)}
189
276
  .gallery-item .cap{padding:6px 10px;font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
190
277
  .gallery-item .cap .cap-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
191
278
  .gallery-item .cap .ss-hash{flex-shrink:0}
279
+ .ss-search{display:flex;gap:8px;margin-bottom:16px;align-items:center}
280
+ .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}
281
+ .ss-search input:focus{outline:none;border-color:var(--accent)}
282
+ .ss-search input::placeholder{color:var(--text3)}
283
+ .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}
284
+ .ss-search button:hover{background:var(--surface3);border-color:var(--accent)}
285
+ .ss-search-result{margin-bottom:20px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
286
+ .ss-search-result img{max-width:100%;max-height:500px;border-radius:var(--r);cursor:pointer;display:block;margin-top:8px}
287
+ .ss-search-result .ss-result-label{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px}
288
+ .ss-search-error{font-size:11px;color:var(--red);margin-bottom:12px}
192
289
 
193
290
  /* ── Inline Run Detail ── */
194
- .run-detail-row td{padding:0!important;border-bottom:2px solid var(--border-hi)}
291
+ .run-detail-row td{padding:0!important;border-bottom:2px solid var(--purple)}
195
292
  .run-detail-row:hover td{background:transparent!important}
196
- .rd-wrap{overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1);max-height:0}
197
- .rd-wrap.open{max-height:2000px}
198
- .rd-inner{padding:16px 20px;background:var(--bg);border-left:3px solid var(--purple);margin:0 8px 8px}
199
- .rd-test{margin-bottom:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
293
+ .rd-wrap{overflow:hidden;transition:max-height .4s cubic-bezier(.4,0,.2,1);max-height:0}
294
+ .rd-wrap.open{max-height:10000px}
295
+ .rd-inner{padding:20px 24px;background:var(--bg);position:relative}
296
+ .rd-inner::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--purple),var(--accent),transparent 80%)}
297
+ .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}
298
+ .rd-summary>div{background:var(--surface);padding:14px 16px;text-align:center;transition:background .15s}
299
+ .rd-summary>div:hover{background:var(--surface2)}
300
+ .rd-s-label{font-size:9px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
301
+ .rd-s-val{font-weight:700;font-size:18px;margin-top:4px}
302
+ .rd-test{margin-bottom:12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .2s}
303
+ .rd-test:hover{border-color:var(--border-hi)}
200
304
  .rd-test:last-child{margin-bottom:0}
201
- .rd-test-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:var(--surface2)}
202
- .rd-test-name{font-weight:600;font-size:12px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
203
- .rd-test-dur{font-size:11px;color:var(--text2);flex-shrink:0}
204
- .rd-test-body{padding:10px 14px}
205
- .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)}
206
- .rd-shots{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
207
- .rd-shot{width:120px;border-radius:4px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:border-color .15s,transform .15s}
208
- .rd-shot:hover{border-color:var(--accent);transform:scale(1.03)}
209
- .rd-shot img{width:100%;height:72px;object-fit:cover;display:block}
210
- .rd-shot .rd-shot-cap{font-size:9px;color:var(--text3);padding:4px 6px;background:var(--surface2)}
305
+ .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}
306
+ .rd-test-head::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px}
307
+ .rd-test.pass .rd-test-head::before{background:var(--green)}
308
+ .rd-test.fail .rd-test-head::before{background:var(--red)}
309
+ .rd-test.flaky .rd-test-head::before{background:var(--amber)}
310
+ .rd-test-name{font-family:var(--sans);font-weight:600;font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
311
+ .rd-test-dur{font-size:11px;color:var(--text2);flex-shrink:0;font-variant-numeric:tabular-nums}
312
+ .rd-test-body{padding:16px}
313
+ .rd-retries{font-size:11px;color:var(--amber);margin-bottom:10px;display:flex;align-items:center;gap:6px}
314
+ .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}
315
+ .rd-shots{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px}
316
+ .rd-shot{width:140px;border-radius:6px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:all .2s;background:var(--surface2)}
317
+ .rd-shot:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.35)}
318
+ .rd-shot img{width:100%;height:84px;object-fit:cover;display:block}
319
+ .rd-shot .rd-shot-cap{font-size:9px;color:var(--text3);padding:5px 8px;background:var(--surface)}
211
320
  .rd-shot .rd-shot-cap .cap-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
212
321
  .rd-shot .rd-shot-cap .ss-hash{margin-top:3px}
213
322
  .rd-shot.err-shot{border-color:rgba(239,68,68,.4)}
214
323
  .rd-shot.err-shot .rd-shot-cap{color:var(--red)}
215
- .rd-logs{margin-top:10px}
216
- .rd-log-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;margin-bottom:4px}
217
- .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}
324
+ .rd-logs{margin-bottom:12px}
325
+ .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}
326
+ .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}
218
327
  .rd-log-item.error{border-left-color:var(--red);color:var(--red)}
219
328
  .rd-log-item.warning,.rd-log-item.warn{border-left-color:var(--amber);color:var(--amber)}
220
- .rd-retries{font-size:10px;color:var(--amber);margin-bottom:6px}
221
- .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}
222
- .rd-summary .rd-s-label{color:var(--text3);font-size:9px;text-transform:uppercase;letter-spacing:.08em}
223
- .rd-summary .rd-s-val{font-weight:700;font-size:16px;margin-top:2px}
329
+ .rd-net-panel{margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--surface)}
330
+ .rd-net-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface2);cursor:pointer;user-select:none;transition:background .15s}
331
+ .rd-net-head:hover{background:var(--surface3)}
332
+ .rd-net-head .net-arrow{font-size:9px;color:var(--text3);transition:transform .2s}
333
+ .rd-net-head.open .net-arrow{transform:rotate(90deg)}
334
+ .rd-net-head .net-title{font-family:var(--sans);font-size:12px;font-weight:600;color:var(--text)}
335
+ .rd-net-head .net-stats{margin-left:auto;display:flex;gap:12px;font-size:10px}
336
+ .rd-net-head .net-stat{color:var(--text3)}
337
+ .rd-net-head .net-stat strong{color:var(--text2)}
338
+ .rd-net-head .net-stat.has-err strong{color:var(--red)}
339
+ .rd-net-body{display:none;max-height:600px;overflow-y:auto}
340
+ .rd-net-head.open~.rd-net-body{display:block}
341
+ .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}
342
+ .rd-net-cols .col-e{width:16px;flex-shrink:0}
343
+ .rd-net-cols .col-m{width:50px;flex-shrink:0}
344
+ .rd-net-cols .col-s{width:60px;flex-shrink:0}
345
+ .rd-net-cols .col-u{flex:1;min-width:0}
346
+ .rd-net-cols .col-d{width:60px;flex-shrink:0;text-align:right}
347
+ .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}
348
+ .rd-net-row:hover{background:var(--surface2)}
349
+ .rd-net-row.has-error{background:rgba(239,68,68,.03)}
350
+ .rd-net-row.has-error:hover{background:rgba(239,68,68,.06)}
351
+ .rd-net-expand{width:16px;flex-shrink:0;font-size:8px;color:var(--text3);transition:transform .2s;text-align:center}
352
+ .rd-net-row.open .rd-net-expand{transform:rotate(90deg);color:var(--accent)}
353
+ .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}
354
+ .rd-net-method.get{background:var(--accent-dim);color:var(--accent)}
355
+ .rd-net-method.post{background:var(--green-dim);color:var(--green)}
356
+ .rd-net-method.put,.rd-net-method.patch{background:var(--amber-dim);color:var(--amber)}
357
+ .rd-net-method.delete{background:var(--red-dim);color:var(--red)}
358
+ .rd-net-status{width:60px;flex-shrink:0;font-size:10px;font-weight:600}
359
+ .rd-net-status.s2xx{color:var(--green)}
360
+ .rd-net-status.s3xx{color:var(--amber)}
361
+ .rd-net-status.s4xx,.rd-net-status.s5xx{color:var(--red)}
362
+ .rd-net-url{flex:1;min-width:0;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
363
+ .rd-net-dur{width:60px;flex-shrink:0;text-align:right;color:var(--text3);font-variant-numeric:tabular-nums}
364
+ .rd-net-detail{display:none;border-bottom:1px solid var(--border);background:var(--bg);overflow:hidden}
365
+ .rd-net-row.open+.rd-net-detail{display:block}
366
+ .rd-nd-section{border-bottom:1px solid var(--border)}
367
+ .rd-nd-section:last-child{border-bottom:none}
368
+ .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}
369
+ .rd-nd-toggle:hover{background:var(--surface2);color:var(--text2)}
370
+ .rd-nd-toggle .nd-arrow{font-size:8px;transition:transform .2s}
371
+ .rd-nd-toggle.open .nd-arrow{transform:rotate(90deg)}
372
+ .rd-nd-toggle .nd-count{margin-left:auto;font-size:9px;font-weight:400;letter-spacing:0;text-transform:none}
373
+ .rd-nd-content{display:none;padding:0 16px 10px;max-height:300px;overflow-y:auto}
374
+ .rd-nd-toggle.open+.rd-nd-content{display:block}
375
+ .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)}
376
+ .rd-hdr-table{display:grid;grid-template-columns:minmax(120px,auto) 1fr;gap:0;font-size:11px}
377
+ .rd-hdr-row{display:contents}
378
+ .rd-hdr-row:hover .rd-hdr-key,.rd-hdr-row:hover .rd-hdr-val{background:var(--surface2)}
379
+ .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}
380
+ .rd-hdr-val{padding:3px 0;color:var(--text2);border-bottom:1px solid var(--border);word-break:break-all}
381
+ .rd-nd-empty{color:var(--text3);font-size:11px;padding:4px 0;font-style:italic}
382
+ .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}
383
+ .copy-btn:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-dim)}
384
+ .copy-btn.copied{color:var(--green);border-color:var(--green);background:var(--green-dim)}
385
+ .rd-error-msg .copy-btn{position:absolute;top:8px;right:8px;opacity:0}
386
+ .rd-error-msg:hover .copy-btn{opacity:1}
387
+ .rd-net-row .copy-btn{opacity:0;padding:1px 6px}
388
+ .rd-net-row:hover .copy-btn{opacity:1}
389
+ .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}
390
+ .rd-net-body .rd-log-item:last-child{border-bottom:none}
391
+ .rd-net-body .rd-log-item.error{border-left-color:var(--red)}
392
+ .rd-net-body .rd-log-item.warn,.rd-net-body .rd-log-item.warning{border-left-color:var(--amber)}
224
393
  tr.expanded td{background:var(--surface2)!important}
225
394
  tr.expanded td:first-child{position:relative}
226
395
  tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--purple)}
227
396
 
397
+ /* ── Screenshot Hash Badge ── */
398
+ .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}
399
+ .ss-hash:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
400
+ .ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
401
+ .ss-hash .ss-icon{font-size:10px;line-height:1}
402
+
403
+ /* ── Trigger Source Badges ── */
404
+ .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}
405
+ .trigger-badge.src-dashboard{background:rgba(127,140,162,.10);color:var(--text2)}
406
+ .trigger-badge.src-mcp{background:var(--purple-dim);color:var(--purple)}
407
+ .trigger-badge.src-cli{background:var(--accent-dim);color:var(--accent)}
408
+ .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
409
+ .trigger-badge .trig-icon{font-size:11px;line-height:1}
410
+
228
411
  /* ── Empty ── */
229
412
  .empty{text-align:center;padding:48px 24px;color:var(--text3)}
230
413
  .empty-icon{font-size:36px;margin-bottom:8px;opacity:.5}
@@ -234,20 +417,70 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
234
417
  .modal.open{display:flex}
235
418
  .modal img{max-width:100%;max-height:90vh;border-radius:var(--r);cursor:default}
236
419
 
237
- /* ── Screenshot Hash Badge ── */
238
- .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}
239
- .ss-hash:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
240
- .ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
241
- .ss-hash .ss-icon{font-size:10px;line-height:1}
420
+ /* ── Toast Notifications (NEW) ── */
421
+ .toast-container{position:fixed;bottom:24px;right:24px;z-index:300;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none}
422
+ .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}
423
+ .toast.success{background:var(--green);border:1px solid rgba(255,255,255,.15)}
424
+ .toast.error{background:var(--red);border:1px solid rgba(255,255,255,.15)}
425
+ .toast.info{background:var(--accent);border:1px solid rgba(255,255,255,.15)}
426
+ .toast.fade-out{animation:toastOut .3s ease forwards}
427
+ @keyframes toastIn{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}
428
+ @keyframes toastOut{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(24px)}}
429
+
430
+ /* ── Filter Bar (NEW) ── */
431
+ .filter-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
432
+ .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}
433
+ .filter-btn:hover{background:var(--surface3);border-color:var(--border-hi)}
434
+ .filter-btn.active{background:var(--accent-dim);border-color:var(--accent);color:var(--accent)}
435
+ .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}
436
+ .filter-bar input:focus{outline:none;border-color:var(--accent)}
437
+ .filter-bar input::placeholder{color:var(--text3)}
438
+
439
+ /* ── Module Cards (NEW) ── */
440
+ .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}
441
+ .module-section-title .mod-icon{color:var(--purple)}
442
+ .module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
443
+ .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}
444
+ .module-card:hover{border-color:var(--border-hi);border-left-color:var(--purple)}
445
+ .module-card-name{font-weight:600;font-size:13px;color:var(--purple);margin-bottom:4px}
446
+ .module-card-desc{font-size:11px;color:var(--text2);margin-bottom:8px}
447
+ .module-card-meta{font-size:10px;color:var(--text3);display:flex;gap:12px}
448
+ .module-card-params{list-style:none;font-size:10px;color:var(--text2);margin-top:6px}
449
+ .module-card-params li{padding:2px 0}
450
+ .module-card-params li::before{content:'$';color:var(--purple);margin-right:4px}
451
+
452
+ /* ── Keyboard Shortcuts Modal (NEW) ── */
453
+ .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}
454
+ .kb-modal.open{display:flex}
455
+ .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}
456
+ .kb-modal-content h2{font-family:var(--sans);font-size:16px;font-weight:700;margin-bottom:16px;color:var(--text)}
457
+ .kb-row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border)}
458
+ .kb-row:last-child{border-bottom:none}
459
+ .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)}
460
+ .kb-desc{font-size:12px;color:var(--text2)}
461
+
462
+ /* ── Actions Panel in Run Detail (NEW) ── */
463
+ .rd-actions-panel{margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--surface)}
464
+ .rd-actions-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface2);cursor:pointer;user-select:none;transition:background .15s}
465
+ .rd-actions-head:hover{background:var(--surface3)}
466
+ .rd-actions-head .act-arrow{font-size:9px;color:var(--text3);transition:transform .2s}
467
+ .rd-actions-head.open .act-arrow{transform:rotate(90deg)}
468
+ .rd-actions-body{display:none;max-height:500px;overflow-y:auto;padding:8px 14px}
469
+ .rd-actions-head.open~.rd-actions-body{display:block}
470
+
471
+ /* ── Serial Badge (NEW) ── */
472
+ .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}
242
473
 
243
474
  /* ── Responsive ── */
244
475
  @media(max-width:768px){
245
- .sidebar{width:60px}.sidebar-logo h1,.sidebar-section-label,.nav-item span:not(.icon),.pool-info,.sidebar select,.sidebar-logo .ver{display:none}
476
+ .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}
246
477
  .nav-item{justify-content:center;padding:12px}.nav-item .icon{width:auto}
247
478
  .main{margin-left:60px}
248
- .suite-grid,.gallery{grid-template-columns:1fr}
479
+ .suite-grid,.gallery,.module-grid{grid-template-columns:1fr}
249
480
  .lr-test-grid{grid-template-columns:1fr}
481
+ .toast-container{right:12px;bottom:12px}
250
482
  }
483
+
251
484
  </style>
252
485
  </head>
253
486
  <body>
@@ -272,13 +505,16 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
272
505
  <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>
273
506
  </div>
274
507
  <div class="nav-item active" data-view="suites">
275
- <i class="icon">&#9655;</i><span>Suites</span>
508
+ <i class="icon">&#9655;</i><span>Suites</span><span class="badge" id="badgeSuites">-</span>
276
509
  </div>
277
510
  <div class="nav-item" data-view="runs">
278
- <i class="icon">&#9776;</i><span>Runs</span>
511
+ <i class="icon">&#9776;</i><span>Runs</span><span class="badge" id="badgeRuns">-</span>
279
512
  </div>
280
513
  <div class="nav-item" data-view="screenshots">
281
- <i class="icon">&#9635;</i><span>Screenshots</span>
514
+ <i class="icon">&#9635;</i><span>Screenshots</span><span class="badge" id="badgeScreenshots">-</span>
515
+ </div>
516
+ <div class="nav-item" data-view="learnings">
517
+ <i class="icon">&#9733;</i><span>Learnings</span><span class="badge" id="badgeLearnings">-</span>
282
518
  </div>
283
519
 
284
520
  <div class="pool-status" id="poolStatus">
@@ -312,6 +548,7 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
312
548
  <span style="color:var(--red)">Fail: <strong id="liveFail">0</strong></span>
313
549
  <span style="color:var(--purple)">Active: <strong id="liveActive">0</strong></span>
314
550
  </div>
551
+ <button class="live-clear-btn" id="liveClearBtn">Clear All</button>
315
552
  </div>
316
553
  <div class="live-progress"><div class="live-progress-fill" id="liveProgressFill" style="width:0"></div></div>
317
554
  <div class="live-tests" id="liveTests"></div>
@@ -327,9 +564,10 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
327
564
  <div class="view active" id="view-suites">
328
565
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
329
566
  <div style="font-family:var(--sans);font-size:16px;font-weight:600">Test Suites</div>
330
- <button class="btn primary" id="btnRunAll">Run All Tests</button>
331
567
  </div>
568
+ <div id="suiteAccordionContainer"></div>
332
569
  <div class="suite-grid" id="suiteGrid"></div>
570
+ <div id="moduleSection"></div>
333
571
  <div class="empty" id="suitesEmpty" style="display:none">
334
572
  <div class="empty-icon">&#9655;</div>
335
573
  <p>No test suites found.</p>
@@ -338,11 +576,20 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
338
576
 
339
577
  <!-- Runs View -->
340
578
  <div class="view" id="view-runs">
341
- <div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Run History</div>
579
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
580
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Run History</div>
581
+ </div>
342
582
  <div class="card">
343
583
  <div class="card-label">Pass Rate Trend</div>
344
584
  <div class="chart" id="trendChart"></div>
345
585
  </div>
586
+ <div class="filter-bar" id="filterBar">
587
+ <button class="filter-btn active" data-filter="all">All</button>
588
+ <button class="filter-btn" data-filter="pass">Pass</button>
589
+ <button class="filter-btn" data-filter="fail">Fail</button>
590
+ <button class="filter-btn" data-filter="mixed">Mixed</button>
591
+ <input type="text" id="runSearchInput" placeholder="Search suite..." spellcheck="false">
592
+ </div>
346
593
  <div class="card" style="padding:0">
347
594
  <div class="tbl-wrap">
348
595
  <table>
@@ -357,9 +604,42 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
357
604
  </div>
358
605
  </div>
359
606
 
607
+ <!-- Learnings View -->
608
+ <div class="view" id="view-learnings">
609
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
610
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Learnings</div>
611
+ <div style="display:flex;gap:8px;align-items:center">
612
+ <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">
613
+ <option value="7">7 days</option>
614
+ <option value="14">14 days</option>
615
+ <option value="30" selected>30 days</option>
616
+ <option value="90">90 days</option>
617
+ </select>
618
+ <button class="btn sm" id="btnExportLearnings">Export MD</button>
619
+ <button class="btn sm" id="btnRefreshLearnings">Refresh</button>
620
+ </div>
621
+ </div>
622
+ <div id="learningsOverview"></div>
623
+ <div id="learningsTrend"></div>
624
+ <div id="learningsFlaky"></div>
625
+ <div id="learningsSelectors"></div>
626
+ <div id="learningsPages"></div>
627
+ <div id="learningsApis"></div>
628
+ <div id="learningsErrors"></div>
629
+ <div class="empty" id="learningsEmpty" style="display:none">
630
+ <div class="empty-icon">&#9733;</div>
631
+ <p>No learnings data yet. Run some tests to start building knowledge.</p>
632
+ </div>
633
+ </div>
634
+
360
635
  <!-- Screenshots View -->
361
636
  <div class="view" id="view-screenshots">
362
637
  <div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Screenshots</div>
638
+ <div class="ss-search">
639
+ <input type="text" id="ssHashInput" placeholder="Search by hash (e.g. ss:a3f2b1c9)" spellcheck="false">
640
+ <button id="ssHashBtn">Search</button>
641
+ </div>
642
+ <div id="ssSearchResult"></div>
363
643
  <div class="gallery" id="screenshotGallery"></div>
364
644
  <div class="empty" id="screenshotsEmpty" style="display:none">
365
645
  <div class="empty-icon">&#9635;</div>
@@ -370,6 +650,22 @@ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bot
370
650
  </div>
371
651
 
372
652
  <div class="modal" id="modal"><img id="modalImg" src="" alt=""></div>
653
+ <div class="toast-container" id="toastContainer"></div>
654
+ <div class="kb-modal" id="kbModal">
655
+ <div class="kb-modal-content">
656
+ <h2>Keyboard Shortcuts</h2>
657
+ <div class="kb-row"><span class="kb-key">1</span><span class="kb-desc">Suites view</span></div>
658
+ <div class="kb-row"><span class="kb-key">2</span><span class="kb-desc">Runs view</span></div>
659
+ <div class="kb-row"><span class="kb-key">3</span><span class="kb-desc">Screenshots view</span></div>
660
+ <div class="kb-row"><span class="kb-key">4</span><span class="kb-desc">Learnings view</span></div>
661
+ <div class="kb-row"><span class="kb-key">5</span><span class="kb-desc">Live view</span></div>
662
+ <div class="kb-row"><span class="kb-key">j / k</span><span class="kb-desc">Navigate runs (next / previous)</span></div>
663
+ <div class="kb-row"><span class="kb-key">Enter</span><span class="kb-desc">Expand / collapse selected run</span></div>
664
+ <div class="kb-row"><span class="kb-key">Esc</span><span class="kb-desc">Close modal / collapse run</span></div>
665
+ <div class="kb-row"><span class="kb-key">r</span><span class="kb-desc">Refresh current view</span></div>
666
+ <div class="kb-row"><span class="kb-key">?</span><span class="kb-desc">Show this help</span></div>
667
+ </div>
668
+ </div>
373
669
 
374
670
  <script>
375
671
  (function(){
@@ -393,6 +689,96 @@ function css(n){return n.replace(/[^a-zA-Z0-9\-_]/g,'_')}
393
689
  function dur(ms){return ms>=1000?(ms/1000).toFixed(1)+'s':ms+'ms'}
394
690
  function fdate(iso){return iso?new Date(iso).toLocaleString():'--'}
395
691
 
692
+ function prettyJson(str){
693
+ if(!str)return '';
694
+ try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
695
+ }
696
+
697
+ function fmtHeaders(h){
698
+ if(!h||typeof h!=='object')return '';
699
+ return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
700
+ }
701
+
702
+ function buildHeaderKV(h){
703
+ if(!h||typeof h!=='object') return el('div',{className:'rd-nd-empty'},'No data');
704
+ var table=el('div',{className:'rd-hdr-table'});
705
+ Object.keys(h).forEach(function(k){
706
+ var row=el('div',{className:'rd-hdr-row'});
707
+ row.appendChild(el('span',{className:'rd-hdr-key'},k));
708
+ row.appendChild(el('span',{className:'rd-hdr-val'},String(h[k])));
709
+ table.appendChild(row);
710
+ });
711
+ return table;
712
+ }
713
+
714
+ function makeCopyBtn(getTextFn){
715
+ var btn=el('span',{className:'copy-btn',onclick:function(e){
716
+ e.stopPropagation();
717
+ var text=typeof getTextFn==='function'?getTextFn():String(getTextFn);
718
+ navigator.clipboard.writeText(text).then(function(){
719
+ btn.textContent='\u2713 Copied';
720
+ btn.classList.add('copied');
721
+ setTimeout(function(){btn.textContent='\u2398 Copy';btn.classList.remove('copied')},1200);
722
+ });
723
+ }},'\u2398 Copy');
724
+ return btn;
725
+ }
726
+
727
+ function buildNdSection(title,contentEl,count,copyText){
728
+ var toggle=el('div',{className:'rd-nd-toggle'},[
729
+ el('span',{className:'nd-arrow'},'\u25B6'),
730
+ el('span',null,title),
731
+ count?el('span',{className:'nd-count'},count+' entries'):null,
732
+ makeCopyBtn(copyText||function(){return contentWrap.textContent})
733
+ ]);
734
+ var contentWrap=el('div',{className:'rd-nd-content'});
735
+ contentWrap.appendChild(contentEl);
736
+ toggle.addEventListener('click',function(e){
737
+ e.stopPropagation();
738
+ toggle.classList.toggle('open');
739
+ });
740
+ return el('div',{className:'rd-nd-section'},[toggle,contentWrap]);
741
+ }
742
+
743
+ function buildNetRow(n){
744
+ var mCls='rd-net-method '+(n.method||'GET').toLowerCase();
745
+ var sCode=n.status||0;
746
+ var sCls='rd-net-status '+(sCode<300?'s2xx':sCode<400?'s3xx':sCode<500?'s4xx':'s5xx');
747
+ var hasDetail=n.requestBody||n.responseBody||n.requestHeaders||n.responseHeaders;
748
+ var rowCls='rd-net-row'+(sCode>=400?' has-error':'');
749
+ var row=el('div',{className:rowCls},[
750
+ el('span',{className:'rd-net-expand'},hasDetail?'\u25B6':''),
751
+ el('span',{className:mCls},n.method||'GET'),
752
+ el('span',{className:sCls},String(sCode)),
753
+ el('span',{className:'rd-net-url'},n.url||''),
754
+ makeCopyBtn(n.url||''),
755
+ el('span',{className:'rd-net-dur'},dur(n.duration))
756
+ ]);
757
+ var detail=null;
758
+ if(hasDetail){
759
+ var sections=[];
760
+ if(n.requestHeaders){
761
+ var hCount=Object.keys(n.requestHeaders).length;
762
+ sections.push(buildNdSection('Request Headers',buildHeaderKV(n.requestHeaders),hCount,fmtHeaders(n.requestHeaders)));
763
+ }
764
+ if(n.requestBody){
765
+ var rbText=prettyJson(n.requestBody);
766
+ sections.push(buildNdSection('Request Body',el('pre',null,rbText),null,rbText));
767
+ }
768
+ if(n.responseHeaders){
769
+ var rhCount=Object.keys(n.responseHeaders).length;
770
+ sections.push(buildNdSection('Response Headers',buildHeaderKV(n.responseHeaders),rhCount,fmtHeaders(n.responseHeaders)));
771
+ }
772
+ if(n.responseBody){
773
+ var respText=prettyJson(n.responseBody);
774
+ sections.push(buildNdSection('Response Body',el('pre',null,respText),null,respText));
775
+ }
776
+ detail=el('div',{className:'rd-net-detail'},sections);
777
+ row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});
778
+ }
779
+ return {row:row,detail:detail};
780
+ }
781
+
396
782
  /* ── Screenshot hash helpers ── */
397
783
  var ssHashCache={};
398
784
  async function ssHash(filePath){
@@ -419,10 +805,55 @@ function createHashBadge(hash){
419
805
  return badge;
420
806
  }
421
807
 
808
+ function createTriggerBadge(source){
809
+ var s=source||'unknown';
810
+ var labels={dashboard:'Dashboard',mcp:'MCP',cli:'CLI',unknown:'--'};
811
+ var icons={dashboard:'\u{1F464}',mcp:'\u{1F916}',cli:'>_',unknown:'\u2022'};
812
+ var badge=el('span',{className:'trigger-badge src-'+s},[
813
+ el('span',{className:'trig-icon'},icons[s]||icons.unknown),
814
+ document.createTextNode(labels[s]||s)
815
+ ]);
816
+ return badge;
817
+ }
818
+
819
+ /* ══════════════════════════════════════════════════════════════════
820
+ Toast Notifications (Improvement 4)
821
+ ══════════════════════════════════════════════════════════════════ */
822
+ function showToast(message,type){
823
+ type=type||'info';
824
+ var container=$('#toastContainer');
825
+ var icons={success:'\u2714',error:'\u2718',info:'\u2139'};
826
+ var t=el('div',{className:'toast '+type},[
827
+ el('span',null,icons[type]||''),
828
+ el('span',null,message)
829
+ ]);
830
+ container.appendChild(t);
831
+ setTimeout(function(){
832
+ t.classList.add('fade-out');
833
+ setTimeout(function(){if(t.parentNode)t.parentNode.removeChild(t)},300);
834
+ },5000);
835
+ }
836
+
837
+ /* ══════════════════════════════════════════════════════════════════
838
+ Download helper (Improvement 8)
839
+ ══════════════════════════════════════════════════════════════════ */
840
+ function downloadFile(filename,content,mimeType){
841
+ var blob=new Blob([content],{type:mimeType||'text/plain'});
842
+ var url=URL.createObjectURL(blob);
843
+ var a=document.createElement('a');
844
+ a.href=url;a.download=filename;
845
+ document.body.appendChild(a);a.click();
846
+ document.body.removeChild(a);
847
+ URL.revokeObjectURL(url);
848
+ }
849
+
422
850
  /* ── State ── */
423
851
  var S={
424
852
  ws:null,project:null,view:'suites',selectedRun:null,
425
- liveRuns:{},liveExpanded:new Set(),liveSSOpen:new Set()
853
+ liveRuns:{},liveCollapsed:new Set(),liveSSOpen:new Set(),
854
+ runFilter:{status:'all',search:''},
855
+ lastLearningsData:null,
856
+ highlightedRunIdx:-1
426
857
  };
427
858
 
428
859
  /* ── Navigation ── */
@@ -442,33 +873,44 @@ function showView(v){
442
873
  $('#view-'+v).classList.add('active');
443
874
  }
444
875
 
445
- /* ── WebSocket ── */
876
+ /* ══════════════════════════════════════════════════════════════════
877
+ WebSocket
878
+ ══════════════════════════════════════════════════════════════════ */
446
879
  function connectWS(){
447
880
  var proto=location.protocol==='https:'?'wss:':'ws:';
448
881
  S.ws=new WebSocket(proto+'//'+location.host);
449
- S.ws.onopen=function(){$('#wsDot').style.background='var(--green)';$('#wsLabel').textContent='ws: connected';$('#wsLabel').style.color='var(--green)'};
450
- S.ws.onclose=function(){$('#wsDot').style.background='var(--red)';$('#wsLabel').textContent='ws: disconnected';$('#wsLabel').style.color='var(--text3)';setTimeout(connectWS,3000)};
882
+ S.ws.onopen=function(){
883
+ $('#wsDot').style.background='var(--green)';$('#wsLabel').textContent='ws: connected';$('#wsLabel').style.color='var(--green)';
884
+ showToast('WebSocket connected','info');
885
+ };
886
+ S.ws.onclose=function(){
887
+ $('#wsDot').style.background='var(--red)';$('#wsLabel').textContent='ws: disconnected';$('#wsLabel').style.color='var(--text3)';
888
+ setTimeout(connectWS,3000);
889
+ };
451
890
  S.ws.onerror=function(){};
452
891
  S.ws.onmessage=function(e){try{handleWS(JSON.parse(e.data))}catch(x){}};
453
892
  }
454
893
 
455
894
  function getLiveRun(m){
456
895
  var rid=m.runId;if(!rid)return null;
457
- if(!S.liveRuns[rid])S.liveRuns[rid]={on:true,done:false,total:0,completed:0,passed:0,failed:0,active:0,tests:{},project:m.project||null,cwd:m.cwd||null,runId:rid,_lastEvent:Date.now()};
896
+ if(!S.liveRuns[rid])S.liveRuns[rid]={on:true,done:false,total:0,completed:0,passed:0,failed:0,active:0,tests:{},project:m.project||null,cwd:m.cwd||null,triggeredBy:m.triggeredBy||null,runId:rid,_lastEvent:Date.now()};
458
897
  S.liveRuns[rid]._lastEvent=Date.now();
459
898
  return S.liveRuns[rid];
460
899
  }
461
900
  function anyLiveRunning(){for(var k in S.liveRuns)if(S.liveRuns[k].on)return true;return false}
462
901
 
463
- /* Staleness guard: if a live run gets no events for 15s and all tests are complete, auto-finish it */
464
902
  setInterval(function(){
465
903
  var changed=false;
466
904
  for(var k in S.liveRuns){
467
905
  var r=S.liveRuns[k];
468
- if(r.on&&!r.done&&Date.now()-r._lastEvent>15000){
469
- if(r.completed>=r.total&&r.total>0){r.on=false;r.done=true;r.active=0;changed=true}
470
- else if(Date.now()-r._lastEvent>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
906
+ var age=Date.now()-r._lastEvent;
907
+ if(r.on&&!r.done){
908
+ if(r.total===0&&age>10000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
909
+ else if(r.completed>=r.total&&r.total>0&&age>15000){r.on=false;r.done=true;r.active=0;changed=true}
910
+ else if(age>30000){r.on=false;r.done=true;r.stale=true;r.active=0;changed=true}
471
911
  }
912
+ if(r.done&&r.stale&&r.total===0&&age>15000){delete S.liveRuns[k];changed=true}
913
+ else if(r.done&&age>120000){delete S.liveRuns[k];changed=true}
472
914
  }
473
915
  if(changed)renderLive();
474
916
  },5000);
@@ -477,50 +919,58 @@ function handleWS(m){
477
919
  switch(m.event){
478
920
  case 'pool:status':renderPool(m.data);break;
479
921
  case 'run:start':
922
+ for(var dk in S.liveRuns){if(S.liveRuns[dk].done)delete S.liveRuns[dk]}
480
923
  var r=getLiveRun(m);
481
924
  r.total=m.total;r.on=true;r.done=false;
482
- S.liveExpanded=new Set();S.liveSSOpen=new Set();
925
+ S.liveCollapsed=new Set();S.liveSSOpen=new Set();
483
926
  showView('live');renderLive();break;
484
927
  case 'test:start':
485
- var r=getLiveRun(m);if(!r)break;
486
- r.active=m.activeCount;
487
- r.tests[m.name]={status:'running',actions:0,totalActions:0,error:null,actionLog:[],screenshots:[]};
928
+ var r2=getLiveRun(m);if(!r2)break;
929
+ r2.active=m.activeCount;
930
+ r2.tests[m.name]={status:'running',actions:0,totalActions:0,error:null,actionLog:[],screenshots:[],serial:m.serial||false};
488
931
  renderLive();break;
489
932
  case 'test:action':
490
- var r=getLiveRun(m);if(!r||!r.tests[m.name])break;
491
- var t=r.tests[m.name];
933
+ var r3=getLiveRun(m);if(!r3||!r3.tests[m.name])break;
934
+ var t=r3.tests[m.name];
492
935
  t.actions=m.actionIndex+1;t.totalActions=m.totalActions;t.actionType=m.action.type;
493
- 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});
936
+ 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});
494
937
  if(m.screenshotPath)t.screenshots.push(m.screenshotPath);
495
938
  renderLive();break;
496
939
  case 'test:retry':
497
- var r=getLiveRun(m);if(!r||!r.tests[m.name])break;
498
- r.tests[m.name].retry=m.attempt+'/'+m.maxAttempts;
940
+ var r4=getLiveRun(m);if(!r4||!r4.tests[m.name])break;
941
+ r4.tests[m.name].retry=m.attempt+'/'+m.maxAttempts;
499
942
  renderLive();break;
500
943
  case 'test:complete':
501
- var r=getLiveRun(m);if(!r)break;
502
- r.completed++;
503
- if(m.success){r.passed++;if(r.tests[m.name])r.tests[m.name].status='passed'}
504
- else{r.failed++;if(r.tests[m.name]){r.tests[m.name].status='failed';r.tests[m.name].error=m.error}}
505
- if(r.tests[m.name]){
506
- r.tests[m.name].duration=m.duration;
507
- if(m.screenshots&&m.screenshots.length)r.tests[m.name].screenshots=m.screenshots;
508
- if(m.errorScreenshot)r.tests[m.name].errorScreenshot=m.errorScreenshot;
944
+ var r5=getLiveRun(m);if(!r5)break;
945
+ r5.completed++;
946
+ if(m.success){r5.passed++;if(r5.tests[m.name])r5.tests[m.name].status='passed'}
947
+ else{r5.failed++;if(r5.tests[m.name]){r5.tests[m.name].status='failed';r5.tests[m.name].error=m.error}}
948
+ if(r5.tests[m.name]){
949
+ r5.tests[m.name].duration=m.duration;
950
+ if(m.screenshots&&m.screenshots.length)r5.tests[m.name].screenshots=m.screenshots;
951
+ if(m.errorScreenshot)r5.tests[m.name].errorScreenshot=m.errorScreenshot;
952
+ if(m.networkLogs&&m.networkLogs.length)r5.tests[m.name].networkLogs=m.networkLogs;
509
953
  }
510
- r.active=Math.max(0,r.active-1);
954
+ r5.active=Math.max(0,r5.active-1);
511
955
  renderLive();break;
512
956
  case 'run:complete':
513
- var r=getLiveRun(m);if(r){r.on=false;r.done=true;r.active=0}
957
+ var r6=getLiveRun(m);if(r6){r6.on=false;r6.done=true;r6.active=0}
958
+ var summary=m.summary||{};
959
+ if(summary.failed>0){showToast('Run complete: '+summary.failed+' failed','error')}
960
+ else{showToast('Run complete: all '+(summary.total||0)+' passed','success')}
514
961
  renderLive();refreshRuns();refreshProjects();break;
515
962
  case 'run:error':
516
- var r=getLiveRun(m);if(r){r.on=false;r.done=true;r.tests.__error={status:'failed',error:m.error}}
963
+ var r7=getLiveRun(m);if(r7){r7.on=false;r7.done=true;r7.tests.__error={status:'failed',error:m.error}}
964
+ showToast('Run error: '+m.error,'error');
517
965
  renderLive();break;
518
966
  case 'db:updated':
519
- refreshRuns();refreshProjects();refreshScreenshots();break;
967
+ refreshRuns();refreshProjects();refreshScreenshots();refreshLearnings();break;
520
968
  }
521
969
  }
522
970
 
523
- /* ── API ── */
971
+ /* ══════════════════════════════════════════════════════════════════
972
+ API & Pool
973
+ ══════════════════════════════════════════════════════════════════ */
524
974
  function api(p){return fetch(p).then(function(r){return r.json()})}
525
975
  function triggerRun(suite,projectId){
526
976
  if(anyLiveRunning())return;
@@ -531,7 +981,6 @@ function triggerRun(suite,projectId){
531
981
  fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
532
982
  }
533
983
 
534
- /* ── Pool ── */
535
984
  function renderPool(d){
536
985
  if(!d)return;
537
986
  $('#poolDot').className='pool-dot '+(d.error||!d.available?'off':'on');
@@ -540,7 +989,9 @@ function renderPool(d){
540
989
  }
541
990
  function refreshStatus(){api('/api/status').then(function(d){renderPool(d.pool)}).catch(function(){})}
542
991
 
543
- /* ── Projects ── */
992
+ /* ══════════════════════════════════════════════════════════════════
993
+ Projects
994
+ ══════════════════════════════════════════════════════════════════ */
544
995
  function refreshProjects(){
545
996
  api('/api/db/projects').then(function(projects){
546
997
  var sel=$('#projectSelect'),prev=sel.value;
@@ -554,77 +1005,234 @@ function refreshProjects(){
554
1005
  $('#projectSelect').addEventListener('change',function(){
555
1006
  S.project=this.value?parseInt(this.value,10):null;
556
1007
  S.selectedRun=null;
557
- refreshRuns();refreshSuites();refreshScreenshots();
1008
+ refreshRuns();refreshSuites();refreshScreenshots();refreshLearnings();
558
1009
  });
559
1010
 
560
- /* ── Suites ── */
1011
+ /* ══════════════════════════════════════════════════════════════════
1012
+ Suites (+ Serial badges, Modules)
1013
+ ══════════════════════════════════════════════════════════════════ */
561
1014
  function refreshSuites(){
562
- var grid=$('#suiteGrid'),empty=$('#suitesEmpty');
1015
+ var grid=$('#suiteGrid'),empty=$('#suitesEmpty'),accordion=$('#suiteAccordionContainer');
563
1016
  grid.textContent='';
1017
+ var moduleSection=$('#moduleSection');
1018
+ moduleSection.textContent='';
564
1019
 
565
1020
  if(S.project){
566
- // Single project — fetch its suites
567
1021
  api('/api/db/projects/'+S.project+'/suites').then(function(suites){
568
1022
  if(!Array.isArray(suites)||suites.length===0){empty.style.display='block';empty.querySelector('p').textContent='No test suites found for this project.';return}
569
1023
  empty.style.display='none';
1024
+ $('#badgeSuites').textContent=suites.length;
570
1025
  renderSuiteCards(grid,suites,S.project);
571
1026
  }).catch(function(){});
1027
+ api('/api/db/projects/'+S.project+'/modules').then(function(modules){
1028
+ renderModules(moduleSection,modules);
1029
+ }).catch(function(){});
572
1030
  } else {
573
- // All projects — fetch each project's suites
574
1031
  api('/api/db/projects').then(function(projects){
575
1032
  if(!Array.isArray(projects)||projects.length===0){empty.style.display='block';empty.querySelector('p').textContent='No projects registered yet.';return}
576
- var loaded=0,hasAny=false;
1033
+ var loaded=0,hasAny=false,totalSuites=0;
577
1034
  projects.forEach(function(p){
578
1035
  api('/api/db/projects/'+p.id+'/suites').then(function(suites){
579
1036
  loaded++;
580
1037
  if(Array.isArray(suites)&&suites.length>0){
581
- hasAny=true;
1038
+ hasAny=true;totalSuites+=suites.length;
582
1039
  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);
583
1040
  grid.appendChild(label);
584
1041
  renderSuiteCards(grid,suites,p.id);
585
1042
  }
586
- if(loaded===projects.length&&!hasAny){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.'}
1043
+ if(loaded===projects.length){
1044
+ $('#badgeSuites').textContent=totalSuites;
1045
+ if(!hasAny){empty.style.display='block';empty.querySelector('p').textContent='No test suites found.'}
1046
+ }
587
1047
  }).catch(function(){loaded++;});
588
1048
  });
589
1049
  }).catch(function(){});
590
1050
  }
591
1051
  }
1052
+
1053
+ function renderProjectAccordion(container,project,suites){
1054
+ var totalTests=suites.reduce(function(sum,s){return sum+(s.testCount||0)},0);
1055
+ var body=el('div',{className:'project-accordion-body'});
1056
+ var innerGrid=el('div',{className:'suite-grid'});
1057
+ renderSuiteCards(innerGrid,suites,project.id);
1058
+ body.appendChild(innerGrid);
1059
+
1060
+ var header=el('div',{className:'project-accordion-header'},[
1061
+ el('span',{className:'project-accordion-chevron'},'\u25B6'),
1062
+ el('span',{className:'project-accordion-name'},project.name),
1063
+ el('div',{className:'project-accordion-meta'},[
1064
+ el('span',{className:'project-accordion-badge'},suites.length+(suites.length===1?' suite':' suites')),
1065
+ el('span',{className:'project-accordion-badge'},totalTests+(totalTests===1?' test':' tests'))
1066
+ ])
1067
+ ]);
1068
+
1069
+ var wrapper=el('div',{className:'project-accordion'},[header,body]);
1070
+
1071
+ header.addEventListener('click',function(){
1072
+ wrapper.classList.toggle('open');
1073
+ });
1074
+
1075
+ container.appendChild(wrapper);
1076
+ }
1077
+
1078
+ var _suiteCache={};
592
1079
  function renderSuiteCards(container,suites,projectId){
593
1080
  suites.forEach(function(s){
594
1081
  var tests=el('ul',{className:'suite-card-tests'});
595
- (s.tests||[]).forEach(function(t){tests.appendChild(el('li',null,t))});
596
1082
  var pid=projectId;
597
- var card=el('div',{className:'suite-card'},[
598
- el('div',{className:'suite-card-head'},[
1083
+ (s.tests||[]).forEach(function(t){
1084
+ var li=el('li',null,t);
1085
+ li.addEventListener('click',function(e){
1086
+ e.stopPropagation();
1087
+ var existing=li.querySelector('.suite-test-steps');
1088
+ if(existing){existing.remove();li.classList.remove('expanded');return}
1089
+ tests.querySelectorAll('.suite-test-steps').forEach(function(d){d.remove()});
1090
+ tests.querySelectorAll('li.expanded').forEach(function(l){l.classList.remove('expanded')});
1091
+ var stepsDiv=el('div',{className:'suite-test-steps'});
1092
+ stepsDiv.appendChild(el('div',{style:'color:var(--text3);font-size:10px'},'Loading...'));
1093
+ li.appendChild(stepsDiv);
1094
+ li.classList.add('expanded');
1095
+ var cacheKey=pid+'::'+s.name;
1096
+ var p=_suiteCache[cacheKey]||api('/api/db/projects/'+pid+'/suites/'+encodeURIComponent(s.name));
1097
+ _suiteCache[cacheKey]=p;
1098
+ p.then(function(data){
1099
+ stepsDiv.textContent='';
1100
+ var test=(data.tests||[]).find(function(x){return x.name===t});
1101
+ if(!test||!test.actions||!test.actions.length){
1102
+ stepsDiv.appendChild(el('div',{style:'color:var(--text3);font-size:10px'},'No actions'));
1103
+ return;
1104
+ }
1105
+ if(test.serial){
1106
+ var sb=el('span',{className:'serial-badge'},'Serial');
1107
+ li.insertBefore(sb,li.querySelector('.suite-test-steps'));
1108
+ }
1109
+ test.actions.forEach(function(a,i){
1110
+ var detail=a.selector||a.value||a.text||'';
1111
+ if(a.selector&&(a.value||a.text))detail=a.selector+' \u2192 '+(a.text||a.value);
1112
+ stepsDiv.appendChild(el('div',{className:'lt-step'},[
1113
+ el('span',{className:'step-icon',style:'color:var(--text3)'},String(i+1)),
1114
+ el('span',{className:'step-type'},a.type),
1115
+ el('span',{className:'step-detail'},detail)
1116
+ ]));
1117
+ });
1118
+ }).catch(function(){
1119
+ stepsDiv.textContent='';
1120
+ stepsDiv.appendChild(el('div',{style:'color:var(--red);font-size:10px'},'Failed to load'));
1121
+ });
1122
+ });
1123
+ tests.appendChild(li);
1124
+ });
1125
+
1126
+ var cardHead=el('div',{className:'suite-card-head',style:'cursor:pointer'},[
1127
+ el('div',{className:'suite-card-icon'},'\u25B6'),
1128
+ el('div',{className:'suite-card-info'},[
599
1129
  el('div',{className:'suite-card-name'},s.name),
600
- el('span',{className:'suite-card-count'},s.testCount+' tests')
1130
+ el('div',{className:'suite-card-file'},s.file||s.name+'.json')
601
1131
  ]),
602
- tests,
603
- el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
1132
+ el('div',{className:'suite-card-count'},[
1133
+ el('div',{className:'suite-card-count-num'},String(s.testCount||0)),
1134
+ el('div',{className:'suite-card-count-lbl'},'tests')
1135
+ ])
1136
+ ]);
1137
+ (function(name,projId){
1138
+ cardHead.addEventListener('click',function(){openSuiteModal(name,projId)});
1139
+ })(s.name,pid);
1140
+
1141
+ var card=el('div',{className:'suite-card'},[
1142
+ cardHead,
1143
+ el('div',{className:'suite-card-body'},[tests]),
1144
+ el('div',{className:'suite-card-footer'},[
1145
+ el('button',{className:'btn sm primary',onclick:function(){triggerRun(s.name,pid)}},'Run Suite')
1146
+ ])
604
1147
  ]);
605
1148
  container.appendChild(card);
606
1149
  });
607
1150
  }
608
1151
 
609
- /* ── Runs ── */
1152
+ function renderModules(container,modules){
1153
+ if(!Array.isArray(modules)||modules.length===0)return;
1154
+ var title=el('div',{className:'module-section-title'},[
1155
+ el('span',{className:'mod-icon'},'\u{1F9E9}'),
1156
+ document.createTextNode(' Reusable Modules ('+modules.length+')')
1157
+ ]);
1158
+ container.appendChild(title);
1159
+ var grid=el('div',{className:'module-grid'});
1160
+ modules.forEach(function(m){
1161
+ var paramsEl=null;
1162
+ if(m.params&&m.params.length){
1163
+ var items=m.params.map(function(p){return el('li',null,typeof p==='string'?p:(p.name||String(p)))});
1164
+ paramsEl=el('ul',{className:'module-card-params'},items);
1165
+ }
1166
+ var card=el('div',{className:'module-card'},[
1167
+ el('div',{className:'module-card-name'},m.name),
1168
+ m.description?el('div',{className:'module-card-desc'},m.description):null,
1169
+ el('div',{className:'module-card-meta'},[
1170
+ el('span',null,m.actionCount+' actions'),
1171
+ m.params&&m.params.length?el('span',null,m.params.length+' params'):null
1172
+ ]),
1173
+ paramsEl
1174
+ ]);
1175
+ grid.appendChild(card);
1176
+ });
1177
+ container.appendChild(grid);
1178
+ }
1179
+
1180
+ /* ══════════════════════════════════════════════════════════════════
1181
+ Runs (+ Filters)
1182
+ ══════════════════════════════════════════════════════════════════ */
1183
+ $$('.filter-btn').forEach(function(btn){
1184
+ btn.addEventListener('click',function(){
1185
+ $$('.filter-btn').forEach(function(b){b.classList.remove('active')});
1186
+ btn.classList.add('active');
1187
+ S.runFilter.status=btn.dataset.filter;
1188
+ applyRunFilters();
1189
+ });
1190
+ });
1191
+ $('#runSearchInput').addEventListener('input',function(){
1192
+ S.runFilter.search=this.value.trim().toLowerCase();
1193
+ applyRunFilters();
1194
+ });
1195
+
1196
+ var _allRunRows=[];
1197
+ function applyRunFilters(){
1198
+ _allRunRows.forEach(function(item){
1199
+ var show=true;
1200
+ var r=item.data;
1201
+ if(S.runFilter.status!=='all'){
1202
+ var total=r.total||0;var passed=r.passed||0;var failed=r.failed||0;
1203
+ if(S.runFilter.status==='pass'&&(failed>0||total===0))show=false;
1204
+ if(S.runFilter.status==='fail'&&failed===0)show=false;
1205
+ if(S.runFilter.status==='mixed'&&(failed===0||passed===0))show=false;
1206
+ }
1207
+ if(show&&S.runFilter.search){
1208
+ var suite=(r.suite_name||'all').toLowerCase();
1209
+ var proj=(r.project_name||'').toLowerCase();
1210
+ if(suite.indexOf(S.runFilter.search)===-1&&proj.indexOf(S.runFilter.search)===-1)show=false;
1211
+ }
1212
+ item.tr.style.display=show?'':'none';
1213
+ if(item.detailTr)item.detailTr.style.display=show?'':'none';
1214
+ });
1215
+ }
1216
+
610
1217
  function refreshRuns(){
611
1218
  var url=S.project?'/api/db/projects/'+S.project+'/runs':'/api/db/runs';
612
1219
  api(url).then(function(rows){
613
1220
  var chart=$('#trendChart'),body=$('#runsBody'),empty=$('#runsEmpty'),head=$('#runsHead');
614
1221
  chart.textContent='';body.textContent='';
615
- if(!Array.isArray(rows)||rows.length===0){empty.style.display='block';head.parentNode.parentNode.style.display='none';return}
1222
+ _allRunRows=[];
1223
+ S.highlightedRunIdx=-1;
1224
+ if(!Array.isArray(rows)||rows.length===0){empty.style.display='block';head.parentNode.parentNode.style.display='none';$('#badgeRuns').textContent='0';return}
616
1225
  empty.style.display='none';head.parentNode.parentNode.style.display='';
1226
+ $('#badgeRuns').textContent=rows.length;
617
1227
 
618
- // Thead
619
1228
  var htr=document.createElement('tr');
620
1229
  var cols=[];
621
1230
  if(!S.project)cols.push('Project');
622
- cols=cols.concat(['Suite','Date','Total','Pass','Fail','Rate','Time']);
1231
+ cols=cols.concat(['Suite','Source','Date','Total','Pass','Fail','Rate','Time']);
623
1232
  cols.forEach(function(c){htr.appendChild(el('th',null,c))});
624
1233
  head.textContent='';head.appendChild(htr);
625
1234
  var colSpan=cols.length;
626
1235
 
627
- // Chart
628
1236
  rows.slice(0,40).slice().reverse().forEach(function(r){
629
1237
  var rate=parseFloat(r.pass_rate)||0;
630
1238
  var color=rate>=90?'var(--green)':rate>=70?'var(--amber)':'var(--red)';
@@ -633,13 +1241,13 @@ function refreshRuns(){
633
1241
  chart.appendChild(bar);
634
1242
  });
635
1243
 
636
- // Rows
637
1244
  rows.forEach(function(r){
638
1245
  var tr=document.createElement('tr');
639
1246
  tr.dataset.runId=r.id;
640
1247
  if(r.id===S.selectedRun)tr.classList.add('expanded');
641
1248
  if(!S.project)tr.appendChild(el('td',{style:'font-weight:600'},r.project_name||'-'));
642
1249
  tr.appendChild(el('td',{style:'color:var(--accent)'},r.suite_name||'all'));
1250
+ var srcTd=document.createElement('td');srcTd.appendChild(createTriggerBadge(r.triggered_by));tr.appendChild(srcTd);
643
1251
  tr.appendChild(el('td',null,fdate(r.generated_at)));
644
1252
  tr.appendChild(el('td',null,String(r.total||0)));
645
1253
  tr.appendChild(el('td',{style:'color:var(--green)'},String(r.passed||0)));
@@ -650,12 +1258,14 @@ function refreshRuns(){
650
1258
  tr.addEventListener('click',function(){toggleDetail(r.id,tr,colSpan)});
651
1259
  body.appendChild(tr);
652
1260
 
653
- // If this run was already expanded, re-expand it
1261
+ var item={tr:tr,data:r,detailTr:null};
654
1262
  if(r.id===S.selectedRun){
655
1263
  var detailTr=createDetailRow(colSpan);
656
1264
  body.appendChild(detailTr);
657
1265
  loadDetailInline(r.id,detailTr);
1266
+ item.detailTr=detailTr;
658
1267
  }
1268
+ _allRunRows.push(item);
659
1269
  });
660
1270
  }).catch(function(){});
661
1271
  }
@@ -666,8 +1276,12 @@ function createDetailRow(colSpan){
666
1276
  var td=document.createElement('td');
667
1277
  td.setAttribute('colspan',colSpan);
668
1278
  var wrap=el('div',{className:'rd-wrap'});
669
- var inner=el('div',{className:'rd-inner'});
670
- inner.innerHTML='<div style="color:var(--text3);font-size:11px"><span class="spinner-small"></span> Loading...</div>';
1279
+ var inner=el('div',{className:'rd-inner'},[
1280
+ el('div',{style:'color:var(--text3);font-size:11px'},[
1281
+ el('span',{className:'spinner-small'}),
1282
+ document.createTextNode(' Loading...')
1283
+ ])
1284
+ ]);
671
1285
  wrap.appendChild(inner);
672
1286
  td.appendChild(wrap);
673
1287
  detailTr.appendChild(td);
@@ -675,7 +1289,6 @@ function createDetailRow(colSpan){
675
1289
  }
676
1290
 
677
1291
  function toggleDetail(id,clickedTr,colSpan){
678
- // If already expanded, collapse
679
1292
  if(S.selectedRun===id){
680
1293
  var existing=clickedTr.nextElementSibling;
681
1294
  if(existing&&existing.classList.contains('run-detail-row')){
@@ -688,7 +1301,6 @@ function toggleDetail(id,clickedTr,colSpan){
688
1301
  return;
689
1302
  }
690
1303
 
691
- // Collapse any other open detail
692
1304
  var prevTr=document.querySelector('#runsBody tr.expanded');
693
1305
  if(prevTr){
694
1306
  prevTr.classList.remove('expanded');
@@ -700,81 +1312,106 @@ function toggleDetail(id,clickedTr,colSpan){
700
1312
  }
701
1313
  }
702
1314
 
703
- // Expand new
704
1315
  S.selectedRun=id;
705
1316
  clickedTr.classList.add('expanded');
706
1317
  var detailTr=createDetailRow(colSpan);
707
1318
  clickedTr.parentNode.insertBefore(detailTr,clickedTr.nextSibling);
708
-
709
- // Animate open
710
1319
  requestAnimationFrame(function(){
711
1320
  requestAnimationFrame(function(){
712
1321
  var w2=detailTr.querySelector('.rd-wrap');
713
1322
  if(w2)w2.classList.add('open');
714
1323
  });
715
1324
  });
716
-
717
1325
  loadDetailInline(id,detailTr);
718
1326
  }
719
1327
 
1328
+ /* ══════════════════════════════════════════════════════════════════
1329
+ Run Detail (+ Action Narratives, Retry badges, Export)
1330
+ ══════════════════════════════════════════════════════════════════ */
720
1331
  function loadDetailInline(id,detailTr){
721
1332
  api('/api/db/runs/'+id).then(function(d){
722
1333
  if(d.error)return;
723
1334
  var inner=detailTr.querySelector('.rd-inner');
724
1335
  inner.textContent='';
725
-
726
1336
  var results=d.results||[];
727
1337
 
728
- // Summary bar
1338
+ var exportBtn=el('div',null,[
1339
+ el('div',{className:'rd-s-label'},'Export'),
1340
+ el('div',{style:'margin-top:4px'},[
1341
+ el('button',{className:'btn sm',onclick:function(e){
1342
+ e.stopPropagation();
1343
+ downloadFile('run-'+id+'.json',JSON.stringify(d,null,2),'application/json');
1344
+ }},'JSON')
1345
+ ])
1346
+ ]);
1347
+ var srcBlock=el('div',null,[el('div',{className:'rd-s-label'},'Source'),el('div',{style:'margin-top:4px'},[createTriggerBadge(d.triggeredBy)])]);
729
1348
  var summ=el('div',{className:'rd-summary'},[
730
- 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')]),
1349
+ 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')]),
1350
+ srcBlock,
731
1351
  el('div',null,[el('div',{className:'rd-s-label'},'Total'),el('div',{className:'rd-s-val'},String(d.summary.total))]),
732
1352
  el('div',null,[el('div',{className:'rd-s-label'},'Passed'),el('div',{className:'rd-s-val',style:'color:var(--green)'},String(d.summary.passed))]),
733
- el('div',null,[el('div',{className:'rd-s-label'},'Failed'),el('div',{className:'rd-s-val',style:'color:var(--red)'},String(d.summary.failed))]),
734
- 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||'-')])
1353
+ 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))]),
1354
+ 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||'-')]),
1355
+ exportBtn
735
1356
  ]);
736
1357
  inner.appendChild(summ);
737
1358
 
738
- // Test result cards
739
1359
  results.forEach(function(r){
740
1360
  var d2=r.durationMs?dur(r.durationMs):r.endTime&&r.startTime?dur(new Date(r.endTime)-new Date(r.startTime)):'-';
741
1361
  var flaky=r.success&&r.attempt>1;
1362
+ var state=flaky?'flaky':(r.success?'pass':'fail');
742
1363
 
743
- // Header: badge + name + duration
744
1364
  var badges=el('div',{style:'display:flex;gap:6px;align-items:center;flex-shrink:0'});
745
1365
  badges.appendChild(el('span',{className:'badge '+(r.success?'pass':'fail')},r.success?'PASS':'FAIL'));
746
1366
  if(flaky)badges.appendChild(el('span',{className:'badge flaky'},'FLAKY'));
747
1367
 
748
- var head=el('div',{className:'rd-test-head'},[
749
- badges,
750
- el('div',{className:'rd-test-name'},r.name),
751
- el('div',{className:'rd-test-dur'},d2)
752
- ]);
753
-
754
- // Body content
1368
+ var head=el('div',{className:'rd-test-head'},[badges,el('div',{className:'rd-test-name'},r.name),el('div',{className:'rd-test-dur'},d2)]);
755
1369
  var body=el('div',{className:'rd-test-body'});
756
1370
 
757
- // Retries info
758
- if(r.maxAttempts>1){
759
- body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts));
1371
+ if(r.maxAttempts>1){body.appendChild(el('div',{className:'rd-retries'},'Attempt '+r.attempt+' of '+r.maxAttempts))}
1372
+ if(r.error){
1373
+ var errDiv=el('div',{className:'rd-error-msg'});
1374
+ errDiv.appendChild(document.createTextNode(r.error));
1375
+ errDiv.appendChild(makeCopyBtn(r.error));
1376
+ body.appendChild(errDiv);
760
1377
  }
761
1378
 
762
- // Error message
763
- if(r.error){
764
- body.appendChild(el('div',{className:'rd-error-msg'},r.error));
1379
+ // Actions panel
1380
+ if(r.actions&&r.actions.length){
1381
+ var passCount=r.actions.filter(function(a){return a.success}).length;
1382
+ var failCount=r.actions.length-passCount;
1383
+ var actHead=el('div',{className:'rd-net-head'},[
1384
+ el('span',{className:'net-arrow'},'\u25B6'),
1385
+ el('span',{className:'net-title'},'Actions'),
1386
+ el('div',{className:'net-stats'},[
1387
+ el('span',{className:'net-stat'},[document.createTextNode('Steps: '),el('strong',null,String(r.actions.length))]),
1388
+ failCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Failed: '),el('strong',null,String(failCount))]):null
1389
+ ])
1390
+ ]);
1391
+ var actBody=el('div',{className:'rd-net-body',style:'padding:8px 14px'});
1392
+ r.actions.forEach(function(a){
1393
+ var label=a.narrative||a.type;
1394
+ var durText=a.duration!=null?dur(a.duration):'';
1395
+ var retryBadge=null;
1396
+ if(a.actionRetries&&a.actionRetries>0){
1397
+ retryBadge=el('span',{className:'badge flaky',style:'font-size:9px;padding:1px 5px'},'\u21BB x'+a.actionRetries);
1398
+ }
1399
+ actBody.appendChild(el('div',{className:'lt-step'},[
1400
+ el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
1401
+ el('span',{className:'step-detail',style:'flex:1'},label),
1402
+ retryBadge,
1403
+ el('span',{className:'step-dur'},durText)
1404
+ ]));
1405
+ });
1406
+ actHead.addEventListener('click',function(){actHead.classList.toggle('open')});
1407
+ body.appendChild(el('div',{className:'rd-net-panel'},[actHead,actBody]));
765
1408
  }
766
1409
 
767
- // Screenshots per test
1410
+ // Screenshots
768
1411
  var shots=[];
769
1412
  var hashes=r.screenshotHashes||{};
770
- (r.screenshots||[]).forEach(function(p){
771
- var fname=p.split('/').pop();
772
- shots.push({path:p,label:fname,type:'screenshot',hash:hashes[p]||null});
773
- });
774
- if(r.errorScreenshot){
775
- var ename=r.errorScreenshot.split('/').pop();
776
- shots.push({path:r.errorScreenshot,label:ename,type:'error',hash:hashes[r.errorScreenshot]||null});
777
- }
1413
+ (r.screenshots||[]).forEach(function(p){shots.push({path:p,label:p.split('/').pop(),type:'screenshot',hash:hashes[p]||null})});
1414
+ if(r.errorScreenshot){shots.push({path:r.errorScreenshot,label:r.errorScreenshot.split('/').pop(),type:'error',hash:hashes[r.errorScreenshot]||null})}
778
1415
  if(shots.length){
779
1416
  var shotsWrap=el('div',{className:'rd-shots'});
780
1417
  shots.forEach(function(s){
@@ -783,95 +1420,138 @@ function loadDetailInline(id,detailTr){
783
1420
  var capEl=el('div',{className:'rd-shot-cap'},[el('span',{className:'cap-name'},s.label)]);
784
1421
  if(s.hash){capEl.appendChild(createHashBadge(s.hash))}
785
1422
  else{(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,s.path)}
786
- var shotEl=el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[
787
- img,capEl
788
- ]);
789
- shotsWrap.appendChild(shotEl);
1423
+ shotsWrap.appendChild(el('div',{className:'rd-shot'+(s.type==='error'?' err-shot':''),onclick:function(e){e.stopPropagation();openModal(src)}},[img,capEl]));
790
1424
  });
791
1425
  body.appendChild(shotsWrap);
792
1426
  }
793
1427
 
794
- // Console logs (errors and warnings only)
1428
+ // Console logs
795
1429
  var cIssues=(r.consoleLogs||[]).filter(function(l){return l.type==='error'||l.type==='warn'||l.type==='warning'});
796
1430
  if(cIssues.length){
797
- var logSec=el('div',{className:'rd-logs'});
798
- logSec.appendChild(el('div',{className:'rd-log-label'},'Console'));
799
- cIssues.forEach(function(l){
800
- logSec.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text));
801
- });
802
- body.appendChild(logSec);
1431
+ var cErrors=cIssues.filter(function(l){return l.type==='error'}).length;
1432
+ var cWarns=cIssues.length-cErrors;
1433
+ var conHead=el('div',{className:'rd-net-head'},[
1434
+ el('span',{className:'net-arrow'},'\u25B6'),
1435
+ el('span',{className:'net-title'},'Console'),
1436
+ el('div',{className:'net-stats'},[
1437
+ cErrors?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(cErrors))]):null,
1438
+ cWarns?el('span',{className:'net-stat'},[document.createTextNode('Warnings: '),el('strong',null,String(cWarns))]):null
1439
+ ]),
1440
+ makeCopyBtn(function(){return cIssues.map(function(l){return '['+l.type+'] '+l.text}).join('\n')})
1441
+ ]);
1442
+ var conBody=el('div',{className:'rd-net-body'});
1443
+ cIssues.forEach(function(l){conBody.appendChild(el('div',{className:'rd-log-item '+l.type},'['+l.type+'] '+l.text))});
1444
+ conHead.addEventListener('click',function(){conHead.classList.toggle('open')});
1445
+ body.appendChild(el('div',{className:'rd-net-panel'},[conHead,conBody]));
803
1446
  }
804
1447
 
805
1448
  // Network errors
806
1449
  if(r.networkErrors&&r.networkErrors.length){
807
- var netSec=el('div',{className:'rd-logs'});
808
- netSec.appendChild(el('div',{className:'rd-log-label'},'Network Errors'));
809
- r.networkErrors.forEach(function(ne){
810
- netSec.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url));
811
- });
812
- body.appendChild(netSec);
1450
+ var neHead=el('div',{className:'rd-net-head'},[
1451
+ el('span',{className:'net-arrow'},'\u25B6'),
1452
+ el('span',{className:'net-title'},'Network Errors'),
1453
+ el('div',{className:'net-stats'},[el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(r.networkErrors.length))])]),
1454
+ makeCopyBtn(function(){return r.networkErrors.map(function(ne){return '['+ne.error+'] '+ne.url}).join('\n')})
1455
+ ]);
1456
+ var neBody=el('div',{className:'rd-net-body'});
1457
+ r.networkErrors.forEach(function(ne){neBody.appendChild(el('div',{className:'rd-log-item error'},'['+ne.error+'] '+ne.url))});
1458
+ neHead.addEventListener('click',function(){neHead.classList.toggle('open')});
1459
+ body.appendChild(el('div',{className:'rd-net-panel'},[neHead,neBody]));
1460
+ }
1461
+
1462
+ // Network panel
1463
+ if(r.networkLogs&&r.networkLogs.length){
1464
+ var errCount=r.networkLogs.filter(function(n){return n.status>=400}).length;
1465
+ var netHead=el('div',{className:'rd-net-head'},[
1466
+ el('span',{className:'net-arrow'},'\u25B6'),
1467
+ el('span',{className:'net-title'},'Network Requests'),
1468
+ el('div',{className:'net-stats'},[
1469
+ el('span',{className:'net-stat'},[document.createTextNode('Total: '),el('strong',null,String(r.networkLogs.length))]),
1470
+ errCount?el('span',{className:'net-stat has-err'},[document.createTextNode('Errors: '),el('strong',null,String(errCount))]):null
1471
+ ])
1472
+ ]);
1473
+ 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')]);
1474
+ var netBody=el('div',{className:'rd-net-body'},[netCols]);
1475
+ r.networkLogs.forEach(function(n){var built=buildNetRow(n);netBody.appendChild(built.row);if(built.detail)netBody.appendChild(built.detail)});
1476
+ netHead.addEventListener('click',function(){netHead.classList.toggle('open')});
1477
+ body.appendChild(el('div',{className:'rd-net-panel'},[netHead,netBody]));
813
1478
  }
814
1479
 
815
- var testCard=el('div',{className:'rd-test'},[head,body]);
816
- inner.appendChild(testCard);
1480
+ inner.appendChild(el('div',{className:'rd-test '+state},[head,body]));
817
1481
  });
818
1482
 
819
- // Re-trigger open animation if not yet open
820
1483
  var w=detailTr.querySelector('.rd-wrap');
821
- if(w&&!w.classList.contains('open')){
822
- requestAnimationFrame(function(){w.classList.add('open')});
823
- }
824
- }).catch(function(err){
1484
+ if(w&&!w.classList.contains('open')){requestAnimationFrame(function(){w.classList.add('open')})}
1485
+ }).catch(function(){
825
1486
  var inner=detailTr.querySelector('.rd-inner');
826
1487
  if(inner)inner.textContent='Failed to load run detail';
827
1488
  });
828
1489
  }
829
1490
 
830
- /* ── Screenshots ── */
1491
+ /* ══════════════════════════════════════════════════════════════════
1492
+ Screenshots
1493
+ ══════════════════════════════════════════════════════════════════ */
831
1494
  function refreshScreenshots(){
832
1495
  var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
833
1496
  gal.textContent='';
834
- if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';return}
1497
+ if(!S.project){empty.style.display='block';empty.querySelector('p').textContent='Select a project to view screenshots.';$('#badgeScreenshots').textContent='-';return}
835
1498
  api('/api/db/projects/'+S.project+'/screenshots').then(function(files){
836
- if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';return}
1499
+ if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
837
1500
  empty.style.display='none';
1501
+ $('#badgeScreenshots').textContent=files.length;
838
1502
  files.forEach(function(f){
839
1503
  var src='/api/image?path='+encodeURIComponent(f.path);
840
1504
  var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
841
1505
  var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
842
- (function(c,fp){
843
- ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))});
844
- })(capEl,f.path);
845
- var item=el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[
846
- img,capEl
847
- ]);gal.appendChild(item);
1506
+ (function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
1507
+ gal.appendChild(el('div',{className:'gallery-item',onclick:function(){openModal(src)}},[img,capEl]));
848
1508
  });
849
1509
  }).catch(function(){});
850
1510
  }
851
1511
 
852
- /* ── Live Execution ── */
1512
+ function searchByHash(){
1513
+ var container=$('#ssSearchResult');
1514
+ container.textContent='';
1515
+ var raw=$('#ssHashInput').value.trim();
1516
+ if(!raw)return;
1517
+ var hash=raw.replace(/^ss:/,'');
1518
+ if(!/^[a-f0-9]{1,8}$/i.test(hash)){
1519
+ container.appendChild(el('div',{className:'ss-search-error'},'Invalid hash format. Expected 8 hex characters (e.g. ss:a3f2b1c9).'));
1520
+ return;
1521
+ }
1522
+ fetch('/api/screenshot-hash/'+hash).then(function(res){
1523
+ if(!res.ok){container.appendChild(el('div',{className:'ss-search-error'},'Screenshot not found for hash: ss:'+hash));return}
1524
+ return res.blob();
1525
+ }).then(function(blob){
1526
+ if(!blob)return;
1527
+ var url=URL.createObjectURL(blob);
1528
+ var wrap=el('div',{className:'ss-search-result'},[el('div',{className:'ss-result-label'},[createHashBadge(hash),el('span',{},'Found')])]);
1529
+ var img=document.createElement('img');img.src=url;img.alt='ss:'+hash;
1530
+ img.addEventListener('click',function(){openModal(url)});
1531
+ wrap.appendChild(img);
1532
+ container.appendChild(wrap);
1533
+ }).catch(function(){container.appendChild(el('div',{className:'ss-search-error'},'Error searching for screenshot.'))});
1534
+ }
1535
+ $('#ssHashBtn').addEventListener('click',searchByHash);
1536
+ $('#ssHashInput').addEventListener('keydown',function(e){if(e.key==='Enter')searchByHash()});
1537
+
1538
+ /* ══════════════════════════════════════════════════════════════════
1539
+ Live Execution (+ Retry badges, Serial badges)
1540
+ ══════════════════════════════════════════════════════════════════ */
1541
+ function clearFinishedLiveRuns(){for(var k in S.liveRuns){if(S.liveRuns[k].done||!S.liveRuns[k].on)delete S.liveRuns[k]}renderLive()}
1542
+ function dismissLiveRun(rid){delete S.liveRuns[rid];renderLive()}
1543
+ $('#liveClearBtn').addEventListener('click',clearFinishedLiveRuns);
1544
+
853
1545
  function renderLive(){
854
1546
  var panel=$('#livePanel'),grid=$('#liveTests'),navLive=$('#navLive'),liveEmpty=$('#liveEmpty');
855
- var runs=S.liveRuns;
856
- var runIds=Object.keys(runs);
1547
+ var runs=S.liveRuns;var runIds=Object.keys(runs);
857
1548
 
858
- if(runIds.length===0){
859
- panel.classList.remove('active');
860
- navLive.style.display='none';
861
- liveEmpty.style.display='block';
862
- return;
863
- }
1549
+ if(runIds.length===0){panel.classList.remove('active');navLive.style.display='none';liveEmpty.style.display='block';$('#liveClearBtn').style.display='none';return}
864
1550
 
865
- navLive.style.display='';
866
- liveEmpty.style.display='none';
867
- panel.classList.add('active');
1551
+ navLive.style.display='';liveEmpty.style.display='none';panel.classList.add('active');
868
1552
 
869
- // Aggregate stats across all runs
870
1553
  var gTotal=0,gCompleted=0,gPassed=0,gFailed=0,gActive=0,gRunning=false,gDone=true;
871
- runIds.forEach(function(rid){
872
- var r=runs[rid];gTotal+=r.total;gCompleted+=r.completed;gPassed+=r.passed;gFailed+=r.failed;gActive+=r.active;
873
- if(r.on)gRunning=true;if(!r.done)gDone=false;
874
- });
1554
+ 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});
875
1555
 
876
1556
  var badgeActive=0;
877
1557
  runIds.forEach(function(rid){var r=runs[rid];Object.keys(r.tests).forEach(function(n){if(n!=='__error'&&r.tests[n].status==='running')badgeActive++})});
@@ -879,17 +1559,13 @@ function renderLive(){
879
1559
  $('#liveBadge').style.background=gRunning?'var(--purple-dim)':gFailed>0?'var(--red-dim)':'var(--green-dim)';
880
1560
  $('#liveBadge').style.color=gRunning?'var(--purple)':gFailed>0?'var(--red)':'var(--green)';
881
1561
 
882
- $('#liveTotal').textContent=gTotal;
883
- $('#livePass').textContent=gPassed;
884
- $('#liveFail').textContent=gFailed;
885
- $('#liveActive').textContent=gActive;
886
- var pct=gTotal>0?gCompleted/gTotal*100:0;
887
- $('#liveProgressFill').style.width=pct+'%';
888
-
889
- // Hide single-project info (now shown per-section)
1562
+ $('#liveTotal').textContent=gTotal;$('#livePass').textContent=gPassed;$('#liveFail').textContent=gFailed;$('#liveActive').textContent=gActive;
1563
+ $('#liveProgressFill').style.width=(gTotal>0?gCompleted/gTotal*100:0)+'%';
890
1564
  $('#liveProject').style.display='none';
891
1565
 
892
- // Header state
1566
+ var hasFinished=runIds.some(function(rid){return runs[rid].done||!runs[rid].on});
1567
+ $('#liveClearBtn').style.display=hasFinished?'inline-block':'none';
1568
+
893
1569
  var lbl=panel.querySelector('.live-header .label');
894
1570
  var anyStale=runIds.some(function(rid){return runs[rid].stale});
895
1571
  if(!gRunning&&gDone){
@@ -898,124 +1574,99 @@ function renderLive(){
898
1574
  var dot=lbl.querySelector('.dot');if(dot)dot.remove();
899
1575
  $('#liveProgressFill').style.background=anyStale?'var(--yellow)':gFailed>0?'var(--red)':'var(--green)';
900
1576
  } else {
901
- if(!lbl.querySelector('.dot')){
902
- lbl.textContent='';
903
- var d=el('span',{className:'dot'});lbl.appendChild(d);lbl.appendChild(document.createTextNode(' RUNNING'));
904
- }
905
- lbl.style.color='var(--purple)';
906
- $('#liveProgressFill').style.background='var(--purple)';
1577
+ if(!lbl.querySelector('.dot')){lbl.textContent='';var d=el('span',{className:'dot'});lbl.appendChild(d);lbl.appendChild(document.createTextNode(' RUNNING'))}
1578
+ lbl.style.color='var(--purple)';$('#liveProgressFill').style.background='var(--purple)';
907
1579
  }
908
1580
 
909
- // Render per-run sections
910
1581
  grid.textContent='';
911
1582
  runIds.forEach(function(rid){
912
1583
  var L=runs[rid];
913
- // Project section header
914
1584
  var projLabel=L.project||(L.cwd?L.cwd.split('/').pop():'Run');
915
1585
  var runStatus=L.done?(L.failed>0?'fail':'pass'):'running';
916
- var sectionHeader=el('div',{className:'lr-section-header '+runStatus},[
917
- el('span',{className:'lr-project-name'},projLabel),
918
- el('span',{className:'lr-section-stats'},[
919
- el('span',{},L.completed+'/'+L.total),
920
- L.failed>0?el('span',{style:'color:var(--red);margin-left:6px'},L.failed+' failed'):null,
921
- L.on?el('span',{className:'spinner-small',style:'margin-left:6px'}):null
922
- ])
923
- ]);
924
- grid.appendChild(sectionHeader);
1586
+ var dismissBtn=null;
1587
+ if(L.done||!L.on){dismissBtn=el('button',{className:'lr-dismiss',onclick:function(e){e.stopPropagation();dismissLiveRun(rid)}},'\u2715')}
1588
+ grid.appendChild(el('div',{className:'lr-section-header '+runStatus},[
1589
+ el('span',{className:'lr-project-name'},projLabel),createTriggerBadge(L.triggeredBy),
1590
+ 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]),
1591
+ dismissBtn
1592
+ ]));
925
1593
 
926
1594
  var testGrid=el('div',{className:'lr-test-grid'});
927
- var names=Object.keys(L.tests);
928
- names.forEach(function(name){
1595
+ Object.keys(L.tests).forEach(function(name){
929
1596
  if(name==='__error')return;
930
- var t=L.tests[name];
931
- var testKey=rid+'::'+name;
1597
+ var t=L.tests[name];var testKey=rid+'::'+name;
932
1598
  var iconText=t.status==='passed'?'\u2714':t.status==='failed'?'\u2718':'\u25CF';
933
1599
  var iconColor=t.status==='passed'?'color:var(--green)':t.status==='failed'?'color:var(--red)':'color:var(--purple)';
934
1600
  var meta='';
935
- if(t.status==='running'){
936
- meta=t.actionType?('Step '+(t.actions||0)+'/'+(t.totalActions||'?')):'starting...';
937
- if(t.retry)meta='Retry '+t.retry;
938
- } else if(t.status==='passed'){meta=t.duration||'done'}
1601
+ if(t.status==='running'){meta=t.actionType?('Step '+(t.actions||0)+'/'+(t.totalActions||'?')):'starting...';if(t.retry)meta='Retry '+t.retry}
1602
+ else if(t.status==='passed'){meta=t.duration||'done'}
939
1603
  else if(t.status==='failed'){meta=t.error||'failed'}
940
- // Action log
1604
+
941
1605
  var stepsEl=el('div',{className:'lt-actions'});
942
1606
  if(t.actionLog&&t.actionLog.length>0){
943
1607
  t.actionLog.forEach(function(a){
944
- var detail=a.selector||a.value||a.text||'';
1608
+ var detail=a.narrative||a.selector||a.value||a.text||'';
945
1609
  var durText=a.duration!=null?(a.duration<1000?a.duration+'ms':(a.duration/1000).toFixed(1)+'s'):'';
1610
+ var retryBadge=null;
1611
+ 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)}
946
1612
  stepsEl.appendChild(el('div',{className:'lt-step'},[
947
1613
  el('span',{className:'step-icon '+(a.success?'ok':'fail')},a.success?'\u2714':'\u2718'),
948
1614
  el('span',{className:'step-type'},a.type),
949
1615
  el('span',{className:'step-detail'},detail),
1616
+ retryBadge,
950
1617
  el('span',{className:'step-dur'},durText)
951
1618
  ]));
952
1619
  });
953
- if(t.status==='running'&&t.actions<t.totalActions){
954
- stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'waiting...')]));
955
- }
1620
+ 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...')]))}
956
1621
  } else if(t.status==='running'){
957
1622
  stepsEl.appendChild(el('div',{className:'lt-step'},[el('span',{className:'step-icon run spinner-small'}),el('span',{className:'step-type',style:'opacity:.6'},'connecting...')]));
958
1623
  }
959
1624
  var isFinished=t.status==='passed'||t.status==='failed';
960
- var isCollapsed=isFinished&&!S.liveExpanded.has(testKey);
961
- var summaryEl=el('div',{className:'lt-summary'},[
962
- el('span',{className:'lt-dur'},t.duration||''),
963
- el('span',{className:'lt-expand'},isCollapsed?'\u25BC':'\u25B2')
964
- ]);
965
- // Screenshots section
1625
+ var isCollapsed=isFinished&&S.liveCollapsed.has(testKey);
1626
+ var summaryEl=el('div',{className:'lt-summary'},[el('span',{className:'lt-dur'},t.duration||''),el('span',{className:'lt-expand'},isCollapsed?'\u25BC':'\u25B2')]);
1627
+
966
1628
  var ssEl=null;
967
1629
  var allSS=(t.screenshots||[]).slice();
968
1630
  if(t.errorScreenshot)allSS.push(t.errorScreenshot);
969
1631
  if(allSS.length>0){
970
1632
  var ssOpen=S.liveSSOpen&&S.liveSSOpen.has(testKey);
971
- var toggle=el('div',{className:'lt-screenshots-toggle'+(ssOpen?' open':'')},[
972
- el('span',{className:'ss-arrow'},'\u25B6'),
973
- el('span',{},'Screenshots ('+allSS.length+')')
974
- ]);
1633
+ var toggle=el('div',{className:'lt-screenshots-toggle'+(ssOpen?' open':'')},[el('span',{className:'ss-arrow'},'\u25B6'),el('span',{},'Screenshots ('+allSS.length+')')]);
975
1634
  var ssGridEl=el('div',{className:'lt-screenshots-grid'});
976
1635
  allSS.forEach(function(ssPath){
977
- var fname=ssPath.split('/').pop();
978
- var isErr=t.errorScreenshot&&ssPath===t.errorScreenshot;
1636
+ var fname=ssPath.split('/').pop();var isErr=t.errorScreenshot&&ssPath===t.errorScreenshot;
979
1637
  var thumb=el('div',{className:'lt-ss-thumb'});
980
- var img=document.createElement('img');
981
- img.src='/api/image?path='+encodeURIComponent(ssPath);
982
- img.alt=fname;img.loading='lazy';
1638
+ var img=document.createElement('img');img.src='/api/image?path='+encodeURIComponent(ssPath);img.alt=fname;img.loading='lazy';
983
1639
  if(isErr)thumb.style.borderColor='var(--red)';
984
1640
  thumb.appendChild(img);
985
1641
  thumb.addEventListener('click',function(e){e.stopPropagation();openModal('/api/image?path='+encodeURIComponent(ssPath),fname)});
986
1642
  var labelEl=el('div',{className:'lt-ss-label'},[el('span',{style:'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'},fname)]);
987
- // Compute hash async and append badge
988
- (function(lbl,sp){
989
- ssHash(sp).then(function(h){lbl.appendChild(createHashBadge(h))});
990
- })(labelEl,ssPath);
1643
+ (function(lbl,sp){ssHash(sp).then(function(h){lbl.appendChild(createHashBadge(h))})})(labelEl,ssPath);
991
1644
  ssGridEl.appendChild(el('div',{},[thumb,labelEl]));
992
1645
  });
993
- toggle.addEventListener('click',function(e){
994
- e.stopPropagation();
995
- if(S.liveSSOpen.has(testKey))S.liveSSOpen.delete(testKey);else S.liveSSOpen.add(testKey);
996
- toggle.classList.toggle('open');
997
- ssGridEl.style.display=ssGridEl.style.display==='grid'?'none':'grid';
998
- });
1646
+ 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'});
999
1647
  if(ssOpen)ssGridEl.style.display='grid';
1000
1648
  ssEl=el('div',{className:'lt-screenshots'},[toggle,ssGridEl]);
1001
1649
  }
1650
+
1651
+ var serialBadge=t.serial?el('span',{className:'serial-badge'},'Serial'):null;
1002
1652
  var card=el('div',{className:'live-test '+t.status+(isCollapsed?' collapsed':'')},[
1003
1653
  el('div',{className:'lt-name'},[
1004
1654
  t.status==='running'?el('span',{className:'spinner'}):el('span',{className:'lt-icon',style:iconColor},iconText),
1005
- document.createTextNode(' '+name),
1006
- summaryEl
1655
+ document.createTextNode(' '+name),serialBadge,summaryEl
1007
1656
  ]),
1008
- el('div',{className:'lt-meta'},meta),
1009
- stepsEl
1657
+ el('div',{className:'lt-meta'},meta),stepsEl
1010
1658
  ]);
1011
1659
  if(ssEl)card.appendChild(ssEl);
1012
- if(isFinished){
1013
- card.addEventListener('click',function(){
1014
- if(S.liveExpanded.has(testKey))S.liveExpanded.delete(testKey);
1015
- else S.liveExpanded.add(testKey);
1016
- renderLive();
1017
- });
1660
+ if(t.networkLogs&&t.networkLogs.length&&!isCollapsed){
1661
+ var liveErrCount=t.networkLogs.filter(function(n){return n.status>=400}).length;
1662
+ 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])]);
1663
+ 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')]);
1664
+ var liveNetBody=el('div',{className:'rd-net-body'},[liveNetCols]);
1665
+ t.networkLogs.forEach(function(n){var built=buildNetRow(n);liveNetBody.appendChild(built.row);if(built.detail)liveNetBody.appendChild(built.detail)});
1666
+ liveNetHead.addEventListener('click',function(e){e.stopPropagation();liveNetHead.classList.toggle('open')});
1667
+ card.appendChild(el('div',{className:'rd-net-panel',style:'margin-top:6px'},[liveNetHead,liveNetBody]));
1018
1668
  }
1669
+ 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()})}
1019
1670
  testGrid.appendChild(card);
1020
1671
  if(!isCollapsed)stepsEl.scrollTop=stepsEl.scrollHeight;
1021
1672
  });
@@ -1023,22 +1674,194 @@ function renderLive(){
1023
1674
  });
1024
1675
  }
1025
1676
 
1026
- /* ── Actions ── */
1027
1677
  $('#btnRunAll').addEventListener('click',function(){triggerRun()});
1028
1678
 
1029
1679
  /* ── Modal ── */
1030
1680
  function openModal(src){$('#modalImg').src=src;$('#modal').classList.add('open')}
1031
1681
  $('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
1032
- document.addEventListener('keydown',function(e){if(e.key==='Escape')$('#modal').classList.remove('open')});
1033
1682
 
1034
- /* ── Init ── */
1683
+ /* ══════════════════════════════════════════════════════════════════
1684
+ Learnings (+ Cross-project, Export)
1685
+ ══════════════════════════════════════════════════════════════════ */
1686
+ function refreshLearnings(){
1687
+ var days=$('#learningsDays').value||30;
1688
+ var url=S.project?'/api/db/projects/'+S.project+'/learnings?days='+days:'/api/db/learnings?days='+days;
1689
+ fetch(url).then(function(r){return r.json()}).then(function(data){
1690
+ if(!data||data.totalRuns===0){
1691
+ $('#learningsEmpty').style.display='block';
1692
+ $('#learningsOverview').textContent='';$('#learningsTrend').textContent='';
1693
+ $('#learningsFlaky').textContent='';$('#learningsSelectors').textContent='';
1694
+ $('#learningsPages').textContent='';$('#learningsApis').textContent='';
1695
+ $('#learningsErrors').textContent='';
1696
+ $('#badgeLearnings').textContent='-';
1697
+ return;
1698
+ }
1699
+ $('#learningsEmpty').style.display='none';
1700
+ S.lastLearningsData=data;
1701
+ var flakyCount=data.flakyTests?data.flakyTests.length:0;
1702
+ $('#badgeLearnings').textContent=flakyCount>0?flakyCount:'\u2714';
1703
+ if(flakyCount>0){$('#badgeLearnings').style.background='var(--amber-dim)';$('#badgeLearnings').style.color='var(--amber)'}
1704
+ else{$('#badgeLearnings').style.background='';$('#badgeLearnings').style.color=''}
1705
+ renderLearnOverview(data);
1706
+ renderLearnTrend(data.recentTrend||[]);
1707
+ renderLearnFlaky(data.flakyTests||[]);
1708
+ renderLearnSelectors(data.unstableSelectors||[]);
1709
+ renderLearnPages(data.failingPages||[]);
1710
+ renderLearnApis(data.apiIssues||[]);
1711
+ renderLearnErrors(data.topErrors||[]);
1712
+ }).catch(function(){$('#learningsEmpty').style.display='block'});
1713
+ }
1714
+
1715
+ function renderLearnOverview(d){
1716
+ var container=$('#learningsOverview');container.textContent='';
1717
+ var grid=document.createElement('div');grid.className='learn-grid';
1718
+ [{val:d.totalRuns,lbl:'Runs',cls:'accent'},{val:d.totalTests,lbl:'Tests',cls:'accent'},
1719
+ {val:d.overallPassRate+'%',lbl:'Pass Rate',cls:d.overallPassRate>=90?'green':d.overallPassRate>=70?'':'red'},
1720
+ {val:d.avgDurationMs<1000?d.avgDurationMs+'ms':(d.avgDurationMs/1000).toFixed(1)+'s',lbl:'Avg Duration',cls:'purple'},
1721
+ {val:(d.flakyTests?d.flakyTests.length:0),lbl:'Flaky Tests',cls:d.flakyTests&&d.flakyTests.length>0?'red':'green'},
1722
+ {val:(d.unstableSelectors?d.unstableSelectors.length:0),lbl:'Unstable Selectors',cls:d.unstableSelectors&&d.unstableSelectors.length>0?'red':'green'}
1723
+ ].forEach(function(item){
1724
+ var stat=document.createElement('div');stat.className='learn-stat';
1725
+ var valEl=document.createElement('div');valEl.className='learn-stat-val '+item.cls;valEl.textContent=item.val;
1726
+ var lblEl=document.createElement('div');lblEl.className='learn-stat-lbl';lblEl.textContent=item.lbl;
1727
+ stat.appendChild(valEl);stat.appendChild(lblEl);grid.appendChild(stat);
1728
+ });
1729
+ container.appendChild(grid);
1730
+ }
1731
+
1732
+ function renderLearnTrend(trend){
1733
+ var container=$('#learningsTrend');container.textContent='';
1734
+ if(!trend.length)return;
1735
+ var card=document.createElement('div');card.className='card';
1736
+ var label=document.createElement('div');label.className='card-label';label.textContent='Pass Rate Trend (7 days)';card.appendChild(label);
1737
+ var chartDiv=document.createElement('div');chartDiv.className='learn-trend-chart';
1738
+ var w=100/trend.length;var ns='http://www.w3.org/2000/svg';
1739
+ var svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 100 100');svg.setAttribute('preserveAspectRatio','none');
1740
+ 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');
1741
+ svg.appendChild(bg);
1742
+ 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);
1743
+ var pts=trend.map(function(t,i){return(i*w+w/2)+','+(100-t.pass_rate)}).join(' ');
1744
+ 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);
1745
+ 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);
1746
+ trend.forEach(function(t,i){
1747
+ 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)');
1748
+ var title=document.createElementNS(ns,'title');title.textContent=t.date+': '+t.pass_rate+'% ('+t.total_tests+' tests)';circle.appendChild(title);svg.appendChild(circle);
1749
+ });
1750
+ chartDiv.appendChild(svg);card.appendChild(chartDiv);
1751
+ var dates=document.createElement('div');dates.style.cssText='display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px';
1752
+ dates.appendChild(el('span',null,trend[0].date));dates.appendChild(el('span',null,trend[trend.length-1].date));
1753
+ card.appendChild(dates);container.appendChild(card);
1754
+ }
1755
+
1756
+ function buildLearnTable(title,headers,rows){
1757
+ var card=document.createElement('div');card.className='card learn-section';
1758
+ var h=document.createElement('div');h.className='learn-section-title';h.textContent=title;card.appendChild(h);
1759
+ var wrap=document.createElement('div');wrap.className='tbl-wrap';
1760
+ var tbl=document.createElement('table');tbl.className='learn-table';
1761
+ var thead=document.createElement('thead');var hr=document.createElement('tr');
1762
+ headers.forEach(function(hdr){var th=document.createElement('th');th.textContent=hdr;hr.appendChild(th)});
1763
+ thead.appendChild(hr);tbl.appendChild(thead);
1764
+ var tbody=document.createElement('tbody');
1765
+ rows.forEach(function(cells){
1766
+ var tr=document.createElement('tr');
1767
+ cells.forEach(function(cell){
1768
+ var td=document.createElement('td');
1769
+ if(cell.code){var code=document.createElement('code');code.textContent=cell.code;td.appendChild(code)}
1770
+ else if(cell.badge){var span=document.createElement('span');span.className='badge '+cell.cls;span.textContent=cell.badge;td.appendChild(span)}
1771
+ else{td.textContent=cell.text!==undefined?cell.text:cell}
1772
+ tr.appendChild(td);
1773
+ });
1774
+ tbody.appendChild(tr);
1775
+ });
1776
+ tbl.appendChild(tbody);wrap.appendChild(tbl);card.appendChild(wrap);return card;
1777
+ }
1778
+
1779
+ 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}]})))}
1780
+ 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||'-'}]})))}
1781
+ 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}]})))}
1782
+ 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||'-'}]})))}
1783
+ 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||'-'}]})))}
1784
+
1785
+ $('#btnRefreshLearnings').addEventListener('click',refreshLearnings);
1786
+ $('#learningsDays').addEventListener('change',refreshLearnings);
1787
+
1788
+ $('#btnExportLearnings').addEventListener('click',function(){
1789
+ var data=S.lastLearningsData;
1790
+ if(!data){showToast('No learnings data to export','error');return}
1791
+ var md='# E2E Learnings Report\n\n';
1792
+ md+='| Metric | Value |\n|--------|-------|\n';
1793
+ md+='| Total Runs | '+data.totalRuns+' |\n';
1794
+ md+='| Total Tests | '+data.totalTests+' |\n';
1795
+ md+='| Pass Rate | '+data.overallPassRate+'% |\n';
1796
+ md+='| Avg Duration | '+dur(data.avgDurationMs)+' |\n\n';
1797
+ if(data.flakyTests&&data.flakyTests.length){
1798
+ md+='## Flaky Tests\n\n| Test | Flaky Rate | Occurrences |\n|------|-----------|-------------|\n';
1799
+ data.flakyTests.forEach(function(f){md+='| '+f.test_name+' | '+f.flaky_rate+'% | '+f.flaky_count+' |\n'});md+='\n';
1800
+ }
1801
+ if(data.unstableSelectors&&data.unstableSelectors.length){
1802
+ md+='## Unstable Selectors\n\n| Selector | Action | Fail Rate |\n|----------|--------|-----------|\n';
1803
+ data.unstableSelectors.forEach(function(s){md+='| `'+s.selector+'` | '+s.action_type+' | '+s.fail_rate+'% |\n'});md+='\n';
1804
+ }
1805
+ downloadFile('learnings-report.md',md,'text/markdown');
1806
+ showToast('Learnings exported','success');
1807
+ });
1808
+
1809
+ /* ══════════════════════════════════════════════════════════════════
1810
+ Keyboard Shortcuts
1811
+ ══════════════════════════════════════════════════════════════════ */
1812
+ document.addEventListener('keydown',function(e){
1813
+ var tag=document.activeElement.tagName;
1814
+ if(tag==='INPUT'||tag==='SELECT'||tag==='TEXTAREA')return;
1815
+ if(e.key==='Escape'){
1816
+ if($('#kbModal').classList.contains('open')){$('#kbModal').classList.remove('open');return}
1817
+ if($('#modal').classList.contains('open')){$('#modal').classList.remove('open');return}
1818
+ if(S.selectedRun!==null){
1819
+ var expanded=document.querySelector('#runsBody tr.expanded');
1820
+ if(expanded){
1821
+ var next=expanded.nextElementSibling;
1822
+ 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)}
1823
+ S.selectedRun=null;
1824
+ }
1825
+ return;
1826
+ }
1827
+ return;
1828
+ }
1829
+ if(e.key==='?'){$('#kbModal').classList.toggle('open');return}
1830
+ var viewMap={'1':'suites','2':'runs','3':'screenshots','4':'learnings','5':'live'};
1831
+ if(viewMap[e.key]){showView(viewMap[e.key]);return}
1832
+ if(e.key==='r'){
1833
+ if(S.view==='suites')refreshSuites();else if(S.view==='runs')refreshRuns();
1834
+ else if(S.view==='screenshots')refreshScreenshots();else if(S.view==='learnings')refreshLearnings();
1835
+ return;
1836
+ }
1837
+ if(S.view==='runs'&&(e.key==='j'||e.key==='k')){
1838
+ var visible=_allRunRows.filter(function(item){return item.tr.style.display!=='none'});
1839
+ if(!visible.length)return;
1840
+ if(e.key==='j')S.highlightedRunIdx=Math.min(S.highlightedRunIdx+1,visible.length-1);
1841
+ if(e.key==='k')S.highlightedRunIdx=Math.max(S.highlightedRunIdx-1,0);
1842
+ 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')});
1843
+ return;
1844
+ }
1845
+ if(S.view==='runs'&&e.key==='Enter'){
1846
+ var visible2=_allRunRows.filter(function(item){return item.tr.style.display!=='none'});
1847
+ if(S.highlightedRunIdx>=0&&S.highlightedRunIdx<visible2.length){visible2[S.highlightedRunIdx].tr.click()}
1848
+ return;
1849
+ }
1850
+ });
1851
+ $('#kbModal').addEventListener('click',function(e){if(e.target===$('#kbModal'))$('#kbModal').classList.remove('open')});
1852
+
1853
+ /* ══════════════════════════════════════════════════════════════════
1854
+ Init
1855
+ ══════════════════════════════════════════════════════════════════ */
1035
1856
  connectWS();
1036
1857
  refreshStatus();
1037
1858
  refreshProjects();
1038
1859
  refreshSuites();
1039
1860
  refreshRuns();
1040
1861
  refreshScreenshots();
1862
+ refreshLearnings();
1041
1863
  })();
1864
+
1042
1865
  </script>
1043
1866
  </body>
1044
1867
  </html>