diff --git a/BACKLOG.md b/BACKLOG.md index 960e8b6..18d0a70 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -20,11 +20,11 @@ Al cerrar un sprint, los ítems `[SPRINT-N]` se promocionan a `[SPRINT-(N+1)]` o ## Features de alta prioridad -### [BACKLOG] Ver procesos asociados a un recurso + navegar a ellos -Desde el catálogo de recursos (`/recursos`), cada recurso debería mostrar cuántos procesos lo usan y permitir navegar a esos procesos directamente. Detectado en validación visual Sprint 4 Etapa 8. Valor: evitar eliminar accidentalmente recursos en uso (hoy hay protección, pero sin visibilidad de qué proceso los usa) y facilitar auditoría del catálogo. +### [RESOLVED] Ver procesos asociados a un recurso + navegar a ellos +Implementado en Sprint 5 Etapa 4. `ResourceCatalogPage` muestra columna "Procesos" con conteo. Click expande lista de procesos con links. `resource-repo.ts` provee `getProcessCountsByResourceId()` y `getProcessesByResourceId()`. Confirmado 2026-06-23. -### [BACKLOG] Campo "empresa" en perfil de usuario -El perfil del usuario hoy muestra nombre y email. Agregar campo `company` o `organization` en la tabla `users` para mostrar a qué empresa pertenece el consultor. Útil cuando haya múltiples consultores de distintas organizaciones (guests). Detectado en validación visual Sprint 4 Etapa 8. +### [RESOLVED] Campo "empresa" en perfil de usuario +Implementado en Sprint 5 Etapa 4. Columna `company` en tabla `users` (Supabase), campo editable en `ProfileSheet`, visible en dropdown `AppHeader`. Confirmado 2026-06-23. ### [RESOLVED] Indicador de simulación desactualizada Implementado en Sprint 3. Confirmado funcionando en validación visual Sprint 4 (muestra "Hay cambios sin simular" junto al botón Simular). Cerrado Sprint 5 planning (2026-06-19). @@ -63,8 +63,8 @@ Manifest + service worker para uso offline. ### [BACKLOG] Zoom/pan del heatmap desde el reporte Hoy es visualización estática. Permitir interacción. -### [SPRINT-5] Controles de zoom en canvas BPMN (fit-to-screen / zoom-in / zoom-out) -Fit-viewport implementado en Sprint 3 pero con bug: clip al borde superior sin centering. Sprint 5 Etapa 2 corrige el bug y agrega controles de zoom-in/zoom-out explícitos. +### [RESOLVED] Controles de zoom en canvas BPMN (fit-to-screen / zoom-in / zoom-out) +Implementado en Sprint 5 Etapa 2. fit-viewport con centering correcto, botones zoom-in/zoom-out/fit en esquina inferior izquierda (1.2x factor, límites 0.1x–4x). OK formal del director 2026-06-19. ### [BACKLOG] Detalle de recursos por actividad en el PDF Tabla de costos por recurso asignado dentro de cada actividad. @@ -115,5 +115,13 @@ Mantener en web app o eliminar cuando InQuality venda a clientes internacionales **Decisión pendiente para:** Sprint 3-4 con feedback de uso real en talleres y demos. -### [DEFERRED] Ícono de InQuality en header de PDFs -Requiere assets de marca consolidados que aún no existen. Re-evaluar cuando InQuality tenga manual de marca completo. \ No newline at end of file +### [BACKLOG] Logo InQuality en header de PDFs +Los assets SÍ existen en `assets/` (4 PNGs documentados en `simulador-web/CLAUDE.md`). La justificación original ("aún no existen") era incorrecta. + +**Assets disponibles:** +- `MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png` — logo horizontal color (candidato principal) +- `MARCA FINAL 2023 AREA DE SEGURIDAD 1 BCO.png` — logo horizontal blanco (para fondos oscuros) + +**Trabajo pendiente:** cargar el PNG vía `html2canvas` o convertirlo a base64 e inyectarlo en el header de los 3 PDFs (Actual, Automatizado, ROI). Ya existe la regla en CLAUDE.md de usar el PNG en lugar de texto "InQ"/"InQuality". + +**Estimación:** Sprint 6 o posterior. Baja prioridad — los PDFs ya tienen "Powered by InQuality" en footer. \ No newline at end of file diff --git a/OBSERVATIONS.md b/OBSERVATIONS.md index 10ad485..ff8437f 100644 --- a/OBSERVATIONS.md +++ b/OBSERVATIONS.md @@ -21,9 +21,10 @@ ### [RESOLVED] Etapa 1 — Workspace loading fix + carry-over skeleton **Estado:** OK formal del director — 2026-06-19. -**Fix principal:** `process-store.ts` y `useReportData.ts` usaban `getSession()` sin timeout; reemplazados por `getSessionSafe()`. Error Boundary + estado `error` inline con retry button agregados. +**Fix principal:** `process-store.ts` y `useReportData.ts` usaban `getSession()` sin timeout; reemplazados inicialmente por `getSessionSafe()`. Error Boundary + estado `error` inline con retry button agregados. +**⚠️ Corrección histórica (Etapa 4E):** `getSessionSafe()` fue identificado como contraproducente y eliminado completamente. Usaba `Promise.race` con un timeout que abandonaba la Promise interna de `getSession()`, dejando operaciones huérfanas ocupando el `processLock` indefinidamente. El fix correcto fue `lockAcquireTimeout: 30000` en `supabase.ts` + eliminación de `getSessionSafe()` en todos los archivos. El código actual NO usa `getSessionSafe()`. **Carry-over a Etapa 2:** `WorkspaceSkeleton.tsx` fue creado pero el spinner del router-level guard (TanStack Router) intercepta antes de que el componente monte. El skeleton no aparece en la práctica. Corregir en Etapa 2 como Fix 0 (bajo esfuerzo). -**SupabaseAuthService.ts:** su `getSession()` directo documentado como excepción intencional (necesita el objeto sesión real, no es barrera descartable). Sin callers activos. Migrar a `getSessionSafe()` cuando tenga callers — pendiente registrar en TECH_DEBT. +**SupabaseAuthService.ts:** su `getSession()` directo documentado como excepción intencional (necesita el objeto sesión real, no es barrera descartable). Sin callers activos. Ver TECH_DEBT.md para el ítem pendiente. --- @@ -60,6 +61,35 @@ --- +### [RESOLVED] Etapa 4 — Recursos por proceso + perfil empresa + fix freeze ProfileSheet + +**Estado:** OK formal del director — 2026-06-23. + +**Entregado:** +- `ResourceCatalogPage`: columna "Procesos" con conteo de procesos que usan cada recurso. `resource-repo.ts` expone `getProcessCountsByResourceId()` y `getProcessesByResourceId()` para carga on-demand. +- Perfil de usuario: campo `company` en tabla `users` (Supabase), campo en `ProfileSheet`, visible en dropdown `AppHeader`. +- Fix `||` vs `??` en cadena de propagación de `name`: `buildAuthUser`, `getUserFromStorage`, `syncProfileFromDB`, `AuthContext useEffect` — evita que Google OAuth con `full_name: ""` deje el botón Guardar deshabilitado. + +**Bug crítico resuelto — freeze de UI al guardar perfil (12 commits, 2026-06-23):** + +Dos causas independientes bajo el mismo síntoma ("la app se congela"): + +1. **Contención de `processLock`:** `supabase.from().update()` en `ProfileSheet` adquiría el lock vía `getSession()` interno, bloqueando otras queries de la página. Fix: reemplazo por `fetch()` nativo con token leído de `localStorage` (`supabase.auth.token`), sin pasar por el cliente Supabase. + +2. **`@radix-ui/react-remove-scroll` no limpiaba `pointer-events:none` del body:** al cerrar el Sheet, el cleanup de Radix no corría porque el re-render de `AuthContext` (disparado por `updateUser`) interrumpía la animación de cierre. `document.body.style.pointerEvents = 'none'` quedaba permanente, bloqueando toda la UI excepto el canvas BPMN (que fija `pointer-events: all` en su propio SVG). Fix: wrapper `Sheet` en `src/components/ui/sheet.tsx` con `useEffect` que observa el prop `open` y limpia el body 400ms después del cierre si no hay otro dialog abierto. + +**Lección operativa (cosechada a `methodology/`):** ante un bug de UI que persiste tras 2+ fixes teóricos basados en lectura de código, pedir evidencia observable directa (DOM, Network, consola) antes de continuar. El diagnóstico definitivo llegó de un dato: `document.body.style.pointerEvents = 'none'`. Ver `INFORME_ETAPA_4_FREEZE_PERFIL.md` para cronología completa. + +**Archivos modificados:** +- `src/auth/SupabaseAuthService.ts` — `||` en `buildAuthUser` y `syncProfileFromDB` +- `src/auth/AuthContext.tsx` — `||` en `getUserFromStorage` y `useEffect` de sync +- `src/components/ProfileSheet.tsx` — fetch nativo, optimistic close, defer `updateUser` 350ms +- `src/components/ui/sheet.tsx` — wrapper defensivo con cleanup de `pointer-events` +- `src/lib/supabase.ts` — `lockAcquireTimeout: 30000`, eliminación de `getSessionSafe()` +- `src/store/library-store.ts`, `src/store/process-store.ts`, `src/features/report/useReportData.ts` — eliminación de `getSessionSafe()` + +--- + ## Hallazgos diferidos (promocionar al cerrar sprint) *(vacío)* diff --git a/TECH_DEBT.md b/TECH_DEBT.md index 5a55a62..682ab16 100644 --- a/TECH_DEBT.md +++ b/TECH_DEBT.md @@ -47,6 +47,19 @@ en los 3 PDFs. Si hay truncamiento, evaluar: ## Deuda media — afecta DX o evolución del producto +### [BACKLOG] `getCurrentUser()` en SupabaseAuthService — sin callers activos +**Estado actual:** `SupabaseAuthService.getCurrentUser()` usa `getSession()` directamente (sin timeout, sin processLock workaround). Es la única excepción documentada a la regla — justificada porque necesita el objeto sesión completo, no solo una barrera. Sin callers activos hoy. +**Impacto si no se resuelve:** ninguno mientras no tenga callers. Si en el futuro algún flujo llama `getCurrentUser()`, podría colgar el processLock si el servidor no responde. +**Solución propuesta:** cuando adquiera callers reales, evaluar si `supabase.auth.getUser()` (token de localStorage, sin lock) es suficiente para ese caso. +**Archivos afectados:** `src/auth/SupabaseAuthService.ts` (~línea 27, comentario explicativo presente). +**Origen:** OBSERVATIONS Sprint 5 Etapa 1 — ítem prometido que faltaba registrar acá. + +### [BACKLOG] `setTimeout(350)` en ProfileSheet — candidato a limpieza +**Estado actual:** `ProfileSheet.handleSave()` difiere `updateUser` 350ms como mitigación de la race condition con Radix. El fix definitivo vive en `src/components/ui/sheet.tsx` (cleanup defensivo de `pointer-events`). El setTimeout es redundante pero inofensivo. +**Solución propuesta:** eliminar el `setTimeout`, verificar que el fix en `sheet.tsx` sea suficiente por sí solo. +**Archivos afectados:** `src/components/ProfileSheet.tsx`. +**Origen:** Sprint 5 Etapa 4G (fix intermedio que quedó al resolverse la causa raíz en 4H). + ### [BACKLOG] E2E specs con Dexie directo — reescribir para Supabase **Estado actual:** `export-pdf.spec.ts` y `validate-dod-etapa4.spec.ts` inyectan datos vía Dexie/IndexedDB directamente. Dexie fue eliminado en Sprint 4 Etapa 4 — estos specs fallan en cualquier entorno donde no exista la DB local del sprint anterior. **Impacto:** 2 specs permanentemente rojos. No bloquean build ni deploy, pero degradan la señal de la suite E2E. diff --git a/sprints/sprint-5/INFORME_ETAPA_4_FREEZE_PERFIL.md b/sprints/sprint-5/INFORME_ETAPA_4_FREEZE_PERFIL.md new file mode 100644 index 0000000..709fab2 --- /dev/null +++ b/sprints/sprint-5/INFORME_ETAPA_4_FREEZE_PERFIL.md @@ -0,0 +1,86 @@ +# Informe — Resolución del freeze al guardar perfil (Sprint 5, Etapa 4) + +**Fecha:** 2026-06-23 +**Síntoma reportado:** al guardar nombre/empresa en el Sheet de "Editar perfil", la aplicación deja de responder a clicks en cualquier parte de la pantalla. Persistía tras múltiples deploys. +**Estado final:** resuelto. Confirmado en producción por el director. + +--- + +## Resumen ejecutivo + +Hubo **dos bugs independientes superpuestos** bajo el mismo síntoma ("la app se congela"): + +1. **Contención de `processLock`** (Supabase Auth v2): operaciones de sincronización de sesión al login competían por un lock interno compartido con el guardado de perfil, pudiendo bloquearlo indefinidamente si el servidor tardaba. +2. **Bug de Radix UI / `react-remove-scroll`**: al cerrar el Sheet, el cleanup que restaura `pointer-events` del `` no se ejecutaba de forma confiable, dejando toda la página no interactiva — salvo los elementos del canvas BPMN, que fijan `pointer-events: all` explícitamente vía su propio CSS y por eso seguían respondiendo. + +El primero se resolvió en las etapas 4B–4F. El segundo, que era la causa del síntoma final reportado, se resolvió en la etapa 4H (dos iteraciones). + +--- + +## Cronología de intentos + +| Etapa | Hipótesis | Fix aplicado | Resultado | +|---|---|---|---| +| 4B (`dd361b1`) | `LibraryPage` recargaba en cada referencia nueva de `user` | `useEffect` dep `user` → `user?.id` | Insuficiente — no era la causa raíz completa | +| 4D (`496c227`) | Deadlock documentado en GoTrueClient.js L3213: callback `async` de `onAuthStateChange` hace `await` de otra llamada Supabase mientras el lock interno está activo | Callback de `onAuthStateChange` pasa a ser síncrono; las queries de DB se mueven a un `useEffect` separado fuera del lock | Necesario pero no suficiente | +| 4B-bis (`3d3ae99`) | El guardado de perfil seguía pasando por `supabase.from().update()`, que adquiere el lock vía `getSession()` internamente | Reemplazo por `fetch` nativo con token leído de `localStorage` | Mitiga, pero introduce su propio problema (ver 4G) | +| 4B-bis (`602957b`) | Las queries de sync de perfil al login podían colgar el lock indefinidamente si el servidor no respondía | `AbortController` con timeout de 6s en esas queries | Buena práctica defensiva, no resuelve el síntoma final | +| 4E (`3ebfd65`) | `getSessionSafe()` (barrera con `Promise.race` + timeout) abandonaba la Promise de `getSession()` al ganar el timeout, dejando una operación huérfana ocupando el lock para siempre | `lockAcquireTimeout: 30000` + eliminación completa de `getSessionSafe()` en 4 archivos | Correcto, resolvía un freeze real pero distinto al reportado al final | +| 4F (`1c53299`) | El botón "Guardar" aparecía deshabilitado porque `user.name` llegaba como cadena vacía (Google devolvía `full_name: ""` y el operador `??` no hace fallback con `""`) | `??` → `||` en 4 puntos de la cadena de propagación del nombre | Corrige un bug real distinto (botón disabled, no freeze) | +| 4G (`7f1f833` → `a1a8b1c` → `985c655` → `aaf1f79`) | El propio fetch nativo, al cerrar el Sheet de forma optimista, dejaba un overlay de Radix "stuck" en el DOM | Patrón optimista (cerrar antes de persistir) + defer de `updateUser` 350ms para no interferir con la animación de cierre de Radix | Mitigaba parcialmente, no resolvía la causa estructural | +| **4H (`1aa1486`)** | El cleanup de `react-remove-scroll` no restauraba `pointer-events` del body al cerrar | Wrapper de `onOpenChange` en el componente `Sheet` con limpieza diferida 400ms | **No se ejecutaba** — error de diagnóstico, ver abajo | +| **4H-2 (`c93d8e8`)** | — | `useEffect` que observa el prop `open` directamente en lugar de `onOpenChange` | **Confirmado funcionando en producción** | + +--- + +## Causa raíz final (4H-2) + +`onOpenChange` de Radix `Dialog.Root` (base del `Sheet` de shadcn/ui) es un canal **unidireccional**: Radix lo invoca cuando decide cerrar por iniciativa propia (Escape, click en el overlay, botón X). Pero `ProfileSheet.handleSave()` cierra el Sheet llamando directamente `onOpenChange(false)`, que en realidad es `setProfileOpen(false)` del componente padre (`AppHeader`) — un cambio de estado controlado desde **afuera** de Radix. En ese flujo, Radix nunca dispara su propio callback `onOpenChange`. + +El primer intento de fix (4H) interceptaba ese callback, por lo que nunca se ejecutaba en el caso real que estaba pasando en producción. El fix correcto (4H-2) observa el prop `open` en sí mismo vía `useEffect`, lo cual cubre ambos canales de cierre sin importar cuál lo disparó. + +```tsx +function Sheet({ open, ...props }: React.ComponentPropsWithoutRef) { + React.useEffect(() => { + if (open) return + const timeoutId = window.setTimeout(() => { + const stillHasOpenDialog = document.querySelector('[role="dialog"][data-state="open"]') + if (!stillHasOpenDialog && document.body.style.pointerEvents === 'none') { + document.body.style.pointerEvents = '' + } + }, 400) + return () => window.clearTimeout(timeoutId) + }, [open]) + + return +} +``` + +El fix vive en el primitivo `Sheet` compartido (`src/components/ui/sheet.tsx`), no en `ProfileSheet` — protege a cualquier Sheet de la aplicación, presente o futuro. + +--- + +## Observación metodológica + +Las primeras ~10 iteraciones (4B a 4G) se basaron en **lectura de código fuente y razonamiento sobre el comportamiento esperado** de Supabase Auth y Radix UI: diagnósticos plausibles, bien fundamentados, pero formulados sobre supuestos del comportamiento del sistema en producción sin observar directamente ese estado. + +El punto de quiebre fue cuando el usuario compartió el **HTML real renderizado de la pantalla congelada**. Ese único dato — `` — redujo el espacio de hipótesis de inmediato: ya no era necesario razonar sobre locks de Supabase, deadlocks de GoTrueClient, o timing de animaciones; el síntoma estaba literalmente escrito en el atributo `style` del elemento raíz. A partir de ahí, el fix llegó en dos iteraciones (una con un error de diagnóstico menor — el canal de `onOpenChange` equivocado — corregido de inmediato al confirmarse que no funcionaba). + +**Lección operativa:** ante un bug de UI que persiste a través de varios fixes basados en teoría, pedir evidencia observable directa (HTML del DOM, captura de Network, captura de consola) antes de seguir iterando sobre hipótesis de código es más eficiente que razonar en capas sobre el comportamiento esperado de librerías de terceros. El código dice lo que *debería* pasar; el DOM en vivo dice lo que *está* pasando. + +--- + +## Archivos modificados (total de la resolución) + +- `src/auth/SupabaseAuthService.ts` +- `src/auth/AuthContext.tsx` +- `src/components/ProfileSheet.tsx` +- `src/components/ui/sheet.tsx` ← fix final +- `src/lib/supabase.ts` +- `src/store/library-store.ts` +- `src/store/process-store.ts` +- `src/features/report/useReportData.ts` + +**Commits:** `dd361b1`, `496c227`, `3d3ae99`, `602957b`, `3ebfd65`, `1c53299`, `7f1f833`, `a1a8b1c`, `985c655`, `aaf1f79`, `1aa1486`, `c93d8e8` + +**Tests:** 528 Vitest verdes en cada commit. Build limpio en cada commit.