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
61 changes: 61 additions & 0 deletions src/app/capacity-planner/_components/demand-band.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { DemandBand } from "../_data/capacity-planner-data";
import styles from "../capacity-planner.module.css";

const levelLabels: Record<DemandBand["level"], string> = {
low: "Low",
moderate: "Moderate",
high: "High",
peak: "Peak",
};

const levelBadgeStyles: Record<DemandBand["level"], string> = {
low: "border-cyan-300/20 bg-cyan-300/10 text-cyan-50",
moderate: "border-emerald-300/20 bg-emerald-300/10 text-emerald-50",
high: "border-amber-300/20 bg-amber-300/10 text-amber-50",
peak: "border-rose-300/20 bg-rose-300/10 text-rose-50",
};

const levelSurfaceStyles: Record<DemandBand["level"], string> = {
low: styles.bandLow,
moderate: styles.bandModerate,
high: styles.bandHigh,
peak: styles.bandPeak,
};

type DemandBandCardProps = {
band: DemandBand;
};

export function DemandBandCard({ band }: DemandBandCardProps) {
return (
<article
className={`${styles.bandCard} ${levelSurfaceStyles[band.level]} rounded-[1.7rem] border p-6`}
role="listitem"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<h3 className="text-lg font-semibold tracking-tight text-white">
{band.label}
</h3>
<p className="text-sm text-slate-300">{band.peakWindow}</p>
</div>
<span
className={`${styles.statusBadge} inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] ${levelBadgeStyles[band.level]}`}
>
{levelLabels[band.level]}
</span>
</div>

<div className="mt-4 rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
Forecasted load
</p>
<p className="mt-1 text-2xl font-semibold tracking-tight text-white">
{band.forecastedLoad}
</p>
</div>

<p className="mt-4 text-sm leading-6 text-slate-300">{band.detail}</p>
</article>
);
}
60 changes: 60 additions & 0 deletions src/app/capacity-planner/_components/planning-notes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { PlanningNote } from "../_data/capacity-planner-data";
import styles from "../capacity-planner.module.css";

const priorityLabels: Record<PlanningNote["priority"], string> = {
info: "Info",
action: "Action",
warning: "Warning",
};

const priorityBadgeStyles: Record<PlanningNote["priority"], string> = {
info: "border-cyan-300/20 bg-cyan-300/10 text-cyan-50",
action: "border-emerald-300/20 bg-emerald-300/10 text-emerald-50",
warning: "border-amber-300/20 bg-amber-300/10 text-amber-50",
};

type PlanningNotesProps = {
notes: PlanningNote[];
};

export function PlanningNotes({ notes }: PlanningNotesProps) {
return (
<section aria-label="Planning notes" className="space-y-5">
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400">
Review notes
</p>
<h2 className="text-3xl font-semibold tracking-tight text-white sm:text-4xl">
Planning notes from the team
</h2>
<p className="max-w-2xl text-sm leading-6 text-slate-300">
Notes, action items, and warnings captured during the current planning
cycle. Each note is tied to a team member and timestamped.
</p>
</div>

<div className="space-y-4" role="list" aria-label="Planning notes list">
{notes.map((note) => (
<article
key={note.id}
className={`${styles.noteCard} rounded-[1.5rem] border border-white/10 p-5`}
role="listitem"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-semibold text-white">{note.author}</p>
<p className="text-xs text-slate-400">{note.timestamp}</p>
</div>
<span
className={`${styles.statusBadge} inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] ${priorityBadgeStyles[note.priority]}`}
>
{priorityLabels[note.priority]}
</span>
</div>
<p className="mt-3 text-sm leading-6 text-slate-300">{note.body}</p>
</article>
))}
</div>
</section>
);
}
68 changes: 68 additions & 0 deletions src/app/capacity-planner/_components/utilization-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { UtilizationCard } from "../_data/capacity-planner-data";
import styles from "../capacity-planner.module.css";

const statusLabels: Record<UtilizationCard["status"], string> = {
healthy: "Healthy",
elevated: "Elevated",
critical: "Critical",
};

const statusBadgeStyles: Record<UtilizationCard["status"], string> = {
healthy: "border-emerald-300/20 bg-emerald-300/10 text-emerald-50",
elevated: "border-amber-300/20 bg-amber-300/10 text-amber-50",
critical: "border-rose-300/20 bg-rose-300/10 text-rose-50",
};

const statusSurfaceStyles: Record<UtilizationCard["status"], string> = {
healthy: styles.utilHealthy,
elevated: styles.utilElevated,
critical: styles.utilCritical,
};

type UtilizationCardItemProps = {
card: UtilizationCard;
};

export function UtilizationCardItem({ card }: UtilizationCardItemProps) {
return (
<article
className={`${styles.utilCard} ${statusSurfaceStyles[card.status]} rounded-[1.7rem] border p-6`}
role="listitem"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<h3 className="text-lg font-semibold tracking-tight text-white">
{card.resource}
</h3>
<p className="text-sm text-slate-300">{card.owner}</p>
</div>
<span
className={`${styles.statusBadge} inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] ${statusBadgeStyles[card.status]}`}
>
{statusLabels[card.status]}
</span>
</div>

<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
Utilization
</p>
<p className="mt-1 text-2xl font-semibold tracking-tight text-white">
{card.currentUtilization}
</p>
</div>
<div className="rounded-2xl border border-white/8 bg-white/5 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
Capacity
</p>
<p className="mt-1 text-sm font-medium text-slate-100">
{card.capacity}
</p>
</div>
</div>

<p className="mt-4 text-sm leading-6 text-slate-300">{card.note}</p>
</article>
);
}
188 changes: 188 additions & 0 deletions src/app/capacity-planner/_data/capacity-planner-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
export type DemandLevel = "low" | "moderate" | "high" | "peak";
export type UtilizationStatus = "healthy" | "elevated" | "critical";

export interface DemandBand {
id: string;
label: string;
level: DemandLevel;
forecastedLoad: string;
peakWindow: string;
detail: string;
}

export interface UtilizationCard {
id: string;
resource: string;
currentUtilization: string;
capacity: string;
status: UtilizationStatus;
owner: string;
note: string;
}

export interface PlanningNote {
id: string;
author: string;
timestamp: string;
body: string;
priority: "info" | "action" | "warning";
}

export interface CapacityPlannerOverview {
eyebrow: string;
title: string;
description: string;
planCycle: string;
region: string;
}

export interface CapacityStat {
label: string;
value: string;
detail: string;
}

export const capacityPlannerOverview: CapacityPlannerOverview = {
eyebrow: "Capacity Planner",
title: "Forecast demand, track utilization, and align capacity before the next planning window.",
description:
"A consolidated view of demand bands, resource utilization, and planning notes that helps teams make informed capacity decisions ahead of load spikes.",
planCycle: "Q2 2026 — Sprint 14",
region: "US-West / Portland cluster",
};

export const capacityStats: CapacityStat[] = [
{
label: "Active demand bands",
value: "4",
detail: "Covering overnight batch, morning API surge, midday steady-state, and evening wind-down",
},
{
label: "Resources tracked",
value: "5",
detail: "Compute nodes, message queues, worker pools, storage I/O, and network egress",
},
{
label: "Open planning notes",
value: "4",
detail: "Two action items, one warning, and one informational note from the last review cycle",
},
];

export const demandBands: DemandBand[] = [
{
id: "band-overnight",
label: "Overnight batch window",
level: "low",
forecastedLoad: "12%",
peakWindow: "01:00 — 05:00 PDT",
detail:
"Scheduled ETL jobs and index rebuilds run during this window. Load stays predictable unless a backfill is queued.",
},
{
id: "band-morning",
label: "Morning API surge",
level: "high",
forecastedLoad: "78%",
peakWindow: "07:30 — 10:00 PDT",
detail:
"User-facing API traffic ramps sharply as west-coast teams come online. Auto-scaling usually absorbs this within 8 minutes.",
},
{
id: "band-midday",
label: "Midday steady-state",
level: "moderate",
forecastedLoad: "45%",
peakWindow: "10:00 — 16:00 PDT",
detail:
"Traffic plateaus after the morning surge. This is the safest window for deploying capacity changes or running load tests.",
},
{
id: "band-evening",
label: "Evening wind-down",
level: "peak",
forecastedLoad: "88%",
peakWindow: "16:00 — 19:00 PDT",
detail:
"End-of-day report generation and cross-region sync jobs push utilization to its daily peak. Pre-scale at 15:30.",
},
];

export const utilizationCards: UtilizationCard[] = [
{
id: "util-compute",
resource: "Compute nodes",
currentUtilization: "62%",
capacity: "48 / 80 vCPUs",
status: "healthy",
owner: "Platform team",
note: "Auto-scaler headroom is sufficient for the next two demand bands.",
},
{
id: "util-queue",
resource: "Message queues",
currentUtilization: "74%",
capacity: "18.5k / 25k msg/s",
status: "elevated",
owner: "Messaging infra",
note: "Consumer lag has been climbing since the last deploy. Monitor closely during the evening peak.",
},
{
id: "util-workers",
resource: "Worker pool",
currentUtilization: "55%",
capacity: "110 / 200 workers",
status: "healthy",
owner: "Job scheduler",
note: "Batch jobs are draining on schedule. No contention expected until the overnight window.",
},
{
id: "util-storage",
resource: "Storage I/O",
currentUtilization: "83%",
capacity: "4.1k / 5k IOPS",
status: "critical",
owner: "Data platform",
note: "Index rebuild backlog is saturating the provisioned IOPS. Consider deferring non-critical writes.",
},
{
id: "util-network",
resource: "Network egress",
currentUtilization: "39%",
capacity: "3.9 / 10 Gbps",
status: "healthy",
owner: "Network ops",
note: "Cross-region replication is well within budget. No action needed this cycle.",
},
];

export const planningNotes: PlanningNote[] = [
{
id: "note-001",
author: "M. Chen",
timestamp: "2026-04-25 09:14 PDT",
body: "Storage IOPS are running hot after the index rebuild was moved to the midday window. Recommend reverting to overnight scheduling.",
priority: "warning",
},
{
id: "note-002",
author: "J. Okafor",
timestamp: "2026-04-25 08:42 PDT",
body: "Pre-scale compute nodes to 72 vCPUs by 15:30 to absorb the evening peak without relying solely on the auto-scaler.",
priority: "action",
},
{
id: "note-003",
author: "S. Petrov",
timestamp: "2026-04-24 17:30 PDT",
body: "Consumer lag on the order-events topic cleared after partition rebalance. Queue utilization should stabilize by tomorrow morning.",
priority: "info",
},
{
id: "note-004",
author: "A. Reyes",
timestamp: "2026-04-24 14:55 PDT",
body: "Add a second worker pool tier for low-priority batch jobs so they don't compete with real-time processing during peak bands.",
priority: "action",
},
];
Loading
Loading