@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.0-beta.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 (262) hide show
  1. package/AGENTS.md +155 -153
  2. package/CHANGELOG.md +326 -301
  3. package/README.md +345 -326
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/generate-task-dag.js +28 -58
  7. package/dist/application/evaluation/candidate-hash.js +75 -0
  8. package/dist/application/evaluation/candidate.js +52 -0
  9. package/dist/application/evaluation/replay.js +289 -0
  10. package/dist/application/evaluation/types.js +130 -0
  11. package/dist/cli/command-definitions.js +17 -4
  12. package/dist/cli/program.js +8 -4
  13. package/dist/commands/cursor-prompt.js +6 -6
  14. package/dist/commands/eval.js +235 -0
  15. package/dist/commands/init.js +544 -506
  16. package/dist/commands/loop-benchmark.js +11 -11
  17. package/dist/commands/pi-reuse-benchmark.js +16 -16
  18. package/dist/executors/pi-sdk-executor.js +38 -24
  19. package/dist/executors/shell-executor.js +34 -2
  20. package/dist/executors/shell-presets.js +20 -0
  21. package/dist/executors/shell-verification.js +7 -0
  22. package/dist/governance/manifest-types.js +1 -0
  23. package/dist/infrastructure/evaluation/candidate-store.js +435 -0
  24. package/dist/infrastructure/evaluation/store.js +40 -0
  25. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  26. package/dist/task/config-types.js +23 -0
  27. package/dist/task/runtime.js +27 -27
  28. package/dist/worker/observe/routes.js +18 -3
  29. package/dist/worker/observe/spec-evidence.js +1 -1
  30. package/dist/worker/observe/static/api.js +46 -46
  31. package/dist/worker/observe/static/app.js +150 -150
  32. package/dist/worker/observe/static/constants.js +148 -148
  33. package/dist/worker/observe/static/copy.js +67 -67
  34. package/dist/worker/observe/static/dag-helpers.js +172 -172
  35. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  36. package/dist/worker/observe/static/dag-layout.js +83 -83
  37. package/dist/worker/observe/static/dag-model.js +72 -72
  38. package/dist/worker/observe/static/dom.js +61 -61
  39. package/dist/worker/observe/static/format-pool.js +67 -67
  40. package/dist/worker/observe/static/format.js +292 -292
  41. package/dist/worker/observe/static/index.html +308 -308
  42. package/dist/worker/observe/static/kpi.js +94 -94
  43. package/dist/worker/observe/static/relations.js +133 -133
  44. package/dist/worker/observe/static/router.js +93 -93
  45. package/dist/worker/observe/static/run-processing.js +148 -148
  46. package/dist/worker/observe/static/shell-chrome.js +68 -68
  47. package/dist/worker/observe/static/state.js +253 -253
  48. package/dist/worker/observe/static/styles.css +1902 -1902
  49. package/dist/worker/observe/static/views/batch.js +227 -227
  50. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  51. package/dist/worker/observe/static/views/dag-inspector.js +607 -596
  52. package/dist/worker/observe/static/views/dag.js +362 -362
  53. package/dist/worker/observe/static/views/dashboard.js +445 -445
  54. package/dist/worker/observe/static/views/failures.js +143 -143
  55. package/dist/worker/observe/static/views/feature.js +492 -492
  56. package/dist/worker/observe/static/views/pool.js +350 -350
  57. package/dist/worker/observe/static/views/run.js +453 -453
  58. package/dist/worker/observe/static/views/session-timeline.js +205 -205
  59. package/dist/worker/observe/static/views/shell.js +7 -7
  60. package/dist/worker/observe/static/views/task.js +314 -314
  61. package/dist/worker/observe/static/views/timeline.js +163 -163
  62. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  63. package/dist/workflows/dag/canvas-observer.js +275 -275
  64. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  65. package/dist/workflows/dag/init-hybrid.js +1415 -200
  66. package/dist/workflows/dag/node-execution.js +9 -0
  67. package/dist/workflows/dag/prompt.js +9 -0
  68. package/dist/workflows/dag/report.js +35 -1
  69. package/dist/workflows/dag/runner.js +28 -2
  70. package/dist/workflows/dag/task-demand-routing.js +383 -0
  71. package/dist/workflows/dag/types.js +50 -13
  72. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  73. package/dist/workflows/dag/validate.js +59 -1
  74. package/docs/README.md +106 -104
  75. package/docs/agent-dag-recovery-playbook.md +195 -193
  76. package/docs/agent-dag-runner.md +67 -67
  77. package/docs/architecture/README.md +26 -26
  78. package/docs/architecture/dag-execution.md +140 -140
  79. package/docs/architecture/evolution.md +54 -54
  80. package/docs/architecture/facts-and-state.md +71 -71
  81. package/docs/architecture/runtime-boundaries.md +191 -191
  82. package/docs/architecture/system-overview.md +93 -93
  83. package/docs/architecture/worker-and-feature.md +85 -85
  84. package/docs/cursor-prompt-sidecar.md +36 -36
  85. package/docs/decisions/README.md +18 -18
  86. package/docs/design/README.md +167 -85
  87. package/docs/development-principles.md +73 -73
  88. package/docs/exec-plans/README.md +6 -6
  89. package/docs/exec-plans/active/README.md +15 -11
  90. package/docs/exec-plans/completed/README.md +85 -74
  91. package/docs/feature-workflow.md +389 -339
  92. package/docs/harness-methodology-debugging.md +153 -153
  93. package/docs/harness-methodology-tdd.md +130 -130
  94. package/docs/harness-methodology-verification.md +27 -27
  95. package/docs/init-surface.manifest.json +289 -280
  96. package/docs/loop-agent-harness.md +142 -141
  97. package/docs/production-readiness.md +96 -96
  98. package/docs/progress/README.md +64 -58
  99. package/docs/reports/README.md +117 -100
  100. package/docs/skills/README.md +7 -7
  101. package/docs/skills/vetted-skill-registry.md +29 -27
  102. package/docs/templates/adr.md +60 -60
  103. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  104. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  105. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  106. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  107. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  108. package/docs/templates/agent-dag-report.schema.json +473 -473
  109. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  110. package/docs/templates/agent-dag.base.json +190 -190
  111. package/docs/templates/agent-dag.final-verification.json +185 -185
  112. package/docs/templates/agent-dag.schema.json +411 -383
  113. package/docs/templates/agent-dag.supervised-implementation.json +501 -501
  114. package/docs/templates/backend-test-analysis.schema.json +44 -0
  115. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +202 -139
  116. package/docs/templates/backend-test-dag.json +311 -288
  117. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -125
  118. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -81
  119. package/docs/templates/exec-plan.md +64 -64
  120. package/docs/templates/feature-spec.md +53 -53
  121. package/docs/templates/frontend-design-contract.md +42 -33
  122. package/docs/templates/frontend-task-constraints.md +35 -25
  123. package/docs/templates/frontend-task-requirement.md +70 -61
  124. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  125. package/docs/templates/frontend-test-dag.json +23 -0
  126. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  127. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  128. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  129. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  130. package/docs/templates/harness.schema.json +221 -221
  131. package/docs/templates/hybrid-dag.json +188 -188
  132. package/docs/templates/init-evolution-review.md +35 -35
  133. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  134. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  135. package/docs/templates/knowledge-sync-dag.json +178 -177
  136. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  137. package/docs/templates/product-line/AGENTS.md +8 -8
  138. package/docs/templates/product-line/README.md +9 -9
  139. package/docs/templates/product-line/acceptance.yaml +14 -14
  140. package/docs/templates/product-line/closeout.yaml +9 -9
  141. package/docs/templates/product-line/design.md +13 -13
  142. package/docs/templates/product-line/links.md +10 -10
  143. package/docs/templates/product-line/requirement.md +17 -17
  144. package/docs/templates/product-line/task-graph.yaml +15 -15
  145. package/docs/templates/product-line/task.yaml +64 -64
  146. package/docs/templates/product-line/test-plan.md +7 -7
  147. package/docs/templates/production-readiness-checklist.md +57 -57
  148. package/docs/templates/progress-log.md +17 -17
  149. package/docs/templates/project-start-checklist.md +9 -9
  150. package/docs/templates/qa-report.md +48 -48
  151. package/docs/templates/sprint-contract.md +29 -29
  152. package/docs/templates/worker-dogfood-evidence.md +80 -80
  153. package/docs/templates/worker-dogfood-setup.md +68 -68
  154. package/docs/verification-matrix.md +70 -67
  155. package/examples/decision-gate-agent-dag.json +177 -177
  156. package/examples/example-dag.json +46 -46
  157. package/examples/hybrid-loop-agent-dag.json +189 -189
  158. package/harness.json +66 -66
  159. package/package.json +88 -52
  160. package/scripts/check-product-line-docs.sh +29 -29
  161. package/scripts/check-task-pool-root.sh +32 -32
  162. package/scripts/kb-bootstrap-init-skeleton.sh +240 -239
  163. package/scripts/kb-graph-incremental-prepare.mjs +386 -372
  164. package/scripts/kb-graph-incremental-prepare.sh +5 -5
  165. package/scripts/kb-graph-materialize.mjs +105 -105
  166. package/scripts/kb-graph-materialize.sh +4 -4
  167. package/scripts/kb-graph-promote.mjs +164 -153
  168. package/scripts/kb-graph-promote.sh +4 -4
  169. package/scripts/kb-query.mjs +554 -554
  170. package/scripts/kb-query.sh +5 -5
  171. package/skills/agent-worker/SKILL.md +39 -39
  172. package/skills/agent-worker/references/agent-worker-operator.md +60 -60
  173. package/skills/ai-engineering-context/SKILL.md +48 -48
  174. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  175. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  176. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  177. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  178. package/skills/analyze-product-dependencies/references/example.md +76 -0
  179. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  180. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  181. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  182. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  183. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  184. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  185. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  186. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  187. package/skills/analyze-product-requirements/SKILL.md +90 -0
  188. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  189. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  190. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  191. package/skills/analyze-product-requirements/references/example.md +86 -0
  192. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  193. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  194. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  195. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  196. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  197. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  198. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  199. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  200. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  201. package/skills/code-review-core/SKILL.md +20 -20
  202. package/skills/codebase-scout/SKILL.md +19 -19
  203. package/skills/frontend-design-review/SKILL.md +66 -61
  204. package/skills/frontend-design-review/references/review-checklist.md +58 -37
  205. package/skills/frontend-implementation/SKILL.md +45 -52
  206. package/skills/frontend-implementation/references/code-standards.md +32 -34
  207. package/skills/frontend-implementation/references/design-spec.md +46 -46
  208. package/skills/frontend-implementation/references/node-contracts.md +76 -63
  209. package/skills/frontend-review/SKILL.md +59 -53
  210. package/skills/frontend-review/references/review-findings.md +47 -42
  211. package/skills/frontend-verification/SKILL.md +53 -40
  212. package/skills/frontend-verification/references/verification-checklist.md +68 -56
  213. package/skills/grill-me/SKILL.md +10 -10
  214. package/skills/grill-with-docs/SKILL.md +88 -88
  215. package/skills/grill-with-docs/adr-format.md +47 -47
  216. package/skills/grill-with-docs/context-format.md +60 -60
  217. package/skills/init-capability-evolution/SKILL.md +70 -70
  218. package/skills/loop-agent/SKILL.md +151 -151
  219. package/skills/loop-agent/references/README.md +67 -67
  220. package/skills/loop-agent/references/command-reference.md +505 -453
  221. package/skills/loop-agent/references/docs-converge.md +126 -126
  222. package/skills/loop-agent/references/harness-policy.md +263 -263
  223. package/skills/loop-agent/references/hybrid-dag.md +238 -233
  224. package/skills/loop-agent/references/learned/README.md +21 -21
  225. package/skills/loop-agent/references/long-running-loop.md +57 -57
  226. package/skills/loop-agent/references/model-routing.md +36 -36
  227. package/skills/loop-agent/references/multi-worktree.md +54 -54
  228. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  229. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  230. package/skills/loop-agent/references/pi-prompt.md +23 -23
  231. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  232. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  233. package/skills/loop-agent/references/task-workflow.md +89 -89
  234. package/skills/loop-agent/references/verification-and-failure-handling.md +139 -139
  235. package/skills/playwright-cli/SKILL.md +420 -0
  236. package/skills/playwright-cli/references/element-attributes.md +23 -0
  237. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  238. package/skills/playwright-cli/references/request-mocking.md +87 -0
  239. package/skills/playwright-cli/references/running-code.md +241 -0
  240. package/skills/playwright-cli/references/session-management.md +225 -0
  241. package/skills/playwright-cli/references/storage-state.md +275 -0
  242. package/skills/playwright-cli/references/test-generation.md +433 -0
  243. package/skills/playwright-cli/references/tracing.md +139 -0
  244. package/skills/playwright-cli/references/video-recording.md +143 -0
  245. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  246. package/skills/requesting-code-review/SKILL.md +101 -101
  247. package/skills/requesting-code-review/code-reviewer.md +168 -168
  248. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  249. package/skills/systematic-debugging/SKILL.md +296 -296
  250. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  251. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  252. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  253. package/skills/systematic-debugging/find-polluter.sh +63 -63
  254. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  255. package/skills/systematic-debugging/test-academic.md +14 -14
  256. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  257. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  258. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  259. package/skills/test-driven-development/SKILL.md +20 -20
  260. package/skills/using-git-worktrees/SKILL.md +215 -215
  261. package/skills/verification-before-completion/SKILL.md +154 -154
  262. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,445 +1,445 @@
1
- /** Observe UI view module (R5). */
2
- import {
3
- UI_TEXT,
4
- LIVENESS_LABELS,
5
- ARTIFACT_LABELS,
6
- ARTIFACT_KEYS,
7
- STEP_LABELS,
8
- STEP_ORDER,
9
- DAG_INSPECTOR_DEFAULT_WIDTH,
10
- DAG_INSPECTOR_MIN_WIDTH,
11
- DAG_INSPECTOR_MAX_WIDTH,
12
- DAG_INSPECTOR_RIGHT_OFFSET,
13
- DAG_INSPECTOR_VIEWPORT_MARGIN,
14
- DAG_INSPECTOR_KEYBOARD_STEP,
15
- TERMINAL_RUN_STATUSES,
16
- DAG_EFFECTIVE_STATUS_LABELS,
17
- STATUS_LABELS,
18
- } from "../constants.js";
19
- import { clearNode, el, metaGrid, buildTable } from "../dom.js";
20
- import {
21
- badge,
22
- badgeClass,
23
- statusLabel,
24
- formatTs,
25
- formatMs,
26
- formatDuration,
27
- formatAgo,
28
- dagStatusBadge,
29
- livenessBadge,
30
- displayDurationMs,
31
- derivedDurationMs,
32
- summaryText,
33
- truncateText,
34
- repoBaseName,
35
- parseMarkdownBlocks,
36
- markdownLinkHref,
37
- } from "../format.js";
38
- import { parseRoute, navigate, buildHashPath } from "../router.js";
39
- import { fetchJson, artifactUrl, parseArtifactPreviewResponse } from "../api.js";
40
- import {
41
- objectRouteLink,
42
- findFeatureInSnapshot,
43
- featureLookupState,
44
- buildObjectRelations,
45
- renderObjectRelationBar,
46
- } from "../relations.js";
47
- import { copyText, renderRecommendedCommand } from "../copy.js";
48
- import {
49
- isPageVisible,
50
- detailPollDelay,
51
- dashboardPollDelay,
52
- captureDagGraphViewportState,
53
- updateDagGraphViewportState,
54
- updateDagTimelineViewportState,
55
- renderViewState,
56
- mountViewState,
57
- } from "../state.js";
58
- import { uiState } from "../state.js";
59
- import { sortPoolTasks } from "../format-pool.js";
60
- import {
61
- dagMetaVisibility,
62
- isDagRunActive,
63
- dashboardVisibleFeatures,
64
- findDagForRun,
65
- } from "../dag-model.js";
66
- import { layoutDag } from "../dag-layout.js";
67
- import { showView, setBreadcrumb, updateHeaderRefresh } from "../shell-chrome.js";
68
- import { kpiCard, renderRepoBanner, renderProjectionFault, failuresInboxLink, artifactLink } from "../kpi.js";
69
- import { dagSortTime, renderActiveDagCard, dagProgress } from "../dag-helpers.js";
70
-
71
- export async function renderDashboard(scrollTo) {
72
- showView("dashboard");
73
- setBreadcrumb([{ label: UI_TEXT.dashboard }]);
74
-
75
- const kpiEl = document.getElementById("dashboard-kpi");
76
- const featureEl = document.getElementById("dashboard-features");
77
- const activeEl = document.getElementById("dashboard-active-dags");
78
- const riskEl = document.getElementById("dashboard-risk");
79
- const dagsEl = document.getElementById("dashboard-dags");
80
- const batchesEl = document.getElementById("dashboard-batches");
81
- const repoEl = document.getElementById("dashboard-repo");
82
- const host = kpiEl || featureEl || activeEl;
83
- if (host) mountViewState(host, "loading", "加载看板…");
84
-
85
- const snapshot = await fetchJson("/api/snapshot");
86
- if (!snapshot) {
87
- if (kpiEl) mountViewState(kpiEl, "error", "无法加载 snapshot。");
88
- return;
89
- }
90
- uiState.lastSnapshot = snapshot;
91
- updateHeaderRefresh(snapshot.generatedAt);
92
-
93
- const dagRuns = snapshot.dagRuns ?? [];
94
- const activeDags = dagRuns.filter(isDagRunActive);
95
- const activeDagIds = new Set(activeDags.map((d) => d.dagRunId));
96
- const recentDags = dagRuns
97
- .filter((d) => !activeDagIds.has(d.dagRunId))
98
- .sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a)))
99
- .slice(0, 10);
100
-
101
- const features = dashboardVisibleFeatures(snapshot.features);
102
- featureEl.hidden = features.length === 0;
103
- clearNode(featureEl);
104
- if (features.length > 0) {
105
- featureEl.appendChild(el("h3", "section-heading", "Feature 决策"));
106
- featureEl.appendChild(
107
- buildTable(
108
- ["Feature", "状态", "下一步(建议)", "原因", "required AC", "证据"],
109
- features.map((feature) => {
110
- const idCell = objectRouteLink(
111
- "feature",
112
- feature.featureId,
113
- snapshot,
114
- );
115
- // Prefer planner-selected next task; never re-rank ready pool order here.
116
- const selected = feature.planning?.selected?.[0];
117
- const nextHint = selected
118
- ? `Next Task: ${selected.taskId} (${selected.priority})`
119
- : feature.nextAction?.label ??
120
- (feature.nextAction?.command
121
- ? truncateText(feature.nextAction.command, 48)
122
- : "—");
123
- return {
124
- onClick: feature.featureId
125
- ? () =>
126
- navigate(`/feature/${encodeURIComponent(feature.featureId)}`)
127
- : undefined,
128
- cells: [
129
- idCell,
130
- feature.statusLabel ?? feature.status,
131
- nextHint,
132
- feature.blockingItems?.[0]?.message ??
133
- `${feature.summary?.tasksSucceeded ?? 0}/${feature.summary?.tasksTotal ?? 0} tasks completed`,
134
- `${feature.summary?.requiredAcCovered ?? 0}/${feature.summary?.requiredAcTotal ?? 0}`,
135
- artifactLink(
136
- "查看",
137
- feature.evidence?.delivery ??
138
- feature.evidence?.closeout ??
139
- feature.evidence?.morningReport ??
140
- feature.evidence?.observeSnapshot,
141
- ),
142
- ],
143
- };
144
- }),
145
- ),
146
- );
147
- }
148
- if (features.length > 0 && (snapshot.projectionWarnings ?? []).length > 0) {
149
- featureEl.appendChild(
150
- el("p", "empty", `投影警告:${snapshot.projectionWarnings.join(";")}`),
151
- );
152
- }
153
-
154
- renderRepoBanner(repoEl, snapshot);
155
-
156
- clearNode(kpiEl);
157
- const dagHealth = snapshot.health?.dag;
158
- const dagActiveRuns = dagHealth?.activeRuns ?? activeDags.length;
159
- const dagRunningNodes =
160
- dagHealth?.runningNodes ??
161
- activeDags
162
- .flatMap((dag) => dag.nodes ?? [])
163
- .filter((node) =>
164
- ["running", "started"].includes((node.status ?? "").toLowerCase()),
165
- ).length;
166
- const dagPendingNodes =
167
- dagHealth?.pendingNodes ??
168
- activeDags
169
- .flatMap((dag) => dag.nodes ?? [])
170
- .filter((node) =>
171
- ["pending", "queued"].includes((node.status ?? "").toLowerCase()),
172
- ).length;
173
- const dagPausedRuns = dagHealth?.pausedRuns ?? 0;
174
- const dagStaleRuns = dagHealth?.staleRuns ?? 0;
175
- const dagInterruptedRuns = dagHealth?.interruptedRuns ?? 0;
176
- const dagInconsistentRuns = dagHealth?.inconsistentRuns ?? 0;
177
- const dagAttentionRuns =
178
- dagHealth?.attentionRuns ??
179
- dagRuns.filter(
180
- (dag) =>
181
- (dag.lifecycle ?? "").toLowerCase() !== "completed" &&
182
- ((dag.lifecycle ?? "").toLowerCase() === "paused" ||
183
- ["stale", "orphaned"].includes((dag.liveness ?? "").toLowerCase()) ||
184
- ["interrupted", "remote-unknown"].includes(dag.effectiveStatus) ||
185
- dag.stateConsistent === false),
186
- ).length;
187
-
188
- const quietCount = snapshot.health?.quietCount ?? 0;
189
- const staleCount = snapshot.health?.staleCount ?? 0;
190
- const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
191
- const failuresCount = snapshot.health?.failuresCount ?? 0;
192
-
193
- if (snapshot.projectionError) {
194
- mountViewState(kpiEl, "degraded", "指标暂不可用:数据投影失败。", {
195
- detail: snapshot.projectionError.message ?? null,
196
- });
197
- // keep legacy banner for non-color / structure parity
198
- kpiEl.appendChild(renderProjectionFault(snapshot.projectionError));
199
- } else {
200
- const dagGroup = el("fieldset", "kpi-group");
201
- dagGroup.appendChild(el("legend", "kpi-group-legend", "DAG 运行"));
202
- const dagCards = el("div", "kpi-grid");
203
- dagCards.appendChild(
204
- kpiCard(
205
- "活跃 DAG",
206
- dagActiveRuns,
207
- "kpi-highlight",
208
- "当前处于运行、静默运行、远端状态未知或等待执行的 DAG 数量(来源:.harness/dag-runs 的有效状态)。",
209
- ),
210
- );
211
- dagCards.appendChild(
212
- kpiCard(
213
- "执行中节点",
214
- dagRunningNodes,
215
- "",
216
- "活跃 DAG 中状态为 running 或 started、正在执行的节点数量;DAG 内执行单元统一称为节点。",
217
- ),
218
- );
219
- dagCards.appendChild(
220
- kpiCard(
221
- "等待节点",
222
- dagPendingNodes,
223
- "",
224
- "活跃 DAG 中状态为 pending 或 queued、尚未开始执行的节点数量。",
225
- ),
226
- );
227
- dagCards.appendChild(
228
- kpiCard(
229
- "暂停 DAG",
230
- dagPausedRuns,
231
- dagPausedRuns > 0 ? "kpi-muted" : "",
232
- "生命周期被暂停的 DAG 运行数量(来源:.harness/dag-runs 下的 lifecycle 目录)。",
233
- ),
234
- );
235
- dagCards.appendChild(
236
- kpiCard(
237
- "需处理 DAG",
238
- dagAttentionRuns,
239
- dagAttentionRuns > 0 ? "kpi-risk" : "",
240
- "未完成且暂停、失联、中断、远端状态未知或状态不一致的 DAG 数量;同一个 DAG 只计一次。",
241
- ),
242
- );
243
- dagGroup.appendChild(dagCards);
244
-
245
- const workerGroup = el("fieldset", "kpi-group");
246
- workerGroup.appendChild(el("legend", "kpi-group-legend", "Worker 执行"));
247
- const workerCards = el("div", "kpi-grid");
248
- workerCards.appendChild(
249
- kpiCard(
250
- "活跃 Worker Task",
251
- snapshot.health?.activeTasks ?? 0,
252
- "",
253
- "当前运行或等待执行的 Worker Task Pool 任务数量(来源:.harness/task-pool 的状态、运行记录与事件;不含 .harness/tasks 下的 task.json)。",
254
- ),
255
- );
256
- workerCards.appendChild(
257
- kpiCard(
258
- "无输出",
259
- quietCount,
260
- quietCount > 0 ? "kpi-muted" : "",
261
- "心跳仍正常,但超过无输出判定时长仍没有新输出的 Worker Task 数量。",
262
- ),
263
- );
264
- workerCards.appendChild(
265
- kpiCard(
266
- "心跳失联",
267
- staleCount,
268
- staleCount > 0 ? "kpi-risk" : "",
269
- "超过心跳失联判定时长仍没有新心跳的 Worker Task 数量。",
270
- ),
271
- );
272
- workerCards.appendChild(
273
- kpiCard(
274
- "即将超时",
275
- timeoutRiskCount,
276
- timeoutRiskCount > 0 ? "kpi-risk" : "",
277
- "命令已执行时长超过 80% 超时的 Worker Task 数量。",
278
- ),
279
- );
280
- workerCards.appendChild(
281
- kpiCard(
282
- "失败",
283
- failuresCount > 0 ? failuresInboxLink(failuresCount) : failuresCount,
284
- failuresCount > 0 ? "kpi-danger" : "",
285
- "状态为 failed 的 Worker Task 数量。",
286
- ),
287
- );
288
- workerGroup.appendChild(workerCards);
289
- kpiEl.appendChild(workerGroup);
290
- kpiEl.appendChild(dagGroup);
291
- }
292
-
293
- const nowMs = Date.now();
294
-
295
- clearNode(activeEl);
296
- activeEl.appendChild(el("h3", "section-heading", UI_TEXT.activeDags));
297
- if (activeDags.length === 0) {
298
- activeEl.appendChild(el("p", "empty", UI_TEXT.noActiveDags));
299
- } else {
300
- const list = el("div", "active-dag-list");
301
- for (const dag of activeDags.sort((a, b) =>
302
- dagSortTime(b).localeCompare(dagSortTime(a)),
303
- )) {
304
- list.appendChild(renderActiveDagCard(dag, nowMs));
305
- }
306
- activeEl.appendChild(list);
307
- }
308
-
309
- clearNode(riskEl);
310
- const dagRiskCount = dagAttentionRuns;
311
- const workerRiskCount = failuresCount + staleCount + timeoutRiskCount;
312
- riskEl.classList.toggle(
313
- "panel-risk-attention",
314
- dagRiskCount > 0 || workerRiskCount > 0,
315
- );
316
- riskEl.appendChild(el("h3", "section-heading", "运行风险"));
317
- if (dagRiskCount === 0 && workerRiskCount === 0) {
318
- riskEl.appendChild(el("p", "risk-empty", "当前没有需要处理的异常。"));
319
- } else {
320
- const riskBlocks = el("div", "risk-blocks");
321
- if (dagRiskCount > 0) {
322
- const dagBlock = el("div", "risk-block risk-block-dag");
323
- dagBlock.appendChild(el("div", "risk-block-label", "需处理 DAG"));
324
- dagBlock.appendChild(el("div", "risk-total", String(dagRiskCount)));
325
- dagBlock.appendChild(
326
- el(
327
- "p",
328
- "risk-summary",
329
- `去重运行数 ${dagAttentionRuns} · 中断 ${dagInterruptedRuns} · 失联 ${dagStaleRuns} · 状态不一致 ${dagInconsistentRuns} · 暂停 ${dagPausedRuns}`,
330
- ),
331
- );
332
- riskBlocks.appendChild(dagBlock);
333
- }
334
- if (workerRiskCount > 0) {
335
- const workerBlock = el("div", "risk-block risk-block-worker");
336
- workerBlock.appendChild(el("div", "risk-block-label", "Worker 风险"));
337
- workerBlock.appendChild(el("div", "risk-total", String(workerRiskCount)));
338
- workerBlock.appendChild(
339
- el(
340
- "p",
341
- "risk-summary",
342
- `失败 ${failuresCount} · 心跳失联 ${staleCount} · 即将超时 ${timeoutRiskCount}`,
343
- ),
344
- );
345
- const link = el("a", "risk-link", "查看异常 Task →");
346
- link.href = "#/failures";
347
- link.addEventListener("click", (event) => {
348
- event.preventDefault();
349
- navigate("/failures");
350
- });
351
- workerBlock.appendChild(link);
352
- riskBlocks.appendChild(workerBlock);
353
- }
354
- riskEl.appendChild(riskBlocks);
355
- }
356
-
357
- clearNode(dagsEl);
358
- dagsEl.id = "dashboard-dags";
359
- dagsEl.appendChild(el("h3", "section-heading", UI_TEXT.recentDags));
360
- if (recentDags.length === 0 && activeDags.length === 0) {
361
- dagsEl.appendChild(el("p", "empty", UI_TEXT.noRecentDags));
362
- } else if (recentDags.length === 0) {
363
- dagsEl.appendChild(el("p", "empty", "暂无历史 DAG 记录"));
364
- } else {
365
- const timeline = el("div", "dag-run-timeline");
366
- for (const dag of recentDags) {
367
- const progress = dagProgress(dag);
368
- const item = el("a", "dag-run-timeline-item");
369
- item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
370
- item.title = dag.title || dag.dagRunId;
371
- item.addEventListener("click", (event) => {
372
- event.preventDefault();
373
- navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
374
- });
375
-
376
- const rail = el("span", "dag-run-timeline-rail");
377
- rail.appendChild(
378
- el(
379
- "span",
380
- `dag-run-timeline-dot status-${badgeClass(dag.effectiveStatus ?? dag.status)}`,
381
- ),
382
- );
383
- item.appendChild(rail);
384
-
385
- const content = el("span", "dag-run-timeline-content");
386
- content.appendChild(
387
- el("span", "dag-run-timeline-title", dag.title || dag.dagRunId),
388
- );
389
- content.appendChild(
390
- el(
391
- "span",
392
- "dag-run-timeline-meta",
393
- `${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`,
394
- ),
395
- );
396
- item.appendChild(content);
397
-
398
- const state = el("span", "dag-run-timeline-status");
399
- state.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
400
- item.appendChild(state);
401
- timeline.appendChild(item);
402
- }
403
- dagsEl.appendChild(timeline);
404
- }
405
-
406
- clearNode(batchesEl);
407
- batchesEl.appendChild(el("h3", "section-heading", UI_TEXT.batches));
408
- const batches = snapshot.batches ?? [];
409
- if (batches.length === 0) {
410
- batchesEl.appendChild(el("p", "empty", UI_TEXT.noBatches));
411
- } else {
412
- const rows = batches.map((batch) => ({
413
- onClick: () => navigate(`/batch/${encodeURIComponent(batch.batchRunId)}`),
414
- cells: [
415
- batch.batchRunId,
416
- batch.featureId ?? "—",
417
- badge(batch.status),
418
- formatTs(batch.startedAt),
419
- summaryText(batch.summary),
420
- ],
421
- }));
422
- batchesEl.appendChild(
423
- buildTable(["批次 ID", "功能", "状态", "开始时间", "摘要"], rows),
424
- );
425
- }
426
-
427
- if (scrollTo === "dags") {
428
- dagsEl.scrollIntoView({ behavior: "smooth" });
429
- }
430
- }
431
-
432
- export function startDashboardPolling(scrollTo) {
433
- if (uiState.dashboardTimer) clearTimeout(uiState.dashboardTimer);
434
- const generation = ++uiState.pollingGeneration;
435
- const poll = async (initialScrollTo) => {
436
- await renderDashboard(initialScrollTo);
437
- if (generation !== uiState.pollingGeneration) return;
438
- uiState.dashboardTimer = setTimeout(() => {
439
- uiState.dashboardTimer = null;
440
- void poll();
441
- }, dashboardPollDelay());
442
- };
443
- void poll(scrollTo);
444
- }
445
-
1
+ /** Observe UI view module (R5). */
2
+ import {
3
+ UI_TEXT,
4
+ LIVENESS_LABELS,
5
+ ARTIFACT_LABELS,
6
+ ARTIFACT_KEYS,
7
+ STEP_LABELS,
8
+ STEP_ORDER,
9
+ DAG_INSPECTOR_DEFAULT_WIDTH,
10
+ DAG_INSPECTOR_MIN_WIDTH,
11
+ DAG_INSPECTOR_MAX_WIDTH,
12
+ DAG_INSPECTOR_RIGHT_OFFSET,
13
+ DAG_INSPECTOR_VIEWPORT_MARGIN,
14
+ DAG_INSPECTOR_KEYBOARD_STEP,
15
+ TERMINAL_RUN_STATUSES,
16
+ DAG_EFFECTIVE_STATUS_LABELS,
17
+ STATUS_LABELS,
18
+ } from "../constants.js";
19
+ import { clearNode, el, metaGrid, buildTable } from "../dom.js";
20
+ import {
21
+ badge,
22
+ badgeClass,
23
+ statusLabel,
24
+ formatTs,
25
+ formatMs,
26
+ formatDuration,
27
+ formatAgo,
28
+ dagStatusBadge,
29
+ livenessBadge,
30
+ displayDurationMs,
31
+ derivedDurationMs,
32
+ summaryText,
33
+ truncateText,
34
+ repoBaseName,
35
+ parseMarkdownBlocks,
36
+ markdownLinkHref,
37
+ } from "../format.js";
38
+ import { parseRoute, navigate, buildHashPath } from "../router.js";
39
+ import { fetchJson, artifactUrl, parseArtifactPreviewResponse } from "../api.js";
40
+ import {
41
+ objectRouteLink,
42
+ findFeatureInSnapshot,
43
+ featureLookupState,
44
+ buildObjectRelations,
45
+ renderObjectRelationBar,
46
+ } from "../relations.js";
47
+ import { copyText, renderRecommendedCommand } from "../copy.js";
48
+ import {
49
+ isPageVisible,
50
+ detailPollDelay,
51
+ dashboardPollDelay,
52
+ captureDagGraphViewportState,
53
+ updateDagGraphViewportState,
54
+ updateDagTimelineViewportState,
55
+ renderViewState,
56
+ mountViewState,
57
+ } from "../state.js";
58
+ import { uiState } from "../state.js";
59
+ import { sortPoolTasks } from "../format-pool.js";
60
+ import {
61
+ dagMetaVisibility,
62
+ isDagRunActive,
63
+ dashboardVisibleFeatures,
64
+ findDagForRun,
65
+ } from "../dag-model.js";
66
+ import { layoutDag } from "../dag-layout.js";
67
+ import { showView, setBreadcrumb, updateHeaderRefresh } from "../shell-chrome.js";
68
+ import { kpiCard, renderRepoBanner, renderProjectionFault, failuresInboxLink, artifactLink } from "../kpi.js";
69
+ import { dagSortTime, renderActiveDagCard, dagProgress } from "../dag-helpers.js";
70
+
71
+ export async function renderDashboard(scrollTo) {
72
+ showView("dashboard");
73
+ setBreadcrumb([{ label: UI_TEXT.dashboard }]);
74
+
75
+ const kpiEl = document.getElementById("dashboard-kpi");
76
+ const featureEl = document.getElementById("dashboard-features");
77
+ const activeEl = document.getElementById("dashboard-active-dags");
78
+ const riskEl = document.getElementById("dashboard-risk");
79
+ const dagsEl = document.getElementById("dashboard-dags");
80
+ const batchesEl = document.getElementById("dashboard-batches");
81
+ const repoEl = document.getElementById("dashboard-repo");
82
+ const host = kpiEl || featureEl || activeEl;
83
+ if (host) mountViewState(host, "loading", "加载看板…");
84
+
85
+ const snapshot = await fetchJson("/api/snapshot");
86
+ if (!snapshot) {
87
+ if (kpiEl) mountViewState(kpiEl, "error", "无法加载 snapshot。");
88
+ return;
89
+ }
90
+ uiState.lastSnapshot = snapshot;
91
+ updateHeaderRefresh(snapshot.generatedAt);
92
+
93
+ const dagRuns = snapshot.dagRuns ?? [];
94
+ const activeDags = dagRuns.filter(isDagRunActive);
95
+ const activeDagIds = new Set(activeDags.map((d) => d.dagRunId));
96
+ const recentDags = dagRuns
97
+ .filter((d) => !activeDagIds.has(d.dagRunId))
98
+ .sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a)))
99
+ .slice(0, 10);
100
+
101
+ const features = dashboardVisibleFeatures(snapshot.features);
102
+ featureEl.hidden = features.length === 0;
103
+ clearNode(featureEl);
104
+ if (features.length > 0) {
105
+ featureEl.appendChild(el("h3", "section-heading", "Feature 决策"));
106
+ featureEl.appendChild(
107
+ buildTable(
108
+ ["Feature", "状态", "下一步(建议)", "原因", "required AC", "证据"],
109
+ features.map((feature) => {
110
+ const idCell = objectRouteLink(
111
+ "feature",
112
+ feature.featureId,
113
+ snapshot,
114
+ );
115
+ // Prefer planner-selected next task; never re-rank ready pool order here.
116
+ const selected = feature.planning?.selected?.[0];
117
+ const nextHint = selected
118
+ ? `Next Task: ${selected.taskId} (${selected.priority})`
119
+ : feature.nextAction?.label ??
120
+ (feature.nextAction?.command
121
+ ? truncateText(feature.nextAction.command, 48)
122
+ : "—");
123
+ return {
124
+ onClick: feature.featureId
125
+ ? () =>
126
+ navigate(`/feature/${encodeURIComponent(feature.featureId)}`)
127
+ : undefined,
128
+ cells: [
129
+ idCell,
130
+ feature.statusLabel ?? feature.status,
131
+ nextHint,
132
+ feature.blockingItems?.[0]?.message ??
133
+ `${feature.summary?.tasksSucceeded ?? 0}/${feature.summary?.tasksTotal ?? 0} tasks completed`,
134
+ `${feature.summary?.requiredAcCovered ?? 0}/${feature.summary?.requiredAcTotal ?? 0}`,
135
+ artifactLink(
136
+ "查看",
137
+ feature.evidence?.delivery ??
138
+ feature.evidence?.closeout ??
139
+ feature.evidence?.morningReport ??
140
+ feature.evidence?.observeSnapshot,
141
+ ),
142
+ ],
143
+ };
144
+ }),
145
+ ),
146
+ );
147
+ }
148
+ if (features.length > 0 && (snapshot.projectionWarnings ?? []).length > 0) {
149
+ featureEl.appendChild(
150
+ el("p", "empty", `投影警告:${snapshot.projectionWarnings.join(";")}`),
151
+ );
152
+ }
153
+
154
+ renderRepoBanner(repoEl, snapshot);
155
+
156
+ clearNode(kpiEl);
157
+ const dagHealth = snapshot.health?.dag;
158
+ const dagActiveRuns = dagHealth?.activeRuns ?? activeDags.length;
159
+ const dagRunningNodes =
160
+ dagHealth?.runningNodes ??
161
+ activeDags
162
+ .flatMap((dag) => dag.nodes ?? [])
163
+ .filter((node) =>
164
+ ["running", "started"].includes((node.status ?? "").toLowerCase()),
165
+ ).length;
166
+ const dagPendingNodes =
167
+ dagHealth?.pendingNodes ??
168
+ activeDags
169
+ .flatMap((dag) => dag.nodes ?? [])
170
+ .filter((node) =>
171
+ ["pending", "queued"].includes((node.status ?? "").toLowerCase()),
172
+ ).length;
173
+ const dagPausedRuns = dagHealth?.pausedRuns ?? 0;
174
+ const dagStaleRuns = dagHealth?.staleRuns ?? 0;
175
+ const dagInterruptedRuns = dagHealth?.interruptedRuns ?? 0;
176
+ const dagInconsistentRuns = dagHealth?.inconsistentRuns ?? 0;
177
+ const dagAttentionRuns =
178
+ dagHealth?.attentionRuns ??
179
+ dagRuns.filter(
180
+ (dag) =>
181
+ (dag.lifecycle ?? "").toLowerCase() !== "completed" &&
182
+ ((dag.lifecycle ?? "").toLowerCase() === "paused" ||
183
+ ["stale", "orphaned"].includes((dag.liveness ?? "").toLowerCase()) ||
184
+ ["interrupted", "remote-unknown"].includes(dag.effectiveStatus) ||
185
+ dag.stateConsistent === false),
186
+ ).length;
187
+
188
+ const quietCount = snapshot.health?.quietCount ?? 0;
189
+ const staleCount = snapshot.health?.staleCount ?? 0;
190
+ const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
191
+ const failuresCount = snapshot.health?.failuresCount ?? 0;
192
+
193
+ if (snapshot.projectionError) {
194
+ mountViewState(kpiEl, "degraded", "指标暂不可用:数据投影失败。", {
195
+ detail: snapshot.projectionError.message ?? null,
196
+ });
197
+ // keep legacy banner for non-color / structure parity
198
+ kpiEl.appendChild(renderProjectionFault(snapshot.projectionError));
199
+ } else {
200
+ const dagGroup = el("fieldset", "kpi-group");
201
+ dagGroup.appendChild(el("legend", "kpi-group-legend", "DAG 运行"));
202
+ const dagCards = el("div", "kpi-grid");
203
+ dagCards.appendChild(
204
+ kpiCard(
205
+ "活跃 DAG",
206
+ dagActiveRuns,
207
+ "kpi-highlight",
208
+ "当前处于运行、静默运行、远端状态未知或等待执行的 DAG 数量(来源:.harness/dag-runs 的有效状态)。",
209
+ ),
210
+ );
211
+ dagCards.appendChild(
212
+ kpiCard(
213
+ "执行中节点",
214
+ dagRunningNodes,
215
+ "",
216
+ "活跃 DAG 中状态为 running 或 started、正在执行的节点数量;DAG 内执行单元统一称为节点。",
217
+ ),
218
+ );
219
+ dagCards.appendChild(
220
+ kpiCard(
221
+ "等待节点",
222
+ dagPendingNodes,
223
+ "",
224
+ "活跃 DAG 中状态为 pending 或 queued、尚未开始执行的节点数量。",
225
+ ),
226
+ );
227
+ dagCards.appendChild(
228
+ kpiCard(
229
+ "暂停 DAG",
230
+ dagPausedRuns,
231
+ dagPausedRuns > 0 ? "kpi-muted" : "",
232
+ "生命周期被暂停的 DAG 运行数量(来源:.harness/dag-runs 下的 lifecycle 目录)。",
233
+ ),
234
+ );
235
+ dagCards.appendChild(
236
+ kpiCard(
237
+ "需处理 DAG",
238
+ dagAttentionRuns,
239
+ dagAttentionRuns > 0 ? "kpi-risk" : "",
240
+ "未完成且暂停、失联、中断、远端状态未知或状态不一致的 DAG 数量;同一个 DAG 只计一次。",
241
+ ),
242
+ );
243
+ dagGroup.appendChild(dagCards);
244
+
245
+ const workerGroup = el("fieldset", "kpi-group");
246
+ workerGroup.appendChild(el("legend", "kpi-group-legend", "Worker 执行"));
247
+ const workerCards = el("div", "kpi-grid");
248
+ workerCards.appendChild(
249
+ kpiCard(
250
+ "活跃 Worker Task",
251
+ snapshot.health?.activeTasks ?? 0,
252
+ "",
253
+ "当前运行或等待执行的 Worker Task Pool 任务数量(来源:.harness/task-pool 的状态、运行记录与事件;不含 .harness/tasks 下的 task.json)。",
254
+ ),
255
+ );
256
+ workerCards.appendChild(
257
+ kpiCard(
258
+ "无输出",
259
+ quietCount,
260
+ quietCount > 0 ? "kpi-muted" : "",
261
+ "心跳仍正常,但超过无输出判定时长仍没有新输出的 Worker Task 数量。",
262
+ ),
263
+ );
264
+ workerCards.appendChild(
265
+ kpiCard(
266
+ "心跳失联",
267
+ staleCount,
268
+ staleCount > 0 ? "kpi-risk" : "",
269
+ "超过心跳失联判定时长仍没有新心跳的 Worker Task 数量。",
270
+ ),
271
+ );
272
+ workerCards.appendChild(
273
+ kpiCard(
274
+ "即将超时",
275
+ timeoutRiskCount,
276
+ timeoutRiskCount > 0 ? "kpi-risk" : "",
277
+ "命令已执行时长超过 80% 超时的 Worker Task 数量。",
278
+ ),
279
+ );
280
+ workerCards.appendChild(
281
+ kpiCard(
282
+ "失败",
283
+ failuresCount > 0 ? failuresInboxLink(failuresCount) : failuresCount,
284
+ failuresCount > 0 ? "kpi-danger" : "",
285
+ "状态为 failed 的 Worker Task 数量。",
286
+ ),
287
+ );
288
+ workerGroup.appendChild(workerCards);
289
+ kpiEl.appendChild(workerGroup);
290
+ kpiEl.appendChild(dagGroup);
291
+ }
292
+
293
+ const nowMs = Date.now();
294
+
295
+ clearNode(activeEl);
296
+ activeEl.appendChild(el("h3", "section-heading", UI_TEXT.activeDags));
297
+ if (activeDags.length === 0) {
298
+ activeEl.appendChild(el("p", "empty", UI_TEXT.noActiveDags));
299
+ } else {
300
+ const list = el("div", "active-dag-list");
301
+ for (const dag of activeDags.sort((a, b) =>
302
+ dagSortTime(b).localeCompare(dagSortTime(a)),
303
+ )) {
304
+ list.appendChild(renderActiveDagCard(dag, nowMs));
305
+ }
306
+ activeEl.appendChild(list);
307
+ }
308
+
309
+ clearNode(riskEl);
310
+ const dagRiskCount = dagAttentionRuns;
311
+ const workerRiskCount = failuresCount + staleCount + timeoutRiskCount;
312
+ riskEl.classList.toggle(
313
+ "panel-risk-attention",
314
+ dagRiskCount > 0 || workerRiskCount > 0,
315
+ );
316
+ riskEl.appendChild(el("h3", "section-heading", "运行风险"));
317
+ if (dagRiskCount === 0 && workerRiskCount === 0) {
318
+ riskEl.appendChild(el("p", "risk-empty", "当前没有需要处理的异常。"));
319
+ } else {
320
+ const riskBlocks = el("div", "risk-blocks");
321
+ if (dagRiskCount > 0) {
322
+ const dagBlock = el("div", "risk-block risk-block-dag");
323
+ dagBlock.appendChild(el("div", "risk-block-label", "需处理 DAG"));
324
+ dagBlock.appendChild(el("div", "risk-total", String(dagRiskCount)));
325
+ dagBlock.appendChild(
326
+ el(
327
+ "p",
328
+ "risk-summary",
329
+ `去重运行数 ${dagAttentionRuns} · 中断 ${dagInterruptedRuns} · 失联 ${dagStaleRuns} · 状态不一致 ${dagInconsistentRuns} · 暂停 ${dagPausedRuns}`,
330
+ ),
331
+ );
332
+ riskBlocks.appendChild(dagBlock);
333
+ }
334
+ if (workerRiskCount > 0) {
335
+ const workerBlock = el("div", "risk-block risk-block-worker");
336
+ workerBlock.appendChild(el("div", "risk-block-label", "Worker 风险"));
337
+ workerBlock.appendChild(el("div", "risk-total", String(workerRiskCount)));
338
+ workerBlock.appendChild(
339
+ el(
340
+ "p",
341
+ "risk-summary",
342
+ `失败 ${failuresCount} · 心跳失联 ${staleCount} · 即将超时 ${timeoutRiskCount}`,
343
+ ),
344
+ );
345
+ const link = el("a", "risk-link", "查看异常 Task →");
346
+ link.href = "#/failures";
347
+ link.addEventListener("click", (event) => {
348
+ event.preventDefault();
349
+ navigate("/failures");
350
+ });
351
+ workerBlock.appendChild(link);
352
+ riskBlocks.appendChild(workerBlock);
353
+ }
354
+ riskEl.appendChild(riskBlocks);
355
+ }
356
+
357
+ clearNode(dagsEl);
358
+ dagsEl.id = "dashboard-dags";
359
+ dagsEl.appendChild(el("h3", "section-heading", UI_TEXT.recentDags));
360
+ if (recentDags.length === 0 && activeDags.length === 0) {
361
+ dagsEl.appendChild(el("p", "empty", UI_TEXT.noRecentDags));
362
+ } else if (recentDags.length === 0) {
363
+ dagsEl.appendChild(el("p", "empty", "暂无历史 DAG 记录"));
364
+ } else {
365
+ const timeline = el("div", "dag-run-timeline");
366
+ for (const dag of recentDags) {
367
+ const progress = dagProgress(dag);
368
+ const item = el("a", "dag-run-timeline-item");
369
+ item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
370
+ item.title = dag.title || dag.dagRunId;
371
+ item.addEventListener("click", (event) => {
372
+ event.preventDefault();
373
+ navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
374
+ });
375
+
376
+ const rail = el("span", "dag-run-timeline-rail");
377
+ rail.appendChild(
378
+ el(
379
+ "span",
380
+ `dag-run-timeline-dot status-${badgeClass(dag.effectiveStatus ?? dag.status)}`,
381
+ ),
382
+ );
383
+ item.appendChild(rail);
384
+
385
+ const content = el("span", "dag-run-timeline-content");
386
+ content.appendChild(
387
+ el("span", "dag-run-timeline-title", dag.title || dag.dagRunId),
388
+ );
389
+ content.appendChild(
390
+ el(
391
+ "span",
392
+ "dag-run-timeline-meta",
393
+ `${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`,
394
+ ),
395
+ );
396
+ item.appendChild(content);
397
+
398
+ const state = el("span", "dag-run-timeline-status");
399
+ state.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
400
+ item.appendChild(state);
401
+ timeline.appendChild(item);
402
+ }
403
+ dagsEl.appendChild(timeline);
404
+ }
405
+
406
+ clearNode(batchesEl);
407
+ batchesEl.appendChild(el("h3", "section-heading", UI_TEXT.batches));
408
+ const batches = snapshot.batches ?? [];
409
+ if (batches.length === 0) {
410
+ batchesEl.appendChild(el("p", "empty", UI_TEXT.noBatches));
411
+ } else {
412
+ const rows = batches.map((batch) => ({
413
+ onClick: () => navigate(`/batch/${encodeURIComponent(batch.batchRunId)}`),
414
+ cells: [
415
+ batch.batchRunId,
416
+ batch.featureId ?? "—",
417
+ badge(batch.status),
418
+ formatTs(batch.startedAt),
419
+ summaryText(batch.summary),
420
+ ],
421
+ }));
422
+ batchesEl.appendChild(
423
+ buildTable(["批次 ID", "功能", "状态", "开始时间", "摘要"], rows),
424
+ );
425
+ }
426
+
427
+ if (scrollTo === "dags") {
428
+ dagsEl.scrollIntoView({ behavior: "smooth" });
429
+ }
430
+ }
431
+
432
+ export function startDashboardPolling(scrollTo) {
433
+ if (uiState.dashboardTimer) clearTimeout(uiState.dashboardTimer);
434
+ const generation = ++uiState.pollingGeneration;
435
+ const poll = async (initialScrollTo) => {
436
+ await renderDashboard(initialScrollTo);
437
+ if (generation !== uiState.pollingGeneration) return;
438
+ uiState.dashboardTimer = setTimeout(() => {
439
+ uiState.dashboardTimer = null;
440
+ void poll();
441
+ }, dashboardPollDelay());
442
+ };
443
+ void poll(scrollTo);
444
+ }
445
+