Rol docente:
- profiles.rol acepta 'alumno'|'docente'|'admin' (migración 0006)
- Docente en checkout: no elige maestro; RPC usa su propio nombre como maestro_responsable
- Perfil/onboarding condicionales por rol: docente sin semestre ni tutor, label matrícula → "Número de empleado"
- catalogo.astro: perfilCompleto para docente solo requiere matrícula
Combobox maestros + separar tutores:
- maestros.es_tutor bool: subset marcado que aparece en el select de tutor del perfil
- MaestroForm: nuevo checkbox "Es tutor"; endpoints POST/PATCH aceptan es_tutor
- admin/maestros.astro: columna Tutor con SVG check
- perfil/onboarding queries filtran es_tutor=true, activo=true
- Checkout SolicitudCart: <select> combobox de maestros activos (reemplaza textarea);
option vacía usa el tutor por defecto; endpoint recibe maestro_id numeric
Fecha default 1 día:
- AccionesSolicitud: enDias(7) → enDias(1) por default al aprobar
Sonido notificación:
- Movido src/audio/sonido_notificacion.mp3 → public/audio/
- BadgeSolicitudes: new Audio('/audio/sonido_notificacion.mp3').play() en INSERT
Issues:
- Límite motivo 250 chars (era 500) en checkout + backend
- break-words en Maestro/Motivo de las 3 vistas admin y VerDetalles
El trigger prestamos_on_auth_user_created coexiste con otro trigger del
proyecto vecino en auth.users y no siempre dispara. Consecuencia:
usuarios que hacían login vía OAuth entraban con user pero sin profile,
y la app los trataba como "no logueado".
- Middleware: si hay user y no hay profile (email @uabc.edu.mx), upsert
del profile con datos del OAuth (nombre desde user_metadata.full_name).
- Migración 0005: policy RLS profiles_insert_self con check
(id=auth.uid() and rol='alumno') — impide escalación a admin desde el
self-heal.
- Backfill manual ejecutado en prod para los 8 users @uabc.edu.mx que
ya existían en auth.users pero no tenían profile.
Perfil de usuario:
- /perfil con avatar, matrícula, semestre, tutor, foto de perfil
- /onboarding opcional (skippeable) para primer login
- Avatar con prioridad: foto propia > Google OAuth > iniciales sobre color HSL
- Endpoint PATCH /api/profile con validación y guard self-only
- Middleware carga semestre, tutor_id, foto_path
- Sidebar footer con Avatar + link a perfil
CRUD admin de maestros:
- /admin/maestros con tabla desktop / cards mobile
- MaestroForm + EliminarMaestro (mismo patrón que categorias)
- Endpoints POST/PATCH/DELETE con guard admin + 23503 traducido
- Subnav de inventario ahora incluye tab Maestros
Home reformulado:
- Alumno: saludo por hora + CTA "¿Qué vas a pedir hoy?" + card de último préstamo activo
- Admin: saludo + 2 KPIs de alerta (Pendientes, Vencidos) + últimas 2 pendientes + actividad reciente (tabla desktop, cards mobile — fix del bug 6)
- Helper saludoHora() y fmtFechaRelativa() en date.ts con offset MX
Checkout con tutor auto:
- SolicitudCart: input maestro → textarea con placeholder = nombre del tutor
- Guard perfil incompleto: CTA "Completa tu perfil" si !matricula || !tutor_id
- RPC crear_solicitud: fallback automático al nombre del tutor si textarea vacío
Fixes:
- Bug upload imagen: Dockerfile ARG + docker-compose build.args pasan PUBLIC_* al bundle client de Vite
- Grid inventario mobile: acciones en grid 2-col con prop compact en botones
- UnidadesManager mobile: tabla reemplazada por cards, select estado con min-width 130px
- Chart donut: Legend horizontal debajo (sin labels internos cortadas)
- Chart barras: YAxis width fijo 100 + tickFormatter que trunca >14 chars
- Panel admin mobile: tabla ahora tiene versión cards mobile (parte del rediseño home)
Buscador catálogo alumno:
- BuscadorCatalogo.tsx con debounce 120ms, normaliza tildes, ?q= en URL
- FiltroCategorias usa data-* attributes, coordinación via window.__labreFiltrar
- Cards con data-nombre y data-numero-inventario para filtro combinable AND
Grid + imágenes + unidades individuales (admin):
- Inventario ahora es grid 2/3/4/5 col con fotos aspect-square
- MaterialForm: input file con preview, checkbox trackeado_por_unidad
- UnidadesManager: agregar/editar/eliminar unidades por material
- Reasignar unidad desde detalles de solicitud (aprobada/activa)
- Alumno ve etiqueta de unidad asignada en mis-préstamos
- Helper materialImg.imgUrl(path) para URL pública del bucket
- Auto-submit debounced del filtro de inventario (input + selects)
Estadísticas separada:
- Nueva ruta /admin/estadisticas con 6 KPIs (movidos de /admin/inventario)
- 3 gráficas recharts: top 10 materiales, solicitudes/día, distribución estados
- Chart.tsx wrapper con paleta UABC + respeta prefers-reduced-motion
- Nav admin: Panel · Solicitudes · Inventario (box) · Estadísticas (stats) · Reportes
- admin/index.astro usa todayMX() (fix inconsistencia)
Notificaciones pendientes (admin):
- Badge SSR con conteo de pendientes sobre ícono Solicitudes (sidebar + dock)
- BadgeSolicitudes.tsx: canal Realtime en prestamos.solicitudes, +1 en INSERT
con toast, refetch en UPDATE (replica identity default no trae old.estado)
- Recharts añadido a package.json
El vale físico del LSC permite pedir varios materiales en un solo
trámite; el sistema modelaba 1 solicitud = 1 material. Migra
prestamos.prestamos -> solicitudes (cabecera) + solicitud_items
(renglones), vía RENAME + backfill (preserva ids/historial real).
- RPC prestamos.crear_solicitud: transaccional, lockea materiales
por fila, arregla la race condition del insert directo anterior.
El alumno ya no inserta directo (RLS lo bloquea).
- sync_stock/log_estado reescritos para iterar renglones por vale.
- maestro_responsable (por vale) y profiles.semestre (perfil,
nullable, sin UI todavía — onboarding queda para después).
- Alumno: carrito (SolicitudCart/AgregarMaterial) reemplaza el
modal de solicitud único por material.
- Admin: 3 vistas de solicitudes, reportes y CSV export listan
renglones por vale; api/admin/prestamos -> api/admin/solicitudes.
De paso corrige un bug preexistente en detalles.ts (audit_log
nunca se ordenaba por la columna correcta).
Verificado: build limpio, ciclo RPC+triggers probado en una
transacción revertida en prod (sin residuo), 9 vistas SSR
smoke-testeadas contra el schema real.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
El verde UABC (#00723F) es demasiado oscuro para texto carbón (~2:1,
falla AA). Corrige BrandMark, nav activo (sidebar/dock), chip de
FiltroCategorias y botón de Google en login a texto/ícono blanco
(~6.2:1). Actualiza DESIGN.md/PRODUCT.md y design.json acorde.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Rediseño hecho en sesión aparte con el skill impeccable. Se registran
las especificaciones y ajustes visuales al sistema ya en producción.
Añade:
- BrandMark.astro con el escudo/logo del laboratorio.
- Favicon actualizado (svg + ico).
- Páginas propias 403 y 404 consistentes con el sistema visual.
- DESIGN.md y PRODUCT.md documentando decisiones.
- .impeccable/design.json con el design system tokenizado.
Ajusta componentes existentes (modales, autocomplete, layout, login,
solicitudes, mis-préstamos, middleware) para alinearse a los tokens
del design system.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
Astro por default rechaza POST con "Cross-site POST form submissions
are forbidden" cuando el header Host y el Origin no matchean
literalmente. Detrás de Cloudflare Tunnel esto pasa siempre porque
Host llega como el hostname público pero Origin trae la URL edge.
Los endpoints ya están protegidos por cookies HttpOnly + SameSite=Lax
y OAuth PKCE, así que CSRF no aplica. Se desactiva checkOrigin.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Alumno:
- Modal de solicitud: campo "Notas" renombrado a "Motivo del préstamo",
ahora requerido, con hint sobre uso (para qué clase o proyecto).
Admin:
- Bandeja de solicitudes pendientes, activos y devoluciones ahora
muestran el motivo del alumno (en tabla desktop y tarjetas móvil).
- /admin/inventario se convirtió en "Estadísticas": grid de KPIs del
día (solicitadas, aprobadas, rechazadas, devoluciones, en préstamo,
vencidos) arriba del inventario existente. Los cambios de estado se
cuentan desde audit_log, el rango del día se calcula automáticamente
en timezone America/Tijuana con DST via Intl API (src/lib/date.ts).
Rediseño móvil:
- Sidebar desktop se puede colapsar con botón hamburguesa. Estado
persiste en localStorage sin flash (script is:inline bloqueante).
- Nav móvil reemplazado por dock píldora flotante con iconos SVG
inline (heroicons outline). Respeta safe-area-inset-bottom.
Notificaciones estilo Sileo:
- Componente Toaster global (React island client:idle) que escucha
CustomEvents. Toasts con backdrop-blur, animación de entrada
translateY+opacity, auto-dismiss 3.8s, click para cerrar. Helper
toastAfterReload deja pendiente en sessionStorage para sobrevivir
location.reload() post-acción.
Diagnóstico:
- /api/whoami devuelve user+profile actuales para depurar sesión.
- Endpoint devolver retorna un objeto `debug` en el 403 con
email+rol vistos por el server (temporal, para trazar bug del 403).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sin esto el middleware redirige a /login antes de entrar al handler,
haciendo que el botón "Continuar con Google" cicle contra el propio
login sin llegar nunca a Google.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
El flow OAuth de Supabase (@supabase/ssr) es PKCE por default: requiere
guardar un code_verifier en cookies del cliente ANTES de mandar al user
a Google. El login.astro anterior linkeaba directo a
supabase.buglabs.dev/auth/v1/authorize saltándose ese paso, así que el
exchangeCodeForSession del callback fallaba sin cookies válidas.
Ahora el botón "Continuar con Google" apunta a /api/auth/signin, que
usa supabase.auth.signInWithOAuth() — este método sí setea el
code_verifier en las cookies del dominio de la app, y después el
callback lo recupera para completar el intercambio.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Vite hornea import.meta.env.PUBLIC_* en build time, pero el Dockerfile
no tiene .env.production disponible durante `docker build` (correcto,
por seguridad). Cambia el server client y la página de login para leer
las vars desde process.env, que sí llega vía env_file en runtime.
El browserClient no lo usa ningún island — se mantiene con
import.meta.env para el día que se necesite.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sistema web para gestión de préstamos de material del Laboratorio de
Sistemas Computacionales de la UABC.
- Backend: Supabase self-hosted, schema aislado `prestamos` con RLS,
triggers de stock y audit log (supabase/migrations/0001_init.sql).
- Auth: Google OAuth restringido a @uabc.edu.mx, verificado en
middleware y como segunda línea en trigger de DB.
- Frontend: Astro 7 (SSR con adapter Node) + React islands + Tailwind
v4 con paleta UABC (primary #00723F, secondary #DD971A) bajo regla
60/30/10.
- Interfaz alumno mobile-first: catálogo con filtro por categorías,
solicitud de préstamos, historial personal.
- Interfaz admin desktop-first: panel con KPIs, bandeja de solicitudes
(aprobar/rechazar/devolver), CRUD de inventario y categorías,
reportes filtrables con export CSV nativo.
- Modales con `<dialog>` nativo, cero librerías de UI adicionales.
- Deploy: Dockerfile multi-stage node:22-alpine + docker-compose para
publicar bajo prestamos.buglabs.dev vía Cloudflare Tunnel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>