feat(web): richer active-job rows on The Floor (source/format/tracks, stage, elapsed, error)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ export function JobRow({
|
||||
meta,
|
||||
note,
|
||||
indeterminate,
|
||||
stageLabel,
|
||||
}: {
|
||||
artist: string;
|
||||
album: string;
|
||||
@@ -30,6 +31,7 @@ export function JobRow({
|
||||
meta?: ReactNode;
|
||||
note?: ReactNode;
|
||||
indeterminate?: boolean;
|
||||
stageLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<article className={`job ${rowClass(kind)}`}>
|
||||
@@ -42,7 +44,7 @@ export function JobRow({
|
||||
{note ? (
|
||||
<div className="note">{note}</div>
|
||||
) : kind !== "attention" ? (
|
||||
<ProgressBar kind={kind} step={step} total={total} indeterminate={indeterminate} />
|
||||
<ProgressBar kind={kind} step={step} total={total} indeterminate={indeterminate} stageLabel={stageLabel} />
|
||||
) : null}
|
||||
</div>
|
||||
<StatusChip kind={kind} label={label} />
|
||||
|
||||
@@ -7,11 +7,13 @@ export function ProgressBar({
|
||||
step,
|
||||
total,
|
||||
indeterminate,
|
||||
stageLabel,
|
||||
}: {
|
||||
kind: Kind;
|
||||
step: number;
|
||||
total: number;
|
||||
indeterminate?: boolean;
|
||||
stageLabel?: string;
|
||||
}) {
|
||||
if (indeterminate) {
|
||||
return (
|
||||
@@ -29,9 +31,7 @@ export function ProgressBar({
|
||||
<div className="bar">
|
||||
<span style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="pct">
|
||||
{step}/{total}
|
||||
</div>
|
||||
<div className="pct">{stageLabel ? `${stageLabel} · step ${step} of ${total}` : `${step}/${total}`}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describeJob } from "./status";
|
||||
import { describeJob, timeAgo } from "./status";
|
||||
|
||||
describe("describeJob", () => {
|
||||
it("maps pipeline states to literal labels + severity kind", () => {
|
||||
@@ -22,3 +22,29 @@ describe("describeJob", () => {
|
||||
expect(describeJob("downloading", "???").step).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeAgo", () => {
|
||||
const now = new Date("2026-07-13T12:00:00Z").getTime();
|
||||
|
||||
it("returns 'just now' for deltas under 60s", () => {
|
||||
expect(timeAgo(new Date(now - 30_000).toISOString(), now)).toBe("just now");
|
||||
});
|
||||
|
||||
it("returns minutes for sub-hour deltas", () => {
|
||||
expect(timeAgo(new Date(now - 5 * 60_000).toISOString(), now)).toBe("5m");
|
||||
});
|
||||
|
||||
it("returns hours for sub-day deltas", () => {
|
||||
expect(timeAgo(new Date(now - 2 * 3600_000).toISOString(), now)).toBe("2h");
|
||||
});
|
||||
|
||||
it("returns days for multi-day deltas", () => {
|
||||
expect(timeAgo(new Date(now - 3 * 86_400_000).toISOString(), now)).toBe("3d");
|
||||
});
|
||||
|
||||
it("returns empty string for null/undefined/invalid input", () => {
|
||||
expect(timeAgo(null, now)).toBe("");
|
||||
expect(timeAgo(undefined, now)).toBe("");
|
||||
expect(timeAgo("not-a-date", now)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,20 @@ const STATE_MAP: Record<string, { label: string; kind: Kind }> = {
|
||||
needs_attention: { label: "Needs attention", kind: "attention" },
|
||||
};
|
||||
|
||||
/** Compact elapsed label from an ISO/Date string to `now` (ms since epoch). */
|
||||
export function timeAgo(value: string | Date | null | undefined, now: number): string {
|
||||
if (!value) return "";
|
||||
const t = new Date(value).getTime();
|
||||
if (Number.isNaN(t)) return "";
|
||||
const s = Math.max(0, Math.floor((now - t) / 1000));
|
||||
if (s < 60) return "just now";
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h`;
|
||||
return `${Math.floor(h / 24)}d`;
|
||||
}
|
||||
|
||||
export function describeJob(
|
||||
state: string,
|
||||
stage: string,
|
||||
|
||||
Reference in New Issue
Block a user