@zq-silk/yui 2.10.0 → 2.12.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/dist/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/callerDiscovery.js +18 -0
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +2 -3
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +12 -12
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +7 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +22 -12
- package/dist/runtime/managedIdentity.js +5 -3
- package/dist/runtime/sessionCaller.js +9 -10
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/maintenanceCaller.js +17 -25
- package/dist/storage/nativeSessionLookup.js +23 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sessionHistorySchema.js +17 -0
- package/dist/storage/sqliteSchema.js +96 -11
- package/dist/storage/sqliteStore.js +71 -4
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +4 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/i18n/en.js +29 -2
- package/dist/web/assets/client/i18n/zh-CN.js +29 -2
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webServer.js +6 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +130 -0
- package/docs/job-runs.md +392 -17
- package/docs/managed-turn-and-session-runtime.md +10 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
- package/docs/release-workflow.md +79 -0
- package/docs/release-workflow.zh-CN.md +58 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +45 -1
- package/docs/storage-baseline.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +12 -5
- package/skills/yui-runtime/VERSION +1 -1
|
@@ -62,6 +62,8 @@ export function beginGateVerification(store, identity, plan, now) {
|
|
|
62
62
|
* artifact is self-contained evidence after the job's own logs are cleaned.
|
|
63
63
|
*/
|
|
64
64
|
export async function recordGateArtifactFromJob(store, home, identity, plan, job, now) {
|
|
65
|
+
if (job.delegation)
|
|
66
|
+
throw new Error("Task delegation is not process verification evidence.");
|
|
65
67
|
const steps = (job.result?.steps ?? []).map((result) => {
|
|
66
68
|
const planned = job.steps.find((step) => step.name === result.name);
|
|
67
69
|
return {
|
|
@@ -216,6 +218,8 @@ function sanitizeLogName(name) {
|
|
|
216
218
|
* failure; the Integration gate has not started when bootstrap fails.
|
|
217
219
|
*/
|
|
218
220
|
export function checkResultsFromGateJob(job, _home) {
|
|
221
|
+
if (job.delegation)
|
|
222
|
+
throw new Error("Task delegation is not process verification evidence.");
|
|
219
223
|
const results = new Map((job.result?.steps ?? []).map((step) => [step.name, step]));
|
|
220
224
|
const checks = [];
|
|
221
225
|
for (const step of job.steps) {
|
|
@@ -98,6 +98,18 @@ function renderSidebar(deps, progress) {
|
|
|
98
98
|
reset(deps, progress);
|
|
99
99
|
});
|
|
100
100
|
el.catalogCount.textContent = fill(t("catalog.count"), { shown: state.tasks.length, total: state.catalogTotal });
|
|
101
|
+
let source = el.filters.querySelector("select");
|
|
102
|
+
if (!source) {
|
|
103
|
+
source = h("select", { "aria-label": t("jobs.taskCategory") });
|
|
104
|
+
source.addEventListener("change", function () {
|
|
105
|
+
state.source = source.value; state.attentionFilter = null; state.query = ""; el.search.value = "";
|
|
106
|
+
syncUrl(state, false); reset(deps, progress);
|
|
107
|
+
});
|
|
108
|
+
el.filters.append(source);
|
|
109
|
+
}
|
|
110
|
+
source.replaceChildren(...["ordinary", "job", "all"].map(value =>
|
|
111
|
+
h("option", { value }, t("jobs.category." + value))));
|
|
112
|
+
source.value = state.query.trim() || state.attentionFilter ? "all" : state.source || "ordinary";
|
|
101
113
|
el.catalogReset.hidden = !state.catalogCursor;
|
|
102
114
|
el.catalogNext.hidden = !state.nextCursor && !state.catalogCursor;
|
|
103
115
|
el.catalogAttentionReset.hidden = !state.attentionFilter;
|
|
@@ -137,6 +149,9 @@ function catalogQuery(state) {
|
|
|
137
149
|
if (state.filter !== "all") query.set("status", state.filter);
|
|
138
150
|
if (state.query.trim()) query.set("search", state.query.trim());
|
|
139
151
|
if (state.attentionFilter) query.set("attention", state.attentionFilter);
|
|
152
|
+
// Search and attention remain cross-category discovery; the server owns filtering.
|
|
153
|
+
if (!state.query.trim() && !state.attentionFilter && state.source !== "all")
|
|
154
|
+
query.set("source", state.source || "ordinary");
|
|
140
155
|
if (state.catalogCursor) query.set("cursor", state.catalogCursor);
|
|
141
156
|
return query.toString();
|
|
142
157
|
}
|
|
@@ -192,6 +207,7 @@ function applyFilters(deps, params) {
|
|
|
192
207
|
state.query = params.get("q") || "";
|
|
193
208
|
state.attentionFilter = ATTENTION_KINDS.includes(params.get("attention")) ? params.get("attention") : null;
|
|
194
209
|
state.catalogAll = params.get("all") === "true";
|
|
210
|
+
state.source = ["ordinary", "job", "all"].includes(params.get("source")) ? params.get("source") : "ordinary";
|
|
195
211
|
deps.el.search.value = state.query;
|
|
196
212
|
}
|
|
197
213
|
|
|
@@ -5,8 +5,35 @@
|
|
|
5
5
|
export const EN = {
|
|
6
6
|
"tabs.jobs": "Jobs",
|
|
7
7
|
"settings.section.jobs": "Jobs & schedules",
|
|
8
|
+
"jobs.builtinSync": "Built-in Project sync · safe fast-forward only",
|
|
9
|
+
"jobs.automatic": "Automatic sync preference",
|
|
10
|
+
"jobs.syncEvery": "Sync interval (milliseconds, minimum 60000)",
|
|
11
|
+
"jobs.enabled": "On",
|
|
12
|
+
"jobs.disabled": "Off",
|
|
13
|
+
"jobs.result": "Business result",
|
|
8
14
|
"jobs.heading": "Jobs & schedules",
|
|
9
|
-
"jobs.help": "Run now
|
|
15
|
+
"jobs.help": "Ownership is separate from execution target. Run now admits one execution; disable prevents future admissions. Cancellation does not prove business or process termination. Global management needs no Operator Session; Task management retains its current authority checks.",
|
|
16
|
+
"jobs.executor": "Execution method",
|
|
17
|
+
"jobs.executionTask": "Open execution Task",
|
|
18
|
+
"jobs.sourceJob": "Created by Job / Run",
|
|
19
|
+
"jobs.taskCategory": "Task category",
|
|
20
|
+
"jobs.category.ordinary": "Ordinary Tasks",
|
|
21
|
+
"jobs.category.job": "Job execution Tasks",
|
|
22
|
+
"jobs.category.all": "All Tasks",
|
|
23
|
+
"jobs.recentSuccess": "Most recent successful Run",
|
|
24
|
+
"jobs.diagnostics": "Execution diagnostics",
|
|
25
|
+
"jobs.artifacts": "Registered fixed artifacts",
|
|
26
|
+
"jobs.registeredOnly": "Only the exact registered owner, Run, commit, path and digest are readable. Linked images must also be registered.",
|
|
27
|
+
"jobs.inspectSaved": "Inspect saved / unregistered evidence",
|
|
28
|
+
"jobs.register": "Register exact saved artifact",
|
|
29
|
+
"jobs.registerHelp": "Registration publishes this saved evidence; it does not rerun work. Saved but unregistered files are not yet formal results.",
|
|
30
|
+
"jobs.cancelBoundary": "Request cancellation of this exact Run only. This does not stop its execution Task or interrupt a shared Agent Session. Inspect the result for confirmation.",
|
|
31
|
+
"jobs.inspectSession": "Inspect Job-owned Session",
|
|
32
|
+
"jobs.stopSession": "Request stop of selected Session",
|
|
33
|
+
"jobs.newSession": "Replace stopped Session",
|
|
34
|
+
"jobs.sessionBoundary": "Only this Job-owned Session is targeted. Stop is a cleanup request; replacement requires proven termination and settled input. Neither operation replays work or removes evidence.",
|
|
35
|
+
"jobs.stale": "Refresh failed. Any retained facts are from the previous observation, not live success.",
|
|
36
|
+
"jobs.unavailable": "Selected Job is unavailable in this management scope.",
|
|
10
37
|
"jobs.details": "Details",
|
|
11
38
|
"jobs.scope": "Ownership",
|
|
12
39
|
"jobs.project": "Target Project",
|
|
@@ -34,7 +61,7 @@ export const EN = {
|
|
|
34
61
|
"jobs.unknown": "Submission outcome is unknown; it will not be resent. Inspect current facts and the original operation before taking another action.",
|
|
35
62
|
"jobs.empty": "No Jobs in this scope. Creating a Job does not run it.",
|
|
36
63
|
"jobs.configuration": "Execution configuration",
|
|
37
|
-
"jobs.taskConfig": "
|
|
64
|
+
"jobs.taskConfig": "Use the documented shell, Task delegation or Agent configuration. Shell requires an exact managed workspace/HEAD; new execution Tasks still require independent activation. Agent input grants no extra authority. No secrets. Executor kind is immutable.",
|
|
38
65
|
"jobs.rebind": "Saving a Project refresh Job explicitly binds its current catalog target. It does not refresh the Project. No arbitrary global shell commands.",
|
|
39
66
|
"jobs.rule": "Time rule",
|
|
40
67
|
"jobs.instant": "Instant / interval anchor (ISO time with UTC offset)",
|
|
@@ -3,8 +3,35 @@
|
|
|
3
3
|
export const ZH_CN = {
|
|
4
4
|
"tabs.jobs": "Job",
|
|
5
5
|
"settings.section.jobs": "Job 与时间计划",
|
|
6
|
+
"jobs.builtinSync": "内置 Project 同步 · 仅安全快进",
|
|
7
|
+
"jobs.automatic": "自动同步偏好",
|
|
8
|
+
"jobs.syncEvery": "同步间隔(毫秒,最少 60000)",
|
|
9
|
+
"jobs.enabled": "开启",
|
|
10
|
+
"jobs.disabled": "关闭",
|
|
11
|
+
"jobs.result": "业务结果",
|
|
6
12
|
"jobs.heading": "Job 与时间计划",
|
|
7
|
-
"jobs.help": "
|
|
13
|
+
"jobs.help": "管理归属与执行目标独立。立即运行只接纳一次执行;关闭计划阻止后续触发。取消不证明业务或进程已经停止。全局管理不要求 Operator Session 在线,Task 管理保留现有权限校验。",
|
|
14
|
+
"jobs.executor": "执行方式",
|
|
15
|
+
"jobs.executionTask": "打开执行 Task",
|
|
16
|
+
"jobs.sourceJob": "创建来源 Job / Run",
|
|
17
|
+
"jobs.taskCategory": "任务分类",
|
|
18
|
+
"jobs.category.ordinary": "普通任务",
|
|
19
|
+
"jobs.category.job": "Job 执行任务",
|
|
20
|
+
"jobs.category.all": "全部任务",
|
|
21
|
+
"jobs.recentSuccess": "最近成功的 Run",
|
|
22
|
+
"jobs.diagnostics": "执行诊断",
|
|
23
|
+
"jobs.artifacts": "已登记固定产物",
|
|
24
|
+
"jobs.registeredOnly": "仅可读取已登记的准确归属、Run、commit、路径及摘要。引用图片也须独立登记。",
|
|
25
|
+
"jobs.inspectSaved": "检查已保存/未登记材料",
|
|
26
|
+
"jobs.register": "补登记准确产物",
|
|
27
|
+
"jobs.registerHelp": "补登记只发布已保存证据,不重跑业务;已保存但未登记的文件尚非正式结果。",
|
|
28
|
+
"jobs.cancelBoundary": "仅请求取消此准确 Run,不停止其执行 Task 或中断共享 Agent Session。请检查结果确认实际状态。",
|
|
29
|
+
"jobs.inspectSession": "检查 Job 自有 Session",
|
|
30
|
+
"jobs.stopSession": "请求停止选中 Session",
|
|
31
|
+
"jobs.newSession": "替换已停止 Session",
|
|
32
|
+
"jobs.sessionBoundary": "仅作用于此 Job 自有 Session。停止是清理请求;替换要求旧实例确已终止且输入已结算。两者都不会重放业务或删除证据。",
|
|
33
|
+
"jobs.stale": "刷新失败,保留内容来自上次观察,不代表实时成功。",
|
|
34
|
+
"jobs.unavailable": "选中的 Job 在此管理范围内不可用。",
|
|
8
35
|
"jobs.details": "详情",
|
|
9
36
|
"jobs.scope": "所属范围",
|
|
10
37
|
"jobs.project": "目标 Project",
|
|
@@ -32,7 +59,7 @@ export const ZH_CN = {
|
|
|
32
59
|
"jobs.unknown": "提交结果未知,不会重发。请先检查当前事实和原操作,再决定后续动作。",
|
|
33
60
|
"jobs.empty": "此范围暂无 Job。创建定义不会立即执行。",
|
|
34
61
|
"jobs.configuration": "执行配置",
|
|
35
|
-
"jobs.taskConfig": "
|
|
62
|
+
"jobs.taskConfig": "使用已文档化的 shell、Task 委托或 Agent 配置。shell 要求准确受管工作区及 HEAD;新执行 Task 仍需独立激活。Agent 输入不扩展权限。禁止包含凭据;执行器类型不可改写。",
|
|
36
63
|
"jobs.rebind": "保存 Project 刷新 Job 会显式绑定当前目录中的目标,但不会执行刷新。全局不开放任意 shell 命令。",
|
|
37
64
|
"jobs.rule": "时间规则",
|
|
38
65
|
"jobs.instant": "执行时间/周期锚点(带 UTC 偏移的 ISO 时间)",
|
|
@@ -21,9 +21,16 @@ export function readManageRoute(url) {
|
|
|
21
21
|
const section = (SECTIONS[area].find(item => item.section === requested) ||
|
|
22
22
|
(area === "models" && requested === "connection" ? SECTIONS.settings.at(-1) : SECTIONS[area][0])).section;
|
|
23
23
|
return { page: section === "connection" ? "models" : area, section,
|
|
24
|
+
...(section === "jobs" ? { job: url.searchParams.get("job"), jobRun: url.searchParams.get("jobRun"),
|
|
25
|
+
artifact: url.searchParams.get("artifact") } : {}),
|
|
24
26
|
source: url.searchParams.get("source") === "command-code" ? "command-code" : "factory" };
|
|
25
27
|
}
|
|
26
28
|
export function manageHref(next) {
|
|
29
|
+
if (next.section === "jobs") {
|
|
30
|
+
const query = new URLSearchParams();
|
|
31
|
+
for (const key of ["job", "jobRun", "artifact"]) if (next[key]) query.set(key, next[key]);
|
|
32
|
+
return "/settings/jobs" + (query.size ? "?" + query : "");
|
|
33
|
+
}
|
|
27
34
|
return "/" + manageArea(next.page, next.section) + "/" + next.section +
|
|
28
35
|
(next.section === "quota" && next.source === "command-code" ? "?source=command-code" : "");
|
|
29
36
|
}
|
|
@@ -81,6 +88,8 @@ export function startManagement() {
|
|
|
81
88
|
if (!link || link.target || link.hasAttribute("download") || event.defaultPrevented ||
|
|
82
89
|
event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
83
90
|
const url = new URL(link.href);
|
|
91
|
+
// Exact Job/Run links initialize a new pinned view, not just a section switch.
|
|
92
|
+
if (url.searchParams.has("job")) return;
|
|
84
93
|
if (url.origin !== location.origin || !/^\/(models|settings)(\/|$)/.test(url.pathname)) return;
|
|
85
94
|
event.preventDefault();
|
|
86
95
|
navigateManage(readManageRoute(url));
|
|
@@ -23,6 +23,7 @@ export function syncUrl(state, replace) {
|
|
|
23
23
|
if (state.query) params.set("q", state.query); else params.delete("q");
|
|
24
24
|
if (state.attentionFilter) params.set("attention", state.attentionFilter); else params.delete("attention");
|
|
25
25
|
if (state.catalogAll) params.set("all", "true"); else params.delete("all");
|
|
26
|
+
if (state.source && state.source !== "ordinary") params.set("source", state.source); else params.delete("source");
|
|
26
27
|
applyUrl(params, replace);
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const JOBS_SCRIPT = String.raw `
|
|
2
2
|
import { h } from "/assets/js/lib/dom.js";
|
|
3
3
|
import { requestJson, submitMutation } from "/assets/js/lib/api.js";
|
|
4
|
+
import { drawArtifact } from "/assets/js/views/task/files.js";
|
|
4
5
|
|
|
5
6
|
// One view for two server-fixed scopes. Refresh only reads facts; it never
|
|
6
7
|
// admits due work, derives nextDue or replaces a selected exact Run with latest.
|
|
@@ -11,7 +12,17 @@ export function mountJobs(root, t, taskId) {
|
|
|
11
12
|
const list = h("div.jobs-list");
|
|
12
13
|
const detail = h("section.jobs-detail", { "aria-label": copy("details") });
|
|
13
14
|
const editor = h("section");
|
|
14
|
-
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
let jobs = [], schedules = [], selected = params.get("job") || undefined,
|
|
17
|
+
selectedRun = params.get("jobRun") || undefined, generation = 0, runGeneration = 0, pending = false, dirty = false;
|
|
18
|
+
const artifactState = {};
|
|
19
|
+
try { artifactState.reference = JSON.parse(params.get("artifact") || "null"); } catch {}
|
|
20
|
+
function pinSelection() {
|
|
21
|
+
const url = new URL(location.href);
|
|
22
|
+
if (selected) url.searchParams.set("job", selected); else url.searchParams.delete("job");
|
|
23
|
+
if (selectedRun) url.searchParams.set("jobRun", selectedRun); else url.searchParams.delete("jobRun");
|
|
24
|
+
history.replaceState(history.state, "", url);
|
|
25
|
+
}
|
|
15
26
|
const blocked = new Set();
|
|
16
27
|
const read = (family, input) => requestJson(base + "/" + family + "?" + new URLSearchParams(input));
|
|
17
28
|
const button = (text, action, primary = false) => h("button.btn" + (primary ? ".btn-primary" : ""), {
|
|
@@ -40,6 +51,11 @@ export function mountJobs(root, t, taskId) {
|
|
|
40
51
|
status(copy("submitting"));
|
|
41
52
|
try {
|
|
42
53
|
const result = await submitMutation(key, base + "/" + family, payload);
|
|
54
|
+
if (result.status === "saved-unregistered") {
|
|
55
|
+
status(copy("registerHelp") + "\n" + result.error, true);
|
|
56
|
+
await refresh(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
43
59
|
status(payload.op === "run"
|
|
44
60
|
? (result.reason === "overlap" ? copy("overlap") : copy("admitted")) + " · " + result.run.id + " · " + result.run.status
|
|
45
61
|
: payload.op === "run-cancel" ? copy("cancelRequested") : copy("saved"));
|
|
@@ -73,39 +89,53 @@ export function mountJobs(root, t, taskId) {
|
|
|
73
89
|
function editJob(job) {
|
|
74
90
|
const id = input(job?.id || "", { required: true, pattern: "[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", disabled: !!job });
|
|
75
91
|
const title = input(job?.title || "", { required: true });
|
|
92
|
+
const kind = choose(taskId ? ["shell", "task", "agent"] : ["builtin", "task", "agent"], job?.configuration.kind);
|
|
93
|
+
kind.disabled = !!job;
|
|
76
94
|
const action = choose(["system.noop", "project.refresh"], job?.configuration.action);
|
|
77
95
|
const project = input(job?.configuration.projectId || "");
|
|
78
96
|
const configuration = h("textarea", { rows: 12, required: true });
|
|
79
97
|
configuration.value = JSON.stringify(job?.configuration || { kind: "shell", owner: { kind: "task" },
|
|
80
98
|
projectId: "", head: "", workspace: "", env: {}, steps: [{ name: "check", command: "true" }] }, null, 2);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
action.
|
|
87
|
-
|
|
99
|
+
const controlled = h("div", null, field(copy("action"), action), field(copy("project"), project), h("p", null, copy("rebind")));
|
|
100
|
+
const configured = h("div", null, h("p", null, copy("taskConfig")), field(copy("configuration"), configuration));
|
|
101
|
+
const fields = [field("ID", id), field(copy("name"), title), field(copy("executor"), kind), controlled, configured];
|
|
102
|
+
const sync = () => {
|
|
103
|
+
controlled.hidden = kind.value !== "builtin"; configured.hidden = !controlled.hidden;
|
|
104
|
+
project.disabled = controlled.hidden || action.value !== "project.refresh"; project.required = !project.disabled;
|
|
105
|
+
configuration.required = !configured.hidden;
|
|
106
|
+
};
|
|
107
|
+
kind.addEventListener("change", () => {
|
|
108
|
+
configuration.value = JSON.stringify(kind.value === "agent"
|
|
109
|
+
? { kind: "agent", target: { kind: "job", agentId: "" }, input: "", concurrency: "serial" }
|
|
110
|
+
: kind.value === "task" ? { kind: "task", goal: "delivery", target: {
|
|
111
|
+
kind: "new", title: "", description: "", projectBindings: [] } }
|
|
112
|
+
: { kind: "shell", owner: { kind: "task" }, projectId: "", head: "", workspace: "", env: {},
|
|
113
|
+
steps: [{ name: "check", command: "true" }] }, null, 2);
|
|
114
|
+
sync();
|
|
115
|
+
});
|
|
116
|
+
action.addEventListener("change", sync); sync();
|
|
88
117
|
form(job ? copy("editJob") : copy("newJob"), fields, () => {
|
|
89
|
-
const config =
|
|
118
|
+
const config = kind.value !== "builtin" ? JSON.parse(configuration.value) : {
|
|
90
119
|
kind: "builtin", action: action.value, input: {},
|
|
91
120
|
...(action.value === "project.refresh" ? { projectId: project.value.trim() } : {})
|
|
92
121
|
};
|
|
93
122
|
void mutate("jobs", { op: job ? "update" : "define", id: id.value.trim(), title: title.value.trim(),
|
|
94
123
|
configuration: config, ...(job ? { expectedRevision: job.revision } : {}) }, result => {
|
|
95
|
-
|
|
124
|
+
selected = result.id; selectedRun = undefined; pinSelection(); closeEditor();
|
|
96
125
|
});
|
|
97
126
|
});
|
|
98
127
|
}
|
|
99
128
|
function editSchedule(schedule) {
|
|
100
129
|
const id = input(schedule?.id || "", { required: true, disabled: !!schedule });
|
|
101
|
-
const jobId = choose(jobs.map(j => j.id), schedule?.jobId || selected);
|
|
102
|
-
|
|
130
|
+
const jobId = choose(jobs.filter(j => !j.builtin || j.id === schedule?.jobId).map(j => j.id), schedule?.jobId || selected);
|
|
131
|
+
jobId.disabled = !!schedule?.builtin;
|
|
132
|
+
const kind = choose(schedule?.builtin ? ["interval", "cron"] : ["once", "interval", "cron"], schedule?.rule.kind || "interval");
|
|
103
133
|
const rule = schedule?.rule;
|
|
104
134
|
const at = input(rule?.at || rule?.anchor || "", { placeholder: "2026-10-17T09:00:00+08:00" });
|
|
105
|
-
const every = input(String(rule?.everyMs || 3600000), { type: "number", min: 1000, step: 1 });
|
|
135
|
+
const every = input(String(rule?.everyMs || 3600000), { type: "number", min: schedule?.builtin ? 60000 : 1000, step: 1 });
|
|
106
136
|
const expression = input(rule?.expression || "0 9 * * *");
|
|
107
137
|
const timezone = input(rule?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
|
|
108
|
-
const when = field(copy("instant"), at), interval = field(copy("every"), every);
|
|
138
|
+
const when = field(copy("instant"), at), interval = field(copy(schedule?.builtin ? "syncEvery" : "every"), every);
|
|
109
139
|
const cron = field(copy("cron"), expression), zone = field(copy("timezone"), timezone);
|
|
110
140
|
const sync = () => {
|
|
111
141
|
when.hidden = kind.value === "cron"; interval.hidden = kind.value !== "interval";
|
|
@@ -118,31 +148,119 @@ export function mountJobs(root, t, taskId) {
|
|
|
118
148
|
const rule = kind.value === "once" ? { kind: "once", at: at.value }
|
|
119
149
|
: kind.value === "interval" ? { kind: "interval", anchor: at.value, everyMs: Number(every.value) }
|
|
120
150
|
: { kind: "cron", expression: expression.value, timezone: timezone.value };
|
|
121
|
-
void mutate("schedules", { op: schedule ? "update" : "create", id: id.value.trim(),
|
|
151
|
+
void mutate("schedules", { op: schedule ? "update" : "create", id: id.value.trim(), rule,
|
|
152
|
+
...(schedule?.builtin ? {} : { jobId: jobId.value, ...(!taskId ? { authorization: "service" } : {}) }),
|
|
122
153
|
...(schedule ? { expectedRevision: schedule.revision } : {}) }, closeEditor);
|
|
123
154
|
});
|
|
124
155
|
}
|
|
125
156
|
async function showRun(jobId, runId, target) {
|
|
157
|
+
const epoch = ++runGeneration;
|
|
126
158
|
selectedRun = runId;
|
|
159
|
+
pinSelection();
|
|
127
160
|
target.replaceChildren(h("p", null, copy("loading")));
|
|
128
161
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
target.replaceChildren(h("h4", null, run.id), fact(copy("state"), run.status),
|
|
162
|
+
const run = await read("jobs", { op: "run-get", id: jobId, runId });
|
|
163
|
+
if (epoch !== runGeneration || selected !== jobId || selectedRun !== runId) return;
|
|
164
|
+
const evidence = h("section"), artifacts = h("section"), session = h("section");
|
|
165
|
+
target.replaceChildren(h("div.settings-stack", null, h("h4", null, run.id), fact(copy("state"), run.status),
|
|
166
|
+
fact(copy("executor"), run.executor || (run.delegation ? "task" : "process")),
|
|
167
|
+
run.delegation ? h("a.btn", { href: "/?task=" + encodeURIComponent(run.delegation.taskId) },
|
|
168
|
+
copy("executionTask") + " · " + run.delegation.taskId) : null,
|
|
169
|
+
...(run.businessResult ? [h("div", null, fact(copy("result"), run.businessResult.summary),
|
|
170
|
+
raw(copy("result"), run.businessResult))] : []),
|
|
171
|
+
run.cancelRequestedAt ? fact(copy("cancelRequested"), run.cancelRequestedAt) : null,
|
|
133
172
|
fact(copy("trigger"), run.trigger.source + " · " + run.trigger.key),
|
|
134
173
|
fact(copy("created"), run.createdAt), fact(copy("finished"), run.terminalAt),
|
|
135
|
-
raw(copy("snapshot"), run),
|
|
136
|
-
h("h4", null, copy("logs")), logs.length ? h("div", null, logs.map(log => h("section", null,
|
|
137
|
-
h("h5", null, log.step + (log.truncated ? " · " + copy("truncated") : "")),
|
|
138
|
-
h("pre.settings-observation", null, log.text)))) : h("p", null, copy("noLogs")));
|
|
174
|
+
raw(copy("snapshot"), run), artifacts, evidence, session));
|
|
139
175
|
if (["queued", "running", "unknown-needs-attention"].includes(run.status)) target.prepend(
|
|
140
|
-
button(copy("cancelRun"), () =>
|
|
141
|
-
|
|
176
|
+
button(copy("cancelRun"), () => {
|
|
177
|
+
if (window.confirm(copy("cancelBoundary") + "\n" + runId))
|
|
178
|
+
void mutate("jobs", { op: "run-cancel", id: jobId, runId });
|
|
179
|
+
}));
|
|
180
|
+
drawArtifacts(artifacts, jobId, run, epoch);
|
|
181
|
+
if (run.executor === "agent" && run.configuration.target.kind === "job") drawSession(session, jobId, runId);
|
|
182
|
+
try {
|
|
183
|
+
const value = await read("jobs", { op: "run-evidence", id: jobId, runId });
|
|
184
|
+
if (epoch !== runGeneration) return;
|
|
185
|
+
evidence.append(raw(copy("diagnostics"), value));
|
|
186
|
+
if (run.executor !== "agent" && !run.delegation) {
|
|
187
|
+
const logs = await read("jobs", { op: "logs", id: jobId, runId });
|
|
188
|
+
if (epoch !== runGeneration) return;
|
|
189
|
+
evidence.append(h("h4", null, copy("logs")), logs.length ? h("div", null, logs.map(log => h("section", null,
|
|
190
|
+
h("h5", null, log.step + (log.truncated ? " · " + copy("truncated") : "")),
|
|
191
|
+
h("pre.settings-observation", null, log.text)))) : h("p", null, copy("noLogs")));
|
|
192
|
+
}
|
|
193
|
+
} catch (error) { if (epoch === runGeneration) evidence.append(h("p.settings-error", null, copy("diagnostics") + ": " + error.message)); }
|
|
194
|
+
} catch (error) { if (epoch === runGeneration) target.replaceChildren(h("p.settings-error", null, error.message)); }
|
|
195
|
+
}
|
|
196
|
+
function drawArtifacts(target, jobId, run, epoch) {
|
|
197
|
+
const refs = run.artifacts || [];
|
|
198
|
+
const viewer = h("div.file-viewer");
|
|
199
|
+
const readRef = async (ref, offset = 0) => {
|
|
200
|
+
const value = await read("jobs", { op: "artifact-preview", id: jobId, runId: run.id,
|
|
201
|
+
reference: JSON.stringify(ref), offset: String(offset) });
|
|
202
|
+
return { ...value, relativePath: ref.relativePath, commit: ref.version };
|
|
203
|
+
};
|
|
204
|
+
const select = async (ref, offset = 0) => {
|
|
205
|
+
artifactState.reference = ref;
|
|
206
|
+
const url = new URL(location.href);
|
|
207
|
+
url.searchParams.set("artifact", JSON.stringify(ref));
|
|
208
|
+
history.replaceState(history.state, "", url);
|
|
209
|
+
const key = JSON.stringify(ref);
|
|
210
|
+
viewer.replaceChildren(h("p", null, copy("loading")));
|
|
211
|
+
try {
|
|
212
|
+
const value = await readRef(ref, offset);
|
|
213
|
+
if (epoch !== runGeneration || JSON.stringify(artifactState.reference) !== key) return;
|
|
214
|
+
drawArtifact(viewer, value, JSON.stringify(ref.owner), t, {
|
|
215
|
+
compare: false,
|
|
216
|
+
readArtifact: async (_owner, path, commit, page) => {
|
|
217
|
+
const exact = refs.find(r => r.relativePath === path && r.version === commit
|
|
218
|
+
&& JSON.stringify(r.owner) === JSON.stringify(ref.owner));
|
|
219
|
+
if (!exact) throw new Error(copy("registeredOnly"));
|
|
220
|
+
return readRef(exact, page);
|
|
221
|
+
}
|
|
222
|
+
}, (_path, _commit, page) => void select(ref, page), artifactState);
|
|
223
|
+
} catch (error) { if (epoch === runGeneration && JSON.stringify(artifactState.reference) === key)
|
|
224
|
+
viewer.replaceChildren(h("p.settings-error", null, error.message)); }
|
|
225
|
+
};
|
|
226
|
+
target.append(h("h4", null, copy("artifacts")), h("p", null, copy("registeredOnly")),
|
|
227
|
+
...refs.map(ref => button(ref.relativePath + " · " + ref.version.slice(0, 12), () => void select(ref))), viewer);
|
|
228
|
+
if (artifactState.reference && refs.some(ref => JSON.stringify(ref) === JSON.stringify(artifactState.reference)))
|
|
229
|
+
void select(artifactState.reference);
|
|
230
|
+
const recovery = h("div");
|
|
231
|
+
target.append(button(copy("inspectSaved"), async () => {
|
|
232
|
+
recovery.replaceChildren();
|
|
233
|
+
try {
|
|
234
|
+
const result = await read("jobs", { op: "artifact-list", id: jobId, runId: run.id });
|
|
235
|
+
const missing = result.repository.saved.filter(ref => !result.artifacts.some(r => JSON.stringify(r) === JSON.stringify(ref)));
|
|
236
|
+
recovery.append(raw(copy("artifacts"), result), h("p", null, copy("registerHelp")),
|
|
237
|
+
...missing.map(ref => button(copy("register") + " · " + ref.relativePath, () => {
|
|
238
|
+
if (window.confirm(copy("registerHelp") + "\n" + ref.version))
|
|
239
|
+
void mutate("jobs", { op: "artifact-register", id: jobId, runId: run.id, reference: ref });
|
|
240
|
+
})));
|
|
241
|
+
} catch (error) { recovery.append(h("p.settings-error", null, error.message)); }
|
|
242
|
+
}), recovery);
|
|
243
|
+
}
|
|
244
|
+
function drawSession(target, jobId, runId) {
|
|
245
|
+
target.append(button(copy("inspectSession"), async () => {
|
|
246
|
+
try {
|
|
247
|
+
const value = await read("jobs", { op: "session-inspect", id: jobId, runId });
|
|
248
|
+
const set = value.sessions, current = set?.sessions?.[set.activeAgentId];
|
|
249
|
+
target.replaceChildren(raw(copy("inspectSession"), value), h("p", null, copy("sessionBoundary")));
|
|
250
|
+
if (current) target.append(...["session-stop", "session-new"].map(op =>
|
|
251
|
+
button(copy(op === "session-stop" ? "stopSession" : "newSession"), () => {
|
|
252
|
+
if (window.confirm(copy("sessionBoundary") + "\n" + current.nativeSessionId))
|
|
253
|
+
void mutate("jobs", { op, id: jobId, runId, expectedSessionId: current.nativeSessionId });
|
|
254
|
+
})));
|
|
255
|
+
} catch (error) { target.append(h("p.settings-error", null, error.message)); }
|
|
256
|
+
}));
|
|
142
257
|
}
|
|
143
258
|
async function showDetail(epoch) {
|
|
144
259
|
const job = jobs.find(j => j.id === selected);
|
|
145
|
-
if (!job) {
|
|
260
|
+
if (!job) {
|
|
261
|
+
detail.replaceChildren(...(selected ? [h("p.settings-error", null, copy("unavailable") + " · " + selected)] : []));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
146
264
|
const [runs, plans] = await Promise.all([
|
|
147
265
|
read("jobs", { op: "runs", id: job.id }),
|
|
148
266
|
Promise.all(schedules.filter(s => s.jobId === job.id).map(s => read("schedules", { op: "show", id: s.id })))
|
|
@@ -150,38 +268,43 @@ export function mountJobs(root, t, taskId) {
|
|
|
150
268
|
if (epoch !== generation) return;
|
|
151
269
|
runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
152
270
|
const summary = run => run ? (run.id || run.runId) + " · " + run.status + " · "
|
|
153
|
-
+ (run.terminalAt || run.finishedAt || run.createdAt || run.attemptedAt)
|
|
271
|
+
+ (run.terminalAt || run.finishedAt || run.createdAt || run.attemptedAt)
|
|
272
|
+
+ (run.summary || run.businessResult?.summary ? " · " + (run.summary || run.businessResult.summary) : "") : null;
|
|
154
273
|
const runDetail = h("section.job-run");
|
|
155
274
|
detail.replaceChildren(h("div.settings-stack", null, h("h3", null, job.title), fact("ID", job.id),
|
|
156
275
|
fact(copy("scope"), taskId || "global"), fact(copy("project"), job.configuration.projectId),
|
|
276
|
+
job.builtin ? h("p", null, copy("builtinSync")) : null,
|
|
157
277
|
fact(copy("revision"), job.revision),
|
|
278
|
+
fact(copy("executor"), job.configuration.kind),
|
|
158
279
|
fact(copy("recentRun"), summary(runs[0])),
|
|
280
|
+
fact(copy("recentSuccess"), summary(runs.find(run => run.businessResult?.outcome === "succeeded"))),
|
|
159
281
|
job.refresh ? h("div", null,
|
|
160
282
|
fact(copy("refreshAttempt"), summary(job.refresh.lastAttempt)),
|
|
161
283
|
fact(copy("refreshSuccess"), summary(job.refresh.lastSuccessfulRefresh))) : null,
|
|
162
284
|
h("div.settings-actions", null,
|
|
163
285
|
button(copy("runNow"), () => void mutate("jobs", { op: "run", id: job.id, requestId: crypto.randomUUID() }, result => {
|
|
164
|
-
selectedRun = result.run.id;
|
|
286
|
+
selectedRun = result.run.id; pinSelection();
|
|
165
287
|
}), true),
|
|
166
|
-
button(copy("editJob"), () => editJob(job)),
|
|
167
|
-
button(copy("newSchedule"), () => editSchedule())),
|
|
288
|
+
!job.builtin ? button(copy("editJob"), () => editJob(job)) : null,
|
|
289
|
+
!job.builtin && !["agent", "task"].includes(job.configuration.kind) ? button(copy("newSchedule"), () => editSchedule()) : null),
|
|
168
290
|
raw(copy("configuration"), job.configuration),
|
|
169
291
|
job.refresh ? raw(copy("refreshFacts"), job.refresh) : null,
|
|
170
292
|
h("h3", null, copy("schedules")),
|
|
171
293
|
plans.length ? h("div.settings-stack", null, plans.map(({ schedule: s, lastSuccess }) => h("article.settings-card", null,
|
|
172
294
|
h("h4", null, s.id), fact(copy("state"), s.state),
|
|
295
|
+
s.builtin ? fact(copy("automatic"), copy(s.builtin.enabled ? "enabled" : "disabled")) : null,
|
|
173
296
|
s.state === "completed" ? h("p", null, copy("onceCompleted")) : null,
|
|
174
297
|
fact(copy("rule"), s.rule.kind === "cron" ? s.rule.expression : s.rule.kind === "once" ? s.rule.at : s.rule.anchor + " + " + s.rule.everyMs + " ms"),
|
|
175
298
|
fact(copy("timezone"), s.rule.timezone || "UTC / offset"),
|
|
176
|
-
fact(copy("nextDue"), s.nextDue), fact(copy("lastAttempt"), s.lastAttempt
|
|
299
|
+
fact(copy("nextDue"), s.state === "enabled" ? s.nextDue : null), fact(copy("lastAttempt"), s.lastAttempt
|
|
177
300
|
? s.lastAttempt.at + " · " + s.lastAttempt.outcome + " · " + (s.lastAttempt.runId || s.lastAttempt.reason || "") : null),
|
|
178
301
|
fact(copy("lastSuccess"), lastSuccess ? lastSuccess.id + " · " + (lastSuccess.terminalAt || lastSuccess.updatedAt) : null),
|
|
179
302
|
s.reason ? h("p.settings-error", null, s.reason) : null,
|
|
180
303
|
h("div.settings-actions", null,
|
|
181
304
|
button(copy("editSchedule"), () => editSchedule(s)),
|
|
182
|
-
button(copy(s.state === "enabled" ? "disable" : "enable"), () => void mutate("schedules", {
|
|
183
|
-
op: s.state === "enabled" ? "disable" : "enable", id: s.id, expectedRevision: s.revision
|
|
184
|
-
})), s.lastAttempt?.runId ?
|
|
305
|
+
button(copy((s.builtin ? s.builtin.enabled : s.state === "enabled") ? "disable" : "enable"), () => void mutate("schedules", {
|
|
306
|
+
op: (s.builtin ? s.builtin.enabled : s.state === "enabled") ? "disable" : "enable", id: s.id, expectedRevision: s.revision
|
|
307
|
+
})), s.lastAttempt?.runId ? h("a.btn", { href: jobLink(taskId, s.lastAttempt.jobId || job.id, s.lastAttempt.runId) }, copy("inspectRun")) : null),
|
|
185
308
|
raw(copy("details"), s)))) : h("p", null, copy("noSchedules")),
|
|
186
309
|
h("h3", null, copy("history")),
|
|
187
310
|
runs.length ? h("div.jobs-runs", null, runs.map(run =>
|
|
@@ -201,14 +324,14 @@ export function mountJobs(root, t, taskId) {
|
|
|
201
324
|
list.replaceChildren(...(jobs.length ? jobs.map(job => h("button.job-choice", {
|
|
202
325
|
type: "button", "aria-pressed": String(selected === job.id), onclick: () => {
|
|
203
326
|
if (pending || !guardDraft()) return;
|
|
204
|
-
closeEditor(); selected = job.id; selectedRun = undefined; void refresh();
|
|
327
|
+
closeEditor(); selected = job.id; selectedRun = undefined; pinSelection(); void refresh();
|
|
205
328
|
}
|
|
206
329
|
}, h("strong", null, job.title), h("code", null, job.id),
|
|
207
|
-
h("small", null, job.configuration.action || job.configuration.kind),
|
|
330
|
+
h("small", null, job.builtin ? copy("builtinSync") : job.configuration.action || job.configuration.kind),
|
|
208
331
|
h("small", null, job.configuration.projectId || "—"))) : [h("p", null, copy("empty"))]));
|
|
209
332
|
await showDetail(epoch);
|
|
210
333
|
if (announce && epoch === generation) status(copy("observed") + " · " + new Date().toISOString());
|
|
211
|
-
} catch (error) { if (epoch === generation) status(error.message, true); }
|
|
334
|
+
} catch (error) { if (epoch === generation) status(copy("stale") + " " + error.message, true); }
|
|
212
335
|
}
|
|
213
336
|
root.classList.add("jobs-manager");
|
|
214
337
|
root.replaceChildren(h("h2", null, copy("heading")),
|
|
@@ -219,4 +342,10 @@ export function mountJobs(root, t, taskId) {
|
|
|
219
342
|
void refresh();
|
|
220
343
|
return { isDirty: () => dirty, isBusy: () => pending, refresh };
|
|
221
344
|
}
|
|
345
|
+
|
|
346
|
+
export function jobLink(taskId, jobId, runId) {
|
|
347
|
+
const query = new URLSearchParams({ job: jobId, ...(runId ? { jobRun: runId } : {}),
|
|
348
|
+
...(taskId ? { task: taskId, section: "jobs" } : {}) });
|
|
349
|
+
return (taskId ? "/" : "/settings/jobs") + "?" + query;
|
|
350
|
+
}
|
|
222
351
|
`;
|
|
@@ -74,7 +74,7 @@ function artifactViewer(viewer, data, t, actions) {
|
|
|
74
74
|
return { element: viewer, read: read, draw: draw };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function drawArtifact(viewer, value, taskId, t, actions, read, state = {}) {
|
|
77
|
+
export function drawArtifact(viewer, value, taskId, t, actions, read, state = {}) {
|
|
78
78
|
clear(viewer);
|
|
79
79
|
if (!value) return;
|
|
80
80
|
const selected = value.after || value;
|
|
@@ -121,12 +121,13 @@ function drawArtifact(viewer, value, taskId, t, actions, read, state = {}) {
|
|
|
121
121
|
viewer.append(h("p.viewer-ref", null, mono("sha256:" + selected.digest)));
|
|
122
122
|
if (value.before) viewer.append(h("p.viewer-ref", null, mono("← git:" + value.before.commit + ":" + value.before.relativePath + " · sha256:" + value.before.digest)));
|
|
123
123
|
viewer.append(h("p.faint.small", null, t("evidence.fixedNote")));
|
|
124
|
-
viewer.append(h("div.record-actions", null, value.kind === "image" ? null : copyText, download,
|
|
124
|
+
viewer.append(h("div.record-actions", null, value.kind === "image" ? null : copyText, download,
|
|
125
|
+
actions.openConversation ? feedback : null));
|
|
125
126
|
if (value.kind === "image") { viewer.append(imagePreview(value, t, undefined, preview)); return; }
|
|
126
127
|
const before = h("input", { placeholder: t("materials.before"), "aria-label": t("materials.before"), maxLength: 64 });
|
|
127
128
|
const compare = button(t("materials.compare"), { variant: "ghost" });
|
|
128
129
|
compare.addEventListener("click", function () { read(selected.relativePath, selected.commit, 0, before.value); });
|
|
129
|
-
viewer.append(h("div.record-actions", null, before, compare));
|
|
130
|
+
if (actions.compare !== false) viewer.append(h("div.record-actions", null, before, compare));
|
|
130
131
|
const source = h("pre.code-block.artifact-text", { tabIndex: 0, "aria-label": t("evidence.fixedText") }, value.content);
|
|
131
132
|
if (!value.before && /\.md$|\.markdown$/i.test(selected.relativePath)) {
|
|
132
133
|
viewer.append(markdownPreview(value, taskId, t, actions, preview));
|
|
@@ -207,7 +208,7 @@ function markdownPreview(value, taskId, t, actions, preview) {
|
|
|
207
208
|
feedback.addEventListener("click", function () { actions.openConversation([artifactRef(value), artifactRef(image)]); });
|
|
208
209
|
slot.append(imagePreview(image, t, entry.alt, saved), h("span.viewer-ref", null,
|
|
209
210
|
path + " · git:" + image.commit + " · sha256:" + image.digest),
|
|
210
|
-
h("span.record-actions", null, download, feedback));
|
|
211
|
+
h("span.record-actions", null, download, actions.openConversation ? feedback : null));
|
|
211
212
|
}
|
|
212
213
|
async function observeRead() {
|
|
213
214
|
load.disabled = true; body.dataset.reading = "true"; status.textContent = t("evidence.reading");
|
|
@@ -12,7 +12,7 @@ import { updateObservation } from "/assets/js/views/task/observation.js";
|
|
|
12
12
|
import { renderDelivery } from "/assets/js/views/task/delivery.js";
|
|
13
13
|
import { renderRuntime } from "/assets/js/views/task/runtime.js";
|
|
14
14
|
import { renderRecords } from "/assets/js/views/task/records.js";
|
|
15
|
-
import { mountJobs } from "/assets/js/views/jobs.js";
|
|
15
|
+
import { mountJobs, jobLink } from "/assets/js/views/jobs.js";
|
|
16
16
|
|
|
17
17
|
// Overview: where it stands. Delivery: what it produced. Runtime: what is
|
|
18
18
|
// live now. Records: what happened. Retired section names stay addressable.
|
|
@@ -94,6 +94,10 @@ function dockToggle(mode, iconName, title, t, ctx) {
|
|
|
94
94
|
|
|
95
95
|
function taskMeta(task, t, locale) {
|
|
96
96
|
return h("div.task-meta", null,
|
|
97
|
+
task.source?.kind === "job-run" ? h("a.btn", {
|
|
98
|
+
href: jobLink(task.source.run.scope.kind === "task" ? task.source.run.scope.taskId : undefined,
|
|
99
|
+
task.source.jobId, task.source.run.runId)
|
|
100
|
+
}, t("jobs.sourceJob") + " · " + task.source.jobId + " / " + task.source.run.runId) : null,
|
|
97
101
|
statusBadge(t, "task", "status", task.status),
|
|
98
102
|
h("span", { dataset: { slot: "exec-badge" } }),
|
|
99
103
|
task.priority ? badge(label(t, "priority", task.priority), task.priority === "urgent" || task.priority === "high" ? "accent" : "idle") : null,
|
package/dist/web/taskPreviews.js
CHANGED
|
@@ -25,7 +25,7 @@ export function createTaskPreviews(store, home) {
|
|
|
25
25
|
};
|
|
26
26
|
const registered = (taskId, jobId) => {
|
|
27
27
|
const job = store.getDurableJob(taskId, jobId);
|
|
28
|
-
if (!store.getTask(taskId) || !job || job.taskId !== taskId || !isHttpPreview(job.env)) {
|
|
28
|
+
if (!store.getTask(taskId) || !job || job.delegation || job.taskId !== taskId || !isHttpPreview(job.env)) {
|
|
29
29
|
throw new WebRequestRejected("Registered Task preview not found.");
|
|
30
30
|
}
|
|
31
31
|
validateHttpPreview(job.env, job.steps);
|
|
@@ -69,7 +69,7 @@ export function createTaskPreviews(store, home) {
|
|
|
69
69
|
async list(taskId) {
|
|
70
70
|
if (!store.getTask(taskId))
|
|
71
71
|
throw new WebRequestRejected("Task not found.");
|
|
72
|
-
const jobs = store.listDurableJobs(taskId).filter(job => isHttpPreview(job.env)).reverse();
|
|
72
|
+
const jobs = store.listDurableJobs(taskId).filter((job) => !job.delegation && isHttpPreview(job.env)).reverse();
|
|
73
73
|
const services = await Promise.all(jobs.slice(0, 16).map(async (job) => {
|
|
74
74
|
let current = state(job);
|
|
75
75
|
let detail = job.result?.unknownReason ?? `Job: ${job.status}`;
|