SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
5 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
10.4 KB · 218 lines tsx
Raw Blame History
1import type { ApiEndpointDoc } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { ExtLink, KeyValueList } from "@/components/entity/kv";6import { SectionNav } from "@/components/entity/section-nav";7import { SourcesFooter } from "@/components/entity/sources-footer";8import { CodeTabs } from "@/components/intelligence/developers/code-tabs";9import { Badge } from "@/components/ui/badge";10import { Button } from "@/components/ui/button";11import { Page, PageHeader, Section } from "@/components/ui/card";12import { EmptyState, OfflineNotice } from "@/components/ui/states";13import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";14import { api } from "@/lib/api";15import { fmtInt } from "@/lib/format";16import { routes, SITE_URL } from "@/lib/routes";17import { pageMetadata } from "@/lib/seo";1819export const revalidate = 600;2021export const metadata: Metadata = pageMetadata({22  title: "API",23  description: "DataCenterIndex public REST API: every endpoint with parameters, response types and copy-ready curl / JavaScript / Python examples. Envelope { data, meta, sources }, weak ETags, per-IP rate limits, OpenAPI 3.1.",24  path: routes.apiDocsPage(),25});2627const GROUP_LABEL: Record<string, string> = {28  facilities: "Facilities",29  projects: "Projects",30  operators: "Operators",31  countries: "Countries",32  metros: "Markets",33  "cloud-regions": "Cloud regions",34  ixps: "Internet exchanges",35  events: "Events",36  rankings: "Rankings",37  dashboard: "Dashboard",38  stats: "Stats",39  map: "Map",40  search: "Search",41  nearby: "Nearby",42  explore: "Explore",43  compare: "Compare",44  ai: "AI infrastructure",45  pulse: "Pulse",46  power: "Power",47  connectivity: "Connectivity",48  coverage: "Coverage",49  "time-machine": "Time machine",50  sources: "Sources",51  download: "Download",52  watchlist: "Watchlist",53  sitemap: "Sitemap",54  docs: "Docs",55};56const GROUP_ORDER = ["facilities", "projects", "operators", "countries", "metros", "cloud-regions", "ixps", "events", "rankings", "dashboard", "stats", "pulse", "ai", "power", "connectivity", "coverage", "explore", "compare", "search", "map", "nearby", "time-machine", "sources", "download", "watchlist", "sitemap", "docs"];5758const linkCls = "text-ink underline decoration-hair-2 hover:text-accent";5960function rewrite(s: string): string {61  return s.replace(/https?:\/\/localhost:8310/g, SITE_URL).replace(/https?:\/\/127\.0\.0\.1:8311/g, SITE_URL);62}6364function MethodBadge({ method }: { method: ApiEndpointDoc["method"] }) {65  return (66    <Badge tone={method === "GET" ? "ok" : method === "POST" ? "accent" : "danger"} size="xs" mono>67      {method}68    </Badge>69  );70}7172export default async function DevelopersPage() {73  const r = await api.docsMeta();74  const docs = r.ok ? r.data : [];75  const groups = Array.from(new Set(docs.map((d) => d.group))).sort((a, b) => {76    const ia = GROUP_ORDER.indexOf(a);77    const ib = GROUP_ORDER.indexOf(b);78    return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);79  });80  const byGroup = new Map(groups.map((g) => [g, docs.filter((d) => d.group === g).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))]));8182  return (83    <Page>84      <Breadcrumbs items={[{ name: "API", href: routes.apiDocsPage() }]} className="pt-3" />85      <PageHeader86        eyebrow="Developers"87        title="Public API"88        description={`${docs.length ? `${fmtInt(docs.length)} endpoints. ` : ""}Read-only JSON over HTTPS, the same data the site renders — every figure carries its sources.`}89        className="pt-3"90        aside={91          <div className="flex flex-wrap gap-2">92            <Button size="sm" href={routes.apiDocs()} external>93              Swagger UI94            </Button>95            <Button size="sm" variant="ghost" href="/api/v1/openapi.json" external>96              OpenAPI 3.1 JSON97            </Button>98          </div>99        }100      />101      {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}102103      <div className="rounded-sm border border-hair bg-surface px-3 py-3 sm:px-4">104        <KeyValueList105          items={[106            { label: "Base URL", value: <span className="figure text-[12.5px]">{SITE_URL}/api/v1</span> },107            {108              label: "Envelope",109              value: (110                <span>111                  Every response is <span className="figure text-[12.5px]">{"{ data, meta, sources }"}</span> — <em>data</em> is the typed payload, <em>meta</em> carries pagination (<span className="figure text-[12px]">total, page, perPage</span>), <span className="figure text-[12px]">generatedAt</span> and a <span className="figure text-[12px]">methodology</span> sentence when a figure needs one, <em>sources</em> lists the sources (licence, attribution) behind the rows. Downloads stream without an envelope.112                </span>113              ),114            },115            { label: "Caching", value: "Weak ETags + s-maxage on every GET — send If-None-Match and expect 304 on unchanged data; lists revalidate within minutes, the change feed within 30 s." },116            { label: "Rate limits", value: "Rate-limited per IP; the current window is reported in the x-ratelimit-limit and x-ratelimit-remaining response headers. Bulk use should go through the download endpoints." },117            { label: "Errors", value: <span className="figure text-[12.5px]">{'{ "error": "…", "statusCode": 400 | 404 | 429 }'}</span> },118            { label: "Partial dates", value: <span className="figure text-[12.5px]">2027 · 2027-06 · 2027-06-15 · 2027-Q2 · 2027-H1</span> },119            { label: "Honesty", value: "null means not published — never zero. Estimates carry isEstimate; MW figures come with capacityScope / capacitySemantics; coverage fields say how much of an aggregate is backed by a figure." },120          ]}121        />122        <p className="mt-2 text-[11.5px] text-ink-3">123          Types are named after the shared contract (<span className="figure">packages/core/src/api-types.ts</span>); the <Link href={routes.methodology()} className={linkCls}>methodology</Link> explains how each value is produced. Re-use terms per source on <Link href={routes.sources()} className={linkCls}>/sources</Link>.124        </p>125      </div>126127      {docs.length === 0 ? (128        <EmptyState className="mt-6" title="Endpoint catalogue unavailable" description="The API did not return /docs-meta; the Swagger UI link above still works when the API is up." />129      ) : (130        <>131          <SectionNav items={groups.map((g) => ({ id: `g-${g}`, label: GROUP_LABEL[g] ?? g, count: byGroup.get(g)?.length ?? 0 }))} className="mt-4" />132          <div className="mt-6 flex flex-col gap-10">133            {groups.map((g) => (134              <Section key={g} id={`g-${g}`} title={GROUP_LABEL[g] ?? g} description={`${byGroup.get(g)?.length ?? 0} ${(byGroup.get(g)?.length ?? 0) === 1 ? "endpoint" : "endpoints"}`} className="scroll-mt-28">135                <div className="flex flex-col gap-6">136                  {(byGroup.get(g) ?? []).map((d) => (137                    <Endpoint key={`${d.method} ${d.path}`} d={d} />138                  ))}139                </div>140              </Section>141            ))}142          </div>143        </>144      )}145146      <SourcesFooter sources={r.ok ? r.sources : undefined} note="The API serves reconciled, validated records with their provenance; the same limits (published figures only, coverage caveats) apply to every payload." />147    </Page>148  );149}150151function Endpoint({ d }: { d: ApiEndpointDoc }) {152  const id = `${d.method}-${d.path.replace(/[^a-z0-9]+/gi, "-")}`;153  return (154    <article id={id} className="rounded-sm border border-hair bg-surface" aria-labelledby={`${id}-h`}>155      <div className="flex flex-wrap items-center gap-2 border-b border-hair px-3 py-2.5 sm:px-4">156        <MethodBadge method={d.method} />157        <h3 id={`${id}-h`} className="figure min-w-0 break-all text-[13.5px] font-semibold text-ink">158          {d.path}159        </h3>160        <span className="ml-auto figure text-[11.5px] text-ink-3" title="Response type (data)">161          → {d.responseSchema}162        </span>163      </div>164      <div className="grid gap-4 px-3 py-3 sm:px-4 lg:grid-cols-2">165        <div className="min-w-0">166          <p className="text-[13px] leading-relaxed text-ink-2">{d.summary}</p>167          {d.params.length > 0 ? (168            <div className="mt-3">169              <Table dense>170                <THead>171                  <tr>172                    <Th>Parameter</Th>173                    <Th className="hidden sm:table-cell">In</Th>174                    <Th>Type</Th>175                    <Th className="hidden md:table-cell">Description</Th>176                  </tr>177                </THead>178                <TBody>179                  {d.params.map((p) => (180                    <Tr key={`${p.in}-${p.name}`}>181                      <Td primary>182                        <span className="figure text-[12.5px] font-medium">{p.name}</span>183                        {p.example && (184                          <div className="figure text-[11px] text-ink-4 md:hidden">185                            e.g. {p.example}186                          </div>187                        )}188                      </Td>189                      <Td label="In" className="hidden text-[12px] text-ink-3 sm:table-cell">190                        {p.in}191                      </Td>192                      <Td label="Type" className="figure text-[12px] text-ink-2">193                        {p.type}194                      </Td>195                      <Td label="Description" className="hidden text-[12px] text-ink-2 md:table-cell">196                        {p.description || <span className="text-ink-4">—</span>}197                        {p.example && <span className="figure text-ink-4"> · e.g. {p.example}</span>}198                      </Td>199                    </Tr>200                  ))}201                </TBody>202              </Table>203            </div>204          ) : (205            <p className="mt-2 text-[11.5px] text-ink-4">No parameters.</p>206          )}207          <p className="mt-2 text-[11.5px] text-ink-3">208            Try it: <ExtLink href={`${SITE_URL}${d.path.replace(/:[a-z]+/gi, (m) => (d.params.find((p) => p.in === "path" && `:${p.name}` === m)?.example ?? m))}`} className="figure text-[11.5px]" />209          </p>210        </div>211        <div className="min-w-0">212          <CodeTabs id={d.path} example={{ curl: rewrite(d.example.curl), js: rewrite(d.example.js), python: rewrite(d.example.python) }} />213        </div>214      </div>215    </article>216  );217}218