diff --git a/admin.html b/admin.html
index 5f8be70..cd2bc20 100644
--- a/admin.html
+++ b/admin.html
@@ -11,6 +11,7 @@
WOV
diff --git a/assets/app.js b/assets/app.js
index 4ebce40..c86e776 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -78,15 +78,30 @@ function escapeHtml(value) {
}
function badge(status) {
+ const value = String(status).toLowerCase();
const className =
- status === "ready" || status === "completed" || status === "ok"
+ value === "ready" || value === "completed" || value === "ok"
? "ok"
- : status === "stopped" || status === "error"
+ : value === "stopped" || value === "error" || value === "failed"
? "error"
: "warn";
return `${escapeHtml(status)}`;
}
+function formatTime(value) {
+ if (!value) return "";
+ const date = new Date(value);
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
+}
+
+function formatElapsed(value) {
+ if (!value) return "";
+ const seconds = Math.floor((Date.now() - new Date(value).getTime()) / 1000);
+ if (Number.isNaN(seconds) || seconds < 0) return "";
+ if (seconds < 60) return `${seconds} 秒`;
+ return `${Math.floor(seconds / 60)} 分钟`;
+}
+
async function loadHealth() {
try {
await api("/health");
@@ -235,6 +250,51 @@ async function loadWorkflows() {
: '
| 暂无工作流 |
';
}
+async function loadRuns() {
+ const tbody = document.getElementById("runList");
+ if (!tbody) return;
+ const runs = await api("/api/runs");
+ tbody.innerHTML = runs.length
+ ? runs
+ .map((run) => {
+ const percent = Math.round((run.progress || 0) * 100);
+ const error = run.error || "";
+ const statusHtml =
+ run.status === "RUNNING"
+ ? `${badge(run.status)} 已运行 ${formatElapsed(run.updated_at)}`
+ : run.status === "QUEUED"
+ ? `${badge(run.status)} 排队中`
+ : badge(run.status);
+ const actions =
+ run.status === "FAILED"
+ ? ``
+ : "";
+ return `
+
+ | ${escapeHtml(run.id)} |
+ ${escapeHtml(run.workflow_id)} |
+ ${statusHtml} |
+ ${escapeHtml(run.current_node_id || "-")} |
+ ${percent}% |
+ ${escapeHtml(formatTime(run.created_at))} |
+ ${error ? escapeHtml(error.slice(0, 60)) : "-"} |
+ ${actions} |
+
`;
+ })
+ .join("")
+ : '| 暂无任务 |
';
+}
+
+async function retryRun(runId) {
+ try {
+ const result = await api(`/api/runs/${runId}/retry`, { method: "POST" });
+ alert(`任务 ${result.id} 已重新排队`);
+ await loadRuns();
+ } catch (error) {
+ alert(`重试失败:${error.message}`);
+ }
+}
+
async function createWorkflow() {
const workflowId = document.getElementById("workflowId").value.trim();
const name = document.getElementById("workflowName").value.trim();
@@ -306,7 +366,7 @@ async function uploadVideo() {
}
async function pollRun(runId, progress, downloads) {
- for (let i = 0; i < 600; i += 1) {
+ for (let i = 0; i < 3600; i += 1) {
const run = await api(`/api/runs/${runId}`);
const percent = Math.round((run.progress || 0) * 100);
progress.textContent = `状态:${run.status} | 当前节点:${run.current_node_id || "-"} | 进度:${percent}%`;
@@ -354,6 +414,11 @@ document.addEventListener("click", (event) => {
const publishWorkflowButton = event.target.closest("[data-publish-workflow]");
if (publishWorkflowButton) {
publishWorkflow(publishWorkflowButton.dataset.publishWorkflow);
+ return;
+ }
+ const retryButton = event.target.closest("[data-retry-run]");
+ if (retryButton) {
+ retryRun(retryButton.dataset.retryRun);
}
});
@@ -406,4 +471,8 @@ document.addEventListener("DOMContentLoaded", async () => {
if (document.getElementById("workflowList")) {
await loadWorkflows();
}
+ if (document.getElementById("runList")) {
+ await loadRuns();
+ setInterval(loadRuns, 5000);
+ }
});
diff --git a/index.html b/index.html
index d3213be..20d90f6 100644
--- a/index.html
+++ b/index.html
@@ -11,6 +11,7 @@
WOV
diff --git a/tasks.html b/tasks.html
new file mode 100644
index 0000000..fc7a549
--- /dev/null
+++ b/tasks.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ WOV 任务管理
+
+
+
+
+
+
+ 任务管理
+ 查看所有任务的执行状态;失败的任务可以重新排队执行。
+
+
+
+
+
+
+ | 任务 ID |
+ 工作流 |
+ 状态 |
+ 当前节点 |
+ 进度 |
+ 创建时间 |
+ 错误信息 |
+ 操作 |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/workflow.html b/workflow.html
index 04ea238..3992a05 100644
--- a/workflow.html
+++ b/workflow.html
@@ -11,6 +11,7 @@
WOV