import { css } from 'lit'; import type { Job, JobLogEntry } from '@store/job-store'; import { isIndeterminate, progressFraction } from '@store/job-store'; /** Icon for a job kind, used in the indicator and job rows. */ export function jobIcon(job: Job): string { switch (job.kind) { case 'library-scan': return 'folder'; case 'index-build': return 'database'; case 'autotag-apply': return 'tags'; case 'catalog-enrich': // The globe rather than the database: this is catalog data // arriving from the network, not the local index being // rebuilt from it. return 'globe'; default: return 'gear'; } } /** Human-readable label for a job state. */ export function stateLabel(job: Job): string { switch (job.state) { case 'queued': return 'Queued'; case 'running': return 'Running'; case 'pausing': return 'Pausing…'; case 'paused': return 'Paused'; case 'cancelling': return 'Cancelling…'; case 'complete': return 'Complete'; case 'cancelled': return 'Stopped'; case 'error': return 'Failed'; default: return job.state; } } /** * Semantic colour name for a state, mapped to CSS custom properties by * the `jobStateStyles` block below. */ export function stateTone( job: Job, ): 'active' | 'paused' | 'danger' | 'success' | 'muted' { switch (job.state) { case 'running': return 'active'; case 'pausing': case 'paused': return 'paused'; case 'error': return 'danger'; case 'complete': return 'success'; default: return 'muted'; } } /** Compact "1,204 / 12,880" progress text, or null when indeterminate. */ export function progressText(job: Job): string | null { if (isIndeterminate(job)) return null; return `${formatCount(job.current)} / ${formatCount(job.total)}`; } /** Progress as a whole-number percentage, or null when indeterminate. */ export function progressPercent(job: Job): number | null { const fraction = progressFraction(job); return fraction === null ? null : Math.round(fraction * 100); } /** * The one-line status shown under a job title: phase plus counts, with * the state folded in when it is something other than plain running. */ export function statusLine(job: Job): string { const parts: string[] = []; const label = stateLabel(job); if (job.state !== 'running' && job.state !== 'queued') { parts.push(label); } // A paused job's phase is often just "Paused", which would render // as "Paused · Paused" alongside the state label. if (job.phase && job.phase !== label) parts.push(job.phase); const progress = progressText(job); if (progress && job.state !== 'complete') parts.push(progress); if (parts.length === 0) parts.push(stateLabel(job)); return parts.join(' · '); } export function formatCount(n: number): string { return n.toLocaleString(); } /** Wall-clock duration of a job, as "1m 24s". */ export function formatElapsed(job: Job): string { const end = job.endedAt && job.endedAt > 0 ? job.endedAt : Date.now(); const seconds = Math.max(0, Math.round((end - job.startedAt) / 1000)); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainder = seconds % 60; if (minutes < 60) return `${minutes}m ${remainder}s`; return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; } /** Clock time for a log entry, e.g. "14:03:21". */ export function formatLogTime(entry: JobLogEntry): string { return new Date(entry.time).toLocaleTimeString(undefined, { hour12: false, }); } /** Renders a job log as plain text for the clipboard. */ export function logToText(job: Job, entries: JobLogEntry[]): string { const header = `${job.title} — ${stateLabel(job)}`; const lines = entries.map((entry) => { const detail = entry.detail ? ` (${entry.detail})` : ''; return `${formatLogTime(entry)} [${entry.level}] ${entry.message}${detail}`; }); return [header, ...lines].join('\n'); } /** * Shared state-tone colour variables. Include in a component's styles * array so `.tone-active`, `.tone-paused` etc. resolve consistently * wherever job state is rendered. */ export const jobStateStyles = css` .tone-active { --job-tone: var(--yj-accent, #ffd43b); } .tone-paused { --job-tone: #f5a623; } .tone-danger { --job-tone: var(--yj-error-text, #ff8787); } .tone-success { --job-tone: var(--yj-success-text, #51cf66); } .tone-muted { --job-tone: var(--yj-text-tertiary, #868e96); } `;