Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo, useState } from "react";
import { Clock3, ListChecks } from "lucide-react";
import { AppHeader, ChecklistDashboard, DashboardError, DifferentialTestingPage, EmptyState, FILTERS, Filters, NewOvenPage, ProjectGroup, RunBurnPage, StreamingDiff, VisualParityPage } from "@components";
import { useDashboardData } from "@hooks";
import { currentSection, filterFromUrl, listHref, selectedBurnlist } from "@lib";
import { currentSection, filterFromUrl, selectedBurnlist } from "@lib";
import type { Filter } from "@lib";

export function App() {
Expand All @@ -23,11 +23,11 @@ export function App() {

return (
<div className="dashboard-app">
<AppHeader section={section} />
<AppHeader detail={progress} section={section} />
<main className="dashboard-main" data-layout={section === "differential-testing" || section === "performance-tracing" || section === "streaming-diff" || section === "visual-parity" || selected ? "full" : "index"} data-section={section}>
{section === "differential-testing" ? <DifferentialTestingPage /> : section === "performance-tracing" ? <DifferentialTestingPage ovenId="performance-tracing" /> : section === "streaming-diff" ? <StreamingDiff projects={projects} projectsLoading={loading} /> : section === "visual-parity" ? <VisualParityPage /> : section === "new-oven" ? <NewOvenPage /> : section === "run-burn" ? <RunBurnPage /> : selected ? (
error ? <DashboardError message={error} /> : loading && !progress ? <EmptyState title="Loading progress" detail="Reading the selected Burnlist." /> : progress ? (
<ChecklistDashboard backHref={listHref(filter)} data={progress} />
<ChecklistDashboard data={progress} />
) : <EmptyState title="Choose a Burnlist" detail="Select an item from the list to inspect its progress." icon={ListChecks} />
) : (
<section className="dashboard-index">
Expand Down
14 changes: 8 additions & 6 deletions dashboard/src/components/AppHeader/AppHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { Settings } from "lucide-react";
import { formatTime, type ChecklistProgressData } from "@lib";

const HEADER_LINKS = [
{ href: "/ovens/new", label: "New Oven", section: "new-oven" },
] as const;

export function AppHeader({ section }: { section: string }) {
export function AppHeader({ detail, section }: { detail: ChecklistProgressData | null; section: string }) {
const title = section === "differential-testing" ? "Differential Testing"
: section === "performance-tracing" ? "Performance Tracing"
: section === "streaming-diff" ? "Streaming Diff"
: section === "visual-parity" ? "Visual Parity" : detail?.title;
return (
<header className="dashboard-header">
<div className="dashboard-header-inner">
<a aria-label="Burnlist home" className="dashboard-brand" href="/">
<img alt="" className="dashboard-brand-logo" src="/favicon.svg" />
<span className="dashboard-brand-name">Burnlist</span>
</a>
{section === "differential-testing" && <div className="dashboard-oven-title">Differential Testing</div>}
{section === "performance-tracing" && <div className="dashboard-oven-title">Performance Tracing</div>}
{section === "streaming-diff" && <div className="dashboard-oven-title">Streaming Diff</div>}
{section === "visual-parity" && <div className="dashboard-oven-title">Visual Parity</div>}
{title && <div className="dashboard-oven-title">{title}</div>}
<nav aria-label="Primary navigation" className="dashboard-primary-nav">
{section !== "differential-testing" && section !== "performance-tracing" && section !== "streaming-diff" && section !== "visual-parity" && HEADER_LINKS.map((link, index) => (
{detail ? <time className="dashboard-detail-time" dateTime={detail.generatedAt}>{formatTime(detail.generatedAt)}</time> : !["differential-testing", "performance-tracing", "streaming-diff", "visual-parity"].includes(section) && HEADER_LINKS.map((link, index) => (
<span className="dashboard-primary-nav-item" key={link.href}>
{index > 0 && <span aria-hidden="true" className="dashboard-primary-nav-separator">·</span>}
<a aria-label={link.label} aria-current={section === link.section ? "page" : undefined} className="dashboard-primary-nav-link" href={link.href} title={link.label}>
Expand Down
247 changes: 247 additions & 0 deletions dashboard/src/components/ChecklistDashboard/ChecklistDashboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/* Checklist detail — Differential Testing structure with Checklist events. */

.dashboard-detail-time {
color: rgba(168, 168, 168, .76);
font: 400 14px/1.2 var(--dashboard-title-font);
white-space: nowrap;
}

body.checklist-detail-view .dashboard-oven-title {
max-width: calc(100% - 360px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.checklist-detail-shell .checklist-kpi-strip .driving-parity-kpi-ratio { font-variant-numeric: tabular-nums; }
.checklist-detail-shell .checklist-kpi-strip .driving-parity-kpi-progress-donut-segment { transition: stroke-dasharray 160ms ease; }

.checklist-detail-shell #burnlist-detail .checklist-progress-workspace {
width: 100%;
height: 340px;
min-height: 340px;
grid-template-columns: 30% minmax(0, 70%);
}

.checklist-detail-shell .checklist-progress-workspace .event-ledger-panel,
.checklist-detail-shell .checklist-progress-workspace .progress-panel {
height: 340px;
}

.checklist-detail-shell .checklist-progress-workspace .work-panel-body { min-height: 0; }
.checklist-detail-shell .checklist-progress-workspace .progress-panel .panel-title-row { flex: 0 0 auto; }
.checklist-detail-shell .checklist-progress-workspace .progress-panel .score {
display: flex;
height: auto;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
overflow: hidden;
}
.checklist-detail-shell .checklist-progress-workspace .progress-panel .chart-wrap {
height: auto;
min-height: 0;
flex: 1 1 auto;
}
.checklist-detail-shell .checklist-progress-workspace .progress-panel .chart { height: 100%; min-height: 0; }
.checklist-detail-shell .burn-chart-label { margin-left: auto; color: var(--text); font-size: 12px; }
.checklist-detail-view .chart .progress-line { stroke: var(--status-green); }
.checklist-detail-view .chart .progress-dot { fill: var(--status-green); }

.checklist-detail-shell .event-ledger-panel .checklist-log-list { width: 100%; }
.checklist-detail-shell .event-ledger-panel .work-panel-body { padding: 12px; }
.checklist-detail-shell .event-ledger-panel .log-table-cell.event { max-width: 8ch; }
.checklist-detail-shell .event-ledger-panel .log-table-cell.result,
.checklist-detail-shell .event-ledger-panel .log-table-cell.delta { color: rgba(97, 211, 148, .78); }
.event-ledger-empty { display: table-caption; padding-top: 12px; color: rgba(168, 168, 168, .58); text-align: left; }

.checklist-events-section {
width: 100%;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(168, 168, 168, .15);
}

.checklist-events-head {
display: flex;
align-items: center;
min-height: 28px;
margin-bottom: 8px;
}

.checklist-events-head h2 {
display: flex;
align-items: center;
margin: 0;
color: var(--muted);
font: 400 16px/1 var(--dashboard-title-font);
}

.checklist-events-head .field-list-count {
margin-left: .5ch;
color: rgba(168, 168, 168, .58);
font: 13px/1 var(--dashboard-font);
}

.event-card-list {
display: grid;
min-width: 760px;
overflow: visible;
gap: 0;
}

.event-card {
display: grid;
grid-template-columns: 30% minmax(0, 70%);
align-items: stretch;
min-height: 90px;
margin-bottom: 6px;
overflow: hidden;
border-radius: 8px;
background: var(--card);
color: rgba(210, 216, 224, .72);
contain-intrinsic-size: 90px;
content-visibility: auto;
}

.event-card.expanded {
min-height: 220px;
contain-intrinsic-size: 220px;
}

.event-card.expandable { cursor: pointer; }
.event-card:focus-visible { outline: 1px solid var(--blue); outline-offset: -1px; }
.event-card:last-child { margin-bottom: 0; }

.event-card-cell {
display: flex;
min-width: 0;
align-items: center;
padding: 10px 12px;
color: var(--muted);
line-height: 1.25;
}

.event-card-identity {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
align-items: flex-start;
overflow: hidden;
gap: 4px;
}

.event-card-id { color: rgba(97, 211, 148, .78); white-space: nowrap; }

.event-card-footer {
display: flex;
width: 100%;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}

.event-card-title {
display: block;
width: 100%;
color: rgba(232, 232, 232, .84);
font-size: 14px;
line-height: 1.3;
overflow-wrap: normal;
text-wrap: pretty;
word-break: normal;
white-space: normal;
}

.event-card-content {
position: relative;
min-width: 0;
height: 100%;
padding: 10px 34px 10px 12px;
overflow: hidden;
line-height: 1.4;
}

.event-card-outcome .event-card-field-label { margin-bottom: 4px; }
.event-card-outcome .event-card-field-value { font-size: 13px; line-height: 1.35; }
.event-card-expand {
position: absolute;
top: 10px;
right: 12px;
color: rgba(168, 168, 168, .45);
}

.event-card-fields {
margin-top: 10px;
border-top: 1px solid rgba(163, 172, 183, .08);
}

.event-card-field {
display: grid;
grid-template-columns: 10ch minmax(0, 1fr);
padding: 6px 0;
border-bottom: 1px solid rgba(163, 172, 183, .06);
line-height: 1.4;
gap: 10px;
}

.event-card-field:last-child { border-bottom: 0; }
.event-card-field-label,
.event-card-field-collapsible summary { color: rgba(168, 168, 168, .58); }
.event-card-field-value { min-width: 0; color: rgba(210, 216, 224, .68); overflow-wrap: anywhere; }
.event-card-field-value p { margin: 0; }
.event-card-field-value p + p { margin-top: 4px; }

.event-card-field-collapsible { display: block; }
.event-card-field-collapsible summary {
display: grid;
grid-template-columns: 10ch max-content minmax(0, 1fr);
align-items: center;
cursor: pointer;
list-style: none;
gap: 10px;
}
.event-card-field-collapsible summary::-webkit-details-marker { display: none; }
.event-card-field-collapsible summary::after { content: "+"; justify-self: end; color: rgba(168, 168, 168, .45); }
.event-card-field-collapsible[open] summary::after { content: "−"; }
.event-card-field-count { color: rgba(168, 168, 168, .36); font-size: 11px; }
.event-card-field-collapsible ul {
margin: 6px 0 2px 10ch;
padding-left: calc(10px + 1.1em);
color: rgba(210, 216, 224, .62);
overflow-wrap: anywhere;
}
.event-card-field-collapsible li + li { margin-top: 3px; }

.event-card-field-collapsible summary:hover,
.event-card-field-collapsible summary:focus-visible { color: rgba(232, 232, 232, .84); }
.event-card-field-collapsible summary:focus-visible { outline: 1px solid rgba(97, 211, 148, .55); outline-offset: 2px; }

.event-card-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-start;
color: rgba(168, 168, 168, .58);
font-variant-numeric: tabular-nums;
text-align: right;
white-space: nowrap;
}

.event-card:hover .event-card-cell { background: #1c1c1c; }

body.checklist-detail-view .dashboard-oven-title { max-width: calc(100% - 190px); font-size: 14px; }
.dashboard-detail-time { display: none; }
.checklist-detail-shell #burnlist-detail .checklist-progress-workspace { height: auto; min-height: 612px; grid-template-rows: 320px 282px; }
.checklist-detail-shell .checklist-progress-workspace .event-ledger-panel,
.checklist-detail-shell .checklist-progress-workspace .progress-panel { height: auto; }
.event-card-list { min-width: 0; }
.event-card {
grid-template-columns: 1fr;
grid-template-rows: minmax(90px, auto) auto;
}
.event-card.expanded { min-height: 260px; }
.event-card-cell { padding: 8px 10px; }
.event-card-content { grid-column: 1 / -1; grid-row: 2; min-height: 90px; }
.event-card-field { grid-template-columns: 1fr; gap: 3px; }
.event-card-field-collapsible summary { grid-template-columns: max-content max-content minmax(0, 1fr); }
.event-card-field-collapsible ul { margin-left: 0; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { build } from "esbuild";

const componentPath = new URL("./ChecklistDashboard.tsx", import.meta.url).pathname;
const libPath = new URL("../../lib", import.meta.url).pathname;

test("checklist detail renders the split progress surface and event card list", async () => {
const outputDir = await mkdtemp(join(process.cwd(), ".checklist-dashboard-test-"));
try {
const outputPath = join(outputDir, "ChecklistDashboard.mjs");
await build({
entryPoints: [componentPath], bundle: true, format: "esm", outfile: outputPath, platform: "node",
alias: { "@lib": libPath }, jsx: "automatic", packages: "external", target: "node18",
});
const { ChecklistDashboard, checklistEventDetailFields } = await import(`${new URL(`file://${outputPath}`).href}?test=${Date.now()}`);
const data = {
generatedAt: "2026-07-15T12:00:00Z", repoKey: "fixture", repo: "fixture", planLabel: "active.md", title: "Fixture Burnlist",
total: 2, done: 2, remaining: 0, percent: 100, warnings: [], active: [],
completed: [
{ id: "B1", title: "First event", completedAt: "2026-07-15T11:40:00Z", detail: "First proof." },
{ id: "B2", title: "Second event", completedAt: "2026-07-15T11:50:00Z", detail: "Completed: 2026-07-15T11:50:00Z\nChanged:\n- src/second.mjs\nProof:\n- node --test second.test.mjs\nOutcome:\n- Second proof.\nFollow-up:\n- None." },
],
history: [
{ time: "2026-07-15T11:40:00Z", done: 1, remaining: 1, total: 2, percent: 50 },
{ time: "2026-07-15T11:50:00Z", done: 2, remaining: 0, total: 2, percent: 100 },
],
};
const markup = renderToStaticMarkup(createElement(ChecklistDashboard, { data }));

assert.match(markup, /aria-label="Burnlist progress KPIs"/u);
assert.match(markup, /class="driving-parity-kpi-gauge driving-parity-kpi-progress-donut" viewBox="0 0 58 58"/u);
assert.match(markup, /class="driving-parity-kpi-progress-donut-segment"[^>]+stroke-dasharray="100\.000 0\.000"/u);
assert.match(markup, /aria-label="Remaining items over time"/u);
assert.doesNotMatch(markup, /aria-label="Burnlist progress chart view"/u);
assert.match(markup, /<span>Age<\/span><span>Event<\/span><span>Result<\/span><span>Delta<\/span><span>Done<\/span>/u);
assert.match(markup, /class="event-card-list"/u);
assert.equal((markup.match(/data-event-card="true"/gu) ?? []).length, 2);
assert.equal(markup.indexOf("Second event") < markup.indexOf("First event"), true);
assert.match(markup, /First proof\./u);
assert.match(markup, /Second proof\./u);
assert.equal((markup.match(/class="event-card-field-label">Outcome/gu) ?? []).length, 2);
assert.match(markup, /aria-expanded="false"/u);
assert.doesNotMatch(markup, /src\/second\.mjs/u);
assert.doesNotMatch(markup, /node --test second\.test\.mjs/u);
assert.doesNotMatch(markup, /Follow-up/u);
assert.deepEqual(checklistEventDetailFields(data.completed[1].detail), [
{ label: "Completed", values: ["2026-07-15T11:50:00Z"] },
{ label: "Changed", values: ["src/second.mjs"] },
{ label: "Proof", values: ["node --test second.test.mjs"] },
{ label: "Outcome", values: ["Second proof."] },
{ label: "Follow-up", values: ["None."] },
]);
assert.doesNotMatch(markup, /Completed: 2026/u);
assert.doesNotMatch(markup, />DONE</u);
assert.doesNotMatch(markup, /<button[^>]*>Changes<\/button>/u);
assert.doesNotMatch(markup, /Burnlist detail view/u);
assert.doesNotMatch(markup, /Repo Graph/u);
} finally {
await rm(outputDir, { force: true, recursive: true });
}
});
Loading
Loading