feat: 新增任务管理页与失败任务重试
This commit is contained in:
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user