RADIA Multi-Pantalla — Documentación técnica
Qué es
Sistema de sincronización entre ventanas del navegador que permite a un radiólogo trabajar con dos monitores: uno con el visor de imagen (2D / 3D / MPR) a pantalla completa, y otro con los hallazgos, informe, chat IA y herramientas de edición.
Flujo típico de un radiólogo profesional:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ MONITOR 1 (Visor) │ │ MONITOR 2 (Editor) │
│ │ │ │
│ 3D / MPR / 2D a máximo │ │ Lista de hallazgos │
│ tamaño posible │ │ Comentarios del doctor │
│ │ │ Chat IA │
│ Sin distracciones: │ │ Informes / PDF │
│ sin hallazgos, sin chat, │ │ Sugerencias IA │
│ sin barra de acciones │ │ Datos del paciente │
│ │ │ │
│ Solo: visor + 3D/MPR/2D │ │ Visor en miniatura │
│ toggle + barra sync │ │ (referencia rápida) │
└─────────────────────────────┘ └─────────────────────────────┘
▲ │
│ BroadcastChannel │
└─────────── sync ───────────────┘
Fase 1 — Mismo dispositivo (COMPLETADA)
Transporte
BroadcastChannel — API nativa del navegador. Sincroniza pestañas/ventanas del mismo origen sin backend.
- Funciona en Chrome, Edge, Firefox, Safari 15.4+
- Funciona con PWA instalada (mismo origen)
- Latencia: < 1ms (mismo proceso del navegador)
- No requiere servidor, WebSocket, ni polling
Archivos
| Archivo | Descripción |
|---|---|
src/hooks/useSyncChannel.ts |
Hook genérico de sync con protocolo tipado |
src/pages/study.astro |
Parsea ?sync=viewer|editor de la URL |
src/components/StudyExplorer.tsx |
Integración completa del sync en el componente principal |
Protocolo de mensajes
Canal: radia-sync-{studyId}
type SyncMessage =
| { type: 'NAVIGATE_SLICE'; slice: number } // Ir a un corte
| { type: 'SELECT_FINDING'; findingId: string|null } // Seleccionar/expandir hallazgo
| { type: 'VIEW_MODE'; mode: '2d'|'3d'|'mpr' } // Cambiar modo de visor
| { type: 'HIGHLIGHT_3D'; findingId: string|null } // Resaltar hallazgo en 3D
| { type: 'MPR_POSITION'; axial; coronal; sagittal } // Posición MPR (reservado)
| { type: 'FINDINGS_CHANGED' } // Recargar hallazgos
| { type: 'SCAN_STATUS'; scanning; text; pct } // Progreso del scan IA
| { type: 'PING'; role: SyncRole } // Heartbeat
| { type: 'PONG'; role: SyncRole } // Respuesta heartbeat
Flujo UX
- Usuario abre un estudio → experiencia normal
- Click en "Multi-pantalla" (icono
Monitor) en la barra de herramientas - La ventana actual pasa a modo Editor (rol
editor) - Se abre automáticamente una nueva ventana con
?sync=viewer - El usuario arrastra la nueva ventana al segundo monitor
- Indicador verde "Sincronizado" cuando la otra ventana responde al heartbeat
- Indicador ámbar "Esperando..." si la otra ventana no está conectada
- Botón
Unlink(icono de cadena rota) para desconectar en cualquier momento
Qué se sincroniza
| Acción | Dirección | Resultado |
|---|---|---|
| Click en hallazgo (editor) | Editor → Visor | El visor salta al corte del hallazgo |
| Cambiar slice (visor, scroll/drag) | Visor → Editor | El editor recibe el slice actual |
| Click en "3D" / "MPR" / "2D" | Bidireccional | Ambas ventanas cambian de modo |
| Highlight 3D en hallazgo | Bidireccional | La cámara 3D enfoca el hallazgo |
| Scan 360° IA completa | Editor → Visor | El visor recarga hallazgos frescos |
| Deep analysis completa | Editor → Visor | El visor recarga hallazgos actualizados |
| Borrar / añadir hallazgo | Editor → Visor | El visor recarga hallazgos |
| Progreso del scan IA | Editor → Visor | Barra de progreso visible en ambos |
Qué NO se sincroniza (por diseño)
- Modales (compartir, demographics, report builder) — estado local de UI
- Filtros de búsqueda de hallazgos — preferencia personal
- Campos de formulario en edición — estado efímero
- Comentarios del doctor (se sincronizan al guardar, via
FINDINGS_CHANGED)
Diseño anti-echo
Incoming sync msg → raw setter (setCurrentSlice) ← NO envía sync
User interaction → wrapper (handleSliceChange) ← SÍ envía sync
Los handlers de sync usan los setters de React directamente. Las interacciones del usuario pasan por wrappers que (1) actualizan estado local Y (2) envían el mensaje sync. No hay posibilidad de bucle infinito.
Modo Viewer — qué se oculta
- Botón "Scan 360° IA" / "Re-analizar"
- Barra de herramientas completa (PDF, Informes, Paciente, Sugerencias, Compartir, Asteroide, Navegar)
- Lista de hallazgos debajo del visor
- Panel lateral de Chat IA
- Grid de herramientas móvil
- Tabs móviles (Visor/Hallazgos/Chat)
- Auto-trigger del diálogo de análisis
Modo Viewer — qué se muestra
- Visor 2D/3D/MPR a
max-h-[calc(100vh-150px)] - Botones 3D / MPR para cambiar modo
- Barra de estado sync (rol + estado conexión + botón desconectar)
- Header con info del estudio (solo lectura)
Fase 2 — Cross-device (PENDIENTE)
Objetivo
Permitir que un radiólogo use dispositivos distintos sincronizados: p.ej. un PC de escritorio con el visor y una tablet con el informe, o dos PCs distintos.
Transporte
BroadcastChannel no funciona entre dispositivos. Opciones:
| Opción | Pros | Contras |
|---|---|---|
| Durable Objects + WebSocket | Tiempo real (<50ms), nativo en Cloudflare | Requiere DO binding, gestión de conexiones |
| Polling a D1 | Simple, sin nueva infraestructura | Latencia (1-3s), más reads a la DB |
| PartyKit / Liveblocks | SDK listo, multiplayer nativo | Dependencia externa, coste adicional |
Recomendación: Durable Objects + WebSocket — ya estamos en Cloudflare Workers, latencia real-time, y escala automáticamente.
Cambios necesarios
wrangler.toml:
+ [[durable_objects.bindings]]
+ name = "SYNC_ROOM"
+ class_name = "SyncRoom"
Nuevo archivo: functions/sync/SyncRoom.ts
- Durable Object class
- WebSocket upgrade handler
- Broadcast a todos los WebSocket conectados
- Room = studyId (un DO por estudio)
Nuevo archivo: functions/api/sync/[studyId].ts
- GET → WebSocket upgrade al DO
Modificar: src/hooks/useSyncChannel.ts
- Detectar si BroadcastChannel tiene peer (actual)
- Si no, fallback a WebSocket: ws://radia.cadences.app/api/sync/{studyId}
- Misma interfaz externa (send, sendSlice, peerConnected, etc.)
- El componente StudyExplorer NO cambia
Autenticación del WebSocket
El WebSocket se autentica con el JWT existente:
- Cliente envía
?token=xxxen la URL de upgrade - El Worker valida el JWT antes de hacer el upgrade
- El DO almacena el
userIden la conexión - Solo el owner del estudio puede unirse a la sala
Topología
Browser A ──WebSocket──► DO (SyncRoom) ◄──WebSocket── Browser B
│
▼
Broadcast msg a todos
los WS conectados
(excepto el emisor)
Fase 3 — Mejoras futuras (IDEAS)
Roles adicionales
presenter: proyectar el estudio en una pantalla de sala (para sesiones clínicas)student: modo observador con anotaciones propias que no afectan al original
Sync de MPR crosshairs
- Cuando el usuario mueve el crosshair en la vista axial del MPR, la otra pantalla actualiza las líneas de corte
- Requiere exponer setters de
axialSlice/coronalSlice/sagittalSlicevía ref imperativo enMPRViewer - El mensaje
MPR_POSITIONya está definido en el protocolo
Cursor compartido
- Mostrar la posición del cursor del otro usuario sobre la imagen
- Útil para "señalar" una zona durante una discusión entre colegas
- Nuevo tipo de mensaje:
CURSOR_POSITION { x, y, slice }
Voice chat integrado
- WebRTC peer-to-peer para audio
- Señalización vía el mismo canal sync (DO)
- Botón de "Llamar" en la barra sync
Anotaciones en tiempo real
- Dibujar sobre la imagen (flechas, círculos) y verlo en la otra pantalla
- Canvas overlay compartido
- Mensajes:
ANNOTATION_ADD,ANNOTATION_REMOVE
Resumen de estado
| Fase | Estado | Backend | Alcance |
|---|---|---|---|
| 1 — BroadcastChannel | ✅ Completada | Ninguno | Mismo navegador/dispositivo |
| 2 — Durable Objects | ⬜ Pendiente | DO + WebSocket | Cross-device |
| 3 — Mejoras | ⬜ Ideas | Varía | Cursor, voice, anotaciones |