# Sprint 3 — Etapa 4: Indicador stale + zoom BPMN ## Contexto Sos Claude Code trabajando en **InQ ROI**. Las reglas de implementación están en `simulador-web/CLAUDE.md`. El brief del sprint está en `sprints/sprint-3/BRIEF.md`. **Estado al iniciar esta etapa:** - Etapas 1-3 completadas. Biblioteca de procesos operativa. Tags y búsqueda funcionando. `SettingsPage` disponible. - `process-store.ts` expone: `updateActivity`, `updateGateway`, `updateResource`, `addResource`, `deleteResource`, `updateProcess`. Ninguna de estas acciones actualiza `process.updatedAt` excepto `updateProcess`. - `simulation-store.ts` tiene `lastSimulatedAt: Date | null` (in-memory, se pierde al recargar) y `persistResults` (guarda en IndexedDB con `executedAt: Date.now()`). - `BpmnCanvas.tsx`: viewer bpmn-js puro, sin toolbar, retorna un `
` que es el contenedor del canvas. Ya llama `canvas.zoom('fit-viewport', 'auto')` al importar el XML. **Esta etapa agrega dos features independientes:** 1. **Indicador stale** — aviso persistente cuando el proceso tiene cambios sin simular. 2. **Botón fit-viewport** — encuadrar el diagrama BPMN en cualquier momento. --- ## Objetivo **D.1 — Indicador de simulación desactualizada** Cuando el usuario modifica actividades, recursos o gateways sin volver a simular, el workspace muestra un indicador visible y persistente (sobrevive a recargas de página). **D.2 — Botón fit-viewport en BpmnCanvas** Un botón flotante sobre el canvas que ejecuta `canvas.zoom('fit-viewport')`. Detectado en validación Sprint 2 como necesidad real en talleres con diagramas grandes. --- ## Arquitectura — leer antes de implementar ### Dependencias entre stores (no crear dependencias circulares) ``` process-store.ts ↑ importado por simulation-store.ts (excepción documentada en el archivo) ``` Para evitar dependencia circular, **`process-store.ts` NO debe importar `simulation-store.ts`**. La coordinación entre stores la hace `WorkspacePage`. ### Flujo correcto para el indicador stale 1. `process-store.ts` — bump `process.updatedAt` en mutaciones de actividades/gateways/recursos. 2. `simulation-store.ts` — agregar `lastPersistedExecutedAt: number | null` (cargado desde IndexedDB, sobrevive recargas) y la acción `loadLatestForProcess(processId)`. 3. `WorkspacePage` — orquesta: llama `loadLatestForProcess` cuando el proceso está cargado, y computa `isStale` comparando los dos valores. --- ## Cambios autorizados y arquitectura ### `src/store/process-store.ts` Agregar bump de `process.updatedAt` en todas las acciones que mutan datos del proceso sin ser `updateProcess`: ```typescript // Patrón a aplicar en: updateActivity, updateGateway, updateResource, addResource, deleteResource // (updateProcess ya actualiza updatedAt correctamente — no modificar) // Ejemplo — updateActivity (aplicar el mismo patrón a las demás): updateActivity: async (activity) => { await activityRepo.save(activity) const current = get().currentProcess if (current) { const updatedProcess = { ...current, updatedAt: Date.now() } await processRepo.save(updatedProcess) set((state) => ({ activities: state.activities.map((a) => (a.id === activity.id ? activity : a)), currentProcess: updatedProcess, })) } else { set((state) => ({ activities: state.activities.map((a) => (a.id === activity.id ? activity : a)), })) } }, ``` **Nota:** la suscripción en `simulation-store.ts` ya llama `invalidateResults()` cuando `currentProcess` cambia. Esto es correcto y esperado — no modificar esa suscripción. ### `src/store/simulation-store.ts` Agregar campo y acción para la comparación persistente: ```typescript interface SimulationState { // ... campos existentes ... lastPersistedExecutedAt: number | null // 🆕 executedAt de la última sim guardada en IndexedDB loadLatestForProcess: (processId: string) => Promise // 🆕 } ``` Implementación: ```typescript lastPersistedExecutedAt: null, loadLatestForProcess: async (processId) => { const sim = await simulationRepo.getLatestByProcess(processId) set({ lastPersistedExecutedAt: sim?.executedAt ?? null }) }, ``` Actualizar también `persistResults` para mantener el campo sincronizado: ```typescript persistResults: async (processId, actual, automated) => { const executedAt = Date.now() await simulationRepo.save({ id: uuidv4(), processId, executedAt, result: actual, resultAutomated: automated, }) set({ resultActual: actual, resultAutomated: automated, lastSimulatedAt: new Date(), lastPersistedExecutedAt: executedAt, // 🆕 error: null, }) }, ``` Y limpiar en `reset`: ```typescript reset: () => set({ ..., lastPersistedExecutedAt: null }) ``` ### `src/features/workspace/WorkspacePage.tsx` **1 — Orquestación (nuevo `useEffect`)** Después de que el proceso se carga, cargar el último `executedAt` persistido: ```typescript const { loadLatestForProcess, lastPersistedExecutedAt } = useSimulationStore() useEffect(() => { if (currentProcess) { loadLatestForProcess(currentProcess.id) } }, [currentProcess?.id, loadLatestForProcess]) ``` **2 — Cómputo del indicador** ```typescript const isStale = Boolean( currentProcess && currentProcess.updatedAt > (lastPersistedExecutedAt ?? 0) ) ``` **3 — UI del indicador** Mostrar un badge sutil **junto al botón Simular** cuando `isStale === true`. El botón Simular ya existe en el header del workspace. Diseño del indicador: - Chip naranja claro: fondo `#fff3e6`, borde `#F59845`, texto `#7a3e00`, ícono `AlertCircle` (Lucide, `h-3 w-3`). - Texto: `"Hay cambios sin simular"`. - Ubicación: inline junto al botón Simular, o como fila adicional debajo del header — a criterio del agente según el layout actual (Nivel 1). - Visible solo cuando `isStale === true`. Desaparece al completar la simulación (porque `persistResults` actualiza `lastPersistedExecutedAt`). ### `src/features/workspace/BpmnCanvas.tsx` Agregar botón flotante de fit-viewport. El canvas actualmente retorna: ```tsx return
``` Cambiar a wrapper con botón superpuesto: ```tsx return (
) ``` Importar `Maximize2` desde `lucide-react`. --- ## Versión con wow (recomendada si costo < 40% del mínimo) **Banner en ReportPage:** En `src/features/report/ReportPage.tsx`, si el proceso tiene cambios sin simular, mostrar un banner sutil en la parte superior del reporte: ``` ⚠️ Los resultados pueden estar desactualizados — hay cambios sin simular. [Ir al workspace →] ``` Diseño: fondo `#fffbeb`, borde `#f59e0b`, texto `#92400e` (amber). Full-width, 40px de alto máximo. Link "Ir al workspace →" navega a `/workspace/$processId`. `ReportPage` ya usa `useProcessStore` para leer `currentProcess`. Necesita también `useSimulationStore().lastPersistedExecutedAt` para el cálculo de `isStale`. --- ## ❌ Lo que NO entra en esta etapa - Cambios en `ActivityPanel.tsx`, `ResourcesPanel.tsx`, `GatewayPanel.tsx` — no tocar - `automatedAssignedResources` — sigue siendo opcional, no promover (Etapa 5) - Cambios en el motor de simulación (`domain/simulation.ts`) - Export PDF/CSV - Biblioteca o library-store - Tags, settings --- ## Verificación antes de entregar ```bash # 1. Build limpio npm run build # 2. Tests (todos los previos deben seguir verdes) npm run test # 3. Control de scope git diff --name-only HEAD ``` El diff debe mostrar únicamente: - `src/store/process-store.ts` - `src/store/simulation-store.ts` - `src/features/workspace/WorkspacePage.tsx` - `src/features/workspace/BpmnCanvas.tsx` - `src/features/report/ReportPage.tsx` (solo si se implementó wow) Si aparece cualquier otro archivo de `src/features/` que no sea workspace/ o report/: revertir y reportar. ### Verificación funcional mandatoria Antes de reportar, verificar manualmente: 1. **Stale persiste en recarga:** modificar una actividad → recargar página → el indicador sigue visible → simular → desaparece. 2. **Sin sim previa:** proceso recién importado sin simular nunca → indicador visible (ya que `updatedAt > 0 > null ?? 0`). 3. **Fit-viewport:** abrir un diagrama complejo → click en el botón → el canvas se encuadra. 4. **Sin falsos positivos:** proceso recién simulado sin cambios posteriores → indicador ausente. --- ## Criterios de aceptación - [ ] Modificar actividad/recurso/gateway actualiza `process.updatedAt` en IndexedDB. - [ ] Indicador "Hay cambios sin simular" aparece en WorkspacePage al detectar `isStale`. - [ ] Indicador desaparece al simular exitosamente. - [ ] Indicador persiste al recargar la página (comparación contra IndexedDB, no solo memoria). - [ ] Proceso sin historial de simulación → indicador visible. - [ ] Botón fit-viewport en BpmnCanvas encuadra el diagrama correctamente. - [ ] Build sin errores TypeScript. - [ ] Todos los tests previos siguen verdes. - [ ] Validación visual del director aprobada. --- ## Reporte esperado al terminar 1. Resultado de `git diff --name-only HEAD`. 2. Resultado de `npm run build` y `npm run test`. 3. Confirmación de la verificación funcional (4 casos). 4. Si se implementó el banner en ReportPage (wow): declararlo. 5. Cualquier decisión de iniciativa tomada (Nivel 1/2/3). 6. Si hubo archivo fuera del scope: declararlo con justificación. --- ## Referencias - `sprints/sprint-3/BRIEF.md` — sección 4, Feature D - `src/store/process-store.ts` — acciones a modificar: `updateActivity`, `updateGateway`, `updateResource`, `addResource`, `deleteResource` - `src/store/simulation-store.ts` — estado actual, suscripción a process-store - `src/features/workspace/BpmnCanvas.tsx` — canvas actual, sin toolbar - `methodology/ANTI_PATTERNS.md` — AP.8 (adelanto silencioso) - `methodology/PATTERNS.md` — C.7 (git diff como control preventivo)