@ronkovic/aad 0.3.9 → 0.5.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.
Files changed (132) hide show
  1. package/README.md +332 -14
  2. package/package.json +6 -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 -68
  6. package/src/__tests__/e2e/resume-e2e.test.ts +9 -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 +3 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  16. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
  17. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
  18. package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
  19. package/src/modules/cli/__tests__/resume.test.ts +4 -0
  20. package/src/modules/cli/__tests__/run.test.ts +37 -0
  21. package/src/modules/cli/__tests__/status.test.ts +1 -0
  22. package/src/modules/cli/app.ts +2 -0
  23. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  24. package/src/modules/cli/commands/cleanup.ts +26 -11
  25. package/src/modules/cli/commands/resume.ts +14 -8
  26. package/src/modules/cli/commands/run.ts +70 -8
  27. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  28. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  29. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  30. package/src/modules/dashboard/routes/sse.ts +3 -2
  31. package/src/modules/dashboard/server.ts +1 -0
  32. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  33. package/src/modules/dashboard/ui/dashboard.html +640 -349
  34. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  35. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  36. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  37. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  38. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  39. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  40. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  41. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  42. package/src/modules/git-workspace/branch-manager.ts +24 -3
  43. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  44. package/src/modules/git-workspace/git-exec.ts +3 -2
  45. package/src/modules/git-workspace/index.ts +10 -1
  46. package/src/modules/git-workspace/merge-service.ts +36 -2
  47. package/src/modules/git-workspace/pr-manager.ts +278 -0
  48. package/src/modules/git-workspace/template-copy.ts +302 -0
  49. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  50. package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
  51. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  52. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  53. package/src/modules/planning/planning.service.ts +16 -2
  54. package/src/modules/planning/project-detection.ts +4 -1
  55. package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
  56. package/src/modules/process-manager/process-manager.ts +2 -1
  57. package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
  58. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  59. package/src/modules/task-execution/executor.ts +163 -4
  60. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  61. package/src/modules/task-execution/phases/merge.ts +44 -2
  62. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  63. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  64. package/src/modules/task-queue/dispatcher.ts +96 -3
  65. package/src/shared/__tests__/config.test.ts +30 -0
  66. package/src/shared/__tests__/events.test.ts +42 -16
  67. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  68. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  69. package/src/shared/config.ts +10 -0
  70. package/src/shared/events.ts +5 -0
  71. package/src/shared/memory-check.ts +2 -2
  72. package/src/shared/prerequisites.ts +190 -0
  73. package/src/shared/shutdown-handler.ts +12 -5
  74. package/src/shared/types.ts +25 -0
  75. package/templates/CLAUDE.md +122 -0
  76. package/templates/settings.json +117 -0
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -4,98 +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
- .conn-badge{font-size:11px;padding:4px 10px;border-radius:12px;font-weight:600}
14
- .conn-badge.connected{background:#66bb6a33;color:#66bb6a}
15
- .conn-badge.disconnected{background:#ef535033;color:#ef5350}
16
-
17
- /* Progress bar */
18
- .progress-section{background:#2d2d2d;border-radius:10px;padding:20px 24px;margin-bottom:20px}
19
- .progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
20
- .progress-header h2{font-size:16px;color:#4fc3f7}
21
- .progress-header .pct{font-size:28px;font-weight:700;color:#4fc3f7}
22
- .progress-bar-track{height:12px;background:#3a3a3a;border-radius:6px;overflow:hidden}
23
- .progress-bar-fill{height:100%;background:linear-gradient(90deg,#4fc3f7,#66bb6a);border-radius:6px;transition:width .6s ease;min-width:0}
24
- .progress-stats{display:flex;gap:24px;margin-top:14px;flex-wrap:wrap}
25
- .progress-stat{text-align:center}
26
- .progress-stat .val{font-size:20px;font-weight:700}
27
- .progress-stat .lbl{font-size:11px;color:#9e9e9e;text-transform:uppercase;letter-spacing:.5px}
28
- .stat-pending .val{color:#42a5f5}
29
- .stat-running .val{color:#ffa726}
30
- .stat-completed .val{color:#66bb6a}
31
- .stat-failed .val{color:#ef5350}
32
-
33
- /* Grid layouts */
34
- .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px}
35
- .grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-bottom:20px}
36
- @media(max-width:900px){.grid-2,.grid-3{grid-template-columns:1fr}}
37
- @media(max-width:600px){body{padding:10px}header{padding:14px}}
38
-
39
- .card{background:#2d2d2d;padding:20px;border-radius:10px}
40
- .card h2{font-size:15px;margin-bottom:14px;color:#4fc3f7;display:flex;align-items:center;justify-content:space-between}
41
- .card h2 .count{font-size:12px;color:#9e9e9e;font-weight:400}
42
-
43
- /* Workers */
44
- .worker-list{display:flex;flex-direction:column;gap:8px}
45
- .worker-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#3a3a3a;border-radius:6px}
46
- .worker-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
47
- .worker-dot.idle{background:#9e9e9e}
48
- .worker-dot.busy{background:#66bb6a;box-shadow:0 0 6px #66bb6a88}
49
- .worker-dot.stopped{background:#ef5350}
50
- .worker-name{font-size:13px;font-weight:600;flex:1}
51
- .worker-task{font-size:11px;color:#9e9e9e}
52
-
53
- /* Task table */
54
- .task-table-wrap{overflow-x:auto}
55
- table{width:100%;border-collapse:collapse;font-size:13px}
56
- 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}
57
- td{padding:7px 10px;border-bottom:1px solid #333;vertical-align:middle}
58
- tr:hover td{background:#333}
59
- .badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;text-transform:uppercase}
60
- .badge-pending{background:#42a5f533;color:#42a5f5}
61
- .badge-running{background:#ffa72633;color:#ffa726}
62
- .badge-completed{background:#66bb6a33;color:#66bb6a}
63
- .badge-failed{background:#ef535033;color:#ef5350}
64
- .deps{font-size:11px;color:#9e9e9e}
65
- .priority{font-size:12px;color:#bbb}
66
-
67
- /* Graph */
68
- .graph-container{position:relative;overflow:auto;min-height:200px}
69
- .graph-container svg{display:block}
70
-
71
- /* Timeline */
72
- .timeline-container{overflow-x:auto;min-height:100px}
73
- .timeline-row{display:flex;align-items:center;margin-bottom:4px;height:26px}
74
- .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}
75
- .timeline-track{flex:1;position:relative;height:18px;background:#3a3a3a;border-radius:3px}
76
- .timeline-bar{position:absolute;height:100%;border-radius:3px;min-width:4px;transition:width .4s ease,left .4s ease}
77
-
78
- /* Logs */
79
- .log-section{background:#2d2d2d;border-radius:10px;padding:20px;margin-bottom:20px}
80
- .log-filters{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center}
81
- .log-filters label{font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer;padding:4px 8px;background:#3a3a3a;border-radius:4px}
82
- .log-filters label input{accent-color:#4fc3f7}
83
- .log-filters select{background:#3a3a3a;color:#e0e0e0;border:1px solid #555;border-radius:4px;padding:4px 8px;font-size:12px}
84
- .log-entries{max-height:350px;overflow-y:auto;font-family:"Courier New",monospace;font-size:12px}
85
- .log-entry{padding:4px 8px;margin-bottom:2px;border-radius:3px;display:flex;gap:8px}
86
- .log-entry .log-time{color:#777;flex-shrink:0}
87
- .log-entry .log-svc{color:#4fc3f7;flex-shrink:0;min-width:60px}
88
- .log-info{background:#1e3a5f44}
89
- .log-warn{background:#5f4b1e44}
90
- .log-error{background:#5f1e1e66}
91
- .log-hidden{display:none}
92
- .empty-msg{color:#666;font-size:13px;padding:20px;text-align:center}
93
- </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>
94
477
  </head>
95
478
  <body>
96
479
  <header>
97
- <div><h1>AAD Dashboard</h1><p>Real-time Task Orchestrator Monitor</p></div>
98
- <span class="conn-badge disconnected" id="conn-status">Disconnected</span>
480
+ <div>
481
+ <h1>AAD Dashboard</h1>
482
+ <p>Real-time Task Orchestrator Monitor</p>
483
+ </div>
99
484
  </header>
100
485
 
101
486
  <!-- Progress Bar -->
@@ -104,24 +489,42 @@
104
489
  <h2>Overall Progress</h2>
105
490
  <span class="pct" id="pct-display">0%</span>
106
491
  </div>
107
- <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>
108
495
  <div class="progress-stats">
109
- <div class="progress-stat stat-pending"><div class="val" id="s-pending">0</div><div class="lbl">Pending</div></div>
110
- <div class="progress-stat stat-running"><div class="val" id="s-running">0</div><div class="lbl">Running</div></div>
111
- <div class="progress-stat stat-completed"><div class="val" id="s-completed">0</div><div class="lbl">Completed</div></div>
112
- <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>
113
512
  </div>
114
513
  </div>
115
514
 
116
- <!-- Workers + Tasks -->
515
+ <!-- Workers + Graph -->
117
516
  <div class="grid-2">
118
517
  <div class="card">
119
518
  <h2>Workers <span class="count" id="workers-count"></span></h2>
120
- <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>
121
522
  </div>
122
523
  <div class="card">
123
524
  <h2>Dependency Graph</h2>
124
- <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>
125
528
  </div>
126
529
  </div>
127
530
 
@@ -130,8 +533,19 @@
130
533
  <h2>Tasks <span class="count" id="tasks-count"></span></h2>
131
534
  <div class="task-table-wrap">
132
535
  <table>
133
- <thead><tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Dependencies</th></tr></thead>
134
- <tbody id="task-tbody"><tr><td colspan="5" 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>
135
549
  </table>
136
550
  </div>
137
551
  </div>
@@ -139,267 +553,144 @@
139
553
  <!-- Timeline -->
140
554
  <div class="card" style="margin-bottom:20px">
141
555
  <h2>Timeline</h2>
142
- <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>
143
559
  </div>
144
560
 
145
561
  <!-- Logs -->
146
562
  <div class="log-section">
147
- <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>
148
566
  <div class="log-filters">
149
567
  <label><input type="checkbox" checked data-level="info"> Info</label>
150
568
  <label><input type="checkbox" checked data-level="warn"> Warn</label>
151
569
  <label><input type="checkbox" checked data-level="error"> Error</label>
152
- <select id="svc-filter"><option value="">All Services</option></select>
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>
153
585
  </div>
154
586
  <div class="log-entries" id="log-entries"></div>
155
587
  </div>
156
588
 
157
- <script>
158
- // State
159
- let allTasks = [];
160
- let logEntries = [];
161
- let knownServices = new Set();
162
- let logFilters = { info: true, warn: true, error: true, service: '' };
163
-
164
- // --- Data fetching ---
165
- async function loadAll() {
166
- try {
167
- const [pRes, wRes, tRes, gRes, tlRes, lRes] = await Promise.all([
168
- fetch('/api/progress'), fetch('/api/workers'), fetch('/api/tasks'),
169
- fetch('/api/graph'), fetch('/api/timeline'), fetch('/api/logs')
170
- ]);
171
- const [progress, workers, tasks, graph, timeline, logs] = await Promise.all([
172
- pRes.json(), wRes.json(), tRes.json(), gRes.json(), tlRes.json(), lRes.json()
173
- ]);
174
- updateProgress(progress);
175
- renderWorkers(workers);
176
- allTasks = tasks.tasks || [];
177
- renderTasks();
178
- renderGraph(graph);
179
- renderTimeline(timeline);
180
- if (Array.isArray(logs)) logs.forEach(e => addLogEntry(e, true));
181
- else if (logs && logs.entries) logs.entries.forEach(e => addLogEntry(e, true));
182
- applyLogFilters();
183
- } catch (e) { console.error('Load failed:', e); }
184
- }
185
-
186
- // --- Progress ---
187
- function updateProgress(data) {
188
- const p = data.progress || data;
189
- const pct = data.percentComplete != null ? data.percentComplete : (p.total > 0 ? Math.round((p.completed / p.total) * 100) : 0);
190
- document.getElementById('pct-display').textContent = pct + '%';
191
- document.getElementById('progress-fill').style.width = pct + '%';
192
- document.getElementById('s-pending').textContent = p.pending || 0;
193
- document.getElementById('s-running').textContent = p.running || 0;
194
- document.getElementById('s-completed').textContent = p.completed || 0;
195
- document.getElementById('s-failed').textContent = p.failed || 0;
196
- }
197
-
198
- // --- Workers ---
199
- function renderWorkers(data) {
200
- const list = document.getElementById('worker-list');
201
- const workers = data.workers || [];
202
- const stats = data.stats || {};
203
- document.getElementById('workers-count').textContent = '(' + (stats.total || workers.length) + ')';
204
- if (!workers.length) { list.innerHTML = '<div class="empty-msg">No workers</div>'; return; }
205
- list.innerHTML = workers.map(w => {
206
- const taskInfo = w.currentTask ? '<span class="worker-task">Task: ' + esc(String(w.currentTask)) + '</span>' : '';
207
- 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>';
208
- }).join('');
209
- }
210
-
211
- // --- Tasks ---
212
- function renderTasks() {
213
- const tbody = document.getElementById('task-tbody');
214
- document.getElementById('tasks-count').textContent = '(' + allTasks.length + ')';
215
- if (!allTasks.length) { tbody.innerHTML = '<tr><td colspan="5" class="empty-msg">No tasks</td></tr>'; return; }
216
- tbody.innerHTML = allTasks.map(t => {
217
- const deps = (t.dependsOn || []).map(d => esc(String(d))).join(', ') || '—';
218
- 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="priority">' + (t.priority || 0) + '</td><td class="deps">' + deps + '</td></tr>';
219
- }).join('');
220
- }
221
-
222
- function updateTaskInList(taskId, updates) {
223
- const idx = allTasks.findIndex(t => t.taskId === taskId);
224
- if (idx >= 0) Object.assign(allTasks[idx], updates);
225
- renderTasks();
226
- }
227
-
228
- // --- Graph ---
229
- function renderGraph(data) {
230
- const container = document.getElementById('graph-container');
231
- const nodes = data.nodes || [];
232
- const edges = data.edges || [];
233
- if (!nodes.length) { container.innerHTML = '<div class="empty-msg">No graph data</div>'; return; }
234
-
235
- // Simple layered layout using topological sort
236
- const adj = {}, inDeg = {};
237
- nodes.forEach(n => { adj[n.id] = []; inDeg[n.id] = 0; });
238
- edges.forEach(e => { if (adj[e.from]) adj[e.from].push(e.to); inDeg[e.to] = (inDeg[e.to] || 0) + 1; });
239
-
240
- // BFS layers
241
- const layers = []; const visited = new Set();
242
- let queue = nodes.filter(n => (inDeg[n.id] || 0) === 0).map(n => n.id);
243
- while (queue.length) {
244
- layers.push([...queue]); const next = [];
245
- queue.forEach(id => { visited.add(id); (adj[id] || []).forEach(to => { inDeg[to]--; if (inDeg[to] <= 0 && !visited.has(to)) next.push(to); }); });
246
- queue = [...new Set(next)];
247
- }
248
- // Add unvisited nodes
249
- nodes.forEach(n => { if (!visited.has(n.id)) { if (!layers.length) layers.push([]); layers[layers.length - 1].push(n.id); } });
250
-
251
- const nodeMap = {}; nodes.forEach(n => nodeMap[n.id] = n);
252
- const colW = 140, rowH = 50, padX = 80, padY = 30;
253
- const maxPerLayer = Math.max(...layers.map(l => l.length), 1);
254
- const svgW = layers.length * colW + padX * 2;
255
- const svgH = maxPerLayer * rowH + padY * 2;
256
- const pos = {};
257
- layers.forEach((layer, li) => {
258
- const offset = (maxPerLayer - layer.length) * rowH / 2;
259
- layer.forEach((id, ni) => { pos[id] = { x: padX + li * colW, y: padY + offset + ni * rowH }; });
260
- });
261
-
262
- const statusColor = { pending: '#42a5f5', running: '#ffa726', completed: '#66bb6a', failed: '#ef5350' };
263
- let svg = '<svg width="' + svgW + '" height="' + svgH + '" xmlns="http://www.w3.org/2000/svg">';
264
- // Edges
265
- edges.forEach(e => {
266
- const from = pos[e.from], to = pos[e.to];
267
- 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)"/>';
268
- });
269
- 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>';
270
- // Nodes
271
- nodes.forEach(n => {
272
- const p = pos[n.id]; if (!p) return;
273
- const c = statusColor[n.status] || '#777';
274
- svg += '<rect x="' + p.x + '" y="' + p.y + '" width="90" height="30" rx="5" fill="' + c + '22" stroke="' + c + '" stroke-width="2"/>';
275
- const label = String(n.id).length > 12 ? String(n.id).slice(0, 11) + '…' : String(n.id);
276
- svg += '<text x="' + (p.x + 45) + '" y="' + (p.y + 19) + '" text-anchor="middle" fill="' + c + '" font-size="10" font-weight="600">' + esc(label) + '</text>';
277
- });
278
- svg += '</svg>';
279
- container.innerHTML = svg;
280
- }
281
-
282
- // --- Timeline ---
283
- function renderTimeline(data) {
284
- const container = document.getElementById('timeline-container');
285
- const tasks = (data.tasks || []).filter(t => t.startTime);
286
- if (!tasks.length) { container.innerHTML = '<div class="empty-msg">No timeline data</div>'; return; }
287
-
288
- const times = tasks.map(t => [new Date(t.startTime).getTime(), t.endTime ? new Date(t.endTime).getTime() : Date.now()]);
289
- const minT = Math.min(...times.map(t => t[0]));
290
- const maxT = Math.max(...times.map(t => t[1]));
291
- const range = maxT - minT || 1;
292
- const statusColor = { pending: '#42a5f5', running: '#ffa726', completed: '#66bb6a', failed: '#ef5350' };
293
-
294
- container.innerHTML = tasks.map((t, i) => {
295
- const s = new Date(t.startTime).getTime();
296
- const e = t.endTime ? new Date(t.endTime).getTime() : Date.now();
297
- const left = ((s - minT) / range * 100).toFixed(2);
298
- const width = (((e - s) / range) * 100).toFixed(2);
299
- const c = statusColor[t.status] || '#777';
300
- const dur = ((e - s) / 1000).toFixed(1) + 's';
301
- 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>';
302
- }).join('');
303
- }
304
-
305
- // --- Logs ---
306
- function addLogEntry(entry, bulk) {
307
- if (!entry) return;
308
- const svc = entry.service || 'unknown';
309
- if (!knownServices.has(svc)) {
310
- knownServices.add(svc);
311
- const sel = document.getElementById('svc-filter');
312
- const opt = document.createElement('option'); opt.value = svc; opt.textContent = svc; sel.appendChild(opt);
313
- }
314
- logEntries.unshift(entry);
315
- if (logEntries.length > 200) logEntries.pop();
316
- if (!bulk) renderLogEntry(entry, true);
317
- }
318
-
319
- function renderLogEntry(entry, prepend) {
320
- const container = document.getElementById('log-entries');
321
- const div = document.createElement('div');
322
- const level = entry.level || 'info';
323
- div.className = 'log-entry log-' + level;
324
- div.dataset.level = level;
325
- div.dataset.service = entry.service || '';
326
- const time = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '';
327
- div.innerHTML = '<span class="log-time">' + time + '</span><span class="log-svc">' + esc(entry.service || '') + '</span><span>' + esc(entry.message || '') + '</span>';
328
- if (prepend) container.insertBefore(div, container.firstChild); else container.appendChild(div);
329
- applyLogFilterToEntry(div);
330
- }
331
-
332
- function renderAllLogs() {
333
- document.getElementById('log-entries').innerHTML = '';
334
- logEntries.forEach(e => renderLogEntry(e, false));
335
- updateLogCount();
336
- }
337
-
338
- function applyLogFilters() {
339
- renderAllLogs();
340
- }
341
-
342
- function applyLogFilterToEntry(div) {
343
- const show = logFilters[div.dataset.level] && (!logFilters.service || div.dataset.service === logFilters.service);
344
- div.classList.toggle('log-hidden', !show);
345
- updateLogCount();
346
- }
347
-
348
- function updateLogCount() {
349
- const visible = document.querySelectorAll('#log-entries .log-entry:not(.log-hidden)').length;
350
- document.getElementById('log-count').textContent = '(' + visible + ')';
351
- }
352
-
353
- // Filter event listeners
354
- document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
355
- cb.addEventListener('change', () => { logFilters[cb.dataset.level] = cb.checked; applyLogFilters(); });
356
- });
357
- document.getElementById('svc-filter').addEventListener('change', function() { logFilters.service = this.value; applyLogFilters(); });
358
-
359
- // --- SSE ---
360
- let evtSource;
361
- function connectSSE() {
362
- if (evtSource) { try { evtSource.close(); } catch(e){} }
363
- evtSource = new EventSource('/events/all');
364
- const badge = document.getElementById('conn-status');
365
-
366
- evtSource.addEventListener('open', () => { badge.textContent = 'Connected'; badge.className = 'conn-badge connected'; });
367
-
368
- evtSource.addEventListener('message', (e) => {
369
- try {
370
- const ev = JSON.parse(e.data);
371
- switch (ev.type) {
372
- case 'log:entry': addLogEntry(ev.entry || ev); break;
373
- case 'progress:updated': updateProgress(ev.state ? { progress: ev.state, percentComplete: ev.percentComplete } : ev); break;
374
- case 'task:dispatched':
375
- case 'task:completed':
376
- case 'task:failed':
377
- if (ev.task) { updateTaskInList(ev.task.taskId, ev.task); }
378
- // Refresh progress
379
- fetch('/api/progress').then(r=>r.json()).then(updateProgress).catch(()=>{});
380
- fetch('/api/timeline').then(r=>r.json()).then(renderTimeline).catch(()=>{});
381
- break;
382
- case 'worker:idle':
383
- case 'worker:busy':
384
- fetch('/api/workers').then(r=>r.json()).then(renderWorkers).catch(()=>{});
385
- break;
386
- }
387
- } catch (err) { console.error('SSE parse error:', err); }
388
- });
389
-
390
- evtSource.addEventListener('error', () => {
391
- badge.textContent = 'Disconnected'; badge.className = 'conn-badge disconnected';
392
- evtSource.close();
393
- setTimeout(connectSSE, 3000);
394
- });
395
- }
396
-
397
- // --- Util ---
398
- function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
399
-
400
- // Init
401
- loadAll();
402
- connectSSE();
403
- </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>
404
695
  </body>
405
696
  </html>