// 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', plural: 'Alarmas', color: BC.fire, count: counts.alarm },
    { cat: 'trouble', label: 'Falla', plural: 'Fallas', color: BC.trouble, count: counts.trouble },
    { cat: 'supervisory', label: 'Supervisión', plural: 'Supervisiones', color: BC.supervisory, count: counts.supervisory },
  ].filter(b => b.count > 0);

  return (
    <div className={'bc-panel-row bc-btn ' + (active ? 'active' : '')}
    draggable="true" onDragStart={onDragStart} onDragOver={onDragOver} onDragEnd={onDragEnd}
    style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px', borderRadius: 6, borderLeft: '1px solid ' + (active ? BC.brand : 'transparent'), borderRight: '1px solid ' + (active ? BC.brand : 'transparent'), borderBottom: '1px solid ' + (active ? BC.brand : 'transparent'), borderTop: dragOver ? '2px solid ' + BC.brand : '1px solid ' + (active ? BC.brand : 'transparent'), marginBottom: 3, cursor: 'grab' }}>
      <div onClick={onClick} style={{ display: 'flex', alignItems: 'center', gap: 10, animation: (urgent && shouldAnimate) ? 'bc-pulse-ring 1.6s ease-in-out infinite' : 'none', cursor: 'pointer' }}>
        <div style={{ width: 30, height: 30, borderRadius: 5, background: urgent ? BC.fire.bg : BC.brandTint, color: urgent ? BC.fire.fg : BC.brand, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: BC.mono, fontSize: 11, fontWeight: 700, border: '1px solid ' + (urgent ? BC.fire.bar : BC.border) }}>
          {index + 1}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: active ? 600 : 500, color: BC.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.description || p.client_id}</div>
          <div style={{ fontSize: 11, color: BC.inkSubtle, marginTop: 2, display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 5, height: 5, borderRadius: 5, background: dotColor }} />
            {aliveLabel}
            {urgent && <span style={{ color: BC.fire.fg, fontWeight: 700 }}>· ALARMA</span>}
          </div>
        </div>
        {alarmCount > 0 &&
        <div style={{ minWidth: 22, height: 18, padding: '0 6px', borderRadius: 9, background: BC.fire.bar, color: '#fff', fontSize: 10, fontWeight: 700, fontFamily: BC.mono, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{alarmCount}</div>
        }
      </div>
      {badges.length > 0 &&
      <div style={{ display: 'flex', gap: 6, paddingLeft: 40, flexWrap: 'wrap' }}>
        {badges.map(b =>
          <button key={b.cat} onClick={(e) => { e.stopPropagation(); onCategoryBadgeClick && onCategoryBadgeClick(b.cat, p.client_id); }}
          style={{ padding: '3px 8px', borderRadius: 4, border: '1px solid ' + b.color.bar, background: b.color.bg, color: b.color.fg, fontSize: 10, fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4 }}>
            <span style={{ fontFamily: BC.mono, fontWeight: 700 }}>{b.count}</span> {b.count === 1 ? b.label : (b.plural || b.label + 's')}
          </button>
        )}
      </div>
      }
    </div>);

}

function CommandBar({ panel, commands, sequences, role, onSend, lastResult, onSendSequence, lastSeqResult }) {
  const disabled = !panel || panel.status !== 'connected' || role === 'viewer';
  if (!panel) {
    return (
      <div style={{ padding: '12px 18px', background: BC.surfaceAlt, borderTop: '1px solid ' + BC.border, fontSize: 12, color: BC.inkFaint, fontStyle: 'italic' }}>
        Seleccione un panel para habilitar los comandos.
      </div>);
  }
  const hasSequences = sequences && sequences.length > 0;
  return (
    <div style={{ padding: '12px 18px', background: BC.surfaceAlt, borderTop: '1px solid ' + BC.border }}>
      {/* Status row */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 8 }}>
        {role === 'viewer' &&
          <div style={{ fontSize: 11, color: BC.inkFaint, fontStyle: 'italic' }}>Modo lector · solo lectura</div>
        }
        {panel.status !== 'connected' &&
          <div style={{ fontSize: 11, color: BC.fire.fg, display: 'flex', alignItems: 'center', gap: 5 }}><Icon name="warning" size={12} /> Panel desconectado</div>
        }
        <div style={{ flex: 1 }} />
        {lastResult &&
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, padding: '3px 10px', borderRadius: 999, background: lastResult.ok ? BC.ok.bg : BC.fire.bg, color: lastResult.ok ? BC.ok.fg : BC.fire.fg, border: '1px solid ' + (lastResult.ok ? BC.ok.bar : BC.fire.bar) }}>
            <Icon name={lastResult.ok ? 'check' : 'warning'} size={12} />{lastResult.msg}
          </div>
        }
        {lastSeqResult &&
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, padding: '3px 10px', borderRadius: 999, background: lastSeqResult.ok ? BC.pending.bg : BC.fire.bg, color: lastSeqResult.ok ? BC.pending.fg : BC.fire.fg, border: '1px solid ' + (lastSeqResult.ok ? BC.pending.bar : BC.fire.bar) }}>
            <Icon name={lastSeqResult.ok ? 'clock' : 'warning'} size={12} />{lastSeqResult.msg}
          </div>
        }
      </div>
      {/* Commands */}
      <div style={{ marginBottom: hasSequences ? 10 : 0 }}>
        <div style={{ fontSize: 10, fontWeight: 700, color: BC.inkSubtle, letterSpacing: '.08em', textTransform: 'uppercase', marginBottom: 6 }}>Comandos</div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
          {(!commands || commands.length === 0) &&
            <div style={{ fontSize: 12, color: BC.inkFaint, fontStyle: 'italic', padding: '4px 0' }}>No hay comandos para este panel.</div>
          }
          {commands && commands.map((cmd) => {
            const adminBlocked = cmd.admin_only && role !== 'admin';
            const isDanger = /reset|evac/i.test(cmd.name);
            const btnDisabled = disabled || adminBlocked;
            return (
              <button key={cmd.id} onClick={() => onSend(cmd)} disabled={btnDisabled}
                title={adminBlocked ? 'Solo administradores' : cmd.description || cmd.name}
                className={'bc-btn ' + (isDanger ? 'bc-danger-btn' : 'bc-cmd-btn')}
                style={{ padding: '7px 14px', border: '1px solid ' + BC.border, background: BC.surface, borderRadius: 6, fontSize: 12, fontWeight: 600, color: isDanger ? BC.fire.fg : BC.ink, display: 'flex', alignItems: 'center', gap: 7, fontFamily: 'inherit' }}>
                <Icon name={isDanger ? 'warning' : cmd.name.toLowerCase().includes('ack') || cmd.name.toLowerCase().includes('reconocer') ? 'check' : 'send'} size={13} />
                {cmd.name}
                {cmd.admin_only && <span style={{ fontSize: 9, padding: '1px 5px', borderRadius: 3, background: adminBlocked ? BC.bgAlt : '#F3E8FF', color: adminBlocked ? BC.inkFaint : '#6B21A8', fontFamily: BC.mono, letterSpacing: '.04em', fontWeight: 700 }}>ADM</span>}
              </button>);
          })}
        </div>
      </div>
      {/* Sequences */}
      {hasSequences && (
        <div>
          <div style={{ fontSize: 10, fontWeight: 700, color: BC.pending.fg, letterSpacing: '.08em', textTransform: 'uppercase', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
            <Icon name="clock" size={11} color={BC.pending.fg}/> Secuencias
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
            {sequences.map((seq) => {
              const adminBlocked = seq.admin_only && role !== 'admin';
              const btnDisabled = disabled || adminBlocked;
              return (
                <button key={seq.id} onClick={() => onSendSequence(seq)} disabled={btnDisabled}
                  title={adminBlocked ? 'Solo administradores' : (seq.description || seq.name) + (seq.timeout_seconds ? ` · ${seq.timeout_seconds}s` : '')}
                  className="bc-btn"
                  style={{ padding: '7px 14px', border: '1px solid ' + (btnDisabled ? BC.border : BC.pending.bar), background: btnDisabled ? BC.surface : BC.pending.bg, borderRadius: 6, fontSize: 12, fontWeight: 600, color: btnDisabled ? BC.inkFaint : BC.pending.fg, display: 'flex', alignItems: 'center', gap: 7, fontFamily: 'inherit', opacity: btnDisabled ? 0.5 : 1 }}>
                  <Icon name="clock" size={13} color={btnDisabled ? BC.inkFaint : BC.pending.fg}/>
                  {seq.name}
                  {seq.timeout_seconds && <span style={{ fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'rgba(0,0,0,.08)', fontFamily: BC.mono, fontWeight: 700 }}>{seq.timeout_seconds}s</span>}
                  {seq.admin_only && <span style={{ fontSize: 9, padding: '1px 5px', borderRadius: 3, background: adminBlocked ? BC.bgAlt : '#F3E8FF', color: adminBlocked ? BC.inkFaint : '#6B21A8', fontFamily: BC.mono, letterSpacing: '.04em', fontWeight: 700 }}>ADM</span>}
                </button>);
            })}
          </div>
        </div>
      )}
    </div>);
}

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) => !search || (e.message || '').toLowerCase().includes(search.toLowerCase()) || (e.cls || '').toLowerCase().includes(search.toLowerCase()) || (e.client_id || '').toLowerCase().includes(search.toLowerCase())).
  filter((e) => {
    if (!showPanelCol || !categoryFilter) return true;
    const isResolved = lastPanelOkByPanel && lastPanelOkByPanel[e.client_id] && e.timestamp && new Date(e.timestamp) < new Date(lastPanelOkByPanel[e.client_id]);
    return classifyCategory(e.cls) === categoryFilter && !isResolved;
  });
  return (
    <div className="bc-events-outer" style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
      <div className="bc-events-scroll-wrap" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
      {/* header */}
      <div className="bc-events-header-row" style={{ display: 'flex', alignItems: 'center', padding: '7px 16px 7px 25px', background: BC.brand, color: '#fff', fontSize: 10, fontWeight: 600, letterSpacing: '.08em', textTransform: 'uppercase' }}>
        <div className="bc-evt-tipo" style={{ width: 96, flex: '0 0 96px' }}>Tipo</div>
        <div style={{ flex: 1 }}>Descripción</div>
        {showPanelCol && <div style={{ width: 170, flex: '0 0 170px' }}>Panel</div>}
        <div className="bc-evt-ts" style={{ width: 130, flex: '0 0 130px', textAlign: 'right' }}>Recibido</div>
      </div>
      <div className="bc-scroll" style={{ flex: 1, minHeight: 0, overflowY: 'auto', background: BC.surface }}>
        {shown.length === 0 &&
        <div style={{ padding: '40px 12px', textAlign: 'center', color: BC.inkFaint, fontSize: 12 }}>
            {search || categoryFilter || panelFilter ? 'Sin eventos para los filtros aplicados.' : 'Esperando eventos…'}
          </div>
        }
        {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 (
            <React.Fragment key={`${ev.id}-${animationGen}`}>
              {showDateSep && (
                <div style={{ display: 'flex', alignItems: 'center', padding: '6px 16px 6px 25px', background: BC.surfaceAlt, borderBottom: '1px solid ' + BC.border, fontSize: 10, fontWeight: 700, color: BC.inkSubtle, letterSpacing: '.06em', textTransform: 'uppercase', gap: 8, position: 'sticky', top: 0, zIndex: 10 }}>
                  <Icon name="clock" size={11} color={BC.inkFaint} />
                  {isToday ? 'Hoy' : new Date(ev.timestamp).toLocaleDateString('es-CL', { weekday: 'long', day: 'numeric', month: 'long' })}
                </div>
              )}
            <div className="bc-events-data-row bc-row-enter" style={{ display: 'flex', alignItems: 'center', height: dens.rowH, padding: '0 16px 0 0', borderBottom: '1px solid ' + BC.divider, background: isFire ? c.bg : 'transparent', '--bc-row-delay': `${Math.min(i * 15, 300)}ms`, opacity: matchesFilter ? 1 : 0.15, pointerEvents: matchesFilter ? 'auto' : 'none', transition: 'color 0.2s, opacity 0.2s' }}>
              <div style={{ width: 3, height: '100%', background: c.bar, marginRight: 10, animation: (isFire && ev.isNew) ? 'bc-pulse-fire 1.4s ease-in-out infinite' : 'none' }} />
              <div className="bc-evt-tipo" style={{ display: 'flex', alignItems: 'center', gap: 6, width: 96, flex: '0 0 96px', fontFamily: BC.mono, fontSize: 10, color: dimmedTextColor || c.fg, fontWeight: isFire ? 700 : 600, letterSpacing: '.04em' }}>
                <span style={{ width: 7, height: 7, borderRadius: 7, background: c.dot, flex: '0 0 7px' }} />
                {classifyLabel(ev.cls)}
              </div>
              <div title={ev.message} style={{ flex: 1, minWidth: 0, fontSize: dens.font, color: dimmedTextColor || (isFire ? c.fg : BC.ink), fontWeight: isFire ? 600 : 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', paddingRight: 12 }}>
                {ev.message}
              </div>
              {showPanelCol &&
              <div style={{ width: 170, flex: '0 0 170px', fontSize: 12, color: dimmedTextColor || BC.inkMuted, display: 'flex', alignItems: 'center', gap: 6 }}>
                  <span style={{ width: 18, height: 18, borderRadius: 3, background: BC.brandTint, color: BC.brand, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: BC.mono, fontSize: 9, fontWeight: 700 }}>{panelIndex && panelIndex[ev.client_id] != null ? panelIndex[ev.client_id] + 1 : ev.client_id}</span>
                  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{panelLookup && panelLookup[ev.client_id] || ev.client_id}</span>
                </div>
              }
              <div className="bc-evt-ts" style={{ width: 130, flex: '0 0 130px', fontFamily: BC.mono, fontSize: 11, color: dimmedTextColor || BC.inkSubtle, textAlign: 'right' }}>
                {ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString('es-CL', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '—'}
              </div>
            </div>
            </React.Fragment>);

        })}
      </div>
      </div>
    </div>);

}

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 (
    <div className="bc-monitor-wrap" style={{ flex: 1, display: 'flex', minHeight: 0 }}>
      {/* Sidebar */}
      <div className={'bc-sidebar' + (sidebarOpen ? ' is-open' : '')} style={{ width: 300, flex: '0 0 300px', background: BC.surfaceAlt, borderRight: '1px solid ' + BC.border, display: 'flex', flexDirection: 'column' }}>
        <div style={{ padding: '14px 16px 8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div style={{ fontSize: 10, fontWeight: 700, color: BC.inkSubtle, letterSpacing: '.09em', textTransform: 'uppercase' }}>Paneles ({clients.length})</div>
        </div>
        <div className="bc-scroll" style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '10px 14px 12px' }}>
          {sortedClients.map((p, i) =>
          <PanelSidebarItem key={p.client_id} p={p} index={i} active={selected === p.client_id} alarmCount={alarmCounts[p.client_id] || 0} onClick={() => { 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} />
          )}
        </div>
        {/* Legend */}
        <div style={{ borderTop: '1px solid ' + BC.border, padding: '12px 16px', fontSize: 11, color: BC.inkSubtle }}>
          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '.09em', textTransform: 'uppercase', marginBottom: 8 }}>Leyenda</div>
          {[['Alarma', BC.fire], ['Falla', BC.trouble], ['Supervisión', BC.supervisory], ['Estado', BC.state]].map(([label, c]) =>
          <div key={label} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0' }}>
              <span style={{ width: 8, height: 8, borderRadius: 8, background: c.dot }} />
              <span style={{ color: BC.ink }}>{label}</span>
            </div>
          )}
        </div>
      </div>

      <div className={'bc-sidebar-overlay' + (sidebarOpen ? ' is-open' : '')} onClick={onCloseSidebar}/>
      {/* Main */}
      <div className="bc-monitor-main" style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', background: BC.surface }}>
        {selectedClient ?
        <>
            {/* Panel header */}
            <div style={{ padding: '14px 18px 12px', borderBottom: '1px solid ' + BC.border, background: BC.surface }}>
              <div className="bc-panel-header-inner" style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
                <div style={{ width: 38, height: 38, borderRadius: 6, background: BC.brandTint, color: BC.brand, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: BC.mono, fontWeight: 700, fontSize: 14, flexShrink: 0 }}>
                  {sortedClients.indexOf(selectedClient) + 1}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 15, fontWeight: 600, color: BC.ink, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                    {selectedClient.description}
                    <StatusPill status={selectedClient.alive_status} />
                  </div>
                  <div className="bc-panel-meta" style={{ fontSize: 11, color: BC.inkSubtle, fontFamily: BC.mono, marginTop: 3, display: 'flex', gap: 18, flexWrap: 'wrap' }}>
                    <span><span style={{ color: BC.inkFaint }}>IP</span> {selectedClient.ip}:{selectedClient.port}</span>
                    <span><span style={{ color: BC.inkFaint }}>conectado</span> {relTime(selectedClient.connected_at)}</span>
                    <span><span style={{ color: BC.inkFaint }}>visto</span> {relTime(selectedClient.last_seen)}</span>
                  </div>
                </div>
                <div className="bc-panel-search" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 12px', background: BC.surfaceAlt, border: '1px solid ' + (search ? BC.brand : BC.border), borderRadius: 999 }}>
                  <Icon name="search" size={13} color={BC.inkFaint} />
                  <input className="bc-input" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Buscar eventos…"
                style={{ border: 'none', background: 'transparent', outline: 'none', fontSize: 12, color: BC.ink, width: 160 }} />
                  {search && <span onClick={() => setSearch('')} style={{ cursor: 'pointer', color: BC.inkMuted, lineHeight: 1 }}>×</span>}
                </div>
              </div>
            </div>
            <EventsTable events={panelEvents} categoryFilter={categoryFilter} search={search} density={density} shouldAnimate={shouldAnimate} lastPanelOkByPanel={lastPanelOkByPanel} animationGen={animationGen} />
            <CommandBar panel={selectedClient} commands={commands} sequences={sequences} role={user.role} onSend={onSendCommand} lastResult={lastCmdResult} onSendSequence={onSendSequence} lastSeqResult={lastSeqResult} />
          </> :

        <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: BC.inkFaint, fontSize: 13 }}>
            Seleccione un panel del listado.
          </div>
        }
      </div>
    </div>);

}

function StreamView({ clients, events, categoryFilter, setCategoryFilter, panelFilter, setPanelFilter, search, setSearch, density, shouldAnimate, countsByClient, lastPanelOkByPanel, animationGen }) {
  const sortedClients = React.useMemo(() => {
    try {
      const panelOrder = JSON.parse(localStorage.getItem('panelOrder') || '[]');
      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;
      });
    } catch { return clients; }
  }, [clients]);

  const panelLookup = {};
  const panelIndex = {};
  sortedClients.forEach((c, i) => { panelLookup[c.client_id] = c.description; panelIndex[c.client_id] = i; });

  return (
    <div style={{ flex: 1, display: 'flex', minHeight: 0, background: BC.surface }}>
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
        <div style={{ padding: '14px 18px 12px', borderBottom: '1px solid ' + BC.border, background: BC.surface, display: 'flex', alignItems: 'center', gap: 14 }}>
          <div>
            <div style={{ fontSize: 15, fontWeight: 600 }}>Stream unificado · todos los paneles</div>
            <div style={{ fontSize: 11, color: BC.inkSubtle, marginTop: 2 }}>{events.length} eventos · ordenados del más reciente al más antiguo</div>
          </div>
          <div style={{ flex: 1 }} />
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 12px', background: BC.surfaceAlt, border: '1px solid ' + (search ? BC.brand : BC.border), borderRadius: 999 }}>
            <Icon name="search" size={13} color={BC.inkFaint} />
            <input className="bc-input" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Buscar eventos / paneles…"
            style={{ border: 'none', background: 'transparent', outline: 'none', fontSize: 12, color: BC.ink, width: 200 }} />
            {search && <span onClick={() => setSearch('')} style={{ cursor: 'pointer', color: BC.inkMuted, lineHeight: 1 }}>×</span>}
          </div>
        </div>
        <EventsTable events={events} categoryFilter={categoryFilter} panelFilter={panelFilter} search={search} panelLookup={panelLookup} panelIndex={panelIndex} showPanelCol density={density} shouldAnimate={shouldAnimate} lastPanelOkByPanel={lastPanelOkByPanel} animationGen={animationGen} />
      </div>
    </div>);

}

Object.assign(window, { MonitorView, StreamView, EventsTable, CommandBar });