export function fmtNum(n: number | string | null | undefined, digits = 0): string { if (n === null || n === undefined || n === "") return "—"; const v = Number(n); if (Number.isNaN(v)) return String(n); if (Math.abs(v) >= 1e9) return (v / 1e9).toFixed(1) + "B"; if (Math.abs(v) >= 1e6) return (v / 1e6).toFixed(1) + "M"; if (Math.abs(v) >= 1e4) return (v / 1e3).toFixed(1) + "k"; return v.toLocaleString("en-US", { maximumFractionDigits: digits }); } export function fmtPct(n: number | null | undefined, digits = 0): string { if (n === null || n === undefined || Number.isNaN(Number(n))) return "—"; return `${(Number(n) * 100).toFixed(digits)}%`; } export function fmtDuration(s: number | null | undefined): string { if (!s && s !== 0) return "—"; const v = Math.round(Number(s)); const h = Math.floor(v / 3600); const m = Math.floor((v % 3600) / 60); const sec = v % 60; return h ? `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}` : `${m}:${String(sec).padStart(2, "0")}`; } export function fmtTime(ts: string | Date | null | undefined): string { if (!ts) return "—"; const d = new Date(ts); return d.toLocaleTimeString("en-CA", { hour12: false }); } export function fmtDate(ts: string | Date | null | undefined): string { if (!ts) return "—"; const d = new Date(ts); return d.toLocaleString("en-CA", { hour12: false, month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }); } export function ago(ts: string | Date | null | undefined): string { if (!ts) return "—"; const diff = (Date.now() - new Date(ts).getTime()) / 1000; if (diff < 60) return `${Math.max(0, Math.round(diff))}s ago`; if (diff < 3600) return `${Math.round(diff / 60)}m ago`; if (diff < 86400) return `${Math.round(diff / 3600)}h ago`; return `${Math.round(diff / 86400)}d ago`; } export function truncate(s: string | null | undefined, n: number): string { if (!s) return ""; return s.length > n ? s.slice(0, n - 1) + "…" : s; } export const TYPE_COLORS: Record = { video: "#38e1ff", channel: "#9d7bff", profile: "#9d7bff", person: "#9d7bff", post: "#ffb347", comment: "#ff7ad9", community: "#43e69a", page: "#43e69a", hashtag: "#6b7a8c", image: "#38e1ff", organization: "#43e69a", }; export const EVENT_COLORS: Record = { ACTION_PLANNED: "#ffb347", ACTION_EXECUTED: "#43e69a", ACTION_FAILED: "#ff5c7a", PAGE_OPENED: "#38e1ff", NETWORK_SCHEMA_DISCOVERED: "#9d7bff", CONNECTOR_PATTERN_LEARNED: "#9d7bff", CONNECTOR_DEGRADED: "#ff5c7a", CONNECTOR_REPAIRED: "#43e69a", LOOP_DETECTED: "#ff5c7a", AUTH_REQUIRED: "#ff5c7a", MEDIA_DISCOVERED: "#ff7ad9", VIDEO_DISCOVERED: "#38e1ff", POST_DISCOVERED: "#ffb347", PROFILE_DISCOVERED: "#9d7bff", }; export function eventColor(t: string): string { return EVENT_COLORS[t] ?? "#6b7a8c"; }