mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-19 03:03:27 +08:00
Ship Cursor-only MCP admin and modular Admin/Chat UI.
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>
This commit is contained in:
parent
2aff012b48
commit
f6f129931f
61 changed files with 18057 additions and 16327 deletions
517
interfaces/admin/static/js/pages/scheduled-jobs.js
Normal file
517
interfaces/admin/static/js/pages/scheduled-jobs.js
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
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 };
|
||||
Loading…
Add table
Add a link
Reference in a new issue