@tea-agent/loop-agent 0.32.0 → 0.33.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 +60 -0
- package/dist/executors/model-routing.js +14 -4
- package/dist/governance/manifest-types.js +34 -7
- package/dist/worker/console/chat/model-resolver.js +114 -34
- package/dist/worker/console/chat/workspace-landing.js +58 -22
- package/dist/worker/console/doctor.js +1 -0
- package/dist/worker/console/night-aux-ticker.js +5 -0
- package/dist/worker/console/pi-readiness.js +26 -17
- package/dist/worker/console/server.js +2 -0
- package/dist/worker/console/static/assets/index-3R-GT3a_.js +29 -0
- package/dist/worker/console/static/assets/index-B0EQt_yq.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +140 -0
- package/dist/worker/console/static-src/app/useConsoleShell.js +101 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +304 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +171 -0
- package/dist/worker/console/static-src/app/useRecoveryActions.js +257 -0
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +334 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +229 -0
- package/dist/worker/console/static-src/chat-view-types.js +2 -0
- package/dist/worker/console/static-src/night/night-types.js +24 -0
- package/dist/worker/console/static-src/night/useNightBoard.js +94 -0
- package/dist/worker/console/static-src/night/useNightWizard.js +171 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +253 -0
- package/dist/worker/console/static-src/operator-chat/format.js +71 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +47 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +77 -0
- package/dist/worker/console/static-src/operator-chat/types.js +1 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +320 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +209 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +218 -0
- package/dist/worker/console/static-src/operator-chat/useComposer.js +125 -0
- package/dist/worker/console/static-src/operator-chat/useInterview.js +108 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +123 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +207 -0
- package/dist/worker/observability/read-model.js +106 -0
- package/dist/worker/observe/routes.js +19 -1
- package/dist/worker/observe/static/api.js +42 -3
- package/dist/worker/observe/static/app.js +4 -0
- package/dist/worker/observe/static/constants.js +10 -0
- package/dist/worker/observe/static/custom-select.js +567 -0
- package/dist/worker/observe/static/index.html +47 -6
- package/dist/worker/observe/static/router.js +54 -1
- package/dist/worker/observe/static/state.js +20 -0
- package/dist/worker/observe/static/styles.css +618 -30
- package/dist/worker/observe/static/views/dag-inspector.js +20 -17
- package/dist/worker/observe/static/views/dag.js +136 -59
- package/dist/worker/observe/static/views/dags.js +877 -0
- package/dist/worker/observe/static/views/dashboard.js +15 -4
- package/dist/workflows/dag/init-hybrid.js +58 -29
- package/docs/templates/agent-worker-production-readiness-checklist.md +26 -24
- package/docs/templates/harness.schema.json +5 -0
- package/harness.json +4 -4
- package/package.json +5 -4
- package/dist/worker/console/static/assets/index-D9gnJn_l.js +0 -29
- package/dist/worker/console/static/assets/index-rajoXwkM.css +0 -1
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
/** Observe UI — formal DAG run history page (independent #/dags). */
|
|
2
|
+
import { UI_TEXT } from "../constants.js";
|
|
3
|
+
import { el, clearNode } from "../dom.js";
|
|
4
|
+
import {
|
|
5
|
+
dagStatusBadge,
|
|
6
|
+
formatDuration,
|
|
7
|
+
formatTs,
|
|
8
|
+
} from "../format.js";
|
|
9
|
+
import {
|
|
10
|
+
buildDagHistoryHash,
|
|
11
|
+
clampDagHistoryPage,
|
|
12
|
+
clampDagHistoryPageSize,
|
|
13
|
+
DAG_HISTORY_ALLOWED_PAGE_SIZES,
|
|
14
|
+
DAG_HISTORY_DEFAULT_PAGE,
|
|
15
|
+
DAG_HISTORY_DEFAULT_PAGE_SIZE,
|
|
16
|
+
navigate,
|
|
17
|
+
parseRoute,
|
|
18
|
+
} from "../router.js";
|
|
19
|
+
import { fetchDagRunHistory, summarizeFetchError } from "../api.js";
|
|
20
|
+
import { copyText } from "../copy.js";
|
|
21
|
+
import { uiState } from "../state.js";
|
|
22
|
+
import { showView, setBreadcrumb, updateHeaderRefresh } from "../shell-chrome.js";
|
|
23
|
+
import { createCustomSelect } from "../custom-select.js";
|
|
24
|
+
|
|
25
|
+
let toolbarBound = false;
|
|
26
|
+
/** Per-button copy success restore timers (WeakMap so multi-copy is independent). */
|
|
27
|
+
const copyFeedbackTimers = new WeakMap();
|
|
28
|
+
let copyToastTimer = null;
|
|
29
|
+
|
|
30
|
+
function ensureHistorySessionState() {
|
|
31
|
+
if (!(uiState.dagHistoryCache instanceof Map)) {
|
|
32
|
+
uiState.dagHistoryCache = new Map();
|
|
33
|
+
}
|
|
34
|
+
if (!(uiState.dagHistoryPrefetchInFlight instanceof Map)) {
|
|
35
|
+
uiState.dagHistoryPrefetchInFlight = new Map();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cacheKey(pageSize, page) {
|
|
40
|
+
return `${pageSize}:${page}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function currentHistoryQuery() {
|
|
44
|
+
const route = parseRoute();
|
|
45
|
+
if (route.view === "dags") {
|
|
46
|
+
return {
|
|
47
|
+
page: clampDagHistoryPage(route.page ?? DAG_HISTORY_DEFAULT_PAGE),
|
|
48
|
+
pageSize: clampDagHistoryPageSize(
|
|
49
|
+
route.pageSize ?? DAG_HISTORY_DEFAULT_PAGE_SIZE,
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
page: DAG_HISTORY_DEFAULT_PAGE,
|
|
55
|
+
pageSize: DAG_HISTORY_DEFAULT_PAGE_SIZE,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function syncHash(page, pageSize, { replace = false } = {}) {
|
|
60
|
+
const next = buildDagHistoryHash(page, pageSize);
|
|
61
|
+
const current = (location.hash || "").replace(/^#/, "") || "/";
|
|
62
|
+
const currentPath = current.split("?")[0];
|
|
63
|
+
const nextPath = next.split("?")[0];
|
|
64
|
+
const currentQs = new URLSearchParams(current.split("?")[1] || "").toString();
|
|
65
|
+
const nextQs = new URLSearchParams(next.split("?")[1] || "").toString();
|
|
66
|
+
if (current === next || (currentPath === nextPath && currentQs === nextQs)) {
|
|
67
|
+
if (current !== next) {
|
|
68
|
+
if (replace && typeof history !== "undefined" && history.replaceState) {
|
|
69
|
+
const base = `${location.pathname}${location.search}`;
|
|
70
|
+
history.replaceState(null, "", `${base}#${next}`);
|
|
71
|
+
} else {
|
|
72
|
+
location.hash = next;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
if (replace && typeof history !== "undefined" && history.replaceState) {
|
|
78
|
+
const base = `${location.pathname}${location.search}`;
|
|
79
|
+
history.replaceState(null, "", `${base}#${next}`);
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
navigate(next);
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setControlsDisabled(disabled) {
|
|
87
|
+
const root = document.getElementById("view-dags");
|
|
88
|
+
if (!root) return;
|
|
89
|
+
for (const elCtrl of root.querySelectorAll(
|
|
90
|
+
"button[data-dags-action], select[data-dags-page-size]",
|
|
91
|
+
)) {
|
|
92
|
+
// Keep current page number button marked but still disable navigation set.
|
|
93
|
+
elCtrl.disabled = Boolean(disabled);
|
|
94
|
+
}
|
|
95
|
+
// Custom pageSize control (button trigger + root.disabled mirror).
|
|
96
|
+
for (const custom of root.querySelectorAll("[data-dags-page-size]")) {
|
|
97
|
+
const isCustomRoot =
|
|
98
|
+
(typeof custom.className === "string" &&
|
|
99
|
+
custom.className.split(/\s+/).includes("custom-select")) ||
|
|
100
|
+
typeof custom.setCustomSelectDisabled === "function";
|
|
101
|
+
if (!isCustomRoot) continue;
|
|
102
|
+
if (typeof custom.setCustomSelectDisabled === "function") {
|
|
103
|
+
custom.setCustomSelectDisabled(Boolean(disabled));
|
|
104
|
+
} else {
|
|
105
|
+
custom.disabled = Boolean(disabled);
|
|
106
|
+
const trigger = custom.querySelector?.(".custom-select-trigger");
|
|
107
|
+
if (trigger) trigger.disabled = Boolean(disabled);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const page = uiState.dagHistoryPage;
|
|
111
|
+
if (page && !disabled) {
|
|
112
|
+
const prev = root.querySelector('[data-dags-action="prev"]');
|
|
113
|
+
const next = root.querySelector('[data-dags-action="next"]');
|
|
114
|
+
const atStart = page.page <= 1;
|
|
115
|
+
const atEnd = page.page >= page.totalPages;
|
|
116
|
+
if (prev) prev.disabled = atStart;
|
|
117
|
+
if (next) next.disabled = atEnd;
|
|
118
|
+
// Current page button stays non-interactive.
|
|
119
|
+
const currentBtn = root.querySelector(
|
|
120
|
+
`[data-dags-action="page"][data-dags-page="${page.page}"]`,
|
|
121
|
+
);
|
|
122
|
+
if (currentBtn) currentBtn.disabled = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bindToolbar() {
|
|
127
|
+
if (toolbarBound) return;
|
|
128
|
+
const root = document.getElementById("view-dags");
|
|
129
|
+
if (!root) return;
|
|
130
|
+
toolbarBound = true;
|
|
131
|
+
root.addEventListener("click", (event) => {
|
|
132
|
+
const target = event.target;
|
|
133
|
+
if (!target || typeof target.closest !== "function") return;
|
|
134
|
+
const actionEl = target.closest("[data-dags-action]");
|
|
135
|
+
if (!actionEl) return;
|
|
136
|
+
const action =
|
|
137
|
+
typeof actionEl.getAttribute === "function"
|
|
138
|
+
? actionEl.getAttribute("data-dags-action")
|
|
139
|
+
: null;
|
|
140
|
+
if (!action) return;
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
event.stopPropagation();
|
|
143
|
+
if (action === "refresh") {
|
|
144
|
+
void loadHistory({ reason: "manual", force: true });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Pagination / pageSize: hash-only. Formal load enters only via startDagsView.
|
|
148
|
+
const page = uiState.dagHistoryPage;
|
|
149
|
+
const q = currentHistoryQuery();
|
|
150
|
+
const totalPages = page?.totalPages ?? 1;
|
|
151
|
+
if (action === "prev") {
|
|
152
|
+
const nextPage = Math.max(1, q.page - 1);
|
|
153
|
+
if (nextPage !== q.page) syncHash(nextPage, q.pageSize);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (action === "next") {
|
|
157
|
+
const nextPage = Math.min(totalPages, q.page + 1);
|
|
158
|
+
if (nextPage !== q.page) syncHash(nextPage, q.pageSize);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (action === "page") {
|
|
162
|
+
const targetPage = clampDagHistoryPage(
|
|
163
|
+
actionEl.getAttribute("data-dags-page"),
|
|
164
|
+
totalPages,
|
|
165
|
+
);
|
|
166
|
+
if (targetPage !== q.page) syncHash(targetPage, q.pageSize);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// pageSize selection is wired directly via createCustomSelect onChange
|
|
171
|
+
// (hash-only). Keep a legacy change listener for any residual native select.
|
|
172
|
+
root.addEventListener("change", (event) => {
|
|
173
|
+
const target = event.target;
|
|
174
|
+
if (!target || typeof target.matches !== "function") return;
|
|
175
|
+
if (!target.matches("[data-dags-page-size]")) return;
|
|
176
|
+
const pageSize = clampDagHistoryPageSize(target.value);
|
|
177
|
+
// Hash-only; formal load via startDagsView / hashchange.
|
|
178
|
+
syncHash(1, pageSize);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function focusHistoryHeading() {
|
|
183
|
+
const title = document.getElementById("dags-page-title");
|
|
184
|
+
if (title && typeof title.focus === "function") {
|
|
185
|
+
try {
|
|
186
|
+
title.focus({ preventScroll: false });
|
|
187
|
+
} catch {
|
|
188
|
+
title.focus();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (typeof title?.scrollIntoView === "function") {
|
|
192
|
+
title.scrollIntoView({ block: "nearest" });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderShell() {
|
|
197
|
+
showView("dags");
|
|
198
|
+
setBreadcrumb([
|
|
199
|
+
{ label: UI_TEXT.dashboard, href: "#/" },
|
|
200
|
+
{ label: UI_TEXT.dagHistory },
|
|
201
|
+
]);
|
|
202
|
+
const title = document.getElementById("dags-page-title");
|
|
203
|
+
if (title) title.textContent = UI_TEXT.dagHistory;
|
|
204
|
+
bindToolbar();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function progressText(run) {
|
|
208
|
+
const finished = run?.progress?.finished ?? 0;
|
|
209
|
+
const total = run?.progress?.total ?? 0;
|
|
210
|
+
return `${finished}/${total}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function announceCopy(message) {
|
|
214
|
+
const live = document.getElementById("dags-copy-announce");
|
|
215
|
+
if (!live) return;
|
|
216
|
+
live.textContent = "";
|
|
217
|
+
// Force polite re-announcement on repeated successes.
|
|
218
|
+
try {
|
|
219
|
+
void live.offsetWidth;
|
|
220
|
+
} catch {
|
|
221
|
+
// ignore non-DOM stubs
|
|
222
|
+
}
|
|
223
|
+
live.textContent = message;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function showCopyToast(message) {
|
|
227
|
+
const toast = document.getElementById("dags-copy-toast");
|
|
228
|
+
if (!toast) return;
|
|
229
|
+
toast.textContent = message;
|
|
230
|
+
toast.hidden = false;
|
|
231
|
+
if (copyToastTimer) clearTimeout(copyToastTimer);
|
|
232
|
+
copyToastTimer = setTimeout(() => {
|
|
233
|
+
toast.hidden = true;
|
|
234
|
+
toast.textContent = "";
|
|
235
|
+
}, 3000);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function handleCopyRunId(event, dagRunId, copyBtn) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
event.stopPropagation();
|
|
241
|
+
const ok = await copyText(dagRunId);
|
|
242
|
+
if (ok) {
|
|
243
|
+
if (copyBtn) {
|
|
244
|
+
const prevTimer = copyFeedbackTimers.get(copyBtn);
|
|
245
|
+
if (prevTimer != null) clearTimeout(prevTimer);
|
|
246
|
+
copyBtn.classList.add("is-copied");
|
|
247
|
+
const timer = setTimeout(() => {
|
|
248
|
+
copyBtn.classList.remove("is-copied");
|
|
249
|
+
copyFeedbackTimers.delete(copyBtn);
|
|
250
|
+
}, 1500);
|
|
251
|
+
copyFeedbackTimers.set(copyBtn, timer);
|
|
252
|
+
}
|
|
253
|
+
announceCopy(UI_TEXT.dagHistoryCopiedAnnounce);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
showCopyToast(UI_TEXT.dagHistoryCopyFailed);
|
|
257
|
+
announceCopy(UI_TEXT.dagHistoryCopyFailed);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildCopyIcon() {
|
|
261
|
+
// Dual-rectangle copy glyph (inline SVG, ~16px).
|
|
262
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
263
|
+
svg.setAttribute("class", "dags-copy-icon dags-copy-icon-rects");
|
|
264
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
265
|
+
svg.setAttribute("width", "16");
|
|
266
|
+
svg.setAttribute("height", "16");
|
|
267
|
+
svg.setAttribute("aria-hidden", "true");
|
|
268
|
+
svg.setAttribute("focusable", "false");
|
|
269
|
+
const r1 = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
270
|
+
r1.setAttribute("x", "5");
|
|
271
|
+
r1.setAttribute("y", "5");
|
|
272
|
+
r1.setAttribute("width", "9");
|
|
273
|
+
r1.setAttribute("height", "9");
|
|
274
|
+
r1.setAttribute("rx", "1.5");
|
|
275
|
+
r1.setAttribute("fill", "none");
|
|
276
|
+
r1.setAttribute("stroke", "currentColor");
|
|
277
|
+
r1.setAttribute("stroke-width", "1.5");
|
|
278
|
+
const r2 = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
279
|
+
r2.setAttribute("x", "2");
|
|
280
|
+
r2.setAttribute("y", "2");
|
|
281
|
+
r2.setAttribute("width", "9");
|
|
282
|
+
r2.setAttribute("height", "9");
|
|
283
|
+
r2.setAttribute("rx", "1.5");
|
|
284
|
+
r2.setAttribute("fill", "none");
|
|
285
|
+
r2.setAttribute("stroke", "currentColor");
|
|
286
|
+
r2.setAttribute("stroke-width", "1.5");
|
|
287
|
+
svg.appendChild(r2);
|
|
288
|
+
svg.appendChild(r1);
|
|
289
|
+
return svg;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function buildCheckIcon() {
|
|
293
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
294
|
+
svg.setAttribute("class", "dags-copy-icon dags-copy-icon-check");
|
|
295
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
296
|
+
svg.setAttribute("width", "16");
|
|
297
|
+
svg.setAttribute("height", "16");
|
|
298
|
+
svg.setAttribute("aria-hidden", "true");
|
|
299
|
+
svg.setAttribute("focusable", "false");
|
|
300
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
301
|
+
path.setAttribute(
|
|
302
|
+
"d",
|
|
303
|
+
"M3.5 8.5 6.5 11.5 12.5 4.5",
|
|
304
|
+
);
|
|
305
|
+
path.setAttribute("fill", "none");
|
|
306
|
+
path.setAttribute("stroke", "currentColor");
|
|
307
|
+
path.setAttribute("stroke-width", "1.8");
|
|
308
|
+
path.setAttribute("stroke-linecap", "round");
|
|
309
|
+
path.setAttribute("stroke-linejoin", "round");
|
|
310
|
+
svg.appendChild(path);
|
|
311
|
+
return svg;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildRow(run) {
|
|
315
|
+
const tr = document.createElement("tr");
|
|
316
|
+
tr.classList.add("clickable", "dags-history-row");
|
|
317
|
+
tr.dataset.dagRunId = run.dagRunId;
|
|
318
|
+
tr.addEventListener("click", (event) => {
|
|
319
|
+
const t = event.target;
|
|
320
|
+
if (t instanceof Element && t.closest("a, button, [data-copy-run-id]")) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
navigate(`/dag/${encodeURIComponent(run.dagRunId)}`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const nameTd = document.createElement("td");
|
|
327
|
+
const nameLink = el("a", "dags-name-link", run.title || run.dagRunId);
|
|
328
|
+
nameLink.href = `#/dag/${encodeURIComponent(run.dagRunId)}`;
|
|
329
|
+
nameLink.addEventListener("click", (event) => {
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
event.stopPropagation();
|
|
332
|
+
navigate(`/dag/${encodeURIComponent(run.dagRunId)}`);
|
|
333
|
+
});
|
|
334
|
+
nameTd.appendChild(nameLink);
|
|
335
|
+
tr.appendChild(nameTd);
|
|
336
|
+
|
|
337
|
+
const idTd = document.createElement("td");
|
|
338
|
+
idTd.className = "dags-run-id-cell";
|
|
339
|
+
const idWrap = el("div", "dags-run-id-wrap");
|
|
340
|
+
const idCode = el("code", "dags-run-id", run.dagRunId);
|
|
341
|
+
idCode.title = run.dagRunId;
|
|
342
|
+
const copyBtn = el("button", "dags-copy-btn dags-copy-btn-icon");
|
|
343
|
+
copyBtn.type = "button";
|
|
344
|
+
copyBtn.setAttribute("data-copy-run-id", run.dagRunId);
|
|
345
|
+
copyBtn.setAttribute("aria-label", `复制运行 ID ${run.dagRunId}`);
|
|
346
|
+
copyBtn.title = UI_TEXT.dagHistoryCopyTooltip;
|
|
347
|
+
copyBtn.appendChild(buildCopyIcon());
|
|
348
|
+
copyBtn.appendChild(buildCheckIcon());
|
|
349
|
+
copyBtn.addEventListener("click", (event) => {
|
|
350
|
+
void handleCopyRunId(event, run.dagRunId, copyBtn);
|
|
351
|
+
});
|
|
352
|
+
idWrap.appendChild(idCode);
|
|
353
|
+
idWrap.appendChild(copyBtn);
|
|
354
|
+
idTd.appendChild(idWrap);
|
|
355
|
+
tr.appendChild(idTd);
|
|
356
|
+
|
|
357
|
+
const statusTd = document.createElement("td");
|
|
358
|
+
statusTd.appendChild(dagStatusBadge(run.effectiveStatus ?? run.status));
|
|
359
|
+
tr.appendChild(statusTd);
|
|
360
|
+
|
|
361
|
+
const startedTd = document.createElement("td");
|
|
362
|
+
startedTd.textContent = formatTs(run.startedAt);
|
|
363
|
+
tr.appendChild(startedTd);
|
|
364
|
+
|
|
365
|
+
const durationTd = document.createElement("td");
|
|
366
|
+
durationTd.textContent = formatDuration(run.durationMs);
|
|
367
|
+
tr.appendChild(durationTd);
|
|
368
|
+
|
|
369
|
+
const progressTd = document.createElement("td");
|
|
370
|
+
progressTd.textContent = progressText(run);
|
|
371
|
+
tr.appendChild(progressTd);
|
|
372
|
+
|
|
373
|
+
return tr;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function renderTable(container, runs) {
|
|
377
|
+
clearNode(container);
|
|
378
|
+
const wrap = el("div", "dags-table-wrap table-scroll");
|
|
379
|
+
const table = document.createElement("table");
|
|
380
|
+
table.className = "dags-history-table";
|
|
381
|
+
const thead = document.createElement("thead");
|
|
382
|
+
const headRow = document.createElement("tr");
|
|
383
|
+
for (const h of ["DAG 名称", "运行 ID", "状态", "开始时间", "耗时", "进度"]) {
|
|
384
|
+
const th = document.createElement("th");
|
|
385
|
+
th.textContent = h;
|
|
386
|
+
if (h === "DAG 名称") th.id = "dags-table-heading";
|
|
387
|
+
headRow.appendChild(th);
|
|
388
|
+
}
|
|
389
|
+
thead.appendChild(headRow);
|
|
390
|
+
table.appendChild(thead);
|
|
391
|
+
const tbody = document.createElement("tbody");
|
|
392
|
+
for (const run of runs) {
|
|
393
|
+
tbody.appendChild(buildRow(run));
|
|
394
|
+
}
|
|
395
|
+
table.appendChild(tbody);
|
|
396
|
+
wrap.appendChild(table);
|
|
397
|
+
container.appendChild(wrap);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Fixed page window:
|
|
402
|
+
* - totalPages <= 7: show all
|
|
403
|
+
* - else: strictly first, last, current, current±1 (clipped) with ellipsis gaps
|
|
404
|
+
* (no near-edge filler pages beyond that set)
|
|
405
|
+
*/
|
|
406
|
+
export function buildPageWindow(current, totalPages) {
|
|
407
|
+
const total = Math.max(1, Number(totalPages) || 1);
|
|
408
|
+
const page = Math.min(Math.max(1, Number(current) || 1), total);
|
|
409
|
+
if (total <= 7) {
|
|
410
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
411
|
+
}
|
|
412
|
+
const pages = new Set([1, total, page]);
|
|
413
|
+
if (page - 1 >= 1) pages.add(page - 1);
|
|
414
|
+
if (page + 1 <= total) pages.add(page + 1);
|
|
415
|
+
const sorted = [...pages].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
|
|
416
|
+
const out = [];
|
|
417
|
+
let prev = 0;
|
|
418
|
+
for (const p of sorted) {
|
|
419
|
+
if (prev && p - prev > 1) out.push("ellipsis");
|
|
420
|
+
out.push(p);
|
|
421
|
+
prev = p;
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function renderPaginationControls(metaEl, {
|
|
427
|
+
page,
|
|
428
|
+
pageSize,
|
|
429
|
+
totalPages,
|
|
430
|
+
total,
|
|
431
|
+
generatedAt: _generatedAt,
|
|
432
|
+
disabled = false,
|
|
433
|
+
showSummary = true,
|
|
434
|
+
busy = false,
|
|
435
|
+
} = {}) {
|
|
436
|
+
clearNode(metaEl);
|
|
437
|
+
const bar = el("div", "dags-pagination");
|
|
438
|
+
if (busy) bar.classList.add("is-busy");
|
|
439
|
+
|
|
440
|
+
const summaryWrap = el("div", "dags-pagination-summary");
|
|
441
|
+
if (showSummary && total != null) {
|
|
442
|
+
summaryWrap.appendChild(
|
|
443
|
+
el(
|
|
444
|
+
"p",
|
|
445
|
+
"dags-meta muted",
|
|
446
|
+
`共 ${total} 条 · 第 ${page} / ${totalPages} 页`,
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
summaryWrap.appendChild(el("p", "dags-meta muted", "加载中…"));
|
|
451
|
+
}
|
|
452
|
+
bar.appendChild(summaryWrap);
|
|
453
|
+
|
|
454
|
+
const pagesWrap = el("div", "dags-pagination-pages");
|
|
455
|
+
const mkIcon = (iconClass) => {
|
|
456
|
+
const icon = el("i", iconClass);
|
|
457
|
+
icon.setAttribute("aria-hidden", "true");
|
|
458
|
+
return icon;
|
|
459
|
+
};
|
|
460
|
+
const mkNavBtn = (action, iconClass, ariaLabel) => {
|
|
461
|
+
const btn = el("button", "btn btn-secondary dags-page-nav");
|
|
462
|
+
btn.type = "button";
|
|
463
|
+
btn.setAttribute("data-dags-action", action);
|
|
464
|
+
btn.setAttribute("aria-label", ariaLabel);
|
|
465
|
+
btn.title = ariaLabel;
|
|
466
|
+
btn.appendChild(mkIcon(iconClass));
|
|
467
|
+
if (disabled) btn.disabled = true;
|
|
468
|
+
return btn;
|
|
469
|
+
};
|
|
470
|
+
const prevBtn = mkNavBtn("prev", "ri-arrow-left-s-line", "上一页");
|
|
471
|
+
const nextBtn = mkNavBtn("next", "ri-arrow-right-s-line", "下一页");
|
|
472
|
+
if (!disabled) {
|
|
473
|
+
if (page <= 1) prevBtn.disabled = true;
|
|
474
|
+
if (page >= totalPages) nextBtn.disabled = true;
|
|
475
|
+
}
|
|
476
|
+
pagesWrap.appendChild(prevBtn);
|
|
477
|
+
|
|
478
|
+
const windowItems = buildPageWindow(page, totalPages);
|
|
479
|
+
for (const item of windowItems) {
|
|
480
|
+
if (item === "ellipsis") {
|
|
481
|
+
const dots = el("span", "dags-page-ellipsis", "…");
|
|
482
|
+
dots.setAttribute("aria-hidden", "true");
|
|
483
|
+
pagesWrap.appendChild(dots);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const btn = el(
|
|
487
|
+
"button",
|
|
488
|
+
`btn btn-secondary dags-page-num${item === page ? " is-current" : ""}`,
|
|
489
|
+
String(item),
|
|
490
|
+
);
|
|
491
|
+
btn.type = "button";
|
|
492
|
+
btn.setAttribute("data-dags-action", "page");
|
|
493
|
+
btn.setAttribute("data-dags-page", String(item));
|
|
494
|
+
btn.setAttribute("aria-label", `第 ${item} 页`);
|
|
495
|
+
if (item === page) {
|
|
496
|
+
btn.setAttribute("aria-current", "page");
|
|
497
|
+
btn.disabled = true;
|
|
498
|
+
} else if (disabled) {
|
|
499
|
+
btn.disabled = true;
|
|
500
|
+
}
|
|
501
|
+
pagesWrap.appendChild(btn);
|
|
502
|
+
}
|
|
503
|
+
pagesWrap.appendChild(nextBtn);
|
|
504
|
+
bar.appendChild(pagesWrap);
|
|
505
|
+
|
|
506
|
+
const tools = el("div", "dags-pagination-tools");
|
|
507
|
+
const sizeLabel = el("label", "dags-page-size-label", "每页");
|
|
508
|
+
const select = createCustomSelect({
|
|
509
|
+
ariaLabel: "每页条数",
|
|
510
|
+
value: String(pageSize),
|
|
511
|
+
options: DAG_HISTORY_ALLOWED_PAGE_SIZES.map((size) => ({
|
|
512
|
+
value: String(size),
|
|
513
|
+
label: String(size),
|
|
514
|
+
})),
|
|
515
|
+
disabled: Boolean(disabled),
|
|
516
|
+
className: "dags-page-size-select",
|
|
517
|
+
onChange: (next) => {
|
|
518
|
+
const nextSize = clampDagHistoryPageSize(next);
|
|
519
|
+
// Hash-only; formal load via startDagsView / hashchange.
|
|
520
|
+
syncHash(1, nextSize);
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
select.setAttribute("data-dags-page-size", "1");
|
|
524
|
+
// Also stamp the trigger so attribute queries and a11y tools find the control.
|
|
525
|
+
const pageSizeTrigger =
|
|
526
|
+
typeof select.getCustomSelectTrigger === "function"
|
|
527
|
+
? select.getCustomSelectTrigger()
|
|
528
|
+
: select.querySelector?.(".custom-select-trigger");
|
|
529
|
+
if (pageSizeTrigger) {
|
|
530
|
+
pageSizeTrigger.setAttribute("data-dags-page-size", "1");
|
|
531
|
+
}
|
|
532
|
+
sizeLabel.appendChild(select);
|
|
533
|
+
tools.appendChild(sizeLabel);
|
|
534
|
+
|
|
535
|
+
const refresh = el("button", "btn btn-secondary dags-refresh-btn");
|
|
536
|
+
refresh.type = "button";
|
|
537
|
+
refresh.setAttribute("data-dags-action", "refresh");
|
|
538
|
+
refresh.setAttribute("aria-label", "刷新");
|
|
539
|
+
refresh.title = "刷新";
|
|
540
|
+
refresh.appendChild(mkIcon("ri-refresh-line"));
|
|
541
|
+
if (disabled) refresh.disabled = true;
|
|
542
|
+
tools.appendChild(refresh);
|
|
543
|
+
bar.appendChild(tools);
|
|
544
|
+
|
|
545
|
+
metaEl.appendChild(bar);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function renderPagination(metaEl, page, { busy = false } = {}) {
|
|
549
|
+
renderPaginationControls(metaEl, {
|
|
550
|
+
page: page.page,
|
|
551
|
+
pageSize: page.pageSize,
|
|
552
|
+
totalPages: page.totalPages,
|
|
553
|
+
total: page.total,
|
|
554
|
+
generatedAt: page.generatedAt,
|
|
555
|
+
disabled: Boolean(busy),
|
|
556
|
+
showSummary: true,
|
|
557
|
+
busy,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** First-load Loading skeleton: controls exist and stay disabled. */
|
|
562
|
+
function renderLoadingControls(metaEl) {
|
|
563
|
+
const q = currentHistoryQuery();
|
|
564
|
+
renderPaginationControls(metaEl, {
|
|
565
|
+
page: q.page,
|
|
566
|
+
pageSize: q.pageSize,
|
|
567
|
+
totalPages: Math.max(1, q.page),
|
|
568
|
+
total: null,
|
|
569
|
+
disabled: true,
|
|
570
|
+
showSummary: false,
|
|
571
|
+
busy: true,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderStatusArea(statusEl, kind, message, detail) {
|
|
576
|
+
clearNode(statusEl);
|
|
577
|
+
if (!kind) {
|
|
578
|
+
statusEl.hidden = true;
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
statusEl.hidden = false;
|
|
582
|
+
const node = el(
|
|
583
|
+
"div",
|
|
584
|
+
`view-state view-state-${kind}${kind === "empty" ? " empty" : ""}`,
|
|
585
|
+
);
|
|
586
|
+
if (kind === "error" || kind === "degraded") {
|
|
587
|
+
node.setAttribute("role", "alert");
|
|
588
|
+
} else if (kind === "loading") {
|
|
589
|
+
node.setAttribute("role", "status");
|
|
590
|
+
node.setAttribute("aria-live", "polite");
|
|
591
|
+
}
|
|
592
|
+
node.appendChild(
|
|
593
|
+
el("p", kind === "empty" ? "empty" : kind === "loading" ? "muted" : null, message),
|
|
594
|
+
);
|
|
595
|
+
if (detail) node.appendChild(el("p", "muted", detail));
|
|
596
|
+
if (kind === "error") {
|
|
597
|
+
const retry = el("button", "btn btn-secondary", "重新加载");
|
|
598
|
+
retry.type = "button";
|
|
599
|
+
retry.setAttribute("data-dags-action", "refresh");
|
|
600
|
+
node.appendChild(retry);
|
|
601
|
+
}
|
|
602
|
+
statusEl.appendChild(node);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function pagePayloadFromBody(body, fallbackPage, fallbackPageSize) {
|
|
606
|
+
const totalPages = Math.max(1, Number(body.totalPages) || 1);
|
|
607
|
+
const page = clampDagHistoryPage(body.page ?? fallbackPage, totalPages);
|
|
608
|
+
const pageSize = clampDagHistoryPageSize(body.pageSize ?? fallbackPageSize);
|
|
609
|
+
return {
|
|
610
|
+
schemaVersion: 1,
|
|
611
|
+
generatedAt: body.generatedAt,
|
|
612
|
+
page,
|
|
613
|
+
pageSize,
|
|
614
|
+
total: Number(body.total) || 0,
|
|
615
|
+
totalPages,
|
|
616
|
+
runs: Array.isArray(body.runs) ? body.runs : [],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function applyPageToState(payload) {
|
|
621
|
+
uiState.dagHistoryPage = payload;
|
|
622
|
+
uiState.dagHistoryError = null;
|
|
623
|
+
uiState.dagHistoryDegraded = false;
|
|
624
|
+
const key = cacheKey(payload.pageSize, payload.page);
|
|
625
|
+
ensureHistorySessionState();
|
|
626
|
+
uiState.dagHistoryCache.set(key, payload);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function paint() {
|
|
630
|
+
const tableEl = document.getElementById("dags-table");
|
|
631
|
+
const statusEl = document.getElementById("dags-status");
|
|
632
|
+
const metaEl = document.getElementById("dags-meta");
|
|
633
|
+
const bannerEl = document.getElementById("dags-degraded");
|
|
634
|
+
if (!tableEl || !statusEl || !metaEl) return;
|
|
635
|
+
|
|
636
|
+
const page = uiState.dagHistoryPage;
|
|
637
|
+
const loading = uiState.dagHistoryLoading;
|
|
638
|
+
const busy = uiState.dagHistoryBusy;
|
|
639
|
+
const error = uiState.dagHistoryError;
|
|
640
|
+
const degraded = uiState.dagHistoryDegraded;
|
|
641
|
+
|
|
642
|
+
if (bannerEl) {
|
|
643
|
+
if (degraded && page) {
|
|
644
|
+
bannerEl.hidden = false;
|
|
645
|
+
bannerEl.textContent = UI_TEXT.dagHistoryRefreshFailed;
|
|
646
|
+
if (typeof bannerEl.setAttribute === "function") {
|
|
647
|
+
bannerEl.setAttribute("role", "alert");
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
bannerEl.hidden = true;
|
|
651
|
+
bannerEl.textContent = "";
|
|
652
|
+
if (typeof bannerEl.removeAttribute === "function") {
|
|
653
|
+
bannerEl.removeAttribute("role");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (loading && !page) {
|
|
659
|
+
renderStatusArea(statusEl, "loading", "加载 DAG 运行历史…");
|
|
660
|
+
clearNode(tableEl);
|
|
661
|
+
renderLoadingControls(metaEl);
|
|
662
|
+
setControlsDisabled(true);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (error && !page) {
|
|
667
|
+
renderStatusArea(statusEl, "error", "无法加载 DAG 运行历史。", error);
|
|
668
|
+
clearNode(tableEl);
|
|
669
|
+
clearNode(metaEl);
|
|
670
|
+
setControlsDisabled(false);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (page) {
|
|
675
|
+
if (page.total === 0) {
|
|
676
|
+
renderStatusArea(
|
|
677
|
+
statusEl,
|
|
678
|
+
"empty",
|
|
679
|
+
UI_TEXT.noFormalDagHistory,
|
|
680
|
+
UI_TEXT.noFormalDagHistoryHint,
|
|
681
|
+
);
|
|
682
|
+
clearNode(tableEl);
|
|
683
|
+
} else {
|
|
684
|
+
renderStatusArea(statusEl, null);
|
|
685
|
+
renderTable(tableEl, page.runs ?? []);
|
|
686
|
+
}
|
|
687
|
+
renderPagination(metaEl, page, { busy: Boolean(busy || loading) });
|
|
688
|
+
updateHeaderRefresh(page.generatedAt, { mode: "manual" });
|
|
689
|
+
setControlsDisabled(Boolean(busy || loading));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
renderStatusArea(statusEl, "empty", UI_TEXT.noFormalDagHistory);
|
|
694
|
+
setControlsDisabled(false);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function abortFormalRequest() {
|
|
698
|
+
const prev = uiState.dagHistoryAbortController;
|
|
699
|
+
if (prev && typeof prev.abort === "function") {
|
|
700
|
+
try {
|
|
701
|
+
prev.abort();
|
|
702
|
+
} catch {
|
|
703
|
+
// ignore
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
uiState.dagHistoryAbortController = null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function invalidateAdjacentHistoryCache(page, pageSize, totalPages) {
|
|
710
|
+
ensureHistorySessionState();
|
|
711
|
+
const neighbors = [page - 1, page + 1].filter(
|
|
712
|
+
(p) => p >= 1 && p <= totalPages,
|
|
713
|
+
);
|
|
714
|
+
for (const neighbor of neighbors) {
|
|
715
|
+
uiState.dagHistoryCache.delete(cacheKey(pageSize, neighbor));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function scheduleAdjacentPrefetch(page, pageSize, totalPages) {
|
|
720
|
+
ensureHistorySessionState();
|
|
721
|
+
const generation = uiState.dagHistoryPrefetchGeneration ?? 0;
|
|
722
|
+
const neighbors = [page - 1, page + 1].filter(
|
|
723
|
+
(p) => p >= 1 && p <= totalPages,
|
|
724
|
+
);
|
|
725
|
+
for (const neighbor of neighbors) {
|
|
726
|
+
const key = cacheKey(pageSize, neighbor);
|
|
727
|
+
if (uiState.dagHistoryCache.has(key)) continue;
|
|
728
|
+
// Same-generation in-flight dedupe only; a stale generation must not
|
|
729
|
+
// block a post-refresh re-prefetch for the same pageSize:page key.
|
|
730
|
+
if (uiState.dagHistoryPrefetchInFlight.get(key) === generation) continue;
|
|
731
|
+
uiState.dagHistoryPrefetchInFlight.set(key, generation);
|
|
732
|
+
void (async () => {
|
|
733
|
+
try {
|
|
734
|
+
const result = await fetchDagRunHistory(neighbor, pageSize);
|
|
735
|
+
// Prefetch never mutates URL, focus, loading, busy, or refresh copy.
|
|
736
|
+
if (!result.ok || !result.body || typeof result.body !== "object") {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const payload = pagePayloadFromBody(result.body, neighbor, pageSize);
|
|
740
|
+
// Only store when generation + pageSize still match (post-refresh guard).
|
|
741
|
+
const current = currentHistoryQuery();
|
|
742
|
+
if (current.pageSize !== pageSize) return;
|
|
743
|
+
if ((uiState.dagHistoryPrefetchGeneration ?? 0) !== generation) return;
|
|
744
|
+
uiState.dagHistoryCache.set(cacheKey(pageSize, payload.page), payload);
|
|
745
|
+
} catch {
|
|
746
|
+
// Silent prefetch failure.
|
|
747
|
+
} finally {
|
|
748
|
+
// Stale finally must not clear a newer generation's in-flight slot.
|
|
749
|
+
if (uiState.dagHistoryPrefetchInFlight.get(key) === generation) {
|
|
750
|
+
uiState.dagHistoryPrefetchInFlight.delete(key);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
})();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function loadHistory(options = {}) {
|
|
758
|
+
ensureHistorySessionState();
|
|
759
|
+
const q = {
|
|
760
|
+
page: options.page ?? currentHistoryQuery().page,
|
|
761
|
+
pageSize: options.pageSize ?? currentHistoryQuery().pageSize,
|
|
762
|
+
};
|
|
763
|
+
const force = options.force === true;
|
|
764
|
+
const reason = options.reason ?? "route";
|
|
765
|
+
|
|
766
|
+
// Normalize illegal hash query without creating a navigation loop.
|
|
767
|
+
const normalized = buildDagHistoryHash(q.page, q.pageSize);
|
|
768
|
+
const current = (location.hash || "").replace(/^#/, "");
|
|
769
|
+
if (current.startsWith("/dags") && current !== normalized) {
|
|
770
|
+
syncHash(q.page, q.pageSize, { replace: true });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const key = cacheKey(q.pageSize, q.page);
|
|
774
|
+
if (!force && uiState.dagHistoryCache.has(key)) {
|
|
775
|
+
// Drop any in-flight formal GET so a late response cannot overwrite this
|
|
776
|
+
// hash-aligned cached paint (advance requestSeq + abort).
|
|
777
|
+
abortFormalRequest();
|
|
778
|
+
uiState.dagHistoryRequestSeq += 1;
|
|
779
|
+
const cached = uiState.dagHistoryCache.get(key);
|
|
780
|
+
uiState.dagHistoryPage = cached;
|
|
781
|
+
uiState.dagHistoryError = null;
|
|
782
|
+
uiState.dagHistoryDegraded = false;
|
|
783
|
+
uiState.dagHistoryLoading = false;
|
|
784
|
+
uiState.dagHistoryBusy = false;
|
|
785
|
+
paint();
|
|
786
|
+
scheduleAdjacentPrefetch(cached.page, cached.pageSize, cached.totalPages);
|
|
787
|
+
if (reason === "paginate") focusHistoryHeading();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const requestSeq = ++uiState.dagHistoryRequestSeq;
|
|
792
|
+
abortFormalRequest();
|
|
793
|
+
const controller =
|
|
794
|
+
typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
795
|
+
uiState.dagHistoryAbortController = controller;
|
|
796
|
+
|
|
797
|
+
const hasPage = Boolean(uiState.dagHistoryPage);
|
|
798
|
+
uiState.dagHistoryError = null;
|
|
799
|
+
if (!hasPage) {
|
|
800
|
+
uiState.dagHistoryLoading = true;
|
|
801
|
+
uiState.dagHistoryBusy = false;
|
|
802
|
+
uiState.dagHistoryDegraded = false;
|
|
803
|
+
} else {
|
|
804
|
+
// Cache-miss navigate / manual refresh: keep old table, light busy.
|
|
805
|
+
uiState.dagHistoryLoading = false;
|
|
806
|
+
uiState.dagHistoryBusy = true;
|
|
807
|
+
}
|
|
808
|
+
// Manual refresh: bump prefetch generation and drop neighbor cache so older
|
|
809
|
+
// prefetches cannot write back after we replace the current page.
|
|
810
|
+
if (force && hasPage) {
|
|
811
|
+
uiState.dagHistoryPrefetchGeneration =
|
|
812
|
+
(uiState.dagHistoryPrefetchGeneration ?? 0) + 1;
|
|
813
|
+
const currentPage = uiState.dagHistoryPage;
|
|
814
|
+
if (currentPage) {
|
|
815
|
+
invalidateAdjacentHistoryCache(
|
|
816
|
+
currentPage.page,
|
|
817
|
+
currentPage.pageSize,
|
|
818
|
+
currentPage.totalPages,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
paint();
|
|
823
|
+
|
|
824
|
+
const result = await fetchDagRunHistory(q.page, q.pageSize, {
|
|
825
|
+
signal: controller?.signal,
|
|
826
|
+
});
|
|
827
|
+
if (requestSeq !== uiState.dagHistoryRequestSeq) return;
|
|
828
|
+
|
|
829
|
+
uiState.dagHistoryLoading = false;
|
|
830
|
+
uiState.dagHistoryBusy = false;
|
|
831
|
+
uiState.dagHistoryAbortController = null;
|
|
832
|
+
|
|
833
|
+
if (result?.aborted) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (!result.ok || !result.body || typeof result.body !== "object") {
|
|
838
|
+
const summary = summarizeFetchError(result);
|
|
839
|
+
if (uiState.dagHistoryPage) {
|
|
840
|
+
uiState.dagHistoryDegraded = true;
|
|
841
|
+
uiState.dagHistoryError = summary;
|
|
842
|
+
} else {
|
|
843
|
+
uiState.dagHistoryDegraded = false;
|
|
844
|
+
uiState.dagHistoryError = summary;
|
|
845
|
+
uiState.dagHistoryPage = null;
|
|
846
|
+
}
|
|
847
|
+
paint();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const payload = pagePayloadFromBody(result.body, q.page, q.pageSize);
|
|
852
|
+
// Server may clamp over-range page; reflect in hash.
|
|
853
|
+
if (payload.page !== q.page || payload.pageSize !== q.pageSize) {
|
|
854
|
+
syncHash(payload.page, payload.pageSize, { replace: true });
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
applyPageToState(payload);
|
|
858
|
+
// Successful manual refresh also ensures neighbors stay invalidated against
|
|
859
|
+
// the newly applied page metadata (totalPages may change).
|
|
860
|
+
if (force) {
|
|
861
|
+
invalidateAdjacentHistoryCache(
|
|
862
|
+
payload.page,
|
|
863
|
+
payload.pageSize,
|
|
864
|
+
payload.totalPages,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
paint();
|
|
868
|
+
scheduleAdjacentPrefetch(payload.page, payload.pageSize, payload.totalPages);
|
|
869
|
+
if (reason === "paginate") focusHistoryHeading();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export async function startDagsView() {
|
|
873
|
+
ensureHistorySessionState();
|
|
874
|
+
renderShell();
|
|
875
|
+
const q = currentHistoryQuery();
|
|
876
|
+
await loadHistory({ reason: "route", page: q.page, pageSize: q.pageSize });
|
|
877
|
+
}
|