927d99c262
- Elimina /api/whoami (endpoint diagnóstico temporal). - Elimina bloque debug del 403 en /api/admin/prestamos/[id]/devolver. - Bitácora con la causa raíz y el fix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
157 lines
25 KiB
Markdown
157 lines
25 KiB
Markdown
## Development
|
||
|
||
When starting the dev server, use background mode:
|
||
|
||
```
|
||
astro dev --background
|
||
```
|
||
|
||
Manage the background server with `astro dev stop`, `astro dev status`, and `astro dev logs`.
|
||
|
||
## Documentation
|
||
|
||
Full documentation: https://docs.astro.build
|
||
|
||
Consult these guides before working on related tasks:
|
||
|
||
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
|
||
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
|
||
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
|
||
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
|
||
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
|
||
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)
|
||
|
||
## Proceso de trabajo de este proyecto
|
||
|
||
Al final de cada prompt del usuario, actualizar la sección **Bitácora** de este archivo con lo que se hizo, decisiones tomadas y qué sigue. No crear archivos de plan aparte: este CLAUDE.md es la única fuente de verdad del progreso.
|
||
|
||
Antes de ejecutar cambios de infraestructura (SSH a buglabs, docker, Cloudflare tunnel, DB) pedir confirmación al usuario si el cambio es destructivo o afecta a otros proyectos que viven en el mismo servidor (genqbar, gitea, etc).
|
||
|
||
Cuando el trabajo se pueda dividir en partes que no toquen los mismos archivos, lanzar agentes en paralelo (Agent tool) en vez de hacerlo secuencial.
|
||
|
||
---
|
||
|
||
# Proyecto: Sistema de Préstamos — Laboratorio de Sistemas Computacionales UABC
|
||
|
||
Sistema web para gestionar préstamos de material del laboratorio. Dos roles: **alumno** (solicita material) y **admin** (gestiona solicitudes, inventario y reportes). Login exclusivo con Google restringido a correo institucional `@uabc.edu.mx`.
|
||
|
||
## Decisiones de arquitectura
|
||
|
||
| Decisión | Elegido | Motivo |
|
||
|---|---|---|
|
||
| Backend | Supabase **self-hosted existente** en buglabs (`supabase.buglabs.dev`) | Ya está corriendo (Postgres 15, Auth, Kong, Studio) — no se despliega uno nuevo. Compartido con otro proyecto (genqbar). |
|
||
| Aislamiento de datos | Schema Postgres propio: **`prestamos`** | `public` ya lo usa genqbar (tablas `profiles`, `edificios`, `eventos`, etc). Evita colisión de nombres. |
|
||
| Auth | Supabase Auth + proveedor Google OAuth (a configurar, hoy no está activo en la instancia) | Auth es compartido entre apps del mismo Supabase; el proveedor Google se habilita una vez a nivel instancia. |
|
||
| Restricción de dominio `@uabc.edu.mx` | **A nivel aplicación** (middleware de Astro tras login), no a nivel GoTrue | GoTrue es compartido con otras apps del servidor; no se puede bloquear el signup global sin afectar a genqbar. |
|
||
| Alta de admin inicial | Manual vía Supabase Studio, después del primer login | Decisión del usuario — no se hardcodea ningún correo admin. |
|
||
| Frontend interactivo | Astro + **React** (islands) | Ya es un proyecto Astro; React da el ecosistema más grande para tablas/formularios/export. |
|
||
| Hosting de la app | Servidor propio (buglabs), adapter **`@astrojs/node`** | Mismo homelab que Supabase; se agrega contenedor + ruta en el túnel de Cloudflare existente. |
|
||
| Subdominio | **prestamos.buglabs.dev** | Se añade al mismo túnel Cloudflare (`3de17b3c-...`) junto a supabase/git/status/genqbar. |
|
||
| Exportar reportes | CSV nativo (sin librería) para v1 | Cero dependencias nuevas; se sube a PDF/Excel solo si se pide explícitamente. |
|
||
|
||
## Modelo de datos propuesto (schema `prestamos`)
|
||
|
||
- `profiles` — id (=auth.users.id), email, nombre, matricula, rol (`alumno`|`admin`), created_at
|
||
- `categorias` — id, nombre
|
||
- `materiales` — id, nombre, categoria_id, descripcion, cantidad_total, cantidad_disponible, numero_inventario, estado (`disponible`|`mantenimiento`|`baja`)
|
||
- `prestamos` — id, alumno_id, material_id, cantidad, estado (`pendiente`|`aprobado`|`rechazado`|`activo`|`devuelto`|`vencido`), fecha_solicitud, fecha_aprobacion, fecha_devolucion_estimada, fecha_devolucion_real, aprobado_por, notas
|
||
|
||
RLS: alumno solo ve/edita sus propios préstamos; admin ve y gestiona todo. Enforced con `auth.uid()` contra `profiles.id` y policies por rol.
|
||
|
||
## Fases de implementación
|
||
|
||
1. **Backend Supabase** — schema `prestamos`, tablas, RLS, trigger `handle_new_user`, seed de categorías. (bloqueante, va primero)
|
||
2. **Scaffolding Astro** — integración React, cliente Supabase, middleware de auth + verificación de dominio/rol, layout base.
|
||
3. **Interfaz alumno** — catálogo de materiales, solicitar préstamo, ver mis préstamos y su estado.
|
||
4. **Interfaz admin — solicitudes** — bandeja de solicitudes (aprobar/rechazar), marcar devoluciones.
|
||
5. **Interfaz admin — inventario** — alta/baja/edición de materiales y categorías.
|
||
6. **Reportes** — filtros (fecha, material, alumno, estado), export CSV, vista de vencidos.
|
||
7. **Deploy** — Dockerfile + compose para el adapter Node, ruta `prestamos.buglabs.dev` en el túnel de Cloudflare de buglabs.
|
||
8. **QA end-to-end** — flujo real de login + alumno + admin en producción.
|
||
|
||
Fases 3, 4/5 y 6 tocan carpetas de rutas distintas (`src/pages/alumno/*`, `src/pages/admin/*`) y pueden correr como agentes en paralelo una vez completa la fase 2. Fase 7 (infra) también es independiente del código de UI y puede prepararse en paralelo.
|
||
|
||
## Bitácora
|
||
|
||
- **2026-08-13** — Plan inicial definido. Se hizo reconocimiento por SSH del homelab buglabs: Supabase self-hosted ya corre ahí (compartido con proyecto "genqbar" en schema `public`), túnel Cloudflare ya gestiona varios subdominios de buglabs.dev, Docker/Compose disponibles. Decisiones de arquitectura tomadas junto con el usuario (ver tabla arriba). Pendiente: confirmación del usuario para arrancar Fase 1 (backend Supabase).
|
||
|
||
- **2026-08-14** — Plan refinado y aprobado. Ajustes: (1) schema `public.profiles` NO se reutiliza — el sistema de préstamos usa `prestamos.profiles` propio y aislado; (2) sí se agrega `prestamos.audit_log` con trigger para trazabilidad de cambios de estado; (3) v1 sin reglas duras de límite/duración (admin decide caso por caso). Se corrigió la identificación del vecino que usa el schema `public`: no es genqbar (genqbar es un estático sin auth), es otra app de check-in por QR corriendo en el 8080. Google OAuth se confirmó ya habilitado en la instancia de Supabase (`GOOGLE_ENABLED=true`, con CLIENT_ID/SECRET presentes en `~/supabase/docker/.env`), no requiere alta nueva. Se añadieron normas de diseño al plan: paleta UABC (primario `#00723F` verde, secundario `#DD971A` dorado, terciario `#F4F7F5`, neutral `#2D3748`) bajo regla 60/30/10, admin=desktop-first y alumno=mobile-first pero ambos cross-device, minimalista con animaciones ligeras, Inter Variable como tipografía, WCAG AA no negociable. Plan guardado en `~/.claude/plans/inicia-con-la-faze-modular-reef.md`.
|
||
|
||
- **2026-08-14 — Fase 1 completada (Backend Supabase)**.
|
||
- Escrita migración `supabase/migrations/0001_init.sql` con: schema `prestamos`; tablas `profiles, categorias, materiales, prestamos, audit_log`; función helper `prestamos.is_admin()`; trigger `prestamos_on_auth_user_created` en `auth.users` que rechaza emails no `@uabc.edu.mx` (segunda línea de defensa) y crea el perfil; triggers `prestamos_sync_stock` (ajusta `cantidad_disponible` según transiciones de estado) y `prestamos_log_estado` (registra cambios en `audit_log`); 11 policies RLS distribuidas en las 5 tablas usando `is_admin()`; seed de 6 categorías.
|
||
- Aplicada por `psql` sobre la instancia de buglabs. Verificado: 5 tablas creadas, 11 policies activas, 3 triggers propios registrados, 6 categorías seed presentes.
|
||
- Editado `~/supabase/docker/.env` en buglabs (backup previo con timestamp): `PGRST_DB_SCHEMAS` incluye ahora `prestamos`, `ADDITIONAL_REDIRECT_URLS` incluye `https://prestamos.buglabs.dev/**`.
|
||
- Recreados contenedores `supabase-rest` y `supabase-auth` con `docker compose up -d` (un simple `restart` no relee `.env`; ese fue un aprendizaje del proceso). Downtime real: ~5s.
|
||
- Corrección aplicada tras primer curl de verificación: faltaba `grant` a `service_role`. Se añadió `grant ... to service_role` + `alter default privileges` (tanto en la DB como en el archivo de migración, para que sea idempotente si se re-aplica). Verificado con `curl` que `GET /rest/v1/categorias` con `Accept-Profile: prestamos` retorna las 6 categorías.
|
||
- Estado: backend listo para consumir desde la app. Próximo paso: Fase 2 (scaffolding Astro + React + Tailwind + adapter Node + cliente Supabase SSR + middleware de auth con verificación de dominio).
|
||
|
||
- **2026-08-14 — Fase 2 completada (Scaffolding Astro)**.
|
||
- Instaladas integraciones vía `npx astro add react node tailwind --yes`. Añadido a `package.json`: `@astrojs/react`, `@astrojs/node`, `tailwindcss` (v4, sin config JS — configura en CSS con `@theme`), `react`, `react-dom`. Adaptador Node en modo `standalone`.
|
||
- Añadidas deps runtime: `@supabase/supabase-js`, `@supabase/ssr`, `@fontsource-variable/inter`.
|
||
- `astro.config.mjs`: se explicitó `output: 'server'` (sin esto, Astro intentaba prerenderizar rutas SSR y el cliente Supabase reventaba). Integraciones: React + Tailwind v4 (vía `@tailwindcss/vite`).
|
||
- `tsconfig.json`: añadido `baseUrl: '.'` + `paths: { "@/*": ["src/*"] }` para import alias.
|
||
- `src/styles/global.css`: paleta UABC como tokens Tailwind v4 (`@theme` con `--color-primary #00723F, --color-secondary #DD971A, --color-surface #F4F7F5, --color-ink #2D3748, --color-danger #C53030`), Inter Variable como `--font-sans`, componentes `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-ghost`, `.card`, `.input`, `.label`. Tap targets ≥44px. `touch-action: manipulation` y `-webkit-tap-highlight-color: transparent` en botones. `text-wrap: balance/pretty` en tipografía.
|
||
- `src/lib/supabase.ts`: helpers `serverClient(cookies)` y `browserClient()`. Ambos apuntan a `db: { schema: 'prestamos' }` para que las queries no necesiten prefijo. Cookies con `httpOnly`, `sameSite: 'lax'`, `secure` solo en prod. Usa el patrón `get/set/remove` (el `getAll/setAll` moderno de `@supabase/ssr` no funcionó con `AstroCookies` de Astro 7 — el método `.getAll()` no existe ahí).
|
||
- `src/env.d.ts`: tipos para `App.Locals` (`supabase`, `user`, `profile`) y `ImportMetaEnv` (`PUBLIC_SUPABASE_URL`, `PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `PUBLIC_APP_URL`).
|
||
- `src/middleware.ts`: instancia `serverClient`, obtiene user, verifica dominio `@uabc.edu.mx` (rechaza + redirect `/login?error=dominio`), carga `profile` desde `prestamos.profiles`, protege rutas privadas (redirect a `/login` si no hay user; 403 en `/admin/*` si no es admin), redirige a `/` si un user logueado visita `/login`.
|
||
- `src/pages/login.astro`: pantalla completa aparte, tarjeta centrada con logo, `<h1>Sistema de Préstamos</h1>`, `<a>` Google (verde primario) que arma `authorize` URL de Supabase, alerta accesible si `?error=dominio|oauth`, nota de dominio institucional al pie. Animación entrada solo detrás de `motion-safe:`.
|
||
- `src/pages/api/auth/callback.ts`: intercambia `code` por sesión, verifica dominio por segunda vez y redirige a `/` (o a `/login?error=…` según el caso).
|
||
- `src/pages/api/auth/signout.ts`: POST → `signOut` → redirect `/login`.
|
||
- `src/layouts/Layout.astro`: HTML base (`lang="es"`, `meta viewport`, `meta theme-color=#00723F`, `color-scheme: light`), incluye skip-to-content link con `focus:not-sr-only`. `title` prop.
|
||
- `src/layouts/AppLayout.astro`: sidebar `bg-primary` con logo enlazado a `/`, nav responsive (sidebar en desktop, topbar en mobile), items admin vs alumno según `rol`, botón signout desktop full + icon-only en mobile con `aria-label`.
|
||
- `src/pages/index.astro`: dashboard con tarjetas según rol (alumno: catálogo + mis préstamos; admin: solicitudes + inventario + reportes). Placeholders navegables — sus destinos se implementarán en Fases 3–6.
|
||
- `.env` local creado (obtenidas ANON y SERVICE_ROLE keys por SSH de buglabs). `.env.example` versionado con placeholders. `.env*` ya estaba en `.gitignore`.
|
||
- Borrado `src/components/Welcome.astro` y `src/assets/*` del starter, `Layout.astro` reescrito para nuestros propósitos.
|
||
- Auditoría de UI con skill `web-design-guidelines` (checklist Vercel Labs): resueltos 6 hallazgos accionables — `transition:all` reemplazado por transiciones específicas por propiedad; `role="button"` sobrante en `<a>` de Google removido; `theme-color` y `color-scheme` metas añadidos; `touch-action: manipulation` en interactivos; logo mobile con `aria-label` sobre link y letra `aria-hidden`; `text-wrap: balance/pretty` en tipografía.
|
||
- Verificación: `npm run build` sin errores. `npm run dev` (background) → `GET /` sin sesión redirige `/login` (200), `/login` renderiza copy correcto, `?error=dominio` muestra la alerta esperada.
|
||
- Aprendizajes registrados: (a) Tailwind v4 requiere `@reference "tailwindcss"` dentro de `<style>` scoped de `.astro` para usar `@apply`; (b) `output: 'server'` es obligatorio en `astro.config.mjs` incluso con adaptador Node — no se infiere; (c) `AstroCookies.getAll()` no existe en Astro 7, usar `get(name)`.
|
||
- Estado: base lista. Fases 3–6 pueden arrancar en paralelo con agentes ahora que hay: middleware con sesión + `profile` en `Astro.locals`, cliente Supabase apuntando al schema `prestamos`, layout con nav responsive, tokens de diseño Tailwind y clases utilitarias `.btn/.card/.input`.
|
||
|
||
- **2026-08-14 — Fases 3, 4, 5, 6, 7 completadas en paralelo (5 agentes)**.
|
||
- Se lanzaron 5 subagentes simultáneos en un solo turno con contratos aislados por carpeta (sin colisión de archivos). Todos terminaron el core aunque tres (Fases 4, 5, 6) marcaron `failed` al final por límite de sesión durante refinamientos post-implementación; el orquestador consolidó y validó que todo compila.
|
||
- **Fase 3 (Alumno)** — 5 archivos: `src/pages/alumno/{catalogo,mis-prestamos}.astro`, `src/pages/api/prestamos/index.ts`, `src/components/alumno/{SolicitarModal,FiltroCategorias}.tsx`. Mobile-first. Catálogo agrupa por categoría en SSR, modal `<dialog>` nativo para solicitar, filtro por chips que sincroniza `?cat=` en URL. Mis-préstamos con secciones "Activos" (pendiente/aprobado/activo) y "Historial" en `<details>`. Endpoint POST valida stock antes de insertar.
|
||
- **Fase 4 (Admin solicitudes)** — 11 archivos: `src/pages/admin/index.astro` (panel con KPIs), `src/pages/admin/solicitudes/{index,activos,historial}.astro`, `src/components/admin/solicitudes/{AccionesSolicitud,MarcarDevuelto,VerDetalles}.tsx`, `src/pages/api/admin/prestamos/[id]/{aprobar,rechazar,devolver,detalles}.ts`. Desktop-first tabla / mobile cards. Flujo simplificado: `pendiente → aprobado` (que también significa activo) → `devuelto`. El estado `activo` del enum queda reservado. Los endpoints usan `.eq('estado', 'pendiente')` como guard anti doble-click; los triggers de DB manejan stock y audit_log.
|
||
- **Fase 5 (Admin inventario)** — 8 archivos: `src/pages/admin/inventario/{index,categorias}.astro`, `src/components/admin/inventario/{MaterialForm,EliminarMaterial,CategoriaForm,EliminarCategoria}.tsx`, `src/pages/api/admin/{materiales,categorias}/[index,\[id\]].ts`. Filtros SSR vía query params. Edición de `cantidad_total` valida que no quede debajo de lo prestado. Manejo de FK violations (23503) con mensaje útil.
|
||
- **Fase 6 (Reportes)** — 6 archivos: `src/pages/admin/reportes.astro` (tabs Historial/Vencidos con filtros por rango de fecha/estado/material/alumno), `src/components/admin/reportes/{Autocomplete,AutocompleteMaterial,AutocompleteAlumno}.tsx` (patrón combobox con teclado y aria-*, se extrajo helper compartido `Autocomplete.tsx`), `src/pages/api/admin/reportes/{materiales,alumnos,export}.ts`. Export CSV nativo (sin librería) con BOM UTF-8, escape de `,"` y `\n`, fechas ISO. Marca `// ponytail: LIMIT 500, paginar cuando pase de eso`.
|
||
- **Fase 7 (Deploy)** — 5 archivos: `Dockerfile` (multi-stage node:22-alpine, 3 stages, healthcheck vía `node -e http.get('/login')`, USER node), `.dockerignore`, `docker-compose.yml` (puerto `127.0.0.1:8082:4321`, log rotation 10MB×3), `.env.production.example`, `deploy/README.md` con pasos de primer deploy + update + rollback + snippet Cloudflare tunnel. Sin nginx delante, sin pm2. Tamaño de imagen esperado ~180-220 MB.
|
||
- **Correcciones post-agentes**: (a) botón "Cerrar sesión" del AppLayout usaba `transition` (all) — cambiado a `transition-colors`. (b) `AutocompleteMaterial` y `AutocompleteAlumno` originalmente tenían `<span class="label">` externo sin asociación al input; el agente 6 detectó la regresión y consolidó en `Autocomplete.tsx` con `<label htmlFor>` propio.
|
||
- **Verificación end-to-end en dev**: `npm run build` pasa limpio. Smoke test sin sesión sobre 14 rutas (páginas + API): `/login` → 200; todas las demás → 302 a `/login`. Middleware protege correctamente `/alumno/*`, `/admin/*` y `/api/*`.
|
||
- Total: **35 archivos nuevos** (25 páginas/endpoints + 10 componentes React + 5 archivos de deploy). Cero librerías nuevas más allá de lo que quedó en Fase 2. Todos los modales son `<dialog>` HTML nativo.
|
||
- **Bugs/hallazgos secundarios** detectados por los agentes (registrados para atender después, no bloqueantes): (i) el middleware ejecuta `getUser()` + fetch de perfil en cada request incluyendo `/api/*`, sin cache — bajo carga alta convendría memoizar por request. (ii) Si `Astro.locals.user` es null, el error de `.id` en el endpoint podría reventar; los endpoints actuales lo cubren con early return pero conviene revisar en la fase de QA.
|
||
- Estado: código completo. Falta solo Fase 8 (deploy real al servidor + QA end-to-end con login real). Pendiente confirmación del usuario para arrancar el deploy.
|
||
|
||
- **2026-08-14 — Fase 8 (Deploy) completada. https://prestamos.buglabs.dev vive.**
|
||
- Código publicado en `git.buglabs.dev/LakG/labre-web` (repo privado). Push HTTPS con token efímero generado vía `docker exec -u git gitea gitea admin user generate-access-token` — el token quedó en Gitea para poderse revocar (`Settings → Applications`), nunca vivió en el `remote` local.
|
||
- DNS: CNAME `prestamos.buglabs.dev → 3de17b3c-....cfargotunnel.com` (proxied) creado desde el dashboard de Cloudflare.
|
||
- En `buglabs`: clonado a `~/labre-web/`, `.env.production` (chmod 600) con las 4 vars reales, `docker compose up -d --build`. Imagen `labre-web-labre-web:latest` construida en ~90s.
|
||
- Cloudflared: usuario añadió la regla `- hostname: prestamos.buglabs.dev / service: http://localhost:8082` antes del `http_status:404` en `/etc/cloudflared/config.yml` y reinició el servicio con `sudo systemctl restart cloudflared`.
|
||
- **Bug encontrado y arreglado en el deploy**: en el primer build el contenedor daba 500 con "Your project's URL and Key are required". Causa: Vite hornea `import.meta.env.PUBLIC_*` en build time, pero el Dockerfile intencionalmente no expone `.env.production` durante `docker build` (secretos no viajan a layers de imagen). Fix en un commit: `src/lib/supabase.ts` y `src/pages/login.astro` leen ahora de `process.env.PUBLIC_*` con fallback a `import.meta.env.*` para dev; en runtime las vars llegan por `env_file` de compose. El `browserClient` (que no lo usa nadie hoy) se mantiene con `import.meta.env` — si en el futuro un island lo importa habrá que pasarlas como `ARG` en el build stage.
|
||
- Verificación end-to-end pública:
|
||
- `dig prestamos.buglabs.dev` → IPs de Cloudflare edge.
|
||
- `curl -sI https://prestamos.buglabs.dev/login` → `HTTP/2 200`, `cf-cache-status: DYNAMIC`.
|
||
- `GET /` sin cookies → 302 a `/login`.
|
||
- `GET /api/prestamos` sin cookies → 302 (middleware protegiendo API).
|
||
- Estado final: **sistema en producción**. Falta el paso operativo humano: (a) hacer un primer login real con cuenta `@uabc.edu.mx` para crear el registro en `prestamos.profiles`; (b) promoverse manualmente a admin desde Supabase Studio o `psql`:
|
||
```sql
|
||
update prestamos.profiles set rol = 'admin' where email = '<tu-correo>@uabc.edu.mx';
|
||
```
|
||
Después, ese usuario puede empezar a cargar categorías/materiales por la UI y arrancar operación real.
|
||
|
||
- **2026-08-15 — Iteración UX post-lanzamiento (en producción)**.
|
||
- **Motivo del préstamo**: campo del modal de solicitud renombrado a "Motivo del préstamo" y hecho requerido con hint "¿Para qué clase o proyecto?". Se muestra ahora en las tres vistas admin (bandeja pendientes, activos, historial) tanto en tabla desktop como tarjetas mobile.
|
||
- **Nav mobile → dock píldora flotante**: reemplazado el topbar horizontal por un dock centrado abajo, íconos SVG inline (heroicons outline) con estilo pill + `backdrop-blur`, respeta `env(safe-area-inset-bottom)`. Item activo con fondo verde primario. Cerrar sesión al final separado por divisor. Alumno: Home/Grid/List/LogOut. Admin: Dashboard/Inbox/Stats/Chart/LogOut.
|
||
- **Sidebar desktop colapsable**: botón hamburguesa arriba del contenido (visible solo desktop) alterna la sidebar con `translate-x-full` y transición de 200ms. Estado persistido en `localStorage`, aplicado sin flash mediante `<script is:inline>` bloqueante en `<head>`.
|
||
- **Toaster estilo Sileo**: componente React global (`src/components/Toaster.tsx`, `client:idle`) montado una vez en el `AppLayout`. Toasts con `backdrop-blur-xl`, `bg-white/85`, icono circular a color según kind (success primary, error danger, info secondary), animación `translateY + scale + opacity` con `cubic-bezier(.22,1,.36,1)`, auto-dismiss 3.8s, click para cerrar, respeta `prefers-reduced-motion`. Helper `toast()` dispara un `CustomEvent('labre:toast')`. Helper adicional `toastAfterReload()` deja el toast en `sessionStorage` para que sobreviva al `location.reload()` post-acción (el Toaster drena la cola al montar). Cableado en `SolicitarModal`, `AccionesSolicitud` (aprobar/rechazar) y `MarcarDevuelto`.
|
||
- **Panel de Estadísticas**: la ruta `/admin/inventario` cambió de "Inventario" a "Estadísticas" (label + ícono `stats` en nav). Agregada arriba una fila de KPIs con 6 tarjetas: solicitadas hoy, aprobadas hoy, rechazadas hoy, devoluciones hoy, en préstamo (total), vencidos (total, resaltado en rojo si > 0). Los cambios de estado se cuentan desde `prestamos.audit_log` filtrando por `estado_nuevo` + rango del día; los totales cuentan directo sobre `prestamos.prestamos`. Las 6 queries corren en `Promise.all`. El rango del día se computa server-side con `todayMX()` (nuevo `src/lib/date.ts`) que usa `Intl.DateTimeFormat` con `timeZone: 'America/Tijuana'` para leer año/mes/día + `longOffset` para obtener el offset actual (con DST). El inventario CRUD original queda intacto debajo.
|
||
- **Diagnóstico**: `/api/whoami` devuelve `{user, profile}` de la sesión actual (útil para probar cookies desde el navegador). Endpoint `/api/admin/prestamos/[id]/devolver` retorna ahora un objeto `debug` con `email` y `profile_rol` cuando devuelve 403 (temporal, para trazar un bug reportado por el usuario en el que "Marcar devuelto" devolvía 403; pendiente confirmar la causa con datos reales de prod).
|
||
- **Preview local vía Cloudflare tunnel efímero**: se probó con `cloudflared tunnel --url http://localhost:4321` + flag temporal `?preview=alumno|admin` en el middleware (que impersonaba con service_role bajo `import.meta.env.DEV`). El flag se removió antes del deploy — el middleware en prod ya no lo lee. También se removió el `server.allowedHosts` del `astro.config.mjs` que se había abierto solo para el túnel.
|
||
- **Deploy**: `git push` → `ssh buglabs "cd ~/labre-web && git pull && docker compose up -d --build"`. Container `labre-web` recreado, healthy en ~15s. Smoke test público: `/login` → 200, `/` → 302, `/api/prestamos` → 302. Downtime real observado: ninguno perceptible.
|
||
- Pendiente: usuario debe visitar `https://prestamos.buglabs.dev/api/whoami` en prod y compartir el JSON (o pegar el objeto `debug` que aparece en la respuesta del "Marcar devuelto") para diagnosticar el 403 residual.
|
||
|
||
- **2026-08-16 — Bug del 403 resuelto: era Astro `security.checkOrigin`**.
|
||
- Causa raíz: Astro 5+ activa por default `security.checkOrigin`, que compara `Origin` con `Host` en peticiones POST/PATCH/PUT/DELETE. Detrás de Cloudflare Tunnel esos headers no matchean literalmente, así que Astro corta con 403 y texto plano `"Cross-site POST form submissions are forbidden"` — nunca llega al handler. Por eso los `console.log` del endpoint no salían y `debug` del body no aparecía: Astro respondía antes.
|
||
- Fix: `security: { checkOrigin: false }` en `astro.config.mjs`. Los endpoints ya están protegidos por cookies `HttpOnly` + `SameSite=Lax` y flow OAuth con PKCE, así que la protección CSRF de Astro es redundante y solo estorba en este stack.
|
||
- Confirmado en prod por el usuario: "Marcar devuelto" ahora funciona. El mismo fix cubre aprobar/rechazar y cualquier otro POST admin que hubiera fallado con el mismo síntoma.
|
||
- Limpieza: removido `/api/whoami` (endpoint diagnóstico) y el bloque `debug` que devolvía email+rol en el 403 del endpoint devolver. Ya no aplican.
|
||
- Aprendizaje: cuando un endpoint responde 403 con **texto plano** ("Cross-site POST..."), es Astro; cuando responde 403 con **JSON** (`{"error":"no autorizado"}`), es lógica del endpoint. Ese matiz habría acelerado el diagnóstico.
|