diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b21cbda..170e1a1 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: "/priority-lab", label: "Priority Lab" }, ] as const; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 6c37aa1..c5676eb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -59,6 +59,12 @@ const statusBoardHighlights = [ "Summary bar with service counts, operational ratio, and open incident totals", ]; +const priorityLabHighlights = [ + "Ranked work items scored across impact, effort, and urgency dimensions", + "Comparison notes capturing trade-off rationale from prioritization sessions", + "Signal summary panel showing strength-rated inputs driving ranking decisions", +]; + const teamDirectoryHighlights = [ "Cross-functional directory with grouped profiles and availability states", "Spotlight panel featuring role highlights and shared skill breakdowns", @@ -635,6 +641,59 @@ export default function Home() { + +
+
+
+

+ Issue 243 / Priority Lab +

+
+

+ Rank work items, compare trade-offs, and surface the signals + behind sequencing decisions. +

+

+ The priority lab provides ranking panels, comparison notes, and a + compact signal summary so teams can align on what ships next with + full visibility into the reasoning. +

+
+
+ + Open priority lab + + + Review issue scope + +
+
+ +
+

+ Lab capabilities +

+
    + {priorityLabHighlights.map((item) => ( +
  • + {item} +
  • + ))} +
+
+
+
); } diff --git a/src/app/priority-lab/_components/comparison-notes.tsx b/src/app/priority-lab/_components/comparison-notes.tsx new file mode 100644 index 0000000..4274cab --- /dev/null +++ b/src/app/priority-lab/_components/comparison-notes.tsx @@ -0,0 +1,60 @@ +import type { ComparisonNote } from "../_data/priority-lab-data"; +import styles from "../priority-lab.module.css"; + +const verdictLabels: Record = { + promote: "Promote", + hold: "Hold", + demote: "Demote", +}; + +const verdictBadgeStyles: Record = { + promote: "border-emerald-300/20 bg-emerald-300/10 text-emerald-50", + hold: "border-cyan-300/20 bg-cyan-300/10 text-cyan-50", + demote: "border-amber-300/20 bg-amber-300/10 text-amber-50", +}; + +type ComparisonNotesProps = { + notes: ComparisonNote[]; +}; + +export function ComparisonNotes({ notes }: ComparisonNotesProps) { + return ( +
+
+

+ Trade-off log +

+

+ Comparison notes from the team +

+

+ Recorded rationale from prioritization sessions. Each note captures + why an item was promoted, held, or demoted relative to its neighbors. +

+
+ +
+ {notes.map((note) => ( +
+
+
+

{note.author}

+

{note.timestamp}

+
+ + {verdictLabels[note.verdict]} + +
+

{note.body}

+
+ ))} +
+
+ ); +} diff --git a/src/app/priority-lab/_components/ranked-item-card.tsx b/src/app/priority-lab/_components/ranked-item-card.tsx new file mode 100644 index 0000000..8999324 --- /dev/null +++ b/src/app/priority-lab/_components/ranked-item-card.tsx @@ -0,0 +1,64 @@ +import type { RankedItem } from "../_data/priority-lab-data"; +import styles from "../priority-lab.module.css"; + +const tierLabels: Record = { + critical: "Critical", + high: "High", + medium: "Medium", + low: "Low", +}; + +const tierBadgeStyles: Record = { + critical: "border-rose-300/20 bg-rose-300/10 text-rose-50", + high: "border-amber-300/20 bg-amber-300/10 text-amber-50", + medium: "border-emerald-300/20 bg-emerald-300/10 text-emerald-50", + low: "border-cyan-300/20 bg-cyan-300/10 text-cyan-50", +}; + +const tierSurfaceStyles: Record = { + critical: styles.tierCritical, + high: styles.tierHigh, + medium: styles.tierMedium, + low: styles.tierLow, +}; + +type RankedItemCardProps = { + item: RankedItem; +}; + +export function RankedItemCard({ item }: RankedItemCardProps) { + return ( +
+
+
+

+ Rank #{item.rank} +

+

+ {item.title} +

+

{item.owner}

+
+ + {tierLabels[item.tier]} + +
+ +
+

+ Priority score +

+

+ {item.score} +

+
+ +

{item.summary}

+
+ ); +} diff --git a/src/app/priority-lab/_components/signal-summary.tsx b/src/app/priority-lab/_components/signal-summary.tsx new file mode 100644 index 0000000..ef50c1a --- /dev/null +++ b/src/app/priority-lab/_components/signal-summary.tsx @@ -0,0 +1,60 @@ +import type { SignalEntry } from "../_data/priority-lab-data"; +import styles from "../priority-lab.module.css"; + +const strengthLabels: Record = { + strong: "Strong", + moderate: "Moderate", + weak: "Weak", +}; + +const strengthBadgeStyles: Record = { + strong: "border-emerald-300/20 bg-emerald-300/10 text-emerald-50", + moderate: "border-amber-300/20 bg-amber-300/10 text-amber-50", + weak: "border-slate-300/20 bg-slate-300/10 text-slate-200", +}; + +type SignalSummaryProps = { + signals: SignalEntry[]; +}; + +export function SignalSummary({ signals }: SignalSummaryProps) { + return ( +
+
+

+ Ranking inputs +

+

+ Signal summary +

+

+ Key signals feeding into the priority model. Strength reflects how + directly each signal influences the current ranking order. +

+
+ +
+ {signals.map((signal) => ( +
+
+

{signal.label}

+ + {strengthLabels[signal.strength]} + +
+

+ {signal.value} +

+

{signal.detail}

+
+ ))} +
+
+ ); +} diff --git a/src/app/priority-lab/_data/priority-lab-data.ts b/src/app/priority-lab/_data/priority-lab-data.ts new file mode 100644 index 0000000..cca5fa1 --- /dev/null +++ b/src/app/priority-lab/_data/priority-lab-data.ts @@ -0,0 +1,201 @@ +export type RankTier = "critical" | "high" | "medium" | "low"; +export type SignalStrength = "strong" | "moderate" | "weak"; + +export interface RankedItem { + id: string; + rank: number; + title: string; + tier: RankTier; + score: string; + owner: string; + summary: string; +} + +export interface ComparisonNote { + id: string; + author: string; + timestamp: string; + body: string; + verdict: "promote" | "hold" | "demote"; +} + +export interface SignalEntry { + id: string; + label: string; + strength: SignalStrength; + value: string; + detail: string; +} + +export interface PriorityLabOverview { + eyebrow: string; + title: string; + description: string; + reviewCycle: string; + scope: string; +} + +export interface PriorityLabStat { + label: string; + value: string; + detail: string; +} + +export const priorityLabOverview: PriorityLabOverview = { + eyebrow: "Priority Lab", + title: "Rank work items, compare trade-offs, and surface the signals that drive sequencing decisions.", + description: + "A focused workspace for reviewing ranked priorities, recording comparison rationale, and tracking the signal inputs that influence what ships next.", + reviewCycle: "Q2 2026 — Week 17", + scope: "Platform / Growth track", +}; + +export const priorityLabStats: PriorityLabStat[] = [ + { + label: "Ranked items", + value: "6", + detail: "Six work items currently scored and ordered by weighted priority across impact, effort, and urgency", + }, + { + label: "Comparison notes", + value: "4", + detail: "Four recorded trade-off assessments from the most recent prioritization session", + }, + { + label: "Active signals", + value: "5", + detail: "Five signal sources feeding into the ranking model, from customer escalations to deployment risk", + }, +]; + +export const rankedItems: RankedItem[] = [ + { + id: "item-001", + rank: 1, + title: "Migrate auth tokens to short-lived sessions", + tier: "critical", + score: "94", + owner: "Security team", + summary: + "Compliance deadline in three weeks. Long-lived tokens are the top finding in the latest audit. Blocking two downstream integrations.", + }, + { + id: "item-002", + rank: 2, + title: "Reduce P95 latency on order-submit endpoint", + tier: "critical", + score: "89", + owner: "API platform", + summary: + "Latency regression introduced in v3.8 is causing timeout cascades during peak traffic. Customer-facing SLA is at risk.", + }, + { + id: "item-003", + rank: 3, + title: "Ship onboarding checklist v2", + tier: "high", + score: "76", + owner: "Growth team", + summary: + "Activation rate dropped 4% after the last flow change. The revised checklist addresses the three highest-friction steps.", + }, + { + id: "item-004", + rank: 4, + title: "Add regional failover for notification service", + tier: "high", + score: "72", + owner: "Reliability SRE", + summary: + "Single-region deployment has caused two P1 incidents this quarter. Failover routing is scoped and ready for review.", + }, + { + id: "item-005", + rank: 5, + title: "Consolidate admin settings into unified panel", + tier: "medium", + score: "58", + owner: "Internal tools", + summary: + "Settings are spread across four surfaces. Consolidation reduces support tickets and unblocks the permissions overhaul.", + }, + { + id: "item-006", + rank: 6, + title: "Update dependency tree for React 19 compatibility", + tier: "low", + score: "41", + owner: "Frontend infra", + summary: + "No breaking changes yet, but three transitive deps have published React 19 adapters. Low urgency, high future leverage.", + }, +]; + +export const comparisonNotes: ComparisonNote[] = [ + { + id: "cmp-001", + author: "L. Nakamura", + timestamp: "2026-04-25 10:30 PDT", + body: "Auth migration jumps ahead of latency fix because the compliance deadline is immovable. Latency is painful but has a workaround via client-side retry budgets.", + verdict: "promote", + }, + { + id: "cmp-002", + author: "R. Fernandez", + timestamp: "2026-04-25 09:15 PDT", + body: "Onboarding checklist v2 stays above failover work. Activation impact is measurable this sprint, while failover has a longer payoff horizon.", + verdict: "hold", + }, + { + id: "cmp-003", + author: "T. Abrams", + timestamp: "2026-04-24 16:45 PDT", + body: "Admin panel consolidation was ranked higher last week but the permissions dependency pushed it down. Revisit once the auth migration lands.", + verdict: "demote", + }, + { + id: "cmp-004", + author: "K. Johal", + timestamp: "2026-04-24 14:10 PDT", + body: "React 19 dep update stays at the bottom. No customer impact, no blocking path. Good candidate for a hack-day slot if one opens up.", + verdict: "hold", + }, +]; + +export const signalEntries: SignalEntry[] = [ + { + id: "sig-001", + label: "Customer escalations", + strength: "strong", + value: "12 open", + detail: "Escalations tied to auth and latency issues account for 80% of this week's volume.", + }, + { + id: "sig-002", + label: "Deployment risk", + strength: "moderate", + value: "Medium", + detail: "Auth migration touches shared middleware. Staged rollout plan is in place but adds two days.", + }, + { + id: "sig-003", + label: "Revenue impact", + strength: "strong", + value: "$140k ARR", + detail: "Three enterprise accounts flagged latency as a renewal blocker in QBR notes.", + }, + { + id: "sig-004", + label: "Team capacity", + strength: "weak", + value: "72%", + detail: "Two engineers on PTO next week. Parallel work on items 3 and 4 will need to serialize.", + }, + { + id: "sig-005", + label: "Compliance deadline", + strength: "strong", + value: "May 16", + detail: "SOC 2 audit window opens May 16. Auth token changes must be in production before that date.", + }, +]; diff --git a/src/app/priority-lab/page.test.tsx b/src/app/priority-lab/page.test.tsx new file mode 100644 index 0000000..7d49748 --- /dev/null +++ b/src/app/priority-lab/page.test.tsx @@ -0,0 +1,89 @@ +import { cleanup, render, screen, within } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + comparisonNotes, + priorityLabOverview, + priorityLabStats, + rankedItems, + signalEntries, +} from "./_data/priority-lab-data"; +import PriorityLabPage from "./page"; + +afterEach(() => { + cleanup(); +}); + +describe("PriorityLabPage", () => { + it("renders the hero with primary heading, overview metadata, and back link", () => { + render(); + + expect( + screen.getByText(priorityLabOverview.title, { selector: "h1" }), + ).toBeInTheDocument(); + expect(screen.getByText(priorityLabOverview.reviewCycle)).toBeInTheDocument(); + expect(screen.getByText(priorityLabOverview.scope)).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /back to overview/i }), + ).toHaveAttribute("href", "/"); + }); + + it("renders summary stats for priority lab", () => { + render(); + + const summarySection = screen.getByLabelText(/priority lab summary/i); + + for (const stat of priorityLabStats) { + const statEl = within(summarySection).getByText(stat.label).closest("article"); + + expect(statEl).toBeTruthy(); + expect(within(statEl as HTMLElement).getByText(stat.value)).toBeInTheDocument(); + } + }); + + it("renders all ranked items with titles, scores, and tier badges", () => { + render(); + + const itemList = screen.getByRole("list", { name: /ranked items/i }); + const items = within(itemList).getAllByRole("listitem"); + + expect(items).toHaveLength(rankedItems.length); + + for (const item of rankedItems) { + expect(within(itemList).getByText(item.title)).toBeInTheDocument(); + expect(within(itemList).getByText(item.score)).toBeInTheDocument(); + expect(within(itemList).getByText(item.owner)).toBeInTheDocument(); + } + }); + + it("renders comparison notes with authors, timestamps, and verdict badges", () => { + render(); + + const notesSection = screen.getByLabelText(/comparison notes$/i); + const notesList = within(notesSection).getByRole("list", { name: /comparison notes list/i }); + const noteItems = within(notesList).getAllByRole("listitem"); + + expect(noteItems).toHaveLength(comparisonNotes.length); + + for (const note of comparisonNotes) { + expect(within(notesList).getByText(note.author)).toBeInTheDocument(); + expect(within(notesList).getByText(note.timestamp)).toBeInTheDocument(); + expect(within(notesList).getByText(note.body)).toBeInTheDocument(); + } + }); + + it("renders signal summary with labels, values, and strength badges", () => { + render(); + + const signalSection = screen.getByLabelText(/signal summary/i); + const signalList = within(signalSection).getByRole("list", { name: /signal entries/i }); + const signalItems = within(signalList).getAllByRole("listitem"); + + expect(signalItems).toHaveLength(signalEntries.length); + + for (const signal of signalEntries) { + expect(within(signalList).getByText(signal.label)).toBeInTheDocument(); + expect(within(signalList).getByText(signal.value)).toBeInTheDocument(); + } + }); +}); diff --git a/src/app/priority-lab/page.tsx b/src/app/priority-lab/page.tsx new file mode 100644 index 0000000..2612283 --- /dev/null +++ b/src/app/priority-lab/page.tsx @@ -0,0 +1,118 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { ComparisonNotes } from "./_components/comparison-notes"; +import { RankedItemCard } from "./_components/ranked-item-card"; +import { SignalSummary } from "./_components/signal-summary"; +import styles from "./priority-lab.module.css"; +import { + comparisonNotes, + priorityLabOverview, + priorityLabStats, + rankedItems, + signalEntries, +} from "./_data/priority-lab-data"; + +export const metadata: Metadata = { + title: "Priority Lab", + description: + "Ranked work items, comparison notes, and signal summaries for sequencing and prioritization decisions.", +}; + +export default function PriorityLabPage() { + return ( +
+
+ {/* Hero */} +
+
+
+ + {priorityLabOverview.eyebrow} + +

+ {priorityLabOverview.title} +

+

+ {priorityLabOverview.description} +

+
+ +
+
+

+ Review cycle +

+

+ {priorityLabOverview.reviewCycle} +

+

+ {priorityLabOverview.scope} +

+
+ + + Back to overview + +
+
+
+ + {/* Stats */} +
+ {priorityLabStats.map((stat) => ( +
+

+ {stat.label} +

+

+ {stat.value} +

+

{stat.detail}

+
+ ))} +
+ + {/* Ranked items */} +
+
+
+

+ Priority ranking +

+

+ Ranked work items by weighted score +

+
+

+ Items are scored across impact, effort, and urgency dimensions. + Higher scores indicate stronger alignment with current priorities. +

+
+ +
+ {rankedItems.map((item) => ( + + ))} +
+
+ + {/* Comparison notes */} + + + {/* Signal summary */} + +
+
+ ); +} diff --git a/src/app/priority-lab/priority-lab.module.css b/src/app/priority-lab/priority-lab.module.css new file mode 100644 index 0000000..126464e --- /dev/null +++ b/src/app/priority-lab/priority-lab.module.css @@ -0,0 +1,107 @@ +.shell { + position: relative; + min-height: 100vh; + overflow: hidden; + background: + radial-gradient(circle at 20% 12%, rgba(168, 85, 247, 0.14), transparent 22%), + radial-gradient(circle at 80% 20%, rgba(99, 102, 241, 0.16), transparent 24%), + radial-gradient(circle at 50% 94%, rgba(34, 211, 238, 0.14), transparent 28%), + linear-gradient(180deg, #08111f 0%, #0e1c34 44%, #030712 100%); +} + +.shell::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(125deg, rgba(255, 255, 255, 0.03), transparent 45%), + repeating-linear-gradient( + 90deg, + rgba(148, 163, 184, 0.06) 0, + rgba(148, 163, 184, 0.06) 1px, + transparent 1px, + transparent 72px + ); + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.95), transparent 96%); +} + +.heroPanel { + position: relative; + background: + linear-gradient(140deg, rgba(15, 23, 42, 0.82), rgba(15, 23, 42, 0.58)), + linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(15, 23, 42, 0.12)); + box-shadow: 0 38px 120px rgba(2, 6, 23, 0.42); +} + +.heroPanel::after { + content: ""; + position: absolute; + right: -5rem; + top: -4rem; + width: 18rem; + height: 18rem; + border-radius: 9999px; + background: radial-gradient(circle, rgba(168, 85, 247, 0.22), transparent 66%); + filter: blur(20px); + pointer-events: none; +} + +.statCard { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(15, 23, 42, 0.28)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.rankCard { + position: relative; + overflow: hidden; + background: + linear-gradient(180deg, rgba(15, 23, 42, 0.82), rgba(6, 11, 24, 0.94)); + box-shadow: 0 26px 70px rgba(2, 6, 23, 0.26); +} + +.tierCritical { + border-color: rgba(251, 113, 133, 0.28); +} + +.tierHigh { + border-color: rgba(251, 191, 36, 0.24); +} + +.tierMedium { + border-color: rgba(52, 211, 153, 0.22); +} + +.tierLow { + border-color: rgba(34, 211, 238, 0.22); +} + +.noteCard { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(15, 23, 42, 0.32)); +} + +.signalCard { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(15, 23, 42, 0.3)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.statusBadge { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +.floatingInfo { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(15, 23, 42, 0.26)); +} + +@media (max-width: 640px) { + .heroPanel, + .rankCard, + .noteCard, + .signalCard, + .statCard { + border-radius: 1.5rem; + } +}