/* ============================================================
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 (
);
}
/* ---------- filter bar -------------------------------------- */
function EvSelect({ value, onChange, options, placeholder, mono }) {
return (
onChange(e.target.value)}
>
{placeholder}
{options.map((o) => (
{o.label}
))}
);
}
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) => (
toggleEnv(env)}
>{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 && (
setF({ feature: '', step: '', team: '', brand: '' })}>
Clear filters
)}
);
}
/* ---------- env summary cards ------------------------------- */
function EvEnvCard({ env, state, onRetry }) {
const cls = "ev-envcard";
if (state.loading) {
return (
);
}
if (state.error) {
return (
{env}
unavailable
{state.error}
↻ Retry
);
}
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 (
onSelect(isSel ? null : { env: row.env, feature: row.feature, incident: ic })}
/>
);
})}
))}
);
}
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
{copied ? 'Copied' : 'Copy'}
{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 (
{series.map((n, i) => (
0 ? Math.max(12, (n / max) * 100) : 4) + '%', background: n > 0 ? color : 'var(--tds-border-subtle)' }} />
))}
);
}
/* ---------- 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
?
›
:
}
{b.label}
{b.sub}
{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 (
onDrill({ env, backendId: b.id, category: null })}
>
{isLoading ? : isErr ? '—' : n === 0 ? · : {n} }
);
})}
{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 (
onDrill({ env, backendId: b.id, category: c })}
>
{isLoading ? : isErr ? '—' : n === 0 || n === null ? · : {n} }
);
})}
);
})}
);
})}
);
}
/* ---------- 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)
copy(g.message, 'pat:' + i)}>
{copied ? 'Copied' : 'Copy'}
{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 (
);
}
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,
});