feat: 新增任务管理页与失败任务重试

This commit is contained in:
cat-shark
2026-08-13 22:01:42 +08:00
parent 164d9971d0
commit 0a028bedcc
5 changed files with 122 additions and 3 deletions
+1
View File
@@ -11,6 +11,7 @@
<a class="brand" href="/">WOV</a> <a class="brand" href="/">WOV</a>
<nav> <nav>
<a href="/">应用中心</a> <a href="/">应用中心</a>
<a href="/tasks.html">任务管理</a>
<a href="/admin.html">管理后台</a> <a href="/admin.html">管理后台</a>
<a href="/workflow.html">工作流</a> <a href="/workflow.html">工作流</a>
</nav> </nav>
+72 -3
View File
@@ -78,15 +78,30 @@ function escapeHtml(value) {
} }
function badge(status) { function badge(status) {
const value = String(status).toLowerCase();
const className = const className =
status === "ready" || status === "completed" || status === "ok" value === "ready" || value === "completed" || value === "ok"
? "ok" ? "ok"
: status === "stopped" || status === "error" : value === "stopped" || value === "error" || value === "failed"
? "error" ? "error"
: "warn"; : "warn";
return `<span class="badge ${className}">${escapeHtml(status)}</span>`; return `<span class="badge ${className}">${escapeHtml(status)}</span>`;
} }
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() { async function loadHealth() {
try { try {
await api("/health"); await api("/health");
@@ -235,6 +250,51 @@ async function loadWorkflows() {
: '<tr><td colspan="5">暂无工作流</td></tr>'; : '<tr><td colspan="5">暂无工作流</td></tr>';
} }
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)} <span class="muted">已运行 ${formatElapsed(run.updated_at)}</span>`
: run.status === "QUEUED"
? `${badge(run.status)} <span class="muted">排队中</span>`
: badge(run.status);
const actions =
run.status === "FAILED"
? `<button class="danger" data-retry-run="${escapeHtml(run.id)}">重试</button>`
: "";
return `
<tr>
<td>${escapeHtml(run.id)}</td>
<td>${escapeHtml(run.workflow_id)}</td>
<td>${statusHtml}</td>
<td>${escapeHtml(run.current_node_id || "-")}</td>
<td>${percent}%</td>
<td>${escapeHtml(formatTime(run.created_at))}</td>
<td title="${escapeHtml(error)}">${error ? escapeHtml(error.slice(0, 60)) : "-"}</td>
<td>${actions}</td>
</tr>`;
})
.join("")
: '<tr><td colspan="8">暂无任务</td></tr>';
}
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() { async function createWorkflow() {
const workflowId = document.getElementById("workflowId").value.trim(); const workflowId = document.getElementById("workflowId").value.trim();
const name = document.getElementById("workflowName").value.trim(); const name = document.getElementById("workflowName").value.trim();
@@ -306,7 +366,7 @@ async function uploadVideo() {
} }
async function pollRun(runId, progress, downloads) { 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 run = await api(`/api/runs/${runId}`);
const percent = Math.round((run.progress || 0) * 100); const percent = Math.round((run.progress || 0) * 100);
progress.textContent = `状态:${run.status} | 当前节点:${run.current_node_id || "-"} | 进度:${percent}%`; 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]"); const publishWorkflowButton = event.target.closest("[data-publish-workflow]");
if (publishWorkflowButton) { if (publishWorkflowButton) {
publishWorkflow(publishWorkflowButton.dataset.publishWorkflow); 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")) { if (document.getElementById("workflowList")) {
await loadWorkflows(); await loadWorkflows();
} }
if (document.getElementById("runList")) {
await loadRuns();
setInterval(loadRuns, 5000);
}
}); });
+1
View File
@@ -11,6 +11,7 @@
<a class="brand" href="/">WOV</a> <a class="brand" href="/">WOV</a>
<nav> <nav>
<a href="/">应用中心</a> <a href="/">应用中心</a>
<a href="/tasks.html">任务管理</a>
<a href="/admin.html">管理后台</a> <a href="/admin.html">管理后台</a>
<a href="/workflow.html">工作流</a> <a href="/workflow.html">工作流</a>
</nav> </nav>
+47
View File
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WOV 任务管理</title>
<link rel="stylesheet" href="/assets/styles.css" />
</head>
<body>
<header class="topbar">
<a class="brand" href="/">WOV</a>
<nav>
<a href="/">应用中心</a>
<a href="/tasks.html">任务管理</a>
<a href="/admin.html">管理后台</a>
<a href="/workflow.html">工作流</a>
</nav>
</header>
<main class="container">
<h1>任务管理</h1>
<p class="muted">查看所有任务的执行状态;失败的任务可以重新排队执行。</p>
<section class="panel">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>任务 ID</th>
<th>工作流</th>
<th>状态</th>
<th>当前节点</th>
<th>进度</th>
<th>创建时间</th>
<th>错误信息</th>
<th>操作</th>
</tr>
</thead>
<tbody id="runList"></tbody>
</table>
</div>
</section>
</main>
<script src="/assets/app.js"></script>
</body>
</html>
+1
View File
@@ -11,6 +11,7 @@
<a class="brand" href="/">WOV</a> <a class="brand" href="/">WOV</a>
<nav> <nav>
<a href="/">应用中心</a> <a href="/">应用中心</a>
<a href="/tasks.html">任务管理</a>
<a href="/admin.html">管理后台</a> <a href="/admin.html">管理后台</a>
<a href="/workflow.html">工作流</a> <a href="/workflow.html">工作流</a>
</nav> </nav>