SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
10.1 KB · 228 lines typescript
Raw Blame History
1export function fmtInt(n: number | string | null | undefined): string {2  const v = typeof n === "string" ? Number(n) : n;3  if (v === null || v === undefined || Number.isNaN(v)) return "—";4  return new Intl.NumberFormat("en-US").format(Math.round(v));5}67export function fmtCompact(n: number | string | null | undefined): string {8  const v = typeof n === "string" ? Number(n) : n;9  if (v === null || v === undefined || Number.isNaN(v)) return "—";10  return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(v);11}1213export function fmtBytes(n: number | string | null | undefined): string {14  const v = typeof n === "string" ? Number(n) : n;15  if (v === null || v === undefined || Number.isNaN(v)) return "—";16  const units = ["B", "KB", "MB", "GB", "TB"];17  let i = 0;18  let x = v;19  while (x >= 1024 && i < units.length - 1) {20    x /= 1024;21    i++;22  }23  return `${x.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;24}2526export function fmtScore(n: number | null | undefined): string {27  if (n === null || n === undefined || Number.isNaN(n)) return "—";28  return n >= 99.95 ? "100" : n.toFixed(n >= 10 ? 0 : 1);29}3031export function fmtPct(n: number | null | undefined, digits = 0): string {32  if (n === null || n === undefined || Number.isNaN(n)) return "—";33  return `${(n * 100).toFixed(digits)}%`;34}3536export function fmtMs(ms: number | null | undefined): string {37  if (ms === null || ms === undefined || Number.isNaN(ms)) return "—";38  if (ms < 1000) return `${Math.round(ms)} ms`;39  if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`;40  if (ms < 3_600_000) return `${Math.round(ms / 60_000)} min`;41  if (ms < 86_400_000) return `${(ms / 3_600_000).toFixed(1)} h`;42  return `${(ms / 86_400_000).toFixed(1)} d`;43}4445export function fmtDuration(seconds: number | null | undefined): string {46  if (seconds === null || seconds === undefined) return "—";47  if (seconds < 60) return `${seconds}s`;48  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;49  if (seconds < 86400) return `${(seconds / 3600).toFixed(1)}h`;50  return `${(seconds / 86400).toFixed(1)}d`;51}5253const pad = (n: number): string => String(n).padStart(2, "0");5455export function utcTime(iso: string | Date | null | undefined, seconds = true): string {56  if (!iso) return "—";57  const d = typeof iso === "string" ? new Date(iso) : iso;58  if (Number.isNaN(d.getTime())) return "—";59  return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}${seconds ? ":" + pad(d.getUTCSeconds()) : ""}`;60}6162export function utcDate(iso: string | Date | null | undefined): string {63  if (!iso) return "—";64  const d = typeof iso === "string" ? new Date(iso) : iso;65  if (Number.isNaN(d.getTime())) return "—";66  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;67}6869export function utcDateTime(iso: string | Date | null | undefined): string {70  if (!iso) return "—";71  return `${utcDate(iso)} ${utcTime(iso)} UTC`;72}7374const MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];75export function dayHeader(iso: string): string {76  const d = new Date(iso);77  return `${MONTHS[d.getUTCMonth()]} ${pad(d.getUTCDate())} · ${d.getUTCFullYear()}`;78}7980export function relTime(iso: string | Date | null | undefined, now = Date.now()): string {81  if (!iso) return "";82  const t = (typeof iso === "string" ? new Date(iso) : iso).getTime();83  if (Number.isNaN(t)) return "";84  const s = Math.max(0, Math.round((now - t) / 1000));85  if (s < 5) return "just now";86  if (s < 60) return `${s}s ago`;87  const m = Math.round(s / 60);88  if (m < 60) return `${m}m ago`;89  const h = Math.round(m / 60);90  if (h < 48) return `${h}h ago`;91  const d = Math.round(h / 24);92  if (d < 30) return `${d}d ago`;93  return `${Math.round(d / 30)}mo ago`;94}9596export function untilTime(iso: string | null | undefined, now = Date.now()): string {97  if (!iso) return "—";98  const t = new Date(iso).getTime();99  const s = Math.round((t - now) / 1000);100  if (s <= 0) return "due";101  if (s < 60) return `in ${s}s`;102  if (s < 3600) return `in ${Math.round(s / 60)}m`;103  return `in ${(s / 3600).toFixed(1)}h`;104}105106export function importanceBand(score: number): "hot" | "high" | "mid" | "low" {107  if (score >= 90) return "hot";108  if (score >= 75) return "high";109  if (score >= 50) return "mid";110  return "low";111}112113export function typeLabel(t: string): string {114  return t.replace(/_/g, " ");115}116117export function hostOf(url: string): string {118  try {119    return new URL(url).hostname.replace(/^www\./, "");120  } catch {121    return url;122  }123}124125export function shortHash(h: string | null | undefined, n = 12): string {126  return h ? h.slice(0, n) : "—";127}128129export const CHANNELS: { key: string; label: string; query: Record<string, string | number | boolean>; ws: string }[] = [130  { key: "all", label: "Everything", query: {}, ws: "events:global" },131  { key: "breaking", label: "Breaking", query: { importance_min: 80 }, ws: "events:breaking" },132  { key: "ai", label: "AI", query: { category: "ai" }, ws: "events:ai" },133  { key: "cyber", label: "Cyber", query: { category: "cyber" }, ws: "events:cyber" },134  { key: "finance", label: "Markets", query: { category: "finance" }, ws: "events:finance" },135  { key: "health", label: "Healthcare", query: { category: "health" }, ws: "events:health" },136  { key: "government", label: "Government", query: { category: "government" }, ws: "events:government" },137  { key: "science", label: "Science", query: { category: "science" }, ws: "events:science" },138  { key: "products", label: "Products", query: { category: "products" }, ws: "events:products" },139  { key: "infrastructure", label: "Infrastructure", query: { category: "infrastructure" }, ws: "events:infrastructure" },140  { key: "news", label: "News", query: { category: "news" }, ws: "events:news" },141  { key: "silent", label: "Silent", query: { silent_change: true }, ws: "events:silent" },142];143144export const CHANNEL_KEYS = ["ai", "cyber", "finance", "health", "government", "science", "products", "infrastructure", "news"] as const;145146// ---------------------------------------------------------------------------------------147// 0.2 additions148// ---------------------------------------------------------------------------------------149150/** Lead time / offsets: "+14 s", "+2 min", "+3.2 h" */151export function fmtOffset(ms: number | null | undefined): string {152  if (ms === null || ms === undefined || Number.isNaN(ms)) return "—";153  const sign = ms < 0 ? "−" : "+";154  const a = Math.abs(ms);155  if (a < 1000) return `${sign}${Math.round(a)} ms`;156  if (a < 60_000) return `${sign}${Math.round(a / 1000)} s`;157  if (a < 3_600_000) return `${sign}${Math.round(a / 60_000)} min`;158  if (a < 86_400_000) return `${sign}${(a / 3_600_000).toFixed(1)} h`;159  return `${sign}${(a / 86_400_000).toFixed(1)} d`;160}161162export function fmtPctDelta(pct: number | null | undefined): string {163  if (pct === null || pct === undefined || Number.isNaN(pct)) return "";164  return `${pct > 0 ? "+" : ""}${Math.abs(pct) >= 100 ? Math.round(pct) : pct}%`;165}166167export function signalBand(score: number | null | undefined): "hot" | "high" | "mid" | "low" {168  return importanceBand(score ?? 0);169}170171export function localDateTime(iso: string | Date | null | undefined): string {172  if (!iso) return "—";173  const d = typeof iso === "string" ? new Date(iso) : iso;174  if (Number.isNaN(d.getTime())) return "—";175  return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: "short" }).format(d);176}177178export const GROUP_LABELS: Record<string, string> = { security: "Security", reliability: "Reliability", product: "Product & API", commercial: "Pricing & terms", corporate: "Corporate", government: "Government & legal", science: "Science & health", transport: "Transport", sports: "Sports", web: "Web" };179180export const CLASS_LABELS: Record<string, string> = { meaningful: "Content", pricing: "Pricing", policy: "Policy", product: "Product", personnel: "Personnel", cosmetic: "Cosmetic", navigation: "Navigation", timestamp: "Timestamp", advertisement: "Advertising", boilerplate: "Boilerplate" };181182export function stateLabel(s: string | null | undefined): string {183  switch (s) {184    case "breaking":185      return "BREAKING";186    case "developing":187      return "DEVELOPING";188    case "confirmed":189      return "CONFIRMED";190    case "watching":191      return "WATCHING";192    case "closed":193      return "CLOSED";194    default:195      return "";196  }197}198199/** Build the live-feed URL for a filter set (spec §99). */200export function feedHref(q: Record<string, string | number | boolean | undefined | null>, base = "/live"): string {201  const p = new URLSearchParams();202  for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== "" && v !== false) p.set(k, String(v));203  const s = p.toString();204  return s ? `${base}?${s}` : base;205}206207export const SAVED_VIEWS: { key: string; label: string; query: Record<string, string | number | boolean> }[] = [208  { key: "ai-releases", label: "AI releases", query: { category: "ai", event_type: "model_release,software_release,product_launch,API_change,api_change" } },209  { key: "cyber-critical", label: "Cyber critical", query: { group: "security", signal_min: 70 } },210  { key: "canada-gov", label: "Canadian government", query: { country: "CA", category: "government" } },211  { key: "market-breaking", label: "Market breaking", query: { category: "finance", signal_min: 75 } },212  { key: "cloud-outages", label: "Cloud outages", query: { category: "infrastructure", group: "reliability" } },213  { key: "silent-pricing", label: "Silent pricing & terms", query: { silent_change: true, group: "commercial" } },214  { key: "first-party-confirmed", label: "First-party & confirmed", query: { first_party: true, confirmed: true } },215];216217/** ISO timestamp `ms` milliseconds ago — keeps `Date.now()` out of component bodies (react-hooks/purity). */218export function agoIso(ms: number): string {219  return new Date(Date.now() - ms).toISOString();220}221222/** True when `iso` falls within the last `windowMs` milliseconds. */223export function withinLast(iso: string | null | undefined, windowMs: number): boolean {224  if (!iso) return false;225  const t = new Date(iso).getTime();226  return Number.isFinite(t) && Date.now() - t < windowMs;227}228