SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
5 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
5.9 KB · 137 lines typescript
Raw Blame History
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