// Monitor view: panels sidebar + selected panel detail with events stream + commands
function PanelSidebarItem({ p, index, active, onClick, alarmCount, shouldAnimate, panelCounts, onCategoryBadgeClick, onDragStart, onDragOver, onDragEnd, dragOver }) {
const dotColor = p.alive_status === 'connected' ? BC.ok.dot : p.alive_status === 'pending' ? BC.pending.dot : BC.fire.dot;
const aliveLabel = p.alive_status === 'connected' ? 'Conectado' : p.alive_status === 'pending' ? 'Pendiente' : 'Sin señal';
const urgent = alarmCount > 0;
const counts = panelCounts || { alarm: 0, trouble: 0, supervisory: 0 };
const badges = [
{ cat: 'alarm', label: 'Alarma', color: BC.fire, count: counts.alarm },
{ cat: 'trouble', label: 'Falla', color: BC.trouble, count: counts.trouble },
{ cat: 'supervisory', label: 'Supervisión', color: BC.supervisory, count: counts.supervisory },
].filter(b => b.count > 0);
return (
{index + 1}
{p.description || p.client_id}
{aliveLabel}
{urgent && · ALARMA}
{alarmCount > 0 &&
{alarmCount}
}
{badges.length > 0 &&
{badges.map(b =>
)}
}
);
}
function CommandBar({ panel, commands, sequences, role, onSend, lastResult, onSendSequence, lastSeqResult }) {
const disabled = !panel || panel.status !== 'connected' || role === 'viewer';
if (!panel) {
return (
Seleccione un panel para habilitar los comandos.
);
}
const hasSequences = sequences && sequences.length > 0;
return (
{/* Status row */}
{role === 'viewer' &&
Modo lector · solo lectura
}
{panel.status !== 'connected' &&
Panel desconectado
}
{lastResult &&
{lastResult.msg}
}
{lastSeqResult &&
{lastSeqResult.msg}
}
{/* Commands */}
Comandos
{(!commands || commands.length === 0) &&
No hay comandos para este panel.
}
{commands && commands.map((cmd) => {
const adminBlocked = cmd.admin_only && role !== 'admin';
const isDanger = /reset|evac/i.test(cmd.name);
const btnDisabled = disabled || adminBlocked;
return (
);
})}
{/* Sequences */}
{hasSequences && (
Secuencias
{sequences.map((seq) => {
const adminBlocked = seq.admin_only && role !== 'admin';
const btnDisabled = disabled || adminBlocked;
return (
);
})}
)}
);
}
function EventsTable({ events, density = 'comfortable', categoryFilter, panelFilter, search, panelLookup, panelIndex, showPanelCol = false, shouldAnimate, lastPanelOkByPanel, animationGen = 0 }) {
const dens = density === 'compact' ? { rowH: 30, font: 12 } : density === 'spacious' ? { rowH: 44, font: 14 } : { rowH: 36, font: 13 };
const shown = events.
filter((e) => e.cls !== 'PANEL_OK').
filter((e) => !search || (e.message || '').toLowerCase().includes(search.toLowerCase()) || (e.cls || '').toLowerCase().includes(search.toLowerCase()) || (e.client_id || '').toLowerCase().includes(search.toLowerCase()));
return (
{/* header */}
Tipo
Descripción
{showPanelCol &&
Panel
}
Recibido
{shown.length === 0 &&
{search || categoryFilter || panelFilter ? 'Sin eventos para los filtros aplicados.' : 'Esperando eventos…'}
}
{shown.map((ev, i) => {
const c = classifyColor(ev.cls);
const cat = classifyCategory(ev.cls);
const isResolved = lastPanelOkByPanel && lastPanelOkByPanel[ev.client_id] && ev.timestamp && new Date(ev.timestamp) < new Date(lastPanelOkByPanel[ev.client_id]);
const isFire = cat === 'alarm' && !isResolved;
const hasFilter = categoryFilter || panelFilter;
const matchesFilter = !hasFilter || ((!categoryFilter || classifyCategory(ev.cls) === categoryFilter) && (!panelFilter || ev.client_id === panelFilter) && !isResolved);
const dimmedTextColor = matchesFilter ? null : BC.inkFaint;
const evDate = ev.timestamp ? new Date(ev.timestamp).toDateString() : null;
const prevDate = i > 0 && shown[i - 1].timestamp ? new Date(shown[i - 1].timestamp).toDateString() : null;
const showDateSep = evDate && (i === 0 || evDate !== prevDate);
const isToday = evDate === new Date().toDateString();
return (
{showDateSep && (
{isToday ? 'Hoy' : new Date(ev.timestamp).toLocaleDateString('es-CL', { weekday: 'long', day: 'numeric', month: 'long' })}
)}
{classifyLabel(ev.cls)}
{ev.message}
{showPanelCol &&
{panelIndex && panelIndex[ev.client_id] != null ? panelIndex[ev.client_id] + 1 : ev.client_id}
{panelLookup && panelLookup[ev.client_id] || ev.client_id}
}
{ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString('es-CL', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '—'}
);
})}
);
}
function MonitorView({ user, clients, events, commandsByClient, sequencesByClient, selected, setSelected, categoryFilter, setCategoryFilter, panelFilter, setPanelFilter, search, setSearch, onSendCommand, lastCmdResult, onSendSequence, lastSeqResult, density, shouldAnimate, newAlarmsByClient, countsByClient, lastPanelOkByPanel, animationGen, sidebarOpen, onCloseSidebar }) {
const [panelOrder, setPanelOrder] = React.useState(() => {
try { return JSON.parse(localStorage.getItem('panelOrder') || '[]'); } catch { return []; }
});
const [dragIdx, setDragIdx] = React.useState(null);
const [dragOverIdx, setDragOverIdx] = React.useState(null);
const sortedClients = React.useMemo(() => {
const order = new Map(panelOrder.map((id, i) => [id, i]));
return [...clients].sort((a, b) => {
const ai = order.has(a.client_id) ? order.get(a.client_id) : Infinity;
const bi = order.has(b.client_id) ? order.get(b.client_id) : Infinity;
return ai - bi;
});
}, [clients, panelOrder]);
const handleDragStart = (i) => (e) => { setDragIdx(i); e.dataTransfer.effectAllowed = 'move'; };
const handleDragOver = (i) => (e) => { e.preventDefault(); setDragOverIdx(i); };
const handleDragEnd = () => {
if (dragIdx !== null && dragOverIdx !== null && dragIdx !== dragOverIdx) {
const reordered = [...sortedClients];
const [moved] = reordered.splice(dragIdx, 1);
reordered.splice(dragOverIdx, 0, moved);
const newOrder = reordered.map(c => c.client_id);
setPanelOrder(newOrder);
localStorage.setItem('panelOrder', JSON.stringify(newOrder));
}
setDragIdx(null);
setDragOverIdx(null);
};
const selectedClient = sortedClients.find((c) => c.client_id === selected);
const panelEvents = events.filter((e) => e.client_id === selected);
const commands = selectedClient ? commandsByClient[selectedClient.client_id] || [] : [];
const sequences = selectedClient ? (sequencesByClient || {})[selectedClient.client_id] || [] : [];
const alarmCounts = newAlarmsByClient || {};
const handleCategoryBadgeClick = (category, clientId) => {
setCategoryFilter(category);
setPanelFilter(clientId);
setSelected(clientId);
};
return (
{/* Sidebar */}
Paneles ({clients.length})
{sortedClients.map((p, i) =>
{ setSelected(p.client_id); onCloseSidebar && onCloseSidebar(); }} shouldAnimate={shouldAnimate} panelCounts={countsByClient[p.client_id]} onCategoryBadgeClick={handleCategoryBadgeClick} onDragStart={handleDragStart(i)} onDragOver={handleDragOver(i)} onDragEnd={handleDragEnd} dragOver={dragOverIdx === i} />
)}
{/* Legend */}
Leyenda
{[['Alarma', BC.fire], ['Falla', BC.trouble], ['Supervisión', BC.supervisory], ['Estado', BC.state]].map(([label, c]) =>
{label}
)}
{/* Main */}
{selectedClient ?
<>
{/* Panel header */}
{clients.indexOf(selectedClient) + 1}
{selectedClient.description}
IP {selectedClient.ip}:{selectedClient.port}
conectado {relTime(selectedClient.connected_at)}
visto {relTime(selectedClient.last_seen)}
setSearch(e.target.value)} placeholder="Buscar eventos…"
style={{ border: 'none', background: 'transparent', outline: 'none', fontSize: 12, color: BC.ink, width: 160 }} />
{search && setSearch('')} style={{ cursor: 'pointer', color: BC.inkMuted, lineHeight: 1 }}>×}
> :
Seleccione un panel del listado.
}
);
}
function StreamView({ clients, events, categoryFilter, setCategoryFilter, panelFilter, setPanelFilter, search, setSearch, density, shouldAnimate, countsByClient, lastPanelOkByPanel, animationGen }) {
const panelLookup = {};
const panelIndex = {};
clients.forEach((c, i) => { panelLookup[c.client_id] = c.description; panelIndex[c.client_id] = i; });
return (
Stream unificado · todos los paneles
{events.length} eventos · ordenados del más reciente al más antiguo
setSearch(e.target.value)} placeholder="Buscar eventos / paneles…"
style={{ border: 'none', background: 'transparent', outline: 'none', fontSize: 12, color: BC.ink, width: 200 }} />
{search && setSearch('')} style={{ cursor: 'pointer', color: BC.inkMuted, lineHeight: 1 }}>×}
);
}
Object.assign(window, { MonitorView, StreamView, EventsTable, CommandBar });