@tea-agent/loop-agent 0.8.0 → 0.10.0-alpha.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.
- package/AGENTS.md +10 -0
- package/CHANGELOG.md +101 -1
- package/README.md +69 -5
- package/dist/application/dag/args.js +13 -16
- package/dist/application/dag/generate-task-dag.js +32 -2
- package/dist/application/dag/run-dag.js +1 -27
- package/dist/application/dag/validate-dag.js +2 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +7 -11
- package/dist/cli/program.js +9 -21
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/dag-reconcile-run.js +118 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +60 -40
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +75 -9
- package/dist/governance/checks.js +6 -3
- package/dist/governance/manifest-types.js +33 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +353 -0
- package/dist/shared/reference-context.js +48 -22
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +30 -13
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +2 -3
- package/dist/worker/cli.js +243 -0
- package/dist/worker/closeout/apply.js +73 -0
- package/dist/worker/closeout/preview.js +30 -0
- package/dist/worker/delivery/final-verification.js +194 -0
- package/dist/worker/delivery/git-transaction.js +354 -0
- package/dist/worker/delivery/package.js +502 -0
- package/dist/worker/feature/decision-loader.js +68 -0
- package/dist/worker/feature/discover.js +14 -0
- package/dist/worker/feature/next-action.js +74 -0
- package/dist/worker/feature/reducer.js +133 -0
- package/dist/worker/feature/review.js +502 -0
- package/dist/worker/feature/run.js +365 -0
- package/dist/worker/feature/types.js +1 -0
- package/dist/worker/follow-up/approve.js +270 -0
- package/dist/worker/follow-up/factory.js +234 -0
- package/dist/worker/follow-up/paths.js +25 -0
- package/dist/worker/follow-up/policy.js +26 -0
- package/dist/worker/follow-up/schema.js +93 -0
- package/dist/worker/follow-up/store.js +96 -0
- package/dist/worker/loop-agent/loop-agent-client.js +345 -101
- package/dist/worker/metrics/projector.js +139 -0
- package/dist/worker/observability/read-model.js +282 -15
- package/dist/worker/observe/paths.js +17 -5
- package/dist/worker/observe/routes.js +78 -20
- package/dist/worker/observe/server.js +8 -6
- package/dist/worker/observe/static/app.js +1045 -177
- package/dist/worker/observe/static/index.html +70 -43
- package/dist/worker/observe/static/styles.css +553 -610
- package/dist/worker/pool/run-store.js +14 -2
- package/dist/worker/pool/validation.js +59 -0
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/report/morning-report.js +41 -6
- package/dist/worker/run-task/run-task.js +23 -13
- package/dist/worker/runner/run-ready.js +89 -11
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/governance-profile.js +10 -0
- package/dist/workflows/dag/init-hybrid.js +601 -26
- package/dist/workflows/dag/lifecycle.js +146 -0
- package/dist/workflows/dag/node-execution.js +64 -7
- package/dist/workflows/dag/prompt.js +16 -0
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/runner.js +176 -119
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/skill-snapshot.js +527 -0
- package/dist/workflows/dag/types.js +45 -9
- package/dist/workflows/dag/validate.js +5 -8
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +35 -12
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +134 -0
- package/docs/architecture/evolution.md +52 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +41 -15
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +39 -13
- package/docs/development-principles.md +1 -1
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +21 -0
- package/docs/feature-workflow.md +44 -4
- package/docs/init-surface.manifest.json +63 -1
- package/docs/loop-agent-harness.md +65 -3
- package/docs/progress/README.md +27 -0
- package/docs/reports/README.md +74 -5
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +4 -2
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +1 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -6
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
- package/docs/templates/backend-test-dag.json +213 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +8 -5
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/init-evolution-review.md +4 -2
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +17 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +6 -11
- package/package.json +22 -44
- package/scripts/check-product-line-docs.sh +10 -3
- package/scripts/check-task-pool-root.sh +1 -1
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +48 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/init-capability-evolution/SKILL.md +1 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +28 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +13 -15
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { layoutDag } from "./dag-layout.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const ACTIVE_DETAIL_POLL_MS = 2000;
|
|
4
|
+
const HIDDEN_POLL_MS = 30000;
|
|
4
5
|
const DASHBOARD_POLL_MS = 5000;
|
|
6
|
+
const DAG_INSPECTOR_DEFAULT_WIDTH = 440;
|
|
7
|
+
const DAG_INSPECTOR_MIN_WIDTH = 360;
|
|
8
|
+
const DAG_INSPECTOR_MAX_WIDTH = 680;
|
|
9
|
+
const DAG_INSPECTOR_RIGHT_OFFSET = 24;
|
|
10
|
+
const DAG_INSPECTOR_VIEWPORT_MARGIN = 48;
|
|
11
|
+
const DAG_INSPECTOR_KEYBOARD_STEP = 24;
|
|
5
12
|
|
|
6
13
|
const UI_TEXT = {
|
|
7
14
|
dashboard: "总览",
|
|
@@ -55,10 +62,28 @@ const STATUS_LABELS = {
|
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
const LIVENESS_LABELS = {
|
|
58
|
-
active: "
|
|
65
|
+
active: "执行器正常",
|
|
66
|
+
"node-quiet": "执行器正常,节点长时间无活动",
|
|
59
67
|
quiet: "无输出(存活)",
|
|
60
|
-
stale: "
|
|
68
|
+
stale: "执行器心跳中断",
|
|
61
69
|
"timeout-risk": "即将超时",
|
|
70
|
+
orphaned: "执行器已退出",
|
|
71
|
+
"unknown-host": "无法判断(在其他主机运行)",
|
|
72
|
+
unknown: "无法判断(无心跳记录)",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const DAG_EFFECTIVE_STATUS_LABELS = {
|
|
76
|
+
pending: "等待执行",
|
|
77
|
+
running: "正在执行",
|
|
78
|
+
"running-quiet": "运行可疑",
|
|
79
|
+
paused: "已暂停",
|
|
80
|
+
interrupted: "执行已中断",
|
|
81
|
+
"remote-unknown": "远端状态未知",
|
|
82
|
+
finished: "已完成",
|
|
83
|
+
failed: "执行失败",
|
|
84
|
+
superseded: "任务已另行完成",
|
|
85
|
+
abandoned: "已放弃",
|
|
86
|
+
unknown: "状态未知",
|
|
62
87
|
};
|
|
63
88
|
|
|
64
89
|
const STEP_LABELS = {
|
|
@@ -132,6 +157,173 @@ let selectedDagNodeId = null;
|
|
|
132
157
|
let sessionEventOffset = 0;
|
|
133
158
|
let sessionEvents = [];
|
|
134
159
|
let lastSnapshot = null;
|
|
160
|
+
let dagInspectorOpen = false;
|
|
161
|
+
let dagInspectorTab = "output";
|
|
162
|
+
let dagInspectorWidth = null;
|
|
163
|
+
let dagGraphViewportState = null;
|
|
164
|
+
const dagGraphViewportStates = new Map();
|
|
165
|
+
let dagTimelineViewportState = null;
|
|
166
|
+
let pollingGeneration = 0;
|
|
167
|
+
|
|
168
|
+
function isPageVisible() {
|
|
169
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function detailPollDelay(status, isVisible) {
|
|
173
|
+
const normalizedStatus = (status ?? "").toLowerCase().replace(/-/g, "_");
|
|
174
|
+
if (TERMINAL_RUN_STATUSES.has(normalizedStatus)) return null;
|
|
175
|
+
return isVisible ? ACTIVE_DETAIL_POLL_MS : HIDDEN_POLL_MS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function dashboardPollDelay() {
|
|
179
|
+
return isPageVisible() ? DASHBOARD_POLL_MS : HIDDEN_POLL_MS;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function parseMarkdownBlocks(markdown) {
|
|
183
|
+
const lines = String(markdown ?? "").replace(/\r\n?/g, "\n").split("\n");
|
|
184
|
+
const blocks = [];
|
|
185
|
+
let paragraph = [];
|
|
186
|
+
let list = null;
|
|
187
|
+
let code = null;
|
|
188
|
+
|
|
189
|
+
const flushParagraph = () => {
|
|
190
|
+
const text = paragraph.join(" ").trim();
|
|
191
|
+
if (text) blocks.push({ type: "paragraph", text });
|
|
192
|
+
paragraph = [];
|
|
193
|
+
};
|
|
194
|
+
const flushList = () => {
|
|
195
|
+
if (list?.items.length) blocks.push(list);
|
|
196
|
+
list = null;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
200
|
+
const line = lines[index] ?? "";
|
|
201
|
+
if (code) {
|
|
202
|
+
if (/^```\s*$/.test(line)) {
|
|
203
|
+
blocks.push({ type: "code", language: code.language, text: code.lines.join("\n") });
|
|
204
|
+
code = null;
|
|
205
|
+
} else {
|
|
206
|
+
code.lines.push(line);
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const fence = line.match(/^```([^\s]*)\s*$/);
|
|
212
|
+
if (fence) {
|
|
213
|
+
flushParagraph();
|
|
214
|
+
flushList();
|
|
215
|
+
code = { language: fence[1] ?? "", lines: [] };
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!line.trim()) {
|
|
219
|
+
flushParagraph();
|
|
220
|
+
flushList();
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
|
224
|
+
if (heading) {
|
|
225
|
+
flushParagraph();
|
|
226
|
+
flushList();
|
|
227
|
+
blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
|
|
231
|
+
flushParagraph();
|
|
232
|
+
flushList();
|
|
233
|
+
blocks.push({ type: "rule" });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (line.startsWith(">")) {
|
|
237
|
+
flushParagraph();
|
|
238
|
+
flushList();
|
|
239
|
+
blocks.push({ type: "quote", text: line.replace(/^>\s?/, "") });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const unordered = line.match(/^[-*+]\s+(.+)$/);
|
|
243
|
+
const ordered = line.match(/^\d+[.)]\s+(.+)$/);
|
|
244
|
+
if (unordered || ordered) {
|
|
245
|
+
flushParagraph();
|
|
246
|
+
const orderedList = Boolean(ordered);
|
|
247
|
+
if (!list || list.ordered !== orderedList) {
|
|
248
|
+
flushList();
|
|
249
|
+
list = { type: "list", ordered: orderedList, items: [] };
|
|
250
|
+
}
|
|
251
|
+
list.items.push((ordered?.[1] ?? unordered?.[1] ?? "").trim());
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (line.includes("|") && /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1] ?? "")) {
|
|
255
|
+
flushParagraph();
|
|
256
|
+
flushList();
|
|
257
|
+
const parseCells = (row) => row.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
|
|
258
|
+
const headers = parseCells(line);
|
|
259
|
+
const rows = [];
|
|
260
|
+
index += 2;
|
|
261
|
+
while (index < lines.length && lines[index].includes("|")) {
|
|
262
|
+
rows.push(parseCells(lines[index]));
|
|
263
|
+
index += 1;
|
|
264
|
+
}
|
|
265
|
+
index -= 1;
|
|
266
|
+
blocks.push({ type: "table", headers, rows });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
flushList();
|
|
270
|
+
paragraph.push(line.trim());
|
|
271
|
+
}
|
|
272
|
+
if (code) blocks.push({ type: "code", language: code.language, text: code.lines.join("\n") });
|
|
273
|
+
flushParagraph();
|
|
274
|
+
flushList();
|
|
275
|
+
return blocks;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function markdownLinkHref(value) {
|
|
279
|
+
const href = typeof value === "string" ? value.trim() : "";
|
|
280
|
+
if (!href) return null;
|
|
281
|
+
if (href.startsWith("#") || href.startsWith("/")) return href;
|
|
282
|
+
try {
|
|
283
|
+
const parsed = new URL(
|
|
284
|
+
href,
|
|
285
|
+
typeof location === "undefined" ? "http://localhost" : location.href,
|
|
286
|
+
);
|
|
287
|
+
return ["http:", "https:", "mailto:"].includes(parsed.protocol) ? href : null;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function updateDagGraphViewportState(_previous, dagRunId, scrollLeft, scrollTop) {
|
|
294
|
+
return {
|
|
295
|
+
dagRunId,
|
|
296
|
+
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
297
|
+
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function captureDagGraphViewportState(previous, dagRunId, viewport) {
|
|
302
|
+
if (!viewport || viewport.dataset?.dagRunId !== dagRunId) return previous;
|
|
303
|
+
return updateDagGraphViewportState(
|
|
304
|
+
previous,
|
|
305
|
+
dagRunId,
|
|
306
|
+
viewport.scrollLeft,
|
|
307
|
+
viewport.scrollTop,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function updateDagTimelineViewportState(
|
|
312
|
+
_previous,
|
|
313
|
+
dagRunId,
|
|
314
|
+
nodeId,
|
|
315
|
+
scrollLeft,
|
|
316
|
+
scrollTop,
|
|
317
|
+
followLatest,
|
|
318
|
+
) {
|
|
319
|
+
return {
|
|
320
|
+
dagRunId,
|
|
321
|
+
nodeId,
|
|
322
|
+
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
323
|
+
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
324
|
+
followLatest: Boolean(followLatest),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
135
327
|
|
|
136
328
|
export function parseHashRoute(hashValue) {
|
|
137
329
|
const hash = hashValue.replace(/^#/, "") || "/";
|
|
@@ -205,16 +397,18 @@ function statusSymbol(status) {
|
|
|
205
397
|
return "◇";
|
|
206
398
|
}
|
|
207
399
|
|
|
208
|
-
function badgeClass(status) {
|
|
400
|
+
export function badgeClass(status) {
|
|
209
401
|
if (!status) return "unknown";
|
|
210
402
|
const key = status.toLowerCase().replace(/_/g, "-");
|
|
211
|
-
if (
|
|
403
|
+
if (["finished", "completed", "succeeded", "done", "superseded"].includes(key)) {
|
|
212
404
|
return "succeeded";
|
|
213
405
|
}
|
|
214
|
-
if (
|
|
406
|
+
if (["error", "failed", "interrupted"].includes(key)) return "failed";
|
|
215
407
|
if (key === "partial-failed") return "partial-failed";
|
|
216
|
-
if (key === "skipped") return "skipped";
|
|
217
|
-
if (key === "running" || key === "started") return "running";
|
|
408
|
+
if (key === "skipped" || key === "abandoned") return "skipped";
|
|
409
|
+
if (key === "running" || key === "started" || key === "running-quiet") return "running";
|
|
410
|
+
if (key === "paused") return "blocked";
|
|
411
|
+
if (key === "remote-unknown") return "unknown";
|
|
218
412
|
return key;
|
|
219
413
|
}
|
|
220
414
|
|
|
@@ -231,12 +425,25 @@ function formatTs(value) {
|
|
|
231
425
|
return d.toLocaleString("zh-CN", { hour12: false });
|
|
232
426
|
}
|
|
233
427
|
|
|
234
|
-
function formatMs(ms) {
|
|
428
|
+
export function formatMs(ms) {
|
|
235
429
|
if (ms === undefined || ms === null) return "—";
|
|
236
430
|
if (ms < 1000) return `${ms}ms`;
|
|
431
|
+
if (ms >= 60_000) {
|
|
432
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
433
|
+
return `${Math.floor(totalSeconds / 60)}min ${totalSeconds % 60}s`;
|
|
434
|
+
}
|
|
237
435
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
238
436
|
}
|
|
239
437
|
|
|
438
|
+
function dagStatusBadge(status) {
|
|
439
|
+
const value = status ?? "unknown";
|
|
440
|
+
return el(
|
|
441
|
+
"span",
|
|
442
|
+
`badge badge-${badgeClass(value)}`,
|
|
443
|
+
DAG_EFFECTIVE_STATUS_LABELS[value] ?? statusLabel(value),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
240
447
|
function formatDuration(ms) {
|
|
241
448
|
if (ms === undefined || ms === null || Number.isNaN(ms)) return "—";
|
|
242
449
|
if (ms < 0) ms = 0;
|
|
@@ -268,13 +475,20 @@ function livenessBadge(liveness) {
|
|
|
268
475
|
}
|
|
269
476
|
|
|
270
477
|
function showView(name) {
|
|
478
|
+
document.body.dataset.view = name;
|
|
479
|
+
if (name !== "dag") closeDagInspector();
|
|
271
480
|
document.querySelectorAll("[data-view]").forEach((section) => {
|
|
272
481
|
section.hidden = section.dataset.view !== name;
|
|
273
482
|
});
|
|
483
|
+
const activeHref = name === "failures" ? "#/failures" : "#/";
|
|
484
|
+
document.querySelectorAll(".nav-link").forEach((link) => {
|
|
485
|
+
link.classList.toggle("nav-link-active", link.getAttribute("href") === activeHref);
|
|
486
|
+
});
|
|
274
487
|
}
|
|
275
488
|
|
|
276
489
|
function setBreadcrumb(parts) {
|
|
277
490
|
const nav = document.getElementById("breadcrumb");
|
|
491
|
+
nav.hidden = parts.length <= 1;
|
|
278
492
|
clearNode(nav);
|
|
279
493
|
parts.forEach((part, i) => {
|
|
280
494
|
if (i > 0) nav.appendChild(document.createTextNode(" / "));
|
|
@@ -304,16 +518,17 @@ function updateHeaderRefresh(generatedAt) {
|
|
|
304
518
|
}
|
|
305
519
|
|
|
306
520
|
function stopTimers() {
|
|
521
|
+
pollingGeneration += 1;
|
|
307
522
|
if (dashboardTimer) {
|
|
308
|
-
|
|
523
|
+
clearTimeout(dashboardTimer);
|
|
309
524
|
dashboardTimer = null;
|
|
310
525
|
}
|
|
311
526
|
if (runPollTimer) {
|
|
312
|
-
|
|
527
|
+
clearTimeout(runPollTimer);
|
|
313
528
|
runPollTimer = null;
|
|
314
529
|
}
|
|
315
530
|
if (dagPollTimer) {
|
|
316
|
-
|
|
531
|
+
clearTimeout(dagPollTimer);
|
|
317
532
|
dagPollTimer = null;
|
|
318
533
|
}
|
|
319
534
|
currentRunId = null;
|
|
@@ -343,6 +558,25 @@ function metaGrid(entries) {
|
|
|
343
558
|
return dl;
|
|
344
559
|
}
|
|
345
560
|
|
|
561
|
+
const TERMINAL_DAG_EFFECTIVE_STATUSES = new Set([
|
|
562
|
+
"finished",
|
|
563
|
+
"failed",
|
|
564
|
+
"superseded",
|
|
565
|
+
"abandoned",
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
export function dagMetaVisibility(dag) {
|
|
569
|
+
const terminal =
|
|
570
|
+
(dag?.lifecycle ?? "").toLowerCase() === "completed"
|
|
571
|
+
|| TERMINAL_DAG_EFFECTIVE_STATUSES.has(dag?.effectiveStatus);
|
|
572
|
+
return {
|
|
573
|
+
showExecutorStatus: !terminal,
|
|
574
|
+
showConsistencyWarning: dag?.stateConsistent === false,
|
|
575
|
+
showResume: dag?.recoveryEligibility?.canResume === true,
|
|
576
|
+
showReconcile: dag?.recoveryEligibility?.canReconcile === true,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
346
580
|
function buildTable(headers, rows) {
|
|
347
581
|
const table = document.createElement("table");
|
|
348
582
|
const thead = document.createElement("thead");
|
|
@@ -427,26 +661,6 @@ function isNodeFailed(status) {
|
|
|
427
661
|
return s === "error" || s === "failed" || s === "partial_failed";
|
|
428
662
|
}
|
|
429
663
|
|
|
430
|
-
function pickDefaultDagNode(nodes) {
|
|
431
|
-
if (!nodes?.length) return null;
|
|
432
|
-
const sorted = [...nodes].sort((a, b) => {
|
|
433
|
-
const ra = Number(a.rank);
|
|
434
|
-
const rb = Number(b.rank);
|
|
435
|
-
if (!Number.isNaN(ra) && !Number.isNaN(rb) && ra !== rb) return ra - rb;
|
|
436
|
-
if (a.rank !== b.rank) return (a.rank ?? "").localeCompare(b.rank ?? "");
|
|
437
|
-
return a.nodeId.localeCompare(b.nodeId);
|
|
438
|
-
});
|
|
439
|
-
const failed = sorted.find((n) => isNodeFailed(n.status));
|
|
440
|
-
if (failed) return failed.nodeId;
|
|
441
|
-
const active = sorted.find((n) => isNodeActive(n.status));
|
|
442
|
-
if (active) return active.nodeId;
|
|
443
|
-
const withOutput = sorted.find(
|
|
444
|
-
(n) => (n.outputPreview || n.errorPreview) && isNodeFinished(n.status),
|
|
445
|
-
);
|
|
446
|
-
if (withOutput) return withOutput.nodeId;
|
|
447
|
-
return sorted[0]?.nodeId ?? null;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
664
|
function truncateText(text, maxLen) {
|
|
451
665
|
if (!text) return "—";
|
|
452
666
|
const normalized = String(text).replace(/\s+/g, " ").trim();
|
|
@@ -480,6 +694,10 @@ function formatSessionEventLabel(event) {
|
|
|
480
694
|
if (!event || typeof event !== "object") return "未知类型";
|
|
481
695
|
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
482
696
|
const toolName = event.toolName ?? event.name ?? "—";
|
|
697
|
+
if (type === "agent_start") return "Agent 已启动";
|
|
698
|
+
if (type === "agent_end") return "Agent 已结束";
|
|
699
|
+
if (type === "agent_settled") return "Agent 已稳定";
|
|
700
|
+
if (type === "turn_start") return "开始新一轮处理";
|
|
483
701
|
if (type === "tool_execution_start" || type === "tool_start") {
|
|
484
702
|
return `调用工具 · ${toolName}`;
|
|
485
703
|
}
|
|
@@ -492,16 +710,53 @@ function formatSessionEventLabel(event) {
|
|
|
492
710
|
return type;
|
|
493
711
|
}
|
|
494
712
|
|
|
495
|
-
function
|
|
496
|
-
if (!event || typeof event !== "object") return "
|
|
713
|
+
function sessionEventTimestamp(event) {
|
|
714
|
+
if (!event || typeof event !== "object") return "";
|
|
497
715
|
const ts =
|
|
498
716
|
(typeof event.timestamp === "string" && event.timestamp) ||
|
|
499
717
|
(typeof event.at === "string" && event.at) ||
|
|
500
718
|
(typeof event.recordedAt === "string" && event.recordedAt);
|
|
501
|
-
|
|
719
|
+
return ts || "";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function formatSessionEventTime(event) {
|
|
723
|
+
const ts = sessionEventTimestamp(event);
|
|
724
|
+
if (!ts) return "未记录时间";
|
|
502
725
|
return formatTs(ts).replace(/\s.*/, "") || formatTs(ts);
|
|
503
726
|
}
|
|
504
727
|
|
|
728
|
+
function sessionEventKind(event) {
|
|
729
|
+
const type = typeof event?.type === "string" ? event.type : "unknown";
|
|
730
|
+
if (type === "tool_execution_start" || type === "tool_start") return "tool-start";
|
|
731
|
+
if (type === "tool_execution_end" || type === "tool_end") return "tool-end";
|
|
732
|
+
if (type === "turn_end" || type === "assistant" || type === "assistant_message") {
|
|
733
|
+
return "assistant";
|
|
734
|
+
}
|
|
735
|
+
if (type.startsWith("agent_") || type === "turn_start") return "lifecycle";
|
|
736
|
+
return "protocol";
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function buildSessionTimelineItems(events) {
|
|
740
|
+
const items = [];
|
|
741
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
742
|
+
const event = events[index];
|
|
743
|
+
const type = typeof event?.type === "string" ? event.type : "unknown";
|
|
744
|
+
if (type === "message_start" || type === "message_end") {
|
|
745
|
+
let count = 1;
|
|
746
|
+
while (index + 1 < events.length) {
|
|
747
|
+
const nextType = events[index + 1]?.type;
|
|
748
|
+
if (nextType !== "message_start" && nextType !== "message_end") break;
|
|
749
|
+
count += 1;
|
|
750
|
+
index += 1;
|
|
751
|
+
}
|
|
752
|
+
items.push({ event, kind: "protocol", protocolCount: count });
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
items.push({ event, kind: sessionEventKind(event) });
|
|
756
|
+
}
|
|
757
|
+
return items;
|
|
758
|
+
}
|
|
759
|
+
|
|
505
760
|
async function mergeSessionEvents(dagRunId, nodeId) {
|
|
506
761
|
if (!dagRunId || !nodeId) return;
|
|
507
762
|
const page = await fetchJson(
|
|
@@ -517,54 +772,219 @@ async function mergeSessionEvents(dagRunId, nodeId) {
|
|
|
517
772
|
}
|
|
518
773
|
|
|
519
774
|
function selectDagNode(dagRunId, nodeId) {
|
|
520
|
-
|
|
775
|
+
const nodeChanged = selectedDagNodeId !== nodeId;
|
|
521
776
|
selectedDagNodeId = nodeId;
|
|
522
|
-
|
|
523
|
-
|
|
777
|
+
dagInspectorOpen = true;
|
|
778
|
+
if (nodeChanged) {
|
|
779
|
+
sessionEventOffset = 0;
|
|
780
|
+
sessionEvents = [];
|
|
781
|
+
}
|
|
524
782
|
void renderDagDetail(dagRunId, false);
|
|
525
783
|
}
|
|
526
784
|
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
785
|
+
function closeDagInspector() {
|
|
786
|
+
dagInspectorOpen = false;
|
|
787
|
+
const panel = document.getElementById("dag-inspector");
|
|
788
|
+
if (panel) {
|
|
789
|
+
panel.classList.remove("is-open");
|
|
790
|
+
panel.setAttribute("aria-hidden", "true");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
533
793
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
794
|
+
function closeDagInspectorOnOutsideClick(event) {
|
|
795
|
+
if (!dagInspectorOpen || !(event.target instanceof Element)) return;
|
|
796
|
+
if (
|
|
797
|
+
event.target.closest(
|
|
798
|
+
"#dag-inspector, .dag-node-table tbody tr, button, a, input, select, textarea, [role='button'], [role='tab']",
|
|
799
|
+
)
|
|
800
|
+
) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
closeDagInspector();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function dagInspectorMaximumWidth() {
|
|
807
|
+
return Math.max(
|
|
808
|
+
DAG_INSPECTOR_MIN_WIDTH,
|
|
809
|
+
Math.min(
|
|
810
|
+
DAG_INSPECTOR_MAX_WIDTH,
|
|
811
|
+
window.innerWidth - DAG_INSPECTOR_VIEWPORT_MARGIN,
|
|
812
|
+
),
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function setDagInspectorWidth(panel, requestedWidth) {
|
|
817
|
+
const maximum = dagInspectorMaximumWidth();
|
|
818
|
+
dagInspectorWidth = Math.min(
|
|
819
|
+
maximum,
|
|
820
|
+
Math.max(DAG_INSPECTOR_MIN_WIDTH, Math.round(requestedWidth)),
|
|
821
|
+
);
|
|
822
|
+
panel.style.setProperty("--dag-inspector-width", `${dagInspectorWidth}px`);
|
|
823
|
+
return { minimum: DAG_INSPECTOR_MIN_WIDTH, maximum, value: dagInspectorWidth };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function createDagInspectorResizeHandle(panel) {
|
|
827
|
+
const handle = el("div", "dag-inspector-resize-handle");
|
|
828
|
+
handle.tabIndex = 0;
|
|
829
|
+
handle.setAttribute("role", "separator");
|
|
830
|
+
handle.setAttribute("aria-orientation", "vertical");
|
|
831
|
+
handle.setAttribute("aria-label", "调整节点检查器宽度");
|
|
832
|
+
handle.title = "拖动调整节点检查器宽度";
|
|
833
|
+
|
|
834
|
+
const updateRange = (requestedWidth) => {
|
|
835
|
+
const range = setDagInspectorWidth(
|
|
836
|
+
panel,
|
|
837
|
+
requestedWidth ?? dagInspectorWidth ?? DAG_INSPECTOR_DEFAULT_WIDTH,
|
|
838
|
+
);
|
|
839
|
+
handle.setAttribute("aria-valuemin", String(range.minimum));
|
|
840
|
+
handle.setAttribute("aria-valuemax", String(range.maximum));
|
|
841
|
+
handle.setAttribute("aria-valuenow", String(range.value));
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
updateRange();
|
|
845
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
846
|
+
if (event.button !== 0) return;
|
|
847
|
+
event.preventDefault();
|
|
848
|
+
handle.setPointerCapture(event.pointerId);
|
|
849
|
+
document.body.classList.add("is-resizing-dag-inspector");
|
|
850
|
+
});
|
|
851
|
+
handle.addEventListener("pointermove", (event) => {
|
|
852
|
+
if (!handle.hasPointerCapture(event.pointerId)) return;
|
|
853
|
+
updateRange(window.innerWidth - event.clientX - DAG_INSPECTOR_RIGHT_OFFSET);
|
|
854
|
+
});
|
|
855
|
+
const stopResizing = (event) => {
|
|
856
|
+
if (handle.hasPointerCapture(event.pointerId)) {
|
|
857
|
+
handle.releasePointerCapture(event.pointerId);
|
|
858
|
+
}
|
|
859
|
+
document.body.classList.remove("is-resizing-dag-inspector");
|
|
860
|
+
};
|
|
861
|
+
handle.addEventListener("pointerup", stopResizing);
|
|
862
|
+
handle.addEventListener("pointercancel", stopResizing);
|
|
863
|
+
handle.addEventListener("keydown", (event) => {
|
|
864
|
+
const currentWidth = dagInspectorWidth ?? DAG_INSPECTOR_DEFAULT_WIDTH;
|
|
865
|
+
if (event.key === "ArrowLeft") {
|
|
866
|
+
event.preventDefault();
|
|
867
|
+
updateRange(currentWidth + DAG_INSPECTOR_KEYBOARD_STEP);
|
|
868
|
+
} else if (event.key === "ArrowRight") {
|
|
869
|
+
event.preventDefault();
|
|
870
|
+
updateRange(currentWidth - DAG_INSPECTOR_KEYBOARD_STEP);
|
|
871
|
+
} else if (event.key === "Home") {
|
|
872
|
+
event.preventDefault();
|
|
873
|
+
updateRange(DAG_INSPECTOR_MIN_WIDTH);
|
|
874
|
+
} else if (event.key === "End") {
|
|
875
|
+
event.preventDefault();
|
|
876
|
+
updateRange(dagInspectorMaximumWidth());
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
return handle;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function captureDagTimelineViewportState(panel) {
|
|
883
|
+
const scrollEl = panel.querySelector(".process-timeline-scroll");
|
|
884
|
+
if (scrollEl?.dataset.dagRunId && scrollEl.dataset.nodeId) {
|
|
885
|
+
const followLatest =
|
|
886
|
+
scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 48;
|
|
887
|
+
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
888
|
+
dagTimelineViewportState,
|
|
889
|
+
scrollEl.dataset.dagRunId,
|
|
890
|
+
scrollEl.dataset.nodeId,
|
|
891
|
+
scrollEl.scrollLeft,
|
|
892
|
+
scrollEl.scrollTop,
|
|
893
|
+
followLatest,
|
|
894
|
+
);
|
|
538
895
|
}
|
|
896
|
+
}
|
|
539
897
|
|
|
898
|
+
function renderSessionTimeline(content, nodeId) {
|
|
899
|
+
content.dataset.dagRunId = currentDagRunId ?? "";
|
|
900
|
+
content.dataset.nodeId = nodeId ?? "";
|
|
901
|
+
content.classList.add("process-timeline-scroll");
|
|
540
902
|
if (!nodeId || sessionEvents.length === 0) {
|
|
541
|
-
|
|
903
|
+
content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
|
|
542
904
|
return;
|
|
543
905
|
}
|
|
544
906
|
|
|
545
|
-
const
|
|
907
|
+
const timelineItems = buildSessionTimelineItems(sessionEvents);
|
|
908
|
+
const hasEventTimes = sessionEvents.some((event) => Boolean(sessionEventTimestamp(event)));
|
|
909
|
+
if (!hasEventTimes) {
|
|
910
|
+
content.appendChild(
|
|
911
|
+
el("p", "process-timeline-note", "事件未记录具体时间,以下按采集顺序展示。"),
|
|
912
|
+
);
|
|
913
|
+
}
|
|
546
914
|
const ul = el("ul", "process-timeline");
|
|
547
|
-
for (
|
|
548
|
-
const
|
|
549
|
-
li
|
|
550
|
-
|
|
915
|
+
for (let index = 0; index < timelineItems.length; index += 1) {
|
|
916
|
+
const item = timelineItems[index];
|
|
917
|
+
const li = el(
|
|
918
|
+
"li",
|
|
919
|
+
`process-timeline-item process-timeline-${item.kind}${hasEventTimes ? " has-time" : ""}`,
|
|
920
|
+
);
|
|
921
|
+
if (index === 0) li.classList.add("is-first");
|
|
922
|
+
if (index === timelineItems.length - 1) li.classList.add("is-last");
|
|
923
|
+
const rail = el("span", "process-timeline-rail");
|
|
924
|
+
rail.appendChild(el("span", "process-timeline-dot"));
|
|
925
|
+
li.appendChild(rail);
|
|
926
|
+
if (hasEventTimes) {
|
|
927
|
+
li.appendChild(el("span", "process-timeline-time", formatSessionEventTime(item.event)));
|
|
928
|
+
}
|
|
929
|
+
const label =
|
|
930
|
+
item.protocolCount > 1
|
|
931
|
+
? `消息收发 · 已合并 ${item.protocolCount} 条协议事件`
|
|
932
|
+
: formatSessionEventLabel(item.event);
|
|
933
|
+
li.appendChild(el("span", "process-timeline-label", label));
|
|
551
934
|
ul.appendChild(li);
|
|
552
935
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
936
|
+
content.appendChild(ul);
|
|
937
|
+
|
|
938
|
+
const timelineState =
|
|
939
|
+
dagTimelineViewportState?.dagRunId === currentDagRunId &&
|
|
940
|
+
dagTimelineViewportState.nodeId === nodeId
|
|
941
|
+
? dagTimelineViewportState
|
|
942
|
+
: null;
|
|
943
|
+
content.addEventListener(
|
|
944
|
+
"scroll",
|
|
945
|
+
() => {
|
|
946
|
+
const followLatest =
|
|
947
|
+
content.scrollHeight - content.scrollTop - content.clientHeight < 48;
|
|
948
|
+
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
949
|
+
dagTimelineViewportState,
|
|
950
|
+
currentDagRunId,
|
|
951
|
+
nodeId,
|
|
952
|
+
content.scrollLeft,
|
|
953
|
+
content.scrollTop,
|
|
954
|
+
followLatest,
|
|
955
|
+
);
|
|
956
|
+
},
|
|
957
|
+
{ passive: true },
|
|
958
|
+
);
|
|
959
|
+
if (timelineState?.followLatest ?? true) {
|
|
960
|
+
content.scrollLeft = timelineState?.scrollLeft ?? 0;
|
|
961
|
+
content.scrollTop = content.scrollHeight;
|
|
962
|
+
} else {
|
|
963
|
+
content.scrollLeft = timelineState.scrollLeft;
|
|
964
|
+
content.scrollTop = timelineState.scrollTop;
|
|
558
965
|
}
|
|
559
966
|
}
|
|
560
967
|
|
|
561
|
-
function isDagRunActive(dag) {
|
|
968
|
+
export function isDagRunActive(dag) {
|
|
969
|
+
if (["running", "running-quiet", "remote-unknown", "pending"].includes(dag.effectiveStatus)) return true;
|
|
970
|
+
// Only "unknown" (and absent) means liveness evidence is insufficient;
|
|
971
|
+
// it must NOT override raw lifecycle/status/node facts. Every other concrete
|
|
972
|
+
// effectiveStatus is authoritative-non-active and short-circuits to false.
|
|
973
|
+
if (dag.effectiveStatus && dag.effectiveStatus !== "unknown") return false;
|
|
562
974
|
const status = (dag.status ?? "").toLowerCase();
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
if (hasActiveNode) return true;
|
|
975
|
+
if ((dag.lifecycle ?? "").toLowerCase() === "paused") return false;
|
|
976
|
+
if (["orphaned", "stale"].includes((dag.liveness ?? "").toLowerCase())) return false;
|
|
566
977
|
if (TERMINAL_RUN_STATUSES.has(status)) return false;
|
|
567
|
-
return
|
|
978
|
+
if (["running", "pending", "started"].includes(status)) return true;
|
|
979
|
+
return (dag.nodes ?? []).some((n) => isNodeActive(n.status));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const DASHBOARD_FEATURE_STATUSES = new Set(["running", "needs_action"]);
|
|
983
|
+
|
|
984
|
+
export function dashboardVisibleFeatures(features) {
|
|
985
|
+
return (features ?? []).filter((feature) =>
|
|
986
|
+
DASHBOARD_FEATURE_STATUSES.has(feature?.status),
|
|
987
|
+
);
|
|
568
988
|
}
|
|
569
989
|
|
|
570
990
|
function dagProgress(dag) {
|
|
@@ -591,13 +1011,69 @@ function findDagForRun(snapshot, workerRunId, task) {
|
|
|
591
1011
|
return undefined;
|
|
592
1012
|
}
|
|
593
1013
|
|
|
594
|
-
|
|
1014
|
+
let kpiHelpSeed = 0;
|
|
1015
|
+
function kpiCard(label, value, extraClass, helpText) {
|
|
595
1016
|
const card = el("div", `kpi-card${extraClass ? ` ${extraClass}` : ""}`);
|
|
596
1017
|
card.appendChild(el("div", "kpi-label", label));
|
|
597
|
-
|
|
1018
|
+
const valueWrap = el("div", "kpi-value-row");
|
|
1019
|
+
valueWrap.appendChild(el("div", "kpi-value", String(value)));
|
|
1020
|
+
if (helpText) {
|
|
1021
|
+
kpiHelpSeed += 1;
|
|
1022
|
+
const tipId = `kpi-tip-${kpiHelpSeed}`;
|
|
1023
|
+
const helpBtn = el("button", "kpi-help");
|
|
1024
|
+
helpBtn.type = "button";
|
|
1025
|
+
helpBtn.setAttribute("aria-describedby", tipId);
|
|
1026
|
+
helpBtn.setAttribute("aria-label", `${label} 指标说明`);
|
|
1027
|
+
const icon = el("i", "ri-question-line");
|
|
1028
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1029
|
+
helpBtn.appendChild(icon);
|
|
1030
|
+
const tip = el("span", "kpi-tip");
|
|
1031
|
+
tip.id = tipId;
|
|
1032
|
+
tip.setAttribute("role", "tooltip");
|
|
1033
|
+
tip.textContent = helpText;
|
|
1034
|
+
valueWrap.appendChild(helpBtn);
|
|
1035
|
+
valueWrap.appendChild(tip);
|
|
1036
|
+
}
|
|
1037
|
+
card.appendChild(valueWrap);
|
|
598
1038
|
return card;
|
|
599
1039
|
}
|
|
600
1040
|
|
|
1041
|
+
function repoBaseName(repoRoot) {
|
|
1042
|
+
if (!repoRoot) return "未绑定仓库";
|
|
1043
|
+
const trimmed = String(repoRoot).replace(/[\\/]+$/, "");
|
|
1044
|
+
const parts = trimmed.split(/[\\/]/);
|
|
1045
|
+
return parts[parts.length - 1] || trimmed || "未绑定仓库";
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function renderRepoBanner(repoEl, snapshot) {
|
|
1049
|
+
if (!repoEl) return;
|
|
1050
|
+
clearNode(repoEl);
|
|
1051
|
+
const repoRoot = snapshot.repoRoot ?? "";
|
|
1052
|
+
const icon = el("i", "ri-git-repository-line");
|
|
1053
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1054
|
+
repoEl.appendChild(icon);
|
|
1055
|
+
const text = el("div", "repo-banner-text");
|
|
1056
|
+
text.appendChild(el("span", "repo-banner-name", repoBaseName(repoRoot)));
|
|
1057
|
+
const pathLine = el("span", "repo-banner-path", repoRoot || "路径未知");
|
|
1058
|
+
pathLine.title = repoRoot || "";
|
|
1059
|
+
text.appendChild(pathLine);
|
|
1060
|
+
repoEl.appendChild(text);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function renderProjectionFault(error) {
|
|
1064
|
+
const banner = el("div", "projection-fault");
|
|
1065
|
+
banner.setAttribute("role", "alert");
|
|
1066
|
+
const icon = el("i", "ri-error-warning-line");
|
|
1067
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1068
|
+
banner.appendChild(icon);
|
|
1069
|
+
const body = el("div", "projection-fault-body");
|
|
1070
|
+
body.appendChild(el("p", "projection-fault-title", "指标暂不可用:数据投影失败。"));
|
|
1071
|
+
body.appendChild(el("p", "projection-fault-message", error.message ?? "投影失败 (projection failed)"));
|
|
1072
|
+
if (error.at) body.appendChild(el("p", "projection-fault-at", `发生时间:${error.at}`));
|
|
1073
|
+
banner.appendChild(body);
|
|
1074
|
+
return banner;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
601
1077
|
function renderActiveDagCard(dag, nowMs) {
|
|
602
1078
|
const card = el("div", "active-dag-card");
|
|
603
1079
|
if (isNodeActive(dag.status) || getActiveNodes(dag).length > 0) {
|
|
@@ -606,7 +1082,7 @@ function renderActiveDagCard(dag, nowMs) {
|
|
|
606
1082
|
const title = dag.title || dag.dagRunId;
|
|
607
1083
|
const header = el("div", "active-dag-header");
|
|
608
1084
|
header.appendChild(el("span", "active-dag-title", title));
|
|
609
|
-
header.appendChild(
|
|
1085
|
+
header.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
610
1086
|
card.appendChild(header);
|
|
611
1087
|
|
|
612
1088
|
const progress = dagProgress(dag);
|
|
@@ -733,6 +1209,7 @@ function renderDagGraph(dag, dagRunId) {
|
|
|
733
1209
|
wrap.appendChild(toolbar);
|
|
734
1210
|
if (edges.length === 0) wrap.appendChild(el("p", "dag-graph-fallback", "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。"));
|
|
735
1211
|
const viewport = el("div", "dag-graph-viewport");
|
|
1212
|
+
viewport.dataset.dagRunId = dagRunId;
|
|
736
1213
|
const svg = svgEl("svg", { class: "dag-graph", viewBox: `0 0 ${layout.width} ${layout.height}`, width: layout.width, height: layout.height, role: "img", "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点` });
|
|
737
1214
|
const defs = svgEl("defs");
|
|
738
1215
|
const marker = svgEl("marker", { id: "dag-arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse" });
|
|
@@ -760,17 +1237,227 @@ function renderDagGraph(dag, dagRunId) {
|
|
|
760
1237
|
svg.appendChild(group);
|
|
761
1238
|
}
|
|
762
1239
|
viewport.appendChild(svg); wrap.appendChild(viewport);
|
|
763
|
-
|
|
1240
|
+
const graphViewportState =
|
|
1241
|
+
dagGraphViewportStates.get(dagRunId) ??
|
|
1242
|
+
(dagGraphViewportState?.dagRunId === dagRunId
|
|
1243
|
+
? dagGraphViewportState
|
|
1244
|
+
: null);
|
|
1245
|
+
viewport.addEventListener(
|
|
1246
|
+
"scroll",
|
|
1247
|
+
() => {
|
|
1248
|
+
dagGraphViewportState = updateDagGraphViewportState(
|
|
1249
|
+
dagGraphViewportState,
|
|
1250
|
+
dagRunId,
|
|
1251
|
+
viewport.scrollLeft,
|
|
1252
|
+
viewport.scrollTop,
|
|
1253
|
+
);
|
|
1254
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
1255
|
+
},
|
|
1256
|
+
{ passive: true },
|
|
1257
|
+
);
|
|
1258
|
+
if (graphViewportState) {
|
|
1259
|
+
const restoreViewport = () => {
|
|
1260
|
+
viewport.scrollLeft = graphViewportState.scrollLeft;
|
|
1261
|
+
viewport.scrollTop = graphViewportState.scrollTop;
|
|
1262
|
+
};
|
|
1263
|
+
if (typeof requestAnimationFrame === "function") {
|
|
1264
|
+
requestAnimationFrame(restoreViewport);
|
|
1265
|
+
} else {
|
|
1266
|
+
restoreViewport();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
reset.addEventListener("click", () => {
|
|
1270
|
+
dagGraphViewportState = updateDagGraphViewportState(
|
|
1271
|
+
dagGraphViewportState,
|
|
1272
|
+
dagRunId,
|
|
1273
|
+
0,
|
|
1274
|
+
0,
|
|
1275
|
+
);
|
|
1276
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
1277
|
+
viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
|
|
1278
|
+
});
|
|
764
1279
|
return wrap;
|
|
765
1280
|
}
|
|
766
1281
|
|
|
767
|
-
function
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1282
|
+
function appendInlineMarkdown(target, text) {
|
|
1283
|
+
const tokenPattern = /(`[^`]*`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g;
|
|
1284
|
+
let cursor = 0;
|
|
1285
|
+
for (const match of text.matchAll(tokenPattern)) {
|
|
1286
|
+
const index = match.index ?? 0;
|
|
1287
|
+
if (index > cursor) target.appendChild(document.createTextNode(text.slice(cursor, index)));
|
|
1288
|
+
const token = match[0];
|
|
1289
|
+
if (token.startsWith("`")) {
|
|
1290
|
+
target.appendChild(el("code", null, token.slice(1, -1)));
|
|
1291
|
+
} else if (token.startsWith("**")) {
|
|
1292
|
+
const strong = document.createElement("strong");
|
|
1293
|
+
strong.textContent = token.slice(2, -2);
|
|
1294
|
+
target.appendChild(strong);
|
|
1295
|
+
} else if (token.startsWith("*")) {
|
|
1296
|
+
const emphasis = document.createElement("em");
|
|
1297
|
+
emphasis.textContent = token.slice(1, -1);
|
|
1298
|
+
target.appendChild(emphasis);
|
|
1299
|
+
} else {
|
|
1300
|
+
const link = token.match(/^\[([^\]]+)\]\((.+)\)$/);
|
|
1301
|
+
const href = markdownLinkHref(link?.[2]);
|
|
1302
|
+
if (link && href) {
|
|
1303
|
+
const anchor = document.createElement("a");
|
|
1304
|
+
anchor.href = href;
|
|
1305
|
+
anchor.target = "_blank";
|
|
1306
|
+
anchor.rel = "noopener noreferrer";
|
|
1307
|
+
anchor.textContent = link[1];
|
|
1308
|
+
target.appendChild(anchor);
|
|
1309
|
+
} else {
|
|
1310
|
+
target.appendChild(document.createTextNode(token));
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
cursor = index + token.length;
|
|
1314
|
+
}
|
|
1315
|
+
if (cursor < text.length) target.appendChild(document.createTextNode(text.slice(cursor)));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function renderMarkdown(content) {
|
|
1319
|
+
const article = el("article", "markdown-output");
|
|
1320
|
+
for (const block of parseMarkdownBlocks(content)) {
|
|
1321
|
+
switch (block.type) {
|
|
1322
|
+
case "heading": {
|
|
1323
|
+
const heading = document.createElement(`h${block.level}`);
|
|
1324
|
+
appendInlineMarkdown(heading, block.text);
|
|
1325
|
+
article.appendChild(heading);
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
case "paragraph": {
|
|
1329
|
+
const paragraph = document.createElement("p");
|
|
1330
|
+
appendInlineMarkdown(paragraph, block.text);
|
|
1331
|
+
article.appendChild(paragraph);
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
case "quote": {
|
|
1335
|
+
const quote = document.createElement("blockquote");
|
|
1336
|
+
appendInlineMarkdown(quote, block.text);
|
|
1337
|
+
article.appendChild(quote);
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
case "list": {
|
|
1341
|
+
const list = document.createElement(block.ordered ? "ol" : "ul");
|
|
1342
|
+
for (const item of block.items) {
|
|
1343
|
+
const listItem = document.createElement("li");
|
|
1344
|
+
appendInlineMarkdown(listItem, item);
|
|
1345
|
+
list.appendChild(listItem);
|
|
1346
|
+
}
|
|
1347
|
+
article.appendChild(list);
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
case "code": {
|
|
1351
|
+
const pre = document.createElement("pre");
|
|
1352
|
+
const code = document.createElement("code");
|
|
1353
|
+
if (block.language) code.dataset.language = block.language;
|
|
1354
|
+
code.textContent = block.text;
|
|
1355
|
+
pre.appendChild(code);
|
|
1356
|
+
article.appendChild(pre);
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
case "table": {
|
|
1360
|
+
const table = document.createElement("table");
|
|
1361
|
+
const head = document.createElement("thead");
|
|
1362
|
+
const headRow = document.createElement("tr");
|
|
1363
|
+
for (const value of block.headers) {
|
|
1364
|
+
const cell = document.createElement("th");
|
|
1365
|
+
appendInlineMarkdown(cell, value);
|
|
1366
|
+
headRow.appendChild(cell);
|
|
1367
|
+
}
|
|
1368
|
+
head.appendChild(headRow);
|
|
1369
|
+
table.appendChild(head);
|
|
1370
|
+
const body = document.createElement("tbody");
|
|
1371
|
+
for (const row of block.rows) {
|
|
1372
|
+
const tableRow = document.createElement("tr");
|
|
1373
|
+
for (const value of row) {
|
|
1374
|
+
const cell = document.createElement("td");
|
|
1375
|
+
appendInlineMarkdown(cell, value);
|
|
1376
|
+
tableRow.appendChild(cell);
|
|
1377
|
+
}
|
|
1378
|
+
body.appendChild(tableRow);
|
|
1379
|
+
}
|
|
1380
|
+
table.appendChild(body);
|
|
1381
|
+
article.appendChild(table);
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
case "rule":
|
|
1385
|
+
article.appendChild(document.createElement("hr"));
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return article;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function renderDagInspector(dagRunId, node) {
|
|
1393
|
+
const panel = document.getElementById("dag-inspector");
|
|
1394
|
+
if (!panel) return;
|
|
1395
|
+
captureDagTimelineViewportState(panel);
|
|
1396
|
+
if (!dagInspectorOpen || !node) {
|
|
1397
|
+
panel.classList.remove("is-open");
|
|
1398
|
+
panel.setAttribute("aria-hidden", "true");
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
clearNode(panel);
|
|
1403
|
+
panel.classList.add("is-open");
|
|
1404
|
+
panel.setAttribute("aria-hidden", "false");
|
|
1405
|
+
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
1406
|
+
const header = el("div", "dag-inspector-header");
|
|
1407
|
+
const title = el("div", "dag-inspector-title");
|
|
1408
|
+
title.appendChild(el("h3", null, "节点检查器"));
|
|
1409
|
+
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
1410
|
+
header.appendChild(title);
|
|
1411
|
+
const controls = el("div", "dag-inspector-controls");
|
|
1412
|
+
controls.appendChild(badge(node.status ?? "unknown"));
|
|
1413
|
+
const close = document.createElement("button");
|
|
1414
|
+
close.type = "button";
|
|
1415
|
+
close.className = "dag-inspector-close";
|
|
1416
|
+
close.setAttribute("aria-label", "关闭节点检查器");
|
|
1417
|
+
const closeIcon = el("i", "ri-close-line");
|
|
1418
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
1419
|
+
close.appendChild(closeIcon);
|
|
1420
|
+
close.addEventListener("click", closeDagInspector);
|
|
1421
|
+
controls.appendChild(close);
|
|
1422
|
+
header.appendChild(controls);
|
|
1423
|
+
|
|
1424
|
+
const tabs = el("div", "dag-inspector-tabs");
|
|
1425
|
+
tabs.setAttribute("role", "tablist");
|
|
1426
|
+
for (const [tab, label, icon] of [["output", "节点输出", "ri-file-text-line"], ["timeline", "执行过程", "ri-route-line"]]) {
|
|
1427
|
+
const button = document.createElement("button");
|
|
1428
|
+
button.type = "button";
|
|
1429
|
+
button.className = "dag-inspector-tab";
|
|
1430
|
+
button.setAttribute("role", "tab");
|
|
1431
|
+
button.setAttribute("aria-selected", String(dagInspectorTab === tab));
|
|
1432
|
+
const tabIcon = el("i", icon);
|
|
1433
|
+
tabIcon.setAttribute("aria-hidden", "true");
|
|
1434
|
+
button.appendChild(tabIcon);
|
|
1435
|
+
button.appendChild(document.createTextNode(` ${label}`));
|
|
1436
|
+
button.addEventListener("click", () => {
|
|
1437
|
+
dagInspectorTab = tab;
|
|
1438
|
+
renderDagInspector(dagRunId, node);
|
|
1439
|
+
});
|
|
1440
|
+
tabs.appendChild(button);
|
|
1441
|
+
}
|
|
1442
|
+
header.appendChild(tabs);
|
|
1443
|
+
panel.appendChild(header);
|
|
1444
|
+
|
|
1445
|
+
const content = el("div", "dag-inspector-content");
|
|
1446
|
+
content.setAttribute("role", "tabpanel");
|
|
1447
|
+
if (dagInspectorTab === "timeline") {
|
|
1448
|
+
renderSessionTimeline(content, node.nodeId);
|
|
1449
|
+
} else if (!node.outputPreview && !node.errorPreview) {
|
|
1450
|
+
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1451
|
+
} else {
|
|
1452
|
+
if (node.outputPreview) content.appendChild(renderMarkdown(node.outputPreview));
|
|
1453
|
+
if (node.errorPreview) {
|
|
1454
|
+
const error = el("section", "markdown-error");
|
|
1455
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
1456
|
+
error.appendChild(renderMarkdown(node.errorPreview));
|
|
1457
|
+
content.appendChild(error);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
panel.appendChild(content);
|
|
774
1461
|
}
|
|
775
1462
|
|
|
776
1463
|
async function renderFailures() {
|
|
@@ -837,9 +1524,12 @@ async function renderDashboard(scrollTo) {
|
|
|
837
1524
|
setBreadcrumb([{ label: UI_TEXT.dashboard }]);
|
|
838
1525
|
|
|
839
1526
|
const kpiEl = document.getElementById("dashboard-kpi");
|
|
1527
|
+
const featureEl = document.getElementById("dashboard-features");
|
|
840
1528
|
const activeEl = document.getElementById("dashboard-active-dags");
|
|
1529
|
+
const riskEl = document.getElementById("dashboard-risk");
|
|
841
1530
|
const dagsEl = document.getElementById("dashboard-dags");
|
|
842
1531
|
const batchesEl = document.getElementById("dashboard-batches");
|
|
1532
|
+
const repoEl = document.getElementById("dashboard-repo");
|
|
843
1533
|
|
|
844
1534
|
const snapshot = await fetchJson("/api/snapshot");
|
|
845
1535
|
if (!snapshot) return;
|
|
@@ -854,15 +1544,88 @@ async function renderDashboard(scrollTo) {
|
|
|
854
1544
|
.sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a)))
|
|
855
1545
|
.slice(0, 10);
|
|
856
1546
|
|
|
1547
|
+
const features = dashboardVisibleFeatures(snapshot.features);
|
|
1548
|
+
featureEl.hidden = features.length === 0;
|
|
1549
|
+
clearNode(featureEl);
|
|
1550
|
+
if (features.length > 0) {
|
|
1551
|
+
featureEl.appendChild(el("h3", "section-heading", "Feature 决策"));
|
|
1552
|
+
featureEl.appendChild(buildTable(
|
|
1553
|
+
["Feature", "状态", "下一步", "原因", "required AC", "证据"],
|
|
1554
|
+
features.map((feature) => ({ cells: [
|
|
1555
|
+
feature.featureId,
|
|
1556
|
+
feature.statusLabel ?? feature.status,
|
|
1557
|
+
feature.nextAction?.command ?? feature.nextAction?.label ?? "—",
|
|
1558
|
+
feature.blockingItems?.[0]?.message ?? `${feature.summary?.tasksSucceeded ?? 0}/${feature.summary?.tasksTotal ?? 0} tasks completed`,
|
|
1559
|
+
`${feature.summary?.requiredAcCovered ?? 0}/${feature.summary?.requiredAcTotal ?? 0}`,
|
|
1560
|
+
artifactLink("查看", feature.evidence?.delivery ?? feature.evidence?.closeout ?? feature.evidence?.morningReport ?? feature.evidence?.observeSnapshot),
|
|
1561
|
+
] })),
|
|
1562
|
+
));
|
|
1563
|
+
}
|
|
1564
|
+
if (features.length > 0 && (snapshot.projectionWarnings ?? []).length > 0) {
|
|
1565
|
+
featureEl.appendChild(el("p", "empty", `投影警告:${snapshot.projectionWarnings.join(";")}`));
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
renderRepoBanner(repoEl, snapshot);
|
|
1569
|
+
|
|
857
1570
|
clearNode(kpiEl);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1571
|
+
const dagHealth = snapshot.health?.dag;
|
|
1572
|
+
const dagActiveRuns = dagHealth?.activeRuns ?? activeDags.length;
|
|
1573
|
+
const dagRunningNodes = dagHealth?.runningNodes
|
|
1574
|
+
?? activeDags.flatMap((dag) => dag.nodes ?? []).filter((node) => ["running", "started"].includes((node.status ?? "").toLowerCase())).length;
|
|
1575
|
+
const dagPendingNodes = dagHealth?.pendingNodes
|
|
1576
|
+
?? activeDags.flatMap((dag) => dag.nodes ?? []).filter((node) => ["pending", "queued"].includes((node.status ?? "").toLowerCase())).length;
|
|
1577
|
+
const dagPausedRuns = dagHealth?.pausedRuns ?? 0;
|
|
1578
|
+
const dagStaleRuns = dagHealth?.staleRuns ?? 0;
|
|
1579
|
+
const dagInterruptedRuns = dagHealth?.interruptedRuns ?? 0;
|
|
1580
|
+
const dagInconsistentRuns = dagHealth?.inconsistentRuns ?? 0;
|
|
1581
|
+
const dagAttentionRuns = dagHealth?.attentionRuns
|
|
1582
|
+
?? dagRuns.filter((dag) => (dag.lifecycle ?? "").toLowerCase() !== "completed" && (
|
|
1583
|
+
(dag.lifecycle ?? "").toLowerCase() === "paused"
|
|
1584
|
+
|| ["stale", "orphaned"].includes((dag.liveness ?? "").toLowerCase())
|
|
1585
|
+
|| ["interrupted", "remote-unknown"].includes(dag.effectiveStatus)
|
|
1586
|
+
|| dag.stateConsistent === false
|
|
1587
|
+
)).length;
|
|
1588
|
+
|
|
1589
|
+
const quietCount = snapshot.health?.quietCount ?? 0;
|
|
1590
|
+
const staleCount = snapshot.health?.staleCount ?? 0;
|
|
1591
|
+
const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
|
|
1592
|
+
const failuresCount = snapshot.health?.failuresCount ?? 0;
|
|
1593
|
+
|
|
1594
|
+
if (snapshot.projectionError) {
|
|
1595
|
+
kpiEl.appendChild(renderProjectionFault(snapshot.projectionError));
|
|
1596
|
+
} else {
|
|
1597
|
+
const dagGroup = el("fieldset", "kpi-group");
|
|
1598
|
+
dagGroup.appendChild(el("legend", "kpi-group-legend", "DAG 运行"));
|
|
1599
|
+
const dagCards = el("div", "kpi-grid");
|
|
1600
|
+
dagCards.appendChild(kpiCard("活跃 DAG", dagActiveRuns, "kpi-highlight",
|
|
1601
|
+
"当前处于运行、静默运行、远端状态未知或等待执行的 DAG 数量(来源:.harness/dag-runs 的有效状态)。"));
|
|
1602
|
+
dagCards.appendChild(kpiCard("执行中节点", dagRunningNodes, "",
|
|
1603
|
+
"活跃 DAG 中状态为 running 或 started、正在执行的节点数量;DAG 内执行单元统一称为节点。"));
|
|
1604
|
+
dagCards.appendChild(kpiCard("等待节点", dagPendingNodes, "",
|
|
1605
|
+
"活跃 DAG 中状态为 pending 或 queued、尚未开始执行的节点数量。"));
|
|
1606
|
+
dagCards.appendChild(kpiCard("暂停 DAG", dagPausedRuns, dagPausedRuns > 0 ? "kpi-muted" : "",
|
|
1607
|
+
"生命周期被暂停的 DAG 运行数量(来源:.harness/dag-runs 下的 lifecycle 目录)。"));
|
|
1608
|
+
dagCards.appendChild(kpiCard("需处理 DAG", dagAttentionRuns, dagAttentionRuns > 0 ? "kpi-risk" : "",
|
|
1609
|
+
"未完成且暂停、失联、中断、远端状态未知或状态不一致的 DAG 数量;同一个 DAG 只计一次。"));
|
|
1610
|
+
dagGroup.appendChild(dagCards);
|
|
1611
|
+
|
|
1612
|
+
const workerGroup = el("fieldset", "kpi-group");
|
|
1613
|
+
workerGroup.appendChild(el("legend", "kpi-group-legend", "Worker 执行"));
|
|
1614
|
+
const workerCards = el("div", "kpi-grid");
|
|
1615
|
+
workerCards.appendChild(kpiCard("活跃 Worker Task", snapshot.health?.activeTasks ?? 0, "",
|
|
1616
|
+
"当前运行或等待执行的 Worker Task Pool 任务数量(来源:.harness/task-pool 的状态、运行记录与事件;不含 .harness/tasks 下的 task.json)。"));
|
|
1617
|
+
workerCards.appendChild(kpiCard("无输出", quietCount, quietCount > 0 ? "kpi-muted" : "",
|
|
1618
|
+
"心跳仍正常,但超过无输出判定时长仍没有新输出的 Worker Task 数量。"));
|
|
1619
|
+
workerCards.appendChild(kpiCard("心跳失联", staleCount, staleCount > 0 ? "kpi-risk" : "",
|
|
1620
|
+
"超过心跳失联判定时长仍没有新心跳的 Worker Task 数量。"));
|
|
1621
|
+
workerCards.appendChild(kpiCard("即将超时", timeoutRiskCount, timeoutRiskCount > 0 ? "kpi-risk" : "",
|
|
1622
|
+
"命令已执行时长超过 80% 超时的 Worker Task 数量。"));
|
|
1623
|
+
workerCards.appendChild(kpiCard("失败", failuresCount, failuresCount > 0 ? "kpi-danger" : "",
|
|
1624
|
+
"状态为 failed 的 Worker Task 数量。"));
|
|
1625
|
+
workerGroup.appendChild(workerCards);
|
|
1626
|
+
kpiEl.appendChild(workerGroup);
|
|
1627
|
+
kpiEl.appendChild(dagGroup);
|
|
1628
|
+
}
|
|
866
1629
|
|
|
867
1630
|
const nowMs = Date.now();
|
|
868
1631
|
|
|
@@ -880,6 +1643,41 @@ async function renderDashboard(scrollTo) {
|
|
|
880
1643
|
activeEl.appendChild(list);
|
|
881
1644
|
}
|
|
882
1645
|
|
|
1646
|
+
clearNode(riskEl);
|
|
1647
|
+
const dagRiskCount = dagAttentionRuns;
|
|
1648
|
+
const workerRiskCount = failuresCount + staleCount + timeoutRiskCount;
|
|
1649
|
+
riskEl.classList.toggle("panel-risk-attention", dagRiskCount > 0 || workerRiskCount > 0);
|
|
1650
|
+
riskEl.appendChild(el("h3", "section-heading", "运行风险"));
|
|
1651
|
+
if (dagRiskCount === 0 && workerRiskCount === 0) {
|
|
1652
|
+
riskEl.appendChild(el("p", "risk-empty", "当前没有需要处理的异常。"));
|
|
1653
|
+
} else {
|
|
1654
|
+
const riskBlocks = el("div", "risk-blocks");
|
|
1655
|
+
if (dagRiskCount > 0) {
|
|
1656
|
+
const dagBlock = el("div", "risk-block risk-block-dag");
|
|
1657
|
+
dagBlock.appendChild(el("div", "risk-block-label", "需处理 DAG"));
|
|
1658
|
+
dagBlock.appendChild(el("div", "risk-total", String(dagRiskCount)));
|
|
1659
|
+
dagBlock.appendChild(el("p", "risk-summary",
|
|
1660
|
+
`去重运行数 ${dagAttentionRuns} · 中断 ${dagInterruptedRuns} · 失联 ${dagStaleRuns} · 状态不一致 ${dagInconsistentRuns} · 暂停 ${dagPausedRuns}`));
|
|
1661
|
+
riskBlocks.appendChild(dagBlock);
|
|
1662
|
+
}
|
|
1663
|
+
if (workerRiskCount > 0) {
|
|
1664
|
+
const workerBlock = el("div", "risk-block risk-block-worker");
|
|
1665
|
+
workerBlock.appendChild(el("div", "risk-block-label", "Worker 风险"));
|
|
1666
|
+
workerBlock.appendChild(el("div", "risk-total", String(workerRiskCount)));
|
|
1667
|
+
workerBlock.appendChild(el("p", "risk-summary",
|
|
1668
|
+
`失败 ${failuresCount} · 心跳失联 ${staleCount} · 即将超时 ${timeoutRiskCount}`));
|
|
1669
|
+
const link = el("a", "risk-link", "查看异常 Task →");
|
|
1670
|
+
link.href = "#/failures";
|
|
1671
|
+
link.addEventListener("click", (event) => {
|
|
1672
|
+
event.preventDefault();
|
|
1673
|
+
navigate("/failures");
|
|
1674
|
+
});
|
|
1675
|
+
workerBlock.appendChild(link);
|
|
1676
|
+
riskBlocks.appendChild(workerBlock);
|
|
1677
|
+
}
|
|
1678
|
+
riskEl.appendChild(riskBlocks);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
883
1681
|
clearNode(dagsEl);
|
|
884
1682
|
dagsEl.id = "dashboard-dags";
|
|
885
1683
|
dagsEl.appendChild(el("h3", "section-heading", UI_TEXT.recentDags));
|
|
@@ -887,24 +1685,45 @@ async function renderDashboard(scrollTo) {
|
|
|
887
1685
|
dagsEl.appendChild(el("p", "empty", UI_TEXT.noRecentDags));
|
|
888
1686
|
} else if (recentDags.length === 0) {
|
|
889
1687
|
dagsEl.appendChild(el("p", "empty", "暂无历史 DAG 记录"));
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1688
|
+
} else {
|
|
1689
|
+
const timeline = el("div", "dag-run-timeline");
|
|
1690
|
+
for (const dag of recentDags) {
|
|
1691
|
+
const progress = dagProgress(dag);
|
|
1692
|
+
const item = el("a", "dag-run-timeline-item");
|
|
1693
|
+
item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
|
|
1694
|
+
item.title = dag.title || dag.dagRunId;
|
|
1695
|
+
item.addEventListener("click", (event) => {
|
|
1696
|
+
event.preventDefault();
|
|
1697
|
+
navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
const rail = el("span", "dag-run-timeline-rail");
|
|
1701
|
+
rail.appendChild(
|
|
1702
|
+
el(
|
|
1703
|
+
"span",
|
|
1704
|
+
`dag-run-timeline-dot status-${badgeClass(dag.effectiveStatus ?? dag.status)}`,
|
|
1705
|
+
),
|
|
1706
|
+
);
|
|
1707
|
+
item.appendChild(rail);
|
|
1708
|
+
|
|
1709
|
+
const content = el("span", "dag-run-timeline-content");
|
|
1710
|
+
content.appendChild(el("span", "dag-run-timeline-title", dag.title || dag.dagRunId));
|
|
1711
|
+
content.appendChild(
|
|
1712
|
+
el(
|
|
1713
|
+
"span",
|
|
1714
|
+
"dag-run-timeline-meta",
|
|
1715
|
+
`${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`,
|
|
1716
|
+
),
|
|
1717
|
+
);
|
|
1718
|
+
item.appendChild(content);
|
|
1719
|
+
|
|
1720
|
+
const state = el("span", "dag-run-timeline-status");
|
|
1721
|
+
state.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
1722
|
+
item.appendChild(state);
|
|
1723
|
+
timeline.appendChild(item);
|
|
1724
|
+
}
|
|
1725
|
+
dagsEl.appendChild(timeline);
|
|
1726
|
+
}
|
|
908
1727
|
|
|
909
1728
|
clearNode(batchesEl);
|
|
910
1729
|
batchesEl.appendChild(el("h3", "section-heading", UI_TEXT.batches));
|
|
@@ -927,15 +1746,6 @@ async function renderDashboard(scrollTo) {
|
|
|
927
1746
|
);
|
|
928
1747
|
}
|
|
929
1748
|
|
|
930
|
-
const failuresFooter = el("p", "dashboard-footer");
|
|
931
|
-
failuresFooter.appendChild(document.createTextNode("失败/失联合计:"));
|
|
932
|
-
failuresFooter.appendChild(
|
|
933
|
-
failuresInboxLink(
|
|
934
|
-
(snapshot.health?.failuresCount ?? 0) + (snapshot.health?.staleCount ?? 0),
|
|
935
|
-
),
|
|
936
|
-
);
|
|
937
|
-
batchesEl.appendChild(failuresFooter);
|
|
938
|
-
|
|
939
1749
|
if (scrollTo === "dags") {
|
|
940
1750
|
dagsEl.scrollIntoView({ behavior: "smooth" });
|
|
941
1751
|
}
|
|
@@ -1111,7 +1921,7 @@ function renderRunDagProgress(task, snapshot) {
|
|
|
1111
1921
|
panel.appendChild(
|
|
1112
1922
|
metaGrid([
|
|
1113
1923
|
["DAG", dag.title || dag.dagRunId],
|
|
1114
|
-
["
|
|
1924
|
+
["当前状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
1115
1925
|
["进度", `${progress.finished}/${progress.total} 个节点`],
|
|
1116
1926
|
[
|
|
1117
1927
|
"详情",
|
|
@@ -1287,7 +2097,7 @@ async function renderRun(workerRunId, initial = true) {
|
|
|
1287
2097
|
.getElementById("run-meta")
|
|
1288
2098
|
.appendChild(el("p", "empty", "找不到该运行。"));
|
|
1289
2099
|
}
|
|
1290
|
-
return;
|
|
2100
|
+
return false;
|
|
1291
2101
|
}
|
|
1292
2102
|
|
|
1293
2103
|
if (snapshot) {
|
|
@@ -1298,13 +2108,22 @@ async function renderRun(workerRunId, initial = true) {
|
|
|
1298
2108
|
currentBatchRunId = task.batchRunId ?? currentBatchRunId;
|
|
1299
2109
|
await mergeRunEvents(workerRunId);
|
|
1300
2110
|
renderRunPanels(task, runEvents, snapshot ?? lastSnapshot);
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
runPollTimer
|
|
1306
|
-
|
|
1307
|
-
|
|
2111
|
+
return !TERMINAL_RUN_STATUSES.has((task.status ?? "").toLowerCase());
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function startRunPolling(workerRunId, initial = true) {
|
|
2115
|
+
if (runPollTimer) clearTimeout(runPollTimer);
|
|
2116
|
+
const generation = ++pollingGeneration;
|
|
2117
|
+
const poll = async (isInitial) => {
|
|
2118
|
+
const active = await renderRun(workerRunId, isInitial);
|
|
2119
|
+
if (generation !== pollingGeneration || !active) return;
|
|
2120
|
+
const delay = detailPollDelay("running", isPageVisible());
|
|
2121
|
+
runPollTimer = setTimeout(() => {
|
|
2122
|
+
runPollTimer = null;
|
|
2123
|
+
void poll(false);
|
|
2124
|
+
}, delay);
|
|
2125
|
+
};
|
|
2126
|
+
void poll(initial);
|
|
1308
2127
|
}
|
|
1309
2128
|
|
|
1310
2129
|
async function renderDagDetail(dagRunId, initial = true) {
|
|
@@ -1319,24 +2138,36 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1319
2138
|
selectedDagNodeId = null;
|
|
1320
2139
|
sessionEventOffset = 0;
|
|
1321
2140
|
sessionEvents = [];
|
|
2141
|
+
dagInspectorOpen = false;
|
|
2142
|
+
dagInspectorTab = "output";
|
|
2143
|
+
dagGraphViewportState = null;
|
|
2144
|
+
dagTimelineViewportState = null;
|
|
1322
2145
|
}
|
|
1323
2146
|
|
|
1324
2147
|
const metaEl = document.getElementById("dag-meta");
|
|
1325
2148
|
const ranksEl = document.getElementById("dag-ranks");
|
|
1326
2149
|
const nodesEl = document.getElementById("dag-nodes");
|
|
1327
|
-
|
|
2150
|
+
dagGraphViewportState = captureDagGraphViewportState(
|
|
2151
|
+
dagGraphViewportState,
|
|
2152
|
+
dagRunId,
|
|
2153
|
+
ranksEl.querySelector(".dag-graph-viewport"),
|
|
2154
|
+
);
|
|
2155
|
+
if (dagGraphViewportState?.dagRunId === dagRunId) {
|
|
2156
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
2157
|
+
}
|
|
1328
2158
|
|
|
1329
2159
|
const dag = await fetchJson(`/api/dag-runs/${encodeURIComponent(dagRunId)}`);
|
|
1330
2160
|
if (!dag) {
|
|
1331
2161
|
clearNode(metaEl);
|
|
1332
2162
|
metaEl.appendChild(el("p", "empty", "找不到该 DAG 运行。"));
|
|
1333
|
-
|
|
1334
|
-
return;
|
|
2163
|
+
closeDagInspector();
|
|
2164
|
+
return false;
|
|
1335
2165
|
}
|
|
1336
2166
|
|
|
1337
2167
|
const nodes = dag.nodes ?? [];
|
|
1338
|
-
if (
|
|
1339
|
-
selectedDagNodeId =
|
|
2168
|
+
if (selectedDagNodeId && !nodes.some((n) => n.nodeId === selectedDagNodeId)) {
|
|
2169
|
+
selectedDagNodeId = null;
|
|
2170
|
+
dagInspectorOpen = false;
|
|
1340
2171
|
if (initial) {
|
|
1341
2172
|
sessionEventOffset = 0;
|
|
1342
2173
|
sessionEvents = [];
|
|
@@ -1352,19 +2183,38 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1352
2183
|
.length;
|
|
1353
2184
|
|
|
1354
2185
|
clearNode(metaEl);
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
2186
|
+
const visibility = dagMetaVisibility(dag);
|
|
2187
|
+
const livenessValue = LIVENESS_LABELS[dag.liveness] ?? dag.liveness ?? LIVENESS_LABELS.unknown;
|
|
2188
|
+
const primaryEntries = [
|
|
2189
|
+
["标题", dag.title || dag.dagRunId],
|
|
2190
|
+
["DAG 运行 ID", dag.dagRunId],
|
|
2191
|
+
["状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
2192
|
+
];
|
|
2193
|
+
if (visibility.showExecutorStatus) primaryEntries.push(["执行器状态", livenessValue]);
|
|
2194
|
+
if (visibility.showConsistencyWarning) primaryEntries.push(["状态一致性", "不一致,需要处理"]);
|
|
2195
|
+
if (visibility.showResume) primaryEntries.push(["允许恢复", "是"]);
|
|
2196
|
+
if (visibility.showReconcile) primaryEntries.push(["允许收口", "是"]);
|
|
2197
|
+
primaryEntries.push(
|
|
2198
|
+
["开始时间", formatTs(dag.startedAt)],
|
|
2199
|
+
["结束时间", formatTs(dag.finishedAt)],
|
|
2200
|
+
["总耗时", formatDuration(dag.durationMs)],
|
|
2201
|
+
["进度", `${progress.finished}/${progress.total}`],
|
|
2202
|
+
["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
|
|
2203
|
+
["DAG 路径", dag.dagPath ?? "—"],
|
|
1367
2204
|
);
|
|
2205
|
+
metaEl.appendChild(metaGrid(primaryEntries));
|
|
2206
|
+
const details = el("details", "meta-diagnostics");
|
|
2207
|
+
details.appendChild(el("summary", null, "诊断信息"));
|
|
2208
|
+
details.appendChild(metaGrid([
|
|
2209
|
+
["当前判定", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
2210
|
+
["原始记录状态", badge(dag.status)],
|
|
2211
|
+
["执行器状态", livenessValue],
|
|
2212
|
+
["生命周期", dag.lifecycle ?? "—"],
|
|
2213
|
+
["状态一致性", visibility.showConsistencyWarning ? "不一致,需要处理" : "一致"],
|
|
2214
|
+
["允许恢复", visibility.showResume ? "是" : "否"],
|
|
2215
|
+
["允许收口", visibility.showReconcile ? "是" : "否"],
|
|
2216
|
+
]));
|
|
2217
|
+
metaEl.appendChild(details);
|
|
1368
2218
|
|
|
1369
2219
|
clearNode(ranksEl);
|
|
1370
2220
|
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
@@ -1375,7 +2225,10 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1375
2225
|
}
|
|
1376
2226
|
|
|
1377
2227
|
clearNode(nodesEl);
|
|
1378
|
-
|
|
2228
|
+
const nodeHeading = el("div", "dag-section-heading");
|
|
2229
|
+
nodeHeading.appendChild(el("h3", null, "节点"));
|
|
2230
|
+
nodeHeading.appendChild(el("span", "dag-section-meta", `共 ${nodes.length} 个节点`));
|
|
2231
|
+
nodesEl.appendChild(nodeHeading);
|
|
1379
2232
|
|
|
1380
2233
|
if (nodes.length === 0) {
|
|
1381
2234
|
nodesEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
@@ -1411,62 +2264,69 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1411
2264
|
],
|
|
1412
2265
|
};
|
|
1413
2266
|
});
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
rows,
|
|
1418
|
-
),
|
|
2267
|
+
const nodeTable = buildTable(
|
|
2268
|
+
["节点 ID", "依赖层", "执行方式", "模型", "状态", "耗时", "备注"],
|
|
2269
|
+
rows,
|
|
1419
2270
|
);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
clearNode(outputEl);
|
|
1426
|
-
outputEl.appendChild(el("h3", null, "节点输出"));
|
|
1427
|
-
const withOutput = nodes.filter((n) => n.outputPreview || n.errorPreview);
|
|
1428
|
-
if (withOutput.length === 0) {
|
|
1429
|
-
outputEl.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1430
|
-
} else {
|
|
1431
|
-
for (const node of withOutput) {
|
|
1432
|
-
if (node.outputPreview) {
|
|
1433
|
-
outputEl.appendChild(
|
|
1434
|
-
expandablePreview(`${node.nodeId} · 输出`, node.outputPreview),
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
if (node.errorPreview) {
|
|
1438
|
-
outputEl.appendChild(
|
|
1439
|
-
expandablePreview(
|
|
1440
|
-
`${node.nodeId} · 错误`,
|
|
1441
|
-
node.errorPreview,
|
|
1442
|
-
"output-preview error-preview",
|
|
1443
|
-
),
|
|
1444
|
-
);
|
|
2271
|
+
nodeTable.classList.add("dag-node-table");
|
|
2272
|
+
for (const row of nodeTable.tBodies[0]?.rows ?? []) {
|
|
2273
|
+
for (const columnIndex of [0, 3, 6]) {
|
|
2274
|
+
const cell = row.cells[columnIndex];
|
|
2275
|
+
if (cell?.textContent) cell.title = cell.textContent;
|
|
1445
2276
|
}
|
|
1446
2277
|
}
|
|
2278
|
+
nodesEl.appendChild(nodeTable);
|
|
1447
2279
|
}
|
|
1448
|
-
}
|
|
1449
2280
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2281
|
+
if (dagInspectorOpen && selectedDagNodeId) {
|
|
2282
|
+
await mergeSessionEvents(dagRunId, selectedDagNodeId);
|
|
2283
|
+
}
|
|
2284
|
+
renderDagInspector(
|
|
2285
|
+
dagRunId,
|
|
2286
|
+
nodes.find((node) => node.nodeId === selectedDagNodeId),
|
|
2287
|
+
);
|
|
2288
|
+
return isDagRunActive(dag);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function startDagPolling(dagRunId, initial = true) {
|
|
2292
|
+
if (dagPollTimer) clearTimeout(dagPollTimer);
|
|
2293
|
+
const generation = ++pollingGeneration;
|
|
2294
|
+
const poll = async (isInitial) => {
|
|
2295
|
+
const active = await renderDagDetail(dagRunId, isInitial);
|
|
2296
|
+
if (generation !== pollingGeneration || !active) return;
|
|
2297
|
+
const delay = detailPollDelay("running", isPageVisible());
|
|
2298
|
+
dagPollTimer = setTimeout(() => {
|
|
2299
|
+
dagPollTimer = null;
|
|
2300
|
+
void poll(false);
|
|
2301
|
+
}, delay);
|
|
2302
|
+
};
|
|
2303
|
+
void poll(initial);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
function startDashboardPolling(scrollTo) {
|
|
2307
|
+
if (dashboardTimer) clearTimeout(dashboardTimer);
|
|
2308
|
+
const generation = ++pollingGeneration;
|
|
2309
|
+
const poll = async (initialScrollTo) => {
|
|
2310
|
+
await renderDashboard(initialScrollTo);
|
|
2311
|
+
if (generation !== pollingGeneration) return;
|
|
2312
|
+
dashboardTimer = setTimeout(() => {
|
|
2313
|
+
dashboardTimer = null;
|
|
2314
|
+
void poll();
|
|
2315
|
+
}, dashboardPollDelay());
|
|
2316
|
+
};
|
|
2317
|
+
void poll(scrollTo);
|
|
1455
2318
|
}
|
|
1456
2319
|
|
|
1457
2320
|
function route() {
|
|
1458
2321
|
stopTimers();
|
|
1459
2322
|
const r = parseRoute();
|
|
1460
2323
|
if (r.view === "dashboard") {
|
|
1461
|
-
|
|
1462
|
-
dashboardTimer = setInterval(() => void renderDashboard(), DASHBOARD_POLL_MS);
|
|
2324
|
+
startDashboardPolling(r.scrollTo);
|
|
1463
2325
|
} else if (r.view === "batch") {
|
|
1464
2326
|
void renderBatch(r.batchRunId);
|
|
1465
2327
|
} else if (r.view === "run") {
|
|
1466
|
-
void renderRun(r.workerRunId, true);
|
|
1467
2328
|
startRunPolling(r.workerRunId);
|
|
1468
2329
|
} else if (r.view === "dag") {
|
|
1469
|
-
void renderDagDetail(r.dagRunId, true);
|
|
1470
2330
|
startDagPolling(r.dagRunId);
|
|
1471
2331
|
} else if (r.view === "failures") {
|
|
1472
2332
|
void renderFailures();
|
|
@@ -1476,4 +2336,12 @@ function route() {
|
|
|
1476
2336
|
if (typeof window !== "undefined") {
|
|
1477
2337
|
window.addEventListener("hashchange", route);
|
|
1478
2338
|
window.addEventListener("load", route);
|
|
2339
|
+
document.addEventListener("pointerdown", closeDagInspectorOnOutsideClick);
|
|
2340
|
+
document.addEventListener("visibilitychange", () => {
|
|
2341
|
+
if (!isPageVisible()) return;
|
|
2342
|
+
const r = parseRoute();
|
|
2343
|
+
if (r.view === "dashboard") startDashboardPolling();
|
|
2344
|
+
if (r.view === "run") startRunPolling(r.workerRunId, false);
|
|
2345
|
+
if (r.view === "dag") startDagPolling(r.dagRunId, false);
|
|
2346
|
+
});
|
|
1479
2347
|
}
|