@tea-agent/loop-agent 0.9.0 → 0.10.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/CHANGELOG.md +42 -11
- package/README.md +20 -0
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/dag-reconcile-run.js +118 -0
- package/dist/commands/init.js +12 -3
- package/dist/governance/manifest-types.js +4 -0
- package/dist/worker/cli.js +216 -0
- package/dist/worker/closeout/apply.js +73 -0
- package/dist/worker/closeout/preview.js +30 -0
- package/dist/worker/delivery/final-verification.js +158 -0
- package/dist/worker/delivery/git-transaction.js +354 -0
- package/dist/worker/delivery/package.js +449 -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 +313 -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/metrics/projector.js +139 -0
- package/dist/worker/observability/read-model.js +256 -15
- package/dist/worker/observe/paths.js +17 -5
- package/dist/worker/observe/static/app.js +443 -61
- package/dist/worker/observe/static/index.html +3 -1
- package/dist/worker/observe/static/styles.css +86 -19
- package/dist/worker/pool/run-store.js +14 -2
- package/dist/worker/pool/validation.js +59 -0
- package/dist/worker/report/morning-report.js +41 -6
- package/dist/worker/run-task/run-task.js +1 -1
- package/dist/worker/runner/run-ready.js +19 -5
- package/dist/workflows/dag/init-hybrid.js +3 -1
- package/dist/workflows/dag/lifecycle.js +146 -0
- package/dist/workflows/dag/node-execution.js +3 -0
- package/dist/workflows/dag/prompt.js +16 -0
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/runner.js +133 -104
- package/dist/workflows/dag/types.js +3 -0
- package/docs/README.md +17 -0
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/design/README.md +11 -5
- package/docs/exec-plans/active/README.md +1 -1
- package/docs/exec-plans/completed/README.md +8 -1
- package/docs/loop-agent-harness.md +45 -2
- package/docs/progress/README.md +2 -0
- package/docs/reports/README.md +10 -0
- package/docs/templates/agent-dag-report.schema.json +5 -3
- package/docs/templates/harness.schema.json +7 -2
- package/docs/templates/init-evolution-review.md +4 -2
- package/docs/verification-matrix.md +7 -0
- package/harness.json +4 -3
- package/package.json +4 -2
- package/scripts/check-product-line-docs.sh +7 -3
- package/skills/init-capability-evolution/SKILL.md +1 -0
- package/skills/loop-agent/references/command-reference.md +21 -0
- package/skills/loop-agent/references/hybrid-dag.md +4 -3
|
@@ -3,6 +3,12 @@ import { layoutDag } from "./dag-layout.js";
|
|
|
3
3
|
const ACTIVE_DETAIL_POLL_MS = 2000;
|
|
4
4
|
const HIDDEN_POLL_MS = 30000;
|
|
5
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;
|
|
6
12
|
|
|
7
13
|
const UI_TEXT = {
|
|
8
14
|
dashboard: "总览",
|
|
@@ -56,10 +62,28 @@ const STATUS_LABELS = {
|
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
const LIVENESS_LABELS = {
|
|
59
|
-
active: "
|
|
65
|
+
active: "执行器正常",
|
|
66
|
+
"node-quiet": "执行器正常,节点长时间无活动",
|
|
60
67
|
quiet: "无输出(存活)",
|
|
61
|
-
stale: "
|
|
68
|
+
stale: "执行器心跳中断",
|
|
62
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: "状态未知",
|
|
63
87
|
};
|
|
64
88
|
|
|
65
89
|
const STEP_LABELS = {
|
|
@@ -135,6 +159,7 @@ let sessionEvents = [];
|
|
|
135
159
|
let lastSnapshot = null;
|
|
136
160
|
let dagInspectorOpen = false;
|
|
137
161
|
let dagInspectorTab = "output";
|
|
162
|
+
let dagInspectorWidth = null;
|
|
138
163
|
let dagGraphViewportState = null;
|
|
139
164
|
const dagGraphViewportStates = new Map();
|
|
140
165
|
let dagTimelineViewportState = null;
|
|
@@ -372,16 +397,18 @@ function statusSymbol(status) {
|
|
|
372
397
|
return "◇";
|
|
373
398
|
}
|
|
374
399
|
|
|
375
|
-
function badgeClass(status) {
|
|
400
|
+
export function badgeClass(status) {
|
|
376
401
|
if (!status) return "unknown";
|
|
377
402
|
const key = status.toLowerCase().replace(/_/g, "-");
|
|
378
|
-
if (
|
|
403
|
+
if (["finished", "completed", "succeeded", "done", "superseded"].includes(key)) {
|
|
379
404
|
return "succeeded";
|
|
380
405
|
}
|
|
381
|
-
if (
|
|
406
|
+
if (["error", "failed", "interrupted"].includes(key)) return "failed";
|
|
382
407
|
if (key === "partial-failed") return "partial-failed";
|
|
383
|
-
if (key === "skipped") return "skipped";
|
|
384
|
-
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";
|
|
385
412
|
return key;
|
|
386
413
|
}
|
|
387
414
|
|
|
@@ -398,12 +425,25 @@ function formatTs(value) {
|
|
|
398
425
|
return d.toLocaleString("zh-CN", { hour12: false });
|
|
399
426
|
}
|
|
400
427
|
|
|
401
|
-
function formatMs(ms) {
|
|
428
|
+
export function formatMs(ms) {
|
|
402
429
|
if (ms === undefined || ms === null) return "—";
|
|
403
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
|
+
}
|
|
404
435
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
405
436
|
}
|
|
406
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
|
+
|
|
407
447
|
function formatDuration(ms) {
|
|
408
448
|
if (ms === undefined || ms === null || Number.isNaN(ms)) return "—";
|
|
409
449
|
if (ms < 0) ms = 0;
|
|
@@ -518,6 +558,25 @@ function metaGrid(entries) {
|
|
|
518
558
|
return dl;
|
|
519
559
|
}
|
|
520
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
|
+
|
|
521
580
|
function buildTable(headers, rows) {
|
|
522
581
|
const table = document.createElement("table");
|
|
523
582
|
const thead = document.createElement("thead");
|
|
@@ -635,6 +694,10 @@ function formatSessionEventLabel(event) {
|
|
|
635
694
|
if (!event || typeof event !== "object") return "未知类型";
|
|
636
695
|
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
637
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 "开始新一轮处理";
|
|
638
701
|
if (type === "tool_execution_start" || type === "tool_start") {
|
|
639
702
|
return `调用工具 · ${toolName}`;
|
|
640
703
|
}
|
|
@@ -647,16 +710,53 @@ function formatSessionEventLabel(event) {
|
|
|
647
710
|
return type;
|
|
648
711
|
}
|
|
649
712
|
|
|
650
|
-
function
|
|
651
|
-
if (!event || typeof event !== "object") return "
|
|
713
|
+
function sessionEventTimestamp(event) {
|
|
714
|
+
if (!event || typeof event !== "object") return "";
|
|
652
715
|
const ts =
|
|
653
716
|
(typeof event.timestamp === "string" && event.timestamp) ||
|
|
654
717
|
(typeof event.at === "string" && event.at) ||
|
|
655
718
|
(typeof event.recordedAt === "string" && event.recordedAt);
|
|
656
|
-
|
|
719
|
+
return ts || "";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function formatSessionEventTime(event) {
|
|
723
|
+
const ts = sessionEventTimestamp(event);
|
|
724
|
+
if (!ts) return "未记录时间";
|
|
657
725
|
return formatTs(ts).replace(/\s.*/, "") || formatTs(ts);
|
|
658
726
|
}
|
|
659
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
|
+
|
|
660
760
|
async function mergeSessionEvents(dagRunId, nodeId) {
|
|
661
761
|
if (!dagRunId || !nodeId) return;
|
|
662
762
|
const page = await fetchJson(
|
|
@@ -691,6 +791,94 @@ function closeDagInspector() {
|
|
|
691
791
|
}
|
|
692
792
|
}
|
|
693
793
|
|
|
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
|
+
|
|
694
882
|
function captureDagTimelineViewportState(panel) {
|
|
695
883
|
const scrollEl = panel.querySelector(".process-timeline-scroll");
|
|
696
884
|
if (scrollEl?.dataset.dagRunId && scrollEl.dataset.nodeId) {
|
|
@@ -716,11 +904,33 @@ function renderSessionTimeline(content, nodeId) {
|
|
|
716
904
|
return;
|
|
717
905
|
}
|
|
718
906
|
|
|
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
|
+
}
|
|
719
914
|
const ul = el("ul", "process-timeline");
|
|
720
|
-
for (
|
|
721
|
-
const
|
|
722
|
-
li
|
|
723
|
-
|
|
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));
|
|
724
934
|
ul.appendChild(li);
|
|
725
935
|
}
|
|
726
936
|
content.appendChild(ul);
|
|
@@ -756,12 +966,27 @@ function renderSessionTimeline(content, nodeId) {
|
|
|
756
966
|
}
|
|
757
967
|
|
|
758
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;
|
|
759
974
|
const status = (dag.status ?? "").toLowerCase();
|
|
975
|
+
if ((dag.lifecycle ?? "").toLowerCase() === "paused") return false;
|
|
976
|
+
if (["orphaned", "stale"].includes((dag.liveness ?? "").toLowerCase())) return false;
|
|
760
977
|
if (TERMINAL_RUN_STATUSES.has(status)) return false;
|
|
761
978
|
if (["running", "pending", "started"].includes(status)) return true;
|
|
762
979
|
return (dag.nodes ?? []).some((n) => isNodeActive(n.status));
|
|
763
980
|
}
|
|
764
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
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
765
990
|
function dagProgress(dag) {
|
|
766
991
|
const nodes = dag.nodes ?? [];
|
|
767
992
|
const finished = nodes.filter((n) => isNodeFinished(n.status)).length;
|
|
@@ -786,13 +1011,69 @@ function findDagForRun(snapshot, workerRunId, task) {
|
|
|
786
1011
|
return undefined;
|
|
787
1012
|
}
|
|
788
1013
|
|
|
789
|
-
|
|
1014
|
+
let kpiHelpSeed = 0;
|
|
1015
|
+
function kpiCard(label, value, extraClass, helpText) {
|
|
790
1016
|
const card = el("div", `kpi-card${extraClass ? ` ${extraClass}` : ""}`);
|
|
791
1017
|
card.appendChild(el("div", "kpi-label", label));
|
|
792
|
-
|
|
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);
|
|
793
1038
|
return card;
|
|
794
1039
|
}
|
|
795
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
|
+
|
|
796
1077
|
function renderActiveDagCard(dag, nowMs) {
|
|
797
1078
|
const card = el("div", "active-dag-card");
|
|
798
1079
|
if (isNodeActive(dag.status) || getActiveNodes(dag).length > 0) {
|
|
@@ -801,7 +1082,7 @@ function renderActiveDagCard(dag, nowMs) {
|
|
|
801
1082
|
const title = dag.title || dag.dagRunId;
|
|
802
1083
|
const header = el("div", "active-dag-header");
|
|
803
1084
|
header.appendChild(el("span", "active-dag-title", title));
|
|
804
|
-
header.appendChild(
|
|
1085
|
+
header.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
805
1086
|
card.appendChild(header);
|
|
806
1087
|
|
|
807
1088
|
const progress = dagProgress(dag);
|
|
@@ -1121,6 +1402,7 @@ function renderDagInspector(dagRunId, node) {
|
|
|
1121
1402
|
clearNode(panel);
|
|
1122
1403
|
panel.classList.add("is-open");
|
|
1123
1404
|
panel.setAttribute("aria-hidden", "false");
|
|
1405
|
+
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
1124
1406
|
const header = el("div", "dag-inspector-header");
|
|
1125
1407
|
const title = el("div", "dag-inspector-title");
|
|
1126
1408
|
title.appendChild(el("h3", null, "节点检查器"));
|
|
@@ -1242,10 +1524,12 @@ async function renderDashboard(scrollTo) {
|
|
|
1242
1524
|
setBreadcrumb([{ label: UI_TEXT.dashboard }]);
|
|
1243
1525
|
|
|
1244
1526
|
const kpiEl = document.getElementById("dashboard-kpi");
|
|
1527
|
+
const featureEl = document.getElementById("dashboard-features");
|
|
1245
1528
|
const activeEl = document.getElementById("dashboard-active-dags");
|
|
1246
1529
|
const riskEl = document.getElementById("dashboard-risk");
|
|
1247
1530
|
const dagsEl = document.getElementById("dashboard-dags");
|
|
1248
1531
|
const batchesEl = document.getElementById("dashboard-batches");
|
|
1532
|
+
const repoEl = document.getElementById("dashboard-repo");
|
|
1249
1533
|
|
|
1250
1534
|
const snapshot = await fetchJson("/api/snapshot");
|
|
1251
1535
|
if (!snapshot) return;
|
|
@@ -1260,21 +1544,88 @@ async function renderDashboard(scrollTo) {
|
|
|
1260
1544
|
.sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a)))
|
|
1261
1545
|
.slice(0, 10);
|
|
1262
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
|
+
|
|
1263
1570
|
clearNode(kpiEl);
|
|
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
|
+
|
|
1264
1589
|
const quietCount = snapshot.health?.quietCount ?? 0;
|
|
1265
1590
|
const staleCount = snapshot.health?.staleCount ?? 0;
|
|
1266
1591
|
const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
|
|
1267
1592
|
const failuresCount = snapshot.health?.failuresCount ?? 0;
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
+
}
|
|
1278
1629
|
|
|
1279
1630
|
const nowMs = Date.now();
|
|
1280
1631
|
|
|
@@ -1292,28 +1643,39 @@ async function renderDashboard(scrollTo) {
|
|
|
1292
1643
|
activeEl.appendChild(list);
|
|
1293
1644
|
}
|
|
1294
1645
|
|
|
1295
|
-
const riskCount = failuresCount + staleCount + timeoutRiskCount;
|
|
1296
1646
|
clearNode(riskEl);
|
|
1297
|
-
|
|
1647
|
+
const dagRiskCount = dagAttentionRuns;
|
|
1648
|
+
const workerRiskCount = failuresCount + staleCount + timeoutRiskCount;
|
|
1649
|
+
riskEl.classList.toggle("panel-risk-attention", dagRiskCount > 0 || workerRiskCount > 0);
|
|
1298
1650
|
riskEl.appendChild(el("h3", "section-heading", "运行风险"));
|
|
1299
|
-
if (
|
|
1651
|
+
if (dagRiskCount === 0 && workerRiskCount === 0) {
|
|
1300
1652
|
riskEl.appendChild(el("p", "risk-empty", "当前没有需要处理的异常。"));
|
|
1301
1653
|
} else {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
el(
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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);
|
|
1317
1679
|
}
|
|
1318
1680
|
|
|
1319
1681
|
clearNode(dagsEl);
|
|
@@ -1339,7 +1701,7 @@ async function renderDashboard(scrollTo) {
|
|
|
1339
1701
|
rail.appendChild(
|
|
1340
1702
|
el(
|
|
1341
1703
|
"span",
|
|
1342
|
-
`dag-run-timeline-dot status-${
|
|
1704
|
+
`dag-run-timeline-dot status-${badgeClass(dag.effectiveStatus ?? dag.status)}`,
|
|
1343
1705
|
),
|
|
1344
1706
|
);
|
|
1345
1707
|
item.appendChild(rail);
|
|
@@ -1356,7 +1718,7 @@ async function renderDashboard(scrollTo) {
|
|
|
1356
1718
|
item.appendChild(content);
|
|
1357
1719
|
|
|
1358
1720
|
const state = el("span", "dag-run-timeline-status");
|
|
1359
|
-
state.appendChild(
|
|
1721
|
+
state.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
1360
1722
|
item.appendChild(state);
|
|
1361
1723
|
timeline.appendChild(item);
|
|
1362
1724
|
}
|
|
@@ -1559,7 +1921,7 @@ function renderRunDagProgress(task, snapshot) {
|
|
|
1559
1921
|
panel.appendChild(
|
|
1560
1922
|
metaGrid([
|
|
1561
1923
|
["DAG", dag.title || dag.dagRunId],
|
|
1562
|
-
["
|
|
1924
|
+
["当前状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
1563
1925
|
["进度", `${progress.finished}/${progress.total} 个节点`],
|
|
1564
1926
|
[
|
|
1565
1927
|
"详情",
|
|
@@ -1821,19 +2183,38 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1821
2183
|
.length;
|
|
1822
2184
|
|
|
1823
2185
|
clearNode(metaEl);
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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 ?? "—"],
|
|
1836
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);
|
|
1837
2218
|
|
|
1838
2219
|
clearNode(ranksEl);
|
|
1839
2220
|
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
@@ -1955,6 +2336,7 @@ function route() {
|
|
|
1955
2336
|
if (typeof window !== "undefined") {
|
|
1956
2337
|
window.addEventListener("hashchange", route);
|
|
1957
2338
|
window.addEventListener("load", route);
|
|
2339
|
+
document.addEventListener("pointerdown", closeDagInspectorOnOutsideClick);
|
|
1958
2340
|
document.addEventListener("visibilitychange", () => {
|
|
1959
2341
|
if (!isPageVisible()) return;
|
|
1960
2342
|
const r = parseRoute();
|