@matware/e2e-runner 1.1.1 → 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 +475 -307
  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 +194 -6
  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 +10 -2
  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 +273 -18
  17. package/src/ai-generate.js +87 -7
  18. package/src/config.js +28 -0
  19. package/src/dashboard.js +156 -6
  20. package/src/db.js +207 -13
  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 +448 -18
  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 +35 -2
  31. package/src/runner.js +120 -46
  32. package/src/verify.js +5 -3
  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 +964 -378
  38. package/templates/docker-compose-neo4j.yml +19 -0
  39. package/templates/e2e.config.js +3 -0
@@ -0,0 +1,413 @@
1
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
2
+ :root{
3
+ --bg:#090a10;--surface:#11131b;--surface2:#181b26;--surface3:#1f2333;
4
+ --border:#232738;--border-hi:#2d3248;
5
+ --text:#dfe1e8;--text2:#7c8198;--text3:#464b62;
6
+ --accent:#3b82f6;--accent-dim:rgba(59,130,246,.12);
7
+ --green:#10b981;--green-dim:rgba(16,185,129,.10);
8
+ --red:#ef4444;--red-dim:rgba(239,68,68,.10);
9
+ --amber:#f59e0b;--amber-dim:rgba(245,158,11,.10);
10
+ --purple:#8b5cf6;--purple-dim:rgba(139,92,246,.12);
11
+ --mono:'JetBrains Mono','SF Mono','Fira Code',monospace;
12
+ --sans:'Outfit','Inter',-apple-system,sans-serif;
13
+ --r:6px;
14
+ }
15
+ html{font-size:13px}
16
+ body{font-family:var(--mono);background:var(--bg);color:var(--text);min-height:100vh;display:flex}
17
+ a{color:var(--accent);text-decoration:none}
18
+ ::selection{background:var(--accent);color:#fff}
19
+
20
+ /* ── Sidebar ── */
21
+ .sidebar{width:232px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:50;overflow-y:auto}
22
+ .sidebar-logo{padding:20px 16px 16px;border-bottom:1px solid var(--border)}
23
+ .sidebar-logo h1{font-family:var(--sans);font-size:15px;font-weight:700;letter-spacing:-.02em}
24
+ .sidebar-logo h1 span{color:var(--accent)}
25
+ .sidebar-logo .ver{font-size:10px;color:var(--text3);margin-top:2px}
26
+
27
+ .sidebar-section{padding:12px 16px 8px}
28
+ .sidebar-section-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.12em;text-transform:uppercase;margin-bottom:8px}
29
+ .sidebar select{width:100%;padding:8px 10px;border-radius:var(--r);border:1px solid var(--border);background:var(--surface2);color:var(--text);font-family:var(--mono);font-size:12px;appearance:auto;cursor:pointer}
30
+ .sidebar select:focus{outline:none;border-color:var(--accent)}
31
+
32
+ .nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;font-size:12px;color:var(--text2);cursor:pointer;border-left:2px solid transparent;transition:all .15s}
33
+ .nav-item:hover{color:var(--text);background:var(--surface2)}
34
+ .nav-item.active{color:var(--accent);border-left-color:var(--accent);background:var(--accent-dim)}
35
+ .nav-item .icon{width:16px;text-align:center;font-style:normal}
36
+ .nav-item .badge{margin-left:auto;background:var(--surface3);color:var(--text2);font-size:10px;padding:1px 6px;border-radius:10px}
37
+
38
+ .pool-status{margin-top:auto;padding:16px;border-top:1px solid var(--border)}
39
+ .pool-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:6px}
40
+ .pool-dot.on{background:var(--green);box-shadow:0 0 8px var(--green)}
41
+ .pool-dot.off{background:var(--red);box-shadow:0 0 8px var(--red)}
42
+ .pool-info{font-size:11px;color:var(--text2);line-height:1.7}
43
+ .pool-info strong{color:var(--text)}
44
+ .ws-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:4px}
45
+
46
+ /* ── Main ── */
47
+ .main{margin-left:232px;flex:1;min-height:100vh;display:flex;flex-direction:column}
48
+ .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}
49
+ .main-header .title{font-family:var(--sans);font-size:16px;font-weight:600}
50
+ .main-header .actions{margin-left:auto;display:flex;gap:8px}
51
+ .view{display:none;padding:24px}
52
+ .view.active{display:block}
53
+ #view-live.active{display:flex;flex-direction:column;flex:1;min-height:calc(100vh - 0px);padding:16px}
54
+
55
+ /* ── Buttons ── */
56
+ .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}
57
+ .btn:hover{background:var(--surface3);border-color:var(--border-hi)}
58
+ .btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
59
+ .btn.primary:hover{background:#2563eb}
60
+ .btn.danger{background:var(--red-dim);border-color:rgba(239,68,68,.3);color:var(--red)}
61
+ .btn:disabled{opacity:.4;cursor:not-allowed}
62
+ .btn.sm{padding:4px 10px;font-size:10px}
63
+
64
+ /* ── Cards ── */
65
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:16px}
66
+ .card-label{font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;margin-bottom:10px}
67
+
68
+ /* ── Stats Row ── */
69
+ .stats{display:flex;gap:24px;margin-bottom:20px}
70
+ .stat-block{text-align:center;min-width:80px}
71
+ .stat-val{font-size:26px;font-weight:700}
72
+ .stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.1em;margin-top:2px}
73
+ .stat-val.green{color:var(--green)}
74
+ .stat-val.red{color:var(--red)}
75
+ .stat-val.accent{color:var(--accent)}
76
+ .stat-val.purple{color:var(--purple)}
77
+
78
+ /* ── Learnings ── */
79
+ .learn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-bottom:20px}
80
+ .learn-stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:14px;text-align:center}
81
+ .learn-stat-val{font-size:24px;font-weight:700;margin-bottom:2px}
82
+ .learn-stat-lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
83
+ .learn-section{margin-bottom:20px}
84
+ .learn-section-title{font-family:var(--sans);font-size:13px;font-weight:600;margin-bottom:10px;color:var(--text)}
85
+ .learn-table{width:100%;border-collapse:collapse;font-size:11px}
86
+ .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}
87
+ .learn-table th:hover{color:var(--text2)}
88
+ .learn-table th.sorted::after{content:' \\25B2';font-size:8px}
89
+ .learn-table th.sorted.desc::after{content:' \\25BC'}
90
+ .learn-table td{padding:6px 10px;border-bottom:1px solid var(--border);color:var(--text2)}
91
+ .learn-table td code{background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:10px;color:var(--text)}
92
+ .learn-table tbody tr:hover td{background:var(--surface2);color:var(--text)}
93
+ .learn-trend-chart{width:100%;height:100px;margin-bottom:20px}
94
+ .learn-trend-chart svg{width:100%;height:100%}
95
+
96
+ /* ── Tables ── */
97
+ .tbl-wrap{overflow-x:auto}
98
+ table{width:100%;border-collapse:collapse;font-size:12px}
99
+ th{text-align:left;font-size:9px;font-weight:600;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;padding:8px 12px;border-bottom:1px solid var(--border)}
100
+ td{padding:8px 12px;border-bottom:1px solid var(--border)}
101
+ tbody tr{cursor:pointer;transition:background .1s}
102
+ tbody tr:hover td{background:var(--surface2)}
103
+ tbody tr.selected td{background:var(--accent-dim)}
104
+ .badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600}
105
+ .badge.pass{background:var(--green-dim);color:var(--green)}
106
+ .badge.fail{background:var(--red-dim);color:var(--red)}
107
+ .badge.flaky{background:var(--amber-dim);color:var(--amber)}
108
+ .badge.run{background:var(--purple-dim);color:var(--purple)}
109
+
110
+ /* ── Trend Chart ── */
111
+ .chart{display:flex;align-items:flex-end;gap:3px;height:60px}
112
+ .chart-bar{flex:1;min-width:3px;max-width:16px;border-radius:2px 2px 0 0;cursor:pointer;position:relative;transition:opacity .15s}
113
+ .chart-bar:hover{opacity:.75}
114
+ .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}
115
+ .chart-bar:hover .tip{display:block}
116
+
117
+ /* ── Suite Cards ── */
118
+ .suite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
119
+ .suite-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .15s}
120
+ .suite-card:hover{border-color:var(--border-hi)}
121
+ .suite-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
122
+ .suite-card-name{font-weight:600;font-size:13px}
123
+ .suite-card-count{font-size:10px;color:var(--text3)}
124
+ .suite-card-tests{list-style:none;margin-bottom:12px}
125
+ .suite-card-tests li{font-size:11px;color:var(--text2);padding:2px 0;padding-left:14px;position:relative;cursor:pointer;transition:color .15s}
126
+ .suite-card-tests li:hover{color:var(--text)}
127
+ .suite-card-tests li::before{content:">";position:absolute;left:0;color:var(--text3)}
128
+ .suite-card-tests li.expanded{color:var(--accent)}
129
+ .suite-card-tests li.expanded::before{content:"v"}
130
+ .suite-test-steps{padding:6px 0 6px 14px;border-left:2px solid var(--border);margin:4px 0 4px 6px}
131
+
132
+ /* ── Live Execution ── */
133
+ .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}
134
+ .live-panel.active{display:flex;flex:1;min-height:0}
135
+ @keyframes fadeSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
136
+ .live-header{padding:14px 16px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--border);background:var(--purple-dim)}
137
+ .live-header .label{font-weight:600;color:var(--purple);font-size:12px;display:flex;align-items:center;gap:8px}
138
+ .live-header .label .dot{width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
139
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
140
+ .live-project{display:flex;align-items:center;padding:2px 10px;background:rgba(255,255,255,.05);border-radius:4px;border:1px solid var(--border)}
141
+ .live-stats{display:flex;gap:16px;font-size:11px;color:var(--text2);margin-left:auto}
142
+ .live-stats span strong{color:var(--text)}
143
+ .live-progress{height:3px;background:var(--surface3)}
144
+ .live-progress-fill{height:100%;background:var(--purple);transition:width .4s;border-radius:0 2px 2px 0}
145
+ .live-tests{padding:12px 16px;display:flex;flex-direction:column;gap:2px;flex:1;overflow-y:auto;min-height:0}
146
+ .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)}
147
+ .live-test.running{border-left-color:var(--purple)}
148
+ .live-test.passed{border-left-color:var(--green)}
149
+ .live-test.failed{border-left-color:var(--red)}
150
+ .live-test.collapsed{cursor:pointer;padding:6px 12px}
151
+ .live-test.collapsed:hover{background:var(--surface3)}
152
+ .live-test.collapsed .lt-meta,.live-test.collapsed .lt-actions,.live-test.collapsed .lt-screenshots{display:none}
153
+ .live-test.collapsed .lt-name{margin-bottom:0}
154
+ .live-test.collapsed .lt-summary{display:flex}
155
+ .live-test .lt-name{font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:6px}
156
+ .live-test .lt-summary{display:none;align-items:center;gap:8px;margin-left:auto;font-size:10px;color:var(--text3);font-family:var(--mono)}
157
+ .live-test .lt-summary .lt-dur{color:var(--text2)}
158
+ .live-test .lt-summary .lt-expand{color:var(--purple);font-size:9px;opacity:.6}
159
+ .live-test .lt-meta{color:var(--text2);font-size:10px;margin-bottom:6px}
160
+ .live-test .lt-icon{font-size:12px}
161
+ .lt-actions{overflow-y:auto;border-top:1px solid var(--border);padding-top:6px;margin-top:4px}
162
+ .lt-step{display:flex;align-items:flex-start;gap:6px;padding:2px 0;font-size:10px;font-family:var(--mono);line-height:1.4}
163
+ .lt-step .step-icon{flex-shrink:0;width:14px;text-align:center}
164
+ .lt-step .step-icon.ok{color:var(--green)}
165
+ .lt-step .step-icon.fail{color:var(--red)}
166
+ .lt-step .step-icon.run{color:var(--purple)}
167
+ .lt-step .step-type{color:var(--purple);font-weight:600;flex-shrink:0}
168
+ .lt-step .step-detail{color:var(--text2);flex:1;min-width:0;white-space:pre-wrap;word-break:break-word}
169
+ .lt-step .step-dur{color:var(--text3);flex-shrink:0;margin-left:auto}
170
+ .lt-screenshots{border-top:1px solid var(--border);margin-top:6px;padding-top:6px}
171
+ .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}
172
+ .lt-screenshots-toggle:hover{color:var(--text)}
173
+ .lt-screenshots-toggle .ss-arrow{transition:transform .2s;font-size:8px}
174
+ .lt-screenshots-toggle.open .ss-arrow{transform:rotate(90deg)}
175
+ .lt-screenshots-grid{display:none;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:6px;padding-top:6px}
176
+ .lt-screenshots-toggle.open+.lt-screenshots-grid{display:grid}
177
+ .lt-ss-thumb{position:relative;border-radius:4px;overflow:hidden;border:1px solid var(--border);cursor:pointer;aspect-ratio:16/10;background:var(--surface2)}
178
+ .lt-ss-thumb img{width:100%;height:100%;object-fit:cover;display:block}
179
+ .lt-ss-thumb:hover{border-color:var(--purple);box-shadow:0 0 0 1px var(--purple)}
180
+ .lt-ss-label{font-size:8px;color:var(--text3);font-family:var(--mono);text-align:center;padding:2px 2px 0;display:flex;align-items:center;justify-content:center;gap:3px;flex-wrap:wrap}
181
+ .lr-section-header{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;margin:6px 0 2px;border-radius:6px;font-family:var(--mono);font-size:12px;font-weight:700;border-left:3px solid var(--purple)}
182
+ .lr-section-header.running{border-color:var(--purple);background:rgba(139,92,246,.08);color:var(--purple)}
183
+ .lr-section-header.pass{border-color:var(--green);background:rgba(52,211,153,.08);color:var(--green)}
184
+ .lr-section-header.fail{border-color:var(--red);background:rgba(248,113,113,.08);color:var(--red)}
185
+ .lr-section-stats{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text2);font-weight:400}
186
+ .lr-project-name{letter-spacing:.5px}
187
+ .lr-test-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px;padding:4px 0 8px}
188
+ .live-done{background:var(--green-dim);color:var(--green);text-align:center;padding:10px;font-weight:600;font-size:12px}
189
+ .live-done.has-failures{background:var(--red-dim);color:var(--red)}
190
+ .live-close{padding:4px 10px;font-size:10px;background:transparent;border:1px solid var(--border);border-radius:var(--r);color:var(--text2);cursor:pointer}
191
+ .live-close:hover{color:var(--text);border-color:var(--border-hi)}
192
+ .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}
193
+ .live-clear-btn:hover{color:var(--text);border-color:var(--border-hi);background:var(--surface3)}
194
+ .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}
195
+ .lr-dismiss:hover{color:var(--red);border-color:rgba(239,68,68,.3);background:var(--red-dim)}
196
+
197
+ .live-nav-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--purple);animation:pulse 1.5s infinite}
198
+ .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}
199
+ .spinner-small{display:inline-block;width:8px;height:8px;border:1.5px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle}
200
+ @keyframes spin{to{transform:rotate(360deg)}}
201
+
202
+ /* ── Screenshots ── */
203
+ .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
204
+ .gallery-item{background:var(--surface2);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;cursor:pointer;transition:border-color .15s}
205
+ .gallery-item:hover{border-color:var(--accent)}
206
+ .gallery-item img{width:100%;height:150px;object-fit:cover;display:block}
207
+ .gallery-item .cap{padding:6px 10px;font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
208
+ .gallery-item .cap .cap-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
209
+ .gallery-item .cap .ss-hash{flex-shrink:0}
210
+ .ss-search{display:flex;gap:8px;margin-bottom:16px;align-items:center}
211
+ .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}
212
+ .ss-search input:focus{outline:none;border-color:var(--accent)}
213
+ .ss-search input::placeholder{color:var(--text3)}
214
+ .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}
215
+ .ss-search button:hover{background:var(--surface3);border-color:var(--accent)}
216
+ .ss-search-result{margin-bottom:20px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r)}
217
+ .ss-search-result img{max-width:100%;max-height:500px;border-radius:var(--r);cursor:pointer;display:block;margin-top:8px}
218
+ .ss-search-result .ss-result-label{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px}
219
+ .ss-search-error{font-size:11px;color:var(--red);margin-bottom:12px}
220
+
221
+ /* ── Inline Run Detail ── */
222
+ .run-detail-row td{padding:0!important;border-bottom:2px solid var(--purple)}
223
+ .run-detail-row:hover td{background:transparent!important}
224
+ .rd-wrap{overflow:hidden;transition:max-height .4s cubic-bezier(.4,0,.2,1);max-height:0}
225
+ .rd-wrap.open{max-height:10000px}
226
+ .rd-inner{padding:20px 24px;background:var(--bg);position:relative}
227
+ .rd-inner::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--purple),var(--accent),transparent 80%)}
228
+ .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}
229
+ .rd-summary>div{background:var(--surface);padding:14px 16px;text-align:center;transition:background .15s}
230
+ .rd-summary>div:hover{background:var(--surface2)}
231
+ .rd-s-label{font-size:9px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.1em}
232
+ .rd-s-val{font-weight:700;font-size:18px;margin-top:4px}
233
+ .rd-test{margin-bottom:12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden;transition:border-color .2s}
234
+ .rd-test:hover{border-color:var(--border-hi)}
235
+ .rd-test:last-child{margin-bottom:0}
236
+ .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}
237
+ .rd-test-head::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px}
238
+ .rd-test.pass .rd-test-head::before{background:var(--green)}
239
+ .rd-test.fail .rd-test-head::before{background:var(--red)}
240
+ .rd-test.flaky .rd-test-head::before{background:var(--amber)}
241
+ .rd-test-name{font-family:var(--sans);font-weight:600;font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
242
+ .rd-test-dur{font-size:11px;color:var(--text2);flex-shrink:0;font-variant-numeric:tabular-nums}
243
+ .rd-test-body{padding:16px}
244
+ .rd-retries{font-size:11px;color:var(--amber);margin-bottom:10px;display:flex;align-items:center;gap:6px}
245
+ .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}
246
+ .rd-shots{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px}
247
+ .rd-shot{width:140px;border-radius:6px;overflow:hidden;border:1px solid var(--border);cursor:pointer;transition:all .2s;background:var(--surface2)}
248
+ .rd-shot:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.35)}
249
+ .rd-shot img{width:100%;height:84px;object-fit:cover;display:block}
250
+ .rd-shot .rd-shot-cap{font-size:9px;color:var(--text3);padding:5px 8px;background:var(--surface)}
251
+ .rd-shot .rd-shot-cap .cap-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
252
+ .rd-shot .rd-shot-cap .ss-hash{margin-top:3px}
253
+ .rd-shot.err-shot{border-color:rgba(239,68,68,.4)}
254
+ .rd-shot.err-shot .rd-shot-cap{color:var(--red)}
255
+ .rd-logs{margin-bottom:12px}
256
+ .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}
257
+ .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}
258
+ .rd-log-item.error{border-left-color:var(--red);color:var(--red)}
259
+ .rd-log-item.warning,.rd-log-item.warn{border-left-color:var(--amber);color:var(--amber)}
260
+ .rd-net-panel{margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--surface)}
261
+ .rd-net-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface2);cursor:pointer;user-select:none;transition:background .15s}
262
+ .rd-net-head:hover{background:var(--surface3)}
263
+ .rd-net-head .net-arrow{font-size:9px;color:var(--text3);transition:transform .2s}
264
+ .rd-net-head.open .net-arrow{transform:rotate(90deg)}
265
+ .rd-net-head .net-title{font-family:var(--sans);font-size:12px;font-weight:600;color:var(--text)}
266
+ .rd-net-head .net-stats{margin-left:auto;display:flex;gap:12px;font-size:10px}
267
+ .rd-net-head .net-stat{color:var(--text3)}
268
+ .rd-net-head .net-stat strong{color:var(--text2)}
269
+ .rd-net-head .net-stat.has-err strong{color:var(--red)}
270
+ .rd-net-body{display:none;max-height:600px;overflow-y:auto}
271
+ .rd-net-head.open~.rd-net-body{display:block}
272
+ .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}
273
+ .rd-net-cols .col-e{width:16px;flex-shrink:0}
274
+ .rd-net-cols .col-m{width:50px;flex-shrink:0}
275
+ .rd-net-cols .col-s{width:60px;flex-shrink:0}
276
+ .rd-net-cols .col-u{flex:1;min-width:0}
277
+ .rd-net-cols .col-d{width:60px;flex-shrink:0;text-align:right}
278
+ .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}
279
+ .rd-net-row:hover{background:var(--surface2)}
280
+ .rd-net-row.has-error{background:rgba(239,68,68,.03)}
281
+ .rd-net-row.has-error:hover{background:rgba(239,68,68,.06)}
282
+ .rd-net-expand{width:16px;flex-shrink:0;font-size:8px;color:var(--text3);transition:transform .2s;text-align:center}
283
+ .rd-net-row.open .rd-net-expand{transform:rotate(90deg);color:var(--accent)}
284
+ .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}
285
+ .rd-net-method.get{background:var(--accent-dim);color:var(--accent)}
286
+ .rd-net-method.post{background:var(--green-dim);color:var(--green)}
287
+ .rd-net-method.put,.rd-net-method.patch{background:var(--amber-dim);color:var(--amber)}
288
+ .rd-net-method.delete{background:var(--red-dim);color:var(--red)}
289
+ .rd-net-status{width:60px;flex-shrink:0;font-size:10px;font-weight:600}
290
+ .rd-net-status.s2xx{color:var(--green)}
291
+ .rd-net-status.s3xx{color:var(--amber)}
292
+ .rd-net-status.s4xx,.rd-net-status.s5xx{color:var(--red)}
293
+ .rd-net-url{flex:1;min-width:0;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
294
+ .rd-net-dur{width:60px;flex-shrink:0;text-align:right;color:var(--text3);font-variant-numeric:tabular-nums}
295
+ .rd-net-detail{display:none;border-bottom:1px solid var(--border);background:var(--bg);overflow:hidden}
296
+ .rd-net-row.open+.rd-net-detail{display:block}
297
+ .rd-nd-section{border-bottom:1px solid var(--border)}
298
+ .rd-nd-section:last-child{border-bottom:none}
299
+ .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}
300
+ .rd-nd-toggle:hover{background:var(--surface2);color:var(--text2)}
301
+ .rd-nd-toggle .nd-arrow{font-size:8px;transition:transform .2s}
302
+ .rd-nd-toggle.open .nd-arrow{transform:rotate(90deg)}
303
+ .rd-nd-toggle .nd-count{margin-left:auto;font-size:9px;font-weight:400;letter-spacing:0;text-transform:none}
304
+ .rd-nd-content{display:none;padding:0 16px 10px;max-height:300px;overflow-y:auto}
305
+ .rd-nd-toggle.open+.rd-nd-content{display:block}
306
+ .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)}
307
+ .rd-hdr-table{display:grid;grid-template-columns:minmax(120px,auto) 1fr;gap:0;font-size:11px}
308
+ .rd-hdr-row{display:contents}
309
+ .rd-hdr-row:hover .rd-hdr-key,.rd-hdr-row:hover .rd-hdr-val{background:var(--surface2)}
310
+ .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}
311
+ .rd-hdr-val{padding:3px 0;color:var(--text2);border-bottom:1px solid var(--border);word-break:break-all}
312
+ .rd-nd-empty{color:var(--text3);font-size:11px;padding:4px 0;font-style:italic}
313
+ .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}
314
+ .copy-btn:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-dim)}
315
+ .copy-btn.copied{color:var(--green);border-color:var(--green);background:var(--green-dim)}
316
+ .rd-error-msg .copy-btn{position:absolute;top:8px;right:8px;opacity:0}
317
+ .rd-error-msg:hover .copy-btn{opacity:1}
318
+ .rd-net-row .copy-btn{opacity:0;padding:1px 6px}
319
+ .rd-net-row:hover .copy-btn{opacity:1}
320
+ .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}
321
+ .rd-net-body .rd-log-item:last-child{border-bottom:none}
322
+ .rd-net-body .rd-log-item.error{border-left-color:var(--red)}
323
+ .rd-net-body .rd-log-item.warn,.rd-net-body .rd-log-item.warning{border-left-color:var(--amber)}
324
+ tr.expanded td{background:var(--surface2)!important}
325
+ tr.expanded td:first-child{position:relative}
326
+ tr.expanded td:first-child::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--purple)}
327
+
328
+ /* ── Screenshot Hash Badge ── */
329
+ .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}
330
+ .ss-hash:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
331
+ .ss-hash.copied{border-color:var(--green);color:var(--green);background:var(--green-dim)}
332
+ .ss-hash .ss-icon{font-size:10px;line-height:1}
333
+
334
+ /* ── Trigger Source Badges ── */
335
+ .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}
336
+ .trigger-badge.src-dashboard{background:rgba(127,140,162,.10);color:var(--text2)}
337
+ .trigger-badge.src-mcp{background:var(--purple-dim);color:var(--purple)}
338
+ .trigger-badge.src-cli{background:var(--accent-dim);color:var(--accent)}
339
+ .trigger-badge.src-unknown{background:rgba(70,75,98,.15);color:var(--text3)}
340
+ .trigger-badge .trig-icon{font-size:11px;line-height:1}
341
+
342
+ /* ── Empty ── */
343
+ .empty{text-align:center;padding:48px 24px;color:var(--text3)}
344
+ .empty-icon{font-size:36px;margin-bottom:8px;opacity:.5}
345
+
346
+ /* ── Modal ── */
347
+ .modal{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;display:none;align-items:center;justify-content:center;padding:24px;cursor:pointer}
348
+ .modal.open{display:flex}
349
+ .modal img{max-width:100%;max-height:90vh;border-radius:var(--r);cursor:default}
350
+
351
+ /* ── Toast Notifications (NEW) ── */
352
+ .toast-container{position:fixed;bottom:24px;right:24px;z-index:300;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none}
353
+ .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}
354
+ .toast.success{background:var(--green);border:1px solid rgba(255,255,255,.15)}
355
+ .toast.error{background:var(--red);border:1px solid rgba(255,255,255,.15)}
356
+ .toast.info{background:var(--accent);border:1px solid rgba(255,255,255,.15)}
357
+ .toast.fade-out{animation:toastOut .3s ease forwards}
358
+ @keyframes toastIn{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}
359
+ @keyframes toastOut{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(24px)}}
360
+
361
+ /* ── Filter Bar (NEW) ── */
362
+ .filter-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
363
+ .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}
364
+ .filter-btn:hover{background:var(--surface3);border-color:var(--border-hi)}
365
+ .filter-btn.active{background:var(--accent-dim);border-color:var(--accent);color:var(--accent)}
366
+ .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}
367
+ .filter-bar input:focus{outline:none;border-color:var(--accent)}
368
+ .filter-bar input::placeholder{color:var(--text3)}
369
+
370
+ /* ── Module Cards (NEW) ── */
371
+ .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}
372
+ .module-section-title .mod-icon{color:var(--purple)}
373
+ .module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
374
+ .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}
375
+ .module-card:hover{border-color:var(--border-hi);border-left-color:var(--purple)}
376
+ .module-card-name{font-weight:600;font-size:13px;color:var(--purple);margin-bottom:4px}
377
+ .module-card-desc{font-size:11px;color:var(--text2);margin-bottom:8px}
378
+ .module-card-meta{font-size:10px;color:var(--text3);display:flex;gap:12px}
379
+ .module-card-params{list-style:none;font-size:10px;color:var(--text2);margin-top:6px}
380
+ .module-card-params li{padding:2px 0}
381
+ .module-card-params li::before{content:'$';color:var(--purple);margin-right:4px}
382
+
383
+ /* ── Keyboard Shortcuts Modal (NEW) ── */
384
+ .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}
385
+ .kb-modal.open{display:flex}
386
+ .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}
387
+ .kb-modal-content h2{font-family:var(--sans);font-size:16px;font-weight:700;margin-bottom:16px;color:var(--text)}
388
+ .kb-row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border)}
389
+ .kb-row:last-child{border-bottom:none}
390
+ .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)}
391
+ .kb-desc{font-size:12px;color:var(--text2)}
392
+
393
+ /* ── Actions Panel in Run Detail (NEW) ── */
394
+ .rd-actions-panel{margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--surface)}
395
+ .rd-actions-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface2);cursor:pointer;user-select:none;transition:background .15s}
396
+ .rd-actions-head:hover{background:var(--surface3)}
397
+ .rd-actions-head .act-arrow{font-size:9px;color:var(--text3);transition:transform .2s}
398
+ .rd-actions-head.open .act-arrow{transform:rotate(90deg)}
399
+ .rd-actions-body{display:none;max-height:500px;overflow-y:auto;padding:8px 14px}
400
+ .rd-actions-head.open~.rd-actions-body{display:block}
401
+
402
+ /* ── Serial Badge (NEW) ── */
403
+ .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}
404
+
405
+ /* ── Responsive ── */
406
+ @media(max-width:768px){
407
+ .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}
408
+ .nav-item{justify-content:center;padding:12px}.nav-item .icon{width:auto}
409
+ .main{margin-left:60px}
410
+ .suite-grid,.gallery,.module-grid{grid-template-columns:1fr}
411
+ .lr-test-grid{grid-template-columns:1fr}
412
+ .toast-container{right:12px;bottom:12px}
413
+ }
@@ -0,0 +1,201 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>E2E Runner</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ /* __STYLES__ */
11
+ </style>
12
+ </head>
13
+ <body>
14
+
15
+ <aside class="sidebar">
16
+ <div class="sidebar-logo">
17
+ <h1><span>E2E</span> Runner</h1>
18
+ <div class="ver">Dashboard</div>
19
+ </div>
20
+
21
+ <div class="sidebar-section">
22
+ <div class="sidebar-section-label">Project</div>
23
+ <select id="projectSelect">
24
+ <option value="">All Projects</option>
25
+ </select>
26
+ </div>
27
+
28
+ <div class="sidebar-section">
29
+ <div class="sidebar-section-label">Navigation</div>
30
+ </div>
31
+ <div class="nav-item" data-view="live" id="navLive" style="display:none">
32
+ <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>
33
+ </div>
34
+ <div class="nav-item active" data-view="suites">
35
+ <i class="icon">&#9655;</i><span>Suites</span><span class="badge" id="badgeSuites">-</span>
36
+ </div>
37
+ <div class="nav-item" data-view="runs">
38
+ <i class="icon">&#9776;</i><span>Runs</span><span class="badge" id="badgeRuns">-</span>
39
+ </div>
40
+ <div class="nav-item" data-view="screenshots">
41
+ <i class="icon">&#9635;</i><span>Screenshots</span><span class="badge" id="badgeScreenshots">-</span>
42
+ </div>
43
+ <div class="nav-item" data-view="learnings">
44
+ <i class="icon">&#9733;</i><span>Learnings</span><span class="badge" id="badgeLearnings">-</span>
45
+ </div>
46
+
47
+ <div class="pool-status" id="poolStatus">
48
+ <div class="pool-info">
49
+ <span class="pool-dot off" id="poolDot"></span>
50
+ <strong>Pool</strong> <span id="poolLabel">--</span>
51
+ </div>
52
+ <div class="pool-info">Sessions: <strong id="poolSessions">-/-</strong></div>
53
+ <div class="pool-info" style="margin-top:6px">
54
+ <span class="ws-dot" id="wsDot" style="background:var(--red)"></span>
55
+ <span id="wsLabel" style="font-size:10px;color:var(--text3)">ws: connecting</span>
56
+ </div>
57
+ </div>
58
+ </aside>
59
+
60
+ <div class="main">
61
+
62
+ <!-- Live View -->
63
+ <div class="view" id="view-live">
64
+ <div class="live-panel active" id="livePanel">
65
+ <div class="live-header">
66
+ <div class="label"><span class="dot"></span> RUNNING</div>
67
+ <div class="live-project" id="liveProject" style="display:none">
68
+ <span style="color:var(--text3);font-size:11px">Project:</span>
69
+ <strong id="liveProjectName" style="font-size:12px;color:var(--text);margin-left:4px"></strong>
70
+ <span id="liveProjectCwd" style="font-size:10px;color:var(--text3);margin-left:8px;font-family:var(--mono);opacity:.7"></span>
71
+ </div>
72
+ <div class="live-stats">
73
+ <span>Total: <strong id="liveTotal">0</strong></span>
74
+ <span style="color:var(--green)">Pass: <strong id="livePass">0</strong></span>
75
+ <span style="color:var(--red)">Fail: <strong id="liveFail">0</strong></span>
76
+ <span style="color:var(--purple)">Active: <strong id="liveActive">0</strong></span>
77
+ </div>
78
+ <button class="live-clear-btn" id="liveClearBtn">Clear All</button>
79
+ </div>
80
+ <div class="live-progress"><div class="live-progress-fill" id="liveProgressFill" style="width:0"></div></div>
81
+ <div class="live-tests" id="liveTests"></div>
82
+ </div>
83
+ <div class="empty" id="liveEmpty">
84
+ <div class="empty-icon" style="font-size:48px;opacity:.3">&#9679;</div>
85
+ <p>No tests running. Start a test from the Suites view or another console.</p>
86
+ <p style="margin-top:8px;font-size:11px;color:var(--text3)">This view activates automatically when tests are detected.</p>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Suites View -->
91
+ <div class="view active" id="view-suites">
92
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
93
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Test Suites</div>
94
+ <button class="btn primary" id="btnRunAll">Run All Tests</button>
95
+ </div>
96
+ <div class="suite-grid" id="suiteGrid"></div>
97
+ <div id="moduleSection"></div>
98
+ <div class="empty" id="suitesEmpty" style="display:none">
99
+ <div class="empty-icon">&#9655;</div>
100
+ <p>No test suites found.</p>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Runs View -->
105
+ <div class="view" id="view-runs">
106
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
107
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Run History</div>
108
+ </div>
109
+ <div class="card">
110
+ <div class="card-label">Pass Rate Trend</div>
111
+ <div class="chart" id="trendChart"></div>
112
+ </div>
113
+ <div class="filter-bar" id="filterBar">
114
+ <button class="filter-btn active" data-filter="all">All</button>
115
+ <button class="filter-btn" data-filter="pass">Pass</button>
116
+ <button class="filter-btn" data-filter="fail">Fail</button>
117
+ <button class="filter-btn" data-filter="mixed">Mixed</button>
118
+ <input type="text" id="runSearchInput" placeholder="Search suite..." spellcheck="false">
119
+ </div>
120
+ <div class="card" style="padding:0">
121
+ <div class="tbl-wrap">
122
+ <table>
123
+ <thead id="runsHead"><tr></tr></thead>
124
+ <tbody id="runsBody"></tbody>
125
+ </table>
126
+ </div>
127
+ </div>
128
+ <div class="empty" id="runsEmpty" style="display:none">
129
+ <div class="empty-icon">&#9776;</div>
130
+ <p>No runs recorded yet.</p>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Learnings View -->
135
+ <div class="view" id="view-learnings">
136
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
137
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600">Learnings</div>
138
+ <div style="display:flex;gap:8px;align-items:center">
139
+ <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">
140
+ <option value="7">7 days</option>
141
+ <option value="14">14 days</option>
142
+ <option value="30" selected>30 days</option>
143
+ <option value="90">90 days</option>
144
+ </select>
145
+ <button class="btn sm" id="btnExportLearnings">Export MD</button>
146
+ <button class="btn sm" id="btnRefreshLearnings">Refresh</button>
147
+ </div>
148
+ </div>
149
+ <div id="learningsOverview"></div>
150
+ <div id="learningsTrend"></div>
151
+ <div id="learningsFlaky"></div>
152
+ <div id="learningsSelectors"></div>
153
+ <div id="learningsPages"></div>
154
+ <div id="learningsApis"></div>
155
+ <div id="learningsErrors"></div>
156
+ <div class="empty" id="learningsEmpty" style="display:none">
157
+ <div class="empty-icon">&#9733;</div>
158
+ <p>No learnings data yet. Run some tests to start building knowledge.</p>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Screenshots View -->
163
+ <div class="view" id="view-screenshots">
164
+ <div style="font-family:var(--sans);font-size:16px;font-weight:600;margin-bottom:20px">Screenshots</div>
165
+ <div class="ss-search">
166
+ <input type="text" id="ssHashInput" placeholder="Search by hash (e.g. ss:a3f2b1c9)" spellcheck="false">
167
+ <button id="ssHashBtn">Search</button>
168
+ </div>
169
+ <div id="ssSearchResult"></div>
170
+ <div class="gallery" id="screenshotGallery"></div>
171
+ <div class="empty" id="screenshotsEmpty" style="display:none">
172
+ <div class="empty-icon">&#9635;</div>
173
+ <p>Select a project to view screenshots.</p>
174
+ </div>
175
+ </div>
176
+
177
+ </div>
178
+
179
+ <div class="modal" id="modal"><img id="modalImg" src="" alt=""></div>
180
+ <div class="toast-container" id="toastContainer"></div>
181
+ <div class="kb-modal" id="kbModal">
182
+ <div class="kb-modal-content">
183
+ <h2>Keyboard Shortcuts</h2>
184
+ <div class="kb-row"><span class="kb-key">1</span><span class="kb-desc">Suites view</span></div>
185
+ <div class="kb-row"><span class="kb-key">2</span><span class="kb-desc">Runs view</span></div>
186
+ <div class="kb-row"><span class="kb-key">3</span><span class="kb-desc">Screenshots view</span></div>
187
+ <div class="kb-row"><span class="kb-key">4</span><span class="kb-desc">Learnings view</span></div>
188
+ <div class="kb-row"><span class="kb-key">5</span><span class="kb-desc">Live view</span></div>
189
+ <div class="kb-row"><span class="kb-key">j / k</span><span class="kb-desc">Navigate runs (next / previous)</span></div>
190
+ <div class="kb-row"><span class="kb-key">Enter</span><span class="kb-desc">Expand / collapse selected run</span></div>
191
+ <div class="kb-row"><span class="kb-key">Esc</span><span class="kb-desc">Close modal / collapse run</span></div>
192
+ <div class="kb-row"><span class="kb-key">r</span><span class="kb-desc">Refresh current view</span></div>
193
+ <div class="kb-row"><span class="kb-key">?</span><span class="kb-desc">Show this help</span></div>
194
+ </div>
195
+ </div>
196
+
197
+ <script>
198
+ /* __SCRIPT__ */
199
+ </script>
200
+ </body>
201
+ </html>