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