spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { Pulse } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { SectionNav } from "@/components/entity/section-nav";6import { SourcesFooter } from "@/components/entity/sources-footer";7import { EventList } from "@/components/event-list";8import { ApiNote, CounterGrid, HonestEmpty, type CounterItem } from "@/components/intelligence/shared";9import { Page, PageHeader, Section } from "@/components/ui/card";10import { CountryFlag } from "@/components/ui/country-flag";11import { SegmentedControl } from "@/components/ui/segmented";12import { ErrorState, OfflineNotice } from "@/components/ui/states";13import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";14import { api } from "@/lib/api";15import { countryName, fmtDateTimeUtc, fmtInt } from "@/lib/format";16import { routes } from "@/lib/routes";17import { pageMetadata } from "@/lib/seo";1819export const revalidate = 60;2021type Window = Pulse["window"];22const WINDOWS: Array<{ value: Window; label: string; suffix: string }> = [23 { value: "24h", label: "24 h", suffix: "in the last 24 hours" },24 { value: "7d", label: "7 days", suffix: "in the last 7 days" },25 { value: "30d", label: "30 days", suffix: "in the last 30 days" },26];27const isWindow = (v: unknown): v is Window => v === "24h" || v === "7d" || v === "30d";2829export const metadata: Metadata = pageMetadata({30 title: "Pulse",31 description: "What changed in the global data center index over the last 24 hours, 7 days or 30 days: new projects, construction starts, openings, announced MW, new markets, power and grid events — deterministic counts from detected changes.",32 path: routes.pulse(),33});3435export default async function PulsePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {36 const sp = await searchParams;37 const raw = typeof sp.window === "string" ? sp.window : undefined;38 const window: Window = isWindow(raw) ? raw : "7d";39 const win = WINDOWS.find((w) => w.value === window)!;40 const r = await api.pulse(window);4142 const header = (43 <PageHeader44 eyebrow="Intelligence"45 title="Pulse"46 description="Measurable changes over a window, counted from detected events and entity records. Every counter is a count, not a score."47 aside={<SegmentedControl label="Window" value={window} options={WINDOWS.map((w) => ({ value: w.value, label: w.label, href: w.value === "7d" ? routes.pulse() : routes.pulse(`window=${w.value}`) }))} />}48 />49 );5051 if (!r.ok) {52 return (53 <Page className="py-6">54 <Breadcrumbs items={[{ name: "Pulse", href: routes.pulse() }]} />55 {header}56 <OfflineNotice detail={r.error} className="mb-4" />57 <ErrorState />58 </Page>59 );60 }61 const d = r.data;62 const methodology = typeof r.meta?.methodology === "string" ? r.meta.methodology : null;6364 const counters: CounterItem[] = [65 { key: "newProjects", value: d.newProjects, label: "new projects detected", href: routes.projects() },66 { key: "projectsEnteredConstruction", value: d.projectsEnteredConstruction, label: "projects entered construction" },67 { key: "facilitiesOpened", value: d.facilitiesOpened, label: "facilities opened" },68 { key: "newlyAnnouncedMw", value: d.newlyAnnouncedMw, label: "of newly announced capacity (≈ planned)", format: "mw" },69 { key: "constructionStartedMw", value: d.constructionStartedMw, label: "of capacity started construction", format: "mw" },70 { key: "cloudRegionsAnnounced", value: d.cloudRegionsAnnounced, label: "cloud regions announced or launched", href: routes.cloudRegions() },71 { key: "powerAgreements", value: d.powerAgreements, label: "power agreements detected", href: routes.power() },72 { key: "gridConstraintEvents", value: d.gridConstraintEvents, label: "grid constraint events", href: routes.power() },73 { key: "acquisitions", value: d.acquisitions, label: "acquisitions detected", href: routes.live("type=acquisition") },74 { key: "financingEvents", value: d.financingEvents, label: "financing events" },75 { key: "newFacilitiesIndexed", value: d.newFacilitiesIndexed, label: "facilities newly indexed", note: "includes the initial import of a source when it falls in the window" },76 { key: "capacityChanges", value: d.capacityChanges, label: "capacity figures changed", href: routes.live("type=capacity_changed,planned_capacity_changed") },77 { key: "eventsTotal", value: d.eventsTotal, label: "changes detected in total", href: routes.live() },78 ];7980 const sections = [81 { id: "counters", label: "Counters" },82 { id: "markets", label: "New markets", count: d.operatorsNewMarkets.length },83 { id: "major", label: "Major events", count: d.majorEvents.length },84 { id: "by-country", label: "By country", count: d.byCountry.length },85 { id: "by-operator", label: "By operator", count: d.byOperator.length },86 ];8788 return (89 <Page>90 <Breadcrumbs items={[{ name: "Pulse", href: routes.pulse() }]} className="pt-3" />91 {header}92 <p className="figure mb-4 border-t border-hair pt-3 text-[12.5px] text-ink-3">93 Changes detected since {fmtDateTimeUtc(d.since)} · window {win.label}94 </p>9596 <SectionNav items={sections} />9798 <div className="flex flex-col gap-8 pt-6">99 <Section id="counters" title="What changed" description={`Deterministic counts ${win.suffix}. A dash means the figure is not published, never zero.`} className="scroll-mt-28">100 <CounterGrid items={counters} suffix={win.suffix} />101 <ApiNote note={methodology ?? "Counters are computed from detected events and entity records in the window; nothing is modelled or extrapolated."} label="Method" className="mt-3" />102 </Section>103104 <Section id="markets" title="Operators entering new markets" description={`Operators whose first facility or project in a market or country was observed ${win.suffix}.`} className="scroll-mt-28">105 {d.operatorsNewMarkets.length === 0 ? (106 <HonestEmpty title="No new market entry detected" detail="an entry is recorded when an operator's first record in a market or country is observed" />107 ) : (108 <ul className="divide-y divide-hair">109 {d.operatorsNewMarkets.map((x, i) => (110 <li key={`${x.operator.id}-${x.market?.id ?? x.countryIso2 ?? i}`} className="flex flex-wrap items-center gap-x-2 gap-y-0 py-1 text-[13px] sm:py-2">111 <Link href={routes.operator(x.operator.slug)} className="inline-flex min-h-10 items-center font-medium text-ink hover:text-accent hover:underline sm:min-h-6">112 {x.operator.name}113 </Link>114 <span aria-hidden className="text-ink-4">115 →116 </span>117 {x.market ? (118 <Link href={routes.market(x.market.slug)} className="inline-flex min-h-10 items-center gap-1.5 text-ink-2 hover:text-ink hover:underline sm:min-h-6">119 {x.countryIso2 && <CountryFlag iso2={x.countryIso2} size={12} />}120 {x.market.name}121 </Link>122 ) : x.countryIso2 ? (123 <Link href={routes.datacenters(`country=${x.countryIso2}`)} title="Facilities in this country" className="inline-flex min-h-10 items-center gap-1.5 text-ink-2 hover:text-ink hover:underline sm:min-h-6">124 <CountryFlag iso2={x.countryIso2} size={12} />125 {countryName(x.countryIso2)} <span className="text-ink-4">(country, no market)</span>126 </Link>127 ) : (128 <span className="text-ink-4">market unknown</span>129 )}130 </li>131 ))}132 </ul>133 )}134 </Section>135136 <Section id="major" title="Major events" description="Highest-significance changes in the window, one row per announcement cluster." className="scroll-mt-28" aside={<Link href={routes.live("significance=major")} className="text-[12.5px] text-ink-3 hover:text-ink">All major changes →</Link>}>137 <EventList events={d.majorEvents} dense emptyTitle="No major event in this window" emptyDescription="Try a longer window." />138 </Section>139140 <Section id="by-country" title="By country" description="Detected changes and new projects per country in the window." className="scroll-mt-28">141 {d.byCountry.length === 0 ? (142 <HonestEmpty title="No change attributed to a country" />143 ) : (144 <Table dense mobileCollapse={{ total: d.byCountry.length, label: "countries" }}>145 <THead>146 <tr>147 <Th>Country</Th>148 <Th numeric>Changes</Th>149 <Th numeric>New projects</Th>150 </tr>151 </THead>152 <TBody>153 {d.byCountry.map((c) => (154 <Tr key={c.iso2}>155 <Td primary>156 <RowLink href={routes.country(c.slug)}>157 <span className="inline-flex items-center gap-1.5">158 <CountryFlag iso2={c.iso2} size={12} />159 {c.name}160 </span>161 </RowLink>162 </Td>163 <Td label="Changes" numeric lead>164 {fmtInt(c.events)}165 </Td>166 <Td label="New projects" numeric>167 {fmtInt(c.newProjects)}168 </Td>169 </Tr>170 ))}171 </TBody>172 </Table>173 )}174 </Section>175176 <Section id="by-operator" title="By operator" description="Operators with the most detected changes in the window." className="scroll-mt-28">177 {d.byOperator.length === 0 ? (178 <HonestEmpty title="No change attributed to an operator" />179 ) : (180 <Table dense mobileCollapse={{ total: d.byOperator.length, label: "operators" }}>181 <THead>182 <tr>183 <Th>Operator</Th>184 <Th numeric>Changes</Th>185 <Th numeric>New projects</Th>186 </tr>187 </THead>188 <TBody>189 {d.byOperator.map((o) => (190 <Tr key={o.id}>191 <Td primary>192 <RowLink href={routes.operator(o.slug)}>{o.name}</RowLink>193 </Td>194 <Td label="Changes" numeric lead>195 {fmtInt(o.events)}196 </Td>197 <Td label="New projects" numeric>198 {fmtInt(o.newProjects)}199 </Td>200 </Tr>201 ))}202 </TBody>203 </Table>204 )}205 </Section>206 </div>207208 <SourcesFooter sources={r.sources} note="Pulse counts are derived from the change feed (events deduplicated per entity and day) and entity records; they are recomputed on every request window and never smoothed." />209 </Page>210 );211}212