feat(webui): enhanced tasks page with countdown and status dots
This commit is contained in:
parent
f0ef14099d
commit
ee5bc46cc6
@ -10,6 +10,7 @@
|
|||||||
let runs = $state({});
|
let runs = $state({});
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
|
let tick = $state(0);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
@ -21,7 +22,7 @@
|
|||||||
jobs = (await api("/api/jobs")).jobs;
|
jobs = (await api("/api/jobs")).jobs;
|
||||||
runs = Object.fromEntries(await Promise.all(jobs.map(async (job) => [
|
runs = Object.fromEntries(await Promise.all(jobs.map(async (job) => [
|
||||||
job.id,
|
job.id,
|
||||||
await api(`/api/jobs/${encodeURIComponent(job.id)}/runs?limit=5`).then((value) => value.runs).catch(() => [])
|
await api(`/api/jobs/${encodeURIComponent(job.id)}/runs?limit=10`).then((value) => value.runs).catch(() => [])
|
||||||
])));
|
])));
|
||||||
}
|
}
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
@ -36,7 +37,43 @@
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
function countdown(ts) {
|
||||||
|
void tick;
|
||||||
|
if (!ts) return "—";
|
||||||
|
const ms = ts < 1e12 ? ts * 1000 : ts;
|
||||||
|
const diff = ms - Date.now();
|
||||||
|
if (diff <= 0) return "即将运行";
|
||||||
|
const mins = Math.floor(diff / 60000);
|
||||||
|
if (mins < 1) return "即将运行";
|
||||||
|
if (mins < 60) return `${mins} 分钟后`;
|
||||||
|
const hours = Math.floor(mins / 60);
|
||||||
|
if (hours < 24) return `${hours} 小时后`;
|
||||||
|
return `${Math.floor(hours / 24)} 天后`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function elapsed(createdAt) {
|
||||||
|
void tick;
|
||||||
|
if (!createdAt) return "";
|
||||||
|
const ms = createdAt < 1e12 ? createdAt * 1000 : createdAt;
|
||||||
|
const secs = Math.floor((Date.now() - ms) / 1000);
|
||||||
|
if (secs < 0) return "";
|
||||||
|
if (secs < 60) return `${secs}s`;
|
||||||
|
if (secs < 3600) return `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
||||||
|
return `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dotColor(status) {
|
||||||
|
if (status === "completed" || status === "success" || status === "ok") return "var(--signal)";
|
||||||
|
if (status === "timeout") return "var(--accent)";
|
||||||
|
if (status === "running") return "var(--info)";
|
||||||
|
return "var(--danger)";
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load();
|
||||||
|
const timer = setInterval(() => { tick += 1; }, 30000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="page active content-page">
|
<section class="page active content-page">
|
||||||
@ -54,15 +91,60 @@
|
|||||||
{:else if error}<div class="empty-card error-text">{error}</div>
|
{:else if error}<div class="empty-card error-text">{error}</div>
|
||||||
{:else if tab === "background"}
|
{:else if tab === "background"}
|
||||||
{#each tasks as task (task.id)}
|
{#each tasks as task (task.id)}
|
||||||
<article class="card"><div class="card-row"><div><h3>{task.prompt.slice(0, 100)}</h3><div class="meta"><span>{task.session_id}</span><span>{formatTime(task.created_at)}</span><span>{task.tool_calls_count} 次工具调用 · {task.iterations} 轮</span></div></div><StatusBadge status={task.status} /></div>{#if task.result}<div class="details"><p>{task.result}</p></div>{/if}{#if task.error}<p class="error-text">{task.error}</p>{/if}</article>
|
<article class="card">
|
||||||
|
<div class="card-row">
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{#if task.status === "running"}<span class="pulse"></span>{/if}
|
||||||
|
{task.prompt.slice(0, 100)}
|
||||||
|
</h3>
|
||||||
|
<div class="meta">
|
||||||
|
<span>{task.session_id}</span>
|
||||||
|
<span>{formatTime(task.created_at)}</span>
|
||||||
|
{#if task.status === "running"}<span class="elapsed">{elapsed(task.created_at)}</span>{/if}
|
||||||
|
<span>{task.tool_calls_count} 次工具调用 · {task.iterations} 轮</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={task.status} />
|
||||||
|
</div>
|
||||||
|
{#if task.result}<div class="details"><p>{task.result}</p></div>{/if}
|
||||||
|
{#if task.error}<p class="error-text">{task.error}</p>{/if}
|
||||||
|
</article>
|
||||||
{:else}<div class="empty-card">暂无后台任务</div>{/each}
|
{:else}<div class="empty-card">暂无后台任务</div>{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each jobs as job (job.id)}
|
{#each jobs as job (job.id)}
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<div class="card-row"><div><h3>{job.name}</h3><p>{job.prompt}</p><div class="meta"><span>{job.job_kind === "monitor" ? "巡检" : "任务"} · {job.delivery_policy}</span><span>{job.channel} · {job.chat_id}</span><span>下次 {formatTime(job.next_run_at)}</span><span>上次 {formatTime(job.last_run_at)}</span></div></div><StatusBadge status={job.enabled ? (job.last_status || "enabled") : "disabled"} /></div>
|
<div class="card-row">
|
||||||
|
<div>
|
||||||
|
<h3>{job.name}</h3>
|
||||||
|
<p>{job.prompt}</p>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="mono cron">{job.cron}</span>
|
||||||
|
<span>{job.job_kind === "monitor" ? "巡检" : "任务"} · {job.delivery_policy}</span>
|
||||||
|
<span>{job.channel} · {job.chat_id}</span>
|
||||||
|
<span>下次 {countdown(job.next_run_at)}</span>
|
||||||
|
<span>上次 {formatTime(job.last_run_at)}</span>
|
||||||
|
</div>
|
||||||
|
{#if runs[job.id]?.length}
|
||||||
|
<div class="status-dots">
|
||||||
|
{#each runs[job.id] as run}
|
||||||
|
<span class="dot" style="background:{dotColor(run.status)}" title="{run.status} {formatTime(run.finished_at)}"></span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={job.enabled ? (job.last_status || "enabled") : "disabled"} />
|
||||||
|
</div>
|
||||||
{#if runs[job.id]?.length}<div class="details">{#each runs[job.id] as run}<div class="card-row run-row"><span class="meta">{formatTime(run.finished_at)} · {run.duration_ms}ms{run.result_kind ? ` · ${run.result_kind}` : ""}{run.delivery_status ? ` · ${run.delivery_status}` : ""}</span><StatusBadge status={run.status} /></div>{/each}</div>{/if}
|
{#if runs[job.id]?.length}<div class="details">{#each runs[job.id] as run}<div class="card-row run-row"><span class="meta">{formatTime(run.finished_at)} · {run.duration_ms}ms{run.result_kind ? ` · ${run.result_kind}` : ""}{run.delivery_status ? ` · ${run.delivery_status}` : ""}</span><StatusBadge status={run.status} /></div>{/each}</div>{/if}
|
||||||
</article>
|
</article>
|
||||||
{:else}<div class="empty-card">暂无定时任务</div>{/each}
|
{:else}<div class="empty-card">暂无定时任务</div>{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cron { font-size: 11px; color: var(--text-soft); background: var(--code-bg); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
|
||||||
|
.status-dots { display: flex; gap: 4px; margin-top: 6px; align-items: center; }
|
||||||
|
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
||||||
|
.elapsed { color: var(--info); font-family: var(--font-mono); font-size: 10px; }
|
||||||
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user