spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { ConfidenceLevel, EventType, SourceKind } from "@dci/core";2import { CONFIDENCE_LEVELS, EVENT_TYPES, SOURCE_KINDS } from "@dci/core";3import type { Query } from "@/lib/api";4import { routes } from "@/lib/routes";56/** Live feed 2.0 query — every key is a URL parameter (URL-as-state). */7export interface LiveQuery2 {8 type?: string;9 entity_type?: string;10 entity_id?: string;11 country?: string;12 metro?: string;13 operator?: string;14 project?: string;15 confidence?: string;16 significance?: "major" | "medium" | "minor";17 min_significance?: number;18 source_kind?: string;19 ai?: boolean;20 /** false → every document (dedupe=0); default true (clusters collapsed) */21 dedupe?: boolean;22 /** true → minor events shown; default hides them (min_significance 45) */23 all?: boolean;24}2526/** Threshold under which an event is "minor" (mirrors significanceBand in @dci/core: < 45). */27export const MINOR_THRESHOLD = 45;2829export const FAMILIES: Array<{ key: string; label: string; types: EventType[] }> = [30 { key: "openings", label: "Openings", types: ["facility_opened", "cloud_region_launched"] },31 { key: "construction", label: "Construction", types: ["construction_started", "phase_announced"] },32 { key: "announcements", label: "Announcements", types: ["project_announced", "expansion_announced", "cloud_region_announced"] },33 { key: "planning", label: "Planning", types: ["planning_filed", "planning_approved"] },34 { key: "capacity", label: "Capacity", types: ["capacity_changed", "planned_capacity_changed"] },35 { key: "status", label: "Status", types: ["status_changed", "project_status_changed", "opening_date_changed"] },36 { key: "lifecycle", label: "Project lifecycle", types: ["project_delayed", "project_cancelled", "operator_expansion"] },37 { key: "ownership", label: "Ownership", types: ["operator_changed", "owner_changed"] },38 { key: "corporate", label: "Corporate", types: ["acquisition", "partnership", "customer_agreement", "executive_change", "investment_announced"] },39 { key: "power", label: "Power & grid", types: ["power_agreement", "grid_connection", "grid_constraint", "utility_event", "land_acquired"] },40 { key: "closures", label: "Closures & incidents", types: ["closure", "incident"] },41];4243const TYPE_SET = new Set<string>(EVENT_TYPES);44const CONF_SET = new Set<string>(CONFIDENCE_LEVELS);45const SK_SET = new Set<string>(SOURCE_KINDS);46const SLUG = /^[a-z0-9][a-z0-9_.-]*$/i;4748export function parseLiveQuery(sp: Record<string, string | string[] | undefined>): LiveQuery2 {49 const one = (k: string) => {50 const v = sp[k];51 const s = Array.isArray(v) ? v[0] : v;52 return s === undefined || s === "" ? undefined : s;53 };54 const q: LiveQuery2 = {};55 const types = new Set<string>();56 for (const t of one("type")?.split(",") ?? []) if (TYPE_SET.has(t)) types.add(t);57 for (const f of one("family")?.split(",") ?? []) for (const t of FAMILIES.find((x) => x.key === f)?.types ?? []) types.add(t);58 if (types.size) q.type = Array.from(types).join(",");59 const et = one("entity_type");60 if (et && SLUG.test(et)) q.entity_type = et;61 const eid = one("entity_id");62 if (eid && SLUG.test(eid)) q.entity_id = eid;63 const c = one("country")?.toUpperCase();64 if (c && /^[A-Z]{2}$/.test(c)) q.country = c;65 for (const k of ["metro", "operator", "project"] as const) {66 const v = one(k);67 if (v && SLUG.test(v)) q[k] = v;68 }69 const conf = one("confidence")70 ?.split(",")71 .filter((x) => CONF_SET.has(x))72 .join(",");73 if (conf) q.confidence = conf;74 const sig = one("significance");75 if (sig === "major" || sig === "medium" || sig === "minor") q.significance = sig;76 const ms = Number(one("min_significance"));77 if (Number.isFinite(ms) && ms > 0) q.min_significance = Math.min(100, Math.floor(ms));78 const sk = one("source_kind")79 ?.split(",")80 .filter((x) => SK_SET.has(x))81 .join(",");82 if (sk) q.source_kind = sk;83 const ai = one("ai");84 if (ai === "1" || ai === "true") q.ai = true;85 const dd = one("dedupe");86 if (dd === "0" || dd === "false") q.dedupe = false;87 const all = one("all");88 if (all === "1" || all === "true") q.all = true;89 return q;90}9192/** URL for the live page from a query (defaults skipped). */93export function liveHref(q: LiveQuery2): string {94 const p = new URLSearchParams();95 if (q.type) p.set("type", q.type);96 if (q.entity_type) p.set("entity_type", q.entity_type);97 if (q.entity_id) p.set("entity_id", q.entity_id);98 if (q.country) p.set("country", q.country);99 if (q.metro) p.set("metro", q.metro);100 if (q.operator) p.set("operator", q.operator);101 if (q.project) p.set("project", q.project);102 if (q.confidence) p.set("confidence", q.confidence);103 if (q.significance) p.set("significance", q.significance);104 if (q.min_significance) p.set("min_significance", String(q.min_significance));105 if (q.source_kind) p.set("source_kind", q.source_kind);106 if (q.ai) p.set("ai", "1");107 if (q.dedupe === false) p.set("dedupe", "0");108 if (q.all) p.set("all", "1");109 return routes.live(p.toString() || undefined);110}111112/** Parameters sent to GET /events (the "hide minor" default becomes min_significance). */113export function liveApiQuery(q: LiveQuery2): Query {114 const hideMinor = !q.all && !q.significance && !q.min_significance;115 return {116 type: q.type,117 entity_type: q.entity_type,118 entity_id: q.entity_id,119 country: q.country,120 metro: q.metro,121 operator: q.operator,122 project: q.project,123 confidence: q.confidence,124 significance: q.significance,125 min_significance: hideMinor ? MINOR_THRESHOLD : q.min_significance,126 source_kind: q.source_kind,127 ai: q.ai ? true : undefined,128 dedupe: q.dedupe === false ? 0 : undefined,129 };130}131132export function countLiveFilters(q: LiveQuery2): number {133 return [q.type, q.entity_type, q.entity_id, q.country, q.metro, q.operator, q.project, q.confidence, q.significance, q.min_significance, q.source_kind, q.ai].filter(Boolean).length;134}135136export type { ConfidenceLevel, SourceKind };137