// 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 });