/* ============================================================ TDS Errors view — presentational components Shares the dashboard's visual vocabulary: tds-* atoms, fp-* failure patterns, cat-* category badges, drawers/tabs. New classes are prefixed ev- (see errors.css). ============================================================ */ const { useState: evUseState, useMemo: evUseMemo, useEffect: evUseEffect } = React; /* ---------- meta ------------------------------------------- */ const EV_CATEGORY_META = { BSS_VALIDATION: { label: 'Validation', full: 'BSS Validation', cls: 'cat-bss-v', color: '#c6800c' }, BSS_UNAVAILABLE: { label: 'Unavailable', full: 'BSS Unavailable', cls: 'cat-bss-u', color: '#c4322a' }, BSS_TIMEOUT: { label: 'Timeout', full: 'BSS Timeout', cls: 'cat-bss-t', color: '#e8b007' }, ITDM_INTERNAL: { label: 'Internal', full: 'iTDM Internal', cls: 'cat-itdm-i', color: '#7c5cd6' }, ITDM_UNAVAILABLE: { label: 'Unavailable', full: 'iTDM Unavailable', cls: 'cat-itdm-u', color: '#e0524a' }, AUTH_EXPIRED: { label: 'Token expired', full: 'Auth Expired', cls: 'cat-auth', color: '#d6589c' }, NETWORK_ERROR: { label: 'Connection', full: 'Network Error', cls: 'cat-net', color: '#3b82d6' }, PROVISIONING: { label: 'Provisioning', full: 'NC Provisioning', cls: 'cat-prov', color: '#0d9488' }, ORDER_TIMEOUT: { label: 'Order stuck', full: 'Order Timeout', cls: 'cat-ordto', color: '#b45309' }, STOCK: { label: 'Stock', full: 'Stock / Logistics', cls: 'cat-stock', color: '#65a30d' }, UNKNOWN: { label: 'Unknown', full: 'Unknown', cls: 'cat-unk', color: '#8d8674' }, }; const EV_STEPS = ['CREATING_CUSTOMER', 'CREATING_ORDER', 'GENERATING_PID']; const EV_STEP_LABEL = { CREATING_CUSTOMER: 'Customer', CREATING_ORDER: 'Order', GENERATING_PID: 'PID' }; /* Upstream systems the creation pipeline depends on. Error categories are grouped per backend so "which backend is failing on which env" is the first thing the section answers. */ const EV_BACKENDS = [ { id: 'BSS', label: 'BSS', sub: 'order & customer system', cats: ['BSS_VALIDATION', 'BSS_UNAVAILABLE', 'BSS_TIMEOUT'] }, { id: 'NC', label: 'Netcracker', sub: 'fulfilment & delivery', cats: ['PROVISIONING', 'ORDER_TIMEOUT', 'STOCK'] }, { id: 'ITDM', label: 'iTDM', sub: 'test-order engine', cats: ['ITDM_INTERNAL', 'ITDM_UNAVAILABLE'] }, { id: 'AUTH', label: 'Auth · Okta', sub: 'service tokens', cats: ['AUTH_EXPIRED'] }, { id: 'NET', label: 'Network', sub: 'transport between systems', cats: ['NETWORK_ERROR'] }, { id: 'UNC', label: 'Uncategorized', sub: 'unparsed upstream errors', cats: ['UNKNOWN'] }, ]; function evCat(c) { return EV_CATEGORY_META[c] || EV_CATEGORY_META.UNKNOWN; } /* ---------- time helpers (UTC source → local render) -------- */ const EV_TZ = (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { return 'local'; } })(); function evFmtTime(ts) { const d = new Date(ts); return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function evFmtDay(ts) { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } function evRelTime(ts) { if (!ts) return '—'; const mins = Math.round((Date.now() - new Date(ts).getTime()) / 60000); if (mins < 2) return 'just now'; if (mins < 60) return mins + ' min ago'; const h = Math.floor(mins / 60); if (h < 24) return h + 'h ago'; return Math.floor(h / 24) + 'd ago'; } function evDuration(hours) { if (hours == null) return '—'; if (hours < 1) return Math.round(hours * 60) + ' min'; if (hours < 48) return Math.round(hours * 10) / 10 + 'h'; return Math.floor(hours / 24) + 'd ' + Math.round(hours % 24) + 'h'; } /* ---------- range picker ------------------------------------ */ function EvRangePicker({ rangeId, customFrom, customTo, range, onChange }) { const api = window.TDS_ERRORS_API; const maxDate = new Date().toISOString().slice(0, 10); const minDate = new Date(Date.now() - api.RETENTION_DAYS * 86400000).toISOString().slice(0, 10); return (
{api.RANGE_PRESETS.map((p) => ( ))}
{rangeId === 'custom' && (
onChange({ customFrom: e.target.value })} aria-label="From date" /> → onChange({ customTo: e.target.value })} aria-label="To date" />
)} {evFmtTime(range.from)} – {evFmtTime(range.to)} · {EV_TZ} {range.clamped && · clipped to {window.TDS_ERRORS_API.RETENTION_DAYS}-day retention}
); } /* ---------- filter bar -------------------------------------- */ function EvSelect({ value, onChange, options, placeholder, mono }) { return ( ); } function EvFilterBar({ filters, setF, features, teams }) { const ENVS = window.TDS_ERRORS_API.ENVS; const activeEnvs = filters.envs; const toggleEnv = (env) => { const next = activeEnvs.includes(env) ? activeEnvs.filter((e) => e !== env) : [...activeEnvs, env]; if (next.length === 0) return; // never zero envs setF({ envs: next }); }; const hasAny = filters.feature || filters.step || filters.team || filters.brand; return (
{ENVS.map((env) => ( ))}
setF({ feature: v })} mono options={features.map((f) => ({ value: f, label: f }))} placeholder="Feature: all" /> setF({ step: v })} options={EV_STEPS.map((s) => ({ value: s, label: EV_STEP_LABEL[s] + ' step' }))} placeholder="Step: all" /> setF({ team: v })} options={teams.map((t) => ({ value: t.name, label: t.displayName + ' (' + t.name + ')' }))} placeholder="Team: all" /> setF({ brand: v })} options={[{ value: 'TELENET', label: 'Telenet' }, { value: 'BASE', label: 'BASE' }]} placeholder="Brand: all" /> {hasAny && ( )}
); } /* ---------- env summary cards ------------------------------- */ function EvEnvCard({ env, state, onRetry }) { const cls = "ev-envcard"; if (state.loading) { return (
{env}
); } if (state.error) { return (
{env} unavailable
{state.error}
); } const d = state.data; if (!d) return null; // Real FailureReport shape: windowedFailures (count in [from,to]) and // byCategory: { CATEGORY: { count, firstSeen, lastSeen, topErrorMessage } }. const total = d.failures ? (d.failures.windowedFailures || 0) : 0; const incs = d.incidents ? d.incidents.incidents : []; const ongoing = incs.filter((i) => i.ongoing).length; const incHours = incs.reduce((s, i) => s + (i.durationHours || 0), 0); const topCat = d.failures && Object.entries(d.failures.byCategory || {}) .sort((a, b) => (b[1].count || 0) - (a[1].count || 0))[0]; return (
0 ? ' has-ongoing' : '')}>
{env} {ongoing > 0 && {ongoing} ongoing}
{total} errors
{incs.length} incident{incs.length !== 1 ? 's' : ''} · {evDuration(incHours)} broken {topCat ? {evCat(topCat[0]).full} {topCat[1].count}× : no errors recorded}
); } /* ---------- incident timeline (gantt) ------------------------ */ function EvTimeline({ range, rows, selected, onSelect, loading }) { const span = range.to - range.from; const pct = (t) => Math.max(0, Math.min(100, ((t - range.from) / span) * 100)); const ticks = evUseMemo(() => { const out = []; const dayMs = 86400000; const days = Math.max(1, Math.round(span / dayMs)); const step = Math.max(1, Math.ceil(days / 6)); const first = new Date(range.from); first.setHours(0, 0, 0, 0); for (let t = first.getTime(); t <= range.to; t += step * dayMs) { if (t >= range.from) out.push(t); } return out; }, [range.from, range.to]); if (loading && rows.length === 0) { return (
{[0, 1, 2].map((i) => (
))}
); } if (rows.length === 0) { return
No availability incidents in this window.
; } return (
{ticks.map((t) => ( {evFmtDay(t)} ))}
{rows.map((row) => (
{row.env} {row.feature}
{ticks.map((t) => )} {row.incidents.map((ic, i) => { const s = new Date(ic.startedAt).getTime(); const e = ic.endedAt ? new Date(ic.endedAt).getTime() : Date.now(); const left = pct(s); const width = Math.max(0.6, pct(e) - left); const isSel = selected && selected.env === row.env && selected.incident === ic; const color = ic.rootCause ? evCat(ic.rootCause).color : 'var(--tds-border-strong)'; return (
))}
); } function EvIncidentDetail({ sel, onClose }) { const [copied, setCopied] = evUseState(false); if (!sel) return null; const ic = sel.incident; const copy = () => { try { navigator.clipboard.writeText(ic.sampleMessage || ''); setCopied(true); setTimeout(() => setCopied(false), 1400); } catch (e) {} }; return (
{sel.env} {sel.feature} {ic.ongoing ? ongoing : resolved} {evFmtTime(ic.startedAt)} → {ic.endedAt ? evFmtTime(ic.endedAt) : 'now'} · {evDuration(ic.durationHours)}
Dominant cause {ic.rootCause ? {evCat(ic.rootCause).full} : none — pool was empty but no failure row overlapped (common for short ~06:00 verify-cron windows)}
Creation attempts during outage {ic.attempts}
{ic.sampleMessage && (
Sample error message
{ic.sampleMessage}
)}
); } /* ---------- sparkline ---------------------------------------- */ function EvSpark({ buckets, bucketMs, range, color }) { const series = evUseMemo(() => { if (!bucketMs) return []; const map = new Map(buckets.map((b) => [b.t, b.n])); // The server buckets on a UTC grid (floor(epoch / bucketMs) * bucketMs) // for hourly AND daily buckets — align the reconstructed series the same // way or the lookups all miss. const out = []; const start = Math.floor(range.from / bucketMs) * bucketMs; for (let t = start; t <= range.to; t += bucketMs) out.push(map.get(t) || 0); return out; }, [buckets, bucketMs, range.from, range.to]); const max = Math.max(1, ...series); if (series.length === 0 || series.every((n) => n === 0)) { return
; } return ( ); } /* ---------- backend × environment matrix --------------------- */ /* Answers "which backend is failing on which env" at a glance. Rows: upstream systems (expandable into their error categories). Cells: error counts in window, shaded by share of the worst cell. Trend: per-backend sparkline over the window (selected envs). */ function EvBackendMatrix({ envs, perEnv, range, onDrill, drill }) { const [expanded, setExpanded] = evUseState({}); const cellCount = (env, cats) => { const d = perEnv[env]; if (!d || d.loading || d.error || !d.byCategory) return null; return cats.reduce((s, c) => s + (d.byCategory[c] || 0), 0); }; const maxCell = evUseMemo(() => { let m = 0; EV_BACKENDS.forEach((b) => envs.forEach((env) => { m = Math.max(m, cellCount(env, b.cats) || 0); })); return Math.max(m, 1); }, [perEnv, envs.join(',')]); const heat = (n) => { if (n === null) return 'is-na'; if (n === 0) return 'is-0'; if (n >= maxCell * 0.6) return 'is-3'; if (n >= maxCell * 0.25) return 'is-2'; return 'is-1'; }; const sparkFor = (cats) => { const merged = new Map(); let bucketMs = null; envs.forEach((env) => { const d = perEnv[env]; if (!d || !d.histogram) return; bucketMs = d.bucketMs; d.histogram.forEach((b) => { const n = cats.reduce((s, c) => s + (b.byCategory[c] || 0), 0); if (n > 0) merged.set(b.t, (merged.get(b.t) || 0) + n); }); }); return { buckets: Array.from(merged.entries()).map(([t, n]) => ({ t, n })), bucketMs }; }; const anyLoading = envs.some((env) => !perEnv[env] || perEnv[env].loading); return (
Backend
{envs.map((env) => (
{env} {perEnv[env] && perEnv[env].error && ⚠}
))}
Trend
{EV_BACKENDS.map((b) => { const isExpanded = !!expanded[b.id]; const rowTotal = envs.reduce((s, env) => s + (cellCount(env, b.cats) || 0), 0); const spark = sparkFor(b.cats); const dominantColor = (() => { const sums = b.cats.map((c) => [c, envs.reduce((s, env) => s + ((perEnv[env] && perEnv[env].byCategory && perEnv[env].byCategory[c]) || 0), 0)]); sums.sort((x, y) => y[1] - x[1]); return evCat(sums[0][0]).color; })(); return (
b.cats.length > 1 && setExpanded((e) => ({ ...e, [b.id]: !e[b.id] }))} role={b.cats.length > 1 ? 'button' : undefined} tabIndex={b.cats.length > 1 ? 0 : undefined} > {b.cats.length > 1 ? :
{envs.map((env) => { const n = cellCount(env, b.cats); const isLoading = !perEnv[env] || perEnv[env].loading; const isErr = !isLoading && perEnv[env].error; const active = drill && drill.env === env && drill.backendId === b.id && !drill.category; return ( ); })}
{anyLoading ?
: }
{isExpanded && b.cats.map((c) => { const spark2 = sparkFor([c]); return (
{evCat(c).label}
{envs.map((env) => { const n = cellCount(env, [c]); const isLoading = !perEnv[env] || perEnv[env].loading; const isErr = !isLoading && perEnv[env].error; const active = drill && drill.env === env && drill.category === c; return ( ); })}
); })} ); })}
); } /* ---------- pattern list (grouped messages) ------------------ */ function EvPatterns({ groups, showEnv }) { const [expanded, setExpanded] = evUseState({}); const [copiedKey, setCopiedKey] = evUseState(null); const copy = (text, key) => { try { navigator.clipboard.writeText(text); } catch (e) {} setCopiedKey(key); setTimeout(() => setCopiedKey(null), 1400); }; if (groups.length === 0) { return
No grouped patterns for this selection.
; } return (
{groups.map((g, i) => { const meta = evCat(g.category); const isOpen = !!expanded[i]; const copied = copiedKey === 'pat:' + i; return (
setExpanded((p) => ({ ...p, [i]: !p[i] }))}> {showEnv && {g.env}} {meta.full} {g.errorCode && {g.errorCode}} {g.count}× {evRelTime(g.lastSeen)} ›
{g.message}
{isOpen && (
Latest raw message (ground truth — categories are best-effort)
{g.message}
First seen: {evRelTime(g.firstSeen)} Last seen: {evRelTime(g.lastSeen)} {g.errorCode && Code: {g.errorCode}}
)}
); })}
); } /* ---------- section header ----------------------------------- */ function EvSection({ title, sub, children, right }) { return (

{title}

{sub &&
{sub}
}
{right}
{children}
); } Object.assign(window, { EV_CATEGORY_META, EV_BACKENDS, EV_STEPS, EV_STEP_LABEL, EV_TZ, evCat, evFmtTime, evFmtDay, evRelTime, evDuration, EvRangePicker, EvFilterBar, EvEnvCard, EvTimeline, EvIncidentDetail, EvSpark, EvBackendMatrix, EvPatterns, EvSection, });