import { useEffect, useState } from "react"; import { api, JobRun } from "../api/haixun"; import { Badge, Button } from "./ui"; import { formatNano } from "../lib/format"; import { jobStatusBadgeClass, jobStatusLabel } from "../lib/jobStatus"; import { AcIcon } from "./AcIcon"; const ACTIVE_STATUSES = ["running", "queued", "pending", "waiting_worker", "cancel_requested"]; export function JobMonitor() { const [jobs, setJobs] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const activeJobs = jobs.filter((j) => ACTIVE_STATUSES.includes(j.status)); async function load() { try { const data = await api.jobs(1); setJobs(data.list || []); } catch { // silent fail for background monitor } } useEffect(() => { void load(); const timer = window.setInterval(() => void load(), 4000); return () => window.clearInterval(timer); }, []); async function cancel(job: JobRun) { setLoading(true); try { await api.cancelJob(job.id); await load(); } finally { setLoading(false); } } const count = activeJobs.length; return ( <> {/* Floating trigger button (always visible) */} {/* Floating panel */} {open && (
進行中的任務 {count > 0 ? `(${count})` : ""}
{activeJobs.length === 0 ? (

目前沒有進行中的任務。

) : ( activeJobs.map((job) => (
{job.template_type} {jobStatusLabel(job.status)}
{job.progress && (
{job.progress.percentage ?? 0}% {job.progress.summary ? `· ${job.progress.summary}` : ""}
)}
{formatNano(job.update_at)} {["running", "queued", "pending"].includes(job.status) && ( )}
{job.id}
)) )}
)} ); }