@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.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 (199) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +57 -53
  3. package/README.md +12 -3
  4. package/dist/application/dag/generate-task-dag.js +28 -58
  5. package/dist/application/evaluation/candidate-hash.js +75 -0
  6. package/dist/application/evaluation/candidate.js +52 -0
  7. package/dist/application/evaluation/replay.js +289 -0
  8. package/dist/application/evaluation/types.js +130 -0
  9. package/dist/cli/command-definitions.js +17 -4
  10. package/dist/cli/program.js +8 -4
  11. package/dist/commands/eval.js +235 -0
  12. package/dist/commands/init.js +131 -24
  13. package/dist/executors/pi-sdk-executor.js +38 -24
  14. package/dist/executors/shell-executor.js +226 -15
  15. package/dist/executors/shell-presets.js +20 -0
  16. package/dist/executors/shell-verification.js +7 -0
  17. package/dist/governance/manifest-types.js +1 -0
  18. package/dist/infrastructure/evaluation/candidate-store.js +439 -0
  19. package/dist/infrastructure/evaluation/store.js +40 -0
  20. package/dist/task/config-types.js +23 -0
  21. package/dist/worker/observe/routes.js +18 -3
  22. package/dist/worker/observe/spec-evidence.js +1 -1
  23. package/dist/worker/observe/static/dom.js +160 -1
  24. package/dist/worker/observe/static/state.js +14 -0
  25. package/dist/worker/observe/static/views/dag-inspector.js +35 -4
  26. package/dist/worker/observe/static/views/dag.js +9 -0
  27. package/dist/worker/observe/static/views/dashboard.js +702 -445
  28. package/dist/worker/observe/static/views/session-timeline.js +15 -1
  29. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  30. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  31. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  32. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  33. package/dist/workflows/dag/decision-envelope.js +57 -2
  34. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  35. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  36. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  37. package/dist/workflows/dag/frontend-repair.js +341 -0
  38. package/dist/workflows/dag/frontend-risk.js +161 -0
  39. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  40. package/dist/workflows/dag/init-hybrid.js +2407 -297
  41. package/dist/workflows/dag/node-execution.js +9 -0
  42. package/dist/workflows/dag/prompt.js +9 -0
  43. package/dist/workflows/dag/repair-artifact.js +43 -3
  44. package/dist/workflows/dag/report.js +35 -1
  45. package/dist/workflows/dag/runner.js +28 -2
  46. package/dist/workflows/dag/skill-instructions.js +4 -2
  47. package/dist/workflows/dag/task-demand-routing.js +383 -0
  48. package/dist/workflows/dag/types.js +71 -13
  49. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  50. package/dist/workflows/dag/validate.js +59 -1
  51. package/docs/README.md +6 -3
  52. package/docs/agent-dag-recovery-playbook.md +5 -3
  53. package/docs/agent-dag-runner.md +3 -3
  54. package/docs/architecture/README.md +3 -3
  55. package/docs/architecture/dag-execution.md +1 -1
  56. package/docs/architecture/evolution.md +13 -13
  57. package/docs/architecture/facts-and-state.md +1 -1
  58. package/docs/architecture/runtime-boundaries.md +7 -7
  59. package/docs/architecture/system-overview.md +3 -3
  60. package/docs/architecture/worker-and-feature.md +3 -3
  61. package/docs/design/README.md +124 -42
  62. package/docs/development-principles.md +4 -4
  63. package/docs/exec-plans/active/README.md +12 -11
  64. package/docs/exec-plans/completed/README.md +33 -0
  65. package/docs/feature-workflow.md +114 -39
  66. package/docs/init-surface.manifest.json +30 -3
  67. package/docs/loop-agent-harness.md +9 -8
  68. package/docs/production-readiness.md +1 -1
  69. package/docs/progress/README.md +23 -1
  70. package/docs/reports/README.md +65 -6
  71. package/docs/skills/vetted-skill-registry.md +2 -0
  72. package/docs/templates/agent-dag.schema.json +29 -1
  73. package/docs/templates/agent-dag.supervised-implementation.json +127 -8
  74. package/docs/templates/backend-test-analysis.schema.json +44 -0
  75. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  76. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  77. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +81 -16
  78. package/docs/templates/backend-test-dag.json +311 -40
  79. package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
  80. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -8
  81. package/docs/templates/backend-test-execution.schema.json +133 -0
  82. package/docs/templates/backend-test-result.schema.json +99 -0
  83. package/docs/templates/branch-merge-report.md +93 -0
  84. package/docs/templates/frontend-design-contract.md +9 -0
  85. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  86. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  87. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  88. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  89. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  90. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  91. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  92. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  93. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  94. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  95. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  96. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  97. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  98. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  99. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  100. package/docs/templates/frontend-eval/metrics.md +138 -0
  101. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  102. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  103. package/docs/templates/frontend-task-constraints.md +10 -0
  104. package/docs/templates/frontend-task-requirement.md +9 -0
  105. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  106. package/docs/templates/frontend-test-dag.json +23 -0
  107. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  108. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  109. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  110. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  111. package/docs/templates/knowledge-graph-bootstrap-dag.json +1 -1
  112. package/docs/templates/knowledge-sync-dag.json +1 -0
  113. package/docs/verification-matrix.md +4 -1
  114. package/examples/decision-gate-agent-dag.json +4 -4
  115. package/examples/hybrid-loop-agent-dag.json +1 -1
  116. package/package.json +2 -2
  117. package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
  118. package/scripts/kb-graph-incremental-prepare.mjs +19 -5
  119. package/scripts/kb-graph-promote.mjs +12 -1
  120. package/skills/ai-engineering-context/SKILL.md +2 -2
  121. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  122. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  123. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  124. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  125. package/skills/analyze-product-dependencies/references/example.md +76 -0
  126. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  127. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  128. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  129. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  130. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  131. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  132. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  133. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  134. package/skills/analyze-product-requirements/SKILL.md +90 -0
  135. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  136. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  137. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  138. package/skills/analyze-product-requirements/references/example.md +86 -0
  139. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  140. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  141. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  142. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  143. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  144. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  145. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  146. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  147. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  148. package/skills/browser-tools/SKILL.md +196 -0
  149. package/skills/browser-tools/browser-content.js +103 -0
  150. package/skills/browser-tools/browser-cookies.js +35 -0
  151. package/skills/browser-tools/browser-eval.js +53 -0
  152. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  153. package/skills/browser-tools/browser-nav.js +44 -0
  154. package/skills/browser-tools/browser-pick.js +162 -0
  155. package/skills/browser-tools/browser-screenshot.js +34 -0
  156. package/skills/browser-tools/browser-start.js +86 -0
  157. package/skills/browser-tools/package-lock.json +2556 -0
  158. package/skills/browser-tools/package.json +19 -0
  159. package/skills/frontend-design-review/SKILL.md +6 -1
  160. package/skills/frontend-design-review/references/review-checklist.md +25 -4
  161. package/skills/frontend-implementation/SKILL.md +25 -30
  162. package/skills/frontend-implementation/references/code-standards.md +20 -22
  163. package/skills/frontend-implementation/references/node-contracts.md +17 -53
  164. package/skills/frontend-review/SKILL.md +10 -4
  165. package/skills/frontend-review/references/review-findings.md +8 -3
  166. package/skills/frontend-verification/SKILL.md +22 -9
  167. package/skills/frontend-verification/references/verification-checklist.md +17 -5
  168. package/skills/grill-with-docs/SKILL.md +5 -5
  169. package/skills/grill-with-docs/adr-format.md +3 -3
  170. package/skills/init-capability-evolution/SKILL.md +5 -5
  171. package/skills/loop-agent/SKILL.md +5 -5
  172. package/skills/loop-agent/references/README.md +3 -3
  173. package/skills/loop-agent/references/command-reference.md +98 -24
  174. package/skills/loop-agent/references/docs-converge.md +15 -15
  175. package/skills/loop-agent/references/harness-policy.md +2 -2
  176. package/skills/loop-agent/references/hybrid-dag.md +32 -22
  177. package/skills/loop-agent/references/multi-worktree.md +1 -1
  178. package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
  179. package/skills/loop-agent/references/task-workflow.md +1 -1
  180. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
  181. package/skills/playwright-cli/SKILL.md +420 -0
  182. package/skills/playwright-cli/references/element-attributes.md +23 -0
  183. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  184. package/skills/playwright-cli/references/request-mocking.md +87 -0
  185. package/skills/playwright-cli/references/running-code.md +241 -0
  186. package/skills/playwright-cli/references/session-management.md +225 -0
  187. package/skills/playwright-cli/references/storage-state.md +275 -0
  188. package/skills/playwright-cli/references/test-generation.md +433 -0
  189. package/skills/playwright-cli/references/tracing.md +139 -0
  190. package/skills/playwright-cli/references/video-recording.md +143 -0
  191. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  192. package/skills/requesting-code-review/SKILL.md +1 -1
  193. package/skills/systematic-debugging/CREATION-LOG.md +3 -3
  194. package/skills/systematic-debugging/SKILL.md +1 -1
  195. package/skills/systematic-debugging/test-academic.md +1 -1
  196. package/skills/systematic-debugging/test-pressure-1.md +1 -1
  197. package/skills/systematic-debugging/test-pressure-2.md +1 -1
  198. package/skills/systematic-debugging/test-pressure-3.md +1 -1
  199. package/skills/verification-before-completion/SKILL.md +1 -1
@@ -1,445 +1,702 @@
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 { el, reconcileKeyed, syncCompatibleNode } 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
+ // Cached KPI fieldsets so successful polls keep node identity instead of
72
+ // rebuilding the whole KPI block each cycle (REQ-OBS-POLL-002). Reset to null
73
+ // when a projection error replaces the block with a degraded/fault node.
74
+ let kpiWorkerGroup = null;
75
+ let kpiDagGroup = null;
76
+ // The loading placeholder mounted before the first successful snapshot. It is
77
+ // removed once on first success so subsequent polls never see it again.
78
+ let dashboardLoadingNode = null;
79
+
80
+ export async function renderDashboard(scrollTo) {
81
+ showView("dashboard");
82
+ setBreadcrumb([{ label: UI_TEXT.dashboard }]);
83
+
84
+ const kpiEl = document.getElementById("dashboard-kpi");
85
+ const featureEl = document.getElementById("dashboard-features");
86
+ const activeEl = document.getElementById("dashboard-active-dags");
87
+ const riskEl = document.getElementById("dashboard-risk");
88
+ const dagsEl = document.getElementById("dashboard-dags");
89
+ const batchesEl = document.getElementById("dashboard-batches");
90
+ const repoEl = document.getElementById("dashboard-repo");
91
+ const host = kpiEl || featureEl || activeEl;
92
+ // Loading only mounts before the first successful snapshot. Subsequent
93
+ // successful polls reuse the stable DOM and never flash the loading state.
94
+ if (host && !uiState.lastSnapshot) {
95
+ dashboardLoadingNode = mountViewState(host, "loading", "加载看板…");
96
+ }
97
+
98
+ const snapshot = await fetchJson("/api/snapshot");
99
+ if (!snapshot) {
100
+ if (kpiEl) mountViewState(kpiEl, "error", "无法加载 snapshot。");
101
+ return;
102
+ }
103
+ uiState.lastSnapshot = snapshot;
104
+ updateHeaderRefresh(snapshot.generatedAt);
105
+ if (dashboardLoadingNode?.parentNode) {
106
+ dashboardLoadingNode.parentNode.removeChild(dashboardLoadingNode);
107
+ }
108
+ dashboardLoadingNode = null;
109
+
110
+ const dagRuns = snapshot.dagRuns ?? [];
111
+ const activeDags = dagRuns.filter(isDagRunActive);
112
+ const activeDagIds = new Set(activeDags.map((d) => d.dagRunId));
113
+ const recentDags = dagRuns
114
+ .filter((d) => !activeDagIds.has(d.dagRunId))
115
+ .sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a)))
116
+ .slice(0, 10);
117
+
118
+ const features = dashboardVisibleFeatures(snapshot.features);
119
+ featureEl.hidden = features.length === 0;
120
+ if (features.length > 0) {
121
+ reconcileKeyed(featureEl, [{ key: "__heading__" }], {
122
+ getKey: () => "__heading__",
123
+ scope: "heading",
124
+ create: () => el("h3", "section-heading", "Feature 决策"),
125
+ update: () => {},
126
+ });
127
+ reconcileKeyed(featureEl, [{ key: "__table__" }], {
128
+ getKey: () => "__table__",
129
+ scope: "table",
130
+ create: () => {
131
+ const table = document.createElement("table");
132
+ const thead = document.createElement("thead");
133
+ const headRow = document.createElement("tr");
134
+ for (const h of [
135
+ "Feature",
136
+ "状态",
137
+ "下一步(建议)",
138
+ "原因",
139
+ "required AC",
140
+ "证据",
141
+ ]) {
142
+ const th = document.createElement("th");
143
+ th.textContent = h;
144
+ headRow.appendChild(th);
145
+ }
146
+ thead.appendChild(headRow);
147
+ table.appendChild(thead);
148
+ table.appendChild(document.createElement("tbody"));
149
+ return table;
150
+ },
151
+ update: (table) => {
152
+ let tbody = table.querySelector("tbody");
153
+ if (!tbody) {
154
+ tbody = document.createElement("tbody");
155
+ table.appendChild(tbody);
156
+ }
157
+ reconcileKeyed(tbody, features, {
158
+ getKey: (feature) => feature.featureId ?? "__nofeature__",
159
+ create: (feature) => {
160
+ const tr = document.createElement("tr");
161
+ tr.classList.add("clickable");
162
+ if (feature.featureId) {
163
+ tr.addEventListener("click", () =>
164
+ navigate(`/feature/${encodeURIComponent(feature.featureId)}`),
165
+ );
166
+ }
167
+ return tr;
168
+ },
169
+ update: (tr, feature) => {
170
+ const idCell = objectRouteLink("feature", feature.featureId, snapshot);
171
+ // Prefer planner-selected next task; never re-rank ready pool order here.
172
+ const selected = feature.planning?.selected?.[0];
173
+ const nextHint = selected
174
+ ? `Next Task: ${selected.taskId} (${selected.priority})`
175
+ : feature.nextAction?.label ??
176
+ (feature.nextAction?.command
177
+ ? truncateText(feature.nextAction.command, 48)
178
+ : "—");
179
+ const cells = [
180
+ idCell,
181
+ feature.statusLabel ?? feature.status,
182
+ nextHint,
183
+ feature.blockingItems?.[0]?.message ??
184
+ `${feature.summary?.tasksSucceeded ?? 0}/${feature.summary?.tasksTotal ?? 0} tasks completed`,
185
+ `${feature.summary?.requiredAcCovered ?? 0}/${feature.summary?.requiredAcTotal ?? 0}`,
186
+ artifactLink(
187
+ "查看",
188
+ feature.evidence?.delivery ??
189
+ feature.evidence?.closeout ??
190
+ feature.evidence?.morningReport ??
191
+ feature.evidence?.observeSnapshot,
192
+ ),
193
+ ];
194
+ syncTableRow(tr, cells);
195
+ },
196
+ });
197
+ },
198
+ });
199
+ } else {
200
+ reconcileKeyed(featureEl, [], {
201
+ getKey: (item) => item.key,
202
+ scope: "table",
203
+ create: () => el("div"),
204
+ });
205
+ }
206
+ reconcileKeyed(
207
+ featureEl,
208
+ features.length > 0 && (snapshot.projectionWarnings ?? []).length > 0
209
+ ? [
210
+ {
211
+ key: "__warning__",
212
+ text: `投影警告:${snapshot.projectionWarnings.join(";")}`,
213
+ },
214
+ ]
215
+ : [],
216
+ {
217
+ getKey: (item) => item.key,
218
+ scope: "warning",
219
+ create: (item) => el("p", "empty", item.text),
220
+ update: (node, item) => {
221
+ node.textContent = item.text;
222
+ },
223
+ },
224
+ );
225
+
226
+ renderRepoBanner(repoEl, snapshot);
227
+
228
+ const dagHealth = snapshot.health?.dag;
229
+ const dagActiveRuns = dagHealth?.activeRuns ?? activeDags.length;
230
+ const dagRunningNodes =
231
+ dagHealth?.runningNodes ??
232
+ activeDags
233
+ .flatMap((dag) => dag.nodes ?? [])
234
+ .filter((node) =>
235
+ ["running", "started"].includes((node.status ?? "").toLowerCase()),
236
+ ).length;
237
+ const dagPendingNodes =
238
+ dagHealth?.pendingNodes ??
239
+ activeDags
240
+ .flatMap((dag) => dag.nodes ?? [])
241
+ .filter((node) =>
242
+ ["pending", "queued"].includes((node.status ?? "").toLowerCase()),
243
+ ).length;
244
+ const dagPausedRuns = dagHealth?.pausedRuns ?? 0;
245
+ const dagStaleRuns = dagHealth?.staleRuns ?? 0;
246
+ const dagInterruptedRuns = dagHealth?.interruptedRuns ?? 0;
247
+ const dagInconsistentRuns = dagHealth?.inconsistentRuns ?? 0;
248
+ const dagAttentionRuns =
249
+ dagHealth?.attentionRuns ??
250
+ dagRuns.filter(
251
+ (dag) =>
252
+ (dag.lifecycle ?? "").toLowerCase() !== "completed" &&
253
+ ((dag.lifecycle ?? "").toLowerCase() === "paused" ||
254
+ ["stale", "orphaned"].includes((dag.liveness ?? "").toLowerCase()) ||
255
+ ["interrupted", "remote-unknown"].includes(dag.effectiveStatus) ||
256
+ dag.stateConsistent === false),
257
+ ).length;
258
+
259
+ const quietCount = snapshot.health?.quietCount ?? 0;
260
+ const staleCount = snapshot.health?.staleCount ?? 0;
261
+ const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
262
+ const failuresCount = snapshot.health?.failuresCount ?? 0;
263
+
264
+ if (snapshot.projectionError) {
265
+ // Drop any cached KPI groups so a later recovery rebuilds them cleanly.
266
+ kpiWorkerGroup = null;
267
+ kpiDagGroup = null;
268
+ reconcileKeyed(
269
+ kpiEl,
270
+ [{ key: "__fault__", error: snapshot.projectionError }],
271
+ {
272
+ getKey: (item) => item.key,
273
+ scope: "kpi",
274
+ create: (item) => {
275
+ const node = renderViewState(
276
+ "degraded",
277
+ "指标暂不可用:数据投影失败。",
278
+ { detail: item.error.message ?? null },
279
+ );
280
+ node.appendChild(renderProjectionFault(item.error));
281
+ return node;
282
+ },
283
+ update: (node, item) => {
284
+ const fresh = renderViewState(
285
+ "degraded",
286
+ "指标暂不可用:数据投影失败。",
287
+ { detail: item.error.message ?? null },
288
+ );
289
+ fresh.appendChild(renderProjectionFault(item.error));
290
+ syncCompatibleNode(node, fresh);
291
+ },
292
+ },
293
+ );
294
+ } else {
295
+ const workerGroup = kpiWorkerGroup ?? (kpiWorkerGroup = el("fieldset", "kpi-group"));
296
+ syncKpiGroup(workerGroup, "Worker 执行", [
297
+ {
298
+ label: "活跃 Worker Task",
299
+ value: snapshot.health?.activeTasks ?? 0,
300
+ extraClass: "",
301
+ helpText:
302
+ "当前运行或等待执行的 Worker Task Pool 任务数量(来源:.harness/task-pool 的状态、运行记录与事件;不含 .harness/tasks 下的 task.json)。",
303
+ },
304
+ {
305
+ label: "无输出",
306
+ value: quietCount,
307
+ extraClass: quietCount > 0 ? "kpi-muted" : "",
308
+ helpText: "心跳仍正常,但超过无输出判定时长仍没有新输出的 Worker Task 数量。",
309
+ },
310
+ {
311
+ label: "心跳失联",
312
+ value: staleCount,
313
+ extraClass: staleCount > 0 ? "kpi-risk" : "",
314
+ helpText: "超过心跳失联判定时长仍没有新心跳的 Worker Task 数量。",
315
+ },
316
+ {
317
+ label: "即将超时",
318
+ value: timeoutRiskCount,
319
+ extraClass: timeoutRiskCount > 0 ? "kpi-risk" : "",
320
+ helpText: "命令已执行时长超过 80% 超时的 Worker Task 数量。",
321
+ },
322
+ {
323
+ label: "失败",
324
+ value: failuresCount > 0 ? failuresInboxLink(failuresCount) : failuresCount,
325
+ extraClass: failuresCount > 0 ? "kpi-danger" : "",
326
+ helpText: "状态为 failed 的 Worker Task 数量。",
327
+ },
328
+ ]);
329
+
330
+ const dagGroup = kpiDagGroup ?? (kpiDagGroup = el("fieldset", "kpi-group"));
331
+ syncKpiGroup(dagGroup, "DAG 运行", [
332
+ {
333
+ label: "活跃 DAG",
334
+ value: dagActiveRuns,
335
+ extraClass: "kpi-highlight",
336
+ helpText:
337
+ "当前处于运行、静默运行、远端状态未知或等待执行的 DAG 数量(来源:.harness/dag-runs 的有效状态)。",
338
+ },
339
+ {
340
+ label: "执行中节点",
341
+ value: dagRunningNodes,
342
+ extraClass: "",
343
+ helpText:
344
+ "活跃 DAG 中状态为 running 或 started、正在执行的节点数量;DAG 内执行单元统一称为节点。",
345
+ },
346
+ {
347
+ label: "等待节点",
348
+ value: dagPendingNodes,
349
+ extraClass: "",
350
+ helpText: "活跃 DAG 中状态为 pending 或 queued、尚未开始执行的节点数量。",
351
+ },
352
+ {
353
+ label: "暂停 DAG",
354
+ value: dagPausedRuns,
355
+ extraClass: dagPausedRuns > 0 ? "kpi-muted" : "",
356
+ helpText: "生命周期被暂停的 DAG 运行数量(来源:.harness/dag-runs 下的 lifecycle 目录)。",
357
+ },
358
+ {
359
+ label: "需处理 DAG",
360
+ value: dagAttentionRuns,
361
+ extraClass: dagAttentionRuns > 0 ? "kpi-risk" : "",
362
+ helpText:
363
+ "未完成且暂停、失联、中断、远端状态未知或状态不一致的 DAG 数量;同一个 DAG 只计一次。",
364
+ },
365
+ ]);
366
+
367
+ reconcileKeyed(
368
+ kpiEl,
369
+ [
370
+ { key: "worker-group", node: workerGroup },
371
+ { key: "dag-group", node: dagGroup },
372
+ ],
373
+ {
374
+ getKey: (item) => item.key,
375
+ scope: "kpi",
376
+ create: (item) => item.node,
377
+ update: () => {},
378
+ },
379
+ );
380
+ if (workerGroup.parentNode !== kpiEl) kpiEl.appendChild(workerGroup);
381
+ if (dagGroup.parentNode !== kpiEl) kpiEl.appendChild(dagGroup);
382
+ }
383
+
384
+ const nowMs = Date.now();
385
+
386
+ reconcileKeyed(activeEl, [{ key: "__heading__" }], {
387
+ getKey: () => "__heading__",
388
+ scope: "heading",
389
+ create: () => el("h3", "section-heading", UI_TEXT.activeDags),
390
+ update: () => {},
391
+ });
392
+ reconcileKeyed(
393
+ activeEl,
394
+ activeDags.length === 0
395
+ ? [{ key: "__empty__", text: UI_TEXT.noActiveDags }]
396
+ : [{ key: "__list__" }],
397
+ {
398
+ getKey: (item) => item.key,
399
+ create: (item) => {
400
+ if (item.key === "__empty__") return el("p", "empty", item.text);
401
+ const list = el("div", "active-dag-list");
402
+ return list;
403
+ },
404
+ update: (list) => {
405
+ reconcileKeyed(
406
+ list,
407
+ activeDags
408
+ .slice()
409
+ .sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a))),
410
+ {
411
+ getKey: (dag) => dag.dagRunId,
412
+ create: (dag) => renderActiveDagCard(dag, nowMs),
413
+ update: (card, dag) =>
414
+ syncCompatibleNode(card, renderActiveDagCard(dag, nowMs)),
415
+ },
416
+ );
417
+ },
418
+ },
419
+ );
420
+
421
+ const dagRiskCount = dagAttentionRuns;
422
+ const workerRiskCount = failuresCount + staleCount + timeoutRiskCount;
423
+ riskEl.classList.toggle(
424
+ "panel-risk-attention",
425
+ dagRiskCount > 0 || workerRiskCount > 0,
426
+ );
427
+ reconcileKeyed(riskEl, [{ key: "__heading__" }], {
428
+ getKey: () => "__heading__",
429
+ scope: "heading",
430
+ create: () => el("h3", "section-heading", "运行风险"),
431
+ update: () => {},
432
+ });
433
+ reconcileKeyed(
434
+ riskEl,
435
+ dagRiskCount === 0 && workerRiskCount === 0
436
+ ? [{ key: "__empty__", text: "当前没有需要处理的异常。" }]
437
+ : [{ key: "__blocks__", dagRiskCount, workerRiskCount, failuresCount, staleCount, timeoutRiskCount, dagAttentionRuns, dagInterruptedRuns, dagStaleRuns, dagInconsistentRuns, dagPausedRuns }],
438
+ {
439
+ getKey: (item) => item.key,
440
+ create: (item) => {
441
+ if (item.key === "__empty__") {
442
+ return el("p", "risk-empty", item.text);
443
+ }
444
+ return el("div", "risk-blocks");
445
+ },
446
+ update: (riskBlocks, item) => {
447
+ if (item.key === "__empty__") return;
448
+ reconcileKeyed(
449
+ riskBlocks,
450
+ item.dagRiskCount > 0
451
+ ? [
452
+ {
453
+ key: "dag",
454
+ total: item.dagRiskCount,
455
+ summary: `去重运行数 ${item.dagAttentionRuns} · 中断 ${item.dagInterruptedRuns} · 失联 ${item.dagStaleRuns} · 状态不一致 ${item.dagInconsistentRuns} · 暂停 ${item.dagPausedRuns}`,
456
+ },
457
+ ]
458
+ : [],
459
+ {
460
+ getKey: (block) => block.key,
461
+ scope: "dag",
462
+ create: (block) => {
463
+ const dagBlock = el("div", "risk-block risk-block-dag");
464
+ dagBlock.appendChild(el("div", "risk-block-label", "需处理 DAG"));
465
+ dagBlock.appendChild(el("div", "risk-total", String(block.total)));
466
+ dagBlock.appendChild(el("p", "risk-summary", block.summary));
467
+ return dagBlock;
468
+ },
469
+ update: (dagBlock, block) => {
470
+ const total = dagBlock.querySelector(".risk-total");
471
+ if (total) total.textContent = String(block.total);
472
+ const summary = dagBlock.querySelector(".risk-summary");
473
+ if (summary) summary.textContent = block.summary;
474
+ },
475
+ },
476
+ );
477
+ reconcileKeyed(
478
+ riskBlocks,
479
+ item.workerRiskCount > 0
480
+ ? [
481
+ {
482
+ key: "worker",
483
+ total: item.workerRiskCount,
484
+ summary: `失败 ${item.failuresCount} · 心跳失联 ${item.staleCount} · 即将超时 ${item.timeoutRiskCount}`,
485
+ },
486
+ ]
487
+ : [],
488
+ {
489
+ getKey: (block) => block.key,
490
+ scope: "worker",
491
+ create: (block) => {
492
+ const workerBlock = el("div", "risk-block risk-block-worker");
493
+ workerBlock.appendChild(el("div", "risk-block-label", "Worker 风险"));
494
+ workerBlock.appendChild(el("div", "risk-total", String(block.total)));
495
+ workerBlock.appendChild(el("p", "risk-summary", block.summary));
496
+ const link = el("a", "risk-link", "查看异常 Task →");
497
+ link.href = "#/failures";
498
+ link.addEventListener("click", (event) => {
499
+ event.preventDefault();
500
+ navigate("/failures");
501
+ });
502
+ workerBlock.appendChild(link);
503
+ return workerBlock;
504
+ },
505
+ update: (workerBlock, block) => {
506
+ const total = workerBlock.querySelector(".risk-total");
507
+ if (total) total.textContent = String(block.total);
508
+ const summary = workerBlock.querySelector(".risk-summary");
509
+ if (summary) summary.textContent = block.summary;
510
+ },
511
+ },
512
+ );
513
+ },
514
+ },
515
+ );
516
+
517
+ dagsEl.id = "dashboard-dags";
518
+ reconcileKeyed(dagsEl, [{ key: "__heading__" }], {
519
+ getKey: () => "__heading__",
520
+ scope: "heading",
521
+ create: () => el("h3", "section-heading", UI_TEXT.recentDags),
522
+ update: () => {},
523
+ });
524
+ reconcileKeyed(
525
+ dagsEl,
526
+ recentDags.length === 0
527
+ ? activeDags.length === 0
528
+ ? [{ key: "__empty__", text: UI_TEXT.noRecentDags }]
529
+ : [{ key: "__empty__", text: "暂无历史 DAG 记录" }]
530
+ : [{ key: "__timeline__" }],
531
+ {
532
+ getKey: (item) => item.key,
533
+ create: (item) => {
534
+ if (item.key === "__empty__") return el("p", "empty", item.text);
535
+ return el("div", "dag-run-timeline");
536
+ },
537
+ update: (timeline) => {
538
+ reconcileKeyed(timeline, recentDags, {
539
+ getKey: (dag) => dag.dagRunId,
540
+ create: (dag) => buildDagTimelineItem(dag),
541
+ update: (item, dag) => syncDagTimelineItem(item, dag),
542
+ });
543
+ },
544
+ },
545
+ );
546
+
547
+ reconcileKeyed(batchesEl, [{ key: "__heading__" }], {
548
+ getKey: () => "__heading__",
549
+ scope: "heading",
550
+ create: () => el("h3", "section-heading", UI_TEXT.batches),
551
+ update: () => {},
552
+ });
553
+ const batches = snapshot.batches ?? [];
554
+ reconcileKeyed(
555
+ batchesEl,
556
+ batches.length === 0 ? [{ key: "__empty__", text: UI_TEXT.noBatches }] : [{ key: "__table__" }],
557
+ {
558
+ getKey: (item) => item.key,
559
+ create: (item) => {
560
+ if (item.key === "__empty__") return el("p", "empty", item.text);
561
+ const table = document.createElement("table");
562
+ const thead = document.createElement("thead");
563
+ const headRow = document.createElement("tr");
564
+ for (const h of ["批次 ID", "功能", "状态", "开始时间", "摘要"]) {
565
+ const th = document.createElement("th");
566
+ th.textContent = h;
567
+ headRow.appendChild(th);
568
+ }
569
+ thead.appendChild(headRow);
570
+ table.appendChild(thead);
571
+ table.appendChild(document.createElement("tbody"));
572
+ return table;
573
+ },
574
+ update: (table) => {
575
+ let tbody = table.querySelector("tbody");
576
+ if (!tbody) {
577
+ tbody = document.createElement("tbody");
578
+ table.appendChild(tbody);
579
+ }
580
+ reconcileKeyed(tbody, batches, {
581
+ getKey: (batch) => batch.batchRunId,
582
+ create: (batch) => {
583
+ const tr = document.createElement("tr");
584
+ tr.classList.add("clickable");
585
+ tr.addEventListener("click", () =>
586
+ navigate(`/batch/${encodeURIComponent(batch.batchRunId)}`),
587
+ );
588
+ return tr;
589
+ },
590
+ update: (tr, batch) => {
591
+ const cells = [
592
+ batch.batchRunId,
593
+ batch.featureId ?? "—",
594
+ badge(batch.status),
595
+ formatTs(batch.startedAt),
596
+ summaryText(batch.summary),
597
+ ];
598
+ syncTableRow(tr, cells);
599
+ },
600
+ });
601
+ },
602
+ },
603
+ );
604
+
605
+ if (scrollTo === "dags") {
606
+ dagsEl.scrollIntoView({ behavior: "smooth" });
607
+ }
608
+ }
609
+
610
+ function syncKpiGroup(group, legendText, cards) {
611
+ let legend = group.querySelector(".kpi-group-legend");
612
+ if (!legend) legend = el("legend", "kpi-group-legend", legendText);
613
+ legend.textContent = legendText;
614
+ let grid = group.querySelector(".kpi-grid");
615
+ if (!grid) grid = el("div", "kpi-grid");
616
+ reconcileKeyed(grid, cards, {
617
+ getKey: (card) => card.label,
618
+ create: (card) =>
619
+ kpiCard(card.label, card.value, card.extraClass, card.helpText),
620
+ update: (node, card) =>
621
+ syncCompatibleNode(
622
+ node,
623
+ kpiCard(card.label, card.value, card.extraClass, card.helpText),
624
+ ),
625
+ });
626
+ if (legend.parentNode !== group) group.appendChild(legend);
627
+ if (grid.parentNode !== group) group.appendChild(grid);
628
+ }
629
+
630
+ function syncTableRow(row, cells) {
631
+ const fresh = document.createElement("tr");
632
+ for (const cell of cells) {
633
+ const td = document.createElement("td");
634
+ if (cell instanceof Node) td.appendChild(cell);
635
+ else td.textContent = cell ?? "—";
636
+ fresh.appendChild(td);
637
+ }
638
+ syncCompatibleNode(row, fresh);
639
+ }
640
+
641
+ function buildDagTimelineItem(dag) {
642
+ const progress = dagProgress(dag);
643
+ const item = el("a", "dag-run-timeline-item");
644
+ item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
645
+ item.title = dag.title || dag.dagRunId;
646
+ item.addEventListener("click", (event) => {
647
+ event.preventDefault();
648
+ navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
649
+ });
650
+
651
+ const rail = el("span", "dag-run-timeline-rail");
652
+ rail.appendChild(
653
+ el(
654
+ "span",
655
+ `dag-run-timeline-dot status-${badgeClass(dag.effectiveStatus ?? dag.status)}`,
656
+ ),
657
+ );
658
+ item.appendChild(rail);
659
+
660
+ const content = el("span", "dag-run-timeline-content");
661
+ content.appendChild(
662
+ el("span", "dag-run-timeline-title", dag.title || dag.dagRunId),
663
+ );
664
+ content.appendChild(
665
+ el(
666
+ "span",
667
+ "dag-run-timeline-meta",
668
+ `${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`,
669
+ ),
670
+ );
671
+ item.appendChild(content);
672
+
673
+ const state = el("span", "dag-run-timeline-status");
674
+ state.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
675
+ item.appendChild(state);
676
+ return item;
677
+ }
678
+
679
+ function syncDagTimelineItem(item, dag) {
680
+ const progress = dagProgress(dag);
681
+ item.title = dag.title || dag.dagRunId;
682
+ const meta = item.querySelector(".dag-run-timeline-meta");
683
+ if (meta) {
684
+ meta.textContent = `${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`;
685
+ }
686
+ const title = item.querySelector(".dag-run-timeline-title");
687
+ if (title) title.textContent = dag.title || dag.dagRunId;
688
+ }
689
+
690
+ export function startDashboardPolling(scrollTo) {
691
+ if (uiState.dashboardTimer) clearTimeout(uiState.dashboardTimer);
692
+ const generation = ++uiState.pollingGeneration;
693
+ const poll = async (initialScrollTo) => {
694
+ await renderDashboard(initialScrollTo);
695
+ if (generation !== uiState.pollingGeneration) return;
696
+ uiState.dashboardTimer = setTimeout(() => {
697
+ uiState.dashboardTimer = null;
698
+ void poll();
699
+ }, dashboardPollDelay());
700
+ };
701
+ void poll(scrollTo);
702
+ }