spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { EventType } from "@dci/core";2import type { Metadata } from "next";3import { LiveFeed2 } from "@/components/intelligence/live/live-feed-2";4import { liveApiQuery, liveHref, MINOR_THRESHOLD, parseLiveQuery } from "@/components/intelligence/live/query";5import { ApiNote } from "@/components/intelligence/shared";6import { Page, PageHeader } from "@/components/ui/card";7import { FilterChips, type Chip } from "@/components/ui/filter-chips";8import { Metric } from "@/components/ui/metric";9import { OfflineNotice } from "@/components/ui/states";10import { api } from "@/lib/api";11import { countryName, fmtInt } from "@/lib/format";12import { CONFIDENCE_SHORT, ENTITY_TYPE_LABEL, EVENT_LABEL, SIGNIFICANCE_LABEL, SOURCE_KIND_LABEL } from "@/lib/labels";13import { routes } from "@/lib/routes";14import { LiveFilters } from "./live-filters";1516export const revalidate = 30;1718export const metadata: Metadata = {19 title: "Live changes",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.",21 alternates: { canonical: "/live" },22};2324const PER_PAGE = 50;2526export default async function LivePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {27 const sp = await searchParams;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()]);31 const stats = dash.ok ? dash.data.stats : null;32 const hidingMinor = !query.all && !query.significance && !query.min_significance;3334 const chips: Chip[] = [];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 }) });4950 return (51 <Page>52 <PageHeader53 eyebrow={54 <span className="inline-flex items-center gap-1.5">55 <span className="inline-block size-1.5 rounded-full bg-accent animate-pulse-soft" /> Live56 </span>57 }58 title="Change feed"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.`}60 aside={61 <div className="flex gap-6">62 <Metric size="sm" value={stats ? fmtInt(stats.events24h) : "—"} label="Last 24 h" />63 <Metric size="sm" value={stats ? fmtInt(stats.events7d) : "—"} label="Last 7 days" />64 </div>65 }66 />67 {!res.ok && <OfflineNotice detail={res.error} className="mb-3" />}68 <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">69 <LiveFilters query={query} total={res.ok ? res.total : null} />70 <FilterChips chips={chips} resetHref={routes.live()} />71 </div>72 <div className="pt-2">73 <LiveFeed2 initial={res.items} initialTotal={res.total} perPage={PER_PAGE} apiQuery={apiQuery} initialError={res.ok ? undefined : res.error} />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.`} />76 </Page>77 );78}79