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%
22.8 KB · 372 lines tsx
Raw Blame History
1import type { Dashboard } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { Metadata } from "next";4import Link from "next/link";5import { EventList } from "@/components/event-list";6import { CapacityOverTime } from "@/components/home/charts";7import { HeroSearch } from "@/components/home/hero";8import { MapPreview } from "@/components/home/map-preview";9import { PipelineFunnel } from "@/components/home/pipeline-funnel";10import { PulseBlock } from "@/components/home/pulse";11import { GlobalTicker } from "@/components/home/ticker";12import { ProjectList } from "@/components/project-list";13import { AiMark, Badge } from "@/components/ui/badge";14import { MoreLink } from "@/components/ui/button";15import { Page, Section } from "@/components/ui/card";16import { BarList } from "@/components/ui/charts";17import { CountryFlag } from "@/components/ui/country-flag";18import { RelativeTime } from "@/components/ui/relative-time";19import { SourceBadge } from "@/components/ui/source-badge";20import { EmptyState, OfflineNotice } from "@/components/ui/states";21import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";22import { api } from "@/lib/api";23import { fmtDate, fmtInt, fmtMw, fmtPct, splitMw } from "@/lib/format";24import { isSiteScopeLabel } from "@/lib/labels";25import { routes } from "@/lib/routes";2627export const revalidate = 60;2829export const metadata: Metadata = {30  title: { absolute: "DataCenterIndex — Global data center infrastructure, indexed and versioned" },31  description: "Facilities, operators, cloud regions, markets, countries, projects, power and connectivity — every figure sourced, versioned and tracked in a live change feed. Known operational capacity, construction pipeline, AI expansion, coverage.",32  alternates: { canonical: "/" },33};3435function Cell({ label, value, unit, note, href }: { label: string; value: string; unit?: string; note?: string; href?: string }) {36  const body = (37    <>38      <div className="label-caps mb-1 truncate">{label}</div>39      <div className="flex items-baseline gap-1 leading-none">40        <span className="figure text-[20px] font-medium tracking-[-0.02em] text-ink">{value}</span>41        {unit && <span className="text-[11px] font-medium text-ink-3">{unit}</span>}42      </div>43      {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}44    </>45  );46  return href ? (47    <Link href={href} className="block min-w-0 py-2 pr-3 hover:[&_.figure]:text-accent">48      {body}49    </Link>50  ) : (51    <div className="min-w-0 py-2 pr-3">{body}</div>52  );53}5455export default async function HomePage() {56  const r0 = await api.dashboard();57  // tolerate an older API payload (image builds prerender against the live API): every 2.0 block is optional58  const r = r0.ok && r0.data ? { ...r0, data: { ...r0.data, pulse: r0.data.pulse ?? null, fastestGrowingMetros: r0.data.fastestGrowingMetros ?? [], majorProjects: r0.data.majorProjects ?? [], operatorExpansion: r0.data.operatorExpansion ?? [], powerEvents: r0.data.powerEvents ?? [], gridConstraints: r0.data.gridConstraints ?? [], coverage: r0.data.coverage ?? null, ingestion: r0.data.ingestion ?? null } as typeof r0.data } : r0;59  const d: Dashboard | null = r.ok ? r.data : null;60  const stats = d?.stats ?? null;61  const cov = d?.coverage ?? null;62  const ing = d?.ingestion ?? null;63  const major = (d?.majorProjects ?? []).filter((p) => p.plannedMw !== null && p.plannedMw >= 100 && (p.capacityScope == null || isSiteScopeLabel(p.capacityScope)));64  const aiMw = splitMw(d?.aiExpansion.plannedMw);6566  return (67    <Page>68      {/* ---------------------------------------------------------------- hero + live map */}69      <section className="relative -mx-3 border-b border-hair px-3 pt-6 sm:-mx-5 sm:px-5 sm:pt-9 lg:-mx-8 lg:px-8">70        <div aria-hidden className="graticule pointer-events-none absolute inset-0" />71        <div className="relative mx-auto max-w-[1280px]">72          <div className="label-caps mb-3 flex items-center gap-2">73            <span className="inline-block size-1.5 rounded-full bg-accent animate-pulse-soft" /> Live index74            {ing?.lastRunAt && (75              <span className="normal-case tracking-normal text-ink-4">76                · last ingestion <RelativeTime iso={ing.lastRunAt} />77              </span>78            )}79          </div>80          <h1 className="max-w-4xl text-[26px] font-semibold uppercase leading-[1.05] tracking-[0.01em] text-ink sm:text-[38px] lg:text-[44px]">Global data center infrastructure</h1>81          <p className="mt-3 max-w-2xl text-[14px] text-ink-2 sm:text-[15.5px]">The structured index of facilities, campuses, operators, cloud regions, markets, projects, power and connectivity — every figure published, sourced, versioned and tracked in a live change feed.</p>82          <HeroSearch />83          <div className="mt-6">84            {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}85            <GlobalTicker stats={stats} className="bg-paper/70 backdrop-blur-[1px]" />86          </div>87        </div>88        <div className="relative mx-auto max-w-[1280px] pb-6 pt-3 sm:pb-8">89          <MapPreview interactive className="h-[300px] sm:h-[420px] lg:h-[460px]">90            <div className="pointer-events-none absolute left-3 top-3 z-[2] flex flex-wrap gap-1.5 text-[11.5px]">91              <span className="rounded-xs border border-hair bg-surface/95 px-2 py-1 text-ink-2 backdrop-blur">{stats ? `${fmtInt(stats.facilities)} facilities · ${fmtInt(stats.countries)} countries · ${fmtInt(stats.metros)} markets` : "Clustered by market"}</span>92              <span className="rounded-xs border border-hair bg-surface/95 px-2 py-1 text-ink-3 backdrop-blur">size = known MW · hollow ring = city-level location</span>93            </div>94          </MapPreview>95        </div>96      </section>9798      <div className="flex flex-col gap-10 pt-8">99        {/* ---------------------------------------------------------------- what changed today */}100        <Section title="What changed today" description={d?.pulse ? `Deterministic counts over the last ${d.pulse.window === "24h" ? "24 hours" : d.pulse.window}, from ${fmtInt(d.pulse.eventsTotal)} deduplicated events.` : "Deterministic counts from the deduplicated event feed."} aside={<MoreLink href={routes.pulse()}>Full pulse</MoreLink>}>101          {d?.pulse ? <PulseBlock pulse={d.pulse} /> : <EmptyState compact title="Pulse unavailable" />}102        </Section>103104        <div className="grid gap-x-8 gap-y-10 lg:grid-cols-2 [&>*]:min-w-0">105          {/* ---------------------------------------------------------------- AI expansion */}106          <Section107            title={108              <span className="inline-flex items-center gap-2">109                AI infrastructure expansion <AiMark size="sm" />110              </span>111            }112            description="Facilities and projects with AI / HPC evidence (confirmed, likely or associated) — published MW only."113            aside={<MoreLink href={routes.ai()}>AI index</MoreLink>}114          >115            <div className="grid grid-cols-3 divide-x divide-hair [&>*+*]:pl-3">116              <Cell label="AI facilities" value={d ? fmtInt(d.aiExpansion.facilities) : "—"} href={routes.datacenters("ai=1")} />117              <Cell label="AI projects" value={d ? fmtInt(d.aiExpansion.projects) : "—"} href={routes.projects("ai=1")} />118              <Cell label="Planned (≈)" value={aiMw?.value ?? "—"} unit={aiMw?.unit ?? "MW"} note="published figures only" />119            </div>120            <div className="mt-3">121              <ProjectList items={(d?.aiExpansion.recent ?? []).slice(0, 5)} emptyTitle="No AI project yet" emptyDescription="Projects with AI / HPC evidence will list here." />122            </div>123          </Section>124125          {/* ---------------------------------------------------------------- construction pipeline */}126          <Section title="Construction pipeline" description="Projects by lifecycle stage with the sum of published MW — company-wide totals are never summed." aside={<MoreLink href={routes.projects()}>Pipeline</MoreLink>}>127            {d ? <PipelineFunnel data={{ byStatus: d.projectsByStatus, openedLast12m: null }} compact /> : <EmptyState compact title="Pipeline unavailable" />}128          </Section>129130          {/* ---------------------------------------------------------------- fastest-growing markets */}131          <Section title="Fastest-growing markets" description="Momentum components over 12 months, shown separately — never collapsed into a score." aside={<MoreLink href={routes.markets()}>Markets</MoreLink>}>132            {d?.fastestGrowingMetros.length ? (133              <Table dense>134                <THead>135                  <tr>136                    <Th>Market</Th>137                    <Th numeric>Announced</Th>138                    <Th numeric>In constr.</Th>139                    <Th numeric className="hidden sm:table-cell">140                      Announced MW141                    </Th>142                    <Th numeric className="hidden md:table-cell">143                      Entrants144                    </Th>145                    <Th numeric className="hidden md:table-cell">146                      Grid ev.147                    </Th>148                  </tr>149                </THead>150                <TBody>151                  {d.fastestGrowingMetros.slice(0, 8).map((m) => (152                    <Tr key={m.id}>153                      <Td primary>154                        <Link href={routes.market(m.slug)} className="inline-flex items-center gap-1.5 font-medium text-ink hover:text-accent hover:underline">155                          <CountryFlag iso2={m.countryIso2} size={12} /> {m.name}156                        </Link>157                      </Td>158                      <Td label="Announced" numeric lead>159                        {fmtInt(m.momentum.projectsAnnounced)}160                      </Td>161                      <Td label="In construction" numeric>162                        {fmtInt(m.momentum.projectsEnteredConstruction)}163                      </Td>164                      <Td label="Announced MW" numeric className="hidden sm:table-cell">165                        {m.momentum.announcedMw !== null ? `≈ ${fmtMw(m.momentum.announcedMw)}` : "—"}166                      </Td>167                      <Td label="New entrants" numeric className="hidden md:table-cell">168                        {fmtInt(m.momentum.newEntrants.length)}169                      </Td>170                      <Td label="Grid events" numeric className="hidden md:table-cell">171                        {fmtInt(m.momentum.gridEvents)}172                      </Td>173                    </Tr>174                  ))}175                </TBody>176              </Table>177            ) : (178              <EmptyState compact title="Momentum not computed yet" />179            )}180          </Section>181182          {/* ---------------------------------------------------------------- major projects */}183          <Section title="Major projects" description="Site-scoped published planned capacity of 100 MW or more (≈ as announced)." aside={<MoreLink href={routes.projects("min_mw=100&sort=mw")}>All ≥ 100 MW</MoreLink>}>184            <ProjectList items={major.slice(0, 8)} emptyTitle="No ≥ 100 MW project with a site-scoped figure" emptyDescription="Company-wide totals are kept as evidence and never shown as a project's MW." />185          </Section>186187          {/* ---------------------------------------------------------------- operator expansion */}188          <Section title="Operator expansion" description="Operators entering new countries or markets, with projects announced over 12 months." aside={<MoreLink href={routes.operators()}>Operators</MoreLink>}>189            {d?.operatorExpansion.length ? (190              <ul className="divide-y divide-hair">191                {d.operatorExpansion.slice(0, 8).map((o) => (192                  <li key={o.operator.id} className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-3 py-2">193                    <div className="min-w-0">194                      <Link href={routes.operator(o.operator.slug)} className="text-[13.5px] font-medium text-ink hover:text-accent hover:underline">195                        {o.operator.name}196                      </Link>197                      <div className="mt-0.5 truncate text-[12px] text-ink-3">198                        {o.newCountries.length ? (199                          <span className="inline-flex items-center gap-1">200                            {o.newCountries.slice(0, 6).map((c) => (201                              <CountryFlag key={c} iso2={c} size={11} />202                            ))}203                            {o.newCountries.length > 6 && <span>+{o.newCountries.length - 6}</span>}204                          </span>205                        ) : null}206                        {o.newMetros.length > 0 && <span className="ml-1.5">{o.newMetros.slice(0, 3).join(", ")}{o.newMetros.length > 3 ? ` +${o.newMetros.length - 3}` : ""}</span>}207                      </div>208                    </div>209                    <div className="figure text-right text-[12px] text-ink-2">210                      <div>211                        {fmtInt(o.newCountries.length)} <span className="text-ink-4">countries</span>212                      </div>213                      <div>214                        {fmtInt(o.newMetros.length)} <span className="text-ink-4">markets</span> · {fmtInt(o.projects12m)} <span className="text-ink-4">prj</span>215                      </div>216                    </div>217                  </li>218                ))}219              </ul>220            ) : (221              <EmptyState compact title="No expansion detected yet" />222            )}223          </Section>224225          {/* ---------------------------------------------------------------- power / grid */}226          <Section title="Power / grid events" description="Power agreements, utility events and public grid constraints — attached to markets or countries, never to a site." aside={<MoreLink href={routes.power()}>Power & grid</MoreLink>}>227            <EventList events={(d?.powerEvents ?? []).slice(0, 5)} dense emptyTitle="No power event yet" />228            {d?.gridConstraints.length ? (229              <div className="mt-3 border-t border-hair pt-3">230                <div className="label-caps mb-1.5">Grid constraints</div>231                <ul className="divide-y divide-hair text-[12.5px]">232                  {d.gridConstraints.slice(0, 4).map((g) => (233                    <li key={g.id} className="flex flex-wrap items-baseline gap-x-2 py-1.5">234                      <Badge tone="outline" size="xs">235                        {g.kind.replace(/_/g, " ")}236                      </Badge>237                      <a href={g.url} target="_blank" rel="noopener noreferrer" className="min-w-0 flex-1 truncate font-medium text-ink hover:text-accent">238                        {g.title}239                      </a>240                      {g.sourceKind && <SourceBadge kind={g.sourceKind} name={g.sourceName} url={g.url} />}241                      <span className="figure text-[11px] text-ink-3">{fmtDate(g.effectiveDate)}</span>242                    </li>243                  ))}244                </ul>245              </div>246            ) : null}247          </Section>248249          {/* ---------------------------------------------------------------- cloud / connectivity */}250          <Section title="Cloud / connectivity" description="Public cloud regions and internet exchanges — providers publish the metro, not the building." aside={<MoreLink href={routes.connectivity()}>Connectivity</MoreLink>}>251            <div className="grid grid-cols-3 divide-x divide-hair [&>*+*]:pl-3">252              <Cell label="Cloud regions" value={stats ? fmtInt(stats.cloudRegions) : "—"} href={routes.cloudRegions()} />253              <Cell label="Internet exchanges" value={stats ? fmtInt(stats.ixps) : "—"} href={routes.ixps()} />254              <Cell label="Connectivity known" value={cov ? fmtPct(cov.connectivityCoverage) : "—"} note="facilities with carriers / IXPs" href={routes.coverage()} />255            </div>256            <div className="mt-3">257              <div className="label-caps mb-2">Largest markets · known operational MW</div>258              {d?.topMetros?.length ? (259                <BarList260                  items={d.topMetros.slice(0, 6).map((m) => ({ rank: m.rank, label: <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={m.countryIso2} size={12} />{m.name}</span>, value: m.value, href: m.href || routes.market(m.slug), coverage: m.coverage, secondary: m.coverage != null ? `${fmtPct(m.coverage)} MW coverage` : undefined, display: fmtMw(m.value) }))}261                />262              ) : (263                <EmptyState compact title="Rankings not computed yet" />264              )}265            </div>266          </Section>267        </div>268269        {/* ---------------------------------------------------------------- capacity over time */}270        <Section title="Known operational capacity over time" description="Cumulative published MW by opening year — only facilities with both a figure and an opening date; earlier years are lower bounds." aside={<MoreLink href={routes.timeMachine()}>Time machine</MoreLink>}>271          <CapacityOverTime data={d?.capacityOverTime ?? []} />272        </Section>273274        {/* ---------------------------------------------------------------- coverage · transparency · ingestion */}275        <div className="grid gap-x-8 gap-y-8 border-t border-hair pt-6 lg:grid-cols-3">276          <div>277            <div className="mb-2 flex items-baseline justify-between">278              <h2 className="text-[14px] font-semibold text-ink">Data coverage</h2>279              <MoreLink href={routes.coverage()}>Coverage</MoreLink>280            </div>281            {cov ? (282              <dl className="divide-y divide-hair text-[12.5px]">283                {[284                  ["Capacity (MW) published", cov.capacityCoverage],285                  ["Operator known", cov.operatorCoverage],286                  ["Precise location", cov.preciseLocationCoverage],287                  ["Any location", cov.anyLocationCoverage],288                  ["Status known", cov.statusCoverage],289                  ["Opening date known", cov.openingDateCoverage],290                  ["Multiple sources", cov.multiSourceCoverage],291                  ["Connectivity known", cov.connectivityCoverage],292                ].map(([k, v]) => (293                  <div key={String(k)} className="flex items-center justify-between gap-3 py-1.5">294                    <dt className="text-ink-2">{k}</dt>295                    <dd className="flex items-center gap-2">296                      <span className="inline-block h-[3px] w-16 bg-surface-3">297                        <span className="block h-full bg-ink-2" style={{ width: `${Math.max(1, Math.round((v as number) * 100))}%` }} />298                      </span>299                      <span className="figure w-10 text-right text-ink">{fmtPct(v as number, (v as number) < 0.1 ? 1 : 0)}</span>300                    </dd>301                  </div>302                ))}303              </dl>304            ) : (305              <EmptyState compact title="Coverage unavailable" />306            )}307            <p className="mt-2 text-[11px] text-ink-4">Share of {cov ? fmtInt(cov.facilities) : ""} indexed facilities. Gaps are shown, never filled.</p>308          </div>309          <div>310            <div className="mb-2 flex items-baseline justify-between">311              <h2 className="text-[14px] font-semibold text-ink">Source transparency</h2>312              <MoreLink href={routes.sources()}>Sources</MoreLink>313            </div>314            <div className="grid grid-cols-2 divide-x divide-hair [&>*+*]:pl-3">315              <Cell label="Sources" value={stats ? fmtInt(stats.sources) : "—"} href={routes.sources()} />316              <Cell label="Archived documents" value={stats ? fmtInt(stats.documents) : "—"} />317              <Cell label="Primary-source share" value={cov ? fmtPct(cov.primarySourceShare) : "—"} note="operator, government, utility, filing" />318              <Cell label="Projects located" value={cov ? `${fmtInt(cov.projectsWithLocation)} / ${fmtInt(cov.projects)}` : "—"} note="with published coordinates" />319            </div>320            <p className="mt-2 text-[12px] text-ink-3">321              Every field carries its source, dates, confidence and extraction method; every figure opens an evidence drawer with the supporting sentence.{" "}322              <Link href={routes.methodology()} className="underline decoration-hair-2 hover:text-ink">323                Methodology324              </Link>{" "}325              ·{" "}326              <Link href={routes.download()} className="underline decoration-hair-2 hover:text-ink">327                Downloads328              </Link>{" "}329              ·{" "}330              <Link href={routes.apiDocsPage()} className="underline decoration-hair-2 hover:text-ink">331                API332              </Link>333            </p>334          </div>335          <div>336            <div className="mb-2 flex items-baseline justify-between">337              <h2 className="text-[14px] font-semibold text-ink">Latest ingestion</h2>338              <MoreLink href={routes.live()}>Live feed</MoreLink>339            </div>340            {ing ? (341              <>342                <div className="grid grid-cols-2 divide-x divide-hair [&>*+*]:pl-3">343                  <Cell label="Last run" value={ing.lastRunAt ? fmtDate(ing.lastRunAt) : "—"} note={ing.lastRunAt ? "UTC" : undefined} />344                  <Cell label="Runs · 24 h" value={fmtInt(ing.runs24h)} />345                  <Cell label="Documents · 24 h" value={fmtInt(ing.documents24h)} note="fetched, archived, diffed" />346                  <Cell label="Connectors healthy" value={`${fmtInt(ing.connectorsHealthy)} / ${fmtInt(ing.connectorsTotal)}`} note={ing.connectorsTotal ? fmtPct(ing.connectorsHealthy / ing.connectorsTotal) : undefined} />347                </div>348                <p className="mt-2 text-[12px] text-ink-3">349                  {fmtInt(ing.events24h)} events detected in 24 h. Pipeline: discover → fetch → archive → extract → normalize → reconcile → validate → version → detect changes → publish.{" "}350                  <Link href={routes.bot()} className="inline-flex items-center gap-1 underline decoration-hair-2 hover:text-ink">351                    Crawler policy <ExternalLink className="size-3" />352                  </Link>353                </p>354              </>355            ) : (356              <EmptyState compact title="Ingestion status unavailable" />357            )}358          </div>359        </div>360      </div>361362      <p className="mt-8 border-t border-hair pt-4 text-[11.5px] text-ink-3">363        {stats ? <>MW figures are published for {fmtPct(stats.mwCoverage)} of indexed facilities; every total is a lower bound computed from sourced figures — nothing is extrapolated or imputed. </> : <>Totals are lower bounds computed from sourced figures only; nothing is extrapolated. </>}364        <Link href={routes.methodology()} className="text-ink-2 underline decoration-hair-2 hover:text-ink">365          How the index is built366        </Link>367        .368      </p>369    </Page>370  );371}372