diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4611bce..3b8d264 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -33,6 +33,7 @@ const navLinks = [ { href: "/queue-monitor", label: "Queue Monitor" }, { href: "/parcel-hub", label: "Parcel Hub" }, { href: "/status-board", label: "Status Board" }, + { href: "/response-ledger", label: "Response Ledger" }, ] as const; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 6b96cfd..96aa30b 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 responseLedgerHighlights = [ + "Chronological action history with status badges and owner attribution", + "Outcome summary cards showing success, partial, and pending results", + "Compact ownership rail with per-owner action counts and resolution rates", +]; + const teamDirectoryHighlights = [ "Cross-functional directory with grouped profiles and availability states", "Spotlight panel featuring role highlights and shared skill breakdowns", @@ -561,6 +567,59 @@ export default function Home() { +
+
+
+

+ Issue 212 / Response Ledger +

+
+

+ Track response actions, outcomes, and ownership across active + incidents. +

+

+ The response ledger collects action history, short outcome + summaries, and a compact ownership rail so operators can review + who did what and how each action resolved. +

+
+
+ + Open response ledger + + + Review issue scope + +
+
+ +
+

+ Ledger features +

+
    + {responseLedgerHighlights.map((item) => ( +
  • + {item} +
  • + ))} +
+
+
+
+
diff --git a/src/app/response-ledger/_components/ledger-entry.tsx b/src/app/response-ledger/_components/ledger-entry.tsx new file mode 100644 index 0000000..9c171ff --- /dev/null +++ b/src/app/response-ledger/_components/ledger-entry.tsx @@ -0,0 +1,76 @@ +import type { ActionEntry } from "../_data/response-ledger-data"; + +type LedgerEntryProps = { + action: ActionEntry; +}; + +const statusStyles: Record = { + completed: "border-l-emerald-500", + "in-progress": "border-l-sky-400", + escalated: "border-l-rose-500", + deferred: "border-l-slate-400", +}; + +const statusBadge: Record = { + completed: "bg-emerald-100 text-emerald-700", + "in-progress": "bg-sky-100 text-sky-700", + escalated: "bg-rose-100 text-rose-700", + deferred: "bg-slate-100 text-slate-600", +}; + +function formatTimestamp(iso: string): string { + const d = new Date(iso); + return d.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} + +export function LedgerEntry({ action }: LedgerEntryProps) { + return ( +
+
+
+ + + {action.status} + +

+ {action.title} +

+
+
+ {action.tags.map((tag) => ( + + {tag} + + ))} +
+
+ +

+ {action.description} +

+ +
+ {action.owner} + · + {action.team} +
+
+ ); +} diff --git a/src/app/response-ledger/_components/outcome-summary.tsx b/src/app/response-ledger/_components/outcome-summary.tsx new file mode 100644 index 0000000..fdc95af --- /dev/null +++ b/src/app/response-ledger/_components/outcome-summary.tsx @@ -0,0 +1,26 @@ +import type { OutcomeSummary } from "../_data/response-ledger-data"; + +type OutcomeSummaryCardProps = { + outcome: OutcomeSummary; +}; + +const resultStyles: Record = { + success: "border-emerald-200 bg-emerald-50 text-emerald-700", + partial: "border-amber-200 bg-amber-50 text-amber-700", + failed: "border-rose-200 bg-rose-50 text-rose-700", + pending: "border-slate-200 bg-slate-50 text-slate-600", +}; + +export function OutcomeSummaryCard({ outcome }: OutcomeSummaryCardProps) { + return ( +
+
+ {outcome.label} + + {outcome.result} + +
+

{outcome.detail}

+
+ ); +} diff --git a/src/app/response-ledger/_components/ownership-rail.tsx b/src/app/response-ledger/_components/ownership-rail.tsx new file mode 100644 index 0000000..9102ac1 --- /dev/null +++ b/src/app/response-ledger/_components/ownership-rail.tsx @@ -0,0 +1,38 @@ +import type { OwnershipRecord } from "../_data/response-ledger-data"; + +type OwnershipRailProps = { + records: OwnershipRecord[]; +}; + +export function OwnershipRail({ records }: OwnershipRailProps) { + return ( +
+

+ Ownership +

+
    + {records.map((record) => ( +
  • +
    +

    + {record.owner} +

    +

    {record.team}

    +
    +
    +

    + {record.actionCount} +

    +

    + {record.resolvedCount} resolved +

    +
    +
  • + ))} +
+
+ ); +} diff --git a/src/app/response-ledger/_data/response-ledger-data.ts b/src/app/response-ledger/_data/response-ledger-data.ts new file mode 100644 index 0000000..35b5d52 --- /dev/null +++ b/src/app/response-ledger/_data/response-ledger-data.ts @@ -0,0 +1,227 @@ +export type ActionStatus = "completed" | "in-progress" | "escalated" | "deferred"; + +export type ActionEntry = { + id: string; + timestamp: string; + title: string; + description: string; + status: ActionStatus; + owner: string; + team: string; + tags: string[]; +}; + +export type OutcomeSummary = { + id: string; + actionId: string; + label: string; + result: "success" | "partial" | "failed" | "pending"; + detail: string; +}; + +export type OwnershipRecord = { + owner: string; + team: string; + actionCount: number; + resolvedCount: number; +}; + +export const actionEntries: ActionEntry[] = [ + { + id: "act-001", + timestamp: "2026-04-25T09:12:00Z", + title: "Reroute inbound traffic to secondary gateway", + description: + "Primary gateway hit capacity limits during the morning surge. Traffic was shifted to the secondary gateway within the SLA window.", + status: "completed", + owner: "Mara Chen", + team: "Network Ops", + tags: ["traffic", "gateway", "capacity"], + }, + { + id: "act-002", + timestamp: "2026-04-25T08:47:00Z", + title: "Escalate database replication lag to DBA on-call", + description: + "Replica lag exceeded 30 seconds on the analytics cluster. Escalated to the DBA rotation after automated recovery stalled.", + status: "escalated", + owner: "Jonas Park", + team: "Data Platform", + tags: ["database", "replication", "escalation"], + }, + { + id: "act-003", + timestamp: "2026-04-25T08:30:00Z", + title: "Deploy hotfix for auth token validation", + description: + "A regression in token validation caused intermittent 401 responses for federated users. Hotfix deployed and canary confirmed healthy.", + status: "completed", + owner: "Priya Gupta", + team: "Identity", + tags: ["auth", "hotfix", "deploy"], + }, + { + id: "act-004", + timestamp: "2026-04-25T08:15:00Z", + title: "Investigate elevated error rate on /v2/search", + description: + "Error rate spiked to 4.2% on the search endpoint. Root cause traced to an upstream index rebuild that temporarily returned partial results.", + status: "in-progress", + owner: "Leo Tanaka", + team: "Search", + tags: ["search", "errors", "investigation"], + }, + { + id: "act-005", + timestamp: "2026-04-25T07:58:00Z", + title: "Defer cache warming for low-priority tenants", + description: + "Cache warming for tier-3 tenants deferred to off-peak window to preserve bandwidth for critical path traffic during the incident.", + status: "deferred", + owner: "Mara Chen", + team: "Network Ops", + tags: ["cache", "deferral", "capacity"], + }, + { + id: "act-006", + timestamp: "2026-04-25T07:40:00Z", + title: "Restart notification relay workers", + description: + "Three relay workers stopped processing after a connection pool timeout. Rolling restart restored delivery within two minutes.", + status: "completed", + owner: "Sam Okoro", + team: "Messaging", + tags: ["notifications", "restart", "workers"], + }, + { + id: "act-007", + timestamp: "2026-04-25T07:22:00Z", + title: "Roll back config change on rate limiter", + description: + "A config push lowered the global rate limit by 40%, triggering throttle alerts across partner integrations. Config reverted to previous version.", + status: "completed", + owner: "Priya Gupta", + team: "Identity", + tags: ["config", "rollback", "rate-limit"], + }, + { + id: "act-008", + timestamp: "2026-04-25T07:05:00Z", + title: "Scale up compute pool for batch processing", + description: + "Batch job queue depth exceeded threshold. Auto-scale triggered but was capped; manual override added four additional nodes.", + status: "in-progress", + owner: "Jonas Park", + team: "Data Platform", + tags: ["compute", "scaling", "batch"], + }, +]; + +export const outcomeSummaries: OutcomeSummary[] = [ + { + id: "out-001", + actionId: "act-001", + label: "Traffic reroute", + result: "success", + detail: "Secondary gateway absorbed 100% of overflow. No dropped requests detected.", + }, + { + id: "out-002", + actionId: "act-002", + label: "Replication lag escalation", + result: "pending", + detail: "DBA on-call acknowledged. Investigating replica configuration drift.", + }, + { + id: "out-003", + actionId: "act-003", + label: "Auth hotfix deployment", + result: "success", + detail: "401 rate returned to baseline within 3 minutes of canary promotion.", + }, + { + id: "out-004", + actionId: "act-004", + label: "Search error investigation", + result: "partial", + detail: "Index rebuild identified as root cause. Monitoring for recurrence after rebuild completes.", + }, + { + id: "out-005", + actionId: "act-005", + label: "Cache warming deferral", + result: "success", + detail: "Bandwidth freed for critical traffic. Tier-3 warming rescheduled for 02:00 UTC.", + }, + { + id: "out-006", + actionId: "act-006", + label: "Relay worker restart", + result: "success", + detail: "All three workers healthy. Notification backlog cleared in 90 seconds.", + }, + { + id: "out-007", + actionId: "act-007", + label: "Rate limiter rollback", + result: "success", + detail: "Partner throttle alerts resolved. No SLA breaches recorded.", + }, + { + id: "out-008", + actionId: "act-008", + label: "Compute scale-up", + result: "partial", + detail: "Four nodes added. Queue depth dropping but not yet below threshold.", + }, +]; + +export function getOwnershipRecords(): OwnershipRecord[] { + const map = new Map(); + + for (const action of actionEntries) { + const key = action.owner; + const existing = map.get(key); + if (existing) { + existing.actionCount += 1; + if (action.status === "completed") existing.resolvedCount += 1; + } else { + map.set(key, { + owner: action.owner, + team: action.team, + actionCount: 1, + resolvedCount: action.status === "completed" ? 1 : 0, + }); + } + } + + return Array.from(map.values()).sort((a, b) => b.actionCount - a.actionCount); +} + +export type ResponseLedgerView = { + actions: ActionEntry[]; + outcomes: OutcomeSummary[]; + ownership: OwnershipRecord[]; + summary: { + totalActions: number; + completedCount: number; + escalatedCount: number; + inProgressCount: number; + }; +}; + +export function getResponseLedgerView(): ResponseLedgerView { + const ownership = getOwnershipRecords(); + + return { + actions: actionEntries, + outcomes: outcomeSummaries, + ownership, + summary: { + totalActions: actionEntries.length, + completedCount: actionEntries.filter((a) => a.status === "completed").length, + escalatedCount: actionEntries.filter((a) => a.status === "escalated").length, + inProgressCount: actionEntries.filter((a) => a.status === "in-progress").length, + }, + }; +} diff --git a/src/app/response-ledger/page.test.tsx b/src/app/response-ledger/page.test.tsx new file mode 100644 index 0000000..6ecdb41 --- /dev/null +++ b/src/app/response-ledger/page.test.tsx @@ -0,0 +1,73 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import ResponseLedgerPage from "./page"; +import { + actionEntries, + outcomeSummaries, + getOwnershipRecords, +} from "./_data/response-ledger-data"; + +describe("ResponseLedgerPage", () => { + it("renders the page heading and summary stats", () => { + render(); + + expect( + screen.getByRole("heading", { level: 1, name: /response ledger/i }), + ).toBeInTheDocument(); + + expect(screen.getByText(String(actionEntries.length))).toBeInTheDocument(); + expect( + screen.getByText( + String(actionEntries.filter((a) => a.status === "completed").length), + ), + ).toBeInTheDocument(); + }); + + it("renders all action history entries", () => { + render(); + + const actionList = screen.getByRole("list", { + name: /response ledger actions/i, + }); + expect(within(actionList).getAllByRole("listitem")).toHaveLength( + actionEntries.length, + ); + + for (const action of actionEntries) { + expect(screen.getByText(action.title)).toBeInTheDocument(); + } + }); + + it("renders outcome summaries", () => { + render(); + + const outcomeList = screen.getByRole("list", { + name: /outcome summaries/i, + }); + expect(within(outcomeList).getAllByRole("listitem")).toHaveLength( + outcomeSummaries.length, + ); + + for (const outcome of outcomeSummaries) { + expect(screen.getByText(outcome.label)).toBeInTheDocument(); + } + }); + + it("renders the ownership rail with all owners", () => { + render(); + + const ownershipList = screen.getByRole("list", { + name: /ownership summary/i, + }); + const records = getOwnershipRecords(); + + expect(within(ownershipList).getAllByRole("listitem")).toHaveLength( + records.length, + ); + + for (const record of records) { + expect(screen.getByText(record.owner)).toBeInTheDocument(); + } + }); +}); diff --git a/src/app/response-ledger/page.tsx b/src/app/response-ledger/page.tsx new file mode 100644 index 0000000..d32041a --- /dev/null +++ b/src/app/response-ledger/page.tsx @@ -0,0 +1,145 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { LedgerEntry } from "./_components/ledger-entry"; +import { OutcomeSummaryCard } from "./_components/outcome-summary"; +import { OwnershipRail } from "./_components/ownership-rail"; +import { getResponseLedgerView } from "./_data/response-ledger-data"; + +export const metadata: Metadata = { + title: "Response Ledger", + description: + "Action history with outcome summaries and a compact ownership rail for operational triage.", +}; + +export default function ResponseLedgerPage() { + const { actions, outcomes, ownership, summary } = getResponseLedgerView(); + + return ( +
+
+ {/* Hero */} +
+
+

+ Response Actions +

+ + Back to overview + +
+ +
+

+ Response Ledger +

+

+ A running log of response actions, their outcomes, and who owns + each thread — designed for fast situational awareness during + active incidents. +

+
+ + {/* Summary stats */} +
+
+

+ Total +

+

+ {summary.totalActions} +

+
+
+

+ Completed +

+

+ {summary.completedCount} +

+
+
+

+ Escalated +

+

+ {summary.escalatedCount} +

+
+
+

+ In Progress +

+

+ {summary.inProgressCount} +

+
+
+
+ + {/* Main content grid: action log + ownership rail */} +
+ {/* Action history */} +
+
+

+ Chronological feed +

+

+ Action history +

+
+ +
+ {actions.map((action) => ( + + ))} +
+
+ + {/* Ownership rail */} + +
+ + {/* Outcome summaries */} +
+
+

+ Results +

+

+ Outcome summaries +

+
+ +
+ {outcomes.map((outcome) => ( +
+ +
+ ))} +
+
+
+
+ ); +}