SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
2 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%

Web 2.0: visual language (scope/semantics/source/significance/stage badges), evidence drawer + claims/capacity history/nearby/data quality, facility page 3.0, project page + pipeline views (table/map/timeline/kanban, funnel), map 2.0 (modes rail, density, time machine, inspector, mobile sheets), home 2.0 (ticker, what changed, funnel, momentum, expansion, power, coverage, ingestion), nav More menu + mobile nav, operator 2.0 + compare, market 2.0 (concentration/momentum), country 2.0 (energy context), AI index, pulse, live feed 2.0, explore, coverage, power, connectivity, download, developers, watchlist, rankings 2.0, methodology claims sections, admin health center/quality/data-gaps/duplicate workbench/extraction debugger/projects/runs rollback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 20 days ago (Sep 12, 2026) parent b2cd712

361 changed files +11,340 −6,729

modified apps/web/next-env.d.ts +2 −2
@@ -1,7 +1,7 @@
1 1 /// <reference types="next" />
2 2 /// <reference types="next/image-types/global" />
3 −import "./.next/dev/types/routes.d.ts";
4 −import "./.next/dev/types/root-params.d.ts";
3 +import "./.next/types/routes.d.ts";
4 +import "./.next/types/root-params.d.ts";
5 5
6 6 // NOTE: This file should not be edited
7 7 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
added apps/web/qa/admin-sweep.mjs +107 −0
@@ -0,0 +1,107 @@
1 +// Admin console sweep: node apps/web/qa/admin-sweep.mjs (Playwright from ~/Desktop/uqo-eval)
2 +// env: BASE (default http://localhost:8314), TOKEN (dev-admin-token), OUT (default qa/sweep-admin), PAGES=name,name, WIDTHS=390,1280
3 +// Logs in through /admin/login (httpOnly cookie), then screenshots every admin page and reports page-level horizontal
4 +// overflow (wide tables must scroll inside their own container, never the page).
5 +import { createRequire } from "node:module";
6 +import { mkdirSync, writeFileSync } from "node:fs";
7 +import { dirname, resolve } from "node:path";
8 +import { fileURLToPath } from "node:url";
9 +
10 +const require = createRequire("/Users/simon-pierreboucher/Desktop/uqo-eval/package.json");
11 +const { chromium } = require("playwright");
12 +const here = dirname(fileURLToPath(import.meta.url));
13 +const OUT = process.env.OUT ?? resolve(here, "sweep-admin");
14 +mkdirSync(OUT, { recursive: true });
15 +const BASE = process.env.BASE ?? "http://localhost:8314";
16 +const TOKEN = process.env.TOKEN ?? "dev-admin-token";
17 +const DOC = process.env.DOC ?? "doc_8b4ef3d4844a957b";
18 +const RUN = process.env.RUN ?? "run_4ke18ugp5yd3";
19 +
20 +const PAGES = [
21 + ["connectors", "/admin/connectors"],
22 + ["connectors-full", "/admin/connectors?cols=full"],
23 + ["quality", "/admin/quality"],
24 + ["data-gaps", "/admin/data-gaps"],
25 + ["matches", "/admin/matches"],
26 + ["projects", "/admin/projects"],
27 + ["document-trace", `/admin/documents/${DOC}?tab=trace`],
28 + ["document-fixture", `/admin/documents/${DOC}?fixture=1`],
29 + ["run", `/admin/runs/${RUN}`],
30 +];
31 +const ONLY = process.env.PAGES ? new Set(process.env.PAGES.split(",")) : null;
32 +const WIDTHS = (process.env.WIDTHS ?? "390,1280").split(",").map(Number);
33 +
34 +function audit() {
35 + const doc = document.documentElement;
36 + const vw = doc.clientWidth;
37 + const wide = [];
38 + for (const el of document.querySelectorAll("body *")) {
39 + const r = el.getBoundingClientRect();
40 + if (r.width > 0 && r.right > vw + 1 && getComputedStyle(el).position !== "fixed") {
41 + // inside a horizontally scrolling container is fine
42 + let p = el.parentElement;
43 + let scrolls = false;
44 + while (p && p !== document.body) {
45 + const cs = getComputedStyle(p);
46 + if ((cs.overflowX === "auto" || cs.overflowX === "scroll") && p.scrollWidth > p.clientWidth + 1) {
47 + scrolls = true;
48 + break;
49 + }
50 + p = p.parentElement;
51 + }
52 + if (!scrolls) wide.push(`${el.tagName.toLowerCase()}.${(typeof el.className === "string" ? el.className : "").split(" ").filter(Boolean).slice(0, 3).join(".")}:+${Math.round(r.right - vw)}`);
53 + }
54 + }
55 + const overflow = doc.scrollWidth > doc.clientWidth ? { scrollWidth: doc.scrollWidth, clientWidth: doc.clientWidth, wide: wide.slice(0, 6) } : null;
56 + const h1 = document.querySelectorAll("h1").length;
57 + const alerts = Array.from(document.querySelectorAll("[role=alert]")).map((a) => a.textContent.trim().slice(0, 120));
58 + return { overflow, h1, alerts, height: doc.scrollHeight, title: document.title };
59 +}
60 +
61 +const browser = await chromium.launch();
62 +const results = [];
63 +for (const w of WIDTHS) {
64 + const mobile = w < 700;
65 + const ctx = await browser.newContext({ viewport: { width: w, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, locale: "en-CA", timezoneId: "America/Toronto" });
66 + const page = await ctx.newPage();
67 + // login
68 + await page.goto(`${BASE}/admin/login`, { waitUntil: "load", timeout: 90000 });
69 + await page.fill('input[name="token"]', TOKEN);
70 + await page.click('button[type="submit"]');
71 + await page.waitForURL((u) => u.pathname.startsWith("/admin") && !u.pathname.startsWith("/admin/login"), { timeout: 60000 }).catch((e) => console.log("login failed", String(e).slice(0, 100)));
72 + for (const [name, path] of PAGES) {
73 + if (ONLY && !ONLY.has(name)) continue;
74 + const errors = [];
75 + const onConsole = (m) => {
76 + if (m.type() === "error") errors.push(m.text().slice(0, 200));
77 + };
78 + page.on("console", onConsole);
79 + const row = { name, path, w };
80 + try {
81 + const resp = await page.goto(`${BASE}${path}`, { waitUntil: "load", timeout: 90000 });
82 + row.status = resp?.status();
83 + await page.waitForLoadState("networkidle", { timeout: 20000 }).catch(() => {});
84 + await page.waitForTimeout(name.startsWith("connectors") ? 6000 : 1200);
85 + await page.waitForFunction(() => !document.querySelector(".animate-pulse-soft"), null, { timeout: 40000 }).catch(() => {});
86 + Object.assign(row, await page.evaluate(audit));
87 + await page.screenshot({ path: resolve(OUT, `${name}-${w}.png`), fullPage: row.height < 16000, timeout: 90000 });
88 + } catch (e) {
89 + row.error = String(e).slice(0, 200);
90 + }
91 + page.off("console", onConsole);
92 + row.errors = [...new Set(errors)];
93 + results.push(row);
94 + const flags = [];
95 + if (row.overflow) flags.push(`OVERFLOW ${JSON.stringify(row.overflow.wide)}`);
96 + if (row.h1 !== 1) flags.push(`h1×${row.h1}`);
97 + if (row.alerts?.length) flags.push(`alerts:${row.alerts.length}`);
98 + if (row.errors.length) flags.push(`console:${row.errors.length}`);
99 + if (row.error) flags.push("ERROR");
100 + console.log(`${String(w).padStart(4)} ${name.padEnd(18)} ${String(row.status ?? "").padEnd(3)} h=${String(row.height ?? "").padStart(5)} ${flags.length ? flags.join(" ") : "ok"}`);
101 + if (row.alerts?.length) for (const a of row.alerts) console.log(` alert: ${a}`);
102 + }
103 + await ctx.close();
104 +}
105 +await browser.close();
106 +writeFileSync(resolve(OUT, "results.json"), JSON.stringify({ base: BASE, at: new Date().toISOString(), results }, null, 1));
107 +console.log(`wrote ${OUT}/results.json`);
added apps/web/qa/shot.mjs +46 −0
@@ -0,0 +1,46 @@
1 +// Viewport screenshot helper: node apps/web/qa/shot.mjs <path> <name> [--w=1280] [--h=900] [--dark] [--scroll=#id] [--click=selector] [--wait=ms] [--full]
2 +// Writes qa/shots/<name>-<w>.png. Playwright from ~/Desktop/uqo-eval.
3 +import { createRequire } from "node:module";
4 +import { mkdirSync } from "node:fs";
5 +import { dirname, resolve } from "node:path";
6 +import { fileURLToPath } from "node:url";
7 +
8 +const require = createRequire("/Users/simon-pierreboucher/Desktop/uqo-eval/package.json");
9 +const { chromium } = require("playwright");
10 +const here = dirname(fileURLToPath(import.meta.url));
11 +const OUT = resolve(here, "shots");
12 +mkdirSync(OUT, { recursive: true });
13 +const BASE = process.env.BASE ?? "http://localhost:8314";
14 +
15 +const [path, name, ...flags] = process.argv.slice(2);
16 +const opt = Object.fromEntries(flags.map((f) => { const m = /^--([^=]+)=?(.*)$/.exec(f); return [m[1], m[2] === "" ? true : m[2]]; }));
17 +const w = Number(opt.w ?? 1280);
18 +const h = Number(opt.h ?? (w < 700 ? 844 : 900));
19 +const mobile = w < 700;
20 +const browser = await chromium.launch();
21 +const ctx = await browser.newContext({ viewport: { width: w, height: h }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: opt.dark ? "dark" : "light", locale: "en-CA" });
22 +await ctx.addInitScript((theme) => { try { window.localStorage.setItem("theme", theme); } catch {} }, opt.dark ? "dark" : "light");
23 +const page = await ctx.newPage();
24 +const errors = [];
25 +page.on("pageerror", (e) => errors.push(String(e).slice(0, 200)));
26 +page.on("console", (m) => { if (m.type() === "error") errors.push(m.text().slice(0, 200)); });
27 +await page.goto(`${BASE}${path}`, { waitUntil: "load", timeout: 90000 });
28 +await page.waitForLoadState("networkidle", { timeout: 15000 }).catch(() => {});
29 +await page.waitForTimeout(Number(opt.wait ?? 800));
30 +if (opt.scroll) {
31 + await page.evaluate((sel) => document.querySelector(sel)?.scrollIntoView({ block: "start" }), opt.scroll);
32 + await page.waitForTimeout(400);
33 +}
34 +if (opt.click) {
35 + const clicks = String(opt.click).split("||");
36 + for (const c of clicks) {
37 + await page.locator(c).first().click({ timeout: 5000 }).catch((e) => errors.push(`click failed ${c}: ${String(e).slice(0, 80)}`));
38 + await page.waitForTimeout(500);
39 + }
40 +}
41 +if (opt.key) { await page.keyboard.press(String(opt.key)); await page.waitForTimeout(300); }
42 +const file = resolve(OUT, `${name}-${w}${opt.dark ? "-dark" : ""}.png`);
43 +await page.screenshot({ path: file, fullPage: !!opt.full });
44 +const overflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth ? `${document.documentElement.scrollWidth}>${document.documentElement.clientWidth}` : null);
45 +console.log(file, overflow ? `OVERFLOW ${overflow}` : "no-overflow", errors.length ? `errors: ${errors.join(" | ")}` : "");
46 +await browser.close();
added apps/web/qa/shots/fac-capacity-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/fac-dark-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/shots/fac-drawer-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/fac-drawer-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/fac-google-quality-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/shots/fac-top-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/fac-top-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-bottom-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-coverage-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-dark-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-mid-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-mid-390-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-mobile-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-more-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-pulse-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-top-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/home-what-changed-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/live-check-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-dark-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-density-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-desk-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-mobile-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-points-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-projects-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/map-year-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-filters-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-index-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-kanban-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-lifecycle-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-map-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-mobile-nearby-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-timeline-390.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-top-1280.png +0 −0

Binary file not shown.

added apps/web/qa/shots/prj-top-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/cloud-region-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/cloud-region-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/cloud-region-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/cloud-region-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-empty-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-empty-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-empty-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/compare-empty-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/ixp-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/ixp-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/ixp-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/ixp-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/operator-equinix-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/operator-equinix-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/operator-equinix-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/operator-equinix-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a1/results.json +320 −0
@@ -0,0 +1,320 @@
1 +{
2 + "base": "http://localhost:8314",
3 + "at": "2026-09-12T05:56:56.727Z",
4 + "results": [
5 + {
6 + "name": "cloud-region",
7 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
8 + "w": 390,
9 + "theme": "light",
10 + "status": 200,
11 + "ttfb": 135,
12 + "overflow": null,
13 + "clipped": [
14 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
15 + "span.sr-only \"Canada\" 47>1"
16 + ],
17 + "small": [
18 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
19 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
20 + "a.hover:text-ink.hover:underline 79×17 \"Cloud regions\"",
21 + "a.hover:text-ink.hover:underline 128×17 \"Amazon Web Services\"",
22 + "a.hover:text-ink.hover:underline 47×19 \"Canada\"",
23 + "a.relative.z-[1].inline-flex 123×24 \"Amazon Web Services\"",
24 + "a.inline-flex.min-h-6.items-center 179×24 \"AWS regions and Local Zo\"",
25 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
26 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
27 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
28 + ],
29 + "smallCount": 10,
30 + "h1Covered": false,
31 + "h1Top": 120,
32 + "headerBottom": 49,
33 + "lang": "en",
34 + "metaDesc": 165,
35 + "h1Count": 1,
36 + "skipped": [],
37 + "unnamed": [],
38 + "ariaCurrent": 0,
39 + "footerOk": true,
40 + "contrast": [],
41 + "contrastChecked": 0,
42 + "height": 4416,
43 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
44 + "bottom": {
45 + "navVisible": true,
46 + "covered": false,
47 + "gap": 1403
48 + },
49 + "errors": [],
50 + "badResponses": []
51 + },
52 + {
53 + "name": "compare",
54 + "path": "/compare/operators?slugs=equinix,digital-realty",
55 + "w": 390,
56 + "theme": "light",
57 + "status": 200,
58 + "ttfb": 86,
59 + "overflow": null,
60 + "clipped": [
61 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
62 + ],
63 + "small": [
64 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
65 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
66 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
67 + "input.h-full.min-w-0.flex-1 336×38 \"Search operators to comp\"",
68 + "a.block.truncate.text-[12.5px] 179×18 \"Digital Realty Zurich ca\"",
69 + "a.block.truncate.text-[12.5px] 179×18 \"With Armada For AI Data \"",
70 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
71 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
72 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
73 + ],
74 + "smallCount": 9,
75 + "h1Covered": false,
76 + "h1Top": 120,
77 + "headerBottom": 49,
78 + "lang": "en",
79 + "metaDesc": 204,
80 + "h1Count": 1,
81 + "skipped": [],
82 + "unnamed": [],
83 + "ariaCurrent": 0,
84 + "footerOk": true,
85 + "contrast": [],
86 + "contrastChecked": 0,
87 + "height": 4394,
88 + "title": "Compare operators · DataCenterIndex",
89 + "bottom": {
90 + "navVisible": true,
91 + "covered": false,
92 + "gap": 1403
93 + },
94 + "errors": [],
95 + "badResponses": []
96 + },
97 + {
98 + "name": "cloud-region",
99 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
100 + "w": 1280,
101 + "theme": "light",
102 + "status": 200,
103 + "ttfb": 547,
104 + "overflow": null,
105 + "clipped": [
106 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
107 + "span.sr-only \"Canada\" 47>1"
108 + ],
109 + "small": [],
110 + "smallCount": 0,
111 + "h1Covered": false,
112 + "h1Top": 136,
113 + "headerBottom": 57,
114 + "lang": "en",
115 + "metaDesc": 165,
116 + "h1Count": 1,
117 + "skipped": [],
118 + "unnamed": [],
119 + "ariaCurrent": 0,
120 + "footerOk": true,
121 + "contrast": [],
122 + "contrastChecked": 0,
123 + "height": 2818,
124 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
125 + "errors": [],
126 + "badResponses": []
127 + },
128 + {
129 + "name": "compare",
130 + "path": "/compare/operators?slugs=equinix,digital-realty",
131 + "w": 1280,
132 + "theme": "light",
133 + "status": 200,
134 + "ttfb": 485,
135 + "overflow": null,
136 + "clipped": [
137 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
138 + "span.sr-only \"United States\" 70>1",
139 + "span.sr-only \"United States\" 70>1"
140 + ],
141 + "small": [],
142 + "smallCount": 0,
143 + "h1Covered": false,
144 + "h1Top": 136,
145 + "headerBottom": 57,
146 + "lang": "en",
147 + "metaDesc": 204,
148 + "h1Count": 1,
149 + "skipped": [],
150 + "unnamed": [],
151 + "ariaCurrent": 0,
152 + "footerOk": true,
153 + "contrast": [],
154 + "contrastChecked": 0,
155 + "height": 2442,
156 + "title": "Compare operators · DataCenterIndex",
157 + "errors": [],
158 + "badResponses": []
159 + },
160 + {
161 + "name": "cloud-region",
162 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
163 + "w": 390,
164 + "theme": "dark",
165 + "status": 200,
166 + "ttfb": 237,
167 + "overflow": null,
168 + "clipped": [
169 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
170 + "span.sr-only \"Canada\" 47>1"
171 + ],
172 + "small": [
173 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
174 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
175 + "a.hover:text-ink.hover:underline 79×17 \"Cloud regions\"",
176 + "a.hover:text-ink.hover:underline 128×17 \"Amazon Web Services\"",
177 + "a.hover:text-ink.hover:underline 47×19 \"Canada\"",
178 + "a.relative.z-[1].inline-flex 123×24 \"Amazon Web Services\"",
179 + "a.inline-flex.min-h-6.items-center 179×24 \"AWS regions and Local Zo\"",
180 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
181 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
182 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
183 + ],
184 + "smallCount": 10,
185 + "h1Covered": false,
186 + "h1Top": 120,
187 + "headerBottom": 49,
188 + "lang": "en",
189 + "metaDesc": 165,
190 + "h1Count": 1,
191 + "skipped": [],
192 + "unnamed": [],
193 + "ariaCurrent": 0,
194 + "footerOk": true,
195 + "contrast": [],
196 + "contrastChecked": 156,
197 + "height": 4436,
198 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
199 + "bottom": {
200 + "navVisible": true,
201 + "covered": false,
202 + "gap": 1403
203 + },
204 + "errors": [
205 + "pageerror SyntaxError: Invalid or unexpected token"
206 + ],
207 + "badResponses": []
208 + },
209 + {
210 + "name": "compare",
211 + "path": "/compare/operators?slugs=equinix,digital-realty",
212 + "w": 390,
213 + "theme": "dark",
214 + "status": 200,
215 + "ttfb": 107,
216 + "overflow": null,
217 + "clipped": [
218 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
219 + ],
220 + "small": [
221 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
222 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
223 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
224 + "input.h-full.min-w-0.flex-1 336×38 \"Search operators to comp\"",
225 + "a.block.truncate.text-[12.5px] 179×18 \"Digital Realty Zurich ca\"",
226 + "a.block.truncate.text-[12.5px] 179×18 \"With Armada For AI Data \"",
227 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
228 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
229 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
230 + ],
231 + "smallCount": 9,
232 + "h1Covered": false,
233 + "h1Top": 120,
234 + "headerBottom": 49,
235 + "lang": "en",
236 + "metaDesc": 204,
237 + "h1Count": 1,
238 + "skipped": [],
239 + "unnamed": [],
240 + "ariaCurrent": 0,
241 + "footerOk": true,
242 + "contrast": [],
243 + "contrastChecked": 152,
244 + "height": 4394,
245 + "title": "Compare operators · DataCenterIndex",
246 + "bottom": {
247 + "navVisible": true,
248 + "covered": false,
249 + "gap": 1403
250 + },
251 + "errors": [
252 + "pageerror SyntaxError: Invalid or unexpected token"
253 + ],
254 + "badResponses": []
255 + },
256 + {
257 + "name": "cloud-region",
258 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
259 + "w": 1280,
260 + "theme": "dark",
261 + "status": 200,
262 + "ttfb": 407,
263 + "overflow": null,
264 + "clipped": [
265 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
266 + "span.sr-only \"Canada\" 47>1"
267 + ],
268 + "small": [],
269 + "smallCount": 0,
270 + "h1Covered": false,
271 + "h1Top": 136,
272 + "headerBottom": 57,
273 + "lang": "en",
274 + "metaDesc": 165,
275 + "h1Count": 1,
276 + "skipped": [],
277 + "unnamed": [],
278 + "ariaCurrent": 0,
279 + "footerOk": true,
280 + "contrast": [],
281 + "contrastChecked": 208,
282 + "height": 2818,
283 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
284 + "errors": [],
285 + "badResponses": []
286 + },
287 + {
288 + "name": "compare",
289 + "path": "/compare/operators?slugs=equinix,digital-realty",
290 + "w": 1280,
291 + "theme": "dark",
292 + "status": 200,
293 + "ttfb": 245,
294 + "overflow": null,
295 + "clipped": [
296 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
297 + "span.sr-only \"United States\" 70>1",
298 + "span.sr-only \"United States\" 70>1"
299 + ],
300 + "small": [],
301 + "smallCount": 0,
302 + "h1Covered": false,
303 + "h1Top": 136,
304 + "headerBottom": 57,
305 + "lang": "en",
306 + "metaDesc": 204,
307 + "h1Count": 1,
308 + "skipped": [],
309 + "unnamed": [],
310 + "ariaCurrent": 0,
311 + "footerOk": true,
312 + "contrast": [],
313 + "contrastChecked": 157,
314 + "height": 2442,
315 + "title": "Compare operators · DataCenterIndex",
316 + "errors": [],
317 + "badResponses": []
318 + }
319 + ]
320 +}
\ No newline at end of file
added apps/web/qa/sweep-a2/country-ca-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-ca-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-ca-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-ca-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-us-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-us-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-us-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/country-us-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/market-nova-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/market-nova-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/market-nova-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/market-nova-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/results.json +363 −0
@@ -0,0 +1,363 @@
1 +{
2 + "base": "http://localhost:8314",
3 + "at": "2026-09-12T05:58:18.141Z",
4 + "results": [
5 + {
6 + "name": "country-us",
7 + "path": "/countries/united-states",
8 + "w": 390,
9 + "theme": "light",
10 + "status": 200,
11 + "ttfb": 1428,
12 + "overflow": null,
13 + "clipped": [
14 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
15 + "span.sr-only \"United States\" 149>1",
16 + "span.sr-only \"United States\" 76>1",
17 + "span.sr-only \"United States\" 76>1",
18 + "span.sr-only \"United States\" 76>1",
19 + "span.sr-only \"United States\" 76>1",
20 + "span.sr-only \"United States\" 76>1",
21 + "span.sr-only \"United States\" 76>1",
22 + "span.sr-only \"United States\" 76>1"
23 + ],
24 + "small": [
25 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
26 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
27 + "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
28 + "a.dci-rowlink.font-medium.text-ink 186×18 \"Red Oak Data Center (DFW\"",
29 + "a.dci-rowlink.font-medium.text-ink 193×35 \"Granite Point Data Cente\"",
30 + "a.truncate.text-[13.5px].font-medium 244×20 \"Comparing Space-Driven D\"",
31 + "a.relative.z-[1].inline-flex 41×24 \"Google\"",
32 + "a.truncate.text-[13.5px].font-medium 104×20 \"Acre AI Campus\"",
33 + "a.relative.z-[1].inline-flex 128×24 \"Amazon Web Services\"",
34 + "a.truncate.text-[13.5px].font-medium 253×20 \"PJM's Power Shortfall Pu\"",
35 + "a.truncate.text-[13.5px].font-medium 97×20 \"GW AI Campus\"",
36 + "a.relative.z-[1].inline-flex 41×24 \"Google\""
37 + ],
38 + "smallCount": 126,
39 + "h1Covered": false,
40 + "h1Top": 120,
41 + "headerBottom": 49,
42 + "lang": "en",
43 + "metaDesc": 298,
44 + "h1Count": 1,
45 + "skipped": [],
46 + "unnamed": [],
47 + "ariaCurrent": 1,
48 + "footerOk": true,
49 + "contrast": [],
50 + "contrastChecked": 0,
51 + "height": 18292,
52 + "title": "Data Centers in United States · DataCenterIndex",
53 + "bottom": {
54 + "navVisible": true,
55 + "covered": false,
56 + "gap": 1402
57 + },
58 + "inter": {
59 + "showAll": {
60 + "before": 10,
61 + "after": 10,
62 + "expanded": false
63 + }
64 + },
65 + "errors": [
66 + "pageerror SyntaxError: Invalid or unexpected token",
67 + "Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead."
68 + ],
69 + "badResponses": []
70 + },
71 + {
72 + "name": "market-nova",
73 + "path": "/datacenters/markets/northern-virginia",
74 + "w": 390,
75 + "theme": "light",
76 + "status": 200,
77 + "ttfb": 798,
78 + "overflow": null,
79 + "clipped": [
80 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
81 + "span.sr-only \"United States\" 82>1",
82 + "span.sr-only \"United States\" 76>1",
83 + "span.sr-only \"United States\" 89>1",
84 + "span.sr-only \"United States\" 89>1",
85 + "span.sr-only \"United States\" 89>1",
86 + "span.sr-only \"United States\" 89>1",
87 + "span.sr-only \"United States\" 89>1",
88 + "span.sr-only \"United States\" 89>1"
89 + ],
90 + "small": [
91 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
92 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
93 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
94 + "a.hover:text-ink.hover:underline 46×17 \"Markets\"",
95 + "a.text-ink.hover:text-accent.hover:underline 82×19 \"United States\"",
96 + "a.inline-flex.h-10.items-center 38×40 \"Map\"",
97 + "a.mt-1.inline-flex.min-h-6 234×24 \"Loudoun County, Virginia\"",
98 + "a.mt-1.inline-flex.min-h-6 126×24 \"Data Center Frontier\"",
99 + "a.truncate.text-[13.5px].font-medium 147×20 \"Tract Ashburn campus\"",
100 + "a.relative.z-[1].inline-flex 29×24 \"Tract\"",
101 + "a.relative.z-[1].inline-flex 119×24 \"Vantage Data Centers\"",
102 + "a.inline-flex.min-h-6.items-center 135×24 \"Vantage Data Centers\""
103 + ],
104 + "smallCount": 63,
105 + "h1Covered": false,
106 + "h1Top": 120,
107 + "headerBottom": 49,
108 + "lang": "en",
109 + "metaDesc": 298,
110 + "h1Count": 1,
111 + "skipped": [],
112 + "unnamed": [],
113 + "ariaCurrent": 2,
114 + "footerOk": true,
115 + "contrast": [],
116 + "contrastChecked": 0,
117 + "height": 13605,
118 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
119 + "bottom": {
120 + "navVisible": true,
121 + "covered": false,
122 + "gap": 1402
123 + },
124 + "errors": [],
125 + "badResponses": []
126 + },
127 + {
128 + "name": "country-us",
129 + "path": "/countries/united-states",
130 + "w": 1280,
131 + "theme": "light",
132 + "status": 200,
133 + "ttfb": 2470,
134 + "overflow": null,
135 + "clipped": [
136 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
137 + "span.sr-only \"United States\" 187>1",
138 + "span.sr-only \"United States\" 76>1",
139 + "span.sr-only \"United States\" 76>1",
140 + "span.sr-only \"United States\" 76>1",
141 + "span.sr-only \"United States\" 76>1",
142 + "span.sr-only \"United States\" 76>1",
143 + "span.sr-only \"United States\" 76>1",
144 + "span.sr-only \"United States\" 76>1"
145 + ],
146 + "small": [],
147 + "smallCount": 0,
148 + "h1Covered": false,
149 + "h1Top": 136,
150 + "headerBottom": 57,
151 + "lang": "en",
152 + "metaDesc": 298,
153 + "h1Count": 1,
154 + "skipped": [],
155 + "unnamed": [],
156 + "ariaCurrent": 1,
157 + "footerOk": true,
158 + "contrast": [],
159 + "contrastChecked": 0,
160 + "height": 17817,
161 + "title": "Data Centers in United States · DataCenterIndex",
162 + "errors": [],
163 + "badResponses": []
164 + },
165 + {
166 + "name": "market-nova",
167 + "path": "/datacenters/markets/northern-virginia",
168 + "w": 1280,
169 + "theme": "light",
170 + "error": "Error: page.goto: Navigation to \"http://localhost:8314/datacenters/markets/northern-virginia\" is interrupted by another navigation to \"http://localhost:8314/countries/united-states\"\nCall log:\n\u001b[2m - ",
171 + "errors": [],
172 + "badResponses": []
173 + },
174 + {
175 + "name": "country-us",
176 + "path": "/countries/united-states",
177 + "w": 390,
178 + "theme": "dark",
179 + "status": 200,
180 + "ttfb": 2075,
181 + "overflow": null,
182 + "clipped": [
183 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
184 + "span.sr-only \"United States\" 149>1",
185 + "span.sr-only \"United States\" 76>1",
186 + "span.sr-only \"United States\" 76>1",
187 + "span.sr-only \"United States\" 76>1",
188 + "span.sr-only \"United States\" 76>1",
189 + "span.sr-only \"United States\" 76>1",
190 + "span.sr-only \"United States\" 76>1",
191 + "span.sr-only \"United States\" 76>1"
192 + ],
193 + "small": [
194 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
195 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
196 + "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
197 + "a.dci-rowlink.font-medium.text-ink 186×18 \"Red Oak Data Center (DFW\"",
198 + "a.dci-rowlink.font-medium.text-ink 193×35 \"Granite Point Data Cente\"",
199 + "a.truncate.text-[13.5px].font-medium 244×20 \"Comparing Space-Driven D\"",
200 + "a.relative.z-[1].inline-flex 41×24 \"Google\"",
201 + "a.truncate.text-[13.5px].font-medium 104×20 \"Acre AI Campus\"",
202 + "a.relative.z-[1].inline-flex 128×24 \"Amazon Web Services\"",
203 + "a.truncate.text-[13.5px].font-medium 253×20 \"PJM's Power Shortfall Pu\"",
204 + "a.truncate.text-[13.5px].font-medium 97×20 \"GW AI Campus\"",
205 + "a.relative.z-[1].inline-flex 41×24 \"Google\""
206 + ],
207 + "smallCount": 126,
208 + "h1Covered": false,
209 + "h1Top": 120,
210 + "headerBottom": 49,
211 + "lang": "en",
212 + "metaDesc": 298,
213 + "h1Count": 1,
214 + "skipped": [],
215 + "unnamed": [],
216 + "ariaCurrent": 1,
217 + "footerOk": true,
218 + "contrast": [],
219 + "contrastChecked": 1079,
220 + "height": 18233,
221 + "title": "Data Centers in United States · DataCenterIndex",
222 + "bottom": {
223 + "navVisible": true,
224 + "covered": false,
225 + "gap": 1403
226 + },
227 + "errors": [],
228 + "badResponses": []
229 + },
230 + {
231 + "name": "market-nova",
232 + "path": "/datacenters/markets/northern-virginia",
233 + "w": 390,
234 + "theme": "dark",
235 + "status": 200,
236 + "ttfb": 616,
237 + "overflow": null,
238 + "clipped": [
239 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
240 + "span.sr-only \"United States\" 82>1",
241 + "span.sr-only \"United States\" 76>1",
242 + "span.sr-only \"United States\" 89>1",
243 + "span.sr-only \"United States\" 89>1",
244 + "span.sr-only \"United States\" 89>1",
245 + "span.sr-only \"United States\" 89>1",
246 + "span.sr-only \"United States\" 89>1",
247 + "span.sr-only \"United States\" 89>1"
248 + ],
249 + "small": [
250 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
251 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
252 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
253 + "a.hover:text-ink.hover:underline 46×17 \"Markets\"",
254 + "a.text-ink.hover:text-accent.hover:underline 82×19 \"United States\"",
255 + "a.inline-flex.h-10.items-center 38×40 \"Map\"",
256 + "a.mt-1.inline-flex.min-h-6 234×24 \"Loudoun County, Virginia\"",
257 + "a.mt-1.inline-flex.min-h-6 126×24 \"Data Center Frontier\"",
258 + "a.truncate.text-[13.5px].font-medium 147×20 \"Tract Ashburn campus\"",
259 + "a.relative.z-[1].inline-flex 29×24 \"Tract\"",
260 + "a.relative.z-[1].inline-flex 119×24 \"Vantage Data Centers\"",
261 + "a.inline-flex.min-h-6.items-center 135×24 \"Vantage Data Centers\""
262 + ],
263 + "smallCount": 63,
264 + "h1Covered": false,
265 + "h1Top": 120,
266 + "headerBottom": 49,
267 + "lang": "en",
268 + "metaDesc": 298,
269 + "h1Count": 1,
270 + "skipped": [],
271 + "unnamed": [],
272 + "ariaCurrent": 2,
273 + "footerOk": true,
274 + "contrast": [],
275 + "contrastChecked": 712,
276 + "height": 13605,
277 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
278 + "bottom": {
279 + "navVisible": true,
280 + "covered": false,
281 + "gap": 1402
282 + },
283 + "errors": [],
284 + "badResponses": []
285 + },
286 + {
287 + "name": "country-us",
288 + "path": "/countries/united-states",
289 + "w": 1280,
290 + "theme": "dark",
291 + "status": 200,
292 + "ttfb": 1171,
293 + "overflow": null,
294 + "clipped": [
295 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
296 + "span.sr-only \"United States\" 187>1",
297 + "span.sr-only \"United States\" 76>1",
298 + "span.sr-only \"United States\" 76>1",
299 + "span.sr-only \"United States\" 76>1",
300 + "span.sr-only \"United States\" 76>1",
301 + "span.sr-only \"United States\" 76>1",
302 + "span.sr-only \"United States\" 76>1",
303 + "span.sr-only \"United States\" 76>1"
304 + ],
305 + "small": [],
306 + "smallCount": 0,
307 + "h1Covered": false,
308 + "h1Top": 136,
309 + "headerBottom": 57,
310 + "lang": "en",
311 + "metaDesc": 298,
312 + "h1Count": 1,
313 + "skipped": [],
314 + "unnamed": [],
315 + "ariaCurrent": 1,
316 + "footerOk": true,
317 + "contrast": [],
318 + "contrastChecked": 1501,
319 + "height": 17817,
320 + "title": "Data Centers in United States · DataCenterIndex",
321 + "errors": [],
322 + "badResponses": []
323 + },
324 + {
325 + "name": "market-nova",
326 + "path": "/datacenters/markets/northern-virginia",
327 + "w": 1280,
328 + "theme": "dark",
329 + "status": 200,
330 + "ttfb": 839,
331 + "overflow": null,
332 + "clipped": [
333 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
334 + "span.sr-only \"United States\" 82>1",
335 + "span.sr-only \"United States\" 76>1",
336 + "span.sr-only \"United States\" 89>1",
337 + "span.sr-only \"United States\" 89>1",
338 + "span.sr-only \"United States\" 89>1",
339 + "span.sr-only \"United States\" 89>1",
340 + "span.sr-only \"United States\" 89>1",
341 + "span.sr-only \"United States\" 89>1"
342 + ],
343 + "small": [],
344 + "smallCount": 0,
345 + "h1Covered": false,
346 + "h1Top": 136,
347 + "headerBottom": 57,
348 + "lang": "en",
349 + "metaDesc": 298,
350 + "h1Count": 1,
351 + "skipped": [],
352 + "unnamed": [],
353 + "ariaCurrent": 2,
354 + "footerOk": true,
355 + "contrast": [],
356 + "contrastChecked": 985,
357 + "height": 10015,
358 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
359 + "errors": [],
360 + "badResponses": []
361 + }
362 + ]
363 +}
\ No newline at end of file
added apps/web/qa/sweep-a2/sec-country-ai-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-ai-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-connectivity-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-connectivity-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-energy-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-energy-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-grid-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-grid-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-operators-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-operators-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-pipeline-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-country-pipeline-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-concentration-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-concentration-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-coverage-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-coverage-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-momentum-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-momentum-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-operators-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-operators-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-pipeline-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-pipeline-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-power-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-a2/sec-market-power-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/connectors-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/connectors-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/connectors-full-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/connectors-full-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/data-gaps-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/data-gaps-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/document-fixture-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/document-fixture-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/document-trace-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/document-trace-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/matches-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/matches-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/projects-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/projects-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/quality-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/quality-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/results.json +134 −0
@@ -0,0 +1,134 @@
1 +{
2 + "base": "http://localhost:8314",
3 + "at": "2026-09-12T06:15:51.093Z",
4 + "results": [
5 + {
6 + "name": "connectors",
7 + "path": "/admin/connectors",
8 + "w": 390,
9 + "status": 200,
10 + "overflow": null,
11 + "h1": 1,
12 + "alerts": [],
13 + "height": 6797,
14 + "title": "Connectors · Admin · DataCenterIndex",
15 + "errors": []
16 + },
17 + {
18 + "name": "connectors-full",
19 + "path": "/admin/connectors?cols=full",
20 + "w": 390,
21 + "status": 200,
22 + "overflow": null,
23 + "h1": 1,
24 + "alerts": [],
25 + "height": 6797,
26 + "title": "Connectors · Admin · DataCenterIndex",
27 + "errors": []
28 + },
29 + {
30 + "name": "matches",
31 + "path": "/admin/matches",
32 + "w": 390,
33 + "status": 200,
34 + "overflow": null,
35 + "h1": 1,
36 + "alerts": [],
37 + "height": 20927,
38 + "title": "Matches · Admin · DataCenterIndex",
39 + "errors": []
40 + },
41 + {
42 + "name": "document-trace",
43 + "path": "/admin/documents/doc_8b4ef3d4844a957b?tab=trace",
44 + "w": 390,
45 + "status": 200,
46 + "overflow": null,
47 + "h1": 1,
48 + "alerts": [
49 + "The maintenance worker did not answer (worker unreachable: fetch failed). The trace is a dry run executed by the worker "
50 + ],
51 + "height": 844,
52 + "title": "doc_8b4ef3d4844a957b · Document · Admin · DataCenterIndex",
53 + "errors": [
54 + "Failed to load resource: the server responded with a status of 502 (Bad Gateway)"
55 + ]
56 + },
57 + {
58 + "name": "document-fixture",
59 + "path": "/admin/documents/doc_8b4ef3d4844a957b?fixture=1",
60 + "w": 390,
61 + "status": 200,
62 + "overflow": null,
63 + "h1": 1,
64 + "alerts": [],
65 + "height": 4510,
66 + "title": "doc_8b4ef3d4844a957b · Document · Admin · DataCenterIndex",
67 + "errors": []
68 + },
69 + {
70 + "name": "connectors",
71 + "path": "/admin/connectors",
72 + "w": 1280,
73 + "status": 200,
74 + "overflow": null,
75 + "h1": 1,
76 + "alerts": [],
77 + "height": 6150,
78 + "title": "Connectors · Admin · DataCenterIndex",
79 + "errors": []
80 + },
81 + {
82 + "name": "connectors-full",
83 + "path": "/admin/connectors?cols=full",
84 + "w": 1280,
85 + "status": 200,
86 + "overflow": null,
87 + "h1": 1,
88 + "alerts": [],
89 + "height": 6150,
90 + "title": "Connectors · Admin · DataCenterIndex",
91 + "errors": []
92 + },
93 + {
94 + "name": "matches",
95 + "path": "/admin/matches",
96 + "w": 1280,
97 + "status": 200,
98 + "overflow": null,
99 + "h1": 1,
100 + "alerts": [],
101 + "height": 10139,
102 + "title": "Matches · Admin · DataCenterIndex",
103 + "errors": []
104 + },
105 + {
106 + "name": "document-trace",
107 + "path": "/admin/documents/doc_8b4ef3d4844a957b?tab=trace",
108 + "w": 1280,
109 + "status": 200,
110 + "overflow": null,
111 + "h1": 1,
112 + "alerts": [
113 + "The maintenance worker did not answer (worker unreachable: fetch failed). The trace is a dry run executed by the worker "
114 + ],
115 + "height": 900,
116 + "title": "doc_8b4ef3d4844a957b · Document · Admin · DataCenterIndex",
117 + "errors": [
118 + "Failed to load resource: the server responded with a status of 502 (Bad Gateway)"
119 + ]
120 + },
121 + {
122 + "name": "document-fixture",
123 + "path": "/admin/documents/doc_8b4ef3d4844a957b?fixture=1",
124 + "w": 1280,
125 + "status": 200,
126 + "overflow": null,
127 + "h1": 1,
128 + "alerts": [],
129 + "height": 2835,
130 + "title": "doc_8b4ef3d4844a957b · Document · Admin · DataCenterIndex",
131 + "errors": []
132 + }
133 + ]
134 +}
\ No newline at end of file
added apps/web/qa/sweep-admin/run-1280.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-admin/run-390.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/ai-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/ai-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/ai-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/ai-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/connectivity-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/connectivity-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/connectivity-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/connectivity-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-sources-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-sources-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-sources-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/coverage-sources-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/power-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/power-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/power-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/power-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-30d-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-30d-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-30d-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-30d-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/pulse-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-b/results.json +420 −0
@@ -0,0 +1,420 @@
1 +{
2 + "base": "http://localhost:8314",
3 + "at": "2026-09-12T05:59:16.303Z",
4 + "results": [
5 + {
6 + "name": "pulse",
7 + "path": "/pulse",
8 + "w": 390,
9 + "theme": "light",
10 + "status": 200,
11 + "ttfb": 454,
12 + "overflow": null,
13 + "clipped": [
14 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
15 + "span.sr-only \"Ukraine\" 47>1",
16 + "span.sr-only \"Belarus\" 46>1",
17 + "span.sr-only \"Russia\" 40>1",
18 + "span.sr-only \"Russia\" 40>1",
19 + "span.sr-only \"North Macedonia\" 106>1",
20 + "span.sr-only \"Russia\" 40>1",
21 + "span.sr-only \"Iran\" 23>1",
22 + "span.sr-only \"Iraq\" 24>1"
23 + ],
24 + "small": [
25 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
26 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
27 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
28 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
29 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
30 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
31 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
32 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
33 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
34 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
35 + "a.relative.z-[1].inline-flex 49×24 \"TierPoint\"",
36 + "a.inline-flex.min-h-6.items-center 65×24 \"TierPoint\""
37 + ],
38 + "smallCount": 35,
39 + "h1Covered": false,
40 + "h1Top": 120,
41 + "headerBottom": 49,
42 + "lang": "en",
43 + "metaDesc": 227,
44 + "h1Count": 1,
45 + "skipped": [],
46 + "unnamed": [],
47 + "ariaCurrent": 1,
48 + "footerOk": true,
49 + "contrast": [],
50 + "contrastChecked": 0,
51 + "height": 8882,
52 + "title": "Pulse · DataCenterIndex",
53 + "bottom": {
54 + "navVisible": true,
55 + "covered": false,
56 + "gap": 1403
57 + },
58 + "errors": [],
59 + "badResponses": []
60 + },
61 + {
62 + "name": "coverage-sources",
63 + "path": "/coverage/sources",
64 + "w": 390,
65 + "theme": "light",
66 + "status": 200,
67 + "ttfb": 750,
68 + "overflow": null,
69 + "clipped": [
70 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
71 + ],
72 + "small": [
73 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
74 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
75 + "a.hover:text-ink.hover:underline 55×17 \"Coverage\"",
76 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
77 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
78 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
79 + ],
80 + "smallCount": 6,
81 + "h1Covered": false,
82 + "h1Top": 120,
83 + "headerBottom": 49,
84 + "lang": "en",
85 + "metaDesc": 189,
86 + "h1Count": 1,
87 + "skipped": [],
88 + "unnamed": [],
89 + "ariaCurrent": 0,
90 + "footerOk": true,
91 + "contrast": [],
92 + "contrastChecked": 0,
93 + "height": 2964,
94 + "title": "Source coverage · DataCenterIndex",
95 + "bottom": {
96 + "navVisible": true,
97 + "covered": false,
98 + "gap": 1402
99 + },
100 + "errors": [],
101 + "badResponses": []
102 + },
103 + {
104 + "name": "power",
105 + "path": "/power",
106 + "w": 390,
107 + "theme": "light",
108 + "status": 200,
109 + "ttfb": 843,
110 + "overflow": null,
111 + "clipped": [
112 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
113 + "span.sr-only \"United States\" 73>1",
114 + "span.sr-only \"United Kingdom\" 87>1",
115 + "span.sr-only \"United States\" 73>1",
116 + "span.sr-only \"United States\" 73>1",
117 + "span.sr-only \"United States\" 89>1",
118 + "span.sr-only \"United Kingdom\" 106>1",
119 + "span.sr-only \"United States\" 89>1",
120 + "span.sr-only \"United States\" 89>1"
121 + ],
122 + "small": [
123 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
124 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
125 + "a.mt-1.inline-flex.min-h-6 174×24 \"Entergy — newsroom· Util\"",
126 + "a.mt-1.inline-flex.min-h-6 111×24 \"Switch· Operator\"",
127 + "a.mt-1.inline-flex.min-h-6 111×24 \"Switch· Operator\"",
128 + "a.mt-1.inline-flex.min-h-6 188×24 \"Stream Data Centers· Ope\"",
129 + "a.mt-1.inline-flex.min-h-6 149×24 \"Global Switch· Operator\"",
130 + "a.mt-1.inline-flex.min-h-6 149×24 \"Global Switch· Operator\"",
131 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\"",
132 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\"",
133 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\"",
134 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\""
135 + ],
136 + "smallCount": 94,
137 + "h1Covered": false,
138 + "h1Top": 120,
139 + "headerBottom": 49,
140 + "lang": "en",
141 + "metaDesc": 211,
142 + "h1Count": 1,
143 + "skipped": [],
144 + "unnamed": [],
145 + "ariaCurrent": 0,
146 + "footerOk": true,
147 + "contrast": [],
148 + "contrastChecked": 0,
149 + "height": 15060,
150 + "title": "Power & grid · DataCenterIndex",
151 + "bottom": {
152 + "navVisible": true,
153 + "covered": false,
154 + "gap": 1403
155 + },
156 + "errors": [],
157 + "badResponses": []
158 + },
159 + {
160 + "name": "pulse",
161 + "path": "/pulse",
162 + "w": 390,
163 + "theme": "dark",
164 + "status": 200,
165 + "ttfb": 387,
166 + "overflow": null,
167 + "clipped": [
168 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
169 + "span.sr-only \"Ukraine\" 47>1",
170 + "span.sr-only \"Belarus\" 46>1",
171 + "span.sr-only \"Russia\" 40>1",
172 + "span.sr-only \"Russia\" 40>1",
173 + "span.sr-only \"North Macedonia\" 106>1",
174 + "span.sr-only \"Russia\" 40>1",
175 + "span.sr-only \"Iran\" 23>1",
176 + "span.sr-only \"Iraq\" 24>1"
177 + ],
178 + "small": [
179 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
180 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
181 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
182 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
183 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
184 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
185 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
186 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
187 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
188 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
189 + "a.relative.z-[1].inline-flex 49×24 \"TierPoint\"",
190 + "a.inline-flex.min-h-6.items-center 65×24 \"TierPoint\""
191 + ],
192 + "smallCount": 35,
193 + "h1Covered": false,
194 + "h1Top": 120,
195 + "headerBottom": 49,
196 + "lang": "en",
197 + "metaDesc": 227,
198 + "h1Count": 1,
199 + "skipped": [],
200 + "unnamed": [],
201 + "ariaCurrent": 1,
202 + "footerOk": true,
203 + "contrast": [],
204 + "contrastChecked": 493,
205 + "height": 8882,
206 + "title": "Pulse · DataCenterIndex",
207 + "bottom": {
208 + "navVisible": true,
209 + "covered": false,
210 + "gap": 1403
211 + },
212 + "errors": [],
213 + "badResponses": []
214 + },
215 + {
216 + "name": "coverage-sources",
217 + "path": "/coverage/sources",
218 + "w": 390,
219 + "theme": "dark",
220 + "status": 200,
221 + "ttfb": 721,
222 + "overflow": null,
223 + "clipped": [
224 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
225 + ],
226 + "small": [
227 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
228 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
229 + "a.hover:text-ink.hover:underline 55×17 \"Coverage\"",
230 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
231 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
232 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
233 + ],
234 + "smallCount": 6,
235 + "h1Covered": false,
236 + "h1Top": 120,
237 + "headerBottom": 49,
238 + "lang": "en",
239 + "metaDesc": 189,
240 + "h1Count": 1,
241 + "skipped": [],
242 + "unnamed": [],
243 + "ariaCurrent": 0,
244 + "footerOk": true,
245 + "contrast": [],
246 + "contrastChecked": 105,
247 + "height": 2964,
248 + "title": "Source coverage · DataCenterIndex",
249 + "bottom": {
250 + "navVisible": true,
251 + "covered": false,
252 + "gap": 1402
253 + },
254 + "errors": [],
255 + "badResponses": []
256 + },
257 + {
258 + "name": "power",
259 + "path": "/power",
260 + "w": 390,
261 + "theme": "dark",
262 + "status": 200,
263 + "ttfb": 781,
264 + "overflow": null,
265 + "clipped": [
266 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
267 + "span.sr-only \"United States\" 73>1",
268 + "span.sr-only \"United Kingdom\" 87>1",
269 + "span.sr-only \"United States\" 73>1",
270 + "span.sr-only \"United States\" 73>1",
271 + "span.sr-only \"United States\" 89>1",
272 + "span.sr-only \"United Kingdom\" 106>1",
273 + "span.sr-only \"United States\" 89>1",
274 + "span.sr-only \"United States\" 89>1"
275 + ],
276 + "small": [
277 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
278 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
279 + "a.mt-1.inline-flex.min-h-6 174×24 \"Entergy — newsroom· Util\"",
280 + "a.mt-1.inline-flex.min-h-6 111×24 \"Switch· Operator\"",
281 + "a.mt-1.inline-flex.min-h-6 111×24 \"Switch· Operator\"",
282 + "a.mt-1.inline-flex.min-h-6 188×24 \"Stream Data Centers· Ope\"",
283 + "a.mt-1.inline-flex.min-h-6 149×24 \"Global Switch· Operator\"",
284 + "a.mt-1.inline-flex.min-h-6 149×24 \"Global Switch· Operator\"",
285 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\"",
286 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\"",
287 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\"",
288 + "a.mt-1.inline-flex.min-h-6 160×24 \"Element Critical· Operat\""
289 + ],
290 + "smallCount": 94,
291 + "h1Covered": false,
292 + "h1Top": 120,
293 + "headerBottom": 49,
294 + "lang": "en",
295 + "metaDesc": 211,
296 + "h1Count": 1,
297 + "skipped": [],
298 + "unnamed": [],
299 + "ariaCurrent": 0,
300 + "footerOk": true,
301 + "contrast": [],
302 + "contrastChecked": 733,
303 + "height": 15060,
304 + "title": "Power & grid · DataCenterIndex",
305 + "bottom": {
306 + "navVisible": true,
307 + "covered": false,
308 + "gap": 1403
309 + },
310 + "errors": [],
311 + "badResponses": []
312 + },
313 + {
314 + "name": "pulse",
315 + "path": "/pulse",
316 + "w": 1280,
317 + "theme": "dark",
318 + "status": 200,
319 + "ttfb": 380,
320 + "overflow": null,
321 + "clipped": [
322 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
323 + "span.sr-only \"Ukraine\" 47>1",
324 + "span.sr-only \"Belarus\" 46>1",
325 + "span.sr-only \"Russia\" 40>1",
326 + "span.sr-only \"Russia\" 40>1",
327 + "span.sr-only \"North Macedonia\" 106>1",
328 + "span.sr-only \"Russia\" 40>1",
329 + "span.sr-only \"Iran\" 23>1",
330 + "span.sr-only \"Iraq\" 24>1"
331 + ],
332 + "small": [],
333 + "smallCount": 0,
334 + "h1Covered": false,
335 + "h1Top": 136,
336 + "headerBottom": 57,
337 + "lang": "en",
338 + "metaDesc": 227,
339 + "h1Count": 1,
340 + "skipped": [],
341 + "unnamed": [],
342 + "ariaCurrent": 1,
343 + "footerOk": true,
344 + "contrast": [],
345 + "contrastChecked": 539,
346 + "height": 5800,
347 + "title": "Pulse · DataCenterIndex",
348 + "errors": [],
349 + "badResponses": []
350 + },
351 + {
352 + "name": "coverage-sources",
353 + "path": "/coverage/sources",
354 + "w": 1280,
355 + "theme": "dark",
356 + "status": 200,
357 + "ttfb": 646,
358 + "overflow": null,
359 + "clipped": [
360 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
361 + ],
362 + "small": [],
363 + "smallCount": 0,
364 + "h1Covered": false,
365 + "h1Top": 136,
366 + "headerBottom": 57,
367 + "lang": "en",
368 + "metaDesc": 189,
369 + "h1Count": 1,
370 + "skipped": [],
371 + "unnamed": [],
372 + "ariaCurrent": 0,
373 + "footerOk": true,
374 + "contrast": [],
375 + "contrastChecked": 911,
376 + "height": 6816,
377 + "title": "Source coverage · DataCenterIndex",
378 + "errors": [],
379 + "badResponses": []
380 + },
381 + {
382 + "name": "power",
383 + "path": "/power",
384 + "w": 1280,
385 + "theme": "dark",
386 + "status": 200,
387 + "ttfb": 752,
388 + "overflow": null,
389 + "clipped": [
390 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
391 + "span.sr-only \"United States\" 73>1",
392 + "span.sr-only \"United Kingdom\" 87>1",
393 + "span.sr-only \"United States\" 73>1",
394 + "span.sr-only \"United States\" 73>1",
395 + "span.sr-only \"United States\" 89>1",
396 + "span.sr-only \"United Kingdom\" 106>1",
397 + "span.sr-only \"United States\" 89>1",
398 + "span.sr-only \"United States\" 89>1"
399 + ],
400 + "small": [],
401 + "smallCount": 0,
402 + "h1Covered": false,
403 + "h1Top": 136,
404 + "headerBottom": 57,
405 + "lang": "en",
406 + "metaDesc": 211,
407 + "h1Count": 1,
408 + "skipped": [],
409 + "unnamed": [],
410 + "ariaCurrent": 0,
411 + "footerOk": true,
412 + "contrast": [],
413 + "contrastChecked": 1501,
414 + "height": 13817,
415 + "title": "Power & grid · DataCenterIndex",
416 + "errors": [],
417 + "badResponses": []
418 + }
419 + ]
420 +}
\ No newline at end of file
added apps/web/qa/sweep-c/developers-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/developers-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/developers-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/developers-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/download-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/download-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/download-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/download-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-projects-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-projects-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-projects-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/explore-projects-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/live-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/live-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/live-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/live-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/methodology-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/methodology-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/methodology-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/methodology-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/ranking-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/ranking-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/ranking-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/ranking-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/rankings-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/rankings-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/rankings-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/rankings-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/results.json +150 −0
@@ -0,0 +1,150 @@
1 +{
2 + "base": "http://localhost:8314",
3 + "at": "2026-09-12T06:07:54.297Z",
4 + "results": [
5 + {
6 + "name": "explore",
7 + "path": "/explore?entity=facilities&country=US&view=charts",
8 + "w": 390,
9 + "theme": "light",
10 + "status": 200,
11 + "ttfb": 210,
12 + "overflow": null,
13 + "clipped": [
14 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
15 + ],
16 + "small": [
17 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
18 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
19 + "a.inline-flex.min-h-10.shrink-0 40×40 \"Reset\"",
20 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
21 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
22 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
23 + ],
24 + "smallCount": 6,
25 + "h1Covered": false,
26 + "h1Top": 112,
27 + "headerBottom": 49,
28 + "lang": "en",
29 + "metaDesc": 236,
30 + "h1Count": 1,
31 + "skipped": [],
32 + "unnamed": [],
33 + "ariaCurrent": 0,
34 + "footerOk": true,
35 + "contrast": [],
36 + "contrastChecked": 0,
37 + "height": 3885,
38 + "title": "Explore · DataCenterIndex",
39 + "bottom": {
40 + "navVisible": true,
41 + "covered": false,
42 + "gap": 1403
43 + },
44 + "errors": [],
45 + "badResponses": []
46 + },
47 + {
48 + "name": "explore",
49 + "path": "/explore?entity=facilities&country=US&view=charts",
50 + "w": 1280,
51 + "theme": "light",
52 + "status": 200,
53 + "ttfb": 99,
54 + "overflow": null,
55 + "clipped": [
56 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
57 + ],
58 + "small": [],
59 + "smallCount": 0,
60 + "h1Covered": false,
61 + "h1Top": 136,
62 + "headerBottom": 57,
63 + "lang": "en",
64 + "metaDesc": 236,
65 + "h1Count": 1,
66 + "skipped": [],
67 + "unnamed": [],
68 + "ariaCurrent": 0,
69 + "footerOk": true,
70 + "contrast": [],
71 + "contrastChecked": 0,
72 + "height": 2610,
73 + "title": "Explore · DataCenterIndex",
74 + "errors": [],
75 + "badResponses": []
76 + },
77 + {
78 + "name": "explore",
79 + "path": "/explore?entity=facilities&country=US&view=charts",
80 + "w": 390,
81 + "theme": "dark",
82 + "status": 200,
83 + "ttfb": 92,
84 + "overflow": null,
85 + "clipped": [
86 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
87 + ],
88 + "small": [
89 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
90 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
91 + "a.inline-flex.min-h-10.shrink-0 40×40 \"Reset\"",
92 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
93 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
94 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
95 + ],
96 + "smallCount": 6,
97 + "h1Covered": false,
98 + "h1Top": 112,
99 + "headerBottom": 49,
100 + "lang": "en",
101 + "metaDesc": 236,
102 + "h1Count": 1,
103 + "skipped": [],
104 + "unnamed": [],
105 + "ariaCurrent": 0,
106 + "footerOk": true,
107 + "contrast": [],
108 + "contrastChecked": 135,
109 + "height": 3885,
110 + "title": "Explore · DataCenterIndex",
111 + "bottom": {
112 + "navVisible": true,
113 + "covered": false,
114 + "gap": 1403
115 + },
116 + "errors": [],
117 + "badResponses": []
118 + },
119 + {
120 + "name": "explore",
121 + "path": "/explore?entity=facilities&country=US&view=charts",
122 + "w": 1280,
123 + "theme": "dark",
124 + "status": 200,
125 + "ttfb": 77,
126 + "overflow": null,
127 + "clipped": [
128 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
129 + ],
130 + "small": [],
131 + "smallCount": 0,
132 + "h1Covered": false,
133 + "h1Top": 136,
134 + "headerBottom": 57,
135 + "lang": "en",
136 + "metaDesc": 236,
137 + "h1Count": 1,
138 + "skipped": [],
139 + "unnamed": [],
140 + "ariaCurrent": 0,
141 + "footerOk": true,
142 + "contrast": [],
143 + "contrastChecked": 231,
144 + "height": 2610,
145 + "title": "Explore · DataCenterIndex",
146 + "errors": [],
147 + "badResponses": []
148 + }
149 + ]
150 +}
\ No newline at end of file
added apps/web/qa/sweep-c/sources-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/sources-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/sources-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/sources-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/watchlist-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/watchlist-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/watchlist-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-c/watchlist-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ai-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ai-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ai-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ai-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/cloud-region-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/cloud-region-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/cloud-region-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/cloud-region-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-empty-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-empty-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-empty-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/compare-empty-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/connectivity-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/connectivity-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/connectivity-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/connectivity-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/country-us-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/country-us-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/country-us-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/country-us-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-sources-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-sources-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-sources-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/coverage-sources-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/developers-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/developers-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/developers-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/developers-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/download-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/download-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/download-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/download-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-projects-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-projects-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-projects-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/explore-projects-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ixp-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ixp-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ixp-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ixp-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/live-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/live-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/live-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/live-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/market-nova-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/market-nova-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/market-nova-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/market-nova-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/methodology-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/methodology-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/methodology-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/methodology-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/operator-equinix-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/operator-equinix-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/operator-equinix-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/operator-equinix-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/power-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/power-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/power-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/power-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-30d-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-30d-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-30d-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-30d-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/pulse-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ranking-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ranking-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ranking-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/ranking-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/rankings-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/rankings-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/rankings-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/rankings-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/results.json +4067 −0
@@ -0,0 +1,4067 @@
1 +{
2 + "base": "http://localhost:8314",
3 + "at": "2026-09-12T06:21:53.691Z",
4 + "results": [
5 + {
6 + "name": "live",
7 + "path": "/live",
8 + "w": 390,
9 + "theme": "light",
10 + "status": 200,
11 + "ttfb": 959,
12 + "overflow": null,
13 + "clipped": [
14 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
15 + "span.sr-only \"United States\" 89>1",
16 + "span.sr-only \"United States\" 89>1",
17 + "span.sr-only \"United States\" 89>1",
18 + "span.sr-only \"United States\" 89>1",
19 + "span.sr-only \"United States\" 89>1",
20 + "span.sr-only \"United Kingdom\" 106>1",
21 + "span.sr-only \"United States\" 89>1",
22 + "span.sr-only \"Canada\" 51>1"
23 + ],
24 + "small": [
25 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
26 + "a.relative.z-[1].inline-flex 52×24 \"DataBank\"",
27 + "a.inline-flex.min-h-6.items-center 68×24 \"DataBank\"",
28 + "a.relative.z-[1].inline-flex 52×24 \"DataBank\"",
29 + "a.inline-flex.min-h-6.items-center 68×24 \"DataBank\"",
30 + "a.relative.z-[1].inline-flex 52×24 \"DataBank\"",
31 + "a.inline-flex.min-h-6.items-center 68×24 \"DataBank\"",
32 + "a.relative.z-[1].inline-flex 39×24 \"Google\"",
33 + "a.inline-flex.min-h-6.items-center 132×24 \"Entergy — newsroom\"",
34 + "a.inline-flex.min-h-6.items-center 245×24 \"American Electric Power \"",
35 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
36 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\""
37 + ],
38 + "smallCount": 103,
39 + "h1Covered": false,
40 + "h1Top": 91,
41 + "headerBottom": 49,
42 + "lang": "en",
43 + "metaDesc": 225,
44 + "h1Count": 1,
45 + "skipped": [],
46 + "unnamed": [],
47 + "ariaCurrent": 2,
48 + "footerOk": true,
49 + "contrast": [],
50 + "contrastChecked": 0,
51 + "height": 14002,
52 + "title": "Live changes · DataCenterIndex",
53 + "bottom": {
54 + "navVisible": true,
55 + "covered": false,
56 + "gap": 1059
57 + },
58 + "inter": {
59 + "filterSheetOpens": true,
60 + "filterSheetCloses": true
61 + },
62 + "errors": [],
63 + "badResponses": []
64 + },
65 + {
66 + "name": "operator-equinix",
67 + "path": "/operators/equinix",
68 + "w": 390,
69 + "theme": "light",
70 + "status": 200,
71 + "ttfb": 1786,
72 + "overflow": null,
73 + "clipped": [
74 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
75 + "span.sr-only \"United States\" 82>1",
76 + "span.sr-only \"United Kingdom\" 99>1",
77 + "span.sr-only \"Australia\" 54>1",
78 + "span.sr-only \"Canada\" 47>1",
79 + "span.sr-only \"Germany\" 56>1",
80 + "span.sr-only \"Japan\" 38>1",
81 + "span.sr-only \"France\" 42>1",
82 + "span.sr-only \"Brazil\" 34>1"
83 + ],
84 + "small": [
85 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
86 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
87 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
88 + "a.inline-flex.items-center.gap-1.5 106×19 \"🇺🇸United States\"",
89 + "a.break-all.text-ink.underline 75×19 \"equinix.com\"",
90 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\"",
91 + "a.inline-flex.min-h-6.items-center 55×24 \"Equinix\"",
92 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\"",
93 + "a.inline-flex.min-h-6.items-center 55×24 \"Equinix\"",
94 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\"",
95 + "a.inline-flex.min-h-6.items-center 55×24 \"Equinix\"",
96 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\""
97 + ],
98 + "smallCount": 67,
99 + "h1Covered": false,
100 + "h1Top": 122,
101 + "headerBottom": 49,
102 + "lang": "en",
103 + "metaDesc": 214,
104 + "h1Count": 1,
105 + "skipped": [],
106 + "unnamed": [],
107 + "ariaCurrent": 2,
108 + "footerOk": true,
109 + "contrast": [],
110 + "contrastChecked": 0,
111 + "height": 11178,
112 + "title": "Equinix · DataCenterIndex",
113 + "bottom": {
114 + "navVisible": true,
115 + "covered": false,
116 + "gap": 1402
117 + },
118 + "errors": [],
119 + "badResponses": []
120 + },
121 + {
122 + "name": "country-us",
123 + "path": "/countries/united-states",
124 + "w": 390,
125 + "theme": "light",
126 + "status": 200,
127 + "ttfb": 2065,
128 + "overflow": null,
129 + "clipped": [
130 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
131 + "span.sr-only \"United States\" 149>1",
132 + "span.sr-only \"United States\" 76>1",
133 + "span.sr-only \"United States\" 76>1",
134 + "span.sr-only \"United States\" 76>1",
135 + "span.sr-only \"United States\" 76>1",
136 + "span.sr-only \"United States\" 76>1",
137 + "span.sr-only \"United States\" 76>1",
138 + "span.sr-only \"United States\" 76>1"
139 + ],
140 + "small": [
141 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
142 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
143 + "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
144 + "a.dci-rowlink.font-medium.text-ink 186×18 \"Red Oak Data Center (DFW\"",
145 + "a.dci-rowlink.font-medium.text-ink 193×35 \"Granite Point Data Cente\"",
146 + "a.truncate.text-[13.5px].font-medium 244×20 \"Comparing Space-Driven D\"",
147 + "a.relative.z-[1].inline-flex 41×24 \"Google\"",
148 + "a.truncate.text-[13.5px].font-medium 104×20 \"Acre AI Campus\"",
149 + "a.relative.z-[1].inline-flex 128×24 \"Amazon Web Services\"",
150 + "a.truncate.text-[13.5px].font-medium 253×20 \"PJM's Power Shortfall Pu\"",
151 + "a.truncate.text-[13.5px].font-medium 97×20 \"GW AI Campus\"",
152 + "a.relative.z-[1].inline-flex 41×24 \"Google\""
153 + ],
154 + "smallCount": 96,
155 + "h1Covered": false,
156 + "h1Top": 120,
157 + "headerBottom": 49,
158 + "lang": "en",
159 + "metaDesc": 298,
160 + "h1Count": 1,
161 + "skipped": [],
162 + "unnamed": [],
163 + "ariaCurrent": 1,
164 + "footerOk": true,
165 + "contrast": [],
166 + "contrastChecked": 0,
167 + "height": 18713,
168 + "title": "Data Centers in United States · DataCenterIndex",
169 + "bottom": {
170 + "navVisible": true,
171 + "covered": false,
172 + "gap": 1403
173 + },
174 + "error": "TimeoutError: locator.getAttribute: Timeout 30000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('#facilities').getByRole('button', { name: /show all/i }).first()\u001b[22m\n",
175 + "errors": [],
176 + "badResponses": []
177 + },
178 + {
179 + "name": "market-nova",
180 + "path": "/datacenters/markets/northern-virginia",
181 + "w": 390,
182 + "theme": "light",
183 + "status": 200,
184 + "ttfb": 1512,
185 + "overflow": null,
186 + "clipped": [
187 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
188 + "span.sr-only \"United States\" 82>1",
189 + "span.sr-only \"United States\" 76>1",
190 + "span.sr-only \"United States\" 89>1",
191 + "span.sr-only \"United States\" 89>1",
192 + "span.sr-only \"United States\" 89>1",
193 + "span.sr-only \"United States\" 89>1",
194 + "span.sr-only \"United States\" 89>1",
195 + "span.sr-only \"United States\" 89>1"
196 + ],
197 + "small": [
198 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
199 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
200 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
201 + "a.hover:text-ink.hover:underline 46×17 \"Markets\"",
202 + "a.text-ink.hover:text-accent.hover:underline 82×19 \"United States\"",
203 + "a.inline-flex.h-10.items-center 38×40 \"Map\"",
204 + "a.mt-1.inline-flex.min-h-6 234×24 \"Loudoun County, Virginia\"",
205 + "a.mt-1.inline-flex.min-h-6 126×24 \"Data Center Frontier\"",
206 + "a.truncate.text-[13.5px].font-medium 147×20 \"Tract Ashburn campus\"",
207 + "a.relative.z-[1].inline-flex 29×24 \"Tract\"",
208 + "a.relative.z-[1].inline-flex 119×24 \"Vantage Data Centers\"",
209 + "a.inline-flex.min-h-6.items-center 135×24 \"Vantage Data Centers\""
210 + ],
211 + "smallCount": 63,
212 + "h1Covered": false,
213 + "h1Top": 120,
214 + "headerBottom": 49,
215 + "lang": "en",
216 + "metaDesc": 298,
217 + "h1Count": 1,
218 + "skipped": [],
219 + "unnamed": [],
220 + "ariaCurrent": 2,
221 + "footerOk": true,
222 + "contrast": [],
223 + "contrastChecked": 0,
224 + "height": 13605,
225 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
226 + "bottom": {
227 + "navVisible": true,
228 + "covered": false,
229 + "gap": 1402
230 + },
231 + "errors": [],
232 + "badResponses": []
233 + },
234 + {
235 + "name": "rankings",
236 + "path": "/rankings",
237 + "w": 390,
238 + "theme": "light",
239 + "status": 200,
240 + "ttfb": 381,
241 + "overflow": null,
242 + "clipped": [
243 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
244 + ],
245 + "small": [
246 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
247 + "a.text-[14.5px].font-semibold.text-ink 170×21 \"Countries by AI faciliti\"",
248 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
249 + "a.text-[14.5px].font-semibold.text-ink 188×21 \"Countries by cloud regio\"",
250 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
251 + "a.text-[14.5px].font-semibold.text-ink 336×21 \"Countries by facilities \"",
252 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
253 + "a.text-[14.5px].font-semibold.text-ink 184×21 \"Countries by facility co\"",
254 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
255 + "a.text-[14.5px].font-semibold.text-ink 234×21 \"Countries by hyperscale \"",
256 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
257 + "a.text-[14.5px].font-semibold.text-ink 255×21 \"Countries by known opera\""
258 + ],
259 + "smallCount": 44,
260 + "h1Covered": false,
261 + "h1Top": 90,
262 + "headerBottom": 49,
263 + "lang": "en",
264 + "metaDesc": 190,
265 + "h1Count": 1,
266 + "skipped": [],
267 + "unnamed": [],
268 + "ariaCurrent": 1,
269 + "footerOk": true,
270 + "contrast": [],
271 + "contrastChecked": 0,
272 + "height": 5457,
273 + "title": "Rankings · DataCenterIndex",
274 + "bottom": {
275 + "navVisible": true,
276 + "covered": false,
277 + "gap": 1059
278 + },
279 + "errors": [],
280 + "badResponses": []
281 + },
282 + {
283 + "name": "ranking",
284 + "path": "/rankings/countries_facilities",
285 + "w": 390,
286 + "theme": "light",
287 + "status": 200,
288 + "ttfb": 828,
289 + "overflow": null,
290 + "clipped": [
291 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
292 + "span.sr-only \"United States\" 83>1",
293 + "span.sr-only \"France\" 43>1",
294 + "span.sr-only \"United Kingdom\" 100>1",
295 + "span.sr-only \"Germany\" 57>1",
296 + "span.sr-only \"Australia\" 55>1",
297 + "span.sr-only \"Netherlands\" 76>1",
298 + "span.sr-only \"India\" 30>1",
299 + "span.sr-only \"Singapore\" 63>1"
300 + ],
301 + "small": [
302 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
303 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
304 + "a.hover:text-ink.hover:underline 52×17 \"Rankings\"",
305 + "a.dci-rowlink.font-medium.text-ink 83×18 \"United States\"",
306 + "a.dci-rowlink.font-medium.text-ink 43×18 \"France\"",
307 + "a.dci-rowlink.font-medium.text-ink 100×18 \"United Kingdom\"",
308 + "a.dci-rowlink.font-medium.text-ink 57×18 \"Germany\"",
309 + "a.dci-rowlink.font-medium.text-ink 55×18 \"Australia\"",
310 + "a.dci-rowlink.font-medium.text-ink 76×18 \"Netherlands\"",
311 + "a.dci-rowlink.font-medium.text-ink 30×18 \"India\"",
312 + "a.dci-rowlink.font-medium.text-ink 63×18 \"Singapore\"",
313 + "a.dci-rowlink.font-medium.text-ink 118×18 \"Russian Federation\""
314 + ],
315 + "smallCount": 16,
316 + "h1Covered": false,
317 + "h1Top": 120,
318 + "headerBottom": 49,
319 + "lang": "en",
320 + "metaDesc": 161,
321 + "h1Count": 1,
322 + "skipped": [],
323 + "unnamed": [],
324 + "ariaCurrent": 1,
325 + "footerOk": true,
326 + "contrast": [],
327 + "contrastChecked": 0,
328 + "height": 2788,
329 + "title": "Countries by facility count · DataCenterIndex",
330 + "bottom": {
331 + "navVisible": true,
332 + "covered": false,
333 + "gap": 1058
334 + },
335 + "errors": [],
336 + "badResponses": []
337 + },
338 + {
339 + "name": "cloud-region",
340 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
341 + "w": 390,
342 + "theme": "light",
343 + "status": 200,
344 + "ttfb": 594,
345 + "overflow": null,
346 + "clipped": [
347 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
348 + "span.sr-only \"Canada\" 47>1"
349 + ],
350 + "small": [
351 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
352 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
353 + "a.hover:text-ink.hover:underline 79×17 \"Cloud regions\"",
354 + "a.hover:text-ink.hover:underline 128×17 \"Amazon Web Services\"",
355 + "a.hover:text-ink.hover:underline 47×19 \"Canada\"",
356 + "a.relative.z-[1].inline-flex 123×24 \"Amazon Web Services\"",
357 + "a.inline-flex.min-h-6.items-center 179×24 \"AWS regions and Local Zo\"",
358 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
359 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
360 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
361 + ],
362 + "smallCount": 10,
363 + "h1Covered": false,
364 + "h1Top": 120,
365 + "headerBottom": 49,
366 + "lang": "en",
367 + "metaDesc": 165,
368 + "h1Count": 1,
369 + "skipped": [],
370 + "unnamed": [],
371 + "ariaCurrent": 0,
372 + "footerOk": true,
373 + "contrast": [],
374 + "contrastChecked": 0,
375 + "height": 4416,
376 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
377 + "bottom": {
378 + "navVisible": true,
379 + "covered": false,
380 + "gap": 1403
381 + },
382 + "errors": [],
383 + "badResponses": []
384 + },
385 + {
386 + "name": "sources",
387 + "path": "/sources",
388 + "w": 390,
389 + "theme": "light",
390 + "status": 200,
391 + "ttfb": 1408,
392 + "overflow": null,
393 + "clipped": [
394 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
395 + ],
396 + "small": [
397 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
398 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
399 + "input.h-9.min-w-0.rounded-sm 157×36 \"\"",
400 + "select.h-9.rounded-sm.border 129×36 \"All kindsOperatorGovernm\"",
401 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
402 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
403 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
404 + ],
405 + "smallCount": 7,
406 + "h1Covered": false,
407 + "h1Top": 112,
408 + "headerBottom": 49,
409 + "lang": "en",
410 + "metaDesc": 162,
411 + "h1Count": 1,
412 + "skipped": [],
413 + "unnamed": [],
414 + "ariaCurrent": 0,
415 + "footerOk": true,
416 + "contrast": [],
417 + "contrastChecked": 0,
418 + "height": 6597,
419 + "title": "Sources · DataCenterIndex",
420 + "bottom": {
421 + "navVisible": true,
422 + "covered": false,
423 + "gap": 1059
424 + },
425 + "errors": [],
426 + "badResponses": []
427 + },
428 + {
429 + "name": "methodology",
430 + "path": "/methodology",
431 + "w": 390,
432 + "theme": "light",
433 + "status": 200,
434 + "ttfb": 764,
435 + "overflow": null,
436 + "clipped": [
437 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
438 + ],
439 + "small": [
440 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
441 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
442 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
443 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
444 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
445 + ],
446 + "smallCount": 5,
447 + "h1Covered": false,
448 + "h1Top": 112,
449 + "headerBottom": 49,
450 + "lang": "en",
451 + "metaDesc": 298,
452 + "h1Count": 1,
453 + "skipped": [],
454 + "unnamed": [],
455 + "ariaCurrent": 0,
456 + "footerOk": true,
457 + "contrast": [],
458 + "contrastChecked": 0,
459 + "height": 21078,
460 + "title": "Methodology · DataCenterIndex",
461 + "bottom": {
462 + "navVisible": true,
463 + "covered": false,
464 + "gap": 1059
465 + },
466 + "errors": [],
467 + "badResponses": []
468 + },
469 + {
470 + "name": "ixp",
471 + "path": "/ixps/de-cix-new-york",
472 + "w": 390,
473 + "theme": "light",
474 + "status": 200,
475 + "ttfb": 546,
476 + "overflow": null,
477 + "clipped": [
478 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
479 + "span.sr-only \"United States\" 82>1"
480 + ],
481 + "small": [
482 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
483 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
484 + "a.hover:text-ink.hover:underline 109×17 \"Internet exchanges\"",
485 + "a.hover:text-ink.hover:underline 82×19 \"United States\"",
486 + "a.dci-rowlink.font-medium.text-ink 82×18 \"1025Connect\"",
487 + "a.dci-rowlink.font-medium.text-ink 166×18 \"165 Halsey Meet-Me Room\"",
488 + "a.dci-rowlink.font-medium.text-ink 192×18 \"375 Pearl, Intergate.Man\"",
489 + "a.dci-rowlink.font-medium.text-ink 101×18 \"501 Franklin Ave\"",
490 + "a.dci-rowlink.font-medium.text-ink 68×18 \"60 Hudson\"",
491 + "a.dci-rowlink.font-medium.text-ink 168×18 \"CoreSite - Secaucus (NY2\"",
492 + "a.dci-rowlink.font-medium.text-ink 231×18 \"CoreSite NY1 - New York \"",
493 + "a.dci-rowlink.font-medium.text-ink 200×18 \"Crown Castle New York (1\""
494 + ],
495 + "smallCount": 18,
496 + "h1Covered": false,
497 + "h1Top": 120,
498 + "headerBottom": 49,
499 + "lang": "en",
500 + "metaDesc": 165,
501 + "h1Count": 1,
502 + "skipped": [],
503 + "unnamed": [],
504 + "ariaCurrent": 0,
505 + "footerOk": true,
506 + "contrast": [],
507 + "contrastChecked": 0,
508 + "height": 4638,
509 + "title": "DE-CIX New York · DataCenterIndex",
510 + "bottom": {
511 + "navVisible": true,
512 + "covered": false,
513 + "gap": 1402
514 + },
515 + "errors": [],
516 + "badResponses": []
517 + },
518 + {
519 + "name": "ai",
520 + "path": "/ai-infrastructure",
521 + "w": 390,
522 + "theme": "light",
523 + "status": 200,
524 + "ttfb": 556,
525 + "overflow": null,
526 + "clipped": [
527 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
528 + "span.sr-only \"India\" 30>1",
529 + "span.sr-only \"United States\" 82>1",
530 + "span.sr-only \"Argentina\" 59>1",
531 + "span.sr-only \"Canada\" 47>1",
532 + "span.sr-only \"Malaysia\" 54>1",
533 + "span.sr-only \"United States\" 82>1",
534 + "span.sr-only \"United States\" 82>1",
535 + "span.sr-only \"United States\" 82>1"
536 + ],
537 + "small": [
538 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
539 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
540 + "a.truncate.text-[13.5px].font-medium 287×20 \"Kao Data Announces Const\"",
541 + "a.relative.z-[1].inline-flex 52×24 \"Kao Data\"",
542 + "a.truncate.text-[13.5px].font-medium 304×20 \"Green Mountain expands d\"",
543 + "a.relative.z-[1].inline-flex 91×24 \"Green Mountain\"",
544 + "a.truncate.text-[13.5px].font-medium 312×20 \"CentralColo Welcomes Bai\"",
545 + "a.relative.z-[1].inline-flex 89×24 \"Element Critical\"",
546 + "a.truncate.text-[13.5px].font-medium 295×20 \"Is There an AI Data Cent\"",
547 + "a.relative.z-[1].inline-flex 118×24 \"Stream Data Centers\"",
548 + "a.truncate.text-[13.5px].font-medium 304×20 \"Element Critical Adds 10\"",
549 + "a.relative.z-[1].inline-flex 89×24 \"Element Critical\""
550 + ],
551 + "smallCount": 25,
552 + "h1Covered": false,
553 + "h1Top": 120,
554 + "headerBottom": 49,
555 + "lang": "en",
556 + "metaDesc": 187,
557 + "h1Count": 1,
558 + "skipped": [],
559 + "unnamed": [],
560 + "ariaCurrent": 1,
561 + "footerOk": true,
562 + "contrast": [],
563 + "contrastChecked": 0,
564 + "height": 7473,
565 + "title": "AI infrastructure · DataCenterIndex",
566 + "bottom": {
567 + "navVisible": true,
568 + "covered": false,
569 + "gap": 1403
570 + },
571 + "errors": [],
572 + "badResponses": []
573 + },
574 + {
575 + "name": "pulse",
576 + "path": "/pulse",
577 + "w": 390,
578 + "theme": "light",
579 + "status": 200,
580 + "ttfb": 754,
581 + "overflow": null,
582 + "clipped": [
583 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
584 + "span.sr-only \"Ukraine\" 47>1",
585 + "span.sr-only \"Belarus\" 46>1",
586 + "span.sr-only \"Russia\" 40>1",
587 + "span.sr-only \"Russia\" 40>1",
588 + "span.sr-only \"North Macedonia\" 106>1",
589 + "span.sr-only \"Russia\" 40>1",
590 + "span.sr-only \"Iran\" 23>1",
591 + "span.sr-only \"Iraq\" 24>1"
592 + ],
593 + "small": [
594 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
595 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
596 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
597 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
598 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
599 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
600 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
601 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
602 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
603 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
604 + "a.relative.z-[1].inline-flex 49×24 \"TierPoint\"",
605 + "a.inline-flex.min-h-6.items-center 65×24 \"TierPoint\""
606 + ],
607 + "smallCount": 35,
608 + "h1Covered": false,
609 + "h1Top": 120,
610 + "headerBottom": 49,
611 + "lang": "en",
612 + "metaDesc": 227,
613 + "h1Count": 1,
614 + "skipped": [],
615 + "unnamed": [],
616 + "ariaCurrent": 1,
617 + "footerOk": true,
618 + "contrast": [],
619 + "contrastChecked": 0,
620 + "height": 8882,
621 + "title": "Pulse · DataCenterIndex",
622 + "bottom": {
623 + "navVisible": true,
624 + "covered": false,
625 + "gap": 1403
626 + },
627 + "errors": [],
628 + "badResponses": []
629 + },
630 + {
631 + "name": "pulse-30d",
632 + "path": "/pulse?window=30d",
633 + "w": 390,
634 + "theme": "light",
635 + "status": 200,
636 + "ttfb": 275,
637 + "overflow": null,
638 + "clipped": [
639 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
640 + "span.sr-only \"Ukraine\" 47>1",
641 + "span.sr-only \"Belarus\" 46>1",
642 + "span.sr-only \"Russia\" 40>1",
643 + "span.sr-only \"Russia\" 40>1",
644 + "span.sr-only \"North Macedonia\" 106>1",
645 + "span.sr-only \"Russia\" 40>1",
646 + "span.sr-only \"Iran\" 23>1",
647 + "span.sr-only \"Iraq\" 24>1"
648 + ],
649 + "small": [
650 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
651 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
652 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
653 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
654 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
655 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
656 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
657 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
658 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
659 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
660 + "a.relative.z-[1].inline-flex 49×24 \"TierPoint\"",
661 + "a.inline-flex.min-h-6.items-center 65×24 \"TierPoint\""
662 + ],
663 + "smallCount": 35,
664 + "h1Covered": false,
665 + "h1Top": 120,
666 + "headerBottom": 49,
667 + "lang": "en",
668 + "metaDesc": 227,
669 + "h1Count": 1,
670 + "skipped": [],
671 + "unnamed": [],
672 + "ariaCurrent": 1,
673 + "footerOk": true,
674 + "contrast": [],
675 + "contrastChecked": 0,
676 + "height": 8882,
677 + "title": "Pulse · DataCenterIndex",
678 + "bottom": {
679 + "navVisible": true,
680 + "covered": false,
681 + "gap": 1403
682 + },
683 + "errors": [],
684 + "badResponses": []
685 + },
686 + {
687 + "name": "explore",
688 + "path": "/explore?entity=facilities&country=US&view=charts",
689 + "w": 390,
690 + "theme": "light",
691 + "status": 200,
692 + "ttfb": 581,
693 + "overflow": null,
694 + "clipped": [
695 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
696 + ],
697 + "small": [
698 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
699 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
700 + "a.inline-flex.min-h-10.shrink-0 40×40 \"Reset\"",
701 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
702 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
703 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
704 + ],
705 + "smallCount": 6,
706 + "h1Covered": false,
707 + "h1Top": 112,
708 + "headerBottom": 49,
709 + "lang": "en",
710 + "metaDesc": 236,
711 + "h1Count": 1,
712 + "skipped": [],
713 + "unnamed": [],
714 + "ariaCurrent": 0,
715 + "footerOk": true,
716 + "contrast": [],
717 + "contrastChecked": 0,
718 + "height": 3885,
719 + "title": "Explore · DataCenterIndex",
720 + "bottom": {
721 + "navVisible": true,
722 + "covered": false,
723 + "gap": 1403
724 + },
725 + "errors": [],
726 + "badResponses": []
727 + },
728 + {
729 + "name": "explore-projects",
730 + "path": "/explore?entity=projects&view=table",
731 + "w": 390,
732 + "theme": "light",
733 + "status": 200,
734 + "ttfb": 142,
735 + "overflow": null,
736 + "clipped": [
737 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
738 + "span.sr-only \"Luxembourg\" 71>1",
739 + "span.sr-only \"United Kingdom\" 91>1",
740 + "span.sr-only \"United Kingdom\" 91>1",
741 + "span.sr-only \"Singapore\" 58>1",
742 + "span.sr-only \"Singapore\" 58>1",
743 + "span.sr-only \"Australia\" 49>1",
744 + "span.sr-only \"United Kingdom\" 91>1",
745 + "span.sr-only \"United Kingdom\" 91>1"
746 + ],
747 + "small": [
748 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
749 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
750 + "a.truncate.text-[13.5px].font-medium 304×20 \"Vantage Data Centers Con\"",
751 + "a.relative.z-[1].inline-flex 124×24 \"Vantage Data Centers\"",
752 + "a.truncate.text-[13.5px].font-medium 287×20 \"VIRTUS ANNOUNCES EXPANSI\"",
753 + "a.relative.z-[1].inline-flex 194×24 \"ST Telemedia Global Data\"",
754 + "a.truncate.text-[13.5px].font-medium 295×20 \"VIRTUS UNVEILS €3BN, 300\"",
755 + "a.relative.z-[1].inline-flex 194×24 \"ST Telemedia Global Data\"",
756 + "a.truncate.text-[13.5px].font-medium 295×20 \"STACK Expands to Arizona\"",
757 + "a.relative.z-[1].inline-flex 120×24 \"STACK Infrastructure\"",
758 + "a.truncate.text-[13.5px].font-medium 304×20 \"Vantage Data Centers exp\"",
759 + "a.relative.z-[1].inline-flex 124×24 \"Vantage Data Centers\""
760 + ],
761 + "smallCount": 53,
762 + "h1Covered": false,
763 + "h1Top": 112,
764 + "headerBottom": 49,
765 + "lang": "en",
766 + "metaDesc": 236,
767 + "h1Count": 1,
768 + "skipped": [],
769 + "unnamed": [],
770 + "ariaCurrent": 1,
771 + "footerOk": true,
772 + "contrast": [],
773 + "contrastChecked": 0,
774 + "height": 5291,
775 + "title": "Explore · DataCenterIndex",
776 + "bottom": {
777 + "navVisible": true,
778 + "covered": false,
779 + "gap": 1403
780 + },
781 + "errors": [],
782 + "badResponses": []
783 + },
784 + {
785 + "name": "compare",
786 + "path": "/compare/operators?slugs=equinix,digital-realty",
787 + "w": 390,
788 + "theme": "light",
789 + "status": 200,
790 + "ttfb": 818,
791 + "overflow": null,
792 + "clipped": [
793 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
794 + ],
795 + "small": [
796 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
797 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
798 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
799 + "input.h-full.min-w-0.flex-1 336×38 \"Search operators to comp\"",
800 + "a.block.truncate.text-[12.5px] 179×18 \"Digital Realty Zurich ca\"",
801 + "a.block.truncate.text-[12.5px] 179×18 \"With Armada For AI Data \"",
802 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
803 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
804 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
805 + ],
806 + "smallCount": 9,
807 + "h1Covered": false,
808 + "h1Top": 120,
809 + "headerBottom": 49,
810 + "lang": "en",
811 + "metaDesc": 204,
812 + "h1Count": 1,
813 + "skipped": [],
814 + "unnamed": [],
815 + "ariaCurrent": 0,
816 + "footerOk": true,
817 + "contrast": [],
818 + "contrastChecked": 0,
819 + "height": 4394,
820 + "title": "Compare operators · DataCenterIndex",
821 + "bottom": {
822 + "navVisible": true,
823 + "covered": false,
824 + "gap": 1403
825 + },
826 + "errors": [],
827 + "badResponses": []
828 + },
829 + {
830 + "name": "compare-empty",
831 + "path": "/compare/operators",
832 + "w": 390,
833 + "theme": "light",
834 + "status": 200,
835 + "ttfb": 36,
836 + "overflow": null,
837 + "clipped": [
838 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
839 + ],
840 + "small": [
841 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
842 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
843 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
844 + "input.h-full.min-w-0.flex-1 336×38 \"Search operators to comp\"",
845 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
846 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
847 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
848 + ],
849 + "smallCount": 7,
850 + "h1Covered": false,
851 + "h1Top": 120,
852 + "headerBottom": 49,
853 + "lang": "en",
854 + "metaDesc": 204,
855 + "h1Count": 1,
856 + "skipped": [],
857 + "unnamed": [],
858 + "ariaCurrent": 0,
859 + "footerOk": true,
860 + "contrast": [],
861 + "contrastChecked": 0,
862 + "height": 2314,
863 + "title": "Compare operators · DataCenterIndex",
864 + "bottom": {
865 + "navVisible": true,
866 + "covered": false,
867 + "gap": 1403
868 + },
869 + "errors": [],
870 + "badResponses": []
871 + },
872 + {
873 + "name": "coverage",
874 + "path": "/coverage",
875 + "w": 390,
876 + "theme": "light",
877 + "status": 200,
878 + "ttfb": 2199,
879 + "overflow": null,
880 + "clipped": [
881 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
882 + "span.sr-only \"United States\" 83>1",
883 + "span.sr-only \"Germany\" 57>1",
884 + "span.sr-only \"United Kingdom\" 100>1",
885 + "span.sr-only \"France\" 43>1",
886 + "span.sr-only \"Brazil\" 35>1",
887 + "span.sr-only \"India\" 30>1",
888 + "span.sr-only \"Australia\" 55>1",
889 + "span.sr-only \"Indonesia\" 60>1"
890 + ],
891 + "small": [
892 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
893 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
894 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
895 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
896 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
897 + ],
898 + "smallCount": 5,
899 + "h1Covered": false,
900 + "h1Top": 120,
901 + "headerBottom": 49,
902 + "lang": "en",
903 + "metaDesc": 218,
904 + "h1Count": 1,
905 + "skipped": [],
906 + "unnamed": [],
907 + "ariaCurrent": 1,
908 + "footerOk": true,
909 + "contrast": [],
910 + "contrastChecked": 0,
911 + "height": 4885,
912 + "title": "Coverage · DataCenterIndex",
913 + "bottom": {
914 + "navVisible": true,
915 + "covered": false,
916 + "gap": 1403
917 + },
918 + "errors": [],
919 + "badResponses": []
920 + },
921 + {
922 + "name": "coverage-sources",
923 + "path": "/coverage/sources",
924 + "w": 390,
925 + "theme": "light",
926 + "status": 200,
927 + "ttfb": 1280,
928 + "overflow": null,
929 + "clipped": [
930 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
931 + ],
932 + "small": [
933 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
934 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
935 + "a.hover:text-ink.hover:underline 55×17 \"Coverage\"",
936 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
937 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
938 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
939 + ],
940 + "smallCount": 6,
941 + "h1Covered": false,
942 + "h1Top": 120,
943 + "headerBottom": 49,
944 + "lang": "en",
945 + "metaDesc": 189,
946 + "h1Count": 1,
947 + "skipped": [],
948 + "unnamed": [],
949 + "ariaCurrent": 0,
950 + "footerOk": true,
951 + "contrast": [],
952 + "contrastChecked": 0,
953 + "height": 2964,
954 + "title": "Source coverage · DataCenterIndex",
955 + "bottom": {
956 + "navVisible": true,
957 + "covered": false,
958 + "gap": 1402
959 + },
960 + "errors": [],
961 + "badResponses": []
962 + },
963 + {
964 + "name": "power",
965 + "path": "/power",
966 + "w": 390,
967 + "theme": "light",
968 + "status": 200,
969 + "ttfb": 1297,
970 + "overflow": null,
971 + "clipped": [
972 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
973 + "span.sr-only \"United States\" 73>1",
974 + "span.sr-only \"United Kingdom\" 87>1",
975 + "span.sr-only \"United States\" 73>1",
976 + "span.sr-only \"United States\" 73>1",
977 + "span.sr-only \"United States\" 89>1",
978 + "span.sr-only \"United Kingdom\" 106>1",
979 + "span.sr-only \"United States\" 89>1",
980 + "span.sr-only \"United States\" 89>1"
981 + ],
982 + "small": [
983 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
984 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
985 + "a.relative.z-[1].inline-flex 39×24 \"Google\"",
986 + "a.inline-flex.min-h-6.items-center 132×24 \"Entergy — newsroom\"",
987 + "a.relative.z-[1].inline-flex 37×24 \"Switch\"",
988 + "a.inline-flex.min-h-6.items-center 53×24 \"Switch\"",
989 + "a.relative.z-[1].inline-flex 37×24 \"Switch\"",
990 + "a.inline-flex.min-h-6.items-center 53×24 \"Switch\"",
991 + "a.relative.z-[1].inline-flex 113×24 \"Stream Data Centers\"",
992 + "a.inline-flex.min-h-6.items-center 129×24 \"Stream Data Centers\"",
993 + "a.relative.z-[1].inline-flex 37×24 \"Switch\"",
994 + "a.inline-flex.min-h-6.items-center 91×24 \"Global Switch\""
995 + ],
996 + "smallCount": 64,
997 + "h1Covered": false,
998 + "h1Top": 120,
999 + "headerBottom": 49,
1000 + "lang": "en",
1001 + "metaDesc": 211,
1002 + "h1Count": 1,
1003 + "skipped": [],
1004 + "unnamed": [],
1005 + "ariaCurrent": 0,
1006 + "footerOk": true,
1007 + "contrast": [],
1008 + "contrastChecked": 0,
1009 + "height": 15540,
1010 + "title": "Power & grid · DataCenterIndex",
1011 + "bottom": {
1012 + "navVisible": true,
1013 + "covered": false,
1014 + "gap": 1403
1015 + },
1016 + "errors": [],
1017 + "badResponses": []
1018 + },
1019 + {
1020 + "name": "connectivity",
1021 + "path": "/connectivity",
1022 + "w": 390,
1023 + "theme": "light",
1024 + "status": 200,
1025 + "ttfb": 954,
1026 + "overflow": null,
1027 + "clipped": [
1028 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1029 + "span.sr-only \"Germany\" 52>1",
1030 + "span.sr-only \"United Kingdom\" 91>1",
1031 + "span.sr-only \"Germany\" 52>1",
1032 + "span.sr-only \"Singapore\" 58>1",
1033 + "span.sr-only \"Brazil\" 32>1",
1034 + "span.sr-only \"Netherlands\" 69>1",
1035 + "span.sr-only \"United States\" 76>1",
1036 + "span.sr-only \"United States\" 76>1"
1037 + ],
1038 + "small": [
1039 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1040 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1041 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
1042 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
1043 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
1044 + ],
1045 + "smallCount": 5,
1046 + "h1Covered": false,
1047 + "h1Top": 120,
1048 + "headerBottom": 49,
1049 + "lang": "en",
1050 + "metaDesc": 239,
1051 + "h1Count": 1,
1052 + "skipped": [],
1053 + "unnamed": [],
1054 + "ariaCurrent": 0,
1055 + "footerOk": true,
1056 + "contrast": [],
1057 + "contrastChecked": 0,
1058 + "height": 5454,
1059 + "title": "Connectivity · DataCenterIndex",
1060 + "bottom": {
1061 + "navVisible": true,
1062 + "covered": false,
1063 + "gap": 1402
1064 + },
1065 + "errors": [],
1066 + "badResponses": []
1067 + },
1068 + {
1069 + "name": "download",
1070 + "path": "/download",
1071 + "w": 390,
1072 + "theme": "light",
1073 + "status": 200,
1074 + "ttfb": 939,
1075 + "overflow": null,
1076 + "clipped": [
1077 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1078 + ],
1079 + "small": [
1080 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1081 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1082 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
1083 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
1084 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
1085 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
1086 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
1087 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
1088 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
1089 + ],
1090 + "smallCount": 9,
1091 + "h1Covered": false,
1092 + "h1Top": 112,
1093 + "headerBottom": 49,
1094 + "lang": "en",
1095 + "metaDesc": 216,
1096 + "h1Count": 1,
1097 + "skipped": [],
1098 + "unnamed": [],
1099 + "ariaCurrent": 0,
1100 + "footerOk": true,
1101 + "contrast": [],
1102 + "contrastChecked": 0,
1103 + "height": 9114,
1104 + "title": "Downloads · DataCenterIndex",
1105 + "bottom": {
1106 + "navVisible": true,
1107 + "covered": false,
1108 + "gap": 1402
1109 + },
1110 + "errors": [],
1111 + "badResponses": []
1112 + },
1113 + {
1114 + "name": "developers",
1115 + "path": "/developers",
1116 + "w": 390,
1117 + "theme": "light",
1118 + "status": 200,
1119 + "ttfb": 699,
1120 + "overflow": null,
1121 + "clipped": [
1122 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1123 + ],
1124 + "small": [
1125 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1126 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1127 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
1128 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
1129 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
1130 + ],
1131 + "smallCount": 5,
1132 + "h1Covered": false,
1133 + "h1Top": 112,
1134 + "headerBottom": 49,
1135 + "lang": "en",
1136 + "metaDesc": 210,
1137 + "h1Count": 1,
1138 + "skipped": [],
1139 + "unnamed": [],
1140 + "ariaCurrent": 0,
1141 + "footerOk": true,
1142 + "contrast": [],
1143 + "contrastChecked": 0,
1144 + "height": 31316,
1145 + "title": "API · DataCenterIndex",
1146 + "bottom": {
1147 + "navVisible": true,
1148 + "covered": false,
1149 + "gap": 1402
1150 + },
1151 + "inter": {
1152 + "tabSwitch": true
1153 + },
1154 + "errors": [],
1155 + "badResponses": []
1156 + },
1157 + {
1158 + "name": "watchlist",
1159 + "path": "/watchlist",
1160 + "w": 390,
1161 + "theme": "light",
1162 + "status": 200,
1163 + "ttfb": 496,
1164 + "overflow": null,
1165 + "clipped": [
1166 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1167 + ],
1168 + "small": [
1169 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1170 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1171 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
1172 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
1173 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
1174 + ],
1175 + "smallCount": 5,
1176 + "h1Covered": false,
1177 + "h1Top": 112,
1178 + "headerBottom": 49,
1179 + "lang": "en",
1180 + "metaDesc": 142,
1181 + "h1Count": 1,
1182 + "skipped": [],
1183 + "unnamed": [],
1184 + "ariaCurrent": 0,
1185 + "footerOk": true,
1186 + "contrast": [],
1187 + "contrastChecked": 0,
1188 + "height": 2009,
1189 + "title": "Watchlist · DataCenterIndex",
1190 + "bottom": {
1191 + "navVisible": true,
1192 + "covered": false,
1193 + "gap": 1059
1194 + },
1195 + "errors": [],
1196 + "badResponses": []
1197 + },
1198 + {
1199 + "name": "live",
1200 + "path": "/live",
1201 + "w": 1280,
1202 + "theme": "light",
1203 + "status": 200,
1204 + "ttfb": 479,
1205 + "overflow": null,
1206 + "clipped": [
1207 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1208 + "span.sr-only \"United States\" 89>1",
1209 + "span.sr-only \"United States\" 89>1",
1210 + "span.sr-only \"United States\" 89>1",
1211 + "span.sr-only \"United States\" 89>1",
1212 + "span.sr-only \"United States\" 89>1",
1213 + "span.sr-only \"United Kingdom\" 106>1",
1214 + "span.sr-only \"United States\" 89>1",
1215 + "span.sr-only \"Canada\" 51>1"
1216 + ],
1217 + "small": [],
1218 + "smallCount": 0,
1219 + "h1Covered": false,
1220 + "h1Top": 107,
1221 + "headerBottom": 57,
1222 + "lang": "en",
1223 + "metaDesc": 225,
1224 + "h1Count": 1,
1225 + "skipped": [],
1226 + "unnamed": [],
1227 + "ariaCurrent": 2,
1228 + "footerOk": true,
1229 + "contrast": [],
1230 + "contrastChecked": 0,
1231 + "height": 8184,
1232 + "title": "Live changes · DataCenterIndex",
1233 + "inter": {
1234 + "filterSheetOpens": true,
1235 + "filterSheetCloses": true
1236 + },
1237 + "errors": [],
1238 + "badResponses": []
1239 + },
1240 + {
1241 + "name": "operator-equinix",
1242 + "path": "/operators/equinix",
1243 + "w": 1280,
1244 + "theme": "light",
1245 + "status": 200,
1246 + "ttfb": 1182,
1247 + "overflow": null,
1248 + "clipped": [
1249 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1250 + "span.sr-only \"United States\" 82>1",
1251 + "span.sr-only \"United Kingdom\" 99>1",
1252 + "span.sr-only \"Australia\" 54>1",
1253 + "span.sr-only \"Canada\" 47>1",
1254 + "span.sr-only \"Germany\" 56>1",
1255 + "span.sr-only \"Japan\" 38>1",
1256 + "span.sr-only \"France\" 42>1",
1257 + "span.sr-only \"Brazil\" 34>1"
1258 + ],
1259 + "small": [],
1260 + "smallCount": 0,
1261 + "h1Covered": false,
1262 + "h1Top": 138,
1263 + "headerBottom": 57,
1264 + "lang": "en",
1265 + "metaDesc": 214,
1266 + "h1Count": 1,
1267 + "skipped": [],
1268 + "unnamed": [],
1269 + "ariaCurrent": 2,
1270 + "footerOk": true,
1271 + "contrast": [],
1272 + "contrastChecked": 0,
1273 + "height": 8785,
1274 + "title": "Equinix · DataCenterIndex",
1275 + "errors": [],
1276 + "badResponses": []
1277 + },
1278 + {
1279 + "name": "country-us",
1280 + "path": "/countries/united-states",
1281 + "w": 1280,
1282 + "theme": "light",
1283 + "status": 200,
1284 + "ttfb": 1854,
1285 + "overflow": null,
1286 + "clipped": [
1287 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1288 + "span.sr-only \"United States\" 187>1",
1289 + "span.sr-only \"United States\" 76>1",
1290 + "span.sr-only \"United States\" 76>1",
1291 + "span.sr-only \"United States\" 76>1",
1292 + "span.sr-only \"United States\" 76>1",
1293 + "span.sr-only \"United States\" 76>1",
1294 + "span.sr-only \"United States\" 76>1",
1295 + "span.sr-only \"United States\" 76>1"
1296 + ],
1297 + "small": [],
1298 + "smallCount": 0,
1299 + "h1Covered": false,
1300 + "h1Top": 136,
1301 + "headerBottom": 57,
1302 + "lang": "en",
1303 + "metaDesc": 298,
1304 + "h1Count": 1,
1305 + "skipped": [],
1306 + "unnamed": [],
1307 + "ariaCurrent": 1,
1308 + "footerOk": true,
1309 + "contrast": [],
1310 + "contrastChecked": 0,
1311 + "height": 17817,
1312 + "title": "Data Centers in United States · DataCenterIndex",
1313 + "errors": [],
1314 + "badResponses": []
1315 + },
1316 + {
1317 + "name": "market-nova",
1318 + "path": "/datacenters/markets/northern-virginia",
1319 + "w": 1280,
1320 + "theme": "light",
1321 + "status": 200,
1322 + "ttfb": 1262,
1323 + "overflow": null,
1324 + "clipped": [
1325 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1326 + "span.sr-only \"United States\" 82>1",
1327 + "span.sr-only \"United States\" 76>1",
1328 + "span.sr-only \"United States\" 89>1",
1329 + "span.sr-only \"United States\" 89>1",
1330 + "span.sr-only \"United States\" 89>1",
1331 + "span.sr-only \"United States\" 89>1",
1332 + "span.sr-only \"United States\" 89>1",
1333 + "span.sr-only \"United States\" 89>1"
1334 + ],
1335 + "small": [],
1336 + "smallCount": 0,
1337 + "h1Covered": false,
1338 + "h1Top": 136,
1339 + "headerBottom": 57,
1340 + "lang": "en",
1341 + "metaDesc": 298,
1342 + "h1Count": 1,
1343 + "skipped": [],
1344 + "unnamed": [],
1345 + "ariaCurrent": 2,
1346 + "footerOk": true,
1347 + "contrast": [],
1348 + "contrastChecked": 0,
1349 + "height": 10015,
1350 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
1351 + "errors": [],
1352 + "badResponses": []
1353 + },
1354 + {
1355 + "name": "rankings",
1356 + "path": "/rankings",
1357 + "w": 1280,
1358 + "theme": "light",
1359 + "status": 200,
1360 + "ttfb": 62,
1361 + "overflow": null,
1362 + "clipped": [
1363 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1364 + ],
1365 + "small": [],
1366 + "smallCount": 0,
1367 + "h1Covered": false,
1368 + "h1Top": 106,
1369 + "headerBottom": 57,
1370 + "lang": "en",
1371 + "metaDesc": 190,
1372 + "h1Count": 1,
1373 + "skipped": [],
1374 + "unnamed": [],
1375 + "ariaCurrent": 1,
1376 + "footerOk": true,
1377 + "contrast": [],
1378 + "contrastChecked": 0,
1379 + "height": 3519,
1380 + "title": "Rankings · DataCenterIndex",
1381 + "errors": [],
1382 + "badResponses": []
1383 + },
1384 + {
1385 + "name": "ranking",
1386 + "path": "/rankings/countries_facilities",
1387 + "w": 1280,
1388 + "theme": "light",
1389 + "status": 200,
1390 + "ttfb": 519,
1391 + "overflow": null,
1392 + "clipped": [
1393 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1394 + "span.sr-only \"United States\" 82>1",
1395 + "span.sr-only \"France\" 42>1",
1396 + "span.sr-only \"United Kingdom\" 99>1",
1397 + "span.sr-only \"Germany\" 56>1",
1398 + "span.sr-only \"Australia\" 54>1",
1399 + "span.sr-only \"Netherlands\" 75>1",
1400 + "span.sr-only \"India\" 30>1",
1401 + "span.sr-only \"Singapore\" 62>1"
1402 + ],
1403 + "small": [],
1404 + "smallCount": 0,
1405 + "h1Covered": false,
1406 + "h1Top": 136,
1407 + "headerBottom": 57,
1408 + "lang": "en",
1409 + "metaDesc": 161,
1410 + "h1Count": 1,
1411 + "skipped": [],
1412 + "unnamed": [],
1413 + "ariaCurrent": 1,
1414 + "footerOk": true,
1415 + "contrast": [],
1416 + "contrastChecked": 0,
1417 + "height": 4733,
1418 + "title": "Countries by facility count · DataCenterIndex",
1419 + "errors": [],
1420 + "badResponses": []
1421 + },
1422 + {
1423 + "name": "cloud-region",
1424 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
1425 + "w": 1280,
1426 + "theme": "light",
1427 + "status": 200,
1428 + "ttfb": 71,
1429 + "overflow": null,
1430 + "clipped": [
1431 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1432 + "span.sr-only \"Canada\" 47>1"
1433 + ],
1434 + "small": [],
1435 + "smallCount": 0,
1436 + "h1Covered": false,
1437 + "h1Top": 136,
1438 + "headerBottom": 57,
1439 + "lang": "en",
1440 + "metaDesc": 165,
1441 + "h1Count": 1,
1442 + "skipped": [],
1443 + "unnamed": [],
1444 + "ariaCurrent": 0,
1445 + "footerOk": true,
1446 + "contrast": [],
1447 + "contrastChecked": 0,
1448 + "height": 2818,
1449 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
1450 + "errors": [],
1451 + "badResponses": []
1452 + },
1453 + {
1454 + "name": "sources",
1455 + "path": "/sources",
1456 + "w": 1280,
1457 + "theme": "light",
1458 + "status": 200,
1459 + "ttfb": 596,
1460 + "overflow": null,
1461 + "clipped": [
1462 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1463 + ],
1464 + "small": [],
1465 + "smallCount": 0,
1466 + "h1Covered": false,
1467 + "h1Top": 136,
1468 + "headerBottom": 57,
1469 + "lang": "en",
1470 + "metaDesc": 162,
1471 + "h1Count": 1,
1472 + "skipped": [],
1473 + "unnamed": [],
1474 + "ariaCurrent": 0,
1475 + "footerOk": true,
1476 + "contrast": [],
1477 + "contrastChecked": 0,
1478 + "height": 9414,
1479 + "title": "Sources · DataCenterIndex",
1480 + "errors": [],
1481 + "badResponses": []
1482 + },
1483 + {
1484 + "name": "methodology",
1485 + "path": "/methodology",
1486 + "w": 1280,
1487 + "theme": "light",
1488 + "status": 200,
1489 + "ttfb": 105,
1490 + "overflow": null,
1491 + "clipped": [
1492 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1493 + ],
1494 + "small": [],
1495 + "smallCount": 0,
1496 + "h1Covered": false,
1497 + "h1Top": 136,
1498 + "headerBottom": 57,
1499 + "lang": "en",
1500 + "metaDesc": 298,
1501 + "h1Count": 1,
1502 + "skipped": [],
1503 + "unnamed": [],
1504 + "ariaCurrent": 0,
1505 + "footerOk": true,
1506 + "contrast": [],
1507 + "contrastChecked": 0,
1508 + "height": 10925,
1509 + "title": "Methodology · DataCenterIndex",
1510 + "errors": [],
1511 + "badResponses": []
1512 + },
1513 + {
1514 + "name": "ixp",
1515 + "path": "/ixps/de-cix-new-york",
1516 + "w": 1280,
1517 + "theme": "light",
1518 + "status": 200,
1519 + "ttfb": 148,
1520 + "overflow": null,
1521 + "clipped": [
1522 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1523 + "span.sr-only \"United States\" 82>1"
1524 + ],
1525 + "small": [],
1526 + "smallCount": 0,
1527 + "h1Covered": false,
1528 + "h1Top": 136,
1529 + "headerBottom": 57,
1530 + "lang": "en",
1531 + "metaDesc": 165,
1532 + "h1Count": 1,
1533 + "skipped": [],
1534 + "unnamed": [],
1535 + "ariaCurrent": 0,
1536 + "footerOk": true,
1537 + "contrast": [],
1538 + "contrastChecked": 0,
1539 + "height": 3833,
1540 + "title": "DE-CIX New York · DataCenterIndex",
1541 + "errors": [
1542 + "pageerror SyntaxError: Invalid or unexpected token"
1543 + ],
1544 + "badResponses": []
1545 + },
1546 + {
1547 + "name": "ai",
1548 + "path": "/ai-infrastructure",
1549 + "w": 1280,
1550 + "theme": "light",
1551 + "status": 200,
1552 + "ttfb": 124,
1553 + "overflow": null,
1554 + "clipped": [
1555 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1556 + "span.sr-only \"India\" 30>1",
1557 + "span.sr-only \"United States\" 82>1",
1558 + "span.sr-only \"Argentina\" 59>1",
1559 + "span.sr-only \"Canada\" 47>1",
1560 + "span.sr-only \"Malaysia\" 54>1",
1561 + "span.sr-only \"United States\" 82>1",
1562 + "span.sr-only \"United States\" 82>1",
1563 + "span.sr-only \"United States\" 82>1"
1564 + ],
1565 + "small": [],
1566 + "smallCount": 0,
1567 + "h1Covered": false,
1568 + "h1Top": 136,
1569 + "headerBottom": 57,
1570 + "lang": "en",
1571 + "metaDesc": 187,
1572 + "h1Count": 1,
1573 + "skipped": [],
1574 + "unnamed": [],
1575 + "ariaCurrent": 1,
1576 + "footerOk": true,
1577 + "contrast": [],
1578 + "contrastChecked": 0,
1579 + "height": 3900,
1580 + "title": "AI infrastructure · DataCenterIndex",
1581 + "errors": [],
1582 + "badResponses": []
1583 + },
1584 + {
1585 + "name": "pulse",
1586 + "path": "/pulse",
1587 + "w": 1280,
1588 + "theme": "light",
1589 + "status": 200,
1590 + "ttfb": 317,
1591 + "overflow": null,
1592 + "clipped": [
1593 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1594 + "span.sr-only \"Ukraine\" 47>1",
1595 + "span.sr-only \"Belarus\" 46>1",
1596 + "span.sr-only \"Russia\" 40>1",
1597 + "span.sr-only \"Russia\" 40>1",
1598 + "span.sr-only \"North Macedonia\" 106>1",
1599 + "span.sr-only \"Russia\" 40>1",
1600 + "span.sr-only \"Iran\" 23>1",
1601 + "span.sr-only \"Iraq\" 24>1"
1602 + ],
1603 + "small": [],
1604 + "smallCount": 0,
1605 + "h1Covered": false,
1606 + "h1Top": 136,
1607 + "headerBottom": 57,
1608 + "lang": "en",
1609 + "metaDesc": 227,
1610 + "h1Count": 1,
1611 + "skipped": [],
1612 + "unnamed": [],
1613 + "ariaCurrent": 1,
1614 + "footerOk": true,
1615 + "contrast": [],
1616 + "contrastChecked": 0,
1617 + "height": 5800,
1618 + "title": "Pulse · DataCenterIndex",
1619 + "errors": [],
1620 + "badResponses": []
1621 + },
1622 + {
1623 + "name": "pulse-30d",
1624 + "path": "/pulse?window=30d",
1625 + "w": 1280,
1626 + "theme": "light",
1627 + "status": 200,
1628 + "ttfb": 299,
1629 + "overflow": null,
1630 + "clipped": [
1631 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1632 + "span.sr-only \"Ukraine\" 47>1",
1633 + "span.sr-only \"Belarus\" 46>1",
1634 + "span.sr-only \"Russia\" 40>1",
1635 + "span.sr-only \"Russia\" 40>1",
1636 + "span.sr-only \"North Macedonia\" 106>1",
1637 + "span.sr-only \"Russia\" 40>1",
1638 + "span.sr-only \"Iran\" 23>1",
1639 + "span.sr-only \"Iraq\" 24>1"
1640 + ],
1641 + "small": [],
1642 + "smallCount": 0,
1643 + "h1Covered": false,
1644 + "h1Top": 136,
1645 + "headerBottom": 57,
1646 + "lang": "en",
1647 + "metaDesc": 227,
1648 + "h1Count": 1,
1649 + "skipped": [],
1650 + "unnamed": [],
1651 + "ariaCurrent": 1,
1652 + "footerOk": true,
1653 + "contrast": [],
1654 + "contrastChecked": 0,
1655 + "height": 5800,
1656 + "title": "Pulse · DataCenterIndex",
1657 + "errors": [
1658 + "pageerror SyntaxError: Invalid or unexpected token"
1659 + ],
1660 + "badResponses": []
1661 + },
1662 + {
1663 + "name": "explore",
1664 + "path": "/explore?entity=facilities&country=US&view=charts",
1665 + "w": 1280,
1666 + "theme": "light",
1667 + "status": 200,
1668 + "ttfb": 85,
1669 + "overflow": null,
1670 + "clipped": [
1671 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1672 + ],
1673 + "small": [],
1674 + "smallCount": 0,
1675 + "h1Covered": false,
1676 + "h1Top": 136,
1677 + "headerBottom": 57,
1678 + "lang": "en",
1679 + "metaDesc": 236,
1680 + "h1Count": 1,
1681 + "skipped": [],
1682 + "unnamed": [],
1683 + "ariaCurrent": 0,
1684 + "footerOk": true,
1685 + "contrast": [],
1686 + "contrastChecked": 0,
1687 + "height": 2610,
1688 + "title": "Explore · DataCenterIndex",
1689 + "errors": [],
1690 + "badResponses": []
1691 + },
1692 + {
1693 + "name": "explore-projects",
1694 + "path": "/explore?entity=projects&view=table",
1695 + "w": 1280,
1696 + "theme": "light",
1697 + "status": 200,
1698 + "ttfb": 138,
1699 + "overflow": null,
1700 + "clipped": [
1701 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1702 + "span.sr-only \"Luxembourg\" 71>1",
1703 + "span.sr-only \"United Kingdom\" 91>1",
1704 + "span.sr-only \"United Kingdom\" 91>1",
1705 + "span.sr-only \"Singapore\" 58>1",
1706 + "span.sr-only \"Singapore\" 58>1",
1707 + "span.sr-only \"Australia\" 49>1",
1708 + "span.sr-only \"United Kingdom\" 91>1",
1709 + "span.sr-only \"United Kingdom\" 91>1"
1710 + ],
1711 + "small": [],
1712 + "smallCount": 0,
1713 + "h1Covered": false,
1714 + "h1Top": 136,
1715 + "headerBottom": 57,
1716 + "lang": "en",
1717 + "metaDesc": 236,
1718 + "h1Count": 1,
1719 + "skipped": [],
1720 + "unnamed": [],
1721 + "ariaCurrent": 1,
1722 + "footerOk": true,
1723 + "contrast": [],
1724 + "contrastChecked": 0,
1725 + "height": 3452,
1726 + "title": "Explore · DataCenterIndex",
1727 + "errors": [
1728 + "pageerror SyntaxError: Invalid or unexpected token"
1729 + ],
1730 + "badResponses": []
1731 + },
1732 + {
1733 + "name": "compare",
1734 + "path": "/compare/operators?slugs=equinix,digital-realty",
1735 + "w": 1280,
1736 + "theme": "light",
1737 + "status": 200,
1738 + "ttfb": 145,
1739 + "overflow": null,
1740 + "clipped": [
1741 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1742 + "span.sr-only \"United States\" 70>1",
1743 + "span.sr-only \"United States\" 70>1"
1744 + ],
1745 + "small": [],
1746 + "smallCount": 0,
1747 + "h1Covered": false,
1748 + "h1Top": 136,
1749 + "headerBottom": 57,
1750 + "lang": "en",
1751 + "metaDesc": 204,
1752 + "h1Count": 1,
1753 + "skipped": [],
1754 + "unnamed": [],
1755 + "ariaCurrent": 0,
1756 + "footerOk": true,
1757 + "contrast": [],
1758 + "contrastChecked": 0,
1759 + "height": 2442,
1760 + "title": "Compare operators · DataCenterIndex",
1761 + "errors": [],
1762 + "badResponses": []
1763 + },
1764 + {
1765 + "name": "compare-empty",
1766 + "path": "/compare/operators",
1767 + "w": 1280,
1768 + "theme": "light",
1769 + "status": 200,
1770 + "ttfb": 38,
1771 + "overflow": null,
1772 + "clipped": [
1773 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1774 + ],
1775 + "small": [],
1776 + "smallCount": 0,
1777 + "h1Covered": false,
1778 + "h1Top": 136,
1779 + "headerBottom": 57,
1780 + "lang": "en",
1781 + "metaDesc": 204,
1782 + "h1Count": 1,
1783 + "skipped": [],
1784 + "unnamed": [],
1785 + "ariaCurrent": 0,
1786 + "footerOk": true,
1787 + "contrast": [],
1788 + "contrastChecked": 0,
1789 + "height": 1228,
1790 + "title": "Compare operators · DataCenterIndex",
1791 + "errors": [],
1792 + "badResponses": []
1793 + },
1794 + {
1795 + "name": "coverage",
1796 + "path": "/coverage",
1797 + "w": 1280,
1798 + "theme": "light",
1799 + "status": 200,
1800 + "ttfb": 1901,
1801 + "overflow": null,
1802 + "clipped": [
1803 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1804 + "span.sr-only \"United States\" 83>1",
1805 + "span.sr-only \"Germany\" 57>1",
1806 + "span.sr-only \"United Kingdom\" 100>1",
1807 + "span.sr-only \"France\" 43>1",
1808 + "span.sr-only \"Brazil\" 35>1",
1809 + "span.sr-only \"India\" 30>1",
1810 + "span.sr-only \"Australia\" 55>1",
1811 + "span.sr-only \"Indonesia\" 60>1"
1812 + ],
1813 + "small": [],
1814 + "smallCount": 0,
1815 + "h1Covered": false,
1816 + "h1Top": 136,
1817 + "headerBottom": 57,
1818 + "lang": "en",
1819 + "metaDesc": 218,
1820 + "h1Count": 1,
1821 + "skipped": [],
1822 + "unnamed": [],
1823 + "ariaCurrent": 1,
1824 + "footerOk": true,
1825 + "contrast": [],
1826 + "contrastChecked": 0,
1827 + "height": 12126,
1828 + "title": "Coverage · DataCenterIndex",
1829 + "errors": [],
1830 + "badResponses": []
1831 + },
1832 + {
1833 + "name": "coverage-sources",
1834 + "path": "/coverage/sources",
1835 + "w": 1280,
1836 + "theme": "light",
1837 + "status": 200,
1838 + "ttfb": 666,
1839 + "overflow": null,
1840 + "clipped": [
1841 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1842 + ],
1843 + "small": [],
1844 + "smallCount": 0,
1845 + "h1Covered": false,
1846 + "h1Top": 136,
1847 + "headerBottom": 57,
1848 + "lang": "en",
1849 + "metaDesc": 189,
1850 + "h1Count": 1,
1851 + "skipped": [],
1852 + "unnamed": [],
1853 + "ariaCurrent": 0,
1854 + "footerOk": true,
1855 + "contrast": [],
1856 + "contrastChecked": 0,
1857 + "height": 6816,
1858 + "title": "Source coverage · DataCenterIndex",
1859 + "errors": [],
1860 + "badResponses": []
1861 + },
1862 + {
1863 + "name": "power",
1864 + "path": "/power",
1865 + "w": 1280,
1866 + "theme": "light",
1867 + "status": 200,
1868 + "ttfb": 827,
1869 + "overflow": null,
1870 + "clipped": [
1871 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1872 + "span.sr-only \"United States\" 73>1",
1873 + "span.sr-only \"United Kingdom\" 87>1",
1874 + "span.sr-only \"United States\" 73>1",
1875 + "span.sr-only \"United States\" 73>1",
1876 + "span.sr-only \"United States\" 89>1",
1877 + "span.sr-only \"United Kingdom\" 106>1",
1878 + "span.sr-only \"United States\" 89>1",
1879 + "span.sr-only \"United States\" 89>1"
1880 + ],
1881 + "small": [],
1882 + "smallCount": 0,
1883 + "h1Covered": false,
1884 + "h1Top": 136,
1885 + "headerBottom": 57,
1886 + "lang": "en",
1887 + "metaDesc": 211,
1888 + "h1Count": 1,
1889 + "skipped": [],
1890 + "unnamed": [],
1891 + "ariaCurrent": 0,
1892 + "footerOk": true,
1893 + "contrast": [],
1894 + "contrastChecked": 0,
1895 + "height": 13817,
1896 + "title": "Power & grid · DataCenterIndex",
1897 + "errors": [
1898 + "pageerror SyntaxError: Invalid or unexpected token"
1899 + ],
1900 + "badResponses": []
1901 + },
1902 + {
1903 + "name": "connectivity",
1904 + "path": "/connectivity",
1905 + "w": 1280,
1906 + "theme": "light",
1907 + "status": 200,
1908 + "ttfb": 264,
1909 + "overflow": null,
1910 + "clipped": [
1911 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1912 + "span.sr-only \"Germany\" 56>1",
1913 + "span.sr-only \"United Kingdom\" 99>1",
1914 + "span.sr-only \"Germany\" 56>1",
1915 + "span.sr-only \"Singapore\" 62>1",
1916 + "span.sr-only \"Brazil\" 34>1",
1917 + "span.sr-only \"Netherlands\" 75>1",
1918 + "span.sr-only \"United States\" 82>1",
1919 + "span.sr-only \"United States\" 82>1"
1920 + ],
1921 + "small": [],
1922 + "smallCount": 0,
1923 + "h1Covered": false,
1924 + "h1Top": 136,
1925 + "headerBottom": 57,
1926 + "lang": "en",
1927 + "metaDesc": 239,
1928 + "h1Count": 1,
1929 + "skipped": [],
1930 + "unnamed": [],
1931 + "ariaCurrent": 0,
1932 + "footerOk": true,
1933 + "contrast": [],
1934 + "contrastChecked": 0,
1935 + "height": 7430,
1936 + "title": "Connectivity · DataCenterIndex",
1937 + "errors": [
1938 + "pageerror SyntaxError: Invalid or unexpected token"
1939 + ],
1940 + "badResponses": []
1941 + },
1942 + {
1943 + "name": "download",
1944 + "path": "/download",
1945 + "w": 1280,
1946 + "theme": "light",
1947 + "status": 200,
1948 + "ttfb": 80,
1949 + "overflow": null,
1950 + "clipped": [
1951 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1952 + ],
1953 + "small": [],
1954 + "smallCount": 0,
1955 + "h1Covered": false,
1956 + "h1Top": 136,
1957 + "headerBottom": 57,
1958 + "lang": "en",
1959 + "metaDesc": 216,
1960 + "h1Count": 1,
1961 + "skipped": [],
1962 + "unnamed": [],
1963 + "ariaCurrent": 0,
1964 + "footerOk": true,
1965 + "contrast": [],
1966 + "contrastChecked": 0,
1967 + "height": 3996,
1968 + "title": "Downloads · DataCenterIndex",
1969 + "errors": [],
1970 + "badResponses": []
1971 + },
1972 + {
1973 + "name": "developers",
1974 + "path": "/developers",
1975 + "w": 1280,
1976 + "theme": "light",
1977 + "status": 200,
1978 + "ttfb": 515,
1979 + "overflow": null,
1980 + "clipped": [
1981 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1982 + ],
1983 + "small": [],
1984 + "smallCount": 0,
1985 + "h1Covered": false,
1986 + "h1Top": 136,
1987 + "headerBottom": 57,
1988 + "lang": "en",
1989 + "metaDesc": 210,
1990 + "h1Count": 1,
1991 + "skipped": [],
1992 + "unnamed": [],
1993 + "ariaCurrent": 0,
1994 + "footerOk": true,
1995 + "contrast": [],
1996 + "contrastChecked": 0,
1997 + "height": 19510,
1998 + "title": "API · DataCenterIndex",
1999 + "inter": {
2000 + "tabSwitch": true
2001 + },
2002 + "errors": [],
2003 + "badResponses": []
2004 + },
2005 + {
2006 + "name": "watchlist",
2007 + "path": "/watchlist",
2008 + "w": 1280,
2009 + "theme": "light",
2010 + "status": 200,
2011 + "ttfb": 311,
2012 + "overflow": null,
2013 + "clipped": [
2014 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2015 + ],
2016 + "small": [],
2017 + "smallCount": 0,
2018 + "h1Covered": false,
2019 + "h1Top": 136,
2020 + "headerBottom": 57,
2021 + "lang": "en",
2022 + "metaDesc": 142,
2023 + "h1Count": 1,
2024 + "skipped": [],
2025 + "unnamed": [],
2026 + "ariaCurrent": 0,
2027 + "footerOk": true,
2028 + "contrast": [],
2029 + "contrastChecked": 0,
2030 + "height": 1025,
2031 + "title": "Watchlist · DataCenterIndex",
2032 + "errors": [],
2033 + "badResponses": []
2034 + },
2035 + {
2036 + "name": "live",
2037 + "path": "/live",
2038 + "w": 390,
2039 + "theme": "dark",
2040 + "status": 200,
2041 + "ttfb": 138,
2042 + "overflow": null,
2043 + "clipped": [
2044 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2045 + "span.sr-only \"United States\" 89>1",
2046 + "span.sr-only \"United States\" 89>1",
2047 + "span.sr-only \"United States\" 89>1",
2048 + "span.sr-only \"United States\" 89>1",
2049 + "span.sr-only \"United States\" 89>1",
2050 + "span.sr-only \"United Kingdom\" 106>1",
2051 + "span.sr-only \"United States\" 89>1",
2052 + "span.sr-only \"Canada\" 51>1"
2053 + ],
2054 + "small": [
2055 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2056 + "a.relative.z-[1].inline-flex 52×24 \"DataBank\"",
2057 + "a.inline-flex.min-h-6.items-center 68×24 \"DataBank\"",
2058 + "a.relative.z-[1].inline-flex 52×24 \"DataBank\"",
2059 + "a.inline-flex.min-h-6.items-center 68×24 \"DataBank\"",
2060 + "a.relative.z-[1].inline-flex 52×24 \"DataBank\"",
2061 + "a.inline-flex.min-h-6.items-center 68×24 \"DataBank\"",
2062 + "a.relative.z-[1].inline-flex 39×24 \"Google\"",
2063 + "a.inline-flex.min-h-6.items-center 132×24 \"Entergy — newsroom\"",
2064 + "a.inline-flex.min-h-6.items-center 245×24 \"American Electric Power \"",
2065 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2066 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\""
2067 + ],
2068 + "smallCount": 103,
2069 + "h1Covered": false,
2070 + "h1Top": 91,
2071 + "headerBottom": 49,
2072 + "lang": "en",
2073 + "metaDesc": 225,
2074 + "h1Count": 1,
2075 + "skipped": [],
2076 + "unnamed": [],
2077 + "ariaCurrent": 2,
2078 + "footerOk": true,
2079 + "contrast": [],
2080 + "contrastChecked": 709,
2081 + "height": 14002,
2082 + "title": "Live changes · DataCenterIndex",
2083 + "bottom": {
2084 + "navVisible": true,
2085 + "covered": false,
2086 + "gap": 1059
2087 + },
2088 + "errors": [],
2089 + "badResponses": []
2090 + },
2091 + {
2092 + "name": "operator-equinix",
2093 + "path": "/operators/equinix",
2094 + "w": 390,
2095 + "theme": "dark",
2096 + "status": 200,
2097 + "ttfb": 605,
2098 + "overflow": null,
2099 + "clipped": [
2100 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2101 + "span.sr-only \"United States\" 82>1",
2102 + "span.sr-only \"United Kingdom\" 99>1",
2103 + "span.sr-only \"Australia\" 54>1",
2104 + "span.sr-only \"Canada\" 47>1",
2105 + "span.sr-only \"Germany\" 56>1",
2106 + "span.sr-only \"Japan\" 38>1",
2107 + "span.sr-only \"France\" 42>1",
2108 + "span.sr-only \"Brazil\" 34>1"
2109 + ],
2110 + "small": [
2111 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2112 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2113 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
2114 + "a.inline-flex.items-center.gap-1.5 106×19 \"🇺🇸United States\"",
2115 + "a.break-all.text-ink.underline 75×19 \"equinix.com\"",
2116 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\"",
2117 + "a.inline-flex.min-h-6.items-center 55×24 \"Equinix\"",
2118 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\"",
2119 + "a.inline-flex.min-h-6.items-center 55×24 \"Equinix\"",
2120 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\"",
2121 + "a.inline-flex.min-h-6.items-center 55×24 \"Equinix\"",
2122 + "a.relative.z-[1].inline-flex 39×24 \"Equinix\""
2123 + ],
2124 + "smallCount": 67,
2125 + "h1Covered": false,
2126 + "h1Top": 122,
2127 + "headerBottom": 49,
2128 + "lang": "en",
2129 + "metaDesc": 214,
2130 + "h1Count": 1,
2131 + "skipped": [],
2132 + "unnamed": [],
2133 + "ariaCurrent": 2,
2134 + "footerOk": true,
2135 + "contrast": [],
2136 + "contrastChecked": 598,
2137 + "height": 11178,
2138 + "title": "Equinix · DataCenterIndex",
2139 + "bottom": {
2140 + "navVisible": true,
2141 + "covered": false,
2142 + "gap": 1402
2143 + },
2144 + "errors": [],
2145 + "badResponses": []
2146 + },
2147 + {
2148 + "name": "country-us",
2149 + "path": "/countries/united-states",
2150 + "w": 390,
2151 + "theme": "dark",
2152 + "status": 200,
2153 + "ttfb": 1227,
2154 + "overflow": null,
2155 + "clipped": [
2156 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2157 + "span.sr-only \"United States\" 149>1",
2158 + "span.sr-only \"United States\" 76>1",
2159 + "span.sr-only \"United States\" 76>1",
2160 + "span.sr-only \"United States\" 76>1",
2161 + "span.sr-only \"United States\" 76>1",
2162 + "span.sr-only \"United States\" 76>1",
2163 + "span.sr-only \"United States\" 76>1",
2164 + "span.sr-only \"United States\" 76>1"
2165 + ],
2166 + "small": [
2167 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2168 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2169 + "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
2170 + "a.dci-rowlink.font-medium.text-ink 186×18 \"Red Oak Data Center (DFW\"",
2171 + "a.dci-rowlink.font-medium.text-ink 193×35 \"Granite Point Data Cente\"",
2172 + "a.truncate.text-[13.5px].font-medium 244×20 \"Comparing Space-Driven D\"",
2173 + "a.relative.z-[1].inline-flex 41×24 \"Google\"",
2174 + "a.truncate.text-[13.5px].font-medium 104×20 \"Acre AI Campus\"",
2175 + "a.relative.z-[1].inline-flex 128×24 \"Amazon Web Services\"",
2176 + "a.truncate.text-[13.5px].font-medium 253×20 \"PJM's Power Shortfall Pu\"",
2177 + "a.truncate.text-[13.5px].font-medium 97×20 \"GW AI Campus\"",
2178 + "a.relative.z-[1].inline-flex 41×24 \"Google\""
2179 + ],
2180 + "smallCount": 96,
2181 + "h1Covered": false,
2182 + "h1Top": 120,
2183 + "headerBottom": 49,
2184 + "lang": "en",
2185 + "metaDesc": 298,
2186 + "h1Count": 1,
2187 + "skipped": [],
2188 + "unnamed": [],
2189 + "ariaCurrent": 1,
2190 + "footerOk": true,
2191 + "contrast": [],
2192 + "contrastChecked": 1079,
2193 + "height": 18713,
2194 + "title": "Data Centers in United States · DataCenterIndex",
2195 + "bottom": {
2196 + "navVisible": true,
2197 + "covered": false,
2198 + "gap": 1403
2199 + },
2200 + "errors": [],
2201 + "badResponses": []
2202 + },
2203 + {
2204 + "name": "market-nova",
2205 + "path": "/datacenters/markets/northern-virginia",
2206 + "w": 390,
2207 + "theme": "dark",
2208 + "status": 200,
2209 + "ttfb": 688,
2210 + "overflow": null,
2211 + "clipped": [
2212 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2213 + "span.sr-only \"United States\" 82>1",
2214 + "span.sr-only \"United States\" 76>1",
2215 + "span.sr-only \"United States\" 89>1",
2216 + "span.sr-only \"United States\" 89>1",
2217 + "span.sr-only \"United States\" 89>1",
2218 + "span.sr-only \"United States\" 89>1",
2219 + "span.sr-only \"United States\" 89>1",
2220 + "span.sr-only \"United States\" 89>1"
2221 + ],
2222 + "small": [
2223 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2224 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2225 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
2226 + "a.hover:text-ink.hover:underline 46×17 \"Markets\"",
2227 + "a.text-ink.hover:text-accent.hover:underline 82×19 \"United States\"",
2228 + "a.inline-flex.h-10.items-center 38×40 \"Map\"",
2229 + "a.mt-1.inline-flex.min-h-6 234×24 \"Loudoun County, Virginia\"",
2230 + "a.mt-1.inline-flex.min-h-6 126×24 \"Data Center Frontier\"",
2231 + "a.truncate.text-[13.5px].font-medium 147×20 \"Tract Ashburn campus\"",
2232 + "a.relative.z-[1].inline-flex 29×24 \"Tract\"",
2233 + "a.relative.z-[1].inline-flex 119×24 \"Vantage Data Centers\"",
2234 + "a.inline-flex.min-h-6.items-center 135×24 \"Vantage Data Centers\""
2235 + ],
2236 + "smallCount": 63,
2237 + "h1Covered": false,
2238 + "h1Top": 120,
2239 + "headerBottom": 49,
2240 + "lang": "en",
2241 + "metaDesc": 298,
2242 + "h1Count": 1,
2243 + "skipped": [],
2244 + "unnamed": [],
2245 + "ariaCurrent": 2,
2246 + "footerOk": true,
2247 + "contrast": [],
2248 + "contrastChecked": 712,
2249 + "height": 13605,
2250 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
2251 + "bottom": {
2252 + "navVisible": true,
2253 + "covered": false,
2254 + "gap": 1402
2255 + },
2256 + "errors": [],
2257 + "badResponses": []
2258 + },
2259 + {
2260 + "name": "rankings",
2261 + "path": "/rankings",
2262 + "w": 390,
2263 + "theme": "dark",
2264 + "status": 200,
2265 + "ttfb": 53,
2266 + "overflow": null,
2267 + "clipped": [
2268 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2269 + ],
2270 + "small": [
2271 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2272 + "a.text-[14.5px].font-semibold.text-ink 170×21 \"Countries by AI faciliti\"",
2273 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
2274 + "a.text-[14.5px].font-semibold.text-ink 188×21 \"Countries by cloud regio\"",
2275 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
2276 + "a.text-[14.5px].font-semibold.text-ink 336×21 \"Countries by facilities \"",
2277 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
2278 + "a.text-[14.5px].font-semibold.text-ink 184×21 \"Countries by facility co\"",
2279 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
2280 + "a.text-[14.5px].font-semibold.text-ink 234×21 \"Countries by hyperscale \"",
2281 + "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
2282 + "a.text-[14.5px].font-semibold.text-ink 255×21 \"Countries by known opera\""
2283 + ],
2284 + "smallCount": 44,
2285 + "h1Covered": false,
2286 + "h1Top": 90,
2287 + "headerBottom": 49,
2288 + "lang": "en",
2289 + "metaDesc": 190,
2290 + "h1Count": 1,
2291 + "skipped": [],
2292 + "unnamed": [],
2293 + "ariaCurrent": 1,
2294 + "footerOk": true,
2295 + "contrast": [],
2296 + "contrastChecked": 188,
2297 + "height": 5457,
2298 + "title": "Rankings · DataCenterIndex",
2299 + "bottom": {
2300 + "navVisible": true,
2301 + "covered": false,
2302 + "gap": 1059
2303 + },
2304 + "errors": [],
2305 + "badResponses": []
2306 + },
2307 + {
2308 + "name": "ranking",
2309 + "path": "/rankings/countries_facilities",
2310 + "w": 390,
2311 + "theme": "dark",
2312 + "status": 200,
2313 + "ttfb": 439,
2314 + "overflow": null,
2315 + "clipped": [
2316 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2317 + "span.sr-only \"United States\" 83>1",
2318 + "span.sr-only \"France\" 43>1",
2319 + "span.sr-only \"United Kingdom\" 100>1",
2320 + "span.sr-only \"Germany\" 57>1",
2321 + "span.sr-only \"Australia\" 55>1",
2322 + "span.sr-only \"Netherlands\" 76>1",
2323 + "span.sr-only \"India\" 30>1",
2324 + "span.sr-only \"Singapore\" 63>1"
2325 + ],
2326 + "small": [
2327 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2328 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2329 + "a.hover:text-ink.hover:underline 52×17 \"Rankings\"",
2330 + "a.dci-rowlink.font-medium.text-ink 83×18 \"United States\"",
2331 + "a.dci-rowlink.font-medium.text-ink 43×18 \"France\"",
2332 + "a.dci-rowlink.font-medium.text-ink 100×18 \"United Kingdom\"",
2333 + "a.dci-rowlink.font-medium.text-ink 57×18 \"Germany\"",
2334 + "a.dci-rowlink.font-medium.text-ink 55×18 \"Australia\"",
2335 + "a.dci-rowlink.font-medium.text-ink 76×18 \"Netherlands\"",
2336 + "a.dci-rowlink.font-medium.text-ink 30×18 \"India\"",
2337 + "a.dci-rowlink.font-medium.text-ink 63×18 \"Singapore\"",
2338 + "a.dci-rowlink.font-medium.text-ink 118×18 \"Russian Federation\""
2339 + ],
2340 + "smallCount": 16,
2341 + "h1Covered": false,
2342 + "h1Top": 120,
2343 + "headerBottom": 49,
2344 + "lang": "en",
2345 + "metaDesc": 161,
2346 + "h1Count": 1,
2347 + "skipped": [],
2348 + "unnamed": [],
2349 + "ariaCurrent": 1,
2350 + "footerOk": true,
2351 + "contrast": [],
2352 + "contrastChecked": 109,
2353 + "height": 2788,
2354 + "title": "Countries by facility count · DataCenterIndex",
2355 + "bottom": {
2356 + "navVisible": true,
2357 + "covered": false,
2358 + "gap": 1058
2359 + },
2360 + "errors": [],
2361 + "badResponses": []
2362 + },
2363 + {
2364 + "name": "cloud-region",
2365 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
2366 + "w": 390,
2367 + "theme": "dark",
2368 + "status": 200,
2369 + "ttfb": 71,
2370 + "overflow": null,
2371 + "clipped": [
2372 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2373 + "span.sr-only \"Canada\" 47>1"
2374 + ],
2375 + "small": [
2376 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2377 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2378 + "a.hover:text-ink.hover:underline 79×17 \"Cloud regions\"",
2379 + "a.hover:text-ink.hover:underline 128×17 \"Amazon Web Services\"",
2380 + "a.hover:text-ink.hover:underline 47×19 \"Canada\"",
2381 + "a.relative.z-[1].inline-flex 123×24 \"Amazon Web Services\"",
2382 + "a.inline-flex.min-h-6.items-center 179×24 \"AWS regions and Local Zo\"",
2383 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2384 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2385 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2386 + ],
2387 + "smallCount": 10,
2388 + "h1Covered": false,
2389 + "h1Top": 120,
2390 + "headerBottom": 49,
2391 + "lang": "en",
2392 + "metaDesc": 165,
2393 + "h1Count": 1,
2394 + "skipped": [],
2395 + "unnamed": [],
2396 + "ariaCurrent": 0,
2397 + "footerOk": true,
2398 + "contrast": [],
2399 + "contrastChecked": 156,
2400 + "height": 4436,
2401 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
2402 + "bottom": {
2403 + "navVisible": true,
2404 + "covered": false,
2405 + "gap": 1403
2406 + },
2407 + "errors": [
2408 + "pageerror SyntaxError: Invalid or unexpected token"
2409 + ],
2410 + "badResponses": []
2411 + },
2412 + {
2413 + "name": "sources",
2414 + "path": "/sources",
2415 + "w": 390,
2416 + "theme": "dark",
2417 + "status": 200,
2418 + "ttfb": 653,
2419 + "overflow": null,
2420 + "clipped": [
2421 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2422 + ],
2423 + "small": [
2424 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2425 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2426 + "input.h-9.min-w-0.rounded-sm 157×36 \"\"",
2427 + "select.h-9.rounded-sm.border 129×36 \"All kindsOperatorGovernm\"",
2428 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2429 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2430 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2431 + ],
2432 + "smallCount": 7,
2433 + "h1Covered": false,
2434 + "h1Top": 112,
2435 + "headerBottom": 49,
2436 + "lang": "en",
2437 + "metaDesc": 162,
2438 + "h1Count": 1,
2439 + "skipped": [],
2440 + "unnamed": [],
2441 + "ariaCurrent": 0,
2442 + "footerOk": true,
2443 + "contrast": [],
2444 + "contrastChecked": 257,
2445 + "height": 6597,
2446 + "title": "Sources · DataCenterIndex",
2447 + "bottom": {
2448 + "navVisible": true,
2449 + "covered": false,
2450 + "gap": 1059
2451 + },
2452 + "errors": [],
2453 + "badResponses": []
2454 + },
2455 + {
2456 + "name": "methodology",
2457 + "path": "/methodology",
2458 + "w": 390,
2459 + "theme": "dark",
2460 + "status": 200,
2461 + "ttfb": 110,
2462 + "overflow": null,
2463 + "clipped": [
2464 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2465 + ],
2466 + "small": [
2467 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2468 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2469 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2470 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2471 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2472 + ],
2473 + "smallCount": 5,
2474 + "h1Covered": false,
2475 + "h1Top": 112,
2476 + "headerBottom": 49,
2477 + "lang": "en",
2478 + "metaDesc": 298,
2479 + "h1Count": 1,
2480 + "skipped": [],
2481 + "unnamed": [],
2482 + "ariaCurrent": 0,
2483 + "footerOk": true,
2484 + "contrast": [],
2485 + "contrastChecked": 478,
2486 + "height": 21078,
2487 + "title": "Methodology · DataCenterIndex",
2488 + "bottom": {
2489 + "navVisible": true,
2490 + "covered": false,
2491 + "gap": 1059
2492 + },
2493 + "errors": [],
2494 + "badResponses": []
2495 + },
2496 + {
2497 + "name": "ixp",
2498 + "path": "/ixps/de-cix-new-york",
2499 + "w": 390,
2500 + "theme": "dark",
2501 + "status": 200,
2502 + "ttfb": 108,
2503 + "overflow": null,
2504 + "clipped": [
2505 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2506 + "span.sr-only \"United States\" 82>1"
2507 + ],
2508 + "small": [
2509 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2510 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2511 + "a.hover:text-ink.hover:underline 109×17 \"Internet exchanges\"",
2512 + "a.hover:text-ink.hover:underline 82×19 \"United States\"",
2513 + "a.dci-rowlink.font-medium.text-ink 82×18 \"1025Connect\"",
2514 + "a.dci-rowlink.font-medium.text-ink 166×18 \"165 Halsey Meet-Me Room\"",
2515 + "a.dci-rowlink.font-medium.text-ink 192×18 \"375 Pearl, Intergate.Man\"",
2516 + "a.dci-rowlink.font-medium.text-ink 101×18 \"501 Franklin Ave\"",
2517 + "a.dci-rowlink.font-medium.text-ink 68×18 \"60 Hudson\"",
2518 + "a.dci-rowlink.font-medium.text-ink 168×18 \"CoreSite - Secaucus (NY2\"",
2519 + "a.dci-rowlink.font-medium.text-ink 231×18 \"CoreSite NY1 - New York \"",
2520 + "a.dci-rowlink.font-medium.text-ink 200×18 \"Crown Castle New York (1\""
2521 + ],
2522 + "smallCount": 18,
2523 + "h1Covered": false,
2524 + "h1Top": 120,
2525 + "headerBottom": 49,
2526 + "lang": "en",
2527 + "metaDesc": 165,
2528 + "h1Count": 1,
2529 + "skipped": [],
2530 + "unnamed": [],
2531 + "ariaCurrent": 0,
2532 + "footerOk": true,
2533 + "contrast": [],
2534 + "contrastChecked": 211,
2535 + "height": 4638,
2536 + "title": "DE-CIX New York · DataCenterIndex",
2537 + "bottom": {
2538 + "navVisible": true,
2539 + "covered": false,
2540 + "gap": 1402
2541 + },
2542 + "errors": [
2543 + "pageerror SyntaxError: Invalid or unexpected token"
2544 + ],
2545 + "badResponses": []
2546 + },
2547 + {
2548 + "name": "ai",
2549 + "path": "/ai-infrastructure",
2550 + "w": 390,
2551 + "theme": "dark",
2552 + "status": 200,
2553 + "ttfb": 114,
2554 + "overflow": null,
2555 + "clipped": [
2556 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2557 + "span.sr-only \"India\" 30>1",
2558 + "span.sr-only \"United States\" 82>1",
2559 + "span.sr-only \"Argentina\" 59>1",
2560 + "span.sr-only \"Canada\" 47>1",
2561 + "span.sr-only \"Malaysia\" 54>1",
2562 + "span.sr-only \"United States\" 82>1",
2563 + "span.sr-only \"United States\" 82>1",
2564 + "span.sr-only \"United States\" 82>1"
2565 + ],
2566 + "small": [
2567 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2568 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2569 + "a.truncate.text-[13.5px].font-medium 287×20 \"Kao Data Announces Const\"",
2570 + "a.relative.z-[1].inline-flex 52×24 \"Kao Data\"",
2571 + "a.truncate.text-[13.5px].font-medium 304×20 \"Green Mountain expands d\"",
2572 + "a.relative.z-[1].inline-flex 91×24 \"Green Mountain\"",
2573 + "a.truncate.text-[13.5px].font-medium 312×20 \"CentralColo Welcomes Bai\"",
2574 + "a.relative.z-[1].inline-flex 89×24 \"Element Critical\"",
2575 + "a.truncate.text-[13.5px].font-medium 295×20 \"Is There an AI Data Cent\"",
2576 + "a.relative.z-[1].inline-flex 118×24 \"Stream Data Centers\"",
2577 + "a.truncate.text-[13.5px].font-medium 304×20 \"Element Critical Adds 10\"",
2578 + "a.relative.z-[1].inline-flex 89×24 \"Element Critical\""
2579 + ],
2580 + "smallCount": 25,
2581 + "h1Covered": false,
2582 + "h1Top": 120,
2583 + "headerBottom": 49,
2584 + "lang": "en",
2585 + "metaDesc": 187,
2586 + "h1Count": 1,
2587 + "skipped": [],
2588 + "unnamed": [],
2589 + "ariaCurrent": 1,
2590 + "footerOk": true,
2591 + "contrast": [],
2592 + "contrastChecked": 406,
2593 + "height": 7473,
2594 + "title": "AI infrastructure · DataCenterIndex",
2595 + "bottom": {
2596 + "navVisible": true,
2597 + "covered": false,
2598 + "gap": 1403
2599 + },
2600 + "errors": [],
2601 + "badResponses": []
2602 + },
2603 + {
2604 + "name": "pulse",
2605 + "path": "/pulse",
2606 + "w": 390,
2607 + "theme": "dark",
2608 + "status": 200,
2609 + "ttfb": 307,
2610 + "overflow": null,
2611 + "clipped": [
2612 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2613 + "span.sr-only \"Ukraine\" 47>1",
2614 + "span.sr-only \"Belarus\" 46>1",
2615 + "span.sr-only \"Russia\" 40>1",
2616 + "span.sr-only \"Russia\" 40>1",
2617 + "span.sr-only \"North Macedonia\" 106>1",
2618 + "span.sr-only \"Russia\" 40>1",
2619 + "span.sr-only \"Iran\" 23>1",
2620 + "span.sr-only \"Iraq\" 24>1"
2621 + ],
2622 + "small": [
2623 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2624 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2625 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2626 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2627 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2628 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2629 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2630 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2631 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2632 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2633 + "a.relative.z-[1].inline-flex 49×24 \"TierPoint\"",
2634 + "a.inline-flex.min-h-6.items-center 65×24 \"TierPoint\""
2635 + ],
2636 + "smallCount": 35,
2637 + "h1Covered": false,
2638 + "h1Top": 120,
2639 + "headerBottom": 49,
2640 + "lang": "en",
2641 + "metaDesc": 227,
2642 + "h1Count": 1,
2643 + "skipped": [],
2644 + "unnamed": [],
2645 + "ariaCurrent": 1,
2646 + "footerOk": true,
2647 + "contrast": [],
2648 + "contrastChecked": 493,
2649 + "height": 8882,
2650 + "title": "Pulse · DataCenterIndex",
2651 + "bottom": {
2652 + "navVisible": true,
2653 + "covered": false,
2654 + "gap": 1403
2655 + },
2656 + "errors": [],
2657 + "badResponses": []
2658 + },
2659 + {
2660 + "name": "pulse-30d",
2661 + "path": "/pulse?window=30d",
2662 + "w": 390,
2663 + "theme": "dark",
2664 + "status": 200,
2665 + "ttfb": 261,
2666 + "overflow": null,
2667 + "clipped": [
2668 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2669 + "span.sr-only \"Ukraine\" 47>1",
2670 + "span.sr-only \"Belarus\" 46>1",
2671 + "span.sr-only \"Russia\" 40>1",
2672 + "span.sr-only \"Russia\" 40>1",
2673 + "span.sr-only \"North Macedonia\" 106>1",
2674 + "span.sr-only \"Russia\" 40>1",
2675 + "span.sr-only \"Iran\" 23>1",
2676 + "span.sr-only \"Iraq\" 24>1"
2677 + ],
2678 + "small": [
2679 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2680 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2681 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2682 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2683 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2684 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2685 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2686 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2687 + "a.relative.z-[1].inline-flex 186×24 \"ST Telemedia Global Data\"",
2688 + "a.inline-flex.min-h-6.items-center 132×24 \"VIRTUS Data Centres\"",
2689 + "a.relative.z-[1].inline-flex 49×24 \"TierPoint\"",
2690 + "a.inline-flex.min-h-6.items-center 65×24 \"TierPoint\""
2691 + ],
2692 + "smallCount": 35,
2693 + "h1Covered": false,
2694 + "h1Top": 120,
2695 + "headerBottom": 49,
2696 + "lang": "en",
2697 + "metaDesc": 227,
2698 + "h1Count": 1,
2699 + "skipped": [],
2700 + "unnamed": [],
2701 + "ariaCurrent": 1,
2702 + "footerOk": true,
2703 + "contrast": [],
2704 + "contrastChecked": 493,
2705 + "height": 9181,
2706 + "title": "Pulse · DataCenterIndex",
2707 + "bottom": {
2708 + "navVisible": true,
2709 + "covered": false,
2710 + "gap": 1403
2711 + },
2712 + "errors": [
2713 + "pageerror SyntaxError: Invalid or unexpected token"
2714 + ],
2715 + "badResponses": []
2716 + },
2717 + {
2718 + "name": "explore",
2719 + "path": "/explore?entity=facilities&country=US&view=charts",
2720 + "w": 390,
2721 + "theme": "dark",
2722 + "status": 200,
2723 + "ttfb": 108,
2724 + "overflow": null,
2725 + "clipped": [
2726 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2727 + ],
2728 + "small": [
2729 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2730 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2731 + "a.inline-flex.min-h-10.shrink-0 40×40 \"Reset\"",
2732 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2733 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2734 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2735 + ],
2736 + "smallCount": 6,
2737 + "h1Covered": false,
2738 + "h1Top": 112,
2739 + "headerBottom": 49,
2740 + "lang": "en",
2741 + "metaDesc": 236,
2742 + "h1Count": 1,
2743 + "skipped": [],
2744 + "unnamed": [],
2745 + "ariaCurrent": 0,
2746 + "footerOk": true,
2747 + "contrast": [],
2748 + "contrastChecked": 135,
2749 + "height": 3885,
2750 + "title": "Explore · DataCenterIndex",
2751 + "bottom": {
2752 + "navVisible": true,
2753 + "covered": false,
2754 + "gap": 1403
2755 + },
2756 + "errors": [
2757 + "pageerror SyntaxError: Invalid or unexpected token"
2758 + ],
2759 + "badResponses": []
2760 + },
2761 + {
2762 + "name": "explore-projects",
2763 + "path": "/explore?entity=projects&view=table",
2764 + "w": 390,
2765 + "theme": "dark",
2766 + "status": 200,
2767 + "ttfb": 140,
2768 + "overflow": null,
2769 + "clipped": [
2770 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2771 + "span.sr-only \"Luxembourg\" 71>1",
2772 + "span.sr-only \"United Kingdom\" 91>1",
2773 + "span.sr-only \"United Kingdom\" 91>1",
2774 + "span.sr-only \"Singapore\" 58>1",
2775 + "span.sr-only \"Singapore\" 58>1",
2776 + "span.sr-only \"Australia\" 49>1",
2777 + "span.sr-only \"United Kingdom\" 91>1",
2778 + "span.sr-only \"United Kingdom\" 91>1"
2779 + ],
2780 + "small": [
2781 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2782 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2783 + "a.truncate.text-[13.5px].font-medium 304×20 \"Vantage Data Centers Con\"",
2784 + "a.relative.z-[1].inline-flex 124×24 \"Vantage Data Centers\"",
2785 + "a.truncate.text-[13.5px].font-medium 287×20 \"VIRTUS ANNOUNCES EXPANSI\"",
2786 + "a.relative.z-[1].inline-flex 194×24 \"ST Telemedia Global Data\"",
2787 + "a.truncate.text-[13.5px].font-medium 295×20 \"VIRTUS UNVEILS €3BN, 300\"",
2788 + "a.relative.z-[1].inline-flex 194×24 \"ST Telemedia Global Data\"",
2789 + "a.truncate.text-[13.5px].font-medium 295×20 \"STACK Expands to Arizona\"",
2790 + "a.relative.z-[1].inline-flex 120×24 \"STACK Infrastructure\"",
2791 + "a.truncate.text-[13.5px].font-medium 304×20 \"Vantage Data Centers exp\"",
2792 + "a.relative.z-[1].inline-flex 124×24 \"Vantage Data Centers\""
2793 + ],
2794 + "smallCount": 53,
2795 + "h1Covered": false,
2796 + "h1Top": 112,
2797 + "headerBottom": 49,
2798 + "lang": "en",
2799 + "metaDesc": 236,
2800 + "h1Count": 1,
2801 + "skipped": [],
2802 + "unnamed": [],
2803 + "ariaCurrent": 1,
2804 + "footerOk": true,
2805 + "contrast": [],
2806 + "contrastChecked": 270,
2807 + "height": 5291,
2808 + "title": "Explore · DataCenterIndex",
2809 + "bottom": {
2810 + "navVisible": true,
2811 + "covered": false,
2812 + "gap": 1403
2813 + },
2814 + "errors": [],
2815 + "badResponses": []
2816 + },
2817 + {
2818 + "name": "compare",
2819 + "path": "/compare/operators?slugs=equinix,digital-realty",
2820 + "w": 390,
2821 + "theme": "dark",
2822 + "status": 200,
2823 + "ttfb": 97,
2824 + "overflow": null,
2825 + "clipped": [
2826 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2827 + ],
2828 + "small": [
2829 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2830 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2831 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
2832 + "input.h-full.min-w-0.flex-1 336×38 \"Search operators to comp\"",
2833 + "a.block.truncate.text-[12.5px] 179×18 \"Digital Realty Zurich ca\"",
2834 + "a.block.truncate.text-[12.5px] 179×18 \"With Armada For AI Data \"",
2835 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2836 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2837 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2838 + ],
2839 + "smallCount": 9,
2840 + "h1Covered": false,
2841 + "h1Top": 120,
2842 + "headerBottom": 49,
2843 + "lang": "en",
2844 + "metaDesc": 204,
2845 + "h1Count": 1,
2846 + "skipped": [],
2847 + "unnamed": [],
2848 + "ariaCurrent": 0,
2849 + "footerOk": true,
2850 + "contrast": [],
2851 + "contrastChecked": 152,
2852 + "height": 4394,
2853 + "title": "Compare operators · DataCenterIndex",
2854 + "bottom": {
2855 + "navVisible": true,
2856 + "covered": false,
2857 + "gap": 1403
2858 + },
2859 + "errors": [
2860 + "pageerror SyntaxError: Invalid or unexpected token"
2861 + ],
2862 + "badResponses": []
2863 + },
2864 + {
2865 + "name": "compare-empty",
2866 + "path": "/compare/operators",
2867 + "w": 390,
2868 + "theme": "dark",
2869 + "status": 200,
2870 + "ttfb": 45,
2871 + "overflow": null,
2872 + "clipped": [
2873 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2874 + ],
2875 + "small": [
2876 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2877 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2878 + "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
2879 + "input.h-full.min-w-0.flex-1 336×38 \"Search operators to comp\"",
2880 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2881 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2882 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2883 + ],
2884 + "smallCount": 7,
2885 + "h1Covered": false,
2886 + "h1Top": 120,
2887 + "headerBottom": 49,
2888 + "lang": "en",
2889 + "metaDesc": 204,
2890 + "h1Count": 1,
2891 + "skipped": [],
2892 + "unnamed": [],
2893 + "ariaCurrent": 0,
2894 + "footerOk": true,
2895 + "contrast": [],
2896 + "contrastChecked": 55,
2897 + "height": 2314,
2898 + "title": "Compare operators · DataCenterIndex",
2899 + "bottom": {
2900 + "navVisible": true,
2901 + "covered": false,
2902 + "gap": 1403
2903 + },
2904 + "errors": [],
2905 + "badResponses": []
2906 + },
2907 + {
2908 + "name": "coverage",
2909 + "path": "/coverage",
2910 + "w": 390,
2911 + "theme": "dark",
2912 + "status": 200,
2913 + "ttfb": 1831,
2914 + "overflow": null,
2915 + "clipped": [
2916 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2917 + "span.sr-only \"United States\" 83>1",
2918 + "span.sr-only \"Germany\" 57>1",
2919 + "span.sr-only \"United Kingdom\" 100>1",
2920 + "span.sr-only \"France\" 43>1",
2921 + "span.sr-only \"Brazil\" 35>1",
2922 + "span.sr-only \"India\" 30>1",
2923 + "span.sr-only \"Australia\" 55>1",
2924 + "span.sr-only \"Indonesia\" 60>1"
2925 + ],
2926 + "small": [
2927 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2928 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2929 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2930 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2931 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2932 + ],
2933 + "smallCount": 5,
2934 + "h1Covered": false,
2935 + "h1Top": 120,
2936 + "headerBottom": 49,
2937 + "lang": "en",
2938 + "metaDesc": 218,
2939 + "h1Count": 1,
2940 + "skipped": [],
2941 + "unnamed": [],
2942 + "ariaCurrent": 1,
2943 + "footerOk": true,
2944 + "contrast": [],
2945 + "contrastChecked": 219,
2946 + "height": 4885,
2947 + "title": "Coverage · DataCenterIndex",
2948 + "bottom": {
2949 + "navVisible": true,
2950 + "covered": false,
2951 + "gap": 1403
2952 + },
2953 + "errors": [],
2954 + "badResponses": []
2955 + },
2956 + {
2957 + "name": "coverage-sources",
2958 + "path": "/coverage/sources",
2959 + "w": 390,
2960 + "theme": "dark",
2961 + "status": 200,
2962 + "ttfb": 699,
2963 + "overflow": null,
2964 + "clipped": [
2965 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2966 + ],
2967 + "small": [
2968 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2969 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2970 + "a.hover:text-ink.hover:underline 55×17 \"Coverage\"",
2971 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
2972 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
2973 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
2974 + ],
2975 + "smallCount": 6,
2976 + "h1Covered": false,
2977 + "h1Top": 120,
2978 + "headerBottom": 49,
2979 + "lang": "en",
2980 + "metaDesc": 189,
2981 + "h1Count": 1,
2982 + "skipped": [],
2983 + "unnamed": [],
2984 + "ariaCurrent": 0,
2985 + "footerOk": true,
2986 + "contrast": [],
2987 + "contrastChecked": 105,
2988 + "height": 2964,
2989 + "title": "Source coverage · DataCenterIndex",
2990 + "bottom": {
2991 + "navVisible": true,
2992 + "covered": false,
2993 + "gap": 1402
2994 + },
2995 + "errors": [],
2996 + "badResponses": []
2997 + },
2998 + {
2999 + "name": "power",
3000 + "path": "/power",
3001 + "w": 390,
3002 + "theme": "dark",
3003 + "status": 200,
3004 + "ttfb": 842,
3005 + "overflow": null,
3006 + "clipped": [
3007 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3008 + "span.sr-only \"United States\" 73>1",
3009 + "span.sr-only \"United Kingdom\" 87>1",
3010 + "span.sr-only \"United States\" 73>1",
3011 + "span.sr-only \"United States\" 73>1",
3012 + "span.sr-only \"United States\" 89>1",
3013 + "span.sr-only \"United Kingdom\" 106>1",
3014 + "span.sr-only \"United States\" 89>1",
3015 + "span.sr-only \"United States\" 89>1"
3016 + ],
3017 + "small": [
3018 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3019 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3020 + "a.relative.z-[1].inline-flex 39×24 \"Google\"",
3021 + "a.inline-flex.min-h-6.items-center 132×24 \"Entergy — newsroom\"",
3022 + "a.relative.z-[1].inline-flex 37×24 \"Switch\"",
3023 + "a.inline-flex.min-h-6.items-center 53×24 \"Switch\"",
3024 + "a.relative.z-[1].inline-flex 37×24 \"Switch\"",
3025 + "a.inline-flex.min-h-6.items-center 53×24 \"Switch\"",
3026 + "a.relative.z-[1].inline-flex 113×24 \"Stream Data Centers\"",
3027 + "a.inline-flex.min-h-6.items-center 129×24 \"Stream Data Centers\"",
3028 + "a.relative.z-[1].inline-flex 37×24 \"Switch\"",
3029 + "a.inline-flex.min-h-6.items-center 91×24 \"Global Switch\""
3030 + ],
3031 + "smallCount": 64,
3032 + "h1Covered": false,
3033 + "h1Top": 120,
3034 + "headerBottom": 49,
3035 + "lang": "en",
3036 + "metaDesc": 211,
3037 + "h1Count": 1,
3038 + "skipped": [],
3039 + "unnamed": [],
3040 + "ariaCurrent": 0,
3041 + "footerOk": true,
3042 + "contrast": [],
3043 + "contrastChecked": 733,
3044 + "height": 15540,
3045 + "title": "Power & grid · DataCenterIndex",
3046 + "bottom": {
3047 + "navVisible": true,
3048 + "covered": false,
3049 + "gap": 1403
3050 + },
3051 + "errors": [],
3052 + "badResponses": []
3053 + },
3054 + {
3055 + "name": "connectivity",
3056 + "path": "/connectivity",
3057 + "w": 390,
3058 + "theme": "dark",
3059 + "status": 200,
3060 + "ttfb": 224,
3061 + "overflow": null,
3062 + "clipped": [
3063 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3064 + "span.sr-only \"Germany\" 52>1",
3065 + "span.sr-only \"United Kingdom\" 91>1",
3066 + "span.sr-only \"Germany\" 52>1",
3067 + "span.sr-only \"Singapore\" 58>1",
3068 + "span.sr-only \"Brazil\" 32>1",
3069 + "span.sr-only \"Netherlands\" 69>1",
3070 + "span.sr-only \"United States\" 76>1",
3071 + "span.sr-only \"United States\" 76>1"
3072 + ],
3073 + "small": [
3074 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3075 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3076 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
3077 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
3078 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
3079 + ],
3080 + "smallCount": 5,
3081 + "h1Covered": false,
3082 + "h1Top": 120,
3083 + "headerBottom": 49,
3084 + "lang": "en",
3085 + "metaDesc": 239,
3086 + "h1Count": 1,
3087 + "skipped": [],
3088 + "unnamed": [],
3089 + "ariaCurrent": 0,
3090 + "footerOk": true,
3091 + "contrast": [],
3092 + "contrastChecked": 231,
3093 + "height": 5454,
3094 + "title": "Connectivity · DataCenterIndex",
3095 + "bottom": {
3096 + "navVisible": true,
3097 + "covered": false,
3098 + "gap": 1402
3099 + },
3100 + "errors": [],
3101 + "badResponses": []
3102 + },
3103 + {
3104 + "name": "download",
3105 + "path": "/download",
3106 + "w": 390,
3107 + "theme": "dark",
3108 + "status": 200,
3109 + "ttfb": 55,
3110 + "overflow": null,
3111 + "clipped": [
3112 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3113 + ],
3114 + "small": [
3115 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3116 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3117 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
3118 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
3119 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
3120 + "input.size-5.accent-[var(--accent)] 20×20 \"\"",
3121 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
3122 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
3123 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
3124 + ],
3125 + "smallCount": 9,
3126 + "h1Covered": false,
3127 + "h1Top": 112,
3128 + "headerBottom": 49,
3129 + "lang": "en",
3130 + "metaDesc": 216,
3131 + "h1Count": 1,
3132 + "skipped": [],
3133 + "unnamed": [],
3134 + "ariaCurrent": 0,
3135 + "footerOk": true,
3136 + "contrast": [],
3137 + "contrastChecked": 352,
3138 + "height": 9114,
3139 + "title": "Downloads · DataCenterIndex",
3140 + "bottom": {
3141 + "navVisible": true,
3142 + "covered": false,
3143 + "gap": 1402
3144 + },
3145 + "errors": [],
3146 + "badResponses": []
3147 + },
3148 + {
3149 + "name": "developers",
3150 + "path": "/developers",
3151 + "w": 390,
3152 + "theme": "dark",
3153 + "status": 200,
3154 + "ttfb": 215,
3155 + "overflow": null,
3156 + "clipped": [
3157 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3158 + ],
3159 + "small": [
3160 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3161 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3162 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
3163 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
3164 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
3165 + ],
3166 + "smallCount": 5,
3167 + "h1Covered": false,
3168 + "h1Top": 112,
3169 + "headerBottom": 49,
3170 + "lang": "en",
3171 + "metaDesc": 210,
3172 + "h1Count": 1,
3173 + "skipped": [],
3174 + "unnamed": [],
3175 + "ariaCurrent": 0,
3176 + "footerOk": true,
3177 + "contrast": [],
3178 + "contrastChecked": 1063,
3179 + "height": 31316,
3180 + "title": "API · DataCenterIndex",
3181 + "bottom": {
3182 + "navVisible": true,
3183 + "covered": false,
3184 + "gap": 1402
3185 + },
3186 + "errors": [
3187 + "pageerror SyntaxError: Invalid or unexpected token"
3188 + ],
3189 + "badResponses": []
3190 + },
3191 + {
3192 + "name": "watchlist",
3193 + "path": "/watchlist",
3194 + "w": 390,
3195 + "theme": "dark",
3196 + "status": 200,
3197 + "ttfb": 27,
3198 + "overflow": null,
3199 + "clipped": [
3200 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3201 + ],
3202 + "small": [
3203 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3204 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3205 + "a.inline-flex.min-h-10.items-center 27×40 \"Map\"",
3206 + "a.inline-flex.min-h-10.items-center 34×40 \"Pulse\"",
3207 + "a.inline-flex.min-h-10.items-center 37×40 \"About\""
3208 + ],
3209 + "smallCount": 5,
3210 + "h1Covered": false,
3211 + "h1Top": 112,
3212 + "headerBottom": 49,
3213 + "lang": "en",
3214 + "metaDesc": 142,
3215 + "h1Count": 1,
3216 + "skipped": [],
3217 + "unnamed": [],
3218 + "ariaCurrent": 0,
3219 + "footerOk": true,
3220 + "contrast": [],
3221 + "contrastChecked": 50,
3222 + "height": 2009,
3223 + "title": "Watchlist · DataCenterIndex",
3224 + "bottom": {
3225 + "navVisible": true,
3226 + "covered": false,
3227 + "gap": 1059
3228 + },
3229 + "errors": [],
3230 + "badResponses": []
3231 + },
3232 + {
3233 + "name": "live",
3234 + "path": "/live",
3235 + "w": 1280,
3236 + "theme": "dark",
3237 + "status": 200,
3238 + "ttfb": 131,
3239 + "overflow": null,
3240 + "clipped": [
3241 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3242 + "span.sr-only \"United States\" 89>1",
3243 + "span.sr-only \"United States\" 89>1",
3244 + "span.sr-only \"United States\" 89>1",
3245 + "span.sr-only \"United States\" 89>1",
3246 + "span.sr-only \"United States\" 89>1",
3247 + "span.sr-only \"United Kingdom\" 106>1",
3248 + "span.sr-only \"United States\" 89>1",
3249 + "span.sr-only \"Canada\" 51>1"
3250 + ],
3251 + "small": [],
3252 + "smallCount": 0,
3253 + "h1Covered": false,
3254 + "h1Top": 107,
3255 + "headerBottom": 57,
3256 + "lang": "en",
3257 + "metaDesc": 225,
3258 + "h1Count": 1,
3259 + "skipped": [],
3260 + "unnamed": [],
3261 + "ariaCurrent": 2,
3262 + "footerOk": true,
3263 + "contrast": [],
3264 + "contrastChecked": 726,
3265 + "height": 8184,
3266 + "title": "Live changes · DataCenterIndex",
3267 + "errors": [],
3268 + "badResponses": []
3269 + },
3270 + {
3271 + "name": "operator-equinix",
3272 + "path": "/operators/equinix",
3273 + "w": 1280,
3274 + "theme": "dark",
3275 + "status": 200,
3276 + "ttfb": 628,
3277 + "overflow": null,
3278 + "clipped": [
3279 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3280 + "span.sr-only \"United States\" 82>1",
3281 + "span.sr-only \"United Kingdom\" 99>1",
3282 + "span.sr-only \"Australia\" 54>1",
3283 + "span.sr-only \"Canada\" 47>1",
3284 + "span.sr-only \"Germany\" 56>1",
3285 + "span.sr-only \"Japan\" 38>1",
3286 + "span.sr-only \"France\" 42>1",
3287 + "span.sr-only \"Brazil\" 34>1"
3288 + ],
3289 + "small": [],
3290 + "smallCount": 0,
3291 + "h1Covered": false,
3292 + "h1Top": 138,
3293 + "headerBottom": 57,
3294 + "lang": "en",
3295 + "metaDesc": 214,
3296 + "h1Count": 1,
3297 + "skipped": [],
3298 + "unnamed": [],
3299 + "ariaCurrent": 2,
3300 + "footerOk": true,
3301 + "contrast": [],
3302 + "contrastChecked": 924,
3303 + "height": 8785,
3304 + "title": "Equinix · DataCenterIndex",
3305 + "errors": [],
3306 + "badResponses": []
3307 + },
3308 + {
3309 + "name": "country-us",
3310 + "path": "/countries/united-states",
3311 + "w": 1280,
3312 + "theme": "dark",
3313 + "status": 200,
3314 + "ttfb": 1288,
3315 + "overflow": null,
3316 + "clipped": [
3317 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3318 + "span.sr-only \"United States\" 187>1",
3319 + "span.sr-only \"United States\" 76>1",
3320 + "span.sr-only \"United States\" 76>1",
3321 + "span.sr-only \"United States\" 76>1",
3322 + "span.sr-only \"United States\" 76>1",
3323 + "span.sr-only \"United States\" 76>1",
3324 + "span.sr-only \"United States\" 76>1",
3325 + "span.sr-only \"United States\" 76>1"
3326 + ],
3327 + "small": [],
3328 + "smallCount": 0,
3329 + "h1Covered": false,
3330 + "h1Top": 136,
3331 + "headerBottom": 57,
3332 + "lang": "en",
3333 + "metaDesc": 298,
3334 + "h1Count": 1,
3335 + "skipped": [],
3336 + "unnamed": [],
3337 + "ariaCurrent": 1,
3338 + "footerOk": true,
3339 + "contrast": [],
3340 + "contrastChecked": 1501,
3341 + "height": 17817,
3342 + "title": "Data Centers in United States · DataCenterIndex",
3343 + "errors": [],
3344 + "badResponses": []
3345 + },
3346 + {
3347 + "name": "market-nova",
3348 + "path": "/datacenters/markets/northern-virginia",
3349 + "w": 1280,
3350 + "theme": "dark",
3351 + "status": 200,
3352 + "ttfb": 701,
3353 + "overflow": null,
3354 + "clipped": [
3355 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3356 + "span.sr-only \"United States\" 82>1",
3357 + "span.sr-only \"United States\" 76>1",
3358 + "span.sr-only \"United States\" 89>1",
3359 + "span.sr-only \"United States\" 89>1",
3360 + "span.sr-only \"United States\" 89>1",
3361 + "span.sr-only \"United States\" 89>1",
3362 + "span.sr-only \"United States\" 89>1",
3363 + "span.sr-only \"United States\" 89>1"
3364 + ],
3365 + "small": [],
3366 + "smallCount": 0,
3367 + "h1Covered": false,
3368 + "h1Top": 136,
3369 + "headerBottom": 57,
3370 + "lang": "en",
3371 + "metaDesc": 298,
3372 + "h1Count": 1,
3373 + "skipped": [],
3374 + "unnamed": [],
3375 + "ariaCurrent": 2,
3376 + "footerOk": true,
3377 + "contrast": [],
3378 + "contrastChecked": 985,
3379 + "height": 10015,
3380 + "title": "Data Centers in Northern Virginia · DataCenterIndex",
3381 + "errors": [],
3382 + "badResponses": []
3383 + },
3384 + {
3385 + "name": "rankings",
3386 + "path": "/rankings",
3387 + "w": 1280,
3388 + "theme": "dark",
3389 + "status": 200,
3390 + "ttfb": 56,
3391 + "overflow": null,
3392 + "clipped": [
3393 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3394 + ],
3395 + "small": [],
3396 + "smallCount": 0,
3397 + "h1Covered": false,
3398 + "h1Top": 106,
3399 + "headerBottom": 57,
3400 + "lang": "en",
3401 + "metaDesc": 190,
3402 + "h1Count": 1,
3403 + "skipped": [],
3404 + "unnamed": [],
3405 + "ariaCurrent": 1,
3406 + "footerOk": true,
3407 + "contrast": [],
3408 + "contrastChecked": 197,
3409 + "height": 3519,
3410 + "title": "Rankings · DataCenterIndex",
3411 + "errors": [],
3412 + "badResponses": []
3413 + },
3414 + {
3415 + "name": "ranking",
3416 + "path": "/rankings/countries_facilities",
3417 + "w": 1280,
3418 + "theme": "dark",
3419 + "status": 200,
3420 + "ttfb": 404,
3421 + "overflow": null,
3422 + "clipped": [
3423 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3424 + "span.sr-only \"United States\" 82>1",
3425 + "span.sr-only \"France\" 42>1",
3426 + "span.sr-only \"United Kingdom\" 99>1",
3427 + "span.sr-only \"Germany\" 56>1",
3428 + "span.sr-only \"Australia\" 54>1",
3429 + "span.sr-only \"Netherlands\" 75>1",
3430 + "span.sr-only \"India\" 30>1",
3431 + "span.sr-only \"Singapore\" 62>1"
3432 + ],
3433 + "small": [],
3434 + "smallCount": 0,
3435 + "h1Covered": false,
3436 + "h1Top": 136,
3437 + "headerBottom": 57,
3438 + "lang": "en",
3439 + "metaDesc": 161,
3440 + "h1Count": 1,
3441 + "skipped": [],
3442 + "unnamed": [],
3443 + "ariaCurrent": 1,
3444 + "footerOk": true,
3445 + "contrast": [],
3446 + "contrastChecked": 640,
3447 + "height": 4733,
3448 + "title": "Countries by facility count · DataCenterIndex",
3449 + "errors": [
3450 + "pageerror SyntaxError: Invalid or unexpected token"
3451 + ],
3452 + "badResponses": []
3453 + },
3454 + {
3455 + "name": "cloud-region",
3456 + "path": "/cloud-regions/amazon-web-services-ca-central-1",
3457 + "w": 1280,
3458 + "theme": "dark",
3459 + "status": 200,
3460 + "ttfb": 67,
3461 + "overflow": null,
3462 + "clipped": [
3463 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3464 + "span.sr-only \"Canada\" 47>1"
3465 + ],
3466 + "small": [],
3467 + "smallCount": 0,
3468 + "h1Covered": false,
3469 + "h1Top": 136,
3470 + "headerBottom": 57,
3471 + "lang": "en",
3472 + "metaDesc": 165,
3473 + "h1Count": 1,
3474 + "skipped": [],
3475 + "unnamed": [],
3476 + "ariaCurrent": 0,
3477 + "footerOk": true,
3478 + "contrast": [],
3479 + "contrastChecked": 208,
3480 + "height": 2818,
3481 + "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
3482 + "errors": [],
3483 + "badResponses": []
3484 + },
3485 + {
3486 + "name": "sources",
3487 + "path": "/sources",
3488 + "w": 1280,
3489 + "theme": "dark",
3490 + "status": 200,
3491 + "ttfb": 572,
3492 + "overflow": null,
3493 + "clipped": [
3494 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3495 + ],
3496 + "small": [],
3497 + "smallCount": 0,
3498 + "h1Covered": false,
3499 + "h1Top": 136,
3500 + "headerBottom": 57,
3501 + "lang": "en",
3502 + "metaDesc": 162,
3503 + "h1Count": 1,
3504 + "skipped": [],
3505 + "unnamed": [],
3506 + "ariaCurrent": 0,
3507 + "footerOk": true,
3508 + "contrast": [],
3509 + "contrastChecked": 1061,
3510 + "height": 9414,
3511 + "title": "Sources · DataCenterIndex",
3512 + "errors": [],
3513 + "badResponses": []
3514 + },
3515 + {
3516 + "name": "methodology",
3517 + "path": "/methodology",
3518 + "w": 1280,
3519 + "theme": "dark",
3520 + "status": 200,
3521 + "ttfb": 93,
3522 + "overflow": null,
3523 + "clipped": [
3524 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3525 + ],
3526 + "small": [],
3527 + "smallCount": 0,
3528 + "h1Covered": false,
3529 + "h1Top": 136,
3530 + "headerBottom": 57,
3531 + "lang": "en",
3532 + "metaDesc": 298,
3533 + "h1Count": 1,
3534 + "skipped": [],
3535 + "unnamed": [],
3536 + "ariaCurrent": 0,
3537 + "footerOk": true,
3538 + "contrast": [],
3539 + "contrastChecked": 545,
3540 + "height": 10925,
3541 + "title": "Methodology · DataCenterIndex",
3542 + "errors": [],
3543 + "badResponses": []
3544 + },
3545 + {
3546 + "name": "ixp",
3547 + "path": "/ixps/de-cix-new-york",
3548 + "w": 1280,
3549 + "theme": "dark",
3550 + "status": 200,
3551 + "ttfb": 105,
3552 + "overflow": null,
3553 + "clipped": [
3554 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3555 + "span.sr-only \"United States\" 82>1"
3556 + ],
3557 + "small": [],
3558 + "smallCount": 0,
3559 + "h1Covered": false,
3560 + "h1Top": 136,
3561 + "headerBottom": 57,
3562 + "lang": "en",
3563 + "metaDesc": 165,
3564 + "h1Count": 1,
3565 + "skipped": [],
3566 + "unnamed": [],
3567 + "ariaCurrent": 0,
3568 + "footerOk": true,
3569 + "contrast": [],
3570 + "contrastChecked": 376,
3571 + "height": 3833,
3572 + "title": "DE-CIX New York · DataCenterIndex",
3573 + "errors": [],
3574 + "badResponses": []
3575 + },
3576 + {
3577 + "name": "ai",
3578 + "path": "/ai-infrastructure",
3579 + "w": 1280,
3580 + "theme": "dark",
3581 + "status": 200,
3582 + "ttfb": 91,
3583 + "overflow": null,
3584 + "clipped": [
3585 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3586 + "span.sr-only \"India\" 30>1",
3587 + "span.sr-only \"United States\" 82>1",
3588 + "span.sr-only \"Argentina\" 59>1",
3589 + "span.sr-only \"Canada\" 47>1",
3590 + "span.sr-only \"Malaysia\" 54>1",
3591 + "span.sr-only \"United States\" 82>1",
3592 + "span.sr-only \"United States\" 82>1",
3593 + "span.sr-only \"United States\" 82>1"
3594 + ],
3595 + "small": [],
3596 + "smallCount": 0,
3597 + "h1Covered": false,
3598 + "h1Top": 136,
3599 + "headerBottom": 57,
3600 + "lang": "en",
3601 + "metaDesc": 187,
3602 + "h1Count": 1,
3603 + "skipped": [],
3604 + "unnamed": [],
3605 + "ariaCurrent": 1,
3606 + "footerOk": true,
3607 + "contrast": [],
3608 + "contrastChecked": 455,
3609 + "height": 3900,
3610 + "title": "AI infrastructure · DataCenterIndex",
3611 + "errors": [],
3612 + "badResponses": []
3613 + },
3614 + {
3615 + "name": "pulse",
3616 + "path": "/pulse",
3617 + "w": 1280,
3618 + "theme": "dark",
3619 + "status": 200,
3620 + "ttfb": 303,
3621 + "overflow": null,
3622 + "clipped": [
3623 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3624 + "span.sr-only \"Ukraine\" 47>1",
3625 + "span.sr-only \"Belarus\" 46>1",
3626 + "span.sr-only \"Russia\" 40>1",
3627 + "span.sr-only \"Russia\" 40>1",
3628 + "span.sr-only \"North Macedonia\" 106>1",
3629 + "span.sr-only \"Russia\" 40>1",
3630 + "span.sr-only \"Iran\" 23>1",
3631 + "span.sr-only \"Iraq\" 24>1"
3632 + ],
3633 + "small": [],
3634 + "smallCount": 0,
3635 + "h1Covered": false,
3636 + "h1Top": 136,
3637 + "headerBottom": 57,
3638 + "lang": "en",
3639 + "metaDesc": 227,
3640 + "h1Count": 1,
3641 + "skipped": [],
3642 + "unnamed": [],
3643 + "ariaCurrent": 1,
3644 + "footerOk": true,
3645 + "contrast": [],
3646 + "contrastChecked": 539,
3647 + "height": 5800,
3648 + "title": "Pulse · DataCenterIndex",
3649 + "errors": [],
3650 + "badResponses": []
3651 + },
3652 + {
3653 + "name": "pulse-30d",
3654 + "path": "/pulse?window=30d",
3655 + "w": 1280,
3656 + "theme": "dark",
3657 + "status": 200,
3658 + "ttfb": 265,
3659 + "overflow": null,
3660 + "clipped": [
3661 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3662 + "span.sr-only \"Ukraine\" 47>1",
3663 + "span.sr-only \"Belarus\" 46>1",
3664 + "span.sr-only \"Russia\" 40>1",
3665 + "span.sr-only \"Russia\" 40>1",
3666 + "span.sr-only \"North Macedonia\" 106>1",
3667 + "span.sr-only \"Russia\" 40>1",
3668 + "span.sr-only \"Iran\" 23>1",
3669 + "span.sr-only \"Iraq\" 24>1"
3670 + ],
3671 + "small": [],
3672 + "smallCount": 0,
3673 + "h1Covered": false,
3674 + "h1Top": 136,
3675 + "headerBottom": 57,
3676 + "lang": "en",
3677 + "metaDesc": 227,
3678 + "h1Count": 1,
3679 + "skipped": [],
3680 + "unnamed": [],
3681 + "ariaCurrent": 1,
3682 + "footerOk": true,
3683 + "contrast": [],
3684 + "contrastChecked": 539,
3685 + "height": 5800,
3686 + "title": "Pulse · DataCenterIndex",
3687 + "errors": [
3688 + "pageerror SyntaxError: Invalid or unexpected token"
3689 + ],
3690 + "badResponses": []
3691 + },
3692 + {
3693 + "name": "explore",
3694 + "path": "/explore?entity=facilities&country=US&view=charts",
3695 + "w": 1280,
3696 + "theme": "dark",
3697 + "status": 200,
3698 + "ttfb": 66,
3699 + "overflow": null,
3700 + "clipped": [
3701 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3702 + ],
3703 + "small": [],
3704 + "smallCount": 0,
3705 + "h1Covered": false,
3706 + "h1Top": 136,
3707 + "headerBottom": 57,
3708 + "lang": "en",
3709 + "metaDesc": 236,
3710 + "h1Count": 1,
3711 + "skipped": [],
3712 + "unnamed": [],
3713 + "ariaCurrent": 0,
3714 + "footerOk": true,
3715 + "contrast": [],
3716 + "contrastChecked": 231,
3717 + "height": 2610,
3718 + "title": "Explore · DataCenterIndex",
3719 + "errors": [],
3720 + "badResponses": []
3721 + },
3722 + {
3723 + "name": "explore-projects",
3724 + "path": "/explore?entity=projects&view=table",
3725 + "w": 1280,
3726 + "theme": "dark",
3727 + "status": 200,
3728 + "ttfb": 133,
3729 + "overflow": null,
3730 + "clipped": [
3731 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3732 + "span.sr-only \"Luxembourg\" 71>1",
3733 + "span.sr-only \"United Kingdom\" 91>1",
3734 + "span.sr-only \"United Kingdom\" 91>1",
3735 + "span.sr-only \"Singapore\" 58>1",
3736 + "span.sr-only \"Singapore\" 58>1",
3737 + "span.sr-only \"Australia\" 49>1",
3738 + "span.sr-only \"United Kingdom\" 91>1",
3739 + "span.sr-only \"United Kingdom\" 91>1"
3740 + ],
3741 + "small": [],
3742 + "smallCount": 0,
3743 + "h1Covered": false,
3744 + "h1Top": 136,
3745 + "headerBottom": 57,
3746 + "lang": "en",
3747 + "metaDesc": 236,
3748 + "h1Count": 1,
3749 + "skipped": [],
3750 + "unnamed": [],
3751 + "ariaCurrent": 1,
3752 + "footerOk": true,
3753 + "contrast": [],
3754 + "contrastChecked": 367,
3755 + "height": 3452,
3756 + "title": "Explore · DataCenterIndex",
3757 + "errors": [],
3758 + "badResponses": []
3759 + },
3760 + {
3761 + "name": "compare",
3762 + "path": "/compare/operators?slugs=equinix,digital-realty",
3763 + "w": 1280,
3764 + "theme": "dark",
3765 + "status": 200,
3766 + "ttfb": 73,
3767 + "overflow": null,
3768 + "clipped": [
3769 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3770 + "span.sr-only \"United States\" 70>1",
3771 + "span.sr-only \"United States\" 70>1"
3772 + ],
3773 + "small": [],
3774 + "smallCount": 0,
3775 + "h1Covered": false,
3776 + "h1Top": 136,
3777 + "headerBottom": 57,
3778 + "lang": "en",
3779 + "metaDesc": 204,
3780 + "h1Count": 1,
3781 + "skipped": [],
3782 + "unnamed": [],
3783 + "ariaCurrent": 0,
3784 + "footerOk": true,
3785 + "contrast": [],
3786 + "contrastChecked": 157,
3787 + "height": 2442,
3788 + "title": "Compare operators · DataCenterIndex",
3789 + "errors": [],
3790 + "badResponses": []
3791 + },
3792 + {
3793 + "name": "compare-empty",
3794 + "path": "/compare/operators",
3795 + "w": 1280,
3796 + "theme": "dark",
3797 + "status": 200,
3798 + "ttfb": 39,
3799 + "overflow": null,
3800 + "clipped": [
3801 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3802 + ],
3803 + "small": [],
3804 + "smallCount": 0,
3805 + "h1Covered": false,
3806 + "h1Top": 136,
3807 + "headerBottom": 57,
3808 + "lang": "en",
3809 + "metaDesc": 204,
3810 + "h1Count": 1,
3811 + "skipped": [],
3812 + "unnamed": [],
3813 + "ariaCurrent": 0,
3814 + "footerOk": true,
3815 + "contrast": [],
3816 + "contrastChecked": 64,
3817 + "height": 1228,
3818 + "title": "Compare operators · DataCenterIndex",
3819 + "errors": [
3820 + "pageerror SyntaxError: Invalid or unexpected token"
3821 + ],
3822 + "badResponses": []
3823 + },
3824 + {
3825 + "name": "coverage",
3826 + "path": "/coverage",
3827 + "w": 1280,
3828 + "theme": "dark",
3829 + "status": 200,
3830 + "ttfb": 1828,
3831 + "overflow": null,
3832 + "clipped": [
3833 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3834 + "span.sr-only \"United States\" 83>1",
3835 + "span.sr-only \"Germany\" 57>1",
3836 + "span.sr-only \"United Kingdom\" 100>1",
3837 + "span.sr-only \"France\" 43>1",
3838 + "span.sr-only \"Brazil\" 35>1",
3839 + "span.sr-only \"India\" 30>1",
3840 + "span.sr-only \"Australia\" 55>1",
3841 + "span.sr-only \"Indonesia\" 60>1"
3842 + ],
3843 + "small": [],
3844 + "smallCount": 0,
3845 + "h1Covered": false,
3846 + "h1Top": 136,
3847 + "headerBottom": 57,
3848 + "lang": "en",
3849 + "metaDesc": 218,
3850 + "h1Count": 1,
3851 + "skipped": [],
3852 + "unnamed": [],
3853 + "ariaCurrent": 1,
3854 + "footerOk": true,
3855 + "contrast": [],
3856 + "contrastChecked": 1501,
3857 + "height": 12126,
3858 + "title": "Coverage · DataCenterIndex",
3859 + "errors": [],
3860 + "badResponses": []
3861 + },
3862 + {
3863 + "name": "coverage-sources",
3864 + "path": "/coverage/sources",
3865 + "w": 1280,
3866 + "theme": "dark",
3867 + "status": 200,
3868 + "ttfb": 678,
3869 + "overflow": null,
3870 + "clipped": [
3871 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3872 + ],
3873 + "small": [],
3874 + "smallCount": 0,
3875 + "h1Covered": false,
3876 + "h1Top": 136,
3877 + "headerBottom": 57,
3878 + "lang": "en",
3879 + "metaDesc": 189,
3880 + "h1Count": 1,
3881 + "skipped": [],
3882 + "unnamed": [],
3883 + "ariaCurrent": 0,
3884 + "footerOk": true,
3885 + "contrast": [],
3886 + "contrastChecked": 911,
3887 + "height": 6816,
3888 + "title": "Source coverage · DataCenterIndex",
3889 + "errors": [],
3890 + "badResponses": []
3891 + },
3892 + {
3893 + "name": "power",
3894 + "path": "/power",
3895 + "w": 1280,
3896 + "theme": "dark",
3897 + "status": 200,
3898 + "ttfb": 767,
3899 + "overflow": null,
3900 + "clipped": [
3901 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3902 + "span.sr-only \"United States\" 73>1",
3903 + "span.sr-only \"United Kingdom\" 87>1",
3904 + "span.sr-only \"United States\" 73>1",
3905 + "span.sr-only \"United States\" 73>1",
3906 + "span.sr-only \"United States\" 89>1",
3907 + "span.sr-only \"United Kingdom\" 106>1",
3908 + "span.sr-only \"United States\" 89>1",
3909 + "span.sr-only \"United States\" 89>1"
3910 + ],
3911 + "small": [],
3912 + "smallCount": 0,
3913 + "h1Covered": false,
3914 + "h1Top": 136,
3915 + "headerBottom": 57,
3916 + "lang": "en",
3917 + "metaDesc": 211,
3918 + "h1Count": 1,
3919 + "skipped": [],
3920 + "unnamed": [],
3921 + "ariaCurrent": 0,
3922 + "footerOk": true,
3923 + "contrast": [],
3924 + "contrastChecked": 1501,
3925 + "height": 13817,
3926 + "title": "Power & grid · DataCenterIndex",
3927 + "errors": [
3928 + "pageerror SyntaxError: Invalid or unexpected token"
3929 + ],
3930 + "badResponses": []
3931 + },
3932 + {
3933 + "name": "connectivity",
3934 + "path": "/connectivity",
3935 + "w": 1280,
3936 + "theme": "dark",
3937 + "status": 200,
3938 + "ttfb": 194,
3939 + "overflow": null,
3940 + "clipped": [
3941 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3942 + "span.sr-only \"Germany\" 56>1",
3943 + "span.sr-only \"United Kingdom\" 99>1",
3944 + "span.sr-only \"Germany\" 56>1",
3945 + "span.sr-only \"Singapore\" 62>1",
3946 + "span.sr-only \"Brazil\" 34>1",
3947 + "span.sr-only \"Netherlands\" 75>1",
3948 + "span.sr-only \"United States\" 82>1",
3949 + "span.sr-only \"United States\" 82>1"
3950 + ],
3951 + "small": [],
3952 + "smallCount": 0,
3953 + "h1Covered": false,
3954 + "h1Top": 136,
3955 + "headerBottom": 57,
3956 + "lang": "en",
3957 + "metaDesc": 239,
3958 + "h1Count": 1,
3959 + "skipped": [],
3960 + "unnamed": [],
3961 + "ariaCurrent": 0,
3962 + "footerOk": true,
3963 + "contrast": [],
3964 + "contrastChecked": 981,
3965 + "height": 7430,
3966 + "title": "Connectivity · DataCenterIndex",
3967 + "errors": [
3968 + "pageerror SyntaxError: Invalid or unexpected token"
3969 + ],
3970 + "badResponses": []
3971 + },
3972 + {
3973 + "name": "download",
3974 + "path": "/download",
3975 + "w": 1280,
3976 + "theme": "dark",
3977 + "status": 200,
3978 + "ttfb": 77,
3979 + "overflow": null,
3980 + "clipped": [
3981 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3982 + ],
3983 + "small": [],
3984 + "smallCount": 0,
3985 + "h1Covered": false,
3986 + "h1Top": 136,
3987 + "headerBottom": 57,
3988 + "lang": "en",
3989 + "metaDesc": 216,
3990 + "h1Count": 1,
3991 + "skipped": [],
3992 + "unnamed": [],
3993 + "ariaCurrent": 0,
3994 + "footerOk": true,
3995 + "contrast": [],
3996 + "contrastChecked": 361,
3997 + "height": 3996,
3998 + "title": "Downloads · DataCenterIndex",
3999 + "errors": [
4000 + "pageerror SyntaxError: Invalid or unexpected token"
4001 + ],
4002 + "badResponses": []
4003 + },
4004 + {
4005 + "name": "developers",
4006 + "path": "/developers",
4007 + "w": 1280,
4008 + "theme": "dark",
4009 + "status": 200,
4010 + "ttfb": 201,
4011 + "overflow": null,
4012 + "clipped": [
4013 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4014 + ],
4015 + "small": [],
4016 + "smallCount": 0,
4017 + "h1Covered": false,
4018 + "h1Top": 136,
4019 + "headerBottom": 57,
4020 + "lang": "en",
4021 + "metaDesc": 210,
4022 + "h1Count": 1,
4023 + "skipped": [],
4024 + "unnamed": [],
4025 + "ariaCurrent": 0,
4026 + "footerOk": true,
4027 + "contrast": [],
4028 + "contrastChecked": 1443,
4029 + "height": 19510,
4030 + "title": "API · DataCenterIndex",
4031 + "errors": [
4032 + "pageerror SyntaxError: Invalid or unexpected token"
4033 + ],
4034 + "badResponses": []
4035 + },
4036 + {
4037 + "name": "watchlist",
4038 + "path": "/watchlist",
4039 + "w": 1280,
4040 + "theme": "dark",
4041 + "status": 200,
4042 + "ttfb": 38,
4043 + "overflow": null,
4044 + "clipped": [
4045 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4046 + ],
4047 + "small": [],
4048 + "smallCount": 0,
4049 + "h1Covered": false,
4050 + "h1Top": 136,
4051 + "headerBottom": 57,
4052 + "lang": "en",
4053 + "metaDesc": 142,
4054 + "h1Count": 1,
4055 + "skipped": [],
4056 + "unnamed": [],
4057 + "ariaCurrent": 0,
4058 + "footerOk": true,
4059 + "contrast": [],
4060 + "contrastChecked": 59,
4061 + "height": 1025,
4062 + "title": "Watchlist · DataCenterIndex",
4063 + "errors": [],
4064 + "badResponses": []
4065 + }
4066 + ]
4067 +}
\ No newline at end of file
added apps/web/qa/sweep-final/sources-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/sources-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/sources-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/sources-390-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/watchlist-1280-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/watchlist-1280-light.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/watchlist-390-dark.png +0 −0

Binary file not shown.

added apps/web/qa/sweep-final/watchlist-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep.mjs +21 −0
@@ -41,8 +41,29 @@ export const PAGES = [
41 41 ["methodology", "/methodology"],
42 42 ["about", "/about"],
43 43 ["bot", "/bot"],
44 + // intelligence graph (2026-09-12)
45 + ["ixp", "/ixps/de-cix-new-york"],
46 + ["ai", "/ai-infrastructure"],
47 + ["pulse", "/pulse"],
48 + ["pulse-30d", "/pulse?window=30d"],
49 + ["explore", "/explore?entity=facilities&country=US&view=charts"],
50 + ["explore-projects", "/explore?entity=projects&view=table"],
51 + ["compare", "/compare/operators?slugs=equinix,digital-realty"],
52 + ["compare-empty", "/compare/operators"],
53 + ["coverage", "/coverage"],
54 + ["coverage-sources", "/coverage/sources"],
55 + ["power", "/power"],
56 + ["connectivity", "/connectivity"],
57 + ["download", "/download"],
58 + ["developers", "/developers"],
59 + ["watchlist", "/watchlist"],
44 60 ["404", "/this-page-does-not-exist"],
45 61 ];
62 +// EXTRA_PAGES="name=/path,name2=/path2" appends ad-hoc pages (QA of a route before it joins the list above)
63 +for (const pair of (process.env.EXTRA_PAGES ?? "").split(",").filter(Boolean)) {
64 + const i = pair.indexOf("=");
65 + if (i > 0) PAGES.push([pair.slice(0, i), pair.slice(i + 1)]);
66 +}
46 67 const ONLY = process.env.PAGES ? new Set(process.env.PAGES.split(",")) : null;
47 68 const LIGHT_W = (process.env.WIDTHS ?? (process.env.QUICK ? "390,1280" : "320,360,375,390,414,430,768,1280")).split(",").map(Number);
48 69 const VARIANTS = [...LIGHT_W.map((w) => ({ w, theme: "light" })), { w: 390, theme: "dark" }, { w: 1280, theme: "dark" }];
modified apps/web/qa/sweep/fac-google-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-google-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-google-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-google-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-operator-mw-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-operator-mw-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-operator-mw-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-operator-mw-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-osm-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-osm-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-osm-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-osm-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-wikidata-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-wikidata-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-wikidata-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/fac-wikidata-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/home-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/home-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/home-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/home-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/map-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/map-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/map-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/map-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/project-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/project-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/project-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/project-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/projects-1280-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/projects-1280-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/projects-390-dark.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/projects-390-light.png +0 −0

Binary file not shown.

modified apps/web/qa/sweep/results.json +716 −5349
@@ -1,6 +1,6 @@
1 1 {
2 2 "base": "http://localhost:8314",
3 − "at": "2026-09-11T14:00:33.338Z",
3 + "at": "2026-09-12T05:38:37.843Z",
4 4 "results": [
5 5 {
6 6 "name": "home",
@@ -8,54 +8,63 @@
8 8 "w": 390,
9 9 "theme": "light",
10 10 "status": 200,
11 − "ttfb": 7,
11 + "ttfb": 260,
12 12 "overflow": null,
13 13 "clipped": [
14 14 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
15 − "span.sr-only \"United States\" 89>1",
16 − "span.sr-only \"United States\" 89>1",
17 − "span.sr-only \"United States\" 89>1",
18 − "span.sr-only \"United States\" 76>1",
19 − "span.sr-only \"United States\" 76>1",
20 − "span.sr-only \"United Kingdom\" 99>1",
21 − "span.sr-only \"France\" 42>1",
22 − "span.sr-only \"Singapore\" 62>1"
15 + "span.sr-only \"Ukraine\" 44>1",
16 + "span.sr-only \"Belarus\" 42>1",
17 + "span.sr-only \"Russia\" 37>1",
18 + "span.sr-only \"Russia\" 37>1",
19 + "span.sr-only \"North Macedonia\" 98>1",
20 + "span.sr-only \"Russia\" 37>1",
21 + "span.sr-only \"Iran\" 22>1",
22 + "span.sr-only \"Iraq\" 22>1"
23 23 ],
24 24 "small": [
25 25 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
26 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
27 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
28 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
29 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
30 − "button.mr-1.5.inline-flex.size-9 36×36 \"Search\"",
31 − "button.shrink-0.rounded-sm.border 111×27 \"Northern Virginia\"",
32 − "button.shrink-0.rounded-sm.border 131×27 \"hyperscale in Ireland\"",
33 − "button.shrink-0.rounded-sm.border 179×27 \"> 100 MW under construct\"",
34 − "button.shrink-0.rounded-sm.border 57×27 \"Equinix\"",
35 − "button.shrink-0.rounded-sm.border 51×27 \"Japan\"",
36 − "a.inline-flex.items-center.gap-1 79×18 \"Open map→\""
37 − ],
38 − "smallCount": 94,
26 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇺🇦Ukrai\"",
27 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇧🇾Belar\"",
28 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇷🇺Russi\"",
29 + "a.inline-flex.h-7.max-w-[260px] 214×28 \"ООО «ДатаПорт»→🇷🇺Russi\"",
30 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇲🇰North\"",
31 + "a.inline-flex.h-7.max-w-[260px] 156×28 \"آفاق اندیش داده پردیس→🇷\"",
32 + "a.inline-flex.h-7.max-w-[260px] 156×28 \"آفاق اندیش داده پردیس→🇮\"",
33 + "a.inline-flex.h-7.max-w-[260px] 156×28 \"آفاق اندیش داده پردیس→🇮\"",
34 + "a.inline-flex.min-w-0.items-center 99×18 \"🇺🇸United States United\"",
35 + "a.inline-flex.min-w-0.items-center 115×18 \"🇬🇧United Kingdom Unite\"",
36 + "a.inline-flex.min-w-0.items-center 74×18 \"🇩🇪Germany Germany\""
37 + ],
38 + "smallCount": 83,
39 39 "h1Covered": false,
40 40 "h1Top": 100,
41 41 "headerBottom": 49,
42 42 "lang": "en",
43 − "metaDesc": 159,
43 + "metaDesc": 234,
44 44 "h1Count": 1,
45 45 "skipped": [],
46 46 "unnamed": [],
47 − "ariaCurrent": 1,
47 + "ariaCurrent": 0,
48 48 "footerOk": true,
49 49 "contrast": [],
50 50 "contrastChecked": 0,
51 − "height": 6300,
52 − "title": "DataCenterIndex — The global index of data center infrastructure",
51 + "height": 10898,
52 + "title": "DataCenterIndex — Global data center infrastructure, indexed and versioned",
53 53 "bottom": {
54 54 "navVisible": true,
55 55 "covered": false,
56 − "gap": 809
56 + "gap": 1506
57 + },
58 + "inter": {
59 + "paletteOpens": true,
60 + "paletteCloses": true,
61 + "themeToggles": true,
62 + "focusVisible": {
63 + "el": "input[Search the index]",
64 + "outline": false,
65 + "ring": false
66 + }
57 67 },
58 − "error": "TimeoutError: locator.click: Timeout 30000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('button[aria-label*=\"theme\" i]').first()\u001b[22m\n\u001b[2m - locator resolved to <button type=\"button\" title=\"Sw",
59 68 "errors": [],
60 69 "badResponses": []
61 70 },
@@ -65,31 +74,33 @@
65 74 "w": 390,
66 75 "theme": "light",
67 76 "status": 200,
68 − "ttfb": 2,
77 + "ttfb": 54,
69 78 "overflow": null,
70 79 "clipped": [
71 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
80 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
81 + "h1.sr-only \"Global map of data center infrastructure\" 259>1"
72 82 ],
73 83 "small": [
74 84 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
75 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
76 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
77 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
78 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
79 85 "button.maplibregl-ctrl-geolocate 32×32 \"Location not available\"",
80 86 "button.maplibregl-ctrl-zoom-in 32×32 \"Zoom in\"",
81 87 "button.maplibregl-ctrl-zoom-out 32×32 \"Zoom out\"",
82 − "input.h-full.min-w-0.flex-1 296×38 \"Search the map\"",
83 − "button.inline-flex.h-8.items-center 32×32 \"Filters\"",
84 − "button.inline-flex.h-8.items-center 93×32 \"Legend\""
85 − ],
86 − "smallCount": 11,
87 − "h1Covered": false,
88 − "h1Top": null,
88 + "input.h-full.min-w-0.flex-1 288×38 \"Search the map\"",
89 + "button.inline-flex.h-8.min-w-10 40×32 \"Filters\"",
90 + "button.inline-flex.shrink-0.items-center 98×36 \"Facilities\"",
91 + "button.inline-flex.shrink-0.items-center 99×36 \"Capacity\"",
92 + "button.inline-flex.shrink-0.items-center 95×36 \"Projects\"",
93 + "button.inline-flex.shrink-0.items-center 96×36 \"AI / HPC\"",
94 + "button.inline-flex.shrink-0.items-center 81×36 \"Cloud\"",
95 + "button.inline-flex.shrink-0.items-center 73×36 \"IXPs\""
96 + ],
97 + "smallCount": 15,
98 + "h1Covered": false,
99 + "h1Top": 48,
89 100 "headerBottom": 49,
90 101 "lang": "en",
91 102 "metaDesc": 127,
92 − "h1Count": 0,
103 + "h1Count": 1,
93 104 "skipped": [],
94 105 "unnamed": [],
95 106 "ariaCurrent": 2,
@@ -110,208 +121,41 @@
110 121 "errors": [],
111 122 "badResponses": []
112 123 },
113 − {
114 − "name": "search",
115 − "path": "/search?q=montreal",
116 − "w": 390,
117 − "theme": "light",
118 − "status": 200,
119 − "ttfb": 7,
120 − "overflow": null,
121 − "clipped": [
122 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
123 − ],
124 − "small": [
125 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
126 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
127 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
128 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
129 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
130 − "a.inline-flex.items-center.justify-center 122×32 \"Open in map\"",
131 − "a.ml-1.text-ink-2.underline 85×17 \"Apply as filters\"",
132 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
133 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
134 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
135 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
136 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\""
137 − ],
138 − "smallCount": 22,
139 − "h1Covered": false,
140 − "h1Top": 90,
141 − "headerBottom": 49,
142 − "lang": "en",
143 − "metaDesc": 159,
144 − "h1Count": 1,
145 − "skipped": [],
146 − "unnamed": [],
147 − "ariaCurrent": 0,
148 − "footerOk": true,
149 − "contrast": [],
150 − "contrastChecked": 0,
151 − "height": 1871,
152 − "title": "“montreal” — search · DataCenterIndex",
153 − "bottom": {
154 − "navVisible": true,
155 − "covered": false,
156 − "gap": 810
157 − },
158 − "errors": [],
159 − "badResponses": []
160 − },
161 − {
162 − "name": "live",
163 − "path": "/live",
164 − "w": 390,
165 − "theme": "light",
166 − "status": 200,
167 − "ttfb": 6,
168 − "overflow": null,
169 − "clipped": [
170 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
171 − "span.sr-only \"United States\" 89>1",
172 − "span.sr-only \"United States\" 89>1",
173 − "span.sr-only \"United States\" 89>1",
174 − "span.sr-only \"United States\" 89>1",
175 − "span.sr-only \"United States\" 89>1",
176 − "span.sr-only \"United States\" 89>1",
177 − "span.sr-only \"United States\" 89>1",
178 − "span.sr-only \"United States\" 89>1"
179 − ],
180 − "small": [
181 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
182 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
183 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
184 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
185 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
186 − "button.inline-flex.items-center.justify-center 80×32 \"Filters\"",
187 − "a.inline-flex.items-center.gap-1 181×17 \"Duke Energy — news relea\"",
188 − "a.inline-flex.items-center.gap-1 245×17 \"American Electric Power \"",
189 − "a.hover:text-ink 85×17 \"Element Critical\"",
190 − "a.inline-flex.items-center.gap-1 101×17 \"Element Critical\"",
191 − "a.hover:text-ink 85×17 \"Element Critical\"",
192 − "a.inline-flex.items-center.gap-1 101×17 \"Element Critical\""
193 − ],
194 − "smallCount": 120,
195 − "h1Covered": false,
196 − "h1Top": 91,
197 − "headerBottom": 49,
198 − "lang": "en",
199 − "metaDesc": 152,
200 − "h1Count": 1,
201 − "skipped": [
202 − "h1→h3"
203 − ],
204 − "unnamed": [],
205 − "ariaCurrent": 1,
206 − "footerOk": true,
207 − "contrast": [],
208 − "contrastChecked": 0,
209 − "height": 10195,
210 − "title": "Live changes · DataCenterIndex",
211 − "bottom": {
212 − "navVisible": true,
213 − "covered": false,
214 − "gap": 809
215 − },
216 − "inter": {
217 − "filterSheetOpens": true,
218 − "filterSheetCloses": true
219 − },
220 − "errors": [],
221 − "badResponses": []
222 − },
223 − {
224 − "name": "datacenters",
225 − "path": "/datacenters",
226 − "w": 390,
227 − "theme": "light",
228 − "status": 200,
229 − "ttfb": 4,
230 − "overflow": null,
231 − "clipped": [
232 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
233 − ],
234 − "small": [
235 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
236 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
237 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
238 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
239 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
240 − "a.inline-flex.items-center.justify-center 122×32 \"Open in map\"",
241 − "button.inline-flex.items-center.justify-center 80×32 \"Filters\"",
242 − "a.font-medium.text-ink.hover:text-accent 186×18 \"Bulk N01 Data Center Cam\"",
243 − "a.font-medium.text-ink.hover:text-accent 225×35 \"Downtown Minneapolis Dat\"",
244 − "a.font-medium.text-ink.hover:text-accent 225×35 \"Dallas Love Field Data C\"",
245 − "a.font-medium.text-ink.hover:text-accent 139×18 \"CloudHQ SAT Campus\"",
246 − "a.font-medium.text-ink.hover:text-accent 183×18 \"Skybox PowerCampus Austi\""
247 − ],
248 − "smallCount": 33,
249 − "h1Covered": false,
250 − "h1Top": 90,
251 − "headerBottom": 49,
252 − "lang": "en",
253 − "metaDesc": 119,
254 − "h1Count": 1,
255 − "skipped": [],
256 − "unnamed": [],
257 − "ariaCurrent": 2,
258 − "footerOk": true,
259 − "contrast": [],
260 − "contrastChecked": 0,
261 − "height": 2286,
262 − "title": "Data centers · DataCenterIndex",
263 − "bottom": {
264 − "navVisible": true,
265 − "covered": false,
266 − "gap": 810
267 − },
268 − "inter": {
269 − "filterSheetOpens": true,
270 − "filterSheetCloses": true,
271 − "paginationPage2": true,
272 − "page2Rows": 50
273 − },
274 − "errors": [
275 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
276 − ],
277 − "badResponses": [
278 − "404 fetch /api/v1/docs?_rsc=2QMc8M2_JReeYqv7"
279 − ]
280 − },
281 124 {
282 125 "name": "fac-operator-mw",
283 126 "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
284 127 "w": 390,
285 128 "theme": "light",
286 129 "status": 200,
287 − "ttfb": 12,
130 + "ttfb": 267,
288 131 "overflow": null,
289 132 "clipped": [
290 133 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
291 134 "span.sr-only \"United States\" 86>1",
292 135 "span.sr-only \"United States\" 82>1",
136 + "span.sr-only \"United States\" 76>1",
293 137 "span.sr-only \"United States\" 89>1"
294 138 ],
295 139 "small": [
296 140 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
297 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
298 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
299 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
300 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
301 141 "a.hover:text-ink.hover:underline 34×17 \"Home\"",
302 142 "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
303 143 "a.hover:text-ink.hover:underline 117×17 \"Minneapolis–St. Paul\"",
304 144 "a.normal-case.tracking-normal.text-[12.5px] 57×18 \"DataBank\"",
305 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
145 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
306 146 "a.hover:text-accent.hover:underline 180×20 \"Minneapolis–St. Paul mar\"",
307 − "a.hover:text-accent.hover:underline 86×20 \"United States\""
147 + "a.hover:text-accent.hover:underline 86×20 \"United States\"",
148 + "button.group/ev.rounded-xs.text-left 95×22 \"Evidence for Status\"",
149 + "button.group/ev.rounded-xs.text-left 141×22 \"Evidence for Confidence\"",
150 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
151 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
308 152 ],
309 − "smallCount": 65,
153 + "smallCount": 87,
310 154 "h1Covered": false,
311 155 "h1Top": 119,
312 156 "headerBottom": 49,
313 157 "lang": "en",
314 − "metaDesc": 212,
158 + "metaDesc": 250,
315 159 "h1Count": 1,
316 160 "skipped": [],
317 161 "unnamed": [],
@@ -319,12 +163,12 @@
319 163 "footerOk": true,
320 164 "contrast": [],
321 165 "contrastChecked": 0,
322 − "height": 6136,
166 + "height": 11739,
323 167 "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
324 168 "bottom": {
325 169 "navVisible": true,
326 170 "covered": false,
327 − "gap": 1106
171 + "gap": 1882
328 172 },
329 173 "errors": [],
330 174 "badResponses": []
@@ -335,7 +179,7 @@
335 179 "w": 390,
336 180 "theme": "light",
337 181 "status": 200,
338 − "ttfb": 12,
182 + "ttfb": 207,
339 183 "overflow": null,
340 184 "clipped": [
341 185 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
@@ -345,24 +189,24 @@
345 189 ],
346 190 "small": [
347 191 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
348 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
349 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
350 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
351 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
352 192 "a.hover:text-ink.hover:underline 34×17 \"Home\"",
353 193 "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
354 194 "a.hover:text-ink.hover:underline 55×17 \"Argentina\"",
355 195 "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
356 196 "a.hover:text-accent.hover:underline 62×20 \"Argentina\"",
197 + "button.group/ev.rounded-xs.text-left 95×22 \"Evidence for Status\"",
198 + "button.group/ev.rounded-xs.text-left 107×22 \"Evidence for Confidence\"",
199 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
357 200 "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
358 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
201 + "a.hover:text-accent.hover:underline 59×19 \"Argentina\"",
202 + "button.group/ev.rounded-xs.text-left 131×18 \"Evidence for Latitude\""
359 203 ],
360 − "smallCount": 46,
204 + "smallCount": 31,
361 205 "h1Covered": false,
362 206 "h1Top": 116,
363 207 "headerBottom": 49,
364 208 "lang": "en",
365 − "metaDesc": 128,
209 + "metaDesc": 141,
366 210 "h1Count": 1,
367 211 "skipped": [],
368 212 "unnamed": [],
@@ -370,12 +214,12 @@
370 214 "footerOk": true,
371 215 "contrast": [],
372 216 "contrastChecked": 0,
373 − "height": 4626,
217 + "height": 8087,
374 218 "title": "Shelter Modernización · DataCenterIndex",
375 219 "bottom": {
376 220 "navVisible": true,
377 221 "covered": false,
378 − "gap": 1106
222 + "gap": 1881
379 223 },
380 224 "errors": [],
381 225 "badResponses": []
@@ -386,12 +230,13 @@
386 230 "w": 390,
387 231 "theme": "light",
388 232 "status": 200,
389 − "ttfb": 21,
233 + "ttfb": 106,
390 234 "overflow": null,
391 235 "clipped": [
392 236 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
393 237 "span.sr-only \"United States\" 86>1",
394 238 "span.sr-only \"United States\" 82>1",
239 + "span.sr-only \"Singapore\" 58>1",
395 240 "span.sr-only \"United States\" 89>1",
396 241 "span.sr-only \"United States\" 89>1",
397 242 "span.sr-only \"United States\" 89>1",
@@ -399,24 +244,24 @@
399 244 ],
400 245 "small": [
401 246 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
402 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
403 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
404 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
405 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
406 247 "a.hover:text-ink.hover:underline 34×17 \"Home\"",
407 248 "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
408 249 "a.hover:text-ink.hover:underline 109×17 \"Hillsboro / Portland\"",
409 250 "a.normal-case.tracking-normal.text-[12.5px] 43×18 \"Google\"",
410 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
251 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
411 252 "a.hover:text-accent.hover:underline 170×20 \"Hillsboro / Portland mar\"",
412 − "a.hover:text-accent.hover:underline 86×20 \"United States\""
253 + "a.hover:text-accent.hover:underline 86×20 \"United States\"",
254 + "button.group/ev.rounded-xs.text-left 95×22 \"Evidence for Status\"",
255 + "button.group/ev.rounded-xs.text-left 141×22 \"Evidence for Confidence\"",
256 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
257 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
413 258 ],
414 − "smallCount": 101,
259 + "smallCount": 78,
415 260 "h1Covered": false,
416 261 "h1Top": 119,
417 262 "headerBottom": 49,
418 263 "lang": "en",
419 − "metaDesc": 203,
264 + "metaDesc": 216,
420 265 "h1Count": 1,
421 266 "skipped": [],
422 267 "unnamed": [],
@@ -424,12 +269,12 @@
424 269 "footerOk": true,
425 270 "contrast": [],
426 271 "contrastChecked": 0,
427 − "height": 10760,
272 + "height": 11047,
428 273 "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
429 274 "bottom": {
430 275 "navVisible": true,
431 276 "covered": false,
432 − "gap": 1106
277 + "gap": 1881
433 278 },
434 279 "errors": [],
435 280 "badResponses": []
@@ -440,7 +285,7 @@
440 285 "w": 390,
441 286 "theme": "light",
442 287 "status": 200,
443 − "ttfb": 13,
288 + "ttfb": 254,
444 289 "overflow": null,
445 290 "clipped": [
446 291 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
@@ -450,24 +295,24 @@
450 295 ],
451 296 "small": [
452 297 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
453 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
454 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
455 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
456 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
457 298 "a.hover:text-ink.hover:underline 34×17 \"Home\"",
458 299 "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
459 300 "a.hover:text-ink.hover:underline 51×17 \"Denmark\"",
460 301 "a.normal-case.tracking-normal.text-[12.5px] 35×18 \"Apple\"",
461 302 "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
462 303 "a.hover:text-accent.hover:underline 58×20 \"Denmark\"",
304 + "button.group/ev.rounded-xs.text-left 83×22 \"Evidence for Status\"",
305 + "button.group/ev.rounded-xs.text-left 168×22 \"Evidence for Confidence\"",
306 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
307 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
463 308 "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
464 309 ],
465 − "smallCount": 57,
310 + "smallCount": 45,
466 311 "h1Covered": false,
467 312 "h1Top": 119,
468 313 "headerBottom": 49,
469 314 "lang": "en",
470 − "metaDesc": 158,
315 + "metaDesc": 171,
471 316 "h1Count": 1,
472 317 "skipped": [],
473 318 "unnamed": [],
@@ -475,456 +320,390 @@
475 320 "footerOk": true,
476 321 "contrast": [],
477 322 "contrastChecked": 0,
478 − "height": 6042,
323 + "height": 8893,
479 324 "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
480 325 "bottom": {
481 326 "navVisible": true,
482 327 "covered": false,
483 − "gap": 1105
328 + "gap": 1882
484 329 },
485 330 "errors": [],
486 331 "badResponses": []
487 332 },
488 333 {
489 − "name": "operators",
490 − "path": "/operators",
334 + "name": "projects",
335 + "path": "/projects",
491 336 "w": 390,
492 337 "theme": "light",
493 338 "status": 200,
494 − "ttfb": 5,
339 + "ttfb": 493,
495 340 "overflow": null,
496 341 "clipped": [
497 342 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
498 − "span.sr-only \"Search operators\" 114>1"
343 + "span.sr-only \"Luxembourg\" 71>1",
344 + "span.sr-only \"United Kingdom\" 91>1",
345 + "span.sr-only \"United Kingdom\" 91>1",
346 + "span.sr-only \"Singapore\" 58>1",
347 + "span.sr-only \"Singapore\" 58>1",
348 + "span.sr-only \"Australia\" 49>1",
349 + "span.sr-only \"United Kingdom\" 91>1",
350 + "span.sr-only \"United Kingdom\" 91>1"
499 351 ],
500 352 "small": [
501 353 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
502 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
503 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
504 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
505 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
506 − "input.h-9.min-w-0.rounded-sm 218×36 \"\"",
507 − "select.h-9.min-w-0.rounded-sm 140×36 \"Kind\"",
508 − "select.h-9.min-w-0.rounded-sm 160×36 \"Headquarters country\"",
509 − "button.inline-flex.items-center.justify-center 59×36 \"Filter\"",
510 − "a.font-medium.text-ink.hover:text-accent 60×18 \"DataBank\"",
511 − "a.font-medium.text-ink.hover:text-accent 81×18 \"EdgeConneX\"",
512 − "a.font-medium.text-ink.hover:text-accent 62×18 \"CyrusOne\""
354 + "a.dci-rowlink.font-medium.text-ink 209×35 \"STACK Expands to Arizona\"",
355 + "a.dci-rowlink.font-medium.text-ink 167×18 \"LON-East Expansion Video\"",
356 + "a.dci-rowlink.font-medium.text-ink 206×35 \"Green Mountain completes\"",
357 + "a.dci-rowlink.font-medium.text-ink 213×35 \"Green Mountain signs 10 \"",
358 + "a.dci-rowlink.font-medium.text-ink 213×35 \"Green Mountain expands d\"",
359 + "a.dci-rowlink.font-medium.text-ink 213×35 \"Teraco Breaks Ground on \"",
360 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Green Mountain expands i\"",
361 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Successful mountain hall\"",
362 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Element Critical Expands\"",
363 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Is There an AI Data Cent\"",
364 + "a.dci-rowlink.font-medium.text-ink 217×35 \"Chicago Data Centers - A\""
513 365 ],
514 − "smallCount": 75,
366 + "smallCount": 22,
515 367 "h1Covered": false,
516 368 "h1Top": 90,
517 369 "headerBottom": 49,
518 370 "lang": "en",
519 − "metaDesc": 139,
371 + "metaDesc": 213,
520 372 "h1Count": 1,
521 373 "skipped": [],
522 374 "unnamed": [],
523 − "ariaCurrent": 2,
375 + "ariaCurrent": 3,
524 376 "footerOk": true,
525 377 "contrast": [],
526 378 "contrastChecked": 0,
527 − "height": 4954,
528 − "title": "Operators · DataCenterIndex",
379 + "height": 9984,
380 + "title": "Data center projects & construction pipeline · DataCenterIndex",
529 381 "bottom": {
530 382 "navVisible": true,
531 383 "covered": false,
532 − "gap": 810
384 + "gap": 1506
533 385 },
534 386 "inter": {
387 + "filterSheetOpens": false,
388 + "filterSheetCloses": true,
535 389 "paginationPage2": true,
536 − "page2Rows": 0
390 + "page2Rows": 50
537 391 },
538 − "errors": [],
392 + "errors": [
393 + "pageerror SyntaxError: Invalid or unexpected token",
394 + "Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead."
395 + ],
539 396 "badResponses": []
540 397 },
541 398 {
542 − "name": "operator-equinix",
543 − "path": "/operators/equinix",
399 + "name": "project",
400 + "path": "/projects/sydney-data-center-project",
544 401 "w": 390,
545 402 "theme": "light",
546 403 "status": 200,
547 − "ttfb": 9,
404 + "ttfb": 624,
548 405 "overflow": null,
549 406 "clipped": [
550 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
407 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
408 + "span.sr-only \"Australia\" 54>1",
409 + "span.sr-only \"Australia\" 54>1",
410 + "span.sr-only \"Australia\" 58>1"
551 411 ],
552 412 "small": [
553 413 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
554 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
555 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
556 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
557 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
558 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
559 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
560 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
561 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
562 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\"",
563 − "a.inline-flex.min-h-7.items-center 60×28 \"Countries\"",
564 − "a.inline-flex.min-h-7.items-center 85×28 \"Cloud regions\""
414 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
415 + "a.hover:text-ink.hover:underline 46×17 \"Projects\"",
416 + "a.hover:text-ink.hover:underline 92×19 \"Sydney market\"",
417 + "button.group/ev.rounded-xs.text-left 137×22 \"Evidence for Status\"",
418 + "button.group/ev.rounded-xs.text-left 171×18 \"Evidence for Announced\"",
419 + "a.inline-flex.max-w-full.items-center 63×16 \"evidence\"",
420 + "a.inline-flex.min-h-6.items-center 63×24 \"W.Media\"",
421 + "a.inline-flex.items-center.gap-1 53×17 \"source\"",
422 + "a.inline-flex.items-center.gap-1 53×17 \"source\"",
423 + "a.inline-flex.items-center.gap-1 53×17 \"source\"",
424 + "a.inline-flex.items-center.gap-1 53×17 \"source\""
565 425 ],
566 − "smallCount": 20,
426 + "smallCount": 25,
567 427 "h1Covered": false,
568 − "h1Top": null,
428 + "h1Top": 120,
569 429 "headerBottom": 49,
570 430 "lang": "en",
571 − "metaDesc": 159,
572 − "h1Count": 0,
431 + "metaDesc": 201,
432 + "h1Count": 1,
573 433 "skipped": [],
574 434 "unnamed": [],
575 − "ariaCurrent": 1,
435 + "ariaCurrent": 2,
576 436 "footerOk": true,
577 437 "contrast": [],
578 438 "contrastChecked": 0,
579 − "height": 1467,
580 − "title": "Operator · DataCenterIndex",
439 + "height": 6508,
440 + "title": "Sydney data center project · DataCenterIndex",
581 441 "bottom": {
582 442 "navVisible": true,
583 443 "covered": false,
584 − "gap": 809
444 + "gap": 1850
585 445 },
586 − "errors": [
587 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
588 − ],
589 − "badResponses": [
590 − "404 fetch /api/v1/docs?_rsc=NlEMKmKlhE08_1aP"
591 − ]
446 + "errors": [],
447 + "badResponses": []
592 448 },
593 449 {
594 − "name": "countries",
595 − "path": "/countries",
596 − "w": 390,
450 + "name": "home",
451 + "path": "/",
452 + "w": 1280,
597 453 "theme": "light",
598 454 "status": 200,
599 − "ttfb": 24,
455 + "ttfb": 121,
600 456 "overflow": null,
601 457 "clipped": [
602 458 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
603 − "span.sr-only \"United States\" 83>1",
604 − "span.sr-only \"Brazil\" 35>1",
605 − "span.sr-only \"Argentina\" 60>1",
606 − "span.sr-only \"Canada\" 48>1",
607 − "span.sr-only \"Mexico\" 45>1",
608 − "span.sr-only \"Chile\" 32>1",
609 − "span.sr-only \"Venezuela\" 65>1",
610 − "span.sr-only \"Peru\" 29>1"
611 − ],
612 − "small": [
613 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
614 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
615 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
616 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
617 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
618 − "a.inline-flex.h-7.shrink-0 78×28 \"All regions\"",
619 − "a.inline-flex.h-7.shrink-0 90×28 \"Americas18\"",
620 − "a.inline-flex.h-7.shrink-0 77×28 \"Europe38\"",
621 − "a.inline-flex.h-7.shrink-0 61×28 \"Asia34\"",
622 − "a.inline-flex.h-7.shrink-0 77×28 \"Oceania3\"",
623 − "a.inline-flex.h-7.shrink-0 71×28 \"Africa12\"",
624 − "a.font-medium.text-ink.hover:text-accent 109×18 \"🇺🇸United StatesUnited \""
459 + "span.sr-only \"Ukraine\" 44>1",
460 + "span.sr-only \"Belarus\" 42>1",
461 + "span.sr-only \"Russia\" 37>1",
462 + "span.sr-only \"Russia\" 37>1",
463 + "span.sr-only \"North Macedonia\" 98>1",
464 + "span.sr-only \"Russia\" 37>1",
465 + "span.sr-only \"Iran\" 22>1",
466 + "span.sr-only \"Iraq\" 22>1"
625 467 ],
626 − "smallCount": 71,
468 + "small": [],
469 + "smallCount": 0,
627 470 "h1Covered": false,
628 − "h1Top": 90,
629 − "headerBottom": 49,
471 + "h1Top": 120,
472 + "headerBottom": 57,
630 473 "lang": "en",
631 − "metaDesc": 117,
474 + "metaDesc": 234,
632 475 "h1Count": 1,
633 476 "skipped": [],
634 477 "unnamed": [],
635 − "ariaCurrent": 2,
478 + "ariaCurrent": 0,
636 479 "footerOk": true,
637 480 "contrast": [],
638 481 "contrastChecked": 0,
639 − "height": 5206,
640 − "title": "Countries · DataCenterIndex",
641 − "bottom": {
642 − "navVisible": true,
643 − "covered": false,
644 − "gap": 810
482 + "height": 5724,
483 + "title": "DataCenterIndex — Global data center infrastructure, indexed and versioned",
484 + "inter": {
485 + "paletteOpens": true,
486 + "paletteCloses": true,
487 + "themeToggles": true,
488 + "focusVisible": {
489 + "el": "button[Search]",
490 + "outline": true,
491 + "ring": false
492 + }
645 493 },
646 − "errors": [
647 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
648 − ],
649 − "badResponses": [
650 − "404 fetch /api/v1/docs?_rsc=rWPI48G8kG2iJ9dR"
651 − ]
494 + "errors": [],
495 + "badResponses": []
652 496 },
653 497 {
654 − "name": "country-us",
655 − "path": "/countries/united-states",
656 − "w": 390,
498 + "name": "map",
499 + "path": "/map",
500 + "w": 1280,
657 501 "theme": "light",
658 502 "status": 200,
659 − "ttfb": 30,
503 + "ttfb": 43,
660 504 "overflow": null,
661 505 "clipped": [
662 506 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
663 − "span.sr-only \"United States\" 149>1",
664 − "span.sr-only \"United States\" 76>1",
665 − "span.sr-only \"United States\" 76>1",
666 − "span.sr-only \"United States\" 76>1",
667 − "span.sr-only \"United States\" 76>1",
668 − "span.sr-only \"United States\" 76>1",
669 − "span.sr-only \"United States\" 76>1",
670 − "span.sr-only \"United States\" 76>1"
671 − ],
672 − "small": [
673 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
674 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
675 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
676 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
677 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
678 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
679 − "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
680 − "a.inline-flex.h-7.shrink-0 211×28 \"#1/46Countries by cloud \"",
681 − "a.inline-flex.h-7.shrink-0 214×28 \"#1/103Countries by facil\"",
682 − "a.inline-flex.h-7.shrink-0 233×28 \"#24/52Facilities per mil\"",
683 − "a.inline-flex.h-7.shrink-0 338×28 \"#14/14Countries by facil\"",
684 − "a.inline-flex.h-7.shrink-0 247×28 \"#1/28Countries by hypers\""
507 + "h1.sr-only \"Global map of data center infrastructure\" 259>1",
508 + "span.sr-only \"Minimum MW\" 91>1",
509 + "span.sr-only \"Maximum MW\" 95>1",
510 + "span.sr-only \"Opened from\" 87>1",
511 + "span.sr-only \"Opened to\" 69>1",
512 + "span.sr-only \"Expected from\" 97>1",
513 + "span.sr-only \"Expected to\" 79>1"
685 514 ],
686 − "smallCount": 106,
515 + "small": [],
516 + "smallCount": 0,
687 517 "h1Covered": false,
688 − "h1Top": 120,
689 − "headerBottom": 49,
518 + "h1Top": 56,
519 + "headerBottom": 57,
690 520 "lang": "en",
691 − "metaDesc": 258,
521 + "metaDesc": 127,
692 522 "h1Count": 1,
693 523 "skipped": [],
694 524 "unnamed": [],
695 525 "ariaCurrent": 2,
696 − "footerOk": true,
526 + "footerOk": false,
697 527 "contrast": [],
698 528 "contrastChecked": 0,
699 − "height": 9191,
700 − "title": "Data Centers in United States · DataCenterIndex",
701 − "bottom": {
702 − "navVisible": true,
703 − "covered": false,
704 − "gap": 1073
529 + "height": 901,
530 + "title": "Global data center map · DataCenterIndex",
531 + "map": {
532 + "controls": 3,
533 + "unreachable": 0,
534 + "canvas": true
705 535 },
706 536 "inter": {
707 − "showAll": {
708 − "before": 10,
709 − "after": 10
710 − }
537 + "tabSwitch": true,
538 + "zoomVisible": true,
539 + "mapPanelOpens": true
711 540 },
712 − "errors": [
713 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
714 − ],
715 − "badResponses": [
716 − "404 fetch /api/v1/docs?_rsc=mmUTfifLZ71cpSIq"
717 − ]
541 + "errors": [],
542 + "badResponses": []
718 543 },
719 544 {
720 − "name": "country-ca",
721 − "path": "/countries/canada",
722 − "w": 390,
545 + "name": "fac-operator-mw",
546 + "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
547 + "w": 1280,
723 548 "theme": "light",
724 549 "status": 200,
725 − "ttfb": 6,
550 + "ttfb": 158,
726 551 "overflow": null,
727 552 "clipped": [
728 553 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
729 − "span.sr-only \"Canada\" 86>1",
730 − "span.sr-only \"Canada\" 43>1",
731 − "span.sr-only \"Canada\" 43>1",
732 − "span.sr-only \"Canada\" 51>1",
733 − "span.sr-only \"Canada\" 51>1",
734 − "span.sr-only \"Canada\" 51>1",
735 − "span.sr-only \"Canada\" 51>1",
736 − "span.sr-only \"Canada\" 51>1"
737 − ],
738 − "small": [
739 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
740 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
741 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
742 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
743 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
744 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
745 − "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
746 − "a.inline-flex.h-7.shrink-0 211×28 \"#7/46Countries by cloud \"",
747 − "a.inline-flex.h-7.shrink-0 221×28 \"#22/103Countries by faci\"",
748 − "a.inline-flex.h-7.shrink-0 233×28 \"#33/52Facilities per mil\"",
749 − "a.inline-flex.h-7.shrink-0 254×28 \"#12/28Countries by hyper\"",
750 − "a.block 366×36 \"Unknown15\""
554 + "span.sr-only \"United States\" 86>1",
555 + "span.sr-only \"United States\" 82>1",
556 + "span.sr-only \"United States\" 76>1",
557 + "span.sr-only \"United States\" 89>1"
751 558 ],
752 − "smallCount": 80,
559 + "small": [],
560 + "smallCount": 0,
753 561 "h1Covered": false,
754 − "h1Top": 120,
755 − "headerBottom": 49,
562 + "h1Top": 135,
563 + "headerBottom": 57,
756 564 "lang": "en",
757 − "metaDesc": 186,
565 + "metaDesc": 250,
758 566 "h1Count": 1,
759 567 "skipped": [],
760 568 "unnamed": [],
761 − "ariaCurrent": 2,
569 + "ariaCurrent": 1,
762 570 "footerOk": true,
763 571 "contrast": [],
764 572 "contrastChecked": 0,
765 − "height": 7567,
766 − "title": "Data Centers in Canada · DataCenterIndex",
767 − "bottom": {
768 − "navVisible": true,
769 − "covered": false,
770 − "gap": 1073
771 − },
772 − "errors": [
773 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
774 − ],
775 − "badResponses": [
776 − "404 fetch /api/v1/docs?_rsc=uoChELS4xfChicuP"
777 − ]
573 + "height": 7094,
574 + "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
575 + "errors": [],
576 + "badResponses": []
778 577 },
779 578 {
780 − "name": "markets",
781 − "path": "/datacenters/markets",
782 − "w": 390,
783 − "theme": "light",
579 + "name": "fac-osm",
580 + "path": "/datacenters/shelter-modernizacion",
581 + "w": 1280,
582 + "theme": "light",
784 583 "status": 200,
785 − "ttfb": 4,
584 + "ttfb": 72,
786 585 "overflow": null,
787 586 "clipped": [
788 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
789 − ],
790 − "small": [
791 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
792 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
793 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
794 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
795 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
796 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
797 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
798 − "a.inline-flex.items-center.justify-center 86×32 \"Open map\"",
799 − "input.h-9.min-w-0.flex-1 366×18 \"Search markets\"",
800 − "select.h-9.min-w-0.flex-1 299×36 \"Country\"",
801 − "button.inline-flex.items-center.justify-center 59×36 \"Filter\"",
802 − "a.inline-flex.size-9.items-center 36×36 \"Next page\""
587 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
588 + "span.sr-only \"Argentina\" 62>1",
589 + "span.sr-only \"Argentina\" 59>1",
590 + "span.sr-only \"Argentina\" 64>1"
803 591 ],
804 − "smallCount": 27,
592 + "small": [],
593 + "smallCount": 0,
805 594 "h1Covered": false,
806 − "h1Top": 120,
807 − "headerBottom": 49,
595 + "h1Top": 132,
596 + "headerBottom": 57,
808 597 "lang": "en",
809 − "metaDesc": 125,
598 + "metaDesc": 141,
810 599 "h1Count": 1,
811 600 "skipped": [],
812 601 "unnamed": [],
813 − "ariaCurrent": 2,
602 + "ariaCurrent": 1,
814 603 "footerOk": true,
815 604 "contrast": [],
816 605 "contrastChecked": 0,
817 − "height": 5208,
818 − "title": "Data center markets · DataCenterIndex",
819 − "bottom": {
820 − "navVisible": true,
821 − "covered": false,
822 − "gap": 810
823 − },
824 − "errors": [
825 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
826 − ],
827 − "badResponses": [
828 − "404 fetch /api/v1/docs?_rsc=pvP-qw1_z45AIAvk"
829 − ]
606 + "height": 4423,
607 + "title": "Shelter Modernización · DataCenterIndex",
608 + "errors": [],
609 + "badResponses": []
830 610 },
831 611 {
832 − "name": "market-nova",
833 − "path": "/datacenters/markets/northern-virginia",
834 − "w": 390,
612 + "name": "fac-google",
613 + "path": "/datacenters/google-the-dalles-oregon-data-center",
614 + "w": 1280,
835 615 "theme": "light",
836 616 "status": 200,
837 − "ttfb": 277,
617 + "ttfb": 129,
838 618 "overflow": null,
839 619 "clipped": [
840 620 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
621 + "span.sr-only \"United States\" 86>1",
841 622 "span.sr-only \"United States\" 82>1",
842 − "span.sr-only \"United States\" 76>1",
843 − "span.sr-only \"United States\" 89>1",
844 − "span.sr-only \"United States\" 89>1",
623 + "span.sr-only \"Singapore\" 58>1",
845 624 "span.sr-only \"United States\" 89>1",
846 625 "span.sr-only \"United States\" 89>1",
847 626 "span.sr-only \"United States\" 89>1",
848 627 "span.sr-only \"United States\" 89>1"
849 628 ],
850 − "small": [
851 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
852 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
853 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
854 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
855 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
856 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
857 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
858 − "a.hover:text-ink.hover:underline 46×17 \"Markets\"",
859 − "a.text-ink.hover:text-accent.hover:underline 82×19 \"United States\"",
860 − "a.inline-flex.h-7.shrink-0 219×28 \"#72/ 152Metros by facili\"",
861 − "a.inline-flex.h-7.shrink-0 230×28 \"#44/ 129Metros by operat\"",
862 − "a.inline-flex.h-10.items-center 38×40 \"Map\""
863 − ],
864 − "smallCount": 97,
629 + "small": [],
630 + "smallCount": 0,
865 631 "h1Covered": false,
866 − "h1Top": 120,
867 − "headerBottom": 49,
632 + "h1Top": 135,
633 + "headerBottom": 57,
868 634 "lang": "en",
869 − "metaDesc": 250,
635 + "metaDesc": 216,
870 636 "h1Count": 1,
871 637 "skipped": [],
872 638 "unnamed": [],
873 − "ariaCurrent": 2,
639 + "ariaCurrent": 1,
874 640 "footerOk": true,
875 641 "contrast": [],
876 642 "contrastChecked": 0,
877 − "height": 8563,
878 − "title": "Data Centers in Northern Virginia · DataCenterIndex",
879 − "bottom": {
880 − "navVisible": true,
881 − "covered": false,
882 − "gap": 1074
883 − },
884 − "errors": [
885 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
643 + "height": 6676,
644 + "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
645 + "errors": [],
646 + "badResponses": []
647 + },
648 + {
649 + "name": "fac-wikidata",
650 + "path": "/datacenters/foulum-data-center",
651 + "w": 1280,
652 + "theme": "light",
653 + "status": 200,
654 + "ttfb": 100,
655 + "overflow": null,
656 + "clipped": [
657 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
658 + "span.sr-only \"Denmark\" 58>1",
659 + "span.sr-only \"Denmark\" 56>1",
660 + "span.sr-only \"Denmark\" 60>1"
886 661 ],
887 − "badResponses": [
888 − "404 fetch /api/v1/docs?_rsc=kksxSvqB6K9jsp-1"
889 − ]
662 + "small": [],
663 + "smallCount": 0,
664 + "h1Covered": false,
665 + "h1Top": 135,
666 + "headerBottom": 57,
667 + "lang": "en",
668 + "metaDesc": 171,
669 + "h1Count": 1,
670 + "skipped": [],
671 + "unnamed": [],
672 + "ariaCurrent": 1,
673 + "footerOk": true,
674 + "contrast": [],
675 + "contrastChecked": 0,
676 + "height": 5021,
677 + "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
678 + "errors": [],
679 + "badResponses": []
890 680 },
891 681 {
892 682 "name": "projects",
893 683 "path": "/projects",
894 − "w": 390,
684 + "w": 1280,
895 685 "theme": "light",
896 686 "status": 200,
897 − "ttfb": 4,
687 + "ttfb": 460,
898 688 "overflow": null,
899 689 "clipped": [
900 690 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
901 − "span.sr-only \"United States\" 76>1",
902 − "span.sr-only \"United States\" 76>1",
903 − "span.sr-only \"Australia\" 49>1",
904 − "span.sr-only \"Canada\" 43>1",
905 − "span.sr-only \"United States\" 76>1",
906 − "span.sr-only \"Norway\" 44>1"
907 − ],
908 − "small": [
909 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
910 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
911 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
912 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
913 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
914 − "a.block 366×36 \"1🇺🇸United States≈ 125 \"",
915 − "a.block 366×36 \"2🇯🇵Japan≈ 12.7 GW16\"",
916 − "a.block 366×36 \"3🇦🇺Australia≈ 26 GW9\"",
917 − "a.block 366×36 \"4🇲🇾Malaysia≈ 15.4 GW8\"",
918 − "a.block 366×36 \"5🇩🇪Germany≈ 21.1 GW4\"",
919 − "a.block 366×36 \"6🇸🇬Singapore≈ 314 MW4\"",
920 − "a.block 366×36 \"7🇫🇮Finland≈ 1.79 GW3\""
691 + "span.sr-only \"Luxembourg\" 77>1",
692 + "span.sr-only \"United Kingdom\" 99>1",
693 + "span.sr-only \"United Kingdom\" 99>1",
694 + "span.sr-only \"Singapore\" 62>1",
695 + "span.sr-only \"Singapore\" 62>1",
696 + "span.sr-only \"Australia\" 54>1",
697 + "span.sr-only \"United Kingdom\" 99>1",
698 + "span.sr-only \"United Kingdom\" 99>1"
921 699 ],
922 − "smallCount": 30,
700 + "small": [],
701 + "smallCount": 0,
923 702 "h1Covered": false,
924 − "h1Top": 90,
925 − "headerBottom": 49,
703 + "h1Top": 106,
704 + "headerBottom": 57,
926 705 "lang": "en",
927 − "metaDesc": 130,
706 + "metaDesc": 213,
928 707 "h1Count": 1,
929 708 "skipped": [],
930 709 "unnamed": [],
@@ -932,33 +711,24 @@
932 711 "footerOk": true,
933 712 "contrast": [],
934 713 "contrastChecked": 0,
935 − "height": 7315,
936 − "title": "Data center projects · DataCenterIndex",
937 − "bottom": {
938 − "navVisible": true,
939 − "covered": false,
940 − "gap": 810
941 − },
714 + "height": 4880,
715 + "title": "Data center projects & construction pipeline · DataCenterIndex",
942 716 "inter": {
943 717 "filterSheetOpens": true,
944 718 "filterSheetCloses": true,
945 719 "paginationPage2": true,
946 720 "page2Rows": 50
947 721 },
948 − "errors": [
949 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
950 − ],
951 − "badResponses": [
952 − "404 fetch /api/v1/docs?_rsc=rVOxZyJZQf-ncF1B"
953 − ]
722 + "errors": [],
723 + "badResponses": []
954 724 },
955 725 {
956 726 "name": "project",
957 727 "path": "/projects/sydney-data-center-project",
958 − "w": 390,
728 + "w": 1280,
959 729 "theme": "light",
960 730 "status": 200,
961 − "ttfb": 11,
731 + "ttfb": 67,
962 732 "overflow": null,
963 733 "clipped": [
964 734 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
@@ -966,24 +736,11 @@
966 736 "span.sr-only \"Australia\" 54>1",
967 737 "span.sr-only \"Australia\" 58>1"
968 738 ],
969 − "small": [
970 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
971 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
972 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
973 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
974 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
975 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
976 − "a.hover:text-ink.hover:underline 46×17 \"Projects\"",
977 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
978 − "a.hover:text-ink.hover:underline 46×19 \"Sydney\"",
979 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
980 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
981 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
982 − ],
983 − "smallCount": 42,
739 + "small": [],
740 + "smallCount": 0,
984 741 "h1Covered": false,
985 − "h1Top": 120,
986 − "headerBottom": 49,
742 + "h1Top": 136,
743 + "headerBottom": 57,
987 744 "lang": "en",
988 745 "metaDesc": 201,
989 746 "h1Count": 1,
@@ -993,4652 +750,390 @@
993 750 "footerOk": true,
994 751 "contrast": [],
995 752 "contrastChecked": 0,
996 − "height": 4322,
753 + "height": 3572,
997 754 "title": "Sydney data center project · DataCenterIndex",
998 − "bottom": {
999 − "navVisible": true,
1000 − "covered": false,
1001 − "gap": 1073
1002 − },
1003 755 "errors": [],
1004 756 "badResponses": []
1005 757 },
1006 758 {
1007 − "name": "rankings",
1008 − "path": "/rankings",
759 + "name": "home",
760 + "path": "/",
1009 761 "w": 390,
1010 − "theme": "light",
762 + "theme": "dark",
1011 763 "status": 200,
1012 − "ttfb": 3,
764 + "ttfb": 107,
1013 765 "overflow": null,
1014 766 "clipped": [
1015 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
767 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
768 + "span.sr-only \"Ukraine\" 44>1",
769 + "span.sr-only \"Belarus\" 42>1",
770 + "span.sr-only \"Russia\" 37>1",
771 + "span.sr-only \"Russia\" 37>1",
772 + "span.sr-only \"North Macedonia\" 98>1",
773 + "span.sr-only \"Russia\" 37>1",
774 + "span.sr-only \"Iran\" 22>1",
775 + "span.sr-only \"Iraq\" 22>1"
1016 776 ],
1017 777 "small": [
1018 778 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1019 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1020 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1021 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1022 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1023 − "a.text-[14.5px].font-semibold.text-ink 170×21 \"Countries by AI faciliti\"",
1024 − "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
1025 − "a.text-[14.5px].font-semibold.text-ink 188×21 \"Countries by cloud regio\"",
1026 − "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
1027 − "a.text-[14.5px].font-semibold.text-ink 336×21 \"Countries by facilities \"",
1028 − "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
1029 − "a.text-[14.5px].font-semibold.text-ink 184×21 \"Countries by facility co\""
1030 − ],
1031 − "smallCount": 60,
779 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇺🇦Ukrai\"",
780 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇧🇾Belar\"",
781 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇷🇺Russi\"",
782 + "a.inline-flex.h-7.max-w-[260px] 214×28 \"ООО «ДатаПорт»→🇷🇺Russi\"",
783 + "a.inline-flex.h-7.max-w-[260px] 166×28 \"ООО «ДатаПорт»→🇲🇰North\"",
784 + "a.inline-flex.h-7.max-w-[260px] 156×28 \"آفاق اندیش داده پردیس→🇷\"",
785 + "a.inline-flex.h-7.max-w-[260px] 156×28 \"آفاق اندیش داده پردیس→🇮\"",
786 + "a.inline-flex.h-7.max-w-[260px] 156×28 \"آفاق اندیش داده پردیس→🇮\"",
787 + "a.inline-flex.min-w-0.items-center 99×18 \"🇺🇸United States United\"",
788 + "a.inline-flex.min-w-0.items-center 115×18 \"🇬🇧United Kingdom Unite\"",
789 + "a.inline-flex.min-w-0.items-center 74×18 \"🇩🇪Germany Germany\""
790 + ],
791 + "smallCount": 83,
1032 792 "h1Covered": false,
1033 − "h1Top": 90,
793 + "h1Top": 100,
1034 794 "headerBottom": 49,
1035 795 "lang": "en",
1036 − "metaDesc": 190,
796 + "metaDesc": 234,
1037 797 "h1Count": 1,
1038 798 "skipped": [],
1039 799 "unnamed": [],
1040 − "ariaCurrent": 2,
800 + "ariaCurrent": 0,
1041 801 "footerOk": true,
1042 802 "contrast": [],
1043 − "contrastChecked": 0,
1044 − "height": 4882,
1045 − "title": "Rankings · DataCenterIndex",
803 + "contrastChecked": 683,
804 + "height": 10898,
805 + "title": "DataCenterIndex — Global data center infrastructure, indexed and versioned",
1046 806 "bottom": {
1047 807 "navVisible": true,
1048 808 "covered": false,
1049 − "gap": 809
809 + "gap": 1506
1050 810 },
1051 811 "errors": [],
1052 812 "badResponses": []
1053 813 },
1054 814 {
1055 − "name": "ranking",
1056 − "path": "/rankings/countries_facilities",
815 + "name": "map",
816 + "path": "/map",
1057 817 "w": 390,
1058 − "theme": "light",
818 + "theme": "dark",
1059 819 "status": 200,
1060 − "ttfb": 23,
820 + "ttfb": 22,
1061 821 "overflow": null,
1062 822 "clipped": [
1063 823 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1064 − "span.sr-only \"United States\" 83>1",
1065 − "span.sr-only \"France\" 43>1",
1066 − "span.sr-only \"United Kingdom\" 100>1",
1067 − "span.sr-only \"Germany\" 57>1",
1068 − "span.sr-only \"Australia\" 55>1",
1069 − "span.sr-only \"Netherlands\" 76>1",
1070 − "span.sr-only \"India\" 30>1",
1071 − "span.sr-only \"Singapore\" 63>1"
824 + "h1.sr-only \"Global map of data center infrastructure\" 259>1"
1072 825 ],
1073 826 "small": [
1074 827 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1075 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1076 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1077 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1078 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1079 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1080 − "a.hover:text-ink.hover:underline 52×17 \"Rankings\"",
1081 − "a.font-medium.text-ink.hover:text-accent 83×18 \"United States\"",
1082 − "a.font-medium.text-ink.hover:text-accent 43×18 \"France\"",
1083 − "a.font-medium.text-ink.hover:text-accent 100×18 \"United Kingdom\"",
1084 − "a.font-medium.text-ink.hover:text-accent 57×18 \"Germany\"",
1085 − "a.font-medium.text-ink.hover:text-accent 55×18 \"Australia\""
1086 − ],
1087 − "smallCount": 32,
1088 − "h1Covered": false,
1089 − "h1Top": 120,
828 + "button.maplibregl-ctrl-geolocate 32×32 \"Location not available\"",
829 + "button.maplibregl-ctrl-zoom-in 32×32 \"Zoom in\"",
830 + "button.maplibregl-ctrl-zoom-out 32×32 \"Zoom out\"",
831 + "input.h-full.min-w-0.flex-1 288×38 \"Search the map\"",
832 + "button.inline-flex.h-8.min-w-10 40×32 \"Filters\"",
833 + "button.inline-flex.shrink-0.items-center 98×36 \"Facilities\"",
834 + "button.inline-flex.shrink-0.items-center 99×36 \"Capacity\"",
835 + "button.inline-flex.shrink-0.items-center 95×36 \"Projects\"",
836 + "button.inline-flex.shrink-0.items-center 96×36 \"AI / HPC\"",
837 + "button.inline-flex.shrink-0.items-center 81×36 \"Cloud\"",
838 + "button.inline-flex.shrink-0.items-center 73×36 \"IXPs\""
839 + ],
840 + "smallCount": 15,
841 + "h1Covered": false,
842 + "h1Top": 48,
1090 843 "headerBottom": 49,
1091 844 "lang": "en",
1092 − "metaDesc": 161,
845 + "metaDesc": 127,
1093 846 "h1Count": 1,
1094 − "skipped": [
1095 − "h1→h3"
1096 − ],
847 + "skipped": [],
1097 848 "unnamed": [],
1098 849 "ariaCurrent": 2,
1099 − "footerOk": true,
850 + "footerOk": false,
1100 851 "contrast": [],
1101 − "contrastChecked": 0,
1102 − "height": 2378,
1103 − "title": "Countries by facility count · DataCenterIndex",
1104 − "bottom": {
1105 − "navVisible": true,
1106 − "covered": false,
1107 − "gap": 809
852 + "contrastChecked": 14,
853 + "height": 845,
854 + "title": "Global data center map · DataCenterIndex",
855 + "map": {
856 + "controls": 3,
857 + "unreachable": 0,
858 + "canvas": true
1108 859 },
1109 − "errors": [
1110 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1111 − ],
1112 − "badResponses": [
1113 − "404 fetch /api/v1/docs?_rsc=8sIz69C0ujSVTYSL"
1114 − ]
860 + "errors": [],
861 + "badResponses": []
1115 862 },
1116 863 {
1117 − "name": "cloud-regions",
1118 − "path": "/cloud-regions",
864 + "name": "fac-operator-mw",
865 + "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
1119 866 "w": 390,
1120 − "theme": "light",
867 + "theme": "dark",
1121 868 "status": 200,
1122 − "ttfb": 4,
869 + "ttfb": 137,
1123 870 "overflow": null,
1124 871 "clipped": [
1125 872 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1126 − "label.sr-only \"Provider\" 55>1",
1127 − "label.sr-only \"Country\" 53>1",
1128 − "label.sr-only \"Status\" 42>1"
873 + "span.sr-only \"United States\" 86>1",
874 + "span.sr-only \"United States\" 82>1",
875 + "span.sr-only \"United States\" 76>1",
876 + "span.sr-only \"United States\" 89>1"
1129 877 ],
1130 878 "small": [
1131 879 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1132 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1133 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1134 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1135 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1136 − "a.inline-flex.items-center.justify-center 106×32 \"Show on map\"",
1137 − "select#cr-provider.h-9.rounded-sm.border 179×36 \"All providersAmazon Web \"",
1138 − "select#cr-country.h-9.rounded-sm.border 175×36 \"All countriesArgentinaAu\"",
1139 − "select#cr-status.h-9.rounded-sm.border 109×36 \"Any statusOperationalAnn\"",
1140 − "button.inline-flex.items-center.justify-center 58×32 \"Apply\"",
1141 − "a.ml-auto.text-[12.5px].text-ink-3 106×18 \"Flat sortable table\"",
1142 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\""
880 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
881 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
882 + "a.hover:text-ink.hover:underline 117×17 \"Minneapolis–St. Paul\"",
883 + "a.normal-case.tracking-normal.text-[12.5px] 57×18 \"DataBank\"",
884 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
885 + "a.hover:text-accent.hover:underline 180×20 \"Minneapolis–St. Paul mar\"",
886 + "a.hover:text-accent.hover:underline 86×20 \"United States\"",
887 + "button.group/ev.rounded-xs.text-left 95×22 \"Evidence for Status\"",
888 + "button.group/ev.rounded-xs.text-left 141×22 \"Evidence for Confidence\"",
889 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
890 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
1143 891 ],
1144 − "smallCount": 26,
892 + "smallCount": 87,
1145 893 "h1Covered": false,
1146 − "h1Top": 90,
894 + "h1Top": 119,
1147 895 "headerBottom": 49,
1148 896 "lang": "en",
1149 − "metaDesc": 155,
897 + "metaDesc": 250,
1150 898 "h1Count": 1,
1151 899 "skipped": [],
1152 900 "unnamed": [],
1153 − "ariaCurrent": 0,
901 + "ariaCurrent": 1,
1154 902 "footerOk": true,
1155 903 "contrast": [],
1156 − "contrastChecked": 0,
1157 − "height": 8052,
1158 − "title": "Cloud regions · DataCenterIndex",
904 + "contrastChecked": 625,
905 + "height": 11739,
906 + "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
1159 907 "bottom": {
1160 908 "navVisible": true,
1161 909 "covered": false,
1162 − "gap": 810
910 + "gap": 1882
1163 911 },
1164 912 "errors": [],
1165 913 "badResponses": []
1166 914 },
1167 915 {
1168 − "name": "cloud-region",
1169 − "path": "/cloud-regions/amazon-web-services-ca-central-1",
916 + "name": "fac-osm",
917 + "path": "/datacenters/shelter-modernizacion",
1170 918 "w": 390,
1171 − "theme": "light",
919 + "theme": "dark",
1172 920 "status": 200,
1173 − "ttfb": 7,
921 + "ttfb": 72,
1174 922 "overflow": null,
1175 923 "clipped": [
1176 924 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1177 − "span.sr-only \"Canada\" 47>1"
925 + "span.sr-only \"Argentina\" 62>1",
926 + "span.sr-only \"Argentina\" 59>1",
927 + "span.sr-only \"Argentina\" 64>1"
1178 928 ],
1179 929 "small": [
1180 930 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1181 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1182 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1183 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1184 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1185 931 "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1186 − "a.hover:text-ink.hover:underline 79×17 \"Cloud regions\"",
1187 − "a.hover:text-ink.hover:underline 128×17 \"Amazon Web Services\"",
1188 − "a.hover:text-ink.hover:underline 47×19 \"Canada\"",
1189 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
1190 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
1191 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\""
932 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
933 + "a.hover:text-ink.hover:underline 55×17 \"Argentina\"",
934 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
935 + "a.hover:text-accent.hover:underline 62×20 \"Argentina\"",
936 + "button.group/ev.rounded-xs.text-left 95×22 \"Evidence for Status\"",
937 + "button.group/ev.rounded-xs.text-left 107×22 \"Evidence for Confidence\"",
938 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
939 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
940 + "a.hover:text-accent.hover:underline 59×19 \"Argentina\"",
941 + "button.group/ev.rounded-xs.text-left 131×18 \"Evidence for Latitude\""
1192 942 ],
1193 − "smallCount": 24,
943 + "smallCount": 31,
1194 944 "h1Covered": false,
1195 − "h1Top": 120,
945 + "h1Top": 116,
1196 946 "headerBottom": 49,
1197 947 "lang": "en",
1198 − "metaDesc": 165,
948 + "metaDesc": 141,
1199 949 "h1Count": 1,
1200 950 "skipped": [],
1201 951 "unnamed": [],
1202 − "ariaCurrent": 0,
952 + "ariaCurrent": 1,
1203 953 "footerOk": true,
1204 954 "contrast": [],
1205 − "contrastChecked": 0,
1206 − "height": 2891,
1207 − "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
955 + "contrastChecked": 296,
956 + "height": 8087,
957 + "title": "Shelter Modernización · DataCenterIndex",
1208 958 "bottom": {
1209 959 "navVisible": true,
1210 960 "covered": false,
1211 − "gap": 1073
961 + "gap": 1881
1212 962 },
1213 − "errors": [
1214 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1215 − ],
1216 − "badResponses": [
1217 − "404 fetch /api/v1/docs?_rsc=5uclAh_QepNQCyIY"
1218 − ]
963 + "errors": [],
964 + "badResponses": []
1219 965 },
1220 966 {
1221 − "name": "ixps",
1222 − "path": "/ixps",
967 + "name": "fac-google",
968 + "path": "/datacenters/google-the-dalles-oregon-data-center",
1223 969 "w": 390,
1224 − "theme": "light",
970 + "theme": "dark",
1225 971 "status": 200,
1226 − "ttfb": 4,
972 + "ttfb": 139,
1227 973 "overflow": null,
1228 974 "clipped": [
1229 975 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1230 − "label.sr-only \"Search\" 46>1",
1231 − "label.sr-only \"Country\" 53>1"
976 + "span.sr-only \"United States\" 86>1",
977 + "span.sr-only \"United States\" 82>1",
978 + "span.sr-only \"Singapore\" 58>1",
979 + "span.sr-only \"United States\" 89>1",
980 + "span.sr-only \"United States\" 89>1",
981 + "span.sr-only \"United States\" 89>1",
982 + "span.sr-only \"United States\" 89>1"
1232 983 ],
1233 984 "small": [
1234 985 "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1235 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1236 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1237 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1238 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1239 − "a.inline-flex.items-center.justify-center 101×32 \"Data centers\"",
1240 − "input#ixp-q.h-9.rounded-sm.border 366×36 \"\"",
1241 − "select#ixp-country.h-9.rounded-sm.border 234×36 \"All countriesAlbaniaArge\"",
1242 − "button.inline-flex.items-center.justify-center 58×32 \"Apply\"",
1243 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
1244 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
1245 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\""
986 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
987 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
988 + "a.hover:text-ink.hover:underline 109×17 \"Hillsboro / Portland\"",
989 + "a.normal-case.tracking-normal.text-[12.5px] 43×18 \"Google\"",
990 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
991 + "a.hover:text-accent.hover:underline 170×20 \"Hillsboro / Portland mar\"",
992 + "a.hover:text-accent.hover:underline 86×20 \"United States\"",
993 + "button.group/ev.rounded-xs.text-left 95×22 \"Evidence for Status\"",
994 + "button.group/ev.rounded-xs.text-left 141×22 \"Evidence for Confidence\"",
995 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
996 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
1246 997 ],
1247 − "smallCount": 24,
998 + "smallCount": 78,
1248 999 "h1Covered": false,
1249 − "h1Top": 90,
1250 − "headerBottom": 49,
1251 − "lang": "en",
1252 − "metaDesc": 100,
1253 − "h1Count": 1,
1254 − "skipped": [],
1255 − "unnamed": [],
1256 − "ariaCurrent": 1,
1257 − "footerOk": true,
1258 − "contrast": [],
1259 − "contrastChecked": 0,
1260 − "height": 2057,
1261 − "title": "Internet exchanges · DataCenterIndex",
1262 − "bottom": {
1263 − "navVisible": true,
1264 − "covered": false,
1265 − "gap": 810
1266 − },
1267 − "errors": [
1268 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1269 − ],
1270 − "badResponses": [
1271 − "404 fetch /api/v1/docs?_rsc=LOh4kTrez0j5S-5D"
1272 − ]
1273 − },
1274 − {
1275 − "name": "sources",
1276 − "path": "/sources",
1277 − "w": 390,
1278 − "theme": "light",
1279 − "status": 200,
1280 − "ttfb": 4,
1281 − "overflow": null,
1282 − "clipped": [
1283 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1284 − ],
1285 − "small": [
1286 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1287 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1288 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1289 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1290 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1291 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1292 − "input.h-9.min-w-0.rounded-sm 157×36 \"\"",
1293 − "select.h-9.rounded-sm.border 129×36 \"All kindsOperatorGovernm\"",
1294 − "button.inline-flex.items-center.justify-center 64×36 \"Apply\"",
1295 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
1296 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
1297 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\""
1298 − ],
1299 − "smallCount": 24,
1300 − "h1Covered": false,
1301 − "h1Top": 112,
1302 − "headerBottom": 49,
1303 − "lang": "en",
1304 − "metaDesc": 162,
1305 − "h1Count": 1,
1306 − "skipped": [
1307 − "h1→h3"
1308 − ],
1309 − "unnamed": [],
1310 − "ariaCurrent": 0,
1311 − "footerOk": true,
1312 − "contrast": [],
1313 − "contrastChecked": 0,
1314 − "height": 6001,
1315 − "title": "Sources · DataCenterIndex",
1316 − "bottom": {
1317 − "navVisible": true,
1318 − "covered": false,
1319 − "gap": 810
1320 − },
1321 − "errors": [
1322 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1323 − ],
1324 − "badResponses": [
1325 − "404 fetch /api/v1/docs?_rsc=9SQ0ZVjwLU4sIjhN"
1326 − ]
1327 − },
1328 − {
1329 − "name": "methodology",
1330 − "path": "/methodology",
1331 − "w": 390,
1332 − "theme": "light",
1333 − "status": 200,
1334 − "ttfb": 4,
1335 − "overflow": null,
1336 − "clipped": [
1337 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1338 − ],
1339 − "small": [
1340 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1341 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1342 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1343 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1344 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1345 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1346 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
1347 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
1348 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
1349 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
1350 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\"",
1351 − "a.inline-flex.min-h-7.items-center 60×28 \"Countries\""
1352 − ],
1353 − "smallCount": 21,
1354 − "h1Covered": false,
1355 − "h1Top": 112,
1356 − "headerBottom": 49,
1357 − "lang": "en",
1358 − "metaDesc": 201,
1359 − "h1Count": 1,
1360 − "skipped": [],
1361 − "unnamed": [],
1362 − "ariaCurrent": 0,
1363 − "footerOk": true,
1364 − "contrast": [],
1365 − "contrastChecked": 0,
1366 − "height": 12570,
1367 − "title": "Methodology · DataCenterIndex",
1368 − "bottom": {
1369 − "navVisible": true,
1370 − "covered": false,
1371 − "gap": 809
1372 − },
1373 − "errors": [
1374 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1375 − ],
1376 − "badResponses": [
1377 − "404 fetch /api/v1/docs?_rsc=HzWsK4yFUoZLFyoa"
1378 − ]
1379 − },
1380 − {
1381 − "name": "about",
1382 − "path": "/about",
1383 − "w": 390,
1384 − "theme": "light",
1385 − "status": 200,
1386 − "ttfb": 1,
1387 − "overflow": null,
1388 − "clipped": [
1389 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1390 − ],
1391 − "small": [
1392 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1393 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1394 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1395 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1396 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1397 − "a.font-medium.text-ink.hover:text-accent 366×24 \"PeeringDB\"",
1398 − "a.font-medium.text-ink.hover:text-accent 366×24 \"OpenStreetMap\"",
1399 − "a.font-medium.text-ink.hover:text-accent 366×24 \"Wikidata\"",
1400 − "a.font-medium.text-ink.hover:text-accent 366×24 \"World Bank\"",
1401 − "a.font-medium.text-ink.hover:text-accent 366×24 \"Operator and government \"",
1402 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
1403 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\""
1404 − ],
1405 − "smallCount": 25,
1406 − "h1Covered": false,
1407 − "h1Top": 90,
1408 − "headerBottom": 49,
1409 − "lang": "en",
1410 − "metaDesc": 85,
1411 − "h1Count": 1,
1412 − "skipped": [],
1413 − "unnamed": [],
1414 − "ariaCurrent": 0,
1415 − "footerOk": true,
1416 − "contrast": [],
1417 − "contrastChecked": 0,
1418 − "height": 3140,
1419 − "title": "About · DataCenterIndex",
1420 − "bottom": {
1421 − "navVisible": true,
1422 − "covered": false,
1423 − "gap": 809
1424 − },
1425 − "errors": [
1426 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1427 − ],
1428 − "badResponses": [
1429 − "404 fetch /api/v1/docs?_rsc=Sdev7PUlRw12F58X"
1430 − ]
1431 − },
1432 − {
1433 − "name": "bot",
1434 − "path": "/bot",
1435 − "w": 390,
1436 − "theme": "light",
1437 − "status": 200,
1438 − "ttfb": 3,
1439 − "overflow": null,
1440 − "clipped": [
1441 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1442 − ],
1443 − "small": [
1444 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1445 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1446 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1447 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1448 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1449 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1450 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
1451 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
1452 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
1453 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
1454 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\"",
1455 − "a.inline-flex.min-h-7.items-center 60×28 \"Countries\""
1456 − ],
1457 − "smallCount": 21,
1458 − "h1Covered": false,
1459 − "h1Top": 112,
1460 − "headerBottom": 49,
1461 − "lang": "en",
1462 − "metaDesc": 179,
1463 − "h1Count": 1,
1464 − "skipped": [],
1465 − "unnamed": [],
1466 − "ariaCurrent": 0,
1467 − "footerOk": true,
1468 − "contrast": [],
1469 − "contrastChecked": 0,
1470 − "height": 4183,
1471 − "title": "About our crawler · DataCenterIndex",
1472 − "bottom": {
1473 − "navVisible": true,
1474 − "covered": false,
1475 − "gap": 810
1476 − },
1477 − "errors": [],
1478 − "badResponses": []
1479 − },
1480 − {
1481 − "name": "404",
1482 − "path": "/this-page-does-not-exist",
1483 − "w": 390,
1484 − "theme": "light",
1485 − "status": 404,
1486 − "ttfb": 1,
1487 − "overflow": null,
1488 − "clipped": [
1489 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1490 − ],
1491 − "small": [
1492 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1493 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
1494 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
1495 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to dark theme\"",
1496 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
1497 − "a.inline-flex.items-center.justify-center 134×36 \"Search the index\"",
1498 − "a.inline-flex.items-center.justify-center 117×36 \"Open the map\"",
1499 − "a.inline-flex.h-9.items-center 52×36 \"Home\""
1500 − ],
1501 − "smallCount": 8,
1502 − "h1Covered": false,
1503 − "h1Top": 231,
1504 − "headerBottom": 49,
1505 − "lang": "en",
1506 − "metaDesc": 159,
1507 − "h1Count": 1,
1508 − "skipped": [],
1509 − "unnamed": [],
1510 − "ariaCurrent": 0,
1511 − "footerOk": false,
1512 − "contrast": [],
1513 − "contrastChecked": 0,
1514 − "height": 844,
1515 − "title": "DataCenterIndex — The global index of data center infrastructure",
1516 − "bottom": {
1517 − "navVisible": true,
1518 − "covered": false
1519 − },
1520 − "errors": [
1521 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1522 − ],
1523 − "badResponses": []
1524 − },
1525 − {
1526 − "name": "home",
1527 − "path": "/",
1528 − "w": 1280,
1529 − "theme": "light",
1530 − "status": 200,
1531 − "ttfb": 3,
1532 − "overflow": null,
1533 − "clipped": [
1534 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1535 − "span.sr-only \"United States\" 89>1",
1536 − "span.sr-only \"United States\" 89>1",
1537 − "span.sr-only \"United States\" 89>1",
1538 − "span.sr-only \"United States\" 76>1",
1539 − "span.sr-only \"United States\" 76>1",
1540 − "span.sr-only \"United Kingdom\" 99>1",
1541 − "span.sr-only \"France\" 42>1",
1542 − "span.sr-only \"Singapore\" 62>1"
1543 − ],
1544 − "small": [],
1545 − "smallCount": 0,
1546 − "h1Covered": false,
1547 − "h1Top": 132,
1548 − "headerBottom": 57,
1549 − "lang": "en",
1550 − "metaDesc": 159,
1551 − "h1Count": 1,
1552 − "skipped": [],
1553 − "unnamed": [],
1554 − "ariaCurrent": 1,
1555 − "footerOk": true,
1556 − "contrast": [],
1557 − "contrastChecked": 0,
1558 − "height": 3750,
1559 − "title": "DataCenterIndex — The global index of data center infrastructure",
1560 − "error": "TimeoutError: locator.click: Timeout 30000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('button[aria-label*=\"theme\" i]').first()\u001b[22m\n\u001b[2m - locator resolved to <button type=\"button\" title=\"Sw",
1561 − "errors": [],
1562 − "badResponses": []
1563 − },
1564 − {
1565 − "name": "map",
1566 − "path": "/map",
1567 − "w": 1280,
1568 − "theme": "light",
1569 − "status": 200,
1570 − "ttfb": 1,
1571 − "overflow": null,
1572 − "clipped": [
1573 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1574 − "span.sr-only \"Minimum MW\" 91>1",
1575 − "span.sr-only \"Maximum MW\" 95>1",
1576 − "span.sr-only \"Opened from\" 87>1",
1577 − "span.sr-only \"Opened to\" 69>1"
1578 − ],
1579 − "small": [],
1580 − "smallCount": 0,
1581 − "h1Covered": false,
1582 − "h1Top": null,
1583 − "headerBottom": 57,
1584 − "lang": "en",
1585 − "metaDesc": 127,
1586 − "h1Count": 0,
1587 − "skipped": [],
1588 − "unnamed": [],
1589 − "ariaCurrent": 2,
1590 − "footerOk": false,
1591 − "contrast": [],
1592 − "contrastChecked": 0,
1593 − "height": 901,
1594 − "title": "Global data center map · DataCenterIndex",
1595 − "map": {
1596 − "controls": 3,
1597 − "unreachable": 0,
1598 − "canvas": true
1599 − },
1600 − "inter": {
1601 − "zoomVisible": true,
1602 − "mapPanelOpens": true
1603 − },
1604 − "errors": [],
1605 − "badResponses": []
1606 − },
1607 − {
1608 − "name": "search",
1609 − "path": "/search?q=montreal",
1610 − "w": 1280,
1611 − "theme": "light",
1612 − "status": 200,
1613 − "ttfb": 5,
1614 − "overflow": null,
1615 − "clipped": [
1616 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1617 − ],
1618 − "small": [],
1619 − "smallCount": 0,
1620 − "h1Covered": false,
1621 − "h1Top": 106,
1622 − "headerBottom": 57,
1623 − "lang": "en",
1624 − "metaDesc": 159,
1625 − "h1Count": 1,
1626 − "skipped": [],
1627 − "unnamed": [],
1628 − "ariaCurrent": 0,
1629 − "footerOk": true,
1630 − "contrast": [],
1631 − "contrastChecked": 0,
1632 − "height": 984,
1633 − "title": "“montreal” — search · DataCenterIndex",
1634 − "errors": [
1635 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1636 − ],
1637 − "badResponses": [
1638 − "404 fetch /api/v1/docs?_rsc=6grVij_EUBxTWnLP"
1639 − ]
1640 − },
1641 − {
1642 − "name": "live",
1643 − "path": "/live",
1644 − "w": 1280,
1645 − "theme": "light",
1646 − "status": 200,
1647 − "ttfb": 12,
1648 − "overflow": null,
1649 − "clipped": [
1650 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1651 − "span.sr-only \"United States\" 89>1",
1652 − "span.sr-only \"United States\" 89>1",
1653 − "span.sr-only \"United States\" 89>1",
1654 − "span.sr-only \"United States\" 89>1",
1655 − "span.sr-only \"United States\" 89>1",
1656 − "span.sr-only \"United States\" 89>1",
1657 − "span.sr-only \"United States\" 89>1",
1658 − "span.sr-only \"United States\" 89>1"
1659 − ],
1660 − "small": [],
1661 − "smallCount": 0,
1662 − "h1Covered": false,
1663 − "h1Top": 107,
1664 − "headerBottom": 57,
1665 − "lang": "en",
1666 − "metaDesc": 152,
1667 − "h1Count": 1,
1668 − "skipped": [
1669 − "h1→h3"
1670 − ],
1671 − "unnamed": [],
1672 − "ariaCurrent": 1,
1673 − "footerOk": true,
1674 − "contrast": [],
1675 − "contrastChecked": 0,
1676 − "height": 7789,
1677 − "title": "Live changes · DataCenterIndex",
1678 − "inter": {
1679 − "filterSheetOpens": true,
1680 − "filterSheetCloses": true
1681 − },
1682 − "errors": [],
1683 − "badResponses": []
1684 − },
1685 − {
1686 − "name": "datacenters",
1687 − "path": "/datacenters",
1688 − "w": 1280,
1689 − "theme": "light",
1690 − "status": 200,
1691 − "ttfb": 13,
1692 − "overflow": null,
1693 − "clipped": [
1694 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1695 − ],
1696 − "small": [],
1697 − "smallCount": 0,
1698 − "h1Covered": false,
1699 − "h1Top": 106,
1700 − "headerBottom": 57,
1701 − "lang": "en",
1702 − "metaDesc": 119,
1703 − "h1Count": 1,
1704 − "skipped": [],
1705 − "unnamed": [],
1706 − "ariaCurrent": 2,
1707 − "footerOk": true,
1708 − "contrast": [],
1709 − "contrastChecked": 0,
1710 − "height": 2652,
1711 − "title": "Data centers · DataCenterIndex",
1712 − "inter": {
1713 − "filterSheetOpens": true,
1714 − "filterSheetCloses": true,
1715 − "paginationPage2": true,
1716 − "page2Rows": 50
1717 − },
1718 − "errors": [
1719 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1720 − ],
1721 − "badResponses": [
1722 − "404 fetch /api/v1/docs?_rsc=2QMc8M2_JReeYqv7"
1723 − ]
1724 − },
1725 − {
1726 − "name": "fac-operator-mw",
1727 − "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
1728 − "w": 1280,
1729 − "theme": "light",
1730 − "status": 200,
1731 − "ttfb": 14,
1732 − "overflow": null,
1733 − "clipped": [
1734 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1735 − "span.sr-only \"United States\" 86>1",
1736 − "span.sr-only \"United States\" 82>1",
1737 − "span.sr-only \"United States\" 89>1"
1738 − ],
1739 − "small": [],
1740 − "smallCount": 0,
1741 − "h1Covered": false,
1742 − "h1Top": 135,
1743 − "headerBottom": 57,
1744 − "lang": "en",
1745 − "metaDesc": 212,
1746 − "h1Count": 1,
1747 − "skipped": [],
1748 − "unnamed": [],
1749 − "ariaCurrent": 1,
1750 − "footerOk": true,
1751 − "contrast": [],
1752 − "contrastChecked": 0,
1753 − "height": 4103,
1754 − "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
1755 − "errors": [],
1756 − "badResponses": []
1757 − },
1758 − {
1759 − "name": "fac-osm",
1760 − "path": "/datacenters/shelter-modernizacion",
1761 − "w": 1280,
1762 − "theme": "light",
1763 − "status": 200,
1764 − "ttfb": 9,
1765 − "overflow": null,
1766 − "clipped": [
1767 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1768 − "span.sr-only \"Argentina\" 62>1",
1769 − "span.sr-only \"Argentina\" 59>1",
1770 − "span.sr-only \"Argentina\" 64>1"
1771 − ],
1772 − "small": [],
1773 − "smallCount": 0,
1774 − "h1Covered": false,
1775 − "h1Top": 132,
1776 − "headerBottom": 57,
1777 − "lang": "en",
1778 − "metaDesc": 128,
1779 − "h1Count": 1,
1780 − "skipped": [],
1781 − "unnamed": [],
1782 − "ariaCurrent": 1,
1783 − "footerOk": true,
1784 − "contrast": [],
1785 − "contrastChecked": 0,
1786 − "height": 3088,
1787 − "title": "Shelter Modernización · DataCenterIndex",
1788 − "errors": [],
1789 − "badResponses": []
1790 − },
1791 − {
1792 − "name": "fac-google",
1793 − "path": "/datacenters/google-the-dalles-oregon-data-center",
1794 − "w": 1280,
1795 − "theme": "light",
1796 − "status": 200,
1797 − "ttfb": 17,
1798 − "overflow": null,
1799 − "clipped": [
1800 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1801 − "span.sr-only \"United States\" 86>1",
1802 − "span.sr-only \"United States\" 82>1",
1803 − "span.sr-only \"United States\" 89>1",
1804 − "span.sr-only \"United States\" 89>1",
1805 − "span.sr-only \"United States\" 89>1",
1806 − "span.sr-only \"United States\" 89>1"
1807 − ],
1808 − "small": [],
1809 − "smallCount": 0,
1810 − "h1Covered": false,
1811 − "h1Top": 135,
1812 − "headerBottom": 57,
1813 − "lang": "en",
1814 − "metaDesc": 203,
1815 − "h1Count": 1,
1816 − "skipped": [],
1817 − "unnamed": [],
1818 − "ariaCurrent": 1,
1819 − "footerOk": true,
1820 − "contrast": [],
1821 − "contrastChecked": 0,
1822 − "height": 6882,
1823 − "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
1824 − "errors": [],
1825 − "badResponses": []
1826 − },
1827 − {
1828 − "name": "fac-wikidata",
1829 − "path": "/datacenters/foulum-data-center",
1830 − "w": 1280,
1831 − "theme": "light",
1832 − "status": 200,
1833 − "ttfb": 12,
1834 − "overflow": null,
1835 − "clipped": [
1836 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1837 − "span.sr-only \"Denmark\" 58>1",
1838 − "span.sr-only \"Denmark\" 56>1",
1839 − "span.sr-only \"Denmark\" 60>1"
1840 − ],
1841 − "small": [],
1842 − "smallCount": 0,
1843 − "h1Covered": false,
1844 − "h1Top": 135,
1845 − "headerBottom": 57,
1846 − "lang": "en",
1847 − "metaDesc": 158,
1848 − "h1Count": 1,
1849 − "skipped": [],
1850 − "unnamed": [],
1851 − "ariaCurrent": 1,
1852 − "footerOk": true,
1853 − "contrast": [],
1854 − "contrastChecked": 0,
1855 − "height": 3868,
1856 − "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
1857 − "errors": [],
1858 − "badResponses": []
1859 − },
1860 − {
1861 − "name": "operators",
1862 − "path": "/operators",
1863 − "w": 1280,
1864 − "theme": "light",
1865 − "status": 200,
1866 − "ttfb": 11,
1867 − "overflow": null,
1868 − "clipped": [
1869 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1870 − "span.sr-only \"Search operators\" 114>1"
1871 − ],
1872 − "small": [],
1873 − "smallCount": 0,
1874 − "h1Covered": false,
1875 − "h1Top": 106,
1876 − "headerBottom": 57,
1877 − "lang": "en",
1878 − "metaDesc": 139,
1879 − "h1Count": 1,
1880 − "skipped": [],
1881 − "unnamed": [],
1882 − "ariaCurrent": 2,
1883 − "footerOk": true,
1884 − "contrast": [],
1885 − "contrastChecked": 0,
1886 − "height": 2622,
1887 − "title": "Operators · DataCenterIndex",
1888 − "inter": {
1889 − "paginationPage2": true,
1890 − "page2Rows": 50
1891 − },
1892 − "errors": [
1893 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
1894 − ],
1895 − "badResponses": [
1896 − "404 fetch /api/v1/docs?_rsc=nAdjiVRfIop78OSR"
1897 − ]
1898 − },
1899 − {
1900 − "name": "operator-equinix",
1901 − "path": "/operators/equinix",
1902 − "w": 1280,
1903 − "theme": "light",
1904 − "status": 200,
1905 − "ttfb": 19,
1906 − "overflow": null,
1907 − "clipped": [
1908 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1909 − "span.sr-only \"Finland\" 45>1",
1910 − "span.sr-only \"Germany\" 56>1",
1911 − "span.sr-only \"Italy\" 26>1",
1912 − "span.sr-only \"Switzerland\" 72>1",
1913 − "span.sr-only \"United States\" 82>1",
1914 − "span.sr-only \"Australia\" 54>1",
1915 − "span.sr-only \"Ireland\" 42>1",
1916 − "span.sr-only \"Netherlands\" 75>1"
1917 − ],
1918 − "small": [],
1919 − "smallCount": 0,
1920 − "h1Covered": false,
1921 − "h1Top": 138,
1922 − "headerBottom": 57,
1923 − "lang": "en",
1924 − "metaDesc": 170,
1925 − "h1Count": 1,
1926 − "skipped": [],
1927 − "unnamed": [],
1928 − "ariaCurrent": 2,
1929 − "footerOk": true,
1930 − "contrast": [],
1931 − "contrastChecked": 0,
1932 − "height": 7121,
1933 − "title": "Equinix · DataCenterIndex",
1934 − "errors": [],
1935 − "badResponses": []
1936 − },
1937 − {
1938 − "name": "countries",
1939 − "path": "/countries",
1940 − "w": 1280,
1941 − "theme": "light",
1942 − "status": 200,
1943 − "ttfb": 20,
1944 − "overflow": null,
1945 − "clipped": [
1946 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1947 − "span.sr-only \"United States\" 83>1",
1948 − "span.sr-only \"Brazil\" 35>1",
1949 − "span.sr-only \"Argentina\" 60>1",
1950 − "span.sr-only \"Canada\" 48>1",
1951 − "span.sr-only \"Mexico\" 45>1",
1952 − "span.sr-only \"Chile\" 32>1",
1953 − "span.sr-only \"Venezuela\" 65>1",
1954 − "span.sr-only \"Peru\" 29>1"
1955 − ],
1956 − "small": [],
1957 − "smallCount": 0,
1958 − "h1Covered": false,
1959 − "h1Top": 106,
1960 − "headerBottom": 57,
1961 − "lang": "en",
1962 − "metaDesc": 117,
1963 − "h1Count": 1,
1964 − "skipped": [],
1965 − "unnamed": [],
1966 − "ariaCurrent": 2,
1967 − "footerOk": true,
1968 − "contrast": [],
1969 − "contrastChecked": 0,
1970 − "height": 5414,
1971 − "title": "Countries · DataCenterIndex",
1972 − "errors": [],
1973 − "badResponses": []
1974 − },
1975 − {
1976 − "name": "country-us",
1977 − "path": "/countries/united-states",
1978 − "w": 1280,
1979 − "theme": "light",
1980 − "status": 200,
1981 − "ttfb": 25,
1982 − "overflow": null,
1983 − "clipped": [
1984 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1985 − "span.sr-only \"United States\" 187>1",
1986 − "span.sr-only \"United States\" 76>1",
1987 − "span.sr-only \"United States\" 76>1",
1988 − "span.sr-only \"United States\" 76>1",
1989 − "span.sr-only \"United States\" 76>1",
1990 − "span.sr-only \"United States\" 76>1",
1991 − "span.sr-only \"United States\" 76>1",
1992 − "span.sr-only \"United States\" 76>1"
1993 − ],
1994 − "small": [],
1995 − "smallCount": 0,
1996 − "h1Covered": false,
1997 − "h1Top": 136,
1998 − "headerBottom": 57,
1999 − "lang": "en",
2000 − "metaDesc": 258,
2001 − "h1Count": 1,
2002 − "skipped": [],
2003 − "unnamed": [],
2004 − "ariaCurrent": 2,
2005 − "footerOk": true,
2006 − "contrast": [],
2007 − "contrastChecked": 0,
2008 − "height": 11218,
2009 − "title": "Data Centers in United States · DataCenterIndex",
2010 − "errors": [],
2011 − "badResponses": []
2012 − },
2013 − {
2014 − "name": "country-ca",
2015 − "path": "/countries/canada",
2016 − "w": 1280,
2017 − "theme": "light",
2018 − "status": 200,
2019 − "ttfb": 14,
2020 − "overflow": null,
2021 − "clipped": [
2022 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2023 − "span.sr-only \"Canada\" 107>1",
2024 − "span.sr-only \"Canada\" 43>1",
2025 − "span.sr-only \"Canada\" 43>1",
2026 − "span.sr-only \"Canada\" 51>1",
2027 − "span.sr-only \"Canada\" 51>1",
2028 − "span.sr-only \"Canada\" 51>1",
2029 − "span.sr-only \"Canada\" 51>1",
2030 − "span.sr-only \"Canada\" 51>1"
2031 − ],
2032 − "small": [],
2033 − "smallCount": 0,
2034 − "h1Covered": false,
2035 − "h1Top": 136,
2036 − "headerBottom": 57,
2037 − "lang": "en",
2038 − "metaDesc": 186,
2039 − "h1Count": 1,
2040 − "skipped": [],
2041 − "unnamed": [],
2042 − "ariaCurrent": 2,
2043 − "footerOk": true,
2044 − "contrast": [],
2045 − "contrastChecked": 0,
2046 − "height": 5842,
2047 − "title": "Data Centers in Canada · DataCenterIndex",
2048 − "errors": [],
2049 − "badResponses": []
2050 − },
2051 − {
2052 − "name": "markets",
2053 − "path": "/datacenters/markets",
2054 − "w": 1280,
2055 − "theme": "light",
2056 − "status": 200,
2057 − "ttfb": 15,
2058 − "overflow": null,
2059 − "clipped": [
2060 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2061 − ],
2062 − "small": [],
2063 − "smallCount": 0,
2064 − "h1Covered": false,
2065 − "h1Top": 136,
2066 − "headerBottom": 57,
2067 − "lang": "en",
2068 − "metaDesc": 125,
2069 − "h1Count": 1,
2070 − "skipped": [],
2071 − "unnamed": [],
2072 − "ariaCurrent": 2,
2073 − "footerOk": true,
2074 − "contrast": [],
2075 − "contrastChecked": 0,
2076 − "height": 2693,
2077 − "title": "Data center markets · DataCenterIndex",
2078 − "errors": [],
2079 − "badResponses": []
2080 − },
2081 − {
2082 − "name": "market-nova",
2083 − "path": "/datacenters/markets/northern-virginia",
2084 − "w": 1280,
2085 − "theme": "light",
2086 − "status": 200,
2087 − "ttfb": 13,
2088 − "overflow": null,
2089 − "clipped": [
2090 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2091 − "span.sr-only \"United States\" 82>1",
2092 − "span.sr-only \"United States\" 76>1",
2093 − "span.sr-only \"United States\" 89>1",
2094 − "span.sr-only \"United States\" 89>1",
2095 − "span.sr-only \"United States\" 89>1",
2096 − "span.sr-only \"United States\" 89>1",
2097 − "span.sr-only \"United States\" 89>1",
2098 − "span.sr-only \"United States\" 89>1"
2099 − ],
2100 − "small": [],
2101 − "smallCount": 0,
2102 − "h1Covered": false,
2103 − "h1Top": 136,
2104 − "headerBottom": 57,
2105 − "lang": "en",
2106 − "metaDesc": 250,
2107 − "h1Count": 1,
2108 − "skipped": [],
2109 − "unnamed": [],
2110 − "ariaCurrent": 2,
2111 − "footerOk": true,
2112 − "contrast": [],
2113 − "contrastChecked": 0,
2114 − "height": 6519,
2115 − "title": "Data Centers in Northern Virginia · DataCenterIndex",
2116 − "errors": [],
2117 − "badResponses": []
2118 − },
2119 − {
2120 − "name": "projects",
2121 − "path": "/projects",
2122 − "w": 1280,
2123 − "theme": "light",
2124 − "status": 200,
2125 − "ttfb": 14,
2126 − "overflow": null,
2127 − "clipped": [
2128 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2129 − "span.sr-only \"United States\" 82>1",
2130 − "span.sr-only \"United States\" 82>1",
2131 − "span.sr-only \"Australia\" 54>1",
2132 − "span.sr-only \"Canada\" 47>1",
2133 − "span.sr-only \"United States\" 82>1",
2134 − "span.sr-only \"Norway\" 48>1"
2135 − ],
2136 − "small": [],
2137 − "smallCount": 0,
2138 − "h1Covered": false,
2139 − "h1Top": 106,
2140 − "headerBottom": 57,
2141 − "lang": "en",
2142 − "metaDesc": 130,
2143 − "h1Count": 1,
2144 − "skipped": [],
2145 − "unnamed": [],
2146 − "ariaCurrent": 3,
2147 − "footerOk": true,
2148 − "contrast": [],
2149 − "contrastChecked": 0,
2150 − "height": 3977,
2151 − "title": "Data center projects · DataCenterIndex",
2152 − "inter": {
2153 − "filterSheetOpens": true,
2154 − "filterSheetCloses": true,
2155 − "paginationPage2": true,
2156 − "page2Rows": 50
2157 − },
2158 − "errors": [
2159 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
2160 − ],
2161 − "badResponses": [
2162 − "404 fetch /api/v1/docs?_rsc=rVOxZyJZQf-ncF1B"
2163 − ]
2164 − },
2165 − {
2166 − "name": "project",
2167 − "path": "/projects/sydney-data-center-project",
2168 − "w": 1280,
2169 − "theme": "light",
2170 − "status": 200,
2171 − "ttfb": 7,
2172 − "overflow": null,
2173 − "clipped": [
2174 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2175 − "span.sr-only \"Australia\" 54>1",
2176 − "span.sr-only \"Australia\" 54>1",
2177 − "span.sr-only \"Australia\" 58>1"
2178 − ],
2179 − "small": [],
2180 − "smallCount": 0,
2181 − "h1Covered": false,
2182 − "h1Top": 136,
2183 − "headerBottom": 57,
2184 − "lang": "en",
2185 − "metaDesc": 201,
2186 − "h1Count": 1,
2187 − "skipped": [],
2188 − "unnamed": [],
2189 − "ariaCurrent": 2,
2190 − "footerOk": true,
2191 − "contrast": [],
2192 − "contrastChecked": 0,
2193 − "height": 2902,
2194 − "title": "Sydney data center project · DataCenterIndex",
2195 − "errors": [],
2196 − "badResponses": []
2197 − },
2198 − {
2199 − "name": "rankings",
2200 − "path": "/rankings",
2201 − "w": 1280,
2202 − "theme": "light",
2203 − "status": 200,
2204 − "ttfb": 3,
2205 − "overflow": null,
2206 − "clipped": [
2207 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2208 − ],
2209 − "small": [],
2210 − "smallCount": 0,
2211 − "h1Covered": false,
2212 − "h1Top": 106,
2213 − "headerBottom": 57,
2214 − "lang": "en",
2215 − "metaDesc": 190,
2216 − "h1Count": 1,
2217 − "skipped": [],
2218 − "unnamed": [],
2219 − "ariaCurrent": 2,
2220 − "footerOk": true,
2221 − "contrast": [],
2222 − "contrastChecked": 0,
2223 − "height": 3333,
2224 − "title": "Rankings · DataCenterIndex",
2225 − "errors": [],
2226 − "badResponses": []
2227 − },
2228 − {
2229 − "name": "ranking",
2230 − "path": "/rankings/countries_facilities",
2231 − "w": 1280,
2232 − "theme": "light",
2233 − "status": 200,
2234 − "ttfb": 12,
2235 − "overflow": null,
2236 − "clipped": [
2237 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2238 − "span.sr-only \"United States\" 82>1",
2239 − "span.sr-only \"France\" 42>1",
2240 − "span.sr-only \"United Kingdom\" 99>1",
2241 − "span.sr-only \"Germany\" 56>1",
2242 − "span.sr-only \"Australia\" 54>1",
2243 − "span.sr-only \"Netherlands\" 75>1",
2244 − "span.sr-only \"India\" 30>1",
2245 − "span.sr-only \"Singapore\" 62>1"
2246 − ],
2247 − "small": [],
2248 − "smallCount": 0,
2249 − "h1Covered": false,
2250 − "h1Top": 136,
2251 − "headerBottom": 57,
2252 − "lang": "en",
2253 − "metaDesc": 161,
2254 − "h1Count": 1,
2255 − "skipped": [
2256 − "h1→h3"
2257 − ],
2258 − "unnamed": [],
2259 − "ariaCurrent": 2,
2260 − "footerOk": true,
2261 − "contrast": [],
2262 − "contrastChecked": 0,
2263 − "height": 4578,
2264 − "title": "Countries by facility count · DataCenterIndex",
2265 − "errors": [],
2266 − "badResponses": []
2267 − },
2268 − {
2269 − "name": "cloud-regions",
2270 − "path": "/cloud-regions",
2271 − "w": 1280,
2272 − "theme": "light",
2273 − "status": 200,
2274 − "ttfb": 30,
2275 − "overflow": null,
2276 − "clipped": [
2277 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2278 − "label.sr-only \"Provider\" 55>1",
2279 − "label.sr-only \"Country\" 53>1",
2280 − "label.sr-only \"Status\" 42>1"
2281 − ],
2282 − "small": [],
2283 − "smallCount": 0,
2284 − "h1Covered": false,
2285 − "h1Top": 106,
2286 − "headerBottom": 57,
2287 − "lang": "en",
2288 − "metaDesc": 155,
2289 − "h1Count": 1,
2290 − "skipped": [],
2291 − "unnamed": [],
2292 − "ariaCurrent": 0,
2293 − "footerOk": true,
2294 − "contrast": [],
2295 − "contrastChecked": 0,
2296 − "height": 12906,
2297 − "title": "Cloud regions · DataCenterIndex",
2298 − "errors": [],
2299 − "badResponses": []
2300 − },
2301 − {
2302 − "name": "cloud-region",
2303 − "path": "/cloud-regions/amazon-web-services-ca-central-1",
2304 − "w": 1280,
2305 − "theme": "light",
2306 − "status": 200,
2307 − "ttfb": 6,
2308 − "overflow": null,
2309 − "clipped": [
2310 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2311 − "span.sr-only \"Canada\" 47>1"
2312 − ],
2313 − "small": [],
2314 − "smallCount": 0,
2315 − "h1Covered": false,
2316 − "h1Top": 136,
2317 − "headerBottom": 57,
2318 − "lang": "en",
2319 − "metaDesc": 165,
2320 − "h1Count": 1,
2321 − "skipped": [],
2322 − "unnamed": [],
2323 − "ariaCurrent": 0,
2324 − "footerOk": true,
2325 − "contrast": [],
2326 − "contrastChecked": 0,
2327 − "height": 1902,
2328 − "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
2329 − "errors": [],
2330 − "badResponses": []
2331 − },
2332 − {
2333 − "name": "ixps",
2334 − "path": "/ixps",
2335 − "w": 1280,
2336 − "theme": "light",
2337 − "status": 200,
2338 − "ttfb": 6,
2339 − "overflow": null,
2340 − "clipped": [
2341 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2342 − "label.sr-only \"Search\" 46>1",
2343 − "label.sr-only \"Country\" 53>1"
2344 − ],
2345 − "small": [],
2346 − "smallCount": 0,
2347 − "h1Covered": false,
2348 − "h1Top": 106,
2349 − "headerBottom": 57,
2350 − "lang": "en",
2351 − "metaDesc": 100,
2352 − "h1Count": 1,
2353 − "skipped": [],
2354 − "unnamed": [],
2355 − "ariaCurrent": 1,
2356 − "footerOk": true,
2357 − "contrast": [],
2358 − "contrastChecked": 0,
2359 − "height": 1171,
2360 − "title": "Internet exchanges · DataCenterIndex",
2361 − "errors": [
2362 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
2363 − ],
2364 − "badResponses": [
2365 − "404 fetch /api/v1/docs?_rsc=LOh4kTrez0j5S-5D"
2366 − ]
2367 − },
2368 − {
2369 − "name": "sources",
2370 − "path": "/sources",
2371 − "w": 1280,
2372 − "theme": "light",
2373 − "status": 200,
2374 − "ttfb": 16,
2375 − "overflow": null,
2376 − "clipped": [
2377 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2378 − ],
2379 − "small": [],
2380 − "smallCount": 0,
2381 − "h1Covered": false,
2382 − "h1Top": 136,
2383 − "headerBottom": 57,
2384 − "lang": "en",
2385 − "metaDesc": 162,
2386 − "h1Count": 1,
2387 − "skipped": [
2388 − "h1→h3"
2389 − ],
2390 − "unnamed": [],
2391 − "ariaCurrent": 0,
2392 − "footerOk": true,
2393 − "contrast": [],
2394 − "contrastChecked": 0,
2395 − "height": 8101,
2396 − "title": "Sources · DataCenterIndex",
2397 − "errors": [],
2398 − "badResponses": []
2399 − },
2400 − {
2401 − "name": "methodology",
2402 − "path": "/methodology",
2403 − "w": 1280,
2404 − "theme": "light",
2405 − "status": 200,
2406 − "ttfb": 3,
2407 − "overflow": null,
2408 − "clipped": [
2409 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2410 − ],
2411 − "small": [],
2412 − "smallCount": 0,
2413 − "h1Covered": false,
2414 − "h1Top": 136,
2415 − "headerBottom": 57,
2416 − "lang": "en",
2417 − "metaDesc": 201,
2418 − "h1Count": 1,
2419 − "skipped": [],
2420 − "unnamed": [],
2421 − "ariaCurrent": 0,
2422 − "footerOk": true,
2423 − "contrast": [],
2424 − "contrastChecked": 0,
2425 − "height": 6645,
2426 − "title": "Methodology · DataCenterIndex",
2427 − "errors": [],
2428 − "badResponses": []
2429 − },
2430 − {
2431 − "name": "about",
2432 − "path": "/about",
2433 − "w": 1280,
2434 − "theme": "light",
2435 − "status": 200,
2436 − "ttfb": 2,
2437 − "overflow": null,
2438 − "clipped": [
2439 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2440 − ],
2441 − "small": [],
2442 − "smallCount": 0,
2443 − "h1Covered": false,
2444 − "h1Top": 106,
2445 − "headerBottom": 57,
2446 − "lang": "en",
2447 − "metaDesc": 85,
2448 − "h1Count": 1,
2449 − "skipped": [],
2450 − "unnamed": [],
2451 − "ariaCurrent": 0,
2452 − "footerOk": true,
2453 − "contrast": [],
2454 − "contrastChecked": 0,
2455 − "height": 1679,
2456 − "title": "About · DataCenterIndex",
2457 − "errors": [],
2458 − "badResponses": []
2459 − },
2460 − {
2461 − "name": "bot",
2462 − "path": "/bot",
2463 − "w": 1280,
2464 − "theme": "light",
2465 − "status": 200,
2466 − "ttfb": 2,
2467 − "overflow": null,
2468 − "clipped": [
2469 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2470 − ],
2471 − "small": [],
2472 − "smallCount": 0,
2473 − "h1Covered": false,
2474 − "h1Top": 136,
2475 − "headerBottom": 57,
2476 − "lang": "en",
2477 − "metaDesc": 179,
2478 − "h1Count": 1,
2479 − "skipped": [],
2480 − "unnamed": [],
2481 − "ariaCurrent": 0,
2482 − "footerOk": true,
2483 − "contrast": [],
2484 − "contrastChecked": 0,
2485 − "height": 2261,
2486 − "title": "About our crawler · DataCenterIndex",
2487 − "errors": [],
2488 − "badResponses": []
2489 − },
2490 − {
2491 − "name": "404",
2492 − "path": "/this-page-does-not-exist",
2493 − "w": 1280,
2494 − "theme": "light",
2495 − "status": 404,
2496 − "ttfb": 1,
2497 − "overflow": null,
2498 − "clipped": [
2499 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2500 − ],
2501 − "small": [],
2502 − "smallCount": 0,
2503 − "h1Covered": false,
2504 − "h1Top": 261,
2505 − "headerBottom": 57,
2506 − "lang": "en",
2507 − "metaDesc": 159,
2508 − "h1Count": 1,
2509 − "skipped": [],
2510 − "unnamed": [],
2511 − "ariaCurrent": 0,
2512 − "footerOk": false,
2513 − "contrast": [],
2514 − "contrastChecked": 0,
2515 − "height": 900,
2516 − "title": "DataCenterIndex — The global index of data center infrastructure",
2517 − "errors": [
2518 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
2519 − ],
2520 − "badResponses": []
2521 − },
2522 − {
2523 − "name": "home",
2524 − "path": "/",
2525 − "w": 390,
2526 − "theme": "dark",
2527 − "status": 200,
2528 − "ttfb": 3,
2529 − "overflow": null,
2530 − "clipped": [
2531 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2532 − "span.sr-only \"United States\" 89>1",
2533 − "span.sr-only \"United States\" 89>1",
2534 − "span.sr-only \"United States\" 89>1",
2535 − "span.sr-only \"United States\" 76>1",
2536 − "span.sr-only \"United States\" 76>1",
2537 − "span.sr-only \"United Kingdom\" 99>1",
2538 − "span.sr-only \"France\" 42>1",
2539 − "span.sr-only \"Singapore\" 62>1"
2540 − ],
2541 − "small": [
2542 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2543 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2544 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2545 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2546 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2547 − "button.mr-1.5.inline-flex.size-9 36×36 \"Search\"",
2548 − "button.shrink-0.rounded-sm.border 111×27 \"Northern Virginia\"",
2549 − "button.shrink-0.rounded-sm.border 131×27 \"hyperscale in Ireland\"",
2550 − "button.shrink-0.rounded-sm.border 179×27 \"> 100 MW under construct\"",
2551 − "button.shrink-0.rounded-sm.border 57×27 \"Equinix\"",
2552 − "button.shrink-0.rounded-sm.border 51×27 \"Japan\"",
2553 − "a.inline-flex.items-center.gap-1 79×18 \"Open map→\""
2554 − ],
2555 − "smallCount": 94,
2556 − "h1Covered": false,
2557 − "h1Top": 100,
2558 − "headerBottom": 49,
2559 − "lang": "en",
2560 − "metaDesc": 159,
2561 − "h1Count": 1,
2562 − "skipped": [],
2563 − "unnamed": [],
2564 − "ariaCurrent": 1,
2565 − "footerOk": true,
2566 − "contrast": [
2567 − {
2568 − "ratio": 3.07,
2569 − "color": "rgb(91, 97, 104)",
2570 − "bg": "rgb(11,15,20)",
2571 − "sample": "span.normal-case.tracking-normal.text-ink-4 \"· updated continuously\"",
2572 − "n": 1
2573 − },
2574 − {
2575 − "ratio": 3.07,
2576 − "color": "rgb(91, 97, 104)",
2577 − "bg": "rgb(11,15,20)",
2578 − "sample": "span.shrink-0.py-1.text-[11.5px] \"Try\"",
2579 − "n": 1
2580 − },
2581 − {
2582 − "ratio": 3.07,
2583 − "color": "rgb(91, 97, 104)",
2584 − "bg": "rgb(11,15,20)",
2585 − "sample": "span.figure.w-5.text-right \"10\"",
2586 − "n": 2
2587 − },
2588 − {
2589 − "ratio": 3.07,
2590 − "color": "rgb(91, 97, 104)",
2591 − "bg": "rgb(11,15,20)",
2592 − "sample": "span.figure.text-ink-4 \"30%\"",
2593 − "n": 2
2594 − }
2595 − ],
2596 − "contrastChecked": 306,
2597 − "height": 6300,
2598 − "title": "DataCenterIndex — The global index of data center infrastructure",
2599 − "bottom": {
2600 − "navVisible": true,
2601 − "covered": false,
2602 − "gap": 809
2603 − },
2604 − "errors": [],
2605 − "badResponses": []
2606 − },
2607 − {
2608 − "name": "map",
2609 − "path": "/map",
2610 − "w": 390,
2611 − "theme": "dark",
2612 − "status": 200,
2613 − "ttfb": 1,
2614 − "overflow": null,
2615 − "clipped": [
2616 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2617 − ],
2618 − "small": [
2619 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2620 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2621 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2622 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2623 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2624 − "button.maplibregl-ctrl-geolocate 32×32 \"Location not available\"",
2625 − "button.maplibregl-ctrl-zoom-in 32×32 \"Zoom in\"",
2626 − "button.maplibregl-ctrl-zoom-out 32×32 \"Zoom out\"",
2627 − "input.h-full.min-w-0.flex-1 296×38 \"Search the map\"",
2628 − "button.inline-flex.h-8.items-center 32×32 \"Filters\"",
2629 − "button.inline-flex.h-8.items-center 93×32 \"Legend\""
2630 − ],
2631 − "smallCount": 11,
2632 − "h1Covered": false,
2633 − "h1Top": null,
2634 − "headerBottom": 49,
2635 − "lang": "en",
2636 − "metaDesc": 127,
2637 − "h1Count": 0,
2638 − "skipped": [],
2639 − "unnamed": [],
2640 − "ariaCurrent": 2,
2641 − "footerOk": false,
2642 − "contrast": [],
2643 − "contrastChecked": 4,
2644 − "height": 845,
2645 − "title": "Global data center map · DataCenterIndex",
2646 − "map": {
2647 − "controls": 3,
2648 − "unreachable": 0,
2649 − "canvas": true
2650 − },
2651 − "errors": [],
2652 − "badResponses": []
2653 − },
2654 − {
2655 − "name": "search",
2656 − "path": "/search?q=montreal",
2657 − "w": 390,
2658 − "theme": "dark",
2659 − "status": 200,
2660 − "ttfb": 4,
2661 − "overflow": null,
2662 − "clipped": [
2663 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2664 − ],
2665 − "small": [
2666 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2667 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2668 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2669 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2670 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2671 − "a.inline-flex.items-center.justify-center 122×32 \"Open in map\"",
2672 − "a.ml-1.text-ink-2.underline 85×17 \"Apply as filters\"",
2673 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
2674 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
2675 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
2676 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
2677 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\""
2678 − ],
2679 − "smallCount": 22,
2680 − "h1Covered": false,
2681 − "h1Top": 90,
2682 − "headerBottom": 49,
2683 − "lang": "en",
2684 − "metaDesc": 159,
2685 − "h1Count": 1,
2686 − "skipped": [],
2687 − "unnamed": [],
2688 − "ariaCurrent": 0,
2689 − "footerOk": true,
2690 − "contrast": [],
2691 − "contrastChecked": 46,
2692 − "height": 1871,
2693 − "title": "“montreal” — search · DataCenterIndex",
2694 − "bottom": {
2695 − "navVisible": true,
2696 − "covered": false,
2697 − "gap": 810
2698 − },
2699 − "errors": [],
2700 − "badResponses": []
2701 − },
2702 − {
2703 − "name": "live",
2704 − "path": "/live",
2705 − "w": 390,
2706 − "theme": "dark",
2707 − "status": 200,
2708 − "ttfb": 9,
2709 − "overflow": null,
2710 − "clipped": [
2711 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2712 − "span.sr-only \"United States\" 89>1",
2713 − "span.sr-only \"United States\" 89>1",
2714 − "span.sr-only \"United States\" 89>1",
2715 − "span.sr-only \"United States\" 89>1",
2716 − "span.sr-only \"United States\" 89>1",
2717 − "span.sr-only \"United States\" 89>1",
2718 − "span.sr-only \"United States\" 89>1",
2719 − "span.sr-only \"United States\" 89>1"
2720 − ],
2721 − "small": [
2722 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2723 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2724 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2725 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2726 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2727 − "button.inline-flex.items-center.justify-center 80×32 \"Filters\"",
2728 − "a.inline-flex.items-center.gap-1 181×17 \"Duke Energy — news relea\"",
2729 − "a.inline-flex.items-center.gap-1 245×17 \"American Electric Power \"",
2730 − "a.hover:text-ink 85×17 \"Element Critical\"",
2731 − "a.inline-flex.items-center.gap-1 101×17 \"Element Critical\"",
2732 − "a.hover:text-ink 85×17 \"Element Critical\"",
2733 − "a.inline-flex.items-center.gap-1 101×17 \"Element Critical\""
2734 − ],
2735 − "smallCount": 120,
2736 − "h1Covered": false,
2737 − "h1Top": 91,
2738 − "headerBottom": 49,
2739 − "lang": "en",
2740 − "metaDesc": 152,
2741 − "h1Count": 1,
2742 − "skipped": [
2743 − "h1→h3"
2744 − ],
2745 − "unnamed": [],
2746 − "ariaCurrent": 1,
2747 − "footerOk": true,
2748 − "contrast": [],
2749 − "contrastChecked": 506,
2750 − "height": 10195,
2751 − "title": "Live changes · DataCenterIndex",
2752 − "bottom": {
2753 − "navVisible": true,
2754 − "covered": false,
2755 − "gap": 809
2756 − },
2757 − "errors": [],
2758 − "badResponses": []
2759 − },
2760 − {
2761 − "name": "datacenters",
2762 − "path": "/datacenters",
2763 − "w": 390,
2764 − "theme": "dark",
2765 − "status": 200,
2766 − "ttfb": 12,
2767 − "overflow": null,
2768 − "clipped": [
2769 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
2770 − ],
2771 − "small": [
2772 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2773 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2774 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2775 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2776 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2777 − "a.inline-flex.items-center.justify-center 122×32 \"Open in map\"",
2778 − "button.inline-flex.items-center.justify-center 80×32 \"Filters\"",
2779 − "a.font-medium.text-ink.hover:text-accent 186×18 \"Bulk N01 Data Center Cam\"",
2780 − "a.font-medium.text-ink.hover:text-accent 225×35 \"Downtown Minneapolis Dat\"",
2781 − "a.font-medium.text-ink.hover:text-accent 225×35 \"Dallas Love Field Data C\"",
2782 − "a.font-medium.text-ink.hover:text-accent 139×18 \"CloudHQ SAT Campus\"",
2783 − "a.font-medium.text-ink.hover:text-accent 183×18 \"Skybox PowerCampus Austi\""
2784 − ],
2785 − "smallCount": 33,
2786 − "h1Covered": false,
2787 − "h1Top": 90,
2788 − "headerBottom": 49,
2789 − "lang": "en",
2790 − "metaDesc": 119,
2791 − "h1Count": 1,
2792 − "skipped": [],
2793 − "unnamed": [],
2794 − "ariaCurrent": 2,
2795 − "footerOk": true,
2796 − "contrast": [],
2797 − "contrastChecked": 98,
2798 − "height": 2286,
2799 − "title": "Data centers · DataCenterIndex",
2800 − "bottom": {
2801 − "navVisible": true,
2802 − "covered": false,
2803 − "gap": 810
2804 − },
2805 − "errors": [],
2806 − "badResponses": []
2807 − },
2808 − {
2809 − "name": "fac-operator-mw",
2810 − "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
2811 − "w": 390,
2812 − "theme": "dark",
2813 − "status": 200,
2814 − "ttfb": 10,
2815 − "overflow": null,
2816 − "clipped": [
2817 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2818 − "span.sr-only \"United States\" 86>1",
2819 − "span.sr-only \"United States\" 82>1",
2820 − "span.sr-only \"United States\" 89>1"
2821 − ],
2822 − "small": [
2823 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2824 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2825 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2826 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2827 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2828 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2829 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
2830 − "a.hover:text-ink.hover:underline 117×17 \"Minneapolis–St. Paul\"",
2831 − "a.normal-case.tracking-normal.text-[12.5px] 57×18 \"DataBank\"",
2832 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
2833 − "a.hover:text-accent.hover:underline 180×20 \"Minneapolis–St. Paul mar\"",
2834 − "a.hover:text-accent.hover:underline 86×20 \"United States\""
2835 − ],
2836 − "smallCount": 65,
2837 − "h1Covered": false,
2838 − "h1Top": 119,
2839 − "headerBottom": 49,
2840 − "lang": "en",
2841 − "metaDesc": 212,
2842 − "h1Count": 1,
2843 − "skipped": [],
2844 − "unnamed": [],
2845 − "ariaCurrent": 1,
2846 − "footerOk": true,
2847 − "contrast": [
2848 − {
2849 − "ratio": 3.07,
2850 − "color": "rgb(91, 97, 104)",
2851 − "bg": "rgb(11,15,20)",
2852 − "sample": "span.figure.text-[11px].text-ink-4 \"16\"",
2853 − "n": 2
2854 − },
2855 − {
2856 − "ratio": 3.07,
2857 − "color": "rgb(91, 97, 104)",
2858 − "bg": "rgb(11,15,20)",
2859 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"High confidence overall — comp\"",
2860 − "n": 1
2861 − },
2862 − {
2863 − "ratio": 3.07,
2864 − "color": "rgb(91, 97, 104)",
2865 − "bg": "rgb(11,15,20)",
2866 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
2867 − "n": 2
2868 − },
2869 − {
2870 − "ratio": 3.07,
2871 − "color": "rgb(91, 97, 104)",
2872 − "bg": "rgb(11,15,20)",
2873 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Operator\"",
2874 − "n": 1
2875 − },
2876 − {
2877 − "ratio": 3.07,
2878 − "color": "rgb(91, 97, 104)",
2879 − "bg": "rgb(11,15,20)",
2880 − "sample": "span.text-ink-4 \"· Publicly accessible operator\"",
2881 − "n": 1
2882 − },
2883 − {
2884 − "ratio": 3.07,
2885 − "color": "rgb(91, 97, 104)",
2886 − "bg": "rgb(11,15,20)",
2887 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
2888 − "n": 1
2889 − }
2890 − ],
2891 − "contrastChecked": 286,
2892 − "height": 6136,
2893 − "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
2894 − "bottom": {
2895 − "navVisible": true,
2896 − "covered": false,
2897 − "gap": 1106
2898 − },
2899 − "errors": [
2900 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
2901 − ],
2902 − "badResponses": [
2903 − "404 fetch /api/v1/docs?_rsc=LHzgujRx9VSP7Y_f"
2904 − ]
2905 − },
2906 − {
2907 − "name": "fac-osm",
2908 − "path": "/datacenters/shelter-modernizacion",
2909 − "w": 390,
2910 − "theme": "dark",
2911 − "status": 200,
2912 − "ttfb": 8,
2913 − "overflow": null,
2914 − "clipped": [
2915 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
2916 − "span.sr-only \"Argentina\" 62>1",
2917 − "span.sr-only \"Argentina\" 59>1",
2918 − "span.sr-only \"Argentina\" 64>1"
2919 − ],
2920 − "small": [
2921 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
2922 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
2923 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
2924 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
2925 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
2926 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
2927 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
2928 − "a.hover:text-ink.hover:underline 55×17 \"Argentina\"",
2929 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
2930 − "a.hover:text-accent.hover:underline 62×20 \"Argentina\"",
2931 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
2932 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
2933 − ],
2934 − "smallCount": 46,
2935 − "h1Covered": false,
2936 − "h1Top": 116,
2937 − "headerBottom": 49,
2938 − "lang": "en",
2939 − "metaDesc": 128,
2940 − "h1Count": 1,
2941 − "skipped": [],
2942 − "unnamed": [],
2943 − "ariaCurrent": 1,
2944 − "footerOk": true,
2945 − "contrast": [
2946 − {
2947 − "ratio": 3.07,
2948 − "color": "rgb(91, 97, 104)",
2949 − "bg": "rgb(11,15,20)",
2950 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Unverified overall — computed \"",
2951 − "n": 1
2952 − },
2953 − {
2954 − "ratio": 3.07,
2955 − "color": "rgb(91, 97, 104)",
2956 − "bg": "rgb(11,15,20)",
2957 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
2958 − "n": 2
2959 − },
2960 − {
2961 − "ratio": 3.07,
2962 − "color": "rgb(91, 97, 104)",
2963 − "bg": "rgb(11,15,20)",
2964 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Community\"",
2965 − "n": 1
2966 − },
2967 − {
2968 − "ratio": 3.07,
2969 − "color": "rgb(91, 97, 104)",
2970 − "bg": "rgb(11,15,20)",
2971 − "sample": "span.text-ink-4 \"· ODbL 1.0 — © OpenStreetMap c\"",
2972 − "n": 1
2973 − },
2974 − {
2975 − "ratio": 3.07,
2976 − "color": "rgb(91, 97, 104)",
2977 − "bg": "rgb(11,15,20)",
2978 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
2979 − "n": 1
2980 − }
2981 − ],
2982 − "contrastChecked": 179,
2983 − "height": 4626,
2984 − "title": "Shelter Modernización · DataCenterIndex",
2985 − "bottom": {
2986 − "navVisible": true,
2987 − "covered": false,
2988 − "gap": 1106
2989 − },
2990 − "errors": [
2991 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
2992 − ],
2993 − "badResponses": [
2994 − "404 fetch /api/v1/docs?_rsc=QORYKIJc7fb0QGjs"
2995 − ]
2996 − },
2997 − {
2998 − "name": "fac-google",
2999 − "path": "/datacenters/google-the-dalles-oregon-data-center",
3000 − "w": 390,
3001 − "theme": "dark",
3002 − "status": 200,
3003 − "ttfb": 13,
3004 − "overflow": null,
3005 − "clipped": [
3006 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3007 − "span.sr-only \"United States\" 86>1",
3008 − "span.sr-only \"United States\" 82>1",
3009 − "span.sr-only \"United States\" 89>1",
3010 − "span.sr-only \"United States\" 89>1",
3011 − "span.sr-only \"United States\" 89>1",
3012 − "span.sr-only \"United States\" 89>1"
3013 − ],
3014 − "small": [
3015 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3016 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3017 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3018 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3019 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3020 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3021 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
3022 − "a.hover:text-ink.hover:underline 109×17 \"Hillsboro / Portland\"",
3023 − "a.normal-case.tracking-normal.text-[12.5px] 43×18 \"Google\"",
3024 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
3025 − "a.hover:text-accent.hover:underline 170×20 \"Hillsboro / Portland mar\"",
3026 − "a.hover:text-accent.hover:underline 86×20 \"United States\""
3027 − ],
3028 − "smallCount": 101,
3029 − "h1Covered": false,
3030 − "h1Top": 119,
3031 − "headerBottom": 49,
3032 − "lang": "en",
3033 − "metaDesc": 203,
3034 − "h1Count": 1,
3035 − "skipped": [],
3036 − "unnamed": [],
3037 − "ariaCurrent": 1,
3038 − "footerOk": true,
3039 − "contrast": [
3040 − {
3041 − "ratio": 3.07,
3042 − "color": "rgb(91, 97, 104)",
3043 − "bg": "rgb(11,15,20)",
3044 − "sample": "span.figure.text-[11px].text-ink-4 \"26\"",
3045 − "n": 2
3046 − },
3047 − {
3048 − "ratio": 3.07,
3049 − "color": "rgb(91, 97, 104)",
3050 − "bg": "rgb(11,15,20)",
3051 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"High confidence overall — comp\"",
3052 − "n": 1
3053 − },
3054 − {
3055 − "ratio": 3.07,
3056 − "color": "rgb(91, 97, 104)",
3057 − "bg": "rgb(11,15,20)",
3058 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
3059 − "n": 2
3060 − },
3061 − {
3062 − "ratio": 3.07,
3063 − "color": "rgb(91, 97, 104)",
3064 − "bg": "rgb(11,15,20)",
3065 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Cloud provider\"",
3066 − "n": 2
3067 − },
3068 − {
3069 − "ratio": 3.07,
3070 − "color": "rgb(91, 97, 104)",
3071 − "bg": "rgb(11,15,20)",
3072 − "sample": "span.text-ink-4 \"· Publicly published operator \"",
3073 − "n": 2
3074 − },
3075 − {
3076 − "ratio": 3.07,
3077 − "color": "rgb(91, 97, 104)",
3078 − "bg": "rgb(11,15,20)",
3079 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
3080 − "n": 1
3081 − }
3082 − ],
3083 − "contrastChecked": 502,
3084 − "height": 10760,
3085 − "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
3086 − "bottom": {
3087 − "navVisible": true,
3088 − "covered": false,
3089 − "gap": 1106
3090 − },
3091 − "errors": [],
3092 − "badResponses": []
3093 − },
3094 − {
3095 − "name": "fac-wikidata",
3096 − "path": "/datacenters/foulum-data-center",
3097 − "w": 390,
3098 − "theme": "dark",
3099 − "status": 200,
3100 − "ttfb": 12,
3101 − "overflow": null,
3102 − "clipped": [
3103 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3104 − "span.sr-only \"Denmark\" 58>1",
3105 − "span.sr-only \"Denmark\" 56>1",
3106 − "span.sr-only \"Denmark\" 60>1"
3107 − ],
3108 − "small": [
3109 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3110 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3111 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3112 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3113 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3114 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3115 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
3116 − "a.hover:text-ink.hover:underline 51×17 \"Denmark\"",
3117 − "a.normal-case.tracking-normal.text-[12.5px] 35×18 \"Apple\"",
3118 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
3119 − "a.hover:text-accent.hover:underline 58×20 \"Denmark\"",
3120 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
3121 − ],
3122 − "smallCount": 57,
3123 − "h1Covered": false,
3124 − "h1Top": 119,
3125 − "headerBottom": 49,
3126 − "lang": "en",
3127 − "metaDesc": 158,
3128 − "h1Count": 1,
3129 − "skipped": [],
3130 − "unnamed": [],
3131 − "ariaCurrent": 1,
3132 − "footerOk": true,
3133 − "contrast": [
3134 − {
3135 − "ratio": 3.07,
3136 − "color": "rgb(91, 97, 104)",
3137 − "bg": "rgb(11,15,20)",
3138 − "sample": "span.figure.text-[11px].text-ink-4 \"15\"",
3139 − "n": 2
3140 − },
3141 − {
3142 − "ratio": 3.07,
3143 − "color": "rgb(91, 97, 104)",
3144 − "bg": "rgb(11,15,20)",
3145 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Moderate confidence overall — \"",
3146 − "n": 1
3147 − },
3148 − {
3149 − "ratio": 3.07,
3150 − "color": "rgb(91, 97, 104)",
3151 − "bg": "rgb(11,15,20)",
3152 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
3153 − "n": 2
3154 − },
3155 − {
3156 − "ratio": 3.07,
3157 − "color": "rgb(91, 97, 104)",
3158 − "bg": "rgb(11,15,20)",
3159 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Community\"",
3160 − "n": 2
3161 − },
3162 − {
3163 − "ratio": 3.07,
3164 − "color": "rgb(91, 97, 104)",
3165 − "bg": "rgb(11,15,20)",
3166 − "sample": "span.text-ink-4 \"· ODbL 1.0 — © OpenStreetMap c\"",
3167 − "n": 2
3168 − },
3169 − {
3170 − "ratio": 3.07,
3171 − "color": "rgb(91, 97, 104)",
3172 − "bg": "rgb(11,15,20)",
3173 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
3174 − "n": 1
3175 − }
3176 − ],
3177 − "contrastChecked": 273,
3178 − "height": 6042,
3179 − "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
3180 − "bottom": {
3181 − "navVisible": true,
3182 − "covered": false,
3183 − "gap": 1105
3184 − },
3185 − "errors": [],
3186 − "badResponses": []
3187 − },
3188 − {
3189 − "name": "operators",
3190 − "path": "/operators",
3191 − "w": 390,
3192 − "theme": "dark",
3193 − "status": 200,
3194 − "ttfb": 10,
3195 − "overflow": null,
3196 − "clipped": [
3197 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3198 − "span.sr-only \"Search operators\" 114>1"
3199 − ],
3200 − "small": [
3201 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3202 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3203 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3204 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3205 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3206 − "input.h-9.min-w-0.rounded-sm 218×36 \"\"",
3207 − "select.h-9.min-w-0.rounded-sm 140×36 \"Kind\"",
3208 − "select.h-9.min-w-0.rounded-sm 160×36 \"Headquarters country\"",
3209 − "button.inline-flex.items-center.justify-center 59×36 \"Filter\"",
3210 − "a.font-medium.text-ink.hover:text-accent 60×18 \"DataBank\"",
3211 − "a.font-medium.text-ink.hover:text-accent 81×18 \"EdgeConneX\"",
3212 − "a.font-medium.text-ink.hover:text-accent 62×18 \"CyrusOne\""
3213 − ],
3214 − "smallCount": 75,
3215 − "h1Covered": false,
3216 − "h1Top": 90,
3217 − "headerBottom": 49,
3218 − "lang": "en",
3219 − "metaDesc": 139,
3220 − "h1Count": 1,
3221 − "skipped": [],
3222 − "unnamed": [],
3223 − "ariaCurrent": 2,
3224 − "footerOk": true,
3225 − "contrast": [],
3226 − "contrastChecked": 268,
3227 − "height": 4954,
3228 − "title": "Operators · DataCenterIndex",
3229 − "bottom": {
3230 − "navVisible": true,
3231 − "covered": false,
3232 − "gap": 810
3233 − },
3234 − "errors": [
3235 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3236 − ],
3237 − "badResponses": [
3238 − "404 fetch /api/v1/docs?_rsc=nAdjiVRfIop78OSR"
3239 − ]
3240 − },
3241 − {
3242 − "name": "operator-equinix",
3243 − "path": "/operators/equinix",
3244 − "w": 390,
3245 − "theme": "dark",
3246 − "status": 200,
3247 − "ttfb": 14,
3248 − "overflow": null,
3249 − "clipped": [
3250 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3251 − "span.sr-only \"Finland\" 45>1",
3252 − "span.sr-only \"Germany\" 56>1",
3253 − "span.sr-only \"Italy\" 26>1",
3254 − "span.sr-only \"Switzerland\" 72>1",
3255 − "span.sr-only \"United States\" 82>1",
3256 − "span.sr-only \"Australia\" 54>1",
3257 − "span.sr-only \"Ireland\" 42>1",
3258 − "span.sr-only \"Netherlands\" 75>1"
3259 − ],
3260 − "small": [
3261 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3262 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3263 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3264 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3265 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3266 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3267 − "a.hover:text-ink.hover:underline 57×17 \"Operators\"",
3268 − "a.inline-flex.items-center.gap-1.5 106×19 \"🇺🇸United States\"",
3269 − "a.break-all.text-ink.underline 75×19 \"equinix.com\"",
3270 − "a.block 366×36 \"1🇫🇮FinlandFinland2\"",
3271 − "a.block 366×36 \"2🇩🇪GermanyGermany2\"",
3272 − "a.block 366×36 \"3🇮🇹ItalyItaly2\""
3273 − ],
3274 − "smallCount": 150,
3275 − "h1Covered": false,
3276 − "h1Top": 122,
3277 − "headerBottom": 49,
3278 − "lang": "en",
3279 − "metaDesc": 170,
3280 − "h1Count": 1,
3281 − "skipped": [],
3282 − "unnamed": [],
3283 − "ariaCurrent": 2,
3284 − "footerOk": true,
3285 − "contrast": [
3286 − {
3287 − "ratio": 3.07,
3288 − "color": "rgb(91, 97, 104)",
3289 − "bg": "rgb(11,15,20)",
3290 − "sample": "span.figure.text-[11px].text-ink-4 \"25\"",
3291 − "n": 5
3292 − },
3293 − {
3294 − "ratio": 3.07,
3295 − "color": "rgb(91, 97, 104)",
3296 − "bg": "rgb(11,15,20)",
3297 − "sample": "span.figure.w-5.text-right \"10\"",
3298 − "n": 1
3299 − },
3300 − {
3301 − "ratio": 3.07,
3302 − "color": "rgb(91, 97, 104)",
3303 − "bg": "rgb(11,15,20)",
3304 − "sample": "span.figure.text-ink-4 \"93%\"",
3305 − "n": 1
3306 − },
3307 − {
3308 − "ratio": 3.07,
3309 − "color": "rgb(91, 97, 104)",
3310 − "bg": "rgb(11,15,20)",
3311 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Operator\"",
3312 − "n": 10
3313 − },
3314 − {
3315 − "ratio": 3.07,
3316 − "color": "rgb(91, 97, 104)",
3317 − "bg": "rgb(11,15,20)",
3318 − "sample": "span.text-ink-4 \"· Publicly accessible operator\"",
3319 − "n": 10
3320 − },
3321 − {
3322 − "ratio": 3.07,
3323 − "color": "rgb(91, 97, 104)",
3324 − "bg": "rgb(11,15,20)",
3325 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
3326 − "n": 1
3327 − },
3328 − {
3329 − "ratio": 3.07,
3330 − "color": "rgb(91, 97, 104)",
3331 − "bg": "rgb(11,15,20)",
3332 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
3333 − "n": 1
3334 − }
3335 − ],
3336 − "contrastChecked": 619,
3337 − "height": 10329,
3338 − "title": "Equinix · DataCenterIndex",
3339 − "bottom": {
3340 − "navVisible": true,
3341 − "covered": false,
3342 − "gap": 1073
3343 − },
3344 − "errors": [],
3345 − "badResponses": []
3346 − },
3347 − {
3348 − "name": "countries",
3349 − "path": "/countries",
3350 − "w": 390,
3351 − "theme": "dark",
3352 − "status": 200,
3353 − "ttfb": 17,
3354 − "overflow": null,
3355 − "clipped": [
3356 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3357 − "span.sr-only \"United States\" 83>1",
3358 − "span.sr-only \"Brazil\" 35>1",
3359 − "span.sr-only \"Argentina\" 60>1",
3360 − "span.sr-only \"Canada\" 48>1",
3361 − "span.sr-only \"Mexico\" 45>1",
3362 − "span.sr-only \"Chile\" 32>1",
3363 − "span.sr-only \"Venezuela\" 65>1",
3364 − "span.sr-only \"Peru\" 29>1"
3365 − ],
3366 − "small": [
3367 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3368 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3369 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3370 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3371 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3372 − "a.inline-flex.h-7.shrink-0 78×28 \"All regions\"",
3373 − "a.inline-flex.h-7.shrink-0 90×28 \"Americas18\"",
3374 − "a.inline-flex.h-7.shrink-0 77×28 \"Europe38\"",
3375 − "a.inline-flex.h-7.shrink-0 61×28 \"Asia34\"",
3376 − "a.inline-flex.h-7.shrink-0 77×28 \"Oceania3\"",
3377 − "a.inline-flex.h-7.shrink-0 71×28 \"Africa12\"",
3378 − "a.font-medium.text-ink.hover:text-accent 109×18 \"🇺🇸United StatesUnited \""
3379 − ],
3380 − "smallCount": 71,
3381 − "h1Covered": false,
3382 − "h1Top": 90,
3383 − "headerBottom": 49,
3384 − "lang": "en",
3385 − "metaDesc": 117,
3386 − "h1Count": 1,
3387 − "skipped": [],
3388 − "unnamed": [],
3389 − "ariaCurrent": 2,
3390 − "footerOk": true,
3391 − "contrast": [
3392 − {
3393 − "ratio": 3.07,
3394 − "color": "rgb(91, 97, 104)",
3395 − "bg": "rgb(11,15,20)",
3396 − "sample": "p.mt-6.text-[11.5px].text-ink-4 \"Per-million uses the latest Wo\"",
3397 − "n": 1
3398 − },
3399 − {
3400 − "ratio": 3.07,
3401 − "color": "rgb(91, 97, 104)",
3402 − "bg": "rgb(11,15,20)",
3403 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"methodology\"",
3404 − "n": 1
3405 − }
3406 − ],
3407 − "contrastChecked": 304,
3408 − "height": 5206,
3409 − "title": "Countries · DataCenterIndex",
3410 − "bottom": {
3411 − "navVisible": true,
3412 − "covered": false,
3413 − "gap": 810
3414 − },
3415 − "errors": [
3416 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3417 − ],
3418 − "badResponses": [
3419 − "404 fetch /api/v1/docs?_rsc=rWPI48G8kG2iJ9dR"
3420 − ]
3421 − },
3422 − {
3423 − "name": "country-us",
3424 − "path": "/countries/united-states",
3425 − "w": 390,
3426 − "theme": "dark",
3427 − "status": 200,
3428 − "ttfb": 23,
3429 − "overflow": null,
3430 − "clipped": [
3431 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3432 − "span.sr-only \"United States\" 149>1",
3433 − "span.sr-only \"United States\" 76>1",
3434 − "span.sr-only \"United States\" 76>1",
3435 − "span.sr-only \"United States\" 76>1",
3436 − "span.sr-only \"United States\" 76>1",
3437 − "span.sr-only \"United States\" 76>1",
3438 − "span.sr-only \"United States\" 76>1",
3439 − "span.sr-only \"United States\" 76>1"
3440 − ],
3441 − "small": [
3442 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3443 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3444 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3445 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3446 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3447 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3448 − "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
3449 − "a.inline-flex.h-7.shrink-0 211×28 \"#1/46Countries by cloud \"",
3450 − "a.inline-flex.h-7.shrink-0 214×28 \"#1/103Countries by facil\"",
3451 − "a.inline-flex.h-7.shrink-0 233×28 \"#24/52Facilities per mil\"",
3452 − "a.inline-flex.h-7.shrink-0 338×28 \"#14/14Countries by facil\"",
3453 − "a.inline-flex.h-7.shrink-0 247×28 \"#1/28Countries by hypers\""
3454 − ],
3455 − "smallCount": 106,
3456 − "h1Covered": false,
3457 − "h1Top": 120,
3458 − "headerBottom": 49,
3459 − "lang": "en",
3460 − "metaDesc": 258,
3461 − "h1Count": 1,
3462 − "skipped": [],
3463 − "unnamed": [],
3464 − "ariaCurrent": 2,
3465 − "footerOk": true,
3466 − "contrast": [
3467 − {
3468 − "ratio": 2.91,
3469 − "color": "rgb(91, 97, 104)",
3470 − "bg": "rgb(16,22,29)",
3471 − "sample": "span.text-ink-4 \"/46\"",
3472 − "n": 5
3473 − },
3474 − {
3475 − "ratio": 3.07,
3476 − "color": "rgb(91, 97, 104)",
3477 − "bg": "rgb(11,15,20)",
3478 − "sample": "span.text-[11px].text-ink-4 \"World Bank\"",
3479 − "n": 1
3480 − },
3481 − {
3482 − "ratio": 3.07,
3483 − "color": "rgb(91, 97, 104)",
3484 − "bg": "rgb(11,15,20)",
3485 − "sample": "span.figure.text-[11px].text-ink-4 \"460\"",
3486 − "n": 9
3487 − },
3488 − {
3489 − "ratio": 3.07,
3490 − "color": "rgb(91, 97, 104)",
3491 − "bg": "rgb(11,15,20)",
3492 − "sample": "span.figure.text-ink-4 \"90%\"",
3493 − "n": 1
3494 − },
3495 − {
3496 − "ratio": 3.07,
3497 − "color": "rgb(91, 97, 104)",
3498 − "bg": "rgb(11,15,20)",
3499 − "sample": "span.figure.w-5.text-right \"10\"",
3500 − "n": 1
3501 − },
3502 − {
3503 − "ratio": 3.07,
3504 − "color": "rgb(91, 97, 104)",
3505 − "bg": "rgb(11,15,20)",
3506 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Source: World Bank, World Deve\"",
3507 − "n": 1
3508 − },
3509 − {
3510 − "ratio": 3.07,
3511 − "color": "rgb(91, 97, 104)",
3512 − "bg": "rgb(11,15,20)",
3513 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Country totals are lower bound\"",
3514 − "n": 1
3515 − },
3516 − {
3517 − "ratio": 3.07,
3518 − "color": "rgb(91, 97, 104)",
3519 − "bg": "rgb(11,15,20)",
3520 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
3521 − "n": 1
3522 − }
3523 − ],
3524 − "contrastChecked": 492,
3525 − "height": 9191,
3526 − "title": "Data Centers in United States · DataCenterIndex",
3527 − "bottom": {
3528 − "navVisible": true,
3529 − "covered": false,
3530 − "gap": 1073
3531 − },
3532 − "errors": [],
3533 − "badResponses": []
3534 − },
3535 − {
3536 − "name": "country-ca",
3537 − "path": "/countries/canada",
3538 − "w": 390,
3539 − "theme": "dark",
3540 − "status": 200,
3541 − "ttfb": 12,
3542 − "overflow": null,
3543 − "clipped": [
3544 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3545 − "span.sr-only \"Canada\" 86>1",
3546 − "span.sr-only \"Canada\" 43>1",
3547 − "span.sr-only \"Canada\" 43>1",
3548 − "span.sr-only \"Canada\" 51>1",
3549 − "span.sr-only \"Canada\" 51>1",
3550 − "span.sr-only \"Canada\" 51>1",
3551 − "span.sr-only \"Canada\" 51>1",
3552 − "span.sr-only \"Canada\" 51>1"
3553 − ],
3554 − "small": [
3555 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3556 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3557 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3558 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3559 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3560 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3561 − "a.hover:text-ink.hover:underline 55×17 \"Countries\"",
3562 − "a.inline-flex.h-7.shrink-0 211×28 \"#7/46Countries by cloud \"",
3563 − "a.inline-flex.h-7.shrink-0 221×28 \"#22/103Countries by faci\"",
3564 − "a.inline-flex.h-7.shrink-0 233×28 \"#33/52Facilities per mil\"",
3565 − "a.inline-flex.h-7.shrink-0 254×28 \"#12/28Countries by hyper\"",
3566 − "a.block 366×36 \"Unknown15\""
3567 − ],
3568 − "smallCount": 80,
3569 − "h1Covered": false,
3570 − "h1Top": 120,
3571 − "headerBottom": 49,
3572 − "lang": "en",
3573 − "metaDesc": 186,
3574 − "h1Count": 1,
3575 − "skipped": [],
3576 − "unnamed": [],
3577 − "ariaCurrent": 2,
3578 − "footerOk": true,
3579 − "contrast": [
3580 − {
3581 − "ratio": 2.91,
3582 − "color": "rgb(91, 97, 104)",
3583 − "bg": "rgb(16,22,29)",
3584 − "sample": "span.text-ink-4 \"/46\"",
3585 − "n": 4
3586 − },
3587 − {
3588 − "ratio": 3.07,
3589 − "color": "rgb(91, 97, 104)",
3590 − "bg": "rgb(11,15,20)",
3591 − "sample": "span.text-[11px].text-ink-4 \"World Bank\"",
3592 − "n": 1
3593 − },
3594 − {
3595 − "ratio": 3.07,
3596 − "color": "rgb(91, 97, 104)",
3597 − "bg": "rgb(11,15,20)",
3598 − "sample": "span.figure.text-[11px].text-ink-4 \"21\"",
3599 − "n": 5
3600 − },
3601 − {
3602 − "ratio": 3.07,
3603 − "color": "rgb(91, 97, 104)",
3604 − "bg": "rgb(11,15,20)",
3605 − "sample": "span.figure.text-ink-4 \"76%\"",
3606 − "n": 2
3607 − },
3608 − {
3609 − "ratio": 3.07,
3610 − "color": "rgb(91, 97, 104)",
3611 − "bg": "rgb(11,15,20)",
3612 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Source: World Bank, World Deve\"",
3613 − "n": 1
3614 − },
3615 − {
3616 − "ratio": 3.07,
3617 − "color": "rgb(91, 97, 104)",
3618 − "bg": "rgb(11,15,20)",
3619 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Country totals are lower bound\"",
3620 − "n": 1
3621 − },
3622 − {
3623 − "ratio": 3.07,
3624 − "color": "rgb(91, 97, 104)",
3625 − "bg": "rgb(11,15,20)",
3626 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
3627 − "n": 1
3628 − }
3629 − ],
3630 − "contrastChecked": 344,
3631 − "height": 7567,
3632 − "title": "Data Centers in Canada · DataCenterIndex",
3633 − "bottom": {
3634 − "navVisible": true,
3635 − "covered": false,
3636 − "gap": 1073
3637 − },
3638 − "errors": [
3639 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3640 − ],
3641 − "badResponses": [
3642 − "404 fetch /api/v1/docs?_rsc=uoChELS4xfChicuP"
3643 − ]
3644 − },
3645 − {
3646 − "name": "markets",
3647 − "path": "/datacenters/markets",
3648 − "w": 390,
3649 − "theme": "dark",
3650 − "status": 200,
3651 − "ttfb": 13,
3652 − "overflow": null,
3653 − "clipped": [
3654 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3655 − ],
3656 − "small": [
3657 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3658 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3659 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3660 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3661 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3662 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3663 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
3664 − "a.inline-flex.items-center.justify-center 86×32 \"Open map\"",
3665 − "input.h-9.min-w-0.flex-1 366×18 \"Search markets\"",
3666 − "select.h-9.min-w-0.flex-1 299×36 \"Country\"",
3667 − "button.inline-flex.items-center.justify-center 59×36 \"Filter\"",
3668 − "a.inline-flex.size-9.items-center 36×36 \"Next page\""
3669 − ],
3670 − "smallCount": 27,
3671 − "h1Covered": false,
3672 − "h1Top": 120,
3673 − "headerBottom": 49,
3674 − "lang": "en",
3675 − "metaDesc": 125,
3676 − "h1Count": 1,
3677 − "skipped": [],
3678 − "unnamed": [],
3679 − "ariaCurrent": 2,
3680 − "footerOk": true,
3681 − "contrast": [
3682 − {
3683 − "ratio": 3.07,
3684 − "color": "rgb(91, 97, 104)",
3685 − "bg": "rgb(11,15,20)",
3686 − "sample": "p.mt-6.text-[11.5px].text-ink-4 \"Facilities are assigned to a m\"",
3687 − "n": 1
3688 − },
3689 − {
3690 − "ratio": 3.07,
3691 − "color": "rgb(91, 97, 104)",
3692 − "bg": "rgb(11,15,20)",
3693 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
3694 − "n": 1
3695 − }
3696 − ],
3697 − "contrastChecked": 235,
3698 − "height": 5208,
3699 − "title": "Data center markets · DataCenterIndex",
3700 − "bottom": {
3701 − "navVisible": true,
3702 − "covered": false,
3703 − "gap": 810
3704 − },
3705 − "errors": [
3706 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3707 − ],
3708 − "badResponses": [
3709 − "404 fetch /api/v1/docs?_rsc=pvP-qw1_z45AIAvk"
3710 − ]
3711 − },
3712 − {
3713 − "name": "market-nova",
3714 − "path": "/datacenters/markets/northern-virginia",
3715 − "w": 390,
3716 − "theme": "dark",
3717 − "status": 200,
3718 − "ttfb": 11,
3719 − "overflow": null,
3720 − "clipped": [
3721 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3722 − "span.sr-only \"United States\" 82>1",
3723 − "span.sr-only \"United States\" 76>1",
3724 − "span.sr-only \"United States\" 89>1",
3725 − "span.sr-only \"United States\" 89>1",
3726 − "span.sr-only \"United States\" 89>1",
3727 − "span.sr-only \"United States\" 89>1",
3728 − "span.sr-only \"United States\" 89>1",
3729 − "span.sr-only \"United States\" 89>1"
3730 − ],
3731 − "small": [
3732 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3733 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3734 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3735 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3736 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3737 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3738 − "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
3739 − "a.hover:text-ink.hover:underline 46×17 \"Markets\"",
3740 − "a.text-ink.hover:text-accent.hover:underline 82×19 \"United States\"",
3741 − "a.inline-flex.h-7.shrink-0 219×28 \"#72/ 152Metros by facili\"",
3742 − "a.inline-flex.h-7.shrink-0 230×28 \"#44/ 129Metros by operat\"",
3743 − "a.inline-flex.h-10.items-center 38×40 \"Map\""
3744 − ],
3745 − "smallCount": 97,
3746 − "h1Covered": false,
3747 − "h1Top": 120,
3748 − "headerBottom": 49,
3749 − "lang": "en",
3750 − "metaDesc": 250,
3751 − "h1Count": 1,
3752 − "skipped": [],
3753 − "unnamed": [],
3754 − "ariaCurrent": 2,
3755 − "footerOk": true,
3756 − "contrast": [
3757 − {
3758 − "ratio": 2.91,
3759 − "color": "rgb(91, 97, 104)",
3760 − "bg": "rgb(16,22,29)",
3761 − "sample": "span.figure.text-ink-4 \"/ 152\"",
3762 − "n": 4
3763 − },
3764 − {
3765 − "ratio": 3.07,
3766 − "color": "rgb(91, 97, 104)",
3767 − "bg": "rgb(11,15,20)",
3768 − "sample": "span.figure.text-[11px].text-ink-4 \"10\"",
3769 − "n": 3
3770 − },
3771 − {
3772 − "ratio": 3.07,
3773 − "color": "rgb(91, 97, 104)",
3774 − "bg": "rgb(11,15,20)",
3775 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
3776 − "n": 1
3777 − },
3778 − {
3779 − "ratio": 3.07,
3780 − "color": "rgb(91, 97, 104)",
3781 − "bg": "rgb(11,15,20)",
3782 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
3783 − "n": 1
3784 − }
3785 − ],
3786 − "contrastChecked": 462,
3787 − "height": 8563,
3788 − "title": "Data Centers in Northern Virginia · DataCenterIndex",
3789 − "bottom": {
3790 − "navVisible": true,
3791 − "covered": false,
3792 − "gap": 1074
3793 − },
3794 − "errors": [
3795 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3796 − ],
3797 − "badResponses": [
3798 − "404 fetch /api/v1/docs?_rsc=kksxSvqB6K9jsp-1"
3799 − ]
3800 − },
3801 − {
3802 − "name": "projects",
3803 − "path": "/projects",
3804 − "w": 390,
3805 − "theme": "dark",
3806 − "status": 200,
3807 − "ttfb": 12,
3808 − "overflow": null,
3809 − "clipped": [
3810 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3811 − "span.sr-only \"United States\" 76>1",
3812 − "span.sr-only \"United States\" 76>1",
3813 − "span.sr-only \"Australia\" 49>1",
3814 − "span.sr-only \"Canada\" 43>1",
3815 − "span.sr-only \"United States\" 76>1",
3816 − "span.sr-only \"Norway\" 44>1"
3817 − ],
3818 − "small": [
3819 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3820 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3821 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3822 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3823 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3824 − "a.block 366×36 \"1🇺🇸United States≈ 125 \"",
3825 − "a.block 366×36 \"2🇯🇵Japan≈ 12.7 GW16\"",
3826 − "a.block 366×36 \"3🇦🇺Australia≈ 26 GW9\"",
3827 − "a.block 366×36 \"4🇲🇾Malaysia≈ 15.4 GW8\"",
3828 − "a.block 366×36 \"5🇩🇪Germany≈ 21.1 GW4\"",
3829 − "a.block 366×36 \"6🇸🇬Singapore≈ 314 MW4\"",
3830 − "a.block 366×36 \"7🇫🇮Finland≈ 1.79 GW3\""
3831 − ],
3832 − "smallCount": 30,
3833 − "h1Covered": false,
3834 − "h1Top": 90,
3835 − "headerBottom": 49,
3836 − "lang": "en",
3837 − "metaDesc": 130,
3838 − "h1Count": 1,
3839 − "skipped": [],
3840 − "unnamed": [],
3841 − "ariaCurrent": 3,
3842 − "footerOk": true,
3843 − "contrast": [
3844 − {
3845 − "ratio": 3.07,
3846 − "color": "rgb(91, 97, 104)",
3847 − "bg": "rgb(11,15,20)",
3848 − "sample": "span.figure.text-ink-4 \"19%\"",
3849 − "n": 3
3850 − },
3851 − {
3852 − "ratio": 3.07,
3853 − "color": "rgb(91, 97, 104)",
3854 − "bg": "rgb(11,15,20)",
3855 − "sample": "span.text-ink-4 \"TBD\"",
3856 − "n": 50
3857 − },
3858 − {
3859 − "ratio": 3.07,
3860 − "color": "rgb(91, 97, 104)",
3861 − "bg": "rgb(11,15,20)",
3862 − "sample": "p.mt-6.text-[11.5px].text-ink-4 \"Projects are reconciled from o\"",
3863 − "n": 1
3864 − },
3865 − {
3866 − "ratio": 3.07,
3867 − "color": "rgb(91, 97, 104)",
3868 − "bg": "rgb(11,15,20)",
3869 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"live feed\"",
3870 − "n": 1
3871 − }
3872 − ],
3873 − "contrastChecked": 344,
3874 − "height": 7315,
3875 − "title": "Data center projects · DataCenterIndex",
3876 − "bottom": {
3877 − "navVisible": true,
3878 − "covered": false,
3879 − "gap": 810
3880 − },
3881 − "errors": [
3882 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3883 − ],
3884 − "badResponses": [
3885 − "404 fetch /api/v1/docs?_rsc=rVOxZyJZQf-ncF1B"
3886 − ]
3887 − },
3888 − {
3889 − "name": "project",
3890 − "path": "/projects/sydney-data-center-project",
3891 − "w": 390,
3892 − "theme": "dark",
3893 − "status": 200,
3894 − "ttfb": 7,
3895 − "overflow": null,
3896 − "clipped": [
3897 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
3898 − "span.sr-only \"Australia\" 54>1",
3899 − "span.sr-only \"Australia\" 54>1",
3900 − "span.sr-only \"Australia\" 58>1"
3901 − ],
3902 − "small": [
3903 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3904 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3905 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3906 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3907 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3908 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
3909 − "a.hover:text-ink.hover:underline 46×17 \"Projects\"",
3910 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
3911 − "a.hover:text-ink.hover:underline 46×19 \"Sydney\"",
3912 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
3913 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\"",
3914 − "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
3915 − ],
3916 − "smallCount": 42,
3917 − "h1Covered": false,
3918 − "h1Top": 120,
3919 − "headerBottom": 49,
3920 − "lang": "en",
3921 − "metaDesc": 201,
3922 − "h1Count": 1,
3923 − "skipped": [],
3924 − "unnamed": [],
3925 − "ariaCurrent": 2,
3926 − "footerOk": true,
3927 − "contrast": [
3928 − {
3929 − "ratio": 3.07,
3930 − "color": "rgb(91, 97, 104)",
3931 − "bg": "rgb(11,15,20)",
3932 − "sample": "span.figure.text-[11px].text-ink-4 \"10\"",
3933 − "n": 2
3934 − },
3935 − {
3936 − "ratio": 3.07,
3937 − "color": "rgb(91, 97, 104)",
3938 − "bg": "rgb(11,15,20)",
3939 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"News\"",
3940 − "n": 1
3941 − },
3942 − {
3943 − "ratio": 3.07,
3944 − "color": "rgb(91, 97, 104)",
3945 − "bg": "rgb(11,15,20)",
3946 − "sample": "span.text-ink-4 \"· Headlines, dates, facts and \"",
3947 − "n": 1
3948 − },
3949 − {
3950 − "ratio": 3.07,
3951 − "color": "rgb(91, 97, 104)",
3952 − "bg": "rgb(11,15,20)",
3953 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
3954 − "n": 1
3955 − },
3956 − {
3957 − "ratio": 3.07,
3958 − "color": "rgb(91, 97, 104)",
3959 − "bg": "rgb(11,15,20)",
3960 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
3961 − "n": 1
3962 − }
3963 − ],
3964 − "contrastChecked": 195,
3965 − "height": 4322,
3966 − "title": "Sydney data center project · DataCenterIndex",
3967 − "bottom": {
3968 − "navVisible": true,
3969 − "covered": false,
3970 − "gap": 1073
3971 − },
3972 − "errors": [
3973 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
3974 − ],
3975 − "badResponses": [
3976 − "404 fetch /api/v1/docs?_rsc=ZwbwKyoxiqLR71eh"
3977 − ]
3978 − },
3979 − {
3980 − "name": "rankings",
3981 − "path": "/rankings",
3982 − "w": 390,
3983 − "theme": "dark",
3984 − "status": 200,
3985 − "ttfb": 2,
3986 − "overflow": null,
3987 − "clipped": [
3988 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
3989 − ],
3990 − "small": [
3991 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
3992 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
3993 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
3994 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
3995 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
3996 − "a.text-[14.5px].font-semibold.text-ink 170×21 \"Countries by AI faciliti\"",
3997 − "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
3998 − "a.text-[14.5px].font-semibold.text-ink 188×21 \"Countries by cloud regio\"",
3999 − "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
4000 − "a.text-[14.5px].font-semibold.text-ink 336×21 \"Countries by facilities \"",
4001 − "a.text-ink-2.hover:text-accent 78×17 \"Full ranking →\"",
4002 − "a.text-[14.5px].font-semibold.text-ink 184×21 \"Countries by facility co\""
4003 − ],
4004 − "smallCount": 60,
4005 − "h1Covered": false,
4006 − "h1Top": 90,
4007 − "headerBottom": 49,
4008 − "lang": "en",
4009 − "metaDesc": 190,
4010 − "h1Count": 1,
4011 − "skipped": [],
4012 − "unnamed": [],
4013 − "ariaCurrent": 2,
4014 − "footerOk": true,
4015 − "contrast": [
4016 − {
4017 − "ratio": 3.07,
4018 − "color": "rgb(91, 97, 104)",
4019 − "bg": "rgb(11,15,20)",
4020 − "sample": "span.figure.text-[11px].text-ink-4 \"11\"",
4021 − "n": 1
4022 − },
4023 − {
4024 − "ratio": 3.07,
4025 − "color": "rgb(91, 97, 104)",
4026 − "bg": "rgb(11,15,20)",
4027 − "sample": "span.text-ink-4 \"No rows computed yet\"",
4028 − "n": 3
4029 − }
4030 − ],
4031 − "contrastChecked": 173,
4032 − "height": 4882,
4033 − "title": "Rankings · DataCenterIndex",
4034 − "bottom": {
4035 − "navVisible": true,
4036 − "covered": false,
4037 − "gap": 809
4038 − },
4039 − "errors": [
4040 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4041 − ],
4042 − "badResponses": [
4043 − "404 fetch /api/v1/docs?_rsc=8uisVwxVtwU7bmA7"
4044 − ]
4045 − },
4046 − {
4047 − "name": "ranking",
4048 − "path": "/rankings/countries_facilities",
4049 − "w": 390,
4050 − "theme": "dark",
4051 − "status": 200,
4052 − "ttfb": 11,
4053 − "overflow": null,
4054 − "clipped": [
4055 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4056 − "span.sr-only \"United States\" 83>1",
4057 − "span.sr-only \"France\" 43>1",
4058 − "span.sr-only \"United Kingdom\" 100>1",
4059 − "span.sr-only \"Germany\" 57>1",
4060 − "span.sr-only \"Australia\" 55>1",
4061 − "span.sr-only \"Netherlands\" 76>1",
4062 − "span.sr-only \"India\" 30>1",
4063 − "span.sr-only \"Singapore\" 63>1"
4064 − ],
4065 − "small": [
4066 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4067 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4068 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4069 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4070 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4071 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
4072 − "a.hover:text-ink.hover:underline 52×17 \"Rankings\"",
4073 − "a.font-medium.text-ink.hover:text-accent 83×18 \"United States\"",
4074 − "a.font-medium.text-ink.hover:text-accent 43×18 \"France\"",
4075 − "a.font-medium.text-ink.hover:text-accent 100×18 \"United Kingdom\"",
4076 − "a.font-medium.text-ink.hover:text-accent 57×18 \"Germany\"",
4077 − "a.font-medium.text-ink.hover:text-accent 55×18 \"Australia\""
4078 − ],
4079 − "smallCount": 32,
4080 − "h1Covered": false,
4081 − "h1Top": 120,
4082 − "headerBottom": 49,
4083 − "lang": "en",
4084 − "metaDesc": 161,
4085 − "h1Count": 1,
4086 − "skipped": [
4087 − "h1→h3"
4088 − ],
4089 − "unnamed": [],
4090 − "ariaCurrent": 2,
4091 − "footerOk": true,
4092 − "contrast": [],
4093 − "contrastChecked": 95,
4094 − "height": 2378,
4095 − "title": "Countries by facility count · DataCenterIndex",
4096 − "bottom": {
4097 − "navVisible": true,
4098 − "covered": false,
4099 − "gap": 809
4100 − },
4101 − "errors": [
4102 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4103 − ],
4104 − "badResponses": [
4105 − "404 fetch /api/v1/docs?_rsc=8sIz69C0ujSVTYSL"
4106 − ]
4107 − },
4108 − {
4109 − "name": "cloud-regions",
4110 − "path": "/cloud-regions",
4111 − "w": 390,
4112 − "theme": "dark",
4113 − "status": 200,
4114 − "ttfb": 26,
4115 − "overflow": null,
4116 − "clipped": [
4117 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4118 − "label.sr-only \"Provider\" 55>1",
4119 − "label.sr-only \"Country\" 53>1",
4120 − "label.sr-only \"Status\" 42>1"
4121 − ],
4122 − "small": [
4123 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4124 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4125 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4126 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4127 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4128 − "a.inline-flex.items-center.justify-center 106×32 \"Show on map\"",
4129 − "select#cr-provider.h-9.rounded-sm.border 179×36 \"All providersAmazon Web \"",
4130 − "select#cr-country.h-9.rounded-sm.border 175×36 \"All countriesArgentinaAu\"",
4131 − "select#cr-status.h-9.rounded-sm.border 109×36 \"Any statusOperationalAnn\"",
4132 − "button.inline-flex.items-center.justify-center 58×32 \"Apply\"",
4133 − "a.ml-auto.text-[12.5px].text-ink-3 106×18 \"Flat sortable table\"",
4134 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\""
4135 − ],
4136 − "smallCount": 26,
4137 − "h1Covered": false,
4138 − "h1Top": 90,
4139 − "headerBottom": 49,
4140 − "lang": "en",
4141 − "metaDesc": 155,
4142 − "h1Count": 1,
4143 − "skipped": [],
4144 − "unnamed": [],
4145 − "ariaCurrent": 0,
4146 − "footerOk": true,
4147 − "contrast": [
4148 − {
4149 − "ratio": 3.07,
4150 − "color": "rgb(91, 97, 104)",
4151 − "bg": "rgb(11,15,20)",
4152 − "sample": "span.figure.text-[11px].text-ink-4 \"79\"",
4153 − "n": 7
4154 − }
4155 − ],
4156 − "contrastChecked": 443,
4157 − "height": 8052,
4158 − "title": "Cloud regions · DataCenterIndex",
4159 − "bottom": {
4160 − "navVisible": true,
4161 − "covered": false,
4162 − "gap": 810
4163 − },
4164 − "errors": [
4165 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4166 − ],
4167 − "badResponses": [
4168 − "404 fetch /api/v1/docs?_rsc=z0v65R_nxS1w6k12"
4169 − ]
4170 − },
4171 − {
4172 − "name": "cloud-region",
4173 − "path": "/cloud-regions/amazon-web-services-ca-central-1",
4174 − "w": 390,
4175 − "theme": "dark",
4176 − "status": 200,
4177 − "ttfb": 5,
4178 − "overflow": null,
4179 − "clipped": [
4180 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4181 − "span.sr-only \"Canada\" 47>1"
4182 − ],
4183 − "small": [
4184 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4185 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4186 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4187 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4188 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4189 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
4190 − "a.hover:text-ink.hover:underline 79×17 \"Cloud regions\"",
4191 − "a.hover:text-ink.hover:underline 128×17 \"Amazon Web Services\"",
4192 − "a.hover:text-ink.hover:underline 47×19 \"Canada\"",
4193 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
4194 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
4195 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\""
4196 − ],
4197 − "smallCount": 24,
4198 − "h1Covered": false,
4199 − "h1Top": 120,
4200 − "headerBottom": 49,
4201 − "lang": "en",
4202 − "metaDesc": 165,
4203 − "h1Count": 1,
4204 − "skipped": [],
4205 − "unnamed": [],
4206 − "ariaCurrent": 0,
4207 − "footerOk": true,
4208 − "contrast": [
4209 − {
4210 − "ratio": 3.07,
4211 − "color": "rgb(91, 97, 104)",
4212 − "bg": "rgb(11,15,20)",
4213 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Region facts are taken verbati\"",
4214 − "n": 1
4215 − },
4216 − {
4217 − "ratio": 3.07,
4218 − "color": "rgb(91, 97, 104)",
4219 − "bg": "rgb(11,15,20)",
4220 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
4221 − "n": 1
4222 − }
4223 − ],
4224 − "contrastChecked": 86,
4225 − "height": 2891,
4226 − "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
4227 − "bottom": {
4228 − "navVisible": true,
4229 − "covered": false,
4230 − "gap": 1073
4231 − },
4232 − "errors": [
4233 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4234 − ],
4235 − "badResponses": [
4236 − "404 fetch /api/v1/docs?_rsc=5uclAh_QepNQCyIY"
4237 − ]
4238 − },
4239 − {
4240 − "name": "ixps",
4241 − "path": "/ixps",
4242 − "w": 390,
4243 − "theme": "dark",
4244 − "status": 200,
4245 − "ttfb": 5,
4246 − "overflow": null,
4247 − "clipped": [
4248 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4249 − "label.sr-only \"Search\" 46>1",
4250 − "label.sr-only \"Country\" 53>1"
4251 − ],
4252 − "small": [
4253 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4254 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4255 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4256 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4257 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4258 − "a.inline-flex.items-center.justify-center 101×32 \"Data centers\"",
4259 − "input#ixp-q.h-9.rounded-sm.border 366×36 \"\"",
4260 − "select#ixp-country.h-9.rounded-sm.border 234×36 \"All countriesAlbaniaArge\"",
4261 − "button.inline-flex.items-center.justify-center 58×32 \"Apply\"",
4262 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
4263 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
4264 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\""
4265 − ],
4266 − "smallCount": 24,
4267 − "h1Covered": false,
4268 − "h1Top": 90,
4269 − "headerBottom": 49,
4270 − "lang": "en",
4271 − "metaDesc": 100,
4272 − "h1Count": 1,
4273 − "skipped": [],
4274 − "unnamed": [],
4275 − "ariaCurrent": 1,
4276 − "footerOk": true,
4277 − "contrast": [
4278 − {
4279 − "ratio": 3.07,
4280 − "color": "rgb(91, 97, 104)",
4281 − "bg": "rgb(11,15,20)",
4282 − "sample": "p.mt-8.text-[12px].text-ink-4 \"Exchange and network data: Pee\"",
4283 − "n": 1
4284 − }
4285 − ],
4286 − "contrastChecked": 60,
4287 − "height": 2057,
4288 − "title": "Internet exchanges · DataCenterIndex",
4289 − "bottom": {
4290 − "navVisible": true,
4291 − "covered": false,
4292 − "gap": 810
4293 − },
4294 − "errors": [
4295 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4296 − ],
4297 − "badResponses": [
4298 − "404 fetch /api/v1/docs?_rsc=LOh4kTrez0j5S-5D"
4299 − ]
4300 − },
4301 − {
4302 − "name": "sources",
4303 − "path": "/sources",
4304 − "w": 390,
4305 − "theme": "dark",
4306 − "status": 200,
4307 − "ttfb": 19,
4308 − "overflow": null,
4309 − "clipped": [
4310 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4311 − ],
4312 − "small": [
4313 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4314 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4315 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4316 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4317 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4318 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
4319 − "input.h-9.min-w-0.rounded-sm 157×36 \"\"",
4320 − "select.h-9.rounded-sm.border 129×36 \"All kindsOperatorGovernm\"",
4321 − "button.inline-flex.items-center.justify-center 64×36 \"Apply\"",
4322 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
4323 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
4324 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\""
4325 − ],
4326 − "smallCount": 24,
4327 − "h1Covered": false,
4328 − "h1Top": 112,
4329 − "headerBottom": 49,
4330 − "lang": "en",
4331 − "metaDesc": 162,
4332 − "h1Count": 1,
4333 − "skipped": [
4334 − "h1→h3"
4335 − ],
4336 − "unnamed": [],
4337 − "ariaCurrent": 0,
4338 − "footerOk": true,
4339 − "contrast": [
4340 − {
4341 − "ratio": 3.07,
4342 − "color": "rgb(91, 97, 104)",
4343 − "bg": "rgb(11,15,20)",
4344 − "sample": "span.figure.text-[11px].text-ink-4 \"60\"",
4345 − "n": 3
4346 − },
4347 − {
4348 − "ratio": 3.07,
4349 − "color": "rgb(91, 97, 104)",
4350 − "bg": "rgb(11,15,20)",
4351 − "sample": "p.mt-6.text-[12px].text-ink-4 \"Kinds rank source authority wh\"",
4352 − "n": 1
4353 − },
4354 − {
4355 − "ratio": 3.07,
4356 − "color": "rgb(91, 97, 104)",
4357 − "bg": "rgb(11,15,20)",
4358 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"reconciliation rules\"",
4359 − "n": 2
4360 − }
4361 − ],
4362 − "contrastChecked": 238,
4363 − "height": 6001,
4364 − "title": "Sources · DataCenterIndex",
4365 − "bottom": {
4366 − "navVisible": true,
4367 − "covered": false,
4368 − "gap": 810
4369 − },
4370 − "errors": [
4371 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4372 − ],
4373 − "badResponses": [
4374 − "404 fetch /api/v1/docs?_rsc=9SQ0ZVjwLU4sIjhN"
4375 − ]
4376 − },
4377 − {
4378 − "name": "methodology",
4379 − "path": "/methodology",
4380 − "w": 390,
4381 − "theme": "dark",
4382 − "status": 200,
4383 − "ttfb": 3,
4384 − "overflow": null,
4385 − "clipped": [
4386 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4387 − ],
4388 − "small": [
4389 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4390 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4391 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4392 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4393 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4394 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
4395 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
4396 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
4397 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
4398 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
4399 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\"",
4400 − "a.inline-flex.min-h-7.items-center 60×28 \"Countries\""
4401 − ],
4402 − "smallCount": 21,
4403 − "h1Covered": false,
4404 − "h1Top": 112,
4405 − "headerBottom": 49,
4406 − "lang": "en",
4407 − "metaDesc": 201,
4408 − "h1Count": 1,
4409 − "skipped": [],
4410 − "unnamed": [],
4411 − "ariaCurrent": 0,
4412 − "footerOk": true,
4413 − "contrast": [
4414 − {
4415 − "ratio": 3.07,
4416 − "color": "rgb(91, 97, 104)",
4417 − "bg": "rgb(11,15,20)",
4418 − "sample": "div.figure.text-[11px].text-ink-4 \"countries_facilities\"",
4419 − "n": 20
4420 − }
4421 − ],
4422 − "contrastChecked": 234,
4423 − "height": 12570,
4424 − "title": "Methodology · DataCenterIndex",
4425 − "bottom": {
4426 − "navVisible": true,
4427 − "covered": false,
4428 − "gap": 809
4429 − },
4430 − "errors": [
4431 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4432 − ],
4433 − "badResponses": [
4434 − "404 fetch /api/v1/docs?_rsc=HzWsK4yFUoZLFyoa"
4435 − ]
4436 − },
4437 − {
4438 − "name": "about",
4439 − "path": "/about",
4440 − "w": 390,
4441 − "theme": "dark",
4442 − "status": 200,
4443 − "ttfb": 1,
4444 − "overflow": null,
4445 − "clipped": [
4446 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4447 − ],
4448 − "small": [
4449 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4450 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4451 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4452 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4453 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4454 − "a.font-medium.text-ink.hover:text-accent 366×24 \"PeeringDB\"",
4455 − "a.font-medium.text-ink.hover:text-accent 366×24 \"OpenStreetMap\"",
4456 − "a.font-medium.text-ink.hover:text-accent 366×24 \"Wikidata\"",
4457 − "a.font-medium.text-ink.hover:text-accent 366×24 \"World Bank\"",
4458 − "a.font-medium.text-ink.hover:text-accent 366×24 \"Operator and government \"",
4459 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
4460 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\""
4461 − ],
4462 − "smallCount": 25,
4463 − "h1Covered": false,
4464 − "h1Top": 90,
4465 − "headerBottom": 49,
4466 − "lang": "en",
4467 − "metaDesc": 85,
4468 − "h1Count": 1,
4469 − "skipped": [],
4470 − "unnamed": [],
4471 − "ariaCurrent": 0,
4472 − "footerOk": true,
4473 − "contrast": [],
4474 − "contrastChecked": 63,
4475 − "height": 3140,
4476 − "title": "About · DataCenterIndex",
4477 − "bottom": {
4478 − "navVisible": true,
4479 − "covered": false,
4480 − "gap": 809
4481 − },
4482 − "errors": [
4483 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4484 − ],
4485 − "badResponses": [
4486 − "404 fetch /api/v1/docs?_rsc=Sdev7PUlRw12F58X"
4487 − ]
4488 − },
4489 − {
4490 − "name": "bot",
4491 − "path": "/bot",
4492 − "w": 390,
4493 − "theme": "dark",
4494 − "status": 200,
4495 − "ttfb": 1,
4496 − "overflow": null,
4497 − "clipped": [
4498 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4499 − ],
4500 − "small": [
4501 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4502 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4503 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4504 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4505 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4506 − "a.hover:text-ink.hover:underline 34×17 \"Home\"",
4507 − "a.inline-flex.items-center.gap-2 160×20 \"DataCenterIndex — home\"",
4508 − "a.inline-flex.min-h-7.items-center 27×28 \"Map\"",
4509 − "a.inline-flex.min-h-7.items-center 78×28 \"Data centers\"",
4510 − "a.inline-flex.min-h-7.items-center 61×28 \"Operators\"",
4511 − "a.inline-flex.min-h-7.items-center 50×28 \"Markets\"",
4512 − "a.inline-flex.min-h-7.items-center 60×28 \"Countries\""
4513 − ],
4514 − "smallCount": 21,
4515 − "h1Covered": false,
4516 − "h1Top": 112,
4517 − "headerBottom": 49,
4518 − "lang": "en",
4519 − "metaDesc": 179,
4520 − "h1Count": 1,
4521 − "skipped": [],
4522 − "unnamed": [],
4523 − "ariaCurrent": 0,
4524 − "footerOk": true,
4525 − "contrast": [],
4526 − "contrastChecked": 80,
4527 − "height": 4183,
4528 − "title": "About our crawler · DataCenterIndex",
4529 − "bottom": {
4530 − "navVisible": true,
4531 − "covered": false,
4532 − "gap": 810
4533 − },
4534 − "errors": [
4535 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4536 − ],
4537 − "badResponses": [
4538 − "404 fetch /api/v1/docs?_rsc=8qIO8lIDYC9gUgf-"
4539 − ]
4540 − },
4541 − {
4542 − "name": "404",
4543 − "path": "/this-page-does-not-exist",
4544 − "w": 390,
4545 − "theme": "dark",
4546 − "status": 404,
4547 − "ttfb": 1,
4548 − "overflow": null,
4549 − "clipped": [
4550 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4551 − ],
4552 − "small": [
4553 − "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
4554 − "a.inline-flex.items-center.gap-2 162×22 \"DataCenterIndex — home\"",
4555 − "button.inline-flex.shrink-0.items-center 36×36 \"Search\"",
4556 − "button.inline-flex.shrink-0.items-center 36×36 \"Switch to light theme\"",
4557 − "button.inline-flex.shrink-0.items-center 36×36 \"Menu\"",
4558 − "a.inline-flex.items-center.justify-center 134×36 \"Search the index\"",
4559 − "a.inline-flex.items-center.justify-center 117×36 \"Open the map\"",
4560 − "a.inline-flex.h-9.items-center 52×36 \"Home\""
4561 − ],
4562 − "smallCount": 8,
4563 − "h1Covered": false,
4564 − "h1Top": 231,
4565 − "headerBottom": 49,
4566 − "lang": "en",
4567 − "metaDesc": 159,
4568 − "h1Count": 1,
4569 − "skipped": [],
4570 − "unnamed": [],
4571 − "ariaCurrent": 0,
4572 − "footerOk": false,
4573 − "contrast": [],
4574 − "contrastChecked": 7,
4575 − "height": 844,
4576 − "title": "DataCenterIndex — The global index of data center infrastructure",
4577 − "bottom": {
4578 − "navVisible": true,
4579 − "covered": false
4580 − },
4581 − "errors": [
4582 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4583 − ],
4584 − "badResponses": []
4585 − },
4586 − {
4587 − "name": "home",
4588 − "path": "/",
4589 − "w": 1280,
4590 − "theme": "dark",
4591 − "status": 200,
4592 − "ttfb": 3,
4593 − "overflow": null,
4594 − "clipped": [
4595 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4596 − "span.sr-only \"United States\" 89>1",
4597 − "span.sr-only \"United States\" 89>1",
4598 − "span.sr-only \"United States\" 89>1",
4599 − "span.sr-only \"United States\" 76>1",
4600 − "span.sr-only \"United States\" 76>1",
4601 − "span.sr-only \"United Kingdom\" 99>1",
4602 − "span.sr-only \"France\" 42>1",
4603 − "span.sr-only \"Singapore\" 62>1"
4604 − ],
4605 − "small": [],
4606 − "smallCount": 0,
4607 − "h1Covered": false,
4608 − "h1Top": 132,
4609 − "headerBottom": 57,
4610 − "lang": "en",
4611 − "metaDesc": 159,
4612 − "h1Count": 1,
4613 − "skipped": [],
4614 − "unnamed": [],
4615 − "ariaCurrent": 1,
4616 − "footerOk": true,
4617 − "contrast": [
4618 − {
4619 − "ratio": 3.07,
4620 − "color": "rgb(91, 97, 104)",
4621 − "bg": "rgb(11,15,20)",
4622 − "sample": "span.normal-case.tracking-normal.text-ink-4 \"· updated continuously\"",
4623 − "n": 1
4624 − },
4625 − {
4626 − "ratio": 3.07,
4627 − "color": "rgb(91, 97, 104)",
4628 − "bg": "rgb(11,15,20)",
4629 − "sample": "span.shrink-0.py-1.text-[11.5px] \"Try\"",
4630 − "n": 1
4631 − },
4632 − {
4633 − "ratio": 3.07,
4634 − "color": "rgb(91, 97, 104)",
4635 − "bg": "rgb(11,15,20)",
4636 − "sample": "span.figure.w-5.text-right \"10\"",
4637 − "n": 2
4638 − },
4639 − {
4640 − "ratio": 3.07,
4641 − "color": "rgb(91, 97, 104)",
4642 − "bg": "rgb(11,15,20)",
4643 − "sample": "span.figure.text-ink-4 \"30%\"",
4644 − "n": 2
4645 − }
4646 − ],
4647 − "contrastChecked": 335,
4648 − "height": 3750,
4649 − "title": "DataCenterIndex — The global index of data center infrastructure",
4650 − "errors": [],
4651 − "badResponses": []
4652 − },
4653 − {
4654 − "name": "map",
4655 − "path": "/map",
4656 − "w": 1280,
4657 − "theme": "dark",
4658 − "status": 200,
4659 − "ttfb": 1,
4660 − "overflow": null,
4661 − "clipped": [
4662 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4663 − "span.sr-only \"Minimum MW\" 91>1",
4664 − "span.sr-only \"Maximum MW\" 95>1",
4665 − "span.sr-only \"Opened from\" 87>1",
4666 − "span.sr-only \"Opened to\" 69>1"
4667 − ],
4668 − "small": [],
4669 − "smallCount": 0,
4670 − "h1Covered": false,
4671 − "h1Top": null,
4672 − "headerBottom": 57,
4673 − "lang": "en",
4674 − "metaDesc": 127,
4675 − "h1Count": 0,
4676 − "skipped": [],
4677 − "unnamed": [],
4678 − "ariaCurrent": 2,
4679 − "footerOk": false,
4680 − "contrast": [],
4681 − "contrastChecked": 14,
4682 − "height": 901,
4683 − "title": "Global data center map · DataCenterIndex",
4684 − "map": {
4685 − "controls": 3,
4686 − "unreachable": 0,
4687 − "canvas": true
4688 − },
4689 − "errors": [],
4690 − "badResponses": []
4691 − },
4692 − {
4693 − "name": "search",
4694 − "path": "/search?q=montreal",
4695 − "w": 1280,
4696 − "theme": "dark",
4697 − "status": 200,
4698 − "ttfb": 4,
4699 − "overflow": null,
4700 − "clipped": [
4701 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4702 − ],
4703 − "small": [],
4704 − "smallCount": 0,
4705 − "h1Covered": false,
4706 − "h1Top": 106,
4707 − "headerBottom": 57,
4708 − "lang": "en",
4709 − "metaDesc": 159,
4710 − "h1Count": 1,
4711 − "skipped": [],
4712 − "unnamed": [],
4713 − "ariaCurrent": 0,
4714 − "footerOk": true,
4715 − "contrast": [],
4716 − "contrastChecked": 55,
4717 − "height": 984,
4718 − "title": "“montreal” — search · DataCenterIndex",
4719 − "errors": [
4720 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
4721 − ],
4722 − "badResponses": [
4723 − "404 fetch /api/v1/docs?_rsc=6grVij_EUBxTWnLP"
4724 − ]
4725 − },
4726 − {
4727 − "name": "live",
4728 − "path": "/live",
4729 − "w": 1280,
4730 − "theme": "dark",
4731 − "status": 200,
4732 − "ttfb": 10,
4733 − "overflow": null,
4734 − "clipped": [
4735 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4736 − "span.sr-only \"United States\" 89>1",
4737 − "span.sr-only \"United States\" 89>1",
4738 − "span.sr-only \"United States\" 89>1",
4739 − "span.sr-only \"United States\" 89>1",
4740 − "span.sr-only \"United States\" 89>1",
4741 − "span.sr-only \"United States\" 89>1",
4742 − "span.sr-only \"United States\" 89>1",
4743 − "span.sr-only \"United States\" 89>1"
4744 − ],
4745 − "small": [],
4746 − "smallCount": 0,
4747 − "h1Covered": false,
4748 − "h1Top": 107,
4749 − "headerBottom": 57,
4750 − "lang": "en",
4751 − "metaDesc": 152,
4752 − "h1Count": 1,
4753 − "skipped": [
4754 − "h1→h3"
4755 − ],
4756 − "unnamed": [],
4757 − "ariaCurrent": 1,
4758 − "footerOk": true,
4759 − "contrast": [],
4760 − "contrastChecked": 519,
4761 − "height": 7789,
4762 − "title": "Live changes · DataCenterIndex",
4763 − "errors": [],
4764 − "badResponses": []
4765 − },
4766 − {
4767 − "name": "datacenters",
4768 − "path": "/datacenters",
4769 − "w": 1280,
4770 − "theme": "dark",
4771 − "status": 200,
4772 − "ttfb": 11,
4773 − "overflow": null,
4774 − "clipped": [
4775 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
4776 − ],
4777 − "small": [],
4778 − "smallCount": 0,
4779 − "h1Covered": false,
4780 − "h1Top": 106,
4781 − "headerBottom": 57,
4782 − "lang": "en",
4783 − "metaDesc": 119,
4784 − "h1Count": 1,
4785 − "skipped": [],
4786 − "unnamed": [],
4787 − "ariaCurrent": 2,
4788 − "footerOk": true,
4789 − "contrast": [],
4790 − "contrastChecked": 521,
4791 − "height": 2652,
4792 − "title": "Data centers · DataCenterIndex",
4793 − "errors": [],
4794 − "badResponses": []
4795 − },
4796 − {
4797 − "name": "fac-operator-mw",
4798 − "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
4799 − "w": 1280,
4800 − "theme": "dark",
4801 − "status": 200,
4802 − "ttfb": 10,
4803 − "overflow": null,
4804 − "clipped": [
4805 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4806 − "span.sr-only \"United States\" 86>1",
4807 − "span.sr-only \"United States\" 82>1",
4808 − "span.sr-only \"United States\" 89>1"
4809 − ],
4810 − "small": [],
4811 − "smallCount": 0,
4812 − "h1Covered": false,
4813 − "h1Top": 135,
4814 − "headerBottom": 57,
4815 − "lang": "en",
4816 − "metaDesc": 212,
4817 − "h1Count": 1,
4818 − "skipped": [],
4819 − "unnamed": [],
4820 − "ariaCurrent": 1,
4821 − "footerOk": true,
4822 − "contrast": [
4823 − {
4824 − "ratio": 3.07,
4825 − "color": "rgb(91, 97, 104)",
4826 − "bg": "rgb(11,15,20)",
4827 − "sample": "span.figure.text-[11px].text-ink-4 \"16\"",
4828 − "n": 2
4829 − },
4830 − {
4831 − "ratio": 3.07,
4832 − "color": "rgb(91, 97, 104)",
4833 − "bg": "rgb(11,15,20)",
4834 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"High confidence overall — comp\"",
4835 − "n": 1
4836 − },
4837 − {
4838 − "ratio": 3.07,
4839 − "color": "rgb(91, 97, 104)",
4840 − "bg": "rgb(11,15,20)",
4841 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
4842 − "n": 2
4843 − },
4844 − {
4845 − "ratio": 3.07,
4846 − "color": "rgb(91, 97, 104)",
4847 − "bg": "rgb(11,15,20)",
4848 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Operator\"",
4849 − "n": 1
4850 − },
4851 − {
4852 − "ratio": 3.07,
4853 − "color": "rgb(91, 97, 104)",
4854 − "bg": "rgb(11,15,20)",
4855 − "sample": "span.text-ink-4 \"· Publicly accessible operator\"",
4856 − "n": 1
4857 − },
4858 − {
4859 − "ratio": 3.07,
4860 − "color": "rgb(91, 97, 104)",
4861 − "bg": "rgb(11,15,20)",
4862 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
4863 − "n": 1
4864 − }
4865 − ],
4866 − "contrastChecked": 373,
4867 − "height": 4103,
4868 − "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
4869 − "errors": [],
4870 − "badResponses": []
4871 − },
4872 − {
4873 − "name": "fac-osm",
4874 − "path": "/datacenters/shelter-modernizacion",
4875 − "w": 1280,
4876 − "theme": "dark",
4877 − "status": 200,
4878 − "ttfb": 8,
4879 − "overflow": null,
4880 − "clipped": [
4881 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4882 − "span.sr-only \"Argentina\" 62>1",
4883 − "span.sr-only \"Argentina\" 59>1",
4884 − "span.sr-only \"Argentina\" 64>1"
4885 − ],
4886 − "small": [],
4887 − "smallCount": 0,
4888 − "h1Covered": false,
4889 − "h1Top": 132,
4890 − "headerBottom": 57,
4891 − "lang": "en",
4892 − "metaDesc": 128,
4893 − "h1Count": 1,
4894 − "skipped": [],
4895 − "unnamed": [],
4896 − "ariaCurrent": 1,
4897 − "footerOk": true,
4898 − "contrast": [
4899 − {
4900 − "ratio": 3.07,
4901 − "color": "rgb(91, 97, 104)",
4902 − "bg": "rgb(11,15,20)",
4903 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Unverified overall — computed \"",
4904 − "n": 1
4905 − },
4906 − {
4907 − "ratio": 3.07,
4908 − "color": "rgb(91, 97, 104)",
4909 − "bg": "rgb(11,15,20)",
4910 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
4911 − "n": 2
4912 − },
4913 − {
4914 − "ratio": 3.07,
4915 − "color": "rgb(91, 97, 104)",
4916 − "bg": "rgb(11,15,20)",
4917 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Community\"",
4918 − "n": 1
4919 − },
4920 − {
4921 − "ratio": 3.07,
4922 − "color": "rgb(91, 97, 104)",
4923 − "bg": "rgb(11,15,20)",
4924 − "sample": "span.text-ink-4 \"· ODbL 1.0 — © OpenStreetMap c\"",
4925 − "n": 1
4926 − },
4927 − {
4928 − "ratio": 3.07,
4929 − "color": "rgb(91, 97, 104)",
4930 − "bg": "rgb(11,15,20)",
4931 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
4932 − "n": 1
4933 − }
4934 − ],
4935 − "contrastChecked": 220,
4936 − "height": 3088,
4937 − "title": "Shelter Modernización · DataCenterIndex",
4938 − "errors": [],
4939 − "badResponses": []
4940 − },
4941 − {
4942 − "name": "fac-google",
4943 − "path": "/datacenters/google-the-dalles-oregon-data-center",
4944 − "w": 1280,
4945 − "theme": "dark",
4946 − "status": 200,
4947 − "ttfb": 14,
4948 − "overflow": null,
4949 − "clipped": [
4950 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
4951 − "span.sr-only \"United States\" 86>1",
4952 − "span.sr-only \"United States\" 82>1",
4953 − "span.sr-only \"United States\" 89>1",
4954 − "span.sr-only \"United States\" 89>1",
4955 − "span.sr-only \"United States\" 89>1",
4956 − "span.sr-only \"United States\" 89>1"
4957 − ],
4958 − "small": [],
4959 − "smallCount": 0,
4960 − "h1Covered": false,
4961 − "h1Top": 135,
4962 − "headerBottom": 57,
4963 − "lang": "en",
4964 − "metaDesc": 203,
4965 − "h1Count": 1,
4966 − "skipped": [],
4967 − "unnamed": [],
4968 − "ariaCurrent": 1,
4969 − "footerOk": true,
4970 − "contrast": [
4971 − {
4972 − "ratio": 3.07,
4973 − "color": "rgb(91, 97, 104)",
4974 − "bg": "rgb(11,15,20)",
4975 − "sample": "span.figure.text-[11px].text-ink-4 \"26\"",
4976 − "n": 2
4977 − },
4978 − {
4979 − "ratio": 3.07,
4980 − "color": "rgb(91, 97, 104)",
4981 − "bg": "rgb(11,15,20)",
4982 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"High confidence overall — comp\"",
4983 − "n": 1
4984 − },
4985 − {
4986 − "ratio": 3.07,
4987 − "color": "rgb(91, 97, 104)",
4988 − "bg": "rgb(11,15,20)",
4989 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
4990 − "n": 2
4991 − },
4992 − {
4993 − "ratio": 3.07,
4994 − "color": "rgb(91, 97, 104)",
4995 − "bg": "rgb(11,15,20)",
4996 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Cloud provider\"",
4997 − "n": 2
4998 − },
4999 − {
5000 − "ratio": 3.07,
5001 − "color": "rgb(91, 97, 104)",
5002 − "bg": "rgb(11,15,20)",
5003 − "sample": "span.text-ink-4 \"· Publicly published operator \"",
5004 − "n": 2
5005 − },
5006 − {
5007 − "ratio": 3.07,
5008 − "color": "rgb(91, 97, 104)",
5009 − "bg": "rgb(11,15,20)",
5010 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
5011 − "n": 1
5012 − }
5013 − ],
5014 − "contrastChecked": 629,
5015 − "height": 6882,
5016 − "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
5017 − "errors": [],
5018 − "badResponses": []
5019 − },
5020 − {
5021 − "name": "fac-wikidata",
5022 − "path": "/datacenters/foulum-data-center",
5023 − "w": 1280,
5024 − "theme": "dark",
5025 − "status": 200,
5026 − "ttfb": 9,
5027 − "overflow": null,
5028 − "clipped": [
5029 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5030 − "span.sr-only \"Denmark\" 58>1",
5031 − "span.sr-only \"Denmark\" 56>1",
5032 − "span.sr-only \"Denmark\" 60>1"
5033 − ],
5034 − "small": [],
5035 − "smallCount": 0,
5036 − "h1Covered": false,
5037 − "h1Top": 135,
5038 − "headerBottom": 57,
5039 − "lang": "en",
5040 − "metaDesc": 158,
5041 − "h1Count": 1,
5042 − "skipped": [],
5043 − "unnamed": [],
5044 − "ariaCurrent": 1,
5045 − "footerOk": true,
5046 − "contrast": [
5047 − {
5048 − "ratio": 3.07,
5049 − "color": "rgb(91, 97, 104)",
5050 − "bg": "rgb(11,15,20)",
5051 − "sample": "span.figure.text-[11px].text-ink-4 \"15\"",
5052 − "n": 2
5053 − },
5054 − {
5055 − "ratio": 3.07,
5056 − "color": "rgb(91, 97, 104)",
5057 − "bg": "rgb(11,15,20)",
5058 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Moderate confidence overall — \"",
5059 − "n": 1
5060 − },
5061 − {
5062 − "ratio": 3.07,
5063 − "color": "rgb(91, 97, 104)",
5064 − "bg": "rgb(11,15,20)",
5065 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"How confidence is computed\"",
5066 − "n": 2
5067 − },
5068 − {
5069 − "ratio": 3.07,
5070 − "color": "rgb(91, 97, 104)",
5071 − "bg": "rgb(11,15,20)",
5072 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Community\"",
5073 − "n": 2
5074 − },
5075 − {
5076 − "ratio": 3.07,
5077 − "color": "rgb(91, 97, 104)",
5078 − "bg": "rgb(11,15,20)",
5079 − "sample": "span.text-ink-4 \"· ODbL 1.0 — © OpenStreetMap c\"",
5080 − "n": 2
5081 − },
5082 − {
5083 − "ratio": 3.07,
5084 − "color": "rgb(91, 97, 104)",
5085 − "bg": "rgb(11,15,20)",
5086 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
5087 − "n": 1
5088 − }
5089 − ],
5090 − "contrastChecked": 350,
5091 − "height": 3868,
5092 − "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
5093 − "errors": [],
5094 − "badResponses": []
5095 − },
5096 − {
5097 − "name": "operators",
5098 − "path": "/operators",
5099 − "w": 1280,
5100 − "theme": "dark",
5101 − "status": 200,
5102 − "ttfb": 10,
5103 − "overflow": null,
5104 − "clipped": [
5105 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5106 − "span.sr-only \"Search operators\" 114>1"
5107 − ],
5108 − "small": [],
5109 − "smallCount": 0,
5110 − "h1Covered": false,
5111 − "h1Top": 106,
5112 − "headerBottom": 57,
5113 − "lang": "en",
5114 − "metaDesc": 139,
5115 − "h1Count": 1,
5116 − "skipped": [],
5117 − "unnamed": [],
5118 − "ariaCurrent": 2,
5119 − "footerOk": true,
5120 − "contrast": [],
5121 − "contrastChecked": 300,
5122 − "height": 2622,
5123 − "title": "Operators · DataCenterIndex",
5124 − "errors": [],
5125 − "badResponses": []
5126 − },
5127 − {
5128 − "name": "operator-equinix",
5129 − "path": "/operators/equinix",
5130 − "w": 1280,
5131 − "theme": "dark",
5132 − "status": 200,
5133 − "ttfb": 15,
5134 − "overflow": null,
5135 − "clipped": [
5136 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5137 − "span.sr-only \"Finland\" 45>1",
5138 − "span.sr-only \"Germany\" 56>1",
5139 − "span.sr-only \"Italy\" 26>1",
5140 − "span.sr-only \"Switzerland\" 72>1",
5141 − "span.sr-only \"United States\" 82>1",
5142 − "span.sr-only \"Australia\" 54>1",
5143 − "span.sr-only \"Ireland\" 42>1",
5144 − "span.sr-only \"Netherlands\" 75>1"
5145 − ],
5146 − "small": [],
5147 − "smallCount": 0,
5148 − "h1Covered": false,
5149 − "h1Top": 138,
5150 − "headerBottom": 57,
5151 − "lang": "en",
5152 − "metaDesc": 170,
5153 − "h1Count": 1,
5154 − "skipped": [],
5155 − "unnamed": [],
5156 − "ariaCurrent": 2,
5157 − "footerOk": true,
5158 − "contrast": [
5159 − {
5160 − "ratio": 3.07,
5161 − "color": "rgb(91, 97, 104)",
5162 − "bg": "rgb(11,15,20)",
5163 − "sample": "span.figure.text-[11px].text-ink-4 \"25\"",
5164 − "n": 5
5165 − },
5166 − {
5167 − "ratio": 3.07,
5168 − "color": "rgb(91, 97, 104)",
5169 − "bg": "rgb(11,15,20)",
5170 − "sample": "span.figure.w-5.text-right \"10\"",
5171 − "n": 1
5172 − },
5173 − {
5174 − "ratio": 3.07,
5175 − "color": "rgb(91, 97, 104)",
5176 − "bg": "rgb(11,15,20)",
5177 − "sample": "span.figure.text-ink-4 \"93%\"",
5178 − "n": 1
5179 − },
5180 − {
5181 − "ratio": 3.07,
5182 − "color": "rgb(91, 97, 104)",
5183 − "bg": "rgb(11,15,20)",
5184 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"Operator\"",
5185 − "n": 10
5186 − },
5187 − {
5188 − "ratio": 3.07,
5189 − "color": "rgb(91, 97, 104)",
5190 − "bg": "rgb(11,15,20)",
5191 − "sample": "span.text-ink-4 \"· Publicly accessible operator\"",
5192 − "n": 10
5193 − },
5194 − {
5195 − "ratio": 3.07,
5196 − "color": "rgb(91, 97, 104)",
5197 − "bg": "rgb(11,15,20)",
5198 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
5199 − "n": 1
5200 − },
5201 − {
5202 − "ratio": 3.07,
5203 − "color": "rgb(91, 97, 104)",
5204 − "bg": "rgb(11,15,20)",
5205 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5206 − "n": 1
5207 − }
5208 − ],
5209 − "contrastChecked": 693,
5210 − "height": 7121,
5211 − "title": "Equinix · DataCenterIndex",
5212 − "errors": [],
5213 − "badResponses": []
5214 − },
5215 − {
5216 − "name": "countries",
5217 − "path": "/countries",
5218 − "w": 1280,
5219 − "theme": "dark",
5220 − "status": 200,
5221 − "ttfb": 17,
5222 − "overflow": null,
5223 − "clipped": [
5224 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5225 − "span.sr-only \"United States\" 83>1",
5226 − "span.sr-only \"Brazil\" 35>1",
5227 − "span.sr-only \"Argentina\" 60>1",
5228 − "span.sr-only \"Canada\" 48>1",
5229 − "span.sr-only \"Mexico\" 45>1",
5230 − "span.sr-only \"Chile\" 32>1",
5231 − "span.sr-only \"Venezuela\" 65>1",
5232 − "span.sr-only \"Peru\" 29>1"
5233 − ],
5234 − "small": [],
5235 − "smallCount": 0,
5236 − "h1Covered": false,
5237 − "h1Top": 106,
5238 − "headerBottom": 57,
5239 − "lang": "en",
5240 − "metaDesc": 117,
5241 − "h1Count": 1,
5242 − "skipped": [],
5243 − "unnamed": [],
5244 − "ariaCurrent": 2,
5245 − "footerOk": true,
5246 − "contrast": [
5247 − {
5248 − "ratio": 3.07,
5249 − "color": "rgb(91, 97, 104)",
5250 − "bg": "rgb(11,15,20)",
5251 − "sample": "span.hidden.text-[10.5px].text-ink-4 \"30% coverage\"",
5252 − "n": 28
5253 − },
5254 − {
5255 − "ratio": 3.07,
5256 − "color": "rgb(91, 97, 104)",
5257 − "bg": "rgb(11,15,20)",
5258 − "sample": "p.mt-6.text-[11.5px].text-ink-4 \"Per-million uses the latest Wo\"",
5259 − "n": 1
5260 − },
5261 − {
5262 − "ratio": 3.07,
5263 − "color": "rgb(91, 97, 104)",
5264 − "bg": "rgb(11,15,20)",
5265 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"methodology\"",
5266 − "n": 1
5267 − }
5268 − ],
5269 − "contrastChecked": 612,
5270 − "height": 5414,
5271 − "title": "Countries · DataCenterIndex",
5272 − "errors": [],
5273 − "badResponses": []
5274 − },
5275 − {
5276 − "name": "country-us",
5277 − "path": "/countries/united-states",
5278 − "w": 1280,
5279 − "theme": "dark",
5280 − "status": 200,
5281 − "ttfb": 23,
5282 − "overflow": null,
5283 − "clipped": [
5284 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5285 − "span.sr-only \"United States\" 187>1",
5286 − "span.sr-only \"United States\" 76>1",
5287 − "span.sr-only \"United States\" 76>1",
5288 − "span.sr-only \"United States\" 76>1",
5289 − "span.sr-only \"United States\" 76>1",
5290 − "span.sr-only \"United States\" 76>1",
5291 − "span.sr-only \"United States\" 76>1",
5292 − "span.sr-only \"United States\" 76>1"
5293 − ],
5294 − "small": [],
5295 − "smallCount": 0,
5296 − "h1Covered": false,
5297 − "h1Top": 136,
5298 − "headerBottom": 57,
5299 − "lang": "en",
5300 − "metaDesc": 258,
5301 − "h1Count": 1,
5302 − "skipped": [],
5303 − "unnamed": [],
5304 − "ariaCurrent": 2,
5305 − "footerOk": true,
5306 − "contrast": [
5307 − {
5308 − "ratio": 2.91,
5309 − "color": "rgb(91, 97, 104)",
5310 − "bg": "rgb(16,22,29)",
5311 − "sample": "span.text-ink-4 \"/46\"",
5312 − "n": 5
5313 − },
5314 − {
5315 − "ratio": 3.07,
5316 − "color": "rgb(91, 97, 104)",
5317 − "bg": "rgb(11,15,20)",
5318 − "sample": "span.text-[11px].text-ink-4 \"World Bank\"",
5319 − "n": 1
5320 − },
5321 − {
5322 − "ratio": 3.07,
5323 − "color": "rgb(91, 97, 104)",
5324 − "bg": "rgb(11,15,20)",
5325 − "sample": "span.figure.text-[11px].text-ink-4 \"460\"",
5326 − "n": 9
5327 − },
5328 − {
5329 − "ratio": 3.07,
5330 − "color": "rgb(91, 97, 104)",
5331 − "bg": "rgb(11,15,20)",
5332 − "sample": "span.figure.text-ink-4 \"90%\"",
5333 − "n": 1
5334 − },
5335 − {
5336 − "ratio": 3.07,
5337 − "color": "rgb(91, 97, 104)",
5338 − "bg": "rgb(11,15,20)",
5339 − "sample": "span.figure.w-5.text-right \"10\"",
5340 − "n": 1
5341 − },
5342 − {
5343 − "ratio": 3.07,
5344 − "color": "rgb(91, 97, 104)",
5345 − "bg": "rgb(11,15,20)",
5346 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Source: World Bank, World Deve\"",
5347 − "n": 1
5348 − },
5349 − {
5350 − "ratio": 3.07,
5351 − "color": "rgb(91, 97, 104)",
5352 − "bg": "rgb(11,15,20)",
5353 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Country totals are lower bound\"",
5354 − "n": 1
5355 − },
5356 − {
5357 − "ratio": 3.07,
5358 − "color": "rgb(91, 97, 104)",
5359 − "bg": "rgb(11,15,20)",
5360 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5361 − "n": 1
5362 − }
5363 − ],
5364 − "contrastChecked": 1218,
5365 − "height": 11218,
5366 − "title": "Data Centers in United States · DataCenterIndex",
5367 − "errors": [],
5368 − "badResponses": []
5369 − },
5370 − {
5371 − "name": "country-ca",
5372 − "path": "/countries/canada",
5373 − "w": 1280,
5374 − "theme": "dark",
5375 − "status": 200,
5376 − "ttfb": 12,
5377 − "overflow": null,
5378 − "clipped": [
5379 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5380 − "span.sr-only \"Canada\" 107>1",
5381 − "span.sr-only \"Canada\" 43>1",
5382 − "span.sr-only \"Canada\" 43>1",
5383 − "span.sr-only \"Canada\" 51>1",
5384 − "span.sr-only \"Canada\" 51>1",
5385 − "span.sr-only \"Canada\" 51>1",
5386 − "span.sr-only \"Canada\" 51>1",
5387 − "span.sr-only \"Canada\" 51>1"
5388 − ],
5389 − "small": [],
5390 − "smallCount": 0,
5391 − "h1Covered": false,
5392 − "h1Top": 136,
5393 − "headerBottom": 57,
5394 − "lang": "en",
5395 − "metaDesc": 186,
5396 − "h1Count": 1,
5397 − "skipped": [],
5398 − "unnamed": [],
5399 − "ariaCurrent": 2,
5400 − "footerOk": true,
5401 − "contrast": [
5402 − {
5403 − "ratio": 2.91,
5404 − "color": "rgb(91, 97, 104)",
5405 − "bg": "rgb(16,22,29)",
5406 − "sample": "span.text-ink-4 \"/46\"",
5407 − "n": 4
5408 − },
5409 − {
5410 − "ratio": 3.07,
5411 − "color": "rgb(91, 97, 104)",
5412 − "bg": "rgb(11,15,20)",
5413 − "sample": "span.text-[11px].text-ink-4 \"World Bank\"",
5414 − "n": 1
5415 − },
5416 − {
5417 − "ratio": 3.07,
5418 − "color": "rgb(91, 97, 104)",
5419 − "bg": "rgb(11,15,20)",
5420 − "sample": "span.figure.text-[11px].text-ink-4 \"21\"",
5421 − "n": 5
5422 − },
5423 − {
5424 − "ratio": 3.07,
5425 − "color": "rgb(91, 97, 104)",
5426 − "bg": "rgb(11,15,20)",
5427 − "sample": "span.figure.text-ink-4 \"76%\"",
5428 − "n": 2
5429 − },
5430 − {
5431 − "ratio": 3.07,
5432 − "color": "rgb(91, 97, 104)",
5433 − "bg": "rgb(11,15,20)",
5434 − "sample": "p.mt-2.text-[11.5px].text-ink-4 \"Source: World Bank, World Deve\"",
5435 − "n": 1
5436 − },
5437 − {
5438 − "ratio": 3.07,
5439 − "color": "rgb(91, 97, 104)",
5440 − "bg": "rgb(11,15,20)",
5441 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Country totals are lower bound\"",
5442 − "n": 1
5443 − },
5444 − {
5445 − "ratio": 3.07,
5446 − "color": "rgb(91, 97, 104)",
5447 − "bg": "rgb(11,15,20)",
5448 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5449 − "n": 1
5450 − }
5451 − ],
5452 − "contrastChecked": 471,
5453 − "height": 5842,
5454 − "title": "Data Centers in Canada · DataCenterIndex",
5455 − "errors": [],
5456 − "badResponses": []
5457 − },
5458 − {
5459 − "name": "markets",
5460 − "path": "/datacenters/markets",
5461 − "w": 1280,
5462 − "theme": "dark",
5463 − "status": 200,
5464 − "ttfb": 13,
5465 − "overflow": null,
5466 − "clipped": [
5467 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
5468 − ],
5469 − "small": [],
5470 − "smallCount": 0,
5471 − "h1Covered": false,
5472 − "h1Top": 136,
5473 − "headerBottom": 57,
1000 + "h1Top": 119,
1001 + "headerBottom": 49,
5474 1002 "lang": "en",
5475 − "metaDesc": 125,
1003 + "metaDesc": 216,
5476 1004 "h1Count": 1,
5477 1005 "skipped": [],
5478 1006 "unnamed": [],
5479 − "ariaCurrent": 2,
1007 + "ariaCurrent": 1,
5480 1008 "footerOk": true,
5481 − "contrast": [
5482 − {
5483 − "ratio": 3.07,
5484 − "color": "rgb(91, 97, 104)",
5485 − "bg": "rgb(11,15,20)",
5486 − "sample": "p.mt-6.text-[11.5px].text-ink-4 \"Facilities are assigned to a m\"",
5487 − "n": 1
5488 − },
5489 − {
5490 − "ratio": 3.07,
5491 − "color": "rgb(91, 97, 104)",
5492 − "bg": "rgb(11,15,20)",
5493 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5494 − "n": 1
5495 − }
5496 − ],
5497 − "contrastChecked": 374,
5498 − "height": 2693,
5499 − "title": "Data center markets · DataCenterIndex",
1009 + "contrast": [],
1010 + "contrastChecked": 546,
1011 + "height": 11047,
1012 + "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
1013 + "bottom": {
1014 + "navVisible": true,
1015 + "covered": false,
1016 + "gap": 1881
1017 + },
5500 1018 "errors": [],
5501 1019 "badResponses": []
5502 1020 },
5503 1021 {
5504 − "name": "market-nova",
5505 − "path": "/datacenters/markets/northern-virginia",
5506 − "w": 1280,
1022 + "name": "fac-wikidata",
1023 + "path": "/datacenters/foulum-data-center",
1024 + "w": 390,
5507 1025 "theme": "dark",
5508 1026 "status": 200,
5509 − "ttfb": 14,
1027 + "ttfb": 119,
5510 1028 "overflow": null,
5511 1029 "clipped": [
5512 1030 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5513 − "span.sr-only \"United States\" 82>1",
5514 − "span.sr-only \"United States\" 76>1",
5515 − "span.sr-only \"United States\" 89>1",
5516 − "span.sr-only \"United States\" 89>1",
5517 − "span.sr-only \"United States\" 89>1",
5518 − "span.sr-only \"United States\" 89>1",
5519 − "span.sr-only \"United States\" 89>1",
5520 − "span.sr-only \"United States\" 89>1"
1031 + "span.sr-only \"Denmark\" 58>1",
1032 + "span.sr-only \"Denmark\" 56>1",
1033 + "span.sr-only \"Denmark\" 60>1"
5521 1034 ],
5522 − "small": [],
5523 − "smallCount": 0,
1035 + "small": [
1036 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1037 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1038 + "a.hover:text-ink.hover:underline 72×17 \"Data centers\"",
1039 + "a.hover:text-ink.hover:underline 51×17 \"Denmark\"",
1040 + "a.normal-case.tracking-normal.text-[12.5px] 35×18 \"Apple\"",
1041 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
1042 + "a.hover:text-accent.hover:underline 58×20 \"Denmark\"",
1043 + "button.group/ev.rounded-xs.text-left 83×22 \"Evidence for Status\"",
1044 + "button.group/ev.rounded-xs.text-left 168×22 \"Evidence for Confidence\"",
1045 + "button.group/ev.rounded-xs.text-left 108×22 \"Evidence for Geo precisi\"",
1046 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (2 sources)\"",
1047 + "button.inline-flex.items-center.rounded-xs 12×12 \"Provenance (1 source)\""
1048 + ],
1049 + "smallCount": 45,
5524 1050 "h1Covered": false,
5525 − "h1Top": 136,
5526 − "headerBottom": 57,
1051 + "h1Top": 119,
1052 + "headerBottom": 49,
5527 1053 "lang": "en",
5528 − "metaDesc": 250,
1054 + "metaDesc": 171,
5529 1055 "h1Count": 1,
5530 1056 "skipped": [],
5531 1057 "unnamed": [],
5532 − "ariaCurrent": 2,
1058 + "ariaCurrent": 1,
5533 1059 "footerOk": true,
5534 − "contrast": [
5535 − {
5536 − "ratio": 2.91,
5537 − "color": "rgb(91, 97, 104)",
5538 − "bg": "rgb(16,22,29)",
5539 − "sample": "span.figure.text-ink-4 \"/ 152\"",
5540 − "n": 4
5541 − },
5542 − {
5543 − "ratio": 3.07,
5544 − "color": "rgb(91, 97, 104)",
5545 − "bg": "rgb(11,15,20)",
5546 − "sample": "span.figure.text-[11px].text-ink-4 \"10\"",
5547 − "n": 3
5548 − },
5549 − {
5550 − "ratio": 3.07,
5551 − "color": "rgb(91, 97, 104)",
5552 − "bg": "rgb(11,15,20)",
5553 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
5554 − "n": 1
5555 − },
5556 − {
5557 − "ratio": 3.07,
5558 − "color": "rgb(91, 97, 104)",
5559 − "bg": "rgb(11,15,20)",
5560 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5561 − "n": 1
5562 − }
1060 + "contrast": [],
1061 + "contrastChecked": 388,
1062 + "height": 8893,
1063 + "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
1064 + "bottom": {
1065 + "navVisible": true,
1066 + "covered": false,
1067 + "gap": 1882
1068 + },
1069 + "errors": [
1070 + "pageerror SyntaxError: Invalid or unexpected token"
5563 1071 ],
5564 − "contrastChecked": 565,
5565 − "height": 6519,
5566 − "title": "Data Centers in Northern Virginia · DataCenterIndex",
5567 − "errors": [],
5568 1072 "badResponses": []
5569 1073 },
5570 1074 {
5571 1075 "name": "projects",
5572 1076 "path": "/projects",
5573 − "w": 1280,
1077 + "w": 390,
5574 1078 "theme": "dark",
5575 1079 "status": 200,
5576 − "ttfb": 15,
1080 + "ttfb": 481,
5577 1081 "overflow": null,
5578 1082 "clipped": [
5579 1083 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5580 − "span.sr-only \"United States\" 82>1",
5581 − "span.sr-only \"United States\" 82>1",
5582 − "span.sr-only \"Australia\" 54>1",
5583 − "span.sr-only \"Canada\" 47>1",
5584 − "span.sr-only \"United States\" 82>1",
5585 − "span.sr-only \"Norway\" 48>1"
1084 + "span.sr-only \"Luxembourg\" 71>1",
1085 + "span.sr-only \"United Kingdom\" 91>1",
1086 + "span.sr-only \"United Kingdom\" 91>1",
1087 + "span.sr-only \"Singapore\" 58>1",
1088 + "span.sr-only \"Singapore\" 58>1",
1089 + "span.sr-only \"Australia\" 49>1",
1090 + "span.sr-only \"United Kingdom\" 91>1",
1091 + "span.sr-only \"United Kingdom\" 91>1"
5586 1092 ],
5587 − "small": [],
5588 − "smallCount": 0,
1093 + "small": [
1094 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1095 + "a.dci-rowlink.font-medium.text-ink 209×35 \"STACK Expands to Arizona\"",
1096 + "a.dci-rowlink.font-medium.text-ink 167×18 \"LON-East Expansion Video\"",
1097 + "a.dci-rowlink.font-medium.text-ink 206×35 \"Green Mountain completes\"",
1098 + "a.dci-rowlink.font-medium.text-ink 213×35 \"Green Mountain signs 10 \"",
1099 + "a.dci-rowlink.font-medium.text-ink 213×35 \"Green Mountain expands d\"",
1100 + "a.dci-rowlink.font-medium.text-ink 213×35 \"Teraco Breaks Ground on \"",
1101 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Green Mountain expands i\"",
1102 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Successful mountain hall\"",
1103 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Element Critical Expands\"",
1104 + "a.dci-rowlink.font-medium.text-ink 209×35 \"Is There an AI Data Cent\"",
1105 + "a.dci-rowlink.font-medium.text-ink 217×35 \"Chicago Data Centers - A\""
1106 + ],
1107 + "smallCount": 22,
5589 1108 "h1Covered": false,
5590 − "h1Top": 106,
5591 − "headerBottom": 57,
1109 + "h1Top": 90,
1110 + "headerBottom": 49,
5592 1111 "lang": "en",
5593 − "metaDesc": 130,
1112 + "metaDesc": 213,
5594 1113 "h1Count": 1,
5595 1114 "skipped": [],
5596 1115 "unnamed": [],
5597 1116 "ariaCurrent": 3,
5598 1117 "footerOk": true,
5599 − "contrast": [
5600 − {
5601 − "ratio": 3.07,
5602 − "color": "rgb(91, 97, 104)",
5603 − "bg": "rgb(11,15,20)",
5604 − "sample": "span.figure.text-ink-4 \"19%\"",
5605 − "n": 3
5606 − },
5607 − {
5608 − "ratio": 3.07,
5609 − "color": "rgb(91, 97, 104)",
5610 − "bg": "rgb(11,15,20)",
5611 − "sample": "span.text-ink-4 \"TBD\"",
5612 − "n": 50
5613 − },
5614 − {
5615 − "ratio": 3.07,
5616 − "color": "rgb(91, 97, 104)",
5617 − "bg": "rgb(11,15,20)",
5618 − "sample": "p.mt-6.text-[11.5px].text-ink-4 \"Projects are reconciled from o\"",
5619 − "n": 1
5620 − },
5621 − {
5622 − "ratio": 3.07,
5623 − "color": "rgb(91, 97, 104)",
5624 − "bg": "rgb(11,15,20)",
5625 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"live feed\"",
5626 − "n": 1
5627 − }
5628 − ],
5629 − "contrastChecked": 473,
5630 − "height": 3977,
5631 − "title": "Data center projects · DataCenterIndex",
1118 + "contrast": [],
1119 + "contrastChecked": 464,
1120 + "height": 9984,
1121 + "title": "Data center projects & construction pipeline · DataCenterIndex",
1122 + "bottom": {
1123 + "navVisible": true,
1124 + "covered": false,
1125 + "gap": 1506
1126 + },
5632 1127 "errors": [],
5633 1128 "badResponses": []
5634 1129 },
5635 1130 {
5636 1131 "name": "project",
5637 1132 "path": "/projects/sydney-data-center-project",
5638 − "w": 1280,
1133 + "w": 390,
5639 1134 "theme": "dark",
5640 1135 "status": 200,
5641 − "ttfb": 7,
1136 + "ttfb": 70,
5642 1137 "overflow": null,
5643 1138 "clipped": [
5644 1139 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
@@ -5646,11 +1141,24 @@
5646 1141 "span.sr-only \"Australia\" 54>1",
5647 1142 "span.sr-only \"Australia\" 58>1"
5648 1143 ],
5649 − "small": [],
5650 − "smallCount": 0,
1144 + "small": [
1145 + "a.sr-only.focus:not-sr-only.focus:fixed 1×1 \"Skip to content\"",
1146 + "a.hover:text-ink.hover:underline 34×17 \"Home\"",
1147 + "a.hover:text-ink.hover:underline 46×17 \"Projects\"",
1148 + "a.hover:text-ink.hover:underline 92×19 \"Sydney market\"",
1149 + "button.group/ev.rounded-xs.text-left 137×22 \"Evidence for Status\"",
1150 + "button.group/ev.rounded-xs.text-left 171×18 \"Evidence for Announced\"",
1151 + "a.inline-flex.max-w-full.items-center 63×16 \"evidence\"",
1152 + "a.inline-flex.min-h-6.items-center 63×24 \"W.Media\"",
1153 + "a.inline-flex.items-center.gap-1 53×17 \"source\"",
1154 + "a.inline-flex.items-center.gap-1 53×17 \"source\"",
1155 + "a.inline-flex.items-center.gap-1 53×17 \"source\"",
1156 + "a.inline-flex.items-center.gap-1 53×17 \"source\""
1157 + ],
1158 + "smallCount": 25,
5651 1159 "h1Covered": false,
5652 − "h1Top": 136,
5653 − "headerBottom": 57,
1160 + "h1Top": 120,
1161 + "headerBottom": 49,
5654 1162 "lang": "en",
5655 1163 "metaDesc": 201,
5656 1164 "h1Count": 1,
@@ -5658,367 +1166,255 @@
5658 1166 "unnamed": [],
5659 1167 "ariaCurrent": 2,
5660 1168 "footerOk": true,
5661 − "contrast": [
5662 − {
5663 − "ratio": 3.07,
5664 − "color": "rgb(91, 97, 104)",
5665 − "bg": "rgb(11,15,20)",
5666 − "sample": "span.figure.text-[11px].text-ink-4 \"10\"",
5667 − "n": 2
5668 − },
5669 − {
5670 − "ratio": 3.07,
5671 − "color": "rgb(91, 97, 104)",
5672 − "bg": "rgb(11,15,20)",
5673 − "sample": "span.shrink-0.text-[11px].text-ink-4 \"News\"",
5674 − "n": 1
5675 − },
5676 − {
5677 − "ratio": 3.07,
5678 − "color": "rgb(91, 97, 104)",
5679 − "bg": "rgb(11,15,20)",
5680 − "sample": "span.text-ink-4 \"· Headlines, dates, facts and \"",
5681 − "n": 1
5682 − },
5683 − {
5684 − "ratio": 3.07,
5685 − "color": "rgb(91, 97, 104)",
5686 − "bg": "rgb(11,15,20)",
5687 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Figures are lower bounds compu\"",
5688 − "n": 1
5689 − },
5690 − {
5691 − "ratio": 3.07,
5692 − "color": "rgb(91, 97, 104)",
5693 − "bg": "rgb(11,15,20)",
5694 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5695 − "n": 1
5696 − }
5697 − ],
5698 − "contrastChecked": 252,
5699 − "height": 2902,
1169 + "contrast": [],
1170 + "contrastChecked": 286,
1171 + "height": 6528,
5700 1172 "title": "Sydney data center project · DataCenterIndex",
5701 − "errors": [],
1173 + "bottom": {
1174 + "navVisible": true,
1175 + "covered": false,
1176 + "gap": 1850
1177 + },
1178 + "errors": [
1179 + "pageerror SyntaxError: Invalid or unexpected token"
1180 + ],
5702 1181 "badResponses": []
5703 1182 },
5704 1183 {
5705 − "name": "rankings",
5706 − "path": "/rankings",
1184 + "name": "home",
1185 + "path": "/",
5707 1186 "w": 1280,
5708 1187 "theme": "dark",
5709 1188 "status": 200,
5710 − "ttfb": 3,
1189 + "ttfb": 109,
5711 1190 "overflow": null,
5712 1191 "clipped": [
5713 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1192 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1193 + "span.sr-only \"Ukraine\" 44>1",
1194 + "span.sr-only \"Belarus\" 42>1",
1195 + "span.sr-only \"Russia\" 37>1",
1196 + "span.sr-only \"Russia\" 37>1",
1197 + "span.sr-only \"North Macedonia\" 98>1",
1198 + "span.sr-only \"Russia\" 37>1",
1199 + "span.sr-only \"Iran\" 22>1",
1200 + "span.sr-only \"Iraq\" 22>1"
5714 1201 ],
5715 1202 "small": [],
5716 1203 "smallCount": 0,
5717 1204 "h1Covered": false,
5718 − "h1Top": 106,
1205 + "h1Top": 120,
5719 1206 "headerBottom": 57,
5720 1207 "lang": "en",
5721 − "metaDesc": 190,
1208 + "metaDesc": 234,
5722 1209 "h1Count": 1,
5723 1210 "skipped": [],
5724 1211 "unnamed": [],
5725 − "ariaCurrent": 2,
1212 + "ariaCurrent": 0,
5726 1213 "footerOk": true,
5727 − "contrast": [
5728 − {
5729 − "ratio": 3.07,
5730 − "color": "rgb(91, 97, 104)",
5731 − "bg": "rgb(11,15,20)",
5732 − "sample": "span.figure.text-[11px].text-ink-4 \"11\"",
5733 − "n": 1
5734 − },
5735 − {
5736 − "ratio": 3.07,
5737 − "color": "rgb(91, 97, 104)",
5738 − "bg": "rgb(11,15,20)",
5739 − "sample": "span.text-ink-4 \"No rows computed yet\"",
5740 − "n": 3
5741 − }
5742 − ],
5743 − "contrastChecked": 182,
5744 − "height": 3333,
5745 − "title": "Rankings · DataCenterIndex",
1214 + "contrast": [],
1215 + "contrastChecked": 718,
1216 + "height": 5724,
1217 + "title": "DataCenterIndex — Global data center infrastructure, indexed and versioned",
5746 1218 "errors": [],
5747 1219 "badResponses": []
5748 1220 },
5749 1221 {
5750 − "name": "ranking",
5751 − "path": "/rankings/countries_facilities",
1222 + "name": "map",
1223 + "path": "/map",
5752 1224 "w": 1280,
5753 1225 "theme": "dark",
5754 1226 "status": 200,
5755 − "ttfb": 13,
1227 + "ttfb": 21,
5756 1228 "overflow": null,
5757 1229 "clipped": [
5758 1230 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5759 − "span.sr-only \"United States\" 82>1",
5760 − "span.sr-only \"France\" 42>1",
5761 − "span.sr-only \"United Kingdom\" 99>1",
5762 − "span.sr-only \"Germany\" 56>1",
5763 − "span.sr-only \"Australia\" 54>1",
5764 − "span.sr-only \"Netherlands\" 75>1",
5765 − "span.sr-only \"India\" 30>1",
5766 − "span.sr-only \"Singapore\" 62>1"
1231 + "h1.sr-only \"Global map of data center infrastructure\" 259>1",
1232 + "span.sr-only \"Minimum MW\" 91>1",
1233 + "span.sr-only \"Maximum MW\" 95>1",
1234 + "span.sr-only \"Opened from\" 87>1",
1235 + "span.sr-only \"Opened to\" 69>1",
1236 + "span.sr-only \"Expected from\" 97>1",
1237 + "span.sr-only \"Expected to\" 79>1"
5767 1238 ],
5768 1239 "small": [],
5769 1240 "smallCount": 0,
5770 1241 "h1Covered": false,
5771 − "h1Top": 136,
1242 + "h1Top": 56,
5772 1243 "headerBottom": 57,
5773 1244 "lang": "en",
5774 − "metaDesc": 161,
1245 + "metaDesc": 127,
5775 1246 "h1Count": 1,
5776 − "skipped": [
5777 − "h1→h3"
5778 − ],
1247 + "skipped": [],
5779 1248 "unnamed": [],
5780 1249 "ariaCurrent": 2,
5781 − "footerOk": true,
1250 + "footerOk": false,
5782 1251 "contrast": [],
5783 − "contrastChecked": 626,
5784 − "height": 4578,
5785 − "title": "Countries by facility count · DataCenterIndex",
1252 + "contrastChecked": 110,
1253 + "height": 901,
1254 + "title": "Global data center map · DataCenterIndex",
1255 + "map": {
1256 + "controls": 3,
1257 + "unreachable": 0,
1258 + "canvas": true
1259 + },
5786 1260 "errors": [],
5787 1261 "badResponses": []
5788 1262 },
5789 1263 {
5790 − "name": "cloud-regions",
5791 − "path": "/cloud-regions",
1264 + "name": "fac-operator-mw",
1265 + "path": "/datacenters/databank-downtown-minneapolis-data-center-msp4",
5792 1266 "w": 1280,
5793 1267 "theme": "dark",
5794 1268 "status": 200,
5795 − "ttfb": 30,
1269 + "ttfb": 137,
5796 1270 "overflow": null,
5797 1271 "clipped": [
5798 1272 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5799 − "label.sr-only \"Provider\" 55>1",
5800 − "label.sr-only \"Country\" 53>1",
5801 − "label.sr-only \"Status\" 42>1"
1273 + "span.sr-only \"United States\" 86>1",
1274 + "span.sr-only \"United States\" 82>1",
1275 + "span.sr-only \"United States\" 76>1",
1276 + "span.sr-only \"United States\" 89>1"
5802 1277 ],
5803 1278 "small": [],
5804 1279 "smallCount": 0,
5805 1280 "h1Covered": false,
5806 − "h1Top": 106,
1281 + "h1Top": 135,
5807 1282 "headerBottom": 57,
5808 1283 "lang": "en",
5809 − "metaDesc": 155,
1284 + "metaDesc": 250,
5810 1285 "h1Count": 1,
5811 1286 "skipped": [],
5812 1287 "unnamed": [],
5813 − "ariaCurrent": 0,
1288 + "ariaCurrent": 1,
5814 1289 "footerOk": true,
5815 − "contrast": [
5816 − {
5817 − "ratio": 3.07,
5818 − "color": "rgb(91, 97, 104)",
5819 − "bg": "rgb(11,15,20)",
5820 − "sample": "span.figure.text-[11px].text-ink-4 \"79\"",
5821 − "n": 7
5822 − }
5823 − ],
5824 − "contrastChecked": 1501,
5825 − "height": 12906,
5826 − "title": "Cloud regions · DataCenterIndex",
1290 + "contrast": [],
1291 + "contrastChecked": 772,
1292 + "height": 7094,
1293 + "title": "Downtown Minneapolis Data Center (MSP4) (Minneapolis, US) · DataCenterIndex",
5827 1294 "errors": [],
5828 1295 "badResponses": []
5829 1296 },
5830 1297 {
5831 − "name": "cloud-region",
5832 − "path": "/cloud-regions/amazon-web-services-ca-central-1",
1298 + "name": "fac-osm",
1299 + "path": "/datacenters/shelter-modernizacion",
5833 1300 "w": 1280,
5834 1301 "theme": "dark",
5835 1302 "status": 200,
5836 − "ttfb": 5,
1303 + "ttfb": 73,
5837 1304 "overflow": null,
5838 1305 "clipped": [
5839 1306 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5840 − "span.sr-only \"Canada\" 47>1"
1307 + "span.sr-only \"Argentina\" 62>1",
1308 + "span.sr-only \"Argentina\" 59>1",
1309 + "span.sr-only \"Argentina\" 64>1"
5841 1310 ],
5842 1311 "small": [],
5843 1312 "smallCount": 0,
5844 1313 "h1Covered": false,
5845 − "h1Top": 136,
1314 + "h1Top": 132,
5846 1315 "headerBottom": 57,
5847 1316 "lang": "en",
5848 − "metaDesc": 165,
1317 + "metaDesc": 141,
5849 1318 "h1Count": 1,
5850 1319 "skipped": [],
5851 1320 "unnamed": [],
5852 − "ariaCurrent": 0,
1321 + "ariaCurrent": 1,
5853 1322 "footerOk": true,
5854 − "contrast": [
5855 − {
5856 − "ratio": 3.07,
5857 − "color": "rgb(91, 97, 104)",
5858 − "bg": "rgb(11,15,20)",
5859 − "sample": "p.mt-3.text-[11.5px].text-ink-4 \"Region facts are taken verbati\"",
5860 − "n": 1
5861 − },
5862 − {
5863 − "ratio": 3.07,
5864 − "color": "rgb(91, 97, 104)",
5865 − "bg": "rgb(11,15,20)",
5866 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"Methodology\"",
5867 − "n": 1
5868 − }
5869 − ],
5870 − "contrastChecked": 102,
5871 − "height": 1902,
5872 − "title": "Amazon Web Services ca-central-1 — Canada (Central) · DataCenterIndex",
1323 + "contrast": [],
1324 + "contrastChecked": 337,
1325 + "height": 4423,
1326 + "title": "Shelter Modernización · DataCenterIndex",
5873 1327 "errors": [],
5874 1328 "badResponses": []
5875 1329 },
5876 1330 {
5877 − "name": "ixps",
5878 − "path": "/ixps",
1331 + "name": "fac-google",
1332 + "path": "/datacenters/google-the-dalles-oregon-data-center",
5879 1333 "w": 1280,
5880 1334 "theme": "dark",
5881 1335 "status": 200,
5882 − "ttfb": 5,
1336 + "ttfb": 171,
5883 1337 "overflow": null,
5884 1338 "clipped": [
5885 1339 "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
5886 − "label.sr-only \"Search\" 46>1",
5887 − "label.sr-only \"Country\" 53>1"
1340 + "span.sr-only \"United States\" 86>1",
1341 + "span.sr-only \"United States\" 82>1",
1342 + "span.sr-only \"Singapore\" 58>1",
1343 + "span.sr-only \"United States\" 89>1",
1344 + "span.sr-only \"United States\" 89>1",
1345 + "span.sr-only \"United States\" 89>1",
1346 + "span.sr-only \"United States\" 89>1"
5888 1347 ],
5889 1348 "small": [],
5890 1349 "smallCount": 0,
5891 1350 "h1Covered": false,
5892 − "h1Top": 106,
1351 + "h1Top": 135,
5893 1352 "headerBottom": 57,
5894 1353 "lang": "en",
5895 − "metaDesc": 100,
1354 + "metaDesc": 216,
5896 1355 "h1Count": 1,
5897 1356 "skipped": [],
5898 1357 "unnamed": [],
5899 1358 "ariaCurrent": 1,
5900 1359 "footerOk": true,
5901 − "contrast": [
5902 − {
5903 − "ratio": 3.07,
5904 − "color": "rgb(91, 97, 104)",
5905 − "bg": "rgb(11,15,20)",
5906 − "sample": "p.mt-8.text-[12px].text-ink-4 \"Exchange and network data: Pee\"",
5907 − "n": 1
5908 − }
5909 − ],
5910 − "contrastChecked": 75,
5911 − "height": 1171,
5912 − "title": "Internet exchanges · DataCenterIndex",
5913 − "errors": [
5914 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
5915 − ],
5916 − "badResponses": [
5917 − "404 fetch /api/v1/docs?_rsc=LOh4kTrez0j5S-5D"
5918 − ]
5919 − },
5920 − {
5921 − "name": "sources",
5922 − "path": "/sources",
5923 − "w": 1280,
5924 − "theme": "dark",
5925 − "status": 200,
5926 − "ttfb": 16,
5927 − "overflow": null,
5928 − "clipped": [
5929 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
5930 − ],
5931 − "small": [],
5932 − "smallCount": 0,
5933 − "h1Covered": false,
5934 − "h1Top": 136,
5935 − "headerBottom": 57,
5936 − "lang": "en",
5937 − "metaDesc": 162,
5938 − "h1Count": 1,
5939 − "skipped": [
5940 − "h1→h3"
5941 − ],
5942 − "unnamed": [],
5943 − "ariaCurrent": 0,
5944 − "footerOk": true,
5945 − "contrast": [
5946 − {
5947 − "ratio": 3.07,
5948 − "color": "rgb(91, 97, 104)",
5949 − "bg": "rgb(11,15,20)",
5950 − "sample": "span.figure.text-[11px].text-ink-4 \"60\"",
5951 − "n": 3
5952 − },
5953 − {
5954 − "ratio": 3.07,
5955 − "color": "rgb(91, 97, 104)",
5956 − "bg": "rgb(11,15,20)",
5957 − "sample": "p.mt-6.text-[12px].text-ink-4 \"Kinds rank source authority wh\"",
5958 − "n": 1
5959 − },
5960 − {
5961 − "ratio": 3.07,
5962 − "color": "rgb(91, 97, 104)",
5963 − "bg": "rgb(11,15,20)",
5964 − "sample": "a.underline.decoration-hair-2.hover:text-ink \"reconciliation rules\"",
5965 − "n": 2
5966 − }
5967 − ],
5968 − "contrastChecked": 880,
5969 − "height": 8101,
5970 − "title": "Sources · DataCenterIndex",
1360 + "contrast": [],
1361 + "contrastChecked": 621,
1362 + "height": 6676,
1363 + "title": "Google The Dalles, Oregon data center (The Dalles, US) · DataCenterIndex",
5971 1364 "errors": [],
5972 1365 "badResponses": []
5973 1366 },
5974 1367 {
5975 − "name": "methodology",
5976 − "path": "/methodology",
1368 + "name": "fac-wikidata",
1369 + "path": "/datacenters/foulum-data-center",
5977 1370 "w": 1280,
5978 1371 "theme": "dark",
5979 1372 "status": 200,
5980 − "ttfb": 3,
1373 + "ttfb": 96,
5981 1374 "overflow": null,
5982 1375 "clipped": [
5983 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1376 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1377 + "span.sr-only \"Denmark\" 58>1",
1378 + "span.sr-only \"Denmark\" 56>1",
1379 + "span.sr-only \"Denmark\" 60>1"
5984 1380 ],
5985 1381 "small": [],
5986 1382 "smallCount": 0,
5987 1383 "h1Covered": false,
5988 − "h1Top": 136,
1384 + "h1Top": 135,
5989 1385 "headerBottom": 57,
5990 1386 "lang": "en",
5991 − "metaDesc": 201,
1387 + "metaDesc": 171,
5992 1388 "h1Count": 1,
5993 1389 "skipped": [],
5994 1390 "unnamed": [],
5995 − "ariaCurrent": 0,
1391 + "ariaCurrent": 1,
5996 1392 "footerOk": true,
5997 − "contrast": [
5998 − {
5999 − "ratio": 3.07,
6000 − "color": "rgb(91, 97, 104)",
6001 − "bg": "rgb(11,15,20)",
6002 − "sample": "div.figure.text-[11px].text-ink-4 \"countries_facilities\"",
6003 − "n": 20
6004 − }
6005 − ],
6006 − "contrastChecked": 292,
6007 − "height": 6645,
6008 − "title": "Methodology · DataCenterIndex",
1393 + "contrast": [],
1394 + "contrastChecked": 465,
1395 + "height": 5021,
1396 + "title": "Foulum Data Center (Viborg Municipality, DK) · DataCenterIndex",
6009 1397 "errors": [],
6010 1398 "badResponses": []
6011 1399 },
6012 1400 {
6013 − "name": "about",
6014 − "path": "/about",
1401 + "name": "projects",
1402 + "path": "/projects",
6015 1403 "w": 1280,
6016 1404 "theme": "dark",
6017 1405 "status": 200,
6018 − "ttfb": 1,
1406 + "ttfb": 442,
6019 1407 "overflow": null,
6020 1408 "clipped": [
6021 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1409 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1410 + "span.sr-only \"Luxembourg\" 77>1",
1411 + "span.sr-only \"United Kingdom\" 99>1",
1412 + "span.sr-only \"United Kingdom\" 99>1",
1413 + "span.sr-only \"Singapore\" 62>1",
1414 + "span.sr-only \"Singapore\" 62>1",
1415 + "span.sr-only \"Australia\" 54>1",
1416 + "span.sr-only \"United Kingdom\" 99>1",
1417 + "span.sr-only \"United Kingdom\" 99>1"
6022 1418 ],
6023 1419 "small": [],
6024 1420 "smallCount": 0,
@@ -6026,29 +1422,32 @@
6026 1422 "h1Top": 106,
6027 1423 "headerBottom": 57,
6028 1424 "lang": "en",
6029 − "metaDesc": 85,
1425 + "metaDesc": 213,
6030 1426 "h1Count": 1,
6031 1427 "skipped": [],
6032 1428 "unnamed": [],
6033 − "ariaCurrent": 0,
1429 + "ariaCurrent": 3,
6034 1430 "footerOk": true,
6035 1431 "contrast": [],
6036 − "contrastChecked": 72,
6037 − "height": 1679,
6038 − "title": "About · DataCenterIndex",
1432 + "contrastChecked": 594,
1433 + "height": 4880,
1434 + "title": "Data center projects & construction pipeline · DataCenterIndex",
6039 1435 "errors": [],
6040 1436 "badResponses": []
6041 1437 },
6042 1438 {
6043 − "name": "bot",
6044 − "path": "/bot",
1439 + "name": "project",
1440 + "path": "/projects/sydney-data-center-project",
6045 1441 "w": 1280,
6046 1442 "theme": "dark",
6047 1443 "status": 200,
6048 − "ttfb": 2,
1444 + "ttfb": 72,
6049 1445 "overflow": null,
6050 1446 "clipped": [
6051 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
1447 + "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1",
1448 + "span.sr-only \"Australia\" 54>1",
1449 + "span.sr-only \"Australia\" 54>1",
1450 + "span.sr-only \"Australia\" 58>1"
6052 1451 ],
6053 1452 "small": [],
6054 1453 "smallCount": 0,
@@ -6056,50 +1455,18 @@
6056 1455 "h1Top": 136,
6057 1456 "headerBottom": 57,
6058 1457 "lang": "en",
6059 − "metaDesc": 179,
1458 + "metaDesc": 201,
6060 1459 "h1Count": 1,
6061 1460 "skipped": [],
6062 1461 "unnamed": [],
6063 − "ariaCurrent": 0,
1462 + "ariaCurrent": 2,
6064 1463 "footerOk": true,
6065 1464 "contrast": [],
6066 − "contrastChecked": 91,
6067 − "height": 2261,
6068 − "title": "About our crawler · DataCenterIndex",
1465 + "contrastChecked": 343,
1466 + "height": 3572,
1467 + "title": "Sydney data center project · DataCenterIndex",
6069 1468 "errors": [],
6070 1469 "badResponses": []
6071 − },
6072 − {
6073 − "name": "404",
6074 − "path": "/this-page-does-not-exist",
6075 − "w": 1280,
6076 − "theme": "dark",
6077 − "status": 404,
6078 − "ttfb": 1,
6079 − "overflow": null,
6080 − "clipped": [
6081 − "a.sr-only.focus:not-sr-only.focus:fixed \"Skip to content\" 99>1"
6082 − ],
6083 − "small": [],
6084 − "smallCount": 0,
6085 − "h1Covered": false,
6086 − "h1Top": 261,
6087 − "headerBottom": 57,
6088 − "lang": "en",
6089 − "metaDesc": 159,
6090 − "h1Count": 1,
6091 − "skipped": [],
6092 − "unnamed": [],
6093 − "ariaCurrent": 0,
6094 − "footerOk": false,
6095 − "contrast": [],
6096 − "contrastChecked": 16,
6097 − "height": 900,
6098 − "title": "DataCenterIndex — The global index of data center infrastructure",
6099 − "errors": [
6100 − "Failed to load resource: the server responded with a status of 404 (Not Found)"
6101 − ],
6102 − "badResponses": []
6103 1470 }
6104 1471 ]
6105 1472 }
\ No newline at end of file
modified apps/web/src/app/(site)/(home)/page.tsx +312 −116
@@ -1,168 +1,364 @@
1 −import type { Dashboard, FacilityStatus } from "@dci/core";
2 −import { PIPELINE_STATUSES } from "@dci/core";
1 +import type { Dashboard } from "@dci/core";
2 +import { ExternalLink } from "lucide-react";
3 3 import type { Metadata } from "next";
4 4 import Link from "next/link";
5 5 import { EventList } from "@/components/event-list";
6 −import { CapacityOverTime, PipelineByYear } from "@/components/home/charts";
7 −import { DashboardMetrics } from "@/components/home/dashboard-metrics";
6 +import { CapacityOverTime } from "@/components/home/charts";
8 7 import { HeroSearch } from "@/components/home/hero";
9 8 import { MapPreview } from "@/components/home/map-preview";
9 +import { PipelineFunnel } from "@/components/home/pipeline-funnel";
10 +import { PulseBlock } from "@/components/home/pulse";
11 +import { GlobalTicker } from "@/components/home/ticker";
10 12 import { ProjectList } from "@/components/project-list";
11 −import { Badge } from "@/components/ui/badge";
13 +import { AiMark, Badge } from "@/components/ui/badge";
12 14 import { MoreLink } from "@/components/ui/button";
13 15 import { Page, Section } from "@/components/ui/card";
14 −import { BarList, StackedBar } from "@/components/ui/charts";
16 +import { BarList } from "@/components/ui/charts";
15 17 import { CountryFlag } from "@/components/ui/country-flag";
16 −import { Metric } from "@/components/ui/metric";
18 +import { RelativeTime } from "@/components/ui/relative-time";
19 +import { SourceBadge } from "@/components/ui/source-badge";
17 20 import { EmptyState, OfflineNotice } from "@/components/ui/states";
21 +import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
18 22 import { api } from "@/lib/api";
19 −import { fmtInt, fmtMw, fmtPct } from "@/lib/format";
23 +import { fmtDate, fmtInt, fmtMw, fmtPct, splitMw } from "@/lib/format";
24 +import { isSiteScopeLabel } from "@/lib/labels";
20 25 import { routes } from "@/lib/routes";
21 26
22 27 export const revalidate = 60;
23 28
24 29 export const metadata: Metadata = {
25 − title: { absolute: "DataCenterIndex — The global index of data center infrastructure" },
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.",
26 32 alternates: { canonical: "/" },
27 33 };
28 34
35 +function 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 +}
54 +
29 55 export default async function HomePage() {
30 56 const r = await api.dashboard();
31 57 const d: Dashboard | null = r.ok ? r.data : null;
32 58 const stats = d?.stats ?? null;
33 −
34 − const pipelineStatus: Partial<Record<FacilityStatus, number>> = {};
35 − for (const p of d?.projectsByStatus ?? []) if (PIPELINE_STATUSES.includes(p.status) || p.status === "cancelled") pipelineStatus[p.status] = (pipelineStatus[p.status] ?? 0) + p.count;
36 − const pipelineMw = (d?.projectsByStatus ?? []).reduce((a, p) => a + (p.mw ?? 0), 0);
59 + const cov = d?.coverage ?? null;
60 + const ing = d?.ingestion ?? null;
61 + const major = (d?.majorProjects ?? []).filter((p) => p.plannedMw !== null && p.plannedMw >= 100 && (p.capacityScope == null || isSiteScopeLabel(p.capacityScope)));
62 + const aiMw = splitMw(d?.aiExpansion.plannedMw);
37 63
38 64 return (
39 65 <Page>
40 − {/* Hero — compact, graticule texture, big search */}
41 − <section className="relative -mx-3 border-b border-hair px-3 pb-6 pt-6 sm:-mx-5 sm:px-5 sm:pb-10 sm:pt-12 lg:-mx-8 lg:px-8">
66 + {/* ---------------------------------------------------------------- hero + live map */}
67 + <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">
42 68 <div aria-hidden className="graticule pointer-events-none absolute inset-0" />
43 69 <div className="relative mx-auto max-w-[1280px]">
44 70 <div className="label-caps mb-3 flex items-center gap-2">
45 − <span className="inline-block size-1.5 rounded-full bg-accent" /> Live index
46 − {stats?.lastCrawlAt && <span className="normal-case tracking-normal text-ink-4">· updated continuously</span>}
71 + <span className="inline-block size-1.5 rounded-full bg-accent animate-pulse-soft" /> Live index
72 + {ing?.lastRunAt && (
73 + <span className="normal-case tracking-normal text-ink-4">
74 + · last ingestion <RelativeTime iso={ing.lastRunAt} />
75 + </span>
76 + )}
47 77 </div>
48 − <h1 className="max-w-3xl text-[28px] font-semibold leading-[1.08] tracking-[-0.025em] text-ink sm:text-[40px] lg:text-[46px]">The global index of data center infrastructure.</h1>
49 − <p className="mt-3 max-w-2xl text-[14px] text-ink-2 sm:text-[16px]">Facilities, operators, cloud regions, markets and projects — every figure sourced, versioned and tracked in a live change feed.</p>
78 + <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>
79 + <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>
50 80 <HeroSearch />
51 − </div>
52 − </section>
53 −
54 − <div className="py-4 sm:py-5">
55 − {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}
56 − <DashboardMetrics stats={stats} />
57 − </div>
58 −
59 − {/* Map preview */}
60 − <section className="pb-6">
61 − <div className="mb-2 flex items-end justify-between">
62 − <div>
63 − <h2 className="text-[15px] font-semibold sm:text-[16px]">Where the capacity is</h2>
64 − <p className="text-[12px] text-ink-3">{stats ? `${fmtInt(stats.facilities)} facilities across ${fmtInt(stats.countries)} countries and ${fmtInt(stats.metros)} markets.` : "Geolocated facilities, clustered by market."}</p>
81 + <div className="mt-6">
82 + {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}
83 + <GlobalTicker stats={stats} className="bg-paper/70 backdrop-blur-[1px]" />
65 84 </div>
66 − <MoreLink href={routes.map()}>Open map</MoreLink>
67 85 </div>
68 − <MapPreview />
86 + <div className="relative mx-auto max-w-[1280px] pb-6 pt-3 sm:pb-8">
87 + <MapPreview interactive className="h-[300px] sm:h-[420px] lg:h-[460px]">
88 + <div className="pointer-events-none absolute left-3 top-3 z-[2] flex flex-wrap gap-1.5 text-[11.5px]">
89 + <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>
90 + <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>
91 + </div>
92 + </MapPreview>
93 + </div>
69 94 </section>
70 95
71 − <div className="grid gap-x-8 gap-y-6 lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]">
72 − {/* Latest changes */}
73 − <Section title="Latest changes" description="Detected by connectors, deduplicated, with source and confidence." aside={<MoreLink href={routes.live()} />}>
74 − <EventList events={(d?.latestEvents ?? []).slice(0, 8)} dense />
96 + <div className="flex flex-col gap-10 pt-8">
97 + {/* ---------------------------------------------------------------- what changed today */}
98 + <Section title="What changed today" description={d ? `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>}>
99 + {d ? <PulseBlock pulse={d.pulse} /> : <EmptyState compact title="Pulse unavailable" />}
75 100 </Section>
76 101
77 − {/* New projects */}
78 − <Section title="New projects" description="Recently announced or filed; MW are planned figures." aside={<MoreLink href={routes.projects()} />}>
79 − <ProjectList items={(d?.newProjects ?? []).slice(0, 8)} />
80 − </Section>
102 + <div className="grid gap-x-8 gap-y-10 lg:grid-cols-2 [&>*]:min-w-0">
103 + {/* ---------------------------------------------------------------- AI expansion */}
104 + <Section
105 + title={
106 + <span className="inline-flex items-center gap-2">
107 + AI infrastructure expansion <AiMark size="sm" />
108 + </span>
109 + }
110 + description="Facilities and projects with AI / HPC evidence (confirmed, likely or associated) — published MW only."
111 + aside={<MoreLink href={routes.ai()}>AI index</MoreLink>}
112 + >
113 + <div className="grid grid-cols-3 divide-x divide-hair [&>*+*]:pl-3">
114 + <Cell label="AI facilities" value={d ? fmtInt(d.aiExpansion.facilities) : "—"} href={routes.datacenters("ai=1")} />
115 + <Cell label="AI projects" value={d ? fmtInt(d.aiExpansion.projects) : "—"} href={routes.projects("ai=1")} />
116 + <Cell label="Planned (≈)" value={aiMw?.value ?? "—"} unit={aiMw?.unit ?? "MW"} note="published figures only" />
117 + </div>
118 + <div className="mt-3">
119 + <ProjectList items={(d?.aiExpansion.recent ?? []).slice(0, 5)} emptyTitle="No AI project yet" emptyDescription="Projects with AI / HPC evidence will list here." />
120 + </div>
121 + </Section>
81 122
82 − {/* Largest markets / operators */}
83 − <Section title="Largest markets" description="By known operational MW. Bars fade when coverage is under 50%." aside={<MoreLink href={routes.rankings("metros")} />}>
84 − {d?.topMetros?.length ? (
85 − <BarList
86 − items={d.topMetros.slice(0, 10).map((r) => ({
87 − rank: r.rank,
88 − label: (
89 − <span className="inline-flex items-center gap-1.5">
90 − <CountryFlag iso2={r.countryIso2} size={12} />
91 − {r.name}
92 − </span>
93 − ),
94 − value: r.value,
95 − href: r.href || routes.market(r.slug),
96 − coverage: r.coverage,
97 − secondary: r.coverage !== undefined && r.coverage !== null ? `${fmtPct(r.coverage)} MW coverage` : undefined,
98 − display: fmtMw(r.value),
99 − }))}
100 − />
101 − ) : (
102 − <EmptyState compact title="Rankings not computed yet" />
103 − )}
104 − </Section>
123 + {/* ---------------------------------------------------------------- construction pipeline */}
124 + <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>}>
125 + {d ? <PipelineFunnel data={{ byStatus: d.projectsByStatus, openedLast12m: null }} compact /> : <EmptyState compact title="Pipeline unavailable" />}
126 + </Section>
105 127
106 − <Section title="Largest operators" description="By facility count and known MW." aside={<MoreLink href={routes.rankings("operators")} />}>
107 − {d?.topOperators?.length ? (
108 − <BarList
109 − items={d.topOperators.slice(0, 10).map((r) => ({ rank: r.rank, label: r.name, value: r.value, href: r.href || routes.operator(r.slug), secondary: r.secondary !== undefined && r.secondary !== null ? fmtMw(r.secondary) : undefined, display: fmtInt(r.value) }))}
110 − />
111 − ) : (
112 − <EmptyState compact title="Rankings not computed yet" />
113 − )}
114 − </Section>
128 + {/* ---------------------------------------------------------------- fastest-growing markets */}
129 + <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>}>
130 + {d?.fastestGrowingMetros.length ? (
131 + <Table dense>
132 + <THead>
133 + <tr>
134 + <Th>Market</Th>
135 + <Th numeric>Announced</Th>
136 + <Th numeric>In constr.</Th>
137 + <Th numeric className="hidden sm:table-cell">
138 + Announced MW
139 + </Th>
140 + <Th numeric className="hidden md:table-cell">
141 + Entrants
142 + </Th>
143 + <Th numeric className="hidden md:table-cell">
144 + Grid ev.
145 + </Th>
146 + </tr>
147 + </THead>
148 + <TBody>
149 + {d.fastestGrowingMetros.slice(0, 8).map((m) => (
150 + <Tr key={m.id}>
151 + <Td primary>
152 + <Link href={routes.market(m.slug)} className="inline-flex items-center gap-1.5 font-medium text-ink hover:text-accent hover:underline">
153 + <CountryFlag iso2={m.countryIso2} size={12} /> {m.name}
154 + </Link>
155 + </Td>
156 + <Td label="Announced" numeric lead>
157 + {fmtInt(m.momentum.projectsAnnounced)}
158 + </Td>
159 + <Td label="In construction" numeric>
160 + {fmtInt(m.momentum.projectsEnteredConstruction)}
161 + </Td>
162 + <Td label="Announced MW" numeric className="hidden sm:table-cell">
163 + {m.momentum.announcedMw !== null ? `≈ ${fmtMw(m.momentum.announcedMw)}` : "—"}
164 + </Td>
165 + <Td label="New entrants" numeric className="hidden md:table-cell">
166 + {fmtInt(m.momentum.newEntrants.length)}
167 + </Td>
168 + <Td label="Grid events" numeric className="hidden md:table-cell">
169 + {fmtInt(m.momentum.gridEvents)}
170 + </Td>
171 + </Tr>
172 + ))}
173 + </TBody>
174 + </Table>
175 + ) : (
176 + <EmptyState compact title="Momentum not computed yet" />
177 + )}
178 + </Section>
179 +
180 + {/* ---------------------------------------------------------------- major projects */}
181 + <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>}>
182 + <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." />
183 + </Section>
115 184
116 − {/* Pipeline */}
117 − <Section title="Construction pipeline" description="Projects by status and by expected opening year." aside={<MoreLink href={routes.projects()} />} className="lg:col-span-2">
118 − <div className="grid gap-6 md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">
119 − <div>
120 − <div className="mb-3 flex items-baseline gap-4">
121 − <Metric size="sm" value={stats ? fmtInt(stats.projects) : "—"} label="Tracked projects" />
122 − <Metric size="sm" value={pipelineMw ? fmtMw(pipelineMw).split(" ")[0] : "—"} unit={pipelineMw ? fmtMw(pipelineMw).split(" ")[1] : undefined} label="Planned MW (≈)" />
185 + {/* ---------------------------------------------------------------- operator expansion */}
186 + <Section title="Operator expansion" description="Operators entering new countries or markets, with projects announced over 12 months." aside={<MoreLink href={routes.operators()}>Operators</MoreLink>}>
187 + {d?.operatorExpansion.length ? (
188 + <ul className="divide-y divide-hair">
189 + {d.operatorExpansion.slice(0, 8).map((o) => (
190 + <li key={o.operator.id} className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-3 py-2">
191 + <div className="min-w-0">
192 + <Link href={routes.operator(o.operator.slug)} className="text-[13.5px] font-medium text-ink hover:text-accent hover:underline">
193 + {o.operator.name}
194 + </Link>
195 + <div className="mt-0.5 truncate text-[12px] text-ink-3">
196 + {o.newCountries.length ? (
197 + <span className="inline-flex items-center gap-1">
198 + {o.newCountries.slice(0, 6).map((c) => (
199 + <CountryFlag key={c} iso2={c} size={11} />
200 + ))}
201 + {o.newCountries.length > 6 && <span>+{o.newCountries.length - 6}</span>}
202 + </span>
203 + ) : null}
204 + {o.newMetros.length > 0 && <span className="ml-1.5">{o.newMetros.slice(0, 3).join(", ")}{o.newMetros.length > 3 ? ` +${o.newMetros.length - 3}` : ""}</span>}
205 + </div>
206 + </div>
207 + <div className="figure text-right text-[12px] text-ink-2">
208 + <div>
209 + {fmtInt(o.newCountries.length)} <span className="text-ink-4">countries</span>
210 + </div>
211 + <div>
212 + {fmtInt(o.newMetros.length)} <span className="text-ink-4">markets</span> · {fmtInt(o.projects12m)} <span className="text-ink-4">prj</span>
213 + </div>
214 + </div>
215 + </li>
216 + ))}
217 + </ul>
218 + ) : (
219 + <EmptyState compact title="No expansion detected yet" />
220 + )}
221 + </Section>
222 +
223 + {/* ---------------------------------------------------------------- power / grid */}
224 + <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>}>
225 + <EventList events={(d?.powerEvents ?? []).slice(0, 5)} dense emptyTitle="No power event yet" />
226 + {d?.gridConstraints.length ? (
227 + <div className="mt-3 border-t border-hair pt-3">
228 + <div className="label-caps mb-1.5">Grid constraints</div>
229 + <ul className="divide-y divide-hair text-[12.5px]">
230 + {d.gridConstraints.slice(0, 4).map((g) => (
231 + <li key={g.id} className="flex flex-wrap items-baseline gap-x-2 py-1.5">
232 + <Badge tone="outline" size="xs">
233 + {g.kind.replace(/_/g, " ")}
234 + </Badge>
235 + <a href={g.url} target="_blank" rel="noopener noreferrer" className="min-w-0 flex-1 truncate font-medium text-ink hover:text-accent">
236 + {g.title}
237 + </a>
238 + {g.sourceKind && <SourceBadge kind={g.sourceKind} name={g.sourceName} url={g.url} />}
239 + <span className="figure text-[11px] text-ink-3">{fmtDate(g.effectiveDate)}</span>
240 + </li>
241 + ))}
242 + </ul>
123 243 </div>
124 − {Object.keys(pipelineStatus).length ? <StackedBar data={pipelineStatus} /> : <EmptyState compact title="No project statuses yet" />}
244 + ) : null}
245 + </Section>
246 +
247 + {/* ---------------------------------------------------------------- cloud / connectivity */}
248 + <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>}>
249 + <div className="grid grid-cols-3 divide-x divide-hair [&>*+*]:pl-3">
250 + <Cell label="Cloud regions" value={stats ? fmtInt(stats.cloudRegions) : "—"} href={routes.cloudRegions()} />
251 + <Cell label="Internet exchanges" value={stats ? fmtInt(stats.ixps) : "—"} href={routes.ixps()} />
252 + <Cell label="Connectivity known" value={cov ? fmtPct(cov.connectivityCoverage) : "—"} note="facilities with carriers / IXPs" href={routes.coverage()} />
125 253 </div>
126 − <div>
127 − <div className="label-caps mb-2">By expected opening year</div>
128 − <PipelineByYear data={d?.pipelineByYear ?? []} />
254 + <div className="mt-3">
255 + <div className="label-caps mb-2">Largest markets · known operational MW</div>
256 + {d?.topMetros?.length ? (
257 + <BarList
258 + 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) }))}
259 + />
260 + ) : (
261 + <EmptyState compact title="Rankings not computed yet" />
262 + )}
129 263 </div>
130 − </div>
131 − </Section>
132 −
133 − {/* AI expansion */}
134 − <Section
135 − title={
136 − <span className="inline-flex items-center gap-2">
137 − AI infrastructure expansion <Badge tone="accent" size="xs">AI</Badge>
138 − </span>
139 − }
140 − description="Facilities and projects flagged as AI / HPC capacity."
141 − aside={<MoreLink href={routes.datacenters("ai=1")} />}
142 − >
143 − <div className="mb-3 grid grid-cols-3 gap-3">
144 − <Metric size="sm" value={d ? fmtInt(d.aiExpansion.facilities) : "—"} label="AI facilities" />
145 − <Metric size="sm" value={d ? fmtInt(d.aiExpansion.projects) : "—"} label="AI projects" />
146 − <Metric size="sm" value={d?.aiExpansion.plannedMw !== null && d?.aiExpansion.plannedMw !== undefined ? fmtMw(d.aiExpansion.plannedMw).split(" ")[0] : "—"} unit={d?.aiExpansion.plannedMw ? fmtMw(d.aiExpansion.plannedMw).split(" ")[1] : undefined} label="Planned MW (≈)" />
147 − </div>
148 − <ProjectList items={(d?.aiExpansion.recent ?? []).slice(0, 5)} emptyTitle="No AI projects yet" emptyDescription="Projects flagged as AI/HPC will list here." />
149 − </Section>
264 + </Section>
265 + </div>
150 266
151 − {/* Capacity over time */}
152 − <Section title="Capacity over time" description="Cumulative known operational MW by opening year. Only facilities with a known figure." aside={<MoreLink href={routes.methodology()}>Methodology</MoreLink>}>
267 + {/* ---------------------------------------------------------------- capacity over time */}
268 + <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>}>
153 269 <CapacityOverTime data={d?.capacityOverTime ?? []} />
154 270 </Section>
271 +
272 + {/* ---------------------------------------------------------------- coverage · transparency · ingestion */}
273 + <div className="grid gap-x-8 gap-y-8 border-t border-hair pt-6 lg:grid-cols-3">
274 + <div>
275 + <div className="mb-2 flex items-baseline justify-between">
276 + <h2 className="text-[14px] font-semibold text-ink">Data coverage</h2>
277 + <MoreLink href={routes.coverage()}>Coverage</MoreLink>
278 + </div>
279 + {cov ? (
280 + <dl className="divide-y divide-hair text-[12.5px]">
281 + {[
282 + ["Capacity (MW) published", cov.capacityCoverage],
283 + ["Operator known", cov.operatorCoverage],
284 + ["Precise location", cov.preciseLocationCoverage],
285 + ["Any location", cov.anyLocationCoverage],
286 + ["Status known", cov.statusCoverage],
287 + ["Opening date known", cov.openingDateCoverage],
288 + ["Multiple sources", cov.multiSourceCoverage],
289 + ["Connectivity known", cov.connectivityCoverage],
290 + ].map(([k, v]) => (
291 + <div key={String(k)} className="flex items-center justify-between gap-3 py-1.5">
292 + <dt className="text-ink-2">{k}</dt>
293 + <dd className="flex items-center gap-2">
294 + <span className="inline-block h-[3px] w-16 bg-surface-3">
295 + <span className="block h-full bg-ink-2" style={{ width: `${Math.max(1, Math.round((v as number) * 100))}%` }} />
296 + </span>
297 + <span className="figure w-10 text-right text-ink">{fmtPct(v as number, (v as number) < 0.1 ? 1 : 0)}</span>
298 + </dd>
299 + </div>
300 + ))}
301 + </dl>
302 + ) : (
303 + <EmptyState compact title="Coverage unavailable" />
304 + )}
305 + <p className="mt-2 text-[11px] text-ink-4">Share of {cov ? fmtInt(cov.facilities) : ""} indexed facilities. Gaps are shown, never filled.</p>
306 + </div>
307 + <div>
308 + <div className="mb-2 flex items-baseline justify-between">
309 + <h2 className="text-[14px] font-semibold text-ink">Source transparency</h2>
310 + <MoreLink href={routes.sources()}>Sources</MoreLink>
311 + </div>
312 + <div className="grid grid-cols-2 divide-x divide-hair [&>*+*]:pl-3">
313 + <Cell label="Sources" value={stats ? fmtInt(stats.sources) : "—"} href={routes.sources()} />
314 + <Cell label="Archived documents" value={stats ? fmtInt(stats.documents) : "—"} />
315 + <Cell label="Primary-source share" value={cov ? fmtPct(cov.primarySourceShare) : "—"} note="operator, government, utility, filing" />
316 + <Cell label="Projects located" value={cov ? `${fmtInt(cov.projectsWithLocation)} / ${fmtInt(cov.projects)}` : "—"} note="with published coordinates" />
317 + </div>
318 + <p className="mt-2 text-[12px] text-ink-3">
319 + Every field carries its source, dates, confidence and extraction method; every figure opens an evidence drawer with the supporting sentence.{" "}
320 + <Link href={routes.methodology()} className="underline decoration-hair-2 hover:text-ink">
321 + Methodology
322 + </Link>{" "}
323 + ·{" "}
324 + <Link href={routes.download()} className="underline decoration-hair-2 hover:text-ink">
325 + Downloads
326 + </Link>{" "}
327 + ·{" "}
328 + <Link href={routes.apiDocsPage()} className="underline decoration-hair-2 hover:text-ink">
329 + API
330 + </Link>
331 + </p>
332 + </div>
333 + <div>
334 + <div className="mb-2 flex items-baseline justify-between">
335 + <h2 className="text-[14px] font-semibold text-ink">Latest ingestion</h2>
336 + <MoreLink href={routes.live()}>Live feed</MoreLink>
337 + </div>
338 + {ing ? (
339 + <>
340 + <div className="grid grid-cols-2 divide-x divide-hair [&>*+*]:pl-3">
341 + <Cell label="Last run" value={ing.lastRunAt ? fmtDate(ing.lastRunAt) : "—"} note={ing.lastRunAt ? "UTC" : undefined} />
342 + <Cell label="Runs · 24 h" value={fmtInt(ing.runs24h)} />
343 + <Cell label="Documents · 24 h" value={fmtInt(ing.documents24h)} note="fetched, archived, diffed" />
344 + <Cell label="Connectors healthy" value={`${fmtInt(ing.connectorsHealthy)} / ${fmtInt(ing.connectorsTotal)}`} note={ing.connectorsTotal ? fmtPct(ing.connectorsHealthy / ing.connectorsTotal) : undefined} />
345 + </div>
346 + <p className="mt-2 text-[12px] text-ink-3">
347 + {fmtInt(ing.events24h)} events detected in 24 h. Pipeline: discover → fetch → archive → extract → normalize → reconcile → validate → version → detect changes → publish.{" "}
348 + <Link href={routes.bot()} className="inline-flex items-center gap-1 underline decoration-hair-2 hover:text-ink">
349 + Crawler policy <ExternalLink className="size-3" />
350 + </Link>
351 + </p>
352 + </>
353 + ) : (
354 + <EmptyState compact title="Ingestion status unavailable" />
355 + )}
356 + </div>
357 + </div>
155 358 </div>
156 359
157 − {/* Coverage note */}
158 360 <p className="mt-8 border-t border-hair pt-4 text-[11.5px] text-ink-3">
159 − {stats ? (
160 − <>
161 − MW figures are known for {fmtPct(stats.mwCoverage)} of indexed facilities; totals are lower bounds, never extrapolated. {fmtInt(stats.sources)} sources · {fmtInt(stats.documents)} archived documents.{" "}
162 − </>
163 − ) : (
164 − <>Totals are lower bounds computed from sourced figures only; nothing is extrapolated. </>
165 − )}
361 + {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. </>}
166 362 <Link href={routes.methodology()} className="text-ink-2 underline decoration-hair-2 hover:text-ink">
167 363 How the index is built
168 364 </Link>
added apps/web/src/app/(site)/ai-infrastructure/loading.tsx +5 −0
@@ -0,0 +1,5 @@
1 +import { IndexLoading } from "@/components/intelligence/index-loading";
2 +
3 +export default function Loading() {
4 + return <IndexLoading />;
5 +}
added apps/web/src/app/(site)/ai-infrastructure/page.tsx +182 −0
@@ -0,0 +1,182 @@
1 +import type { AiEvidence } from "@dci/core";
2 +import type { Metadata } from "next";
3 +import Link from "next/link";
4 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
5 +import { SectionNav } from "@/components/entity/section-nav";
6 +import { SourcesFooter } from "@/components/entity/sources-footer";
7 +import { EventList } from "@/components/event-list";
8 +import { AiFacilitiesTable } from "@/components/intelligence/ai/ai-facilities-table";
9 +import { ApiNote, CoverageBar, HonestEmpty, MwFigure, PillLink, PublishedOnlyNote, ShareList } from "@/components/intelligence/shared";
10 +import { ProjectList } from "@/components/project-list";
11 +import { AiMark } from "@/components/ui/badge";
12 +import { Page, PageHeader, Section } from "@/components/ui/card";
13 +import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
14 +import { ErrorState, OfflineNotice } from "@/components/ui/states";
15 +import { api } from "@/lib/api";
16 +import { fmtInt, fmtMw, fmtPct } from "@/lib/format";
17 +import { AI_EVIDENCE_LABEL, STATUS_LABEL, statusVar } from "@/lib/labels";
18 +import { routes } from "@/lib/routes";
19 +import { pageMetadata } from "@/lib/seo";
20 +
21 +export const revalidate = 120;
22 +
23 +export const metadata: Metadata = pageMetadata({
24 + title: "AI infrastructure",
25 + description: "AI / HPC data centers and projects with their evidence level (confirmed, likely, associated), the project pipeline by stage, top operators, markets and countries — published figures only.",
26 + path: routes.ai(),
27 +});
28 +
29 +const LEVELS: Array<{ level: AiEvidence; rule: string; counted: boolean }> = [
30 + { level: "confirmed", rule: "A source explicitly describes the site as AI / HPC / GPU / accelerated-computing infrastructure.", counted: true },
31 + { level: "likely", rule: "AI-ready, high-density, liquid- or direct-to-chip cooling or GPU signals in the source.", counted: true },
32 + { level: "associated", rule: "Only an AI tenant, customer or company mention — reported separately, NOT counted as AI infrastructure.", counted: false },
33 + { level: "unknown", rule: "No signal in any indexed source. A single keyword never confirms a site.", counted: false },
34 +];
35 +
36 +export default async function AiInfrastructurePage() {
37 + const r = await api.aiIndex();
38 + if (!r.ok) {
39 + return (
40 + <Page className="py-6">
41 + <Breadcrumbs items={[{ name: "AI infrastructure", href: routes.ai() }]} className="mb-4" />
42 + <OfflineNotice detail={r.error} className="mb-4" />
43 + <ErrorState />
44 + </Page>
45 + );
46 + }
47 + const d = r.data;
48 + const s = d.stats;
49 + const stages = [...d.pipelineByStage].sort((a, b) => b.count - a.count);
50 + const stageMax = Math.max(1, ...stages.map((x) => x.count));
51 + const sections = [
52 + { id: "evidence", label: "Evidence levels" },
53 + { id: "pipeline", label: "Project pipeline", count: stages.length },
54 + { id: "top", label: "Top operators & places" },
55 + { id: "announcements", label: "Announcements", count: d.recentAnnouncements.length },
56 + { id: "projects", label: "Recent projects", count: d.recentProjects.length },
57 + { id: "facilities", label: "Facilities", count: d.facilities.length },
58 + ];
59 +
60 + return (
61 + <Page>
62 + <Breadcrumbs items={[{ name: "AI infrastructure", href: routes.ai() }]} className="pt-3" />
63 + <PageHeader
64 + eyebrow="Intelligence"
65 + title="AI infrastructure"
66 + description="Data centers and projects with evidence of AI / HPC use, ranked and counted only when a source says so. Counts and MW cover confirmed + likely sites; associated mentions are reported separately."
67 + aside={<PillLink href={routes.map("layer=ai")}>Open the AI layer on the map →</PillLink>}
68 + />
69 +
70 + <MetricRow cols={5}>
71 + <MetricCell>
72 + <Metric value={fmtInt(s.facilities)} label="AI facilities" note={`${fmtInt(s.confirmed)} confirmed · ${fmtInt(s.likely)} likely`} />
73 + </MetricCell>
74 + <MetricCell>
75 + <Metric value={fmtInt(s.projects)} label="AI projects" note="live project records" />
76 + </MetricCell>
77 + <MetricCell>
78 + <Metric value={<MwFigure mw={s.plannedMw} estimate />} label="Planned MW" note={s.plannedMw === null ? "no published figure" : "projects, published ≈ figures"} />
79 + </MetricCell>
80 + <MetricCell>
81 + <Metric value={<MwFigure mw={s.constructionMw} />} label="Construction MW" note={s.constructionMw === null ? "no published figure" : "under construction"} />
82 + </MetricCell>
83 + <MetricCell>
84 + <Metric value={fmtPct(s.mwCoverage)} label="MW coverage" note="share of AI records with a figure" />
85 + </MetricCell>
86 + </MetricRow>
87 + <MetricRow cols={3} className="border-t-0 [&>div]:border-t-0">
88 + <MetricCell>
89 + <Metric size="sm" value={fmtInt(s.countries)} label="Countries" />
90 + </MetricCell>
91 + <MetricCell>
92 + <Metric size="sm" value={fmtInt(s.operators)} label="Operators" />
93 + </MetricCell>
94 + <MetricCell>
95 + <Metric size="sm" value={fmtInt(s.associated)} label="Associated mentions" note="reported separately, not counted" />
96 + </MetricCell>
97 + </MetricRow>
98 +
99 + <SectionNav items={sections} className="mt-4" />
100 +
101 + <div className="flex flex-col gap-8 pt-6">
102 + <Section id="evidence" title="Evidence levels" description="How a site earns the AI mark. Only confirmed and likely sites are counted above." className="scroll-mt-28">
103 + <ul className="divide-y divide-hair">
104 + {LEVELS.map((l) => (
105 + <li key={l.level} className="grid grid-cols-[auto_minmax(0,1fr)] items-start gap-x-3 py-2.5 sm:grid-cols-[140px_minmax(0,1fr)_120px]">
106 + <div className="pt-0.5">{l.level === "unknown" ? <span className="text-[11.5px] uppercase tracking-[0.06em] text-ink-4">Unknown</span> : <AiMark evidence={l.level} size="sm" />}</div>
107 + <p className="text-[13px] text-ink-2">
108 + <span className="mr-1 font-medium text-ink">{AI_EVIDENCE_LABEL[l.level].replace("AI · ", "").replace(/^./, (c) => c.toUpperCase())}.</span>
109 + {l.rule}
110 + </p>
111 + <span className={`col-start-2 text-[11.5px] sm:col-start-3 sm:text-right ${l.counted ? "text-ok" : "text-ink-4"}`}>{l.counted ? "counted" : "not counted"}</span>
112 + </li>
113 + ))}
114 + </ul>
115 + <ApiNote note={d.evidenceNote} label="From the index" className="mt-3" />
116 + </Section>
117 +
118 + <Section id="pipeline" title="Project pipeline by stage" description="AI project records by lifecycle stage; MW are planned, published figures (≈) and are never added to operational capacity." className="scroll-mt-28">
119 + {stages.length === 0 ? (
120 + <HonestEmpty title="No AI project in the pipeline" detail="projects appear when an announcement with AI evidence is ingested" />
121 + ) : (
122 + <ol className="divide-y divide-hair">
123 + {stages.map((st) => (
124 + <li key={st.status} className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-x-3 py-2 text-[13px]">
125 + <div className="flex min-w-0 items-center gap-2">
126 + <span aria-hidden className="inline-block size-2 shrink-0 rounded-full" style={{ background: statusVar(st.status) }} />
127 + <span className="truncate text-ink">{STATUS_LABEL[st.status]}</span>
128 + <span className="hidden h-[4px] min-w-[60px] flex-1 bg-surface-3 sm:block" aria-hidden>
129 + <span className="block h-full" style={{ width: `${(st.count / stageMax) * 100}%`, background: statusVar(st.status) }} />
130 + </span>
131 + </div>
132 + <span className="figure text-ink-2">
133 + {fmtInt(st.count)} <span className="text-ink-4">{st.count === 1 ? "project" : "projects"}</span>
134 + </span>
135 + <span className="figure w-[96px] text-right text-ink-2">{st.mw === null ? <span className="text-ink-4">MW —</span> : <>≈ {fmtMw(st.mw)}</>}</span>
136 + </li>
137 + ))}
138 + </ol>
139 + )}
140 + <PublishedOnlyNote className="mt-2" />
141 + </Section>
142 +
143 + <Section id="top" title="Top operators, markets and countries" description="Ranked by confirmed + likely AI facilities and projects; planned MW shown when published." className="scroll-mt-28">
144 + <div className="grid gap-6 lg:grid-cols-3">
145 + <TopColumn title="Operators" rows={d.topOperators.map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), facilities: o.facilities, projects: o.projects, plannedMw: o.plannedMw }))} />
146 + <TopColumn title="Markets" rows={d.topMetros.map((m) => ({ key: m.id, label: m.name, href: routes.market(m.slug), facilities: m.facilities, projects: m.projects, plannedMw: m.plannedMw, iso2: m.countryIso2 }))} />
147 + <TopColumn title="Countries" rows={d.topCountries.map((c) => ({ key: c.iso2, label: c.name, href: routes.country(c.slug), facilities: c.facilities, projects: c.projects, plannedMw: c.plannedMw, iso2: c.iso2 }))} />
148 + </div>
149 + </Section>
150 +
151 + <Section id="announcements" title="New announcements" description="Events with AI evidence, newest first, each linked to its source." className="scroll-mt-28" aside={<Link href={routes.live("ai=1")} className="text-[12.5px] text-ink-3 hover:text-ink">Live feed (AI) →</Link>}>
152 + <EventList events={d.recentAnnouncements} dense emptyTitle="No AI announcement detected yet" emptyDescription="Announcements appear here when a source describes new AI / HPC capacity." />
153 + </Section>
154 +
155 + <Section id="projects" title="Recent projects" description="Latest AI project records." className="scroll-mt-28" aside={<Link href={routes.projects("ai=1")} className="text-[12.5px] text-ink-3 hover:text-ink">All AI projects →</Link>}>
156 + <ProjectList items={d.recentProjects} emptyTitle="No AI project yet" />
157 + </Section>
158 +
159 + <Section id="facilities" title="Facilities" description="Every facility flagged AI, with the evidence level per row." className="scroll-mt-28" aside={<span className="inline-flex items-center gap-2 text-[12px] text-ink-3"><span>MW coverage</span><CoverageBar value={s.mwCoverage} width="w-[110px]" /></span>}>
160 + <AiFacilitiesTable items={d.facilities} />
161 + </Section>
162 + </div>
163 +
164 + <SourcesFooter sources={r.sources} note="AI status is never inferred from a single keyword; each evidence level is backed by the sentence that supports it. MW figures are published, site-scoped and containment-aware." />
165 + </Page>
166 + );
167 +}
168 +
169 +function TopColumn({ title, rows }: { title: string; rows: Array<{ key: string; label: string; href: string; facilities: number; projects: number; plannedMw: number | null; iso2?: string | null }> }) {
170 + const total = rows.reduce((a, x) => a + x.facilities + x.projects, 0);
171 + return (
172 + <div>
173 + <div className="label-caps mb-2">{title}</div>
174 + {rows.length === 0 ? (
175 + <HonestEmpty title={`No ${title.toLowerCase()} yet`} />
176 + ) : (
177 + <ShareList unit="records" items={rows.map((x) => ({ key: x.key, label: x.label, href: x.href, count: x.facilities + x.projects, share: total ? (x.facilities + x.projects) / total : 0, mw: x.plannedMw, iso2: x.iso2 ?? null }))} />
178 + )}
179 + <p className="mt-1.5 text-[11px] text-ink-4">records = facilities + projects · MW = planned, published</p>
180 + </div>
181 + );
182 +}
modified apps/web/src/app/(site)/cloud-regions/[slug]/page.tsx +35 −4
@@ -7,7 +7,10 @@ import { JsonLd } from "@/components/entity/json-ld";
7 7 import { ExtLink, KeyValueList } from "@/components/entity/kv";
8 8 import { SectionNav } from "@/components/entity/section-nav";
9 9 import { SourcesFooter } from "@/components/entity/sources-footer";
10 +import { EventList } from "@/components/event-list";
11 +import { ProvenanceTable } from "@/components/entity/provenance";
10 12 import { FacilityTable } from "@/components/facility-table";
13 +import { ApiNote, HonestEmpty } from "@/components/intelligence/shared";
11 14 import { Badge, PrecisionBadge } from "@/components/ui/badge";
12 15 import { Page, Section } from "@/components/ui/card";
13 16 import { CountryFlag } from "@/components/ui/country-flag";
@@ -60,7 +63,10 @@ export default async function CloudRegionPage({ params }: { params: Params }) {
60 63 const cHref = countryHref(d.countryIso2);
61 64 const cName = d.countryName ?? (d.countryIso2 ? countryName(d.countryIso2) : null);
62 65 const siblings = (d.siblingRegions ?? []).filter((s) => s.id !== d.id);
63 − const facilities = d.facilitiesInMetro ?? [];
66 + const hosts = d.hostFacilities ?? [];
67 + const facilities = (d.marketFacilities?.length ? d.marketFacilities : d.facilitiesInMetro) ?? [];
68 + const events = d.events ?? [];
69 + const provenance = d.provenance ?? [];
64 70 const ids = Object.entries(d.externalIds ?? {}).filter(([k]) => k !== "kind");
65 71 const hasGeo = typeof d.lat === "number" && typeof d.lng === "number";
66 72
@@ -79,7 +85,10 @@ export default async function CloudRegionPage({ params }: { params: Params }) {
79 85 const nav = [
80 86 { id: "location", label: "Location" },
81 87 { id: "siblings", label: `Other ${d.provider.name} regions`, count: siblings.length },
82 − { id: "facilities", label: "Facilities in this market" },
88 + { id: "hosting", label: hosts.length ? "Host facilities" : "Hosting" },
89 + { id: "facilities", label: "Facilities in this market", count: facilities.length },
90 + { id: "events", label: "Events", count: events.length },
91 + { id: "provenance", label: "Provenance", count: provenance.length },
83 92 { id: "identifiers", label: "Identifiers", count: ids.length },
84 93 { id: "source", label: "Source" },
85 94 ];
@@ -133,13 +142,16 @@ export default async function CloudRegionPage({ params }: { params: Params }) {
133 142 <p className="mt-1 text-[12px] text-ink-3">Location shown at {PRECISION_LABEL[d.geoPrecision].toLowerCase().replace(" location", "")} — cloud providers publish the metro, not the building.</p>
134 143 </header>
135 144
136 − <MetricRow cols={3}>
145 + <MetricRow cols={4}>
137 146 <MetricCell>
138 147 <Metric value={d.availabilityZones ?? "—"} label="Availability zones" note={d.availabilityZones === null ? "not published" : undefined} />
139 148 </MetricCell>
140 149 <MetricCell>
141 150 <Metric value={fmtDate(d.launchedOn)} label="Launched" note={d.launchedOn ? undefined : "not published"} />
142 151 </MetricCell>
152 + <MetricCell>
153 + <Metric value={fmtDate(d.announcedOn)} label="Announced" note={d.announcedOn ? undefined : "not published"} />
154 + </MetricCell>
143 155 <MetricCell>
144 156 <Metric value={fmtInt(siblings.length)} label={`Other ${d.provider.name} regions`} note={`${fmtInt(facilities.length)} indexed ${facilities.length === 1 ? "facility" : "facilities"} in this market`} />
145 157 </MetricCell>
@@ -210,8 +222,27 @@ export default async function CloudRegionPage({ params }: { params: Params }) {
210 222 </Section>
211 223 )}
212 224
225 + <Section id="hosting" title={hosts.length ? "Host facilities (publicly verified)" : "Hosting"} description={hosts.length ? "Facilities a source publicly states host this region." : "Cloud providers publish the metro, not the building."} className="scroll-mt-28">
226 + {hosts.length ? (
227 + <FacilityTable items={hosts} columns={["operator", "country", "status", "mw"]} />
228 + ) : (
229 + <p className="text-[13.5px] text-ink-2">
230 + Region associated with {d.metro ? <>the <Link href={routes.market(d.metro.slug)} className="font-medium text-ink hover:text-accent hover:underline">{d.metro.name}</Link> market</> : cName ? <>a market in {cName}</> : <>a market</>} — not hosted at a specific facility.
231 + </p>
232 + )}
233 + <ApiNote note={d.note} className="mt-3" />
234 + </Section>
235 +
213 236 <Section id="facilities" title="Facilities in this market" description={d.metro ? `Indexed data centers in the ${d.metro.name} market — not necessarily hosting this region.` : "Indexed data centers around the published location — not necessarily hosting this region."} className="scroll-mt-28">
214 − {facilities.length ? <FacilityTable items={facilities} columns={["operator", "country", "status", "mw"]} /> : <EmptyState compact title="No indexed facility in this metro yet" description="Facilities appear as operator, registry and dataset connectors observe them." />}
237 + {facilities.length ? <FacilityTable items={facilities} columns={["operator", "country", "status", "mw"]} /> : <HonestEmpty title="No indexed facility in this metro yet" detail="facilities appear as operator, registry and dataset connectors observe them" />}
238 + </Section>
239 +
240 + <Section id="events" title="Events" description="Announcement, launch and update events detected for this region." className="scroll-mt-28">
241 + <EventList events={events} dense emptyTitle="No event recorded for this region" emptyDescription="Announcements and launches appear here when the provider's region list changes." />
242 + </Section>
243 +
244 + <Section id="provenance" title="Provenance" description="Field by field: which source backs which value, with first / last observation." className="scroll-mt-28">
245 + <ProvenanceTable items={provenance} emptyDescription="No field-level provenance has been published for this region yet." />
215 246 </Section>
216 247
217 248 {ids.length > 0 && (
added apps/web/src/app/(site)/compare/operators/loading.tsx +15 −0
@@ -0,0 +1,15 @@
1 +import { Page } from "@/components/ui/card";
2 +import { Skeleton, SkeletonBlock, SkeletonRows } from "@/components/ui/skeleton";
3 +
4 +export default function Loading() {
5 + return (
6 + <Page className="py-4" aria-busy>
7 + <Skeleton className="mb-4 h-3 w-40" />
8 + <Skeleton className="mb-2 h-8 w-64 max-w-full" />
9 + <Skeleton className="mb-6 h-3.5 w-80 max-w-full" />
10 + <Skeleton className="mb-3 h-10 w-full max-w-md" />
11 + <SkeletonBlock className="mt-6 h-[320px]" />
12 + <SkeletonRows rows={6} className="mt-6" />
13 + </Page>
14 + );
15 +}
added apps/web/src/app/(site)/compare/operators/page.tsx +275 −0
@@ -0,0 +1,275 @@
1 +import type { OperatorComparison } from "@dci/core";
2 +import type { Metadata } from "next";
3 +import Link from "next/link";
4 +import type { ReactNode } from "react";
5 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
6 +import { SourcesFooter } from "@/components/entity/sources-footer";
7 +import { OperatorPicker } from "@/components/intelligence/compare/operator-picker";
8 +import { CoverageBar, HonestEmpty, PublishedOnlyNote } from "@/components/intelligence/shared";
9 +import { Badge } from "@/components/ui/badge";
10 +import { Page, PageHeader, Section } from "@/components/ui/card";
11 +import { CountryFlag } from "@/components/ui/country-flag";
12 +import { EmptyState, OfflineNotice } from "@/components/ui/states";
13 +import { api } from "@/lib/api";
14 +import { fmtInt, fmtMw, fmtPct } from "@/lib/format";
15 +import { OPERATOR_KIND_LABEL } from "@/lib/labels";
16 +import { routes } from "@/lib/routes";
17 +import { pageMetadata } from "@/lib/seo";
18 +import type { SearchParams } from "@/lib/sort";
19 +
20 +export const revalidate = 120;
21 +
22 +const MAX = 5;
23 +
24 +export const metadata: Metadata = pageMetadata({
25 + title: "Compare operators",
26 + description: "Side-by-side comparison of 2–5 data center operators: facilities, countries, markets, known MW with its coverage, pipeline, AI facilities, new markets entered and recent projects — published figures only.",
27 + path: routes.compareOperators(),
28 +});
29 +
30 +/* Presets verified against the index (top operators by facility count on 2026-09-12). */
31 +const PRESETS: Array<{ label: string; slugs: string[] }> = [
32 + { label: "Equinix vs Digital Realty", slugs: ["equinix", "digital-realty"] },
33 + { label: "US wholesale: STACK · CyrusOne · Vantage · QTS", slugs: ["stack-infrastructure", "cyrusone", "vantage-data-centers", "qts"] },
34 + { label: "DataBank vs EdgeConneX", slugs: ["databank", "edgeconnex"] },
35 +];
36 +
37 +type Op = OperatorComparison["operators"][number];
38 +
39 +function parseSlugs(sp: SearchParams): string[] {
40 + const raw = sp.slugs;
41 + const s = Array.isArray(raw) ? raw.join(",") : (raw ?? "");
42 + const out: string[] = [];
43 + for (const part of s.split(",")) {
44 + const v = part.trim().toLowerCase();
45 + if (v && /^[a-z0-9][a-z0-9-]*$/.test(v) && !out.includes(v)) out.push(v);
46 + }
47 + return out.slice(0, MAX);
48 +}
49 +
50 +const dash = <span className="text-ink-4">—</span>;
51 +const mw = (v: number | null | undefined, est?: boolean) => (v === null || v === undefined ? dash : <span className="figure">{est ? "≈ " : ""}{fmtMw(v)}</span>);
52 +const int = (v: number | null | undefined) => (v === null || v === undefined ? dash : <span className="figure">{fmtInt(v)}</span>);
53 +
54 +interface RowDef {
55 + key: string;
56 + label: string;
57 + hint?: string;
58 + cell: (o: Op) => ReactNode;
59 + /** row belongs to a group header */
60 + group?: string;
61 +}
62 +
63 +function rows(): RowDef[] {
64 + const win = (o: Op, label: "12m" | "3y" | "5y") => o.velocity.windows.find((w) => w.label === label);
65 + return [
66 + { key: "kind", label: "Kind", cell: (o) => (o.kind ? OPERATOR_KIND_LABEL[o.kind] : dash) },
67 + { key: "hq", label: "Headquarters", cell: (o) => (o.hqCountryIso2 ? <CountryFlag iso2={o.hqCountryIso2} withName /> : dash) },
68 + { key: "facilities", label: "Facilities", group: "Footprint", cell: (o) => int(o.facilityCount) },
69 + { key: "countries", label: "Countries", cell: (o) => int(o.countryCount) },
70 + { key: "metros", label: "Markets", cell: (o) => int(o.metroCount) },
71 + { key: "known", label: "Known operational MW", group: "Capacity (published figures only)", hint: "Σ IT / total power over operational facilities", cell: (o) => mw(o.knownMw) },
72 + { key: "coverage", label: "Capacity coverage", hint: "share of facilities with any MW figure", cell: (o) => <CoverageBar value={o.coverage ?? o.mwCoverage ?? o.pipeline.mwCoverage} width="w-full max-w-[160px]" label="Capacity coverage" /> },
73 + { key: "construction", label: "Under construction MW", cell: (o) => mw(o.constructionMw ?? o.pipeline.construction.mw, true) },
74 + { key: "planned", label: "Planned MW", cell: (o) => mw(o.plannedMw ?? (o.pipeline.approved.mw === null && o.pipeline.announced.mw === null ? null : (o.pipeline.approved.mw ?? 0) + (o.pipeline.announced.mw ?? 0)), true) },
75 + { key: "p-op", label: "Operational sites", group: "Pipeline buckets", cell: (o) => int(o.pipeline.operational.count) },
76 + { key: "p-con", label: "Under construction sites", cell: (o) => int(o.pipeline.construction.count) },
77 + { key: "p-app", label: "Approved / permitting sites", cell: (o) => int(o.pipeline.approved.count) },
78 + { key: "p-ann", label: "Announced / proposed sites", cell: (o) => int(o.pipeline.announced.count) },
79 + { key: "projects", label: "Project records", cell: (o) => int(o.projectCount ?? o.pipeline.projects.reduce((a, p) => a + p.count, 0)) },
80 + { key: "pmw", label: "Project planned MW", cell: (o) => mw(o.projectPlannedMw, true) },
81 + { key: "ai", label: "AI facilities", group: "AI & cloud", hint: "confirmed + likely evidence", cell: (o) => int(o.aiCount) },
82 + { key: "cloud", label: "Cloud regions", cell: (o) => int(o.cloudRegionCount) },
83 + {
84 + key: "newmarkets",
85 + label: "New markets entered (12 m)",
86 + group: "Expansion",
87 + cell: (o) =>
88 + o.newMarkets12m.length ? (
89 + <span className="flex flex-wrap gap-1">
90 + {o.newMarkets12m.slice(0, 8).map((m) => (
91 + <Link key={m.id} href={routes.market(m.slug)} className="inline-flex h-10 items-center rounded-xs border border-hair bg-surface px-2 text-[11.5px] text-ink hover:text-accent sm:h-6 sm:px-1.5">
92 + {m.name}
93 + </Link>
94 + ))}
95 + {o.newMarkets12m.length > 8 && <span className="text-[11.5px] text-ink-4">+{o.newMarkets12m.length - 8}</span>}
96 + </span>
97 + ) : (
98 + <span className="text-ink-4">none</span>
99 + ),
100 + },
101 + { key: "v12", label: "New facilities · 12 m", cell: (o) => int(win(o, "12m")?.newFacilities) },
102 + { key: "v3", label: "New facilities · 3 y", cell: (o) => int(win(o, "3y")?.newFacilities) },
103 + { key: "v5", label: "New facilities · 5 y", cell: (o) => int(win(o, "5y")?.newFacilities) },
104 + { key: "c12", label: "New countries · 12 m", cell: (o) => int(win(o, "12m")?.newCountries) },
105 + { key: "np12", label: "New projects · 12 m", cell: (o) => int(win(o, "12m")?.newProjects) },
106 + {
107 + key: "recent",
108 + label: "Recent projects",
109 + group: "Projects",
110 + cell: (o) =>
111 + o.recentProjects.length ? (
112 + <ul className="flex flex-col gap-1">
113 + {o.recentProjects.slice(0, 3).map((p) => (
114 + <li key={p.id} className="min-w-0">
115 + <Link href={routes.project(p.slug)} className="block truncate text-[12.5px] text-ink hover:text-accent hover:underline" title={p.name}>
116 + {p.name}
117 + </Link>
118 + <span className="figure text-[11px] text-ink-3">
119 + {p.plannedMw !== null ? `≈ ${fmtMw(p.plannedMw)}` : "MW —"}
120 + {p.countryIso2 ? ` · ${p.countryIso2}` : ""}
121 + </span>
122 + </li>
123 + ))}
124 + </ul>
125 + ) : (
126 + <span className="text-ink-4">none</span>
127 + ),
128 + },
129 + ];
130 +}
131 +
132 +function ComparisonGrid({ ops }: { ops: Op[] }) {
133 + const defs = rows();
134 + const cols = `minmax(150px,200px) repeat(${ops.length}, minmax(150px, 1fr))`;
135 + return (
136 + <>
137 + {/* ≥ 640 px: one column per operator (scrolls horizontally inside its own container for 4–5 operators) */}
138 + <div className="hidden overflow-x-auto rounded-sm border border-hair sm:block">
139 + <div role="table" aria-label="Operator comparison" className="min-w-[640px] text-[13px]" style={{ display: "grid", gridTemplateColumns: cols }}>
140 + <div role="row" className="contents">
141 + <div role="columnheader" className="label-caps sticky left-0 z-[1] border-b border-hair bg-surface px-3 py-2.5">
142 + Metric
143 + </div>
144 + {ops.map((o) => (
145 + <div key={o.id} role="columnheader" className="border-b border-l border-hair bg-surface px-3 py-2.5">
146 + <Link href={routes.operator(o.slug)} className="block truncate text-[13.5px] font-semibold text-ink hover:text-accent hover:underline" title={o.name}>
147 + {o.name}
148 + </Link>
149 + <div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-ink-3">
150 + {o.kind && (
151 + <Badge tone={o.kind === "hyperscaler" ? "accent" : "outline"} size="xs">
152 + {OPERATOR_KIND_LABEL[o.kind]}
153 + </Badge>
154 + )}
155 + {o.hqCountryIso2 && <CountryFlag iso2={o.hqCountryIso2} size={11} />}
156 + </div>
157 + </div>
158 + ))}
159 + </div>
160 + {defs.map((d) => (
161 + <div key={d.key} role="row" className="contents">
162 + {d.group && (
163 + <div role="rowheader" className="label-caps border-b border-hair bg-surface-2 px-3 py-1.5 text-ink-3" style={{ gridColumn: `1 / span ${ops.length + 1}` }}>
164 + {d.group}
165 + </div>
166 + )}
167 + <div role="rowheader" className="sticky left-0 z-[1] border-b border-hair bg-paper px-3 py-2 text-ink-2">
168 + {d.label}
169 + {d.hint && <div className="text-[11px] text-ink-4">{d.hint}</div>}
170 + </div>
171 + {ops.map((o) => (
172 + <div key={o.id} role="cell" className="min-w-0 border-b border-l border-hair px-3 py-2 text-ink">
173 + {d.cell(o)}
174 + </div>
175 + ))}
176 + </div>
177 + ))}
178 + </div>
179 + </div>
180 + {/* < 640 px: stacked per-operator cards */}
181 + <div className="flex flex-col gap-4 sm:hidden">
182 + {ops.map((o) => (
183 + <section key={o.id} className="rounded-sm border border-hair bg-surface" aria-label={o.name}>
184 + <div className="border-b border-hair px-3 py-2.5">
185 + <Link href={routes.operator(o.slug)} className="text-[14px] font-semibold text-ink hover:text-accent">
186 + {o.name}
187 + </Link>
188 + <div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-ink-3">
189 + {o.kind && (
190 + <Badge tone="outline" size="xs">
191 + {OPERATOR_KIND_LABEL[o.kind]}
192 + </Badge>
193 + )}
194 + {o.hqCountryIso2 && <CountryFlag iso2={o.hqCountryIso2} withName />}
195 + </div>
196 + </div>
197 + <dl className="divide-y divide-hair px-3">
198 + {defs
199 + .filter((d) => d.key !== "kind" && d.key !== "hq")
200 + .map((d) => (
201 + <div key={d.key} className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)] gap-x-3 py-2 text-[12.5px]">
202 + <dt className="text-ink-3">{d.label}</dt>
203 + <dd className="min-w-0 text-right text-ink [&_.flex-wrap]:justify-end">{d.cell(o)}</dd>
204 + </div>
205 + ))}
206 + </dl>
207 + </section>
208 + ))}
209 + </div>
210 + </>
211 + );
212 +}
213 +
214 +export default async function CompareOperatorsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
215 + const sp = await searchParams;
216 + const slugs = parseSlugs(sp);
217 + const r = slugs.length >= 2 ? await api.compareOperators(slugs) : null;
218 + const ops = r?.ok ? r.data.operators : [];
219 + const names: Record<string, string> = {};
220 + for (const o of ops) names[o.slug] = o.name;
221 + const missing = slugs.filter((s) => !names[s]);
222 + // slugs picked but not yet resolved (< 2 selected): resolve names for the chips
223 + if (slugs.length === 1) {
224 + const one = await api.operator(slugs[0]!);
225 + if (one.ok) names[slugs[0]!] = one.data.name;
226 + }
227 +
228 + return (
229 + <Page>
230 + <Breadcrumbs items={[{ name: "Operators", href: routes.operators() }, { name: "Compare", href: routes.compareOperators() }]} className="pt-3" />
231 + <PageHeader eyebrow="Compare" title="Compare operators" description="Two to five operators side by side. Capacity figures are published values only and every operator carries its capacity coverage — a low coverage means the MW column understates reality." />
232 +
233 + <OperatorPicker slugs={slugs} names={names} />
234 +
235 + <div className="mt-6">
236 + {slugs.length < 2 ? (
237 + <EmptyState
238 + title={slugs.length === 1 ? "Add a second operator to compare" : "Pick two or more operators"}
239 + description="Use the search box above, or start from a preset."
240 + action={
241 + <ul className="flex flex-wrap justify-center gap-1.5">
242 + {PRESETS.map((p) => (
243 + <li key={p.label}>
244 + <Link href={routes.compareOperators(p.slugs)} className="inline-flex h-10 items-center 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">
245 + {p.label}
246 + </Link>
247 + </li>
248 + ))}
249 + </ul>
250 + }
251 + />
252 + ) : r && !r.ok ? (
253 + <>
254 + <OfflineNotice detail={r.error} className="mb-3" />
255 + <HonestEmpty title="Comparison unavailable" detail="the index API did not answer; the selection is kept in the URL" />
256 + </>
257 + ) : (
258 + <>
259 + {missing.length > 0 && <HonestEmpty className="mb-3" title={`Unknown operator ${missing.length === 1 ? "slug" : "slugs"}: ${missing.join(", ")}`} detail="not in the index — remove the chip or search again" />}
260 + {ops.length >= 2 ? (
261 + <Section title="Side by side" description={`${ops.length} operators · ${r?.data.generatedAt ? `computed ${new Date(r.data.generatedAt).toISOString().slice(0, 10)} UTC` : ""}`}>
262 + <ComparisonGrid ops={ops} />
263 + <PublishedOnlyNote className="mt-3" extra={<>Capacity coverage = facilities with any MW figure ÷ facilities ({ops.map((o) => `${o.name} ${fmtPct(o.coverage ?? o.mwCoverage ?? 0)}`).join(" · ")}). </>} />
264 + </Section>
265 + ) : (
266 + <HonestEmpty title="Fewer than two operators could be resolved" detail="check the slugs in the URL" />
267 + )}
268 + </>
269 + )}
270 + </div>
271 +
272 + <SourcesFooter sources={r?.ok ? r.sources : undefined} note="Comparison rows are read from each operator's record: counts are exact, MW figures are lower bounds from published values, and 'new facilities' rely on opening dates or first observation (see each operator's velocity section)." />
273 + </Page>
274 + );
275 +}
added apps/web/src/app/(site)/connectivity/loading.tsx +5 −0
@@ -0,0 +1,5 @@
1 +import { IndexLoading } from "@/components/intelligence/index-loading";
2 +
3 +export default function Loading() {
4 + return <IndexLoading />;
5 +}
added apps/web/src/app/(site)/connectivity/page.tsx +291 −0
@@ -0,0 +1,291 @@
1 +import type { CloudRegionSummary } from "@dci/core";
2 +import type { Metadata } from "next";
3 +import Link from "next/link";
4 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
5 +import { SectionNav } from "@/components/entity/section-nav";
6 +import { SourcesFooter } from "@/components/entity/sources-footer";
7 +import { Bars } from "@/components/intelligence/client-charts";
8 +import { ApiNote, CoverageBar, HonestEmpty, PillLink } from "@/components/intelligence/shared";
9 +import { Page, PageHeader, Section } from "@/components/ui/card";
10 +import { CountryFlag } from "@/components/ui/country-flag";
11 +import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
12 +import { ErrorState, OfflineNotice } from "@/components/ui/states";
13 +import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
14 +import { api } from "@/lib/api";
15 +import { fmtInt, placeLine } from "@/lib/format";
16 +import { routes } from "@/lib/routes";
17 +import { pageMetadata } from "@/lib/seo";
18 +
19 +export const revalidate = 120;
20 +
21 +export const metadata: Metadata = pageMetadata({
22 + title: "Connectivity",
23 + description: "Internet exchanges, cloud regions by provider, carrier hotels and connectivity density by market — from published IXP↔facility links and provider region lists. Submarine cable landing stations are listed only once a licensed source exists.",
24 + path: routes.connectivity(),
25 +});
26 +
27 +const IXP_LIMIT = 50;
28 +
29 +export default async function ConnectivityPage() {
30 + const r = await api.connectivity();
31 + if (!r.ok) {
32 + return (
33 + <Page className="py-6">
34 + <Breadcrumbs items={[{ name: "Connectivity", href: routes.connectivity() }]} className="mb-4" />
35 + <OfflineNotice detail={r.error} className="mb-4" />
36 + <ErrorState />
37 + </Page>
38 + );
39 + }
40 + const d = r.data;
41 + const ixps = [...d.ixps].sort((a, b) => b.facilityCount - a.facilityCount).slice(0, IXP_LIMIT);
42 +
43 + // cloud regions grouped by provider
44 + const byProvider = new Map<string, { provider: CloudRegionSummary["provider"]; regions: CloudRegionSummary[] }>();
45 + for (const cr of d.cloudRegions) {
46 + const g = byProvider.get(cr.provider.id) ?? { provider: cr.provider, regions: [] };
47 + g.regions.push(cr);
48 + byProvider.set(cr.provider.id, g);
49 + }
50 + const providers = Array.from(byProvider.values()).sort((a, b) => b.regions.length - a.regions.length);
51 + const statusSplit = (regions: CloudRegionSummary[]) => {
52 + const c: Record<string, number> = {};
53 + for (const x of regions) c[x.status] = (c[x.status] ?? 0) + 1;
54 + return Object.entries(c)
55 + .sort((a, b) => b[1] - a[1])
56 + .map(([k, v]) => `${v} ${k}`)
57 + .join(" · ");
58 + };
59 + const metrosCovered = d.byMetro.filter((m) => m.ixps > 0 || m.cloudRegions > 0 || m.facilitiesWithCarriers > 0).length;
60 +
61 + const sections = [
62 + { id: "ixps", label: "Internet exchanges", count: ixps.length },
63 + { id: "cloud", label: "Cloud regions", count: providers.length },
64 + { id: "carrier-hotels", label: "Carrier hotels", count: d.carrierHotels.length },
65 + { id: "metros", label: "By market", count: d.byMetro.length },
66 + { id: "landing", label: "Landing stations" },
67 + ];
68 +
69 + return (
70 + <Page>
71 + <Breadcrumbs items={[{ name: "Connectivity", href: routes.connectivity() }]} className="pt-3" />
72 + <PageHeader eyebrow="Intelligence" title="Connectivity" description="Where networks meet: internet exchanges, public cloud regions, carrier-dense facilities and the markets that concentrate them." aside={<PillLink href={routes.map("layer=connectivity")}>Connectivity layer on the map →</PillLink>} />
73 +
74 + <MetricRow cols={5}>
75 + <MetricCell>
76 + <Metric value={fmtInt(d.ixps.length)} label="Internet exchanges" note="with facility links" />
77 + </MetricCell>
78 + <MetricCell>
79 + <Metric value={fmtInt(d.cloudRegions.length)} label="Cloud regions" note={`${fmtInt(providers.length)} providers`} />
80 + </MetricCell>
81 + <MetricCell>
82 + <Metric value={fmtInt(d.carrierHotels.length)} label="Carrier hotels" note="type carrier hotel or ≥ 20 carriers" />
83 + </MetricCell>
84 + <MetricCell>
85 + <Metric value={fmtInt(metrosCovered)} label="Markets covered" note={`of ${fmtInt(d.byMetro.length)} listed`} />
86 + </MetricCell>
87 + <MetricCell>
88 + <Metric value={fmtInt(d.landingStations.length)} label="Landing stations" note={d.landingStations.length === 0 ? "no licensed connector yet" : "licensed source"} />
89 + </MetricCell>
90 + </MetricRow>
91 + <ApiNote note={d.note} label="From the index" className="mt-3" />
92 +
93 + <SectionNav items={sections} className="mt-4" />
94 +
95 + <div className="flex flex-col gap-8 pt-6">
96 + <Section id="ixps" title="Internet exchanges" description={`Top ${fmtInt(ixps.length)} by facilities where the exchange is present. Network counts are shown only when published.`} className="scroll-mt-28" aside={<Link href={routes.ixps()} className="text-[12.5px] text-ink-3 hover:text-ink">All exchanges →</Link>}>
97 + {ixps.length === 0 ? (
98 + <HonestEmpty title="No internet exchange linked to a facility yet" />
99 + ) : (
100 + <Table dense mobileCollapse={{ total: ixps.length, label: "exchanges" }}>
101 + <THead>
102 + <tr>
103 + <Th>Exchange</Th>
104 + <Th>Country</Th>
105 + <Th className="hidden md:table-cell">Market</Th>
106 + <Th numeric className="hidden sm:table-cell">
107 + Networks
108 + </Th>
109 + <Th numeric>Facilities</Th>
110 + <Th numeric className="hidden sm:table-cell">
111 + Operators
112 + </Th>
113 + </tr>
114 + </THead>
115 + <TBody>
116 + {ixps.map((x) => (
117 + <Tr key={x.id}>
118 + <Td primary>
119 + <RowLink href={routes.ixp(x.slug)}>{x.name}</RowLink>
120 + {x.nameLong && x.nameLong !== x.name && <div className="hidden text-[11.5px] text-ink-3 sm:block">{x.nameLong}</div>}
121 + </Td>
122 + <Td label="Country">
123 + <CountryFlag iso2={x.countryIso2} withName className="max-w-[140px] text-ink-2" />
124 + </Td>
125 + <Td label="Market" className="hidden md:table-cell" muted>
126 + {x.metro ? (
127 + <Link href={routes.market(x.metro.slug)} className="hover:text-ink hover:underline">
128 + {x.metro.name}
129 + </Link>
130 + ) : (
131 + (x.city ?? "—")
132 + )}
133 + </Td>
134 + <Td label="Networks" numeric className="hidden sm:table-cell">
135 + {x.networkCount === null ? <span className="text-ink-4">—</span> : fmtInt(x.networkCount)}
136 + </Td>
137 + <Td label="Facilities" numeric lead>
138 + {fmtInt(x.facilityCount)}
139 + </Td>
140 + <Td label="Operators" numeric className="hidden sm:table-cell">
141 + {fmtInt(x.operatorCount)}
142 + </Td>
143 + </Tr>
144 + ))}
145 + </TBody>
146 + </Table>
147 + )}
148 + </Section>
149 +
150 + <Section id="cloud" title="Cloud regions by provider" description="Public cloud regions as published by each provider; regions are associated with a market, not a facility, unless a source says so." className="scroll-mt-28" aside={<Link href={routes.cloudRegions()} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>
151 + {providers.length === 0 ? (
152 + <HonestEmpty title="No cloud region indexed" />
153 + ) : (
154 + <Bars
155 + showRank={false}
156 + items={providers.map((p) => ({
157 + label: p.provider.name,
158 + value: p.regions.length,
159 + href: `${routes.cloudRegions()}?provider=${encodeURIComponent(p.provider.slug)}`,
160 + secondary: statusSplit(p.regions),
161 + display: `${fmtInt(p.regions.length)} ${p.regions.length === 1 ? "region" : "regions"}`,
162 + }))}
163 + />
164 + )}
165 + </Section>
166 +
167 + <Section id="carrier-hotels" title="Carrier hotels" description="Facilities typed carrier hotel or with ≥ 20 published carriers, by carrier count." className="scroll-mt-28">
168 + {d.carrierHotels.length === 0 ? (
169 + <HonestEmpty title="No carrier hotel identified" detail="carrier counts come from published facility pages and registries" />
170 + ) : (
171 + <Table dense mobileCollapse={{ total: d.carrierHotels.length, label: "facilities" }}>
172 + <THead>
173 + <tr>
174 + <Th>Facility</Th>
175 + <Th className="hidden sm:table-cell">Operator</Th>
176 + <Th>Location</Th>
177 + <Th numeric>Carriers</Th>
178 + <Th numeric className="hidden sm:table-cell">
179 + IXPs
180 + </Th>
181 + </tr>
182 + </THead>
183 + <TBody>
184 + {d.carrierHotels.map((f) => (
185 + <Tr key={f.id}>
186 + <Td primary>
187 + <RowLink href={routes.facility(f.slug)}>{f.name}</RowLink>
188 + <div className="text-[11.5px] text-ink-3 sm:hidden">{f.operator?.name ?? "Operator unknown"}</div>
189 + </Td>
190 + <Td label="Operator" className="hidden sm:table-cell">
191 + {f.operator ? (
192 + <Link href={routes.operator(f.operator.slug)} className="text-ink-2 hover:text-ink hover:underline">
193 + {f.operator.name}
194 + </Link>
195 + ) : (
196 + <span className="text-ink-4">—</span>
197 + )}
198 + </Td>
199 + <Td label="Location" muted>
200 + <span className="inline-flex items-center gap-1.5">
201 + <CountryFlag iso2={f.countryIso2} size={12} />
202 + <span className="truncate">{placeLine(f)}</span>
203 + </span>
204 + </Td>
205 + <Td label="Carriers" numeric lead>
206 + {f.carriersCount === null || f.carriersCount === undefined ? <span className="text-ink-4">—</span> : fmtInt(f.carriersCount)}
207 + </Td>
208 + <Td label="IXPs" numeric className="hidden sm:table-cell">
209 + {f.ixpCount === null || f.ixpCount === undefined ? <span className="text-ink-4">—</span> : fmtInt(f.ixpCount)}
210 + </Td>
211 + </Tr>
212 + ))}
213 + </TBody>
214 + </Table>
215 + )}
216 + </Section>
217 +
218 + <Section id="metros" title="By market" description="Exchanges, cloud regions and the share of facilities with published carriers, per market." className="scroll-mt-28">
219 + {d.byMetro.length === 0 ? (
220 + <HonestEmpty title="No market with connectivity data" />
221 + ) : (
222 + <Table dense mobileCollapse={{ total: d.byMetro.length, label: "markets" }}>
223 + <THead>
224 + <tr>
225 + <Th>Market</Th>
226 + <Th numeric>IXPs</Th>
227 + <Th numeric>Cloud regions</Th>
228 + <Th className="hidden sm:table-cell">Facilities with carriers</Th>
229 + <Th numeric className="hidden md:table-cell">
230 + Facilities
231 + </Th>
232 + </tr>
233 + </THead>
234 + <TBody>
235 + {d.byMetro.map((m) => (
236 + <Tr key={m.id}>
237 + <Td primary>
238 + <RowLink href={routes.market(m.slug)}>
239 + <span className="inline-flex items-center gap-1.5">
240 + <CountryFlag iso2={m.countryIso2} size={12} />
241 + {m.name}
242 + </span>
243 + </RowLink>
244 + </Td>
245 + <Td label="IXPs" numeric lead>
246 + {fmtInt(m.ixps)}
247 + </Td>
248 + <Td label="Cloud regions" numeric>
249 + {fmtInt(m.cloudRegions)}
250 + </Td>
251 + <Td label="Facilities with carriers" className="hidden sm:table-cell">
252 + <span className="inline-flex items-center gap-2">
253 + <CoverageBar value={m.facilities ? m.facilitiesWithCarriers / m.facilities : null} width="w-[110px]" label="Facilities with published carriers" />
254 + <span className="figure text-[11.5px] text-ink-3">
255 + {fmtInt(m.facilitiesWithCarriers)}/{fmtInt(m.facilities)}
256 + </span>
257 + </span>
258 + </Td>
259 + <Td label="Facilities" numeric className="hidden md:table-cell">
260 + {fmtInt(m.facilities)}
261 + </Td>
262 + </Tr>
263 + ))}
264 + </TBody>
265 + </Table>
266 + )}
267 + </Section>
268 +
269 + <Section id="landing" title="Submarine cable landing stations" description="Listed only from a licensed source." className="scroll-mt-28">
270 + {d.landingStations.length === 0 ? (
271 + <HonestEmpty title="No landing station indexed" detail="submarine cable landing stations are empty until a licensed connector exists — never inferred from proximity or news" />
272 + ) : (
273 + <ul className="divide-y divide-hair">
274 + {d.landingStations.map((l) => (
275 + <li key={l.id} className="flex items-center justify-between gap-3 py-2 text-[13px]">
276 + <span className="inline-flex items-center gap-1.5 text-ink">
277 + <CountryFlag iso2={l.countryIso2} size={12} />
278 + {l.name}
279 + </span>
280 + <span className="text-[11.5px] text-ink-3">{l.sourceName}</span>
281 + </li>
282 + ))}
283 + </ul>
284 + )}
285 + </Section>
286 + </div>
287 +
288 + <SourcesFooter sources={r.sources} note="IXP presence comes from published exchange↔facility links; carrier counts and cloud regions are taken as published. Nothing here is inferred from geography." />
289 + </Page>
290 + );
291 +}
modified apps/web/src/app/(site)/countries/[slug]/page.tsx +164 −64
@@ -5,11 +5,15 @@ import { notFound } from "next/navigation";
5 5 import { Breadcrumbs } from "@/components/entity/breadcrumbs";
6 6 import { EntityMap } from "@/components/entity/entity-map";
7 7 import { JsonLd } from "@/components/entity/json-ld";
8 −import { KeyValueList } from "@/components/entity/kv";
9 8 import { fPageHref, PagedFacilities, parseFPage } from "@/components/entity/paged-facilities";
10 9 import { SectionNav } from "@/components/entity/section-nav";
11 10 import { SourcesFooter } from "@/components/entity/sources-footer";
12 11 import { EventList } from "@/components/event-list";
12 +import { ClaimsTable } from "@/components/evidence";
13 +import { FacilityTable } from "@/components/facility-table";
14 +import { EnergyContextPanel } from "@/components/intelligence/country/energy";
15 +import { CoverageBar, CoverageStrip, GridConstraintList, HonestEmpty, PillLink, PipelineBars, PublishedOnlyNote, ShareList } from "@/components/intelligence/shared";
16 +import { WatchButton } from "@/components/intelligence/watch-button";
13 17 import { ProjectList } from "@/components/project-list";
14 18 import { Badge, EstimateMark } from "@/components/ui/badge";
15 19 import { Page, Section } from "@/components/ui/card";
@@ -39,13 +43,13 @@ const zoomFor = (iso2: string) => (HUGE.has(iso2) ? 5 : SMALL.has(iso2) ? 7 : 5.
39 43
40 44 function describe(c: CountryDetail): string {
41 45 const bits = [`${fmtInt(c.facilityCount)} data center ${pluralize(c.facilityCount, "facility", "facilities")}`];
42 − if (c.knownMw) bits.push(`${fmtMw(c.knownMw)} known operational capacity`);
46 + if (c.knownMw) bits.push(`${fmtMw(c.knownMw)} known operational capacity (${fmtPct(c.mwCoverage)} of facilities publish a figure)`);
43 47 const pipe = (c.constructionMw ?? 0) + (c.plannedMw ?? 0);
44 48 if (pipe) bits.push(`≈ ${fmtMw(pipe)} in the pipeline`);
45 49 bits.push(`${fmtInt(c.operatorCount)} ${pluralize(c.operatorCount, "operator")}`);
46 50 bits.push(`${fmtInt(c.cloudRegionCount)} cloud ${pluralize(c.cloudRegionCount, "region")}`);
47 51 if (c.hyperscaleCount) bits.push(`${fmtInt(c.hyperscaleCount)} hyperscale`);
48 − return `Data centers in ${c.name}: ${bits.join(", ")}. Markets, operators, cloud regions, projects and the change history — every figure sourced.`;
52 + return `Data centers in ${c.name}: ${bits.join(", ")}. Markets, operators, project pipeline, AI infrastructure, cloud footprint, internet exchanges, energy context and grid constraints — every figure sourced.`;
49 53 }
50 54
51 55 export async function generateMetadata({ params }: Props): Promise<Metadata> {
@@ -65,7 +69,9 @@ function Mw({ mw, estimate }: { mw: number | null | undefined; estimate?: boolea
65 69 </>
66 70 );
67 71 }
68 −const unitOf = (mw: number | null | undefined) => (splitMw(mw)?.unit ?? undefined);
72 +const unitOf = (mw: number | null | undefined) => splitMw(mw)?.unit ?? undefined;
73 +
74 +const IXP_ROWS = 15;
69 75
70 76 export default async function CountryPage({ params, searchParams }: Props) {
71 77 const [{ slug }, sp] = await Promise.all([params, searchParams]);
@@ -85,13 +91,19 @@ export default async function CountryPage({ params, searchParams }: Props) {
85 91 const facTotal = meta.facilitiesTotal ?? c.facilityCount;
86 92 const facPer = meta.facilitiesPerPage ?? 50;
87 93 const base = routes.country(c.slug);
88 − const coverage = fmtPct(c.mwCoverage);
89 94 const statusTotal = Object.values(c.statusBreakdown).reduce((a, b) => a + (b ?? 0), 0);
90 95 const types = (Object.entries(c.typeBreakdown) as Array<[FacilityType, number]>).filter(([, n]) => n > 0).sort((a, b) => b[1] - a[1]);
91 96 const metros = [...c.metros].sort((a, b) => b.facilityCount - a.facilityCount);
92 97 const topOps = [...c.topOperators].sort((a, b) => b.facilityCount - a.facilityCount).slice(0, 10);
93 98 const cloud = [...c.cloudRegions].sort((a, b) => a.provider.name.localeCompare(b.provider.name) || a.code.localeCompare(b.code));
94 − const hasElectricity = c.renewableShare !== null || c.electricityTwh !== null;
99 + const ixps = [...(c.ixps ?? [])].sort((a, b) => b.facilityCount - a.facilityCount || a.name.localeCompare(b.name));
100 + const ixpCount = c.ixpCount ?? ixps.length;
101 + const aiFacilities = c.aiFacilities ?? [];
102 + const aiProjects = c.aiProjects ?? [];
103 + const gridConstraints = c.gridConstraints ?? [];
104 + const claims = c.claims ?? [];
105 + const hasAi = aiFacilities.length > 0 || aiProjects.length > 0;
106 + const facTotalForShare = c.facilityCount || 1;
95 107
96 108 const jsonLd = compactJsonLd({
97 109 "@context": "https://schema.org",
@@ -105,15 +117,21 @@ export default async function CountryPage({ params, searchParams }: Props) {
105 117
106 118 const sections = [
107 119 { id: "map", label: "Map", count: c.facilityCount },
108 − { id: "growth", label: "Growth", count: c.growth.length },
120 + { id: "growth", label: "Growth" },
109 121 { id: "breakdown", label: "Breakdown", count: statusTotal },
110 − { id: "operators", label: "Operators", count: topOps.length },
111 122 { id: "markets", label: "Markets", count: metros.length },
112 − { id: "cloud", label: "Cloud regions", count: cloud.length },
123 + { id: "operators", label: "Operators", count: topOps.length },
124 + { id: "pipeline", label: "Pipeline" },
125 + { id: "ai", label: "AI", count: hasAi ? aiFacilities.length + aiProjects.length : 0 },
126 + { id: "cloud", label: "Cloud", count: cloud.length },
127 + { id: "connectivity", label: "Connectivity" },
113 128 { id: "projects", label: "Projects", count: c.recentProjects.length },
114 129 { id: "changes", label: "Changes", count: c.recentEvents.length },
115 − { id: "electricity", label: "Electricity", count: hasElectricity ? 1 : 0 },
130 + { id: "energy", label: "Energy" },
131 + { id: "grid", label: "Grid" },
132 + { id: "coverage", label: "Coverage" },
116 133 { id: "facilities", label: "Facilities", count: facTotal },
134 + { id: "claims", label: "Claims", count: claims.length },
117 135 ];
118 136
119 137 return (
@@ -124,24 +142,32 @@ export default async function CountryPage({ params, searchParams }: Props) {
124 142 </div>
125 143
126 144 <header className="py-5 sm:py-7">
127 − <div className="label-caps">Country · {c.iso2}</div>
128 − <h1 className="mt-1.5 flex items-center gap-2.5 text-[24px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">
129 − <CountryFlag iso2={c.iso2} size={26} />
130 − <span>Data centers in {c.name}</span>
131 − </h1>
132 − <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-2">
133 − {(c.region || c.subregion) && <span>{[c.region, c.subregion].filter((v, i, a): v is string => Boolean(v) && a.indexOf(v) === i).join(" · ")}</span>}
134 − {c.population !== null && (
135 − <span className="figure text-ink-3" title="Population, latest World Bank vintage">
136 − {fmtCompact(c.population)} residents
137 − </span>
138 − )}
139 − {c.gdpUsd !== null && (
140 − <span className="figure text-ink-3" title="GDP, current US$, World Bank">
141 − {fmtUsd(c.gdpUsd)} GDP
142 − </span>
143 − )}
144 − {(c.population !== null || c.gdpUsd !== null) && <span className="text-[11px] text-ink-4">World Bank</span>}
145 + <div className="flex flex-wrap items-start justify-between gap-3">
146 + <div className="min-w-0">
147 + <div className="label-caps">Country · {c.iso2}</div>
148 + <h1 className="mt-1.5 flex items-center gap-2.5 text-[24px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">
149 + <CountryFlag iso2={c.iso2} size={26} />
150 + <span>Data centers in {c.name}</span>
151 + </h1>
152 + <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-2">
153 + {(c.region || c.subregion) && <span>{[c.region, c.subregion].filter((v, i, a): v is string => Boolean(v) && a.indexOf(v) === i).join(" · ")}</span>}
154 + {c.population !== null && (
155 + <span className="figure text-ink-3" title="Population, latest World Bank vintage">
156 + {fmtCompact(c.population)} residents
157 + </span>
158 + )}
159 + {c.gdpUsd !== null && (
160 + <span className="figure text-ink-3" title="GDP, current US$, World Bank">
161 + {fmtUsd(c.gdpUsd)} GDP
162 + </span>
163 + )}
164 + {(c.population !== null || c.gdpUsd !== null) && <span className="text-[11px] text-ink-4">World Bank</span>}
165 + </div>
166 + </div>
167 + <div className="flex flex-wrap items-center gap-2">
168 + <WatchButton entityType="country" slug={c.slug} name={c.name} />
169 + <PillLink href={routes.explore(`entity=facilities&country=${c.iso2}`)}>Explore →</PillLink>
170 + </div>
145 171 </div>
146 172 </header>
147 173
@@ -150,19 +176,19 @@ export default async function CountryPage({ params, searchParams }: Props) {
150 176 <Metric value={fmtInt(c.facilityCount)} label="Facilities" note={`${fmtInt(c.operationalCount)} operational`} />
151 177 </MetricCell>
152 178 <MetricCell>
153 − <Metric value={<Mw mw={c.knownMw} />} unit={unitOf(c.knownMw)} label="Operational MW" note={c.knownMw ? `${coverage} of facilities publish MW` : `no published figure · coverage ${coverage}`} />
179 + <Metric value={<Mw mw={c.knownMw} />} unit={unitOf(c.knownMw)} label="Operational MW" note={<CoverageBar value={c.mwCoverage} label="MW coverage" width="w-full max-w-[150px]" />} />
154 180 </MetricCell>
155 181 <MetricCell>
156 − <Metric value={<Mw mw={c.constructionMw} estimate />} unit={unitOf(c.constructionMw)} label="Under construction" note={`${fmtInt(c.constructionCount)} ${pluralize(c.constructionCount, "site")}`} />
182 + <Metric value={<Mw mw={c.constructionMw} estimate />} unit={unitOf(c.constructionMw)} label="Under construction" note={`${fmtInt(c.constructionCount)} ${pluralize(c.constructionCount, "site")}${c.constructionMw === null && c.constructionCount ? ", MW not published" : ""}`} />
157 183 </MetricCell>
158 184 <MetricCell>
159 − <Metric value={<Mw mw={c.plannedMw} estimate />} unit={unitOf(c.plannedMw)} label="Planned MW" note={`${fmtInt(c.plannedCount)} planned ${pluralize(c.plannedCount, "site")}`} />
185 + <Metric value={<Mw mw={c.plannedMw} estimate />} unit={unitOf(c.plannedMw)} label="Planned MW" note={`${fmtInt(c.plannedCount)} planned ${pluralize(c.plannedCount, "site")}${c.plannedMw === null && c.plannedCount ? ", MW not published" : ""}`} />
160 186 </MetricCell>
161 187 <MetricCell>
162 − <Metric value={c.announcedInvestmentUsd ? fmtUsd(c.announcedInvestmentUsd) : "—"} label="Announced investment" note={c.announcedInvestmentUsd ? "projects, published" : "not published"} />
188 + <Metric value={c.announcedInvestmentUsd ? fmtUsd(c.announcedInvestmentUsd) : "—"} label="Announced investment" note={c.announcedInvestmentUsd ? "site-scoped project figures, published" : "not published"} />
163 189 </MetricCell>
164 190 </MetricRow>
165 − <MetricRow cols={4} className="border-t-0 [&>div]:border-t-0">
191 + <MetricRow cols={5} className="border-t-0 [&>div]:border-t-0">
166 192 <MetricCell>
167 193 <Metric size="sm" value={fmtInt(c.operatorCount)} label="Operators" />
168 194 </MetricCell>
@@ -173,7 +199,10 @@ export default async function CountryPage({ params, searchParams }: Props) {
173 199 <Metric size="sm" value={fmtInt(c.hyperscaleCount)} label="Hyperscale" />
174 200 </MetricCell>
175 201 <MetricCell>
176 − <Metric size="sm" value={fmtInt(c.aiCount)} label="AI facilities" />
202 + <Metric size="sm" value={fmtInt(c.aiCount)} label="AI facilities" note="confirmed + likely" />
203 + </MetricCell>
204 + <MetricCell>
205 + <Metric size="sm" value={fmtInt(ixpCount)} label="Internet exchanges" />
177 206 </MetricCell>
178 207 </MetricRow>
179 208
@@ -181,7 +210,7 @@ export default async function CountryPage({ params, searchParams }: Props) {
181 210 <div className="-mx-3 mt-3 flex items-center gap-1.5 overflow-x-auto px-3 no-scrollbar sm:mx-0 sm:flex-wrap sm:px-0" aria-label="Ranking positions">
182 211 <span className="label-caps shrink-0">Rankings</span>
183 212 {c.rankings.map((rk) => (
184 − <Link key={rk.key} href={routes.ranking(rk.key)} className="inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-sm border border-hair bg-surface px-2 text-[12px] text-ink hover:bg-surface-2">
213 + <Link key={rk.key} href={routes.ranking(rk.key)} className="inline-flex h-10 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-sm border border-hair bg-surface px-2 text-[12px] text-ink hover:bg-surface-2 sm:h-7">
185 214 <span className="figure font-medium">
186 215 #{rk.rank}
187 216 <span className="text-ink-4">/{rk.total}</span>
@@ -216,21 +245,8 @@ export default async function CountryPage({ params, searchParams }: Props) {
216 245 </div>
217 246 </Section>
218 247
219 − {topOps.length > 0 && (
220 − <Section id="operators" title="Top operators" description={`${fmtInt(c.operatorCount)} ${pluralize(c.operatorCount, "operator")} with facilities in ${c.name}`} className="scroll-mt-28" aside={<Link href={routes.datacenters(`country=${c.iso2}`)} className="text-[12.5px] text-ink-3 hover:text-ink">All facilities →</Link>}>
221 − <CountBarList
222 − items={topOps.map((o) => ({
223 − label: o.name,
224 − value: o.facilityCount,
225 − href: routes.operator(o.slug),
226 − secondary: o.kind ? OPERATOR_KIND_LABEL[o.kind] : undefined,
227 − }))}
228 − />
229 − </Section>
230 − )}
231 −
232 248 {metros.length > 0 && (
233 − <Section id="markets" title="Markets" description={`${fmtInt(metros.length)} ${pluralize(metros.length, "metro")} with indexed infrastructure`} className="scroll-mt-28">
249 + <Section id="markets" title="Top markets" description={`${fmtInt(metros.length)} ${pluralize(metros.length, "metro")} with indexed infrastructure`} className="scroll-mt-28" aside={<Link href={routes.markets()} className="text-[12.5px] text-ink-3 hover:text-ink">All markets →</Link>}>
234 250 <Table dense mobileCollapse={{ total: metros.length, label: "markets" }}>
235 251 <THead>
236 252 <tr>
@@ -291,8 +307,36 @@ export default async function CountryPage({ params, searchParams }: Props) {
291 307 </Section>
292 308 )}
293 309
310 + {topOps.length > 0 && (
311 + <Section id="operators" title="Top operators" description={`${fmtInt(c.operatorCount)} ${pluralize(c.operatorCount, "operator")} with facilities in ${c.name}; share of the country's facility count`} className="scroll-mt-28" aside={<Link href={routes.datacenters(`country=${c.iso2}`)} className="text-[12.5px] text-ink-3 hover:text-ink">All facilities →</Link>}>
312 + <ShareList items={topOps.map((o) => ({ key: o.id, label: <>{o.name}{o.kind && <span className="ml-1.5 text-[11px] text-ink-3">{OPERATOR_KIND_LABEL[o.kind]}</span>}</>, href: routes.operator(o.slug), count: o.facilityCount, share: o.facilityCount / facTotalForShare, mw: o.knownMw ?? null }))} unit="facilities" />
313 + </Section>
314 + )}
315 +
316 + <Section id="pipeline" title="Project pipeline" description="Facilities by lifecycle bucket with the MW they publish, then project records by stage." className="scroll-mt-28" aside={<Link href={routes.projects(`country=${c.iso2}`)} className="text-[12.5px] text-ink-3 hover:text-ink">All projects →</Link>}>
317 + {c.pipeline ? <PipelineBars pipeline={c.pipeline} /> : <HonestEmpty title="Pipeline breakdown unavailable" />}
318 + <PublishedOnlyNote className="mt-3" />
319 + </Section>
320 +
321 + {hasAi && (
322 + <Section id="ai" title="AI infrastructure" description="Facilities and projects with confirmed or likely AI / HPC evidence. A single keyword never confirms a site." className="scroll-mt-28" aside={<Link href={routes.ai()} className="text-[12.5px] text-ink-3 hover:text-ink">AI infrastructure index →</Link>}>
323 + {aiFacilities.length > 0 && (
324 + <div className="mb-4">
325 + <div className="label-caps mb-2">Facilities ({fmtInt(aiFacilities.length)})</div>
326 + <FacilityTable items={aiFacilities} columns={["operator", "market", "status", "mw", "type"]} />
327 + </div>
328 + )}
329 + {aiProjects.length > 0 && (
330 + <div>
331 + <div className="label-caps mb-2">Projects ({fmtInt(aiProjects.length)})</div>
332 + <ProjectList items={aiProjects} />
333 + </div>
334 + )}
335 + </Section>
336 + )}
337 +
294 338 {cloud.length > 0 && (
295 − <Section id="cloud" title="Cloud regions" description={`${fmtInt(cloud.length)} public cloud ${pluralize(cloud.length, "region")} located in ${c.name}`} className="scroll-mt-28" aside={<Link href={`${routes.cloudRegions()}?country=${c.iso2}`} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>
339 + <Section id="cloud" title="Cloud footprint" description={`${fmtInt(cloud.length)} public cloud ${pluralize(cloud.length, "region")} located in ${c.name} — associated with a market, not a specific facility unless a source says so`} className="scroll-mt-28" aside={<Link href={`${routes.cloudRegions()}?country=${c.iso2}`} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>
296 340 <Table dense mobileCollapse={{ total: cloud.length, label: "cloud regions" }}>
297 341 <THead>
298 342 <tr>
@@ -350,6 +394,59 @@ export default async function CountryPage({ params, searchParams }: Props) {
350 394 </Section>
351 395 )}
352 396
397 + <Section id="connectivity" title="Connectivity" description={`Internet exchanges with a published presence in ${c.name}'s facilities (PeeringDB and exchange operators).`} className="scroll-mt-28" aside={<Link href={`${routes.ixps()}?country=${c.iso2}`} className="text-[12.5px] text-ink-3 hover:text-ink">All {fmtInt(ixpCount)} IXPs →</Link>}>
398 + {ixps.length ? (
399 + <>
400 + <Table dense mobileCollapse={{ total: Math.min(ixps.length, IXP_ROWS), label: "exchanges" }}>
401 + <THead>
402 + <tr>
403 + <Th>Exchange</Th>
404 + <Th className="hidden sm:table-cell">City</Th>
405 + <Th numeric>Networks</Th>
406 + <Th numeric>Facilities</Th>
407 + </tr>
408 + </THead>
409 + <TBody>
410 + {ixps.slice(0, IXP_ROWS).map((x) => (
411 + <Tr key={x.id}>
412 + <Td primary>
413 + <RowLink href={routes.ixp(x.slug)}>{x.name}</RowLink>
414 + {x.nameLong && x.nameLong !== x.name && <span className="ml-1.5 hidden text-[11.5px] text-ink-3 md:inline">{x.nameLong}</span>}
415 + </Td>
416 + <Td label="City" className="hidden sm:table-cell" muted>
417 + {x.city ?? (x.metro?.name ?? "—")}
418 + </Td>
419 + <Td label="Networks" numeric>
420 + {x.networkCount === null ? <span className="text-ink-4" title="Not published">—</span> : fmtInt(x.networkCount)}
421 + </Td>
422 + <Td label="Facilities" numeric lead>
423 + {fmtInt(x.facilityCount)}
424 + </Td>
425 + </Tr>
426 + ))}
427 + </TBody>
428 + </Table>
429 + {ixps.length > IXP_ROWS && (
430 + <p className="mt-2 text-[11.5px] text-ink-4">
431 + First {IXP_ROWS} of {fmtInt(ixps.length)} exchanges by facility presence —{" "}
432 + <Link href={`${routes.ixps()}?country=${c.iso2}`} className="underline decoration-hair-2 hover:text-ink">
433 + see all
434 + </Link>
435 + .
436 + </p>
437 + )}
438 + </>
439 + ) : (
440 + <HonestEmpty title="No internet exchange presence recorded" detail="IXP presence comes from published IXP ↔ facility links; none is inferred" />
441 + )}
442 + <p className="mt-2 text-[11px] text-ink-4">
443 + Network counts are shown only when the exchange publishes them.{" "}
444 + <Link href={routes.connectivity()} className="underline decoration-hair-2 hover:text-ink">
445 + Connectivity index
446 + </Link>
447 + </p>
448 + </Section>
449 +
353 450 {c.recentProjects.length > 0 && (
354 451 <Section id="projects" title="Recent projects" description="Announced and under-construction developments." className="scroll-mt-28" aside={<Link href={routes.projects(`country=${c.iso2}`)} className="text-[12.5px] text-ink-3 hover:text-ink">All projects →</Link>}>
355 452 <ProjectList items={c.recentProjects} />
@@ -362,24 +459,27 @@ export default async function CountryPage({ params, searchParams }: Props) {
362 459 </Section>
363 460 )}
364 461
365 − {hasElectricity && (
366 − <Section id="electricity" title="Electricity context" description="National indicators for reading capacity figures against the grid." className="scroll-mt-28">
367 − <KeyValueList
368 − items={[
369 − { label: "Renewable share", value: c.renewableShare !== null ? <span className="figure">{fmtPct(c.renewableShare / 100, 1)} of electricity output</span> : null },
370 − { label: "Electricity", value: c.electricityTwh !== null ? <span className="figure">{fmtInt(c.electricityTwh)} TWh / year</span> : null },
371 − { label: "Known DC load", value: c.knownMw && c.electricityTwh ? <span className="figure" title="Known operational MW × 8 760 h ÷ national electricity output (upper bound: assumes full utilisation)">≤ {fmtPct((c.knownMw * 8.76) / 1000 / c.electricityTwh, 2)} of output at full load</span> : null },
372 − ]}
373 − />
374 − <p className="mt-2 text-[11.5px] text-ink-4">
375 − Source: World Bank, World Development Indicators (CC BY 4.0) — latest available vintage.
376 − </p>
377 − </Section>
378 − )}
462 + <Section id="energy" title="Power & energy context" description="National electricity indicators for reading capacity figures against the grid." className="scroll-mt-28" aside={<Link href={routes.power()} className="text-[12.5px] text-ink-3 hover:text-ink">Power & grid →</Link>}>
463 + {c.energy ? <EnergyContextPanel e={c.energy} knownMw={c.knownMw} /> : <HonestEmpty title="No national electricity indicators recorded" />}
464 + </Section>
465 +
466 + <Section id="grid" title="Grid constraints" description="Moratoria, grid delays, load caps, new transmission, regulation and large-load queues reported for this country — observed events and curated public reports only, nothing inferred." className="scroll-mt-28">
467 + <GridConstraintList items={gridConstraints} emptyDetail="no report or grid event has been indexed for this country; that is an absence of reports, not proof of an unconstrained grid" />
468 + </Section>
469 +
470 + <Section id="coverage" title="Coverage" description="How much of this country the index actually knows — shown so the figures above can be read honestly." className="scroll-mt-28" aside={<Link href={routes.coverage()} className="text-[12.5px] text-ink-3 hover:text-ink">Coverage report →</Link>}>
471 + {c.coverage ? <CoverageStrip row={c.coverage} /> : <HonestEmpty title="Coverage not computed" />}
472 + </Section>
379 473
380 474 <Section id="facilities" title="Facilities" description={`${fmtInt(facTotal)} indexed · sorted by known capacity`} className="scroll-mt-28" aside={<Link href={routes.datacenters(`country=${c.iso2}`)} className="text-[12.5px] text-ink-3 hover:text-ink">Filter & export →</Link>}>
381 475 <PagedFacilities items={c.facilities} total={facTotal} page={fPage} perPage={facPer} hrefFor={(p) => fPageHref(base, sp, p)} columns={["operator", "market", "status", "mw", "type", "confidence"]} />
382 476 </Section>
477 +
478 + {claims.length > 0 && (
479 + <Section id="claims" title="Claims about this country" description="Country-scoped figures (national programmes, aggregate capacity statements) — shown as evidence, never summed into facility totals." className="scroll-mt-28">
480 + <ClaimsTable claims={claims} kind="all" />
481 + </Section>
482 + )}
383 483 </div>
384 484
385 485 <SourcesFooter sources={r.sources} note="Country totals are lower bounds computed from published or filed values only; coverage shows the share of facilities with a capacity figure. Population, GDP and electricity: World Bank (CC BY 4.0)." />
modified apps/web/src/app/(site)/datacenters/[slug]/page.tsx +593 −513
@@ -1,7 +1,8 @@
1 −import type { FacilityDetail, FacilitySummary } from "@dci/core";
1 +import type { EventDTO, FacilityDetail } from "@dci/core";
2 2 import { ExternalLink } from "lucide-react";
3 3 import type { Metadata } from "next";
4 4 import Link from "next/link";
5 +import type { ReactNode } from "react";
5 6 import { Breadcrumbs, type Crumb } from "@/components/entity/breadcrumbs";
6 7 import { EntityMap } from "@/components/entity/entity-map";
7 8 import { JsonLd } from "@/components/entity/json-ld";
@@ -10,18 +11,20 @@ import { ProvenanceTable, WithProvenance, provFor } from "@/components/entity/pr
10 11 import { SectionNav } from "@/components/entity/section-nav";
11 12 import { SourcesFooter } from "@/components/entity/sources-footer";
12 13 import { EventList } from "@/components/event-list";
14 +import { CapacityHistoryChart, ClaimsTable, DataQualityPanel, Evidence, EvidenceProvider, NearbyInfrastructurePanel } from "@/components/evidence";
15 +import { FacilityTable } from "@/components/facility-table";
13 16 import { ProjectList } from "@/components/project-list";
14 −import { Badge, ConfidenceBadge, EstimateMark, FlagBadges, PrecisionBadge, StatusBadge, TypeBadge } from "@/components/ui/badge";
17 +import { AiMark, Badge, ConfidenceBadge, EstimateMark, FlagBadges, PrecisionBadge, ScopeBadge, SemanticsLabel, StatusBadge, TypeBadge } from "@/components/ui/badge";
15 18 import { Page, Section } from "@/components/ui/card";
16 19 import { CountryFlag } from "@/components/ui/country-flag";
17 −import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
18 20 import { ProvenancePopover } from "@/components/ui/popover";
21 +import { SourceBadge } from "@/components/ui/source-badge";
19 22 import { ErrorState, OfflineNotice } from "@/components/ui/states";
20 23 import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
21 −import { SourceHistory } from "@/components/ui/timeline";
24 +import { SourceHistory, TimelineList, type TimelineEntry } from "@/components/ui/timeline";
22 25 import { countryHref } from "@/lib/countries";
23 −import { bestMw, fmtDate, fmtInt, fmtMw, placeLine } from "@/lib/format";
24 −import { CONFIDENCE_LABEL, PRECISION_LABEL, STATUS_LABEL, TYPE_LABEL, isImprecise } from "@/lib/labels";
26 +import { bestMw, fmtDate, fmtInt, fmtMw, fmtPct, placeLine, splitMw } from "@/lib/format";
27 +import { CONFIDENCE_LABEL, EVENT_LABEL, PRECISION_LABEL, STATUS_LABEL, TYPE_LABEL, isImprecise } from "@/lib/labels";
25 28 import { routes } from "@/lib/routes";
26 29 import { NOT_FOUND_METADATA, absoluteUrl, compactJsonLd, pageMetadata } from "@/lib/seo";
27 30 import { getFacility, mwIsEstimate } from "./data";
@@ -32,14 +35,14 @@ type Params = { params: Promise<{ slug: string }> };
32 35
33 36 function describe(f: FacilityDetail): string {
34 37 const { mw, kind } = bestMw(f);
35 − const parts = [`${f.name} — ${TYPE_LABEL[f.facilityType].toLowerCase()} data center`];
38 + const parts = [`${f.name} — ${TYPE_LABEL[f.facilityType].toLowerCase()} data center${f.recordScope === "campus" ? " campus" : ""}`];
36 39 if (f.operator) parts[0] += ` operated by ${f.operator.name}`;
37 40 const place = placeLine(f);
38 41 if (place !== "—") parts[0] += ` in ${place}`;
39 42 parts.push(`Status: ${STATUS_LABEL[f.status].toLowerCase()}.`);
40 − if (mw !== null) parts.push(`${kind === "it" ? "IT capacity" : kind === "total" ? "Total power" : "Planned power"}: ${mwIsEstimate(f) ? "≈ " : ""}${fmtMw(mw)}.`);
43 + if (mw !== null) parts.push(`${kind === "it" ? "Known IT capacity" : kind === "total" ? "Total power" : "Planned build-out"}: ${mwIsEstimate(f) ? "≈ " : ""}${fmtMw(mw)} (published figure).`);
41 44 if (f.openedOn) parts.push(`Opened ${fmtDate(f.openedOn)}.`);
42 − parts.push("Sourced, versioned and tracked on DataCenterIndex.");
45 + parts.push("Every figure sourced, versioned and tracked on DataCenterIndex.");
43 46 return parts.join(" ");
44 47 }
45 48
@@ -54,14 +57,6 @@ export async function generateMetadata({ params }: Params): Promise<Metadata> {
54 57
55 58 /* ------------------------------------------------------------------ helpers */
56 59
57 −function haversineKm(a: { lat: number; lng: number }, b: { lat: number; lng: number }): number {
58 − const R = 6371;
59 − const dLat = ((b.lat - a.lat) * Math.PI) / 180;
60 − const dLng = ((b.lng - a.lng) * Math.PI) / 180;
61 − const s = Math.sin(dLat / 2) ** 2 + Math.cos((a.lat * Math.PI) / 180) * Math.cos((b.lat * Math.PI) / 180) * Math.sin(dLng / 2) ** 2;
62 − return 2 * R * Math.asin(Math.sqrt(s));
63 −}
64 −
65 60 const EXTERNAL_ID_LINKS: Record<string, { label: string; href: (v: string) => string }> = {
66 61 wikidata: { label: "Wikidata", href: (v) => `https://www.wikidata.org/wiki/${v}` },
67 62 wikipedia_en: { label: "Wikipedia", href: (v) => (v.startsWith("http") ? v : `https://en.wikipedia.org/wiki/${v}`) },
@@ -87,6 +82,48 @@ function completenessTone(c: number): string {
87 82 return c >= 70 ? "bg-ok" : c >= 40 ? "bg-ink-3" : "bg-warn";
88 83 }
89 84
85 +/** One header figure: caps label, big value (or badge), note line. */
86 +function HeaderFigure({ label, children, note, className }: { label: ReactNode; children: ReactNode; note?: ReactNode; className?: string }) {
87 + return (
88 + <div className={`min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0 ${className ?? ""}`}>
89 + <div className="label-caps mb-1 truncate">{label}</div>
90 + <div className="min-w-0">{children}</div>
91 + {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}
92 + </div>
93 + );
94 +}
95 +
96 +/** MW figure for the header: value + unit in mono, estimate mark, semantics + scope underneath; honest "Not published". */
97 +function MwFigure({ field, aliases, mw, estimate, semantics, scope, planned, fallbackNote }: { field: string; aliases?: string[]; mw: number | null; estimate?: boolean; semantics: string; scope?: string | null; planned?: boolean; fallbackNote: string }) {
98 + const s = splitMw(mw);
99 + if (!s) {
100 + return (
101 + <Evidence field={field} fields={aliases} display="—" block>
102 + <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>
103 + <div className="mt-1 text-[11px] leading-tight text-ink-3">{fallbackNote}</div>
104 + </Evidence>
105 + );
106 + }
107 + return (
108 + <Evidence field={field} fields={aliases} display={`${estimate || planned ? "≈ " : ""}${fmtMw(mw)}`} block>
109 + <div className="flex items-baseline gap-1 leading-none">
110 + {(estimate || planned) && <EstimateMark className="text-[14px]" />}
111 + <span className="figure text-[22px] font-medium tracking-[-0.03em] text-ink sm:text-[26px]">{s.value}</span>
112 + <span className="text-[12px] font-medium text-ink-3">{s.unit}</span>
113 + </div>
114 + <div className="mt-1 flex flex-wrap items-center gap-1.5">
115 + <SemanticsLabel semantics={semantics} />
116 + {scope && <ScopeBadge scope={scope} />}
117 + {estimate && <span className="text-[11px] text-ink-3">estimate</span>}
118 + </div>
119 + </Evidence>
120 + );
121 +}
122 +
123 +function eventDate(e: EventDTO): string {
124 + return e.effectiveDate ?? e.detectedAt;
125 +}
126 +
90 127 /* ------------------------------------------------------------------ page */
91 128
92 129 export default async function FacilityPage({ params }: Params) {
@@ -105,31 +142,56 @@ export default async function FacilityPage({ params }: Params) {
105 142
106 143 const f = r.data;
107 144 const prov = f.provenance ?? [];
145 + const claims = f.claims ?? [];
146 + const history = f.capacityHistory ?? [];
108 147 const est = mwIsEstimate(f);
109 148 const hasGeo = f.lat !== null && f.lng !== null;
110 149 const imprecise = isImprecise(f.geoPrecision);
111 − const anyMw = f.itCapacityMw !== null || f.totalPowerMw !== null || f.plannedPowerMw !== null;
112 150 const cHref = await countryHref(f.countryIso2, f.countryName);
113 151 const place = placeLine(f);
152 + const best = bestMw(f);
153 + const isCampus = f.recordScope === "campus" || f.buildings.length > 0;
154 + const parent = f.campus ?? f.parentFacility ?? null;
155 + const power = f.powerContext;
156 + const dq = f.dataQuality;
114 157
115 158 const crumbs: Crumb[] = [{ name: "Data centers", href: routes.datacenters() }];
116 159 if (f.metro) crumbs.push({ name: f.metro.name, href: routes.market(f.metro.slug) });
117 160 else if (cHref && f.countryName) crumbs.push({ name: f.countryName, href: cHref });
161 + if (parent) crumbs.push({ name: parent.name, href: routes.facility(parent.slug) });
118 162 crumbs.push({ name: f.name, href: routes.facility(f.slug) });
119 163
120 − const nearby = (f.nearby ?? []).map((n: FacilitySummary) => ({ ...n, km: hasGeo && n.lat !== null && n.lng !== null ? haversineKm({ lat: f.lat!, lng: f.lng! }, { lat: n.lat, lng: n.lng }) : null }));
121 164 const connectivityEmpty = !f.carriers.length && !f.cloudProviders.length && !f.ixps.length && !f.carriersCount && !f.ixpCount && !f.networksCount;
165 + const powerEmpty = f.utilityCapacityMw == null && f.gridConnectionMw == null && !power?.powerEvents.length && !power?.gridConstraints.length;
166 + const tenants = f.tenants.filter((t) => t.role !== "carrier");
167 + const carrierTenants = f.tenants.filter((t) => t.role === "carrier");
168 +
169 + // timeline: dated milestones + status / capacity events with an effective date
170 + const timeline: TimelineEntry[] = [];
171 + if (f.announcedOn) timeline.push({ date: f.announcedOn, title: "Announced", tone: "accent", meta: <WithProvenance items={prov} fields={["announcedOn", "announced_on"]}>as published</WithProvenance> });
172 + if (f.constructionStartedOn) timeline.push({ date: f.constructionStartedOn, title: "Construction started", tone: "warn", meta: <WithProvenance items={prov} fields={["constructionStartedOn", "construction_started_on"]}>as published</WithProvenance> });
173 + if (f.openedOn) timeline.push({ date: f.openedOn, title: "Opened", tone: "ok", meta: <WithProvenance items={prov} fields={["openedOn", "opened_on"]}>as published</WithProvenance> });
174 + for (const e of f.events.filter((e) => ["status_changed", "capacity_changed", "planned_capacity_changed", "opening_date_changed", "operator_changed", "owner_changed", "expansion_announced", "closure"].includes(e.eventType)).slice(0, 20)) {
175 + timeline.push({ date: eventDate(e), title: EVENT_LABEL[e.eventType], description: e.title !== f.name ? e.title : undefined, meta: <span>{e.sourceName}</span>, href: e.url, tone: e.eventType === "closure" ? "danger" : "neutral" });
176 + }
177 + timeline.push({ date: f.firstSeen, title: <span className="text-ink-2">First indexed</span>, tone: "neutral", meta: <span>{fmtInt(f.sourceCount ?? dq.sourceCount)} sources since</span> });
178 + if (f.lastVerified) timeline.push({ date: f.lastVerified, title: <span className="text-ink-2">Last verified against a primary source</span>, tone: "ok" });
179 + timeline.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
122 180
123 181 const sections = [
124 182 { id: "overview", label: "Overview" },
125 − { id: "location", label: "Location" },
183 + { id: "capacity", label: "Capacity" },
184 + { id: "timeline", label: "Timeline", count: timeline.length },
185 + { id: "ownership", label: "Operator / ownership" },
126 186 { id: "connectivity", label: "Connectivity" },
127 − { id: "cloud", label: "Cloud regions", count: f.cloudRegions.length },
128 − { id: "projects", label: "Projects", count: f.projects.length },
129 − { id: "nearby", label: "Nearby", count: nearby.length },
130 − { id: "provenance", label: "Provenance", count: prov.length },
131 − { id: "history", label: "Source history", count: f.sourceHistory.length },
132 − { id: "changes", label: "Changes", count: f.events.length },
187 + { id: "power", label: "Power" },
188 + { id: "cloud", label: "Cloud / tenants" },
189 + { id: "phases", label: "Project phases", count: f.projects.length },
190 + { id: isCampus ? "buildings" : "campus", label: isCampus ? "Buildings" : "Campus", count: isCampus ? f.buildings.length : parent ? 1 : 0 },
191 + { id: "nearby", label: "Nearby", count: hasGeo ? 1 : 0 },
192 + { id: "events", label: "Event history", count: f.events.length },
193 + { id: "evidence", label: "Source evidence", count: prov.length + claims.length },
194 + { id: "quality", label: "Data quality" },
133 195 ];
134 196
135 197 const jsonLd = compactJsonLd({
@@ -141,545 +203,563 @@ export default async function FacilityPage({ params }: Params) {
141 203 description: f.description ?? undefined,
142 204 url: absoluteUrl(routes.facility(f.slug)),
143 205 sameAs: [f.website, ...Object.entries(f.externalIds).map(([k, v]) => (EXTERNAL_ID_LINKS[k] ? EXTERNAL_ID_LINKS[k].href(String(v)) : null))].filter(Boolean),
144 − address: compactJsonLd({
145 − "@type": "PostalAddress",
146 − streetAddress: f.address ?? undefined,
147 − addressLocality: f.city ?? undefined,
148 − addressRegion: f.regionName ?? undefined,
149 − postalCode: f.postalCode ?? undefined,
150 − addressCountry: f.countryIso2 ?? undefined,
151 − }),
206 + address: compactJsonLd({ "@type": "PostalAddress", streetAddress: f.address ?? undefined, addressLocality: f.city ?? undefined, addressRegion: f.regionName ?? undefined, postalCode: f.postalCode ?? undefined, addressCountry: f.countryIso2 ?? undefined }),
152 207 geo: hasGeo && f.geoPrecision !== "unknown" ? { "@type": "GeoCoordinates", latitude: f.lat, longitude: f.lng } : undefined,
153 − containedInPlace: f.metro ? { "@type": "Place", name: f.metro.name, url: absoluteUrl(routes.market(f.metro.slug)) } : undefined,
208 + containedInPlace: parent ? { "@type": "Place", name: parent.name, url: absoluteUrl(routes.facility(parent.slug)) } : f.metro ? { "@type": "Place", name: f.metro.name, url: absoluteUrl(routes.market(f.metro.slug)) } : undefined,
154 209 additionalProperty: [
155 210 { "@type": "PropertyValue", name: "status", value: STATUS_LABEL[f.status] },
156 211 { "@type": "PropertyValue", name: "facilityType", value: TYPE_LABEL[f.facilityType] },
157 212 f.itCapacityMw !== null ? { "@type": "PropertyValue", name: "IT capacity", value: f.itCapacityMw, unitText: "MW" } : null,
158 213 f.totalPowerMw !== null ? { "@type": "PropertyValue", name: "Total power", value: f.totalPowerMw, unitText: "MW" } : null,
214 + f.plannedPowerMw !== null ? { "@type": "PropertyValue", name: "Planned power", value: f.plannedPowerMw, unitText: "MW" } : null,
159 215 { "@type": "PropertyValue", name: "geoPrecision", value: PRECISION_LABEL[f.geoPrecision] },
160 216 ].filter(Boolean),
161 217 });
162 218
163 − return (
164 − <Page className="pb-8">
165 − <JsonLd data={jsonLd} />
166 − <div className="pt-3">
167 − <Breadcrumbs items={crumbs} />
168 − </div>
219 + const opLink = (o: { slug: string; name: string } | null | undefined, empty: string) =>
220 + o ? (
221 + <Link href={routes.operator(o.slug)} className="text-ink hover:text-accent hover:underline">
222 + {o.name}
223 + </Link>
224 + ) : (
225 + <span className="text-[12.5px] text-ink-3">{empty}</span>
226 + );
169 227
170 − {/* ---------------------------------------------------------------- header */}
171 − <header className="py-4 sm:py-6">
172 − <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">
173 − <span>Data center</span>
174 − {f.operator && (
175 − <>
176 − <span aria-hidden className="text-ink-4">
177 − ·
178 − </span>
179 − <Link href={routes.operator(f.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">
180 − {f.operator.name}
181 − </Link>
182 − </>
183 − )}
184 − {f.owner && f.owner.id !== f.operator?.id && (
185 − <>
186 − <span aria-hidden className="text-ink-4">
187 − ·
188 − </span>
189 − <span className="normal-case tracking-normal text-[12px] text-ink-3">
190 − owned by{" "}
191 − <Link href={routes.operator(f.owner.slug)} className="text-ink-2 hover:text-accent hover:underline">
192 − {f.owner.name}
193 − </Link>
194 − </span>
195 − </>
196 − )}
228 + return (
229 + <EvidenceProvider data={{ entityName: f.name, entityKind: "facility", claims, provenance: prov, history }}>
230 + <Page className="pb-8">
231 + <JsonLd data={jsonLd} />
232 + <div className="pt-3">
233 + <Breadcrumbs items={crumbs} />
197 234 </div>
198 − <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">
199 − <WithProvenance items={prov} fields="name">
200 − {f.name}
201 − </WithProvenance>
202 − </h1>
203 − {place !== "—" && (
204 − <p className="mt-1 flex flex-wrap items-center gap-x-1.5 text-[13.5px] text-ink-2">
205 − <CountryFlag iso2={f.countryIso2} size={13} />
206 − <span>{f.city}</span>
207 − {f.metro && (
235 +
236 + {/* ---------------------------------------------------------------- header */}
237 + <header className="py-4 sm:py-6">
238 + <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">
239 + <span>{isCampus ? "Data center campus" : f.recordScope === "building" ? "Data center building" : "Data center"}</span>
240 + {f.operator && (
208 241 <>
209 − <span aria-hidden className="text-ink-4">
210 − ·
211 − </span>
212 − <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">
213 − {f.metro.name} market
242 + <span aria-hidden className="text-ink-4">·</span>
243 + <Link href={routes.operator(f.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">
244 + {f.operator.name}
214 245 </Link>
215 246 </>
216 247 )}
217 − {f.countryName && (
248 + {parent && (
218 249 <>
219 − <span aria-hidden className="text-ink-4">
220 − ·
221 − </span>
222 − {cHref ? (
223 − <Link href={cHref} className="hover:text-accent hover:underline">
224 − {f.countryName}
250 + <span aria-hidden className="text-ink-4">·</span>
251 + <span className="normal-case tracking-normal text-[12px] text-ink-3">
252 + part of{" "}
253 + <Link href={routes.facility(parent.slug)} className="text-ink-2 hover:text-accent hover:underline">
254 + {parent.name}
225 255 </Link>
226 − ) : (
227 − <span>{f.countryName}</span>
228 − )}
256 + </span>
229 257 </>
230 258 )}
231 − </p>
232 − )}
233 − <div className="mt-3 flex flex-wrap items-center gap-1.5">
234 − <WithProvenance items={prov} fields="status">
235 − <StatusBadge status={f.status} />
236 − </WithProvenance>
237 − <WithProvenance items={prov} fields={["facilityType", "facility_type"]}>
259 + </div>
260 + <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">
261 + <WithProvenance items={prov} fields="name">
262 + {f.name}
263 + </WithProvenance>
264 + </h1>
265 + {place !== "—" && (
266 + <p className="mt-1 flex flex-wrap items-center gap-x-1.5 text-[13.5px] text-ink-2">
267 + <CountryFlag iso2={f.countryIso2} size={13} />
268 + {f.city && <span>{f.city}</span>}
269 + {f.metro && (
270 + <>
271 + <span aria-hidden className="text-ink-4">·</span>
272 + <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">
273 + {f.metro.name} market
274 + </Link>
275 + </>
276 + )}
277 + {f.countryName && (
278 + <>
279 + <span aria-hidden className="text-ink-4">·</span>
280 + {cHref ? (
281 + <Link href={cHref} className="hover:text-accent hover:underline">
282 + {f.countryName}
283 + </Link>
284 + ) : (
285 + <span>{f.countryName}</span>
286 + )}
287 + </>
288 + )}
289 + </p>
290 + )}
291 + <div className="mt-3 flex flex-wrap items-center gap-1.5">
238 292 <TypeBadge type={f.facilityType} />
239 − </WithProvenance>
240 − <FlagBadges isAi={f.isAi} isHyperscale={f.isHyperscale} size="sm" />
241 − <ConfidenceBadge level={f.confidence} short={false} />
242 − <span className="inline-flex h-[22px] items-center gap-1.5 rounded-xs border border-hair-2 px-2 text-[11.5px] text-ink-2" title={`Completeness ${f.completeness}/100 — share of tracked fields with a sourced value`}>
243 − <span className="inline-block h-[4px] w-10 overflow-hidden rounded-[1px] bg-surface-3" aria-hidden>
244 − <span className={`block h-full ${completenessTone(f.completeness)}`} style={{ width: `${Math.max(2, Math.min(100, f.completeness))}%` }} />
293 + {f.isAi ? <AiMark evidence={f.aiEvidence} size="sm" /> : null}
294 + <FlagBadges isHyperscale={f.isHyperscale} size="sm" />
295 + {f.recordScope && <ScopeBadge scope={f.recordScope} size="sm" short={false} />}
296 + <span className="inline-flex h-[22px] items-center gap-1.5 rounded-xs border border-hair-2 px-2 text-[11.5px] text-ink-2" title={`Completeness ${f.completeness}/100 — share of tracked fields with a sourced value`}>
297 + <span className="inline-block h-[4px] w-10 overflow-hidden rounded-[1px] bg-surface-3" aria-hidden>
298 + <span className={`block h-full ${completenessTone(f.completeness)}`} style={{ width: `${Math.max(2, Math.min(100, f.completeness))}%` }} />
299 + </span>
300 + <span className="figure">{f.completeness}%</span>
301 + <span className="text-ink-3">complete</span>
245 302 </span>
246 − <span className="figure">{f.completeness}%</span>
247 − <span className="text-ink-3">complete</span>
248 − </span>
249 − <WithProvenance items={prov} fields={["geoPrecision", "geo_precision", "lat", "lng", "geo", "coordinates"]}>
250 − <PrecisionBadge precision={f.geoPrecision} short={false} />
251 − </WithProvenance>
303 + </div>
304 + {f.aliases.length > 0 && (
305 + <p className="mt-2 text-[12px] text-ink-3">
306 + Also known as <span className="text-ink-2">{f.aliases.slice(0, 6).join(", ")}</span>
307 + {f.aliases.length > 6 && ` and ${f.aliases.length - 6} more`}
308 + </p>
309 + )}
310 + </header>
311 +
312 + {/* ---------------------------------------------------------------- header figures (each opens the evidence drawer) */}
313 + <div className="grid grid-cols-2 border-y border-hair [&>*]:min-w-0 max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*:last-child:nth-child(odd)]:col-span-2 max-sm:[&>*]:border-hair sm:grid-cols-4 sm:[&>*:nth-child(n+5)]:border-t sm:[&>*:nth-child(4n+1)]:pl-0 sm:[&>*:not(:nth-child(4n+1))]:border-l sm:[&>*]:border-hair xl:grid-cols-7 xl:[&>*:nth-child(n+5)]:border-t-0 xl:[&>*:nth-child(4n+1)]:pl-4 xl:[&>*:first-child]:pl-0 xl:[&>*+*]:border-l">
314 + <HeaderFigure label="Status">
315 + <Evidence field="status" display={STATUS_LABEL[f.status]}>
316 + <StatusBadge status={f.status} />
317 + </Evidence>
318 + {f.openedOn && <div className="figure mt-1 text-[11px] text-ink-3">opened {fmtDate(f.openedOn)}</div>}
319 + </HeaderFigure>
320 + <HeaderFigure label="Known IT capacity">
321 + <MwFigure field="itCapacityMw" aliases={["it_capacity_mw", "critical_power_mw"]} mw={f.itCapacityMw} estimate={est && best.kind === "it"} semantics={f.capacitySemantics && best.kind === "it" ? f.capacitySemantics : "it_capacity_mw"} scope={best.kind === "it" ? f.capacityScope : null} fallbackNote="Not published by any source" />
322 + </HeaderFigure>
323 + <HeaderFigure label="Total power">
324 + <MwFigure field="totalPowerMw" aliases={["total_power_mw", "current_power_mw"]} mw={f.totalPowerMw} estimate={est && best.kind === "total"} semantics={f.capacitySemantics && best.kind === "total" ? f.capacitySemantics : "current_power_mw"} scope={best.kind === "total" ? f.capacityScope : null} fallbackNote="Not published by any source" />
325 + </HeaderFigure>
326 + <HeaderFigure label="Planned build-out">
327 + <MwFigure field="plannedPowerMw" aliases={["planned_power_mw", "phase_mw", "plannedMw"]} mw={f.plannedPowerMw ?? f.ultimateCampusMw ?? null} planned semantics={f.plannedPowerMw !== null ? "planned_power_mw" : "ultimate_campus_mw"} scope={best.kind === "planned" ? f.capacityScope : null} fallbackNote={f.status === "operational" ? "No expansion figure published" : "Not published by any source"} />
328 + </HeaderFigure>
329 + <HeaderFigure label="Confidence" note={`${fmtInt(dq.sourceCount)} ${dq.sourceCount === 1 ? "source" : "sources"} · ${fmtInt(dq.primarySourceCount)} primary`}>
330 + <Evidence field="confidence" fields={["status", "itCapacityMw", "totalPowerMw"]} display={CONFIDENCE_LABEL[f.confidence]}>
331 + <ConfidenceBadge level={f.confidence} short={false} />
332 + </Evidence>
333 + </HeaderFigure>
334 + <HeaderFigure label="Location precision" note={hasGeo ? (imprecise ? "ring marks the area, not the building" : `${f.lat!.toFixed(4)}, ${f.lng!.toFixed(4)}`) : "no coordinates published"}>
335 + <Evidence field="geoPrecision" fields={["geo_precision", "lat", "lng", "geo", "coordinates"]} display={PRECISION_LABEL[f.geoPrecision]}>
336 + <PrecisionBadge precision={f.geoPrecision} short={false} />
337 + </Evidence>
338 + </HeaderFigure>
339 + <HeaderFigure label="Last verified" note={f.lastVerified ? "against a primary source" : "no primary-source verification yet"}>
340 + <Evidence field="lastVerified" fields={["last_verified"]} display={f.lastVerified ? fmtDate(f.lastVerified) : "—"} block>
341 + <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${f.lastVerified ? "text-ink" : "text-ink-4"}`}>{f.lastVerified ? fmtDate(f.lastVerified) : "—"}</div>
342 + </Evidence>
343 + </HeaderFigure>
252 344 </div>
253 − {f.aliases.length > 0 && (
254 − <p className="mt-2 text-[12px] text-ink-3">
255 − Also known as <span className="text-ink-2">{f.aliases.slice(0, 6).join(", ")}</span>
256 − {f.aliases.length > 6 && ` and ${f.aliases.length - 6} more`}
257 − </p>
258 − )}
259 − </header>
260 −
261 − {/* ---------------------------------------------------------------- metrics */}
262 − <MetricRow cols={anyMw ? 4 + [f.buildingSqm, f.rackCount, f.pue].filter((v) => v !== null).length : 2 + [f.buildingSqm, f.rackCount, f.pue].filter((v) => v !== null).length}>
263 − {anyMw ? (
264 − <>
265 − <MetricCell>
266 − <Metric
267 − label={
268 − <WithProvenance items={prov} fields={["itCapacityMw", "it_capacity_mw"]}>
269 − IT capacity
270 − </WithProvenance>
271 − }
272 − value={f.itCapacityMw === null ? "—" : `${est ? "≈ " : ""}${fmtMw(f.itCapacityMw).replace(/ (MW|GW)$/, "")}`}
273 − unit={f.itCapacityMw === null ? undefined : fmtMw(f.itCapacityMw).endsWith("GW") ? "GW" : "MW"}
274 − note={f.itCapacityMw === null ? "not published" : est ? "estimate" : undefined}
275 − />
276 − </MetricCell>
277 − <MetricCell>
278 − <Metric
279 − label={
280 − <WithProvenance items={prov} fields={["totalPowerMw", "total_power_mw"]}>
281 − Total power
282 − </WithProvenance>
283 − }
284 − value={f.totalPowerMw === null ? "—" : `${est ? "≈ " : ""}${fmtMw(f.totalPowerMw).replace(/ (MW|GW)$/, "")}`}
285 − unit={f.totalPowerMw === null ? undefined : fmtMw(f.totalPowerMw).endsWith("GW") ? "GW" : "MW"}
286 − note={f.totalPowerMw === null ? "not published" : est ? "estimate" : undefined}
287 − />
288 − </MetricCell>
289 − <MetricCell>
290 − <Metric
291 − label={
292 − <WithProvenance items={prov} fields={["plannedPowerMw", "planned_power_mw"]}>
293 − Planned power
294 − </WithProvenance>
295 − }
296 − value={
297 − f.plannedPowerMw === null ? (
298 − "—"
299 − ) : (
300 − <>
301 − <EstimateMark className="mr-1 text-[0.7em]" />
302 − {fmtMw(f.plannedPowerMw).replace(/ (MW|GW)$/, "")}
303 − </>
304 − )
305 − }
306 − unit={f.plannedPowerMw === null ? undefined : fmtMw(f.plannedPowerMw).endsWith("GW") ? "GW" : "MW"}
307 − note={f.plannedPowerMw === null ? "not published" : "planned figure"}
308 − />
309 − </MetricCell>
310 − </>
311 − ) : (
312 − <MetricCell>
313 − <Metric label="Capacity" value="—" note="MW not published by any source" />
314 − </MetricCell>
315 − )}
316 − {f.buildingSqm !== null && (
317 − <MetricCell>
318 − <Metric
319 − label={
320 − <WithProvenance items={prov} fields={["buildingSqm", "building_sqm"]}>
321 − Building
322 − </WithProvenance>
323 − }
324 − value={fmtInt(f.buildingSqm)}
325 − unit="m²"
326 − />
327 − </MetricCell>
328 − )}
329 − {f.rackCount !== null && (
330 − <MetricCell>
331 − <Metric
332 − label={
333 − <WithProvenance items={prov} fields={["rackCount", "rack_count"]}>
334 − Racks
335 − </WithProvenance>
336 − }
337 − value={fmtInt(f.rackCount)}
338 − />
339 − </MetricCell>
340 − )}
341 − {f.pue !== null && (
342 − <MetricCell>
343 − <Metric
344 − label={
345 − <WithProvenance items={prov} fields="pue">
346 − PUE
347 − </WithProvenance>
348 − }
349 − value={f.pue.toFixed(2)}
350 − />
351 − </MetricCell>
352 − )}
353 − <MetricCell>
354 − <Metric
355 − label={
356 − <WithProvenance items={prov} fields={["openedOn", "opened_on"]}>
357 − Opened
358 − </WithProvenance>
359 − }
360 − value={f.openedOn ? fmtDate(f.openedOn) : "—"}
361 − size="sm"
362 − note={f.openedOn ? undefined : f.status === "operational" ? "date not published" : STATUS_LABEL[f.status].toLowerCase()}
363 − />
364 − </MetricCell>
365 − </MetricRow>
366 −
367 − <div className="mt-4">
368 − <SectionNav items={sections} />
369 − </div>
345 + <p className="mt-2 text-[11px] text-ink-4">Figures are published values only (≈ marks estimates and planned figures); click any figure for its sources, sentence-level evidence and history. Nothing is imputed.</p>
370 346
371 − <div className="flex flex-col gap-8 pt-6">
372 − {/* ------------------------------------------------------------ overview */}
373 − <Section id="overview" title="Overview" className="scroll-mt-28">
374 − <div className="grid gap-6 md:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)]">
375 − <div className="min-w-0">
376 − {f.description ? (
377 − <p className="text-[14px] leading-relaxed text-ink-2">
378 − <WithProvenance items={prov} fields="description">
379 − <span>{f.description}</span>
380 − </WithProvenance>
381 − </p>
382 − ) : (
383 − <NotPublished what="Description" />
384 − )}
385 − {f.certifications.length > 0 && (
386 − <div className="mt-4">
387 − <div className="label-caps mb-1.5 flex items-center gap-1">
388 − Certifications <ProvenancePopover items={provFor(prov, "certifications")} size={12} />
389 − </div>
390 − <div className="flex flex-wrap gap-1.5">
391 − {f.certifications.map((c) => (
392 − <Badge key={c} tone="outline">
393 − {c}
394 − </Badge>
395 − ))}
347 + <div className="mt-4">
348 + <SectionNav items={sections} />
349 + </div>
350 +
351 + <div className="flex flex-col gap-9 pt-6">
352 + {/* ------------------------------------------------------------ overview */}
353 + <Section id="overview" title="Overview" className="scroll-mt-28">
354 + <div className="grid gap-6 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">
355 + <div className="min-w-0">
356 + {f.description ? (
357 + <p className="text-[14px] leading-relaxed text-ink-2">
358 + <WithProvenance items={prov} fields="description">
359 + <span>{f.description}</span>
360 + </WithProvenance>
361 + </p>
362 + ) : (
363 + <NotPublished what="Description" />
364 + )}
365 + <KeyValueList
366 + className="mt-4"
367 + columns={2}
368 + items={[
369 + { label: "Type", value: <TypeBadge type={f.facilityType} size="xs" /> },
370 + { label: "Tier", value: f.tier ? <WithProvenance items={prov} fields="tier">{f.tier}</WithProvenance> : null },
371 + { label: "Building", value: f.buildingSqm !== null ? <Evidence field="buildingSqm" fields={["building_sqm"]}><span className="figure">{fmtInt(f.buildingSqm)} m²</span></Evidence> : null },
372 + { label: "Site area", value: f.siteAreaHa !== null ? <Evidence field="siteAreaHa" fields={["site_area_ha"]}><span className="figure">{fmtInt(f.siteAreaHa)} ha</span></Evidence> : null },
373 + { label: "Racks", value: f.rackCount !== null ? <Evidence field="rackCount" fields={["rack_count"]}><span className="figure">{fmtInt(f.rackCount)}</span></Evidence> : null },
374 + { label: "PUE", value: f.pue !== null ? <Evidence field="pue"><span className="figure">{f.pue.toFixed(2)}</span></Evidence> : null },
375 + { label: "Cooling", value: f.coolingType ? <WithProvenance items={prov} fields={["coolingType", "cooling_type"]}>{f.coolingType}</WithProvenance> : null },
376 + { label: "Renewable claim", value: f.renewableClaim ? <WithProvenance items={prov} fields={["renewableClaim", "renewable_claim"]}>{f.renewableClaim}</WithProvenance> : null },
377 + { label: "Website", value: f.website ? <WithProvenance items={prov} fields="website"><ExtLink href={f.website} /></WithProvenance> : null },
378 + ...externalIdRows(f.externalIds),
379 + { label: "First indexed", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.firstSeen)}</span> },
380 + { label: "Record updated", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.updatedAt)}</span> },
381 + ]}
382 + />
383 + {f.certifications.length > 0 && (
384 + <div className="mt-4">
385 + <div className="label-caps mb-1.5 flex items-center gap-1">
386 + Certifications <ProvenancePopover items={provFor(prov, "certifications")} size={12} />
387 + </div>
388 + <div className="flex flex-wrap gap-1.5">
389 + {f.certifications.map((c) => (
390 + <Badge key={c} tone="outline">
391 + {c}
392 + </Badge>
393 + ))}
394 + </div>
396 395 </div>
397 − </div>
398 − )}
396 + )}
397 + </div>
398 + <div className="min-w-0">
399 + {hasGeo ? (
400 + <EntityMap center={{ lat: f.lat!, lng: f.lng! }} precision={f.geoPrecision} filters={f.countryIso2 ? { country: f.countryIso2 } : undefined} className="h-[200px] sm:h-[260px]" openLabel="Open in map" />
401 + ) : (
402 + <div className="flex min-h-[140px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>
403 + )}
404 + <KeyValueList
405 + className="mt-3"
406 + items={[
407 + { label: "Address", value: f.address ? <WithProvenance items={prov} fields="address">{f.address}</WithProvenance> : null },
408 + { label: "Postal code", value: f.postalCode ? <span className="figure">{f.postalCode}</span> : null },
409 + { label: "City", value: f.city ? <WithProvenance items={prov} fields="city">{f.city}</WithProvenance> : null },
410 + { label: "Region", value: f.regionName ? <WithProvenance items={prov} fields={["regionName", "region_name"]}>{f.regionName}</WithProvenance> : null },
411 + { label: "Market", value: f.metro ? <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">{f.metro.name}</Link> : null },
412 + { label: "Country", value: f.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={f.countryIso2} size={13} />{cHref ? <Link href={cHref} className="hover:text-accent hover:underline">{f.countryName ?? f.countryIso2}</Link> : <span>{f.countryName ?? f.countryIso2}</span>}</span> : null },
413 + { label: "Coordinates", value: hasGeo ? <Evidence field="lat" fields={["lng", "geo", "coordinates", "geoPrecision"]}><span className="figure text-[12.5px]">{f.lat!.toFixed(4)}, {f.lng!.toFixed(4)}</span></Evidence> : null },
414 + { label: "Geo source", value: f.geoSource ? <span className="figure text-[12px] text-ink-3">{f.geoSource}</span> : null },
415 + ]}
416 + />
417 + {!f.address && !hasGeo && <NotPublished what="Address and coordinates" className="mt-2" />}
418 + {hasGeo && imprecise && <p className="mt-2 text-[12px] text-ink-3">Location shown at {PRECISION_LABEL[f.geoPrecision].toLowerCase().replace(" location", "")}: the exact site has not been published by the indexed sources; the ring marks the area, not the building.</p>}
419 + </div>
399 420 </div>
421 + </Section>
422 +
423 + {/* ------------------------------------------------------------ capacity */}
424 + <Section id="capacity" title="Capacity" className="scroll-mt-28" description="Every capacity figure published about this record, by what it measures (IT load ≠ total power ≠ planned ≠ grid connection), its scope and its source. Only site-scoped figures populate the columns above.">
425 + <div className="grid gap-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
426 + <div className="min-w-0">
427 + <div className="label-caps mb-2">Capacity claims</div>
428 + <ClaimsTable claims={claims} kind="capacity" emptyNote={best.mw !== null ? "No sentence-level capacity claim yet — the figures above come from structured source fields listed under Source evidence." : "No source has published a capacity figure for this record."} />
429 + {(f.utilityCapacityMw != null || f.gridConnectionMw != null || f.ultimateCampusMw != null) && (
430 + <KeyValueList
431 + className="mt-4"
432 + items={[
433 + { label: "Utility capacity", value: f.utilityCapacityMw != null ? <Evidence field="utilityCapacityMw"><span className="figure">{fmtMw(f.utilityCapacityMw)}</span> <SemanticsLabel semantics="utility_capacity_mw" /></Evidence> : null },
434 + { label: "Grid connection", value: f.gridConnectionMw != null ? <Evidence field="gridConnectionMw"><span className="figure">{fmtMw(f.gridConnectionMw)}</span> <SemanticsLabel semantics="grid_connection_mw" /></Evidence> : null },
435 + { label: "Ultimate build-out", value: f.ultimateCampusMw != null ? <Evidence field="ultimateCampusMw"><EstimateMark className="mr-0.5" /><span className="figure">{fmtMw(f.ultimateCampusMw)}</span> <SemanticsLabel semantics="ultimate_campus_mw" /></Evidence> : null },
436 + ]}
437 + />
438 + )}
439 + </div>
440 + <div className="min-w-0">
441 + <div className="label-caps mb-2">Capacity history</div>
442 + <CapacityHistoryChart points={history} height={170} />
443 + </div>
444 + </div>
445 + </Section>
446 +
447 + {/* ------------------------------------------------------------ timeline */}
448 + <Section id="timeline" title="Timeline" className="scroll-mt-28" description="Dated milestones and detected changes, oldest first. Partial dates are shown as published.">
449 + <TimelineList items={timeline} dense />
450 + </Section>
451 +
452 + {/* ------------------------------------------------------------ operator / ownership */}
453 + <Section id="ownership" title="Operator / ownership" className="scroll-mt-28" description="Operator, owner, developer, landowner and tenants are distinct roles and are never merged.">
400 454 <KeyValueList
455 + columns={2}
401 456 items={[
402 − { label: "Operator", value: f.operator ? <Link href={routes.operator(f.operator.slug)} className="hover:text-accent hover:underline">{f.operator.name}</Link> : null },
403 − { label: "Owner", value: f.owner && f.owner.id !== f.operator?.id ? <Link href={routes.operator(f.owner.slug)} className="hover:text-accent hover:underline">{f.owner.name}</Link> : null },
404 − { label: "Campus", value: f.campus ? <Link href={routes.facility(f.campus.slug)} className="hover:text-accent hover:underline">{f.campus.name}</Link> : null },
405 − { label: "Tier", value: f.tier ? <WithProvenance items={prov} fields="tier">{f.tier}</WithProvenance> : null },
406 − { label: "Cooling", value: f.coolingType ? <WithProvenance items={prov} fields={["coolingType", "cooling_type"]}>{f.coolingType}</WithProvenance> : null },
407 − { label: "Renewable", value: f.renewableClaim ? <WithProvenance items={prov} fields={["renewableClaim", "renewable_claim"]}>{f.renewableClaim}</WithProvenance> : null },
408 − { label: "Site area", value: f.siteAreaHa !== null ? <WithProvenance items={prov} fields={["siteAreaHa", "site_area_ha"]}><span className="figure">{fmtInt(f.siteAreaHa)} ha</span></WithProvenance> : null },
409 − { label: "Announced", value: f.announcedOn ? <WithProvenance items={prov} fields={["announcedOn", "announced_on"]}><span className="figure">{fmtDate(f.announcedOn)}</span></WithProvenance> : null },
410 − { label: "Construction", value: f.constructionStartedOn ? <WithProvenance items={prov} fields={["constructionStartedOn", "construction_started_on"]}><span className="figure">started {fmtDate(f.constructionStartedOn)}</span></WithProvenance> : null },
411 − { label: "Website", value: f.website ? <WithProvenance items={prov} fields="website"><ExtLink href={f.website} /></WithProvenance> : null },
412 − ...externalIdRows(f.externalIds),
413 − { label: "First indexed", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.firstSeen)}</span> },
414 − { label: "Last verified", value: f.lastVerified ? <span className="figure text-[12.5px] text-ink-2">{fmtDate(f.lastVerified)}</span> : <span className="text-[12.5px] text-ink-3">no primary-source verification yet</span>, keepEmpty: true },
457 + { label: "Operator", value: <WithProvenance items={prov} fields={["operator", "operatorName"]}>{opLink(f.operator, "Not published")}</WithProvenance>, keepEmpty: true },
458 + { label: "Owner", value: f.owner ? <WithProvenance items={prov} fields={["owner", "ownerName"]}>{opLink(f.owner, "")}{f.owner.id === f.operator?.id && <span className="ml-1.5 text-[11.5px] text-ink-3">(same as operator)</span>}</WithProvenance> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
459 + { label: "Developer", value: opLink(f.developer, "Not published"), keepEmpty: true },
460 + { label: "Landowner", value: opLink(f.landowner, "Not published"), keepEmpty: true },
461 + {
462 + label: "Tenants",
463 + value: tenants.length ? (
464 + <ul className="flex flex-wrap gap-1.5">
465 + {tenants.map((t) => (
466 + <li key={t.id}>
467 + <Link href={routes.operator(t.slug)} className="inline-flex h-7 items-center gap-1.5 rounded-xs border border-hair px-2 text-[12px] text-ink hover:text-accent">
468 + {t.name} <span className="text-ink-4">{t.role}</span>
469 + </Link>
470 + </li>
471 + ))}
472 + </ul>
473 + ) : (
474 + <span className="text-[12.5px] text-ink-3">No tenant published{carrierTenants.length ? ` (carriers are listed under Connectivity)` : ""}</span>
475 + ),
476 + keepEmpty: true,
477 + },
415 478 ]}
416 479 />
417 − </div>
418 − </Section>
419 −
420 − {/* ------------------------------------------------------------ location */}
421 − <Section id="location" title="Location" className="scroll-mt-28" aside={<PrecisionBadge precision={f.geoPrecision} short={false} size="xs" />}>
422 − <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
423 − <div className="min-w-0">
424 − <KeyValueList
425 − items={[
426 − { label: "Address", value: f.address ? <WithProvenance items={prov} fields="address">{f.address}</WithProvenance> : null },
427 − { label: "Postal code", value: f.postalCode ? <span className="figure">{f.postalCode}</span> : null },
428 − { label: "City", value: f.city ? <WithProvenance items={prov} fields="city">{f.city}</WithProvenance> : null },
429 − { label: "Region", value: f.regionName ? <WithProvenance items={prov} fields={["regionName", "region_name"]}>{f.regionName}</WithProvenance> : null },
430 − { label: "Market", value: f.metro ? <Link href={routes.market(f.metro.slug)} className="hover:text-accent hover:underline">{f.metro.name}</Link> : null },
431 − {
432 − label: "Country",
433 − value: f.countryIso2 ? (
434 − <WithProvenance items={prov} fields={["countryIso2", "country_iso2"]}>
435 − <span className="inline-flex items-center gap-1.5">
436 − <CountryFlag iso2={f.countryIso2} size={13} />
437 − {cHref ? <Link href={cHref} className="hover:text-accent hover:underline">{f.countryName ?? f.countryIso2}</Link> : <span>{f.countryName ?? f.countryIso2}</span>}
438 − </span>
439 − </WithProvenance>
440 − ) : null,
441 − },
442 − {
443 − label: "Coordinates",
444 − value: hasGeo ? (
445 − <WithProvenance items={prov} fields={["lat", "lng", "geo", "coordinates", "geoPrecision"]}>
446 − <span className="figure text-[12.5px]">
447 − {f.lat!.toFixed(4)}, {f.lng!.toFixed(4)}
448 − </span>
449 − </WithProvenance>
450 − ) : null,
451 − },
452 − { label: "Geo source", value: f.geoSource ? <span className="figure text-[12px] text-ink-3">{f.geoSource}</span> : null },
453 − ]}
454 − />
455 − {!f.address && !hasGeo && <NotPublished what="Address and coordinates" className="mt-2" />}
456 − {hasGeo && imprecise && (
457 − <p className="mt-3 rounded-sm border border-dashed border-hair-2 px-3 py-2 text-[12.5px] text-ink-3">
458 − <span className="font-medium text-ink-2">Location shown at {PRECISION_LABEL[f.geoPrecision].toLowerCase().replace(" location", "")}.</span> The exact site has not been published by the sources indexed; the ring marks the area, not the building.
459 − </p>
460 − )}
461 − </div>
462 − {hasGeo ? (
463 − <EntityMap center={{ lat: f.lat!, lng: f.lng! }} precision={f.geoPrecision} filters={f.countryIso2 ? { country: f.countryIso2 } : undefined} className="h-[220px] sm:h-[300px]" openLabel="Open in map" />
464 − ) : (
465 − <div className="flex min-h-[160px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>
480 + {f.events.some((e) => e.eventType === "operator_changed" || e.eventType === "owner_changed" || e.eventType === "acquisition") && (
481 + <div className="mt-4">
482 + <div className="label-caps mb-1.5">Ownership changes</div>
483 + <EventList events={f.events.filter((e) => e.eventType === "operator_changed" || e.eventType === "owner_changed" || e.eventType === "acquisition")} dense />
484 + </div>
466 485 )}
467 − </div>
468 − </Section>
486 + </Section>
469 487
470 − {/* ------------------------------------------------------------ connectivity */}
471 − <Section id="connectivity" title="Connectivity" className="scroll-mt-28" description="Carriers, cloud on-ramps and internet exchanges present at this site, as published by the operator or PeeringDB.">
472 − {connectivityEmpty ? (
473 − <NotPublished what="Connectivity (carriers, cloud on-ramps, IXPs)" />
474 − ) : (
475 − <div className="grid gap-4 sm:grid-cols-3">
476 − <div>
477 − <div className="label-caps mb-1.5">
478 − Carriers {f.carriersCount !== null && f.carriersCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.carriersCount)}</span>}
479 − {f.networksCount !== null && f.networksCount !== undefined && <span className="ml-2 normal-case tracking-normal text-ink-4">{fmtInt(f.networksCount)} networks</span>}
488 + {/* ------------------------------------------------------------ connectivity */}
489 + <Section id="connectivity" title="Connectivity" className="scroll-mt-28" description="Carriers, cloud on-ramps and internet exchanges present at this site, as published by the operator or PeeringDB.">
490 + {connectivityEmpty ? (
491 + <NotPublished what="Connectivity (carriers, cloud on-ramps, IXPs)" />
492 + ) : (
493 + <div className="grid gap-4 sm:grid-cols-3">
494 + <div>
495 + <div className="label-caps mb-1.5">
496 + Carriers {f.carriersCount !== null && f.carriersCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.carriersCount)}</span>}
497 + {f.networksCount !== null && f.networksCount !== undefined && <span className="ml-2 normal-case tracking-normal text-ink-4">{fmtInt(f.networksCount)} networks</span>}
498 + </div>
499 + {f.carriers.length ? (
500 + <ul className="divide-y divide-hair text-[13px]">
501 + {f.carriers.map((c) => (
502 + <li key={c.id} className="flex items-baseline justify-between gap-2 py-1.5">
503 + <Link href={routes.operator(c.slug)} className="truncate text-ink hover:text-accent hover:underline">
504 + {c.name}
505 + </Link>
506 + {c.asn && <span className="figure shrink-0 text-[11.5px] text-ink-3">AS{c.asn}</span>}
507 + </li>
508 + ))}
509 + </ul>
510 + ) : carrierTenants.length ? (
511 + <ul className="divide-y divide-hair text-[13px]">
512 + {carrierTenants.map((c) => (
513 + <li key={c.id} className="py-1.5">
514 + <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">
515 + {c.name}
516 + </Link>
517 + </li>
518 + ))}
519 + </ul>
520 + ) : (
521 + <p className="text-[12.5px] text-ink-3">{f.carriersCount ? "Names not published; count from PeeringDB." : "None listed."}</p>
522 + )}
523 + </div>
524 + <div>
525 + <div className="label-caps mb-1.5">Cloud on-ramps</div>
526 + {f.cloudProviders.length ? (
527 + <ul className="divide-y divide-hair text-[13px]">
528 + {f.cloudProviders.map((c) => (
529 + <li key={c.id} className="py-1.5">
530 + <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">
531 + {c.name}
532 + </Link>
533 + </li>
534 + ))}
535 + </ul>
536 + ) : (
537 + <p className="text-[12.5px] text-ink-3">None listed.</p>
538 + )}
539 + </div>
540 + <div>
541 + <div className="label-caps mb-1.5">
542 + Internet exchanges {f.ixpCount !== null && f.ixpCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.ixpCount)}</span>}
543 + </div>
544 + {f.ixps.length ? (
545 + <ul className="divide-y divide-hair text-[13px]">
546 + {f.ixps.map((x) => (
547 + <li key={x.id} className="py-1.5">
548 + <Link href={routes.ixp(x.slug)} className="text-ink hover:text-accent hover:underline">
549 + {x.name}
550 + </Link>
551 + </li>
552 + ))}
553 + </ul>
554 + ) : (
555 + <p className="text-[12.5px] text-ink-3">{f.ixpCount ? "Names not published; count from PeeringDB." : "None listed."}</p>
556 + )}
480 557 </div>
481 − {f.carriers.length ? (
482 − <ul className="divide-y divide-hair text-[13px]">
483 − {f.carriers.map((c) => (
484 − <li key={c.id} className="flex items-baseline justify-between gap-2 py-1.5">
485 − <Link href={routes.operator(c.slug)} className="truncate text-ink hover:text-accent hover:underline">
486 − {c.name}
487 − </Link>
488 − {c.asn && <span className="figure shrink-0 text-[11.5px] text-ink-3">AS{c.asn}</span>}
489 − </li>
490 − ))}
491 − </ul>
492 − ) : (
493 − <p className="text-[12.5px] text-ink-3">{f.carriersCount ? "Names not published; count from PeeringDB." : "None listed."}</p>
494 − )}
495 558 </div>
496 − <div>
497 − <div className="label-caps mb-1.5">Cloud providers</div>
498 − {f.cloudProviders.length ? (
499 − <ul className="divide-y divide-hair text-[13px]">
500 − {f.cloudProviders.map((c) => (
501 − <li key={c.id} className="py-1.5">
502 − <Link href={routes.operator(c.slug)} className="text-ink hover:text-accent hover:underline">
503 − {c.name}
504 − </Link>
505 − </li>
506 − ))}
507 − </ul>
508 − ) : (
509 − <p className="text-[12.5px] text-ink-3">None listed.</p>
559 + )}
560 + </Section>
561 +
562 + {/* ------------------------------------------------------------ power */}
563 + <Section id="power" title="Power" className="scroll-mt-28" description="Utility-side figures (power available or contracted from the grid) are never compared with IT load. Grid events and constraints are attached to the market or country, not to the site.">
564 + <div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
565 + <div className="min-w-0">
566 + <KeyValueList
567 + items={[
568 + { label: "Utility capacity", value: f.utilityCapacityMw != null ? <Evidence field="utilityCapacityMw"><span className="figure">{fmtMw(f.utilityCapacityMw)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
569 + { label: "Grid connection", value: f.gridConnectionMw != null ? <Evidence field="gridConnectionMw"><span className="figure">{fmtMw(f.gridConnectionMw)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
570 + { label: "Renewable claim", value: f.renewableClaim ?? <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
571 + ]}
572 + />
573 + {power?.countryEnergy && (f.countryName || f.countryIso2) && (
574 + <div className="mt-4">
575 + <div className="label-caps mb-1.5">Country energy context · {f.countryName ?? f.countryIso2}</div>
576 + <div className="grid grid-cols-2 gap-3">
577 + <div>
578 + <div className="figure text-[18px] font-medium text-ink">{power.countryEnergy.renewableShare !== null ? fmtPct(power.countryEnergy.renewableShare / 100) : "—"}</div>
579 + <div className="text-[11px] text-ink-3">renewable share of generation{power.countryEnergy.statsYear ? ` · ${power.countryEnergy.statsYear}` : ""}</div>
580 + </div>
581 + <div>
582 + <div className="figure text-[18px] font-medium text-ink">{power.countryEnergy.electricityTwh !== null ? `${fmtInt(power.countryEnergy.electricityTwh)} TWh` : "—"}</div>
583 + <div className="text-[11px] text-ink-3">national generation{power.countryEnergy.statsYear ? ` · ${power.countryEnergy.statsYear}` : ""}</div>
584 + </div>
585 + </div>
586 + <p className="mt-2 text-[11px] text-ink-4">National grid averages — they do not describe this facility&apos;s contracted electricity or power purchase agreements.</p>
587 + </div>
510 588 )}
511 589 </div>
512 − <div>
513 − <div className="label-caps mb-1.5">
514 − Internet exchanges {f.ixpCount !== null && f.ixpCount !== undefined && <span className="figure text-ink-3">{fmtInt(f.ixpCount)}</span>}
515 − </div>
516 − {f.ixps.length ? (
517 − <ul className="divide-y divide-hair text-[13px]">
518 − {f.ixps.map((x) => (
519 − <li key={x.id} className="py-1.5">
520 − <Link href={routes.ixp(x.slug)} className="text-ink hover:text-accent hover:underline">
521 − {x.name}
522 − </Link>
590 + <div className="min-w-0">
591 + <div className="label-caps mb-1.5">Power events</div>
592 + {power?.powerEvents.length ? <EventList events={power.powerEvents} dense /> : <p className="text-[12.5px] text-ink-3">No power agreement or utility event linked to this facility.</p>}
593 + <div className="label-caps mb-1.5 mt-4">Grid constraints in this market</div>
594 + {power?.gridConstraints.length ? (
595 + <ul className="divide-y divide-hair">
596 + {power.gridConstraints.map((g) => (
597 + <li key={g.id} className="py-2 text-[13px]">
598 + <div className="flex flex-wrap items-center gap-1.5">
599 + <Badge tone="outline" size="xs">
600 + {g.kind.replace(/_/g, " ")}
601 + </Badge>
602 + {g.sourceKind && <SourceBadge kind={g.sourceKind} name={g.sourceName} url={g.url} />}
603 + <span className="figure ml-auto text-[11.5px] text-ink-3">{fmtDate(g.effectiveDate)}</span>
604 + </div>
605 + <a href={g.url} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 font-medium text-ink hover:text-accent">
606 + {g.title} <ExternalLink className="size-3" />
607 + </a>
608 + {g.summary && <p className="mt-0.5 text-[12.5px] text-ink-2">{g.summary}</p>}
523 609 </li>
524 610 ))}
525 611 </ul>
526 612 ) : (
527 − <p className="text-[12.5px] text-ink-3">{f.ixpCount ? "Names not published; count from PeeringDB." : "None listed."}</p>
613 + <p className="text-[12.5px] text-ink-3">No public grid constraint reported for this market.</p>
528 614 )}
615 + {powerEmpty && power?.note && <p className="mt-3 text-[11px] text-ink-4">{power.note}</p>}
529 616 </div>
530 617 </div>
531 − )}
532 − </Section>
533 −
534 − {/* ------------------------------------------------------------ cloud regions */}
535 − {f.cloudRegions.length > 0 && (
536 − <Section id="cloud" title={`Cloud regions in ${f.metro ? `the ${f.metro.name} market` : "this market"}`} className="scroll-mt-28" description="Public cloud regions published for the same metro. Cloud providers publish the metro, not the building — presence here does not imply this facility hosts them." aside={<Link href={routes.cloudRegions()} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>
537 − <Table dense>
538 − <THead>
539 − <tr>
540 − <Th>Provider</Th>
541 − <Th>Code</Th>
542 − <Th className="hidden sm:table-cell">Name</Th>
543 − <Th numeric className="hidden md:table-cell">
544 − AZs
545 − </Th>
546 − <Th className="hidden md:table-cell">Launched</Th>
547 − <Th>Status</Th>
548 − </tr>
549 − </THead>
550 − <TBody>
551 − {f.cloudRegions.map((cr) => (
552 − <Tr key={cr.id}>
553 − <Td primary>
554 − <Link href={routes.operator(cr.provider.slug)} className="font-medium text-ink hover:text-accent hover:underline">
555 − {cr.provider.name}
556 − </Link>
557 − <span className="ml-2 text-[12px] text-ink-3 sm:hidden">{cr.name}</span>
558 − </Td>
559 − <Td label="Code" className="figure text-[12.5px]">
560 − <RowLink href={routes.cloudRegion(cr.slug)} className="font-normal">
561 − {cr.code}
562 − </RowLink>
563 − </Td>
564 − <Td label="Name" className="hidden text-ink-2 sm:table-cell">
565 − {cr.name}
566 − </Td>
567 − <Td label="AZs" numeric className="hidden md:table-cell">
568 − {cr.availabilityZones ?? "—"}
569 − </Td>
570 − <Td label="Launched" className="figure hidden text-[12.5px] text-ink-3 md:table-cell">
571 − {fmtDate(cr.launchedOn)}
572 − </Td>
573 − <Td label="Status">
574 − <Badge tone={cr.status === "operational" ? "ok" : cr.status === "announced" ? "info" : cr.status === "restricted" ? "warn" : "neutral"} size="xs">
575 − {cr.status}
576 − </Badge>
577 − {cr.isSovereign && (
578 − <Badge tone="accent" size="xs" className="ml-1">
579 − Sovereign
618 + </Section>
619 +
620 + {/* ------------------------------------------------------------ cloud / tenants */}
621 + <Section id="cloud" title="Cloud / tenants" className="scroll-mt-28" description={f.cloudRegions.length ? "Public cloud regions published for the same metro. Providers publish the metro, not the building — presence here does not imply this facility hosts them." : "Cloud providers publish regions per metro, not per building."} aside={<Link href={routes.cloudRegions()} className="text-[12.5px] text-ink-3 hover:text-ink">All cloud regions →</Link>}>
622 + {f.cloudRegions.length ? (
623 + <Table dense>
624 + <THead>
625 + <tr>
626 + <Th>Provider</Th>
627 + <Th>Code</Th>
628 + <Th className="hidden sm:table-cell">Name</Th>
629 + <Th numeric className="hidden md:table-cell">
630 + AZs
631 + </Th>
632 + <Th className="hidden md:table-cell">Launched</Th>
633 + <Th>Status</Th>
634 + </tr>
635 + </THead>
636 + <TBody>
637 + {f.cloudRegions.map((cr) => (
638 + <Tr key={cr.id}>
639 + <Td primary>
640 + <Link href={routes.operator(cr.provider.slug)} className="font-medium text-ink hover:text-accent hover:underline">
641 + {cr.provider.name}
642 + </Link>
643 + <span className="ml-2 text-[12px] text-ink-3 sm:hidden">{cr.name}</span>
644 + </Td>
645 + <Td label="Code" className="figure text-[12.5px]">
646 + <RowLink href={routes.cloudRegion(cr.slug)} className="font-normal">
647 + {cr.code}
648 + </RowLink>
649 + </Td>
650 + <Td label="Name" className="hidden text-ink-2 sm:table-cell">
651 + {cr.name}
652 + </Td>
653 + <Td label="AZs" numeric className="hidden md:table-cell">
654 + {cr.availabilityZones ?? "—"}
655 + </Td>
656 + <Td label="Launched" className="figure hidden text-[12.5px] text-ink-3 md:table-cell">
657 + {fmtDate(cr.launchedOn)}
658 + </Td>
659 + <Td label="Status">
660 + <Badge tone={cr.status === "operational" ? "ok" : cr.status === "announced" ? "info" : cr.status === "restricted" ? "warn" : "neutral"} size="xs">
661 + {cr.status}
580 662 </Badge>
581 − )}
582 − </Td>
583 − </Tr>
663 + {cr.isSovereign && (
664 + <Badge tone="accent" size="xs" className="ml-1">
665 + Sovereign
666 + </Badge>
667 + )}
668 + </Td>
669 + </Tr>
670 + ))}
671 + </TBody>
672 + </Table>
673 + ) : (
674 + <p className="text-[12.5px] text-ink-3">No public cloud region is published for this market.</p>
675 + )}
676 + {tenants.length > 0 && (
677 + <p className="mt-3 text-[12.5px] text-ink-2">
678 + Published tenants: {tenants.map((t, i) => (
679 + <span key={t.id}>
680 + {i > 0 && ", "}
681 + <Link href={routes.operator(t.slug)} className="hover:text-accent hover:underline">
682 + {t.name}
683 + </Link>
684 + </span>
584 685 ))}
585 − </TBody>
586 − </Table>
686 + .
687 + </p>
688 + )}
587 689 </Section>
588 − )}
589 690
590 − {/* ------------------------------------------------------------ projects */}
591 − {f.projects.length > 0 && (
592 − <Section id="projects" title="Projects" className="scroll-mt-28" description="Announced or under-construction projects linked to this facility or to the same operator in this market.">
593 − <ProjectList items={f.projects} />
691 + {/* ------------------------------------------------------------ project phases */}
692 + <Section id="phases" title="Project phases" className="scroll-mt-28" description="Announced or under-construction phases linked to this facility, or to the same operator in this market. Planned MW are published figures (≈).">
693 + <ProjectList items={f.projects} emptyTitle="No project phase linked" emptyDescription="Expansion phases appear here when a source publishes them." />
594 694 </Section>
595 − )}
596 −
597 − {/* ------------------------------------------------------------ nearby */}
598 − {nearby.length > 0 && (
599 − <Section id="nearby" title="Nearby facilities" className="scroll-mt-28" description="Indexed facilities within 25 km (great-circle distance between published coordinates).">
600 − <ul className="divide-y divide-hair">
601 − {nearby.map((n) => {
602 − const { mw, kind } = bestMw(n);
603 − return (
604 − <li key={n.id} className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 py-2.5">
605 − <div className="min-w-0">
606 − <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
607 − <Link href={routes.facility(n.slug)} className="truncate text-[13.5px] font-medium text-ink hover:text-accent hover:underline">
608 − {n.name}
609 − </Link>
610 − <StatusBadge status={n.status} short size="xs" />
611 − <FlagBadges isAi={n.isAi} isHyperscale={n.isHyperscale} />
612 − </div>
613 − <div className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-[12px] text-ink-3">
614 − {n.operator ? (
615 − <Link href={routes.operator(n.operator.slug)} className="text-ink-2 hover:text-ink">
616 − {n.operator.name}
617 − </Link>
618 − ) : (
619 − <span>Operator unknown</span>
620 − )}
621 − {n.city && (
622 − <>
623 − <span aria-hidden>·</span>
624 − <span className="truncate">{n.city}</span>
625 − </>
626 − )}
627 − {isImprecise(n.geoPrecision) && (
628 − <>
629 − <span aria-hidden>·</span>
630 − <span>{PRECISION_LABEL[n.geoPrecision].toLowerCase()}</span>
631 − </>
632 − )}
633 − </div>
634 − </div>
635 − <div className="flex flex-col items-end text-right">
636 − <span className="figure text-[13.5px] font-medium text-ink">
637 − {mw === null ? (
638 − <span className="text-ink-4">—</span>
639 − ) : (
640 − <>
641 − {kind === "planned" && <EstimateMark className="mr-0.5 text-[11px]" />}
642 − {fmtMw(mw)}
643 − </>
644 − )}
645 − </span>
646 − <span className="figure text-[11px] text-ink-3">{n.km === null ? "distance n/a" : n.km < 1 ? `${Math.round(n.km * 1000)} m` : `${n.km.toFixed(n.km < 10 ? 1 : 0)} km`}</span>
647 − </div>
648 − </li>
649 − );
650 − })}
651 − </ul>
695 +
696 + {/* ------------------------------------------------------------ buildings / campus */}
697 + {isCampus ? (
698 + <Section id="buildings" title="Buildings" className="scroll-mt-28" description="Buildings contained in this campus. A campus whose buildings publish figures contributes no MW itself; totals are never double-counted." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(f.buildings.length)}</span>}>
699 + <FacilityTable items={f.buildings} columns={["operator", "status", "mw", "type", "confidence"]} emptyTitle="No building published yet" emptyDescription="This record is a campus; individual buildings appear once a source publishes them." />
700 + </Section>
701 + ) : (
702 + <Section id="campus" title="Campus" className="scroll-mt-28" description="Containment: a building's figure never stands for its campus, and a campus figure is never written to a building.">
703 + {parent ? (
704 + <p className="text-[13.5px] text-ink-2">
705 + This {f.recordScope === "building" ? "building" : "facility"} is part of{" "}
706 + <Link href={routes.facility(parent.slug)} className="font-medium text-ink hover:text-accent hover:underline">
707 + {parent.name}
708 + </Link>
709 + .
710 + </p>
711 + ) : (
712 + <p className="text-[12.5px] text-ink-3">Not part of a published campus record.</p>
713 + )}
714 + </Section>
715 + )}
716 +
717 + {/* ------------------------------------------------------------ nearby */}
718 + <Section id="nearby" title="Nearby infrastructure" className="scroll-mt-28" description="Great-circle distance between published coordinates. Proximity never implies a connection.">
719 + {hasGeo ? <NearbyInfrastructurePanel initial={f.nearbyInfrastructure} center={{ lat: f.lat!, lng: f.lng! }} excludeId={f.id} /> : <NotPublished what="Coordinates — nearby infrastructure" />}
720 + </Section>
721 +
722 + {/* ------------------------------------------------------------ event history */}
723 + <Section id="events" title="Event history" className="scroll-mt-28" description="Detected changes to this record — status, capacity, dates, operator — with their source and significance." aside={<Link href={routes.live()} className="inline-flex items-center gap-1 text-[12.5px] text-ink-3 hover:text-ink">Live feed <ExternalLink className="size-3" /></Link>}>
724 + <EventList events={f.events} dense emptyTitle="No change detected yet" emptyDescription="Status, capacity and date changes will be listed here as connectors re-crawl the sources." />
725 + {f.sourceHistory.length > 0 && (
726 + <details className="mt-4 group">
727 + <summary className="cursor-pointer text-[12.5px] font-medium text-ink-2 hover:text-ink">
728 + Source observation log <span className="figure text-ink-3">{fmtInt(f.sourceHistory.length)}</span>
729 + </summary>
730 + <div className="mt-3">
731 + <SourceHistory items={[...f.sourceHistory].sort((a, b) => (a.date < b.date ? 1 : -1)).slice(0, 60)} />
732 + {f.sourceHistory.length > 60 && <p className="mt-2 text-[11.5px] text-ink-4">Showing the 60 most recent of {fmtInt(f.sourceHistory.length)} entries.</p>}
733 + </div>
734 + </details>
735 + )}
652 736 </Section>
653 − )}
654 −
655 − {/* ------------------------------------------------------------ provenance */}
656 − <Section id="provenance" title="Provenance" className="scroll-mt-28" description="Every field with the source it was observed on, when, at what confidence and by which method." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(prov.length)} rows</span>}>
657 − <ProvenanceTable items={prov} />
658 − <p className="mt-2 text-[11.5px] text-ink-4">
659 − {CONFIDENCE_LABEL[f.confidence]} overall — computed from the best source kind and the number of independent primary sources.{" "}
660 − <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">
661 − How confidence is computed
662 − </Link>
663 − </p>
664 − </Section>
665 −
666 − {/* ------------------------------------------------------------ source history */}
667 − {f.sourceHistory.length > 0 && (
668 − <Section id="history" title="Source history" className="scroll-mt-28" description="Observations, verifications and detected changes across sources, newest first.">
669 − <SourceHistory items={[...f.sourceHistory].sort((a, b) => (a.date < b.date ? 1 : -1)).slice(0, 60)} />
670 − {f.sourceHistory.length > 60 && <p className="mt-2 text-[11.5px] text-ink-4">Showing the 60 most recent of {fmtInt(f.sourceHistory.length)} entries.</p>}
737 +
738 + {/* ------------------------------------------------------------ source evidence */}
739 + <Section id="evidence" title="Source evidence" className="scroll-mt-28" description="Every observation behind this record: field, value, source, dates, confidence and method. “Shown” marks the winner backing the displayed value." aside={<span className="figure text-[12px] text-ink-3">{fmtInt(prov.length)} observations</span>}>
740 + <ProvenanceTable items={prov} />
741 + {claims.some((c) => /_usd$/.test(c.predicate)) && (
742 + <div className="mt-4">
743 + <div className="label-caps mb-2">Investment claims</div>
744 + <ClaimsTable claims={claims} kind="investment" />
745 + </div>
746 + )}
747 + <p className="mt-2 text-[11.5px] text-ink-4">
748 + {CONFIDENCE_LABEL[f.confidence]} overall — computed from the best source kind and the number of independent primary sources.{" "}
749 + <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">
750 + How confidence is computed
751 + </Link>
752 + </p>
671 753 </Section>
672 − )}
673 754
674 − {/* ------------------------------------------------------------ changes */}
675 − {f.events.length > 0 && (
676 − <Section id="changes" title="Changes" className="scroll-mt-28" description="Detected changes to this record — status, capacity, dates, operator." aside={<Link href={routes.live()} className="inline-flex items-center gap-1 text-[12.5px] text-ink-3 hover:text-ink">Live feed <ExternalLink className="size-3" /></Link>}>
677 − <EventList events={f.events} dense />
755 + {/* ------------------------------------------------------------ data quality */}
756 + <Section id="quality" title="Data quality" className="scroll-mt-28">
757 + <DataQualityPanel q={dq} />
678 758 </Section>
679 − )}
680 − </div>
759 + </div>
681 760
682 − <SourcesFooter sources={r.sources} updatedAt={f.updatedAt} />
683 − </Page>
761 + <SourcesFooter sources={r.sources} updatedAt={f.updatedAt} />
762 + </Page>
763 + </EvidenceProvider>
684 764 );
685 765 }
modified apps/web/src/app/(site)/datacenters/markets/[slug]/page.tsx +234 −160
@@ -10,16 +10,23 @@ import { fPageHref, PagedFacilities, parseFPage } from "@/components/entity/page
10 10 import { SectionNav } from "@/components/entity/section-nav";
11 11 import { SourcesFooter } from "@/components/entity/sources-footer";
12 12 import { EventList } from "@/components/event-list";
13 +import { ClaimsTable } from "@/components/evidence";
14 +import { FacilityTable } from "@/components/facility-table";
15 +import { MarketConcentrationPanel } from "@/components/intelligence/market/concentration";
16 +import { MarketMomentumPanel } from "@/components/intelligence/market/momentum";
17 +import { OpeningTimeline } from "@/components/intelligence/market/opening-timeline";
18 +import { ApiNote, CoverageBar, CoverageStrip, GridConstraintList, HonestEmpty, PillLink, PipelineBars, PublishedOnlyNote, ShareList } from "@/components/intelligence/shared";
19 +import { WatchButton } from "@/components/intelligence/watch-button";
13 20 import { ProjectList } from "@/components/project-list";
14 21 import { Badge, EstimateMark } from "@/components/ui/badge";
15 22 import { Page, Section } from "@/components/ui/card";
16 −import { BarList, StackedBar } from "@/components/ui/charts";
23 +import { StackedBar } from "@/components/ui/charts";
17 24 import { CountryFlag } from "@/components/ui/country-flag";
18 25 import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
19 26 import { ErrorState, OfflineNotice } from "@/components/ui/states";
20 27 import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
21 28 import type { DetailFacilitiesMeta } from "@/lib/dto";
22 −import { fmtDate, fmtInt, fmtMw } from "@/lib/format";
29 +import { fmtDate, fmtInt, fmtMw, fmtPct, pluralize, splitMw } from "@/lib/format";
23 30 import { routes } from "@/lib/routes";
24 31 import { compactJsonLd, NOT_FOUND_METADATA, pageMetadata } from "@/lib/seo";
25 32 import type { SearchParams } from "@/lib/sort";
@@ -33,12 +40,12 @@ type Props = { params: Promise<{ slug: string }>; searchParams: Promise<SearchPa
33 40
34 41 function describe(m: MetroDetail): string {
35 42 const parts = [`${fmtInt(m.facilityCount)} data center ${m.facilityCount === 1 ? "facility" : "facilities"} in ${m.name}${m.countryName ? `, ${m.countryName}` : ""}`];
36 − if (m.knownMw !== null) parts.push(`${fmtMw(m.knownMw)} known operational capacity`);
43 + if (m.knownMw !== null) parts.push(`${fmtMw(m.knownMw)} known operational capacity (${fmtPct(m.mwCoverage)} of facilities publish a figure)`);
37 44 const pipe = pipelineMw(m);
38 45 if (pipe !== null) parts.push(`${fmtMw(pipe)} in the pipeline`);
39 46 parts.push(`${fmtInt(m.operatorCount)} operators`);
40 47 if (m.cloudRegionCount) parts.push(`${fmtInt(m.cloudRegionCount)} cloud regions`);
41 − return `${parts.join(", ")}. Sourced facilities, operators, cloud regions, IXPs and projects for the ${m.name} market.`;
48 + return `${parts.join(", ")}. Operators, concentration, momentum, pipeline, connectivity, power context and coverage for the ${m.name} market — every figure sourced.`;
42 49 }
43 50
44 51 export async function generateMetadata({ params }: Props): Promise<Metadata> {
@@ -50,6 +57,18 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
50 57
51 58 const CONSTRAINT_LABEL: Record<MetroDetail["constraints"][number]["kind"], string> = { power: "Power", land: "Land", water: "Water", moratorium: "Moratorium", grid: "Grid" };
52 59
60 +function Mw({ mw, estimate }: { mw: number | null | undefined; estimate?: boolean }) {
61 + const s = splitMw(mw);
62 + if (!s) return <span className="text-ink-4">—</span>;
63 + return (
64 + <>
65 + {estimate && <EstimateMark className="mr-0.5 text-[0.6em]" />}
66 + {s.value}
67 + </>
68 + );
69 +}
70 +const unitOf = (mw: number | null | undefined) => splitMw(mw)?.unit ?? undefined;
71 +
53 72 export default async function MarketPage({ params, searchParams }: Props) {
54 73 const [{ slug }, sp] = await Promise.all([params, searchParams]);
55 74 const fPage = parseFPage(sp.fPage);
@@ -71,6 +90,11 @@ export default async function MarketPage({ params, searchParams }: Props) {
71 90 const pipe = pipelineMw(m);
72 91 const cHref = countryHref(map, m.countryIso2);
73 92 const base = routes.market(m.slug);
93 + const aiFacilities = m.aiFacilities ?? [];
94 + const gridConstraints = m.gridConstraints ?? [];
95 + const claims = m.claims ?? [];
96 + const openingTimeline = m.openingTimeline ?? [];
97 + const aiCount = m.aiCount ?? aiFacilities.length;
74 98
75 99 const statusData: Partial<Record<FacilityStatus, number>> = {};
76 100 if (m.operationalCount) statusData.operational = m.operationalCount;
@@ -80,17 +104,23 @@ export default async function MarketPage({ params, searchParams }: Props) {
80 104 if (other > 0) statusData.unknown = other;
81 105
82 106 const hasCloud = m.cloudProviders.length > 0 || m.cloudRegions.length > 0;
107 + const powerItems = gridConstraints.length + m.constraints.length;
83 108 const sections = [
84 109 { id: "map", label: "Map" },
85 110 { id: "status", label: "Status" },
86 111 { id: "operators", label: "Operators", count: m.operators.length },
87 − { id: "cloud", label: "Cloud", count: hasCloud ? m.cloudRegions.length || m.cloudProviders.length : 0 },
88 − { id: "ixps", label: "Internet exchanges", count: m.ixps.length },
89 − { id: "constraints", label: "Constraints", count: m.constraints.length },
90 − { id: "growth", label: "Growth" },
112 + { id: "pipeline", label: "Pipeline" },
113 + { id: "openings", label: "Openings" },
114 + { id: "concentration", label: "Concentration" },
115 + { id: "momentum", label: "Momentum" },
116 + { id: "connectivity", label: "Connectivity" },
117 + { id: "power", label: "Power" },
118 + { id: "coverage", label: "Coverage" },
119 + { id: "ai", label: "AI", count: aiFacilities.length },
91 120 { id: "projects", label: "Projects", count: m.projects.length },
92 121 { id: "changes", label: "Changes", count: m.recentEvents.length },
93 122 { id: "facilities", label: "Facilities", count: facTotal },
123 + { id: "claims", label: "Claims", count: claims.length },
94 124 ];
95 125
96 126 const jsonLd = compactJsonLd({
@@ -103,37 +133,48 @@ export default async function MarketPage({ params, searchParams }: Props) {
103 133 description: describe(m),
104 134 });
105 135
136 + const topOps = [...m.operators].sort((a, b) => b.facilityCount - a.facilityCount).slice(0, 15);
137 + const opsTotalFac = m.facilityCount || 1;
138 +
106 139 return (
107 140 <Page>
108 141 <Breadcrumbs items={[{ name: "Data centers", href: routes.datacenters() }, { name: "Markets", href: routes.markets() }, { name: m.name, href: base }]} className="pt-3" />
109 142 <JsonLd data={jsonLd} />
110 143
111 144 <header className="py-5 sm:py-7">
112 − <div className="label-caps mb-1.5">Market</div>
113 − <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">Data centers in {m.name}</h1>
114 − <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-ink-2">
115 − <CountryFlag iso2={m.countryIso2} size={13} />
116 − {cHref ? (
117 − <Link href={cHref} className="text-ink hover:text-accent hover:underline">
118 − {m.countryName ?? m.countryIso2}
119 − </Link>
120 − ) : (
121 − <span>{m.countryName ?? m.countryIso2}</span>
122 − )}
123 − {m.regionName && m.regionName !== m.name && (
124 − <>
145 + <div className="flex flex-wrap items-start justify-between gap-3">
146 + <div className="min-w-0">
147 + <div className="label-caps mb-1.5">Market</div>
148 + <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">Data centers in {m.name}</h1>
149 + <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-ink-2">
150 + <CountryFlag iso2={m.countryIso2} size={13} />
151 + {cHref ? (
152 + <Link href={cHref} className="text-ink hover:text-accent hover:underline">
153 + {m.countryName ?? m.countryIso2}
154 + </Link>
155 + ) : (
156 + <span>{m.countryName ?? m.countryIso2}</span>
157 + )}
158 + {m.regionName && m.regionName !== m.name && (
159 + <>
160 + <span aria-hidden className="text-ink-4">
161 + ·
162 + </span>
163 + <span className="text-ink-3">{m.regionName}</span>
164 + </>
165 + )}
125 166 <span aria-hidden className="text-ink-4">
126 167 ·
127 168 </span>
128 − <span className="text-ink-3">{m.regionName}</span>
129 − </>
130 − )}
131 − <span aria-hidden className="text-ink-4">
132 − ·
133 − </span>
134 − <span className="figure text-[12px] text-ink-3">
135 − {m.lat.toFixed(2)}, {m.lng.toFixed(2)}
136 − </span>
169 + <span className="figure text-[12px] text-ink-3">
170 + {m.lat.toFixed(2)}, {m.lng.toFixed(2)}
171 + </span>
172 + </div>
173 + </div>
174 + <div className="flex flex-wrap items-center gap-2">
175 + <WatchButton entityType="metro" slug={m.slug} entityId={m.id} name={m.name} />
176 + <PillLink href={routes.explore(`entity=facilities&metro=${m.slug}`)}>Explore →</PillLink>
177 + </div>
137 178 </div>
138 179 {m.description && <p className="mt-2 max-w-2xl text-[13.5px] text-ink-2">{m.description}</p>}
139 180 {m.aliases.length > 0 && (
@@ -164,34 +205,16 @@ export default async function MarketPage({ params, searchParams }: Props) {
164 205
165 206 <MetricRow cols={4}>
166 207 <MetricCell>
167 − <Metric value={fmtInt(m.facilityCount)} label="Facilities" size="sm" />
168 − </MetricCell>
169 − <MetricCell>
170 − <Metric value={m.knownMw === null ? "—" : fmtMw(m.knownMw).split(" ")[0]} unit={m.knownMw === null ? undefined : fmtMw(m.knownMw).split(" ")[1]} label="Known MW" note={m.knownMw === null ? "no published figure" : "operational, published"} size="sm" />
208 + <Metric value={fmtInt(m.facilityCount)} label="Facilities" note={`${fmtInt(m.operationalCount)} operational`} size="sm" />
171 209 </MetricCell>
172 210 <MetricCell>
173 − <Metric value={m.constructionMw === null ? "—" : fmtMw(m.constructionMw).split(" ")[0]} unit={m.constructionMw === null ? undefined : fmtMw(m.constructionMw).split(" ")[1]} label="Under construction" note={m.constructionMw === null ? `${fmtInt(m.constructionCount)} ${m.constructionCount === 1 ? "site" : "sites"}, MW not published` : `${fmtInt(m.constructionCount)} ${m.constructionCount === 1 ? "site" : "sites"}`} size="sm" />
211 + <Metric value={fmtInt(m.operatorCount)} label="Operators" size="sm" />
174 212 </MetricCell>
175 213 <MetricCell>
176 − <Metric
177 − value={
178 − m.plannedMw === null ? (
179 − "—"
180 − ) : (
181 − <>
182 − <EstimateMark className="mr-0.5 text-[0.6em]" />
183 − {fmtMw(m.plannedMw).split(" ")[0]}
184 − </>
185 − )
186 − }
187 − unit={m.plannedMw === null ? undefined : fmtMw(m.plannedMw).split(" ")[1]}
188 − label="Planned"
189 − note={`${fmtInt(m.plannedCount)} ${m.plannedCount === 1 ? "site" : "sites"}${m.plannedMw === null ? ", MW not published" : ""}`}
190 − size="sm"
191 − />
214 + <Metric value={<Mw mw={m.knownMw} />} unit={unitOf(m.knownMw)} label="Known MW" note={<CoverageBar value={m.mwCoverage ?? null} label="MW coverage" width="w-full max-w-[140px]" />} size="sm" />
192 215 </MetricCell>
193 216 <MetricCell>
194 − <Metric value={fmtInt(m.operatorCount)} label="Operators" size="sm" />
217 + <Metric value={<Mw mw={pipe} estimate />} unit={unitOf(pipe)} label="Pipeline MW" note={pipe === null ? `${fmtInt(m.constructionCount + m.plannedCount)} ${pluralize(m.constructionCount + m.plannedCount, "site")}, MW not published` : `${fmtInt(m.constructionCount)} under construction · ${fmtInt(m.plannedCount)} planned`} size="sm" />
195 218 </MetricCell>
196 219 <MetricCell>
197 220 <Metric value={fmtInt(m.cloudRegionCount)} label="Cloud regions" size="sm" />
@@ -200,14 +223,17 @@ export default async function MarketPage({ params, searchParams }: Props) {
200 223 <Metric value={fmtInt(m.ixpCount)} label="IXPs" size="sm" />
201 224 </MetricCell>
202 225 <MetricCell>
203 − <Metric value={fmtInt(m.projectCount)} label="Projects" size="sm" />
226 + <Metric value={fmtInt(aiCount)} label="AI / HPC sites" note="confirmed + likely evidence" size="sm" />
227 + </MetricCell>
228 + <MetricCell>
229 + <Metric value={fmtInt(m.projectCount)} label="Projects" note={m.projectPlannedMw ? `≈ ${fmtMw(m.projectPlannedMw)} planned` : undefined} size="sm" />
204 230 </MetricCell>
205 231 </MetricRow>
206 232
207 233 {m.rankings.length > 0 && (
208 234 <div className="-mx-3 flex gap-1.5 overflow-x-auto px-3 py-3 no-scrollbar sm:mx-0 sm:flex-wrap sm:px-0" aria-label="Ranking positions">
209 235 {m.rankings.map((rk) => (
210 − <Link key={rk.key} href={routes.ranking(rk.key)} className="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-xs border border-hair bg-surface px-2 text-[12px] text-ink-2 hover:border-hair-2 hover:text-ink">
236 + <Link key={rk.key} href={routes.ranking(rk.key)} className="inline-flex h-10 shrink-0 items-center gap-1.5 rounded-xs border border-hair bg-surface px-2 text-[12px] text-ink-2 hover:border-hair-2 hover:text-ink sm:h-7">
211 237 <span className="figure font-medium text-ink">#{rk.rank}</span>
212 238 <span className="figure text-ink-4">/ {rk.total}</span>
213 239 <span>{rk.label}</span>
@@ -227,85 +253,114 @@ export default async function MarketPage({ params, searchParams }: Props) {
227 253 <StackedBar data={statusData} total={m.facilityCount} />
228 254 </Section>
229 255
230 − {m.operators.length > 0 && (
231 − <Section id="operators" title="Operators" description="By facilities operated in this market." className="scroll-mt-28" aside={<span className="figure text-[12px] text-ink-3">{fmtInt(m.operatorCount)} total</span>}>
232 − <BarList items={m.operators.slice(0, 15).map((o) => ({ label: o.name, value: o.facilityCount, href: routes.operator(o.slug), secondary: o.knownMw !== null ? fmtMw(o.knownMw) : undefined }))} />
233 − </Section>
234 − )}
256 + <Section id="operators" title="Top operators" description="By facilities operated in this market; share of the market's facility count." className="scroll-mt-28" aside={<span className="figure text-[12px] text-ink-3">{fmtInt(m.operatorCount)} total</span>}>
257 + {topOps.length ? <ShareList items={topOps.map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), count: o.facilityCount, share: o.facilityCount / opsTotalFac, mw: o.knownMw ?? null }))} unit="facilities" /> : <HonestEmpty title="No operator attributed yet" />}
258 + </Section>
235 259
236 − {hasCloud && (
237 − <Section id="cloud" title="Cloud" description="Public cloud providers with a region in this market." className="scroll-mt-28">
238 − {m.cloudProviders.length > 0 && (
239 − <ul className="mb-3 flex flex-wrap gap-1.5">
240 − {m.cloudProviders.map((p) => (
241 − <li key={p.id}>
242 − <Link href={routes.operator(p.slug)} className="inline-flex h-7 items-center gap-1.5 rounded-xs border border-hair bg-surface px-2 text-[12.5px] text-ink hover:border-hair-2 hover:text-accent">
243 − {p.name}
244 − <span className="figure text-[11px] text-ink-3">
245 − {p.regionCount} {p.regionCount === 1 ? "region" : "regions"}
246 − </span>
247 − </Link>
248 − </li>
249 − ))}
250 − </ul>
251 − )}
252 − {m.cloudRegions.length > 0 && (
253 − <Table dense mobileCollapse={{ total: m.cloudRegions.length, label: "cloud regions" }}>
254 − <THead>
255 − <tr>
256 − <Th>Region</Th>
257 − <Th className="hidden sm:table-cell">Provider</Th>
258 − <Th className="hidden md:table-cell">Code</Th>
259 − <Th numeric>AZs</Th>
260 − <Th>Status</Th>
261 − <Th className="hidden lg:table-cell">Launched</Th>
262 − </tr>
263 − </THead>
264 − <TBody>
265 − {m.cloudRegions.map((cr) => (
266 − <Tr key={cr.id}>
267 − <Td primary>
268 − <RowLink href={routes.cloudRegion(cr.slug)}>{cr.name}</RowLink>
269 − <div className="text-[11.5px] text-ink-3 sm:hidden">
270 − {cr.provider.name} · <span className="figure">{cr.code}</span>
271 − </div>
272 − </Td>
273 − <Td label="Provider" className="hidden sm:table-cell">
274 − <Link href={routes.operator(cr.provider.slug)} className="text-ink-2 hover:text-ink hover:underline">
275 − {cr.provider.name}
276 − </Link>
277 − </Td>
278 − <Td label="Code" className="figure hidden text-[12px] md:table-cell">
279 − {cr.code}
280 − </Td>
281 − <Td label="AZs" numeric>
282 − {cr.availabilityZones ?? <span className="text-ink-4">—</span>}
283 − </Td>
284 − <Td label="Status">
285 − <span className="inline-flex flex-wrap gap-1">
286 − <Badge tone={cr.status === "operational" ? "ok" : cr.status === "announced" ? "info" : "neutral"} size="xs">
287 − {cr.status}
288 − </Badge>
289 − {cr.isSovereign && (
290 − <Badge tone="outline" size="xs" title="Sovereign cloud region">
291 − Sovereign
292 − </Badge>
293 − )}
260 + <Section id="pipeline" title="Construction pipeline" description="Facilities by lifecycle bucket with the MW they publish, then project records by stage." className="scroll-mt-28" aside={<Link href={routes.projects(`metro=${m.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">All projects →</Link>}>
261 + {m.pipeline ? <PipelineBars pipeline={m.pipeline} /> : <HonestEmpty title="Pipeline breakdown unavailable" />}
262 + <PublishedOnlyNote className="mt-3" />
263 + </Section>
264 +
265 + <Section id="openings" title="Opening timeline" description="Facilities opened per published opening year." className="scroll-mt-28">
266 + {openingTimeline.length ? (
267 + <OpeningTimeline data={openingTimeline} />
268 + ) : (
269 + <HonestEmpty title="No published opening dates for this market" detail="operators and registries indexed here have not published when their facilities opened; nothing is estimated" />
270 + )}
271 + {m.growth.length >= 2 && (
272 + <div className="mt-5">
273 + <div className="label-caps mb-2">Cumulative growth</div>
274 + <GrowthChart data={m.growth} />
275 + </div>
276 + )}
277 + </Section>
278 +
279 + <Section id="concentration" title="Market concentration" description="Herfindahl–Hirschman index and top-operator shares — on facility counts and, separately, on known MW." className="scroll-mt-28">
280 + {m.concentration ? <MarketConcentrationPanel c={m.concentration} facilityCount={m.facilityCount} /> : <HonestEmpty title="Concentration not computed" />}
281 + </Section>
282 +
283 + <Section id="momentum" title="Market momentum" description="Separate, transparent components over the last 12 months. There is no composite score: each count stands on its own and links to its evidence." className="scroll-mt-28">
284 + {m.momentum ? <MarketMomentumPanel m={m.momentum} metroSlug={m.slug} countryIso2={m.countryIso2} /> : <HonestEmpty title="Momentum not computed" />}
285 + </Section>
286 +
287 + <Section id="connectivity" title="Connectivity" description="Public cloud regions associated with this market and internet exchanges present in its facilities." className="scroll-mt-28" aside={<Link href={routes.connectivity()} className="text-[12px] text-ink-3 hover:text-ink">Connectivity index →</Link>}>
288 + {hasCloud ? (
289 + <div className="mb-5">
290 + <div className="label-caps mb-2">Cloud</div>
291 + {m.cloudProviders.length > 0 && (
292 + <ul className="mb-3 flex flex-wrap gap-1.5">
293 + {m.cloudProviders.map((p) => (
294 + <li key={p.id}>
295 + <Link href={routes.operator(p.slug)} className="inline-flex h-10 items-center gap-1.5 rounded-xs border border-hair bg-surface px-2 text-[12.5px] text-ink hover:border-hair-2 hover:text-accent sm:h-8">
296 + {p.name}
297 + <span className="figure text-[11px] text-ink-3">
298 + {p.regionCount} {p.regionCount === 1 ? "region" : "regions"}
294 299 </span>
295 − </Td>
296 − <Td label="Launched" className="figure hidden text-[12px] text-ink-3 lg:table-cell">
297 − {fmtDate(cr.launchedOn)}
298 − </Td>
299 − </Tr>
300 + </Link>
301 + </li>
300 302 ))}
301 − </TBody>
302 − </Table>
303 − )}
304 − </Section>
305 − )}
306 −
307 − {m.ixps.length > 0 && (
308 − <Section id="ixps" title="Internet exchanges" description="IXPs with a presence in this market (PeeringDB)." className="scroll-mt-28">
303 + </ul>
304 + )}
305 + {m.cloudRegions.length > 0 && (
306 + <Table dense mobileCollapse={{ total: m.cloudRegions.length, label: "cloud regions" }}>
307 + <THead>
308 + <tr>
309 + <Th>Region</Th>
310 + <Th className="hidden sm:table-cell">Provider</Th>
311 + <Th className="hidden md:table-cell">Code</Th>
312 + <Th numeric>AZs</Th>
313 + <Th>Status</Th>
314 + <Th className="hidden lg:table-cell">Launched</Th>
315 + </tr>
316 + </THead>
317 + <TBody>
318 + {m.cloudRegions.map((cr) => (
319 + <Tr key={cr.id}>
320 + <Td primary>
321 + <RowLink href={routes.cloudRegion(cr.slug)}>{cr.name}</RowLink>
322 + <div className="text-[11.5px] text-ink-3 sm:hidden">
323 + {cr.provider.name} · <span className="figure">{cr.code}</span>
324 + </div>
325 + </Td>
326 + <Td label="Provider" className="hidden sm:table-cell">
327 + <Link href={routes.operator(cr.provider.slug)} className="text-ink-2 hover:text-ink hover:underline">
328 + {cr.provider.name}
329 + </Link>
330 + </Td>
331 + <Td label="Code" className="figure hidden text-[12px] md:table-cell">
332 + {cr.code}
333 + </Td>
334 + <Td label="AZs" numeric>
335 + {cr.availabilityZones ?? <span className="text-ink-4">—</span>}
336 + </Td>
337 + <Td label="Status">
338 + <span className="inline-flex flex-wrap gap-1">
339 + <Badge tone={cr.status === "operational" ? "ok" : cr.status === "announced" ? "info" : "neutral"} size="xs">
340 + {cr.status}
341 + </Badge>
342 + {cr.isSovereign && (
343 + <Badge tone="outline" size="xs" title="Sovereign cloud region">
344 + Sovereign
345 + </Badge>
346 + )}
347 + </span>
348 + </Td>
349 + <Td label="Launched" className="figure hidden text-[12px] text-ink-3 lg:table-cell">
350 + {fmtDate(cr.launchedOn)}
351 + </Td>
352 + </Tr>
353 + ))}
354 + </TBody>
355 + </Table>
356 + )}
357 + <p className="mt-1.5 text-[11px] text-ink-4">Cloud regions are associated with the market, not hosted at a specific facility unless a source says so.</p>
358 + </div>
359 + ) : (
360 + <HonestEmpty title="No public cloud region associated with this market" className="mb-5" />
361 + )}
362 + <div className="label-caps mb-2">Internet exchanges</div>
363 + {m.ixps.length ? (
309 364 <ul className="divide-y divide-hair">
310 365 {m.ixps.map((x) => (
311 366 <li key={x.id} className="flex items-baseline justify-between gap-3 py-2">
@@ -316,46 +371,59 @@ export default async function MarketPage({ params, searchParams }: Props) {
316 371 </li>
317 372 ))}
318 373 </ul>
319 − </Section>
320 − )}
374 + ) : (
375 + <HonestEmpty title="No IXP presence recorded in this market" detail="IXP presence comes from published IXP ↔ facility links; none is inferred" />
376 + )}
377 + </Section>
321 378
322 − {m.constraints.length > 0 && (
323 − <Section id="constraints" title="Constraints" description="Power, land, water, grid and moratorium mentions detected in indexed sources for this market. Each item links to its source." className="scroll-mt-28">
324 − <ul className="divide-y divide-hair">
325 − {m.constraints.map((c, i) => (
326 − <li key={i} className="py-2.5">
327 − <div className="flex flex-wrap items-center gap-2">
328 − <Badge tone={c.kind === "moratorium" || c.kind === "power" ? "warn" : "outline"} size="xs">
329 − {CONSTRAINT_LABEL[c.kind]}
330 − </Badge>
331 − {c.date && (
332 − <time className="figure text-[11.5px] text-ink-3" dateTime={c.date}>
333 − {fmtDate(c.date)}
334 − </time>
335 − )}
336 − </div>
337 − <p className="mt-1 text-[13px] text-ink-2">{c.summary}</p>
338 − <a href={c.url} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 text-[11.5px] text-ink-3 hover:text-accent">
339 − {c.sourceName} <ExternalLink className="size-3" />
340 − </a>
341 − </li>
342 − ))}
343 − </ul>
344 − </Section>
345 − )}
379 + <Section id="power" title="Power context" description="Grid constraints, utility events and power agreements affecting this market." className="scroll-mt-28" aside={<Link href={routes.power()} className="text-[12px] text-ink-3 hover:text-ink">Power & grid →</Link>}>
380 + <GridConstraintList items={gridConstraints} showPlace={false} />
381 + {m.constraints.length > 0 && (
382 + <div className="mt-4">
383 + <div className="label-caps mb-1.5">Constraint mentions in indexed sources</div>
384 + <ul className="divide-y divide-hair">
385 + {m.constraints.map((c, i) => (
386 + <li key={i} className="py-2.5">
387 + <div className="flex flex-wrap items-center gap-2">
388 + <Badge tone={c.kind === "moratorium" || c.kind === "power" ? "warn" : "outline"} size="xs">
389 + {CONSTRAINT_LABEL[c.kind]}
390 + </Badge>
391 + {c.date && (
392 + <time className="figure text-[11.5px] text-ink-3" dateTime={c.date}>
393 + {fmtDate(c.date)}
394 + </time>
395 + )}
396 + </div>
397 + <p className="mt-1 text-[13px] text-ink-2">{c.summary}</p>
398 + <a href={c.url} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex min-h-6 items-center gap-1 text-[11.5px] text-ink-3 hover:text-accent">
399 + {c.sourceName} <ExternalLink className="size-3" />
400 + </a>
401 + </li>
402 + ))}
403 + </ul>
404 + </div>
405 + )}
406 + <ApiNote label="Reading" className="mt-3" note={`Observed events and curated public reports only — nothing is inferred${powerItems === 0 ? "; the absence of items means no report was indexed, not that the grid is unconstrained" : ""}. Utility and grid MW are supply-side figures, never IT load.`} />
407 + </Section>
346 408
347 − <Section id="growth" title="Growth" description="Cumulative facilities by published opening year. Facilities without an opening date are not charted." className="scroll-mt-28">
348 − <GrowthChart data={m.growth} />
409 + <Section id="coverage" title="Coverage" description="How much of this market the index actually knows — shown so the figures above can be read honestly." className="scroll-mt-28" aside={<Link href={routes.coverage()} className="text-[12px] text-ink-3 hover:text-ink">Coverage report →</Link>}>
410 + {m.coverage ? <CoverageStrip row={m.coverage} /> : <HonestEmpty title="Coverage not computed" />}
349 411 </Section>
350 412
413 + {aiFacilities.length > 0 && (
414 + <Section id="ai" title="AI / HPC facilities" description="Facilities with confirmed or likely AI evidence in this market." className="scroll-mt-28" aside={<Link href={routes.ai()} className="text-[12px] text-ink-3 hover:text-ink">AI infrastructure →</Link>}>
415 + <FacilityTable items={aiFacilities} columns={["operator", "status", "mw", "type"]} />
416 + </Section>
417 + )}
418 +
351 419 {m.projects.length > 0 && (
352 − <Section id="projects" title="Pipeline & projects" description="Announced, permitted and under-construction projects linked to this market." className="scroll-mt-28" aside={<Link href={routes.projects(`metro=${m.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">All projects →</Link>}>
420 + <Section id="projects" title="Projects" description="Announced, permitted and under-construction projects linked to this market." className="scroll-mt-28" aside={<Link href={routes.projects(`metro=${m.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">All projects →</Link>}>
353 421 <ProjectList items={m.projects} />
354 422 </Section>
355 423 )}
356 424
357 425 {m.recentEvents.length > 0 && (
358 − <Section id="changes" title="Recent changes" description="Detected changes on facilities and projects in this market." className="scroll-mt-28" aside={<Link href={routes.live(`country=${m.countryIso2}`)} className="text-[12px] text-ink-3 hover:text-ink">Live feed →</Link>}>
426 + <Section id="changes" title="Recent changes" description="Detected changes on facilities and projects in this market." className="scroll-mt-28" aside={<Link href={routes.live(`metro=${m.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">Live feed →</Link>}>
359 427 <EventList events={m.recentEvents} dense />
360 428 </Section>
361 429 )}
@@ -363,6 +431,12 @@ export default async function MarketPage({ params, searchParams }: Props) {
363 431 <Section id="facilities" title="Facilities" description="Sorted by best known capacity (IT, total or ≈ planned)." className="scroll-mt-28" aside={<span className="figure text-[12px] text-ink-3">{fmtInt(facTotal)}</span>}>
364 432 <PagedFacilities items={m.facilities} total={facTotal} page={facPage} perPage={facPer} hrefFor={(p) => fPageHref(base, sp, p)} columns={["operator", "status", "mw", "type", "confidence"]} emptyTitle="No facilities indexed in this market yet" />
365 433 </Section>
434 +
435 + {claims.length > 0 && (
436 + <Section id="claims" title="Claims about this market" description="Figures asserted about the market itself (metro-scoped) — shown as evidence, never summed into facility totals." className="scroll-mt-28">
437 + <ClaimsTable claims={claims} kind="all" />
438 + </Section>
439 + )}
366 440 </div>
367 441
368 442 <SourcesFooter sources={r.sources} note={pipe === null && m.knownMw === null ? "No MW figure has been published by the sources indexed for this market; counts are exact, capacities are shown only when a source publishes them." : undefined} />
added apps/web/src/app/(site)/developers/loading.tsx +14 −0
@@ -0,0 +1,14 @@
1 +import { Page } from "@/components/ui/card";
2 +import { Skeleton, SkeletonBlock, SkeletonRows } from "@/components/ui/skeleton";
3 +
4 +export default function Loading() {
5 + return (
6 + <Page className="py-4" aria-busy>
7 + <Skeleton className="mb-4 h-3 w-24" />
8 + <Skeleton className="mb-2 h-8 w-48" />
9 + <Skeleton className="mb-6 h-3.5 w-96 max-w-full" />
10 + <SkeletonBlock className="h-40" />
11 + <SkeletonRows rows={10} className="mt-6" />
12 + </Page>
13 + );
14 +}
added apps/web/src/app/(site)/developers/page.tsx +217 −0
@@ -0,0 +1,217 @@
1 +import type { ApiEndpointDoc } from "@dci/core";
2 +import type { Metadata } from "next";
3 +import Link from "next/link";
4 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
5 +import { ExtLink, KeyValueList } from "@/components/entity/kv";
6 +import { SectionNav } from "@/components/entity/section-nav";
7 +import { SourcesFooter } from "@/components/entity/sources-footer";
8 +import { CodeTabs } from "@/components/intelligence/developers/code-tabs";
9 +import { Badge } from "@/components/ui/badge";
10 +import { Button } from "@/components/ui/button";
11 +import { Page, PageHeader, Section } from "@/components/ui/card";
12 +import { EmptyState, OfflineNotice } from "@/components/ui/states";
13 +import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
14 +import { api } from "@/lib/api";
15 +import { fmtInt } from "@/lib/format";
16 +import { routes, SITE_URL } from "@/lib/routes";
17 +import { pageMetadata } from "@/lib/seo";
18 +
19 +export const revalidate = 600;
20 +
21 +export 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 +});
26 +
27 +const 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 +};
56 +const 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"];
57 +
58 +const linkCls = "text-ink underline decoration-hair-2 hover:text-accent";
59 +
60 +function 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 +}
63 +
64 +function 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 +}
71 +
72 +export 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))]));
81 +
82 + return (
83 + <Page>
84 + <Breadcrumbs items={[{ name: "API", href: routes.apiDocsPage() }]} className="pt-3" />
85 + <PageHeader
86 + 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 UI
94 + </Button>
95 + <Button size="sm" variant="ghost" href="/api/v1/openapi.json" external>
96 + OpenAPI 3.1 JSON
97 + </Button>
98 + </div>
99 + }
100 + />
101 + {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}
102 +
103 + <div className="rounded-sm border border-hair bg-surface px-3 py-3 sm:px-4">
104 + <KeyValueList
105 + 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>
126 +
127 + {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 + )}
145 +
146 + <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 +}
150 +
151 +function 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 +}
added apps/web/src/app/(site)/download/loading.tsx +14 −0
@@ -0,0 +1,14 @@
1 +import { Page } from "@/components/ui/card";
2 +import { Skeleton, SkeletonBlock, SkeletonRows } from "@/components/ui/skeleton";
3 +
4 +export default function Loading() {
5 + return (
6 + <Page className="py-4" aria-busy>
7 + <Skeleton className="mb-4 h-3 w-32" />
8 + <Skeleton className="mb-2 h-8 w-48" />
9 + <Skeleton className="mb-6 h-3.5 w-96 max-w-full" />
10 + <SkeletonBlock className="h-14" />
11 + <SkeletonRows rows={8} className="mt-6" />
12 + </Page>
13 + );
14 +}
added apps/web/src/app/(site)/download/page.tsx +134 −0
@@ -0,0 +1,134 @@
1 +import type { Metadata } from "next";
2 +import Link from "next/link";
3 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
4 +import { SectionNav } from "@/components/entity/section-nav";
5 +import { SourcesFooter } from "@/components/entity/sources-footer";
6 +import { DatasetForm } from "@/components/intelligence/download/dataset-form";
7 +import { ApiNote, HonestEmpty } from "@/components/intelligence/shared";
8 +import { Badge } from "@/components/ui/badge";
9 +import { Page, PageHeader, Section } from "@/components/ui/card";
10 +import { EmptyState, OfflineNotice } from "@/components/ui/states";
11 +import { api } from "@/lib/api";
12 +import { fmtInt } from "@/lib/format";
13 +import { routes } from "@/lib/routes";
14 +import { pageMetadata } from "@/lib/seo";
15 +
16 +export const revalidate = 120;
17 +
18 +export const metadata: Metadata = pageMetadata({
19 + title: "Downloads",
20 + description: "Download the index as CSV, JSON or GeoJSON: facilities, projects, operators, events, cloud regions, internet exchanges, countries and markets — licence-gated, with attribution and excluded sources listed per dataset.",
21 + path: routes.download(),
22 +});
23 +
24 +export default async function DownloadPage() {
25 + const r = await api.downloadDatasets();
26 + const datasets = r.ok ? r.data : [];
27 + return (
28 + <Page>
29 + <Breadcrumbs items={[{ name: "Downloads", href: routes.download() }]} className="pt-3" />
30 + <PageHeader eyebrow="Downloads" title="Datasets" description="Every dataset streams straight from the API with the filters below. Rows whose only sources forbid redistribution are excluded and listed; each row keeps the names of the sources it was built from." className="pt-3" />
31 + {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}
32 + <ApiNote label="Limits" note="Large datasets are capped per request (50 000 rows) — narrow with filters to fetch a complete slice. JSON responses carry meta.excludedSources; CSV/GeoJSON carry the same list in the x-dci-excluded-sources header. Figures are published values only, with the coverage caveats of the API." className="mb-4">
33 + {" "}
34 + <Link href={routes.apiDocsPage()} className="underline decoration-hair-2 hover:text-ink">
35 + API documentation
36 + </Link>
37 + </ApiNote>
38 +
39 + {datasets.length === 0 ? (
40 + <EmptyState title="No dataset available" description="The API did not return the dataset registry." />
41 + ) : (
42 + <>
43 + <SectionNav items={datasets.map((d) => ({ id: d.key, label: d.label, count: d.rows }))} />
44 + <div className="mt-6 flex flex-col gap-10">
45 + {datasets.map((d) => (
46 + <Section
47 + key={d.key}
48 + id={d.key}
49 + title={d.label}
50 + description={d.description}
51 + className="scroll-mt-28"
52 + aside={
53 + <div className="flex flex-col items-end gap-1">
54 + <span className="figure text-[13px] text-ink">
55 + {fmtInt(d.rows)} <span className="text-ink-3">rows</span>
56 + </span>
57 + <span className="inline-flex gap-1">
58 + {d.formats.map((f) => (
59 + <Badge key={f} tone="outline" size="xs" mono>
60 + {f}
61 + </Badge>
62 + ))}
63 + </span>
64 + </div>
65 + }
66 + >
67 + <div className="grid gap-5 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
68 + <div className="min-w-0">
69 + <DatasetForm dataset={d} />
70 + </div>
71 + <div className="min-w-0 flex flex-col gap-3 text-[12.5px]">
72 + <div>
73 + <div className="label-caps mb-1">Licence</div>
74 + <p className="leading-relaxed text-ink-2">{d.license}</p>
75 + </div>
76 + <div>
77 + <div className="label-caps mb-1">Attribution ({fmtInt(d.attribution.length)})</div>
78 + {d.attribution.length === 0 ? (
79 + <p className="text-ink-3">No attribution line recorded for this dataset&rsquo;s sources — the source registry applies.</p>
80 + ) : d.attribution.length <= 6 ? (
81 + <ul className="flex flex-wrap gap-1">
82 + {d.attribution.map((a) => (
83 + <li key={a}>
84 + <Badge tone="outline" size="xs" className="max-w-[260px]">
85 + <span className="truncate">{a}</span>
86 + </Badge>
87 + </li>
88 + ))}
89 + </ul>
90 + ) : (
91 + <details>
92 + <summary className="inline-flex min-h-10 cursor-pointer items-center text-ink-2 hover:text-ink sm:min-h-0">Show all {fmtInt(d.attribution.length)} attribution lines</summary>
93 + <ul className="mt-1.5 flex flex-wrap gap-1">
94 + {d.attribution.map((a) => (
95 + <li key={a}>
96 + <Badge tone="outline" size="xs" className="max-w-[260px]">
97 + <span className="truncate">{a}</span>
98 + </Badge>
99 + </li>
100 + ))}
101 + </ul>
102 + </details>
103 + )}
104 + </div>
105 + <div>
106 + <div className="label-caps mb-1">Excluded sources</div>
107 + {d.excludedSources.length === 0 ? (
108 + <p className="text-ink-3">None — every source behind this dataset allows redistribution with attribution.</p>
109 + ) : (
110 + <ul className="divide-y divide-hair">
111 + {d.excludedSources.map((s) => (
112 + <li key={s.id} className="py-1.5">
113 + <Link href={routes.source(s.id)} className="font-medium text-ink hover:text-accent hover:underline">
114 + {s.name}
115 + </Link>
116 + <span className="text-ink-3"> — {s.reason}</span>
117 + </li>
118 + ))}
119 + </ul>
120 + )}
121 + </div>
122 + {d.filters.length === 0 && <HonestEmpty title="Whole-table dataset" detail="no filter is defined for this key" />}
123 + </div>
124 + </div>
125 + </Section>
126 + ))}
127 + </div>
128 + </>
129 + )}
130 +
131 + <SourcesFooter sources={r.ok ? r.sources : undefined} note="Re-users must keep the per-row source attribution and respect each source's licence (see /sources). Nothing is imputed; rows without a published figure carry an empty cell, not zero." />
132 + </Page>
133 + );
134 +}
added apps/web/src/app/(site)/explore/loading.tsx +22 −0
@@ -0,0 +1,22 @@
1 +import { Page } from "@/components/ui/card";
2 +import { Skeleton, SkeletonBlock, SkeletonRows } from "@/components/ui/skeleton";
3 +
4 +export default function Loading() {
5 + return (
6 + <Page wide className="py-4" aria-busy>
7 + <Skeleton className="mb-4 h-3 w-32" />
8 + <Skeleton className="mb-2 h-8 w-56" />
9 + <Skeleton className="mb-6 h-3.5 w-96 max-w-full" />
10 + <div className="grid gap-6 lg:grid-cols-[300px_minmax(0,1fr)]">
11 + <div className="hidden lg:block">
12 + <SkeletonRows rows={10} />
13 + </div>
14 + <div>
15 + <Skeleton className="mb-3 h-6 w-64" />
16 + <SkeletonBlock className="h-10" />
17 + <SkeletonRows rows={12} className="mt-4" />
18 + </div>
19 + </div>
20 + </Page>
21 + );
22 +}
added apps/web/src/app/(site)/explore/page.tsx +161 −0
@@ -0,0 +1,161 @@
1 +import type { FacilityStatus, FacilitySummary, ProjectSummary } from "@dci/core";
2 +import type { Metadata } from "next";
3 +import Link from "next/link";
4 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
5 +import { SourcesFooter } from "@/components/entity/sources-footer";
6 +import { FacilityTable } from "@/components/facility-table";
7 +import { ExploreCharts } from "@/components/intelligence/explore/explore-charts";
8 +import { ExploreMap } from "@/components/intelligence/explore/explore-map";
9 +import { QueryBuilder } from "@/components/intelligence/explore/query-builder";
10 +import { ResultBar } from "@/components/intelligence/explore/result-bar";
11 +import { ApiNote } from "@/components/intelligence/shared";
12 +import { ProjectList } from "@/components/project-list";
13 +import { Page, PageHeader } from "@/components/ui/card";
14 +import { FilterChips, type Chip } from "@/components/ui/filter-chips";
15 +import { Pagination } from "@/components/ui/pagination";
16 +import { SegmentedControl } from "@/components/ui/segmented";
17 +import { EmptyState, OfflineNotice } from "@/components/ui/states";
18 +import { api } from "@/lib/api";
19 +import { EXPLORE_PER_PAGE, exploreHref, parseExploreQuery, toggleCsv } from "@/lib/explore-query";
20 +import { countryName, fmtInt } from "@/lib/format";
21 +import { CONFIDENCE_SHORT, PRECISION_SHORT, STATUS_LABEL, TYPE_LABEL } from "@/lib/labels";
22 +import { routes } from "@/lib/routes";
23 +import { pageMetadata } from "@/lib/seo";
24 +
25 +export const revalidate = 120;
26 +
27 +export const metadata: Metadata = pageMetadata({
28 + title: "Explore",
29 + description: "Structured query builder over every indexed facility and project: status, country, market, operator, type, published MW, AI evidence, dates, confidence and location precision — as a table, a map or charts. Every view is a shareable URL.",
30 + path: routes.explore(),
31 +});
32 +
33 +type SP = Record<string, string | string[] | undefined>;
34 +
35 +export default async function ExplorePage({ searchParams }: { searchParams: Promise<SP> }) {
36 + const sp = await searchParams;
37 + const q = parseExploreQuery(sp);
38 + const entity = q.entity ?? "facilities";
39 + const view = q.view ?? "table";
40 + const r = await api.explore(q);
41 + const data = r.ok ? r.data : null;
42 + const page = q.page ?? 1;
43 + const perPage = q.per_page ?? EXPLORE_PER_PAGE;
44 + const total = data?.total ?? 0;
45 + const methodology = r.ok ? (r.meta?.methodology as string | undefined) : undefined;
46 +
47 + const chips: Chip[] = [];
48 + const drop = (key: string, member?: string) => exploreHref(q, member ? { [key]: toggleCsv(q[key as keyof typeof q] as string | undefined, member) } : { [key]: undefined, ...(key === "min_mw" ? { max_mw: undefined } : {}), ...(key === "opened_from" ? { opened_to: undefined } : {}), ...(key === "expected_after" ? { expected_before: undefined } : {}) });
49 + if (q.q) chips.push({ key: "q", label: "Search", value: q.q, removeHref: drop("q") });
50 + for (const s of q.status?.split(",") ?? []) chips.push({ key: `st:${s}`, label: "Status", value: STATUS_LABEL[s as FacilityStatus] ?? s, removeHref: drop("status", s) });
51 + for (const c of q.country?.split(",") ?? []) chips.push({ key: `c:${c}`, label: "Country", value: countryName(c), removeHref: drop("country", c) });
52 + if (q.metro) chips.push({ key: "metro", label: "Market", value: q.metro, removeHref: drop("metro") });
53 + if (q.operator) chips.push({ key: "op", label: "Operator", value: q.operator, removeHref: drop("operator") });
54 + for (const t of q.type?.split(",") ?? []) chips.push({ key: `t:${t}`, label: "Type", value: TYPE_LABEL[t as keyof typeof TYPE_LABEL] ?? t, removeHref: drop("type", t) });
55 + if (q.min_mw !== undefined || q.max_mw !== undefined) chips.push({ key: "mw", label: "MW", value: q.min_mw !== undefined && q.max_mw !== undefined ? `${q.min_mw}–${q.max_mw}` : q.min_mw !== undefined ? `≥ ${q.min_mw}` : `≤ ${q.max_mw}`, removeHref: drop("min_mw") });
56 + if (q.ai) chips.push({ key: "ai", label: "AI evidence", value: q.ai === "any" ? "any" : `≥ ${q.ai}`, removeHref: drop("ai") });
57 + if (q.hyperscale) chips.push({ key: "hs", label: "Hyperscale", value: "yes", removeHref: drop("hyperscale") });
58 + if (q.has_mw) chips.push({ key: "hasmw", label: "Known MW", value: "yes", removeHref: drop("has_mw") });
59 + if (q.opened_from !== undefined || q.opened_to !== undefined) chips.push({ key: "opened", label: "Opened", value: `${q.opened_from ?? "…"}–${q.opened_to ?? "…"}`, removeHref: drop("opened_from") });
60 + if (q.expected_after !== undefined || q.expected_before !== undefined) chips.push({ key: "expected", label: "Expected", value: `${q.expected_after ?? "…"}–${q.expected_before ?? "…"}`, removeHref: drop("expected_after") });
61 + if (q.announced_since) chips.push({ key: "since", label: "Announced since", value: q.announced_since, removeHref: drop("announced_since") });
62 + for (const c of q.confidence?.split(",") ?? []) chips.push({ key: `cf:${c}`, label: "Confidence", value: CONFIDENCE_SHORT[c as keyof typeof CONFIDENCE_SHORT] ?? c, removeHref: drop("confidence", c) });
63 + for (const p of q.location_precision?.split(",") ?? []) chips.push({ key: `lp:${p}`, label: "Precision", value: PRECISION_SHORT[p as keyof typeof PRECISION_SHORT] ?? p, removeHref: drop("location_precision", p) });
64 + for (const c of q.project_class?.split(",") ?? []) chips.push({ key: `pc:${c}`, label: "Class", value: c.replace(/_/g, " ").toLowerCase(), removeHref: drop("project_class", c) });
65 +
66 + const viewHref = (v: "table" | "map" | "charts") => exploreHref(q, { view: v, page: undefined });
67 + const countryHrefBase = entity === "projects" ? routes.projects("country=") : routes.datacenters("country=");
68 + const items = data?.items ?? [];
69 + const facilities = entity === "facilities" ? (items as FacilitySummary[]) : [];
70 + const projects = entity === "projects" ? (items as ProjectSummary[]) : [];
71 +
72 + return (
73 + <Page wide>
74 + <Breadcrumbs items={[{ name: "Explore", href: routes.explore() }]} className="pt-3" />
75 + <PageHeader eyebrow="Explore" title="Query the index" description="Build a structured query over facilities or projects. Facets, charts and the map cover the whole filtered set; the URL is the saved view." className="pt-3" />
76 + {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}
77 +
78 + <div className="grid gap-6 lg:grid-cols-[300px_minmax(0,1fr)] lg:gap-8">
79 + <aside className="lg:sticky lg:top-16 lg:self-start" aria-label="Query builder">
80 + <QueryBuilder query={q} facets={data?.facets ?? null} />
81 + </aside>
82 +
83 + <div className="min-w-0">
84 + <div className="sticky top-12 z-[20] -mx-3 flex flex-col gap-2 border-b border-hair bg-paper/95 px-3 py-2 backdrop-blur sm:top-14 sm:mx-0 sm:px-0">
85 + <ResultBar total={r.ok ? total : null} entity={entity} mwCoverage={data ? data.mwCoverage : null} />
86 + <div className="flex flex-wrap items-center gap-2">
87 + <SegmentedControl
88 + label="View"
89 + value={view}
90 + options={[
91 + { value: "table", label: "Table", href: viewHref("table") },
92 + { value: "map", label: `Map`, href: viewHref("map"), title: data ? `${fmtInt(data.map.total)} points with coordinates` : undefined },
93 + { value: "charts", label: "Charts", href: viewHref("charts") },
94 + ]}
95 + />
96 + <FilterChips chips={chips} resetHref={exploreHref({ entity, view: q.view })} className="min-w-0 flex-1" />
97 + </div>
98 + </div>
99 +
100 + <div className="pt-4">
101 + {!data ? (
102 + <EmptyState title="Results unavailable" description="The index API did not respond. The page recovers automatically once it is reachable." />
103 + ) : total === 0 ? (
104 + <EmptyState title={`No ${entity === "projects" ? "project" : "infrastructure"} record matches`} description="Loosen a filter or clear them all — a zero here means nothing in the index matches, not that nothing exists." action={<Link href={exploreHref({ entity })} className="text-[12.5px] text-ink underline decoration-hair-2 hover:text-accent">Clear filters</Link>} />
105 + ) : view === "map" ? (
106 + <ExploreMap query={q} total={data.map.total} degraded={data.map.degraded} />
107 + ) : view === "charts" ? (
108 + <ExploreCharts charts={data.charts} entity={entity} countryHrefBase={countryHrefBase} />
109 + ) : (
110 + <>
111 + {entity === "facilities" ? <FacilityTable items={facilities} columns={["operator", "market", "country", "status", "mw", "type", "confidence"]} emptyTitle="No facility on this page" /> : <ProjectList items={projects} emptyTitle="No project on this page" />}
112 + <Pagination page={page} perPage={perPage} total={total} hrefFor={(p) => exploreHref(q, { page: p })} label={entity === "projects" ? "projects" : "facilities"} className="mt-3" />
113 + </>
114 + )}
115 + </div>
116 +
117 + {data && (
118 + <div className="mt-6 grid gap-3 sm:grid-cols-2">
119 + <FacetBlock title="Status" items={data.facets.status.map((f) => ({ key: f.key, label: STATUS_LABEL[f.key as FacilityStatus] ?? f.key, count: f.count, href: exploreHref(q, { status: toggleCsv(q.status, f.key), page: undefined }), active: (q.status ?? "").split(",").includes(f.key) }))} />
120 + <FacetBlock title="AI evidence" items={data.facets.ai.map((f) => ({ key: f.key, label: f.key, count: f.count, href: f.key === "unknown" ? undefined : exploreHref(q, { ai: f.key as never, page: undefined }), active: q.ai === f.key }))} note="Counts by the strongest evidence level on each record; “unknown” = no signal." />
121 + {data.facets.type && data.facets.type.length > 0 && <FacetBlock title="Type" items={data.facets.type.map((f) => ({ key: f.key, label: TYPE_LABEL[f.key as keyof typeof TYPE_LABEL] ?? f.key, count: f.count, href: exploreHref(q, { type: toggleCsv(q.type, f.key), page: undefined }), active: (q.type ?? "").split(",").includes(f.key) }))} />}
122 + <FacetBlock title="Operators" items={data.facets.operator.slice(0, 12).map((f) => ({ key: f.key, label: f.name, count: f.count, href: exploreHref(q, { operator: q.operator === f.key ? undefined : f.key, page: undefined }), active: q.operator === f.key }))} />
123 + </div>
124 + )}
125 +
126 + <ApiNote note={methodology} label="Method" className="mt-6" />
127 + </div>
128 + </div>
129 +
130 + <SourcesFooter sources={r.ok ? r.sources : undefined} note="Result counts are exact over indexed records; MW figures are published values only (containment-aware), never extrapolated. mwCoverage is the share of matching records with any MW figure." />
131 + </Page>
132 + );
133 +}
134 +
135 +function FacetBlock({ title, items, note }: { title: string; items: Array<{ key: string; label: string; count: number; href?: string; active: boolean }>; note?: string }) {
136 + if (!items.length) return null;
137 + return (
138 + <div className="rounded-sm border border-hair bg-surface px-3 py-2.5">
139 + <h2 className="label-caps mb-1.5">{title}</h2>
140 + <ul className="flex flex-wrap gap-1.5">
141 + {items.map((it) => {
142 + const cls = `inline-flex h-10 items-center gap-1.5 rounded-sm border px-2 text-[12px] sm:h-7 ${it.active ? "border-ink bg-ink text-paper" : "border-hair-2 bg-surface text-ink-2 hover:bg-surface-2"}`;
143 + return (
144 + <li key={it.key}>
145 + {it.href ? (
146 + <Link href={it.href} scroll={false} className={cls} aria-pressed={it.active}>
147 + {it.label} <span className="figure text-[11px] opacity-70">{fmtInt(it.count)}</span>
148 + </Link>
149 + ) : (
150 + <span className={cls}>
151 + {it.label} <span className="figure text-[11px] opacity-70">{fmtInt(it.count)}</span>
152 + </span>
153 + )}
154 + </li>
155 + );
156 + })}
157 + </ul>
158 + {note && <p className="mt-1.5 text-[11px] text-ink-4">{note}</p>}
159 + </div>
160 + );
161 +}
modified apps/web/src/app/(site)/ixps/[slug]/page.tsx +109 −14
@@ -2,17 +2,22 @@ import type { Metadata } from "next";
2 2 import Link from "next/link";
3 3 import { notFound } from "next/navigation";
4 4 import { Breadcrumbs } from "@/components/entity/breadcrumbs";
5 +import { EntityMap } from "@/components/entity/entity-map";
5 6 import { JsonLd } from "@/components/entity/json-ld";
6 7 import { ExtLink, KeyValueList } from "@/components/entity/kv";
7 8 import { SectionNav } from "@/components/entity/section-nav";
8 9 import { SourcesFooter } from "@/components/entity/sources-footer";
9 10 import { FacilityTable } from "@/components/facility-table";
11 +import { ApiNote, HonestEmpty, ShareList } from "@/components/intelligence/shared";
12 +import { StatusBadge } from "@/components/ui/badge";
10 13 import { Page, Section } from "@/components/ui/card";
11 14 import { CountryFlag } from "@/components/ui/country-flag";
12 15 import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
13 −import { EmptyState, OfflineNotice } from "@/components/ui/states";
16 +import { OfflineNotice } from "@/components/ui/states";
17 +import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
18 +import { SourceHistory } from "@/components/ui/timeline";
14 19 import { api } from "@/lib/api";
15 −import { countryName, fmtInt } from "@/lib/format";
20 +import { countryName, fmtInt, placeLine, pluralize } from "@/lib/format";
16 21 import { routes } from "@/lib/routes";
17 22 import { NOT_FOUND_METADATA, absoluteUrl, compactJsonLd, pageMetadata } from "@/lib/seo";
18 23 import { countryHrefMap } from "../../cloud-regions/cloud-format";
@@ -21,12 +26,13 @@ export const revalidate = 300;
21 26
22 27 type Params = Promise<{ slug: string }>;
23 28
24 −function describe(d: { name: string; nameLong: string | null; city: string | null; countryIso2: string | null; networkCount: number | null; facilityCount: number }): string {
29 +function describe(d: { name: string; nameLong: string | null; city: string | null; countryIso2: string | null; networkCount: number | null; facilityCount: number; operators?: unknown[] }): string {
25 30 const place = [d.city, d.countryIso2 ? countryName(d.countryIso2) : null].filter(Boolean).join(", ");
26 31 const parts = [`${d.name}${d.nameLong && d.nameLong !== d.name ? ` (${d.nameLong})` : ""} — internet exchange point${place ? ` in ${place}` : ""}`];
27 32 if (d.networkCount !== null) parts.push(`${fmtInt(d.networkCount)} connected networks`);
28 33 parts.push(`present in ${fmtInt(d.facilityCount)} indexed ${d.facilityCount === 1 ? "facility" : "facilities"}`);
29 − return `${parts.join(", ")}.`;
34 + if (d.operators?.length) parts.push(`${fmtInt(d.operators.length)} operators`);
35 + return `${parts.join(", ")}. Facilities, operators, nearby infrastructure and sources.`;
30 36 }
31 37
32 38 /** External identifier → public URL when the registry is known. */
@@ -62,6 +68,11 @@ export default async function IxpPage({ params }: { params: Params }) {
62 68 const cHref = countryHref(d.countryIso2);
63 69 const ids = Object.entries(d.externalIds ?? {});
64 70 const facilities = d.facilities ?? [];
71 + const operators = [...(d.operators ?? [])].sort((a, b) => b.facilityCount - a.facilityCount);
72 + const opTotal = operators.reduce((a, o) => a + o.facilityCount, 0) || 1;
73 + const nearby = d.nearbyFacilities ?? [];
74 + const hasGeo = typeof d.lat === "number" && typeof d.lng === "number";
75 + const history = d.sourceHistory ?? [];
65 76
66 77 const jsonLd = compactJsonLd({
67 78 "@context": "https://schema.org",
@@ -74,6 +85,15 @@ export default async function IxpPage({ params }: { params: Params }) {
74 85 address: d.city || cName ? compactJsonLd({ "@type": "PostalAddress", addressLocality: d.city, addressCountry: d.countryIso2 }) : undefined,
75 86 });
76 87
88 + const sections = [
89 + { id: "location", label: "Location" },
90 + { id: "facilities", label: "Facilities", count: facilities.length },
91 + { id: "operators", label: "Operators", count: operators.length },
92 + { id: "nearby", label: "Nearby infrastructure" },
93 + { id: "identifiers", label: "Identifiers", count: ids.length },
94 + { id: "history", label: "Source history", count: history.length },
95 + ];
96 +
77 97 return (
78 98 <Page>
79 99 <JsonLd data={jsonLd} />
@@ -115,27 +135,96 @@ export default async function IxpPage({ params }: { params: Params }) {
115 135 </p>
116 136 </header>
117 137
118 − <MetricRow cols={3}>
138 + <MetricRow cols={4}>
119 139 <MetricCell>
120 − <Metric value={d.networkCount === null ? "—" : fmtInt(d.networkCount)} label="Connected networks" note={d.networkCount === null ? "not published" : "as published by the source"} />
140 + <Metric value={d.networkCount === null ? "—" : fmtInt(d.networkCount)} label="Participants (networks)" note={d.networkCount === null ? "not published by the sources indexed" : "as published by the source"} />
121 141 </MetricCell>
122 142 <MetricCell>
123 143 <Metric value={fmtInt(d.facilityCount)} label="Facilities" note="indexed data centers with this exchange present" />
124 144 </MetricCell>
145 + <MetricCell>
146 + <Metric value={fmtInt(operators.length)} label="Operators" note={operators.length ? "operating those facilities" : "no facility linked yet"} />
147 + </MetricCell>
125 148 <MetricCell>
126 149 <Metric value={d.regionContinent ?? cName ?? "—"} label={d.regionContinent ? "Region" : "Country"} />
127 150 </MetricCell>
128 151 </MetricRow>
129 152
130 − <SectionNav items={[{ id: "facilities", label: "Facilities" }, { id: "identifiers", label: "Identifiers", count: ids.length }]} className="mt-4" />
153 + <SectionNav items={sections} className="mt-4" />
131 154
132 155 <div className="mt-6 flex flex-col gap-8">
133 − <Section id="facilities" title="Facilities where this exchange is present" description={facilities.length ? `${fmtInt(facilities.length)} indexed ${facilities.length === 1 ? "facility" : "facilities"}` : undefined} className="scroll-mt-28">
134 − {facilities.length ? <FacilityTable items={facilities} columns={["operator", "market", "country", "status", "mw"]} /> : <EmptyState compact title="No facility linked yet" description="Facility presence comes from the exchange's published facility list or from facility pages; it appears as soon as it is observed." />}
156 + <Section id="location" title="Location" description={hasGeo ? "Reference point of the exchange (metro reference when the exchange publishes no coordinates)." : "The exchange publishes no coordinates of its own."} className="scroll-mt-28">
157 + {hasGeo ? <EntityMap center={{ lat: d.lat as number, lng: d.lng as number }} zoom={9} filters={d.countryIso2 ? { country: d.countryIso2 } : undefined} precision="metro" openLabel={`Open ${d.name} in map`} className="h-[220px] sm:h-[320px]" /> : <HonestEmpty title="No coordinates" detail={d.metro ? <>the exchange is placed in the <Link href={routes.market(d.metro.slug)} className="underline decoration-hair-2 hover:text-ink">{d.metro.name}</Link> market on maps</> : "no metro reference either — the exchange is listed but not mapped"} />}
158 + <div className="mt-3">
159 + <KeyValueList
160 + items={[
161 + { label: "City", value: d.city },
162 + { label: "Country", value: cName ? (cHref ? <Link href={cHref} className="hover:text-accent hover:underline">{cName}</Link> : cName) : null },
163 + { label: "Market", value: d.metro ? <Link href={routes.market(d.metro.slug)} className="hover:text-accent hover:underline">{d.metro.name}</Link> : null },
164 + { label: "Website", value: d.website ? <ExtLink href={d.website} /> : null },
165 + ]}
166 + columns={2}
167 + />
168 + </div>
169 + <ApiNote note={d.trafficNote} label="Traffic & location" className="mt-3" />
170 + </Section>
171 +
172 + <Section id="facilities" title="Facilities where this exchange is present" description={facilities.length ? `${fmtInt(facilities.length)} indexed ${pluralize(facilities.length, "facility", "facilities")}` : undefined} className="scroll-mt-28">
173 + {facilities.length ? <FacilityTable items={facilities} columns={["operator", "market", "country", "status", "mw"]} /> : <HonestEmpty title="No facility linked yet" detail="facility presence comes from the exchange's published facility list or from facility pages; it appears as soon as it is observed" />}
135 174 </Section>
136 175
137 − {ids.length > 0 && (
138 − <Section id="identifiers" title="Identifiers" description="External registry identifiers as observed." className="scroll-mt-28">
176 + <Section id="operators" title="Operators present" description="Operators of the facilities where the exchange is reachable, by number of facilities." className="scroll-mt-28">
177 + {operators.length ? <ShareList items={operators.slice(0, 20).map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), count: o.facilityCount, share: o.facilityCount / opTotal }))} /> : <HonestEmpty title="No operator known" detail="operators appear once a linked facility carries one" />}
178 + </Section>
179 +
180 + <Section id="nearby" title="Nearby infrastructure" description="Indexed facilities around the exchange's reference point, by distance." className="scroll-mt-28">
181 + {nearby.length ? (
182 + <Table dense mobileCollapse={{ total: nearby.length, label: "facilities" }}>
183 + <THead>
184 + <tr>
185 + <Th>Facility</Th>
186 + <Th className="hidden sm:table-cell">Operator</Th>
187 + <Th className="hidden md:table-cell">Place</Th>
188 + <Th>Status</Th>
189 + <Th numeric>Distance</Th>
190 + </tr>
191 + </THead>
192 + <TBody>
193 + {nearby.map((f) => (
194 + <Tr key={f.id}>
195 + <Td primary>
196 + <RowLink href={routes.facility(f.slug)}>{f.name}</RowLink>
197 + <div className="text-[11.5px] text-ink-3 sm:hidden">{f.operator?.name ?? "Operator unknown"}</div>
198 + </Td>
199 + <Td label="Operator" className="hidden sm:table-cell">
200 + {f.operator ? (
201 + <Link href={routes.operator(f.operator.slug)} className="text-ink-2 hover:text-ink hover:underline">
202 + {f.operator.name}
203 + </Link>
204 + ) : (
205 + <span className="text-ink-4">—</span>
206 + )}
207 + </Td>
208 + <Td label="Place" className="hidden text-ink-3 md:table-cell">
209 + {placeLine(f)}
210 + </Td>
211 + <Td label="Status">
212 + <StatusBadge status={f.status} short size="xs" />
213 + </Td>
214 + <Td label="Distance" numeric lead>
215 + {f.distanceKm < 1 ? `${Math.round(f.distanceKm * 1000)} m` : `${f.distanceKm.toFixed(1)} km`}
216 + </Td>
217 + </Tr>
218 + ))}
219 + </TBody>
220 + </Table>
221 + ) : (
222 + <HonestEmpty title={hasGeo ? "No other indexed facility nearby" : "Not computed"} detail={hasGeo ? "within the search radius" : "the exchange has no coordinates, so distances cannot be measured"} />
223 + )}
224 + </Section>
225 +
226 + <Section id="identifiers" title="Identifiers" description="External registry identifiers as observed." className="scroll-mt-28">
227 + {ids.length ? (
139 228 <KeyValueList
140 229 items={ids.map(([k, v]) => {
141 230 const href = idHref(k, v);
@@ -143,11 +232,17 @@ export default async function IxpPage({ params }: { params: Params }) {
143 232 })}
144 233 columns={2}
145 234 />
146 − </Section>
147 − )}
235 + ) : (
236 + <HonestEmpty title="No external identifier recorded" />
237 + )}
238 + </Section>
239 +
240 + <Section id="history" title="Source history" description="Observations that shaped this record." className="scroll-mt-28">
241 + {history.length ? <SourceHistory items={history.slice(0, 40)} /> : <HonestEmpty title="No source history yet" />}
242 + </Section>
148 243 </div>
149 244
150 − <SourcesFooter sources={r.sources} updatedAt={d.updatedAt} note="Exchange facts are shown as published by PeeringDB and the exchange operator; network counts are not estimated." />
245 + <SourcesFooter sources={r.sources} updatedAt={d.updatedAt} note="Exchange facts are shown as published by PeeringDB and the exchange operator; participant counts are never estimated and coordinates are metro reference points unless the exchange publishes its own." />
151 246 </Page>
152 247 );
153 248 }
modified apps/web/src/app/(site)/live/live-filters.tsx +119 −43
@@ -1,57 +1,55 @@
1 1 "use client";
2 2
3 3 import type { EventType } from "@dci/core";
4 −import { EVENT_TYPES } from "@dci/core";
4 +import { CONFIDENCE_LEVELS, EVENT_TYPES, SOURCE_KINDS } from "@dci/core";
5 5 import { SlidersHorizontal } from "lucide-react";
6 6 import { useRouter } from "next/navigation";
7 7 import { useState } from "react";
8 −import type { LiveQuery } from "@/components/live-feed";
8 +import { countLiveFilters, FAMILIES, liveHref, type LiveQuery2 } from "@/components/intelligence/live/query";
9 9 import { Button } from "@/components/ui/button";
10 10 import { ToggleChip } from "@/components/ui/filter-chips";
11 11 import { Sheet } from "@/components/ui/sheet";
12 12 import { fmtInt } from "@/lib/format";
13 −import { EVENT_LABEL } from "@/lib/labels";
14 −import { routes } from "@/lib/routes";
13 +import { CONFIDENCE_SHORT, ENTITY_TYPE_LABEL, EVENT_LABEL, SOURCE_KIND_LABEL } from "@/lib/labels";
15 14
16 −const SIG = [
17 − { v: 0, label: "All" },
18 − { v: 25, label: "≥ 25" },
19 − { v: 50, label: "≥ 50 · notable" },
20 − { v: 80, label: "≥ 80 · major" },
15 +const BANDS: Array<{ key: LiveQuery2["significance"] | "all" | "default"; label: string; hint: string }> = [
16 + { key: "default", label: "Notable", hint: "≥ 45 (minor hidden)" },
17 + { key: "major", label: "Major", hint: "≥ 75" },
18 + { key: "medium", label: "Medium", hint: "45–74" },
19 + { key: "minor", label: "Minor", hint: "< 45" },
20 + { key: "all", label: "All", hint: "every band" },
21 21 ];
22 22
23 −const QUICK: Array<{ label: string; types: EventType[] }> = [
24 − { label: "Openings", types: ["facility_opened", "cloud_region_launched"] },
25 − { label: "Construction", types: ["construction_started", "phase_announced"] },
26 − { label: "Announcements", types: ["project_announced", "expansion_announced", "cloud_region_announced", "investment_announced"] },
27 − { label: "Planning", types: ["planning_filed", "planning_approved"] },
28 − { label: "Capacity", types: ["capacity_changed", "planned_capacity_changed"] },
29 − { label: "Status", types: ["status_changed", "project_status_changed", "opening_date_changed"] },
30 − { label: "Ownership", types: ["operator_changed", "owner_changed", "acquisition"] },
31 − { label: "Closures & incidents", types: ["closure", "incident"] },
32 −];
23 +const inputCls = "h-10 w-full rounded-sm border border-hair-2 bg-surface px-2.5 text-[13px] outline-none focus:border-ink sm:h-9";
24 +
25 +function bandOf(q: LiveQuery2): (typeof BANDS)[number]["key"] {
26 + if (q.significance) return q.significance;
27 + if (q.all || q.min_significance) return "all";
28 + return "default";
29 +}
33 30
34 −export function LiveFilters({ query, total }: { query: LiveQuery; total: number | null }) {
31 +export function LiveFilters({ query, total }: { query: LiveQuery2; total: number | null }) {
35 32 const router = useRouter();
36 33 const [open, setOpen] = useState(false);
37 − const [draft, setDraft] = useState<LiveQuery>(query);
34 + const [draft, setDraft] = useState<LiveQuery2>(query);
38 35 const [synced, setSynced] = useState(query);
39 36 if (synced !== query) {
40 37 setSynced(query);
41 38 setDraft(query);
42 39 }
43 40 const types = new Set((draft.type ?? "").split(",").filter(Boolean));
44 − const active = [query.type, query.country, query.operator, query.min_significance].filter(Boolean).length;
41 + const active = countLiveFilters(query);
45 42
46 − const apply = (q: LiveQuery) => {
47 − const p = new URLSearchParams();
48 − if (q.type) p.set("type", q.type);
49 − if (q.country) p.set("country", q.country);
50 − if (q.operator) p.set("operator", q.operator);
51 − if (q.min_significance) p.set("min_significance", String(q.min_significance));
52 − router.replace(routes.live(p.toString()), { scroll: false });
43 + const apply = (q: LiveQuery2) => {
44 + router.replace(liveHref(q), { scroll: false });
53 45 setOpen(false);
54 46 };
47 + const setBand = (q: LiveQuery2, key: (typeof BANDS)[number]["key"]): LiveQuery2 => {
48 + const next: LiveQuery2 = { ...q, significance: undefined, min_significance: undefined, all: undefined };
49 + if (key === "all") next.all = true;
50 + else if (key !== "default") next.significance = key;
51 + return next;
52 + };
55 53 const toggleTypes = (list: EventType[]) => {
56 54 const all = list.every((t) => types.has(t));
57 55 const next = new Set(types);
@@ -61,29 +59,45 @@ export function LiveFilters({ query, total }: { query: LiveQuery; total: number
61 59 }
62 60 setDraft({ ...draft, type: next.size ? Array.from(next).join(",") : undefined });
63 61 };
62 + const toggleCsv = (key: "confidence" | "source_kind", member: string) => {
63 + const set = new Set((draft[key] ?? "").split(",").filter(Boolean));
64 + if (set.has(member)) set.delete(member);
65 + else set.add(member);
66 + setDraft({ ...draft, [key]: set.size ? Array.from(set).join(",") : undefined });
67 + };
68 + const band = bandOf(query);
69 + const dedupe = query.dedupe !== false;
64 70
65 71 return (
66 − <div className="flex items-center gap-2">
72 + <div className="flex flex-wrap items-center gap-2">
67 73 <Button size="sm" variant={active ? "primary" : "secondary"} onClick={() => setOpen(true)} aria-haspopup="dialog">
68 74 <SlidersHorizontal className="size-3.5" strokeWidth={1.75} /> Filters{active > 0 && <span className="figure ml-0.5 rounded-xs bg-paper/20 px-1 text-[11px]">{active}</span>}
69 75 </Button>
70 − <div className="-mx-1 hidden gap-1 overflow-x-auto px-1 no-scrollbar md:flex">
71 − {SIG.map((s) => (
72 − <ToggleChip key={s.v} active={(query.min_significance ?? 0) === s.v} onClick={() => apply({ ...query, min_significance: s.v || undefined })} className="h-7 text-[12px]">
73 − {s.label}
76 + <div className="hidden gap-1 md:flex" aria-label="Significance">
77 + {BANDS.filter((b) => b.key !== "all").map((b) => (
78 + <ToggleChip key={b.key} active={band === b.key} onClick={() => apply(setBand(query, b.key))} className="h-8 text-[12px]" dot={undefined}>
79 + {b.label} <span className="figure text-[10.5px] opacity-70">{b.hint}</span>
74 80 </ToggleChip>
75 81 ))}
76 82 </div>
83 + <ToggleChip active={Boolean(query.all)} onClick={() => apply(query.all ? { ...query, all: undefined } : setBand(query, "all"))} className="h-10 text-[12px] sm:h-8">
84 + Show all changes
85 + </ToggleChip>
86 + <ToggleChip active={dedupe} onClick={() => apply({ ...query, dedupe: dedupe ? false : undefined })} className="h-10 text-[12px] sm:h-8">
87 + Deduplicated
88 + </ToggleChip>
77 89 {total !== null && (
78 90 <span className="figure ml-auto text-[12.5px] text-ink-3" aria-live="polite">
79 91 {fmtInt(total)} {total === 1 ? "change" : "changes"}
80 92 </span>
81 93 )}
94 +
82 95 <Sheet
83 96 open={open}
84 97 onClose={() => setOpen(false)}
85 98 title="Filter the change feed"
86 − heightVh={82}
99 + description="Every filter is a URL parameter — share the address to share the view."
100 + heightVh={86}
87 101 footer={
88 102 <div className="flex items-center justify-between gap-2">
89 103 <Button variant="ghost" onClick={() => setDraft({})}>
@@ -99,41 +113,103 @@ export function LiveFilters({ query, total }: { query: LiveQuery; total: number
99 113 <fieldset className="py-3 first:pt-0">
100 114 <legend className="label-caps mb-2">Significance</legend>
101 115 <div className="flex flex-wrap gap-1.5">
102 − {SIG.map((s) => (
103 − <ToggleChip key={s.v} active={(draft.min_significance ?? 0) === s.v} onClick={() => setDraft({ ...draft, min_significance: s.v || undefined })}>
104 − {s.label}
116 + {BANDS.map((b) => (
117 + <ToggleChip key={b.key} active={bandOf(draft) === b.key} onClick={() => setDraft(setBand(draft, b.key))}>
118 + {b.label} <span className="figure text-[10.5px] opacity-70">{b.hint}</span>
105 119 </ToggleChip>
106 120 ))}
107 121 </div>
122 + <p className="mt-1.5 text-[11px] text-ink-4">Significance is a 0–100 score from the event type, the magnitude of the change and the source authority. “Notable” hides minor changes (page edits, small field updates).</p>
123 + </fieldset>
124 +
125 + <fieldset className="py-3">
126 + <legend className="label-caps mb-2">View</legend>
127 + <div className="flex flex-wrap gap-1.5">
128 + <ToggleChip active={draft.dedupe !== false} onClick={() => setDraft({ ...draft, dedupe: draft.dedupe === false ? undefined : false })}>
129 + Deduplicated view
130 + </ToggleChip>
131 + <ToggleChip active={Boolean(draft.ai)} onClick={() => setDraft({ ...draft, ai: draft.ai ? undefined : true })}>
132 + AI / HPC only
133 + </ToggleChip>
134 + </div>
135 + <p className="mt-1.5 text-[11px] text-ink-4">Deduplicated collapses documents describing the same announcement — rows show “also reported by N sources”. Switch it off to see every document.</p>
108 136 </fieldset>
137 +
109 138 <fieldset className="py-3">
110 139 <legend className="label-caps mb-2">Event family</legend>
111 140 <div className="flex flex-wrap gap-1.5">
112 − {QUICK.map((g) => (
113 − <ToggleChip key={g.label} active={g.types.every((t) => types.has(t))} onClick={() => toggleTypes(g.types)}>
141 + {FAMILIES.map((g) => (
142 + <ToggleChip key={g.key} active={g.types.every((t) => types.has(t))} onClick={() => toggleTypes(g.types)}>
114 143 {g.label}
115 144 </ToggleChip>
116 145 ))}
117 146 </div>
118 147 </fieldset>
148 +
119 149 <fieldset className="py-3">
120 150 <legend className="label-caps mb-2">Event type</legend>
121 151 <div className="flex flex-wrap gap-1.5">
122 152 {EVENT_TYPES.map((t) => (
123 − <ToggleChip key={t} active={types.has(t)} onClick={() => toggleTypes([t])} className="h-7 text-[12px]">
153 + <ToggleChip key={t} active={types.has(t)} onClick={() => toggleTypes([t])} className="h-9 text-[12px] sm:h-7">
124 154 {EVENT_LABEL[t]}
125 155 </ToggleChip>
126 156 ))}
127 157 </div>
128 158 </fieldset>
159 +
160 + <fieldset className="py-3">
161 + <legend className="label-caps mb-2">Source type</legend>
162 + <div className="flex flex-wrap gap-1.5">
163 + {SOURCE_KINDS.map((k) => (
164 + <ToggleChip key={k} active={(draft.source_kind ?? "").split(",").includes(k)} onClick={() => toggleCsv("source_kind", k)} className="h-9 text-[12px] sm:h-7">
165 + {SOURCE_KIND_LABEL[k]}
166 + </ToggleChip>
167 + ))}
168 + </div>
169 + </fieldset>
170 +
171 + <fieldset className="py-3">
172 + <legend className="label-caps mb-2">Confidence</legend>
173 + <div className="flex flex-wrap gap-1.5">
174 + {CONFIDENCE_LEVELS.map((c) => (
175 + <ToggleChip key={c} active={(draft.confidence ?? "").split(",").includes(c)} onClick={() => toggleCsv("confidence", c)} className="h-9 text-[12px] sm:h-7">
176 + {CONFIDENCE_SHORT[c]}
177 + </ToggleChip>
178 + ))}
179 + </div>
180 + </fieldset>
181 +
129 182 <fieldset className="grid gap-3 py-3 sm:grid-cols-2">
183 + <label className="block">
184 + <span className="label-caps mb-1 block">Entity type</span>
185 + <select value={draft.entity_type ?? ""} onChange={(e) => setDraft({ ...draft, entity_type: e.target.value || undefined })} className={inputCls}>
186 + <option value="">Any entity</option>
187 + {Object.entries(ENTITY_TYPE_LABEL).map(([k, v]) => (
188 + <option key={k} value={k}>
189 + {v}
190 + </option>
191 + ))}
192 + </select>
193 + </label>
194 + <label className="block">
195 + <span className="label-caps mb-1 block">Entity id</span>
196 + <input value={draft.entity_id ?? ""} onChange={(e) => setDraft({ ...draft, entity_id: e.target.value.trim() || undefined })} placeholder="fac_… / prj_…" className={`${inputCls} figure`} />
197 + </label>
130 198 <label className="block">
131 199 <span className="label-caps mb-1 block">Country (ISO2)</span>
132 − <input value={draft.country ?? ""} onChange={(e) => setDraft({ ...draft, country: e.target.value.toUpperCase().replace(/[^A-Z]/g, "").slice(0, 2) || undefined })} placeholder="US" className="figure h-9 w-full rounded-sm border border-hair-2 bg-surface px-2.5 text-[13px] outline-none focus:border-ink" />
200 + <input value={draft.country ?? ""} onChange={(e) => setDraft({ ...draft, country: e.target.value.toUpperCase().replace(/[^A-Z]/g, "").slice(0, 2) || undefined })} placeholder="US" className={`${inputCls} figure`} />
201 + </label>
202 + <label className="block">
203 + <span className="label-caps mb-1 block">Market slug</span>
204 + <input value={draft.metro ?? ""} onChange={(e) => setDraft({ ...draft, metro: e.target.value.trim() || undefined })} placeholder="northern-virginia" className={`${inputCls} figure`} />
133 205 </label>
134 206 <label className="block">
135 207 <span className="label-caps mb-1 block">Operator slug</span>
136 − <input value={draft.operator ?? ""} onChange={(e) => setDraft({ ...draft, operator: e.target.value || undefined })} placeholder="equinix" className="h-9 w-full rounded-sm border border-hair-2 bg-surface px-2.5 text-[13px] outline-none focus:border-ink" />
208 + <input value={draft.operator ?? ""} onChange={(e) => setDraft({ ...draft, operator: e.target.value.trim() || undefined })} placeholder="equinix" className={`${inputCls} figure`} />
209 + </label>
210 + <label className="block">
211 + <span className="label-caps mb-1 block">Project slug</span>
212 + <input value={draft.project ?? ""} onChange={(e) => setDraft({ ...draft, project: e.target.value.trim() || undefined })} placeholder="project slug or prj_…" className={`${inputCls} figure`} />
137 213 </label>
138 214 </fieldset>
139 215 </div>
modified apps/web/src/app/(site)/live/page.tsx +27 −35
@@ -1,14 +1,15 @@
1 1 import type { EventType } from "@dci/core";
2 −import { EVENT_TYPES } from "@dci/core";
3 2 import type { Metadata } from "next";
4 −import { LiveFeed } from "@/components/live-feed";
3 +import { LiveFeed2 } from "@/components/intelligence/live/live-feed-2";
4 +import { liveApiQuery, liveHref, MINOR_THRESHOLD, parseLiveQuery } from "@/components/intelligence/live/query";
5 +import { ApiNote } from "@/components/intelligence/shared";
5 6 import { Page, PageHeader } from "@/components/ui/card";
6 7 import { FilterChips, type Chip } from "@/components/ui/filter-chips";
7 8 import { Metric } from "@/components/ui/metric";
8 9 import { OfflineNotice } from "@/components/ui/states";
9 10 import { api } from "@/lib/api";
10 −import { fmtInt } from "@/lib/format";
11 −import { EVENT_LABEL } from "@/lib/labels";
11 +import { countryName, fmtInt } from "@/lib/format";
12 +import { CONFIDENCE_SHORT, ENTITY_TYPE_LABEL, EVENT_LABEL, SIGNIFICANCE_LABEL, SOURCE_KIND_LABEL } from "@/lib/labels";
12 13 import { routes } from "@/lib/routes";
13 14 import { LiveFilters } from "./live-filters";
14 15
@@ -16,45 +17,35 @@ export const revalidate = 30;
16 17
17 18 export const metadata: Metadata = {
18 19 title: "Live changes",
19 − description: "Every detected change in the global data center index — new facilities, capacity and status changes, project announcements — with source and confidence.",
20 + description: "Every detected change in the global data center index — new facilities, capacity and status changes, project announcements, power and corporate events — deduplicated per announcement, with source, significance and confidence.",
20 21 alternates: { canonical: "/live" },
21 22 };
22 23
23 24 const PER_PAGE = 50;
24 −const TYPE_SET = new Set<string>(EVENT_TYPES);
25 25
26 26 export default async function LivePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
27 27 const sp = await searchParams;
28 − const one = (k: string) => (typeof sp[k] === "string" ? (sp[k] as string) : undefined);
29 − const type = one("type")
30 − ?.split(",")
31 − .filter((t) => TYPE_SET.has(t))
32 − .join(",");
33 − const country = one("country")?.toUpperCase();
34 − const operator = one("operator");
35 − const minSig = Number(one("min_significance"));
36 − const query = { type: type || undefined, country: country || undefined, operator: operator || undefined, min_significance: Number.isFinite(minSig) && minSig > 0 ? minSig : undefined };
37 −
38 − const [res, dash] = await Promise.all([api.events({ ...query, per_page: PER_PAGE }), api.dashboard()]);
28 + const query = parseLiveQuery(sp);
29 + const apiQuery = liveApiQuery(query);
30 + const [res, dash] = await Promise.all([api.events({ ...apiQuery, per_page: PER_PAGE }), api.dashboard()]);
39 31 const stats = dash.ok ? dash.data.stats : null;
32 + const hidingMinor = !query.all && !query.significance && !query.min_significance;
40 33
41 34 const chips: Chip[] = [];
42 − const qsWithout = (drop: string, member?: string) => {
43 − const p = new URLSearchParams();
44 − if (query.type && drop !== "type") p.set("type", query.type);
45 − if (query.type && drop === "type" && member) {
46 − const rest = query.type.split(",").filter((t) => t !== member);
47 − if (rest.length) p.set("type", rest.join(","));
48 − }
49 − if (query.country && drop !== "country") p.set("country", query.country);
50 − if (query.operator && drop !== "operator") p.set("operator", query.operator);
51 − if (query.min_significance && drop !== "min_significance") p.set("min_significance", String(query.min_significance));
52 − return routes.live(p.toString());
53 − };
54 − for (const t of query.type?.split(",") ?? []) chips.push({ key: `t:${t}`, label: "Type", value: EVENT_LABEL[t as EventType] ?? t, removeHref: qsWithout("type", t) });
55 − if (query.country) chips.push({ key: "c", label: "Country", value: query.country, removeHref: qsWithout("country") });
56 − if (query.operator) chips.push({ key: "o", label: "Operator", value: query.operator, removeHref: qsWithout("operator") });
57 − if (query.min_significance) chips.push({ key: "s", label: "Significance", value: `≥ ${query.min_significance}`, removeHref: qsWithout("min_significance") });
35 + for (const t of query.type?.split(",") ?? []) chips.push({ key: `t:${t}`, label: "Type", value: EVENT_LABEL[t as EventType] ?? t, removeHref: liveHref({ ...query, type: query.type!.split(",").filter((x) => x !== t).join(",") || undefined }) });
36 + if (query.entity_type) chips.push({ key: "et", label: "Entity", value: ENTITY_TYPE_LABEL[query.entity_type] ?? query.entity_type, removeHref: liveHref({ ...query, entity_type: undefined }) });
37 + if (query.entity_id) chips.push({ key: "eid", label: "Entity id", value: query.entity_id, removeHref: liveHref({ ...query, entity_id: undefined }) });
38 + if (query.country) chips.push({ key: "c", label: "Country", value: countryName(query.country), removeHref: liveHref({ ...query, country: undefined }) });
39 + if (query.metro) chips.push({ key: "m", label: "Market", value: query.metro, removeHref: liveHref({ ...query, metro: undefined }) });
40 + if (query.operator) chips.push({ key: "o", label: "Operator", value: query.operator, removeHref: liveHref({ ...query, operator: undefined }) });
41 + if (query.project) chips.push({ key: "p", label: "Project", value: query.project, removeHref: liveHref({ ...query, project: undefined }) });
42 + for (const c of query.confidence?.split(",") ?? []) chips.push({ key: `cf:${c}`, label: "Confidence", value: CONFIDENCE_SHORT[c as keyof typeof CONFIDENCE_SHORT] ?? c, removeHref: liveHref({ ...query, confidence: query.confidence!.split(",").filter((x) => x !== c).join(",") || undefined }) });
43 + if (query.significance) chips.push({ key: "s", label: "Significance", value: SIGNIFICANCE_LABEL[query.significance], removeHref: liveHref({ ...query, significance: undefined }) });
44 + if (query.min_significance) chips.push({ key: "ms", label: "Significance", value: `≥ ${query.min_significance}`, removeHref: liveHref({ ...query, min_significance: undefined }) });
45 + for (const k of query.source_kind?.split(",") ?? []) chips.push({ key: `sk:${k}`, label: "Source", value: SOURCE_KIND_LABEL[k as keyof typeof SOURCE_KIND_LABEL] ?? k, removeHref: liveHref({ ...query, source_kind: query.source_kind!.split(",").filter((x) => x !== k).join(",") || undefined }) });
46 + if (query.ai) chips.push({ key: "ai", label: "AI / HPC", value: "only", removeHref: liveHref({ ...query, ai: undefined }) });
47 + if (query.dedupe === false) chips.push({ key: "dd", label: "View", value: "every document", removeHref: liveHref({ ...query, dedupe: undefined }) });
48 + if (query.all) chips.push({ key: "all", label: "Significance", value: "all changes", removeHref: liveHref({ ...query, all: undefined }) });
58 49
59 50 return (
60 51 <Page>
@@ -65,7 +56,7 @@ export default async function LivePage({ searchParams }: { searchParams: Promise
65 56 </span>
66 57 }
67 58 title="Change feed"
68 − description="Every change detected by the connectors, deduplicated per entity and day, with its source and confidence. Refreshes every minute."
59 + description={`Every change detected by the connectors, with its source, significance and confidence. ${hidingMinor ? `Minor changes (significance below ${MINOR_THRESHOLD}) are hidden by default.` : "Showing every significance band."} ${query.dedupe === false ? "Every document is listed." : "Documents describing the same announcement are collapsed into one row."} Refreshes every minute.`}
69 60 aside={
70 61 <div className="flex gap-6">
71 62 <Metric size="sm" value={stats ? fmtInt(stats.events24h) : "—"} label="Last 24 h" />
@@ -79,8 +70,9 @@ export default async function LivePage({ searchParams }: { searchParams: Promise
79 70 <FilterChips chips={chips} resetHref={routes.live()} />
80 71 </div>
81 72 <div className="pt-2">
82 − <LiveFeed initial={res.items} initialTotal={res.total} perPage={PER_PAGE} query={query} initialError={res.ok ? undefined : res.error} />
73 + <LiveFeed2 initial={res.items} initialTotal={res.total} perPage={PER_PAGE} apiQuery={apiQuery} initialError={res.ok ? undefined : res.error} />
83 74 </div>
75 + <ApiNote label="How to read" className="mt-4" note={`Significance is a 0–100 score; “major” ≥ 75, “medium” 45–74, “minor” < ${MINOR_THRESHOLD}. The deduplicated view groups documents that describe the same announcement (same entity, type, value and day) and shows the other sources on each row. Every row links to the page it was observed on.`} />
84 76 </Page>
85 77 );
86 78 }
added apps/web/src/app/(site)/methodology/claims-sections.tsx +341 −0
@@ -0,0 +1,341 @@
1 +import type { AuthorityField, ClaimScope, SourceKind } from "@dci/core";
2 +import { AI_EVIDENCE_LEVELS, AUTHORITY_FIELDS, CAPACITY_PREDICATE_LABEL, CLAIM_SCOPES, FIELD_AUTHORITY, INVESTMENT_PREDICATE_LABEL, PROJECT_CLASSES, SOURCE_KINDS, isSiteScope } from "@dci/core";
3 +import Link from "next/link";
4 +import type { ReactNode } from "react";
5 +import { AiMark, ScopeBadge, StageBadge, TierMark } from "@/components/ui/badge";
6 +import { Section } from "@/components/ui/card";
7 +import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
8 +import { AI_EVIDENCE_LABEL, AUTHORITY_TIER_LABEL, PROJECT_STAGE_ORDER, SOURCE_KIND_LABEL } from "@/lib/labels";
9 +import { routes } from "@/lib/routes";
10 +
11 +/* ------------------------------------------------------------------------------------------
12 + Methodology §9–17: the claim-first architecture (docs/CLAIMS.md), rendered from the same code
13 + tables the pipeline uses (@dci/core claims.ts) so the page can never drift from the rules.
14 + ------------------------------------------------------------------------------------------ */
15 +
16 +const linkCls = "text-ink underline decoration-hair-2 hover:text-accent";
17 +function P({ children }: { children: ReactNode }) {
18 + return <p className="mt-3 first:mt-0">{children}</p>;
19 +}
20 +function Code({ children }: { children: ReactNode }) {
21 + return <code className="figure text-[12.5px]">{children}</code>;
22 +}
23 +
24 +const CLAIM_COLUMNS: Array<[string, string]> = [
25 + ["subject_type, subject_id", "facility / campus / project / operator / market / country the figure is about"],
26 + ["predicate", "what is asserted — it_capacity_mw, critical_power_mw, utility_capacity_mw, grid_connection_mw, current_power_mw, planned_power_mw, ultimate_campus_mw, phase_mw, project_investment_usd, campus_investment_usd, company_investment_usd, country_program_usd, multi_year_capex_usd, deal_value_usd, status…"],
27 + ["value, value_text, unit", "the figure (MW, USD) or text"],
28 + ["scope, scope_reason", "building · facility · campus · metro · country · portfolio · company · unknown, and the rule that decided"],
29 + ["evidence_text, evidence_start, evidence_end", "the supporting sentence and its offsets in the parsed text — no sentence, no numerical claim (structured spec tables record the extraction method instead)"],
30 + ["source_id, url, document_id, published_at, retrieved_at", "where and when"],
31 + ["authority_tier", "A–E for this field (see field-level authority)"],
32 + ["parser_name, parser_version, run_id", "who extracted it — enables reprocessing and rollback"],
33 + ["status", "current · superseded (same source, same page, new value) · rejected (review / rollback) · review (critical sanity flag) · unscoped (describes a portfolio, a company, a country, a metro, or its scope is unknown)"],
34 +];
35 +
36 +const SCOPE_MEANING: Record<ClaimScope, string> = {
37 + building: "One hall or building on a site — may populate a building record; never stands for its campus.",
38 + facility: "A data center as one record — populates the facility's own columns.",
39 + campus: "The whole campus / park — populates the campus row; never written to a building.",
40 + metro: "A market statistic (“the Northern Virginia market adds 1 GW”) — evidence only.",
41 + country: "A national programme or country total — evidence only.",
42 + portfolio: "A company's footprint or pipeline (“across its portfolio”) — evidence only.",
43 + company: "Capex, guidance, multi-year plans, market-research figures — evidence only.",
44 + unknown: "The sentence gives no scope signal and the record has none — stored, never assigned.",
45 +};
46 +
47 +const BLOCKING: Array<[string, string]> = [
48 + ["scope_*", "figure describes a portfolio, company, country, metro or unknown scope — kept as a claim, not assigned"],
49 + ["scope_campus_on_building", "campus-level figure on a building record"],
50 + ["mw_market_statistic", "≥ 20 GW — an industry / market statistic, not a site"],
51 + ["mw_invalid", "zero, negative or non-numeric MW"],
52 + ["inv_deal_value_usd · inv_multi_year_capex_usd · inv_company_investment_usd · inv_country_program_usd", "deal value, capex, company or country programme filed as a project investment"],
53 + ["inv_gt_500b", "more than $500 B on one record"],
54 +];
55 +const NON_BLOCKING: Array<[string, string, string]> = [
56 + ["mw_single_site_gt_1000", "critical", "single facility above 1 000 MW without a campus designation"],
57 + ["mw_building_gt_500", "warn", "one building above 500 MW"],
58 + ["mw_change_5x", "warn", "figure changed more than 5× against the previous value"],
59 + ["mw_money_collision", "critical", "a money figure with the same number sits in the same sentence — unit to verify"],
60 + ["inv_single_site_gt_50b", "critical", "more than $50 B on one site"],
61 + ["mw_semantics_default", "info", "the sentence does not say what kind of MW this is; semantics defaulted from the record status"],
62 + ["mw_utility_not_it", "info", "utility / grid figure — supply side, never IT load"],
63 +];
64 +
65 +const CLASS_GROUPS: Array<{ title: string; note: string; classes: string[] }> = [
66 + { title: "Physical — may create or modify a project", note: "Creation additionally needs the evidence threshold: an explicit name or operator, a location, and a development verb. Company domiciles (“Denver-based”, “headquartered in”) are stripped before locating the project.", classes: ["NEW_BUILD", "EXPANSION", "CONSTRUCTION_START", "PERMIT", "LAND_ACQUISITION", "GRID_CONNECTION"] },
67 + { title: "Associated — attach an event to an identifiable project, never create one", note: "Power agreements, financing and partnerships are corporate context; on operator pages they are listed apart from physical projects.", classes: ["POWER_AGREEMENT", "FINANCING", "ACQUISITION", "PARTNERSHIP", "CUSTOMER_AGREEMENT"] },
68 + { title: "Never a project", note: "Executive appointments, sustainability reports, product news and general company news are recorded as events at most.", classes: PROJECT_CLASSES.filter((c) => !["NEW_BUILD", "EXPANSION", "CONSTRUCTION_START", "PERMIT", "LAND_ACQUISITION", "GRID_CONNECTION", "POWER_AGREEMENT", "FINANCING", "ACQUISITION", "PARTNERSHIP", "CUSTOMER_AGREEMENT"].includes(c)) },
69 +];
70 +
71 +export const CLAIMS_NAV = [
72 + { id: "claims", label: "Claims" },
73 + { id: "scopes", label: "Scopes" },
74 + { id: "semantics", label: "Capacity semantics" },
75 + { id: "authority", label: "Authority" },
76 + { id: "quality", label: "Sanity & quality" },
77 + { id: "classes", label: "Announcement classes" },
78 + { id: "lifecycle", label: "Lifecycle" },
79 + { id: "ai-evidence", label: "AI evidence" },
80 + { id: "coverage-ranking", label: "Coverage-aware ranking" },
81 +];
82 +
83 +export function ClaimsSections({ from }: { from: number }) {
84 + const n = (i: number) => `${from + i} · `;
85 + return (
86 + <>
87 + <Section id="claims" className="scroll-mt-28" title={`${n(0)}Claims — the claim-first architecture`}>
88 + <P>
89 + Since 2026-09-12 every numerical figure the index extracts is stored as a <strong className="text-ink">claim</strong> before it can become a displayed value. A claim is one assertion by one document about one subject, with its scope, its semantics, the sentence that supports it, the parser that produced it and the field-level authority of its source. Reconciliation then decides which claim (if any) populates a column. <strong className="text-ink">Nothing is deleted when sources disagree</strong> — every claim stays visible in the evidence drawer.
90 + </P>
91 + <div className="mt-4">
92 + <Table dense>
93 + <THead>
94 + <tr>
95 + <Th>Column</Th>
96 + <Th>Meaning</Th>
97 + </tr>
98 + </THead>
99 + <TBody>
100 + {CLAIM_COLUMNS.map(([c, m]) => (
101 + <Tr key={c}>
102 + <Td primary className="align-top">
103 + <Code>{c}</Code>
104 + </Td>
105 + <Td label="Meaning" className="text-[12.5px] text-ink-2">
106 + {m}
107 + </Td>
108 + </Tr>
109 + ))}
110 + </TBody>
111 + </Table>
112 + </div>
113 + <P>
114 + The winner behind each displayed value is marked (<Code>is_winner</Code>); <Code>run_id</Code> on claims, provenance rows, events and document versions ties every change to a connector run, which can be rolled back as a whole. Daily snapshots keep global totals, per-operator and per-country totals, rankings and stage counts for “as of” views and automated regression checks (facilities −5 %, known MW +20 %, one operator +10 GW in a day, one connector &gt; 500 records a day, &gt; 200 location changes a day).
115 + </P>
116 + </Section>
117 +
118 + <Section id="scopes" className="scroll-mt-28" title={`${n(1)}Scopes`}>
119 + <P>
120 + Only <strong className="text-ink">site scopes</strong> (building, facility, campus) may populate a facility&rsquo;s or project&rsquo;s capacity and investment columns. Portfolio, company, country, metro and unknown scopes are stored and shown as evidence, never summed. Scope is classified deterministically from the sentence (<Code>classifyScope</Code>): company → portfolio → country → metro → campus → building → facility → the record&rsquo;s own scope when the sentence has no signal (structured parsers) → unknown. A campus figure is never written to a building record; a building&rsquo;s figure never stands for its campus.
121 + </P>
122 + <div className="mt-4">
123 + <Table dense>
124 + <THead>
125 + <tr>
126 + <Th>Scope</Th>
127 + <Th className="hidden sm:table-cell">Populates a site&rsquo;s figures?</Th>
128 + <Th>Meaning</Th>
129 + </tr>
130 + </THead>
131 + <TBody>
132 + {CLAIM_SCOPES.map((s) => (
133 + <Tr key={s}>
134 + <Td primary>
135 + <ScopeBadge scope={s} short={false} size="sm" />
136 + </Td>
137 + <Td label="Populates" className="hidden text-[12.5px] sm:table-cell">
138 + {isSiteScope(s) ? <span className="text-ok">yes — site scope</span> : <span className="text-ink-3">no — evidence only</span>}
139 + </Td>
140 + <Td label="Meaning" className="text-[12.5px] text-ink-2">
141 + {SCOPE_MEANING[s]}
142 + <span className="sm:hidden"> {isSiteScope(s) ? "(site scope)" : "(evidence only)"}</span>
143 + </Td>
144 + </Tr>
145 + ))}
146 + </TBody>
147 + </Table>
148 + </div>
149 + </Section>
150 +
151 + <Section id="semantics" className="scroll-mt-28" title={`${n(2)}Capacity semantics`}>
152 + <P>
153 + IT load, critical power, utility capacity, grid connection, current power, planned capacity, ultimate build-out and phase capacity are <strong className="text-ink">different predicates and different columns</strong>. The displayed figure carries <Code>capacityScope</Code> and <Code>capacitySemantics</Code> so a page can say what “300 MW” means. Utility and grid figures are supply-side — never IT load and never comparable with IT capacity. When a sentence gives no cue the predicate defaults from the field / record status and the claim is flagged <Code>mw_semantics_default</Code>.
154 + </P>
155 + <div className="mt-4 grid gap-4 md:grid-cols-2">
156 + <Table dense>
157 + <THead>
158 + <tr>
159 + <Th>Capacity predicate</Th>
160 + <Th>Label</Th>
161 + </tr>
162 + </THead>
163 + <TBody>
164 + {Object.entries(CAPACITY_PREDICATE_LABEL).map(([k, v]) => (
165 + <Tr key={k}>
166 + <Td primary>
167 + <Code>{k}</Code>
168 + </Td>
169 + <Td label="Label" className="text-[12.5px] text-ink-2">
170 + {v}
171 + </Td>
172 + </Tr>
173 + ))}
174 + </TBody>
175 + </Table>
176 + <Table dense>
177 + <THead>
178 + <tr>
179 + <Th>Investment predicate</Th>
180 + <Th>Label</Th>
181 + </tr>
182 + </THead>
183 + <TBody>
184 + {Object.entries(INVESTMENT_PREDICATE_LABEL).map(([k, v]) => (
185 + <Tr key={k}>
186 + <Td primary>
187 + <Code>{k}</Code>
188 + </Td>
189 + <Td label="Label" className="text-[12.5px] text-ink-2">
190 + {v}
191 + </Td>
192 + </Tr>
193 + ))}
194 + </TBody>
195 + </Table>
196 + </div>
197 + <P>Only project and campus investment may stand for a site; deal values, capex, company investment and country programmes are stored as evidence.</P>
198 + </Section>
199 +
200 + <Section id="authority" className="scroll-mt-28" title={`${n(3)}Field-level authority`}>
201 + <P>
202 + One universal source rank is wrong: OpenStreetMap is excellent for geometry and useless for capacity; PeeringDB is the reference for interconnection; planning filings beat press releases for approved capacity; utility filings beat everything for grid requirements. <Code>FIELD_AUTHORITY</Code> therefore gives a tier per field. Estimates and LLM-only extractions drop one tier; a human review raises one. The table below is rendered from the code the pipeline runs.
203 + </P>
204 + <div className="mt-4 -mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">
205 + <table className="dci-table dense min-w-[720px] text-[12px]">
206 + <thead className="text-left">
207 + <tr>
208 + <Th>Field</Th>
209 + {SOURCE_KINDS.map((k) => (
210 + <Th key={k} className="text-center">
211 + {SOURCE_KIND_LABEL[k as SourceKind]}
212 + </Th>
213 + ))}
214 + </tr>
215 + </thead>
216 + <TBody>
217 + {AUTHORITY_FIELDS.map((f) => (
218 + <tr key={f} className="align-middle">
219 + <td className="px-2 py-1.5 pl-0 font-medium capitalize text-ink">{f as AuthorityField}</td>
220 + {SOURCE_KINDS.map((k) => (
221 + <td key={k} className="px-2 py-1.5 text-center">
222 + <TierMark tier={FIELD_AUTHORITY[f][k as SourceKind]} />
223 + </td>
224 + ))}
225 + </tr>
226 + ))}
227 + </TBody>
228 + </table>
229 + </div>
230 + <ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-[11.5px] text-ink-3">
231 + {(Object.keys(AUTHORITY_TIER_LABEL) as Array<keyof typeof AUTHORITY_TIER_LABEL>).map((t) => (
232 + <li key={t} className="inline-flex items-center gap-1.5">
233 + <TierMark tier={t} /> {AUTHORITY_TIER_LABEL[t].replace(/^Tier [A-E] — /, "")}
234 + </li>
235 + ))}
236 + </ul>
237 + </Section>
238 +
239 + <Section id="quality" className="scroll-mt-28" title={`${n(4)}Sanity engines & quality flags`}>
240 + <P>
241 + <Code>capacitySanity</Code> and <Code>investmentSanity</Code> run on every claim. <strong className="text-ink">Blocking</strong> flags keep the figure out of the columns (it stays a claim). <strong className="text-ink">Non-blocking</strong> critical flags assign the figure but open a quality flag prioritised by impact; the figure stays visible and marked until a reviewer resolves or dismisses the flag. Every entity page lists its open flags in the data-quality block.
242 + </P>
243 + <div className="mt-4 grid gap-4 md:grid-cols-2">
244 + <div>
245 + <h3 className="text-[13.5px] font-semibold text-ink">Blocking</h3>
246 + <ul className="mt-1.5 divide-y divide-hair text-[12.5px]">
247 + {BLOCKING.map(([c, m]) => (
248 + <li key={c} className="py-1.5">
249 + <Code>{c}</Code>
250 + <div className="text-ink-2">{m}</div>
251 + </li>
252 + ))}
253 + </ul>
254 + </div>
255 + <div>
256 + <h3 className="text-[13.5px] font-semibold text-ink">Non-blocking (flagged)</h3>
257 + <ul className="mt-1.5 divide-y divide-hair text-[12.5px]">
258 + {NON_BLOCKING.map(([c, sev, m]) => (
259 + <li key={c} className="py-1.5">
260 + <Code>{c}</Code> <span className={sev === "critical" ? "text-danger" : sev === "warn" ? "text-warn" : "text-ink-3"}>· {sev}</span>
261 + <div className="text-ink-2">{m}</div>
262 + </li>
263 + ))}
264 + </ul>
265 + </div>
266 + </div>
267 + <P>Review priority weighs the figure&rsquo;s magnitude, how many pages display it and the number of sources that disagree; the admin console works the queue from the top.</P>
268 + </Section>
269 +
270 + <Section id="classes" className="scroll-mt-28" title={`${n(5)}Announcement classes (projects)`}>
271 + <P>
272 + Before a news article can create a project, <Code>classifyProjectEvent</Code> labels it. Only physical classes may create or modify a project; associated classes attach an event to an identifiable project; the rest never become projects. This is what keeps executive appointments, market-research articles and capex guidance out of the pipeline.
273 + </P>
274 + <div className="mt-4 flex flex-col gap-4">
275 + {CLASS_GROUPS.map((g) => (
276 + <div key={g.title}>
277 + <h3 className="text-[13.5px] font-semibold text-ink">{g.title}</h3>
278 + <div className="mt-1.5 flex flex-wrap gap-1.5">
279 + {g.classes.map((c) => (
280 + <span key={c} className="figure inline-flex h-[22px] items-center rounded-xs border border-hair-2 px-2 text-[11px] text-ink-2">
281 + {c}
282 + </span>
283 + ))}
284 + </div>
285 + <p className="mt-1.5 text-[12.5px] text-ink-3">{g.note}</p>
286 + </div>
287 + ))}
288 + </div>
289 + </Section>
290 +
291 + <Section id="lifecycle" className="scroll-mt-28" title={`${n(6)}Lifecycle state machine`}>
292 + <P>
293 + <Code>projectTransition(from, to)</Code> accepts forward moves along the funnel; <em>delayed</em> and <em>cancelled</em> are side branches; leaving <em>delayed</em> resumes where the project was. A backward move needs a source that outranks the stored one for the status field, otherwise it is flagged <Code>status_backward</Code>. Stage dates (<Code>permit_filed_on</Code>, <Code>approved_on</Code>, <Code>construction_started_on</Code>, <Code>opened_on</Code>) are set from the announcement that moved the stage — earliest evidence wins.
294 + </P>
295 + <ol className="mt-4 flex flex-wrap items-center gap-1.5" aria-label="Lifecycle stages in order">
296 + {PROJECT_STAGE_ORDER.filter((s) => s !== "delayed" && s !== "cancelled").map((s, i, arr) => (
297 + <li key={s} className="inline-flex items-center gap-1.5">
298 + <StageBadge stage={s} size="sm" />
299 + {i < arr.length - 1 && <span aria-hidden className="text-ink-4">→</span>}
300 + </li>
301 + ))}
302 + </ol>
303 + <div className="mt-2 flex flex-wrap items-center gap-1.5 text-[12.5px] text-ink-3">
304 + side branches: <StageBadge stage="delayed" size="sm" /> <StageBadge stage="cancelled" size="sm" />
305 + </div>
306 + </Section>
307 +
308 + <Section id="ai-evidence" className="scroll-mt-28" title={`${n(7)}AI evidence levels`}>
309 + <P>A single keyword never confirms an AI site. Each facility and project carries an evidence level; AI counts and MW on the site cover confirmed + likely only, and figures are published, site-scoped values (containment-aware), never extrapolated.</P>
310 + <ul className="mt-3 divide-y divide-hair text-[12.5px]">
311 + {AI_EVIDENCE_LEVELS.map((lvl) => (
312 + <li key={lvl} className="flex flex-wrap items-start gap-x-3 gap-y-1 py-2">
313 + <span className="w-[150px] shrink-0">
314 + <AiMark evidence={lvl} size="sm" />
315 + </span>
316 + <span className="min-w-0 flex-1 text-ink-2">
317 + <strong className="text-ink">{AI_EVIDENCE_LABEL[lvl]}</strong>
318 + {lvl === "confirmed" && " — a source explicitly describes the site as AI / HPC / GPU / accelerated-computing infrastructure."}
319 + {lvl === "likely" && " — AI-ready, high-density, liquid- or direct-to-chip cooling or GPU signals in the source."}
320 + {lvl === "associated" && " — only an AI tenant, customer or company mention; reported separately, not counted as AI infrastructure."}
321 + {lvl === "unknown" && " — no signal in any source."}
322 + </span>
323 + </li>
324 + ))}
325 + </ul>
326 + <P>
327 + See the <Link href={routes.ai()} className={linkCls}>AI infrastructure index</Link>.
328 + </P>
329 + </Section>
330 +
331 + <Section id="coverage-ranking" className="scroll-mt-28" title={`${n(8)}Coverage-aware ranking`}>
332 + <P>
333 + Every aggregate uses the containment-aware facility view: a campus whose buildings publish figures contributes no MW itself; a building without a figure under a campus with one is “covered”; a campus with buildings is not counted as an extra facility. Each MW aggregate exposes its <strong className="text-ink">coverage</strong> (facilities with any figure ÷ facilities) and rankings gate on it (≥ 5 facilities, ≥ 35 %). Market concentration (HHI, top-3 / top-5 shares) is computed on facility counts and, separately, on known MW with its coverage — the MW view is labelled partial when coverage is low. Momentum is shown as separate counters (projects announced, MW entering construction, new entrants, openings, cloud additions, grid events), never collapsed into a score.
334 + </P>
335 + <P>
336 + Browse the <Link href={routes.rankings()} className={linkCls}>rankings</Link> and the <Link href={routes.coverage()} className={linkCls}>coverage report</Link>.
337 + </P>
338 + </Section>
339 + </>
340 + );
341 +}
modified apps/web/src/app/(site)/methodology/page.tsx +7 −3
@@ -9,10 +9,11 @@ import { Page, PageHeader, Section } from "@/components/ui/card";
9 9 import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
10 10 import { routes } from "@/lib/routes";
11 11 import { pageMetadata } from "@/lib/seo";
12 +import { CLAIMS_NAV, ClaimsSections } from "./claims-sections";
12 13
13 14 export const metadata: Metadata = pageMetadata({
14 15 title: "Methodology",
15 − description: "How DataCenterIndex builds its graph: pipeline, provenance, confidence levels, location precision, capacity and estimate rules, reconciliation, change detection, rankings and the legal crawling policy.",
16 + description: "How DataCenterIndex builds its graph: pipeline, provenance, confidence levels, location precision, capacity and estimate rules, reconciliation, change detection, rankings, the claim-first architecture (scopes, capacity semantics, field-level authority, sanity engines, announcement classes, lifecycle), AI evidence, coverage-aware ranking and the legal crawling policy.",
16 17 path: routes.methodology(),
17 18 });
18 19
@@ -25,6 +26,7 @@ const NAV = [
25 26 { id: "reconciliation", label: "Reconciliation" },
26 27 { id: "changes", label: "Changes" },
27 28 { id: "rankings", label: "Rankings" },
29 + ...CLAIMS_NAV,
28 30 { id: "crawling", label: "Crawling" },
29 31 { id: "limitations", label: "Limitations" },
30 32 ];
@@ -311,7 +313,9 @@ export default function MethodologyPage() {
311 313 </P>
312 314 </Section>
313 315
314 − <Section id="crawling" className="scroll-mt-28" title="9 · Legal crawling policy">
316 + <ClaimsSections from={9} />
317 +
318 + <Section id="crawling" className="scroll-mt-28" title="18 · Legal crawling policy">
315 319 <ul className="list-disc space-y-1.5 pl-5">
316 320 <li><strong className="text-ink">robots.txt is respected</strong> for every host, including <code className="figure text-[12.5px]">Crawl-delay</code>; rules are re-read at least daily. The few exceptions are public query APIs governed by their own published limits (for example the Wikidata SPARQL endpoint), and they are documented on the <Link href={routes.sources()} className={linkCls}>source page</Link>.</li>
317 321 <li><strong className="text-ink">Rate limits per host</strong> — by default 30 requests per minute with at most 2 concurrent connections, lower where the source asks for it.</li>
@@ -325,7 +329,7 @@ export default function MethodologyPage() {
325 329 </P>
326 330 </Section>
327 331
328 − <Section id="limitations" className="scroll-mt-28" title="10 · Limitations & corrections">
332 + <Section id="limitations" className="scroll-mt-28" title="19 · Limitations & corrections">
329 333 <P>Coverage is uneven: it follows what operators, regulators and registries publish, which is far denser in North America and Europe than in other regions, and denser for colocation and hyperscale sites than for enterprise or government facilities. Capacity figures are lower bounds; the absence of a figure means it was not published, not that it is zero. Cloud-region coordinates are city-level by construction. Rankings are recomputed as coverage grows and their history is kept.</P>
330 334 <P>
331 335 Corrections, additional sources and removal requests: <a href="mailto:contact@spboucher.ai" className={linkCls}>contact@spboucher.ai</a>. Every value on the site can be traced to a document version, so a correction is applied at the source and propagates through the same pipeline as any other observation.
modified apps/web/src/app/(site)/operators/[slug]/page.tsx +193 −91
@@ -1,3 +1,4 @@
1 +import type { EventDTO, EventType } from "@dci/core";
1 2 import type { Metadata } from "next";
2 3 import Link from "next/link";
3 4 import { notFound } from "next/navigation";
@@ -9,22 +10,28 @@ import { fPageHref, PagedFacilities, parseFPage } from "@/components/entity/page
9 10 import { SectionNav } from "@/components/entity/section-nav";
10 11 import { SourcesFooter } from "@/components/entity/sources-footer";
11 12 import { EventList } from "@/components/event-list";
13 +import { ClaimsTable, DataQualityPanel } from "@/components/evidence";
14 +import { FacilityTable } from "@/components/facility-table";
15 +import { ExpansionVelocityBlock } from "@/components/intelligence/operator/velocity";
16 +import { CoverageBar, HonestEmpty, MwFigure, PillLink, PipelineBars, PublishedOnlyNote, ShareList } from "@/components/intelligence/shared";
17 +import { WatchButton } from "@/components/intelligence/watch-button";
12 18 import { ProjectList } from "@/components/project-list";
13 −import { Badge, EstimateMark } from "@/components/ui/badge";
19 +import { Badge } from "@/components/ui/badge";
14 20 import { Page, Section } from "@/components/ui/card";
15 21 import { StackedBar } from "@/components/ui/charts";
16 −import { CountBarList } from "./bars";
17 22 import { CountryFlag } from "@/components/ui/country-flag";
18 23 import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
19 24 import { EmptyState, ErrorState, OfflineNotice } from "@/components/ui/states";
25 +import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
20 26 import { SourceHistory } from "@/components/ui/timeline";
21 27 import { api } from "@/lib/api";
22 28 import type { DetailFacilitiesMeta } from "@/lib/dto";
23 −import { countryName, fmtInt, fmtMw, pluralize, splitMw } from "@/lib/format";
29 +import { countryName, fmtDate, fmtInt, fmtMw, fmtPct, pluralize } from "@/lib/format";
24 30 import { OPERATOR_KIND_LABEL } from "@/lib/labels";
25 31 import { routes } from "@/lib/routes";
26 32 import { compactJsonLd, NOT_FOUND_METADATA, pageMetadata } from "@/lib/seo";
27 33 import type { SearchParams } from "@/lib/sort";
34 +import { CloudStatusBadge } from "../../cloud-regions/cloud-format";
28 35
29 36 export const revalidate = 300;
30 37
@@ -36,7 +43,7 @@ function describe(o: { name: string; kind: string | null; facilityCount: number;
36 43 if (o.knownMw) parts.push(`${fmtMw(o.knownMw)} known operational capacity`);
37 44 if (o.plannedMw) parts.push(`≈ ${fmtMw(o.plannedMw)} in the pipeline`);
38 45 if (o.hqCountryIso2) parts.push(`headquartered in ${countryName(o.hqCountryIso2)}`);
39 − return `${parts.join(", ")}. Sourced facility list, footprint map, projects and change history on DataCenterIndex.`;
46 + return `${parts.join(", ")}. Footprint map, pipeline, expansion velocity, geographic diversity, corporate events, claims and data quality on DataCenterIndex.`;
40 47 }
41 48
42 49 export async function generateMetadata({ params }: Props): Promise<Metadata> {
@@ -46,16 +53,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
46 53 return pageMetadata({ title: r.data.name, description: describe(r.data), path: routes.operator(slug) });
47 54 }
48 55
49 −function MwValue({ mw, estimate }: { mw: number | null | undefined; estimate?: boolean }) {
50 − const s = splitMw(mw);
51 − if (!s) return <span className="text-ink-4">—</span>;
52 − return (
53 − <>
54 − {estimate && <EstimateMark className="mr-0.5 text-[0.6em]" />}
55 − {s.value}
56 − </>
57 − );
58 −}
56 +/** Corporate events are served separately by the API; everything else on the operator is a physical change. */
57 +const CORPORATE_TYPES = new Set<EventType>(["acquisition", "investment_announced", "partnership", "executive_change", "customer_agreement", "owner_changed", "operator_changed", "power_agreement"]);
59 58
60 59 export default async function OperatorPage({ params, searchParams }: Props) {
61 60 const [{ slug }, sp] = await Promise.all([params, searchParams]);
@@ -76,14 +75,19 @@ export default async function OperatorPage({ params, searchParams }: Props) {
76 75 const facPer = meta.facilitiesPerPage ?? 50;
77 76 const base = routes.operator(slug);
78 77 const kindLabel = o.kind ? OPERATOR_KIND_LABEL[o.kind] : null;
79 − const knownUnit = splitMw(o.knownMw)?.unit ?? "MW";
80 − const plannedUnit = splitMw(o.plannedMw)?.unit ?? "MW";
81 78 const aliases = o.aliases.filter((a) => a && a !== o.name);
82 79 const sameAs = Object.values(o.externalIds ?? {})
83 80 .map(String)
84 81 .filter((v) => /^https?:\/\//.test(v));
85 − const countries = [...o.countries].sort((a, b) => b.facilityCount - a.facilityCount);
86 82 const statusTotal = Object.values(o.statusBreakdown).reduce((a, b) => a + (b ?? 0), 0);
83 + const corporateIds = new Set(o.corporateEvents.map((e) => e.id));
84 + // physical = every non-corporate event (generic types such as news / page_changed stay on this side rather than being dropped)
85 + const physical: EventDTO[] = o.recentEvents.filter((e) => !corporateIds.has(e.id) && !CORPORATE_TYPES.has(e.eventType));
86 + const corporate: EventDTO[] = o.corporateEvents.length ? o.corporateEvents : o.recentEvents.filter((e) => CORPORATE_TYPES.has(e.eventType));
87 + const constructionMw = o.constructionMw ?? o.pipeline.construction.mw;
88 + const plannedMw = o.plannedMw ?? (o.pipeline.approved.mw === null && o.pipeline.announced.mw === null ? null : (o.pipeline.approved.mw ?? 0) + (o.pipeline.announced.mw ?? 0));
89 + const coverage = o.mwCoverage ?? o.pipeline.mwCoverage;
90 + const cloud = [...o.cloudRegions].sort((a, b) => a.code.localeCompare(b.code));
87 91
88 92 const jsonLd = compactJsonLd({
89 93 "@context": "https://schema.org",
@@ -99,11 +103,17 @@ export default async function OperatorPage({ params, searchParams }: Props) {
99 103 });
100 104
101 105 const sections = [
102 − { id: "footprint", label: "Footprint", count: countries.length + statusTotal },
103 106 { id: "map", label: "Map", count: o.facilityCount },
104 − { id: "facilities", label: "Facilities", count: facTotal },
107 + { id: "pipeline", label: "Pipeline" },
108 + { id: "geography", label: "Geography", count: o.topCountries.length + o.topMetros.length },
109 + { id: "velocity", label: "Velocity" },
110 + { id: "events", label: "Events", count: physical.length + corporate.length },
111 + { id: "ai", label: "AI facilities", count: o.aiFacilities.length },
112 + { id: "cloud", label: "Cloud regions", count: cloud.length },
105 113 { id: "projects", label: "Projects", count: o.projects.length },
106 − { id: "changes", label: "Changes", count: o.recentEvents.length },
114 + { id: "facilities", label: "Facilities", count: facTotal },
115 + { id: "claims", label: "Claims", count: o.claims.length },
116 + { id: "quality", label: "Data quality" },
107 117 { id: "history", label: "Source history", count: o.sourceHistory.length },
108 118 ];
109 119
@@ -122,27 +132,45 @@ export default async function OperatorPage({ params, searchParams }: Props) {
122 132 {kindLabel}
123 133 </Badge>
124 134 )}
125 − </div>
126 − <h1 className="mt-1.5 text-[24px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">{o.name}</h1>
127 − <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-2">
128 − {o.hqCountryIso2 && (
129 − <span className="inline-flex items-center gap-1.5">
130 − <span className="text-ink-3">HQ</span>
131 − <Link href={routes.country(o.hqCountryIso2.toLowerCase())} className="inline-flex items-center gap-1.5 hover:text-ink hover:underline">
132 − <CountryFlag iso2={o.hqCountryIso2} withName />
133 − </Link>
134 − </span>
135 + {o.isCloudProvider && (
136 + <Badge tone="outline" size="xs">
137 + Cloud provider
138 + </Badge>
135 139 )}
136 − {o.website && <ExtLink href={o.website} className="no-underline hover:underline" />}
137 − {o.parent && (
138 − <span>
139 − <span className="text-ink-3">Part of </span>
140 − <Link href={routes.operator(o.parent.slug)} className="text-ink hover:text-accent hover:underline">
141 − {o.parent.name}
142 − </Link>
143 − </span>
140 + {o.isCarrier && (
141 + <Badge tone="outline" size="xs">
142 + Carrier
143 + </Badge>
144 144 )}
145 145 </div>
146 + <div className="mt-1.5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
147 + <div className="min-w-0">
148 + <h1 className="text-[24px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[30px]">{o.name}</h1>
149 + <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] text-ink-2">
150 + {o.hqCountryIso2 && (
151 + <span className="inline-flex items-center gap-1.5">
152 + <span className="text-ink-3">HQ</span>
153 + <Link href={routes.country(o.hqCountryIso2.toLowerCase())} className="inline-flex items-center gap-1.5 hover:text-ink hover:underline">
154 + <CountryFlag iso2={o.hqCountryIso2} withName />
155 + </Link>
156 + </span>
157 + )}
158 + {o.website && <ExtLink href={o.website} className="no-underline hover:underline" />}
159 + {o.parent && (
160 + <span>
161 + <span className="text-ink-3">Part of </span>
162 + <Link href={routes.operator(o.parent.slug)} className="text-ink hover:text-accent hover:underline">
163 + {o.parent.name}
164 + </Link>
165 + </span>
166 + )}
167 + </div>
168 + </div>
169 + <div className="flex shrink-0 flex-wrap items-center gap-2">
170 + <WatchButton entityType="operator" slug={o.slug} entityId={o.id} name={o.name} />
171 + <PillLink href={routes.compareOperators([o.slug])}>Compare with…</PillLink>
172 + </div>
173 + </div>
146 174 {o.description && <p className="mt-3 max-w-2xl text-[13.5px] text-ink-2">{o.description}</p>}
147 175 {aliases.length > 0 && (
148 176 <div className="mt-3 flex flex-wrap items-center gap-1.5">
@@ -157,95 +185,169 @@ export default async function OperatorPage({ params, searchParams }: Props) {
157 185 )}
158 186 </header>
159 187
160 − <MetricRow cols={6}>
188 + <MetricRow cols={5}>
161 189 <MetricCell>
162 − <Metric value={fmtInt(o.facilityCount)} label="Facilities" />
190 + <Metric value={fmtInt(o.facilityCount)} label="Facilities" note={statusTotal ? `${fmtInt(o.pipeline.operational.count)} operational` : undefined} />
163 191 </MetricCell>
164 192 <MetricCell>
165 193 <Metric value={fmtInt(o.countryCount)} label="Countries" />
166 194 </MetricCell>
167 195 <MetricCell>
168 − <Metric value={o.metroCount === undefined ? "—" : fmtInt(o.metroCount)} label="Metros" />
196 + <Metric value={o.metroCount === undefined ? "—" : fmtInt(o.metroCount)} label="Markets" />
169 197 </MetricCell>
170 198 <MetricCell>
171 − <Metric value={<MwValue mw={o.knownMw} />} unit={o.knownMw ? knownUnit : undefined} label="Known MW" note={o.knownMw ? "operational, published figures" : "no published figure"} />
199 + <Metric value={<MwFigure mw={o.knownMw} unitClassName="text-[0.5em]" />} label="Known operational MW" note={o.knownMw ? `published figures · ${fmtPct(coverage)} coverage` : `no published figure · ${fmtPct(coverage)} of facilities publish MW`} />
172 200 </MetricCell>
173 201 <MetricCell>
174 − <Metric value={<MwValue mw={o.plannedMw} estimate />} unit={o.plannedMw ? plannedUnit : undefined} label="Pipeline MW" note={o.plannedMw ? "planned + construction" : "no published figure"} />
202 + <Metric value={<MwFigure mw={constructionMw} estimate unitClassName="text-[0.5em]" />} label="Under construction" note={constructionMw === null ? `${fmtInt(o.pipeline.construction.count)} ${pluralize(o.pipeline.construction.count, "site")}, MW not published` : `${fmtInt(o.pipeline.construction.count)} ${pluralize(o.pipeline.construction.count, "site")}`} />
203 + </MetricCell>
204 + </MetricRow>
205 + <MetricRow cols={5} className="border-t-0 [&>div]:border-t-0">
206 + <MetricCell>
207 + <Metric size="sm" value={<MwFigure mw={plannedMw} estimate unitClassName="text-[0.5em]" />} label="Planned MW" note={plannedMw === null ? "not published" : "approved + announced"} />
208 + </MetricCell>
209 + <MetricCell>
210 + <Metric size="sm" value={fmtInt(o.aiCount ?? o.aiFacilities.length)} label="AI sites" />
211 + </MetricCell>
212 + <MetricCell>
213 + <Metric size="sm" value={fmtInt(o.cloudRegionCount ?? cloud.length)} label="Cloud regions" note={o.cloudRegionCount || cloud.length ? "cloud exposure" : "none published"} />
175 214 </MetricCell>
176 215 <MetricCell>
177 − <Metric value={o.projectCount === undefined ? fmtInt(o.projects.length) : fmtInt(o.projectCount)} label="Projects" />
216 + <Metric size="sm" value={fmtInt(o.projectCount ?? o.projects.length)} label="Projects" note={o.projectPlannedMw ? `≈ ${fmtMw(o.projectPlannedMw)} planned` : undefined} />
217 + </MetricCell>
218 + <MetricCell>
219 + <Metric size="sm" value={<CoverageBar value={coverage} width="w-full" label="Capacity coverage" />} label="Capacity coverage" note="share of facilities with a MW figure" />
178 220 </MetricCell>
179 221 </MetricRow>
180 222
181 223 <SectionNav items={sections} className="mt-4" />
182 224
183 225 <div className="flex flex-col gap-8 pt-6">
184 − <Section id="footprint" title="Footprint" description={`${fmtInt(countries.length)} ${pluralize(countries.length, "country", "countries")} · ${fmtInt(statusTotal)} facilities by status`} className="scroll-mt-28">
226 + <Section id="map" title="Portfolio map" description="Facilities operated; hollow rings are city-level locations." className="scroll-mt-28">
227 + <EntityMap filters={{ operator: o.slug }} center={{ lat: 25, lng: 10 }} zoom={1.2} openLabel="Open footprint in map" className="h-[240px] sm:h-[360px]" />
228 + {statusTotal > 0 && (
229 + <div className="mt-3">
230 + <div className="label-caps mb-1.5">Status</div>
231 + <StackedBar data={o.statusBreakdown} height={10} />
232 + </div>
233 + )}
234 + </Section>
235 +
236 + <Section id="pipeline" title="Pipeline" description="Facilities by lifecycle bucket with known MW — containment-aware (a campus and its buildings are never both counted)." className="scroll-mt-28">
237 + <PipelineBars pipeline={o.pipeline} />
238 + <PublishedOnlyNote className="mt-2" />
239 + </Section>
240 +
241 + <Section id="geography" title="Geographic diversity" description="Share of the operator's facilities by country and by market." className="scroll-mt-28">
185 242 <div className="grid gap-6 md:grid-cols-2">
186 243 <div>
187 − <div className="label-caps mb-2">Facilities by country</div>
188 − {countries.length ? (
189 − <CountBarList items={countries.slice(0, 12).map((c) => ({ label: <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={c.iso2} size={12} />{c.name}</span>, value: c.facilityCount, href: routes.country(c.slug), secondary: c.knownMw ? fmtMw(c.knownMw) : undefined }))} />
190 − ) : (
191 − <EmptyState compact title="No facilities located yet" />
192 − )}
193 − {countries.length > 12 && <p className="mt-2 text-[11.5px] text-ink-4">Top 12 of {countries.length} countries shown — the facility table below covers all of them.</p>}
244 + <div className="label-caps mb-2">Top countries</div>
245 + {o.topCountries.length ? <ShareList items={o.topCountries.map((c) => ({ key: c.iso2, label: c.name, href: routes.country(c.slug), count: c.facilityCount, share: c.share, mw: c.knownMw, iso2: c.iso2 }))} /> : <HonestEmpty title="No facility located in a country yet" />}
246 + {o.countries.length > o.topCountries.length && <p className="mt-2 text-[11.5px] text-ink-4">Top {o.topCountries.length} of {o.countries.length} countries — the facility table covers all of them.</p>}
194 247 </div>
195 248 <div>
196 − <div className="label-caps mb-2">Status</div>
197 − {statusTotal ? <StackedBar data={o.statusBreakdown} height={12} /> : <EmptyState compact title="No status data" />}
198 − {o.metros.length > 0 && (
199 − <div className="mt-5">
200 − <div className="label-caps mb-2">Top markets</div>
201 − <ul className="flex flex-wrap gap-1.5">
202 − {[...o.metros]
203 − .sort((a, b) => b.facilityCount - a.facilityCount)
204 − .slice(0, 10)
205 − .map((m) => (
206 − <li key={m.id}>
207 − <Link href={routes.market(m.slug)} className="inline-flex h-7 items-center gap-1.5 rounded-sm border border-hair bg-surface px-2 text-[12px] text-ink hover:bg-surface-2">
208 − <CountryFlag iso2={m.countryIso2} size={11} />
209 − {m.name}
210 − <span className="figure text-ink-3">{m.facilityCount}</span>
211 − </Link>
212 − </li>
213 − ))}
214 − </ul>
215 − </div>
216 − )}
249 + <div className="label-caps mb-2">Top markets</div>
250 + {o.topMetros.length ? <ShareList items={o.topMetros.map((m) => ({ key: m.id, label: m.name, href: routes.market(m.slug), count: m.facilityCount, share: m.share, mw: m.knownMw, iso2: m.countryIso2 }))} /> : <HonestEmpty title="No facility assigned to a market yet" detail="facilities outside any defined metro are not counted here" />}
251 + {o.metros.length > o.topMetros.length && <p className="mt-2 text-[11.5px] text-ink-4">Top {o.topMetros.length} of {o.metros.length} markets.</p>}
217 252 </div>
218 253 </div>
219 254 </Section>
220 255
221 − <Section id="map" title="Map" description="Facilities operated; hollow rings are city-level locations." className="scroll-mt-28">
222 − <EntityMap filters={{ operator: o.slug }} center={{ lat: 25, lng: 10 }} zoom={1.2} openLabel="Open footprint in map" className="h-[240px] sm:h-[360px]" />
256 + <Section id="velocity" title="Expansion velocity" description="New facilities, projects, countries and markets per window; the basis of every date is labelled." className="scroll-mt-28">
257 + <ExpansionVelocityBlock velocity={o.velocity} />
258 + </Section>
259 +
260 + <Section id="events" title="Recent events" description="Physical changes (facilities, projects, openings, capacity) are kept apart from corporate events (acquisitions, financing, partnerships, executive changes)." className="scroll-mt-28" aside={<Link href={routes.live(`operator=${o.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">Live feed →</Link>}>
261 + <div className="grid gap-6 lg:grid-cols-2">
262 + <div>
263 + <h3 className="mb-1.5 text-[13px] font-semibold text-ink">
264 + Physical <span className="figure text-[11px] font-normal text-ink-3">{fmtInt(physical.length)}</span>
265 + </h3>
266 + <EventList events={physical.slice(0, 12)} dense emptyTitle="No physical change detected yet" emptyDescription="Facility, project, opening and capacity changes appear here as connectors observe them." />
267 + </div>
268 + <div>
269 + <h3 className="mb-1.5 text-[13px] font-semibold text-ink">
270 + Corporate <span className="figure text-[11px] font-normal text-ink-3">{fmtInt(corporate.length)}</span>
271 + </h3>
272 + <EventList events={corporate.slice(0, 12)} dense emptyTitle="No corporate event recorded" emptyDescription="Acquisitions, financing, partnerships and executive changes are attached to the operator, never turned into projects." />
273 + </div>
274 + </div>
275 + </Section>
276 +
277 + <Section id="ai" title="AI facilities" description="Sites with confirmed or likely AI / HPC evidence; a single keyword never confirms a site." className="scroll-mt-28" aside={<Link href={routes.ai()} className="text-[12px] text-ink-3 hover:text-ink">AI index →</Link>}>
278 + {o.aiFacilities.length ? <FacilityTable items={o.aiFacilities} columns={["market", "country", "status", "mw", "type"]} /> : <HonestEmpty title="No AI / HPC facility with evidence" detail="facilities appear here when a source describes them as AI, HPC, GPU or accelerated-computing infrastructure" />}
279 + </Section>
280 +
281 + <Section id="cloud" title="Cloud regions" description={o.isCloudProvider ? "Public cloud regions operated by this provider." : "Cloud regions associated with this operator's markets or tenants."} className="scroll-mt-28">
282 + {cloud.length ? (
283 + <Table dense mobileCollapse={{ total: cloud.length, label: "cloud regions" }}>
284 + <THead>
285 + <tr>
286 + <Th>Code</Th>
287 + <Th className="hidden sm:table-cell">Region</Th>
288 + <Th>Country</Th>
289 + <Th numeric className="hidden md:table-cell">
290 + AZs
291 + </Th>
292 + <Th>Status</Th>
293 + <Th className="hidden lg:table-cell">Launched</Th>
294 + </tr>
295 + </THead>
296 + <TBody>
297 + {cloud.map((cr) => (
298 + <Tr key={cr.id}>
299 + <Td primary>
300 + <RowLink href={routes.cloudRegion(cr.slug)} className="figure text-[12.5px]">
301 + {cr.code}
302 + </RowLink>
303 + <div className="text-[11.5px] text-ink-3 sm:hidden">{cr.name}</div>
304 + </Td>
305 + <Td label="Region" className="hidden text-ink-2 sm:table-cell">
306 + {cr.name}
307 + </Td>
308 + <Td label="Country">
309 + <CountryFlag iso2={cr.countryIso2} withName className="max-w-[160px] text-ink-2" />
310 + </Td>
311 + <Td label="AZs" numeric className="hidden md:table-cell">
312 + {cr.availabilityZones ?? <span className="text-ink-4">—</span>}
313 + </Td>
314 + <Td label="Status">
315 + <CloudStatusBadge status={cr.status} />
316 + </Td>
317 + <Td label="Launched" className="figure hidden text-[12px] text-ink-3 lg:table-cell">
318 + {fmtDate(cr.launchedOn)}
319 + </Td>
320 + </Tr>
321 + ))}
322 + </TBody>
323 + </Table>
324 + ) : (
325 + <HonestEmpty title="No cloud region linked" detail="cloud exposure is published by the providers' region lists" />
326 + )}
327 + </Section>
328 +
329 + <Section id="projects" title="Projects" description="Announced, permitted and under-construction developments linked to this operator (announcements, not facilities)." className="scroll-mt-28" aside={o.projects.length > 0 ? <Link href={routes.projects(`operator=${o.slug}`)} className="text-[12px] text-ink-3 hover:text-ink">All projects →</Link> : undefined}>
330 + <ProjectList items={o.projects} emptyTitle="No project linked to this operator" emptyDescription="Projects are created only from announcements that name a site, a location and a development verb." />
223 331 </Section>
224 332
225 333 <Section id="facilities" title="Facilities" description={`${fmtInt(facTotal)} indexed · sorted by known capacity`} className="scroll-mt-28">
226 334 <PagedFacilities items={o.facilities} total={facTotal} page={fPage} perPage={facPer} hrefFor={(p) => fPageHref(base, sp, p)} columns={["market", "country", "status", "mw", "type", "confidence"]} />
227 335 </Section>
228 336
229 − {o.projects.length > 0 && (
230 − <Section id="projects" title="Projects" description="Announced and under-construction developments linked to this operator." className="scroll-mt-28">
231 − <ProjectList items={o.projects} />
232 − </Section>
233 − )}
337 + <Section id="claims" title="Claims" description="Every capacity and investment figure asserted about this operator, with its scope. Portfolio and company figures are shown as evidence, never summed into facilities." className="scroll-mt-28">
338 + <ClaimsTable claims={o.claims} kind="all" emptyNote="No operator-level claim has been extracted yet — portfolio and company figures appear here once a document asserts one." />
339 + </Section>
234 340
235 − {o.recentEvents.length > 0 && (
236 − <Section id="changes" title="Recent changes" description="Detected on the operator's facilities and projects." className="scroll-mt-28">
237 − <EventList events={o.recentEvents} dense />
238 − </Section>
239 − )}
341 + <Section id="quality" title="Data quality" description="Sources, completeness, claims by status and open sanity flags for this record." className="scroll-mt-28">
342 + <DataQualityPanel q={o.dataQuality} />
343 + </Section>
240 344
241 − {o.sourceHistory.length > 0 && (
242 − <Section id="history" title="Source history" description="Observations and page changes that shaped this record." className="scroll-mt-28">
243 − <SourceHistory items={o.sourceHistory.slice(0, 40)} />
244 − </Section>
245 − )}
345 + <Section id="history" title="Source history" description="Observations and page changes that shaped this record." className="scroll-mt-28">
346 + {o.sourceHistory.length ? <SourceHistory items={o.sourceHistory.slice(0, 40)} /> : <EmptyState compact title="No source history yet" />}
347 + </Section>
246 348 </div>
247 349
248 − <SourcesFooter sources={r.sources} updatedAt={o.updatedAt} />
350 + <SourcesFooter sources={r.sources} updatedAt={o.updatedAt} note={o.knownMw === null ? `No operational MW figure has been published by the sources indexed for ${o.name}; counts are exact, capacities appear only when a source publishes them.` : undefined} />
249 351 </Page>
250 352 );
251 353 }
added apps/web/src/app/(site)/power/loading.tsx +5 −0
@@ -0,0 +1,5 @@
1 +import { IndexLoading } from "@/components/intelligence/index-loading";
2 +
3 +export default function Loading() {
4 + return <IndexLoading />;
5 +}
added apps/web/src/app/(site)/power/page.tsx +233 −0
@@ -0,0 +1,233 @@
1 +import type { PowerOverview } from "@dci/core";
2 +import { ExternalLink } from "lucide-react";
3 +import type { Metadata } from "next";
4 +import Link from "next/link";
5 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
6 +import { SectionNav } from "@/components/entity/section-nav";
7 +import { SourcesFooter } from "@/components/entity/sources-footer";
8 +import { EventList } from "@/components/event-list";
9 +import { CountryEnergyTable } from "@/components/intelligence/power/country-energy-table";
10 +import { ApiNote, GRID_KIND_LABEL, GridConstraintList, GridKindBadge, HonestEmpty, PillLink } from "@/components/intelligence/shared";
11 +import { Badge } from "@/components/ui/badge";
12 +import { Page, PageHeader, Section } from "@/components/ui/card";
13 +import { cn } from "@/components/ui/cn";
14 +import { CountryFlag } from "@/components/ui/country-flag";
15 +import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
16 +import { ErrorState, OfflineNotice } from "@/components/ui/states";
17 +import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
18 +import { api } from "@/lib/api";
19 +import { fmtInt, fmtMw } from "@/lib/format";
20 +import { OPERATOR_KIND_LABEL } from "@/lib/labels";
21 +import { routes } from "@/lib/routes";
22 +import { pageMetadata } from "@/lib/seo";
23 +import { hrefWith, spGet, type SearchParams } from "@/lib/sort";
24 +
25 +export const revalidate = 120;
26 +
27 +export 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 +});
32 +
33 +/** The eight constraint kinds of the spec, always listed (0 → “none recorded”). */
34 +const SPEC_KINDS = ["moratorium", "grid_delay", "capacity_restriction", "load_cap", "new_transmission", "new_substation", "regulation", "large_load_queue"] as const;
35 +
36 +const LOAD_KIND_LABEL: Record<PowerOverview["largeLoads"][number]["kind"], string> = { utility_capacity: "Utility capacity", grid_connection: "Grid connection", planned_load: "Planned load" };
37 +
38 +export 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));
59 +
60 + 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 + ];
67 +
68 + 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>} />
72 +
73 + <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>
90 +
91 + <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" />
102 +
103 + <SectionNav items={sections} className="mt-4" />
104 +
105 + <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 + Reset
117 + </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>
122 +
123 + <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>
126 +
127 + <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>
189 +
190 + <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>
193 +
194 + <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>
212 +
213 + <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 +}
217 +
218 +function 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 +}
modified apps/web/src/app/(site)/projects/(index)/page.tsx +115 −94
@@ -1,32 +1,36 @@
1 1 import type { FacilityStatus } from "@dci/core";
2 +import { Activity, Columns3, Map as MapIcon, Table2 } from "lucide-react";
2 3 import type { Metadata } from "next";
3 4 import Link from "next/link";
4 5 import { Suspense } from "react";
5 6 import { PipelineByYear } from "@/components/home/charts";
6 −import { ConfidenceBadge, EstimateMark, StatusBadge } from "@/components/ui/badge";
7 +import { PipelineFunnel } from "@/components/home/pipeline-funnel";
8 +import { AiMark, ConfidenceBadge, EstimateMark, PrecisionBadge, ScopeBadge, StatusBadge } from "@/components/ui/badge";
7 9 import { Page, PageHeader } from "@/components/ui/card";
8 −import { BarList, StackedBar } from "@/components/ui/charts";
10 +import { BarList } from "@/components/ui/charts";
9 11 import { CountryFlag } from "@/components/ui/country-flag";
10 12 import { FilterChips, type Chip } from "@/components/ui/filter-chips";
11 −import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
12 13 import { Pagination } from "@/components/ui/pagination";
14 +import { SegmentedControl } from "@/components/ui/segmented";
13 15 import { EmptyState, OfflineNotice } from "@/components/ui/states";
14 16 import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
15 17 import { api } from "@/lib/api";
16 18 import { fmtDate, fmtInt, fmtMw, fmtUsd, placeLine } from "@/lib/format";
17 19 import { fmtPartialDateSafe } from "@/lib/format-project";
18 −import { STATUS_SHORT } from "@/lib/labels";
20 +import { STATUS_SHORT, isSiteScopeLabel } from "@/lib/labels";
19 21 import { routes } from "@/lib/routes";
20 22 import { pageMetadata } from "@/lib/seo";
21 23 import { spGet, spInt, type SearchParams } from "@/lib/sort";
22 24 import { ProjectFilters } from "../filters";
23 −import { projectQueryString, type ProjectQuery } from "../query";
25 +import { ProjectsMapView } from "../projects-map";
26 +import { PROJECT_CLASS_OPTIONS, PROJECT_VIEWS, projectQueryString, type ProjectQuery, type ProjectView } from "../query";
27 +import { ProjectsKanban, ProjectsTimeline, investmentCell } from "../views";
24 28
25 29 export const revalidate = 120;
26 30
27 31 export const metadata: Metadata = pageMetadata({
28 − title: "Data center projects",
29 − description: "Announced, permitted and under-construction data center projects worldwide with planned capacity, investment and expected opening.",
32 + title: "Data center projects & construction pipeline",
33 + description: "Announced, permitted, approved and under-construction data center projects worldwide — lifecycle stages, planned MW (published figures only), investment and expected opening. Table, map, timeline and kanban views.",
30 34 path: routes.projects(),
31 35 });
32 36
@@ -35,6 +39,8 @@ const SORTS = ["updated", "mw", "announced", "opening"] as const;
35 39
36 40 function parseQuery(sp: SearchParams): ProjectQuery {
37 41 const q: ProjectQuery = {};
42 + const view = spGet(sp, "view");
43 + if (view && (PROJECT_VIEWS as readonly string[]).includes(view)) q.view = view as ProjectView;
38 44 const s = spGet(sp, "status");
39 45 if (s) q.status = s;
40 46 const c = spGet(sp, "country");
@@ -45,6 +51,12 @@ function parseQuery(sp: SearchParams): ProjectQuery {
45 51 if (Number.isFinite(mw) && mw > 0) q.min_mw = mw;
46 52 const ai = spGet(sp, "ai");
47 53 if (ai === "1" || ai === "true") q.ai = true;
54 + const cls = spGet(sp, "class");
55 + if (cls) q.project_class = cls;
56 + const ef = Number(spGet(sp, "expected_from"));
57 + if (Number.isFinite(ef) && ef > 1990) q.expected_from = ef;
58 + const et = Number(spGet(sp, "expected_to"));
59 + if (Number.isFinite(et) && et > 1990) q.expected_to = et;
48 60 const text = spGet(sp, "q");
49 61 if (text) q.q = text;
50 62 const sort = spGet(sp, "sort");
@@ -58,10 +70,12 @@ function parseQuery(sp: SearchParams): ProjectQuery {
58 70 export default async function ProjectsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
59 71 const sp = await searchParams;
60 72 const query = parseQuery(sp);
73 + const view: ProjectView = query.view ?? "table";
61 74 const page = query.page ?? 1;
75 + const perPage = view === "table" ? PER_PAGE : 100;
62 76 const [pipe, list, countriesRes] = await Promise.all([
63 77 api.projectPipeline(),
64 − api.projects({ status: query.status, country: query.country, operator: query.operator, min_mw: query.min_mw, ai: query.ai, q: query.q, sort: query.sort ?? "updated", order: query.order, page, per_page: PER_PAGE }),
78 + api.projects({ status: query.status, country: query.country, operator: query.operator, min_mw: query.min_mw, ai: query.ai, project_class: query.project_class, expected_from: query.expected_from, expected_to: query.expected_to, q: query.q, sort: query.sort ?? (view === "timeline" ? "opening" : "updated"), order: query.order ?? (view === "timeline" ? "asc" : undefined), page, per_page: perPage }),
65 79 api.countries(),
66 80 ]);
67 81 const p = pipe.ok ? pipe.data : null;
@@ -79,53 +93,43 @@ export default async function ProjectsPage({ searchParams }: { searchParams: Pro
79 93 const byYear = (p?.byYear ?? []).map((y) => ({ year: y.year, count: y.count, mw: y.mw }));
80 94
81 95 const chips: Chip[] = [];
82 − for (const s of (query.status ?? "").split(",").filter(Boolean)) chips.push({ key: `status-${s}`, label: "Status", value: STATUS_SHORT[s as FacilityStatus] ?? s, removeHref: href({ status: (query.status ?? "").split(",").filter((x) => x && x !== s).join(",") || undefined }) });
96 + for (const s of (query.status ?? "").split(",").filter(Boolean)) chips.push({ key: `status-${s}`, label: "Stage", value: STATUS_SHORT[s as FacilityStatus] ?? s, removeHref: href({ status: (query.status ?? "").split(",").filter((x) => x && x !== s).join(",") || undefined }) });
83 97 if (query.country) chips.push({ key: "country", label: "Country", value: countries.find((c) => c.iso2 === query.country)?.name ?? query.country, removeHref: href({ country: undefined }) });
84 98 if (query.operator) chips.push({ key: "operator", label: "Operator", value: query.operator, removeHref: href({ operator: undefined }) });
85 99 if (query.min_mw) chips.push({ key: "min_mw", label: "Planned", value: `≥ ${fmtMw(query.min_mw)}`, removeHref: href({ min_mw: undefined }) });
86 100 if (query.ai) chips.push({ key: "ai", label: "AI", value: "AI infrastructure", removeHref: href({ ai: undefined }) });
101 + for (const c of (query.project_class ?? "").split(",").filter(Boolean)) chips.push({ key: `class-${c}`, label: "Class", value: PROJECT_CLASS_OPTIONS.find((o) => o.v === c)?.label ?? c, removeHref: href({ project_class: (query.project_class ?? "").split(",").filter((x) => x && x !== c).join(",") || undefined }) });
102 + if (query.expected_from || query.expected_to) chips.push({ key: "expected", label: "Expected", value: `${query.expected_from ?? "…"}–${query.expected_to ?? "…"}`, removeHref: href({ expected_from: undefined, expected_to: undefined }) });
87 103 if (query.q) chips.push({ key: "q", label: "Search", value: query.q, removeHref: href({ q: undefined }) });
88 104
105 + const views: Array<{ value: ProjectView; label: React.ReactNode; title: string }> = [
106 + { value: "table", label: <><Table2 className="size-3.5" strokeWidth={1.75} /> Table</>, title: "Sortable table" },
107 + { value: "map", label: <><MapIcon className="size-3.5" strokeWidth={1.75} /> Map</>, title: "Located projects on the map" },
108 + { value: "timeline", label: <><Activity className="size-3.5" strokeWidth={1.75} /> Timeline</>, title: "By expected opening year" },
109 + { value: "kanban", label: <><Columns3 className="size-3.5" strokeWidth={1.75} /> Kanban</>, title: "Columns by lifecycle stage" },
110 + ];
111 +
89 112 return (
90 113 <Page>
91 − <PageHeader eyebrow="Tracker" title="Data center projects" description={p ? `${fmtInt(p.totals.projects)} projects tracked from announcement to opening. Planned MW is a published figure (≈ never imputed); investment as announced.` : "Announced, permitted and under-construction projects worldwide."} />
114 + <PageHeader
115 + eyebrow="Pipeline"
116 + title="Data center projects"
117 + description={p ? `${fmtInt(p.totals.projects)} projects tracked from rumour to opening. Planned MW are published, site-scoped figures (≈ never imputed); company-wide totals are kept as evidence, never summed.` : "Announced, permitted, approved and under-construction projects worldwide."}
118 + aside={<SegmentedControl label="View" value={view} options={views.map((v) => ({ value: v.value, label: v.label, title: v.title, href: href({ view: v.value === "table" ? undefined : v.value }) }))} />}
119 + />
92 120
93 121 {!pipe.ok && <OfflineNotice detail={pipe.error} className="mb-3" />}
94 122
95 − <MetricRow cols={4}>
96 − <MetricCell>
97 − <Metric value={p ? fmtInt(p.totals.projects) : "—"} label="Projects" size="sm" />
98 − </MetricCell>
99 − <MetricCell>
100 − <Metric
101 − value={
102 − p?.totals.plannedMw === null || p?.totals.plannedMw === undefined ? (
103 − "—"
104 − ) : (
105 − <>
106 − <EstimateMark className="mr-0.5 text-[0.6em]" />
107 − {fmtMw(p.totals.plannedMw).split(" ")[0]}
108 − </>
109 − )
110 − }
111 − unit={p?.totals.plannedMw ? fmtMw(p.totals.plannedMw).split(" ")[1] : undefined}
112 − label="Planned capacity"
113 − note="sum of published planned MW"
114 − size="sm"
115 − />
116 − </MetricCell>
117 − <MetricCell>
118 − <Metric value={p?.totals.investmentUsd ? fmtUsd(p.totals.investmentUsd) : "—"} label="Announced investment" note={p?.totals.investmentUsd ? "where disclosed" : "not disclosed"} size="sm" />
119 − </MetricCell>
120 − <MetricCell>
121 − <Metric value={p ? fmtInt(p.totals.ai) : "—"} label="AI projects" size="sm" />
122 − </MetricCell>
123 − </MetricRow>
123 + {/* funnel */}
124 + <section aria-label="Construction pipeline funnel" className="border-y border-hair py-4">
125 + <PipelineFunnel data={{ byStatus: p?.byStatus ?? [], openedLast12m: null }} />
126 + </section>
124 127
125 128 <div className="grid gap-6 py-6 md:grid-cols-3">
126 129 <div>
127 − <div className="label-caps mb-2">By status</div>
128 − {Object.keys(byStatus).length ? <StackedBar data={byStatus} /> : <EmptyState compact title="No projects yet" />}
130 + <div className="label-caps mb-2">Investment announced</div>
131 + <div className="figure text-[22px] font-medium text-ink">{p?.totals.investmentUsd ? fmtUsd(p.totals.investmentUsd) : "—"}</div>
132 + <p className="text-[11.5px] text-ink-3">{p?.totals.investmentUsd ? "where disclosed, site-scoped only" : "not disclosed"} · {p ? `${fmtInt(p.totals.ai)} AI projects` : ""}</p>
129 133 </div>
130 134 <div>
131 135 <div className="label-caps mb-2">By expected year</div>
@@ -134,7 +138,7 @@ export default async function ProjectsPage({ searchParams }: { searchParams: Pro
134 138 <div>
135 139 <div className="label-caps mb-2">Top countries</div>
136 140 {p?.byCountry.length ? (
137 − <BarList items={p.byCountry.slice(0, 8).map((c) => ({ label: <CountryFlag iso2={c.countryIso2} withName />, value: c.count, href: c.slug ? routes.country(c.slug) : href({ country: c.countryIso2 }), secondary: c.mw !== null ? `≈ ${fmtMw(c.mw)}` : undefined }))} />
141 + <BarList items={p.byCountry.slice(0, 8).map((c) => ({ label: <CountryFlag iso2={c.countryIso2} withName />, value: c.count, href: href({ country: c.countryIso2 }), secondary: c.mw !== null ? `≈ ${fmtMw(c.mw)}` : undefined }))} />
138 142 ) : (
139 143 <EmptyState compact title="No located projects yet" description="Country appears once a project is geolocated." />
140 144 )}
@@ -146,11 +150,17 @@ export default async function ProjectsPage({ searchParams }: { searchParams: Pro
146 150 <ProjectFilters query={query} total={list.total} countries={countries} />
147 151 </Suspense>
148 152 </div>
149 − <FilterChips chips={chips} resetHref={routes.projects()} className="py-2" />
153 + <FilterChips chips={chips} resetHref={routes.projects(view !== "table" ? `view=${view}` : undefined)} className="py-2" />
150 154
151 155 <div className="pt-1">
152 156 {list.items.length === 0 ? (
153 157 <EmptyState compact title={chips.length ? "No projects match these filters" : "No projects tracked yet"} description={chips.length ? "Loosen a filter or clear them all." : "Announced and under-construction projects appear once ingested."} />
158 + ) : view === "map" ? (
159 + <ProjectsMapView items={list.items} total={list.total} projectStatus={query.status} />
160 + ) : view === "timeline" ? (
161 + <ProjectsTimeline items={list.items} />
162 + ) : view === "kanban" ? (
163 + <ProjectsKanban items={list.items} counts={byStatus} />
154 164 ) : (
155 165 <Table>
156 166 <THead>
@@ -158,7 +168,7 @@ export default async function ProjectsPage({ searchParams }: { searchParams: Pro
158 168 <Th>Project</Th>
159 169 <Th className="hidden sm:table-cell">Operator</Th>
160 170 <Th>Location</Th>
161 − <Th>Status</Th>
171 + <Th>Stage</Th>
162 172 <Th numeric sort={sortState("mw")} sortHref={sortHref("mw")}>
163 173 Planned MW
164 174 </Th>
@@ -175,65 +185,76 @@ export default async function ProjectsPage({ searchParams }: { searchParams: Pro
175 185 </tr>
176 186 </THead>
177 187 <TBody>
178 − {list.items.map((pr) => (
179 − <Tr key={pr.id}>
180 − <Td primary>
181 − <RowLink href={routes.project(pr.slug)}>{pr.name}</RowLink>
182 − <div className="text-[11.5px] text-ink-3 sm:hidden">{pr.operator?.name ?? "Operator unknown"}</div>
183 − </Td>
184 − <Td label="Operator" className="hidden sm:table-cell">
185 − {pr.operator ? (
186 − <Link href={routes.operator(pr.operator.slug)} className="text-ink-2 hover:text-ink hover:underline">
187 − {pr.operator.name}
188 − </Link>
189 − ) : (
190 − <span className="text-ink-4">—</span>
191 − )}
192 − </Td>
193 − <Td label="Location">
194 − <span className="inline-flex max-w-[220px] items-center gap-1.5 text-ink-2">
195 − <CountryFlag iso2={pr.countryIso2} size={12} />
196 − <span className="truncate">{pr.metro ? <Link href={routes.market(pr.metro.slug)} className="hover:text-ink hover:underline">{pr.metro.name}</Link> : placeLine(pr)}</span>
197 − </span>
198 − </Td>
199 − <Td label="Status">
200 − <StatusBadge status={pr.status} short size="xs" />
201 − </Td>
202 − <Td label="Planned MW" numeric lead>
203 − {pr.plannedMw === null ? (
204 − <span className="text-ink-4">—</span>
205 − ) : (
206 − <span>
207 − <EstimateMark className="mr-0.5" />
208 − {fmtMw(pr.plannedMw)}
188 + {list.items.map((pr) => {
189 + const site = pr.capacityScope == null || isSiteScopeLabel(pr.capacityScope);
190 + return (
191 + <Tr key={pr.id}>
192 + <Td primary>
193 + <span className="inline-flex flex-wrap items-center gap-1.5">
194 + <RowLink href={routes.project(pr.slug)}>{pr.name}</RowLink>
195 + {pr.isAi && <AiMark evidence={pr.aiEvidence} />}
196 + </span>
197 + <div className="text-[11.5px] text-ink-3 sm:hidden">{pr.operator?.name ?? "Operator unknown"}</div>
198 + </Td>
199 + <Td label="Operator" className="hidden sm:table-cell">
200 + {pr.operator ? (
201 + <Link href={routes.operator(pr.operator.slug)} className="text-ink-2 hover:text-ink hover:underline">
202 + {pr.operator.name}
203 + </Link>
204 + ) : (
205 + <span className="text-ink-4">—</span>
206 + )}
207 + </Td>
208 + <Td label="Location">
209 + <span className="inline-flex max-w-[220px] items-center gap-1.5 text-ink-2">
210 + <CountryFlag iso2={pr.countryIso2} size={12} />
211 + <span className="truncate">{pr.metro ? <Link href={routes.market(pr.metro.slug)} className="hover:text-ink hover:underline">{pr.metro.name}</Link> : placeLine(pr)}</span>
212 + {pr.geoPrecision && pr.geoPrecision !== "exact" && pr.geoPrecision !== "parcel" && <PrecisionBadge precision={pr.geoPrecision} size="xs" className="hidden md:inline-flex" />}
209 213 </span>
210 − )}
211 − </Td>
212 − <Td label="Investment" numeric className="hidden lg:table-cell">
213 − {pr.investmentUsd === null ? <span className="text-ink-4">—</span> : fmtUsd(pr.investmentUsd)}
214 − </Td>
215 − <Td label="Announced" className="figure hidden text-[12px] text-ink-3 lg:table-cell">
216 − {fmtDate(pr.announcedOn)}
217 − </Td>
218 − <Td label="Expected" className="figure text-[12px]">
219 − {pr.expectedOpening ? fmtPartialDateSafe(pr.expectedOpening) : <span className="text-ink-4">TBD</span>}
220 − </Td>
221 − <Td label="Confidence" className="hidden lg:table-cell">
222 − <ConfidenceBadge level={pr.confidence} size="xs" />
223 − </Td>
224 − </Tr>
225 − ))}
214 + </Td>
215 + <Td label="Stage">
216 + <StatusBadge status={pr.status} short size="xs" />
217 + </Td>
218 + <Td label="Planned MW" numeric lead>
219 + {pr.plannedMw === null ? (
220 + <span className="text-ink-4">—</span>
221 + ) : site ? (
222 + <span>
223 + <EstimateMark className="mr-0.5" />
224 + {fmtMw(pr.plannedMw)}
225 + </span>
226 + ) : (
227 + <span className="inline-flex items-center gap-1 text-ink-4" title={`${fmtMw(pr.plannedMw)} is a ${pr.capacityScope}-level figure — not this site's`}>
228 + — <ScopeBadge scope={pr.capacityScope} />
229 + </span>
230 + )}
231 + </Td>
232 + <Td label="Investment" numeric className="hidden lg:table-cell">
233 + {investmentCell(pr) === "—" ? <span className="text-ink-4">—</span> : investmentCell(pr)}
234 + </Td>
235 + <Td label="Announced" className="figure hidden text-[12px] text-ink-3 lg:table-cell">
236 + {fmtDate(pr.announcedOn)}
237 + </Td>
238 + <Td label="Expected" className="figure text-[12px]">
239 + {pr.expectedOpening ? fmtPartialDateSafe(pr.expectedOpening) : <span className="text-ink-4">TBD</span>}
240 + </Td>
241 + <Td label="Confidence" className="hidden lg:table-cell">
242 + <ConfidenceBadge level={pr.confidence} size="xs" />
243 + </Td>
244 + </Tr>
245 + );
246 + })}
226 247 </TBody>
227 248 </Table>
228 249 )}
229 250 </div>
230 − <Pagination page={page} perPage={PER_PAGE} total={list.total} hrefFor={(pg) => routes.projects(projectQueryString({ ...query, page: pg }))} label="projects" className="mt-3" />
251 + <Pagination page={page} perPage={perPage} total={list.total} hrefFor={(pg) => routes.projects(projectQueryString({ ...query, page: pg }))} label="projects" className="mt-3" />
231 252 <p className="mt-6 text-[11.5px] text-ink-4">
232 − Projects are reconciled from operator press releases, planning filings and news; status changes are versioned and appear in the{" "}
253 + Projects are reconciled from operator press releases, planning filings and news through a lifecycle state machine; status changes are versioned and appear in the{" "}
233 254 <Link href={routes.live("type=project_announced,project_status_changed,construction_started")} className="underline decoration-hair-2 hover:text-ink">
234 255 live feed
235 256 </Link>
236 − .
257 + . Locations are shown at the precision published (a city-level ring is never a site).
237 258 </p>
238 259 </Page>
239 260 );
modified apps/web/src/app/(site)/projects/[slug]/page.tsx +277 −146
@@ -3,6 +3,7 @@ import { partialDateSortKey } from "@dci/core";
3 3 import type { Metadata } from "next";
4 4 import Link from "next/link";
5 5 import { notFound } from "next/navigation";
6 +import type { ReactNode } from "react";
6 7 import { Breadcrumbs } from "@/components/entity/breadcrumbs";
7 8 import { EntityMap } from "@/components/entity/entity-map";
8 9 import { JsonLd } from "@/components/entity/json-ld";
@@ -11,15 +12,16 @@ import { ProvenanceTable, WithProvenance } from "@/components/entity/provenance"
11 12 import { SectionNav } from "@/components/entity/section-nav";
12 13 import { SourcesFooter } from "@/components/entity/sources-footer";
13 14 import { EventList } from "@/components/event-list";
14 −import { ConfidenceBadge, EstimateMark, StatusBadge } from "@/components/ui/badge";
15 +import { ClaimsTable, DataQualityPanel, Evidence, EvidenceProvider, NearbyInfrastructurePanel } from "@/components/evidence";
16 +import { ProjectStages, ProjectVelocity } from "@/components/project-stages";
17 +import { AiMark, Badge, ConfidenceBadge, EstimateMark, PrecisionBadge, ScopeBadge, SemanticsLabel, StatusBadge } from "@/components/ui/badge";
15 18 import { Page, Section } from "@/components/ui/card";
16 19 import { CountryFlag } from "@/components/ui/country-flag";
17 −import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";
18 20 import { ErrorState, OfflineNotice } from "@/components/ui/states";
19 21 import { SourceHistory, TimelineList, type TimelineEntry } from "@/components/ui/timeline";
20 −import { countryName, fmtDate, fmtInt, fmtMw, fmtUsd, placeLine } from "@/lib/format";
22 +import { countryName, fmtDate, fmtInt, fmtMw, fmtUsd, placeLine, splitMw } from "@/lib/format";
21 23 import { fmtPartialDateSafe } from "@/lib/format-project";
22 −import { EVENT_LABEL, STATUS_LABEL } from "@/lib/labels";
24 +import { CONFIDENCE_LABEL, EVENT_LABEL, PRECISION_LABEL, STATUS_LABEL, isSiteScopeLabel } from "@/lib/labels";
23 25 import { routes } from "@/lib/routes";
24 26 import { absoluteUrl, compactJsonLd, NOT_FOUND_METADATA, pageMetadata } from "@/lib/seo";
25 27 import { getProject } from "./data";
@@ -34,10 +36,15 @@ function describe(p: ProjectDetail): string {
34 36 if (p.operator) bits.push(`by ${p.operator.name}`);
35 37 const place = placeLine(p);
36 38 if (place !== "—") bits.push(`in ${place}`);
37 − if (p.plannedMw !== null) bits.push(`— ≈ ${fmtMw(p.plannedMw)} planned`);
38 − if (p.investmentUsd !== null) bits.push(`, ${fmtUsd(p.investmentUsd)} announced`);
39 + if (p.plannedMw !== null && siteScoped(p.capacityScope)) bits.push(`— ≈ ${fmtMw(p.plannedMw)} planned (published figure)`);
40 + if (p.investmentUsd !== null && siteScoped(p.investmentScope)) bits.push(`, ${fmtUsd(p.investmentUsd)} announced`);
39 41 if (p.expectedOpening) bits.push(`, expected ${fmtPartialDateSafe(p.expectedOpening)}`);
40 − return `${bits.join(" ")}. Timeline, status history and provenance.`;
42 + return `${bits.join(" ")}. Lifecycle stages, timeline, claims with scope and provenance.`;
43 +}
44 +
45 +/** A header figure may only stand for the project when its scope is a site scope (or unknown-but-assigned legacy). */
46 +function siteScoped(scope: string | null | undefined): boolean {
47 + return scope === null || scope === undefined || isSiteScopeLabel(scope);
41 48 }
42 49
43 50 export async function generateMetadata({ params }: Props): Promise<Metadata> {
@@ -66,12 +73,37 @@ function toneFor(t: string): TimelineEntry["tone"] {
66 73 return "accent";
67 74 case "closure":
68 75 case "incident":
76 + case "project_cancelled":
69 77 return "danger";
70 78 default:
71 79 return "neutral";
72 80 }
73 81 }
74 82
83 +function Figure({ label, children, note }: { label: ReactNode; children: ReactNode; note?: ReactNode }) {
84 + return (
85 + <div className="min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0">
86 + <div className="label-caps mb-1 truncate">{label}</div>
87 + <div className="min-w-0">{children}</div>
88 + {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}
89 + </div>
90 + );
91 +}
92 +
93 +const PROJECT_CLASS_LABEL: Record<string, string> = {
94 + NEW_BUILD: "New build",
95 + EXPANSION: "Expansion",
96 + CONSTRUCTION_START: "Construction start",
97 + PERMIT: "Permit",
98 + LAND_ACQUISITION: "Land acquisition",
99 + GRID_CONNECTION: "Grid connection",
100 + POWER_AGREEMENT: "Power agreement",
101 + FINANCING: "Financing",
102 + ACQUISITION: "Acquisition",
103 + PARTNERSHIP: "Partnership",
104 + CUSTOMER_AGREEMENT: "Customer agreement",
105 +};
106 +
75 107 export default async function ProjectPage({ params }: Props) {
76 108 const { slug } = await params;
77 109 const r = await getProject(slug);
@@ -88,26 +120,30 @@ export default async function ProjectPage({ params }: Props) {
88 120 const base = routes.project(p.slug);
89 121 const place = placeLine(p);
90 122 const hasGeo = p.lat !== null && p.lng !== null;
123 + const precision = p.geoPrecision ?? (hasGeo ? "city" : "unknown");
124 + const claims = p.claims ?? [];
125 + const history = p.history ?? [];
126 + const mwSite = siteScoped(p.capacityScope);
127 + const usdSite = siteScoped(p.investmentScope);
128 + const dq = p.dataQuality;
91 129
92 130 const timeline: TimelineEntry[] = [...p.timeline]
93 131 .sort((a, b) => partialDateSortKey(a.date.length > 10 ? a.date.slice(0, 10) : a.date) - partialDateSortKey(b.date.length > 10 ? b.date.slice(0, 10) : b.date))
94 132 .map((t) => ({ date: t.date, title: humanType(t.type), description: t.description, meta: t.sourceName ? <span>{t.sourceName}</span> : undefined, href: t.url, tone: toneFor(t.type) }));
95 − if (p.expectedOpening) {
96 − const last = timeline.length ? partialDateSortKey(timeline[timeline.length - 1]!.date.slice(0, 10)) : Number.NEGATIVE_INFINITY;
97 − if (partialDateSortKey(p.expectedOpening) >= last) {
98 − timeline.push({ date: p.expectedOpening, title: <span className="text-ink-2">Expected opening</span>, description: p.phaseCount ? `phase 1 of ${fmtInt(p.phaseCount)} — expected, as published by the source` : "expected, as published by the source", tone: "neutral" });
99 − }
100 − }
101 − const statusHistory: TimelineEntry[] = p.statusHistory.map((s) => ({ date: s.date, title: `${s.from ? `${STATUS_LABEL[s.from]} → ` : ""}${STATUS_LABEL[s.to]}`, href: s.url, tone: s.to === "under_construction" ? "warn" : s.to === "operational" ? "ok" : s.to === "cancelled" || s.to === "delayed" ? "danger" : "neutral" }));
133 + for (const s of p.statusHistory) timeline.push({ date: s.date, title: `${s.from ? `${STATUS_LABEL[s.from]} → ` : ""}${STATUS_LABEL[s.to]}`, href: s.url, tone: s.to === "under_construction" ? "warn" : s.to === "operational" ? "ok" : s.to === "cancelled" || s.to === "delayed" ? "danger" : "neutral" });
134 + if (p.expectedOpening) timeline.push({ date: p.expectedOpening, title: <span className="text-ink-2">Expected opening</span>, description: p.phaseCount ? `phase 1 of ${fmtInt(p.phaseCount)} — expected, as published by the source` : "expected, as published by the source", tone: "neutral" });
135 + timeline.sort((a, b) => partialDateSortKey(a.date.slice(0, 10)) - partialDateSortKey(b.date.slice(0, 10)));
102 136
103 137 const sections = [
138 + { id: "lifecycle", label: "Lifecycle" },
139 + { id: "figures", label: "Figures & claims" },
104 140 { id: "timeline", label: "Timeline", count: timeline.length },
105 − { id: "status-history", label: "Status history", count: statusHistory.length },
106 − { id: "about", label: "About", count: p.description || p.sourceUrl ? 1 : 0 },
107 − { id: "location", label: "Location", count: hasGeo || place !== "—" ? 1 : 0 },
108 − { id: "provenance", label: "Provenance", count: p.provenance.length },
109 − { id: "changes", label: "Changes", count: p.events.length },
110 − { id: "history", label: "Source history", count: p.sourceHistory.length },
141 + { id: "about", label: "About", count: p.description || p.sourceUrl || p.facility || p.campus ? 1 : 0 },
142 + { id: "location", label: "Location" },
143 + { id: "nearby", label: "Nearby", count: hasGeo ? 1 : 0 },
144 + { id: "related", label: "Related events", count: p.relatedEvents.length + p.events.length },
145 + { id: "provenance", label: "Source evidence", count: p.provenance.length },
146 + { id: "quality", label: "Data quality" },
111 147 ];
112 148
113 149 const jsonLd = compactJsonLd({
@@ -119,163 +155,258 @@ export default async function ProjectPage({ params }: Props) {
119 155 sameAs: p.sourceUrl ?? undefined,
120 156 foundingDate: p.announcedOn ?? undefined,
121 157 parentOrganization: p.operator ? { "@type": "Organization", name: p.operator.name, url: absoluteUrl(routes.operator(p.operator.slug)) } : undefined,
122 − location: place !== "—" || hasGeo ? compactJsonLd({ "@type": "Place", name: place !== "—" ? place : undefined, address: p.countryIso2 ? { "@type": "PostalAddress", addressLocality: p.city ?? undefined, addressRegion: p.regionName ?? undefined, addressCountry: p.countryIso2 } : undefined, geo: hasGeo ? { "@type": "GeoCoordinates", latitude: p.lat, longitude: p.lng } : undefined }) : undefined,
158 + location: place !== "—" || hasGeo ? compactJsonLd({ "@type": "Place", name: place !== "—" ? place : undefined, address: p.countryIso2 ? { "@type": "PostalAddress", addressLocality: p.city ?? undefined, addressRegion: p.regionName ?? undefined, addressCountry: p.countryIso2 } : undefined, geo: hasGeo && precision !== "unknown" ? { "@type": "GeoCoordinates", latitude: p.lat, longitude: p.lng } : undefined }) : undefined,
159 + additionalProperty: [
160 + { "@type": "PropertyValue", name: "status", value: STATUS_LABEL[p.status] },
161 + p.plannedMw !== null && mwSite ? { "@type": "PropertyValue", name: "Planned capacity", value: p.plannedMw, unitText: "MW" } : null,
162 + { "@type": "PropertyValue", name: "geoPrecision", value: PRECISION_LABEL[precision] },
163 + ].filter(Boolean),
123 164 });
124 165
166 + const mwSplit = splitMw(p.plannedMw);
167 +
125 168 return (
126 − <Page>
127 − <Breadcrumbs items={[{ name: "Projects", href: routes.projects() }, { name: p.name, href: base }]} className="pt-3" />
128 − <JsonLd data={jsonLd} />
169 + <EvidenceProvider data={{ entityName: p.name, entityKind: "project", claims, provenance: p.provenance, history }}>
170 + <Page>
171 + <Breadcrumbs items={[{ name: "Projects", href: routes.projects() }, { name: p.name, href: base }]} className="pt-3" />
172 + <JsonLd data={jsonLd} />
129 173
130 − <header className="py-5 sm:py-7">
131 − <div className="label-caps mb-1.5">Project</div>
132 − <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{p.name}</h1>
133 − <div className="mt-2 flex flex-wrap items-center gap-1.5">
134 − <WithProvenance items={p.provenance} fields={["status"]}>
135 − <StatusBadge status={p.status} />
136 − </WithProvenance>
137 − <ConfidenceBadge level={p.confidence} short={false} />
138 − </div>
139 − <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-ink-2">
140 − {p.operator ? (
141 − <Link href={routes.operator(p.operator.slug)} className="font-medium text-ink hover:text-accent hover:underline">
142 − {p.operator.name}
143 − </Link>
144 − ) : (
145 − <span className="text-ink-3">Operator not published</span>
146 − )}
147 − {p.facility && (
148 − <>
149 − <span aria-hidden className="text-ink-4">
150 − ·
174 + <header className="py-5 sm:py-7">
175 + <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">
176 + <span>Project</span>
177 + {p.projectClass && PROJECT_CLASS_LABEL[p.projectClass] && (
178 + <>
179 + <span aria-hidden className="text-ink-4">·</span>
180 + <span>{PROJECT_CLASS_LABEL[p.projectClass]}</span>
181 + </>
182 + )}
183 + {p.operator && (
184 + <>
185 + <span aria-hidden className="text-ink-4">·</span>
186 + <Link href={routes.operator(p.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">
187 + {p.operator.name}
188 + </Link>
189 + </>
190 + )}
191 + </div>
192 + <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{p.name}</h1>
193 + <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-ink-2">
194 + {!p.operator && <span className="text-ink-3">Operator not published</span>}
195 + {p.developer && p.developer.id !== p.operator?.id && (
196 + <span>
197 + developed by{" "}
198 + <Link href={routes.operator(p.developer.slug)} className="text-ink hover:text-accent hover:underline">
199 + {p.developer.name}
200 + </Link>
151 201 </span>
152 − <Link href={routes.facility(p.facility.slug)} className="hover:text-ink hover:underline">
153 − Facility: {p.facility.name}
154 − </Link>
155 − </>
156 − )}
157 − {(place !== "—" || p.metro) && (
158 − <>
159 − <span aria-hidden className="text-ink-4">
160 − ·
202 + )}
203 + {p.tenant && (
204 + <span>
205 + for{" "}
206 + <Link href={routes.operator(p.tenant.slug)} className="text-ink hover:text-accent hover:underline">
207 + {p.tenant.name}
208 + </Link>
161 209 </span>
210 + )}
211 + {(place !== "—" || p.metro) && (
162 212 <span className="inline-flex items-center gap-1.5">
163 213 <CountryFlag iso2={p.countryIso2} size={12} />
164 − {p.metro ? (
214 + {p.metro && (
165 215 <Link href={routes.market(p.metro.slug)} className="hover:text-ink hover:underline">
166 − {p.metro.name}
216 + {p.metro.name} market
167 217 </Link>
168 − ) : null}
218 + )}
169 219 {p.metro && place !== "—" && <span className="text-ink-4">·</span>}
170 220 {place !== "—" && <span>{place}</span>}
171 221 </span>
172 − </>
173 − )}
174 − </div>
175 − </header>
222 + )}
223 + </div>
224 + <div className="mt-3 flex flex-wrap items-center gap-1.5">
225 + {p.isAi && <AiMark evidence={p.aiEvidence} size="sm" />}
226 + {p.evidenceLevel && (
227 + <Badge tone={p.evidenceLevel === "strong" ? "ok" : p.evidenceLevel === "weak" ? "warn" : "outline"} size="sm" title="Creation evidence: name or operator + location + development verb">
228 + {p.evidenceLevel} evidence
229 + </Badge>
230 + )}
231 + {p.phaseCount !== null && (
232 + <Badge tone="outline" size="sm">
233 + {fmtInt(p.phaseCount)} phases
234 + </Badge>
235 + )}
236 + </div>
237 + </header>
176 238
177 − <MetricRow cols={Math.min(6, 2 + [p.investmentUsd, p.announcedOn, p.acreage, p.phaseCount].filter((v) => v !== null).length)}>
178 − <MetricCell>
179 − <Metric
180 − value={
181 − p.plannedMw === null ? (
182 − "—"
183 − ) : (
184 − <WithProvenance items={p.provenance} fields={["plannedMw", "plannedPowerMw"]}>
185 − <EstimateMark className="mr-0.5 text-[0.6em]" />
186 − {fmtMw(p.plannedMw).split(" ")[0]}
187 − </WithProvenance>
188 − )
189 − }
190 − unit={p.plannedMw === null ? undefined : fmtMw(p.plannedMw).split(" ")[1]}
191 − label="Planned capacity"
192 − note={p.plannedMw === null ? "not published" : "as announced"}
193 − size="sm"
194 − />
195 − </MetricCell>
196 − {p.investmentUsd !== null && (
197 − <MetricCell>
198 − <Metric value={<WithProvenance items={p.provenance} fields={["investmentUsd"]}>{fmtUsd(p.investmentUsd)}</WithProvenance>} label="Investment" note="as announced" size="sm" />
199 − </MetricCell>
200 − )}
201 − {p.announcedOn && (
202 − <MetricCell>
203 − <Metric value={<WithProvenance items={p.provenance} fields={["announcedOn"]}>{fmtDate(p.announcedOn)}</WithProvenance>} label="Announced" size="sm" />
204 − </MetricCell>
205 − )}
206 − <MetricCell>
207 − <Metric value={p.expectedOpening ? <WithProvenance items={p.provenance} fields={["expectedOpening"]}>{fmtPartialDateSafe(p.expectedOpening)}</WithProvenance> : "—"} label="Expected opening" note={p.expectedOpening ? undefined : "not published"} size="sm" />
208 − </MetricCell>
209 − {p.acreage !== null && (
210 − <MetricCell>
211 − <Metric value={fmtInt(p.acreage)} unit="acres" label="Site" size="sm" />
212 − </MetricCell>
213 − )}
214 − {p.phaseCount !== null && (
215 − <MetricCell>
216 − <Metric value={fmtInt(p.phaseCount)} label="Phases" size="sm" />
217 − </MetricCell>
218 − )}
219 − </MetricRow>
239 + {/* header figures */}
240 + <div className="grid grid-cols-2 border-y border-hair [&>*]:min-w-0 max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*]:border-hair sm:grid-cols-3 sm:[&>*:nth-child(n+4)]:border-t sm:[&>*:nth-child(3n+1)]:pl-0 sm:[&>*:not(:nth-child(3n+1))]:border-l sm:[&>*]:border-hair lg:grid-cols-6 lg:[&>*:nth-child(n+4)]:border-t-0 lg:[&>*:nth-child(3n+1)]:pl-4 lg:[&>*:first-child]:pl-0 lg:[&>*+*]:border-l">
241 + <Figure label="Stage">
242 + <Evidence field="status" display={STATUS_LABEL[p.status]}>
243 + <StatusBadge status={p.status} />
244 + </Evidence>
245 + </Figure>
246 + <Figure label="Planned capacity">
247 + {p.plannedMw !== null && mwSite && mwSplit ? (
248 + <Evidence field="plannedMw" fields={["plannedPowerMw", "planned_power_mw", "phase_mw"]} display={`≈ ${fmtMw(p.plannedMw)}`} block>
249 + <div className="flex items-baseline gap-1 leading-none">
250 + <EstimateMark className="text-[14px]" />
251 + <span className="figure text-[22px] font-medium tracking-[-0.03em] text-ink sm:text-[26px]">{mwSplit.value}</span>
252 + <span className="text-[12px] font-medium text-ink-3">{mwSplit.unit}</span>
253 + </div>
254 + <div className="mt-1 flex flex-wrap items-center gap-1.5">
255 + <SemanticsLabel semantics={p.capacitySemantics ?? "planned_power_mw"} />
256 + {p.capacityScope && <ScopeBadge scope={p.capacityScope} />}
257 + </div>
258 + </Evidence>
259 + ) : (
260 + <Evidence field="plannedMw" fields={["plannedPowerMw", "planned_power_mw", "phase_mw"]} display="—" block>
261 + <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>
262 + <div className="mt-1 text-[11px] leading-tight text-ink-3">{p.plannedMw !== null ? `${fmtMw(p.plannedMw)} is a ${p.capacityScope}-level figure — not this site's (see claims)` : "Not published by any source"}</div>
263 + </Evidence>
264 + )}
265 + </Figure>
266 + <Figure label="Investment">
267 + {p.investmentUsd !== null && usdSite ? (
268 + <Evidence field="investmentUsd" fields={["investment_usd"]} display={fmtUsd(p.investmentUsd)} block>
269 + <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink sm:text-[26px]">{fmtUsd(p.investmentUsd)}</div>
270 + <div className="mt-1 flex flex-wrap items-center gap-1.5">
271 + <SemanticsLabel semantics={p.investmentSemantics ?? "project_investment_usd"} />
272 + {p.investmentScope && <ScopeBadge scope={p.investmentScope} />}
273 + {p.investmentCurrency && p.investmentCurrency !== "USD" && p.investmentOriginal && <span className="figure text-[11px] text-ink-3">{fmtInt(p.investmentOriginal)} {p.investmentCurrency}</span>}
274 + </div>
275 + </Evidence>
276 + ) : (
277 + <Evidence field="investmentUsd" fields={["investment_usd"]} display="—" block>
278 + <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>
279 + <div className="mt-1 text-[11px] leading-tight text-ink-3">{p.investmentUsd !== null ? `${fmtUsd(p.investmentUsd)} is a ${p.investmentScope}-level figure — not this site's (see claims)` : "Not disclosed"}</div>
280 + </Evidence>
281 + )}
282 + </Figure>
283 + <Figure label="Expected opening" note={p.expectedOpening ? "as published" : "not published"}>
284 + <Evidence field="expectedOpening" fields={["expected_opening"]} display={p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "—"} block>
285 + <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${p.expectedOpening ? "text-ink" : "text-ink-4"}`}>{p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "—"}</div>
286 + </Evidence>
287 + </Figure>
288 + <Figure label="Announced" note={p.announcedOn ? undefined : "date not published"}>
289 + <Evidence field="announcedOn" fields={["announced_on"]} display={p.announcedOn ? fmtDate(p.announcedOn) : "—"} block>
290 + <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${p.announcedOn ? "text-ink" : "text-ink-4"}`}>{p.announcedOn ? fmtDate(p.announcedOn) : "—"}</div>
291 + </Evidence>
292 + </Figure>
293 + <Figure label="Confidence" note={`${fmtInt(dq.sourceCount)} ${dq.sourceCount === 1 ? "source" : "sources"} · ${fmtInt(dq.primarySourceCount)} primary`}>
294 + <ConfidenceBadge level={p.confidence} short={false} />
295 + </Figure>
296 + </div>
297 + <p className="mt-2 text-[11px] text-ink-4">Planned MW and investment are published figures (≈ never imputed). A company-wide or portfolio figure is never shown as this project&apos;s own; it is listed under “Figures not assigned to this site”. Click a figure for its evidence.</p>
220 298
221 − <SectionNav items={sections} className="mt-2" />
299 + <SectionNav items={sections} className="mt-4" />
222 300
223 − <div className="flex flex-col gap-8 pt-6">
224 − <Section id="timeline" title="Construction timeline" description="Dated milestones from the sources indexed; partial dates (year, quarter) are shown as published." className="scroll-mt-28">
225 − {timeline.length ? <TimelineList items={timeline} /> : <p className="text-[12.5px] text-ink-3">No dated milestone has been published for this project yet.</p>}
226 − </Section>
301 + <div className="flex flex-col gap-9 pt-6">
302 + <Section id="lifecycle" title="Lifecycle" className="scroll-mt-28" description="Rumored → proposed → announced → permitting → approved → construction → opened. Dates come from the announcement that moved the stage; delayed / cancelled are side branches.">
303 + <ProjectStages stages={p.stages} />
304 + <div className="mt-6">
305 + <div className="label-caps mb-2">Velocity</div>
306 + <ProjectVelocity v={p.velocityDays} />
307 + </div>
308 + </Section>
227 309
228 − {statusHistory.length > 0 && (
229 − <Section id="status-history" title="Status history" description="Detected status transitions." className="scroll-mt-28">
230 − <TimelineList items={statusHistory} dense />
310 + <Section id="figures" title="Figures & claims" className="scroll-mt-28" description="Every capacity and investment claim extracted for this project with its scope. Only site-scoped claims can become the project's figures.">
311 + <div className="flex flex-col gap-5">
312 + <div>
313 + <div className="label-caps mb-2">Capacity claims</div>
314 + <ClaimsTable claims={claims} kind="capacity" emptyNote={p.plannedMw !== null ? "No sentence-level capacity claim stored yet — the planned figure comes from the source fields listed under Source evidence." : "No source has published a capacity figure for this project."} />
315 + </div>
316 + <div>
317 + <div className="label-caps mb-2">Investment claims</div>
318 + <ClaimsTable claims={claims} kind="investment" emptyNote={p.investmentUsd !== null ? "No sentence-level investment claim stored yet — the figure comes from the source fields listed under Source evidence." : "No investment figure disclosed."} />
319 + </div>
320 + {((p.plannedMw !== null && !mwSite) || (p.investmentUsd !== null && !usdSite)) && (
321 + <div>
322 + <div className="mb-1.5 flex flex-wrap items-baseline gap-x-2">
323 + <h3 className="text-[13px] font-semibold text-ink">Figures not assigned to this site</h3>
324 + <span className="text-[11.5px] text-ink-3">scope is wider than the project — shown as context only</span>
325 + </div>
326 + <KeyValueList
327 + items={[
328 + { label: "Planned MW", value: p.plannedMw !== null && !mwSite ? <span className="inline-flex flex-wrap items-center gap-1.5"><span className="figure">≈ {fmtMw(p.plannedMw)}</span><ScopeBadge scope={p.capacityScope} /><SemanticsLabel semantics={p.capacitySemantics} /></span> : null },
329 + { label: "Investment", value: p.investmentUsd !== null && !usdSite ? <span className="inline-flex flex-wrap items-center gap-1.5"><span className="figure">{fmtUsd(p.investmentUsd)}</span><ScopeBadge scope={p.investmentScope} /><SemanticsLabel semantics={p.investmentSemantics} /></span> : null },
330 + ]}
331 + />
332 + </div>
333 + )}
334 + </div>
231 335 </Section>
232 − )}
233 336
234 − {(p.description || p.sourceUrl) && (
235 − <Section id="about" title="About" className="scroll-mt-28">
236 − {p.description && <p className="max-w-3xl text-[14px] leading-relaxed text-ink-2">{p.description}</p>}
237 − {p.sourceUrl && (
238 − <p className="mt-3 text-[12.5px] text-ink-3">
239 − Primary source: <ExtLink href={p.sourceUrl} />
240 − </p>
241 − )}
337 + <Section id="timeline" title="Timeline" className="scroll-mt-28" description="Dated milestones and status transitions from the sources indexed; partial dates (year, quarter) are shown as published.">
338 + {timeline.length ? <TimelineList items={timeline} /> : <p className="text-[12.5px] text-ink-3">No dated milestone has been published for this project yet.</p>}
242 339 </Section>
243 − )}
244 340
245 − {(hasGeo || place !== "—") && (
246 − <Section id="location" title="Location" description={hasGeo ? "Approximate site location as published; the ring marks the uncertainty." : "Location as published; no coordinates yet."} className="scroll-mt-28">
247 − {hasGeo ? <EntityMap center={{ lat: p.lat!, lng: p.lng! }} zoom={10} precision="approximate" filters={p.countryIso2 ? { country: p.countryIso2 } : undefined} className="h-[220px] sm:h-[300px]" /> : null}
341 + <Section id="about" title="About" className="scroll-mt-28">
342 + {p.description ? <p className="max-w-3xl text-[14px] leading-relaxed text-ink-2">{p.description}</p> : <p className="text-[12.5px] text-ink-3">No description published.</p>}
248 343 <KeyValueList
249 − className={hasGeo ? "mt-3" : undefined}
344 + className="mt-3"
345 + columns={2}
250 346 items={[
251 − { label: "City", value: p.city },
252 − { label: "Region", value: p.regionName },
253 − { label: "Country", value: p.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={p.countryIso2} size={12} />{countryName(p.countryIso2)}</span> : null },
254 − { label: "Market", value: p.metro ? <Link href={routes.market(p.metro.slug)} className="hover:text-accent hover:underline">{p.metro.name}</Link> : null },
255 − { label: "Coordinates", value: hasGeo ? <span className="figure">{p.lat!.toFixed(4)}, {p.lng!.toFixed(4)}</span> : null },
347 + { label: "Operator", value: p.operator ? <Link href={routes.operator(p.operator.slug)} className="hover:text-accent hover:underline">{p.operator.name}</Link> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
348 + { label: "Developer", value: p.developer ? <Link href={routes.operator(p.developer.slug)} className="hover:text-accent hover:underline">{p.developer.name}</Link> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
349 + { label: "Tenant", value: p.tenant ? <Link href={routes.operator(p.tenant.slug)} className="hover:text-accent hover:underline">{p.tenant.name}</Link> : null },
350 + { label: "Facility", value: p.facility ? <Link href={routes.facility(p.facility.slug)} className="hover:text-accent hover:underline">{p.facility.name}</Link> : null },
351 + { label: "Campus", value: p.campus ? <Link href={routes.facility(p.campus.slug)} className="hover:text-accent hover:underline">{p.campus.name}</Link> : null },
352 + { label: "Site", value: p.acreage !== null ? <span className="figure">{fmtInt(p.acreage)} acres</span> : null },
353 + { label: "Primary source", value: p.sourceUrl ? <ExtLink href={p.sourceUrl} /> : null },
354 + { label: "Last update", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(p.lastUpdate)}</span> },
256 355 ]}
257 356 />
258 357 </Section>
259 − )}
260 358
261 − <Section id="provenance" title="Provenance" description="Every field with its source, observation dates, confidence and extraction method." className="scroll-mt-28">
262 − <ProvenanceTable items={p.provenance} />
263 − </Section>
359 + <Section id="location" title="Location" className="scroll-mt-28" description={hasGeo ? `${PRECISION_LABEL[precision]} — the ring marks the area published by the sources, not a parcel.` : "Location as published; no coordinates yet."} aside={<PrecisionBadge precision={precision} short={false} size="xs" />}>
360 + <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
361 + <KeyValueList
362 + items={[
363 + { label: "City", value: p.city },
364 + { label: "Region", value: p.regionName },
365 + { label: "Country", value: p.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={p.countryIso2} size={12} />{countryName(p.countryIso2)}</span> : null },
366 + { label: "Market", value: p.metro ? <Link href={routes.market(p.metro.slug)} className="hover:text-accent hover:underline">{p.metro.name}</Link> : null },
367 + { label: "Coordinates", value: hasGeo ? <Evidence field="lat" fields={["lng", "geo", "coordinates", "geoPrecision"]}><span className="figure">{p.lat!.toFixed(precision === "exact" || precision === "parcel" ? 4 : 2)}, {p.lng!.toFixed(precision === "exact" || precision === "parcel" ? 4 : 2)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },
368 + { label: "Precision", value: <WithProvenance items={p.provenance} fields={["geoPrecision", "geo_precision"]}><PrecisionBadge precision={precision} short={false} size="xs" /></WithProvenance>, keepEmpty: true },
369 + ]}
370 + />
371 + {hasGeo ? <EntityMap center={{ lat: p.lat!, lng: p.lng! }} precision={precision} filters={p.countryIso2 ? { country: p.countryIso2 } : undefined} className="h-[220px] sm:h-[280px]" /> : <div className="flex min-h-[140px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>}
372 + </div>
373 + </Section>
264 374
265 − {p.events.length > 0 && (
266 − <Section id="changes" title="Changes" description="Detected events on this project." className="scroll-mt-28">
267 − <EventList events={p.events} dense />
375 + <Section id="nearby" title="Nearby infrastructure" className="scroll-mt-28" description="Great-circle distance from the published (often city-level) point. Proximity never implies a connection.">
376 + {hasGeo ? <NearbyInfrastructurePanel initial={p.nearbyInfrastructure} center={{ lat: p.lat!, lng: p.lng! }} /> : <p className="rounded-sm border border-dashed border-hair-2 px-3 py-2.5 text-[12.5px] text-ink-3">Nearby infrastructure needs a published location for this project.</p>}
268 377 </Section>
269 − )}
270 378
271 − {p.sourceHistory.length > 0 && (
272 − <Section id="history" title="Source history" description="When each source was observed, changed or verified." className="scroll-mt-28">
273 − <SourceHistory items={p.sourceHistory} />
379 + <Section id="related" title="Related events" className="scroll-mt-28" description="Events attached to this project — announcements, power agreements, financing, status changes — deduplicated across sources.">
380 + <EventList events={[...p.relatedEvents, ...p.events.filter((e) => !p.relatedEvents.some((x) => x.id === e.id))]} dense emptyTitle="No related event yet" emptyDescription="Announcements, agreements and status changes will list here as connectors detect them." />
274 381 </Section>
275 − )}
276 − </div>
277 382
278 − <SourcesFooter sources={r.sources} updatedAt={p.lastUpdate} />
279 − </Page>
383 + <Section id="provenance" title="Source evidence" className="scroll-mt-28" description="Every field with its source, observation dates, confidence and extraction method. “Shown” marks the winner backing the displayed value.">
384 + <ProvenanceTable items={p.provenance} />
385 + {p.sourceHistory.length > 0 && (
386 + <details className="mt-4">
387 + <summary className="cursor-pointer text-[12.5px] font-medium text-ink-2 hover:text-ink">
388 + Source observation log <span className="figure text-ink-3">{fmtInt(p.sourceHistory.length)}</span>
389 + </summary>
390 + <div className="mt-3">
391 + <SourceHistory items={p.sourceHistory} />
392 + </div>
393 + </details>
394 + )}
395 + <p className="mt-2 text-[11.5px] text-ink-4">
396 + {CONFIDENCE_LABEL[p.confidence]} overall.{" "}
397 + <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">
398 + How confidence is computed
399 + </Link>
400 + </p>
401 + </Section>
402 +
403 + <Section id="quality" title="Data quality" className="scroll-mt-28">
404 + <DataQualityPanel q={dq} />
405 + </Section>
406 + </div>
407 +
408 + <SourcesFooter sources={r.sources} updatedAt={p.lastUpdate} />
409 + </Page>
410 + </EvidenceProvider>
280 411 );
281 412 }
modified apps/web/src/app/(site)/projects/filters.tsx +45 −7
@@ -8,10 +8,11 @@ import { useState } from "react";
8 8 import { Button } from "@/components/ui/button";
9 9 import { ToggleChip } from "@/components/ui/filter-chips";
10 10 import { Sheet } from "@/components/ui/sheet";
11 +import { StatusDot } from "@/components/ui/status-dot";
11 12 import { fmtInt } from "@/lib/format";
12 13 import { STATUS_SHORT } from "@/lib/labels";
13 14 import { routes } from "@/lib/routes";
14 −import { PROJECT_SORTS, projectQueryString, type ProjectQuery } from "./query";
15 +import { PROJECT_CLASS_OPTIONS, PROJECT_SORTS, projectQueryString, type ProjectQuery } from "./query";
15 16
16 17 const STATUSES: FacilityStatus[] = [...PIPELINE_STATUSES, "cancelled"];
17 18
@@ -68,6 +69,40 @@ function Fields({ draft, setDraft, countries }: { draft: ProjectQuery; setDraft:
68 69 </select>
69 70 </label>
70 71 </fieldset>
72 + <fieldset className="py-3">
73 + <legend className="label-caps mb-2">Class</legend>
74 + <div className="flex flex-wrap gap-1.5">
75 + {PROJECT_CLASS_OPTIONS.map((c) => {
76 + const cur = new Set((draft.project_class ?? "").split(",").filter(Boolean));
77 + const on = cur.has(c.v);
78 + return (
79 + <ToggleChip
80 + key={c.v}
81 + active={on}
82 + onClick={() => {
83 + const next = new Set(cur);
84 + if (on) next.delete(c.v);
85 + else next.add(c.v);
86 + setDraft({ ...draft, project_class: next.size ? Array.from(next).join(",") : undefined });
87 + }}
88 + >
89 + {c.label}
90 + </ToggleChip>
91 + );
92 + })}
93 + </div>
94 + <p className="mt-1.5 text-[11px] text-ink-4">Physical classes only — power agreements, financing and partnerships are events, never projects.</p>
95 + </fieldset>
96 + <fieldset className="grid gap-3 py-3 sm:grid-cols-2">
97 + <label className="flex flex-col gap-1 text-[12px] text-ink-3">
98 + Expected opening from
99 + <input className={field} type="number" inputMode="numeric" min={2000} max={2045} placeholder="2026" value={draft.expected_from ?? ""} onChange={(e) => setDraft({ ...draft, expected_from: e.target.value ? Number(e.target.value) : undefined })} />
100 + </label>
101 + <label className="flex flex-col gap-1 text-[12px] text-ink-3">
102 + Expected opening to
103 + <input className={field} type="number" inputMode="numeric" min={2000} max={2045} placeholder="2030" value={draft.expected_to ?? ""} onChange={(e) => setDraft({ ...draft, expected_to: e.target.value ? Number(e.target.value) : undefined })} />
104 + </label>
105 + </fieldset>
71 106 <fieldset className="py-3">
72 107 <legend className="label-caps mb-2">Flags</legend>
73 108 <ToggleChip active={!!draft.ai} onClick={() => setDraft({ ...draft, ai: draft.ai ? undefined : true })}>
@@ -88,7 +123,7 @@ export function ProjectFilters({ query, total, countries }: { query: ProjectQuer
88 123 setSynced(query);
89 124 setDraft(query);
90 125 }
91 − const active = [query.status, query.country, query.operator, query.min_mw, query.ai, query.q].filter(Boolean).length;
126 + const active = [query.status, query.country, query.operator, query.min_mw, query.ai, query.q, query.project_class, query.expected_from, query.expected_to].filter(Boolean).length;
92 127 const apply = (q: ProjectQuery) => {
93 128 router.replace(routes.projects(projectQueryString({ ...q, page: undefined })), { scroll: false });
94 129 setOpen(false);
@@ -107,14 +142,17 @@ export function ProjectFilters({ query, total, countries }: { query: ProjectQuer
107 142 <SlidersHorizontal className="size-3.5" strokeWidth={1.75} /> Filters
108 143 {active > 0 && <span className="figure ml-0.5 rounded-xs bg-paper/20 px-1 text-[11px]">{active}</span>}
109 144 </Button>
110 − <div className="-mx-1 hidden gap-1 overflow-x-auto px-1 no-scrollbar md:flex">
111 − {(["announced", "permitting", "approved", "under_construction", "delayed"] as FacilityStatus[]).map((s) => (
112 − <ToggleChip key={s} active={statuses.has(s)} onClick={() => quickToggle(s)} className="h-7 text-[12px]">
145 + <div className="-mx-1 hidden min-w-0 flex-1 gap-1 overflow-x-auto px-1 no-scrollbar md:flex">
146 + {STATUSES.map((s) => (
147 + <ToggleChip key={s} active={statuses.has(s)} onClick={() => quickToggle(s)} className="h-7 text-[12px]" dot={<StatusDot status={s} size={6} />}>
113 148 {STATUS_SHORT[s]}
114 149 </ToggleChip>
115 150 ))}
151 + <ToggleChip active={!!query.ai} onClick={() => apply({ ...query, ai: query.ai ? undefined : true })} className="h-7 text-[12px]">
152 + AI
153 + </ToggleChip>
116 154 </div>
117 − <label className="ml-auto hidden items-center gap-1.5 text-[12px] text-ink-3 md:inline-flex">
155 + <label className="ml-auto hidden shrink-0 items-center gap-1.5 text-[12px] text-ink-3 md:inline-flex">
118 156 Sort
119 157 <select className="h-7 rounded-sm border border-hair-2 bg-surface px-1.5 text-[12px] text-ink outline-none focus-visible:ring-2 focus-visible:ring-accent" value={query.sort ?? "updated"} onChange={(e) => apply({ ...query, sort: e.target.value as ProjectQuery["sort"] })}>
120 158 {PROJECT_SORTS.map((s) => (
@@ -136,7 +174,7 @@ export function ProjectFilters({ query, total, countries }: { query: ProjectQuer
136 174 heightVh={84}
137 175 footer={
138 176 <div className="flex items-center justify-between gap-2">
139 − <Button variant="ghost" onClick={() => setDraft({})}>
177 + <Button variant="ghost" onClick={() => setDraft({ view: draft.view })}>
140 178 Reset
141 179 </Button>
142 180 <Button variant="primary" onClick={() => apply(draft)}>
added apps/web/src/app/(site)/projects/projects-map.tsx +50 −0
@@ -0,0 +1,50 @@
1 +"use client";
2 +
3 +import type { MapPoint, MapResponse, ProjectSummary } from "@dci/core";
4 +import dynamic from "next/dynamic";
5 +import Link from "next/link";
6 +import { useCallback, useState } from "react";
7 +import { StatusBadge } from "@/components/ui/badge";
8 +import { Button } from "@/components/ui/button";
9 +import { Skeleton } from "@/components/ui/skeleton";
10 +import { fmtInt, fmtMw } from "@/lib/format";
11 +import { PRECISION_LABEL } from "@/lib/labels";
12 +import { routes } from "@/lib/routes";
13 +
14 +const MapCanvas = dynamic(() => import("@/components/map/map-canvas").then((m) => m.MapCanvas), { ssr: false, loading: () => <Skeleton className="h-full w-full rounded-none" /> });
15 +
16 +/**
17 + * Projects MAP view: the shared canvas in `layer=projects` mode (square stage-coloured markers, hollow ring for
18 + * city-level points). Projects without published coordinates cannot be drawn — the caption says how many are missing
19 + * instead of guessing a location.
20 + */
21 +export function ProjectsMapView({ items, total, projectStatus }: { items: ProjectSummary[]; total: number; projectStatus?: string }) {
22 + const [data, setData] = useState<MapResponse | null>(null);
23 + const [sel, setSel] = useState<MapPoint | null>(null);
24 + const onData = useCallback((r: MapResponse | null) => setData(r), []);
25 + const located = items.filter((p) => p.lat !== null && p.lng !== null).length;
26 + return (
27 + <div className="flex flex-col gap-2">
28 + <div className="relative h-[52vh] min-h-[320px] w-full overflow-hidden rounded-sm border border-hair bg-surface-2">
29 + <MapCanvas layer="projects" extraQuery={{ project_status: projectStatus }} onData={onData} onSelect={setSel} className="h-full w-full" />
30 + {sel && (
31 + <div className="absolute bottom-3 left-3 right-3 z-[5] flex flex-wrap items-center gap-2 rounded-sm border border-hair bg-surface/95 px-3 py-2 text-[12.5px] backdrop-blur sm:right-auto sm:max-w-[420px]">
32 + <StatusBadge status={sel.s} short size="xs" />
33 + <span className="min-w-0 flex-1 truncate font-medium text-ink">{sel.n}</span>
34 + <span className="figure text-ink-2">{sel.mw != null ? `≈ ${fmtMw(sel.mw)}` : "—"}</span>
35 + <span className="text-[11px] text-ink-3">{PRECISION_LABEL[sel.p]}</span>
36 + <Button href={sel.k === "project" ? routes.project(sel.slug) : routes.facility(sel.slug)} size="xs" variant="primary">
37 + Open
38 + </Button>
39 + </div>
40 + )}
41 + </div>
42 + <p className="text-[11.5px] text-ink-3">
43 + {data ? <>{fmtInt(data.total)} located {data.total === 1 ? "project" : "projects"} drawn</> : "Loading…"} · {fmtInt(located)} of the {fmtInt(items.length)} projects on this page have published coordinates{total > items.length ? ` (${fmtInt(total)} match in total)` : ""}. Square markers are coloured by stage; a hollow ring means a city-level location, never a site.{" "}
44 + <Link href={routes.map("layer=projects")} className="underline decoration-hair-2 hover:text-ink">
45 + Open in the full map
46 + </Link>
47 + </p>
48 + </div>
49 + );
50 +}
modified apps/web/src/app/(site)/projects/query.ts +22 −0
@@ -1,10 +1,18 @@
1 1 /** URL state of the project tracker — shared by the server page and the client filter bar (no "use client" here). */
2 +export const PROJECT_VIEWS = ["table", "map", "timeline", "kanban"] as const;
3 +export type ProjectView = (typeof PROJECT_VIEWS)[number];
4 +
2 5 export interface ProjectQuery {
6 + view?: ProjectView;
3 7 status?: string;
4 8 country?: string;
5 9 operator?: string;
6 10 min_mw?: number;
7 11 ai?: boolean;
12 + /** project class (NEW_BUILD, EXPANSION, …), comma list */
13 + project_class?: string;
14 + expected_from?: number;
15 + expected_to?: number;
8 16 q?: string;
9 17 sort?: "updated" | "mw" | "announced" | "opening";
10 18 order?: "asc" | "desc";
@@ -18,14 +26,28 @@ export const PROJECT_SORTS: Array<{ v: NonNullable<ProjectQuery["sort"]>; label:
18 26 { v: "opening", label: "Expected opening" },
19 27 ];
20 28
29 +/** Physical project classes (may create or move a project); associated classes are events, never projects. */
30 +export const PROJECT_CLASS_OPTIONS: Array<{ v: string; label: string }> = [
31 + { v: "NEW_BUILD", label: "New build" },
32 + { v: "EXPANSION", label: "Expansion" },
33 + { v: "CONSTRUCTION_START", label: "Construction start" },
34 + { v: "PERMIT", label: "Permit" },
35 + { v: "LAND_ACQUISITION", label: "Land acquisition" },
36 + { v: "GRID_CONNECTION", label: "Grid connection" },
37 +];
38 +
21 39 export function projectQueryString(q: ProjectQuery): string {
22 40 const p = new URLSearchParams();
41 + if (q.view && q.view !== "table") p.set("view", q.view);
23 42 if (q.q) p.set("q", q.q);
24 43 if (q.status) p.set("status", q.status);
25 44 if (q.country) p.set("country", q.country);
26 45 if (q.operator) p.set("operator", q.operator);
27 46 if (q.min_mw) p.set("min_mw", String(q.min_mw));
28 47 if (q.ai) p.set("ai", "1");
48 + if (q.project_class) p.set("class", q.project_class);
49 + if (q.expected_from) p.set("expected_from", String(q.expected_from));
50 + if (q.expected_to) p.set("expected_to", String(q.expected_to));
29 51 if (q.sort && q.sort !== "updated") p.set("sort", q.sort);
30 52 if (q.order) p.set("order", q.order);
31 53 if (q.page && q.page > 1) p.set("page", String(q.page));
added apps/web/src/app/(site)/projects/views.tsx +133 −0
@@ -0,0 +1,133 @@
1 +import type { FacilityStatus, ProjectSummary } from "@dci/core";
2 +import { partialDateSortKey } from "@dci/core";
3 +import Link from "next/link";
4 +import { AiMark, EstimateMark, PrecisionBadge, ScopeBadge, StatusBadge } from "@/components/ui/badge";
5 +import { cn } from "@/components/ui/cn";
6 +import { CountryFlag } from "@/components/ui/country-flag";
7 +import { EmptyState } from "@/components/ui/states";
8 +import { StatusDot } from "@/components/ui/status-dot";
9 +import { fmtInt, fmtMw, fmtUsd, placeLine } from "@/lib/format";
10 +import { fmtPartialDateSafe } from "@/lib/format-project";
11 +import { STATUS_SHORT, isSiteScopeLabel } from "@/lib/labels";
12 +import { routes } from "@/lib/routes";
13 +
14 +/* ------------------------------------------------------------------ shared dense card */
15 +
16 +function mwCell(p: ProjectSummary) {
17 + if (p.plannedMw === null) return <span className="text-ink-4">—</span>;
18 + const site = p.capacityScope === null || p.capacityScope === undefined || isSiteScopeLabel(p.capacityScope);
19 + if (!site) return <span className="text-ink-4" title={`${fmtMw(p.plannedMw)} is a ${p.capacityScope}-level figure, not this site's`}>— <ScopeBadge scope={p.capacityScope} className="align-middle" /></span>;
20 + return (
21 + <span>
22 + <EstimateMark className="mr-0.5" />
23 + {fmtMw(p.plannedMw)}
24 + </span>
25 + );
26 +}
27 +
28 +export function ProjectCard({ p, showStatus = true, className }: { p: ProjectSummary; showStatus?: boolean; className?: string }) {
29 + return (
30 + <article className={cn("relative rounded-sm border border-hair bg-surface px-2.5 py-2 text-[12.5px]", className)}>
31 + <div className="flex items-start justify-between gap-2">
32 + <Link href={routes.project(p.slug)} className="min-w-0 font-medium leading-snug text-ink hover:text-accent after:absolute after:inset-0 after:content-['']">
33 + <span className="line-clamp-2">{p.name}</span>
34 + </Link>
35 + <span className="figure shrink-0 text-[13px] font-medium text-ink">{mwCell(p)}</span>
36 + </div>
37 + <div className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-[11.5px] text-ink-3">
38 + {p.operator ? <span className="truncate text-ink-2">{p.operator.name}</span> : <span>Operator unknown</span>}
39 + <span aria-hidden>·</span>
40 + <CountryFlag iso2={p.countryIso2} size={11} />
41 + <span className="truncate">{p.metro?.name ?? placeLine(p)}</span>
42 + </div>
43 + <div className="mt-1.5 flex flex-wrap items-center gap-1">
44 + {showStatus && <StatusBadge status={p.status} short size="xs" />}
45 + {p.isAi && <AiMark evidence={p.aiEvidence} />}
46 + {p.geoPrecision && p.geoPrecision !== "unknown" && p.geoPrecision !== "exact" && <PrecisionBadge precision={p.geoPrecision} size="xs" />}
47 + <span className="figure ml-auto text-[11px] text-ink-3" title="Expected opening">
48 + {p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "TBD"}
49 + </span>
50 + </div>
51 + </article>
52 + );
53 +}
54 +
55 +/* ------------------------------------------------------------------ TIMELINE */
56 +
57 +/** Projects grouped by expected opening year (undated last), each year with its published-MW sum. */
58 +export function ProjectsTimeline({ items }: { items: ProjectSummary[] }) {
59 + if (!items.length) return <EmptyState compact title="No projects match these filters" />;
60 + const groups = new Map<string, ProjectSummary[]>();
61 + for (const p of items) {
62 + const k = p.expectedOpening ? p.expectedOpening.slice(0, 4) : "TBD";
63 + if (!groups.has(k)) groups.set(k, []);
64 + groups.get(k)!.push(p);
65 + }
66 + const keys = Array.from(groups.keys()).sort((a, b) => (a === "TBD" ? 1 : b === "TBD" ? -1 : a.localeCompare(b)));
67 + return (
68 + <ol className="relative ml-2 border-l border-hair">
69 + {keys.map((k) => {
70 + const list = groups.get(k)!.sort((a, b) => partialDateSortKey((a.expectedOpening ?? "9999").slice(0, 10)) - partialDateSortKey((b.expectedOpening ?? "9999").slice(0, 10)));
71 + const mw = list.filter((p) => p.plannedMw !== null && (p.capacityScope == null || isSiteScopeLabel(p.capacityScope))).reduce((a, p) => a + (p.plannedMw ?? 0), 0);
72 + const withMw = list.filter((p) => p.plannedMw !== null).length;
73 + return (
74 + <li key={k} className="relative pb-6 pl-5 last:pb-0">
75 + <span aria-hidden className={cn("absolute -left-[5px] top-[7px] size-[9px] rounded-full ring-2 ring-paper", k === "TBD" ? "border border-hair-2 bg-paper" : "bg-ink")} />
76 + <div className="mb-2 flex flex-wrap items-baseline gap-x-3 gap-y-0.5">
77 + <h3 className="figure text-[16px] font-semibold text-ink">{k === "TBD" ? "Expected opening not published" : k}</h3>
78 + <span className="figure text-[12px] text-ink-3">
79 + {fmtInt(list.length)} {list.length === 1 ? "project" : "projects"}
80 + {mw > 0 && <> · ≈ {fmtMw(mw)} published ({fmtInt(withMw)} of {fmtInt(list.length)} with a figure)</>}
81 + </span>
82 + </div>
83 + <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
84 + {list.map((p) => (
85 + <ProjectCard key={p.id} p={p} />
86 + ))}
87 + </div>
88 + </li>
89 + );
90 + })}
91 + </ol>
92 + );
93 +}
94 +
95 +/* ------------------------------------------------------------------ KANBAN */
96 +
97 +const KANBAN: FacilityStatus[] = ["rumored", "proposed", "announced", "permitting", "approved", "under_construction", "partially_operational", "delayed", "cancelled"];
98 +
99 +export function ProjectsKanban({ items, counts }: { items: ProjectSummary[]; counts?: Partial<Record<FacilityStatus, number>> }) {
100 + if (!items.length) return <EmptyState compact title="No projects match these filters" />;
101 + const cols = KANBAN.map((s) => ({ s, list: items.filter((p) => p.status === s || (s === "announced" && p.status === "expansion")) })).filter((c) => c.list.length || (counts?.[c.s] ?? 0) > 0);
102 + return (
103 + <div className="-mx-3 overflow-x-auto px-3 pb-2 no-scrollbar sm:mx-0 sm:px-0">
104 + <ol className="flex min-w-max gap-3" aria-label="Projects by stage">
105 + {cols.map((c) => {
106 + const mw = c.list.filter((p) => p.plannedMw !== null && (p.capacityScope == null || isSiteScopeLabel(p.capacityScope))).reduce((a, p) => a + (p.plannedMw ?? 0), 0);
107 + return (
108 + <li key={c.s} className="w-[260px] shrink-0 sm:w-[280px]">
109 + <div className="mb-2 flex items-baseline gap-2 border-b-2 pb-1.5" style={{ borderColor: `var(--st-${c.s === "under_construction" ? "construction" : c.s === "announced" || c.s === "proposed" ? "planned" : c.s === "approved" || c.s === "permitting" ? "permitting" : c.s === "partially_operational" ? "operational" : c.s === "delayed" ? "delayed" : c.s === "cancelled" ? "closed" : "rumored"})` }}>
110 + <StatusDot status={c.s} size={7} />
111 + <h3 className="text-[12.5px] font-semibold uppercase tracking-[0.06em] text-ink">{STATUS_SHORT[c.s]}</h3>
112 + <span className="figure text-[11.5px] text-ink-3">
113 + {fmtInt(counts?.[c.s] ?? c.list.length)}
114 + {mw > 0 && ` · ≈ ${fmtMw(mw)}`}
115 + </span>
116 + </div>
117 + <div className="flex flex-col gap-2">
118 + {c.list.length ? c.list.map((p) => <ProjectCard key={p.id} p={p} showStatus={false} />) : <p className="rounded-sm border border-dashed border-hair-2 px-2.5 py-3 text-[12px] text-ink-4">None on this page — {fmtInt(counts?.[c.s] ?? 0)} in total.</p>}
119 + </div>
120 + </li>
121 + );
122 + })}
123 + </ol>
124 + </div>
125 + );
126 +}
127 +
128 +/* ------------------------------------------------------------------ helper used by the map view list */
129 +export function investmentCell(p: ProjectSummary) {
130 + if (p.investmentUsd === null) return "—";
131 + const site = p.investmentScope == null || isSiteScopeLabel(p.investmentScope);
132 + return site ? fmtUsd(p.investmentUsd) : "—";
133 +}
added apps/web/src/app/(site)/pulse/loading.tsx +5 −0
@@ -0,0 +1,5 @@
1 +import { IndexLoading } from "@/components/intelligence/index-loading";
2 +
3 +export default function Loading() {
4 + return <IndexLoading />;
5 +}
added apps/web/src/app/(site)/pulse/page.tsx +211 −0
@@ -0,0 +1,211 @@
1 +import type { Pulse } from "@dci/core";
2 +import type { Metadata } from "next";
3 +import Link from "next/link";
4 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
5 +import { SectionNav } from "@/components/entity/section-nav";
6 +import { SourcesFooter } from "@/components/entity/sources-footer";
7 +import { EventList } from "@/components/event-list";
8 +import { ApiNote, CounterGrid, HonestEmpty, type CounterItem } from "@/components/intelligence/shared";
9 +import { Page, PageHeader, Section } from "@/components/ui/card";
10 +import { CountryFlag } from "@/components/ui/country-flag";
11 +import { SegmentedControl } from "@/components/ui/segmented";
12 +import { ErrorState, OfflineNotice } from "@/components/ui/states";
13 +import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
14 +import { api } from "@/lib/api";
15 +import { countryName, fmtDateTimeUtc, fmtInt } from "@/lib/format";
16 +import { routes } from "@/lib/routes";
17 +import { pageMetadata } from "@/lib/seo";
18 +
19 +export const revalidate = 60;
20 +
21 +type Window = Pulse["window"];
22 +const 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 +];
27 +const isWindow = (v: unknown): v is Window => v === "24h" || v === "7d" || v === "30d";
28 +
29 +export 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 +});
34 +
35 +export 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);
41 +
42 + const header = (
43 + <PageHeader
44 + 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 + );
50 +
51 + 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;
63 +
64 + 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 + ];
79 +
80 + 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 + ];
87 +
88 + 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>
95 +
96 + <SectionNav items={sections} />
97 +
98 + <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>
103 +
104 + <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>
135 +
136 + <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>
139 +
140 + <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>
175 +
176 + <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>
207 +
208 + <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 +}
modified apps/web/src/app/(site)/rankings/(index)/page.tsx +4 −0
@@ -83,6 +83,10 @@ export default async function RankingsPage() {
83 83 {!ok && <OfflineNotice detail="rankings unavailable" className="mb-3" />}
84 84 {ok && <SectionNav items={groups.map((g) => ({ id: g.scope, label: SCOPE_LABEL[g.scope], count: g.items.length }))} className="mb-4" />}
85 85 {!ok && <EmptyState title="Rankings unavailable" description="The index API did not return any ranking. They are recomputed by the worker after each crawl." />}
86 + <p className="mb-6 rounded-sm border border-dashed border-hair-2 px-3 py-2 text-[12.5px] leading-relaxed text-ink-2">
87 + <span className="label-caps mr-1.5">Method</span>
88 + Known / published figures only — every MW value is the sum of figures a source published or filed (containment-aware: a campus and its buildings are never both counted); entities without a figure are absent, not estimated. MW-based rankings apply a coverage gate (≥ 5 facilities and ≥ 35 % of them with a figure), so a short list means thin disclosure, not few players. Count-based rankings have no gate. AI, project and IXP rankings appear here as soon as the worker computes them.
89 + </p>
86 90
87 91 <div className="flex flex-col gap-8">
88 92 {groups.map((g) => (
modified apps/web/src/app/(site)/rankings/[key]/page.tsx +22 −8
@@ -6,11 +6,14 @@ import { JsonLd } from "@/components/entity/json-ld";
6 6 import { Badge } from "@/components/ui/badge";
7 7 import { cn } from "@/components/ui/cn";
8 8 import { Card, CardBody, CardHeader, Page } from "@/components/ui/card";
9 +import { LinkTabs } from "@/components/ui/tabs";
10 +import { CoverageBar } from "@/components/intelligence/shared";
9 11 import { CountryFlag } from "@/components/ui/country-flag";
10 12 import { EmptyState, OfflineNotice } from "@/components/ui/states";
11 13 import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";
12 14 import { api } from "@/lib/api";
13 15 import { fmtDate, fmtDelta, fmtInt, fmtPct } from "@/lib/format";
16 +import type { SearchParams } from "@/lib/sort";
14 17 import { routes } from "@/lib/routes";
15 18 import { NOT_FOUND_METADATA, absoluteUrl, pageMetadata } from "@/lib/seo";
16 19 import { SCOPE_LABEL, firstSentence, fmtRankValue, rowHref, secondaryLabel } from "../ranking-format";
@@ -31,8 +34,9 @@ export async function generateMetadata({ params }: { params: Params }): Promise<
31 34 });
32 35 }
33 36
34 −export default async function RankingPage({ params }: { params: Params }) {
35 − const { key } = await params;
37 +export default async function RankingPage({ params, searchParams }: { params: Params; searchParams: Promise<SearchParams> }) {
38 + const [{ key }, sp] = await Promise.all([params, searchParams]);
39 + const mode = sp.mode === "all" ? "all" : "strict";
36 40 const r = await api.ranking(key, { limit: 200 });
37 41 if (!r.ok && (r.status === 404 || r.status === 400)) notFound();
38 42 if (!r.ok) {
@@ -82,8 +86,23 @@ export default async function RankingPage({ params }: { params: Params }) {
82 86 )}
83 87 </div>
84 88 {d.minCoverage !== undefined && d.minCoverage !== null && <p className="mt-2 max-w-2xl text-[12.5px] text-ink-3">Entities with fewer than 5 facilities or MW coverage below {fmtPct(d.minCoverage)} are excluded, not estimated.</p>}
89 + <p className="mt-1.5 max-w-2xl text-[12px] text-ink-4">Known / published figures only — entities without a figure are absent, not estimated.</p>
85 90 </header>
86 91
92 + {d.minCoverage !== undefined && d.minCoverage !== null && (
93 + <div className="mb-4">
94 + <LinkTabs
95 + items={[
96 + { href: routes.ranking(d.key), label: "Strict", active: mode === "strict", count: d.total },
97 + { href: `${routes.ranking(d.key)}?mode=all`, label: "All observed", active: mode === "all" },
98 + ]}
99 + />
100 + <p className="mt-2 text-[11.5px] text-ink-3">
101 + <strong className="font-medium text-ink-2">Strict</strong> = coverage gate applied (≥ 5 facilities, ≥ {fmtPct(d.minCoverage)} with a figure). <strong className="font-medium text-ink-2">All observed</strong> would list every entity with at least one published figure regardless of coverage — the API applies the gate server-side and does not expose an ungated view yet, so this tab shows the same rows.
102 + </p>
103 + </div>
104 + )}
105 +
87 106 {rows.length === 0 ? (
88 107 <EmptyState
89 108 title="No entity meets the coverage gate yet"
@@ -145,12 +164,7 @@ export default async function RankingPage({ params }: { params: Params }) {
145 164 {row.coverage === null || row.coverage === undefined ? (
146 165 <span className="text-ink-4">—</span>
147 166 ) : (
148 − <span className="flex w-full items-center gap-2 sm:w-[120px]">
149 − <span className="figure w-10 text-right text-[12px] text-ink-2">{fmtPct(row.coverage)}</span>
150 − <span className="h-[3px] min-w-0 flex-1 bg-surface-3" aria-hidden>
151 − <span className={cn("block h-full", row.coverage < 0.5 ? "bg-ink-4" : "bg-ink-2")} style={{ width: `${Math.max(2, Math.min(100, row.coverage * 100))}%` }} />
152 − </span>
153 − </span>
167 + <CoverageBar value={row.coverage} width="w-full sm:w-[120px]" label="Share of facilities with a MW figure" />
154 168 )}
155 169 </Td>
156 170 )}
modified apps/web/src/app/(site)/sources/(index)/page.tsx +30 −1
@@ -48,6 +48,26 @@ function getter(key: SortKey): (s: SourceWithCounts) => number | string | null {
48 48 }
49 49 }
50 50
51 +const REDIST_TONE: Record<string, "ok" | "info" | "warn" | "neutral"> = { allowed: "ok", attribution: "info", restricted: "warn", unknown: "neutral" };
52 +const REDIST_LABEL: Record<string, string> = { allowed: "Redistribution allowed", attribution: "Attribution required", restricted: "Restricted", unknown: "Re-use terms unknown" };
53 +
54 +/** Redistribution / licence badge from the source registry (drives /download gating). */
55 +function ReuseBadge({ s }: { s: SourceWithCounts }) {
56 + const r = s.redistribution ?? "unknown";
57 + return (
58 + <span className="inline-flex flex-wrap gap-1">
59 + <Badge tone={REDIST_TONE[r] ?? "neutral"} size="xs" title={`${REDIST_LABEL[r] ?? r}${s.license ? ` — ${s.license}` : ""}`}>
60 + {r === "attribution" ? "Attribution" : r === "allowed" ? "Allowed" : r === "restricted" ? "Restricted" : "Unknown"}
61 + </Badge>
62 + {s.attributionRequired && r !== "attribution" && (
63 + <Badge tone="outline" size="xs" title="Attribution line must accompany every re-use">
64 + Attribution
65 + </Badge>
66 + )}
67 + </span>
68 + );
69 +}
70 +
51 71 function truncate(s: string | null | undefined, n: number): string {
52 72 if (!s) return "—";
53 73 return s.length > n ? `${s.slice(0, n - 1)}…` : s;
@@ -62,6 +82,7 @@ function SourceTable({ items, sp, grouped, sort }: { items: SourceWithCounts[];
62 82 <tr>
63 83 <Th {...th("name")}>Source</Th>
64 84 {!grouped && <Th {...th("kind")}>Kind</Th>}
85 + <Th className="hidden md:table-cell">Re-use</Th>
65 86 <Th className="hidden lg:table-cell">License</Th>
66 87 <Th className="hidden xl:table-cell">Attribution</Th>
67 88 <Th numeric {...th("documents")}>
@@ -93,6 +114,9 @@ function SourceTable({ items, sp, grouped, sort }: { items: SourceWithCounts[];
93 114 </Badge>
94 115 </Td>
95 116 )}
117 + <Td label="Re-use" className="hidden md:table-cell">
118 + <ReuseBadge s={s} />
119 + </Td>
96 120 <Td label="License" className="hidden max-w-[240px] text-[12px] text-ink-2 lg:table-cell" title={s.license ?? undefined}>
97 121 {truncate(s.license, 60)}
98 122 </Td>
@@ -164,8 +188,13 @@ export default async function SourcesPage({ searchParams }: { searchParams: Prom
164 188 <PageHeader
165 189 eyebrow="Registry"
166 190 title="Sources"
167 − description="Every source the index reads, with its licence, attribution and how much of the graph it backs. Facts only — each value links back to the page it was observed on."
191 + description="Every source the index reads, with its licence, attribution, re-use terms and how much of the graph it backs. Facts only — each value links back to the page it was observed on."
168 192 className="pt-3"
193 + aside={
194 + <Button size="sm" href={`${routes.coverage()}/sources`}>
195 + Coverage analytics →
196 + </Button>
197 + }
169 198 />
170 199 {!res.items.length && !res.total && <OfflineNotice detail="sources unavailable" className="mb-3" />}
171 200
added apps/web/src/app/(site)/watchlist/page.tsx +24 −0
@@ -0,0 +1,24 @@
1 +import type { Metadata } from "next";
2 +import { Breadcrumbs } from "@/components/entity/breadcrumbs";
3 +import { WatchlistView } from "@/components/intelligence/watchlist/watchlist-view";
4 +import { Page, PageHeader } from "@/components/ui/card";
5 +import { routes } from "@/lib/routes";
6 +
7 +export const dynamic = "force-dynamic";
8 +
9 +export const metadata: Metadata = {
10 + title: "Watchlist",
11 + description: "Your private watchlist of operators, markets, countries and projects, with a change feed limited to what you follow. Cookie-based, no account.",
12 + alternates: { canonical: routes.watchlist() },
13 + robots: { index: false, follow: true },
14 +};
15 +
16 +export default function WatchlistPage() {
17 + return (
18 + <Page>
19 + <Breadcrumbs items={[{ name: "Watchlist", href: routes.watchlist() }]} className="pt-3" />
20 + <PageHeader eyebrow="Private" title="Watchlist" description="Operators, markets, countries and projects you follow, and every change detected on them. Stored against a private cookie on this browser — no account, nothing shared." className="pt-3" />
21 + <WatchlistView />
22 + </Page>
23 + );
24 +}
added apps/web/src/app/admin/(shell)/data-gaps/page.tsx +13 −0
@@ -0,0 +1,13 @@
1 +import type { Metadata } from "next";
2 +import { Suspense } from "react";
3 +import { DataGapsView } from "@/components/admin/data-gaps";
4 +
5 +export const metadata: Metadata = { title: "Data gaps" };
6 +
7 +export default function AdminDataGapsPage() {
8 + return (
9 + <Suspense fallback={null}>
10 + <DataGapsView />
11 + </Suspense>
12 + );
13 +}
modified apps/web/src/app/admin/(shell)/documents/[id]/page.tsx +12 −4
@@ -1,12 +1,20 @@
1 1 import type { Metadata } from "next";
2 −import { DocumentInspectView } from "@/components/admin/document-inspect";
2 +import { Suspense } from "react";
3 +import { DocumentWorkbench } from "@/components/admin/document-workbench";
4 +import { traceFixture } from "@/components/admin/trace-fixture";
3 5
4 6 export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
5 7 const { id } = await params;
6 8 return { title: `${id} · Document` };
7 9 }
8 10
9 −export default async function AdminDocumentPage({ params }: { params: Promise<{ id: string }> }) {
10 − const { id } = await params;
11 − return <DocumentInspectView id={id} />;
11 +export default async function AdminDocumentPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<Record<string, string | string[] | undefined>> }) {
12 + const [{ id }, sp] = await Promise.all([params, searchParams]);
13 + // dev-only layout fixture for the extraction debugger when the worker is not running locally
14 + const fixture = process.env.NODE_ENV !== "production" && sp.fixture === "1" ? traceFixture(id) : null;
15 + return (
16 + <Suspense fallback={null}>
17 + <DocumentWorkbench id={id} fixture={fixture} />
18 + </Suspense>
19 + );
12 20 }
added apps/web/src/app/admin/(shell)/projects/page.tsx +13 −0
@@ -0,0 +1,13 @@
1 +import type { Metadata } from "next";
2 +import { Suspense } from "react";
3 +import { ProjectsAdmin } from "@/components/admin/projects-admin";
4 +
5 +export const metadata: Metadata = { title: "Projects" };
6 +
7 +export default function AdminProjectsPage() {
8 + return (
9 + <Suspense fallback={null}>
10 + <ProjectsAdmin />
11 + </Suspense>
12 + );
13 +}
added apps/web/src/app/admin/(shell)/quality/page.tsx +13 −0
@@ -0,0 +1,13 @@
1 +import type { Metadata } from "next";
2 +import { Suspense } from "react";
3 +import { QualityDashboard } from "@/components/admin/quality-dashboard";
4 +
5 +export const metadata: Metadata = { title: "Data quality" };
6 +
7 +export default function AdminQualityPage() {
8 + return (
9 + <Suspense fallback={null}>
10 + <QualityDashboard />
11 + </Suspense>
12 + );
13 +}
added apps/web/src/app/admin/(shell)/runs/[id]/page.tsx +12 −0
@@ -0,0 +1,12 @@
1 +import type { Metadata } from "next";
2 +import { RunDetailView } from "@/components/admin/run-detail";
3 +
4 +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
5 + const { id } = await params;
6 + return { title: `${id} · Run` };
7 +}
8 +
9 +export default async function AdminRunPage({ params }: { params: Promise<{ id: string }> }) {
10 + const { id } = await params;
11 + return <RunDetailView id={id} />;
12 +}
modified apps/web/src/app/globals.css +2 −1
@@ -228,7 +228,7 @@
228 228 @media (max-width: 639.98px) {
229 229 .dci-table.responsive thead { display: none; }
230 230 .dci-table.responsive, .dci-table.responsive tbody { display: block; width: 100%; }
231 − .dci-table.responsive tr { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 12px; row-gap: 3px; width: 100%; border-bottom: 1px solid var(--hair); padding: 9px 0; }
231 + .dci-table.responsive tr { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 12px; row-gap: 3px; width: 100%; border-bottom: 1px solid var(--hair); padding: 11px 0; min-height: 40px; }
232 232 .dci-table.responsive td { order: 2; display: inline-flex; flex: 0 0 auto; align-items: baseline; gap: 5px; min-width: 0; max-width: 100%; padding: 0 !important; border: 0 !important; font-size: 12px; line-height: 1.35; text-align: left; }
233 233 .dci-table.responsive td.hidden { display: none; }
234 234 .dci-table.responsive td[data-label]::before { content: attr(data-label); color: var(--ink-3); font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif; font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; flex: 0 0 auto; }
@@ -258,6 +258,7 @@
258 258 /* MapLibre overrides — keep controls on brand and small. */
259 259 .maplibregl-ctrl-group { border-radius: 3px !important; box-shadow: none !important; border: 1px solid var(--hair-2); background: var(--surface) !important; }
260 260 .maplibregl-ctrl-group button { width: 32px !important; height: 32px !important; }
261 +@media (max-width: 767.98px) { .maplibregl-ctrl-group button { width: 40px !important; height: 40px !important; } .maplibregl-ctrl-group button .maplibregl-ctrl-icon { background-size: 20px; } }
261 262 .dark .maplibregl-ctrl-group button .maplibregl-ctrl-icon { filter: invert(1); }
262 263 .maplibregl-ctrl-attrib { font-size: 10px !important; background: color-mix(in oklab, var(--surface) 85%, transparent) !important; color: var(--ink-3) !important; }
263 264 .maplibregl-ctrl-attrib a { color: var(--ink-2) !important; }
modified apps/web/src/components/admin/connector-detail.tsx +3 −1
@@ -213,7 +213,9 @@ export function ConnectorDetailView({ id }: { id: string }) {
213 213 {fmtInt(n(s.fetched))} fetched · {fmtInt(n(s.changed))} changed · {fmtInt(n(s.entities))} entities · <span className={cn((n(s.failed) ?? 0) > 0 && "text-danger")}>{fmtInt(n(s.failed))} failed</span>
214 214 {n(s.credits) ? <span className="text-accent"> · {fmtInt(n(s.credits))} cr</span> : null}
215 215 </span>
216 − <Mono className="ml-auto hidden text-ink-4 sm:inline">{r.id}</Mono>
216 + <Link href={`/admin/runs/${encodeURIComponent(r.id)}`} className="ml-auto hidden font-mono text-[11.5px] text-ink-3 hover:text-accent hover:underline sm:inline" title="Run detail: changes written, rollback">
217 + {r.id} →
218 + </Link>
217 219 {r.error && <span className="w-full truncate text-[11.5px] text-danger">{r.error}</span>}
218 220 </span>
219 221 }
modified apps/web/src/components/admin/connectors-table.tsx +255 −73
@@ -8,15 +8,52 @@ import { DASH, fmtInt } from "@/lib/format";
8 8 import { SOURCE_KIND_LABEL } from "@/lib/labels";
9 9 import type { ConnectorHealthDTO } from "@/lib/admin-api";
10 10 import { PauseResumeButton, RunForm, ScheduleEditor } from "./connector-actions";
11 −import { ErrorNote, fmtMs, fmtPct01, HealthDot, Mono, RefreshButton, Segmented, Select } from "./primitives";
11 +import { ErrorNote, fmtMs, fmtPct01, HEALTH, HealthBadge, HealthDot, Mono, RefreshButton, Segmented, Select } from "./primitives";
12 12 import { AdminPageHeader } from "./shell";
13 −import { useAdminQuery } from "./use-admin-query";
13 +import { useToast } from "./toast";
14 +import { useAdminMutation, useAdminQuery } from "./use-admin-query";
14 15 import { useUrlState } from "./use-url-state";
15 16
16 −type SortKey = "id" | "kind" | "health" | "lastRunAt" | "nextRunAt" | "discovered" | "fetched" | "changed" | "failed" | "extractionSuccess" | "avgResponseMs" | "credits" | "parserVersion";
17 −const HEALTH_ORDER: Record<string, number> = { failing: 0, degraded: 1, ok: 2, never_run: 3, paused: 4 };
18 −const HEALTH_FILTERS = ["all", "ok", "degraded", "failing", "paused", "never_run", "enabled", "disabled"] as const;
17 +/* ------------------------------------------------------------------------------------------
18 + Connector HEALTH CENTER (§11–12): one row per connector with the crawl / extraction / publication
19 + counters of the last window, a spec status badge, quarantine and reprocess controls.
20 + ------------------------------------------------------------------------------------------ */
21 +
22 +type SortKey =
23 + | "id"
24 + | "kind"
25 + | "health"
26 + | "lastRunAt"
27 + | "lastSuccessAt"
28 + | "lastFailureAt"
29 + | "nextRunAt"
30 + | "urlsDiscovered"
31 + | "urlsFetched"
32 + | "newDocs"
33 + | "changedDocs"
34 + | "extracted"
35 + | "recordsCreated"
36 + | "recordsModified"
37 + | "rejectedClaims"
38 + | "httpErrors"
39 + | "antiBotEscalations"
40 + | "scrapflyRequests"
41 + | "firecrawlRequests"
42 + | "failed"
43 + | "extractionSuccess"
44 + | "avgResponseMs"
45 + | "credits"
46 + | "parserVersion";
47 +
48 +const HEALTH_ORDER: Record<string, number> = { blocked: 0, failing: 1, schema_change: 2, degraded: 3, quarantine: 4, no_new_content: 5, ok: 6, never_run: 7, paused: 8 };
49 +const HEALTH_FILTERS = ["all", "ok", "degraded", "blocked", "schema_change", "no_new_content", "failing", "quarantine", "paused", "never_run", "enabled", "disabled"] as const;
19 50 type HealthFilter = (typeof HEALTH_FILTERS)[number];
51 +const COLUMN_SETS = ["compact", "full"] as const;
52 +type ColumnSet = (typeof COLUMN_SETS)[number];
53 +
54 +function num(v: unknown): number | null {
55 + return typeof v === "number" && Number.isFinite(v) ? v : null;
56 +}
20 57
21 58 function sortValue(c: ConnectorHealthDTO, k: SortKey): number | string {
22 59 switch (k) {
@@ -26,25 +63,39 @@ function sortValue(c: ConnectorHealthDTO, k: SortKey): number | string {
26 63 return HEALTH_ORDER[c.health] ?? 9;
27 64 case "lastRunAt":
28 65 case "nextRunAt":
66 + case "lastSuccessAt":
67 + case "lastFailureAt":
29 68 return c[k] ? new Date(c[k] as string).getTime() : -1;
30 − case "extractionSuccess":
31 − case "avgResponseMs":
32 − return c[k] ?? -1;
69 + case "id":
70 + case "kind":
71 + case "parserVersion":
72 + return c[k];
33 73 default: {
34 − const v = c[k];
35 − return typeof v === "number" ? v : String(v ?? "");
74 + const v = num(c[k]);
75 + return v ?? -1;
36 76 }
37 77 }
38 78 }
39 79
80 +/** A counter cell: "—" when the API does not carry the field (older API), highlighted when non-zero and `hot`. */
81 +function Count({ v, hot, className }: { v: number | null | undefined; hot?: "accent" | "warn" | "danger"; className?: string }) {
82 + if (v === null || v === undefined) return <span className="text-ink-4">{DASH}</span>;
83 + return <span className={cn(v > 0 && hot === "accent" && "text-accent-ink", v > 0 && hot === "warn" && "text-warn", v > 0 && hot === "danger" && "text-danger", className)}>{fmtInt(v)}</span>;
84 +}
85 +
40 86 export function ConnectorsTable() {
41 87 const q = useAdminQuery<ConnectorHealthDTO[]>("connectors", { refreshMs: 60_000, timeoutMs: 30_000 });
42 88 const [params, setParams] = useUrlState();
43 89 const kind = params.kind ?? "";
44 90 const health = (HEALTH_FILTERS.includes(params.health as HealthFilter) ? params.health : "all") as HealthFilter;
91 + const columns = (COLUMN_SETS.includes(params.cols as ColumnSet) ? params.cols : "compact") as ColumnSet;
92 + const full = columns === "full";
45 93 const search = params.q ?? "";
46 94 const [sort, setSort] = useState<{ key: SortKey; dir: "asc" | "desc" }>({ key: "health", dir: "asc" });
47 95 const [open, setOpen] = useState<{ id: string; mode: "run" | "schedule" } | null>(null);
96 + const { busy, run } = useAdminMutation();
97 + const toast = useToast();
98 + const [bulk, setBulk] = useState<{ done: number; total: number } | null>(null);
48 99
49 100 const list = useMemo(() => q.data ?? [], [q.data]);
50 101 const counts = useMemo(() => {
@@ -70,25 +121,66 @@ export function ConnectorsTable() {
70 121 return out;
71 122 }, [list, kind, health, search, sort]);
72 123
73 − const th = (key: SortKey, label: string, numeric?: boolean) => (
74 − <Th numeric={numeric} sort={sort.key === key ? sort.dir : null} className="cursor-pointer select-none" onClick={() => setSort((s) => ({ key, dir: s.key === key && s.dir === "asc" ? "desc" : s.key === key ? "asc" : numeric ? "desc" : "asc" }))}>
124 + const totals = useMemo(() => {
125 + const t = { urlsFetched: 0, newDocs: 0, changedDocs: 0, recordsCreated: 0, rejectedClaims: 0, httpErrors: 0, antiBotEscalations: 0, scrapfly: 0, firecrawl: 0 };
126 + for (const c of rows) {
127 + t.urlsFetched += c.urlsFetched ?? c.fetched ?? 0;
128 + t.newDocs += c.newDocs ?? 0;
129 + t.changedDocs += c.changedDocs ?? c.changed ?? 0;
130 + t.recordsCreated += c.recordsCreated ?? 0;
131 + t.rejectedClaims += c.rejectedClaims ?? 0;
132 + t.httpErrors += c.httpErrors ?? 0;
133 + t.antiBotEscalations += c.antiBotEscalations ?? 0;
134 + t.scrapfly += c.scrapflyRequests ?? c.cost.scrapfly;
135 + t.firecrawl += c.firecrawlRequests ?? c.cost.firecrawl;
136 + }
137 + return t;
138 + }, [rows]);
139 +
140 + const th = (key: SortKey, label: string, numeric?: boolean, title?: string) => (
141 + <Th numeric={numeric} sort={sort.key === key ? sort.dir : null} className="cursor-pointer select-none" title={title} onClick={() => setSort((s) => ({ key, dir: s.key === key && s.dir === "asc" ? "desc" : s.key === key ? "asc" : numeric ? "desc" : "asc" }))}>
75 142 {label}
76 143 </Th>
77 144 );
78 145
146 + /** Sequential POST run {task: reprocess} for every enabled, non-quarantined connector in the current view. */
147 + const reprocessAll = async () => {
148 + const targets = rows.filter((c) => c.enabled && !c.quarantine && c.health !== "paused");
149 + if (!targets.length) return toast({ tone: "warn", title: "Nothing to reprocess", description: "No enabled connector in the current view." });
150 + if (!window.confirm(`Enqueue a REPROCESS run (re-extract stored bodies, no network fetch) for ${targets.length} enabled connector${targets.length === 1 ? "" : "s"} in the current view?`)) return;
151 + setBulk({ done: 0, total: targets.length });
152 + let ok = 0;
153 + let failed = 0;
154 + for (const c of targets) {
155 + const r = await run<{ enqueued: boolean }>(`reprocess-all`, `connectors/${encodeURIComponent(c.id)}/run`, { body: { task: "reprocess" } });
156 + if (r.ok) ok++;
157 + else failed++;
158 + setBulk((b) => (b ? { ...b, done: b.done + 1 } : b));
159 + }
160 + setBulk(null);
161 + toast({ tone: failed ? "warn" : "ok", title: `Reprocess enqueued for ${ok} connector${ok === 1 ? "" : "s"}`, description: failed ? `${failed} failed to enqueue` : undefined });
162 + void q.refresh();
163 + };
164 +
79 165 return (
80 166 <div>
81 − <AdminPageHeader title="Connectors" description={q.data ? `${fmtInt(list.length)} connectors · ${fmtInt(counts.enabled ?? 0)} enabled · health computed from the last run and document registry` : "Connector control center"} aside={<RefreshButton onClick={() => void q.refresh()} busy={q.refreshing} updatedAt={q.updatedAt} />} />
167 + <AdminPageHeader
168 + title="Connector health center"
169 + description={q.data ? `${fmtInt(list.length)} connectors · ${fmtInt(counts.enabled ?? 0)} enabled · counters over the last window (documents registry + connector runs), status from the last run` : "Connector control center"}
170 + aside={
171 + <>
172 + <Button size="xs" variant="secondary" onClick={() => void reprocessAll()} disabled={busy !== null || bulk !== null || rows.length === 0} title="POST run {task: reprocess} for every enabled connector in the current view, one after the other">
173 + {bulk ? `Reprocessing ${bulk.done}/${bulk.total}…` : "Reprocess stale (all enabled)"}
174 + </Button>
175 + <RefreshButton onClick={() => void q.refresh()} busy={q.refreshing} updatedAt={q.updatedAt} />
176 + </>
177 + }
178 + />
82 179 <ErrorNote error={q.error && !q.data ? q.error : null} onRetry={() => void q.refresh()} className="mb-3" />
83 180
84 181 <div className="mb-3 flex flex-col gap-2 lg:flex-row lg:items-center">
85 − <Segmented
86 − label="Health"
87 − value={health}
88 − onChange={(v) => setParams({ health: v === "all" ? null : v })}
89 − options={HEALTH_FILTERS.map((h) => ({ value: h, label: h === "all" ? "All" : h === "never_run" ? "Never run" : h[0]!.toUpperCase() + h.slice(1), count: counts[h] ?? 0 }))}
90 − />
91 − <div className="flex flex-1 gap-2">
182 + <Segmented label="Health" value={health} onChange={(v) => setParams({ health: v === "all" ? null : v })} options={HEALTH_FILTERS.map((h) => ({ value: h, label: h === "all" ? "All" : h === "enabled" || h === "disabled" ? h[0]!.toUpperCase() + h.slice(1) : (HEALTH[h]?.label ?? h).toLowerCase().replace(/^./, (m) => m.toUpperCase()), count: counts[h] ?? 0 }))} />
183 + <div className="flex flex-1 flex-wrap gap-2">
92 184 <Select value={kind} onChange={(e) => setParams({ kind: e.target.value || null })} className="w-[170px] shrink-0" aria-label="Kind">
93 185 <option value="">All kinds</option>
94 186 {kinds.map((k) => (
@@ -97,34 +189,51 @@ export function ConnectorsTable() {
97 189 </option>
98 190 ))}
99 191 </Select>
100 − <SearchInput size="sm" value={search} onChange={(v) => setParams({ q: v || null })} placeholder="Search id, name, domain" wrapperClassName="flex-1" />
192 + <SearchInput size="sm" value={search} onChange={(v) => setParams({ q: v || null })} placeholder="Search id, name, domain" wrapperClassName="min-w-[180px] flex-1" />
193 + <Segmented label="Columns" value={columns} onChange={(v) => setParams({ cols: v === "compact" ? null : v })} options={[{ value: "compact", label: "Compact" }, { value: "full", label: "All columns" }]} />
101 194 </div>
102 195 </div>
103 196
197 + {q.data && rows.length > 0 && (
198 + <p className="mb-2 text-[11.5px] text-ink-3">
199 + In view: <span className="figure text-ink-2">{fmtInt(totals.urlsFetched)}</span> URLs fetched · <span className="figure text-ink-2">{fmtInt(totals.newDocs)}</span> new docs · <span className="figure text-ink-2">{fmtInt(totals.changedDocs)}</span> changed · <span className="figure text-ink-2">{fmtInt(totals.recordsCreated)}</span> records created · <span className={cn("figure", totals.rejectedClaims > 0 ? "text-warn" : "text-ink-2")}>{fmtInt(totals.rejectedClaims)}</span> rejected claims · <span className={cn("figure", totals.httpErrors > 0 ? "text-danger" : "text-ink-2")}>{fmtInt(totals.httpErrors)}</span> HTTP errors · <span className="figure text-ink-2">{fmtInt(totals.antiBotEscalations)}</span> anti-bot escalations · Scrapfly <span className="figure text-ink-2">{fmtInt(totals.scrapfly)}</span> / Firecrawl <span className="figure text-ink-2">{fmtInt(totals.firecrawl)}</span> requests.
200 + </p>
201 + )}
202 +
104 203 {q.loading && !q.data ? (
105 204 <SkeletonRows rows={10} />
106 205 ) : rows.length === 0 ? (
107 206 <EmptyState compact title="No connectors match" description="Adjust the health / kind filters or the search." />
108 207 ) : (
109 − <div className="overflow-x-auto rounded-sm border border-hair bg-surface">
110 − <Table className="min-w-[1180px] text-[12.5px] sm:[&_td]:py-1.5" responsive={false}>
208 + <div className="max-w-full overflow-x-auto rounded-sm border border-hair bg-surface">
209 + <Table className={cn("text-[12.5px] sm:[&_td]:py-1.5", full ? "min-w-[2400px]" : "min-w-[1360px]")} responsive={false}>
111 210 <THead>
112 211 <Tr>
113 212 <Th className="pl-3">{""}</Th>
114 − {th("id", "Connector")}
213 + {th("id", "Source")}
214 + {full && <Th>Domain</Th>}
115 215 {th("kind", "Kind")}
116 − <Th>Mode</Th>
117 − <Th>State</Th>
216 + {th("health", "Status")}
118 217 {th("lastRunAt", "Last crawl")}
119 − {th("nextRunAt", "Next")}
120 − {th("discovered", "Disc.", true)}
121 − {th("fetched", "Fetched", true)}
122 − {th("changed", "Changed", true)}
123 − {th("failed", "Failed", true)}
124 − {th("extractionSuccess", "Extract %", true)}
125 − {th("avgResponseMs", "Avg ms", true)}
126 − {th("credits", "Cost", true)}
127 − {th("parserVersion", "Parser")}
218 + {full && th("lastSuccessAt", "Last success")}
219 + {th("lastFailureAt", "Last failure")}
220 + {full && th("nextRunAt", "Next")}
221 + {th("urlsDiscovered", "URLs disc.", true, "URLs discovered")}
222 + {th("urlsFetched", "URLs fetched", true)}
223 + {th("newDocs", "New docs", true)}
224 + {th("changedDocs", "Changed", true, "Changed documents")}
225 + {th("extracted", "Extract.", true, "Extractions")}
226 + {full && th("extractionSuccess", "Extract %", true, "Extraction success rate")}
227 + {th("recordsCreated", "Rec. created", true, "Records created")}
228 + {full && th("recordsModified", "Rec. modified", true, "Records modified")}
229 + {th("rejectedClaims", "Rej. claims", true, "Rejected claims")}
230 + {th("httpErrors", "HTTP err.", true, "HTTP errors")}
231 + {full && th("antiBotEscalations", "Anti-bot", true, "Anti-bot escalations (L2+)")}
232 + {full && th("scrapflyRequests", "Scrapfly", true, "Scrapfly requests")}
233 + {full && th("firecrawlRequests", "Firecrawl", true, "Firecrawl requests")}
234 + {th("avgResponseMs", "Avg latency", true)}
235 + {full && th("credits", "Credits", true, "Premium credits (7 d)")}
236 + {full && th("parserVersion", "Parser")}
128 237 <Th className="pr-3">Actions</Th>
129 238 </Tr>
130 239 </THead>
@@ -132,81 +241,154 @@ export function ConnectorsTable() {
132 241 {rows.map((c) => {
133 242 const isOpen = open?.id === c.id;
134 243 return (
135 − <ConnectorRow key={c.id} c={c} open={isOpen ? open.mode : null} onToggle={(mode) => setOpen((o) => (o?.id === c.id && o.mode === mode ? null : { id: c.id, mode }))} onChanged={() => void q.refresh()} mutate={q.mutate} />
244 + <ConnectorRow key={c.id} c={c} full={full} open={isOpen ? open.mode : null} onToggle={(mode) => setOpen((o) => (o?.id === c.id && o.mode === mode ? null : { id: c.id, mode }))} onChanged={() => void q.refresh()} mutate={q.mutate} />
136 245 );
137 246 })}
138 247 </TBody>
139 248 </Table>
140 249 </div>
141 250 )}
251 + <p className="mt-2 text-[11px] text-ink-4">Status: HEALTHY · DEGRADED (partial failures / slow) · BLOCKED (anti-bot or robots) · SCHEMA CHANGE (parser yields nothing on changed pages) · NO NEW CONTENT · FAILED · QUARANTINE (extracts and previews, publishes nothing) · PAUSED · NEVER RUN. Counters that the API does not report yet show “—”.</p>
142 252 </div>
143 253 );
144 254 }
145 255
146 −function ConnectorRow({ c, open, onToggle, onChanged, mutate }: { c: ConnectorHealthDTO; open: "run" | "schedule" | null; onToggle: (m: "run" | "schedule") => void; onChanged: () => void; mutate: (fn: (prev: ConnectorHealthDTO[] | null) => ConnectorHealthDTO[] | null) => void }) {
256 +function ConnectorRow({ c, full, open, onToggle, onChanged, mutate }: { c: ConnectorHealthDTO; full: boolean; open: "run" | "schedule" | null; onToggle: (m: "run" | "schedule") => void; onChanged: () => void; mutate: (fn: (prev: ConnectorHealthDTO[] | null) => ConnectorHealthDTO[] | null) => void }) {
147 257 const cost = c.cost;
258 + const { busy, run } = useAdminMutation();
259 + const toast = useToast();
260 + const quarantined = Boolean(c.quarantine) || c.health === "quarantine";
261 + const colSpan = full ? 27 : 18;
262 +
263 + const toggleQuarantine = async () => {
264 + const on = !quarantined;
265 + if (on && !window.confirm(`Quarantine ${c.id}? It will keep extracting and previewing but publish nothing until released.`)) return;
266 + const r = await run<{ id: string; quarantine: boolean; health: string }>("quarantine", `connectors/${encodeURIComponent(c.id)}/quarantine`, { body: { on } });
267 + if (r.ok) {
268 + toast({ tone: "ok", title: on ? `${c.id} quarantined` : `${c.id} released from quarantine` });
269 + mutate((prev) => prev?.map((x) => (x.id === c.id ? { ...x, quarantine: r.data.quarantine, health: r.data.health as ConnectorHealthDTO["health"] } : x)) ?? prev);
270 + } else toast({ tone: "danger", title: "Quarantine change failed", description: r.error });
271 + };
272 + const reprocess = async () => {
273 + const r = await run<{ enqueued: boolean; job?: { id?: string } }>("reprocess", `connectors/${encodeURIComponent(c.id)}/run`, { body: { task: "reprocess" } });
274 + if (r.ok) toast({ tone: "ok", title: `Reprocess enqueued · ${c.id}`, description: r.data.job?.id ? `job ${r.data.job.id}` : "re-extracts stored bodies, no network fetch" });
275 + else toast({ tone: "danger", title: "Reprocess failed to enqueue", description: r.error });
276 + };
277 +
148 278 return (
149 279 <>
150 280 <Tr className={cn(!c.enabled && "opacity-70", open && "bg-surface-2/60")}>
151 281 <Td className="pl-3">
152 − <HealthDot health={c.lastStatus === "running" ? "running" : c.health} />
282 + <HealthDot health={c.lastStatus === "running" ? "running" : quarantined ? "quarantine" : c.health} />
153 283 </Td>
154 284 <Td primary>
155 285 <Link href={`/admin/connectors/${encodeURIComponent(c.id)}`} className="font-mono text-[12.5px] font-medium text-ink hover:text-accent hover:underline">
156 286 {c.id}
157 287 </Link>
158 − <div className="max-w-[240px] truncate text-[11.5px] text-ink-3" title={`${c.sourceName} · ${c.domain}`}>
288 + <div className="max-w-[220px] truncate text-[11.5px] text-ink-3" title={`${c.sourceName} · ${c.domain}`}>
159 289 {c.sourceName}
290 + {!full && <span className="text-ink-4"> · {c.domain}</span>}
160 291 </div>
161 292 </Td>
293 + {full && (
294 + <Td muted>
295 + <Mono className="text-[11.5px]">{c.domain}</Mono>
296 + </Td>
297 + )}
162 298 <Td muted>{SOURCE_KIND_LABEL[c.kind] ?? c.kind}</Td>
163 299 <Td>
164 − <Badge size="xs" mono tone="outline">
165 − {c.mode}
166 − </Badge>
167 − </Td>
168 − <Td>
169 − {c.health === "paused" ? (
170 − <Badge size="xs" tone="warn">
171 − paused
172 − </Badge>
173 − ) : c.enabled ? (
174 − <Badge size="xs" tone="ok">
175 − enabled
176 − </Badge>
177 − ) : (
178 − <Badge size="xs">disabled</Badge>
179 − )}
300 + <div className="flex flex-col items-start gap-1">
301 + <HealthBadge health={c.lastStatus === "running" ? "running" : quarantined ? "quarantine" : c.health} />
302 + <span className="flex flex-wrap gap-1">
303 + {!c.enabled && <Badge size="xs">disabled</Badge>}
304 + <Badge size="xs" mono tone="outline">
305 + {c.mode}
306 + </Badge>
307 + {(c.consecutiveFailures ?? 0) > 0 && (
308 + <Badge size="xs" tone="danger" title="Consecutive failed runs">
309 + ×{c.consecutiveFailures}
310 + </Badge>
311 + )}
312 + {c.blockedSince && (
313 + <Badge size="xs" tone="danger" title={`Blocked since ${c.blockedSince}`}>
314 + blocked <RelativeTime iso={c.blockedSince} />
315 + </Badge>
316 + )}
317 + </span>
318 + </div>
180 319 </Td>
181 320 <Td muted>{c.lastRunAt ? <RelativeTime iso={c.lastRunAt} /> : DASH}</Td>
182 − <Td muted>{c.nextRunAt ? <RelativeTime iso={c.nextRunAt} /> : DASH}</Td>
183 − <Td numeric>{fmtInt(c.discovered)}</Td>
184 − <Td numeric>{fmtInt(c.fetched)}</Td>
185 − <Td numeric className={cn(c.changed > 0 && "text-accent-ink")}>
186 − {fmtInt(c.changed)}
321 + {full && <Td muted>{c.lastSuccessAt ? <RelativeTime iso={c.lastSuccessAt} /> : DASH}</Td>}
322 + <Td className={cn(c.lastFailureAt && c.lastSuccessAt && c.lastFailureAt > c.lastSuccessAt ? "text-danger" : "text-ink-3")}>{c.lastFailureAt ? <RelativeTime iso={c.lastFailureAt} /> : DASH}</Td>
323 + {full && <Td muted>{c.nextRunAt ? <RelativeTime iso={c.nextRunAt} /> : DASH}</Td>}
324 + <Td numeric>
325 + <Count v={c.urlsDiscovered ?? c.discovered} />
187 326 </Td>
188 − <Td numeric className={cn(c.failed > 0 && "text-danger")}>
189 − {fmtInt(c.failed)}
327 + <Td numeric>
328 + <Count v={c.urlsFetched ?? c.fetched} />
190 329 </Td>
191 − <Td numeric className={cn(c.extractionSuccess != null && c.extractionSuccess < 0.7 && "text-warn")}>
192 − {fmtPct01(c.extractionSuccess)}
330 + <Td numeric>
331 + <Count v={c.newDocs} hot="accent" />
193 332 </Td>
194 − <Td numeric>{c.avgResponseMs != null ? fmtMs(c.avgResponseMs) : DASH}</Td>
195 − <Td numeric title={`direct ${cost.direct} · firecrawl ${cost.firecrawl} · scrapfly ${cost.scrapfly} · ${cost.credits} credits (7 d)`}>
196 − <span className="text-ink-3">{fmtInt(cost.direct)}</span>
197 − {cost.firecrawl > 0 && <span className="ml-1 text-st-planned">fc {fmtInt(cost.firecrawl)}</span>}
198 − {cost.scrapfly > 0 && <span className="ml-1 text-st-permitting">sf {fmtInt(cost.scrapfly)}</span>}
199 − {cost.credits > 0 && <span className="ml-1 text-accent">{fmtInt(cost.credits)} cr</span>}
333 + <Td numeric>
334 + <Count v={c.changedDocs ?? c.changed} hot="accent" />
200 335 </Td>
201 − <Td>
202 − <Mono>{c.parserVersion}</Mono>
336 + <Td numeric>
337 + <Count v={c.extracted} />
203 338 </Td>
339 + {full && <Td numeric className={cn(c.extractionSuccess != null && c.extractionSuccess < 0.7 && "text-warn")}>{fmtPct01(c.extractionSuccess)}</Td>}
340 + <Td numeric>
341 + <Count v={c.recordsCreated} hot="accent" />
342 + </Td>
343 + {full && (
344 + <Td numeric>
345 + <Count v={c.recordsModified} />
346 + </Td>
347 + )}
348 + <Td numeric>
349 + <Count v={c.rejectedClaims} hot="warn" />
350 + </Td>
351 + <Td numeric>
352 + <Count v={c.httpErrors ?? c.failed} hot="danger" />
353 + </Td>
354 + {full && (
355 + <Td numeric>
356 + <Count v={c.antiBotEscalations} hot="warn" />
357 + </Td>
358 + )}
359 + {full && (
360 + <Td numeric>
361 + <Count v={c.scrapflyRequests ?? cost.scrapfly} />
362 + </Td>
363 + )}
364 + {full && (
365 + <Td numeric>
366 + <Count v={c.firecrawlRequests ?? cost.firecrawl} />
367 + </Td>
368 + )}
369 + <Td numeric>{c.avgResponseMs != null ? fmtMs(c.avgResponseMs) : DASH}</Td>
370 + {full && (
371 + <Td numeric title={`direct ${cost.direct} · firecrawl ${cost.firecrawl} · scrapfly ${cost.scrapfly} · ${cost.credits} credits (7 d)`}>
372 + {cost.credits > 0 ? <span className="text-accent">{fmtInt(cost.credits)}</span> : <span className="text-ink-4">0</span>}
373 + </Td>
374 + )}
375 + {full && (
376 + <Td>
377 + <Mono>{c.parserVersion}</Mono>
378 + </Td>
379 + )}
204 380 <Td className="pr-3">
205 381 <div className="flex items-center gap-1">
206 382 <Button size="xs" variant={open === "run" ? "primary" : "secondary"} onClick={() => onToggle("run")}>
207 383 Run
208 384 </Button>
385 + <Button size="xs" variant="ghost" onClick={() => void reprocess()} disabled={busy !== null} title="Re-extract stored bodies with the current parser (no network fetch)">
386 + Reprocess
387 + </Button>
209 388 <PauseResumeButton connector={c} onChanged={(paused) => mutate((prev) => prev?.map((x) => (x.id === c.id ? { ...x, health: paused ? "paused" : x.lastRunAt ? "ok" : "never_run", enabled: !paused && x.enabled } : x)) ?? prev)} />
389 + <Button size="xs" variant={quarantined ? "secondary" : "ghost"} onClick={() => void toggleQuarantine()} disabled={busy !== null} title={quarantined ? "Release: the connector publishes again" : "Quarantine: extract and preview, publish nothing"}>
390 + {quarantined ? "Release" : "Quarantine"}
391 + </Button>
210 392 <Button size="xs" variant={open === "schedule" ? "primary" : "ghost"} onClick={() => onToggle("schedule")}>
211 393 Schedule
212 394 </Button>
@@ -215,7 +397,7 @@ function ConnectorRow({ c, open, onToggle, onChanged, mutate }: { c: ConnectorHe
215 397 </Tr>
216 398 {open && (
217 399 <tr className="bg-surface-2/60">
218 − <td colSpan={16} className="px-3 pb-3 pt-1">
400 + <td colSpan={colSpan} className="px-3 pb-3 pt-1">
219 401 {open === "run" ? <RunForm connector={c} compact onDone={onChanged} /> : <ScheduleEditor connector={c} onSaved={(schedule) => mutate((prev) => prev?.map((x) => (x.id === c.id ? { ...x, schedule } : x)) ?? prev)} />}
220 402 </td>
221 403 </tr>
added apps/web/src/components/admin/data-gaps.tsx +26 −0
@@ -0,0 +1,127 @@
1 +"use client";
2 +
3 +import Link from "next/link";
4 +import { useMemo } from "react";
5 +import { EmptyState, SkeletonBlock } from "@/components/ui";
6 +import { cn } from "@/components/ui/cn";
7 +import { fmtInt } from "@/lib/format";
8 +import type { DataGaps } from "@/lib/admin-api";
9 +import { ErrorNote, Panel, RefreshButton, Stat } from "./primitives";
10 +import { AdminPageHeader } from "./shell";
11 +import { useAdminQuery } from "./use-admin-query";
12 +
13 +/* ------------------------------------------------------------------------------------------
14 + /admin/data-gaps — what we do not know, ranked for enrichment. Counters come from the worker
15 + (GET /data-gaps proxied by the API); a 502 means the maintenance worker is unreachable.
16 + ------------------------------------------------------------------------------------------ */
17 +
18 +const GROUPS: Array<{ key: string; label: string; prefix: RegExp; description: string }> = [
19 + { key: "facilities", label: "Facilities", prefix: /^facilities_/, description: "Live facility records (merged duplicates excluded)." },
20 + { key: "projects", label: "Projects", prefix: /^projects_/, description: "Live project records (hidden and merged excluded)." },
21 + { key: "operators", label: "Operators", prefix: /^operators_/, description: "Operator registry." },
22 + { key: "other", label: "Pipeline & other entities", prefix: /.*/, description: "Reconciliation queue, quality flags, claims, cloud regions, IXPs." },
23 +];
24 +
25 +/** Where a gap can be worked on — only links that are guaranteed to exist. */
26 +const LINKS: Record<string, string> = {

Diff truncated — file too large.