GameControl - Gestión de Salas Gaming

Sprint 0.3-C/D — Motor de Salas Core V2

PLAN DE IMPLEMENTACIÓN — Pendiente aprobación No se ejecutan cambios destructivos ni se toca producción hasta aprobación.


1. Auditoría consolidada

1.1 Realtime — 4 canales duplicados para sesiones

# Archivo Canal Tabla Callback Duplicado?
1 useSalas.js:140 salas-hook-rt-{random} sesiones cargarSesionesActivas() ❌ Sí (3 otros)
2 useDashboard.js:329 dashboard-rt-v2 ventas, sesiones, gastos fetchKPIs() ❌ sesiones duplicado
3 EventLive.jsx:573 event-live-sesiones sesiones cargar() ❌ Sí (3 otros)
4 TVDisplay.jsx:332 tv-sesiones sesiones cargar() ❌ Sí (3 otros)
5 databaseService.js:77 realtime:{tabla} dinámico callback ✅ No usado

Impacto: 1 cambio en sesiones → 4 callbacks → 4 fetches → 4x renders.

1.2 Estado — 4 fuentes de verdad para sesiones

Componente Fuente Duplicado?
useSalas hook → Zustand sesiones Store global ✅ Correcto
Salas.jsx → useSalas() Hook ✅ Correcto
MonitorSalasActivas → useSalas() Hook ✅ Correcto
Dashboard.jsx → useState local Local ❌ Duplicado
TVDisplay.jsx → useState local Local ❌ Duplicado
EventLive.jsx → useState local Local ❌ Duplicado
Ventas.jsx → useState salas local Local ❌ Duplicado
Reportes.jsx → useState data Local ⚠️ Aceptable (histórico)

1.3 Refresh — 12 llamadas redundantes a cargarSesionesActivas

Archivo Línea Contexto Redundante?
useSalas.js 137 Initial load ❌ No
useSalas.js 143 Realtime callback ❌ No (pero optimizable)
useSalas.js 178 Después abrirSesion ✅ Sí
useSalas.js 185 Después retry abrirSesion ✅ Sí
useSalas.js 212 Después agregarTiempo ✅ Sí
useSalas.js 274 Después agregarProducto ✅ Sí
useSalas.js 335 Después agregarProductos ✅ Sí
useSalas.js 347 Después trasladarSesion ✅ Sí
useSalas.js 444 Después finalizarSesion ✅ Sí
useSalas.js 509 Después anularSesion ✅ Sí
useSalas.js 619 Después editarSesionAdmin ✅ Sí
ModalFinalizarSesion.jsx 212 Después RPC ✅ Sí
ModalTienda.jsx 175 Después RPC ok ✅ Sí
ModalTienda.jsx 180 Después RPC idempotente ✅ Sí

CargarSalas redundantes: 3 (líneas 533, 540, 549 en useSalas.js)

1.4 Tick — explosión de timers

Componente Timers Rate Por sesión?
TarjetaSala (useTemporizador) 1 por estación ocupada 1000ms ✅ Sí
TVDisplay (useTimer) 1 por sesión 1000ms ✅ Sí
EventLive (useTimer) 1 por sesión 1000ms ✅ Sí
LiveMonitor (SesionRow) 1 por sesión 10000ms ✅ Sí
MonitorSalasActivas (CardSesion) 1 por sesión 10000ms ✅ Sí
TablaSesionesActivas 1 global 60000ms ❌ No
TVDisplay clock 1 global 1000ms ❌ No
TVDisplay poll 1 global 20000ms ❌ No
EventLive poll 1 global 20000ms ❌ No

Escenario típico (15 estaciones ocupadas): 47 timers concurrentes → ~44 renders/s.

1.5 Rendimiento — memoización

Componente React.memo useMemo useCallback
TarjetaSala ❌ ❌ ❌
EstacionOcupada ❌ ❌ ❌
TVDisplay ❌ ❌ ✅
EventLive ❌ ❌ ✅
LiveMonitor ❌ ❌ ❌
MonitorSalasActivas ❌ ✅ 2 ❌

0 componentes memoizados. Cada tick de timer propaga re-render a todo el árbol.


2. Arquitectura propuesta

2.1 Realtime centralizado

src/lib/realtimeService.js  (NUEVO)
  ├── subscribe(table, callback) → unsubscribe
  ├── Una sola channel por tabla (singleton)
  ├── Múltiples callbacks registrados
  └── Cleanup automático cuando no hay suscriptores

Patrón:

2.2 Estado — Zustand como única fuente

Dato Store Quién escribe Quién lee
salas Zustand useSalas.cargarSalas Salas, Dashboard, Ventas, TVDisplay, EventLive
sesiones Zustand useSalas.cargarSesionesActivas Salas, Monitor, TVDisplay, EventLive, Dashboard
productos Zustand (futuro hook) Dashboard, Stock, Ventas

Cambios:

2.3 Refresh — eliminar duplicados

Patrón Antes Después
Después de operación DB cargarSesionesActivas() manual Realtime actualiza Zustand
Fallback si realtime falla — cargarSesionesActivas() manual (solo en error)
Initial load cargarSesionesActivas() Igual (necesario)
Polling TVDisplay/EventLive 20s Eliminado (realtime basta)

Regla: después de una operación DB exitosa, NO llamar cargarSesionesActivas(). El realtime se encarga. Si el realtime falla (timeout 3s), hay un fallback manual.

2.4 Tick global

src/hooks/useGlobalTick.js  (NUEVO)
  ├── 1 setInterval a 1000ms (singleton)
  ├── Expone `now` via Zustand: useGameStore.now
  ├── Componentes leen `now` y calculan tiempo restante sin state local
  └── Cleanup cuando no hay consumidores

Patrón:

Resultado: 1 timer global reemplaza 47 timers.

2.5 Rendimiento — React.memo selectivo

Componente Memo? Razón
EstacionOcupada ✅ Re-render solo si su sesión cambia
TarjetaSala ✅ Re-render solo si su sala cambia
SesionRow (LiveMonitor) ✅ Re-render solo si su sesión cambia
CardSesion (Monitor) ✅ Re-render solo si su sesión cambia
TarjetaEstacion (TV) ✅ Re-render solo si su sesión cambia
CircleTimer (EventLive) ✅ Re-render solo si su sesión cambia

Criterio: memoizar componentes que reciben props estables (sala/sesion) y no deben re-renderizar cuando otras sesiones cambian.


3. Archivos afectados

3.1 Archivos nuevos (2)

Archivo Propósito
src/lib/realtimeService.js Singleton realtime — 1 channel por tabla
src/hooks/useGlobalTick.js 1 timer global — expone now via Zustand

3.2 Archivos modificados (10)

Archivo Cambios Riesgo
src/store/useGameStore.js +now: Date.now(), +setNow Bajo
src/App.jsx Montar useGlobalTick() 1 vez Bajo
src/hooks/useSalas.js Usar realtimeService, eliminar 9 cargarSesionesActivas redundantes, eliminar 3 cargarSalas redundantes Medio
src/hooks/useDashboard.js Eliminar suscripción sesiones, usar Zustand Medio
src/components/salas/TarjetaSala.jsx Reemplazar useTemporizador por now del store, React.memo en EstacionOcupada Medio
src/components/dashboard/LiveMonitor.jsx Eliminar timer propio, usar now Medio
src/components/dashboard/MonitorSalasActivas.jsx Eliminar timer propio, usar now Medio
src/pages/TVDisplay.jsx Usar useSalas() en vez de useState, eliminar canal+poll, usar now Alto
src/pages/EventLive.jsx Usar useSalas() en vez de useState, eliminar canal+poll, usar now Alto
src/components/salas/ModalFinalizarSesion.jsx Eliminar cargarSesionesActivas() post-RPC Bajo
src/components/salas/ModalTienda.jsx Eliminar cargarSesionesActivas() post-RPC Bajo
src/pages/Dashboard.jsx Usar Zustand para salas/productos Medio
src/pages/Ventas.jsx Usar Zustand para salas Bajo
src/components/salas/TablaSesionesActivas.jsx Eliminar timer propio, usar now Bajo

3.3 Archivos NO tocados

Archivo Razón
src/pages/Reportes.jsx Histórico, acceptable local state
RPCs No crear nuevas
SQL schema No tocar
RLS No tocar
js/salas.js (legacy) No en uso React
Estados canónicos No modificar
Lógica financiera No tocar

4. Plan de implementación por fases

Fase 1: Infraestructura (sin tocar componentes)

Paso Archivo Cambio
1.1 src/lib/realtimeService.js Crear singleton realtime
1.2 src/store/useGameStore.js +now, +setNow
1.3 src/hooks/useGlobalTick.js Crear hook tick global
1.4 src/App.jsx Montar useGlobalTick()

Verificación: Build PASS. App funciona igual (tick corre pero nadie lo usa aún).

Fase 2: Realtime consolidación

Paso Archivo Cambio
2.1 src/hooks/useSalas.js Reemplazar canal propio por realtimeService.subscribe('sesiones', cb) + realtimeService.subscribe('salas', cb)
2.2 src/hooks/useDashboard.js Eliminar listener sesiones, mantener ventas+gastos via realtimeService
2.3 src/pages/TVDisplay.jsx Eliminar canal propio, usar realtimeService
2.4 src/pages/EventLive.jsx Eliminar canal propio, usar realtimeService

Verificación: 1 canal por tabla. Build PASS. Realtime funciona.

Fase 3: Estado unificado

Paso Archivo Cambio
3.1 src/pages/TVDisplay.jsx Reemplazar useState(sesiones) por useSalas()
3.2 src/pages/EventLive.jsx Reemplazar useState(sesiones) por useSalas()
3.3 src/pages/Dashboard.jsx Reemplazar useState(salas/productos) por Zustand
3.4 src/pages/Ventas.jsx Reemplazar useState(salas) por Zustand

Verificación: 1 fuente de verdad. Build PASS. UI funciona.

Fase 4: Refresh cleanup

Paso Archivo Cambio
4.1 src/hooks/useSalas.js Eliminar 9 cargarSesionesActivas() post-operación
4.2 src/hooks/useSalas.js Eliminar 3 cargarSalas() post-operación
4.3 src/components/salas/ModalFinalizarSesion.jsx Eliminar cargarSesionesActivas() post-RPC
4.4 src/components/salas/ModalTienda.jsx Eliminar cargarSesionesActivas() post-RPC
4.5 src/pages/TVDisplay.jsx Eliminar polling 20s
4.6 src/pages/EventLive.jsx Eliminar polling 20s

Verificación: 1 fetch por cambio DB (via realtime). Build PASS.

Fase 5: Tick global

Paso Archivo Cambio
5.1 src/components/salas/TarjetaSala.jsx Reemplazar useTemporizador por now del store
5.2 src/components/dashboard/LiveMonitor.jsx Eliminar timer, usar now
5.3 src/components/dashboard/MonitorSalasActivas.jsx Eliminar timer, usar now
5.4 src/components/salas/TablaSesionesActivas.jsx Eliminar timer, usar now
5.5 src/pages/TVDisplay.jsx Reemplazar useTimer por now
5.6 src/pages/EventLive.jsx Reemplazar useTimer por now

Verificación: 1 timer global. Build PASS.

Fase 6: React.memo

Paso Archivo Cambio
6.1 TarjetaSala.jsx React.memo(EstacionOcupada)
6.2 TarjetaSala.jsx React.memo(TarjetaSala)
6.3 LiveMonitor.jsx React.memo(SesionRow)
6.4 MonitorSalasActivas.jsx React.memo(CardSesion)
6.5 TVDisplay.jsx React.memo(TarjetaEstacion)
6.6 EventLive.jsx React.memo(CircleTimer)

Verificación: Renders reducidos. Build PASS.

Fase 7: Medición + regresión

Paso Acción
7.1 Medir renders/s antes/después (React DevTools Profiler)
7.2 Test manual: abrir sesión, agregar tiempo, agregar productos, finalizar, anular
7.3 Test manual: TVDisplay, EventLive muestran sesiones correctas
7.4 Test manual: Dashboard KPIs actualizan
7.5 Test manual: Ventas/Stock/Sesiones sin regresión
7.6 Build final

5. Tests necesarios

ID Test Validación
R1 Abrir sesión → aparece en Salas sin refresh manual ✅
R2 Finalizar sesión → desaparece de Salas sin refresh manual ✅
R3 Anular sesión → desaparece de Salas sin refresh manual ✅
R4 Agregar tiempo → timer actualiza sin refresh manual ✅
R5 Agregar productos → total actualiza sin refresh manual ✅
R6 TVDisplay muestra sesiones en tiempo real ✅
R7 EventLive muestra sesiones en tiempo real ✅
R8 Dashboard KPIs actualizan en tiempo real ✅
T1 1 timer global corriendo (no 47) ✅
T2 Timer cuenta regresivo correcto ✅
T3 Beep suena al vencer ✅
T4 Modo libre cuenta progresivo ✅
P1 React DevTools: renders/s < 10 (objetivo) ✅
P2 React DevTools: sin re-renders en cascada ✅
B1 npm run build PASS ✅
B2 Sin regresión Ventas ✅
B3 Sin regresión Stock ✅
B4 Sin regresión Sesiones ✅

6. Rollback

Rollback por fase

Cada fase es independiente y reversible:

Fase Rollback
1 (infra) Eliminar realtimeService.js, useGlobalTick.js, revertir store
2 (realtime) Revertir a canales individuales
3 (estado) Revertir a useState locales
4 (refresh) Restaurar cargarSesionesActivas() calls
5 (tick) Restaurar useTemporizador/useTimer
6 (memo) Quitar React.memo

Rollback total

git revert <commit>

7. Criterios de salida

Criterio Estado
Realtime centralizado (1 canal por tabla) ⏳
Sin subscriptions duplicadas ⏳
1 tick global ⏳
Reducción medible de renders (< 10/s) ⏳
Build PASS ⏳
Regresión Salas PASS ⏳
Sin regresión Ventas/Stock/Sesiones ⏳

8. Estimación de impacto

Métrica Antes Después (estimado)
Canales realtime para sesiones 4 1
Timers concurrentes (15 sesiones) 47 1
cargarSesionesActivas por operación 2 (manual + realtime) 1 (realtime)
Renders/s ~44 < 10
Fuentes de verdad sesiones 4 1 (Zustand)
Componentes memoizados 0 6

Plan pendiente aprobación. No se ejecutan cambios hasta confirmación. Sprint 0.3-C/D — Motor de Salas Core V2