spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { PowerOverview } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { Metadata } from "next";4import Link from "next/link";5import { Breadcrumbs } from "@/components/entity/breadcrumbs";6import { SectionNav } from "@/components/entity/section-nav";7import { SourcesFooter } from "@/components/entity/sources-footer";8import { EventList } from "@/components/event-list";9import { CountryEnergyTable } from "@/components/intelligence/power/country-energy-table";10import { ApiNote, GRID_KIND_LABEL, GridConstraintList, GridKindBadge, HonestEmpty, PillLink } from "@/components/intelligence/shared";11import { Badge } from "@/components/ui/badge";12import { Page, PageHeader, Section } from "@/components/ui/card";13import { cn } from "@/components/ui/cn";14import { CountryFlag } from "@/components/ui/country-flag";15import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";16import { ErrorState, OfflineNotice } from "@/components/ui/states";17import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";18import { api } from "@/lib/api";19import { fmtInt, fmtMw } from "@/lib/format";20import { OPERATOR_KIND_LABEL } from "@/lib/labels";21import { routes } from "@/lib/routes";22import { pageMetadata } from "@/lib/seo";23import { hrefWith, spGet, type SearchParams } from "@/lib/sort";2425export const revalidate = 120;2627export const metadata: Metadata = pageMetadata({28 title: "Power & grid",29 description: "Grid constraints (moratoria, grid delays, load caps, new transmission), power agreements, the largest published utility and grid-connection loads, and national electricity context — observed facts, no inference.",30 path: routes.power(),31});3233/** The eight constraint kinds of the spec, always listed (0 → “none recorded”). */34const SPEC_KINDS = ["moratorium", "grid_delay", "capacity_restriction", "load_cap", "new_transmission", "new_substation", "regulation", "large_load_queue"] as const;3536const LOAD_KIND_LABEL: Record<PowerOverview["largeLoads"][number]["kind"], string> = { utility_capacity: "Utility capacity", grid_connection: "Grid connection", planned_load: "Planned load" };3738export default async function PowerPage({ searchParams }: { searchParams: Promise<SearchParams> }) {39 const sp = await searchParams;40 const r = await api.power();41 if (!r.ok) {42 return (43 <Page className="py-6">44 <Breadcrumbs items={[{ name: "Power & grid", href: routes.power() }]} className="mb-4" />45 <OfflineNotice detail={r.error} className="mb-4" />46 <ErrorState />47 </Page>48 );49 }50 const d = r.data;51 const kindCounts = new Map<string, number>();52 for (const g of d.gridConstraints) kindCounts.set(g.kind, (kindCounts.get(g.kind) ?? 0) + 1);53 const kinds = Array.from(kindCounts.keys()).sort();54 const kindFilter = spGet(sp, "kind");55 const activeKind = kindFilter && kindCounts.has(kindFilter) ? kindFilter : null;56 const constraints = activeKind ? d.gridConstraints.filter((g) => g.kind === activeKind) : d.gridConstraints;57 const specWithCounts = SPEC_KINDS.map((k) => ({ kind: k, count: kindCounts.get(k) ?? 0 }));58 const extraKinds = kinds.filter((k) => !(SPEC_KINDS as readonly string[]).includes(k));5960 const sections = [61 { id: "constraints", label: "Grid constraints", count: d.gridConstraints.length },62 { id: "events", label: "Power events", count: d.powerEvents.length },63 { id: "loads", label: "Large loads", count: d.largeLoads.length },64 { id: "energy", label: "Country energy", count: d.countryEnergy.length },65 { id: "utilities", label: "Utilities", count: d.utilities.length },66 ];6768 return (69 <Page>70 <Breadcrumbs items={[{ name: "Power & grid", href: routes.power() }]} className="pt-3" />71 <PageHeader eyebrow="Intelligence" title="Power & grid" description="Where electricity constrains or enables data center growth: curated constraint reports, detected utility and power-agreement events, the largest published loads and each country's electricity mix." aside={<PillLink href={routes.map("layer=power")}>Power layer on the map →</PillLink>} />7273 <MetricRow cols={5}>74 <MetricCell>75 <Metric value={fmtInt(d.gridConstraints.length)} label="Constraints & grid events" note={`${kinds.length} ${kinds.length === 1 ? "kind" : "kinds"} recorded`} />76 </MetricCell>77 <MetricCell>78 <Metric value={fmtInt(d.powerEvents.length)} label="Power events" note="recent, source-linked" />79 </MetricCell>80 <MetricCell>81 <Metric value={fmtInt(d.largeLoads.length)} label="Large loads" note="published site-scoped MW" />82 </MetricCell>83 <MetricCell>84 <Metric value={fmtInt(d.countryEnergy.length)} label="Countries with energy stats" />85 </MetricCell>86 <MetricCell>87 <Metric value={fmtInt(d.utilities.length)} label="Utilities" note="operators named energy / power" />88 </MetricCell>89 </MetricRow>9091 <div className="mt-4 grid gap-px overflow-hidden rounded-sm border border-hair bg-hair md:grid-cols-2" role="note" aria-label="Observed fact versus analytical inference">92 <div className="bg-surface px-3 py-3">93 <div className="label-caps mb-1 text-ok">Observed fact</div>94 <p className="text-[12.5px] leading-relaxed text-ink-2">Everything on this page is observed: published utility-capacity and grid-connection MW on facilities and projects, curated public constraint reports (moratoria, grid delays, load caps, new transmission) and detected grid, utility and power-agreement events — each with a link to the page it was read from.</p>95 </div>96 <div className="bg-surface px-3 py-3">97 <div className="label-caps mb-1 text-ink-3">Analytical inference</div>98 <p className="text-[12.5px] leading-relaxed text-ink-2">None. We do not infer available capacity, constraint severity, queue position or supply adequacy from these facts, and a national grid average is never applied to a facility. Absence of a constraint here means none was published — not that the grid is unconstrained.</p>99 </div>100 </div>101 <ApiNote note={d.note} label="From the index" className="mt-3" />102103 <SectionNav items={sections} className="mt-4" />104105 <div className="flex flex-col gap-8 pt-6">106 <Section id="constraints" title="Grid constraints" description="Curated reports and detected events by kind. Each item links to its source; the badge is the kind as classified." className="scroll-mt-28">107 <div className="mb-3 flex flex-wrap gap-1.5" aria-label="Constraint kinds">108 {specWithCounts.map(({ kind, count }) => (109 <KindChip key={kind} kind={kind} count={count} active={activeKind === kind} sp={sp} />110 ))}111 {extraKinds.map((kind) => (112 <KindChip key={kind} kind={kind} count={kindCounts.get(kind) ?? 0} active={activeKind === kind} sp={sp} />113 ))}114 {activeKind && (115 <Link href={hrefWith(routes.power(), sp, { kind: undefined }, { anchor: "constraints" })} scroll={false} className="inline-flex h-10 items-center px-1.5 text-[12px] text-ink-3 hover:text-ink sm:h-7">116 Reset117 </Link>118 )}119 </div>120 <GridConstraintList items={constraints} emptyTitle={activeKind ? `No ${GRID_KIND_LABEL[activeKind] ?? activeKind} recorded` : "No grid constraint recorded"} emptyDetail="a constraint is listed only when a public report or a detected event describes it" />121 </Section>122123 <Section id="events" title="Power events" description="Detected power-agreement, grid-connection and utility events, newest first." className="scroll-mt-28" aside={<Link href={routes.live("type=power_agreement,grid_connection,grid_constraint,utility_event")} className="text-[12.5px] text-ink-3 hover:text-ink">Live feed →</Link>}>124 <EventList events={d.powerEvents} dense emptyTitle="No power event detected yet" emptyDescription="Power agreements and grid events appear as utility and operator sources publish them." />125 </Section>126127 <Section id="loads" title="Large loads" description="Largest published site-scoped figures: utility capacity, grid connection or planned load — supply-side MW, never IT load." className="scroll-mt-28">128 {d.largeLoads.length === 0 ? (129 <HonestEmpty title="No large load published" detail="figures appear when a source publishes a utility, grid-connection or planned load for a site" />130 ) : (131 <Table dense mobileCollapse={{ total: d.largeLoads.length, label: "loads" }}>132 <THead>133 <tr>134 <Th>Site</Th>135 <Th>Kind</Th>136 <Th numeric>MW</Th>137 <Th className="hidden sm:table-cell">Country</Th>138 <Th className="hidden md:table-cell">Market</Th>139 <Th className="hidden lg:table-cell">Source</Th>140 </tr>141 </THead>142 <TBody>143 {d.largeLoads.map((l, i) => {144 const site = l.facility ?? l.project;145 const href = l.facility ? routes.facility(l.facility.slug) : l.project ? routes.project(l.project.slug) : null;146 return (147 <Tr key={`${l.kind}-${site?.id ?? i}`}>148 <Td primary>149 {href && site ? <RowLink href={href}>{site.name}</RowLink> : <span className="text-ink-3">unlinked site</span>}150 <div className="text-[11px] text-ink-4">{l.facility ? "facility" : l.project ? "project" : ""}</div>151 </Td>152 <Td label="Kind">153 <Badge tone={l.kind === "planned_load" ? "info" : "outline"} size="xs">154 {LOAD_KIND_LABEL[l.kind]}155 </Badge>156 </Td>157 <Td label="MW" numeric lead>158 {l.kind === "planned_load" && <span className="mr-0.5 text-ink-3">≈</span>}159 {fmtMw(l.mw)}160 </Td>161 <Td label="Country" className="hidden sm:table-cell">162 <CountryFlag iso2={l.countryIso2} withName className="max-w-[140px] text-ink-2" />163 </Td>164 <Td label="Market" className="hidden md:table-cell" muted>165 {l.metro ? (166 <Link href={routes.market(l.metro.slug)} className="hover:text-ink hover:underline">167 {l.metro.name}168 </Link>169 ) : (170 "—"171 )}172 </Td>173 <Td label="Source" className="hidden text-[12px] lg:table-cell">174 {l.url ? (175 <a href={l.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-ink-2 hover:text-accent">176 {l.sourceName ?? "source"} <ExternalLink className="size-3" />177 </a>178 ) : (179 <span className="text-ink-3">{l.sourceName ?? "—"}</span>180 )}181 </Td>182 </Tr>183 );184 })}185 </TBody>186 </Table>187 )}188 </Section>189190 <Section id="energy" title="Country energy context" description="National electricity mix next to the known data center load — context for reading MW figures, not a facility's contracted supply." className="scroll-mt-28">191 <CountryEnergyTable rows={d.countryEnergy} sp={sp} />192 </Section>193194 <Section id="utilities" title="Utilities" description="Operators whose name says energy / power / electric and that appear as tenants or in events." className="scroll-mt-28">195 {d.utilities.length === 0 ? (196 <HonestEmpty title="No utility identified" detail="utilities are listed only when they appear as tenants or in detected events" />197 ) : (198 <ul className="flex flex-wrap gap-1.5">199 {d.utilities.map((u) => (200 <li key={u.id}>201 <Link href={routes.operator(u.slug)} className="inline-flex h-10 items-center gap-1.5 rounded-sm border border-hair bg-surface px-2.5 text-[12.5px] text-ink hover:border-hair-2 hover:text-accent sm:h-8">202 {u.name}203 {u.kind && <span className="text-[11px] text-ink-3">{OPERATOR_KIND_LABEL[u.kind as keyof typeof OPERATOR_KIND_LABEL] ?? u.kind}</span>}204 <span className="figure text-[11px] text-ink-3">{fmtInt(u.facilityCount)}</span>205 </Link>206 </li>207 ))}208 </ul>209 )}210 </Section>211 </div>212213 <SourcesFooter sources={r.sources} note="Utility and grid MW are supply-side figures as published; national statistics come from the World Bank (CC BY 4.0). No capacity, severity or adequacy is inferred." />214 </Page>215 );216}217218function KindChip({ kind, count, active, sp }: { kind: string; count: number; active: boolean; sp: SearchParams }) {219 const label = GRID_KIND_LABEL[kind] ?? kind.replace(/_/g, " ");220 const inner = (221 <>222 <GridKindBadge kind={kind} />223 <span className={cn("figure text-[11.5px]", count ? "text-ink-2" : "text-ink-4")}>{count ? fmtInt(count) : "none recorded"}</span>224 </>225 );226 const cls = cn("inline-flex h-10 items-center gap-1.5 rounded-sm border px-2 text-[12px] sm:h-8", active ? "border-ink bg-surface-2" : "border-hair bg-surface", count ? "hover:border-hair-2" : "cursor-default");227 if (!count) return <span className={cls} title={`${label}: none recorded`}>{inner}</span>;228 return (229 <Link href={hrefWith(routes.power(), sp, { kind: active ? undefined : kind }, { anchor: "constraints" })} scroll={false} aria-pressed={active} className={cls} title={`Filter: ${label}`}>230 {inner}231 </Link>232 );233}234