import type { ConfidenceLevel, EventType, SourceKind } from "@dci/core"; import { CONFIDENCE_LEVELS, EVENT_TYPES, SOURCE_KINDS } from "@dci/core"; import type { Query } from "@/lib/api"; import { routes } from "@/lib/routes"; /** Live feed 2.0 query — every key is a URL parameter (URL-as-state). */ export interface LiveQuery2 { type?: string; entity_type?: string; entity_id?: string; country?: string; metro?: string; operator?: string; project?: string; confidence?: string; significance?: "major" | "medium" | "minor"; min_significance?: number; source_kind?: string; ai?: boolean; /** false → every document (dedupe=0); default true (clusters collapsed) */ dedupe?: boolean; /** true → minor events shown; default hides them (min_significance 45) */ all?: boolean; } /** Threshold under which an event is "minor" (mirrors significanceBand in @dci/core: < 45). */ export const MINOR_THRESHOLD = 45; export const FAMILIES: Array<{ key: string; label: string; types: EventType[] }> = [ { key: "openings", label: "Openings", types: ["facility_opened", "cloud_region_launched"] }, { key: "construction", label: "Construction", types: ["construction_started", "phase_announced"] }, { key: "announcements", label: "Announcements", types: ["project_announced", "expansion_announced", "cloud_region_announced"] }, { key: "planning", label: "Planning", types: ["planning_filed", "planning_approved"] }, { key: "capacity", label: "Capacity", types: ["capacity_changed", "planned_capacity_changed"] }, { key: "status", label: "Status", types: ["status_changed", "project_status_changed", "opening_date_changed"] }, { key: "lifecycle", label: "Project lifecycle", types: ["project_delayed", "project_cancelled", "operator_expansion"] }, { key: "ownership", label: "Ownership", types: ["operator_changed", "owner_changed"] }, { key: "corporate", label: "Corporate", types: ["acquisition", "partnership", "customer_agreement", "executive_change", "investment_announced"] }, { key: "power", label: "Power & grid", types: ["power_agreement", "grid_connection", "grid_constraint", "utility_event", "land_acquired"] }, { key: "closures", label: "Closures & incidents", types: ["closure", "incident"] }, ]; const TYPE_SET = new Set(EVENT_TYPES); const CONF_SET = new Set(CONFIDENCE_LEVELS); const SK_SET = new Set(SOURCE_KINDS); const SLUG = /^[a-z0-9][a-z0-9_.-]*$/i; export function parseLiveQuery(sp: Record): LiveQuery2 { const one = (k: string) => { const v = sp[k]; const s = Array.isArray(v) ? v[0] : v; return s === undefined || s === "" ? undefined : s; }; const q: LiveQuery2 = {}; const types = new Set(); for (const t of one("type")?.split(",") ?? []) if (TYPE_SET.has(t)) types.add(t); for (const f of one("family")?.split(",") ?? []) for (const t of FAMILIES.find((x) => x.key === f)?.types ?? []) types.add(t); if (types.size) q.type = Array.from(types).join(","); const et = one("entity_type"); if (et && SLUG.test(et)) q.entity_type = et; const eid = one("entity_id"); if (eid && SLUG.test(eid)) q.entity_id = eid; const c = one("country")?.toUpperCase(); if (c && /^[A-Z]{2}$/.test(c)) q.country = c; for (const k of ["metro", "operator", "project"] as const) { const v = one(k); if (v && SLUG.test(v)) q[k] = v; } const conf = one("confidence") ?.split(",") .filter((x) => CONF_SET.has(x)) .join(","); if (conf) q.confidence = conf; const sig = one("significance"); if (sig === "major" || sig === "medium" || sig === "minor") q.significance = sig; const ms = Number(one("min_significance")); if (Number.isFinite(ms) && ms > 0) q.min_significance = Math.min(100, Math.floor(ms)); const sk = one("source_kind") ?.split(",") .filter((x) => SK_SET.has(x)) .join(","); if (sk) q.source_kind = sk; const ai = one("ai"); if (ai === "1" || ai === "true") q.ai = true; const dd = one("dedupe"); if (dd === "0" || dd === "false") q.dedupe = false; const all = one("all"); if (all === "1" || all === "true") q.all = true; return q; } /** URL for the live page from a query (defaults skipped). */ export function liveHref(q: LiveQuery2): string { const p = new URLSearchParams(); if (q.type) p.set("type", q.type); if (q.entity_type) p.set("entity_type", q.entity_type); if (q.entity_id) p.set("entity_id", q.entity_id); if (q.country) p.set("country", q.country); if (q.metro) p.set("metro", q.metro); if (q.operator) p.set("operator", q.operator); if (q.project) p.set("project", q.project); if (q.confidence) p.set("confidence", q.confidence); if (q.significance) p.set("significance", q.significance); if (q.min_significance) p.set("min_significance", String(q.min_significance)); if (q.source_kind) p.set("source_kind", q.source_kind); if (q.ai) p.set("ai", "1"); if (q.dedupe === false) p.set("dedupe", "0"); if (q.all) p.set("all", "1"); return routes.live(p.toString() || undefined); } /** Parameters sent to GET /events (the "hide minor" default becomes min_significance). */ export function liveApiQuery(q: LiveQuery2): Query { const hideMinor = !q.all && !q.significance && !q.min_significance; return { type: q.type, entity_type: q.entity_type, entity_id: q.entity_id, country: q.country, metro: q.metro, operator: q.operator, project: q.project, confidence: q.confidence, significance: q.significance, min_significance: hideMinor ? MINOR_THRESHOLD : q.min_significance, source_kind: q.source_kind, ai: q.ai ? true : undefined, dedupe: q.dedupe === false ? 0 : undefined, }; } export function countLiveFilters(q: LiveQuery2): number { 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; } export type { ConfidenceLevel, SourceKind };