@ronkovic/aad 0.3.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/aad.js +2 -0
  4. package/package.json +78 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
  6. package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
  7. package/src/__tests__/integration/cli-smoke.test.ts +175 -0
  8. package/src/__tests__/integration/pipeline.test.ts +346 -0
  9. package/src/bun-imports.d.ts +14 -0
  10. package/src/main.ts +52 -0
  11. package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
  12. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
  13. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
  14. package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
  15. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
  16. package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
  17. package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
  18. package/src/modules/claude-provider/claude-provider.port.ts +35 -0
  19. package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
  20. package/src/modules/claude-provider/effort-strategy.ts +94 -0
  21. package/src/modules/claude-provider/index.ts +32 -0
  22. package/src/modules/claude-provider/provider-registry.ts +92 -0
  23. package/src/modules/claude-provider/retry.ts +81 -0
  24. package/src/modules/cli/__tests__/app.test.ts +160 -0
  25. package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
  26. package/src/modules/cli/__tests__/commands.test.ts +186 -0
  27. package/src/modules/cli/__tests__/output.test.ts +329 -0
  28. package/src/modules/cli/__tests__/resume.test.ts +324 -0
  29. package/src/modules/cli/__tests__/run.test.ts +168 -0
  30. package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
  31. package/src/modules/cli/__tests__/status.test.ts +144 -0
  32. package/src/modules/cli/app.ts +241 -0
  33. package/src/modules/cli/commands/cleanup.ts +120 -0
  34. package/src/modules/cli/commands/resume.ts +156 -0
  35. package/src/modules/cli/commands/run.ts +322 -0
  36. package/src/modules/cli/commands/status.ts +101 -0
  37. package/src/modules/cli/index.ts +29 -0
  38. package/src/modules/cli/output.ts +256 -0
  39. package/src/modules/cli/shutdown.ts +122 -0
  40. package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
  41. package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
  42. package/src/modules/dashboard/__tests__/server.test.ts +120 -0
  43. package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
  44. package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
  45. package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
  46. package/src/modules/dashboard/index.ts +8 -0
  47. package/src/modules/dashboard/routes/api.ts +84 -0
  48. package/src/modules/dashboard/routes/sse.ts +37 -0
  49. package/src/modules/dashboard/server.ts +111 -0
  50. package/src/modules/dashboard/services/file-watcher.ts +36 -0
  51. package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
  52. package/src/modules/dashboard/services/state-aggregator.ts +132 -0
  53. package/src/modules/dashboard/ui/dashboard.html +405 -0
  54. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
  55. package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
  56. package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
  57. package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
  58. package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
  59. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
  60. package/src/modules/git-workspace/branch-manager.ts +191 -0
  61. package/src/modules/git-workspace/git-exec.ts +124 -0
  62. package/src/modules/git-workspace/index.ts +17 -0
  63. package/src/modules/git-workspace/memory-sync.ts +89 -0
  64. package/src/modules/git-workspace/merge-service.ts +156 -0
  65. package/src/modules/git-workspace/settings-merge.ts +95 -0
  66. package/src/modules/git-workspace/worktree-manager.ts +199 -0
  67. package/src/modules/logging/__tests__/log-store.test.ts +242 -0
  68. package/src/modules/logging/__tests__/logger.test.ts +81 -0
  69. package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
  70. package/src/modules/logging/index.ts +7 -0
  71. package/src/modules/logging/log-store.ts +80 -0
  72. package/src/modules/logging/logger.ts +55 -0
  73. package/src/modules/logging/transports/sse-transport.ts +28 -0
  74. package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
  75. package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
  76. package/src/modules/multi-repo/index.ts +12 -0
  77. package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
  78. package/src/modules/multi-repo/repo-context.ts +71 -0
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
  133. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
  134. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
  135. package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
  136. package/src/modules/persistence/__tests__/index.test.ts +38 -0
  137. package/src/modules/persistence/__tests__/stores.test.ts +594 -0
  138. package/src/modules/persistence/file-lock.ts +158 -0
  139. package/src/modules/persistence/fs-run-store.ts +73 -0
  140. package/src/modules/persistence/fs-task-store.ts +152 -0
  141. package/src/modules/persistence/fs-worker-store.ts +116 -0
  142. package/src/modules/persistence/in-memory-stores.ts +98 -0
  143. package/src/modules/persistence/index.ts +60 -0
  144. package/src/modules/persistence/stores.port.ts +60 -0
  145. package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
  146. package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
  147. package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
  148. package/src/modules/planning/file-conflict-validator.ts +135 -0
  149. package/src/modules/planning/index.ts +40 -0
  150. package/src/modules/planning/planning.service.ts +262 -0
  151. package/src/modules/planning/project-detection.ts +525 -0
  152. package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
  153. package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
  154. package/src/modules/plugin/index.ts +3 -0
  155. package/src/modules/plugin/plugin-loader.ts +46 -0
  156. package/src/modules/plugin/plugin-manager.ts +90 -0
  157. package/src/modules/plugin/plugin.types.ts +37 -0
  158. package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
  159. package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
  160. package/src/modules/process-manager/index.ts +5 -0
  161. package/src/modules/process-manager/process-manager.ts +193 -0
  162. package/src/modules/process-manager/worker.ts +106 -0
  163. package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
  164. package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
  165. package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
  166. package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
  167. package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
  168. package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
  169. package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
  170. package/src/modules/task-execution/executor.ts +303 -0
  171. package/src/modules/task-execution/index.ts +45 -0
  172. package/src/modules/task-execution/phases/default-spawner.ts +49 -0
  173. package/src/modules/task-execution/phases/implementer-green.ts +100 -0
  174. package/src/modules/task-execution/phases/merge.ts +122 -0
  175. package/src/modules/task-execution/phases/reviewer.ts +160 -0
  176. package/src/modules/task-execution/phases/tester-red.ts +100 -0
  177. package/src/modules/task-execution/phases/tester-verify.ts +120 -0
  178. package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
  179. package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
  180. package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
  181. package/src/modules/task-queue/__tests__/task.test.ts +130 -0
  182. package/src/modules/task-queue/dependency-resolver.ts +171 -0
  183. package/src/modules/task-queue/dispatcher.ts +372 -0
  184. package/src/modules/task-queue/index.ts +16 -0
  185. package/src/modules/task-queue/task-plan.ts +40 -0
  186. package/src/modules/task-queue/task.ts +67 -0
  187. package/src/shared/__tests__/config.test.ts +204 -0
  188. package/src/shared/__tests__/errors.test.ts +285 -0
  189. package/src/shared/__tests__/events.test.ts +496 -0
  190. package/src/shared/__tests__/types.test.ts +360 -0
  191. package/src/shared/config.ts +133 -0
  192. package/src/shared/errors.ts +128 -0
  193. package/src/shared/events.ts +171 -0
  194. package/src/shared/types.ts +143 -0
  195. package/tsconfig.json +30 -0
@@ -0,0 +1,132 @@
1
+ import type { LogStore, LogEntry, LogFilter } from "../../logging";
2
+ import type { TaskId, WorkerId, ProgressState, Task } from "../../../shared/types";
3
+ import type { TaskStore } from "../../task-queue";
4
+ import type { ProcessManager } from "../../process-manager";
5
+
6
+ export interface StateAggregatorOptions {
7
+ logStore: LogStore;
8
+ taskStore: TaskStore;
9
+ processManager: ProcessManager;
10
+ }
11
+
12
+ export class StateAggregator {
13
+ private logStore: LogStore;
14
+ private taskStore: TaskStore;
15
+ private processManager: ProcessManager;
16
+
17
+ constructor(options: StateAggregatorOptions) {
18
+ this.logStore = options.logStore;
19
+ this.taskStore = options.taskStore;
20
+ this.processManager = options.processManager;
21
+ }
22
+
23
+ async getProgress(): Promise<{ progress: ProgressState; percentComplete: number }> {
24
+ const tasks = await this.taskStore.getAll();
25
+
26
+ const progress: ProgressState = {
27
+ total: tasks.length,
28
+ pending: 0,
29
+ running: 0,
30
+ completed: 0,
31
+ failed: 0,
32
+ };
33
+
34
+ for (const task of tasks) {
35
+ switch (task.status) {
36
+ case "pending":
37
+ progress.pending++;
38
+ break;
39
+ case "running":
40
+ progress.running++;
41
+ break;
42
+ case "completed":
43
+ progress.completed++;
44
+ break;
45
+ case "failed":
46
+ progress.failed++;
47
+ break;
48
+ }
49
+ }
50
+
51
+ const percentComplete =
52
+ progress.total > 0 ? Math.round((progress.completed / progress.total) * 100) : 0;
53
+
54
+ return { progress, percentComplete };
55
+ }
56
+
57
+ async getTasks(): Promise<{ tasks: Task[]; total: number }> {
58
+ const tasks = await this.taskStore.getAll();
59
+ return {
60
+ tasks,
61
+ total: tasks.length,
62
+ };
63
+ }
64
+
65
+ getTaskLogs(taskId: TaskId): LogEntry[] {
66
+ return this.logStore.query({ taskId: taskId as string });
67
+ }
68
+
69
+ getWorkers(): {
70
+ stats: ReturnType<ProcessManager["getStats"]>;
71
+ workers: Array<{ id: WorkerId; status: "idle" | "busy" | "stopped" }>;
72
+ } {
73
+ const workerIds = this.processManager.getAllWorkers();
74
+ const workers = workerIds.map((id) => {
75
+ const worker = this.processManager.getWorker(id);
76
+ return {
77
+ id,
78
+ status: worker?.isIdle() ? "idle" as const : worker?.isBusy() ? "busy" as const : "stopped" as const,
79
+ };
80
+ });
81
+
82
+ return {
83
+ stats: this.processManager.getStats(),
84
+ workers,
85
+ };
86
+ }
87
+
88
+ async getGraph(): Promise<{ nodes: Array<{ id: string; status: string }>; edges: Array<{ from: string; to: string }> }> {
89
+ const tasks = await this.taskStore.getAll();
90
+ const nodes = tasks.map((task) => ({
91
+ id: task.taskId as string,
92
+ status: task.status,
93
+ }));
94
+
95
+ const edges: Array<{ from: string; to: string }> = [];
96
+
97
+ for (const task of tasks) {
98
+ for (const depId of task.dependsOn) {
99
+ edges.push({
100
+ from: depId as string,
101
+ to: task.taskId as string,
102
+ });
103
+ }
104
+ }
105
+
106
+ return { nodes, edges };
107
+ }
108
+
109
+ async getTimeline(): Promise<{
110
+ tasks: Array<{
111
+ id: string;
112
+ status: string;
113
+ startTime?: string;
114
+ endTime?: string;
115
+ }>;
116
+ }> {
117
+ const tasks = await this.taskStore.getAll();
118
+
119
+ return {
120
+ tasks: tasks.map((task) => ({
121
+ id: task.taskId as string,
122
+ status: task.status,
123
+ startTime: task.startTime,
124
+ endTime: task.endTime,
125
+ })),
126
+ };
127
+ }
128
+
129
+ queryLogs(filter: LogFilter): LogEntry[] {
130
+ return this.logStore.query(filter);
131
+ }
132
+ }
@@ -0,0 +1,405 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>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>
94
+ </head>
95
+ <body>
96
+ <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>
99
+ </header>
100
+
101
+ <!-- Progress Bar -->
102
+ <div class="progress-section">
103
+ <div class="progress-header">
104
+ <h2>Overall Progress</h2>
105
+ <span class="pct" id="pct-display">0%</span>
106
+ </div>
107
+ <div class="progress-bar-track"><div class="progress-bar-fill" id="progress-fill" style="width:0%"></div></div>
108
+ <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>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Workers + Tasks -->
117
+ <div class="grid-2">
118
+ <div class="card">
119
+ <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>
121
+ </div>
122
+ <div class="card">
123
+ <h2>Dependency Graph</h2>
124
+ <div class="graph-container" id="graph-container"><div class="empty-msg">No graph data</div></div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Task Table -->
129
+ <div class="card" style="margin-bottom:20px">
130
+ <h2>Tasks <span class="count" id="tasks-count"></span></h2>
131
+ <div class="task-table-wrap">
132
+ <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>
135
+ </table>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Timeline -->
140
+ <div class="card" style="margin-bottom:20px">
141
+ <h2>Timeline</h2>
142
+ <div class="timeline-container" id="timeline-container"><div class="empty-msg">No timeline data</div></div>
143
+ </div>
144
+
145
+ <!-- Logs -->
146
+ <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>
148
+ <div class="log-filters">
149
+ <label><input type="checkbox" checked data-level="info"> Info</label>
150
+ <label><input type="checkbox" checked data-level="warn"> Warn</label>
151
+ <label><input type="checkbox" checked data-level="error"> Error</label>
152
+ <select id="svc-filter"><option value="">All Services</option></select>
153
+ </div>
154
+ <div class="log-entries" id="log-entries"></div>
155
+ </div>
156
+
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>
404
+ </body>
405
+ </html>