Files
labre-web/AGENTS.md
T

150 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 36.
- `.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 36 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.