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:
Jonathan
2026-07-13 22:05:07 +02:00
parent 3ec1547e10
commit 381426082e
7 changed files with 198 additions and 32 deletions
+3 -1
View File
@@ -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} />
+3 -3
View File
@@ -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>
);
}
+27 -1
View File
@@ -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("");
});
});
+14
View File
@@ -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,