@probelabs/visor 0.1.97 → 0.1.100
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.
- package/README.md +16 -15
- package/action.yml +7 -2
- package/defaults/.visor.yaml +7 -6
- package/dist/action-cli-bridge.d.ts +1 -0
- package/dist/action-cli-bridge.d.ts.map +1 -1
- package/dist/ai-review-service.d.ts.map +1 -1
- package/dist/check-execution-engine.d.ts +8 -2
- package/dist/check-execution-engine.d.ts.map +1 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/debug-visualizer/debug-span-exporter.d.ts +47 -0
- package/dist/debug-visualizer/debug-span-exporter.d.ts.map +1 -0
- package/dist/debug-visualizer/trace-reader.d.ts +117 -0
- package/dist/debug-visualizer/trace-reader.d.ts.map +1 -0
- package/dist/debug-visualizer/ui/index.html +2568 -0
- package/dist/debug-visualizer/ws-server.d.ts +99 -0
- package/dist/debug-visualizer/ws-server.d.ts.map +1 -0
- package/dist/defaults/.visor.yaml +7 -6
- package/dist/failure-condition-evaluator.d.ts.map +1 -1
- package/dist/generated/config-schema.d.ts +7 -3
- package/dist/generated/config-schema.d.ts.map +1 -1
- package/dist/generated/config-schema.json +7 -3
- package/dist/git-repository-analyzer.d.ts +1 -7
- package/dist/git-repository-analyzer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17668 -1760
- package/dist/liquid-extensions.d.ts +1 -1
- package/dist/liquid-extensions.d.ts.map +1 -1
- package/dist/output/code-review/schema.json +2 -2
- package/dist/output/traces/run-2025-10-22T10-40-34-055Z.ndjson +218 -0
- package/dist/pr-analyzer.d.ts +2 -1
- package/dist/pr-analyzer.d.ts.map +1 -1
- package/dist/providers/ai-check-provider.d.ts.map +1 -1
- package/dist/providers/check-provider-registry.d.ts.map +1 -1
- package/dist/providers/check-provider.interface.d.ts +17 -6
- package/dist/providers/check-provider.interface.d.ts.map +1 -1
- package/dist/providers/command-check-provider.d.ts.map +1 -1
- package/dist/providers/github-ops-provider.d.ts.map +1 -1
- package/dist/providers/http-check-provider.d.ts.map +1 -1
- package/dist/providers/human-input-check-provider.d.ts +78 -0
- package/dist/providers/human-input-check-provider.d.ts.map +1 -0
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/mcp-check-provider.d.ts.map +1 -1
- package/dist/providers/memory-check-provider.d.ts.map +1 -1
- package/dist/sdk/check-execution-engine-F3662LY7.mjs +11 -0
- package/dist/sdk/{chunk-I3GQJIR7.mjs → chunk-B5QBV2QJ.mjs} +2 -2
- package/dist/sdk/chunk-B5QBV2QJ.mjs.map +1 -0
- package/dist/sdk/{chunk-IG3BFIIN.mjs → chunk-FVS5CJ5S.mjs} +30 -1
- package/dist/sdk/chunk-FVS5CJ5S.mjs.map +1 -0
- package/dist/sdk/{chunk-YXOWIDEF.mjs → chunk-TUTOLSFV.mjs} +15 -3
- package/dist/sdk/chunk-TUTOLSFV.mjs.map +1 -0
- package/dist/sdk/{chunk-4VK6WTYU.mjs → chunk-X2JKUOE5.mjs} +1375 -570
- package/dist/sdk/chunk-X2JKUOE5.mjs.map +1 -0
- package/dist/sdk/{liquid-extensions-GMEGEGC3.mjs → liquid-extensions-KVL4MKRH.mjs} +2 -2
- package/dist/sdk/{mermaid-telemetry-4DUEYCLE.mjs → mermaid-telemetry-FBF6D35S.mjs} +2 -2
- package/dist/sdk/sdk.d.mts +62 -4
- package/dist/sdk/sdk.d.ts +62 -4
- package/dist/sdk/sdk.js +1658 -723
- package/dist/sdk/sdk.js.map +1 -1
- package/dist/sdk/sdk.mjs +60 -15
- package/dist/sdk/sdk.mjs.map +1 -1
- package/dist/sdk/{tracer-init-RJGAIOBP.mjs → tracer-init-WC75N5NW.mjs} +2 -2
- package/dist/sdk.d.ts +5 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/telemetry/file-span-exporter.d.ts.map +1 -1
- package/dist/telemetry/opentelemetry.d.ts +2 -0
- package/dist/telemetry/opentelemetry.d.ts.map +1 -1
- package/dist/telemetry/state-capture.d.ts +53 -0
- package/dist/telemetry/state-capture.d.ts.map +1 -0
- package/dist/telemetry/trace-helpers.d.ts.map +1 -1
- package/dist/traces/run-2025-10-22T10-40-34-055Z.ndjson +218 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/config.d.ts +44 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/config-loader.d.ts +5 -0
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/file-exclusion.d.ts +50 -0
- package/dist/utils/file-exclusion.d.ts.map +1 -0
- package/dist/utils/interactive-prompt.d.ts +26 -0
- package/dist/utils/interactive-prompt.d.ts.map +1 -0
- package/dist/utils/sandbox.d.ts +26 -0
- package/dist/utils/sandbox.d.ts.map +1 -0
- package/dist/utils/stdin-reader.d.ts +22 -0
- package/dist/utils/stdin-reader.d.ts.map +1 -0
- package/dist/utils/tracer-init.d.ts +0 -5
- package/dist/utils/tracer-init.d.ts.map +1 -1
- package/package.json +8 -4
- package/dist/output/traces/run-2025-10-19T14-24-36-341Z.ndjson +0 -40
- package/dist/output/traces/run-2025-10-19T14-24-48-674Z.ndjson +0 -40
- package/dist/output/traces/run-2025-10-19T14-24-49-238Z.ndjson +0 -40
- package/dist/output/traces/run-2025-10-19T14-24-49-761Z.ndjson +0 -40
- package/dist/output/traces/run-2025-10-19T14-24-50-279Z.ndjson +0 -12
- package/dist/sdk/check-execution-engine-S7BFPVWA.mjs +0 -11
- package/dist/sdk/chunk-4VK6WTYU.mjs.map +0 -1
- package/dist/sdk/chunk-I3GQJIR7.mjs.map +0 -1
- package/dist/sdk/chunk-IG3BFIIN.mjs.map +0 -1
- package/dist/sdk/chunk-YXOWIDEF.mjs.map +0 -1
- package/dist/traces/run-2025-10-19T14-24-36-341Z.ndjson +0 -40
- package/dist/traces/run-2025-10-19T14-24-48-674Z.ndjson +0 -40
- package/dist/traces/run-2025-10-19T14-24-49-238Z.ndjson +0 -40
- package/dist/traces/run-2025-10-19T14-24-49-761Z.ndjson +0 -40
- package/dist/traces/run-2025-10-19T14-24-50-279Z.ndjson +0 -12
- /package/dist/sdk/{check-execution-engine-S7BFPVWA.mjs.map → check-execution-engine-F3662LY7.mjs.map} +0 -0
- /package/dist/sdk/{liquid-extensions-GMEGEGC3.mjs.map → liquid-extensions-KVL4MKRH.mjs.map} +0 -0
- /package/dist/sdk/{mermaid-telemetry-4DUEYCLE.mjs.map → mermaid-telemetry-FBF6D35S.mjs.map} +0 -0
- /package/dist/sdk/{tracer-init-RJGAIOBP.mjs.map → tracer-init-WC75N5NW.mjs.map} +0 -0
|
@@ -0,0 +1,2568 @@
|
|
|
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>Visor Debug Visualizer</title>
|
|
7
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
font-family:
|
|
18
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
|
19
|
+
sans-serif;
|
|
20
|
+
background: #1e1e1e;
|
|
21
|
+
color: #d4d4d4;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#app {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
height: 100vh;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Header */
|
|
32
|
+
header {
|
|
33
|
+
background: #252526;
|
|
34
|
+
padding: 16px 24px;
|
|
35
|
+
border-bottom: 1px solid #3e3e42;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
header h1 {
|
|
42
|
+
font-size: 18px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
color: #cccccc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.hidden {
|
|
48
|
+
display: none !important;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.header-controls {
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 12px;
|
|
54
|
+
align-items: center;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.btn {
|
|
58
|
+
background: #0e639c;
|
|
59
|
+
color: white;
|
|
60
|
+
border: none;
|
|
61
|
+
padding: 6px 14px;
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
font-size: 13px;
|
|
65
|
+
transition: background 0.2s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.btn:hover {
|
|
69
|
+
background: #1177bb;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.btn-secondary {
|
|
73
|
+
background: #3e3e42;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.btn-secondary:hover {
|
|
77
|
+
background: #4e4e52;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
input[type='file'] {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.file-info {
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
color: #858585;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Main Content */
|
|
90
|
+
#main {
|
|
91
|
+
flex: 1;
|
|
92
|
+
display: flex;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Config Sidebar */
|
|
97
|
+
#config-sidebar {
|
|
98
|
+
width: 800px;
|
|
99
|
+
background: #1e1e1e;
|
|
100
|
+
border-right: 1px solid #3e3e42;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#config-sidebar.hidden {
|
|
108
|
+
display: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Graph Container */
|
|
112
|
+
#graph-container {
|
|
113
|
+
flex: 1;
|
|
114
|
+
position: relative;
|
|
115
|
+
background: #1e1e1e;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#graph-svg {
|
|
120
|
+
width: 100%;
|
|
121
|
+
height: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Inspector Panel */
|
|
125
|
+
#inspector {
|
|
126
|
+
width: 400px;
|
|
127
|
+
background: #252526;
|
|
128
|
+
border-left: 1px solid #3e3e42;
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#inspector.hidden {
|
|
135
|
+
display: none;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.inspector-header {
|
|
139
|
+
padding: 16px;
|
|
140
|
+
border-bottom: 1px solid #3e3e42;
|
|
141
|
+
display: flex;
|
|
142
|
+
justify-content: space-between;
|
|
143
|
+
align-items: center;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.inspector-header h2 {
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
color: #cccccc;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.close-btn {
|
|
153
|
+
background: none;
|
|
154
|
+
border: none;
|
|
155
|
+
color: #858585;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
font-size: 18px;
|
|
158
|
+
padding: 0;
|
|
159
|
+
width: 24px;
|
|
160
|
+
height: 24px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.close-btn:hover {
|
|
164
|
+
color: #cccccc;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.inspector-tabs {
|
|
168
|
+
display: flex;
|
|
169
|
+
border-bottom: 1px solid #3e3e42;
|
|
170
|
+
background: #2d2d30;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tab {
|
|
174
|
+
padding: 8px 16px;
|
|
175
|
+
background: none;
|
|
176
|
+
border: none;
|
|
177
|
+
color: #858585;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
font-size: 12px;
|
|
180
|
+
border-bottom: 2px solid transparent;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.tab:hover {
|
|
184
|
+
color: #cccccc;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.tab.active {
|
|
188
|
+
color: #ffffff;
|
|
189
|
+
border-bottom-color: #0e639c;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.inspector-content {
|
|
193
|
+
flex: 1;
|
|
194
|
+
overflow-y: auto;
|
|
195
|
+
padding: 16px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.tab-panel {
|
|
199
|
+
display: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.tab-panel.active {
|
|
203
|
+
display: block;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.json-viewer {
|
|
207
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
line-height: 1.6;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.json-key {
|
|
213
|
+
color: #9cdcfe;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.json-string {
|
|
217
|
+
color: #ce9178;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.json-number {
|
|
221
|
+
color: #b5cea8;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.json-boolean {
|
|
225
|
+
color: #569cd6;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.json-null {
|
|
229
|
+
color: #569cd6;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.info-row {
|
|
233
|
+
display: flex;
|
|
234
|
+
margin-bottom: 8px;
|
|
235
|
+
font-size: 13px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.info-label {
|
|
239
|
+
color: #858585;
|
|
240
|
+
min-width: 100px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.info-value {
|
|
244
|
+
color: #cccccc;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Legend */
|
|
248
|
+
.legend {
|
|
249
|
+
position: absolute;
|
|
250
|
+
bottom: 20px;
|
|
251
|
+
left: 20px;
|
|
252
|
+
background: rgba(37, 37, 38, 0.95);
|
|
253
|
+
border: 1px solid #3e3e42;
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
padding: 12px 16px;
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.legend-title {
|
|
260
|
+
font-weight: 600;
|
|
261
|
+
margin-bottom: 8px;
|
|
262
|
+
color: #cccccc;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.legend-item {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
margin-bottom: 4px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.legend-color {
|
|
272
|
+
width: 12px;
|
|
273
|
+
height: 12px;
|
|
274
|
+
border-radius: 2px;
|
|
275
|
+
margin-right: 8px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Graph Styles */
|
|
279
|
+
.node {
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
stroke: #3e3e42;
|
|
282
|
+
stroke-width: 2px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.node:hover {
|
|
286
|
+
stroke: #ffffff;
|
|
287
|
+
stroke-width: 3px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.node.selected {
|
|
291
|
+
stroke: #0e639c;
|
|
292
|
+
stroke-width: 3px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.node-label {
|
|
296
|
+
font-size: 11px;
|
|
297
|
+
fill: #cccccc;
|
|
298
|
+
text-anchor: middle;
|
|
299
|
+
pointer-events: none;
|
|
300
|
+
user-select: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.link {
|
|
304
|
+
stroke: #3e3e42;
|
|
305
|
+
stroke-width: 1.5px;
|
|
306
|
+
stroke-opacity: 0.6;
|
|
307
|
+
fill: none;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.link-data-flow {
|
|
311
|
+
stroke-dasharray: 5, 5;
|
|
312
|
+
stroke: #569cd6;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Status Colors */
|
|
316
|
+
.status-pending {
|
|
317
|
+
fill: #6e6e6e;
|
|
318
|
+
}
|
|
319
|
+
.status-running {
|
|
320
|
+
fill: #0e639c;
|
|
321
|
+
}
|
|
322
|
+
.status-completed {
|
|
323
|
+
fill: #4ec9b0;
|
|
324
|
+
}
|
|
325
|
+
.status-error {
|
|
326
|
+
fill: #f48771;
|
|
327
|
+
}
|
|
328
|
+
.status-skipped {
|
|
329
|
+
fill: #dcdcaa;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Loading State */
|
|
333
|
+
#loading {
|
|
334
|
+
position: absolute;
|
|
335
|
+
top: 50%;
|
|
336
|
+
left: 50%;
|
|
337
|
+
transform: translate(-50%, -50%);
|
|
338
|
+
text-align: center;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#loading.hidden {
|
|
342
|
+
display: none;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.spinner {
|
|
346
|
+
border: 3px solid #3e3e42;
|
|
347
|
+
border-top: 3px solid #0e639c;
|
|
348
|
+
border-radius: 50%;
|
|
349
|
+
width: 40px;
|
|
350
|
+
height: 40px;
|
|
351
|
+
animation: spin 1s linear infinite;
|
|
352
|
+
margin: 0 auto 16px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@keyframes spin {
|
|
356
|
+
0% {
|
|
357
|
+
transform: rotate(0deg);
|
|
358
|
+
}
|
|
359
|
+
100% {
|
|
360
|
+
transform: rotate(360deg);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Empty State */
|
|
365
|
+
#empty-state {
|
|
366
|
+
position: absolute;
|
|
367
|
+
top: 50%;
|
|
368
|
+
left: 50%;
|
|
369
|
+
transform: translate(-50%, -50%);
|
|
370
|
+
text-align: center;
|
|
371
|
+
color: #858585;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#empty-state.hidden {
|
|
375
|
+
display: none;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#empty-state h2 {
|
|
379
|
+
font-size: 18px;
|
|
380
|
+
margin-bottom: 8px;
|
|
381
|
+
color: #cccccc;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#empty-state p {
|
|
385
|
+
font-size: 13px;
|
|
386
|
+
margin-bottom: 20px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* Timeline Controls */
|
|
390
|
+
#timeline-container {
|
|
391
|
+
height: 120px;
|
|
392
|
+
background: #252526;
|
|
393
|
+
border-top: 1px solid #3e3e42;
|
|
394
|
+
display: flex;
|
|
395
|
+
flex-direction: column;
|
|
396
|
+
padding: 12px 24px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#timeline-container.hidden {
|
|
400
|
+
display: none;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.timeline-controls {
|
|
404
|
+
display: flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
gap: 12px;
|
|
407
|
+
margin-bottom: 12px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.playback-btn {
|
|
411
|
+
background: #3e3e42;
|
|
412
|
+
color: #cccccc;
|
|
413
|
+
border: none;
|
|
414
|
+
padding: 8px 12px;
|
|
415
|
+
border-radius: 4px;
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
font-size: 14px;
|
|
418
|
+
transition: background 0.2s;
|
|
419
|
+
min-width: 36px;
|
|
420
|
+
height: 36px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.playback-btn:hover {
|
|
424
|
+
background: #4e4e52;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.playback-btn.active {
|
|
428
|
+
background: #0e639c;
|
|
429
|
+
color: white;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.playback-btn:disabled {
|
|
433
|
+
opacity: 0.5;
|
|
434
|
+
cursor: not-allowed;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.timeline-info {
|
|
438
|
+
flex: 1;
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
gap: 16px;
|
|
442
|
+
font-size: 12px;
|
|
443
|
+
color: #858585;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.timeline-time {
|
|
447
|
+
color: #cccccc;
|
|
448
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.speed-control {
|
|
452
|
+
display: flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
gap: 8px;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.speed-btn {
|
|
458
|
+
background: #3e3e42;
|
|
459
|
+
color: #858585;
|
|
460
|
+
border: none;
|
|
461
|
+
padding: 4px 8px;
|
|
462
|
+
border-radius: 3px;
|
|
463
|
+
cursor: pointer;
|
|
464
|
+
font-size: 11px;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.speed-btn.active {
|
|
468
|
+
background: #0e639c;
|
|
469
|
+
color: white;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* Timeline Scrubber */
|
|
473
|
+
.timeline-scrubber {
|
|
474
|
+
position: relative;
|
|
475
|
+
height: 40px;
|
|
476
|
+
background: #1e1e1e;
|
|
477
|
+
border-radius: 4px;
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.timeline-track {
|
|
482
|
+
position: absolute;
|
|
483
|
+
top: 18px;
|
|
484
|
+
left: 0;
|
|
485
|
+
right: 0;
|
|
486
|
+
height: 4px;
|
|
487
|
+
background: #3e3e42;
|
|
488
|
+
border-radius: 2px;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.timeline-progress {
|
|
492
|
+
position: absolute;
|
|
493
|
+
top: 0;
|
|
494
|
+
left: 0;
|
|
495
|
+
height: 100%;
|
|
496
|
+
background: #0e639c;
|
|
497
|
+
border-radius: 2px;
|
|
498
|
+
transition: width 0.1s linear;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.timeline-handle {
|
|
502
|
+
position: absolute;
|
|
503
|
+
top: 50%;
|
|
504
|
+
transform: translate(-50%, -50%);
|
|
505
|
+
width: 16px;
|
|
506
|
+
height: 16px;
|
|
507
|
+
background: #ffffff;
|
|
508
|
+
border: 2px solid #0e639c;
|
|
509
|
+
border-radius: 50%;
|
|
510
|
+
cursor: grab;
|
|
511
|
+
transition: left 0.1s linear;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.timeline-handle:active {
|
|
515
|
+
cursor: grabbing;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.timeline-events {
|
|
519
|
+
position: absolute;
|
|
520
|
+
top: 0;
|
|
521
|
+
left: 0;
|
|
522
|
+
right: 0;
|
|
523
|
+
height: 100%;
|
|
524
|
+
pointer-events: none;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.timeline-event-marker {
|
|
528
|
+
position: absolute;
|
|
529
|
+
top: 50%;
|
|
530
|
+
transform: translate(-50%, -50%);
|
|
531
|
+
width: 8px;
|
|
532
|
+
height: 8px;
|
|
533
|
+
border-radius: 50%;
|
|
534
|
+
pointer-events: all;
|
|
535
|
+
cursor: pointer;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.timeline-event-marker.check-started {
|
|
539
|
+
background: #569cd6;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.timeline-event-marker.check-completed {
|
|
543
|
+
background: #4ec9b0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.timeline-event-marker.check-failed {
|
|
547
|
+
background: #f48771;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.timeline-event-marker.state-snapshot {
|
|
551
|
+
background: #dcdcaa;
|
|
552
|
+
border: 2px solid #858585;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* Snapshot Panel */
|
|
556
|
+
#snapshot-panel {
|
|
557
|
+
width: 350px;
|
|
558
|
+
background: #252526;
|
|
559
|
+
border-right: 1px solid #3e3e42;
|
|
560
|
+
display: flex;
|
|
561
|
+
flex-direction: column;
|
|
562
|
+
overflow: hidden;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#snapshot-panel.hidden {
|
|
566
|
+
display: none;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.snapshot-header {
|
|
570
|
+
padding: 16px;
|
|
571
|
+
border-bottom: 1px solid #3e3e42;
|
|
572
|
+
display: flex;
|
|
573
|
+
justify-content: space-between;
|
|
574
|
+
align-items: center;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.snapshot-header h2 {
|
|
578
|
+
font-size: 14px;
|
|
579
|
+
font-weight: 600;
|
|
580
|
+
color: #cccccc;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.snapshot-list {
|
|
584
|
+
flex: 1;
|
|
585
|
+
overflow-y: auto;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.snapshot-item {
|
|
589
|
+
padding: 12px 16px;
|
|
590
|
+
border-bottom: 1px solid #3e3e42;
|
|
591
|
+
cursor: pointer;
|
|
592
|
+
transition: background 0.2s;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.snapshot-item:hover {
|
|
596
|
+
background: #2d2d30;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.snapshot-item.active {
|
|
600
|
+
background: #094771;
|
|
601
|
+
border-left: 3px solid #0e639c;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.snapshot-item-header {
|
|
605
|
+
display: flex;
|
|
606
|
+
justify-content: space-between;
|
|
607
|
+
align-items: center;
|
|
608
|
+
margin-bottom: 4px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.snapshot-check-id {
|
|
612
|
+
font-size: 12px;
|
|
613
|
+
font-weight: 600;
|
|
614
|
+
color: #cccccc;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.snapshot-time {
|
|
618
|
+
font-size: 11px;
|
|
619
|
+
color: #858585;
|
|
620
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.snapshot-summary {
|
|
624
|
+
font-size: 11px;
|
|
625
|
+
color: #858585;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/* Diff Viewer */
|
|
629
|
+
.diff-viewer {
|
|
630
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
631
|
+
font-size: 12px;
|
|
632
|
+
line-height: 1.6;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.diff-added {
|
|
636
|
+
background: rgba(78, 201, 176, 0.2);
|
|
637
|
+
color: #4ec9b0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.diff-removed {
|
|
641
|
+
background: rgba(244, 135, 113, 0.2);
|
|
642
|
+
color: #f48771;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.diff-modified {
|
|
646
|
+
background: rgba(220, 220, 170, 0.2);
|
|
647
|
+
color: #dcdcaa;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* Scrollbar */
|
|
651
|
+
::-webkit-scrollbar {
|
|
652
|
+
width: 10px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
::-webkit-scrollbar-track {
|
|
656
|
+
background: #1e1e1e;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
::-webkit-scrollbar-thumb {
|
|
660
|
+
background: #3e3e42;
|
|
661
|
+
border-radius: 5px;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
::-webkit-scrollbar-thumb:hover {
|
|
665
|
+
background: #4e4e52;
|
|
666
|
+
}
|
|
667
|
+
</style>
|
|
668
|
+
</head>
|
|
669
|
+
<body>
|
|
670
|
+
<div id="app">
|
|
671
|
+
<header>
|
|
672
|
+
<h1>🔍 Visor Debug Visualizer</h1>
|
|
673
|
+
<div class="header-controls">
|
|
674
|
+
<!-- Live Mode Controls -->
|
|
675
|
+
<div
|
|
676
|
+
id="live-controls"
|
|
677
|
+
class="hidden"
|
|
678
|
+
style="display: flex; gap: 8px; align-items: center"
|
|
679
|
+
>
|
|
680
|
+
<button class="btn" id="btn-start-execution" onclick="liveMode.start()">
|
|
681
|
+
▶ Start Execution
|
|
682
|
+
</button>
|
|
683
|
+
<button
|
|
684
|
+
class="btn btn-secondary hidden"
|
|
685
|
+
id="btn-pause-execution"
|
|
686
|
+
onclick="liveMode.pause()"
|
|
687
|
+
>
|
|
688
|
+
⏸ Pause
|
|
689
|
+
</button>
|
|
690
|
+
<button
|
|
691
|
+
class="btn btn-secondary hidden"
|
|
692
|
+
id="btn-resume-execution"
|
|
693
|
+
onclick="liveMode.resume()"
|
|
694
|
+
>
|
|
695
|
+
▶ Resume
|
|
696
|
+
</button>
|
|
697
|
+
<button
|
|
698
|
+
class="btn btn-secondary hidden"
|
|
699
|
+
id="btn-stop-execution"
|
|
700
|
+
onclick="liveMode.stop()"
|
|
701
|
+
>
|
|
702
|
+
⏹ Stop
|
|
703
|
+
</button>
|
|
704
|
+
<button
|
|
705
|
+
class="btn btn-secondary hidden"
|
|
706
|
+
id="btn-reset-execution"
|
|
707
|
+
onclick="liveMode.reset()"
|
|
708
|
+
>
|
|
709
|
+
🔄 Reset
|
|
710
|
+
</button>
|
|
711
|
+
<span id="live-status" style="font-size: 12px; color: #858585; margin-left: 8px"
|
|
712
|
+
>Waiting to start...</span
|
|
713
|
+
>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
<span class="file-info" id="file-info">No trace loaded</span>
|
|
717
|
+
<label for="file-input" class="btn btn-secondary"> 📂 Load Trace </label>
|
|
718
|
+
<input type="file" id="file-input" accept=".ndjson,.json" />
|
|
719
|
+
</div>
|
|
720
|
+
</header>
|
|
721
|
+
|
|
722
|
+
<div id="main">
|
|
723
|
+
<!-- Config Sidebar -->
|
|
724
|
+
<div id="config-sidebar" class="hidden">
|
|
725
|
+
<div
|
|
726
|
+
style="
|
|
727
|
+
padding: 12px 16px;
|
|
728
|
+
background: #252526;
|
|
729
|
+
border-bottom: 1px solid #3e3e42;
|
|
730
|
+
display: flex;
|
|
731
|
+
justify-content: space-between;
|
|
732
|
+
align-items: center;
|
|
733
|
+
"
|
|
734
|
+
>
|
|
735
|
+
<h2 style="color: #dcdcaa; font-size: 14px; font-weight: 600">
|
|
736
|
+
📋 Configuration Editor
|
|
737
|
+
</h2>
|
|
738
|
+
<button id="apply-config-btn" class="btn" style="padding: 4px 12px; font-size: 12px">
|
|
739
|
+
Apply Changes
|
|
740
|
+
</button>
|
|
741
|
+
</div>
|
|
742
|
+
<div id="config-editor-container" style="flex: 1; overflow: hidden"></div>
|
|
743
|
+
<div
|
|
744
|
+
style="
|
|
745
|
+
padding: 8px 16px;
|
|
746
|
+
background: #252526;
|
|
747
|
+
border-top: 1px solid #3e3e42;
|
|
748
|
+
font-size: 11px;
|
|
749
|
+
color: #858585;
|
|
750
|
+
"
|
|
751
|
+
>
|
|
752
|
+
Edit the configuration and click "Apply Changes" to update dynamically.
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
<!-- Snapshot Panel -->
|
|
757
|
+
<div id="snapshot-panel" class="hidden">
|
|
758
|
+
<div class="snapshot-header">
|
|
759
|
+
<h2>Snapshots</h2>
|
|
760
|
+
<button class="close-btn" onclick="toggleSnapshotPanel()">×</button>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="snapshot-list" id="snapshot-list">
|
|
763
|
+
<!-- Dynamically populated -->
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<div id="graph-container">
|
|
768
|
+
<svg id="graph-svg"></svg>
|
|
769
|
+
|
|
770
|
+
<div id="loading" class="hidden">
|
|
771
|
+
<div class="spinner"></div>
|
|
772
|
+
<p>Loading trace...</p>
|
|
773
|
+
</div>
|
|
774
|
+
|
|
775
|
+
<div id="empty-state">
|
|
776
|
+
<h2>No Trace Loaded</h2>
|
|
777
|
+
<p>Load a trace file to visualize execution</p>
|
|
778
|
+
<label for="file-input" class="btn"> 📂 Load Trace File </label>
|
|
779
|
+
</div>
|
|
780
|
+
|
|
781
|
+
<div
|
|
782
|
+
id="config-display"
|
|
783
|
+
class="hidden"
|
|
784
|
+
style="padding: 40px; max-width: 1200px; margin: 0 auto; color: #cccccc"
|
|
785
|
+
>
|
|
786
|
+
<h2 style="color: #dcdcaa; margin-bottom: 24px">📋 Loaded Configuration</h2>
|
|
787
|
+
<pre
|
|
788
|
+
id="config-content"
|
|
789
|
+
style="
|
|
790
|
+
background: #1e1e1e;
|
|
791
|
+
padding: 24px;
|
|
792
|
+
border-radius: 8px;
|
|
793
|
+
overflow-x: auto;
|
|
794
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
795
|
+
font-size: 13px;
|
|
796
|
+
line-height: 1.6;
|
|
797
|
+
border: 1px solid #3e3e42;
|
|
798
|
+
color: #d4d4d4;
|
|
799
|
+
white-space: pre-wrap;
|
|
800
|
+
word-wrap: break-word;
|
|
801
|
+
"
|
|
802
|
+
></pre>
|
|
803
|
+
<p style="color: #858585; margin-top: 16px; font-size: 13px">
|
|
804
|
+
Click "Start Execution" to begin analyzing your repository with this configuration.
|
|
805
|
+
</p>
|
|
806
|
+
</div>
|
|
807
|
+
|
|
808
|
+
<div class="legend">
|
|
809
|
+
<div class="legend-title">Status</div>
|
|
810
|
+
<div class="legend-item">
|
|
811
|
+
<div class="legend-color status-pending"></div>
|
|
812
|
+
<span>Pending</span>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="legend-item">
|
|
815
|
+
<div class="legend-color status-running"></div>
|
|
816
|
+
<span>Running</span>
|
|
817
|
+
</div>
|
|
818
|
+
<div class="legend-item">
|
|
819
|
+
<div class="legend-color status-completed"></div>
|
|
820
|
+
<span>Completed</span>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="legend-item">
|
|
823
|
+
<div class="legend-color status-error"></div>
|
|
824
|
+
<span>Error</span>
|
|
825
|
+
</div>
|
|
826
|
+
<div class="legend-item">
|
|
827
|
+
<div class="legend-color status-skipped"></div>
|
|
828
|
+
<span>Skipped</span>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
<div id="inspector" class="hidden">
|
|
834
|
+
<div class="inspector-header">
|
|
835
|
+
<h2 id="inspector-title">Check Details</h2>
|
|
836
|
+
<button class="close-btn" onclick="closeInspector()">×</button>
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<div class="inspector-tabs">
|
|
840
|
+
<button class="tab active" onclick="switchTab('overview', event)">Overview</button>
|
|
841
|
+
<button class="tab" onclick="switchTab('input', event)">Input</button>
|
|
842
|
+
<button class="tab" onclick="switchTab('output', event)">Output</button>
|
|
843
|
+
<button class="tab" onclick="switchTab('events', event)">Events</button>
|
|
844
|
+
<button class="tab" onclick="switchTab('diff', event)">Diff</button>
|
|
845
|
+
<button class="tab" onclick="switchTab('config', event)">Config</button>
|
|
846
|
+
<button class="tab" onclick="switchTab('results', event)">Results</button>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<div class="inspector-content">
|
|
850
|
+
<div id="tab-overview" class="tab-panel active"></div>
|
|
851
|
+
<div id="tab-input" class="tab-panel"></div>
|
|
852
|
+
<div id="tab-output" class="tab-panel"></div>
|
|
853
|
+
<div id="tab-events" class="tab-panel"></div>
|
|
854
|
+
<div id="tab-diff" class="tab-panel"></div>
|
|
855
|
+
<div id="tab-config" class="tab-panel"></div>
|
|
856
|
+
<div id="tab-results" class="tab-panel"></div>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
<!-- Timeline Container -->
|
|
862
|
+
<div id="timeline-container" class="hidden">
|
|
863
|
+
<div class="timeline-controls">
|
|
864
|
+
<button
|
|
865
|
+
class="playback-btn"
|
|
866
|
+
id="btn-first"
|
|
867
|
+
onclick="timeTravel.seekToStart()"
|
|
868
|
+
title="First"
|
|
869
|
+
>
|
|
870
|
+
⏮
|
|
871
|
+
</button>
|
|
872
|
+
<button
|
|
873
|
+
class="playback-btn"
|
|
874
|
+
id="btn-prev"
|
|
875
|
+
onclick="timeTravel.stepBackward()"
|
|
876
|
+
title="Previous"
|
|
877
|
+
>
|
|
878
|
+
⏪
|
|
879
|
+
</button>
|
|
880
|
+
<button
|
|
881
|
+
class="playback-btn"
|
|
882
|
+
id="btn-play"
|
|
883
|
+
onclick="timeTravel.togglePlay()"
|
|
884
|
+
title="Play/Pause"
|
|
885
|
+
>
|
|
886
|
+
▶
|
|
887
|
+
</button>
|
|
888
|
+
<button
|
|
889
|
+
class="playback-btn"
|
|
890
|
+
id="btn-next"
|
|
891
|
+
onclick="timeTravel.stepForward()"
|
|
892
|
+
title="Next"
|
|
893
|
+
>
|
|
894
|
+
⏩
|
|
895
|
+
</button>
|
|
896
|
+
<button class="playback-btn" id="btn-last" onclick="timeTravel.seekToEnd()" title="Last">
|
|
897
|
+
⏭
|
|
898
|
+
</button>
|
|
899
|
+
|
|
900
|
+
<div class="timeline-info">
|
|
901
|
+
<span
|
|
902
|
+
>Event <span class="timeline-time" id="current-event">0</span> /
|
|
903
|
+
<span class="timeline-time" id="total-events">0</span></span
|
|
904
|
+
>
|
|
905
|
+
<span class="timeline-time" id="current-time">00:00.000</span>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<div class="speed-control">
|
|
909
|
+
<span>Speed:</span>
|
|
910
|
+
<button class="speed-btn" onclick="timeTravel.setSpeed(0.5)">0.5×</button>
|
|
911
|
+
<button class="speed-btn active" onclick="timeTravel.setSpeed(1)">1×</button>
|
|
912
|
+
<button class="speed-btn" onclick="timeTravel.setSpeed(2)">2×</button>
|
|
913
|
+
<button class="speed-btn" onclick="timeTravel.setSpeed(5)">5×</button>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
<button
|
|
917
|
+
class="playback-btn"
|
|
918
|
+
id="btn-snapshots"
|
|
919
|
+
onclick="toggleSnapshotPanel()"
|
|
920
|
+
title="Toggle Snapshots"
|
|
921
|
+
>
|
|
922
|
+
📸
|
|
923
|
+
</button>
|
|
924
|
+
</div>
|
|
925
|
+
|
|
926
|
+
<div class="timeline-scrubber" id="timeline-scrubber">
|
|
927
|
+
<div class="timeline-track">
|
|
928
|
+
<div class="timeline-progress" id="timeline-progress"></div>
|
|
929
|
+
</div>
|
|
930
|
+
<div class="timeline-events" id="timeline-events">
|
|
931
|
+
<!-- Event markers dynamically populated -->
|
|
932
|
+
</div>
|
|
933
|
+
<div class="timeline-handle" id="timeline-handle"></div>
|
|
934
|
+
</div>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
<script>
|
|
939
|
+
// ========================================================================
|
|
940
|
+
// Global State
|
|
941
|
+
// ========================================================================
|
|
942
|
+
let currentTrace = null;
|
|
943
|
+
let selectedNode = null;
|
|
944
|
+
let simulation = null;
|
|
945
|
+
let previousSnapshot = null; // For diff view
|
|
946
|
+
|
|
947
|
+
// ========================================================================
|
|
948
|
+
// Trace Parser (Inline version of trace-reader.ts)
|
|
949
|
+
// ========================================================================
|
|
950
|
+
async function parseTraceFile(file) {
|
|
951
|
+
const text = await file.text();
|
|
952
|
+
const lines = text.trim().split('\n');
|
|
953
|
+
const spans = [];
|
|
954
|
+
|
|
955
|
+
for (const line of lines) {
|
|
956
|
+
if (!line.trim()) continue;
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
const rawSpan = JSON.parse(line);
|
|
960
|
+
const span = processRawSpan(rawSpan);
|
|
961
|
+
spans.push(span);
|
|
962
|
+
} catch (e) {
|
|
963
|
+
console.warn('Failed to parse line:', e);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (spans.length === 0) {
|
|
968
|
+
throw new Error('No valid spans found in trace file');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Build tree
|
|
972
|
+
const tree = buildExecutionTree(spans);
|
|
973
|
+
|
|
974
|
+
// Extract snapshots
|
|
975
|
+
const snapshots = extractStateSnapshots(spans);
|
|
976
|
+
|
|
977
|
+
// Compute timeline
|
|
978
|
+
const timeline = computeTimeline(spans);
|
|
979
|
+
|
|
980
|
+
// Calculate metadata
|
|
981
|
+
const sortedSpans = [...spans].sort((a, b) => compareTimeValues(a.startTime, b.startTime));
|
|
982
|
+
const firstSpan = sortedSpans[0];
|
|
983
|
+
const lastSpan = sortedSpans[sortedSpans.length - 1];
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
runId: tree.checkId,
|
|
987
|
+
traceId: firstSpan.traceId,
|
|
988
|
+
spans,
|
|
989
|
+
tree,
|
|
990
|
+
timeline,
|
|
991
|
+
snapshots,
|
|
992
|
+
metadata: {
|
|
993
|
+
startTime: timeValueToISO(firstSpan.startTime),
|
|
994
|
+
endTime: timeValueToISO(lastSpan.endTime),
|
|
995
|
+
duration: timeValueToMillis(lastSpan.endTime) - timeValueToMillis(firstSpan.startTime),
|
|
996
|
+
totalSpans: spans.length,
|
|
997
|
+
totalSnapshots: snapshots.length,
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function processRawSpan(raw) {
|
|
1003
|
+
return {
|
|
1004
|
+
traceId: raw.traceId || '',
|
|
1005
|
+
spanId: raw.spanId || '',
|
|
1006
|
+
parentSpanId: raw.parentSpanId,
|
|
1007
|
+
name: raw.name || 'unknown',
|
|
1008
|
+
startTime: raw.startTime || [0, 0],
|
|
1009
|
+
endTime: raw.endTime || raw.startTime || [0, 0],
|
|
1010
|
+
duration:
|
|
1011
|
+
timeValueToMillis(raw.endTime || raw.startTime) - timeValueToMillis(raw.startTime),
|
|
1012
|
+
attributes: raw.attributes || {},
|
|
1013
|
+
events: (raw.events || []).map(evt => ({
|
|
1014
|
+
name: evt.name || 'unknown',
|
|
1015
|
+
time: evt.time || [0, 0],
|
|
1016
|
+
timestamp: evt.timestamp || timeValueToISO(evt.time || [0, 0]),
|
|
1017
|
+
attributes: evt.attributes || {},
|
|
1018
|
+
})),
|
|
1019
|
+
status: raw.status?.code === 2 ? 'error' : 'ok',
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function buildExecutionTree(spans) {
|
|
1024
|
+
const nodeMap = new Map();
|
|
1025
|
+
|
|
1026
|
+
// Create nodes
|
|
1027
|
+
for (const span of spans) {
|
|
1028
|
+
const node = createExecutionNode(span);
|
|
1029
|
+
nodeMap.set(span.spanId, node);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Build relationships
|
|
1033
|
+
let rootNode;
|
|
1034
|
+
for (const span of spans) {
|
|
1035
|
+
const node = nodeMap.get(span.spanId);
|
|
1036
|
+
if (!span.parentSpanId) {
|
|
1037
|
+
rootNode = node;
|
|
1038
|
+
} else {
|
|
1039
|
+
const parent = nodeMap.get(span.parentSpanId);
|
|
1040
|
+
if (parent) {
|
|
1041
|
+
parent.children.push(node);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return (
|
|
1047
|
+
rootNode || {
|
|
1048
|
+
checkId: 'root',
|
|
1049
|
+
type: 'run',
|
|
1050
|
+
status: 'completed',
|
|
1051
|
+
children: Array.from(nodeMap.values()),
|
|
1052
|
+
span: spans[0],
|
|
1053
|
+
state: {},
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function createExecutionNode(span) {
|
|
1059
|
+
const attrs = span.attributes;
|
|
1060
|
+
// Extract check ID from span name (e.g., "visor.check.hello" -> "hello")
|
|
1061
|
+
let checkId = attrs['visor.check.id'] || attrs['visor.run.id'];
|
|
1062
|
+
if (!checkId && span.name.startsWith('visor.check.')) {
|
|
1063
|
+
checkId = span.name.replace('visor.check.', '');
|
|
1064
|
+
}
|
|
1065
|
+
if (!checkId) {
|
|
1066
|
+
checkId = span.name === 'visor.run' ? 'visor.run' : span.spanId;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
let type = 'unknown';
|
|
1070
|
+
if (span.name === 'visor.run') type = 'run';
|
|
1071
|
+
else if (span.name.startsWith('visor.check.')) type = 'check';
|
|
1072
|
+
else if (span.name === 'visor.check') type = 'check';
|
|
1073
|
+
else if (span.name.startsWith('visor.provider.')) type = 'provider';
|
|
1074
|
+
|
|
1075
|
+
let status = 'completed';
|
|
1076
|
+
if (span.status === 'error') status = 'error';
|
|
1077
|
+
else if (attrs['visor.check.skipped']) status = 'skipped';
|
|
1078
|
+
|
|
1079
|
+
const state = {
|
|
1080
|
+
inputContext: parseJSON(attrs['visor.check.input.context']),
|
|
1081
|
+
output: parseJSON(attrs['visor.check.output']),
|
|
1082
|
+
metadata: {
|
|
1083
|
+
type: attrs['visor.check.type'],
|
|
1084
|
+
duration: span.duration,
|
|
1085
|
+
provider: attrs['visor.provider.type'],
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
if (span.status === 'error') {
|
|
1090
|
+
state.errors = [attrs['visor.check.error'] || 'Unknown error'];
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return { checkId, type, status, children: [], span, state };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function extractStateSnapshots(spans) {
|
|
1097
|
+
const snapshots = [];
|
|
1098
|
+
for (const span of spans) {
|
|
1099
|
+
for (const event of span.events) {
|
|
1100
|
+
if (event.name === 'state.snapshot') {
|
|
1101
|
+
const attrs = event.attributes;
|
|
1102
|
+
snapshots.push({
|
|
1103
|
+
checkId: attrs['visor.snapshot.check_id'] || 'unknown',
|
|
1104
|
+
timestamp: attrs['visor.snapshot.timestamp'] || event.timestamp,
|
|
1105
|
+
timestampNanos: event.time,
|
|
1106
|
+
outputs: parseJSON(attrs['visor.snapshot.outputs']) || {},
|
|
1107
|
+
memory: parseJSON(attrs['visor.snapshot.memory']) || {},
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
snapshots.sort((a, b) => compareTimeValues(a.timestampNanos, b.timestampNanos));
|
|
1113
|
+
return snapshots;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function computeTimeline(spans) {
|
|
1117
|
+
const events = [];
|
|
1118
|
+
for (const span of spans) {
|
|
1119
|
+
const checkId = span.attributes['visor.check.id'] || span.spanId;
|
|
1120
|
+
|
|
1121
|
+
events.push({
|
|
1122
|
+
type: 'check.started',
|
|
1123
|
+
checkId,
|
|
1124
|
+
timestamp: timeValueToISO(span.startTime),
|
|
1125
|
+
timestampNanos: span.startTime,
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
events.push({
|
|
1129
|
+
type: span.status === 'error' ? 'check.failed' : 'check.completed',
|
|
1130
|
+
checkId,
|
|
1131
|
+
timestamp: timeValueToISO(span.endTime),
|
|
1132
|
+
timestampNanos: span.endTime,
|
|
1133
|
+
duration: span.duration,
|
|
1134
|
+
status: span.status,
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
for (const evt of span.events) {
|
|
1138
|
+
events.push({
|
|
1139
|
+
type: evt.name === 'state.snapshot' ? 'state.snapshot' : 'event',
|
|
1140
|
+
checkId,
|
|
1141
|
+
timestamp: evt.timestamp,
|
|
1142
|
+
timestampNanos: evt.time,
|
|
1143
|
+
metadata: { eventName: evt.name, attributes: evt.attributes },
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
events.sort((a, b) => compareTimeValues(a.timestampNanos, b.timestampNanos));
|
|
1148
|
+
return events;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Utility functions
|
|
1152
|
+
function timeValueToMillis(tv) {
|
|
1153
|
+
return tv[0] * 1000 + tv[1] / 1_000_000;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function timeValueToISO(tv) {
|
|
1157
|
+
return new Date(timeValueToMillis(tv)).toISOString();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function compareTimeValues(a, b) {
|
|
1161
|
+
if (a[0] !== b[0]) return a[0] - b[0];
|
|
1162
|
+
return a[1] - b[1];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function parseJSON(str) {
|
|
1166
|
+
if (typeof str !== 'string') return null;
|
|
1167
|
+
try {
|
|
1168
|
+
return JSON.parse(str);
|
|
1169
|
+
} catch (e) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// ========================================================================
|
|
1175
|
+
// File Handling
|
|
1176
|
+
// ========================================================================
|
|
1177
|
+
document.getElementById('file-input').addEventListener('change', async e => {
|
|
1178
|
+
const file = e.target.files[0];
|
|
1179
|
+
if (!file) return;
|
|
1180
|
+
|
|
1181
|
+
showLoading();
|
|
1182
|
+
hideEmptyState();
|
|
1183
|
+
|
|
1184
|
+
try {
|
|
1185
|
+
currentTrace = await parseTraceFile(file);
|
|
1186
|
+
document.getElementById('file-info').textContent =
|
|
1187
|
+
`${file.name} (${currentTrace.metadata.totalSpans} spans, ${currentTrace.metadata.duration.toFixed(0)}ms)`;
|
|
1188
|
+
|
|
1189
|
+
visualizeTrace(currentTrace);
|
|
1190
|
+
timeTravel.init(currentTrace);
|
|
1191
|
+
hideLoading();
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
alert('Failed to parse trace file: ' + error.message);
|
|
1194
|
+
console.error(error);
|
|
1195
|
+
showEmptyState();
|
|
1196
|
+
hideLoading();
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Check for URL parameter
|
|
1201
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1202
|
+
const traceUrl = urlParams.get('trace');
|
|
1203
|
+
if (traceUrl) {
|
|
1204
|
+
loadTraceFromUrl(traceUrl);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function loadTraceFromUrl(url) {
|
|
1208
|
+
showLoading();
|
|
1209
|
+
hideEmptyState();
|
|
1210
|
+
|
|
1211
|
+
try {
|
|
1212
|
+
const response = await fetch(url);
|
|
1213
|
+
const blob = await response.blob();
|
|
1214
|
+
const file = new File([blob], url.split('/').pop());
|
|
1215
|
+
currentTrace = await parseTraceFile(file);
|
|
1216
|
+
|
|
1217
|
+
document.getElementById('file-info').textContent =
|
|
1218
|
+
`${url.split('/').pop()} (${currentTrace.metadata.totalSpans} spans)`;
|
|
1219
|
+
|
|
1220
|
+
visualizeTrace(currentTrace);
|
|
1221
|
+
timeTravel.init(currentTrace);
|
|
1222
|
+
hideLoading();
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
alert('Failed to load trace from URL: ' + error.message);
|
|
1225
|
+
console.error(error);
|
|
1226
|
+
showEmptyState();
|
|
1227
|
+
hideLoading();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// ========================================================================
|
|
1232
|
+
// Visualization
|
|
1233
|
+
// ========================================================================
|
|
1234
|
+
// Keep track of graph container
|
|
1235
|
+
let graphContainer = null;
|
|
1236
|
+
|
|
1237
|
+
function visualizeTrace(trace) {
|
|
1238
|
+
const svg = d3.select('#graph-svg');
|
|
1239
|
+
const width = document.getElementById('graph-container').clientWidth;
|
|
1240
|
+
const height = document.getElementById('graph-container').clientHeight;
|
|
1241
|
+
|
|
1242
|
+
// Convert tree to graph nodes and links
|
|
1243
|
+
const { nodes, links } = treeToGraph(trace.tree);
|
|
1244
|
+
|
|
1245
|
+
// Add hierarchy levels for vertical positioning
|
|
1246
|
+
assignHierarchyLevels(trace.tree);
|
|
1247
|
+
|
|
1248
|
+
// Initialize graph container and zoom on first render
|
|
1249
|
+
if (!graphContainer) {
|
|
1250
|
+
svg.selectAll('*').remove(); // Clear only on first render
|
|
1251
|
+
graphContainer = svg.append('g');
|
|
1252
|
+
|
|
1253
|
+
// Add zoom behavior once
|
|
1254
|
+
svg.call(
|
|
1255
|
+
d3
|
|
1256
|
+
.zoom()
|
|
1257
|
+
.scaleExtent([0.1, 4])
|
|
1258
|
+
.on('zoom', event => {
|
|
1259
|
+
graphContainer.attr('transform', event.transform);
|
|
1260
|
+
})
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Preserve positions of existing nodes and set initial positions for new ones
|
|
1265
|
+
if (simulation && simulation.nodes().length > 0) {
|
|
1266
|
+
const existingNodes = new Map(simulation.nodes().map(n => [n.id, n]));
|
|
1267
|
+
nodes.forEach(node => {
|
|
1268
|
+
const existing = existingNodes.get(node.id);
|
|
1269
|
+
if (existing) {
|
|
1270
|
+
// Preserve existing node position
|
|
1271
|
+
node.x = existing.x;
|
|
1272
|
+
node.y = existing.y;
|
|
1273
|
+
node.vx = existing.vx;
|
|
1274
|
+
node.vy = existing.vy;
|
|
1275
|
+
} else {
|
|
1276
|
+
// New node - set initial position based on hierarchy to avoid overlap
|
|
1277
|
+
node.x = width / 2 + (Math.random() - 0.5) * 100;
|
|
1278
|
+
node.y = 100 + (node.level || 0) * 200;
|
|
1279
|
+
node.vx = 0;
|
|
1280
|
+
node.vy = 0;
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
} else {
|
|
1284
|
+
// First render - initialize all nodes
|
|
1285
|
+
nodes.forEach(node => {
|
|
1286
|
+
node.x = width / 2 + (Math.random() - 0.5) * 100;
|
|
1287
|
+
node.y = 100 + (node.level || 0) * 200;
|
|
1288
|
+
node.vx = 0;
|
|
1289
|
+
node.vy = 0;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Create or update force simulation
|
|
1294
|
+
if (!simulation || simulation.nodes().length === 0) {
|
|
1295
|
+
// Initial creation
|
|
1296
|
+
simulation = d3
|
|
1297
|
+
.forceSimulation(nodes)
|
|
1298
|
+
.force(
|
|
1299
|
+
'link',
|
|
1300
|
+
d3
|
|
1301
|
+
.forceLink(links)
|
|
1302
|
+
.id(d => d.id)
|
|
1303
|
+
.distance(200)
|
|
1304
|
+
.strength(1)
|
|
1305
|
+
)
|
|
1306
|
+
.force('charge', d3.forceManyBody().strength(-1000))
|
|
1307
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
1308
|
+
.force('collision', d3.forceCollide().radius(70).strength(1.5))
|
|
1309
|
+
.force('y', d3.forceY(d => 100 + (d.level || 0) * 200).strength(0.8))
|
|
1310
|
+
.force('x', d3.forceX(width / 2).strength(0.05))
|
|
1311
|
+
.alphaDecay(0.01);
|
|
1312
|
+
} else {
|
|
1313
|
+
// Update existing simulation with new nodes/links
|
|
1314
|
+
simulation.nodes(nodes);
|
|
1315
|
+
simulation.force('link').links(links);
|
|
1316
|
+
simulation.alpha(0.5).restart(); // Reheat simulation to settle new nodes
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const g = graphContainer;
|
|
1320
|
+
|
|
1321
|
+
// Update links using data join
|
|
1322
|
+
const link = g
|
|
1323
|
+
.selectAll('.link')
|
|
1324
|
+
.data(links, d => `${d.source.id || d.source}-${d.target.id || d.target}`)
|
|
1325
|
+
.join(
|
|
1326
|
+
enter =>
|
|
1327
|
+
enter
|
|
1328
|
+
.append('path')
|
|
1329
|
+
.attr('class', 'link')
|
|
1330
|
+
.attr('stroke', '#3e3e42')
|
|
1331
|
+
.attr('stroke-width', 2),
|
|
1332
|
+
update => update,
|
|
1333
|
+
exit => exit.remove()
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
// Update nodes using data join
|
|
1337
|
+
const node = g
|
|
1338
|
+
.selectAll('.node')
|
|
1339
|
+
.data(nodes, d => d.id)
|
|
1340
|
+
.join(
|
|
1341
|
+
enter =>
|
|
1342
|
+
enter
|
|
1343
|
+
.append('circle')
|
|
1344
|
+
.attr('class', d => `node status-${d.status}`)
|
|
1345
|
+
.attr('r', 25)
|
|
1346
|
+
.attr('stroke', '#252526')
|
|
1347
|
+
.attr('stroke-width', 3)
|
|
1348
|
+
.call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded))
|
|
1349
|
+
.on('click', (event, d) => {
|
|
1350
|
+
event.stopPropagation();
|
|
1351
|
+
selectNode(d);
|
|
1352
|
+
}),
|
|
1353
|
+
update => update.attr('class', d => `node status-${d.status}`),
|
|
1354
|
+
exit => exit.remove()
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
// Update labels using data join
|
|
1358
|
+
const label = g
|
|
1359
|
+
.selectAll('.node-label')
|
|
1360
|
+
.data(nodes, d => d.id)
|
|
1361
|
+
.join(
|
|
1362
|
+
enter =>
|
|
1363
|
+
enter
|
|
1364
|
+
.append('text')
|
|
1365
|
+
.attr('class', 'node-label')
|
|
1366
|
+
.attr('dy', 35)
|
|
1367
|
+
.attr('text-anchor', 'middle')
|
|
1368
|
+
.style('pointer-events', 'none')
|
|
1369
|
+
.style('fill', '#d4d4d4')
|
|
1370
|
+
.style('font-size', '12px')
|
|
1371
|
+
.text(d =>
|
|
1372
|
+
d.checkId.length > 15 ? d.checkId.substring(0, 12) + '...' : d.checkId
|
|
1373
|
+
),
|
|
1374
|
+
update =>
|
|
1375
|
+
update.text(d =>
|
|
1376
|
+
d.checkId.length > 15 ? d.checkId.substring(0, 12) + '...' : d.checkId
|
|
1377
|
+
),
|
|
1378
|
+
exit => exit.remove()
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
// Update positions on tick
|
|
1382
|
+
simulation.on('tick', () => {
|
|
1383
|
+
link.attr('d', d => {
|
|
1384
|
+
const dx = d.target.x - d.source.x;
|
|
1385
|
+
const dy = d.target.y - d.source.y;
|
|
1386
|
+
const dr = Math.sqrt(dx * dx + dy * dy);
|
|
1387
|
+
return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
|
1391
|
+
|
|
1392
|
+
label.attr('x', d => d.x).attr('y', d => d.y);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function treeToGraph(tree, nodes = [], links = [], parent = null) {
|
|
1397
|
+
const node = {
|
|
1398
|
+
id: tree.checkId,
|
|
1399
|
+
checkId: tree.checkId,
|
|
1400
|
+
type: tree.type,
|
|
1401
|
+
status: tree.status,
|
|
1402
|
+
level: tree.level || 0,
|
|
1403
|
+
data: tree,
|
|
1404
|
+
};
|
|
1405
|
+
nodes.push(node);
|
|
1406
|
+
|
|
1407
|
+
if (parent) {
|
|
1408
|
+
links.push({ source: parent.id, target: node.id });
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
for (const child of tree.children) {
|
|
1412
|
+
treeToGraph(child, nodes, links, node);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return { nodes, links };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Assign hierarchy levels for vertical layout
|
|
1419
|
+
function assignHierarchyLevels(tree, level = 0) {
|
|
1420
|
+
if (!tree) return;
|
|
1421
|
+
tree.level = level;
|
|
1422
|
+
for (const child of tree.children || []) {
|
|
1423
|
+
assignHierarchyLevels(child, level + 1);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Drag handlers
|
|
1428
|
+
function dragStarted(event, d) {
|
|
1429
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1430
|
+
d.fx = d.x;
|
|
1431
|
+
d.fy = d.y;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function dragged(event, d) {
|
|
1435
|
+
d.fx = event.x;
|
|
1436
|
+
d.fy = event.y;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function dragEnded(event, d) {
|
|
1440
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
1441
|
+
d.fx = null;
|
|
1442
|
+
d.fy = null;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ========================================================================
|
|
1446
|
+
// Inspector
|
|
1447
|
+
// ========================================================================
|
|
1448
|
+
function selectNode(node) {
|
|
1449
|
+
selectedNode = node;
|
|
1450
|
+
|
|
1451
|
+
// Update visual selection
|
|
1452
|
+
d3.selectAll('.node').classed('selected', false);
|
|
1453
|
+
d3.selectAll('.node')
|
|
1454
|
+
.filter(d => d.id === node.id)
|
|
1455
|
+
.classed('selected', true);
|
|
1456
|
+
|
|
1457
|
+
// Show inspector
|
|
1458
|
+
document.getElementById('inspector').classList.remove('hidden');
|
|
1459
|
+
document.getElementById('inspector-title').textContent = node.checkId;
|
|
1460
|
+
|
|
1461
|
+
// Populate tabs
|
|
1462
|
+
populateOverview(node);
|
|
1463
|
+
populateInput(node);
|
|
1464
|
+
populateOutput(node);
|
|
1465
|
+
populateEvents(node);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function closeInspector() {
|
|
1469
|
+
document.getElementById('inspector').classList.add('hidden');
|
|
1470
|
+
d3.selectAll('.node').classed('selected', false);
|
|
1471
|
+
selectedNode = null;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function switchTab(tabName, event) {
|
|
1475
|
+
// Update tab buttons
|
|
1476
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
1477
|
+
tab.classList.remove('active');
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
// If called from onclick, highlight the clicked tab
|
|
1481
|
+
if (event && event.target) {
|
|
1482
|
+
event.target.classList.add('active');
|
|
1483
|
+
} else {
|
|
1484
|
+
// If called programmatically, find and highlight the tab by name
|
|
1485
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
1486
|
+
if (tab.textContent.toLowerCase() === tabName.toLowerCase()) {
|
|
1487
|
+
tab.classList.add('active');
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Update tab panels
|
|
1493
|
+
document.querySelectorAll('.tab-panel').forEach(panel => {
|
|
1494
|
+
panel.classList.remove('active');
|
|
1495
|
+
});
|
|
1496
|
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function populateOverview(node) {
|
|
1500
|
+
const data = node.data;
|
|
1501
|
+
const html = `
|
|
1502
|
+
<div class="info-row">
|
|
1503
|
+
<span class="info-label">Check ID:</span>
|
|
1504
|
+
<span class="info-value">${data.checkId}</span>
|
|
1505
|
+
</div>
|
|
1506
|
+
<div class="info-row">
|
|
1507
|
+
<span class="info-label">Type:</span>
|
|
1508
|
+
<span class="info-value">${data.type}</span>
|
|
1509
|
+
</div>
|
|
1510
|
+
<div class="info-row">
|
|
1511
|
+
<span class="info-label">Status:</span>
|
|
1512
|
+
<span class="info-value">${data.status}</span>
|
|
1513
|
+
</div>
|
|
1514
|
+
<div class="info-row">
|
|
1515
|
+
<span class="info-label">Duration:</span>
|
|
1516
|
+
<span class="info-value">${data.span.duration.toFixed(2)}ms</span>
|
|
1517
|
+
</div>
|
|
1518
|
+
<div class="info-row">
|
|
1519
|
+
<span class="info-label">Start Time:</span>
|
|
1520
|
+
<span class="info-value">${timeValueToISO(data.span.startTime)}</span>
|
|
1521
|
+
</div>
|
|
1522
|
+
<div class="info-row">
|
|
1523
|
+
<span class="info-label">End Time:</span>
|
|
1524
|
+
<span class="info-value">${timeValueToISO(data.span.endTime)}</span>
|
|
1525
|
+
</div>
|
|
1526
|
+
${
|
|
1527
|
+
data.state.metadata?.type
|
|
1528
|
+
? `
|
|
1529
|
+
<div class="info-row">
|
|
1530
|
+
<span class="info-label">Check Type:</span>
|
|
1531
|
+
<span class="info-value">${data.state.metadata.type}</span>
|
|
1532
|
+
</div>
|
|
1533
|
+
`
|
|
1534
|
+
: ''
|
|
1535
|
+
}
|
|
1536
|
+
${
|
|
1537
|
+
data.state.errors
|
|
1538
|
+
? `
|
|
1539
|
+
<div class="info-row">
|
|
1540
|
+
<span class="info-label">Error:</span>
|
|
1541
|
+
<span class="info-value" style="color: #f48771;">${data.state.errors.join(', ')}</span>
|
|
1542
|
+
</div>
|
|
1543
|
+
`
|
|
1544
|
+
: ''
|
|
1545
|
+
}
|
|
1546
|
+
`;
|
|
1547
|
+
document.getElementById('tab-overview').innerHTML = html;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function populateInput(node) {
|
|
1551
|
+
const input = node.data.state.inputContext;
|
|
1552
|
+
const html = input
|
|
1553
|
+
? `<div class="json-viewer">${syntaxHighlightJSON(input)}</div>`
|
|
1554
|
+
: '<p style="color: #858585;">No input context available</p>';
|
|
1555
|
+
document.getElementById('tab-input').innerHTML = html;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function populateOutput(node) {
|
|
1559
|
+
const output = node.data.state.output;
|
|
1560
|
+
const html = output
|
|
1561
|
+
? `<div class="json-viewer">${syntaxHighlightJSON(output)}</div>`
|
|
1562
|
+
: '<p style="color: #858585;">No output available</p>';
|
|
1563
|
+
document.getElementById('tab-output').innerHTML = html;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function populateEvents(node) {
|
|
1567
|
+
const events = node.data.span.events;
|
|
1568
|
+
if (events.length === 0) {
|
|
1569
|
+
document.getElementById('tab-events').innerHTML =
|
|
1570
|
+
'<p style="color: #858585;">No events</p>';
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
const html = events
|
|
1575
|
+
.map(
|
|
1576
|
+
evt => `
|
|
1577
|
+
<div style="margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #3e3e42;">
|
|
1578
|
+
<div class="info-row">
|
|
1579
|
+
<span class="info-label">Event:</span>
|
|
1580
|
+
<span class="info-value">${evt.name}</span>
|
|
1581
|
+
</div>
|
|
1582
|
+
<div class="info-row">
|
|
1583
|
+
<span class="info-label">Time:</span>
|
|
1584
|
+
<span class="info-value">${evt.timestamp}</span>
|
|
1585
|
+
</div>
|
|
1586
|
+
${
|
|
1587
|
+
Object.keys(evt.attributes).length > 0
|
|
1588
|
+
? `
|
|
1589
|
+
<div style="margin-top: 8px;">
|
|
1590
|
+
<div class="info-label" style="margin-bottom: 4px;">Attributes:</div>
|
|
1591
|
+
<div class="json-viewer">${syntaxHighlightJSON(evt.attributes)}</div>
|
|
1592
|
+
</div>
|
|
1593
|
+
`
|
|
1594
|
+
: ''
|
|
1595
|
+
}
|
|
1596
|
+
</div>
|
|
1597
|
+
`
|
|
1598
|
+
)
|
|
1599
|
+
.join('');
|
|
1600
|
+
|
|
1601
|
+
document.getElementById('tab-events').innerHTML = html;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function syntaxHighlightJSON(obj) {
|
|
1605
|
+
const json = JSON.stringify(obj, null, 2);
|
|
1606
|
+
return json
|
|
1607
|
+
.replace(/&/g, '&')
|
|
1608
|
+
.replace(/</g, '<')
|
|
1609
|
+
.replace(/>/g, '>')
|
|
1610
|
+
.replace(
|
|
1611
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
1612
|
+
match => {
|
|
1613
|
+
let cls = 'json-number';
|
|
1614
|
+
if (/^"/.test(match)) {
|
|
1615
|
+
if (/:$/.test(match)) {
|
|
1616
|
+
cls = 'json-key';
|
|
1617
|
+
} else {
|
|
1618
|
+
cls = 'json-string';
|
|
1619
|
+
}
|
|
1620
|
+
} else if (/true|false/.test(match)) {
|
|
1621
|
+
cls = 'json-boolean';
|
|
1622
|
+
} else if (/null/.test(match)) {
|
|
1623
|
+
cls = 'json-null';
|
|
1624
|
+
}
|
|
1625
|
+
return '<span class="' + cls + '">' + match + '</span>';
|
|
1626
|
+
}
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// ========================================================================
|
|
1631
|
+
// UI Helpers
|
|
1632
|
+
// ========================================================================
|
|
1633
|
+
function showLoading() {
|
|
1634
|
+
document.getElementById('loading').classList.remove('hidden');
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function hideLoading() {
|
|
1638
|
+
document.getElementById('loading').classList.add('hidden');
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function showEmptyState() {
|
|
1642
|
+
document.getElementById('empty-state').classList.remove('hidden');
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function hideEmptyState() {
|
|
1646
|
+
document.getElementById('empty-state').classList.add('hidden');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// ========================================================================
|
|
1650
|
+
// Time-Travel Debugging
|
|
1651
|
+
// ========================================================================
|
|
1652
|
+
const timeTravel = {
|
|
1653
|
+
currentIndex: 0,
|
|
1654
|
+
isPlaying: false,
|
|
1655
|
+
playbackSpeed: 1,
|
|
1656
|
+
playbackInterval: null,
|
|
1657
|
+
|
|
1658
|
+
init(trace) {
|
|
1659
|
+
if (!trace || !trace.timeline || trace.timeline.length === 0) {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Show timeline
|
|
1664
|
+
document.getElementById('timeline-container').classList.remove('hidden');
|
|
1665
|
+
document.getElementById('total-events').textContent = trace.timeline.length;
|
|
1666
|
+
|
|
1667
|
+
// Build timeline scrubber
|
|
1668
|
+
this.buildTimelineScrubber(trace.timeline);
|
|
1669
|
+
|
|
1670
|
+
// Populate snapshot panel
|
|
1671
|
+
this.populateSnapshotPanel(trace.snapshots);
|
|
1672
|
+
|
|
1673
|
+
// Seek to start
|
|
1674
|
+
this.seekToIndex(0);
|
|
1675
|
+
},
|
|
1676
|
+
|
|
1677
|
+
buildTimelineScrubber(timeline) {
|
|
1678
|
+
const eventsContainer = document.getElementById('timeline-events');
|
|
1679
|
+
eventsContainer.innerHTML = '';
|
|
1680
|
+
|
|
1681
|
+
if (timeline.length === 0) return;
|
|
1682
|
+
|
|
1683
|
+
const startTime = timeValueToMillis(timeline[0].timestampNanos);
|
|
1684
|
+
const endTime = timeValueToMillis(timeline[timeline.length - 1].timestampNanos);
|
|
1685
|
+
const duration = endTime - startTime;
|
|
1686
|
+
|
|
1687
|
+
timeline.forEach((event, index) => {
|
|
1688
|
+
const eventTime = timeValueToMillis(event.timestampNanos);
|
|
1689
|
+
const position = duration > 0 ? ((eventTime - startTime) / duration) * 100 : 0;
|
|
1690
|
+
|
|
1691
|
+
const marker = document.createElement('div');
|
|
1692
|
+
marker.className = `timeline-event-marker ${event.type.replace('.', '-')}`;
|
|
1693
|
+
marker.style.left = `${position}%`;
|
|
1694
|
+
marker.title = `${event.type} - ${event.checkId}`;
|
|
1695
|
+
marker.onclick = () => this.seekToIndex(index);
|
|
1696
|
+
eventsContainer.appendChild(marker);
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
// Add scrubber click handler
|
|
1700
|
+
const scrubber = document.getElementById('timeline-scrubber');
|
|
1701
|
+
scrubber.onclick = e => {
|
|
1702
|
+
const rect = scrubber.getBoundingClientRect();
|
|
1703
|
+
const x = e.clientX - rect.left;
|
|
1704
|
+
const percent = x / rect.width;
|
|
1705
|
+
const index = Math.floor(percent * timeline.length);
|
|
1706
|
+
this.seekToIndex(Math.max(0, Math.min(timeline.length - 1, index)));
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// Add drag handler for handle
|
|
1710
|
+
const handle = document.getElementById('timeline-handle');
|
|
1711
|
+
let isDragging = false;
|
|
1712
|
+
|
|
1713
|
+
handle.onmousedown = e => {
|
|
1714
|
+
isDragging = true;
|
|
1715
|
+
e.preventDefault();
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
document.onmousemove = e => {
|
|
1719
|
+
if (!isDragging) return;
|
|
1720
|
+
|
|
1721
|
+
const rect = scrubber.getBoundingClientRect();
|
|
1722
|
+
const x = e.clientX - rect.left;
|
|
1723
|
+
const percent = Math.max(0, Math.min(1, x / rect.width));
|
|
1724
|
+
const index = Math.floor(percent * timeline.length);
|
|
1725
|
+
this.seekToIndex(Math.max(0, Math.min(timeline.length - 1, index)));
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
document.onmouseup = () => {
|
|
1729
|
+
isDragging = false;
|
|
1730
|
+
};
|
|
1731
|
+
},
|
|
1732
|
+
|
|
1733
|
+
populateSnapshotPanel(snapshots) {
|
|
1734
|
+
const list = document.getElementById('snapshot-list');
|
|
1735
|
+
list.innerHTML = '';
|
|
1736
|
+
|
|
1737
|
+
if (!snapshots || snapshots.length === 0) {
|
|
1738
|
+
list.innerHTML = '<p style="padding: 16px; color: #858585;">No snapshots available</p>';
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
snapshots.forEach((snapshot, index) => {
|
|
1743
|
+
const item = document.createElement('div');
|
|
1744
|
+
item.className = 'snapshot-item';
|
|
1745
|
+
item.onclick = () => this.jumpToSnapshot(snapshot);
|
|
1746
|
+
|
|
1747
|
+
const outputCount = Object.keys(snapshot.outputs || {}).length;
|
|
1748
|
+
const memoryCount = Object.keys(snapshot.memory || {}).length;
|
|
1749
|
+
|
|
1750
|
+
item.innerHTML = `
|
|
1751
|
+
<div class="snapshot-item-header">
|
|
1752
|
+
<span class="snapshot-check-id">${snapshot.checkId}</span>
|
|
1753
|
+
<span class="snapshot-time">${new Date(snapshot.timestamp).toLocaleTimeString()}</span>
|
|
1754
|
+
</div>
|
|
1755
|
+
<div class="snapshot-summary">
|
|
1756
|
+
${outputCount} outputs, ${memoryCount} memory keys
|
|
1757
|
+
</div>
|
|
1758
|
+
`;
|
|
1759
|
+
|
|
1760
|
+
list.appendChild(item);
|
|
1761
|
+
});
|
|
1762
|
+
},
|
|
1763
|
+
|
|
1764
|
+
seekToIndex(index) {
|
|
1765
|
+
if (!currentTrace || !currentTrace.timeline) return;
|
|
1766
|
+
|
|
1767
|
+
this.currentIndex = Math.max(0, Math.min(currentTrace.timeline.length - 1, index));
|
|
1768
|
+
const event = currentTrace.timeline[this.currentIndex];
|
|
1769
|
+
|
|
1770
|
+
// Update UI
|
|
1771
|
+
document.getElementById('current-event').textContent = this.currentIndex + 1;
|
|
1772
|
+
|
|
1773
|
+
const startTime = timeValueToMillis(currentTrace.timeline[0].timestampNanos);
|
|
1774
|
+
const eventTime = timeValueToMillis(event.timestampNanos);
|
|
1775
|
+
const elapsed = eventTime - startTime;
|
|
1776
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
1777
|
+
const millis = Math.floor(elapsed % 1000);
|
|
1778
|
+
document.getElementById('current-time').textContent =
|
|
1779
|
+
`${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}.${String(millis).padStart(3, '0')}`;
|
|
1780
|
+
|
|
1781
|
+
// Update scrubber position
|
|
1782
|
+
const percent =
|
|
1783
|
+
currentTrace.timeline.length > 1
|
|
1784
|
+
? (this.currentIndex / (currentTrace.timeline.length - 1)) * 100
|
|
1785
|
+
: 0;
|
|
1786
|
+
document.getElementById('timeline-handle').style.left = `${percent}%`;
|
|
1787
|
+
document.getElementById('timeline-progress').style.width = `${percent}%`;
|
|
1788
|
+
|
|
1789
|
+
// Apply state at this point in time
|
|
1790
|
+
this.applyStateAtIndex(this.currentIndex);
|
|
1791
|
+
},
|
|
1792
|
+
|
|
1793
|
+
applyStateAtIndex(index) {
|
|
1794
|
+
if (!currentTrace) return;
|
|
1795
|
+
|
|
1796
|
+
// Build execution state up to this point
|
|
1797
|
+
const eventsUpToNow = currentTrace.timeline.slice(0, index + 1);
|
|
1798
|
+
const activeChecks = new Set();
|
|
1799
|
+
const completedChecks = new Set();
|
|
1800
|
+
const failedChecks = new Set();
|
|
1801
|
+
|
|
1802
|
+
for (const event of eventsUpToNow) {
|
|
1803
|
+
if (event.type === 'check.started') {
|
|
1804
|
+
activeChecks.add(event.checkId);
|
|
1805
|
+
} else if (event.type === 'check.completed') {
|
|
1806
|
+
activeChecks.delete(event.checkId);
|
|
1807
|
+
completedChecks.add(event.checkId);
|
|
1808
|
+
} else if (event.type === 'check.failed') {
|
|
1809
|
+
activeChecks.delete(event.checkId);
|
|
1810
|
+
failedChecks.add(event.checkId);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// Update graph node colors to reflect state at this point
|
|
1815
|
+
d3.selectAll('.node').attr('class', d => {
|
|
1816
|
+
const checkId = d.checkId;
|
|
1817
|
+
let status = 'pending';
|
|
1818
|
+
|
|
1819
|
+
if (failedChecks.has(checkId)) {
|
|
1820
|
+
status = 'error';
|
|
1821
|
+
} else if (completedChecks.has(checkId)) {
|
|
1822
|
+
status = 'completed';
|
|
1823
|
+
} else if (activeChecks.has(checkId)) {
|
|
1824
|
+
status = 'running';
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
return `node status-${status}`;
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// Highlight current event's check
|
|
1831
|
+
const currentEvent = currentTrace.timeline[index];
|
|
1832
|
+
if (currentEvent && currentEvent.checkId) {
|
|
1833
|
+
d3.selectAll('.node')
|
|
1834
|
+
.filter(d => d.checkId === currentEvent.checkId)
|
|
1835
|
+
.classed('selected', true);
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
|
|
1839
|
+
togglePlay() {
|
|
1840
|
+
if (this.isPlaying) {
|
|
1841
|
+
this.pause();
|
|
1842
|
+
} else {
|
|
1843
|
+
this.play();
|
|
1844
|
+
}
|
|
1845
|
+
},
|
|
1846
|
+
|
|
1847
|
+
play() {
|
|
1848
|
+
if (!currentTrace || !currentTrace.timeline) return;
|
|
1849
|
+
|
|
1850
|
+
this.isPlaying = true;
|
|
1851
|
+
document.getElementById('btn-play').textContent = '⏸';
|
|
1852
|
+
document.getElementById('btn-play').classList.add('active');
|
|
1853
|
+
|
|
1854
|
+
const baseInterval = 100; // 100ms per event at 1x speed
|
|
1855
|
+
const interval = baseInterval / this.playbackSpeed;
|
|
1856
|
+
|
|
1857
|
+
this.playbackInterval = setInterval(() => {
|
|
1858
|
+
if (this.currentIndex >= currentTrace.timeline.length - 1) {
|
|
1859
|
+
this.pause();
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
this.seekToIndex(this.currentIndex + 1);
|
|
1863
|
+
}, interval);
|
|
1864
|
+
},
|
|
1865
|
+
|
|
1866
|
+
pause() {
|
|
1867
|
+
this.isPlaying = false;
|
|
1868
|
+
document.getElementById('btn-play').textContent = '▶';
|
|
1869
|
+
document.getElementById('btn-play').classList.remove('active');
|
|
1870
|
+
|
|
1871
|
+
if (this.playbackInterval) {
|
|
1872
|
+
clearInterval(this.playbackInterval);
|
|
1873
|
+
this.playbackInterval = null;
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
|
|
1877
|
+
stepForward() {
|
|
1878
|
+
if (!currentTrace) return;
|
|
1879
|
+
this.pause();
|
|
1880
|
+
this.seekToIndex(this.currentIndex + 1);
|
|
1881
|
+
},
|
|
1882
|
+
|
|
1883
|
+
stepBackward() {
|
|
1884
|
+
this.pause();
|
|
1885
|
+
this.seekToIndex(this.currentIndex - 1);
|
|
1886
|
+
},
|
|
1887
|
+
|
|
1888
|
+
seekToStart() {
|
|
1889
|
+
this.pause();
|
|
1890
|
+
this.seekToIndex(0);
|
|
1891
|
+
},
|
|
1892
|
+
|
|
1893
|
+
seekToEnd() {
|
|
1894
|
+
if (!currentTrace) return;
|
|
1895
|
+
this.pause();
|
|
1896
|
+
this.seekToIndex(currentTrace.timeline.length - 1);
|
|
1897
|
+
},
|
|
1898
|
+
|
|
1899
|
+
setSpeed(speed) {
|
|
1900
|
+
this.playbackSpeed = speed;
|
|
1901
|
+
|
|
1902
|
+
// Update active speed button
|
|
1903
|
+
document.querySelectorAll('.speed-btn').forEach(btn => {
|
|
1904
|
+
btn.classList.remove('active');
|
|
1905
|
+
if (btn.textContent === `${speed}×`) {
|
|
1906
|
+
btn.classList.add('active');
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
// Restart playback if playing
|
|
1911
|
+
if (this.isPlaying) {
|
|
1912
|
+
this.pause();
|
|
1913
|
+
this.play();
|
|
1914
|
+
}
|
|
1915
|
+
},
|
|
1916
|
+
|
|
1917
|
+
jumpToSnapshot(snapshot) {
|
|
1918
|
+
if (!currentTrace) return;
|
|
1919
|
+
|
|
1920
|
+
// Find the timeline event for this snapshot
|
|
1921
|
+
const index = currentTrace.timeline.findIndex(
|
|
1922
|
+
evt =>
|
|
1923
|
+
evt.type === 'state.snapshot' &&
|
|
1924
|
+
evt.checkId === snapshot.checkId &&
|
|
1925
|
+
evt.timestamp === snapshot.timestamp
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
if (index >= 0) {
|
|
1929
|
+
this.seekToIndex(index);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Update snapshot panel active state
|
|
1933
|
+
const items = document.querySelectorAll('.snapshot-item');
|
|
1934
|
+
items.forEach((item, i) => {
|
|
1935
|
+
item.classList.toggle('active', currentTrace.snapshots[i] === snapshot);
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
// Show diff if there's a previous snapshot
|
|
1939
|
+
if (previousSnapshot) {
|
|
1940
|
+
this.showDiff(previousSnapshot, snapshot);
|
|
1941
|
+
}
|
|
1942
|
+
previousSnapshot = snapshot;
|
|
1943
|
+
},
|
|
1944
|
+
|
|
1945
|
+
showDiff(prev, current) {
|
|
1946
|
+
const diffHtml = this.computeDiff(prev.outputs, current.outputs);
|
|
1947
|
+
document.getElementById('tab-diff').innerHTML = diffHtml;
|
|
1948
|
+
},
|
|
1949
|
+
|
|
1950
|
+
computeDiff(prevOutputs, currentOutputs) {
|
|
1951
|
+
const allKeys = new Set([
|
|
1952
|
+
...Object.keys(prevOutputs || {}),
|
|
1953
|
+
...Object.keys(currentOutputs || {}),
|
|
1954
|
+
]);
|
|
1955
|
+
|
|
1956
|
+
if (allKeys.size === 0) {
|
|
1957
|
+
return '<p style="color: #858585;">No outputs to compare</p>';
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const changes = [];
|
|
1961
|
+
|
|
1962
|
+
for (const key of allKeys) {
|
|
1963
|
+
const prevValue = prevOutputs?.[key];
|
|
1964
|
+
const currentValue = currentOutputs?.[key];
|
|
1965
|
+
|
|
1966
|
+
if (prevValue === undefined && currentValue !== undefined) {
|
|
1967
|
+
changes.push({
|
|
1968
|
+
type: 'added',
|
|
1969
|
+
key,
|
|
1970
|
+
value: currentValue,
|
|
1971
|
+
});
|
|
1972
|
+
} else if (prevValue !== undefined && currentValue === undefined) {
|
|
1973
|
+
changes.push({
|
|
1974
|
+
type: 'removed',
|
|
1975
|
+
key,
|
|
1976
|
+
value: prevValue,
|
|
1977
|
+
});
|
|
1978
|
+
} else if (JSON.stringify(prevValue) !== JSON.stringify(currentValue)) {
|
|
1979
|
+
changes.push({
|
|
1980
|
+
type: 'modified',
|
|
1981
|
+
key,
|
|
1982
|
+
prevValue,
|
|
1983
|
+
currentValue,
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (changes.length === 0) {
|
|
1989
|
+
return '<p style="color: #858585;">No changes detected</p>';
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
let html = '<div class="diff-viewer">';
|
|
1993
|
+
for (const change of changes) {
|
|
1994
|
+
if (change.type === 'added') {
|
|
1995
|
+
html += `<div class="diff-added">+ ${change.key}: ${JSON.stringify(change.value, null, 2)}</div>`;
|
|
1996
|
+
} else if (change.type === 'removed') {
|
|
1997
|
+
html += `<div class="diff-removed">- ${change.key}: ${JSON.stringify(change.value, null, 2)}</div>`;
|
|
1998
|
+
} else if (change.type === 'modified') {
|
|
1999
|
+
html += `<div class="diff-modified">~ ${change.key}:</div>`;
|
|
2000
|
+
html += `<div class="diff-removed"> - ${JSON.stringify(change.prevValue, null, 2)}</div>`;
|
|
2001
|
+
html += `<div class="diff-added"> + ${JSON.stringify(change.currentValue, null, 2)}</div>`;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
html += '</div>';
|
|
2005
|
+
return html;
|
|
2006
|
+
},
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
function toggleSnapshotPanel() {
|
|
2010
|
+
const panel = document.getElementById('snapshot-panel');
|
|
2011
|
+
panel.classList.toggle('hidden');
|
|
2012
|
+
document.getElementById('btn-snapshots').classList.toggle('active');
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// Keyboard shortcuts
|
|
2016
|
+
document.addEventListener('keydown', e => {
|
|
2017
|
+
if (!currentTrace || !currentTrace.timeline) return;
|
|
2018
|
+
|
|
2019
|
+
// Ignore if user is typing in an input
|
|
2020
|
+
if (e.target.tagName === 'INPUT') return;
|
|
2021
|
+
|
|
2022
|
+
switch (e.key) {
|
|
2023
|
+
case ' ': // Space - play/pause
|
|
2024
|
+
e.preventDefault();
|
|
2025
|
+
timeTravel.togglePlay();
|
|
2026
|
+
break;
|
|
2027
|
+
case 'ArrowLeft': // Left arrow - step backward
|
|
2028
|
+
e.preventDefault();
|
|
2029
|
+
timeTravel.stepBackward();
|
|
2030
|
+
break;
|
|
2031
|
+
case 'ArrowRight': // Right arrow - step forward
|
|
2032
|
+
e.preventDefault();
|
|
2033
|
+
timeTravel.stepForward();
|
|
2034
|
+
break;
|
|
2035
|
+
case 'Home': // Home - seek to start
|
|
2036
|
+
e.preventDefault();
|
|
2037
|
+
timeTravel.seekToStart();
|
|
2038
|
+
break;
|
|
2039
|
+
case 'End': // End - seek to end
|
|
2040
|
+
e.preventDefault();
|
|
2041
|
+
timeTravel.seekToEnd();
|
|
2042
|
+
break;
|
|
2043
|
+
case 's': // S - toggle snapshot panel
|
|
2044
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
2045
|
+
toggleSnapshotPanel();
|
|
2046
|
+
}
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
// ========================================================================
|
|
2052
|
+
// HTTP Polling Live Mode with Execution Control
|
|
2053
|
+
// ========================================================================
|
|
2054
|
+
const liveMode = {
|
|
2055
|
+
isLive: false,
|
|
2056
|
+
isRunning: false,
|
|
2057
|
+
isPaused: false,
|
|
2058
|
+
pollingInterval: null,
|
|
2059
|
+
lastSpanCount: 0,
|
|
2060
|
+
baseUrl: '',
|
|
2061
|
+
configEditor: null,
|
|
2062
|
+
currentConfig: null,
|
|
2063
|
+
|
|
2064
|
+
init(baseUrl) {
|
|
2065
|
+
console.log('[live] Initializing live mode with base URL:', baseUrl);
|
|
2066
|
+
this.isLive = true;
|
|
2067
|
+
this.baseUrl = baseUrl;
|
|
2068
|
+
|
|
2069
|
+
// Show live controls
|
|
2070
|
+
document.getElementById('live-controls').classList.remove('hidden');
|
|
2071
|
+
document.getElementById('file-info').textContent = 'Live Mode - Ready';
|
|
2072
|
+
|
|
2073
|
+
// Load initial config (will hide empty state once loaded)
|
|
2074
|
+
this.loadConfig();
|
|
2075
|
+
},
|
|
2076
|
+
|
|
2077
|
+
async loadConfig() {
|
|
2078
|
+
try {
|
|
2079
|
+
console.log('[live] Fetching config from:', `${this.baseUrl}/api/config`);
|
|
2080
|
+
const response = await fetch(`${this.baseUrl}/api/config`);
|
|
2081
|
+
const data = await response.json();
|
|
2082
|
+
console.log('[live] Loaded config:', data);
|
|
2083
|
+
console.log('[live] Config data exists?', !!data.config);
|
|
2084
|
+
console.log('[live] Config keys:', data.config ? Object.keys(data.config) : 'none');
|
|
2085
|
+
|
|
2086
|
+
if (data.config) {
|
|
2087
|
+
// Store config
|
|
2088
|
+
this.currentConfig = data.config;
|
|
2089
|
+
|
|
2090
|
+
// Initialize Monaco editor
|
|
2091
|
+
this.initializeEditor(data.config);
|
|
2092
|
+
|
|
2093
|
+
// Show config sidebar
|
|
2094
|
+
document.getElementById('config-sidebar').classList.remove('hidden');
|
|
2095
|
+
|
|
2096
|
+
console.log('[live] Config editor initialized successfully');
|
|
2097
|
+
} else {
|
|
2098
|
+
console.warn('[live] No config data received');
|
|
2099
|
+
}
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
console.error('[live] Failed to load config:', error);
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2104
|
+
|
|
2105
|
+
initializeEditor(config) {
|
|
2106
|
+
// Convert config to YAML
|
|
2107
|
+
const yamlText = this.objectToYAML(config, 0);
|
|
2108
|
+
|
|
2109
|
+
// Configure Monaco
|
|
2110
|
+
require.config({
|
|
2111
|
+
paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' },
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
require(['vs/editor/editor.main'], () => {
|
|
2115
|
+
// Create editor
|
|
2116
|
+
this.configEditor = monaco.editor.create(
|
|
2117
|
+
document.getElementById('config-editor-container'),
|
|
2118
|
+
{
|
|
2119
|
+
value: yamlText,
|
|
2120
|
+
language: 'yaml',
|
|
2121
|
+
theme: 'vs-dark',
|
|
2122
|
+
automaticLayout: true,
|
|
2123
|
+
fontSize: 13,
|
|
2124
|
+
lineNumbers: 'on',
|
|
2125
|
+
minimap: { enabled: false },
|
|
2126
|
+
scrollBeyondLastLine: false,
|
|
2127
|
+
wordWrap: 'off',
|
|
2128
|
+
wrappingStrategy: 'advanced',
|
|
2129
|
+
tabSize: 2,
|
|
2130
|
+
}
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
console.log('[live] Monaco editor created');
|
|
2134
|
+
|
|
2135
|
+
// Handle apply button
|
|
2136
|
+
document.getElementById('apply-config-btn').onclick = () => this.applyConfigChanges();
|
|
2137
|
+
});
|
|
2138
|
+
},
|
|
2139
|
+
|
|
2140
|
+
async applyConfigChanges() {
|
|
2141
|
+
try {
|
|
2142
|
+
const yamlText = this.configEditor.getValue();
|
|
2143
|
+
console.log('[live] Applying config changes...');
|
|
2144
|
+
console.log('[live] New YAML:', yamlText.substring(0, 200));
|
|
2145
|
+
|
|
2146
|
+
// Parse YAML to JSON (simple conversion - you might want a proper YAML parser)
|
|
2147
|
+
// For now, we'll send the YAML text and let the server handle it
|
|
2148
|
+
const response = await fetch(`${this.baseUrl}/api/config`, {
|
|
2149
|
+
method: 'POST',
|
|
2150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2151
|
+
body: JSON.stringify({ yaml: yamlText }),
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
if (response.ok) {
|
|
2155
|
+
console.log('[live] Config updated successfully');
|
|
2156
|
+
alert('Configuration updated! Changes will apply to the next execution.');
|
|
2157
|
+
} else {
|
|
2158
|
+
console.error('[live] Failed to update config');
|
|
2159
|
+
alert('Failed to update configuration. Check console for details.');
|
|
2160
|
+
}
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
console.error('[live] Error applying config:', error);
|
|
2163
|
+
alert('Error: ' + error.message);
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
|
|
2167
|
+
async start() {
|
|
2168
|
+
if (this.isRunning) return;
|
|
2169
|
+
|
|
2170
|
+
console.log('[live] Sending start signal to server...');
|
|
2171
|
+
|
|
2172
|
+
// Send start signal to server
|
|
2173
|
+
try {
|
|
2174
|
+
const response = await fetch(`${this.baseUrl}/api/start`, {
|
|
2175
|
+
method: 'POST',
|
|
2176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
if (!response.ok) {
|
|
2180
|
+
console.error('[live] Failed to send start signal:', response.statusText);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
console.log('[live] Start signal sent successfully');
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
console.error('[live] Error sending start signal:', error);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
this.isRunning = true;
|
|
2191
|
+
this.isPaused = false;
|
|
2192
|
+
console.log('[live] Starting execution - beginning HTTP polling');
|
|
2193
|
+
|
|
2194
|
+
// Hide empty state if still visible
|
|
2195
|
+
document.getElementById('empty-state').classList.add('hidden');
|
|
2196
|
+
// Keep config sidebar visible
|
|
2197
|
+
|
|
2198
|
+
// Update UI
|
|
2199
|
+
this.updateStatus('Running...');
|
|
2200
|
+
document.getElementById('btn-start-execution').classList.add('hidden');
|
|
2201
|
+
document.getElementById('btn-pause-execution').classList.remove('hidden');
|
|
2202
|
+
document.getElementById('btn-stop-execution').classList.remove('hidden');
|
|
2203
|
+
|
|
2204
|
+
// Start HTTP polling (every second)
|
|
2205
|
+
this.startPolling();
|
|
2206
|
+
},
|
|
2207
|
+
|
|
2208
|
+
startPolling() {
|
|
2209
|
+
console.log('[live] Starting HTTP polling (1 second interval)');
|
|
2210
|
+
|
|
2211
|
+
// Initial fetch
|
|
2212
|
+
this.pollSpans();
|
|
2213
|
+
|
|
2214
|
+
// Poll every second
|
|
2215
|
+
this.pollingInterval = setInterval(() => {
|
|
2216
|
+
if (!this.isPaused && this.isRunning) {
|
|
2217
|
+
this.pollSpans();
|
|
2218
|
+
}
|
|
2219
|
+
}, 1000);
|
|
2220
|
+
},
|
|
2221
|
+
|
|
2222
|
+
async pollSpans() {
|
|
2223
|
+
try {
|
|
2224
|
+
const response = await fetch(`${this.baseUrl}/api/spans`);
|
|
2225
|
+
const data = await response.json();
|
|
2226
|
+
|
|
2227
|
+
console.log(
|
|
2228
|
+
`[live] Polled ${data.total} total spans (last count: ${this.lastSpanCount})`
|
|
2229
|
+
);
|
|
2230
|
+
|
|
2231
|
+
// Only process if we have new spans
|
|
2232
|
+
if (data.total > this.lastSpanCount) {
|
|
2233
|
+
const newSpans = data.spans.slice(this.lastSpanCount);
|
|
2234
|
+
console.log(`[live] Processing ${newSpans.length} new spans`);
|
|
2235
|
+
|
|
2236
|
+
for (const span of newSpans) {
|
|
2237
|
+
this.handleLiveSpan(span);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
this.lastSpanCount = data.total;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// Also poll for results
|
|
2244
|
+
await this.pollResults();
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
console.error('[live] Failed to poll spans:', error);
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
|
|
2250
|
+
async pollResults() {
|
|
2251
|
+
try {
|
|
2252
|
+
const response = await fetch(`${this.baseUrl}/api/results`);
|
|
2253
|
+
const data = await response.json();
|
|
2254
|
+
|
|
2255
|
+
if (data.results) {
|
|
2256
|
+
console.log('[live] Results received:', data.results);
|
|
2257
|
+
this.displayResults(data.results);
|
|
2258
|
+
}
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
console.error('[live] Failed to poll results:', error);
|
|
2261
|
+
}
|
|
2262
|
+
},
|
|
2263
|
+
|
|
2264
|
+
displayResults(results) {
|
|
2265
|
+
const resultsTab = document.getElementById('tab-results');
|
|
2266
|
+
if (!resultsTab) return;
|
|
2267
|
+
|
|
2268
|
+
// Build HTML for results
|
|
2269
|
+
let html = '<div style="padding: 16px;">';
|
|
2270
|
+
html += '<h3 style="color: #dcdcaa; margin-bottom: 16px;">Execution Results</h3>';
|
|
2271
|
+
|
|
2272
|
+
// Display each check group
|
|
2273
|
+
for (const [checkName, checkResults] of Object.entries(results)) {
|
|
2274
|
+
html += `<div style="margin-bottom: 24px; border: 1px solid #3e3e42; border-radius: 4px; padding: 16px; background: #1e1e1e;">`;
|
|
2275
|
+
html += `<h4 style="color: #569cd6; margin-bottom: 12px;">📝 ${checkName}</h4>`;
|
|
2276
|
+
|
|
2277
|
+
if (Array.isArray(checkResults)) {
|
|
2278
|
+
for (const result of checkResults) {
|
|
2279
|
+
const statusColor =
|
|
2280
|
+
result.status === 'success'
|
|
2281
|
+
? '#4ec9b0'
|
|
2282
|
+
: result.status === 'error'
|
|
2283
|
+
? '#f48771'
|
|
2284
|
+
: '#dcdcaa';
|
|
2285
|
+
html += `<div style="margin-bottom: 12px; padding: 12px; background: #252526; border-left: 3px solid ${statusColor};">`;
|
|
2286
|
+
html += `<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">`;
|
|
2287
|
+
html += `<strong style="color: ${statusColor};">${result.status?.toUpperCase() || 'UNKNOWN'}</strong>`;
|
|
2288
|
+
if (result.duration) {
|
|
2289
|
+
html += `<span style="color: #858585;">${result.duration}ms</span>`;
|
|
2290
|
+
}
|
|
2291
|
+
html += `</div>`;
|
|
2292
|
+
|
|
2293
|
+
if (result.message) {
|
|
2294
|
+
html += `<div style="color: #cccccc; margin-bottom: 8px;">${this.escapeHtml(result.message)}</div>`;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (result.issues && result.issues.length > 0) {
|
|
2298
|
+
html += `<div style="margin-top: 12px;">`;
|
|
2299
|
+
html += `<strong style="color: #dcdcaa;">Issues (${result.issues.length}):</strong>`;
|
|
2300
|
+
html += `<ul style="margin: 8px 0; padding-left: 20px;">`;
|
|
2301
|
+
for (const issue of result.issues) {
|
|
2302
|
+
const severityColor =
|
|
2303
|
+
issue.severity === 'critical'
|
|
2304
|
+
? '#f48771'
|
|
2305
|
+
: issue.severity === 'error'
|
|
2306
|
+
? '#f48771'
|
|
2307
|
+
: issue.severity === 'warning'
|
|
2308
|
+
? '#dcdcaa'
|
|
2309
|
+
: '#858585';
|
|
2310
|
+
html += `<li style="color: ${severityColor}; margin-bottom: 4px;">`;
|
|
2311
|
+
html += `[${issue.severity?.toUpperCase() || 'INFO'}] ${this.escapeHtml(issue.title || issue.message || 'No message')}`;
|
|
2312
|
+
html += `</li>`;
|
|
2313
|
+
}
|
|
2314
|
+
html += `</ul></div>`;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
html += `</div>`;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
html += `</div>`;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
html += '</div>';
|
|
2325
|
+
resultsTab.innerHTML = html;
|
|
2326
|
+
|
|
2327
|
+
// Auto-switch to results tab after execution completes
|
|
2328
|
+
console.log('[live] Results displayed, total checks:', Object.keys(results).length);
|
|
2329
|
+
},
|
|
2330
|
+
|
|
2331
|
+
escapeHtml(text) {
|
|
2332
|
+
const div = document.createElement('div');
|
|
2333
|
+
div.textContent = text;
|
|
2334
|
+
return div.innerHTML;
|
|
2335
|
+
},
|
|
2336
|
+
|
|
2337
|
+
stopPolling() {
|
|
2338
|
+
if (this.pollingInterval) {
|
|
2339
|
+
console.log('[live] Stopping HTTP polling');
|
|
2340
|
+
clearInterval(this.pollingInterval);
|
|
2341
|
+
this.pollingInterval = null;
|
|
2342
|
+
}
|
|
2343
|
+
},
|
|
2344
|
+
|
|
2345
|
+
async pause() {
|
|
2346
|
+
if (!this.isRunning || this.isPaused) return;
|
|
2347
|
+
|
|
2348
|
+
this.isPaused = true;
|
|
2349
|
+
console.log('[live] Pausing execution');
|
|
2350
|
+
|
|
2351
|
+
try {
|
|
2352
|
+
await fetch(`${this.baseUrl}/api/pause`, { method: 'POST' });
|
|
2353
|
+
} catch (e) {
|
|
2354
|
+
console.warn('[live] pause endpoint failed', e);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// Update UI
|
|
2358
|
+
this.updateStatus('Paused');
|
|
2359
|
+
document.getElementById('btn-pause-execution').classList.add('hidden');
|
|
2360
|
+
document.getElementById('btn-resume-execution').classList.remove('hidden');
|
|
2361
|
+
},
|
|
2362
|
+
|
|
2363
|
+
async resume() {
|
|
2364
|
+
if (!this.isPaused) return;
|
|
2365
|
+
|
|
2366
|
+
this.isPaused = false;
|
|
2367
|
+
console.log('[live] Resuming execution');
|
|
2368
|
+
|
|
2369
|
+
try {
|
|
2370
|
+
await fetch(`${this.baseUrl}/api/resume`, { method: 'POST' });
|
|
2371
|
+
} catch (e) {
|
|
2372
|
+
console.warn('[live] resume endpoint failed', e);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// Update UI
|
|
2376
|
+
this.updateStatus('Running...');
|
|
2377
|
+
document.getElementById('btn-resume-execution').classList.add('hidden');
|
|
2378
|
+
document.getElementById('btn-pause-execution').classList.remove('hidden');
|
|
2379
|
+
},
|
|
2380
|
+
|
|
2381
|
+
async stop() {
|
|
2382
|
+
if (!this.isRunning) return;
|
|
2383
|
+
|
|
2384
|
+
this.isRunning = false;
|
|
2385
|
+
this.isPaused = false;
|
|
2386
|
+
console.log('[live] Stopping execution');
|
|
2387
|
+
|
|
2388
|
+
// Stop polling
|
|
2389
|
+
this.stopPolling();
|
|
2390
|
+
|
|
2391
|
+
try {
|
|
2392
|
+
await fetch(`${this.baseUrl}/api/stop`, { method: 'POST' });
|
|
2393
|
+
} catch (e) {
|
|
2394
|
+
console.warn('[live] stop endpoint failed', e);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Update UI
|
|
2398
|
+
this.updateStatus('Stopped');
|
|
2399
|
+
this.showResetButton();
|
|
2400
|
+
},
|
|
2401
|
+
|
|
2402
|
+
async reset() {
|
|
2403
|
+
console.log('[live] Resetting execution');
|
|
2404
|
+
|
|
2405
|
+
// Stop polling
|
|
2406
|
+
this.stopPolling();
|
|
2407
|
+
|
|
2408
|
+
// Clear trace
|
|
2409
|
+
currentTrace = null;
|
|
2410
|
+
this.isRunning = false;
|
|
2411
|
+
this.isPaused = false;
|
|
2412
|
+
this.lastSpanCount = 0;
|
|
2413
|
+
|
|
2414
|
+
try {
|
|
2415
|
+
await fetch(`${this.baseUrl}/api/reset`, { method: 'POST' });
|
|
2416
|
+
} catch (e) {
|
|
2417
|
+
console.warn('[live] reset endpoint failed', e);
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// Clear visualization
|
|
2421
|
+
const svg = d3.select('#graph-svg');
|
|
2422
|
+
svg.selectAll('*').remove();
|
|
2423
|
+
graphContainer = null; // Reset graph container for fresh start
|
|
2424
|
+
if (simulation) {
|
|
2425
|
+
simulation.stop();
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// Hide timeline
|
|
2429
|
+
document.getElementById('timeline-container').classList.add('hidden');
|
|
2430
|
+
|
|
2431
|
+
// Reset UI
|
|
2432
|
+
this.updateStatus('Ready - Click Start to begin execution');
|
|
2433
|
+
document.getElementById('btn-reset-execution').classList.add('hidden');
|
|
2434
|
+
document.getElementById('btn-stop-execution').classList.add('hidden');
|
|
2435
|
+
document.getElementById('btn-pause-execution').classList.add('hidden');
|
|
2436
|
+
document.getElementById('btn-resume-execution').classList.add('hidden');
|
|
2437
|
+
document.getElementById('btn-start-execution').classList.remove('hidden');
|
|
2438
|
+
},
|
|
2439
|
+
|
|
2440
|
+
displayConfig(config) {
|
|
2441
|
+
const configPanel = document.getElementById('tab-config');
|
|
2442
|
+
if (!config) {
|
|
2443
|
+
configPanel.innerHTML = '<p style="color: #858585;">No configuration available</p>';
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// Convert config object to readable YAML-like format
|
|
2448
|
+
let configText = '';
|
|
2449
|
+
|
|
2450
|
+
if (typeof config === 'string') {
|
|
2451
|
+
configText = config;
|
|
2452
|
+
} else {
|
|
2453
|
+
// Format as YAML-like text
|
|
2454
|
+
configText = this.objectToYAML(config, 0);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
configPanel.innerHTML = `
|
|
2458
|
+
<div class="json-viewer" style="padding: 16px;">
|
|
2459
|
+
<h3 style="margin-top: 0; color: #dcdcaa;">Loaded Configuration</h3>
|
|
2460
|
+
<pre style="background: #1e1e1e; padding: 12px; border-radius: 4px; overflow-x: auto;">${configText}</pre>
|
|
2461
|
+
</div>
|
|
2462
|
+
`;
|
|
2463
|
+
},
|
|
2464
|
+
|
|
2465
|
+
objectToYAML(obj, indent = 0) {
|
|
2466
|
+
const spaces = ' '.repeat(indent);
|
|
2467
|
+
let yaml = '';
|
|
2468
|
+
|
|
2469
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2470
|
+
if (value === null || value === undefined) {
|
|
2471
|
+
yaml += `${spaces}${key}: null\n`;
|
|
2472
|
+
} else if (Array.isArray(value)) {
|
|
2473
|
+
yaml += `${spaces}${key}:\n`;
|
|
2474
|
+
value.forEach(item => {
|
|
2475
|
+
if (typeof item === 'object' && item !== null) {
|
|
2476
|
+
yaml += `${spaces} -\n`;
|
|
2477
|
+
yaml += this.objectToYAML(item, indent + 2)
|
|
2478
|
+
.split('\n')
|
|
2479
|
+
.map(line => (line ? `${spaces} ${line}` : ''))
|
|
2480
|
+
.join('\n');
|
|
2481
|
+
} else {
|
|
2482
|
+
yaml += `${spaces} - ${item}\n`;
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
} else if (typeof value === 'object') {
|
|
2486
|
+
yaml += `${spaces}${key}:\n`;
|
|
2487
|
+
yaml += this.objectToYAML(value, indent + 1);
|
|
2488
|
+
} else if (typeof value === 'string') {
|
|
2489
|
+
// Quote strings if they contain special characters
|
|
2490
|
+
const needsQuotes =
|
|
2491
|
+
value.includes(':') || value.includes('#') || value.includes('\n');
|
|
2492
|
+
yaml += `${spaces}${key}: ${needsQuotes ? `"${value}"` : value}\n`;
|
|
2493
|
+
} else {
|
|
2494
|
+
yaml += `${spaces}${key}: ${value}\n`;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
return yaml;
|
|
2499
|
+
},
|
|
2500
|
+
|
|
2501
|
+
handleLiveSpan(span) {
|
|
2502
|
+
// Add span to current trace
|
|
2503
|
+
if (!currentTrace) {
|
|
2504
|
+
currentTrace = {
|
|
2505
|
+
runId: 'live',
|
|
2506
|
+
traceId: span.traceId,
|
|
2507
|
+
spans: [],
|
|
2508
|
+
tree: null,
|
|
2509
|
+
timeline: [],
|
|
2510
|
+
snapshots: [],
|
|
2511
|
+
metadata: {
|
|
2512
|
+
startTime: new Date().toISOString(),
|
|
2513
|
+
endTime: new Date().toISOString(),
|
|
2514
|
+
duration: 0,
|
|
2515
|
+
totalSpans: 0,
|
|
2516
|
+
totalSnapshots: 0,
|
|
2517
|
+
},
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
currentTrace.spans.push(span);
|
|
2522
|
+
|
|
2523
|
+
// Rebuild tree incrementally
|
|
2524
|
+
currentTrace.tree = buildExecutionTree(currentTrace.spans);
|
|
2525
|
+
|
|
2526
|
+
// Re-visualize with updated data
|
|
2527
|
+
visualizeTrace(currentTrace);
|
|
2528
|
+
|
|
2529
|
+
// Update metadata
|
|
2530
|
+
currentTrace.metadata.totalSpans = currentTrace.spans.length;
|
|
2531
|
+
currentTrace.metadata.endTime = new Date().toISOString();
|
|
2532
|
+
|
|
2533
|
+
this.updateStatus(`Running... (${currentTrace.spans.length} spans)`);
|
|
2534
|
+
},
|
|
2535
|
+
|
|
2536
|
+
handleStateUpdate(data) {
|
|
2537
|
+
// Update node state in real-time
|
|
2538
|
+
console.log('[live] State update for', data.checkId, data.state);
|
|
2539
|
+
},
|
|
2540
|
+
|
|
2541
|
+
updateStatus(text) {
|
|
2542
|
+
document.getElementById('live-status').textContent = text;
|
|
2543
|
+
document.getElementById('file-info').textContent = `Live Mode - ${text}`;
|
|
2544
|
+
},
|
|
2545
|
+
|
|
2546
|
+
showResetButton() {
|
|
2547
|
+
document.getElementById('btn-pause-execution').classList.add('hidden');
|
|
2548
|
+
document.getElementById('btn-resume-execution').classList.add('hidden');
|
|
2549
|
+
document.getElementById('btn-stop-execution').classList.add('hidden');
|
|
2550
|
+
document.getElementById('btn-reset-execution').classList.remove('hidden');
|
|
2551
|
+
},
|
|
2552
|
+
};
|
|
2553
|
+
|
|
2554
|
+
// Check if debug server URL is injected (live mode)
|
|
2555
|
+
console.log(
|
|
2556
|
+
'[init] Checking for DEBUG_SERVER_URL:',
|
|
2557
|
+
typeof window.DEBUG_SERVER_URL,
|
|
2558
|
+
window.DEBUG_SERVER_URL
|
|
2559
|
+
);
|
|
2560
|
+
if (typeof window.DEBUG_SERVER_URL !== 'undefined') {
|
|
2561
|
+
console.log('[init] DEBUG_SERVER_URL found, initializing live mode');
|
|
2562
|
+
liveMode.init(window.DEBUG_SERVER_URL);
|
|
2563
|
+
} else {
|
|
2564
|
+
console.log('[init] No DEBUG_SERVER_URL found, not in live mode');
|
|
2565
|
+
}
|
|
2566
|
+
</script>
|
|
2567
|
+
</body>
|
|
2568
|
+
</html>
|