Bitácora: v1.7 en producción (overlay + baneos + carrito persistente + fix upload)
This commit is contained in:
@@ -258,6 +258,19 @@ Fases 3, 4/5 y 6 tocan carpetas de rutas distintas (`src/pages/alumno/*`, `src/p
|
|||||||
- **Verificación**: `npm run build` limpio (1.52s). Deploy: 2 commits (migración + código), push con token efímero, `ssh buglabs '... set -a && . .env.production && set +a && docker compose up -d --build'` (mismo patrón que 0004). Container healthy en 23s. Smoke test post-deploy: `/login` → 200; `/audio/sonido_notificacion.mp3` → 200 con `content-type: audio/mpeg` (audio sirve correctamente); BD verifica `profiles_rol_check` incluye docente, 3 maestros existentes con `es_tutor` booleano correcto.
|
- **Verificación**: `npm run build` limpio (1.52s). Deploy: 2 commits (migración + código), push con token efímero, `ssh buglabs '... set -a && . .env.production && set +a && docker compose up -d --build'` (mismo patrón que 0004). Container healthy en 23s. Smoke test post-deploy: `/login` → 200; `/audio/sonido_notificacion.mp3` → 200 con `content-type: audio/mpeg` (audio sirve correctamente); BD verifica `profiles_rol_check` incluye docente, 3 maestros existentes con `es_tutor` booleano correcto.
|
||||||
- **Deploy completado**: **v1.6 en producción en https://prestamos.buglabs.dev**. El usuario se encarga de: (a) desactivar Cloudflare Web Analytics para el dominio (bug de sha512/CORS); (b) marcar como docente vía SQL a las cuentas que corresponda.
|
- **Deploy completado**: **v1.6 en producción en https://prestamos.buglabs.dev**. El usuario se encarga de: (a) desactivar Cloudflare Web Analytics para el dominio (bug de sha512/CORS); (b) marcar como docente vía SQL a las cuentas que corresponda.
|
||||||
|
|
||||||
|
- **2026-08-27 — v1.7: overlay de carga, lista negra de cuentas, carrito persistente + fix definitivo del upload**. 3 features + 1 cambio UX + 2 bugs residuales. Fase 1 con Bash/grep + logs de Storage en prod (sin Explore agents — bug root cause obvio en los logs), Fase 2 sin Plan agent (contexto claro), 2 preguntas al usuario (modelo de baneos, estrategia fix upload), plan escrito y aprobado, ejecución con migración yo + 3 agentes paralelos.
|
||||||
|
- **Bug del upload — root cause definitivo (via logs storage de prod)**: `"role":"anon"` + `"error":"new row violates row-level security policy"` (código 42501). Causa real: `browserClient()` en `src/lib/supabase.ts` usa `createBrowserClient` de `@supabase/ssr` — en el browser NO puede leer las cookies de auth porque están seteadas con `httpOnly: true` (correcto por seguridad, JS del browser no las puede leer nunca). Al hacer `.storage.from(bucket).upload()`, la request sale con `Authorization: Bearer <anon_key>` (sin JWT del user), Storage la evalúa como `role: 'anon'`, y RLS rechaza. **Los fixes previos (Dockerfile ARG PUBLIC_*, guard typeof process en supabase.ts) NO tocaban este bug** — solo aseguraban que la URL del Supabase estuviera horneada en el bundle, pero el bundle nunca pudo autenticar contra Storage. Fix elegido: **endpoints proxy server-side** que reciben multipart, validan la sesión con la cookie httpOnly (server sí la lee via `serverClient(cookies)`), y suben con `serviceClient()` (bypass RLS). Cookies siguen httpOnly — cero riesgo XSS.
|
||||||
|
- **Decisiones tomadas con el usuario**: (1) baneos = **tabla separada** `prestamos.baneos(profile_id, razon, banned_at, banned_by, expires_at, unbanned_at, unbanned_by)` — historial completo + soporte para baneos temporales (aunque UI solo expone permanentes esta ronda); (2) fix upload = endpoints proxy; (3) overlay de carga literal (blur + spinner) según petición del usuario — no ClientRouter (evita side effects en islands y Realtime).
|
||||||
|
- **Migración `0007_baneos.sql`** aplicada limpia: nueva tabla `baneos` con partial unique index `where unbanned_at is null` (garantiza máximo 1 baneo activo por profile), RLS `baneos_admin_all` (admin CRUD) + `baneos_read_self` (user ve los suyos para /banned), helper SQL `prestamos.is_banned(uid) returns boolean` `security definer stable` que retorna true si existe baneo activo no expirado — invocable por el middleware vía `.rpc('is_banned', {p_uid})`.
|
||||||
|
- **3 agentes paralelos** con contratos aislados — **los 3 completos limpios en un pase** (a diferencia de v1.6 donde P y H cortaron por límite de sesión):
|
||||||
|
- **B · Baneos + panel + middleware guard** (9 archivos): `banned.astro` (pantalla completa patrón /403, muestra razón + fecha + expires + quién baneó + botón signout), `POST /api/admin/baneos` (guards admin + no-self-ban + razón 5-500 chars + expires_at futuro opcional, 23505 → 409), `POST /api/admin/baneos/[id]/desbanear` (update idempotente con `where unbanned_at is null`, 404 si ya inactivo), `BanearForm.tsx` (dialog con textarea razón + contador; deshabilitado con tooltip si adminSelf), `DesbanearButton.tsx` (window.confirm simple), `/admin/usuarios.astro` (2 queries paralelas profiles + baneos activos, cruce en memoria, tabla desktop + cards mobile), middleware con nuevo array `BANNED_ALLOWED = ['/banned', '/api/auth/signout']` para escapar del rewrite y evitar loop, RPC `is_banned` solo se llama si `profile && rol !== 'admin'` (evita costo en admin/no-login), nav admin `+ Usuarios`.
|
||||||
|
- **U · Uploads proxy** (4 archivos): `POST /api/upload/material-foto/[id].ts` (guard admin, valida mime `/^image\//` + `size <= 2MB`, path `${id}/${Date.now()}.${ext}`, sube con `serviceClient().storage.from('materiales-fotos').upload({upsert:true, contentType})`, actualiza `materiales.imagen_path` con locals.supabase), `POST /api/upload/avatar.ts` (guard user autenticado, path `${uid}/${Date.now()}.${ext}` al bucket `avatares`, actualiza `profiles.foto_path`), refactor `MaterialForm.tsx` `uploadFotoFor` para fetch multipart al endpoint (eliminó import de browserClient y del PATCH cliente del imagen_path — el endpoint ya lo persiste, evita doble escritura), refactor `PerfilForm.tsx` mismo patrón. Decisión no obvia: `uploadFotoFor` mantiene el patrón toast+return-null (no throw) del original — preserva la UX de "material creado sin foto" si el upload falla, en vez de bloquear la creación completa. Anotado en comentario: el "Quitar foto" en edit-mode nunca se propagaba a la BD (bug preexistente fuera de scope).
|
||||||
|
- **X · UX chico** (3 archivos): `Layout.astro` con `<div id="page-loader" hidden>` + estilos `position:fixed backdrop-filter:blur(8px)` + spinner mono animado (respeta prefers-reduced-motion) + script inline que captura clicks en `<a href>` y submits de `<form>` mismo origen (guardas: modifier keys, target=_blank, anchor#, javascript:/mailto:/tel:, cross-origin, defaultPrevented — este último es clave: los forms fetch-managed llaman preventDefault en su onSubmit React, así que el overlay no se dispara falsamente para ellos), `pageshow` limpia el overlay por bfcache. `SolicitudCart.tsx` con useEffect hidratar+persistir `labre:cart:v1` en localStorage + guard `cartHydrated` (evita que la primera pasada del effect pise el localStorage antes de leerlo) + botón Vaciar en footer del checkout con `window.confirm` (solo dentro del bloque perfilCompleto). `admin/inventario/index.astro` eliminado el `<script>` inline de auto-submit debounced que se había agregado en v1.5.
|
||||||
|
- **Verificación combinada**: `npm run build` limpio (2.74s en dev; 2.64s en el agente B). Sin conflictos de merge. Smoke test público post-deploy: `/login` → 200, `/banned` → 302 (protegido, redirige a login sin sesión), `/admin/usuarios` → 302 (protegido), `/api/upload/avatar` → 302 (middleware protegiendo API antes del handler). Container healthy en ~90s.
|
||||||
|
- **Aprendizajes registrados**: (a) los "fixes" del bug de upload en sesiones previas (Dockerfile ARG, guard typeof process) NUNCA fueron el fix real — solo eran precondiciones necesarias. El bug real requiere abandonar la idea de que el `browserClient` pueda hablar directo con Storage cuando las cookies son httpOnly. Diagnóstico definitivo llegó por leer logs de `supabase-storage` container donde el error 42501/anon estaba explícito; no era necesario reproducir en browser. (b) Contratos de agentes aislados por CARPETA (no por archivo) escalan mucho mejor — 3 agentes editaron 3 conjuntos disjuntos de rutas/componentes/endpoints sin overhead de coordinación. (c) El uso de `preventDefault` como señal semántica funciona bien: cualquier form que llama `preventDefault()` en su `onSubmit` React no dispara el overlay global, sin necesidad de opt-out explícito por form. (d) `partial unique index` `where unbanned_at is null` es el patrón limpio para "máximo un baneo activo por profile" — evita constraint compleja y da el error 23505 traducible.
|
||||||
|
- **Deliberadamente NO tocado**: (i) super admin (mencionado por usuario como consideración futura); (ii) baneo automático por rate limiting; (iii) UI para expires_at en baneos (columna existe, se puede exponer si se pide); (iv) compresión de imágenes cliente-side (si las fotos que sube el admin regularmente pasan de 2MB se agrega); (v) el bug preexistente de "Quitar foto" en MaterialForm que no propagaba null a la BD.
|
||||||
|
- **Deploy completado**: 2 commits (migración + código), push con token efímero de Gitea, `set -a && . .env.production && set +a && docker compose up -d --build` en buglabs (patrón del build args). Container healthy. **v1.7 en producción en https://prestamos.buglabs.dev**. El usuario puede empezar a: (a) crear/actualizar fotos de materiales y perfil (bug arreglado); (b) banear cuentas problemáticas desde `/admin/usuarios`.
|
||||||
|
|
||||||
- **2026-08-26 — Primera ronda de QA manual con agent-browser (post v1.6)**. Se siguió el proceso de `CLAUDE.md`/`AGENTS.md`: bypass temporal `?preview=alumno|admin|docente` en el middleware (solo `import.meta.env.DEV`), revertido al terminar (`git diff src/middleware.ts` queda limpio).
|
- **2026-08-26 — Primera ronda de QA manual con agent-browser (post v1.6)**. Se siguió el proceso de `CLAUDE.md`/`AGENTS.md`: bypass temporal `?preview=alumno|admin|docente` en el middleware (solo `import.meta.env.DEV`), revertido al terminar (`git diff src/middleware.ts` queda limpio).
|
||||||
- **Bug crítico encontrado y arreglado — `process is not defined` rompía la hidratación de 3 formularios en producción**: `src/lib/supabase.ts` leía `process.env.X ?? import.meta.env.X` (orden fijado en la sesión del 24-ago para el bug de upload en Docker). En el browser `process` no existe como global — evaluar `process.env` revienta con `ReferenceError` **antes** de que el `??` pueda caer al fallback. Como el módulo se evalúa completo al importarse (aunque solo se use `browserClient`), esto tumbaba la hidratación de **`PerfilForm.tsx`** (usado en `/perfil` y `/onboarding`) y de **`MaterialForm.tsx`** (admin, alta/edición de material) — los tres quedaban sin JS: los botones "Guardar" hacían un submit nativo del `<form>` (sin `action`, sin querystring) que caía en `/login` en vez de llamar al endpoint. Confirmado con red real: antes del fix, `PATCH /api/profile` nunca se disparaba (submit nativo); con el fix, sí, y persiste correctamente. Fix: guard `typeof process !== 'undefined'` antes de leer `process.env` en las 3 constantes de `supabase.ts` — mantiene la prioridad process→import.meta.env para el server (necesaria por el fix de Docker de esa sesión) sin tocar `process` en el bundle de browser. `avatar.ts` y `materialImg.ts` ya tenían el orden inverso (`import.meta.env` primero) por eso nunca mostraron el bug. **Pendiente: este fix vive solo en el working tree, no se ha commiteado ni desplegado** — el bug sigue viivo en producción hasta que se despliegue.
|
- **Bug crítico encontrado y arreglado — `process is not defined` rompía la hidratación de 3 formularios en producción**: `src/lib/supabase.ts` leía `process.env.X ?? import.meta.env.X` (orden fijado en la sesión del 24-ago para el bug de upload en Docker). En el browser `process` no existe como global — evaluar `process.env` revienta con `ReferenceError` **antes** de que el `??` pueda caer al fallback. Como el módulo se evalúa completo al importarse (aunque solo se use `browserClient`), esto tumbaba la hidratación de **`PerfilForm.tsx`** (usado en `/perfil` y `/onboarding`) y de **`MaterialForm.tsx`** (admin, alta/edición de material) — los tres quedaban sin JS: los botones "Guardar" hacían un submit nativo del `<form>` (sin `action`, sin querystring) que caía en `/login` en vez de llamar al endpoint. Confirmado con red real: antes del fix, `PATCH /api/profile` nunca se disparaba (submit nativo); con el fix, sí, y persiste correctamente. Fix: guard `typeof process !== 'undefined'` antes de leer `process.env` en las 3 constantes de `supabase.ts` — mantiene la prioridad process→import.meta.env para el server (necesaria por el fix de Docker de esa sesión) sin tocar `process` en el bundle de browser. `avatar.ts` y `materialImg.ts` ya tenían el orden inverso (`import.meta.env` primero) por eso nunca mostraron el bug. **Pendiente: este fix vive solo en el working tree, no se ha commiteado ni desplegado** — el bug sigue viivo en producción hasta que se despliegue.
|
||||||
- **Hallazgo operativo, no bug de código — no había ningún perfil `rol='admin'`** en la base de datos de producción al momento de probar (los 10 perfiles reales eran todos `alumno`/`docente`). Bloqueaba por completo `/admin/*`. Con confirmación del usuario, se promovió `amado.garcia.ramirez@uabc.edu.mx` a `admin` vía `PATCH` directo a PostgREST con `service_role`. No se investigó la causa de cómo se quedó sin admin (posble que nunca se re-promovió tras alguna migración, o que el admin real usaba otro correo ya no presente) — si vuelve a pasar, vale la pena revisar.
|
- **Hallazgo operativo, no bug de código — no había ningún perfil `rol='admin'`** en la base de datos de producción al momento de probar (los 10 perfiles reales eran todos `alumno`/`docente`). Bloqueaba por completo `/admin/*`. Con confirmación del usuario, se promovió `amado.garcia.ramirez@uabc.edu.mx` a `admin` vía `PATCH` directo a PostgREST con `service_role`. No se investigó la causa de cómo se quedó sin admin (posble que nunca se re-promovió tras alguna migración, o que el admin real usaba otro correo ya no presente) — si vuelve a pasar, vale la pena revisar.
|
||||||
|
|||||||
Reference in New Issue
Block a user