/* global React */
// =======================================================================
// BlazeConnector Admin v3 — Operations pages
// Dashboard · Mensajería · Colas · Logs · Alertas
// =======================================================================
const { useState: _us, useEffect: _ue, useMemo: _um, useRef: _ur, useCallback: _uc } = React;
/* =========================================================================
DASHBOARD
========================================================================= */
function PageDashboard({ tenant, sse, systemHealth }) {
const [tick, setTick] = _us(0);
_ue(() => { const id = setInterval(() => setTick(t => t + 1), 4000); return () => clearInterval(id); }, []);
const isOutage = systemHealth === 'outage';
const isDegraded = systemHealth === 'degraded';
// KPIs adjusted by health
const baseRate = isOutage ? 12.4 : isDegraded ? 3.1 : 0.6;
const baseLat = isOutage ? 1840 : isDegraded ? 720 : 318;
const baseFailed = isOutage ? 412 : isDegraded ? 84 : 18;
const kpis = [
{ label: 'Mensajes / min', value: 1242 + (tick % 5) * 4, unit: '', delta: 3.2, series: window.DASHBOARD_SERIES.msgsPerMin, color: 'orange', foot: 'últimos 60m · ↑ vs ayer' },
{ label: 'Error rate', value: baseRate + '%', unit: '', delta: isOutage ? 412 : isDegraded ? 84 : -12, deltaTone: isOutage || isDegraded ? 'down' : 'up', series: window.DASHBOARD_SERIES.errorsPerMin, color: isOutage ? 'danger' : isDegraded ? 'warn' : 'green', foot: baseFailed + ' fallos / 60m' },
{ label: 'Latencia p95', value: baseLat, unit: 'ms', delta: isOutage ? 240 : isDegraded ? 64 : -8, deltaTone: isOutage || isDegraded ? 'down' : 'up', series: window.DASHBOARD_SERIES.latencyP95, color: isDegraded ? 'warn' : 'blue', foot: 'p99 ' + (baseLat * 2.1).toFixed(0) + 'ms' },
{ label: 'Tenants activos', value: 10, unit: '/ 12', delta: 0, series: [10,10,10,10,11,11,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10], color: 'violet', foot: '2 suspendidos · 0 nuevos hoy' },
{ label: 'Logs / min', value: 2418, unit: '', delta: 8.4, series: window.DASHBOARD_SERIES.inbound, color: 'teal', foot: 'ingest p95 142ms' },
{ label: 'Workers activos', value: 48, unit: '/ 56', delta: 0, series: window.DASHBOARD_SERIES.workerActive, color: 'green', foot: '8 idle · 0 caídos' }
];
const queueDepth = window.QUEUES.reduce((a, q) => a + q.pending + q.active, 0);
const recentActivity = _um(() => window.genMessages(8), [tick]);
return (
Dashboard
Estado operacional · {tenant.id === '*' ? 'todos los tenants' : tenant.name} · actualizado hace {tick * 4}s
{(isOutage || isDegraded) && (
{isOutage ? 'Outage parcial en pagos.cybersource' : 'Degradación en cola messaging — backlog elevado'}
{isOutage ? 'CyberSource no responde desde 17:18. Affected: 1 tenant. Fallback activo: pendiente.' : 'Backlog 1,842 mensajes. p95 ingest +84%. Auto-scale de workers en curso.'}
)}
{kpis.map((k) => )}
>}>
({ value: c.count, color: c.color }))} centerLabel="107k" centerSub="MENSAJES" size={130} />
{window.CHANNEL_BREAKDOWN.map(c => (
{c.label}
{c.count.toLocaleString()}
{c.pct}%
))}
a + q.workers, 0) + ' workers'}>
{window.QUEUES.map(q => {
const total = q.pending + q.active + q.retry;
const max = 2000;
const isCritical = q.pending > 500;
return (
{q.name}
{isCritical && }
{total.toLocaleString()}
{q.throughputPerMin}/m
);
})}
active
pending
retry
!a.ack).length + ' sin reconocer'}
actions={}>
{window.ALERTS.slice(0, 5).map(a => (
{a.title}
{a.source}{a.tenant ? ' · ' + a.tenant : ''} · hace {a.age}
{a.ack ?
ack :
}
))}
Ir a mensajería }>
{window.fmtTime(r.ts)} },
{ label: 'Tenant', render: (r) => {r.tenantName} },
{ label: 'Canal', width: 60, render: (r) => },
{ label: 'Dirección', width: 80, render: (r) => {r.direction} },
{ label: 'Estado', width: 100, render: (r) => },
{ label: 'Destinatario', render: (r) => {r.to || r.from}, faint: true },
{ label: 'Mensaje', render: (r) => {r.preview} },
{ label: 'Latencia', width: 80, numeric: true, render: (r) => {r.latencyMs}ms }
]} rows={recentActivity} />
);
}
/* =========================================================================
MESSAGING
========================================================================= */
function PageMessaging({ tenant }) {
const [messages] = _us(() => window.genMessages(120));
const [selected, setSelected] = _us(null);
const [search, setSearch] = _us('');
const [statusF, setStatusF] = _us('all');
const [channelF, setChannelF] = _us('all');
const [directionF, setDirectionF] = _us('all');
const filtered = _um(() => {
const ql = search.toLowerCase();
return messages.filter(m => {
if (tenant.id !== '*' && m.tenant !== tenant.id) return false;
if (statusF !== 'all' && m.status !== statusF) return false;
if (channelF !== 'all' && m.channel !== channelF) return false;
if (directionF !== 'all' && m.direction !== directionF) return false;
if (ql) {
const blob = (m.to + ' ' + m.from + ' ' + m.contactName + ' ' + m.preview + ' ' + m.id + ' ' + m.trace_id + ' ' + (m.template || '')).toLowerCase();
if (!blob.includes(ql)) return false;
}
return true;
});
}, [messages, tenant, statusF, channelF, directionF, search]);
const stats = _um(() => {
const s = { delivered: 0, sent: 0, queued: 0, failed: 0, read: 0, expired: 0 };
filtered.forEach(m => { s[m.status] = (s[m.status] || 0) + 1; });
return s;
}, [filtered]);
return (
Mensajería
Explorador operacional · {filtered.length.toLocaleString()} mensajes · ventana 60m
{[
{ lbl: 'Total', val: filtered.length, tone: 'neutral' },
{ lbl: 'Delivered', val: stats.delivered, tone: 'success' },
{ lbl: 'Sent', val: stats.sent, tone: 'info' },
{ lbl: 'Queued', val: stats.queued, tone: 'warn' },
{ lbl: 'Failed', val: stats.failed, tone: 'danger' },
{ lbl: 'Read', val: stats.read, tone: 'success' }
].map((s) => (
{s.lbl}
{s.val.toLocaleString()}
))}
setStatusF(statusF === 'all' ? 'failed' : statusF === 'failed' ? 'queued' : statusF === 'queued' ? 'delivered' : 'all')} />
setChannelF(channelF === 'all' ? 'whatsapp' : channelF === 'whatsapp' ? 'telegram' : channelF === 'telegram' ? 'chatwoot' : 'all')} />
setDirectionF(directionF === 'all' ? 'outbound' : directionF === 'outbound' ? 'inbound' : 'all')} />
{window.fmtTime(r.ts)} },
{ label: 'Tenant', width: 160, render: (r) => {r.tenantName} },
{ label: 'Canal', width: 56, render: (r) => },
{ label: 'Dir', width: 56, render: (r) => },
{ label: 'Estado', width: 110, render: (r) => },
{ label: 'Plantilla', width: 180, render: (r) => r.template ? {r.template} : — },
{ label: 'Destinatario', width: 130, render: (r) => {r.to || r.from} },
{ label: 'Contenido', render: (r) => {r.preview} },
{ label: 'Lat.', width: 60, numeric: true, render: (r) => 500 ? 'var(--st-warn-fg)' : 'var(--app-fg2)' }}>{r.latencyMs}ms },
{ label: 'Retries', width: 60, numeric: true, render: (r) => r.retries > 0 ? {r.retries} : 0 },
{ label: 'msg_id', width: 130, render: (r) => {r.id} }
]} rows={filtered} />
{filtered.length.toLocaleString()} mensajes · página 1 / {Math.ceil(filtered.length / 50)}
1
{selected &&
setSelected(null)} />}
);
}
function MessageDetailDrawer({ msg, onClose }) {
const timeline = [
{ ts: msg.ts, label: 'Mensaje creado', meta: 'connector.dispatcher.v3', tone: 'info' },
{ ts: new Date(new Date(msg.ts).getTime() + 80).toISOString(), label: 'Encolado · messaging', meta: 'asynq · weight=4', tone: 'info' },
{ ts: new Date(new Date(msg.ts).getTime() + 142).toISOString(), label: 'Dispatch a Meta Cloud API', meta: 'wabacloud.proxy · POST /messages', tone: 'info' },
{ ts: new Date(new Date(msg.ts).getTime() + msg.latencyMs).toISOString(), label: msg.status === 'failed' ? 'FAILED · rate_limit_exceeded' : msg.status === 'queued' ? 'Esperando aceptación' : 'Aceptado · ' + msg.providerMessageId.slice(0, 16) + '…', meta: 'meta.graph status=' + (msg.status === 'failed' ? '429' : '202') + ' · ' + msg.latencyMs + 'ms', tone: msg.tone },
...(msg.status === 'delivered' || msg.status === 'read'
? [{ ts: new Date(new Date(msg.ts).getTime() + msg.latencyMs + 1200).toISOString(), label: 'Webhook: ' + msg.status, meta: 'msgr.webhook · message_status', tone: 'success' }]
: []),
...(msg.status === 'read'
? [{ ts: new Date(new Date(msg.ts).getTime() + msg.latencyMs + 8400).toISOString(), label: 'Webhook: read', meta: 'msgr.webhook · message_status', tone: 'success' }]
: []),
...(msg.retries > 0
? [{ ts: new Date(new Date(msg.ts).getTime() + 30000).toISOString(), label: 'Retry #' + msg.retries + ' encolado', meta: 'backoff exponential · jitter ±20%', tone: 'warn' }]
: [])
];
return (
Reintentar}
>}>
Resumen
- Estado
- Tenant
- {msg.tenantName}
- Canal
- {window.CHANNEL_LABEL[msg.channel]}
- Dirección
- {msg.direction}
- Provider
- {msg.provider}
- Provider msg id
- {msg.providerMessageId}
- {msg.direction === 'outbound' ? 'Destinatario' : 'Remitente'}
- {msg.to || msg.from} ({msg.contactName})
- Plantilla
- {msg.template || —}
- Latencia
- {msg.latencyMs}ms
- Retries
- {msg.retries}
- trace_id
- {msg.trace_id}
Timeline
{timeline.map((t, i) => (
{t.label}
{window.fmtFullTime(t.ts)}·{t.meta}
))}
);
}
/* =========================================================================
QUEUES
========================================================================= */
function PageQueues({ tenant }) {
const [tick, setTick] = _us(0);
_ue(() => { const id = setInterval(() => setTick(t => t + 1), 3000); return () => clearInterval(id); }, []);
const live = window.QUEUES.map((q, i) => ({ ...q,
active: Math.max(0, q.active + Math.floor(Math.sin(tick + i) * 8)),
throughputPerMin: Math.max(0, q.throughputPerMin + Math.floor((Math.random() - 0.5) * 20))
}));
const totals = live.reduce((a, q) => ({ active: a.active + q.active, pending: a.pending + q.pending, retry: a.retry + q.retry, failed: a.failed + q.failed, completed: a.completed + q.completed, throughput: a.throughput + q.throughputPerMin, workers: a.workers + q.workers }), { active: 0, pending: 0, retry: 0, failed: 0, completed: 0, throughput: 0, workers: 0 });
return (
Colas
Asynq · {live.length} colas · {totals.workers} workers · {totals.throughput.toLocaleString()} msgs/m
{live.map(q => {
const tone = q.pending > 500 ? 'warn' : q.failed > 5 ? 'danger' : 'success';
return (
{q.name}
weight={q.weight} · workers={q.workers}
{[
{ lbl: 'Active', val: q.active, color: '#1E7BFF' },
{ lbl: 'Pending', val: q.pending, color: '#F2A900' },
{ lbl: 'Retry', val: q.retry, color: '#7A3CFF' },
{ lbl: 'Failed', val: q.failed, color: '#E03E3E' },
{ lbl: 'p95', val: q.p95 + 'ms', color: 'var(--app-fg2)' }
].map((s, i) => (
{s.lbl}
{typeof s.val === 'number' ? s.val.toLocaleString() : s.val}
))}
Math.max(0, v * (q.weight / 12) + (Math.random() - 0.5) * 40))} color={tone === 'warn' ? 'warn' : tone === 'danger' ? 'danger' : 'blue'} />
Throughput: {q.throughputPerMin}/m
Completed: {q.completed.toLocaleString()}
);
})}
);
}
/* =========================================================================
LOGS — streaming, virtualized-ish
========================================================================= */
function PageLogs({ tenant, sseEvents }) {
const [logs, setLogs] = _us(() => window.genLogs(80));
const [search, setSearch] = _us('');
const [levelF, setLevelF] = _us('all');
const [svcF, setSvcF] = _us('all');
const [expanded, setExpanded] = _us(null);
const [paused, setPaused] = _us(false);
const scrollRef = _ur(null);
_ue(() => {
if (paused) return;
const id = setInterval(() => {
const tpl = window.randomFrom(window.ERROR_TEMPLATES);
const tnt = window.randomFrom(window.TENANTS);
const newLog = {
id: 'log_' + window.randHex(10),
ts: new Date().toISOString(),
level: tpl.level,
svc: tpl.svc,
tenant: tnt.id, tenantName: tnt.name,
msg: tpl.msg, code: tpl.code,
trace_id: window.TRACE_PREFIX(), span_id: window.randHex(8),
attrs: { env: 'production', region: 'sdq-1', host: 'connector-worker-' + (Math.floor(Math.random() * 6) + 1), request_id: 'req_' + window.randHex(12), user_agent: 'BlazeConnector/3.4.2 (go1.22)' },
isNew: true
};
setLogs(prev => [newLog, ...prev].slice(0, 200));
}, 2200 + Math.random() * 2000);
return () => clearInterval(id);
}, [paused]);
const filtered = _um(() => {
const ql = search.toLowerCase();
return logs.filter(l => {
if (tenant.id !== '*' && l.tenant !== tenant.id) return false;
if (levelF !== 'all' && l.level !== levelF) return false;
if (svcF !== 'all' && l.svc !== svcF) return false;
if (ql) {
const blob = (l.msg + ' ' + l.svc + ' ' + l.tenantName + ' ' + l.trace_id + ' ' + (l.code || '')).toLowerCase();
if (!blob.includes(ql)) return false;
}
return true;
});
}, [logs, search, levelF, svcF, tenant]);
const distrib = _um(() => {
const out = { error: 0, warn: 0, info: 0, debug: 0 };
logs.forEach(l => { out[l.level] = (out[l.level] || 0) + 1; });
return out;
}, [logs]);
return (
Logs
Stream estructurado · {filtered.length} / {logs.length} eventos · {paused ? 'pausado' : 'live'}
ERROR {distrib.error}
WARN {distrib.warn}
INFO {distrib.info}
DEBUG {distrib.debug}
setLevelF(levelF === 'all' ? 'error' : levelF === 'error' ? 'warn' : levelF === 'warn' ? 'info' : 'all')} />
setSvcF(svcF === 'all' ? 'msgr.dispatch' : svcF === 'msgr.dispatch' ? 'pagos.callback' : 'all')} />
timestamplevelservicetenantmessage
{filtered.length === 0
?
: filtered.map((l) => (
setExpanded(expanded === l.id ? null : l.id)}>
{l.ts.slice(11, 23)}
{l.level}
{l.svc}
{l.tenantName}
{l.msg} {l.code && · code={l.code}}
{expanded === l.id && (
)}
))}
);
}
/* =========================================================================
ALERTAS
========================================================================= */
function PageAlerts({ tenant }) {
const [filter, setFilter] = _us('active');
const [search, setSearch] = _us('');
const rows = _um(() => {
const ql = search.toLowerCase();
return window.ALERTS.filter(a => {
if (tenant.id !== '*' && a.tenant && a.tenant !== tenant.id) return false;
if (filter === 'active' && a.ack) return false;
if (filter === 'critical' && a.severity !== 'critical') return false;
if (filter === 'ack' && !a.ack) return false;
if (ql && !(a.title + ' ' + a.source).toLowerCase().includes(ql)) return false;
return true;
});
}, [filter, tenant, search]);
return (
Alertas
{window.ALERTS.filter(a => !a.ack).length} sin reconocer · {window.ALERTS.length} totales · políticas activas: 24
a.severity === 'critical').length} color="danger" foot={window.ALERTS.filter(a => a.severity === 'critical' && !a.ack).length + ' sin ack'} />
a.severity === 'warning').length} color="warn" foot={window.ALERTS.filter(a => a.severity === 'warning' && !a.ack).length + ' sin ack'} />
a.severity === 'info').length} color="blue" />
{['active','critical','ack','all'].map(f => (
))}
},
{ label: 'Título', render: (a) => },
{ label: 'Tenant', width: 160, render: (a) => a.tenant ? t.id === a.tenant) || {}).name || a.tenant }} size={16} /> {(window.TENANTS.find(t => t.id === a.tenant) || {}).name || a.tenant} : global },
{ label: 'Disparo', width: 140, render: (a) => {window.fmtTime(a.fired)} · {a.age} },
{ label: 'Estado', width: 100, render: (a) => a.ack ? : },
{ label: '', width: 200, render: (a) =>
{a.runbook && }
{!a.ack && }
}
]} rows={rows} />
);
}
Object.assign(window, { PageDashboard, PageMessaging, PageQueues, PageLogs, PageAlerts });