Bitácora: perfil + onboarding + home + fixes en producción

This commit is contained in:
2026-08-24 08:50:01 -07:00
parent e159e8d297
commit d0f01ea18c
+13
View File
@@ -227,3 +227,16 @@ Fases 3, 4/5 y 6 tocan carpetas de rutas distintas (`src/pages/alumno/*`, `src/p
- **Aprendizajes registrados**: (a) para carreras de asignación de unidad entre alumnos concurrentes, `FOR UPDATE SKIP LOCKED` en el `SELECT` aguanta hasta el commit — para blindar entre transacciones distintas hay que **cambiar el estado al momento de crear** (no al aprobar), y eso obliga a extender los triggers para las transiciones que antes no importaban (`pendiente → rechazado` para liberar la reserva); (b) Supabase Realtime en self-hosted con `replica identity default` solo emite la PK en `payload.old`, así que cualquier lógica que dependa del estado anterior en UPDATE necesita refetch — o cambiar la tabla a `replica identity full` (más ancho de banda, pero payload completo); (c) contratos de agentes aislados por carpeta escalan bien cuando cada uno reescribe un archivo entero — el conflicto real es cuando 2 agentes editan el mismo archivo con `Edit` string-match, ahí sí hay que serializar; (d) los `<dialog>` HTML nativos no anidan (abrir uno dentro de otro rompe el foco) — cuando se necesita "submodal", mejor embebido en el mismo dialog o pasar a `<div>` posicionado; (e) `CLAUDE.md` en este repo es symlink a `AGENTS.md` — editar el path real (aprendido cuando `Edit` rechazó el symlink).
- **Deliberadamente NO tocado**: (i) cards del alumno en el catálogo no tienen foto todavía — el select SSR ya trae `imagen_path` (agregado por el track B para no romper el shape), pero pintar la miniatura en la card del alumno es un paso siguiente si el usuario lo pide; (ii) onboarding para `semestre` sigue pendiente (mismo backlog que 2026-08-17); (iii) `browser-image-compression` no se instaló — si las fotos que suba el admin pesan mucho, se agrega después.
- **Deploy completado**: 2 commits separados (migración + código, mismo patrón que 0002). Push a `git.buglabs.dev/LakG/labre-web` con token efímero de Gitea (mismo patrón que Fase 8/0002). `ssh buglabs 'cd ~/labre-web && git pull && docker compose up -d --build'` → contenedor recreado, `healthy` en ~26s. **Sistema en producción con las 5 features vivas en https://prestamos.buglabs.dev.**
- **2026-08-24 — Perfil de usuario + onboarding + home reformulado + fix del bug de upload + 5 bugs mobile**. Iteración de UX y bugs sobre la app ya viva en prod. Se hizo Fase 1 con 3 Explore agents (bug upload, home/onboarding, bugs mobile), 4 preguntas cerradas al usuario sobre decisiones grandes (maestro en clase, tabla maestros, foto default, onboarding bloqueante), plan escrito y aprobado, ejecución en 4 tracks paralelos + 1 migración BD.
- **Decisiones tomadas con el usuario**: (1) `maestro_responsable` = **`<textarea>` que el alumno escribe a mano si está en clase**; placeholder muestra el nombre del tutor; si queda vacío, la RPC autocompleta con el tutor guardado — cero infra de horarios, cero fricción; (2) **tabla admin-managed `maestros`** simple (solo nombre + activo), CRUD en nueva subpágina bajo `/admin/maestros`; (3) foto default = **`avatar_url` del OAuth de Google** (viene en `user_metadata`), fallback a iniciales sobre color HSL derivado del email; (4) onboarding = **todo opcional** pero el checkout bloquea con CTA "Completa tu perfil" si faltan matrícula o tutor.
- **Migración `0004_perfil_maestros_avatares.sql`** aplicada limpia en un solo `psql -f` (mismo patrón que 0001/0002/0003): tabla `prestamos.maestros(id, nombre unique, activo, created_at)` con RLS (todos leen, solo admin escribe) + seed `('Sin especificar')`; `prestamos.profiles.tutor_id int` (FK → maestros on delete set null) y `prestamos.profiles.foto_path text`; bucket público `avatares` con policies `for select to anon/authenticated` y `for all to authenticated using (bucket_id='avatares' and (storage.foldername(name))[1] = auth.uid()::text)` — el prefijo de carpeta es el UUID del usuario, así RLS deja escribir solo en tu propia carpeta. RPC `crear_solicitud` reescrita con fallback: si `p_maestro_responsable` viene null/vacío, hace `select nombre from maestros where id = (select tutor_id from profiles where id = auth.uid())`; si tampoco hay tutor, sigue el raise `maestro_responsable_requerido` que ahora cae al bloqueo del checkout.
- **4 tracks paralelizados** — 2 completos limpios (F, C), 2 cortados por límite de sesión pero con la mayoría del trabajo escrito antes de morir (H y P). El orquestador completó los archivos que faltaban directamente en el hilo principal.
- **F · Fixes** (8 archivos, completo): (i) Bug upload — `Dockerfile` con `ARG PUBLIC_SUPABASE_URL/ANON_KEY/APP_URL` + `ENV` al inicio del stage `build`, `docker-compose.yml` con `build.args: {PUBLIC_*: ${VAR}}` (`SUPABASE_SERVICE_ROLE_KEY` deliberadamente fuera — solo runtime), `.dockerignore` intacto (no exponer `.env.production` en la imagen), `src/lib/supabase.ts` con `browserClient()` unificado sobre las constantes que ya usan fallback `process.env ?? import.meta.env`. Verificado post-deploy con `grep "supabase\.buglabs\.dev" /app/dist/client/_astro/*.js` — la URL correcta aparece en `MaterialForm`, `PerfilForm`, y el chunk compartido de supabase; el bug está muerto. (ii) Grid inventario mobile — wrapper `flex gap-2` → `grid grid-cols-2 gap-2`; nuevo prop `compact` en `MaterialForm` y `EliminarMaterial` que renderiza el trigger con `w-full text-sm px-2 minHeight:36px`. (iii) `UnidadesManager` mobile — tabla reemplazada por cards `grid-cols-1 sm:grid-cols-[1fr_auto_auto]`, select estado con `minWidth:130px`, notas full-width abajo. (iv) `Chart` donut — con la skill `dataviz`: quitar labels internos (`label={false}`), agregar `<Legend layout="horizontal" verticalAlign="bottom">` con `formatter` "label (N)" — patrón canónico de la skill ("legend always present for ≥ 2 series"; "selective direct labels — never a number on every point"). (v) `Chart` barras — `YAxis width={100}` fijo + `tickFormatter` que trunca `> 14 chars`; se descartó `window.innerWidth` en render (SSR undefined + recharts no re-renderea `YAxis` en resize) y se descartó rotar labels a `-45°` (viola legibilidad); el tooltip mantiene el label completo, cero pérdida de información.
- **C · Checkout con tutor auto** (2 archivos, completo): `catalogo.astro` hace SSR directo del `profiles.matricula + tutor_id` y join a `maestros.nombre` (el middleware no expone tutor_id explícito — se consulta local); pasa `tutorNombre` y `perfilCompleto: !!(profile.matricula && profile.tutor_id)` como props al `CartProvider`. En `SolicitudCart`, el `<input type="text">` de maestro responsable es ahora `<textarea rows={2} maxLength={200}>` sin `required`, con `placeholder={tutorNombre ?? 'Escribe...'}` + hint verde "se usará: <tutor>" si hay tutor. Guard perfil incompleto en el footer del modal: si `!perfilCompleto`, reemplaza botones por card con CTA "Completa tu perfil" → `/perfil`; el `submit()` early-returns como defensa extra. El `toastAfterReload` post-envío detecta cuando el textarea quedó vacío y hubo tutor, y lo menciona en la descripción ("maestro: X (tu tutor)").
- **H · Home reformulado** (parcial → completado en hilo principal): agente alcanzó a escribir `src/pages/index.astro` (alumno con saludo, CTA "¿Qué vas a pedir hoy?", card de último préstamo activo con estado + preview + fecha humanizada, empty state amigable) y agregar `saludoHora()` + `fmtFechaRelativa()` a `src/lib/date.ts` (ambos con offset MX vía `Intl.DateTimeFormat`, respetan DST). Falló antes de reescribir `admin/index.astro`. El orquestador lo completó con: saludo por hora + 2 KPIs de alerta compactos (Pendientes verde si >0 / Vencidos rojo si >0) + sección "Requieren tu atención" con últimas 2 pendientes en cards grid 2-col + sección "Actividad reciente" con últimas 5 (**tabla desktop, cards mobile — arregla de paso el bug 6**). Los 2 KPIs restantes (En préstamo, Agotados) se movieron a `/admin/estadisticas` que ya los tiene.
- **P · Perfil + onboarding + maestros CRUD** (parcial → completado en hilo principal): agente alcanzó a escribir `src/pages/perfil.astro`, `src/pages/onboarding.astro`, `src/pages/api/profile.ts`, `src/components/profile/{Avatar,PerfilForm}.tsx`, `src/lib/avatar.ts` (con prioridad `foto_path` → `googleAvatarUrl` → iniciales sobre HSL hash del email), y a editar `middleware.ts`/`env.d.ts`/`AppLayout.astro`. Falló antes de crear el CRUD admin de maestros. El orquestador completó: `POST /api/admin/maestros`, `PATCH/DELETE /api/admin/maestros/[id]` (con 23503 → "N alumnos como tutor, reasígnalos"), `MaestroForm.tsx` (adaptado de `CategoriaForm` + checkbox `activo`), `EliminarMaestro.tsx` (adaptado de `EliminarCategoria`), `/admin/maestros` con tabla desktop/cards mobile + subnav Materiales · Categorías · Maestros, y edit de `categorias.astro` para agregar la tab Maestros al subnav existente.
- **Verificación combinada**: `npm run build` limpio (2.24s). Deploy: 2 commits (migración + código), push con token efímero, `ssh buglabs 'cd ~/labre-web && git pull && set -a && . .env.production && set +a && docker compose up -d --build'` — el `set -a; . .env.production; set +a` es nuevo: Docker Compose expande `${VAR}` en `build.args` desde el **shell** (no desde `env_file`), así que hay que exportar las vars del `.env.production` al environment antes del compose. Container `healthy` en 31s. Smoke test público: `/login` → 200; `/perfil`, `/admin/maestros`, `/onboarding` → 302 (middleware protegiendo); bundle client contiene `supabase.buglabs.dev` en los 3 chunks esperados (bug del upload confirmado muerto).
- **Aprendizajes registrados**: (a) los agentes fallando por límite de sesión son un modo de degradación normal cuando el trabajo por agente es grande — la mitigación es dividir mejor los tracks (agentes P y H fueron los más grandes, ambos cortaron); alternativa: preparar prompts para que cada agente escriba archivos incrementalmente y el orquestador pueda completar los huecos con menos contexto; (b) Docker Compose lee `.env` **implícitamente** para expansión de `${VAR}` en el YAML pero NO lee `env_file` para expansión — para vars en `build.args`, o bien renombras `.env.production` a `.env`, o exportas al shell antes del compose (elegimos lo segundo, menos disruptivo con la config existente); (c) `Storage RLS` con path por carpeta = uid es el patrón limpio para "cada quien sube la suya" — `(storage.foldername(name))[1] = auth.uid()::text` en el `using`/`with check`, no necesita RPC ni endpoint intermedio; (d) para RPCs con fallback lógico (como `crear_solicitud` autocompletando el tutor), poner la lógica en la BD y no en el endpoint es más robusto porque el mismo comportamiento aplica si algún día se llama la RPC desde otro cliente; (e) `CLAUDE.md` es symlink a `AGENTS.md` (aprendido en la sesión anterior, sigue vigente — editar el path real).
- **Deliberadamente NO tocado**: horarios académicos automáticos (fuera de alcance explícito — usuario eligió textarea manual); notificaciones al maestro cuando se aprueba un vale (backlog); comprimir avatares antes de subir (si pesan mucho se agrega después); PWA y browser notifications (backlog viejo).
- **Deploy completado**: **sistema en producción con perfil + onboarding + home reformulado + maestros CRUD + upload arreglado + 5 bugs mobile arreglados en https://prestamos.buglabs.dev**.