@ronkovic/aad 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +42 -4
  2. package/package.json +1 -1
  3. package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
  4. package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -69
  6. package/src/__tests__/e2e/resume-e2e.test.ts +7 -11
  7. package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
  8. package/src/__tests__/e2e/status-e2e.test.ts +227 -0
  9. package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
  10. package/src/__tests__/helpers/index.ts +6 -0
  11. package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
  12. package/src/__tests__/helpers/mock-logger.ts +36 -0
  13. package/src/__tests__/helpers/wait-helpers.ts +34 -0
  14. package/src/__tests__/integration/pipeline.test.ts +2 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +79 -0
  16. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +2 -0
  17. package/src/modules/cli/__tests__/cleanup.test.ts +1 -0
  18. package/src/modules/cli/__tests__/resume.test.ts +3 -0
  19. package/src/modules/cli/__tests__/run.test.ts +36 -0
  20. package/src/modules/cli/__tests__/status.test.ts +1 -0
  21. package/src/modules/cli/app.ts +2 -0
  22. package/src/modules/cli/commands/resume.ts +11 -6
  23. package/src/modules/cli/commands/run.ts +14 -2
  24. package/src/modules/dashboard/ui/dashboard.html +640 -474
  25. package/src/modules/planning/__tests__/planning-service.test.ts +2 -0
  26. package/src/modules/process-manager/__tests__/process-manager.test.ts +2 -0
  27. package/src/modules/process-manager/process-manager.ts +2 -1
  28. package/src/modules/task-execution/__tests__/executor.test.ts +420 -10
  29. package/src/modules/task-execution/executor.ts +76 -0
  30. package/src/modules/task-queue/dispatcher.ts +46 -2
  31. package/src/shared/__tests__/config.test.ts +30 -0
  32. package/src/shared/__tests__/events.test.ts +42 -16
  33. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  34. package/src/shared/config.ts +4 -0
  35. package/src/shared/events.ts +5 -0
  36. package/src/shared/memory-check.ts +2 -2
  37. package/src/shared/shutdown-handler.ts +12 -5
  38. package/src/shared/types.ts +12 -0
  39. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +0 -127
@@ -4,103 +4,483 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>AAD Dashboard</title>
7
- <style>
8
- *{margin:0;padding:0;box-sizing:border-box}
9
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#1a1a1a;color:#e0e0e0;padding:20px;line-height:1.4}
10
- header{background:#2d2d2d;padding:20px 24px;border-radius:10px;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
11
- header h1{font-size:22px;font-weight:700;color:#4fc3f7}
12
- header p{color:#9e9e9e;font-size:13px}
13
- .activity-cell{font-size:11px;font-weight:600}
14
- .activity-connected{color:#66bb6a}
15
- .activity-reconnecting{color:#ffa726}
16
- .activity-disconnected{color:#ef5350}
17
- .activity-phase-red{color:#ef5350}
18
- .activity-phase-green{color:#66bb6a}
19
- .activity-phase-verify{color:#42a5f5}
20
- .activity-phase-review{color:#ab47bc}
21
- .activity-phase-merge{color:#ffa726}
22
-
23
- /* Progress bar */
24
- .progress-section{background:#2d2d2d;border-radius:10px;padding:20px 24px;margin-bottom:20px}
25
- .progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
26
- .progress-header h2{font-size:16px;color:#4fc3f7}
27
- .progress-header .pct{font-size:28px;font-weight:700;color:#4fc3f7}
28
- .progress-bar-track{height:12px;background:#3a3a3a;border-radius:6px;overflow:hidden}
29
- .progress-bar-fill{height:100%;background:linear-gradient(90deg,#4fc3f7,#66bb6a);border-radius:6px;transition:width .6s ease;min-width:0}
30
- .progress-stats{display:flex;gap:24px;margin-top:14px;flex-wrap:wrap}
31
- .progress-stat{text-align:center}
32
- .progress-stat .val{font-size:20px;font-weight:700}
33
- .progress-stat .lbl{font-size:11px;color:#9e9e9e;text-transform:uppercase;letter-spacing:.5px}
34
- .stat-pending .val{color:#42a5f5}
35
- .stat-running .val{color:#ffa726}
36
- .stat-completed .val{color:#66bb6a}
37
- .stat-failed .val{color:#ef5350}
38
-
39
- /* Grid layouts */
40
- .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px}
41
- .grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-bottom:20px}
42
- @media(max-width:900px){.grid-2,.grid-3{grid-template-columns:1fr}}
43
- @media(max-width:600px){body{padding:10px}header{padding:14px}}
44
-
45
- .card{background:#2d2d2d;padding:20px;border-radius:10px}
46
- .card h2{font-size:15px;margin-bottom:14px;color:#4fc3f7;display:flex;align-items:center;justify-content:space-between}
47
- .card h2 .count{font-size:12px;color:#9e9e9e;font-weight:400}
48
-
49
- /* Workers */
50
- .worker-list{display:flex;flex-direction:column;gap:8px}
51
- .worker-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#3a3a3a;border-radius:6px}
52
- .worker-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
53
- .worker-dot.idle{background:#9e9e9e}
54
- .worker-dot.busy{background:#66bb6a;box-shadow:0 0 6px #66bb6a88}
55
- .worker-dot.stopped{background:#ef5350}
56
- .worker-name{font-size:13px;font-weight:600;flex:1}
57
- .worker-task{font-size:11px;color:#9e9e9e}
58
-
59
- /* Task table */
60
- .task-table-wrap{overflow-x:auto}
61
- table{width:100%;border-collapse:collapse;font-size:13px}
62
- th{text-align:left;padding:8px 10px;border-bottom:2px solid #3a3a3a;color:#9e9e9e;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
63
- td{padding:7px 10px;border-bottom:1px solid #333;vertical-align:middle}
64
- tr:hover td{background:#333}
65
- .badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;text-transform:uppercase}
66
- .badge-pending{background:#42a5f533;color:#42a5f5}
67
- .badge-running{background:#ffa72633;color:#ffa726}
68
- .badge-completed{background:#66bb6a33;color:#66bb6a}
69
- .badge-failed{background:#ef535033;color:#ef5350}
70
- .deps{font-size:11px;color:#9e9e9e}
71
- .priority{font-size:12px;color:#bbb}
72
-
73
- /* Graph */
74
- .graph-container{position:relative;overflow:auto;min-height:200px}
75
- .graph-container svg{display:block}
76
-
77
- /* Timeline */
78
- .timeline-container{overflow-x:auto;min-height:100px}
79
- .timeline-row{display:flex;align-items:center;margin-bottom:4px;height:26px}
80
- .timeline-label{width:100px;flex-shrink:0;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px;text-align:right;color:#bbb}
81
- .timeline-track{flex:1;position:relative;height:18px;background:#3a3a3a;border-radius:3px}
82
- .timeline-bar{position:absolute;height:100%;border-radius:3px;min-width:4px;transition:width .4s ease,left .4s ease}
83
-
84
- /* Logs */
85
- .log-section{background:#2d2d2d;border-radius:10px;padding:20px;margin-bottom:20px}
86
- .log-filters{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center}
87
- .log-filters label{font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer;padding:4px 8px;background:#3a3a3a;border-radius:4px}
88
- .log-filters label input{accent-color:#4fc3f7}
89
- .log-filters select{background:#3a3a3a;color:#e0e0e0;border:1px solid #555;border-radius:4px;padding:4px 8px;font-size:12px}
90
- .log-entries{max-height:350px;overflow-y:auto;font-family:"Courier New",monospace;font-size:12px}
91
- .log-entry{padding:4px 8px;margin-bottom:2px;border-radius:3px;display:flex;gap:8px}
92
- .log-entry .log-time{color:#777;flex-shrink:0}
93
- .log-entry .log-svc{color:#4fc3f7;flex-shrink:0;min-width:60px}
94
- .log-info{background:#1e3a5f44}
95
- .log-warn{background:#5f4b1e44}
96
- .log-error{background:#5f1e1e66}
97
- .log-hidden{display:none}
98
- .empty-msg{color:#666;font-size:13px;padding:20px;text-align:center}
99
- </style>
7
+ <style>/* Reset & Base */
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
16
+ background: #1a1a1a;
17
+ color: #e0e0e0;
18
+ padding: 20px;
19
+ line-height: 1.4;
20
+ }
21
+
22
+ /* Header */
23
+ header {
24
+ background: #2d2d2d;
25
+ padding: 20px 24px;
26
+ border-radius: 10px;
27
+ margin-bottom: 20px;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ flex-wrap: wrap;
32
+ gap: 10px;
33
+ }
34
+
35
+ header h1 {
36
+ font-size: 22px;
37
+ font-weight: 700;
38
+ color: #4fc3f7;
39
+ }
40
+
41
+ header p {
42
+ color: #9e9e9e;
43
+ font-size: 13px;
44
+ }
45
+
46
+ /* Activity Indicators */
47
+ .activity-cell {
48
+ font-size: 11px;
49
+ font-weight: 600;
50
+ }
51
+
52
+ .activity-connected {
53
+ color: #66bb6a;
54
+ }
55
+
56
+ .activity-reconnecting {
57
+ color: #ffa726;
58
+ }
59
+
60
+ .activity-disconnected {
61
+ color: #ef5350;
62
+ }
63
+
64
+ .activity-phase-red {
65
+ color: #ef5350;
66
+ }
67
+
68
+ .activity-phase-green {
69
+ color: #66bb6a;
70
+ }
71
+
72
+ .activity-phase-verify {
73
+ color: #42a5f5;
74
+ }
75
+
76
+ .activity-phase-review {
77
+ color: #ab47bc;
78
+ }
79
+
80
+ .activity-phase-merge {
81
+ color: #ffa726;
82
+ }
83
+
84
+ /* Progress Section */
85
+ .progress-section {
86
+ background: #2d2d2d;
87
+ border-radius: 10px;
88
+ padding: 20px 24px;
89
+ margin-bottom: 20px;
90
+ }
91
+
92
+ .progress-header {
93
+ display: flex;
94
+ justify-content: space-between;
95
+ align-items: center;
96
+ margin-bottom: 12px;
97
+ }
98
+
99
+ .progress-header h2 {
100
+ font-size: 16px;
101
+ color: #4fc3f7;
102
+ }
103
+
104
+ .progress-header .pct {
105
+ font-size: 28px;
106
+ font-weight: 700;
107
+ color: #4fc3f7;
108
+ }
109
+
110
+ .progress-bar-track {
111
+ height: 12px;
112
+ background: #3a3a3a;
113
+ border-radius: 6px;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .progress-bar-fill {
118
+ height: 100%;
119
+ background: linear-gradient(90deg, #4fc3f7, #66bb6a);
120
+ border-radius: 6px;
121
+ transition: width 0.6s ease;
122
+ min-width: 0;
123
+ }
124
+
125
+ .progress-stats {
126
+ display: flex;
127
+ gap: 24px;
128
+ margin-top: 14px;
129
+ flex-wrap: wrap;
130
+ }
131
+
132
+ .progress-stat {
133
+ text-align: center;
134
+ }
135
+
136
+ .progress-stat .val {
137
+ font-size: 20px;
138
+ font-weight: 700;
139
+ }
140
+
141
+ .progress-stat .lbl {
142
+ font-size: 11px;
143
+ color: #9e9e9e;
144
+ text-transform: uppercase;
145
+ letter-spacing: 0.5px;
146
+ }
147
+
148
+ .stat-pending .val {
149
+ color: #42a5f5;
150
+ }
151
+
152
+ .stat-running .val {
153
+ color: #ffa726;
154
+ }
155
+
156
+ .stat-completed .val {
157
+ color: #66bb6a;
158
+ }
159
+
160
+ .stat-failed .val {
161
+ color: #ef5350;
162
+ }
163
+
164
+ /* Grid Layouts */
165
+ .grid-2 {
166
+ display: grid;
167
+ grid-template-columns: 1fr 1fr;
168
+ gap: 20px;
169
+ margin-bottom: 20px;
170
+ }
171
+
172
+ .grid-3 {
173
+ display: grid;
174
+ grid-template-columns: 1fr 1fr 1fr;
175
+ gap: 20px;
176
+ margin-bottom: 20px;
177
+ }
178
+
179
+ @media (max-width: 900px) {
180
+ .grid-2,
181
+ .grid-3 {
182
+ grid-template-columns: 1fr;
183
+ }
184
+ }
185
+
186
+ @media (max-width: 600px) {
187
+ body {
188
+ padding: 10px;
189
+ }
190
+ header {
191
+ padding: 14px;
192
+ }
193
+ }
194
+
195
+ /* Card */
196
+ .card {
197
+ background: #2d2d2d;
198
+ padding: 20px;
199
+ border-radius: 10px;
200
+ }
201
+
202
+ .card h2 {
203
+ font-size: 15px;
204
+ margin-bottom: 14px;
205
+ color: #4fc3f7;
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: space-between;
209
+ }
210
+
211
+ .card h2 .count {
212
+ font-size: 12px;
213
+ color: #9e9e9e;
214
+ font-weight: 400;
215
+ }
216
+
217
+ /* Workers */
218
+ .worker-list {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 8px;
222
+ }
223
+
224
+ .worker-item {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 10px;
228
+ padding: 8px 12px;
229
+ background: #3a3a3a;
230
+ border-radius: 6px;
231
+ }
232
+
233
+ .worker-dot {
234
+ width: 10px;
235
+ height: 10px;
236
+ border-radius: 50%;
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .worker-dot.idle {
241
+ background: #9e9e9e;
242
+ }
243
+
244
+ .worker-dot.busy {
245
+ background: #66bb6a;
246
+ box-shadow: 0 0 6px #66bb6a88;
247
+ }
248
+
249
+ .worker-dot.stopped {
250
+ background: #ef5350;
251
+ }
252
+
253
+ .worker-name {
254
+ font-size: 13px;
255
+ font-weight: 600;
256
+ flex: 1;
257
+ }
258
+
259
+ .worker-task {
260
+ font-size: 11px;
261
+ color: #9e9e9e;
262
+ }
263
+
264
+ /* Task Table */
265
+ .task-table-wrap {
266
+ overflow-x: auto;
267
+ }
268
+
269
+ table {
270
+ width: 100%;
271
+ border-collapse: collapse;
272
+ font-size: 13px;
273
+ }
274
+
275
+ th {
276
+ text-align: left;
277
+ padding: 8px 10px;
278
+ border-bottom: 2px solid #3a3a3a;
279
+ color: #9e9e9e;
280
+ font-weight: 600;
281
+ font-size: 11px;
282
+ text-transform: uppercase;
283
+ letter-spacing: 0.5px;
284
+ white-space: nowrap;
285
+ }
286
+
287
+ td {
288
+ padding: 7px 10px;
289
+ border-bottom: 1px solid #333;
290
+ vertical-align: middle;
291
+ }
292
+
293
+ tr:hover td {
294
+ background: #333;
295
+ }
296
+
297
+ .badge {
298
+ display: inline-block;
299
+ padding: 2px 8px;
300
+ border-radius: 10px;
301
+ font-size: 11px;
302
+ font-weight: 600;
303
+ text-transform: uppercase;
304
+ }
305
+
306
+ .badge-pending {
307
+ background: #42a5f533;
308
+ color: #42a5f5;
309
+ }
310
+
311
+ .badge-running {
312
+ background: #ffa72633;
313
+ color: #ffa726;
314
+ }
315
+
316
+ .badge-completed {
317
+ background: #66bb6a33;
318
+ color: #66bb6a;
319
+ }
320
+
321
+ .badge-failed {
322
+ background: #ef535033;
323
+ color: #ef5350;
324
+ }
325
+
326
+ .deps {
327
+ font-size: 11px;
328
+ color: #9e9e9e;
329
+ }
330
+
331
+ .priority {
332
+ font-size: 12px;
333
+ color: #bbb;
334
+ }
335
+
336
+ /* Graph */
337
+ .graph-container {
338
+ position: relative;
339
+ overflow: auto;
340
+ min-height: 200px;
341
+ }
342
+
343
+ .graph-container svg {
344
+ display: block;
345
+ }
346
+
347
+ /* Timeline */
348
+ .timeline-container {
349
+ overflow-x: auto;
350
+ min-height: 100px;
351
+ }
352
+
353
+ .timeline-row {
354
+ display: flex;
355
+ align-items: center;
356
+ margin-bottom: 4px;
357
+ height: 26px;
358
+ }
359
+
360
+ .timeline-label {
361
+ width: 100px;
362
+ flex-shrink: 0;
363
+ font-size: 11px;
364
+ overflow: hidden;
365
+ text-overflow: ellipsis;
366
+ white-space: nowrap;
367
+ padding-right: 8px;
368
+ text-align: right;
369
+ color: #bbb;
370
+ }
371
+
372
+ .timeline-track {
373
+ flex: 1;
374
+ position: relative;
375
+ height: 18px;
376
+ background: #3a3a3a;
377
+ border-radius: 3px;
378
+ }
379
+
380
+ .timeline-bar {
381
+ position: absolute;
382
+ height: 100%;
383
+ border-radius: 3px;
384
+ min-width: 4px;
385
+ transition: width 0.4s ease, left 0.4s ease;
386
+ }
387
+
388
+ /* Logs */
389
+ .log-section {
390
+ background: #2d2d2d;
391
+ border-radius: 10px;
392
+ padding: 20px;
393
+ margin-bottom: 20px;
394
+ }
395
+
396
+ .log-filters {
397
+ display: flex;
398
+ gap: 8px;
399
+ margin-bottom: 12px;
400
+ flex-wrap: wrap;
401
+ align-items: center;
402
+ }
403
+
404
+ .log-filters label {
405
+ font-size: 12px;
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 4px;
409
+ cursor: pointer;
410
+ padding: 4px 8px;
411
+ background: #3a3a3a;
412
+ border-radius: 4px;
413
+ }
414
+
415
+ .log-filters label input {
416
+ accent-color: #4fc3f7;
417
+ }
418
+
419
+ .log-filters select {
420
+ background: #3a3a3a;
421
+ color: #e0e0e0;
422
+ border: 1px solid #555;
423
+ border-radius: 4px;
424
+ padding: 4px 8px;
425
+ font-size: 12px;
426
+ }
427
+
428
+ .log-entries {
429
+ max-height: 350px;
430
+ overflow-y: auto;
431
+ font-family: "Courier New", monospace;
432
+ font-size: 12px;
433
+ }
434
+
435
+ .log-entry {
436
+ padding: 4px 8px;
437
+ margin-bottom: 2px;
438
+ border-radius: 3px;
439
+ display: flex;
440
+ gap: 8px;
441
+ }
442
+
443
+ .log-entry .log-time {
444
+ color: #777;
445
+ flex-shrink: 0;
446
+ }
447
+
448
+ .log-entry .log-svc {
449
+ color: #4fc3f7;
450
+ flex-shrink: 0;
451
+ min-width: 60px;
452
+ }
453
+
454
+ .log-info {
455
+ background: #1e3a5f44;
456
+ }
457
+
458
+ .log-warn {
459
+ background: #5f4b1e44;
460
+ }
461
+
462
+ .log-error {
463
+ background: #5f1e1e66;
464
+ }
465
+
466
+ .log-hidden {
467
+ display: none;
468
+ }
469
+
470
+ .empty-msg {
471
+ color: #666;
472
+ font-size: 13px;
473
+ padding: 20px;
474
+ text-align: center;
475
+ }
476
+ </style>
100
477
  </head>
101
478
  <body>
102
479
  <header>
103
- <div><h1>AAD Dashboard</h1><p>Real-time Task Orchestrator Monitor</p></div>
480
+ <div>
481
+ <h1>AAD Dashboard</h1>
482
+ <p>Real-time Task Orchestrator Monitor</p>
483
+ </div>
104
484
  </header>
105
485
 
106
486
  <!-- Progress Bar -->
@@ -109,24 +489,42 @@
109
489
  <h2>Overall Progress</h2>
110
490
  <span class="pct" id="pct-display">0%</span>
111
491
  </div>
112
- <div class="progress-bar-track"><div class="progress-bar-fill" id="progress-fill" style="width:0%"></div></div>
492
+ <div class="progress-bar-track">
493
+ <div class="progress-bar-fill" id="progress-fill" style="width:0%"></div>
494
+ </div>
113
495
  <div class="progress-stats">
114
- <div class="progress-stat stat-pending"><div class="val" id="s-pending">0</div><div class="lbl">Pending</div></div>
115
- <div class="progress-stat stat-running"><div class="val" id="s-running">0</div><div class="lbl">Running</div></div>
116
- <div class="progress-stat stat-completed"><div class="val" id="s-completed">0</div><div class="lbl">Completed</div></div>
117
- <div class="progress-stat stat-failed"><div class="val" id="s-failed">0</div><div class="lbl">Failed</div></div>
496
+ <div class="progress-stat stat-pending">
497
+ <div class="val" id="s-pending">0</div>
498
+ <div class="lbl">Pending</div>
499
+ </div>
500
+ <div class="progress-stat stat-running">
501
+ <div class="val" id="s-running">0</div>
502
+ <div class="lbl">Running</div>
503
+ </div>
504
+ <div class="progress-stat stat-completed">
505
+ <div class="val" id="s-completed">0</div>
506
+ <div class="lbl">Completed</div>
507
+ </div>
508
+ <div class="progress-stat stat-failed">
509
+ <div class="val" id="s-failed">0</div>
510
+ <div class="lbl">Failed</div>
511
+ </div>
118
512
  </div>
119
513
  </div>
120
514
 
121
- <!-- Workers + Tasks -->
515
+ <!-- Workers + Graph -->
122
516
  <div class="grid-2">
123
517
  <div class="card">
124
518
  <h2>Workers <span class="count" id="workers-count"></span></h2>
125
- <div class="worker-list" id="worker-list"><div class="empty-msg">No workers</div></div>
519
+ <div class="worker-list" id="worker-list">
520
+ <div class="empty-msg">No workers</div>
521
+ </div>
126
522
  </div>
127
523
  <div class="card">
128
524
  <h2>Dependency Graph</h2>
129
- <div class="graph-container" id="graph-container"><div class="empty-msg">No graph data</div></div>
525
+ <div class="graph-container" id="graph-container">
526
+ <div class="empty-msg">No graph data</div>
527
+ </div>
130
528
  </div>
131
529
  </div>
132
530
 
@@ -135,8 +533,19 @@
135
533
  <h2>Tasks <span class="count" id="tasks-count"></span></h2>
136
534
  <div class="task-table-wrap">
137
535
  <table>
138
- <thead><tr><th>ID</th><th>Title</th><th>Status</th><th>Activity</th><th>Priority</th><th>Dependencies</th></tr></thead>
139
- <tbody id="task-tbody"><tr><td colspan="6" class="empty-msg">No tasks</td></tr></tbody>
536
+ <thead>
537
+ <tr>
538
+ <th>ID</th>
539
+ <th>Title</th>
540
+ <th>Status</th>
541
+ <th>Activity</th>
542
+ <th>Priority</th>
543
+ <th>Dependencies</th>
544
+ </tr>
545
+ </thead>
546
+ <tbody id="task-tbody">
547
+ <tr><td colspan="6" class="empty-msg">No tasks</td></tr>
548
+ </tbody>
140
549
  </table>
141
550
  </div>
142
551
  </div>
@@ -144,387 +553,144 @@
144
553
  <!-- Timeline -->
145
554
  <div class="card" style="margin-bottom:20px">
146
555
  <h2>Timeline</h2>
147
- <div class="timeline-container" id="timeline-container"><div class="empty-msg">No timeline data</div></div>
556
+ <div class="timeline-container" id="timeline-container">
557
+ <div class="empty-msg">No timeline data</div>
558
+ </div>
148
559
  </div>
149
560
 
150
561
  <!-- Logs -->
151
562
  <div class="log-section">
152
- <h2 style="font-size:15px;color:#4fc3f7;margin-bottom:10px">Logs <span class="count" id="log-count" style="font-size:12px;color:#9e9e9e;font-weight:400"></span></h2>
563
+ <h2 style="font-size:15px;color:#4fc3f7;margin-bottom:10px">
564
+ Logs <span class="count" id="log-count" style="font-size:12px;color:#9e9e9e;font-weight:400"></span>
565
+ </h2>
153
566
  <div class="log-filters">
154
567
  <label><input type="checkbox" checked data-level="info"> Info</label>
155
568
  <label><input type="checkbox" checked data-level="warn"> Warn</label>
156
569
  <label><input type="checkbox" checked data-level="error"> Error</label>
157
- <select id="svc-filter"><option value="">All Services</option></select>
158
- <input type="text" id="log-search" placeholder="Search logs..." style="background:#3a3a3a;color:#e0e0e0;border:1px solid #555;border-radius:4px;padding:4px 10px;font-size:12px;width:160px">
159
- <button id="filter-reset" style="background:#4fc3f7;color:#1a1a1a;border:none;border-radius:4px;padding:4px 10px;font-size:12px;font-weight:600;cursor:pointer;transition:background .2s">Reset</button>
570
+ <select id="svc-filter">
571
+ <option value="">All Services</option>
572
+ </select>
573
+ <input
574
+ type="text"
575
+ id="log-search"
576
+ placeholder="Search logs..."
577
+ style="background:#3a3a3a;color:#e0e0e0;border:1px solid #555;border-radius:4px;padding:4px 10px;font-size:12px;width:160px"
578
+ >
579
+ <button
580
+ id="filter-reset"
581
+ style="background:#4fc3f7;color:#1a1a1a;border:none;border-radius:4px;padding:4px 10px;font-size:12px;font-weight:600;cursor:pointer;transition:background .2s"
582
+ >
583
+ Reset
584
+ </button>
160
585
  </div>
161
586
  <div class="log-entries" id="log-entries"></div>
162
587
  </div>
163
588
 
164
- <script>
165
- // State
166
- let allTasks = [];
167
- let logEntries = [];
168
- let knownServices = new Set();
169
- let logFilters = { info: true, warn: true, error: true, service: '', search: '' };
170
- let taskPhases = {}; // { [taskId]: { phase, status } }
171
- let sseStatus = 'disconnected'; // Global SSE connection state
172
- let reconnectAttempts = 0;
173
-
174
- // --- Data fetching ---
175
- async function loadAll() {
176
- try {
177
- const [pRes, wRes, tRes, gRes, tlRes, lRes] = await Promise.all([
178
- fetch('/api/progress'), fetch('/api/workers'), fetch('/api/tasks'),
179
- fetch('/api/graph'), fetch('/api/timeline'), fetch('/api/logs')
180
- ]);
181
- const [progress, workers, tasks, graph, timeline, logs] = await Promise.all([
182
- pRes.json(), wRes.json(), tRes.json(), gRes.json(), tlRes.json(), lRes.json()
183
- ]);
184
- updateProgress(progress);
185
- renderWorkers(workers);
186
- allTasks = tasks.tasks || [];
187
- renderTasks();
188
- renderGraph(graph);
189
- renderTimeline(timeline);
190
- if (Array.isArray(logs)) logs.forEach(e => addLogEntry(e, true));
191
- else if (logs && logs.entries) logs.entries.forEach(e => addLogEntry(e, true));
192
- applyLogFilters();
193
- } catch (e) { console.error('Load failed:', e); }
194
- }
195
-
196
- // --- Progress ---
197
- function updateProgress(data) {
198
- const p = data.progress || data;
199
- const pct = data.percentComplete != null ? data.percentComplete : (p.total > 0 ? Math.round((p.completed / p.total) * 100) : 0);
200
- document.getElementById('pct-display').textContent = pct + '%';
201
- document.getElementById('progress-fill').style.width = pct + '%';
202
- document.getElementById('s-pending').textContent = p.pending || 0;
203
- document.getElementById('s-running').textContent = p.running || 0;
204
- document.getElementById('s-completed').textContent = p.completed || 0;
205
- document.getElementById('s-failed').textContent = p.failed || 0;
206
- }
207
-
208
- // --- Workers ---
209
- function renderWorkers(data) {
210
- const list = document.getElementById('worker-list');
211
- const workers = data.workers || [];
212
- const stats = data.stats || {};
213
- document.getElementById('workers-count').textContent = '(' + (stats.total || workers.length) + ')';
214
- if (!workers.length) { list.innerHTML = '<div class="empty-msg">No workers</div>'; return; }
215
- list.innerHTML = workers.map(w => {
216
- const taskInfo = w.currentTask ? '<span class="worker-task">Task: ' + esc(String(w.currentTask)) + '</span>' : '';
217
- return '<div class="worker-item"><span class="worker-dot ' + w.status + '"></span><span class="worker-name">' + esc(String(w.id)) + '</span>' + taskInfo + '<span class="badge badge-' + w.status + '" style="margin-left:auto">' + w.status + '</span></div>';
218
- }).join('');
219
- }
220
-
221
- // --- Tasks ---
222
- function renderTasks() {
223
- const tbody = document.getElementById('task-tbody');
224
- document.getElementById('tasks-count').textContent = '(' + allTasks.length + ')';
225
- if (!allTasks.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty-msg">No tasks</td></tr>'; return; }
226
- tbody.innerHTML = allTasks.map(t => {
227
- const deps = (t.dependsOn || []).map(d => esc(String(d))).join(', ') || '—';
228
- const activity = getActivityDisplay(t.taskId, t.status);
229
- return '<tr><td style="font-family:monospace;font-size:11px">' + esc(String(t.taskId)) + '</td><td>' + esc(t.title || '—') + '</td><td><span class="badge badge-' + t.status + '">' + t.status + '</span></td><td class="activity-cell">' + activity + '</td><td class="priority">' + (t.priority || 0) + '</td><td class="deps">' + deps + '</td></tr>';
230
- }).join('');
231
- }
232
-
233
- function getActivityDisplay(taskId, status) {
234
- if (status === 'running') {
235
- const phaseInfo = taskPhases[taskId];
236
- if (phaseInfo) {
237
- const phaseLabels = { red: 'Red (Tests)', green: 'Green (Impl)', verify: 'Verify', review: 'Review', merge: 'Merge' };
238
- const phaseLabel = phaseLabels[phaseInfo.phase] || phaseInfo.phase;
239
- if (phaseInfo.status === 'running') {
240
- return '<span class="activity-phase-' + phaseInfo.phase + '">' + esc(phaseLabel) + '</span>';
241
- } else if (phaseInfo.status === 'completed') {
242
- return '<span class="activity-phase-' + phaseInfo.phase + '">✓ ' + esc(phaseLabel) + '</span>';
243
- } else if (phaseInfo.status === 'failed') {
244
- return '<span class="activity-phase-' + phaseInfo.phase + '">✗ ' + esc(phaseLabel) + '</span>';
245
- }
246
- }
247
- return '<span class="activity-phase-green">Running</span>';
248
- } else {
249
- // Non-RUNNING: show SSE connection status
250
- if (sseStatus === 'connected') {
251
- return '<span class="activity-connected">Connected</span>';
252
- } else if (sseStatus === 'reconnecting') {
253
- return '<span class="activity-reconnecting">Reconnecting...</span>';
254
- } else {
255
- return '<span class="activity-disconnected">Disconnected</span>';
256
- }
257
- }
258
- }
259
-
260
- function updateActivityColumn(taskId) {
261
- renderTasks();
262
- }
263
-
264
- function updateAllActivityColumns() {
265
- renderTasks();
266
- }
267
-
268
- function updateTaskInList(taskId, updates) {
269
- const idx = allTasks.findIndex(t => t.taskId === taskId);
270
- if (idx >= 0) Object.assign(allTasks[idx], updates);
271
- renderTasks();
272
- }
273
-
274
- // --- Graph ---
275
- function renderGraph(data) {
276
- const container = document.getElementById('graph-container');
277
- const nodes = data.nodes || [];
278
- const edges = data.edges || [];
279
- if (!nodes.length) { container.innerHTML = '<div class="empty-msg">No graph data</div>'; return; }
280
-
281
- // Simple layered layout using topological sort
282
- const adj = {}, inDeg = {};
283
- nodes.forEach(n => { adj[n.id] = []; inDeg[n.id] = 0; });
284
- edges.forEach(e => { if (adj[e.from]) adj[e.from].push(e.to); inDeg[e.to] = (inDeg[e.to] || 0) + 1; });
285
-
286
- // BFS layers
287
- const layers = []; const visited = new Set();
288
- let queue = nodes.filter(n => (inDeg[n.id] || 0) === 0).map(n => n.id);
289
- while (queue.length) {
290
- layers.push([...queue]); const next = [];
291
- queue.forEach(id => { visited.add(id); (adj[id] || []).forEach(to => { inDeg[to]--; if (inDeg[to] <= 0 && !visited.has(to)) next.push(to); }); });
292
- queue = [...new Set(next)];
293
- }
294
- // Add unvisited nodes
295
- nodes.forEach(n => { if (!visited.has(n.id)) { if (!layers.length) layers.push([]); layers[layers.length - 1].push(n.id); } });
296
-
297
- const nodeMap = {}; nodes.forEach(n => nodeMap[n.id] = n);
298
- const colW = 140, rowH = 50, padX = 80, padY = 30;
299
- const maxPerLayer = Math.max(...layers.map(l => l.length), 1);
300
- const svgW = layers.length * colW + padX * 2;
301
- const svgH = maxPerLayer * rowH + padY * 2;
302
- const pos = {};
303
- layers.forEach((layer, li) => {
304
- const offset = (maxPerLayer - layer.length) * rowH / 2;
305
- layer.forEach((id, ni) => { pos[id] = { x: padX + li * colW, y: padY + offset + ni * rowH }; });
306
- });
307
-
308
- const statusColor = { pending: '#42a5f5', running: '#ffa726', completed: '#66bb6a', failed: '#ef5350' };
309
- let svg = '<svg width="' + svgW + '" height="' + svgH + '" xmlns="http://www.w3.org/2000/svg">';
310
- // Edges
311
- edges.forEach(e => {
312
- const from = pos[e.from], to = pos[e.to];
313
- if (from && to) svg += '<line x1="' + (from.x + 90) + '" y1="' + (from.y + 15) + '" x2="' + to.x + '" y2="' + (to.y + 15) + '" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>';
314
- });
315
- svg += '<defs><marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/></marker></defs>';
316
- // Nodes
317
- nodes.forEach(n => {
318
- const p = pos[n.id]; if (!p) return;
319
- const c = statusColor[n.status] || '#777';
320
- svg += '<rect x="' + p.x + '" y="' + p.y + '" width="90" height="30" rx="5" fill="' + c + '22" stroke="' + c + '" stroke-width="2"/>';
321
- const label = String(n.id).length > 12 ? String(n.id).slice(0, 11) + '…' : String(n.id);
322
- svg += '<text x="' + (p.x + 45) + '" y="' + (p.y + 19) + '" text-anchor="middle" fill="' + c + '" font-size="10" font-weight="600">' + esc(label) + '</text>';
323
- });
324
- svg += '</svg>';
325
- container.innerHTML = svg;
326
- }
327
-
328
- // --- Timeline ---
329
- function renderTimeline(data) {
330
- const container = document.getElementById('timeline-container');
331
- const tasks = (data.tasks || []).filter(t => t.startTime);
332
- if (!tasks.length) { container.innerHTML = '<div class="empty-msg">No timeline data</div>'; return; }
333
-
334
- const times = tasks.map(t => [new Date(t.startTime).getTime(), t.endTime ? new Date(t.endTime).getTime() : Date.now()]);
335
- const minT = Math.min(...times.map(t => t[0]));
336
- const maxT = Math.max(...times.map(t => t[1]));
337
- const range = maxT - minT || 1;
338
- const statusColor = { pending: '#42a5f5', running: '#ffa726', completed: '#66bb6a', failed: '#ef5350' };
339
-
340
- container.innerHTML = tasks.map((t, i) => {
341
- const s = new Date(t.startTime).getTime();
342
- const e = t.endTime ? new Date(t.endTime).getTime() : Date.now();
343
- const left = ((s - minT) / range * 100).toFixed(2);
344
- const width = (((e - s) / range) * 100).toFixed(2);
345
- const c = statusColor[t.status] || '#777';
346
- const dur = ((e - s) / 1000).toFixed(1) + 's';
347
- return '<div class="timeline-row"><span class="timeline-label" title="' + esc(String(t.id)) + '">' + esc(String(t.id)) + '</span><div class="timeline-track"><div class="timeline-bar" style="left:' + left + '%;width:' + width + '%;background:' + c + '" title="' + dur + '"></div></div></div>';
348
- }).join('');
349
- }
350
-
351
- // --- Logs ---
352
- function addLogEntry(entry, bulk) {
353
- if (!entry) return;
354
- const svc = entry.service || 'unknown';
355
- if (!knownServices.has(svc)) {
356
- knownServices.add(svc);
357
- const sel = document.getElementById('svc-filter');
358
- const opt = document.createElement('option'); opt.value = svc; opt.textContent = svc; sel.appendChild(opt);
359
- }
360
- logEntries.unshift(entry);
361
- if (logEntries.length > 200) logEntries.pop();
362
- if (!bulk) renderLogEntry(entry, true);
363
- }
364
-
365
- function renderLogEntry(entry, prepend) {
366
- const container = document.getElementById('log-entries');
367
- const div = document.createElement('div');
368
- const level = entry.level || 'info';
369
- div.className = 'log-entry log-' + level;
370
- div.dataset.level = level;
371
- div.dataset.service = entry.service || '';
372
- const time = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '';
373
- div.innerHTML = '<span class="log-time">' + time + '</span><span class="log-svc">' + esc(entry.service || '') + '</span><span>' + esc(entry.message || '') + '</span>';
374
- if (prepend) container.insertBefore(div, container.firstChild); else container.appendChild(div);
375
- applyLogFilterToEntry(div);
376
- }
377
-
378
- function renderAllLogs() {
379
- document.getElementById('log-entries').innerHTML = '';
380
- logEntries.forEach(e => renderLogEntry(e, false));
381
- updateLogCount();
382
- }
383
-
384
- function applyLogFilters() {
385
- renderAllLogs();
386
- }
387
-
388
- function applyLogFilterToEntry(div) {
389
- let show = logFilters[div.dataset.level] && (!logFilters.service || div.dataset.service === logFilters.service);
390
- if (show && logFilters.search) {
391
- const searchLower = logFilters.search.toLowerCase();
392
- const text = div.textContent.toLowerCase();
393
- show = text.includes(searchLower);
394
- }
395
- div.classList.toggle('log-hidden', !show);
396
- }
397
-
398
- function updateLogCount() {
399
- const total = logEntries.length;
400
- const visible = document.querySelectorAll('#log-entries .log-entry:not(.log-hidden)').length;
401
- document.getElementById('log-count').textContent = '(Showing ' + visible + '/' + total + ')';
402
- }
403
-
404
- // Load filters from localStorage
405
- function loadFiltersFromStorage() {
406
- try {
407
- const saved = localStorage.getItem('aad-log-filters');
408
- if (saved) {
409
- const parsed = JSON.parse(saved);
410
- logFilters = { ...logFilters, ...parsed };
411
- document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
412
- cb.checked = logFilters[cb.dataset.level] !== false;
413
- });
414
- const svcSelect = document.getElementById('svc-filter');
415
- if (svcSelect && logFilters.service) svcSelect.value = logFilters.service;
416
- const searchInput = document.getElementById('log-search');
417
- if (searchInput && logFilters.search) searchInput.value = logFilters.search;
418
- }
419
- } catch (e) { console.error('Failed to load filters:', e); }
420
- }
421
-
422
- // Save filters to localStorage
423
- function saveFiltersToStorage() {
424
- try {
425
- localStorage.setItem('aad-log-filters', JSON.stringify(logFilters));
426
- } catch (e) { console.error('Failed to save filters:', e); }
427
- }
428
-
429
- // Reset filters
430
- function resetFilters() {
431
- logFilters = { info: true, warn: true, error: true, service: '', search: '' };
432
- document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
433
- cb.checked = true;
434
- });
435
- document.getElementById('svc-filter').value = '';
436
- document.getElementById('log-search').value = '';
437
- saveFiltersToStorage();
438
- applyLogFilters();
439
- }
440
-
441
- // Filter event listeners
442
- document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
443
- cb.addEventListener('change', () => {
444
- logFilters[cb.dataset.level] = cb.checked;
445
- saveFiltersToStorage();
446
- applyLogFilters();
447
- });
448
- });
449
- document.getElementById('svc-filter').addEventListener('change', function() {
450
- logFilters.service = this.value;
451
- saveFiltersToStorage();
452
- applyLogFilters();
453
- });
454
- document.getElementById('log-search').addEventListener('input', function() {
455
- logFilters.search = this.value;
456
- saveFiltersToStorage();
457
- applyLogFilters();
458
- });
459
- document.getElementById('filter-reset').addEventListener('click', resetFilters);
460
-
461
- // --- SSE ---
462
- let evtSource;
463
- function connectSSE() {
464
- if (evtSource) { try { evtSource.close(); } catch(e){} }
465
- evtSource = new EventSource('/events/all');
466
-
467
- evtSource.addEventListener('open', () => {
468
- reconnectAttempts = 0;
469
- sseStatus = 'connected';
470
- updateAllActivityColumns();
471
- });
472
-
473
- evtSource.addEventListener('heartbeat', () => {
474
- sseStatus = 'connected';
475
- updateAllActivityColumns();
476
- });
477
-
478
- evtSource.addEventListener('message', (e) => {
479
- try {
480
- const ev = JSON.parse(e.data);
481
- switch (ev.type) {
482
- case 'log:entry': addLogEntry(ev.entry || ev); break;
483
- case 'progress:updated': updateProgress(ev.state ? { progress: ev.state, percentComplete: ev.percentComplete } : ev); break;
484
- case 'execution:phase:started':
485
- taskPhases[ev.taskId] = { phase: ev.phase, status: 'running' };
486
- updateActivityColumn(ev.taskId);
487
- break;
488
- case 'execution:phase:completed':
489
- taskPhases[ev.taskId] = { phase: ev.phase, status: 'completed' };
490
- updateActivityColumn(ev.taskId);
491
- break;
492
- case 'execution:phase:failed':
493
- taskPhases[ev.taskId] = { phase: ev.phase, status: 'failed' };
494
- updateActivityColumn(ev.taskId);
495
- break;
496
- case 'task:dispatched':
497
- case 'task:completed':
498
- case 'task:failed':
499
- if (ev.task) { updateTaskInList(ev.task.taskId, ev.task); }
500
- // Refresh progress
501
- fetch('/api/progress').then(r=>r.json()).then(updateProgress).catch(()=>{});
502
- fetch('/api/timeline').then(r=>r.json()).then(renderTimeline).catch(()=>{});
503
- break;
504
- case 'worker:idle':
505
- case 'worker:busy':
506
- fetch('/api/workers').then(r=>r.json()).then(renderWorkers).catch(()=>{});
507
- break;
508
- }
509
- } catch (err) { console.error('SSE parse error:', err); }
510
- });
511
-
512
- evtSource.addEventListener('error', () => {
513
- reconnectAttempts++;
514
- sseStatus = reconnectAttempts > 3 ? 'disconnected' : 'reconnecting';
515
- updateAllActivityColumns();
516
- evtSource.close();
517
- setTimeout(connectSSE, Math.min(reconnectAttempts * 3000, 9000));
518
- });
519
- }
520
-
521
- // --- Util ---
522
- function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
523
-
524
- // Init
525
- loadFiltersFromStorage();
526
- loadAll();
527
- connectSSE();
528
- </script>
589
+ <script type="module">class f{baseUrl;constructor(t){this.baseUrl=t}async getProgress(){let t=await fetch(`${this.baseUrl}/api/progress`);if(!t.ok)throw Error(`Failed to fetch progress: ${t.statusText}`);return t.json()}async getWorkers(){let t=await fetch(`${this.baseUrl}/api/workers`);if(!t.ok)throw Error(`Failed to fetch workers: ${t.statusText}`);return t.json()}async getTasks(){let t=await fetch(`${this.baseUrl}/api/tasks`);if(!t.ok)throw Error(`Failed to fetch tasks: ${t.statusText}`);return t.json()}async getGraph(){let t=await fetch(`${this.baseUrl}/api/graph`);if(!t.ok)throw Error(`Failed to fetch graph: ${t.statusText}`);return t.json()}async getTimeline(){let t=await fetch(`${this.baseUrl}/api/timeline`);if(!t.ok)throw Error(`Failed to fetch timeline: ${t.statusText}`);return t.json()}async getLogs(){let t=await fetch(`${this.baseUrl}/api/logs`);if(!t.ok)throw Error(`Failed to fetch logs: ${t.statusText}`);let i=await t.json();return Array.isArray(i)?i:i.entries||[]}async getTaskLogs(t){let i=await fetch(`${this.baseUrl}/api/tasks/${t}/logs`);if(!i.ok)throw Error(`Failed to fetch task logs: ${i.statusText}`);return i.json()}}class c{url;onEvent;eventSource=null;status="disconnected";reconnectAttempts=0;reconnectTimer=null;connectionChangeListeners=[];constructor(t,i){this.url=t;this.onEvent=i}connect(){if(this.eventSource)try{this.eventSource.close()}catch(t){}this.eventSource=new EventSource(this.url),this.eventSource.addEventListener("open",()=>{this.reconnectAttempts=0,this.setStatus("connected")}),this.eventSource.addEventListener("heartbeat",()=>{this.setStatus("connected")}),this.eventSource.addEventListener("message",(t)=>{try{let i=JSON.parse(t.data);this.onEvent(i)}catch(i){console.error("SSE parse error:",i)}}),this.eventSource.addEventListener("error",()=>{this.reconnectAttempts++;let t=this.reconnectAttempts>3?"disconnected":"reconnecting";if(this.setStatus(t),this.eventSource)this.eventSource.close();let i=Math.min(this.reconnectAttempts*3000,9000);this.reconnectTimer=setTimeout(()=>{this.connect()},i)})}disconnect(){if(this.reconnectTimer)clearTimeout(this.reconnectTimer),this.reconnectTimer=null;if(this.eventSource){try{this.eventSource.close()}catch(t){}this.eventSource=null}this.setStatus("disconnected")}isConnected(){return this.status==="connected"}getStatus(){return this.status}getReconnectAttempts(){return this.reconnectAttempts}onConnectionChange(t){this.connectionChangeListeners.push(t)}setStatus(t){if(this.status!==t)this.status=t,this.connectionChangeListeners.forEach((i)=>{try{i(t)}catch(n){console.error("Connection change listener error:",n)}})}}class x{state={progress:{pending:0,running:0,completed:0,failed:0,total:0},tasks:[],workers:[],graph:{nodes:[],edges:[]},timeline:{tasks:[]},logs:[],taskPhases:{}};listeners=[];getState(){return{...this.state}}updateProgress(t){this.state.progress=t,this.notify()}updateTasks(t){this.state.tasks=t,this.notify()}updateTask(t,i){let n=this.state.tasks.findIndex((e)=>e.taskId===t);if(n>=0)this.state.tasks[n]={...this.state.tasks[n],...i},this.notify()}updateWorkers(t){this.state.workers=t,this.notify()}updateGraph(t){this.state.graph=t,this.notify()}updateTimeline(t){this.state.timeline=t,this.notify()}addLog(t){if(this.state.logs.unshift(t),this.state.logs.length>200)this.state.logs=this.state.logs.slice(0,200);this.notify()}updateTaskPhase(t,i,n){this.state.taskPhases[t]={phase:i,status:n},this.notify()}subscribe(t){return this.listeners.push(t),()=>{let i=this.listeners.indexOf(t);if(i>=0)this.listeners.splice(i,1)}}notify(){let t=this.getState();this.listeners.forEach((i)=>{try{i(t)}catch(n){console.error("State listener error:",n)}})}}function v(t){let i=t.total||1,n=i>0?Math.round(t.completed/i*100):0,e=document.getElementById("pct-display"),o=document.getElementById("progress-fill"),d=document.getElementById("s-pending"),s=document.getElementById("s-running"),l=document.getElementById("s-completed"),p=document.getElementById("s-failed");if(e)e.textContent=`${n}%`;if(o)o.style.width=`${n}%`;if(d)d.textContent=String(t.pending||0);if(s)s.textContent=String(t.running||0);if(l)l.textContent=String(t.completed||0);if(p)p.textContent=String(t.failed||0)}function E(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function $(t){let i=document.getElementById("worker-list"),n=document.getElementById("workers-count");if(!i)return;if(n)n.textContent=`(${t.length})`;if(!t.length){i.innerHTML='<div class="empty-msg">No workers</div>';return}i.innerHTML=t.map((e)=>{let o=e.currentTask?`<span class="worker-task">Task: ${E(String(e.currentTask))}</span>`:"";return`
590
+ <div class="worker-item">
591
+ <span class="worker-dot ${e.status}"></span>
592
+ <span class="worker-name">${E(String(e.id))}</span>
593
+ ${o}
594
+ <span class="badge badge-${e.status}" style="margin-left:auto">${e.status}</span>
595
+ </div>
596
+ `}).join("")}function y(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class u{panelEl=null;overlayEl=null;constructor(){this.createPanel(),this.setupEventListeners()}createPanel(){this.overlayEl=document.createElement("div"),this.overlayEl.className="task-detail-overlay",this.overlayEl.style.cssText=`
597
+ display: none;
598
+ position: fixed;
599
+ top: 0;
600
+ left: 0;
601
+ right: 0;
602
+ bottom: 0;
603
+ background: rgba(0, 0, 0, 0.7);
604
+ z-index: 1000;
605
+ cursor: pointer;
606
+ `,this.panelEl=document.createElement("div"),this.panelEl.className="task-detail-panel",this.panelEl.style.cssText=`
607
+ display: none;
608
+ position: fixed;
609
+ top: 50%;
610
+ left: 50%;
611
+ transform: translate(-50%, -50%);
612
+ background: #2d2d2d;
613
+ border-radius: 10px;
614
+ padding: 24px;
615
+ max-width: 600px;
616
+ width: 90%;
617
+ max-height: 80vh;
618
+ overflow-y: auto;
619
+ z-index: 1001;
620
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
621
+ `,document.body.appendChild(this.overlayEl),document.body.appendChild(this.panelEl)}setupEventListeners(){this.overlayEl?.addEventListener("click",()=>this.close()),document.addEventListener("keydown",(t)=>{if(t.key==="Escape"&&this.isOpen())this.close()})}show(t,i){if(!this.panelEl||!this.overlayEl)return;let n={red:"Red (Tests)",green:"Green (Implementation)",verify:"Verify",review:"Review",merge:"Merge"},e=i?`<div class="phase-info">
622
+ <strong>Current Phase:</strong>
623
+ <span class="activity-phase-${i.phase}">
624
+ ${y(n[i.phase]||i.phase)}
625
+ (${i.status})
626
+ </span>
627
+ </div>`:"",o=t.startTime?new Date(t.startTime).toLocaleString():"—",d=t.endTime?new Date(t.endTime).toLocaleString():"—",s=this.calculateDuration(t);this.panelEl.innerHTML=`
628
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
629
+ <h2 style="color: #4fc3f7; margin: 0; font-size: 18px;">Task Details</h2>
630
+ <button class="close-btn" style="background: #ef5350; color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 14px; font-weight: 600;">Close</button>
631
+ </div>
632
+
633
+ <div style="display: flex; flex-direction: column; gap: 16px;">
634
+ <div>
635
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Task ID</div>
636
+ <div style="font-family: monospace; font-size: 13px; color: #e0e0e0;">${y(String(t.taskId))}</div>
637
+ </div>
638
+
639
+ <div>
640
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Title</div>
641
+ <div style="font-size: 15px; color: #e0e0e0; font-weight: 500;">${y(t.title||"—")}</div>
642
+ </div>
643
+
644
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
645
+ <div>
646
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Status</div>
647
+ <div><span class="badge badge-${t.status}">${t.status}</span></div>
648
+ </div>
649
+ <div>
650
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Priority</div>
651
+ <div style="font-size: 14px; color: #e0e0e0;">${t.priority||0}</div>
652
+ </div>
653
+ </div>
654
+
655
+ ${e}
656
+
657
+ <div>
658
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Dependencies</div>
659
+ <div style="font-size: 13px; color: #e0e0e0;">
660
+ ${t.dependsOn&&t.dependsOn.length>0?t.dependsOn.map((p)=>`<code style="background: #3a3a3a; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">${y(String(p))}</code>`).join(" "):"—"}
661
+ </div>
662
+ </div>
663
+
664
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
665
+ <div>
666
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Start Time</div>
667
+ <div style="font-size: 12px; color: #e0e0e0;">${o}</div>
668
+ </div>
669
+ <div>
670
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">End Time</div>
671
+ <div style="font-size: 12px; color: #e0e0e0;">${d}</div>
672
+ </div>
673
+ </div>
674
+
675
+ ${s?`<div>
676
+ <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Duration</div>
677
+ <div style="font-size: 13px; color: #66bb6a; font-weight: 600;">${s}</div>
678
+ </div>`:""}
679
+ </div>
680
+ `;let l=this.panelEl.querySelector(".close-btn");if(l)l.addEventListener("click",()=>this.close());this.overlayEl.style.display="block",this.panelEl.style.display="block"}close(){if(this.overlayEl)this.overlayEl.style.display="none";if(this.panelEl)this.panelEl.style.display="none"}isOpen(){return this.panelEl?.style.display==="block"}calculateDuration(t){if(!t.startTime)return null;let i=new Date(t.startTime).getTime(),e=(t.endTime?new Date(t.endTime).getTime():Date.now())-i,o=Math.floor(e/1000),d=Math.floor(o/60),s=Math.floor(d/60);if(s>0)return`${s}h ${d%60}m`;else if(d>0)return`${d}m ${o%60}s`;else return`${o}s`}}function m(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function L(t,i,n){if(t.status==="running"){let e=i[t.taskId];if(e){let d={red:"Red (Tests)",green:"Green (Impl)",verify:"Verify",review:"Review",merge:"Merge"}[e.phase]||e.phase,s=e.status==="completed"?"✓ ":e.status==="failed"?"✗ ":"";return`<span class="activity-phase-${e.phase}">${s}${m(d)}</span>`}return'<span class="activity-phase-green">Running</span>'}else if(n==="connected")return'<span class="activity-connected">Connected</span>';else if(n==="reconnecting")return'<span class="activity-reconnecting">Reconnecting...</span>';else return'<span class="activity-disconnected">Disconnected</span>'}var M=new u;function g(t,i,n){let e=document.getElementById("task-tbody"),o=document.getElementById("tasks-count");if(!e)return;if(o)o.textContent=`(${t.length})`;if(!t.length){e.innerHTML='<tr><td colspan="6" class="empty-msg">No tasks</td></tr>';return}e.innerHTML=t.map((d)=>{let s=(d.dependsOn||[]).map((p)=>m(String(p))).join(", ")||"—",l=L(d,i,n);return`
681
+ <tr data-task-id="${m(String(d.taskId))}" style="cursor: pointer;">
682
+ <td style="font-family:monospace;font-size:11px">${m(String(d.taskId))}</td>
683
+ <td>${m(d.title||"—")}</td>
684
+ <td><span class="badge badge-${d.status}">${d.status}</span></td>
685
+ <td class="activity-cell">${l}</td>
686
+ <td class="priority">${d.priority||0}</td>
687
+ <td class="deps">${s}</td>
688
+ </tr>
689
+ `}).join(""),e.querySelectorAll("tr[data-task-id]").forEach((d)=>{d.addEventListener("click",()=>{let s=d.dataset.taskId,l=t.find((p)=>String(p.taskId)===s);if(l){let p=i[l.taskId];M.show(l,p)}})})}function b(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class h{filters={info:!0,warn:!0,error:!0,service:"",search:""};autoScroll=!0;constructor(){this.setupEventListeners(),this.loadFiltersFromStorage()}setupEventListeners(){document.querySelectorAll(".log-filters input[data-level]").forEach((o)=>{o.addEventListener("change",()=>{let d=o,s=d.dataset.level;this.filters[s]=d.checked,this.saveFiltersToStorage()})});let t=document.getElementById("svc-filter");if(t)t.addEventListener("change",()=>{this.filters.service=t.value,this.saveFiltersToStorage()});let i=document.getElementById("log-search");if(i)i.addEventListener("input",()=>{this.filters.search=i.value,this.saveFiltersToStorage()});let n=document.getElementById("filter-reset");if(n)n.addEventListener("click",()=>{this.resetFilters()});let e=document.getElementById("log-entries");if(e)e.addEventListener("scroll",()=>{let o=e.scrollHeight-e.scrollTop<=e.clientHeight+10;this.autoScroll=o})}renderLog(t,i=!0){let n=document.getElementById("log-entries");if(!n)return;let e=document.createElement("div"),o=t.level||"info";e.className=`log-entry log-${o}`,e.dataset.level=o,e.dataset.service=t.service||"";let d=t.timestamp?new Date(t.timestamp).toLocaleTimeString():"";if(e.innerHTML=`
690
+ <span class="log-time">${d}</span>
691
+ <span class="log-svc">${b(t.service||"")}</span>
692
+ <span>${b(t.message||"")}</span>
693
+ `,i)n.insertBefore(e,n.firstChild);else n.appendChild(e);if(this.applyFilterToEntry(e),this.autoScroll&&i)n.scrollTop=0;this.updateLogCount()}renderAllLogs(t){let i=document.getElementById("log-entries");if(!i)return;i.innerHTML="",t.forEach((n)=>this.renderLog(n,!1)),this.updateLogCount(),this.updateServiceFilter(t)}applyFilterToEntry(t){let i=t.dataset.level,n=i?this.filters[i]:!1;if(n&&this.filters.service)n=t.dataset.service===this.filters.service;if(n&&this.filters.search){let e=this.filters.search.toLowerCase();n=(t.textContent?.toLowerCase()||"").includes(e)}t.classList.toggle("log-hidden",!n)}updateLogCount(){let t=document.getElementById("log-entries"),i=document.getElementById("log-count");if(!t||!i)return;let n=t.querySelectorAll(".log-entry").length,e=t.querySelectorAll(".log-entry:not(.log-hidden)").length;i.textContent=`(Showing ${e}/${n})`}updateServiceFilter(t){let i=document.getElementById("svc-filter");if(!i)return;let n=new Set(t.map((o)=>o.service).filter(Boolean)),e=i.value;if(i.innerHTML='<option value="">All Services</option>',n.forEach((o)=>{let d=document.createElement("option");d.value=o,d.textContent=o,i.appendChild(d)}),e)i.value=e}resetFilters(){this.filters={info:!0,warn:!0,error:!0,service:"",search:""},document.querySelectorAll(".log-filters input[data-level]").forEach((n)=>{n.checked=!0});let t=document.getElementById("svc-filter");if(t)t.value="";let i=document.getElementById("log-search");if(i)i.value="";this.saveFiltersToStorage(),this.reapplyFilters()}reapplyFilters(){let t=document.getElementById("log-entries");if(!t)return;t.querySelectorAll(".log-entry").forEach((i)=>{this.applyFilterToEntry(i)}),this.updateLogCount()}loadFiltersFromStorage(){try{let t=localStorage.getItem("aad-log-filters");if(t){let i=JSON.parse(t);this.filters={...this.filters,...i},document.querySelectorAll(".log-filters input[data-level]").forEach((o)=>{let d=o,s=d.dataset.level;d.checked=this.filters[s]!==!1});let n=document.getElementById("svc-filter");if(n&&this.filters.service)n.value=this.filters.service;let e=document.getElementById("log-search");if(e&&this.filters.search)e.value=this.filters.search}}catch(t){console.error("Failed to load filters:",t)}}saveFiltersToStorage(){try{localStorage.setItem("aad-log-filters",JSON.stringify(this.filters))}catch(t){console.error("Failed to save filters:",t)}this.reapplyFilters()}}var z="http://localhost:7333",r=new f(z),a=new x,T=new c(`${z}/events/all`,P),D=new h;a.subscribe((t)=>{v(t.progress),$(t.workers),g(t.tasks,t.taskPhases,T.getStatus())});function P(t){switch(t.type){case"log:entry":a.addLog(t.entry),D.renderLog(t.entry,!0);break;case"progress:updated":a.updateProgress(t.state);break;case"execution:phase:started":a.updateTaskPhase(t.taskId,t.phase,"running");break;case"execution:phase:completed":a.updateTaskPhase(t.taskId,t.phase,"completed");break;case"execution:phase:failed":a.updateTaskPhase(t.taskId,t.phase,"failed");break;case"task:dispatched":case"task:completed":case"task:failed":if(t.task)a.updateTask(t.task.taskId,t.task);O(),G();break;case"worker:idle":case"worker:busy":B();break;case"heartbeat":break}}async function S(){try{let[t,i,n,e,o,d]=await Promise.all([r.getProgress(),r.getWorkers(),r.getTasks(),r.getGraph(),r.getTimeline(),r.getLogs()]);a.updateProgress(t.progress),a.updateWorkers(i.workers),a.updateTasks(n.tasks),a.updateGraph(e),a.updateTimeline(o),d.forEach((s)=>a.addLog(s)),D.renderAllLogs(d)}catch(t){console.error("Failed to load initial data:",t)}}async function O(){try{let t=await r.getProgress();a.updateProgress(t.progress)}catch(t){console.error("Failed to refresh progress:",t)}}async function B(){try{let t=await r.getWorkers();a.updateWorkers(t.workers)}catch(t){console.error("Failed to refresh workers:",t)}}async function G(){try{let t=await r.getTimeline();a.updateTimeline(t)}catch(t){console.error("Failed to refresh timeline:",t)}}S();T.connect();T.onConnectionChange((t)=>{console.log("SSE connection status:",t),g(a.getState().tasks,a.getState().taskPhases,t)});export{a as store,T as sseClient,r as apiClient};
694
+ </script>
529
695
  </body>
530
696
  </html>