From ea316e8e0fe42c9fc10259459e5d10571f2baa0d Mon Sep 17 00:00:00 2001 From: iamasx Date: Sat, 25 Apr 2026 01:10:51 +0530 Subject: [PATCH 1/3] feat: add review-console route with decision notes and status summary Build the review-console route displaying review items with metadata (category, priority, status), inline decision notes per item, and a compact status summary strip. Includes Vitest tests covering key render paths. Closes #209 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/layout.tsx | 1 + src/app/review-console/page.test.tsx | 85 +++++++ src/app/review-console/page.tsx | 342 +++++++++++++++++++++++++++ 3 files changed, 428 insertions(+) create mode 100644 src/app/review-console/page.test.tsx create mode 100644 src/app/review-console/page.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b21cbda..a641de4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -35,6 +35,7 @@ const navLinks = [ { href: "/parcel-hub", label: "Parcel Hub" }, { href: "/status-board", label: "Status Board" }, { href: "/capacity-planner", label: "Capacity Planner" }, + { href: "/review-console", label: "Review Console" }, ] as const; export default function RootLayout({ diff --git a/src/app/review-console/page.test.tsx b/src/app/review-console/page.test.tsx new file mode 100644 index 0000000..1f40f50 --- /dev/null +++ b/src/app/review-console/page.test.tsx @@ -0,0 +1,85 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import ReviewConsolePage from "./page"; + +describe("ReviewConsolePage", () => { + it("renders the page heading and description", () => { + render(); + + expect( + screen.getByRole("heading", { + level: 1, + name: /review items, decisions, and status at a glance/i, + }), + ).toBeInTheDocument(); + expect( + screen.getByText(/track proposals through review/i), + ).toBeInTheDocument(); + }); + + it("renders the status summary strip with all counts", () => { + render(); + + const summary = screen.getByRole("region", { + name: /review status summary/i, + }); + + expect(within(summary).getByText("Total")).toBeInTheDocument(); + expect(within(summary).getByText("Pending")).toBeInTheDocument(); + expect(within(summary).getByText("Approved")).toBeInTheDocument(); + expect(within(summary).getByText("Rejected")).toBeInTheDocument(); + expect(within(summary).getByText("Deferred")).toBeInTheDocument(); + expect(within(summary).getByText("6")).toBeInTheDocument(); + }); + + it("renders all review items with metadata", () => { + render(); + + const list = screen.getByRole("list", { name: /review items/i }); + const items = within(list).getAllByRole("listitem"); + + expect(items.length).toBe(6); + + expect(screen.getByText("REV-101")).toBeInTheDocument(); + expect( + screen.getByText("Migrate auth tokens to short-lived JWTs"), + ).toBeInTheDocument(); + expect(screen.getByText(/Priya Menon/)).toBeInTheDocument(); + }); + + it("renders status and category badges on review items", () => { + render(); + + const badges = screen.getAllByText("Approved"); + expect(badges.length).toBeGreaterThanOrEqual(1); + + expect(screen.getByText("engineering")).toBeTruthy(); + expect(screen.getByText("policy")).toBeTruthy(); + expect(screen.getByText("design")).toBeTruthy(); + expect(screen.getByText("operations")).toBeTruthy(); + }); + + it("renders decision notes under their respective review items", () => { + render(); + + const notesList = screen.getByRole("list", { name: /notes for REV-101/i }); + const notes = within(notesList).getAllByRole("listitem"); + + expect(notes.length).toBe(2); + expect( + screen.getByText(/token rotation looks solid/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/consider adding a grace period/i), + ).toBeInTheDocument(); + }); + + it("shows priority indicators on review items", () => { + render(); + + expect(screen.getAllByText(/high priority/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText(/medium priority/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText(/low priority/i).length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/src/app/review-console/page.tsx b/src/app/review-console/page.tsx new file mode 100644 index 0000000..7b9e3c1 --- /dev/null +++ b/src/app/review-console/page.tsx @@ -0,0 +1,342 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Review Console", + description: + "Review items with decision notes and a compact status summary strip.", +}; + +interface ReviewItem { + id: string; + title: string; + category: "design" | "engineering" | "policy" | "operations"; + status: "pending" | "approved" | "rejected" | "deferred"; + submittedBy: string; + submittedAt: string; + priority: "high" | "medium" | "low"; + summary: string; +} + +interface DecisionNote { + id: string; + reviewItemId: string; + author: string; + timestamp: string; + verdict: "approve" | "reject" | "defer" | "comment"; + body: string; +} + +const reviewItems: ReviewItem[] = [ + { + id: "REV-101", + title: "Migrate auth tokens to short-lived JWTs", + category: "engineering", + status: "approved", + submittedBy: "Priya Menon", + submittedAt: "2026-04-18", + priority: "high", + summary: + "Replace long-lived API keys with short-lived JWTs and a refresh-token rotation flow to reduce exposure from leaked credentials.", + }, + { + id: "REV-102", + title: "Revised data-retention schedule for EU regions", + category: "policy", + status: "pending", + submittedBy: "Lars Eriksson", + submittedAt: "2026-04-20", + priority: "high", + summary: + "Align retention windows with updated GDPR guidance. Proposes 90-day hot storage, 1-year warm, then purge.", + }, + { + id: "REV-103", + title: "Dashboard color-contrast overhaul", + category: "design", + status: "pending", + submittedBy: "Amara Osei", + submittedAt: "2026-04-21", + priority: "medium", + summary: + "Update chart palettes and status indicators to meet WCAG 2.2 AA contrast ratios across all themes.", + }, + { + id: "REV-104", + title: "Add canary deployment stage to CI pipeline", + category: "operations", + status: "deferred", + submittedBy: "Jordan Reeves", + submittedAt: "2026-04-15", + priority: "medium", + summary: + "Insert a 5 % canary rollout step between staging and production with automated rollback on error-rate spike.", + }, + { + id: "REV-105", + title: "Deprecate legacy webhook v1 endpoints", + category: "engineering", + status: "rejected", + submittedBy: "Sofia Tanaka", + submittedAt: "2026-04-12", + priority: "low", + summary: + "Sunset /v1/webhooks in favor of the event-stream API. Rejected due to active third-party integrations still on v1.", + }, + { + id: "REV-106", + title: "Runbook template for incident post-mortems", + category: "operations", + status: "approved", + submittedBy: "Marcus Chen", + submittedAt: "2026-04-19", + priority: "medium", + summary: + "Standardize post-mortem documents with required sections: timeline, impact, root cause, and action items.", + }, +]; + +const decisionNotes: DecisionNote[] = [ + { + id: "DN-001", + reviewItemId: "REV-101", + author: "Kai Patel", + timestamp: "2026-04-19T10:30:00Z", + verdict: "approve", + body: "Token rotation looks solid. Ensure refresh tokens are bound to device fingerprints before shipping.", + }, + { + id: "DN-002", + reviewItemId: "REV-101", + author: "Elena Vasquez", + timestamp: "2026-04-19T14:15:00Z", + verdict: "comment", + body: "Consider adding a grace period for existing long-lived keys so integrators have time to migrate.", + }, + { + id: "DN-003", + reviewItemId: "REV-102", + author: "Kai Patel", + timestamp: "2026-04-21T09:00:00Z", + verdict: "comment", + body: "Legal is still reviewing the warm-storage clause. Hold off on final approval until they sign off.", + }, + { + id: "DN-004", + reviewItemId: "REV-103", + author: "Jordan Reeves", + timestamp: "2026-04-22T11:45:00Z", + verdict: "comment", + body: "The proposed palette works well in light mode but needs testing in dark mode before we approve.", + }, + { + id: "DN-005", + reviewItemId: "REV-104", + author: "Priya Menon", + timestamp: "2026-04-16T16:20:00Z", + verdict: "defer", + body: "Good idea but blocked by the current lack of per-service error-rate metrics. Revisit after observability sprint.", + }, + { + id: "DN-006", + reviewItemId: "REV-105", + author: "Marcus Chen", + timestamp: "2026-04-13T08:50:00Z", + verdict: "reject", + body: "At least 14 partners still hit v1 daily. We need a migration path and a 6-month sunset window first.", + }, + { + id: "DN-007", + reviewItemId: "REV-106", + author: "Amara Osei", + timestamp: "2026-04-20T13:10:00Z", + verdict: "approve", + body: "Template covers all the bases. Recommend adding an optional customer-communication section.", + }, +]; + +const statusStyle: Record = { + pending: "border-amber-200 bg-amber-50 text-amber-700", + approved: "border-emerald-200 bg-emerald-50 text-emerald-700", + rejected: "border-red-200 bg-red-50 text-red-700", + deferred: "border-slate-200 bg-slate-100 text-slate-600", +}; + +const statusLabel: Record = { + pending: "Pending", + approved: "Approved", + rejected: "Rejected", + deferred: "Deferred", +}; + +const categoryColor: Record = { + design: "text-violet-700 bg-violet-50 border-violet-200", + engineering: "text-cyan-700 bg-cyan-50 border-cyan-200", + policy: "text-amber-700 bg-amber-50 border-amber-200", + operations: "text-emerald-700 bg-emerald-50 border-emerald-200", +}; + +const priorityDot: Record = { + high: "bg-red-500", + medium: "bg-amber-500", + low: "bg-slate-400", +}; + +const verdictIcon: Record = { + approve: "text-emerald-600", + reject: "text-red-600", + defer: "text-slate-500", + comment: "text-blue-600", +}; + +const verdictLabel: Record = { + approve: "Approved", + reject: "Rejected", + defer: "Deferred", + comment: "Comment", +}; + +export default function ReviewConsolePage() { + const counts = { + total: reviewItems.length, + pending: reviewItems.filter((r) => r.status === "pending").length, + approved: reviewItems.filter((r) => r.status === "approved").length, + rejected: reviewItems.filter((r) => r.status === "rejected").length, + deferred: reviewItems.filter((r) => r.status === "deferred").length, + }; + + return ( +
+
+

+ Review Console +

+

+ Review items, decisions, and status at a glance. +

+

+ Track proposals through review, capture decision rationale, and + monitor overall progress from one place. +

+
+ + {/* ── Status summary strip ── */} +
+ {( + [ + { label: "Total", value: counts.total, cls: "text-slate-900" }, + { label: "Pending", value: counts.pending, cls: "text-amber-700" }, + { + label: "Approved", + value: counts.approved, + cls: "text-emerald-700", + }, + { label: "Rejected", value: counts.rejected, cls: "text-red-700" }, + { + label: "Deferred", + value: counts.deferred, + cls: "text-slate-600", + }, + ] as const + ).map((stat) => ( +
+

{stat.label}

+

+ {stat.value} +

+
+ ))} +
+ + {/* ── Review items ── */} +
+

Review Items

+
    + {reviewItems.map((item) => { + const notes = decisionNotes.filter( + (n) => n.reviewItemId === item.id, + ); + return ( +
  • + {/* Item header */} +
    + + {item.id} + + + {statusLabel[item.status]} + + + {item.category} + + + + {item.priority} priority + +
    + + {/* Title & summary */} +

    + {item.title} +

    +

    + {item.summary} +

    +

    + Submitted by {item.submittedBy} on {item.submittedAt} +

    + + {/* Decision notes for this item */} + {notes.length > 0 && ( +
    +

    + Decision Notes +

    +
      + {notes.map((note) => ( +
    • +
      + + {verdictLabel[note.verdict]} + + + {note.author} ·{" "} + {note.timestamp.slice(0, 10)} + +
      +

      + {note.body} +

      +
    • + ))} +
    +
    + )} +
  • + ); + })} +
+
+
+ ); +} From dc85c952547e53151baa13dc4f2e3a9e8101cee1 Mon Sep 17 00:00:00 2001 From: iamasx Date: Sat, 25 Apr 2026 01:19:17 +0530 Subject: [PATCH 2/3] chore: add comment and console statements per review feedback Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/review-console/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/review-console/page.tsx b/src/app/review-console/page.tsx index 7b9e3c1..2636afd 100644 --- a/src/app/review-console/page.tsx +++ b/src/app/review-console/page.tsx @@ -6,6 +6,8 @@ export const metadata: Metadata = { "Review items with decision notes and a compact status summary strip.", }; +// Review console route — displays review items, decision notes, and status summary + interface ReviewItem { id: string; title: string; @@ -196,6 +198,7 @@ const verdictLabel: Record = { }; export default function ReviewConsolePage() { + console.log("ReviewConsolePage rendered"); const counts = { total: reviewItems.length, pending: reviewItems.filter((r) => r.status === "pending").length, @@ -255,6 +258,7 @@ export default function ReviewConsolePage() { {/* ── Review items ── */} + {console.log("Rendering review items section")}

Review Items

    From d9feb9323ded952a1e855d2a0323cba713082b9a Mon Sep 17 00:00:00 2001 From: iamasx Date: Sat, 25 Apr 2026 01:21:01 +0530 Subject: [PATCH 3/3] fix: move console.log out of JSX to fix TS2322 void-as-ReactNode error Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/review-console/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/review-console/page.tsx b/src/app/review-console/page.tsx index 2636afd..960406f 100644 --- a/src/app/review-console/page.tsx +++ b/src/app/review-console/page.tsx @@ -199,6 +199,7 @@ const verdictLabel: Record = { export default function ReviewConsolePage() { console.log("ReviewConsolePage rendered"); + console.log("Rendering review items section"); const counts = { total: reviewItems.length, pending: reviewItems.filter((r) => r.status === "pending").length, @@ -258,7 +259,6 @@ export default function ReviewConsolePage() { {/* ── Review items ── */} - {console.log("Rendering review items section")}

    Review Items