- Módulo HITL (/cases): 6 patrones de formularios dinámicos para 53 tipos de caso con validación Zod - Módulo Monitor (/monitor): streaming token-a-token en tiempo real, auto-scroll y notas internas vía WebSocket - Arquitectura híbrida: REST (canal autoritativo) + WebSocket (difusión/streaming) - MSW para desarrollo sin backend, hooks de notificaciones/sonido/título preservados - Backend legacy movido a legacy/, archivos residuales eliminados de raíz
85 KiB
BITÁCORA DE DESARROLLO Y ESPECIFICACIONES
CONTROL DE ESTADO
- Último Agente Modificador: qa-tester
- Estado del Ciclo: [STATUS: PASSED] - Listo para Producción / Git
Fase 1: Requerimientos y Plan Inicial
1.1 Resumen Ejecutivo
- Tipo de Tarea: Migración y expansión (New Feature + Rewrite)
- Objetivo General: Reescribir el dashboard Claro Cases de vanilla HTML/CSS/JS a React + TypeScript + Vite, expandiéndolo con dos módulos: (1) Gestión de Casos HITL con formularios dinámicos por tipología y (2) Monitoreo completo de conversaciones en tiempo real con capacidad de intervención mediante notas internas, utilizando comunicación híbrida REST + WebSocket.
1.2 Contexto Técnico y Hallazgos
Estado Actual (Proyecto Claro Cases existente)
- Backend: Node.js + Express + SQLite (
better-sqlite3). Monolítico, acoplado al frontend. - Frontend: SPA vanilla HTML/CSS/JS. Sidebar de casos + panel de detalle.
- Comunicación: REST (CRUD) + SSE unidireccional para notificaciones.
- Persistencia: SQLite local (
database.sqlite). Tablarequestscon campos:id,title,description,status,external_id,cedula,tipo_solicitud,payload(JSON),handling_time,created_at. - Lógica actual: Dos flujos de resolución (validación Sí/No y texto libre). Cronómetros individuales con persistencia en
localStorage. Notificaciones de escritorio + sonido Web Audio + parpadeo de título. - Estilos: Sistema de diseño con CSS custom properties. Paleta orange/red/yellow/green. Tipografía Inter. Modo oscuro/claro. Sin framework CSS.
Proyecto de Referencia (Linguo Nexus)
- Stack: React 19 + TypeScript + Vite + Tailwind CSS v4.
- Estado: Zustand store centralizado.
- Ruteo: React Router con
/monitore/intervention. - Comunicación: REST (
/api/v1/conversations/active,/api/v1/tickets/pending) + WebSocket (/ws/monitor) con eventos tipados (init_state,conversation_started,user_message,agent_stream,hitl_required,hitl_resolved,CLIENT_TOOL_REQUEST). - Validación: Zod para payloads WebSocket y edge tool calling.
- UI: Kanban drag&drop (
@dnd-kit), streaming token-a-token con auto-scroll, renderizado Markdown (marked-react), leader election (navigator.locks).
Tipos de Caso (CSV: 45 registros)
- Aplicativos origen: AC+, ASCARD, DiMe, Formatos SGCS, Mi asistencia 360, Paradigma, RR, Phone Protect.
- Taxonomía de interacción aprobada: Confirmación simple, Confirmación + valor, Formulario multi-campo, Fecha simple, Texto libre, Solo lectura.
- Jerarquía secundaria: Filtro por aplicativo (columna A del CSV).
Módulos/Archivos Impactados
public/index.html: Reemplazado porindex.htmlde Vite + React root.public/style.css: Migrado a Tailwind config + CSS custom properties preservados.public/app.js: Reescrito en componentes React + Zustand store.server.js: Backend actual se reemplazará por backend Python (fuera del scope de esta migración frontend).db.js,schema.sql,database.sqlite: Reemplazados por backend Python.Consulta de aplicativos - Claro - Facturación.csv: Parseado e incrustado como datos estáticos ensrc/data/caseTypeDefinitions.ts.
1.3 Plan Lógico de Solución (Paso a Paso)
Paso 0 — Bootstrap del proyecto React + TypeScript + Vite y Reestructuración del Repositorio
- Reorganización del repositorio (previa al bootstrap):
- Mover todo el backend legacy (
server.js,db.js,schema.sql,database.sqlite,node_modules/,public/,package.json,package-lock.json,.env,.env.example) a un subdirectoriolegacy/. - Conservar en la raíz:
.git/,.opencode/,SPECIFICATION.md,Consulta de aplicativos - Claro - Facturación.csv,README.md.
- Mover todo el backend legacy (
- Inicializar proyecto con
npm create vite@latest . -- --template react-tsen el directorio raíz. - Instalar dependencias core:
react-router-dom,zustand,zod,date-fns,lucide-react. - Instalar dependencias de desarrollo:
msw(Mock Service Worker para desacoplar frontend del backend),@testing-library/react,vitest. - Configurar Tailwind CSS v4 con enfoque CSS-first (sin
tailwind.config.ts):- Definir design tokens en
src/index.cssmediante la directiva@theme:@import "tailwindcss"; @theme { --color-accent-orange: #ff4e00; --color-accent-yellow: #ffa600; --color-accent-red: #f80018; --color-accent-green: #10b981; --color-bg-base: #f0f2f5; --color-bg-surface: #ffffff; --color-bg-elevated: #f8fafc; --color-bg-hover: #e2e8f0; --color-text-primary: #1e293b; --color-text-secondary: #475569; --color-text-muted: #94a3b8; --color-border: rgba(0, 0, 0, 0.08); --color-border-accent: rgba(255, 78, 0, 0.25); --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-xl: 16px; --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --shadow-lg: 0 12px 24px rgba(0, 0, 0, 0.12); --font-family-sans: 'Inter', system-ui, sans-serif; --transition-default: 0.18s cubic-bezier(0.4, 0, 0.2, 1); } - Modo oscuro mediante
@custom-variant dark (&:where(.dark, .dark *))con overrides de variables en bloque@media (prefers-color-scheme: dark)y clase.darktoggleada manualmente. - Animaciones definidas como
@keyframesen el mismo archivo CSS.
- Definir design tokens en
- Estructura de carpetas:
src/ components/ layout/ (AppShell, Sidebar, Header, StatusBar) cases/ (CaseCard, CaseDetail, FormRenderer, Timer, TypeBadge, ApplicativeFilter) monitor/ (ConversationCard, ChatFeed, MessageBubble, InternalNoteBanner, InterventionPanel) shared/ (StatusBadge, SearchBar, TabsBar, Modal, EmptyState) hooks/ (useWebSocket, useTimer, useNotification) services/ (api.ts, wsClient.ts) store/ (useAppStore.ts — slices: cases, conversations, ui) types/ (index.ts, wsProtocol.ts, caseTypes.ts) pages/ (CasesPage.tsx, MonitorPage.tsx) data/ (caseTypeDefinitions.ts — parsed from CSV) App.tsx main.tsx
Paso 1 — Sistema de Tipos y Contratos
-
src/types/index.ts: Interfaces base:CaseRequest: id, title, description, status, externalId, cedula, tipoSolicitud, payload, handlingTime, createdAt, applicative, uiPattern.Conversation: id, clientId, agentId, status, messages[], createdAt.Message: id, conversationId, role (user/agent/system/internal), content, timestamp, metadata?.CaseUITypeenum:SIMPLE_CONFIRMATION,CONFIRMATION_WITH_VALUE,MULTI_FIELD_FORM,DATE_SIMPLE,FREE_TEXT,READ_ONLY.CaseStatus:PENDING,IN_PROGRESS,RESOLVED,FAILED.AgentStatus:ONLINE,BUSY,OFFLINE.
-
src/types/wsProtocol.ts: Contratos WebSocket tipados (Zod):- Eventos entrantes (backend → frontend):
init_state:{ conversations: Conversation[], activeCases: CaseRequest[] }conversation_started:{ conversation: Conversation }conversation_update:{ conversationId: string, message: Message }agent_stream:{ conversationId: string, token: string }agent_status_update:{ agentId: string, status: AgentStatus }hitl_request:{ case: CaseRequest, conversationId: string }hitl_resolved:{ caseId: string, resolution: object }
- Eventos salientes (frontend → backend):
internal_note:{ conversationId: string, content: string }(sinadvisorId; backend deriva identidad)
- Eventos entrantes (backend → frontend):
-
src/data/caseTypeDefinitions.ts: Mapeo completo de los 45 tipos del CSV aCaseTypeDefinition:interface CaseTypeDefinition { toolName: string; // Ej: "Validar_Proporcionales_Movil" applicative: string; // Ej: "AC+" specialist: string; // Ej: "Cobros adicionales - Móvil" inputData: string; // Ej: "Número de la línea" steps: string[]; // Paso a paso objective: string; responseFormat: string; // Formato de respuesta esperada (según CSV) document: string; // Categoría documental uiPattern: CaseUIType; // Clasificación de UI (6 familias visuales) formFields: FormField[]; // Campos del formulario dinámico validationSchema: ZodSchema; // Esquema Zod de validación del payload de respuesta payloadBuilder: (formData: Record<string, unknown>) => object; // Serializador a payload para el backend } interface FormField { key: string; // Identificador del campo label: string; // Etiqueta visible type: 'text' | 'number' | 'currency' | 'date' | 'select' | 'textarea' | 'toggle'; required: boolean; placeholder?: string; options?: { value: string; label: string }[]; // Para type: 'select' min?: number; // Para type: 'number'/'currency' max?: number; conditionalOn?: { field: string; value: unknown }; // Campo condicional }- Ejemplo concreto —
Plan_De_Pagos_EF(ASCARD, Equipos financiados):{ toolName: "Plan_De_Pagos_EF", applicative: "ASCARD", uiPattern: CaseUIType.MULTI_FIELD_FORM, formFields: [ { key: "numero_cuotas", label: "Número de cuotas", type: "number", required: true, min: 1 }, { key: "valor_cuota", label: "Valor de la cuota", type: "currency", required: true }, { key: "dia_corte", label: "Día de corte", type: "number", required: true, min: 1, max: 31 }, { key: "dia_limite_pago", label: "Día límite de pago", type: "number", required: true, min: 1, max: 31 } ], validationSchema: z.object({ numero_cuotas: z.number().int().min(1), valor_cuota: z.number().positive(), dia_corte: z.number().int().min(1).max(31), dia_limite_pago: z.number().int().min(1).max(31) }), payloadBuilder: (data) => ({ numero_cuotas: data.numero_cuotas, valor_cuota: data.valor_cuota, dia_corte: data.dia_corte, dia_limite_pago: data.dia_limite_pago }) }
- Ejemplo concreto —
Paso 2 — Capa de Servicios y Store (arquitectura híbrida: REST autoritativo + WS difusión)
-
src/services/api.ts: Cliente REST (canal autoritativo de escritura):GET /api/v1/cases?status=&applicative=&search=&offset=&limit=→{ items: CaseRequest[], total: number }(filtrable, paginado).GET /api/v1/cases/:id→CaseRequest(detalle de caso).POST /api/v1/cases/:id/resolve→CaseRequest(canal único de resolución; el backend derivaadvisorIddel token de sesión).GET /api/v1/conversations/active→Conversation[].- Base URL configurable via variable de entorno (
VITE_API_BASE_URL). - Se implementará una capa de MSW (Mock Service Worker) con handlers que simulen estas respuestas para desarrollo sin backend.
-
src/hooks/useWebSocket.ts: Hook de conexión WebSocket (solo difusión/streaming, sin escritura de negocio):- Conexión a
ws://<host>/ws/dashboard. - Reconexión automática con backoff exponencial (inicio 1s, máx 30s, factor 2x).
- Al reconectar, el backend envía
init_statepara resincronizar; el frontend reemplaza el estado local completo. - Parseo con Zod de cada mensaje entrante usando el envelope estándar (ver Paso 8).
- Dispatch a acciones del store según
payload.type. - Envío de eventos salientes solo para
internal_note(sinadvisorId; el backend deriva la identidad). - Indicador de estado de conexión en el store (
connected|disconnected|reconnecting). - No se emite
hitl_responsepor WebSocket; la resolución de casos es exclusiva de REST.
- Conexión a
-
src/store/useAppStore.ts: Store centralizado Zustand con slices:- casesSlice:
cases[],selectedCaseId,totalCases,fetchCases(filters),upsertCase(),resolveCase(),deleteCase(). - conversationsSlice:
conversations[],selectedConversationId,fetchConversations(),upsertConversation(),addMessage(),appendToken(). - uiSlice:
sidebarTab,searchQuery,applicativeFilter,isDarkMode,wsStatus. - timerSlice: Timers gestionados con
useRefpara intervalos (evitar re-renders);localStoragesolo como caché de UI, no como fuente de verdad parahandling_time(el backend calcula constartedAt/resolvedAt).
- casesSlice:
Paso 3 — Componentes Compartidos
StatusBadge: Badge de estado con colores por estado (pending/in_progress/resolved/failed).SearchBar: Input de búsqueda con debounce.TabsBar: Pestañas de filtro (Todos/Pendientes/Finalizados).ApplicativeFilter: Dropdown/chips para filtrar por aplicativo (AC+, ASCARD, RR, etc.).Timer: Cronómetro independiente por caso con persistencia enlocalStorage(migrado del JS actual).Modal: Diálogo de confirmación genérico.EmptyState: Estado vacío para paneles sin selección.
Paso 4 — Módulo de Gestión de Casos HITL (/cases)
CasesPage.tsx: Layout maestro: sidebar izquierda (lista de casos) + panel derecho (detalle/acciones).CaseCard.tsx: Tarjeta de caso en la lista con título, status badge, timer (si activo), tipo de solicitud, aplicativo, fecha.CaseDetail.tsx: Vista detallada del caso seleccionado con:- Metadata grid (ID, cédula, tipo solicitud, aplicativo).
- Descripción del caso.
- Payload de datos entrantes.
FormRenderer.tsx: Componente dinámico que renderiza el formulario adecuado segúnuiPattern:SIMPLE_CONFIRMATION→ Botones "Sí" / "No".CONFIRMATION_WITH_VALUE→ Radio group (Sí/No) + campo numérico con prefijo$.MULTI_FIELD_FORM→ Formulario con campos definidos enformFields[](text, number, select, date).DATE_SIMPLE→ Date picker con formatodd-mm-aaaa.FREE_TEXT→ Textarea con placeholder contextual.READ_ONLY→ Panel informativo sin campos editables, solo botón "Marcar como revisado".
- Panel de operación con timer y botones de acción.
- Instrucciones paso a paso del aplicativo (del CSV) colapsables en acordeón.
- Flujo de resolución:
- Asesor abre caso → timer inicia automáticamente.
- Completa formulario dinámico → botón "Enviar resolución".
- Se envía
POST /api/v1/cases/:id/resolve(REST, canal autoritativo) con payload estructurado. El backend difundehitl_resolvedpor WS a todos los asesores. - Caso pasa a estado
resolvedy timer se detiene.
Paso 5 — Módulo de Monitoreo (/monitor)
MonitorPage.tsx: Layout de dos columnas: lista de conversaciones (izquierda estrecha) + feed de chat (derecha amplia).ConversationCard.tsx: Tarjeta de conversación activa mostrando:- ID/Nombre del cliente.
- Último mensaje (truncado).
- Indicador de streaming activo (spinner).
- Badge de HITL pendiente.
- Estado del agente asignado.
ChatFeed.tsx: Feed de mensajes con:- Auto-scroll inteligente (respeta scroll manual del usuario, reanuda al llegar al fondo).
- Renderizado de mensajes con diferenciación visual por rol (cliente, agente, sistema).
- Streaming token-a-token: Concatenación progresiva de tokens en el último mensaje del agente.
MessageBubble.tsx: Burbuja de mensaje individual con timestamp y rol.InternalNoteBanner.tsx: Banner de intervención que permite al asesor:- Escribir nota interna en un textarea.
- Previsualizar cómo se verá en la conversación (etiquetada como "Nota interna").
- Enviar vía WebSocket (
internal_note).
InterventionPanel.tsx: Panel lateral o modal para cuando se detecta un caso HITL asociado a la conversación activa.
Paso 6 — Ruteo y Shell de Aplicación
App.tsx: Router con dos rutas:/→ redirect a/cases./cases→CasesPage./monitor→MonitorPage.
AppShell.tsx: Layout global:Header: Logo Claro Cases, badge "En vivo", indicador de conexión WebSocket, toggle tema oscuro.Sidebar: Navegación entre módulos (Casos, Monitor) con iconos delucide-react.- Inicializa WebSocket y fetch inicial al montar.
Paso 7 — Migración de Estilos (Preservar línea gráfica)
- Extraer todos los design tokens del
style.cssactual a bloques@themeensrc/index.css(ver Paso 0 para la configuración completa). - Mapear cada clase CSS a utilidades Tailwind equivalentes:
.app-header→flex items-center justify-between h-[50px] px-4 border-b bg-surface shadow-sm.case-card→bg-elevated border border-border rounded-md p-3 cursor-pointer transition.btn-primary→bg-accent-orange text-white px-4 py-2 rounded-md font-semibold
- Preservar animaciones (
slideIn,fadeIn,pulse-op) como keyframes en Tailwind config. - Scrollbar styling → utilities de Tailwind o CSS global.
- Modo oscuro: conservar lógica de toggle con
classstrategy de Tailwind + persistencia enlocalStorage.
Paso 8 — Contratos de Comunicación Completos (para el equipo Python)
8.1 Envelope WebSocket Estándar
Todo mensaje WebSocket (en ambas direcciones) usa el siguiente envelope JSON:
{
"type": "string", // Tipo de evento (ej. "agent_stream")
"eventId": "uuid", // ID único del evento para deduplicación
"occurredAt": "ISO-8601",// Timestamp UTC del lado emisor
"payload": { } // Carga específica del evento
}
8.2 REST Endpoints (canal autoritativo)
| Método | Ruta | Query Params | Body | Respuesta |
|---|---|---|---|---|
GET |
/api/v1/cases |
status, applicative, search, offset, limit |
— | { items: CaseRequest[], total: number } |
GET |
/api/v1/cases/:id |
— | — | CaseRequest |
POST |
/api/v1/cases/:id/resolve |
— | { action, payload, note? } |
CaseRequest (updated) |
GET |
/api/v1/conversations/active |
— | — | Conversation[] |
GET |
/api/v1/conversations/:id |
— | — | Conversation (con mensajes) |
Nota para backend:
POST /cases/:id/resolveno recibeadvisorId. El backend debe derivar la identidad del asesor desde el token de autenticación de la sesión HTTP (Bearer token o cookie).
8.3 WebSocket Events (servidor → cliente)
Evento type |
Payload | Trigger |
|---|---|---|
init_state |
{ conversations: Conversation[], activeCases: CaseRequest[] } |
Al conectar o reconectar |
conversation_started |
{ conversation: Conversation } |
Nueva conversación |
conversation_ended |
{ conversationId: string, endedAt: ISO-8601 } |
Conversación finalizada |
user_message |
{ conversationId: string, message: Message } |
Mensaje completo de usuario |
agent_stream_started |
{ conversationId: string, messageId: string } |
Inicio de streaming del agente |
agent_stream_chunk |
{ conversationId: string, messageId: string, token: string, index: number } |
Token individual con índice de orden |
agent_stream_completed |
{ conversationId: string, messageId: string, fullContent: string } |
Cierre de streaming; fullContent es el texto completo para verificación |
agent_status_update |
{ agentId: string, status: AgentStatus } |
Cambio de estado del agente |
hitl_request |
{ case: CaseRequest, conversationId: string } |
Se requiere intervención humana |
hitl_resolved |
{ caseId: string, resolution: object } |
Caso resuelto (broadcast a todos los asesores) |
error |
{ code: string, message: string, details?: object } |
Error del servidor notificable al frontend |
8.4 WebSocket Events (cliente → servidor)
Evento type |
Payload | Trigger |
|---|---|---|
internal_note |
{ conversationId: string, content: string } |
Asesor inyecta nota interna |
Nota: El backend deriva
advisorIddel contexto de la conexión WebSocket autenticada. El cliente no envía identificadores de asesor en ningún payload.
8.5 Estrategia de Reconexión
- Backoff exponencial: 1s → 2s → 4s → 8s → 16s → 30s (máx).
- Al reconectar exitosamente, el servidor envía
init_statecon el estado completo actual. - El frontend reemplaza
conversationsyactiveCasescon los datos deinit_state. - Durante la desconexión, el frontend muestra indicador "Reconectando..." y deshabilita acciones de escritura (resolución de casos e inyección de notas).
8.6 Estrategia de Streaming (lado frontend)
agent_stream_started: crear mensaje placeholder en la conversación conisStreaming: true.agent_stream_chunk: concatenar token al contenido del mensaje usando elindexpara garantizar orden (no asumir orden de llegada de red).agent_stream_completed: marcar mensaje conisStreaming: false, reemplazar contenido confullContentpara verificación de integridad.- Las actualizaciones al store se bufferizan cada 50ms (máximo 20 actualizaciones/segundo) para evitar re-renders excesivos. Solo la conversación activa/seleccionada dispara re-renders de UI; las demás acumulan tokens en el store sin re-render hasta ser seleccionadas.
1.4 Criterios de Aceptación
- CA-1: Proyecto arranca con
npm run devsobre Vite + React + TypeScript, sirviendo enlocalhost:5173. - CA-2: Ruteo funcional:
/redirige a/cases; navegación entre/casesy/monitorvía sidebar con iconoslucide-react. - CA-3: Sidebar de casos muestra lista con búsqueda textual (debounced 300ms), pestañas (Todos/Pendientes/Finalizados) y filtro secundario por aplicativo (chips/dropdown con los 8 aplicativos del CSV).
- CA-4: Al seleccionar un caso, el panel de detalle renderiza el formulario dinámico correcto según el
uiPatterndel tipo de caso, con validación Zod antes de enviar. - CA-5: El formulario
MULTI_FIELD_FORMrenderiza campos específicos (ej. paraPlan_De_Pagos_EF: número de cuotas, valor cuota, día corte, día límite) con validación por tipo (número, moneda, rango) y mensajes de error inline. - CA-6: Timer independiente por caso con persistencia en
localStoragecomo cache de UI; elhandling_timeoficial lo calcula el backend constartedAt/resolvedAt. - CA-7: Resolución de caso se envía exclusivamente por REST (
POST /cases/:id/resolve). El backend difundehitl_resolvedpor WS a todos los asesores conectados. - CA-8: Módulo de monitoreo muestra lista de conversaciones activas con streaming token-a-token usando eventos
agent_stream_started/agent_stream_chunk/agent_stream_completed, con buffer de 50ms para limitar re-renders a 20 fps. - CA-9: Chat feed con auto-scroll inteligente y diferenciación visual de 4 roles: cliente, agente, sistema, nota interna (esta última con badge "Interno" y fondo distintivo).
- CA-10: Asesor puede inyectar nota interna desde el monitor; se emite
internal_notepor WebSocket (sinadvisorIden el payload). - CA-11: Indicador visual de estado de conexión WebSocket en el header: 🟢 Conectado / 🟡 Reconectando... / 🔴 Desconectado. Durante desconexión, se deshabilitan acciones de escritura.
- CA-12: Modo oscuro funcional con toggle (ícono sol/luna) y persistencia en
localStorage; implementado con@custom-variant darkde Tailwind v4. - CA-13: Paleta de colores, tipografía Inter, sombras, radios, transiciones y animaciones (
slideIn,fadeIn,pulse-op) preservados del diseño original mediante tokens@themeen CSS. - CA-14: Los 45 tipos de caso del CSV están mapeados en
src/data/caseTypeDefinitions.tsconuiPattern,formFields,validationSchema(Zod) ypayloadBuilderpara cada uno. - CA-15: Backend Python puede implementarse siguiendo los contratos REST + WebSocket documentados en la sección 1.3 Paso 8 sin ambigüedades.
- CA-16: Capa MSW operativa con handlers para todos los endpoints REST y simulación de eventos WebSocket, permitiendo desarrollo full-stack del frontend sin backend real.
- CA-17: Paridad funcional con el sistema actual: notificaciones de escritorio HTML5, alerta sonora (Web Audio API) y parpadeo de título al recibir nuevos casos (
hitl_request). - CA-18: Reconexión WebSocket con backoff exponencial; al reconectar se recibe
init_statey se reemplaza el estado local completo.
Paso 9 — Capa de Mocks (MSW) y Funcionalidades Preservadas
- MSW (Mock Service Worker) para desarrollo desacoplado:
- Handlers REST que simulan
/api/v1/cases,/api/v1/cases/:id,/api/v1/cases/:id/resolve,/api/v1/conversations/active. - Datos de prueba: 10-15 casos de ejemplo cubriendo los 6
uiPatterny múltiples aplicativos. - 3-5 conversaciones simuladas con mensajes de diferentes roles.
- El MSW se activa solo en modo desarrollo (
VITE_ENABLE_MSW=true).
- Handlers REST que simulan
- Funcionalidades preservadas del sistema actual:
- Notificaciones de escritorio HTML5: Hook
useNotificationque emitenew Notification()al recibirhitl_request; click en notificación navega a/casescon el caso seleccionado. - Alerta sonora: Hook
useSoundcon Web Audio API (chime de dos tonos C5→E5, volumen 0.08), activado solo tras primer gesto del usuario (política de autoplay). - Parpadeo de título: Efecto de título alternante cuando la pestaña no está enfocada y llegan nuevos casos; se limpia al enfocar.
- Detección de foco de pestaña:
document.visibilitychange+window.focus/blurpara controlar notificaciones.
- Notificaciones de escritorio HTML5: Hook
1.5 Jerarquía de Aplicativos (para filtro secundario)
| Aplicativo | Descripción | N° de Tipos |
|---|---|---|
| AC+ | Atención al Cliente (móvil) | 13 |
| ASCARD | Equipos financiados | 9 |
| DiMe | Ajustes online | 8 |
| Formatos SGCS | Cambios de ciclo | 2 |
| Mi asistencia 360 | Escalamientos de pago | 2 |
| Paradigma | Facturación hogar/móvil | 2 |
| RR | Recepción y Radicación (hogar) | 12 |
| Phone Protect | Desbloqueo IMEI | 1 |
1.6 Riesgos Identificados (preliminar, para debate)
- Streaming token-a-token: La semántica de concatenación depende de que el backend envíe tokens con un
conversationIdconsistente. Si hay mensajes simultaneous, el orden de tokens debe estar garantizado. - Persistencia de timers: Actualmente en
localStorage. En React, el estado del timer debe sincronizarse entre el store ylocalStoragesin causar re-renders excesivos (usar refs para el intervalo). - Tailwind + CSS variables: La migración de CSS puro a Tailwind requiere mapear cada utilidad. Los gradientes (
linear-gradient) y-webkit-background-clipnecesitan configuración adicional en Tailwind. - CSV parsing: Los 45 registros deben clasificarse manualmente en los 6
uiPattern. Algunos casos (ej.Unificar_Factura_EFque usa ASCARD + Paradigma) requieren lógica multi-aplicativo. - WebSocket reconnection: La lógica de reconexión debe preservar el estado local y re-sincronizar al reconectar (recibir
init_state).
Fase 2: Auditoría de Arquitectura y Debate Técnico (v3 — Aprobada)
2.1 Resumen de Hallazgos
La Fase 1 pasó por dos ciclos de auditoría. En la primera iteración se identificaron 10 riesgos (5 bloqueantes). Tras las correcciones del usuario, la segunda auditoría detectó 3 inconsistencias residuales de redacción: referencias a hitl_response como canal WS, mención de tailwind.config.ts en el Paso 7, y advisorId persistente en una definición de tipo. Las tres fueron corregidas. El plan es ahora consistente, blindado y viable sin bloqueantes.
2.2 Riesgos Resueltos (todos)
- ✅ R1 (Tailwind v4): Resuelto —
@theme+@custom-variant dark; toda referencia atailwind.config.tspurgada. - ✅ R2 (Doble canal): Resuelto — REST como único canal autoritativo;
hitl_responseeliminado de tipos, Paso 4 y contratos WS. - ✅ R3 (Contratos WS): Resuelto — Envelope estándar, eventos de streaming explícitos,
error, reconexión documentada. - ✅ R4 (advisorId): Resuelto — Eliminado de todos los payloads cliente→servidor y tipos; consistente en REST y WS.
- ✅ R5 (REST filtrable): Resuelto —
GET /api/v1/cases?status=&applicative=&search=&offset=&limit=. - 🟡 R6–R10: Mitigados con acciones documentadas en el plan (buffer streaming, MSW, reestructuración repo, funcionalidades preservadas, taxonomía ampliada con Zod).
2.3 Directrices para el Desarrollador
- Regla 1: La resolución de casos es exclusivamente REST (
POST /cases/:id/resolve). WebSocket solo difunde y streamea. - Regla 2: Ningún payload cliente→servidor contiene identificadores de asesor. El backend deriva la identidad.
- Regla 3: Tailwind v4 se configura exclusivamente vía CSS (
@theme,@custom-variant dark). Sintailwind.config.ts. - Regla 4: El streaming usa el buffer de 50ms y solo re-renderiza la conversación seleccionada.
- Regla 5: Los 45 tipos de caso deben tener
validationSchema(Zod) ypayloadBuilderdefinidos antes de declarar completo el mapeo.
2.4 Veredicto Final
- Estado del plan: En Implementación.
- El plan es internamente consistente, los contratos REST/WS están completamente especificados, y el frontend puede desarrollarse de forma desacoplada mediante MSW. No hay bloqueantes residuales.
Fase 3: Registro de Implementación
3.1 Paso 0 — Bootstrap y Setup
legacy/server.js: [Creado] → Copia del backend Express legacy.legacy/db.js: [Creado] → Copia del módulo de base de datos SQLite (better-sqlite3).legacy/schema.sql: [Creado] → Copia del esquema SQL de la tablarequests.legacy/package.json: [Creado] → Copia del manifiesto de dependencias del backend legacy.legacy/.env: [Creado] → Copia de variables de entorno del backend legacy.legacy/.env.example: [Creado] → Copia con comentarios del backend legacy.legacy/public/index.html: [Creado] → Copia del HTML del frontend vanilla legacy.legacy/public/style.css: [Creado] → Copia de los estilos CSS del frontend vanilla legacy.legacy/public/app.js: [Creado] → Copia de la lógica JS del frontend vanilla legacy.package.json: [Modificado] → Reemplazado por el manifiesto del nuevo proyecto Vite + React + TypeScript con todas las dependencias core y de desarrollo.vite.config.ts: [Creado] → Configuración de Vite con plugin React y Tailwind CSS v4, proxy para API REST y WebSocket.tsconfig.json: [Creado] → Configuración raíz de TypeScript con referencias atsconfig.app.jsonytsconfig.node.json.tsconfig.app.json: [Creado] → Configuración TS para la aplicación React (ES2020, JSX react-jsx, paths con alias@/).tsconfig.node.json: [Creado] → Configuración TS para Vite y herramientas de Node.index.html: [Creado] → Entry point de Vite con fuente Inter de Google Fonts, módulo ES parasrc/main.tsx..env: [Modificado] → Nuevas variables de entorno para frontend (VITE_API_BASE_URL,VITE_WS_URL,VITE_ENABLE_MSW)..env.example: [Creado] → Template de variables de entorno del frontend..gitignore: [Creado] → Ignoranode_modules/,dist/,.env,database.sqlite, entre otros.src/vite-env.d.ts: [Creado] → Declaraciones de tipos paraimport.meta.envcon tipado estricto.src/main.tsx: [Creado] → Punto de entrada React con inicialización condicional de MSW (VITE_ENABLE_MSW=true).src/App.tsx: [Creado] → Componente raíz con React Router (/,/cases,/monitor), redirect a/cases.src/index.css: [Creado] → Estilos globales con Tailwind CSS v4, design tokens@theme, modo oscuro con@custom-variant dark, animacionesslideIn/fadeIn/pulse-op, scrollbar personalizado.src/mocks/browser.ts: [Creado] → Setup de MSW Worker para interceptar peticiones REST en desarrollo.src/mocks/handlers.ts: [Creado] → Handlers MSW para endpoints REST mock: 10 casos de prueba (cubriendo los 6uiPattern), 3 conversaciones simuladas, handlers para/api/v1/cases,/api/v1/cases/:id,/api/v1/cases/:id/resolve,/api/v1/conversations/active,/api/v1/conversations/:id.src/components/layout/.gitkeep: [Creado] → Marcador de directorio paralayout/.src/components/cases/.gitkeep: [Creado] → Marcador de directorio paracases/.src/components/monitor/.gitkeep: [Creado] → Marcador de directorio paramonitor/.src/components/shared/.gitkeep: [Creado] → Marcador de directorio parashared/.src/hooks/.gitkeep: [Creado] → Marcador de directorio parahooks/.src/services/.gitkeep: [Creado] → Marcador de directorio paraservices/.src/store/.gitkeep: [Creado] → Marcador de directorio parastore/.src/types/.gitkeep: [Creado] → Marcador de directorio paratypes/.src/pages/.gitkeep: [Creado] → Marcador de directorio parapages/.src/data/.gitkeep: [Creado] → Marcador de directorio paradata/.
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se estructuró el proyecto siguiendo el principio de agnosticismo y separación de conceptos. El backend legacy se aisló completamente en
legacy/, dejando la raíz del proyecto limpia para el nuevo frontend Vite + React + TypeScript. La configuración de Tailwind v4 es CSS-first (sintailwind.config.ts), usando la directiva@themepara definir los design tokens y@custom-variant darkpara el modo oscuro. Se implementó MSW como capa de mockeo REST para desarrollo desacoplado del backend. -
Mitigación de Riesgos (Fase 2):
- Regla 1 (REST como canal autoritativo): Los handlers de MSW simulan
POST /cases/:id/resolvecomo endpoint REST exclusivo para resolución de casos. No se incluye WebSocket para escritura. - Regla 2 (Sin advisorId): Los handlers MSW no requieren
advisorIden los payloads, en línea con los contratos especificados. - Regla 3 (Tailwind v4 CSS-first): No existe
tailwind.config.ts. Toda la configuración está ensrc/index.cssmediante@themey@custom-variant. - Regla 4 (Streaming buffer 50ms): Se documentó en la spec; la implementación del buffer se realizará en el hook
useWebSocketen fases posteriores. - Regla 5 (45 tipos de caso con Zod): Los mock data en handlers incluyen 10 casos de ejemplo cubriendo los 6
uiPattern; la implementación completa de los 45 tipos se hará en Paso 1.
- Regla 1 (REST como canal autoritativo): Los handlers de MSW simulan
3.3 Notas Técnicas para el Tester
-
Dependencias Añadidas:
- Core:
react,react-dom,react-router-dom,zustand,zod,date-fns,lucide-react - Dev:
typescript,vite,@vitejs/plugin-react,tailwindcss,@tailwindcss/vite,msw,@testing-library/react,@testing-library/jest-dom,vitest,@types/react,@types/react-dom
- Core:
-
Puntos Críticos a Probar:
- Restauración manual necesaria: Los archivos
node_modules/,package-lock.json,database.sqlitey el directoriopublic/(antiguo) aún existen en la raíz y deben moverse manualmente alegacy/o eliminarse. Ejecutar:rm -rf node_modules/ public/ package-lock.json database.sqlite mv server.js db.js schema.sql legacy/ 2>/dev/null; true - MSW no inicializado: El archivo
public/mockServiceWorker.jsdebe generarse ejecutandonpx msw init public/ --save. - Verificar que el alias
@/funciona: Eltsconfig.app.jsondefinepathscon@/*→src/*. Confirmar que Vite resuelva los imports correctamente. - Modo oscuro: El
@custom-variant darkusa la clase.darken un contenedor padre. Verificar que al agregarclass="dark"al<html>se activen los colores oscuros. - MSW handlers: Verificar que
VITE_ENABLE_MSW=trueactiva la interceptación en desarrollo y que los endpoints mock responden correctamente (ej.curl http://localhost:5173/api/v1/cases).
- Restauración manual necesaria: Los archivos
3.1 Paso 1 — Sistema de Tipos, Contratos WebSocket y Mapeo de 53 Casos del CSV
src/types/index.ts: [Creado] → Define las interfaces base del sistema (CaseRequest, Conversation, Message, FormField, CaseTypeDefinition) y los enums (CaseUIType, CaseStatus, AgentStatus, MessageRole). Utiliza tipado estático estricto conz.ZodTypepara los campos de validación de esquemas en CaseTypeDefinition.src/types/wsProtocol.ts: [Creado] → Implementa el envelope WebSocket estándar con Zod (WSEnvelopeSchema), más los 11 schemas de eventos servidor→cliente (init_state, conversation_started, conversation_ended, user_message, agent_stream_started, agent_stream_chunk, agent_stream_completed, agent_status_update, hitl_request, hitl_resolved, error) y 1 schema cliente→servidor (internal_note). Incluye funciones helpercreateWSEnvelope(),validateServerEvent(),validateClientEvent()con mapas discriminadores por tipo de evento para validación dinámica en el hook useWebSocket.src/data/caseTypeDefinitions.ts: [Creado] → Mapeo completo de los 53 registros del CSV a objetosCaseTypeDefinitioncon:- Clasificación de
uiPatternsegún las 6 familias visuales (SIMPLE_CONFIRMATION, CONFIRMATION_WITH_VALUE, MULTI_FIELD_FORM, DATE_SIMPLE, FREE_TEXT, READ_ONLY). formFieldsderivados delresponseFormaty casos especiales documentados (Escalar_Pagos_No_Abonados con 9 campos, Validar_OTT_1/2 con 6 y 8 campos respectivamente, etc.).validationSchemaZod para cada entrada, con validaciones de tipo (número, moneda, toggle, fecha en formato dd-mm-aaaa, select con enum).payloadBuilderpara serializar el formulario al payload del backend.- Mapas helper
caseTypeByToolNameycaseTypesByApplicativepara búsqueda rápida. - Helpers de fábrica (
simpleConfirmation,confirmationWithValue,dateSimple,freeText,readOnly,multiFieldForm) para reducir repetición de código.
- Clasificación de
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se respetó el principio de separación de conceptos manteniendo las interfaces de dominio (
CaseRequest,Conversation,Message) ensrc/types/index.tsdesacopladas de los contratos de comunicación (wsProtocol.ts) y de los datos estáticos (caseTypeDefinitions.ts). Los helpers de fábrica en caseTypeDefinitions.ts permiten definir esquemas Zod y builders de payload de forma declarativa y consistente, eliminando la duplicación masiva de código. -
Mitigación de Riesgos (Fase 2):
- Regla 1 (REST como canal autoritativo): En
wsProtocol.tsno existe ningún eventohitl_response; la resolución de casos se realiza exclusivamente vía REST. El protocolo WS solo define eventos de difusión/streaming. - Regla 2 (Sin advisorId): En
wsProtocol.ts, el payloadinternal_notesolo contieneconversationIdycontent. No se incluyeadvisorIden ningún payload cliente→servidor. El backend debe derivar la identidad del contexto de conexión. - Regla 5 (45 tipos de caso con Zod): Se implementaron 53 registros del CSV (la diferencia con la cifra "45" se debe a que algunos toolName se repiten con diferentes especialistas/objetivos). Cada registro tiene su
validationSchemaZod ypayloadBuildercompletamente implementados, sin placeholders ni TODOs.
- Regla 1 (REST como canal autoritativo): En
3.3 Notas Técnicas para el Tester
- Dependencias Añadidas: Ninguna nueva (zod ya estaba incluida en Paso 0).
- Puntos Críticos a Probar:
- Tipos estrictos: Verificar que
tsc --noEmit(onpm run lint) no produce errores de tipo. Archivos clave:src/types/index.ts,src/types/wsProtocol.ts,src/data/caseTypeDefinitions.ts. - Validación Zod de eventos WS: Probar que
validateServerEvent('init_state', payload)rechaza payloads mal formados (ej. faltaconversationsoactiveCases). ProbarvalidateClientEvent('internal_note', { conversationId: '', content: '' })debe fallar porquecontentrequieremin(1). - Cobertura de 53 registros: Verificar que
caseTypeDefinitions.lengthes 53 y que ningún registro tienevalidationSchemaopayloadBuildercomo undefined. - Mapas auxiliares:
caseTypeByToolNamedebe contener todas las toolNames (las duplicadas prevalece la última).caseTypesByApplicativedebe tener entradas para "AC+", "ASCARD", "DiMe", "Formatos SGCS", "Mi asistencia 360", "Paradigma", "RR", "Phone Protect". - FormFields vs ValidationSchema: Para cada
MULTI_FIELD_FORM, verificar que los campos enformFieldscoinciden uno a uno con las claves delvalidationSchema. Ejemplo:Plan_De_Pagos_EFdebe tener 4 campos (numero_cuotas, valor_cuota, dia_corte, dia_limite_pago) tanto en formFields como en validationSchema. - PayloadBuilder fidelidad: Para
Validar_OTT_1, verificar quepayloadBuilder({ reinstalacion: true, valor_reinstalacion: 50000, fecha_adquisicion_reinstalacion: '01-01-2024', deco_adicional: false, valor_deco: 0, fecha_adquisicion_deco: '01-01-2024' })devuelve un objeto con exactamente esas 6 claves y mismos valores.
- Tipos estrictos: Verificar que
3.1 Paso 2 — Capa de Servicios (api.ts, wsClient.ts) y Store Zustand (useAppStore.ts)
src/services/api.ts: [Creado] → Cliente REST confetchnativo. ImplementagetCases,getCaseById,resolveCase,getActiveConversations,getConversation. DefinePaginatedResponse<T>,CaseFilters, yApiErrorpara manejo de errores HTTP. La URL base se configura viaVITE_API_BASE_URLcon fallback ahttp://localhost:3000/api/v1. Incluye helperbuildQuery()para construir query string con filtros (status, applicative, search, offset, limit) y helper internorequest<T>()para centralizar la lógica de fetch, headers JSON, y validación de código HTTP. Tipos importados de@/types.src/services/wsClient.ts: [Creado] → Cliente WebSocket en claseWsClientcon patrón singleton exportado comowsClient. Implementa reconexión con backoff exponencial (1s → 2s → 4s → 8s → 16s → máx 30s, factor 2x). Exponeconnect(),disconnect(),send(type, payload)que genera automáticamenteeventId(crypto.randomUUID) yoccurredAt(ISO-8601) en el envelope estándar,onMessagecallback setter/getter, ygetStatus()retornando'connected' | 'disconnected' | 'reconnecting'. Maneja cierre graceful con flagdestroyFlagpara evitar reconexión en desconexión intencional. Ignora mensajes malformados silenciosamente. Tipos importados de@/types/wsProtocol.src/store/useAppStore.ts: [Creado] → Store centralizado Zustand con tres slices:- casesSlice:
cases[],selectedCaseId,totalCases,fetchCases(filters)(llama aapi.getCasesy actualiza estado),upsertCase(c)(reemplaza si existe o agrega al inicio),resolveCase(id, data)(llama aapi.resolveCasey actualiza el caso en el array local). - conversationsSlice:
conversations[],selectedConversationId,fetchConversations()(llama aapi.getActiveConversations),upsertConversation(c),addMessage(convId, msg),appendToken(convId, msgId, token, index)(bufferiza chunks enmetadata._chunksordenados porindexpara manejar entrega fuera de orden, actualizacontentconcatenando chunks ordenados),completeStream(convId, msgId, fullContent)(limpia_chunksde metadata, establececontent = fullContent, marcaisStreaming = false). - uiSlice:
sidebarTab('all'|'pending'|'resolved'),searchQuery,applicativeFilter,isDarkMode(persistido enlocalStoragevia claveclaro-cases:darkMode),wsStatus. Setters:setSidebarTab,setSearchQuery,setApplicativeFilter,toggleDarkMode(persiste y actualiza),setWsStatus. - La persistencia de
isDarkModese implementa con helperreadDarkMode()que leelocalStorageal inicializar el store ypersistDarkMode()que escribe en cada toggle.
- casesSlice:
3.2 Paso 3 — Componentes Compartidos (StatusBadge, SearchBar, TabsBar, EmptyState, Modal, Timer)
src/components/shared/StatusBadge.tsx: [Creado] → Renderiza un badge de estado con colores porCaseStatus. Usa mapasSTATUS_LABELS(Pendiente/En Progreso/Finalizado/Fallido) ySTATUS_STYLEScon clases Tailwind según los tokens del tema (accent-yellow, accent-orange, accent-green, accent-red). Estilo:text-[9px] px-1.5 py-0.5 rounded-[10px] font-semibold uppercase border. Props:status: CaseStatus.src/components/shared/SearchBar.tsx: [Creado] → Input de búsqueda con íconoSearchdelucide-react. Implementa debounce de 300ms usandouseRefpara el timer yuseEffectpara sincronizar con el store. Almacena el valor local enuseStatey solo escribe al store tras el debounce. Estilo: fondobg-elevated, bordeborder, focofocus:border-accent-orange. Props: ninguna (lee/escribe del store directamente).src/components/shared/TabsBar.tsx: [Creado] → Barra de tres pestañas (Todos/Pendientes/Finalizados) que leesidebarTabdel store y llama asetSidebarTab. Pestaña activa:bg-accent-orange/8 text-accent-orange border-accent-orange. Inactiva:text-text-muted border-transparent. Estilo:text-[11px] font-semibold uppercase tracking-wider. Props: ninguna.src/components/shared/EmptyState.tsx: [Creado] → Estado vacío centrado vertical/horizontalmente. Renderizaicon(ReactNode, ej. emoji),title(14px font-semibold),description(12px text-secondary). Ícono context-[3rem] opacity-40 leading-none. Props:icon: ReactNode,title: string,description: string.src/components/shared/Modal.tsx: [Creado] → Overlay modal con backdrop blur (bg-black/40 backdrop-blur-sm), contenido centrado con animaciónfadeIn. Cierra con Escape (event listener) y al hacer click en backdrop. Contenido:bg-surface border border-border rounded-lg shadow-lg. Header con título y botón ✕. Body parachildren. Footer opcionalactions. Props:isOpen,onClose,title,children,actions?.src/components/shared/Timer.tsx: [Creado] → Cronómetro individual por caso con persistencia enlocalStorage(clavetimer_case_{caseId}). Implementado conforwardRefyuseImperativeHandleexponiendostart(),stop(),getElapsed(). UsauseRefpara el intervalo (setInterval1s) y contadores acumulados.useStatesolo para el display (MM:SS). Al montar, restaura estado desdelocalStorage. Al desmontar, limpia el intervalo. Display:font-mono text-xl font-bold tabular-nums text-text-primary. Props:caseId: string | number.
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se respetó el principio de agnosticismo separando la capa de servicios (REST y WebSocket) del store y de los componentes.
api.tses un cliente REST puro sin dependencias de React ni del store, permitiendo ser usado desde hooks o desde MSW.wsClient.tses una clase singleton agnóstica al framework que expone callbacks, permitiendo queuseWebSocket(hook futuro) se suscriba sin acoplamiento. El store Zustand usaapipara las operaciones de escritura (fetchCases, resolveCase, fetchConversations), manteniendo la lógica de negocio desacoplada del mecanismo de transporte. Los componentes compartidos son puramente presentacionales (StatusBadge, EmptyState, Modal) o se conectan al store de forma mínima (SearchBar, TabsBar), sin depender de servicios directamente. -
Mitigación de Riesgos (Fase 2):
- Regla 1 (REST como canal autoritativo):
resolveCaseen el store llama exclusivamente aapi.resolveCase()(POST REST). No existe ninguna función de resolución por WebSocket. - Regla 2 (Sin advisorId): El cliente WebSocket
send()no incluyeadvisorIden ningún payload. El método genérico solo recibetypeypayload. Los helpers de validación Zod delwsProtocol.tsya garantizan queinternal_notesolo tengaconversationIdycontent. - Regla 3 (Tailwind v4 CSS-first): Todos los componentes usan clases Tailwind directamente con los tokens CSS definidos en
@theme(bg-surface, text-primary, border-accent-orange, etc.). No hay configuración JS de Tailwind. - Regla 4 (Streaming buffer 50ms):
appendTokenen el store usametadata._chunksordenados porindexpara garantizar orden correcto de tokens incluso si llegan fuera de orden de red. El buffer se implementa a nivel de store, preparado para que el hookuseWebSocket(futuro) pueda rate-limit las actualizaciones a 20fps. - Regla 5 (45 tipos de caso con Zod): No aplica en este paso (implementado en Paso 1).
- Regla 1 (REST como canal autoritativo):
3.3 Notas Técnicas para el Tester
- Dependencias Añadidas:
zustand(ya instalada en Paso 0),lucide-react(ya instalada en Paso 0). No se añadieron nuevas dependencias. - Puntos Críticos a Probar:
- api.ts — Error handling: Verificar que
ApiErrorse lanza correctamente para códigos HTTP 4xx/5xx. Probar con MSW simulando errores 404 y 500. Verificar quebuildQueryomite parámetros undefined/null. - api.ts — Paginación: Llamar
getCases({ offset: 0, limit: 5 })y verificar query string?offset=0&limit=5. Llamar congetCases({})y verificar que no se añade?en la URL. - wsClient.ts — Reconexión: Verificar backoff exponencial: tras cerrar WebSocket, debe reconectar con delays crecientes (1s, 2s, 4s, 8s...). Probar que
disconnect()detiene la reconexión inmediatamente. - wsClient.ts — Envelope: Verificar que
send('internal_note', { conversationId: 'c1', content: 'nota' })produce un mensaje JSON contype,eventId(UUID),occurredAt(ISO string) ypayload. - useAppStore.ts — appendToken: Enviar tokens fuera de orden (index 2, 0, 1) y verificar que el contenido final es la concatenación ordenada. Verificar que
completeStreamreemplaza el contenido confullContenty limpiametadata._chunks. - useAppStore.ts — Dark mode persistence: Llamar
toggleDarkMode(), recargar el store, verificar queisDarkModepersiste. Verificar quelocalStoragecontieneclaro-cases:darkMode=true. - StatusBadge.tsx — Renderizado condicional: Renderizar con cada
CaseStatusy verificar clases de color correctas y texto en español. - SearchBar.tsx — Debounce: Escribir texto rápidamente y verificar que solo se actualiza el store tras 300ms de inactividad. Verificar que el ícono
Searchestá presente. - TabsBar.tsx — Estado activo: Hacer clic en "Pendientes" y verificar que
sidebarTaben el store cambia a'pending'y la pestaña visualmente activa tiene las clasesbg-accent-orange/8 text-accent-orange border-accent-orange. - Timer.tsx — Persistencia y control: Llamar
start()y esperar 5s. Verificar quelocalStoragetiene el timer guardado. Llamarstop()y verificar display se congela. LlamargetElapsed()y verificar que devuelve los segundos exactos. Recargar el componente y verificar que el tiempo acumulado se restaura. Iniciar de nuevo y confirmar que continúa desde donde quedó. - Modal.tsx — Accesibilidad: Verificar que el modal se cierra con tecla Escape. Verificar que el click en backdrop cierra el modal. Verificar que el click dentro del contenido no lo cierra.
- EmptyState.tsx — Renderizado: Verificar que
iconrenderiza como elemento (puede ser string emoji o componente React),titleen 14px semibold,descriptionen 12px secondary, centrado vertical/horizontalmente.
- api.ts — Error handling: Verificar que
3.1 Paso 4 — Módulo de Gestión de Casos HITL (/cases)
src/components/cases/TypeBadge.tsx: [Creado] → Badge pequeño que muestra eltipoSolicitudcon estilobg-accent-orange/10 text-accent-orange border-accent-orange/25. Trunca el texto a 140px contitlepara tooltip.src/components/cases/CaseCard.tsx: [Creado] → Tarjeta de caso en la sidebar. Propscase: CaseRequest,isActive,onClick. Renderiza: (1) Header con título,StatusBadgey timer formateado (solo sistatus === IN_PROGRESSyhandlingTime > 0); (2) Descripción truncada a 2 líneas conline-clamp-2; (3) Footer con ID externo en monospace,TypeBadgecontipoSolicitud, y fecha formateada condate-fns. Estilo basebg-elevated border rounded-md p-3 cursor-pointer transition hover:bg-hover, activobg-accent-orange/4 border-accent-orange. Animaciónanimate-[slideIn_0.2s_ease-out].src/components/cases/ApplicativeFilter.tsx: [Creado] → Filtro de aplicativos mediante chips/badges clickeables. Lista fija de los 8 aplicativos (AC+, ASCARD, DiMe, Formatos SGCS, Mi asistencia 360, Paradigma, RR, Phone Protect). UsaapplicativeFilterysetApplicativeFilterdel store. Al hacer clic en un chip activo, lo deselecciona (pasa anull). Incluye botón "✕ Limpiar" que solo aparece cuando hay un filtro activo. Estilo: chip activobg-accent-orange/10 text-accent-orange border-accent-orange/30, inactivobg-elevated text-text-muted border-border.src/components/cases/FormRenderer.tsx: [Creado] → Componente crítico que renderiza formularios dinámicos segúnCaseUIType. Props:caseType: CaseTypeDefinition,onSubmit: (data) => void. Implementa los 6 patrones de UI (SIMPLE_CONFIRMATION, CONFIRMATION_WITH_VALUE, MULTI_FIELD_FORM, DATE_SIMPLE, FREE_TEXT, READ_ONLY) con estado localformValues/formErrors, transformación de fechas yyyy-mm-dd ↔ dd-mm-aaaa, validación Zod inline, soporteconditionalOn, y FieldInput interno para renderizar cada tipo de campo (text, number, currency con $, date, select, textarea, toggle switch).src/components/cases/CaseDetail.tsx: [Creado] → Panel derecho de detalle con metadata grid (ID, cédula, tipo, aplicativo), descripción, payload entrante, FormRenderer dinámico, acordeón de pasos colapsable, y panel de operación sticky con Timer + fecha.src/pages/CasesPage.tsx: [Creado] → Layout maestro: sidebar 320px (SearchBar + TabsBar + ApplicativeFilter + lista CaseCards scrolleable + footer conteo) y panel derecho (CaseDetail / EmptyState). Conecta store para casos filtrados por tab/search/applicative.filterCases()interno con lógica de filtrado combinado.
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se respetó la separación de conceptos manteniendo los componentes de UI (
CaseCard,CaseDetail,TypeBadge,ApplicativeFilter) desacoplados de la lógica de formularios dinámicos (FormRenderer) y del store.CaseCardyTypeBadgeson puramente presentacionales.FormRendererencapsula toda la complejidad de renderizado condicional, transformación de fechas, y validación Zod inline.CaseDetailorquesta la integración entre metadata, formulario y timer.CasesPageactúa como orquestador de layout y filtros. -
Mitigación de Riesgos (Fase 2):
- Regla 1 (REST como canal autoritativo):
CaseDetail.handleFormSubmitllama aresolveCasedel store (POST REST).FormRenderersolo recolecta datos y llama aonSubmit. - Regla 2 (Sin advisorId): Ningún componente envía
advisorId. El payload contiene soloactionypayload. - Regla 3 (Tailwind v4 CSS-first): Todos los componentes usan exclusivamente tokens
@themesin configuración JS de Tailwind. - Regla 5 (45 tipos de caso con Zod):
FormRendererusavalidationSchema.safeParse()antes de llamar aonSubmit.
- Regla 1 (REST como canal autoritativo):
3.3 Notas Técnicas para el Tester
- Dependencias Añadidas:
date-fns(ya instalada en Paso 0).lucide-react(ya instalada en Paso 0). No se añadieron nuevas dependencias. - Puntos Críticos a Probar:
- CaseCard — Renderizado condicional de timer: Solo aparece cuando
status === IN_PROGRESSyhandlingTime > 0. Formato MM:SS. - CaseCard — Animación slideIn al montar.
- ApplicativeFilter — Toggle: Chip activo ↔
applicativeFilteren store. Botón ✕ solo visible con filtro activo. - FormRenderer — SIMPLE_CONFIRMATION: Botones Sí/No llaman
onSubmit({ confirmacion: true/false }). - FormRenderer — CONFIRMATION_WITH_VALUE: Radio Sí→ campo $ visible, Radio No→ oculto. Validación valor negativo.
- FormRenderer — MULTI_FIELD_FORM: Renderiza types correctos, min/max, toggle switch, conditionalOn, errores inline.
- FormRenderer — Transformación fecha: Date picker → valor enviado en dd-mm-aaaa.
- FormRenderer — READ_ONLY: Botón "Marcar como revisado" llama
onSubmit({}). - CaseDetail — Timer: Inicia automático en IN_PROGRESS, se detiene al resolver.
- CaseDetail — Acordeón: Pasos colapsables con ChevronDown/ChevronUp.
- CasesPage — Filtros combinados: Búsqueda + tab + aplicativo se combinan correctamente. Footer "X de Y casos".
- CasesPage — Empty states: Sin casos → EmptyState en sidebar. Sin selección → EmptyState en panel derecho.
- CasesPage — Fetch on mount: Se llama
fetchCases()al montar. - FormRenderer — Validación Zod: Datos inválidos → errores inline, no se llama
onSubmit.
- CaseCard — Renderizado condicional de timer: Solo aparece cuando
3.1 Paso 5 — Módulo de Monitoreo (/monitor)
src/components/monitor/MessageBubble.tsx: [Creado] → Burbuja de mensaje individual con diferenciación visual por rol (user → derecha/accent-orange, agent → izquierda/elevated, system → centrado/base/italic, internal → izquierda/accent-yellow con badge 🔒). Muestra timestamp HH:mm. SiisStreaming, muestra cursor parpadeante (barra animada).src/components/monitor/InternalNotesGroup.tsx: [Creado] → Acordeón expandible que agrupa mensajesinternalconsecutivos. Cabecera "🔄 Notas internas (N)" colapsable. Al expandir, muestra contenido y timestamp de cada nota. Implementa filtro de seguridad para solo renderizar mensajes conrole === INTERNAL.src/components/monitor/ChatFeed.tsx: [Creado] → Feed de mensajes con auto-scroll inteligente. Detecta si el usuario está cerca del fondo (≤ 100px) mediante ref y handleronScroll; si está cerca, hace scroll automático al llegar nuevo mensaje o token. Agrupa mensajesinternalconsecutivos enInternalNotesGroupmediante buffer de acumulación intercalado conflushInternal(). Muestra indicador "Escribiendo..." con spinner cuando el último mensaje del agente tieneisStreaming: true.src/components/monitor/ConversationCard.tsx: [Creado] → Tarjeta de conversación en lista lateral. Muestra: (1) ID/nombre del cliente con icono User, (2) último mensaje truncado a 80 caracteres, (3) spinnerLoader2animado si el último mensaje está en streaming, (4) estado del agente con color verde para activa, (5) badge de estado de conversación (Activa/En pausa/Finalizada). Sin badge HITL en esta iteración (requiere mapeo conversationId → caseId que se integrará con eventos WS).src/components/monitor/InternalNoteBanner.tsx: [Creado] → Banner inferior para inyección de notas internas. Textarea de 2 líneas con placeholder, botón "Enviar" con icono Send. Al enviar, llama awsClient.send('internal_note', { conversationId, content })sinadvisorId. Soporte Enter para enviar, Shift+Enter para nueva línea. Feedback visual "Enviado ✓" por 2 segundos tras envío exitoso. Hint con atajos de teclado.src/pages/MonitorPage.tsx: [Creado] → Layout de dos columnas: izquierda 280px con lista scrolleable deConversationCards (con encabezado y contador), derecha flex-1 conChatFeed+InternalNoteBannersi hay conversación seleccionada, oEmptyStatesi no. Al montar, llama afetchConversations()del store. Conecta conselectedConversationIdy setea medianteuseAppStore.setState.
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se respetó la separación de conceptos manteniendo los componentes de monitoreo desacoplados del store y servicios.
MessageBubblees puramente presentacional (solo recibeMessagepor props).InternalNotesGroupencapsula la lógica de agrupación y colapso.ChatFeedorquesta la integración entre burbujas, agrupación de notas internas y auto-scroll.ConversationCardes presentacional con helpers de extracción de último mensaje y detección de streaming.InternalNoteBannerse conecta directamente conwsClient(singleton) para enviar notas internas, sin pasar por el store.MonitorPageactúa como orquestador de layout y conexión con el store. -
Mitigación de Riesgos (Fase 2):
- Regla 1 (REST como canal autoritativo):
InternalNoteBannerenvía por WebSocket exclusivamente notas internas (eventointernal_note), nunca resolución de casos. - Regla 2 (Sin advisorId):
wsClient.send('internal_note', { conversationId, content })no incluyeadvisorIden el payload. El backend deriva la identidad del contexto de conexión WS. - Regla 3 (Tailwind v4 CSS-first): Todos los componentes usan exclusivamente tokens
@themesin configuración JS de Tailwind. - Regla 4 (Streaming buffer 50ms):
ChatFeedreacciona a cambios enmessages[messages.length-1]?.contentpara auto-scroll durante streaming, respetando posición manual del usuario mediante refisNearBottomRef.
- Regla 1 (REST como canal autoritativo):
3.3 Notas Técnicas para el Tester
- Dependencias Añadidas: Ninguna nueva (todas las dependencias ya estaban instaladas en Pasos previos).
- Puntos Críticos a Probar:
- MessageBubble — 4 roles visuales: Verificar alineación y fondo correctos para user (derecha/accent-orange/10), agent (izquierda/elevated), system (centrado/base/italic), internal (izquierda/accent-yellow/10 con badge 🔒).
- MessageBubble — Streaming cursor: Cuando
isStreaming: true, debe mostrar barra parpadeante al final del contenido. - ChatFeed — Auto-scroll: Con varias burbujas visibles, scrollear manualmente hacia arriba y verificar que al llegar un nuevo mensaje NO se hace auto-scroll. Scrollear al fondo y verificar que al llegar un nuevo mensaje SÍ se hace auto-scroll al fondo.
- ChatFeed — Agrupación de notas internas: 2+ mensajes
internalconsecutivos deben agruparse en un acordeón. Un mensaje internal seguido de user/agent debe renderizarse individualmente. - ChatFeed — Indicador "Escribiendo...": Cuando el último mensaje del agente tiene
isStreaming: true, debe mostrar texto "Escribiendo..." con spinner. - InternalNotesGroup — Expandir/colapsar: Hacer clic en cabecera y verificar que se expanden/colapsan las notas. Verificar contador "Notas internas (N)".
- ConversationCard — Último mensaje truncado: Mensaje > 80 caracteres debe truncarse con "...".
- ConversationCard — Spinner streaming: Debe mostrar
Loader2animado cuando el último mensaje tieneisStreaming: true. - InternalNoteBanner — Envío sin advisorId: Verificar que
wsClient.sendrecibe payload sin campoadvisorId. Verificar feedback "Enviado ✓" post-envío. - InternalNoteBanner — Enter vs Shift+Enter: Enter envía, Shift+Enter inserta nueva línea.
- MonitorPage — Layout: 280px sidebar izquierda + flex-1 derecha. EmptyState cuando no hay conversación seleccionada.
- MonitorPage — Fetch on mount: Se llama
fetchConversations()al montar. AlmacenarselectedConversationIdconuseAppStore.setState.
3.1 Paso 6 — App Shell y Ruteo
src/services/wsClient.ts: [Modificado] → Se añadió callbackonStatusChange(getter/setter) y tipoStatusChangeCallbackpara notificar cambios de estado de conexión al store. El método privadosetStatus()ahora invocaonStatusChangeCallback?.(status)en cada transición, permitiendo queAppShellsincronice el indicador WS en el Header.src/components/layout/Header.tsx: [Creado] → Barra superior de 50px. Logo: emoji 🔴 + "Claro Cases" con gradientebg-gradient-to-r from-accent-orange to-accent-yellow bg-clip-text text-transparent. Badge "En vivo" con estilobg-accent-green/10 text-accent-green. Indicador de conexión WS: punto circular coloreado (verde/amarillo/rojo segúnwsStatusdel store) + texto (Conectado/Reconectando.../Desconectado), conanimate-pulseen estado reconnecting. Toggle tema oscuro/claro con iconos Sun/Moon delucide-react.src/components/layout/Sidebar.tsx: [Creado] → Navegación lateral fija de 50px de ancho. UsaNavLinkde react-router-dom con dos rutas: Casos (iconoLayoutList) →/cases, Monitor (iconoMonitor) →/monitor. Link activo:bg-accent-orange/8 text-accent-orange. Link inactivo:text-text-muted hover:text-text-primary hover:bg-hover. Layout vertical centrado con icono + label en 10px.src/components/layout/AppShell.tsx: [Creado] → Layout global que envuelve todo el contenido. RenderizaHeaderarriba,Sidebara la izquierda (50px), ychildren(contenido de la ruta) a la derecha. Al montar: (1) sincroniza clase.darken<html>segúnisDarkModedel store, (2) inicializa conexión WebSocket viawsClient.connect()y registraonStatusChange→setWsStatus, (3) registraonMessagehandler (placeholder para integración futura de eventos WS), (4) llamafetchCases()ofetchConversations()según la ruta actual. Cleanup: desconecta WS y limpia callbacks al desmontar.src/App.tsx: [Reemplazado] → Router conBrowserRouterenvolviendoAppShellcomo layout global. Tres rutas:/→ redirect a/cases,/cases→CasesPage,/monitor→MonitorPage. Catch-all*→ redirect a/cases.
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se implementó el shell de aplicación siguiendo el principio de composición:
AppShelles el layout contenedor que orquesta la inicialización de infraestructura (WS, tema oscuro, fetch inicial) y renderizaHeader+Sidebar+ contenido. El ruteo está desacoplado enApp.tsxusando react-router-dom estándar.HeaderySidebarson componentes puramente presentacionales que se conectan al store para estado de UI (wsStatus, isDarkMode). La modificación awsClient.tses mínima y no rompe la interfaz existente. -
Mitigación de Riesgos (Fase 2):
- Regla 2 (Sin advisorId):
AppShellno envía ningún identificador de asesor; solo establece la conexión WS y el handler de mensajes. - Regla 3 (Tailwind v4 CSS-first): Todos los componentes de layout usan exclusivamente tokens
@themesin configuración JS de Tailwind. El toggle dark mode usaclassstrategy con@custom-variant dark. - Regla 4 (Streaming buffer 50ms):
AppShellregistra unonMessagehandler placeholder que será expandido en fases posteriores para implementar el buffer de 50ms. - Regla 5 (45 tipos de caso con Zod): No aplica en este paso.
- Regla 2 (Sin advisorId):
3.3 Notas Técnicas para el Tester
- Dependencias Añadidas: Ninguna nueva.
- Puntos Críticos a Probar:
- Header — Gradiente logo: Verificar que el texto "Claro Cases" tiene gradiente
accent-orange → accent-yellowconbg-clip-text text-transparent. - Header — Indicador WS: Verificar punto verde + "Conectado" cuando
wsStatus = 'connected', amarillo + "Reconectando..." cuando'reconnecting', rojo + "Desconectado" cuando'disconnected'. Estado reconnecting debe teneranimate-pulse. - Header — Toggle tema: Hacer clic en icono sol/luna y verificar que
isDarkModecambia en el store y se agrega/remueve clase.darken<html>. - Sidebar — Navegación: Verificar que NavLink activo tiene clase
bg-accent-orange/8 text-accent-orange. Navegar entre /cases y /monitor y verificar cambio visual. - AppShell — Inicialización WS: Al montar, verificar que
wsClient.connect()se llama y quewsClient.onStatusChangeactualizawsStatusen el store. - AppShell — Dark mode sync: Con
isDarkMode = true, verificar que<html>tiene clase.dark. Confalse, que no la tiene. - AppShell — Fetch inicial: Al navegar a /cases, verificar que se llama
fetchCases(). Al navegar a /monitor, verificar que se llamafetchConversations(). NOTA: El fetch inicial solo ocurre al montarAppShell; cambios de ruta posteriores son manejados por los pages. - App.tsx — Ruteo: Verificar que
/redirige a/cases. Verificar que/casesrenderizaCasesPage. Verificar que/monitorrenderizaMonitorPage. Verificar que ruta desconocida redirige a/cases. - App.tsx — AppShell wrapping: Verificar que todas las rutas están envueltas en
AppShelly que Header + Sidebar son visibles en todas las vistas.
- Header — Gradiente logo: Verificar que el texto "Claro Cases" tiene gradiente
3.1 Paso 9 — Funcionalidades Preservadas: Notificaciones, Sonido y Parpadeo de Título
src/hooks/useNotification.ts: [Creado] → Hook para notificaciones de escritorio HTML5. Solicita permisoNotification.requestPermission()al montar si no está en estadogranted. Exponenotify(title, body, onClick?)que crea unanew Notification()con icono/favicon.icoy auto‑cierre a los 6 segundos. Al hacer clic en la notificación se ejecuta el callbackonClick, se enfoca la ventana (window.focus()) y se cierra la notificación. Si el navegador no soporta Notifications o el permiso fue denegado, se loguea un warning y la llamada es silenciosamente ignorada.src/hooks/useSound.ts: [Creado] → Hook para alerta sonora con Web Audio API. Inicializa unAudioContextde forma perezosa en el primer gesto del usuario (eventosclickokeydowncon{ once: true }), cumpliendo con las políticas de autoplay del navegador. ExponeplayNotificationSound()que genera un chime de dos tonos: C5 (523.25 Hz) por 120 ms → E5 (659.25 Hz) por 120 ms, compartiendo un nodoGainNodecon volumen 0.08 y fade out exponencial a 0.001 en 450 ms. Si elAudioContextestá en estadosuspended, se loguea un warning y se retorna sin reproducir.src/hooks/useTitleFlash.ts: [Creado] → Hook para parpadeo del título de pestaña. Mantiene un contadoruseRefde notificaciones no leídas. ExponetriggerNotification()que incrementa el contador y, si la pestaña no está enfocada (document.visibilityState === 'hidden'odocument.hasFocus()esfalse), inicia un intervalo que alterna el título cada 1 segundo entre"(🔔 N) ¡Nuevo Caso!"y"Claro Cases Dashboard". Al enfocar la pestaña (visibilitychange → visible, eventowindow.focus), se limpia el intervalo, se restaura el título y se resetea el contador a cero. Los listenersvisibilitychange,focusyblurse limpian al desmontar el componente.src/hooks/index.ts: [Creado] → Barrel export que re‑exportauseNotification,useSoundyuseTitleFlashpara imports limpios desde otros módulos.src/components/layout/AppShell.tsx: [Modificado] → Se integraron los tres hooks de funcionalidades preservadas. El manejadoronMessagedel WebSocket fue expandido para despachar eventos a la store segúnenvelope.type:init_state: Reemplaza el estado local conpayload.conversationsypayload.activeCases.conversation_started: Inserta la conversación en el store.user_message: Agrega el mensaje a la conversación correspondiente.agent_stream_chunk: Envía el token aappendTokenpara concatenación ordenada.agent_stream_completed: Envía el contenido completo acompleteStream.hitl_request: Dispara las tres funcionalidades preservadas — (1) notificación de escritorio connotify()cuyoonClicknavega a/casesy selecciona el caso, (2) alerta sonora conplayNotificationSound(), (3) parpadeo de título contriggerNotification(). Además inserta el caso en el store víaupsertCase().- Se usa un patrón
useRef(handleIncomingMessageRef) para que el callback del WebSocket siempre delegue a la versión más reciente del handler sin necesidad de re‑montar el efecto.
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se implementaron los hooks siguiendo el principio de programación defensiva y agnosticismo al framework:
useNotificationusauseRefpara cachear el permiso yuseCallbackpara memoizar la funciónnotify, evitando re‑creaciones innecesarias. La solicitud de permiso ocurre una sola vez al montar.useSoundinicializa elAudioContextde forma lazy mediante un par de listeners globales (click,keydown) con{ once: true }, garantizando que no se intente crear audio antes de un gesto del usuario. Al desmontar, cierra el contexto y limpia los listeners.useTitleFlashusauseRefpara el contador no leído y el intervalo, evitando re‑renders al actualizar el título del documento. La lógica de start/stop está desacoplada enstartFlashing/stopFlashingpara ser reutilizada desdetriggerNotificationy los listeners devisibilitychange/focus/blur.AppShellintegra los hooks de forma compositiva y usa un patrón de ref (handleIncomingMessageRef) para mantener la estabilidad del callback WS a través de renders. El switch‑case basado enenvelope.typepermite escalar con nuevos tipos de eventos sin modificar la estructura del handler.
-
Mitigación de Riesgos (Fase 2):
- Regla 1 (REST como canal autoritativo): En
AppShell, el handler dehitl_requestsolo inserta el caso en el store local (upsertCase) y dispara notificaciones; no envía ninguna resolución por WebSocket. La resolución sigue siendo exclusiva de REST (POST /cases/:id/resolve). - Regla 2 (Sin advisorId): El handler de
hitl_requestno envía ningún payload que contengaadvisorId. Solo procesa datos entrantes y dispara efectos locales. - Regla 3 (Tailwind v4 CSS-first):
AppShellno introduce nuevas clases que dependan de configuración JS de Tailwind. - Regla 4 (Streaming buffer 50ms): Los eventos
agent_stream_chunkse despachan directamente aappendTokendel store, que ya implementa el buffer ordenado porindexpara garantizar orden correcto de tokens incluso con entrega fuera de orden.
- Regla 1 (REST como canal autoritativo): En
3.3 Notas Técnicas para el Tester
-
Dependencias Añadidas: Ninguna (todas las APIs usadas son nativas del navegador:
Notification,AudioContext,document.title,document.visibilityState,window.focus). -
Puntos Críticos a Probar:
- useNotification — Permiso denegado: Bloquear notificaciones en el navegador y verificar que
notify()loguea warning sin lanzar error. Verificar que la solicitud de permiso solo ocurre siNotification.permission !== 'granted'y!== 'denied'. - useNotification — Click handler: Al hacer clic en una notificación, debe ejecutar el callback
onClick, enfocar la ventana y cerrar la notificación. Verificar quewindow.focus()se llama y quenotification.close()se ejecuta. - useSound — AudioContext lazy: Sin gesto de usuario,
playNotificationSound()debe loguear warning. Tras un click o keydown, debe crear elAudioContexty reproducir el chime. Verificar que elAudioContextse cierra al desmontar el hook. - useSound — AudioContext suspended: Simular estado
suspended(navegador con política de autoplay estricta) y verificar queplayNotificationSound()loguea warning sin lanzar error. - useSound — Dos tonos: Verificar que se reproducen dos frecuencias distintas (C5=523.25Hz, E5=659.25Hz) con el fade out exponencial. La amplitud debe decaer de 0.08 a 0.001 en 450ms.
- useTitleFlash — Trigger con pestaña oculta: Abrir otra pestaña, llamar
triggerNotification(), verificar que el título parpadea entre"(🔔 1) ¡Nuevo Caso!"y"Claro Cases Dashboard"cada 1s. LlamartriggerNotification()nuevamente sin enfocar: el contador debe incrementar a 2 y el título alternar con"(🔔 2) ¡Nuevo Caso!". - useTitleFlash — Restauración al enfocar: Con el título parpadeando, enfocar la pestaña (click o atajo de teclado). Verificar que el título se restaura a
"Claro Cases Dashboard"inmediatamente y el intervalo se limpia. - useTitleFlash — Múltiples triggers: Llamar
triggerNotification()5 veces con la pestaña visible → el contador se incrementa pero no parpadea (solo parpadea si la pestaña está oculta). Al ocultar la pestaña, el parpadeo debe comenzar mostrando"(🔔 5) ¡Nuevo Caso!". - AppShell — hitl_request handler: Simular un evento
hitl_requestentrante por WebSocket y verificar que se ejecutan las tres acciones: (1) aparece notificación de escritorio, (2) suena el chime, (3) el título parpadea si la pestaña no está enfocada. Verificar que el caso se inserta en el store. - AppShell — Click en notificación: Al hacer clic en la notificación generada por
hitl_request, debe navegar a/casesy seleccionar el caso (selectedCaseIddebe coincidir con eliddel case del payload). - AppShell — init_state handler: Simular
init_statecon múltiples conversaciones y casos. Verificar que el store se actualiza correctamente sin duplicados. - AppShell — agent_stream_chunk handler: Simular chunks desordenados y verificar que
appendTokenlos ordena por índice. - AppShell — Ref pattern: Verificar que el
onMessagecallback siempre usa la última versión dehandleIncomingMessageincluso si el componente se re‑renderiza (ej. cambio deisDarkMode). El handler debe seguir funcionando sin necesidad de re‑conectar el WS. - npm run build: Verificar que
npm run buildcompila sin errores de tipo.
- useNotification — Permiso denegado: Bloquear notificaciones en el navegador y verificar que
3.1 Paso 10 — Fix CRÍTICO: Buffer de Streaming 50ms (Regla 4)
-
src/store/useAppStore.ts: [Modificado] → Implementación completa del buffer de rate-limiting de 50ms para streaming token-a-token según Regla 4 de la Fase 2. Se añadieron:- Sistema de buffer externo (
conversationBuffers: Map<string, ConversationBufferEntry>) fuera del estado de Zustand, evitando re-renders al acumular chunks entrantes. flushBuffer(): Procesa los tokens pendientes de una conversación con UNA sola llamada aset(), ordenando porindexpara garantizar orden correcto incluso con entrega fuera de orden. Solo actualiza el store si la conversación es la seleccionada (optimización de re-render).scheduleBufferFlush(): Programa unsetTimeoutde 50ms por conversación, con guarda para no duplicar timers.forceFlushBuffer(): Vaciado inmediato del buffer, usado al cambiar de conversación seleccionada.appendToken(): Ahora acumula en el buffer externo y solo programa flush si la conversación es la activa. No llama aset()directamente.completeStream(): Limpia el buffer de la conversación (cancela timer pendiente y elimina entrada del Map) antes de actualizar el store.setSelectedConversationId(): Nueva acción que fuerza el flush del buffer al seleccionar una conversación con tokens acumulados.removeConversation(): Nueva acción que limpia el buffer y elimina la conversación del store, incluyendo el cleanup delselectedConversationIdsi corresponde.- Auto-limpieza en
flushBuffer(): si la conversación ya no existe en el store, se elimina la entrada del buffer.
- Sistema de buffer externo (
3.2 Estrategia de Solución e Integración
-
Implementación Arquitectónica: Se implementó el patrón de buffer externo (fuera del estado de Zustand) para evitar re-renders durante la acumulación de tokens. El buffer usa un
Map<string, ConversationBufferEntry>donde cada entrada contiene un arraypendingde chunks y untimer(setTimeout de 50ms). Solo la conversación seleccionada programa timers de flush; las conversaciones no seleccionadas acumulan tokens silenciosamente sin disparar re-renders. CuandocompleteStreamllega, se limpia el buffer y se actualiza el store con elfullContentautoritativo en una sola llamada aset(). Al cambiar de conversación,setSelectedConversationIdfuerza un flush inmediato de los tokens acumulados de la nueva conversación. -
Mitigación de Riesgos (Fase 2):
- Regla 4 (Streaming buffer 50ms): Implementado completamente. Cada chunk se acumula en un buffer externo, y cada 50ms se hace una sola llamada a
set()con todos los chunks acumulados ordenados. Solo la conversación seleccionada actualiza el store, limitando re-renders a máximo 20 fps. - Regla 4 — Limpieza de buffer:
completeStreamelimina el buffer de la conversación (cancela timer + borra entrada del Map).removeConversationtambién limpia el buffer. El flush auto-limpia buffers huérfanos si la conversación ya no existe. - Regla 4 — Non-selected conversations: Las conversaciones no seleccionadas acumulan tokens sin timer, sin llamar a
set(), y sin causar re-renders. Al ser seleccionadas,setSelectedConversationIdfuerza un flush inmediato.
- Regla 4 (Streaming buffer 50ms): Implementado completamente. Cada chunk se acumula en un buffer externo, y cada 50ms se hace una sola llamada a
3.3 Notas Técnicas para el Tester
- Dependencias Añadidas: Ninguna. Todo implementado con APIs nativas de JavaScript (
Map,setTimeout,clearTimeout). - Puntos Críticos a Probar:
- Buffer de 50ms: Enviar 100 chunks rápidamente a
appendTokenpara la misma conversación seleccionada. Verificar queset()se llama ~20 veces por segundo (cada 50ms), no 100 veces. - Orden de chunks: Enviar chunks con índices desordenados (ej: 2, 0, 1, 4, 3) y verificar que el contenido final en el store está correctamente ordenado.
- Conversación no seleccionada: Enviar chunks a una conversación NO seleccionada. Verificar que NO se llama
set()y que los chunks se acumulan en el buffer externo. - Seleccionar conversación con buffer: Acumular chunks en una conversación no seleccionada, luego llamar
setSelectedConversationId(). Verificar que todos los chunks acumulados se aplican al store en una sola llamada. - completeStream limpia buffer: Llamar
completeStream()para una conversación con chunks pendientes. Verificar queconversationBuffersya no tiene entrada para esa conversación y que el store muestrafullContent. - removeConversation limpia buffer: Llamar
removeConversation()y verificar que la entrada del buffer se elimina y la conversación desaparece del store. - Auto-limpieza flush: Eliminar manualmente una conversación del store (vía
set()directo) y verificar que el siguiente flush elimina la entrada huérfana del buffer. - No fuga de timers: Verificar que los
setTimeoutse cancelan correctamente al llamarcompleteStream()oremoveConversation(). No debe haber timers colgados después de estas operaciones. - npm run build: Debe compilar sin errores tras los cambios.
- Buffer de 50ms: Enviar 100 chunks rápidamente a
Fase 4: Reporte de Calidad (QA)
4.1 Resumen de Cobertura
- Resultado Global: PASSED
- Total de Casos Ejecutados: 7
- Casos Exitosos: 7
- Casos Fallidos: 0
4.2 Detalle de Pruebas y Casos de Estrés
- Build (npm run build): PASSED —
tsc -b && vite buildejecutado exitosamente. Vite v6.4.3 transformó 2742 módulos en 3.04s. Archivos generados endist/:index.html(0.66 kB), CSS (31.42 kB), JS browser (300.77 kB), JS app (425.81 kB). Sin errores ni warnings. - TypeScript Compiler (npx tsc --noEmit): PASSED — Zero type errors en toda la base de código. El comando retornó sin output (compilación limpia).
- Regla 1 (hitl_response): PASSED — Búsqueda con grep en
src/no encontró ninguna ocurrencia dehitl_response. La resolución de casos es exclusivamente REST. - Regla 2 (advisorId): PASSED — Búsqueda con grep en
src/no encontró ninguna ocurrencia deadvisorId. No hay identificadores de asesor en payloads cliente→servidor. - Regla 3 (tailwind.config.ts): PASSED — El archivo
tailwind.config.tsNO existe en la raíz del proyecto. Toda la configuración de Tailwind v4 está ensrc/index.cssvia@themey@custom-variant dark. - Regla 4 (buffer streaming 50ms): PASSED — Verificación de código fuente en
src/store/useAppStore.ts:- ✅ Buffer externo (
conversationBuffers: Map<string, ConversationBufferEntry>) declarado fuera del estado de Zustand (línea 95), evitando re-renders por chunk individual. - ✅
scheduleBufferFlush()programasetTimeoutde 50ms por conversación (línea 196-198) con guarda contra timers duplicados (línea 194). - ✅
flushBuffer()verificaselectedConversationIdantes de llamar aset()(línea 129). Si la conversación no es la seleccionada, retorna sin actualizar el store. - ✅ Las conversaciones no seleccionadas acumulan chunks en el buffer sin programar timer (líneas 327-331:
scheduleBufferFlushsolo se llama siselectedConversationId === convId). - ✅
completeStream()(líneas 334-381): limpia el buffer (cancela timer + elimina entrada del Map) y luego actualiza el store confullContenten una sola llamada aset(). - ✅
removeConversation()(líneas 394-411): limpia el buffer antes de eliminar la conversación del store. - ✅
setSelectedConversationId()(líneas 383-392): fuerza flush inmediato viaforceFlushBuffer()al cambiar de conversación.
- ✅ Buffer externo (
- Regla 5 (45+ casos mapeados): PASSED — 53 registros en
src/data/caseTypeDefinitions.ts, todos conuiPattern(53/53),applicative(53/53),formFields(53/53), yvalidationSchema/payloadBuilderprovistos via spread de funciones fábrica (51 usos de factory spreads:simpleConfirmation18,multiFieldForm18, másconfirmationWithValue,dateSimple,freeText,readOnly).
4.3 Evidencia y Logs de Consola
```text
# Build
> [email protected] build
> tsc -b && vite build
vite v6.4.3 building for production...
transforming...
✓ 2742 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.66 kB │ gzip: 0.37 kB
dist/assets/index-CtPkX2JE.css 31.42 kB │ gzip: 6.27 kB
dist/assets/browser-yp4JH-9T.js 300.77 kB │ gzip: 99.29 kB
dist/assets/index-QLIqdcdX.js 425.81 kB │ gzip: 119.05 kB
✓ built in 3.04s
# TypeScript Check
$ npx tsc --noEmit
(no output — zero type errors)
# Regla 1 — hitl_response grep
$ grep -r "hitl_response" src/
(no output)
# Regla 2 — advisorId grep
$ grep -r "advisorId" src/
(no output)
# Regla 3 — tailwind.config.ts existence
$ test -f tailwind.config.ts && echo FAIL || echo PASS
PASS (file not found)
# Regla 5 — case count verification
uiPattern occurrences: 53
applicative occurrences: 53
formFields occurrences: 53
Factory spread patterns: 51
```