0247b979ca
Segunda iteración del rediseño (sesión separada con skill impeccable). Toca prácticamente toda la UI: layout base, tokens (global.css), todas las páginas de alumno y admin, componentes de solicitudes, inventario, catálogo, filtros, toaster y modales. Docs y sistema: - DESIGN.md y PRODUCT.md actualizados con la nueva iteración. - .impeccable/design.json refleja tokens vigentes. - .impeccable/review/ contiene screenshots de la revisión visual. Dependencias: - Añade @fontsource-variable/jetbrains-mono (fuente monoespaciada). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
190 lines
34 KiB
Markdown
190 lines
34 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.
|
||
|
||
- **2026-08-16 — Auditoría `/impeccable`: "quita el sloop de IA y reforma el diseño"**.
|
||
- Se generaron `PRODUCT.md` y `DESIGN.md` (skill impeccable) documentando el sistema de diseño ya implementado ("La Bitácora Digital"). Veredicto tras leer todo el código y capturar screenshots reales (login, catálogo, panel admin, solicitudes, inventario, reportes, mobile) vía un bypass temporal de middleware solo-DEV (`?preview=alumno|admin`, revertido al terminar): el sistema **no** tenía slop visual genérico típico de IA (sin gradientes, sin glassmorphism, sin morado/azul SaaS, sin emojis) — el trabajo previo ya era disciplinado. El slop real encontrado era puntual:
|
||
1. El favicon era el cohete default de Astro (boilerplate del scaffold, nunca reemplazado).
|
||
2. El "logo" era un cuadrado con la letra "L" — el placeholder más genérico posible, repetido en sidebar, header mobile y login sin ningún glifo propio detrás.
|
||
3. `/admin` para no-admins devolvía texto plano sin estilo (`new Response('Acceso denegado', {status:403})`), rompiendo el sistema de diseño.
|
||
4. No existía página 404 propia (caía al default de Astro).
|
||
5. Íconos "fake": los botones de cerrar modal usaban el carácter `×`, el estado vacío de solicitudes usaba `✓`, el chevron de historial usaba `›` — inconsistente con el resto del sistema que sí dibuja íconos SVG reales (heroicons-outline).
|
||
6. Código muerto/inconsistente: `EliminarMaterial` y `RechazarDialog` cambiaban color en hover vía `onMouseEnter/onMouseLeave` en JS (con una custom property `--h` sin usar) en vez de CSS, y el dropdown de `Autocomplete` usaba `shadow-lg` de Tailwind en vez de respetar la regla "sin sombra fuera de dock/toast" del sistema.
|
||
- Fix aplicado: nuevo glifo de marca propio (`src/components/BrandMark.astro`) — una ficha/etiqueta con perforación, evoca el ticket de préstamo físico — reemplaza la "L" en sidebar/header/login y es ahora también el favicon (`public/favicon.svg` + `.ico` regenerado). Es un mark de producto, no un intento de escudo institucional UABC (ese sigue pendiente y sin fabricar, por instrucción explícita). Se unificó el color del chip del mark (antes dorado en nav vs. verde en login) a wash blanco/verde consistente. Se agregaron `src/pages/403.astro` y `src/pages/404.astro` con el mismo lenguaje visual del login; el middleware ahora usa `context.rewrite('/403')` en vez de devolver texto plano. Los `×`/`✓`/`›` se reemplazaron por SVG inline consistentes con el stroke-style existente. Se limpió el hover JS→CSS y el `shadow-lg` del autocomplete.
|
||
- Documentación actualizada: `PRODUCT.md` (Brand Commitments ya no dice "100% tipográfica sin logo"; aclara que el BrandMark es mark de producto, no escudo oficial) y `DESIGN.md` (nueva subsección "Brand Mark" en Components, nueva regla Do/Don't: "todo ícono es SVG dibujado, nunca un carácter unicode").
|
||
- Verificación: `npm run build` limpio, detector mecánico de impeccable (`detect.mjs`) sin hallazgos sobre los 15 archivos tocados, screenshots antes/después confirmando visualmente cada fix (incluyendo hover de "Eliminar" y el toggle del historial).
|
||
- No se tocó IA de información (paneles Panel vs. Estadísticas se solapan en KPIs pero no se reestructuraron — fuera de alcance de "quitar slop"), ni se generó contenido de relleno para las páginas con poco dato real (1 material de seed) — eso es estado temprano de datos, no slop de diseño.
|
||
- Estado: cambios visuales completos y verificados. Servidor de dev quedó corriendo en background (`astro dev status`/`astro dev logs`/`astro dev stop` para gestionarlo). Pendiente: deploy a producción cuando el usuario lo confirme.
|
||
|
||
- **2026-08-16 — Deploy del rediseño impeccable a producción**. Confirmado por el usuario. `git push` + `ssh buglabs 'cd ~/labre-web && git pull && docker compose up -d --build'`. Container recreado (healthy en ~8s), sin downtime perceptible. Smoke test: `/login` → 200, `/` → 302 (redirige por middleware sin sesión). Cambios ya vivos en https://prestamos.buglabs.dev.
|
||
|
||
- **2026-08-16 — Reemplazo total de identidad visual: MotherDuck (`/impeccable`)**. El usuario pegó el `DESIGN.md` completo de MotherDuck ("crayon-coded terminal on cream paper", neobrutalista) y pidió que este proyecto adoptara ese estilo. Se trató como redesign de mundo visual completo (no refinamiento) siguiendo `reference/new-work.md` del skill impeccable: el brief venía 100% fijado (colores, tipografía, sombra, radios, componentes documentados), así que se saltó el proceso de generación de conceptos/dice-roll ("el brief gana" — instrucción explícita del skill cuando hay un mundo visual ya pinneado) y se fue directo a comprometer el mundo, construir, e inspeccionar. Sin generación de imágenes disponible en este entorno → build "code-led" (sin comps).
|
||
- Contrato de dirección grabado como comentario HTML al inicio de `<body>` en `Layout.astro` (THESIS/OWN-WORLD/STORY/FIRST VIEWPORT/FORM/FINISH), verificado que sobrevive en el build de producción (`dist/`).
|
||
- **Reasignación semántica de la paleta** (la traducción real del trabajo — MotherDuck es un sitio de marketing sin estados de status; este es un panel operativo que sí los necesita):
|
||
- Verde institucional → **Sky Crayon `#6fc2ff`**, único color de acción rellena (botones primarios, nav activo).
|
||
- Dorado institucional → **Duck Bill Orange `#ff9538`**, acento disperso (antes reservado a "ilustración/marquee" en el brief original, reutilizado aquí porque la app no tiene ninguna de las dos cosas).
|
||
- Rojo de error → **Coral Sketch**, mismo problema que el sistema anterior con el verde: el fill claro (`#f38e84`) falla contraste AA como texto, así que se separó en `--color-danger` (fill/borde) y `--color-danger-text` (`#a83224`, oscurecido) — fix aplicado con sed dirigido en los 14 archivos que usaban `color: var(--color-danger)` como texto, verificado que no tocó ningún `border-color`/`background` de paso.
|
||
- **Mint Sketch `#38c1b0`** se sacó del set decorativo "rainbow" del brief original y se fijó como color de status "positivo" (disponible/aprobado/devuelto) — el brief de MotherDuck nunca necesitó esto porque no tiene estados de negocio.
|
||
- **Regla del texto siempre carbón**: ningún fill o borde de color lleva texto blanco o del mismo color — siempre `--color-ink` (`#383838`). Se auditó y corrigió cada instancia de `color: white`/`text-white` en botones de eliminar/rechazar y el chip activo de filtro de categorías.
|
||
- **Radio 2px en todo el sistema**: override de `--radius-lg/2xl/3xl` a `2px` en el `@theme` de Tailwind (cascada automática a casi todo el árbol) + `sed` global de `rounded-full` → `rounded-[2px]` en 8 archivos (dock móvil, toaster, badges de estado). Cero píldoras/círculos sobrevivieron.
|
||
- **Sombra dura única** `-6px 6px 0 0 var(--color-ink)` con interacción "press" (`translate(-6px,6px)` en hover, vuelve a su posición en active a mitad de camino) aplicada vía clases compartidas nuevas en `global.css` (`.btn`, `.card-raised`, `.card-link`) — cero cambios por archivo necesarios en la mayoría de botones/links porque ya usaban esas clases compartidas. Los 9 `<dialog>` nativos (modales) sí se tocaron uno por uno vía `sed` para añadir borde + sombra ya que no usaban una clase compartida.
|
||
- **Reskin manual** (no cubierto por cascada de tokens): `AppLayout.astro` (sidebar pasó de panel verde sólido a panel blanco con borde carbón; dock móvil pasó de píldora flotante con blur a barra inferior plana sin sombra, fiel a la regla del brief "nav buttons are flat"), `Toaster.tsx` (quitado `backdrop-blur-xl`/`bg-white/85`/sombra ambiental → borde + sombra dura), `login.astro`/`403.astro`/`404.astro` (headline uppercase, mismo tratamiento de card elevada), favicon (recoloreado carbón-sobre-crema, regenerado `.ico` vía chromium headless + imagemagick).
|
||
- **Nuevo `BrandMark`**: mismo glifo de ficha/etiqueta de la sesión anterior, sin cambios de forma — solo de color (antes verde/dorado institucional, ahora carbón-sobre-azul-cielo o wash blanco translúcido sobre panel blanco).
|
||
- Verificación: `npm run build` limpio; detector mecánico (`detect.mjs`) sin hallazgos sobre los 27 archivos tocados; ronda de screenshots batched (desktop 1440px + mobile 390px) sobre 11 páginas vía bypass temporal de middleware solo-DEV (mismo patrón de sesiones anteriores, revertido al terminar) — sin defectos materiales, no hizo falta una segunda ronda de fixes.
|
||
- **Revisión de cierre y documentación**: sin subagentes `impeccable-finish-reviewer`/`impeccable-documenter` instalados en este entorno, ambos pases corrieron in-thread siguiendo `reference/degraded/finish-reviewer.md` y `reference/degraded/documenter.md` — disclosure explícito de la sustitución. `DESIGN.md` reescrito completo desde el mundo construido (ya no describe "La Bitácora Digital" verde institucional). `PRODUCT.md` → Brand Commitments actualizado con nota de reemplazo fechada y los valores activos.
|
||
- **Deliberadamente NO tocado**: arquitectura de layout (sidebar desktop + dock mobile se mantienen, solo cambió su piel), copy/contenido, lógica de negocio, el escudo oficial UABC (sigue sin fabricarse, por instrucción explícita en `PRODUCT.md`).
|
||
- Estado: cambios completos y verificados localmente, **sin commitear ni desplegar** — el usuario ya tiene un deploy previo (UABC verde) en producción; este reemplazo de identidad es mucho más grande y espera confirmación explícita antes de `git push` + deploy.
|