export function JobStatusPanel({ job }) {
const percent = Math.round(job?.progress?.percent || 0);
const transfer = transferStats(job);
return (
Job Status
{job?.status || 'idle'}
{job?.error ? : null}
{job?.progress?.detail || job?.currentStep || 'No active job'}
{job ? `${percent}%` : '-'}
{job ? (
) : null}
{(job?.logs || ['No logs yet.']).join('\n')}
);
}
function Metric({ label, value }) {
return (
);
}
function Info({ label, value, tone = 'text-zinc-300' }) {
return (
{label}
{value}
);
}
function transferStats(job) {
const currentBytes = Number(job?.progress?.currentBytes || 0);
const totalBytes = Number(job?.progress?.totalBytes || 0);
const startedAt = job?.startedAt ? new Date(job.startedAt).getTime() : 0;
const finishedAt = job?.finishedAt ? new Date(job.finishedAt).getTime() : Date.now();
const elapsedSeconds = startedAt ? Math.max(1, (finishedAt - startedAt) / 1000) : 0;
const bytesPerSecond = currentBytes && elapsedSeconds ? currentBytes / elapsedSeconds : 0;
const remainingBytes = totalBytes && currentBytes ? Math.max(0, totalBytes - currentBytes) : 0;
const remainingSeconds = bytesPerSecond && remainingBytes ? remainingBytes / bytesPerSecond : 0;
return {
transferred: currentBytes && totalBytes
? `${formatBytes(currentBytes)} / ${formatBytes(totalBytes)}`
: currentBytes
? formatBytes(currentBytes)
: '-',
rate: bytesPerSecond ? `${formatBytes(bytesPerSecond)}/s` : '-',
eta: remainingSeconds ? formatDuration(remainingSeconds) : job?.status === 'success' ? 'Done' : '-',
};
}
function formatBytes(value) {
if (!value) return '-';
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
let size = Number(value);
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex += 1;
}
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}
function formatDuration(secondsValue) {
const totalSeconds = Math.max(0, Math.round(secondsValue));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours) return `${hours}h ${minutes}m`;
if (minutes) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
}
function statusClass(status) {
if (status === 'success') return 'border-emerald-800 bg-emerald-950 text-emerald-300';
if (status === 'failed') return 'border-red-800 bg-red-950 text-red-300';
if (status === 'running' || status === 'queued') return 'border-cyan-800 bg-cyan-950 text-cyan-300';
return 'border-zinc-800 bg-zinc-950 text-zinc-400';
}
function formatTime(value) {
if (!value) return '-';
return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'medium' }).format(new Date(value));
}