export function fmtInt(n: number | string | null | undefined): string { const v = typeof n === "string" ? Number(n) : n; if (v === null || v === undefined || Number.isNaN(v)) return "—"; return new Intl.NumberFormat("en-US").format(Math.round(v)); } export function fmtCompact(n: number | string | null | undefined): string { const v = typeof n === "string" ? Number(n) : n; if (v === null || v === undefined || Number.isNaN(v)) return "—"; return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(v); } export function fmtBytes(n: number | string | null | undefined): string { const v = typeof n === "string" ? Number(n) : n; if (v === null || v === undefined || Number.isNaN(v)) return "—"; const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; let x = v; while (x >= 1024 && i < units.length - 1) { x /= 1024; i++; } return `${x.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } export function fmtScore(n: number | null | undefined): string { if (n === null || n === undefined || Number.isNaN(n)) return "—"; return n >= 99.95 ? "100" : n.toFixed(n >= 10 ? 0 : 1); } export function fmtPct(n: number | null | undefined, digits = 0): string { if (n === null || n === undefined || Number.isNaN(n)) return "—"; return `${(n * 100).toFixed(digits)}%`; } export function fmtMs(ms: number | null | undefined): string { if (ms === null || ms === undefined || Number.isNaN(ms)) return "—"; if (ms < 1000) return `${Math.round(ms)} ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`; if (ms < 3_600_000) return `${Math.round(ms / 60_000)} min`; if (ms < 86_400_000) return `${(ms / 3_600_000).toFixed(1)} h`; return `${(ms / 86_400_000).toFixed(1)} d`; } export function fmtDuration(seconds: number | null | undefined): string { if (seconds === null || seconds === undefined) return "—"; if (seconds < 60) return `${seconds}s`; if (seconds < 3600) return `${Math.round(seconds / 60)}m`; if (seconds < 86400) return `${(seconds / 3600).toFixed(1)}h`; return `${(seconds / 86400).toFixed(1)}d`; } const pad = (n: number): string => String(n).padStart(2, "0"); export function utcTime(iso: string | Date | null | undefined, seconds = true): string { if (!iso) return "—"; const d = typeof iso === "string" ? new Date(iso) : iso; if (Number.isNaN(d.getTime())) return "—"; return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}${seconds ? ":" + pad(d.getUTCSeconds()) : ""}`; } export function utcDate(iso: string | Date | null | undefined): string { if (!iso) return "—"; const d = typeof iso === "string" ? new Date(iso) : iso; if (Number.isNaN(d.getTime())) return "—"; return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`; } export function utcDateTime(iso: string | Date | null | undefined): string { if (!iso) return "—"; return `${utcDate(iso)} ${utcTime(iso)} UTC`; } const MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]; export function dayHeader(iso: string): string { const d = new Date(iso); return `${MONTHS[d.getUTCMonth()]} ${pad(d.getUTCDate())} · ${d.getUTCFullYear()}`; } export function relTime(iso: string | Date | null | undefined, now = Date.now()): string { if (!iso) return ""; const t = (typeof iso === "string" ? new Date(iso) : iso).getTime(); if (Number.isNaN(t)) return ""; const s = Math.max(0, Math.round((now - t) / 1000)); if (s < 5) return "just now"; if (s < 60) return `${s}s ago`; const m = Math.round(s / 60); if (m < 60) return `${m}m ago`; const h = Math.round(m / 60); if (h < 48) return `${h}h ago`; const d = Math.round(h / 24); if (d < 30) return `${d}d ago`; return `${Math.round(d / 30)}mo ago`; } export function untilTime(iso: string | null | undefined, now = Date.now()): string { if (!iso) return "—"; const t = new Date(iso).getTime(); const s = Math.round((t - now) / 1000); if (s <= 0) return "due"; if (s < 60) return `in ${s}s`; if (s < 3600) return `in ${Math.round(s / 60)}m`; return `in ${(s / 3600).toFixed(1)}h`; } export function importanceBand(score: number): "hot" | "high" | "mid" | "low" { if (score >= 90) return "hot"; if (score >= 75) return "high"; if (score >= 50) return "mid"; return "low"; } export function typeLabel(t: string): string { return t.replace(/_/g, " "); } export function hostOf(url: string): string { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; } } export function shortHash(h: string | null | undefined, n = 12): string { return h ? h.slice(0, n) : "—"; } export const CHANNELS: { key: string; label: string; query: Record; ws: string }[] = [ { key: "all", label: "Everything", query: {}, ws: "events:global" }, { key: "breaking", label: "Breaking", query: { importance_min: 80 }, ws: "events:breaking" }, { key: "ai", label: "AI", query: { category: "ai" }, ws: "events:ai" }, { key: "cyber", label: "Cyber", query: { category: "cyber" }, ws: "events:cyber" }, { key: "finance", label: "Markets", query: { category: "finance" }, ws: "events:finance" }, { key: "health", label: "Healthcare", query: { category: "health" }, ws: "events:health" }, { key: "government", label: "Government", query: { category: "government" }, ws: "events:government" }, { key: "science", label: "Science", query: { category: "science" }, ws: "events:science" }, { key: "products", label: "Products", query: { category: "products" }, ws: "events:products" }, { key: "infrastructure", label: "Infrastructure", query: { category: "infrastructure" }, ws: "events:infrastructure" }, { key: "news", label: "News", query: { category: "news" }, ws: "events:news" }, { key: "silent", label: "Silent", query: { silent_change: true }, ws: "events:silent" }, ]; export const CHANNEL_KEYS = ["ai", "cyber", "finance", "health", "government", "science", "products", "infrastructure", "news"] as const; // --------------------------------------------------------------------------------------- // 0.2 additions // --------------------------------------------------------------------------------------- /** Lead time / offsets: "+14 s", "+2 min", "+3.2 h" */ export function fmtOffset(ms: number | null | undefined): string { if (ms === null || ms === undefined || Number.isNaN(ms)) return "—"; const sign = ms < 0 ? "−" : "+"; const a = Math.abs(ms); if (a < 1000) return `${sign}${Math.round(a)} ms`; if (a < 60_000) return `${sign}${Math.round(a / 1000)} s`; if (a < 3_600_000) return `${sign}${Math.round(a / 60_000)} min`; if (a < 86_400_000) return `${sign}${(a / 3_600_000).toFixed(1)} h`; return `${sign}${(a / 86_400_000).toFixed(1)} d`; } export function fmtPctDelta(pct: number | null | undefined): string { if (pct === null || pct === undefined || Number.isNaN(pct)) return ""; return `${pct > 0 ? "+" : ""}${Math.abs(pct) >= 100 ? Math.round(pct) : pct}%`; } export function signalBand(score: number | null | undefined): "hot" | "high" | "mid" | "low" { return importanceBand(score ?? 0); } export function localDateTime(iso: string | Date | null | undefined): string { if (!iso) return "—"; const d = typeof iso === "string" ? new Date(iso) : iso; if (Number.isNaN(d.getTime())) return "—"; 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); } export const GROUP_LABELS: Record = { security: "Security", reliability: "Reliability", product: "Product & API", commercial: "Pricing & terms", corporate: "Corporate", government: "Government & legal", science: "Science & health", transport: "Transport", sports: "Sports", web: "Web" }; export const CLASS_LABELS: Record = { meaningful: "Content", pricing: "Pricing", policy: "Policy", product: "Product", personnel: "Personnel", cosmetic: "Cosmetic", navigation: "Navigation", timestamp: "Timestamp", advertisement: "Advertising", boilerplate: "Boilerplate" }; export function stateLabel(s: string | null | undefined): string { switch (s) { case "breaking": return "BREAKING"; case "developing": return "DEVELOPING"; case "confirmed": return "CONFIRMED"; case "watching": return "WATCHING"; case "closed": return "CLOSED"; default: return ""; } } /** Build the live-feed URL for a filter set (spec §99). */ export function feedHref(q: Record, base = "/live"): string { const p = new URLSearchParams(); for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== "" && v !== false) p.set(k, String(v)); const s = p.toString(); return s ? `${base}?${s}` : base; } export const SAVED_VIEWS: { key: string; label: string; query: Record }[] = [ { key: "ai-releases", label: "AI releases", query: { category: "ai", event_type: "model_release,software_release,product_launch,API_change,api_change" } }, { key: "cyber-critical", label: "Cyber critical", query: { group: "security", signal_min: 70 } }, { key: "canada-gov", label: "Canadian government", query: { country: "CA", category: "government" } }, { key: "market-breaking", label: "Market breaking", query: { category: "finance", signal_min: 75 } }, { key: "cloud-outages", label: "Cloud outages", query: { category: "infrastructure", group: "reliability" } }, { key: "silent-pricing", label: "Silent pricing & terms", query: { silent_change: true, group: "commercial" } }, { key: "first-party-confirmed", label: "First-party & confirmed", query: { first_party: true, confirmed: true } }, ]; /** ISO timestamp `ms` milliseconds ago — keeps `Date.now()` out of component bodies (react-hooks/purity). */ export function agoIso(ms: number): string { return new Date(Date.now() - ms).toISOString(); } /** True when `iso` falls within the last `windowMs` milliseconds. */ export function withinLast(iso: string | null | undefined, windowMs: number): boolean { if (!iso) return false; const t = new Date(iso).getTime(); return Number.isFinite(t) && Date.now() - t < windowMs; }