604776052d9e96ab4b6876fa462c9715a4f30a76 diff --git a/public/style.css b/public/style.css index 63119bd3212855595862f2c2ff7cc375f3029770..c5220ab9fee147104a0c0b58813d5367c7320baa 100644 --- a/public/style.css +++ b/public/style.css @@ -183,3 +183,158 @@ main.md strong { font-weight: 600; } background: var(--accent); color: var(--bg); } + +/* --- reports / dashboard ---------------------------------------------- */ + +.report-mockup-banner { + background: var(--code-bg); + border: 1px dashed var(--border); + padding: 0.7rem 1rem; + border-radius: 4px; + font-size: 0.85rem; + color: var(--muted); + margin: 0 0 2rem; + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; +} +.report-mockup-banner a { + color: var(--muted); + text-decoration: underline; + text-underline-offset: 2px; +} +.report-mockup-banner a:hover { color: var(--fg); } + +.report-tiles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin: 1.5rem 0 2.5rem; +} + +.report-tile { + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.2rem 1.2rem 1rem; + background: var(--code-bg); +} + +.report-tile-name { + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; + font-size: 0.8rem; + text-transform: lowercase; + letter-spacing: 0.04em; + color: var(--muted); + margin: 0 0 0.6rem; +} +.report-tile-name a { + color: inherit; + text-decoration: none; +} +.report-tile-name a:hover { color: var(--fg); } + +.report-tile-score { + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; + font-size: 2.2rem; + font-weight: 600; + letter-spacing: -0.02em; + margin: 0; + line-height: 1.1; +} +.report-tile-score-suffix { + font-size: 0.95rem; + color: var(--muted); + font-weight: 400; +} + +.report-tile-trend { + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; + font-size: 0.9rem; + margin: 0.4rem 0 0.6rem; +} +.report-tile-trend.up { color: var(--green); } +.report-tile-trend.down { color: var(--red); } +.report-tile-trend.flat { color: var(--muted); } + +.report-tile-volume { + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; + font-size: 0.78rem; + color: var(--muted); + margin: 0 0 0.8rem; +} + +.report-tile-issue { + font-size: 0.82rem; + color: var(--muted); + border-top: 1px solid var(--border); + padding-top: 0.7rem; +} +.report-tile-issue strong { + color: var(--fg); + font-weight: 500; +} + +.report-bars { + margin: 1rem 0 2rem; +} +.report-bar-row { + display: grid; + grid-template-columns: 180px 1fr 50px; + align-items: center; + gap: 0.8rem; + margin: 0.5rem 0; + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; + font-size: 0.85rem; +} +.report-bar-label { color: var(--muted); } +.report-bar-track { + height: 10px; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 2px; + overflow: hidden; +} +.report-bar-fill { + display: block; + height: 100%; + background: var(--accent); +} +.report-bar-fill.red { background: var(--red); } +.report-bar-fill.green { background: var(--green); } +.report-bar-fill.muted { background: var(--muted); } +.report-bar-pct { text-align: right; color: var(--fg); } + +.report-streak { + display: inline-block; + padding: 0.4rem 0.8rem; + border: 1px solid var(--border); + border-radius: 4px; + font-family: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; + font-size: 0.85rem; + color: var(--muted); + margin: 0 0 1.5rem; +} +.report-streak-num { + font-weight: 600; + color: var(--fg); +} +.report-streak.broken { + color: var(--red); + border-color: var(--red); +} +.report-streak.broken .report-streak-num { color: var(--red); } +.report-streak.long { + color: var(--green); + border-color: var(--green); +} +.report-streak.long .report-streak-num { color: var(--green); } + +.report-sparkline { + width: 100%; + height: 80px; + display: block; + margin: 0.5rem 0 1.2rem; +} + +@media (max-width: 600px) { + .report-tiles { grid-template-columns: 1fr; } + .report-bar-row { grid-template-columns: 130px 1fr 50px; } +} diff --git a/src/reports.ts b/src/reports.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c11445c14b0bf3e154074e040adac53f82412b1 --- /dev/null +++ b/src/reports.ts @@ -0,0 +1,298 @@ +// Mockup reporting layer for tdd.md. +// +// All data here is FAKE — wired up only so the management/exec view and +// per-agent drill-down can be designed in the browser before the real +// project-tracking pipeline (block 1) exists. +// +// Real reporting needs: +// - GitHub App / webhook ingest of pushes on tracked branches +// - per-commit judging without hidden tests (red-fails / green-passes / +// no-test-deletion / no-regression) +// - agent attribution (commit footer convention or wrapper-driven) +// Once that exists, the same generators in this file accept real data. + +interface RecentFlagged { + date: string; + repo: string; + sha: string; + phase: "red" | "green" | "refactor"; + failure: string; + pts: number; +} + +interface FailureSlice { + label: string; + pct: number; + tone: "red" | "green" | "muted" | "accent"; +} + +export interface AgentReport { + slug: "claude-code" | "cursor" | "aider"; + name: string; + score: number; + delta: number; + commits: number; + phaseCoveragePct: number; + streak: number; + streakBroken: boolean; + topIssueLabel: string; + topIssuePct: number; + failureMix: FailureSlice[]; + trend: number[]; + recent: RecentFlagged[]; +} + +export const DEMO_PERIOD = "2026-01-01 → 2026-03-31"; +export const DEMO_ORG = "acme-corp"; +export const DEMO_REPOS = 4; + +export const DEMO_REPORTS: AgentReport[] = [ + { + slug: "claude-code", + name: "Claude Code", + score: 78, + delta: +6, + commits: 612, + phaseCoveragePct: 92, + streak: 47, + streakBroken: false, + topIssueLabel: "red-did-not-fail", + topIssuePct: 8, + failureMix: [ + { label: "clean cycles", pct: 84, tone: "green" }, + { label: "red-did-not-fail", pct: 8, tone: "red" }, + { label: "broken refactor", pct: 4, tone: "red" }, + { label: "test-deleted", pct: 2, tone: "red" }, + { label: "no phase tag", pct: 2, tone: "muted" }, + ], + trend: [72, 73, 71, 74, 72, 75, 73, 75, 77, 76, 75, 76, 78, 77, 79, 78, 77, 79, 80, 78, 79, 80, 79, 81, 80, 82, 81, 80, 79, 78], + recent: [ + { date: "2026-03-29", repo: "api-gateway", sha: "f1c8b3a", phase: "red", failure: "red-did-not-fail", pts: -5 }, + { date: "2026-03-24", repo: "billing-service", sha: "9d2e1f4", phase: "refactor", failure: "broken refactor", pts: -5 }, + { date: "2026-03-18", repo: "data-pipeline", sha: "62a9cb7", phase: "green", failure: "no phase tag (parent)", pts: 0 }, + ], + }, + { + slug: "cursor", + name: "Cursor", + score: 54, + delta: -15, + commits: 489, + phaseCoveragePct: 71, + streak: 3, + streakBroken: true, + topIssueLabel: "test-deleted in refactor", + topIssuePct: 14, + failureMix: [ + { label: "clean cycles", pct: 64, tone: "green" }, + { label: "test-deleted", pct: 14, tone: "red" }, + { label: "red-did-not-fail", pct: 9, tone: "red" }, + { label: "broken refactor", pct: 7, tone: "red" }, + { label: "no phase tag", pct: 6, tone: "muted" }, + ], + trend: [69, 70, 71, 72, 70, 71, 72, 73, 72, 71, 72, 70, 68, 65, 60, 55, 50, 52, 54, 53, 56, 54, 52, 55, 53, 54, 56, 55, 54, 54], + recent: [ + { date: "2026-03-28", repo: "api-gateway", sha: "a1b2c3d", phase: "refactor", failure: "test-deleted", pts: -20 }, + { date: "2026-03-26", repo: "api-gateway", sha: "4e5f6a7", phase: "green", failure: "broken refactor", pts: -5 }, + { date: "2026-03-23", repo: "billing-service", sha: "8b9c0d1", phase: "red", failure: "red-did-not-fail", pts: -5 }, + { date: "2026-03-21", repo: "api-gateway", sha: "2e3f4a5", phase: "refactor", failure: "test-deleted", pts: -20 }, + { date: "2026-03-19", repo: "data-pipeline", sha: "6b7c8d9", phase: "refactor", failure: "broken refactor", pts: -5 }, + ], + }, + { + slug: "aider", + name: "Aider", + score: 89, + delta: +2, + commits: 146, + phaseCoveragePct: 96, + streak: 89, + streakBroken: false, + topIssueLabel: "broken refactor", + topIssuePct: 3, + failureMix: [ + { label: "clean cycles", pct: 94, tone: "green" }, + { label: "broken refactor", pct: 3, tone: "red" }, + { label: "red-did-not-fail", pct: 2, tone: "red" }, + { label: "no phase tag", pct: 1, tone: "muted" }, + ], + trend: [87, 88, 89, 88, 87, 89, 90, 89, 88, 89, 90, 88, 89, 90, 91, 89, 88, 89, 90, 89, 90, 91, 89, 88, 89, 90, 89, 90, 89, 89], + recent: [ + { date: "2026-03-27", repo: "data-pipeline", sha: "3a4b5c6", phase: "refactor", failure: "broken refactor", pts: -5 }, + { date: "2026-03-15", repo: "billing-service", sha: "7d8e9f0", phase: "red", failure: "red-did-not-fail", pts: -5 }, + ], + }, +]; + +const escape = (s: string): string => + s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); + +const trendArrow = (delta: number): { glyph: string; cls: string } => + delta > 0 ? { glyph: "↑", cls: "up" } : delta < 0 ? { glyph: "↓", cls: "down" } : { glyph: "→", cls: "flat" }; + +const sparkline = (values: number[], height = 60, width = 320): string => { + if (values.length === 0) return ""; + const min = Math.min(...values); + const max = Math.max(...values); + const range = Math.max(1, max - min); + const stepX = width / Math.max(1, values.length - 1); + const pad = 6; + const innerH = height - pad * 2; + const points = values + .map((v, i) => { + const x = (i * stepX).toFixed(1); + const y = (pad + innerH - ((v - min) / range) * innerH).toFixed(1); + return `${x},${y}`; + }) + .join(" "); + return ``; +}; + +const tile = (a: AgentReport): string => { + const arr = trendArrow(a.delta); + const deltaStr = a.delta > 0 ? `+${a.delta}` : `${a.delta}`; + return `
${a.score} / 100
+${arr.glyph} ${escape(deltaStr)}
+${a.commits.toLocaleString()} commits
+