SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
13.8 KB · 234 lines tsx
Raw Blame History
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