mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards. Co-authored-by: Cursor <cursoragent@cursor.com>
517 lines
22 KiB
JavaScript
517 lines
22 KiB
JavaScript
import { t, tf, el, tdCell, apiGet, apiPost, apiRequest, resolveAdminApiUrl, getStoredAuthToken, formatSystemLocalDateTime } from "../core.js";
|
|
import { hasPermission } from "./authz.js";
|
|
|
|
function formatScheduledJobDelivery(job) {
|
|
const d = (job && job.delivery && typeof job.delivery === "object") ? job.delivery : {};
|
|
const parts = [];
|
|
if (d.weixin && d.weixin.enabled) parts.push(t("scheduledJobs.deliveryWeixin"));
|
|
if (d.whatsapp && d.whatsapp.enabled) parts.push(t("scheduledJobs.deliveryWhatsapp"));
|
|
return parts.length ? parts.join(" + ") : "—";
|
|
}
|
|
|
|
function formatScheduledJobPlaybook(job) {
|
|
const steps = Number(job && job.steps_n != null ? job.steps_n : 0) || 0;
|
|
if (job && job.playbook) return tf("scheduledJobs.playbookYes", { n: String(steps) });
|
|
if (job && job.has_recipe) return tf("scheduledJobs.playbookPartial", { n: String(steps) });
|
|
return t("scheduledJobs.playbookNo");
|
|
}
|
|
|
|
function buildScheduledJobDeliveryPayload(job, waChatId) {
|
|
const existing = (job && job.delivery && typeof job.delivery === "object") ? job.delivery : {};
|
|
const wa = existing.whatsapp && typeof existing.whatsapp === "object" ? { ...existing.whatsapp } : {};
|
|
const wx =
|
|
existing.weixin && typeof existing.weixin === "object"
|
|
? { ...existing.weixin }
|
|
: { enabled: true, fixed: true };
|
|
const chat = String(waChatId || "").trim();
|
|
wa.enabled = Boolean(chat);
|
|
wa.chat_id = chat;
|
|
wa.target_type = chat.includes("@g.us") ? "group" : "direct";
|
|
if (wx.enabled === undefined) wx.enabled = true;
|
|
return { whatsapp: wa, weixin: wx };
|
|
}
|
|
|
|
async function renderScheduledJobs() {
|
|
let scheduledJobMenuEl = null;
|
|
let editingJob = null;
|
|
const canWrite = hasPermission("admin:runtime:write");
|
|
const closeScheduledJobMenu = () => {
|
|
if (scheduledJobMenuEl && scheduledJobMenuEl.parentNode) {
|
|
scheduledJobMenuEl.remove();
|
|
}
|
|
scheduledJobMenuEl = null;
|
|
};
|
|
|
|
const status = el("select", { class: "input input--compact" }, [
|
|
el("option", { value: "", text: t("scheduledJobs.all") }),
|
|
el("option", { value: "active", text: t("scheduledJobs.statusActive") }),
|
|
el("option", { value: "paused", text: t("scheduledJobs.statusPaused") }),
|
|
]);
|
|
const tbody = el("tbody", {});
|
|
const runsTbody = el("tbody", {});
|
|
const runsEmpty = el("div", { class: "muted", text: t("scheduledJobs.noRuns") });
|
|
const runsTable = el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("scheduledJobs.colJob") }),
|
|
el("th", { text: t("scheduledJobs.colRunStatus") }),
|
|
el("th", { text: t("scheduledJobs.colFailClass") }),
|
|
el("th", { text: t("scheduledJobs.colFinished") }),
|
|
el("th", { text: t("scheduledJobs.colError") }),
|
|
]),
|
|
]),
|
|
runsTbody,
|
|
]),
|
|
]);
|
|
const runsBox = el("div", {}, [runsEmpty, runsTable]);
|
|
runsTable.style.display = "none";
|
|
const statsBox = el("div", { class: "muted", text: "" });
|
|
const attentionBox = el("div", { class: "muted", text: "" });
|
|
const msg = el("div", { class: "muted", text: "" });
|
|
const weixinInfo = el("div", { class: "muted", text: "" });
|
|
|
|
const failClassLabel = (fc) => {
|
|
const key = `scheduledJobs.failClass.${String(fc || "").trim()}`;
|
|
const labeled = t(key);
|
|
return labeled === key ? String(fc || "—") : labeled;
|
|
};
|
|
|
|
const renderRunsTable = (items, jobNameFallback) => {
|
|
const rows = Array.isArray(items) ? items : [];
|
|
runsTbody.replaceChildren();
|
|
if (!rows.length) {
|
|
runsEmpty.style.display = "";
|
|
runsTable.style.display = "none";
|
|
runsEmpty.textContent = t("scheduledJobs.noRuns");
|
|
return;
|
|
}
|
|
runsEmpty.style.display = "none";
|
|
runsTable.style.display = "";
|
|
for (const run of rows.slice(0, 30)) {
|
|
const st = String(run.status || "");
|
|
const fc = String(run.failure_class || "");
|
|
const err = String(run.error || "").trim();
|
|
const name = String(run.job_name || jobNameFallback || run.job_id || "—");
|
|
const finished = run.finished_at
|
|
? formatSystemLocalDateTime(String(run.finished_at))
|
|
: (run.created_at ? formatSystemLocalDateTime(String(run.created_at)) : "—");
|
|
const statusCell = el("td", { text: st || "—" });
|
|
if (st.toLowerCase() === "failed" || st.toLowerCase() === "error") {
|
|
statusCell.style.color = "var(--danger, #c44)";
|
|
} else if (st.toLowerCase() === "success") {
|
|
statusCell.style.color = "var(--success, #2a7)";
|
|
}
|
|
runsTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: name.slice(0, 40) }),
|
|
statusCell,
|
|
el("td", { text: fc ? failClassLabel(fc) : "—" }),
|
|
el("td", { text: finished }),
|
|
el("td", { text: err ? err.slice(0, 120) : "—" }),
|
|
])
|
|
);
|
|
}
|
|
};
|
|
|
|
const loadStats = async () => {
|
|
try {
|
|
const stats = await apiGet("/admin/api/scheduled-jobs/stats?recent_limit=80");
|
|
const finished = Number(stats.recent_success || 0) + Number(stats.recent_failed || 0);
|
|
const pct = Number(stats.recent_fail_rate_pct || 0);
|
|
const classes = stats.recent_fail_classes || {};
|
|
const classParts = Object.keys(classes).map((k) => `${failClassLabel(k)} ${classes[k]}`);
|
|
statsBox.textContent = [
|
|
tf("scheduledJobs.statsFailRate", {
|
|
pct: String(pct),
|
|
failed: String(stats.recent_failed || 0),
|
|
finished: String(finished),
|
|
}),
|
|
classParts.length
|
|
? `${t("scheduledJobs.statsClasses")}: ${classParts.join(" · ")}`
|
|
: t("scheduledJobs.statsNone"),
|
|
tf("scheduledJobs.statsJobsFailed", { n: String((stats.jobs_last_failed || []).length) }),
|
|
].join(" | ");
|
|
const failedJobs = stats.jobs_last_failed || [];
|
|
if (!failedJobs.length) {
|
|
attentionBox.textContent = "";
|
|
} else {
|
|
attentionBox.textContent =
|
|
`${t("scheduledJobs.attentionTitle")}: ` +
|
|
failedJobs
|
|
.slice(0, 8)
|
|
.map((j) => `${j.name || j.id}${j.last_run_at ? ` @ ${formatSystemLocalDateTime(String(j.last_run_at))}` : ""}`)
|
|
.join(" · ");
|
|
}
|
|
const fails = stats.recent_failures || [];
|
|
if (fails.length) {
|
|
renderRunsTable(fails, "");
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
statsBox.textContent = String(e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const field = (label, control, wide) =>
|
|
el("div", { class: wide ? "form-field form-field--full" : "form-field" }, [
|
|
el("label", { class: "form-field__label", text: label }),
|
|
control,
|
|
]);
|
|
|
|
const editModal = el("div", { class: "session-monitor-modal u-hidden" });
|
|
const editTitle = el("div", { class: "card__title", text: t("scheduledJobs.editTitle") });
|
|
const editNameInput = el("input", { class: "input", placeholder: t("scheduledJobs.phName") });
|
|
const editKindInput = el("select", { class: "input" }, [
|
|
el("option", { value: "cron", text: "cron" }),
|
|
el("option", { value: "once", text: "once" }),
|
|
el("option", { value: "interval", text: "interval" }),
|
|
]);
|
|
const editExprInput = el("input", { class: "input", placeholder: t("scheduledJobs.phExpr") });
|
|
const editPromptInput = el("textarea", { class: "input", rows: "5", placeholder: t("scheduledJobs.phPrompt") });
|
|
const editSpecialistInput = el("input", { class: "input", placeholder: t("scheduledJobs.phSpecialist") });
|
|
const editWaChatInput = el("input", { class: "input", placeholder: t("scheduledJobs.phWhatsapp") });
|
|
const editWeixinInfo = el("div", { class: "muted", text: "" });
|
|
const closeEditModal = () => {
|
|
editingJob = null;
|
|
editModal.style.display = "none";
|
|
};
|
|
const openEditModal = (job) => {
|
|
if (!job || !job.id) return;
|
|
editingJob = job;
|
|
editTitle.textContent = `${t("scheduledJobs.editTitle")}: ${String(job.name || job.id)}`;
|
|
editNameInput.value = String(job.name || "");
|
|
editKindInput.value = String(job.schedule_kind || "cron");
|
|
editExprInput.value = String(job.schedule_expr || "");
|
|
editPromptInput.value = String(job.prompt_text || "");
|
|
editSpecialistInput.value = String(job.specialist || "generalist");
|
|
const wa = job.delivery && job.delivery.whatsapp ? job.delivery.whatsapp : {};
|
|
editWaChatInput.value = String(wa.chat_id || "");
|
|
const wx = job.delivery && job.delivery.weixin ? job.delivery.weixin : {};
|
|
editWeixinInfo.textContent = wx.external_user_id
|
|
? tf("scheduledJobs.weixinFixed", { id: String(wx.external_user_id) })
|
|
: weixinInfo.textContent || t("scheduledJobs.weixinMissing");
|
|
editModal.style.display = "flex";
|
|
setTimeout(() => editNameInput.focus(), 0);
|
|
};
|
|
editModal.addEventListener("click", (e) => {
|
|
if (e.target === editModal) closeEditModal();
|
|
});
|
|
const editSaveBtn = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("action.save"),
|
|
onclick: async () => {
|
|
if (!editingJob || !editingJob.id) return;
|
|
const name = String(editNameInput.value || "").trim();
|
|
const promptText = String(editPromptInput.value || "").trim();
|
|
const scheduleExpr = String(editExprInput.value || "").trim();
|
|
if (!name || !promptText || !scheduleExpr) {
|
|
msg.textContent = t("scheduledJobs.requiredFields");
|
|
return;
|
|
}
|
|
try {
|
|
await apiRequest("PATCH", `/admin/api/scheduled-jobs/${encodeURIComponent(String(editingJob.id))}`, {
|
|
name,
|
|
schedule_kind: editKindInput.value,
|
|
schedule_expr: scheduleExpr,
|
|
prompt_text: promptText,
|
|
specialist: String(editSpecialistInput.value || "generalist").trim() || "generalist",
|
|
delivery: buildScheduledJobDeliveryPayload(editingJob, editWaChatInput.value),
|
|
});
|
|
closeEditModal();
|
|
msg.textContent = t("scheduledJobs.updated");
|
|
await loadJobs();
|
|
} catch (e) {
|
|
msg.textContent = String(e && e.message ? e.message : e);
|
|
}
|
|
},
|
|
});
|
|
const editModalCard = el("div", { class: "card session-monitor-modal__card", style: "width:min(640px,96vw);" }, [
|
|
editTitle,
|
|
editWeixinInfo,
|
|
el("div", { class: "form-stack" }, [
|
|
field(t("scheduledJobs.fieldName"), editNameInput, true),
|
|
el("div", { class: "form-grid" }, [
|
|
field(t("scheduledJobs.fieldKind"), editKindInput),
|
|
field(t("scheduledJobs.fieldExpr"), editExprInput),
|
|
]),
|
|
field(t("scheduledJobs.fieldPrompt"), editPromptInput, true),
|
|
el("div", { class: "form-grid" }, [
|
|
field(t("scheduledJobs.fieldSpecialist"), editSpecialistInput),
|
|
field(t("scheduledJobs.fieldWhatsapp"), editWaChatInput),
|
|
]),
|
|
el("div", { class: "form-actions" }, [
|
|
el("button", { class: "btn", text: t("scheduledJobs.cancel"), onclick: closeEditModal }),
|
|
editSaveBtn,
|
|
]),
|
|
]),
|
|
]);
|
|
editModal.appendChild(editModalCard);
|
|
|
|
async function loadLatestRuns(items) {
|
|
const jobs = Array.isArray(items) ? items : [];
|
|
if (!jobs.length) {
|
|
renderRunsTable([], "");
|
|
return;
|
|
}
|
|
const withLast = jobs.filter((j) => String(j.last_run_at || "").trim());
|
|
const target = withLast.length ? withLast[0] : jobs[0];
|
|
if (!target || !target.id) {
|
|
renderRunsTable([], "");
|
|
return;
|
|
}
|
|
const runs = await apiGet(`/admin/api/scheduled-jobs/${encodeURIComponent(String(target.id))}/runs?limit=10`);
|
|
const runItems = (runs.items || []).map((r) => ({ ...r, job_name: target.name || target.id }));
|
|
renderRunsTable(runItems, String(target.name || target.id || ""));
|
|
}
|
|
|
|
async function loadJobs() {
|
|
closeScheduledJobMenu();
|
|
msg.textContent = t("scheduledJobs.loading");
|
|
const st = String(status.value || "").trim();
|
|
const q = st ? `?status=${encodeURIComponent(st)}` : "";
|
|
const resp = await apiGet(`/admin/api/scheduled-jobs${q}`);
|
|
tbody.replaceChildren();
|
|
for (const job of resp.items || []) {
|
|
const jobId = String(job.id || "");
|
|
const btnMore = el("button", {
|
|
class: "chat-sess-more",
|
|
text: "⋯",
|
|
title: t("scheduledJobs.menuTitle"),
|
|
onclick: (ev) => {
|
|
ev.stopPropagation();
|
|
closeScheduledJobMenu();
|
|
const menu = el("div", { class: "chat-sess-menu-pop u-overlay-fixed" }, [
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("scheduledJobs.viewRuns"),
|
|
onclick: async () => {
|
|
closeScheduledJobMenu();
|
|
const runs = await apiGet(`/admin/api/scheduled-jobs/${encodeURIComponent(jobId)}/runs`);
|
|
const runItems = (runs.items || []).map((r) => ({ ...r, job_name: job.name || jobId }));
|
|
renderRunsTable(runItems, String(job.name || jobId));
|
|
},
|
|
}),
|
|
...(canWrite
|
|
? [
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("scheduledJobs.edit"),
|
|
onclick: () => {
|
|
closeScheduledJobMenu();
|
|
openEditModal(job);
|
|
},
|
|
}),
|
|
]
|
|
: []),
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: job.status === "active" ? t("scheduledJobs.pause") : t("scheduledJobs.resume"),
|
|
onclick: async () => {
|
|
closeScheduledJobMenu();
|
|
const path =
|
|
job.status === "active"
|
|
? `/admin/api/scheduled-jobs/${encodeURIComponent(jobId)}/pause`
|
|
: `/admin/api/scheduled-jobs/${encodeURIComponent(jobId)}/resume`;
|
|
await apiPost(path, {});
|
|
await loadJobs();
|
|
},
|
|
}),
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("scheduledJobs.runNow"),
|
|
onclick: async () => {
|
|
closeScheduledJobMenu();
|
|
const out = await apiPost(`/admin/api/scheduled-jobs/${encodeURIComponent(jobId)}/run-now`, {});
|
|
if (out && out.skipped) {
|
|
msg.textContent = t("scheduledJobs.skippedOverlap");
|
|
} else {
|
|
msg.textContent = t("scheduledJobs.triggered");
|
|
}
|
|
await loadJobs();
|
|
},
|
|
}),
|
|
...(canWrite
|
|
? [
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("scheduledJobs.delete"),
|
|
onclick: async () => {
|
|
closeScheduledJobMenu();
|
|
if (!window.confirm(t("scheduledJobs.deleteConfirm"))) return;
|
|
await fetch(resolveAdminApiUrl(`/admin/api/scheduled-jobs/${encodeURIComponent(jobId)}`), {
|
|
method: "DELETE",
|
|
headers: { authorization: `Bearer ${getStoredAuthToken()}`, accept: "application/json" },
|
|
});
|
|
await loadJobs();
|
|
},
|
|
}),
|
|
]
|
|
: []),
|
|
]);
|
|
const rect = ev.currentTarget.getBoundingClientRect();
|
|
document.body.appendChild(menu);
|
|
const mrect = menu.getBoundingClientRect();
|
|
const pad = 8;
|
|
let left = rect.left - 120;
|
|
let top = rect.bottom + 4;
|
|
if (top + mrect.height > window.innerHeight - pad) {
|
|
top = rect.top - 4 - mrect.height;
|
|
}
|
|
left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width));
|
|
top = Math.max(pad, Math.min(top, window.innerHeight - pad - mrect.height));
|
|
menu.style.left = `${left}px`;
|
|
menu.style.top = `${top}px`;
|
|
scheduledJobMenuEl = menu;
|
|
const close = (e) => {
|
|
if (!menu.contains(e.target)) {
|
|
closeScheduledJobMenu();
|
|
document.removeEventListener("click", close);
|
|
}
|
|
};
|
|
setTimeout(() => document.addEventListener("click", close), 0);
|
|
},
|
|
});
|
|
const lastStatus = String(job.last_run_status || "").trim();
|
|
const lastRun = job.last_run_at
|
|
? `${formatSystemLocalDateTime(String(job.last_run_at))}${lastStatus ? ` (${lastStatus})` : ""}`
|
|
: "—";
|
|
const lastCell = el("td", { text: lastRun });
|
|
if (lastStatus.toLowerCase() === "failed") {
|
|
lastCell.style.color = "var(--danger, #c44)";
|
|
} else if (lastStatus.toLowerCase() === "success") {
|
|
lastCell.style.color = "var(--success, #2a7)";
|
|
}
|
|
const tr = el("tr", {}, [
|
|
tdCell(job.name || "", 24),
|
|
tdCell(`${job.schedule_kind}:${job.schedule_expr}`, 28),
|
|
tdCell(formatScheduledJobPlaybook(job), 18),
|
|
tdCell(job.status || "", 10),
|
|
tdCell(job.next_run_at || "—", 20),
|
|
lastCell,
|
|
tdCell(job.specialist || "", 14),
|
|
tdCell(formatScheduledJobDelivery(job), 12),
|
|
el("td", { class: "table__cell--actions" }, [
|
|
el("div", { class: "table__cell-actions" }, [btnMore]),
|
|
]),
|
|
]);
|
|
tbody.appendChild(tr);
|
|
}
|
|
msg.textContent = tf("scheduledJobs.count", { count: String((resp.items || []).length) });
|
|
const showedFails = await loadStats();
|
|
if (!showedFails) {
|
|
await loadLatestRuns(resp.items || []);
|
|
}
|
|
}
|
|
|
|
const nameInput = el("input", { class: "input", placeholder: t("scheduledJobs.phName") });
|
|
const kindInput = el("select", { class: "input" }, [
|
|
el("option", { value: "cron", text: "cron" }),
|
|
el("option", { value: "once", text: "once" }),
|
|
el("option", { value: "interval", text: "interval" }),
|
|
]);
|
|
const exprInput = el("input", { class: "input", placeholder: t("scheduledJobs.phExpr") });
|
|
const promptInput = el("textarea", { class: "input", rows: "5", placeholder: t("scheduledJobs.phPrompt") });
|
|
const specialistInput = el("input", { class: "input", placeholder: t("scheduledJobs.phSpecialist"), value: "generalist" });
|
|
const waChatInput = el("input", { class: "input", placeholder: t("scheduledJobs.phWhatsapp") });
|
|
|
|
async function loadMeta() {
|
|
const meta = await apiGet("/admin/api/scheduled-jobs/meta/targets");
|
|
const wx = meta.weixin_binding || {};
|
|
weixinInfo.textContent = wx.external_user_id
|
|
? tf("scheduledJobs.weixinFixed", { id: String(wx.external_user_id) })
|
|
: t("scheduledJobs.weixinMissing");
|
|
}
|
|
|
|
await loadMeta();
|
|
await loadJobs();
|
|
|
|
const createCardChildren = [
|
|
el("div", { class: "card__title", text: t("scheduledJobs.createTitle") }),
|
|
weixinInfo,
|
|
el("div", { class: "form-stack" }, [
|
|
field(t("scheduledJobs.fieldName"), nameInput, true),
|
|
el("div", { class: "form-grid" }, [
|
|
field(t("scheduledJobs.fieldKind"), kindInput),
|
|
field(t("scheduledJobs.fieldExpr"), exprInput),
|
|
]),
|
|
field(t("scheduledJobs.fieldPrompt"), promptInput, true),
|
|
el("div", { class: "form-grid" }, [
|
|
field(t("scheduledJobs.fieldSpecialist"), specialistInput),
|
|
field(t("scheduledJobs.fieldWhatsapp"), waChatInput),
|
|
]),
|
|
el("div", { class: "form-actions" }, [
|
|
el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("scheduledJobs.create"),
|
|
onclick: async () => {
|
|
await apiPost("/admin/api/scheduled-jobs", {
|
|
name: nameInput.value,
|
|
schedule_kind: kindInput.value,
|
|
schedule_expr: exprInput.value,
|
|
prompt_text: promptInput.value,
|
|
specialist: specialistInput.value,
|
|
whatsapp_chat_id: waChatInput.value,
|
|
delivery: buildScheduledJobDeliveryPayload(null, waChatInput.value),
|
|
});
|
|
await loadJobs();
|
|
},
|
|
}),
|
|
]),
|
|
]),
|
|
];
|
|
|
|
return el("div", {}, [
|
|
editModal,
|
|
el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("title.scheduledJobs") }),
|
|
el("div", { class: "row", style: "gap:8px;align-items:center;flex-wrap:wrap;" }, [
|
|
status,
|
|
el("button", { class: "btn", text: t("action.refresh"), onclick: () => loadJobs() }),
|
|
msg,
|
|
]),
|
|
el("div", { class: "card__title", style: "margin-top:10px;font-size:13px;", text: t("scheduledJobs.statsTitle") }),
|
|
statsBox,
|
|
attentionBox,
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("colgroup", {}, [
|
|
el("col", { style: "width:14%" }),
|
|
el("col", { style: "width:14%" }),
|
|
el("col", { class: "u-w-12p" }),
|
|
el("col", { style: "width:8%" }),
|
|
el("col", { class: "u-w-12p" }),
|
|
el("col", { class: "u-w-12p" }),
|
|
el("col", { style: "width:8%" }),
|
|
el("col", { style: "width:10%" }),
|
|
el("col", { style: "width:10%" }),
|
|
]),
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("scheduledJobs.colName") }),
|
|
el("th", { text: t("scheduledJobs.colSchedule") }),
|
|
el("th", { text: t("scheduledJobs.colPlaybook") }),
|
|
el("th", { text: t("scheduledJobs.colStatus") }),
|
|
el("th", { text: t("scheduledJobs.colNextRun") }),
|
|
el("th", { text: t("scheduledJobs.colLastRun") }),
|
|
el("th", { text: t("scheduledJobs.colSpecialist") }),
|
|
el("th", { text: t("scheduledJobs.colDelivery") }),
|
|
el("th", { text: t("scheduledJobs.colActions") }),
|
|
]),
|
|
]),
|
|
tbody,
|
|
]),
|
|
]),
|
|
]),
|
|
...(canWrite ? [el("div", { class: "card" }, createCardChildren)] : []),
|
|
el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: `${t("scheduledJobs.runHistory")} · ${t("scheduledJobs.runHistoryHint")}` }),
|
|
runsBox,
|
|
]),
|
|
]);
|
|
}
|
|
|
|
|
|
export { formatScheduledJobDelivery, formatScheduledJobPlaybook, buildScheduledJobDeliveryPayload, renderScheduledJobs };
|