@lovenyberg/ove 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,973 @@
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>Ove — Trace Viewer</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ :root {
11
+ --bg: #1a1a1a;
12
+ --bg-panel: #161616;
13
+ --bg-item: #1e1e1e;
14
+ --bg-item-hover: #252525;
15
+ --bg-item-active: #2a2a2a;
16
+ --border: #2a2a2a;
17
+ --border-light: #333;
18
+ --text: #e0e0e0;
19
+ --text-dim: #777;
20
+ --text-muted: #555;
21
+ --accent: #8ab4f8;
22
+
23
+ --green: #4ade80;
24
+ --green-dim: #16361f;
25
+ --blue: #60a5fa;
26
+ --blue-dim: #152540;
27
+ --gray: #888;
28
+ --gray-dim: #222;
29
+ --white: #e0e0e0;
30
+ --red: #f28b82;
31
+ --red-dim: #3b1a1a;
32
+ --amber: #fbbf24;
33
+ --amber-dim: #3b2e0a;
34
+ --cyan: #22d3ee;
35
+ --cyan-dim: #0a2e33;
36
+ }
37
+
38
+ body {
39
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
40
+ background: var(--bg);
41
+ color: var(--text);
42
+ height: 100vh;
43
+ display: flex;
44
+ flex-direction: column;
45
+ overflow: hidden;
46
+ }
47
+
48
+ header {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ padding: 0.5rem 1rem;
53
+ border-bottom: 1px solid var(--border);
54
+ background: var(--bg-panel);
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ .header-left {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 1rem;
62
+ }
63
+
64
+ .header-left h1 {
65
+ font-size: 0.85rem;
66
+ font-weight: 600;
67
+ letter-spacing: 0.05em;
68
+ color: var(--text-dim);
69
+ }
70
+
71
+ .header-left h1 span { color: var(--text); }
72
+
73
+ .nav-link {
74
+ color: var(--text-dim);
75
+ text-decoration: none;
76
+ font-size: 0.75rem;
77
+ padding: 0.2rem 0.5rem;
78
+ border: 1px solid var(--border);
79
+ border-radius: 3px;
80
+ transition: all 0.15s;
81
+ }
82
+ .nav-link:hover { color: var(--text); border-color: var(--border-light); }
83
+
84
+ .header-right {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.75rem;
88
+ }
89
+
90
+ .toggle-wrap {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 0.4rem;
94
+ font-size: 0.7rem;
95
+ color: var(--text-dim);
96
+ cursor: pointer;
97
+ user-select: none;
98
+ }
99
+
100
+ .toggle-wrap input { display: none; }
101
+
102
+ .toggle-track {
103
+ width: 28px;
104
+ height: 14px;
105
+ background: var(--border);
106
+ border-radius: 7px;
107
+ position: relative;
108
+ transition: background 0.2s;
109
+ }
110
+
111
+ .toggle-track::after {
112
+ content: '';
113
+ position: absolute;
114
+ top: 2px;
115
+ left: 2px;
116
+ width: 10px;
117
+ height: 10px;
118
+ background: var(--text-dim);
119
+ border-radius: 50%;
120
+ transition: all 0.2s;
121
+ }
122
+
123
+ .toggle-wrap input:checked + .toggle-track {
124
+ background: var(--green-dim);
125
+ }
126
+
127
+ .toggle-wrap input:checked + .toggle-track::after {
128
+ left: 16px;
129
+ background: var(--green);
130
+ }
131
+
132
+ .filter-select {
133
+ background: var(--bg-item);
134
+ border: 1px solid var(--border);
135
+ color: var(--text);
136
+ font-family: inherit;
137
+ font-size: 0.7rem;
138
+ padding: 0.2rem 0.4rem;
139
+ border-radius: 3px;
140
+ cursor: pointer;
141
+ }
142
+ .filter-select:focus { outline: 1px solid var(--accent); }
143
+
144
+ .layout {
145
+ display: flex;
146
+ flex: 1;
147
+ overflow: hidden;
148
+ }
149
+
150
+ /* ── Left panel: task list ── */
151
+ .task-panel {
152
+ width: 340px;
153
+ min-width: 260px;
154
+ border-right: 1px solid var(--border);
155
+ display: flex;
156
+ flex-direction: column;
157
+ background: var(--bg-panel);
158
+ flex-shrink: 0;
159
+ }
160
+
161
+ .task-panel-header {
162
+ padding: 0.5rem 0.75rem;
163
+ border-bottom: 1px solid var(--border);
164
+ font-size: 0.65rem;
165
+ color: var(--text-muted);
166
+ text-transform: uppercase;
167
+ letter-spacing: 0.1em;
168
+ display: flex;
169
+ justify-content: space-between;
170
+ align-items: center;
171
+ }
172
+
173
+ .task-count {
174
+ color: var(--text-dim);
175
+ font-variant-numeric: tabular-nums;
176
+ }
177
+
178
+ .task-list {
179
+ flex: 1;
180
+ overflow-y: auto;
181
+ scrollbar-width: thin;
182
+ scrollbar-color: var(--border) transparent;
183
+ }
184
+
185
+ .task-item {
186
+ padding: 0.6rem 0.75rem;
187
+ border-bottom: 1px solid var(--border);
188
+ cursor: pointer;
189
+ transition: background 0.1s;
190
+ position: relative;
191
+ }
192
+
193
+ .task-item:hover { background: var(--bg-item-hover); }
194
+ .task-item.active { background: var(--bg-item-active); }
195
+
196
+ .task-item.active::before {
197
+ content: '';
198
+ position: absolute;
199
+ left: 0;
200
+ top: 0;
201
+ bottom: 0;
202
+ width: 2px;
203
+ background: var(--accent);
204
+ }
205
+
206
+ .task-row-top {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
+ margin-bottom: 0.3rem;
211
+ }
212
+
213
+ .badge {
214
+ font-size: 0.6rem;
215
+ padding: 0.1rem 0.35rem;
216
+ border-radius: 2px;
217
+ font-weight: 600;
218
+ letter-spacing: 0.03em;
219
+ text-transform: uppercase;
220
+ flex-shrink: 0;
221
+ }
222
+
223
+ .badge-pending { background: var(--amber-dim); color: var(--amber); }
224
+ .badge-running { background: var(--cyan-dim); color: var(--cyan); animation: pulse 2s infinite; }
225
+ .badge-completed { background: var(--green-dim); color: var(--green); }
226
+ .badge-failed { background: var(--red-dim); color: var(--red); }
227
+
228
+ @keyframes pulse {
229
+ 0%, 100% { opacity: 1; }
230
+ 50% { opacity: 0.6; }
231
+ }
232
+
233
+ .task-repo {
234
+ font-size: 0.7rem;
235
+ color: var(--accent);
236
+ white-space: nowrap;
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ }
240
+
241
+ .task-time {
242
+ font-size: 0.6rem;
243
+ color: var(--text-muted);
244
+ margin-left: auto;
245
+ flex-shrink: 0;
246
+ font-variant-numeric: tabular-nums;
247
+ }
248
+
249
+ .task-prompt {
250
+ font-size: 0.7rem;
251
+ color: var(--text-dim);
252
+ white-space: nowrap;
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ line-height: 1.4;
256
+ }
257
+
258
+ /* ── Right panel ── */
259
+ .trace-panel {
260
+ flex: 1;
261
+ display: flex;
262
+ flex-direction: column;
263
+ overflow: hidden;
264
+ }
265
+
266
+ .trace-header {
267
+ padding: 0.5rem 1rem;
268
+ border-bottom: 1px solid var(--border);
269
+ font-size: 0.65rem;
270
+ color: var(--text-muted);
271
+ text-transform: uppercase;
272
+ letter-spacing: 0.1em;
273
+ display: flex;
274
+ justify-content: space-between;
275
+ align-items: center;
276
+ flex-shrink: 0;
277
+ }
278
+
279
+ .trace-task-id {
280
+ color: var(--text-dim);
281
+ font-size: 0.65rem;
282
+ font-variant-numeric: tabular-nums;
283
+ }
284
+
285
+ .copy-btn {
286
+ background: var(--bg-item);
287
+ border: 1px solid var(--border);
288
+ color: var(--text-dim);
289
+ font-family: inherit;
290
+ font-size: 0.6rem;
291
+ padding: 0.15rem 0.5rem;
292
+ border-radius: 3px;
293
+ cursor: pointer;
294
+ transition: all 0.15s;
295
+ margin-left: 0.5rem;
296
+ }
297
+ .copy-btn:hover { color: var(--text); border-color: var(--border-light); }
298
+ .copy-btn.copied { color: var(--green); border-color: var(--green-dim); }
299
+
300
+ /* ── Task context card ── */
301
+ .task-context {
302
+ padding: 0.75rem 1rem;
303
+ border-bottom: 1px solid var(--border);
304
+ background: var(--bg-panel);
305
+ flex-shrink: 0;
306
+ }
307
+
308
+ .task-context-row {
309
+ display: flex;
310
+ gap: 1rem;
311
+ margin-bottom: 0.4rem;
312
+ align-items: baseline;
313
+ }
314
+
315
+ .task-context-row:last-child { margin-bottom: 0; }
316
+
317
+ .ctx-label {
318
+ font-size: 0.6rem;
319
+ color: var(--text-muted);
320
+ text-transform: uppercase;
321
+ letter-spacing: 0.05em;
322
+ width: 3.5rem;
323
+ flex-shrink: 0;
324
+ }
325
+
326
+ .ctx-value {
327
+ font-size: 0.7rem;
328
+ color: var(--text);
329
+ line-height: 1.4;
330
+ min-width: 0;
331
+ }
332
+
333
+ .ctx-value.dim { color: var(--text-dim); }
334
+
335
+ .ctx-prompt {
336
+ font-size: 0.7rem;
337
+ color: var(--text);
338
+ line-height: 1.5;
339
+ white-space: pre-wrap;
340
+ word-break: break-word;
341
+ max-height: 6rem;
342
+ overflow-y: auto;
343
+ scrollbar-width: thin;
344
+ scrollbar-color: var(--border) transparent;
345
+ }
346
+
347
+ .ctx-result {
348
+ font-size: 0.65rem;
349
+ color: var(--text-dim);
350
+ line-height: 1.4;
351
+ white-space: pre-wrap;
352
+ word-break: break-word;
353
+ max-height: 4rem;
354
+ overflow-y: auto;
355
+ scrollbar-width: thin;
356
+ scrollbar-color: var(--border) transparent;
357
+ }
358
+
359
+ .ctx-meta {
360
+ display: flex;
361
+ gap: 1.5rem;
362
+ flex-wrap: wrap;
363
+ }
364
+
365
+ .ctx-meta-item {
366
+ font-size: 0.6rem;
367
+ color: var(--text-muted);
368
+ }
369
+
370
+ .ctx-meta-item span { color: var(--text-dim); }
371
+
372
+ /* ── Trace timeline ── */
373
+ #traceContent {
374
+ flex: 1;
375
+ min-height: 0;
376
+ display: flex;
377
+ flex-direction: column;
378
+ }
379
+
380
+ .trace-empty {
381
+ flex: 1;
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ color: var(--text-muted);
386
+ font-size: 0.8rem;
387
+ }
388
+
389
+ .trace-timeline {
390
+ flex: 1;
391
+ overflow-y: auto;
392
+ padding: 0.5rem 0;
393
+ scrollbar-width: thin;
394
+ scrollbar-color: var(--border) transparent;
395
+ }
396
+
397
+ .trace-event {
398
+ display: flex;
399
+ padding: 0.35rem 1rem;
400
+ gap: 0.75rem;
401
+ position: relative;
402
+ transition: background 0.1s;
403
+ }
404
+
405
+ .trace-event:hover { background: var(--bg-item-hover); }
406
+
407
+ .trace-event::before {
408
+ content: '';
409
+ position: absolute;
410
+ left: 2.05rem;
411
+ top: 0;
412
+ bottom: 0;
413
+ width: 1px;
414
+ background: var(--border);
415
+ }
416
+
417
+ .trace-event:first-child::before { top: 50%; }
418
+ .trace-event:last-child::before { bottom: 50%; }
419
+ .trace-event:only-child::before { display: none; }
420
+
421
+ .trace-dot-col {
422
+ width: 12px;
423
+ display: flex;
424
+ align-items: flex-start;
425
+ justify-content: center;
426
+ padding-top: 0.35rem;
427
+ flex-shrink: 0;
428
+ position: relative;
429
+ z-index: 1;
430
+ }
431
+
432
+ .trace-dot {
433
+ width: 7px;
434
+ height: 7px;
435
+ border-radius: 50%;
436
+ flex-shrink: 0;
437
+ }
438
+
439
+ .trace-dot.lifecycle { background: var(--green); box-shadow: 0 0 4px var(--green); }
440
+ .trace-dot.tool { background: var(--blue); box-shadow: 0 0 4px var(--blue); }
441
+ .trace-dot.status { background: var(--gray); }
442
+ .trace-dot.output { background: var(--white); }
443
+ .trace-dot.error { background: var(--red); box-shadow: 0 0 4px var(--red); }
444
+
445
+ .trace-ts {
446
+ font-size: 0.6rem;
447
+ color: var(--text-muted);
448
+ width: 5.5rem;
449
+ flex-shrink: 0;
450
+ padding-top: 0.15rem;
451
+ font-variant-numeric: tabular-nums;
452
+ }
453
+
454
+ .trace-body { flex: 1; min-width: 0; }
455
+
456
+ .trace-kind {
457
+ font-size: 0.55rem;
458
+ text-transform: uppercase;
459
+ letter-spacing: 0.08em;
460
+ margin-bottom: 0.15rem;
461
+ font-weight: 600;
462
+ }
463
+
464
+ .trace-kind.lifecycle { color: var(--green); }
465
+ .trace-kind.tool { color: var(--blue); }
466
+ .trace-kind.status { color: var(--gray); }
467
+ .trace-kind.output { color: var(--white); }
468
+ .trace-kind.error { color: var(--red); }
469
+
470
+ .trace-summary {
471
+ font-size: 0.75rem;
472
+ color: var(--text);
473
+ line-height: 1.4;
474
+ word-break: break-word;
475
+ }
476
+
477
+ .trace-detail {
478
+ font-size: 0.65rem;
479
+ color: var(--text-dim);
480
+ line-height: 1.4;
481
+ margin-top: 0.25rem;
482
+ white-space: pre-wrap;
483
+ word-break: break-word;
484
+ max-height: 0;
485
+ overflow: hidden;
486
+ transition: max-height 0.2s ease;
487
+ }
488
+
489
+ .trace-event.expanded .trace-detail { max-height: 40rem; }
490
+
491
+ .trace-detail-toggle {
492
+ font-size: 0.6rem;
493
+ color: var(--text-muted);
494
+ cursor: pointer;
495
+ margin-top: 0.2rem;
496
+ display: inline-block;
497
+ }
498
+ .trace-detail-toggle:hover { color: var(--text-dim); }
499
+
500
+ .loading {
501
+ display: flex;
502
+ align-items: center;
503
+ gap: 0.5rem;
504
+ padding: 1rem;
505
+ font-size: 0.75rem;
506
+ color: var(--text-muted);
507
+ }
508
+
509
+ .spinner {
510
+ width: 12px;
511
+ height: 12px;
512
+ border: 1.5px solid var(--border);
513
+ border-top-color: var(--text-dim);
514
+ border-radius: 50%;
515
+ animation: spin 0.8s linear infinite;
516
+ }
517
+
518
+ @keyframes spin { to { transform: rotate(360deg); } }
519
+
520
+ ::-webkit-scrollbar { width: 6px; }
521
+ ::-webkit-scrollbar-track { background: transparent; }
522
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
523
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
524
+ </style>
525
+ </head>
526
+ <body>
527
+ <header>
528
+ <div class="header-left">
529
+ <img src="/logo.png" style="width:24px;height:24px;border-radius:3px;object-fit:cover" alt="Ove">
530
+ <h1><span>ove</span> trace</h1>
531
+ <a href="/" class="nav-link">chat</a>
532
+ <a href="/status" class="nav-link">status</a>
533
+ </div>
534
+ <div class="header-right">
535
+ <select class="filter-select" id="repoFilter">
536
+ <option value="">all repos</option>
537
+ </select>
538
+ <select class="filter-select" id="userFilter">
539
+ <option value="">all users</option>
540
+ </select>
541
+ <select class="filter-select" id="statusFilter">
542
+ <option value="">all statuses</option>
543
+ <option value="running">running</option>
544
+ <option value="pending">pending</option>
545
+ <option value="completed">completed</option>
546
+ <option value="failed">failed</option>
547
+ </select>
548
+ <label class="toggle-wrap">
549
+ <input type="checkbox" id="autoRefresh" />
550
+ <div class="toggle-track"></div>
551
+ auto-refresh
552
+ </label>
553
+ </div>
554
+ </header>
555
+
556
+ <div class="layout">
557
+ <div class="task-panel">
558
+ <div class="task-panel-header">
559
+ <span>Tasks</span>
560
+ <span class="task-count" id="taskCount"></span>
561
+ </div>
562
+ <div class="task-list" id="taskList"></div>
563
+ </div>
564
+
565
+ <div class="trace-panel">
566
+ <div class="trace-header">
567
+ <span>Trace Timeline</span>
568
+ <span>
569
+ <span class="trace-task-id" id="traceTaskId"></span>
570
+ <button class="copy-btn" id="expandAllBtn" style="display:none">show all details</button>
571
+ <button class="copy-btn" id="copyTraceBtn" style="display:none">copy json</button>
572
+ </span>
573
+ </div>
574
+ <div id="taskContext"></div>
575
+ <div id="traceContent">
576
+ <div class="trace-empty">Select a task to view its trace</div>
577
+ </div>
578
+ </div>
579
+ </div>
580
+
581
+ <script>
582
+ var API_KEY = localStorage.getItem("ove-api-key") || prompt("API Key:");
583
+ if (API_KEY) localStorage.setItem("ove-api-key", API_KEY);
584
+
585
+ var taskListEl = document.getElementById("taskList");
586
+ var taskCountEl = document.getElementById("taskCount");
587
+ var taskContextEl = document.getElementById("taskContext");
588
+ var traceContentEl = document.getElementById("traceContent");
589
+ var traceTaskIdEl = document.getElementById("traceTaskId");
590
+ var repoFilterEl = document.getElementById("repoFilter");
591
+ var userFilterEl = document.getElementById("userFilter");
592
+ var statusFilterEl = document.getElementById("statusFilter");
593
+ var autoRefreshEl = document.getElementById("autoRefresh");
594
+ var copyTraceBtnEl = document.getElementById("copyTraceBtn");
595
+ var expandAllBtnEl = document.getElementById("expandAllBtn");
596
+
597
+ var selectedTaskId = null;
598
+ var allTasks = [];
599
+ var lastTraceEvents = null;
600
+ var refreshTimer = null;
601
+
602
+ function apiHeaders() {
603
+ return { "X-API-Key": API_KEY };
604
+ }
605
+
606
+ // ── URL state management ──
607
+ function readUrlState() {
608
+ var params = new URLSearchParams(window.location.search);
609
+ return {
610
+ task: params.get("task"),
611
+ repo: params.get("repo") || "",
612
+ user: params.get("user") || "",
613
+ status: params.get("status") || "",
614
+ };
615
+ }
616
+
617
+ function pushUrlState() {
618
+ var params = new URLSearchParams();
619
+ if (selectedTaskId) params.set("task", selectedTaskId);
620
+ if (repoFilterEl.value) params.set("repo", repoFilterEl.value);
621
+ if (userFilterEl.value) params.set("user", userFilterEl.value);
622
+ if (statusFilterEl.value) params.set("status", statusFilterEl.value);
623
+ var qs = params.toString();
624
+ var url = window.location.pathname + (qs ? "?" + qs : "");
625
+ if (url !== window.location.pathname + window.location.search) {
626
+ history.replaceState(null, "", url);
627
+ }
628
+ }
629
+
630
+ function fmtTime(iso) {
631
+ if (!iso) return "";
632
+ var d = new Date(iso);
633
+ var diffMins = Math.floor((Date.now() - d.getTime()) / 60000);
634
+ if (diffMins < 1) return "just now";
635
+ if (diffMins < 60) return diffMins + "m ago";
636
+ var diffHrs = Math.floor(diffMins / 60);
637
+ if (diffHrs < 24) return diffHrs + "h ago";
638
+ return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
639
+ }
640
+
641
+ function fmtTimeFull(iso) {
642
+ if (!iso) return "";
643
+ var d = new Date(iso);
644
+ return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
645
+ }
646
+
647
+ function fmtTraceTime(iso) {
648
+ if (!iso) return "";
649
+ var d = new Date(iso);
650
+ return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false })
651
+ + "." + String(d.getMilliseconds()).padStart(3, "0");
652
+ }
653
+
654
+ function truncate(s, n) {
655
+ if (!s) return "";
656
+ return s.length > n ? s.slice(0, n) + "..." : s;
657
+ }
658
+
659
+ function el(tag, cls, textContent) {
660
+ var e = document.createElement(tag);
661
+ if (cls) e.className = cls;
662
+ if (textContent != null) e.textContent = textContent;
663
+ return e;
664
+ }
665
+
666
+ function clear(node) {
667
+ while (node.firstChild) node.removeChild(node.firstChild);
668
+ }
669
+
670
+ function getFilteredTasks() {
671
+ var repo = repoFilterEl.value;
672
+ var user = userFilterEl.value;
673
+ return allTasks.filter(function (t) {
674
+ if (repo && t.repo !== repo) return false;
675
+ if (user && t.userId !== user) return false;
676
+ return true;
677
+ });
678
+ }
679
+
680
+ function populateFilterOptions() {
681
+ var repos = [];
682
+ var users = [];
683
+ var repoSet = {};
684
+ var userSet = {};
685
+ for (var i = 0; i < allTasks.length; i++) {
686
+ var t = allTasks[i];
687
+ if (!repoSet[t.repo]) { repoSet[t.repo] = true; repos.push(t.repo); }
688
+ if (t.userId && !userSet[t.userId]) { userSet[t.userId] = true; users.push(t.userId); }
689
+ }
690
+
691
+ var prevRepo = repoFilterEl.value;
692
+ var prevUser = userFilterEl.value;
693
+
694
+ clear(repoFilterEl);
695
+ repoFilterEl.appendChild(el("option", null, "all repos"));
696
+ repoFilterEl.firstChild.value = "";
697
+ for (var i = 0; i < repos.length; i++) {
698
+ var opt = el("option", null, repos[i]);
699
+ opt.value = repos[i];
700
+ repoFilterEl.appendChild(opt);
701
+ }
702
+ repoFilterEl.value = prevRepo;
703
+
704
+ clear(userFilterEl);
705
+ userFilterEl.appendChild(el("option", null, "all users"));
706
+ userFilterEl.firstChild.value = "";
707
+ for (var i = 0; i < users.length; i++) {
708
+ var opt = el("option", null, users[i]);
709
+ opt.value = users[i];
710
+ userFilterEl.appendChild(opt);
711
+ }
712
+ userFilterEl.value = prevUser;
713
+ }
714
+
715
+ async function fetchTasks() {
716
+ var status = statusFilterEl.value;
717
+ var params = new URLSearchParams({ key: API_KEY, limit: "100" });
718
+ if (status) params.set("status", status);
719
+ try {
720
+ var res = await fetch("/api/tasks?" + params, { headers: apiHeaders() });
721
+ if (!res.ok) return;
722
+ allTasks = await res.json();
723
+ populateFilterOptions();
724
+ renderTasks();
725
+ } catch (e) {
726
+ console.error("Failed to fetch tasks:", e);
727
+ }
728
+ }
729
+
730
+ function renderTasks() {
731
+ var tasks = getFilteredTasks();
732
+ taskCountEl.textContent = String(tasks.length);
733
+ var scrollTop = taskListEl.scrollTop;
734
+
735
+ clear(taskListEl);
736
+
737
+ if (tasks.length === 0) {
738
+ taskListEl.appendChild(el("div", "loading", "No tasks found"));
739
+ return;
740
+ }
741
+
742
+ for (var i = 0; i < tasks.length; i++) {
743
+ var task = tasks[i];
744
+ var item = el("div", "task-item" + (task.id === selectedTaskId ? " active" : ""));
745
+
746
+ var rowTop = el("div", "task-row-top");
747
+ rowTop.appendChild(el("span", "badge badge-" + task.status, task.status));
748
+ rowTop.appendChild(el("span", "task-repo", task.repo));
749
+ rowTop.appendChild(el("span", "task-time", fmtTime(task.createdAt)));
750
+ item.appendChild(rowTop);
751
+
752
+ item.appendChild(el("div", "task-prompt", truncate(task.prompt, 120)));
753
+
754
+ (function (id) {
755
+ item.addEventListener("click", function () { selectTask(id); });
756
+ })(task.id);
757
+ taskListEl.appendChild(item);
758
+ }
759
+
760
+ taskListEl.scrollTop = scrollTop;
761
+ }
762
+
763
+ function renderTaskContext(task) {
764
+ clear(taskContextEl);
765
+ if (!task) return;
766
+
767
+ var ctx = el("div", "task-context");
768
+
769
+ // Meta row: user, repo, status, timestamps
770
+ var meta = el("div", "ctx-meta");
771
+ var items = [
772
+ ["user", task.userId],
773
+ ["repo", task.repo],
774
+ ["status", task.status],
775
+ ["created", fmtTimeFull(task.createdAt)],
776
+ ];
777
+ if (task.completedAt) {
778
+ items.push(["done", fmtTimeFull(task.completedAt)]);
779
+ var ms = new Date(task.completedAt).getTime() - new Date(task.createdAt).getTime();
780
+ var secs = Math.floor(ms / 1000);
781
+ var duration;
782
+ if (secs < 60) { duration = secs + "s"; }
783
+ else if (secs < 3600) { duration = Math.floor(secs / 60) + "m " + (secs % 60) + "s"; }
784
+ else { duration = Math.floor(secs / 3600) + "h " + Math.floor((secs % 3600) / 60) + "m"; }
785
+ items.push(["duration", duration]);
786
+ }
787
+
788
+ for (var i = 0; i < items.length; i++) {
789
+ var mi = el("div", "ctx-meta-item");
790
+ mi.appendChild(document.createTextNode(items[i][0] + " "));
791
+ mi.appendChild(el("span", null, items[i][1]));
792
+ meta.appendChild(mi);
793
+ }
794
+ ctx.appendChild(meta);
795
+
796
+ // Prompt
797
+ var promptRow = el("div", "task-context-row");
798
+ promptRow.style.marginTop = "0.5rem";
799
+ promptRow.appendChild(el("div", "ctx-label", "prompt"));
800
+ promptRow.appendChild(el("div", "ctx-prompt", task.prompt));
801
+ ctx.appendChild(promptRow);
802
+
803
+ // Result (if available)
804
+ if (task.result) {
805
+ var resultRow = el("div", "task-context-row");
806
+ resultRow.appendChild(el("div", "ctx-label", "result"));
807
+ resultRow.appendChild(el("div", "ctx-result", task.result));
808
+ ctx.appendChild(resultRow);
809
+ }
810
+
811
+ taskContextEl.appendChild(ctx);
812
+ }
813
+
814
+ async function selectTask(taskId) {
815
+ selectedTaskId = taskId;
816
+ pushUrlState();
817
+ renderTasks();
818
+ traceTaskIdEl.textContent = taskId.slice(0, 8) + "...";
819
+
820
+ // Show task context
821
+ var task = allTasks.find(function (t) { return t.id === taskId; });
822
+ lastSelectedStatus = task ? task.status : null;
823
+ renderTaskContext(task || null);
824
+ lastTraceEvents = null;
825
+ copyTraceBtnEl.style.display = "none";
826
+ expandAllBtnEl.style.display = "none";
827
+
828
+ var loadingDiv = el("div", "loading");
829
+ loadingDiv.appendChild(el("div", "spinner"));
830
+ loadingDiv.appendChild(document.createTextNode("Loading trace..."));
831
+ clear(traceContentEl);
832
+ traceContentEl.appendChild(loadingDiv);
833
+
834
+ try {
835
+ var res = await fetch("/api/trace/" + encodeURIComponent(taskId) + "?key=" + encodeURIComponent(API_KEY), { headers: apiHeaders() });
836
+ if (!res.ok) {
837
+ clear(traceContentEl);
838
+ traceContentEl.appendChild(el("div", "trace-empty", "Failed to load trace"));
839
+ return;
840
+ }
841
+ var events = await res.json();
842
+ lastTraceEvents = { task: task || { id: taskId }, events: events };
843
+ var hasEvents = events.length > 0;
844
+ var hasDetails = hasEvents && events.some(function (e) { return !!e.detail; });
845
+ copyTraceBtnEl.style.display = hasEvents ? "inline-block" : "none";
846
+ expandAllBtnEl.style.display = hasDetails ? "inline-block" : "none";
847
+ expandAllBtnEl.textContent = "show all details";
848
+ renderTrace(events);
849
+ } catch (e) {
850
+ clear(traceContentEl);
851
+ traceContentEl.appendChild(el("div", "trace-empty", "Error: " + e.message));
852
+ }
853
+ }
854
+
855
+ function renderTrace(events) {
856
+ clear(traceContentEl);
857
+
858
+ if (events.length === 0) {
859
+ traceContentEl.appendChild(el("div", "trace-empty", "No trace events recorded for this task"));
860
+ return;
861
+ }
862
+
863
+ var timeline = el("div", "trace-timeline");
864
+
865
+ for (var i = 0; i < events.length; i++) {
866
+ var ev = events[i];
867
+ var row = el("div", "trace-event");
868
+
869
+ var dotCol = el("div", "trace-dot-col");
870
+ dotCol.appendChild(el("div", "trace-dot " + ev.kind));
871
+ row.appendChild(dotCol);
872
+
873
+ row.appendChild(el("div", "trace-ts", fmtTraceTime(ev.ts)));
874
+
875
+ var body = el("div", "trace-body");
876
+ body.appendChild(el("div", "trace-kind " + ev.kind, ev.kind));
877
+ body.appendChild(el("div", "trace-summary", ev.summary));
878
+
879
+ if (ev.detail) {
880
+ body.appendChild(el("div", "trace-detail", ev.detail));
881
+ (function (r) {
882
+ var toggle = el("span", "trace-detail-toggle", "show detail");
883
+ toggle.addEventListener("click", function () {
884
+ r.classList.toggle("expanded");
885
+ toggle.textContent = r.classList.contains("expanded") ? "hide detail" : "show detail";
886
+ });
887
+ body.appendChild(toggle);
888
+ })(row);
889
+ }
890
+
891
+ row.appendChild(body);
892
+ timeline.appendChild(row);
893
+ }
894
+
895
+ traceContentEl.appendChild(timeline);
896
+ }
897
+
898
+ // Auto-refresh
899
+ var lastSelectedStatus = null;
900
+ autoRefreshEl.addEventListener("change", function () {
901
+ if (autoRefreshEl.checked) {
902
+ refreshTimer = setInterval(async function () {
903
+ await fetchTasks();
904
+ if (selectedTaskId) {
905
+ var task = allTasks.find(function (t) { return t.id === selectedTaskId; });
906
+ if (task) {
907
+ var wasActive = lastSelectedStatus === "running" || lastSelectedStatus === "pending";
908
+ var justFinished = wasActive && (task.status === "completed" || task.status === "failed");
909
+ var stillActive = task.status === "running" || task.status === "pending";
910
+ if (stillActive || justFinished) {
911
+ await selectTask(selectedTaskId);
912
+ }
913
+ lastSelectedStatus = task.status;
914
+ }
915
+ }
916
+ }, 3000);
917
+ } else {
918
+ clearInterval(refreshTimer);
919
+ refreshTimer = null;
920
+ }
921
+ });
922
+
923
+ statusFilterEl.addEventListener("change", function () { fetchTasks(); pushUrlState(); });
924
+ repoFilterEl.addEventListener("change", function () { renderTasks(); pushUrlState(); });
925
+ userFilterEl.addEventListener("change", function () { renderTasks(); pushUrlState(); });
926
+
927
+ expandAllBtnEl.addEventListener("click", function () {
928
+ var timeline = traceContentEl.querySelector(".trace-timeline");
929
+ if (!timeline) return;
930
+ var events = timeline.querySelectorAll(".trace-event");
931
+ var allExpanded = expandAllBtnEl.textContent === "hide all details";
932
+ for (var i = 0; i < events.length; i++) {
933
+ var toggle = events[i].querySelector(".trace-detail-toggle");
934
+ if (!toggle) continue;
935
+ if (allExpanded) {
936
+ events[i].classList.remove("expanded");
937
+ toggle.textContent = "show detail";
938
+ } else {
939
+ events[i].classList.add("expanded");
940
+ toggle.textContent = "hide detail";
941
+ }
942
+ }
943
+ expandAllBtnEl.textContent = allExpanded ? "show all details" : "hide all details";
944
+ });
945
+
946
+ copyTraceBtnEl.addEventListener("click", function () {
947
+ if (!lastTraceEvents) return;
948
+ var json = JSON.stringify(lastTraceEvents, null, 2);
949
+ navigator.clipboard.writeText(json).then(function () {
950
+ copyTraceBtnEl.textContent = "copied!";
951
+ copyTraceBtnEl.classList.add("copied");
952
+ setTimeout(function () {
953
+ copyTraceBtnEl.textContent = "copy json";
954
+ copyTraceBtnEl.classList.remove("copied");
955
+ }, 1500);
956
+ });
957
+ });
958
+
959
+ // ── Init: restore state from URL ──
960
+ (async function init() {
961
+ var state = readUrlState();
962
+ statusFilterEl.value = state.status;
963
+ await fetchTasks();
964
+ if (state.repo) repoFilterEl.value = state.repo;
965
+ if (state.user) userFilterEl.value = state.user;
966
+ if (state.repo || state.user) renderTasks();
967
+ if (state.task) {
968
+ await selectTask(state.task);
969
+ }
970
+ })();
971
+ </script>
972
+ </body>
973
+ </html>