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>
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'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’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 > 500 records a day, > 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’s or project’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’s own scope when the sentence has no signal (structured parsers) → unknown. A campus figure is never written to a building record; a building’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’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’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'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.