Plataforma web para VIMET — Nutrición y entrenamiento especializado en alteraciones metabólicas (Córdoba, Argentina).
Stack: Next.js 14 · TypeScript · Tailwind CSS · Shadcn/UI · Supabase · Resend · Vercel
npm install
cp .env.example .env.local # completar variables
npm run dev| Comando | Descripción |
|---|---|
npm run dev | Dev server (localhost:3000) |
npm run build | Build producción |
npm start | Serve producción |
npx playwright test | Tests E2E |
Ver .env.example para el listado completo. Obligatorias:
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
RESEND_API_KEY
RESEND_FROM_EMAIL
COMPANY_EMAIL
NEXT_PUBLIC_SITE_URL
Vercel + Supabase. Configurar las variables de entorno en el panel de Vercel.
| Versión | Fecha | Cambios |
|---|---|---|
| v0.20.1 | 2026-08-15 | Fix de seguridad: vulnerabilidad alta de nanoid (Dependabot #31, CVE-2026-67213, loop infinito en customAlphabet/customRandom con size=0) — llegaba transitivo vía postcss (Tailwind), resuelto con npm audit fix dentro del rango semver ya declarado en package.json (solo lockfile, sin tocar versiones). npm audit → 0 vulnerabilidades. De paso, el mensaje de "Nueva medición" cuando no se carga ningún dato pasa de "Cargá al menos un dato de la medición." a "Cargá al menos una ficha completa (Medidas básicas o Datos ISAK)." — el mensaje anterior no reflejaba que la validación exige un grupo entero, no un campo suelto. |
| v0.20.0 | 2026-08-15 | Rediseño de los 3 gráficos de antropometría, contrastado contra un PDF real de ISAKmetry que compartió la nutricionista. El gráfico de "Adiposidad" (línea de los 8 pliegues) se cortaba abajo: las etiquetas del eje X estaban rotadas -35° y el viewBox del SVG no tenía margen para esa rotación (aparecían truncadas: "Tríceps" como "eeps", "Subescapular" como "ular"). El PDF de referencia confirma que esas etiquetas van horizontales sin rotar — se saca la rotación (con wrap a 2 líneas para labels con espacio, ej. "Cresta ilíaca") en vez de agrandar el padding, que era un parche sobre la causa real. De paso, rediseño visual de los 3 gráficos (category-line-chart.tsx, bar-chart.tsx, evolution-chart.tsx): grilla sólida hairline en vez de punteada, relleno degradado + tooltip on hover/touch en el gráfico de línea, barras capadas a 28px con esquina redondeada solo arriba (antes ocupaban 50% del slot y se veían gruesas con 1-3 categorías) + animación de entrada. axisLabel opcional nuevo en línea/barras (replica el título de eje Y rotado del PDF) y fila de Brazo/Muslo/Pierna corregidos (2 decimales) arriba del gráfico de Muscularidad, mirando la tabla de "Resultados" que el PDF muestra antes de su propio gráfico. |
| v0.19.0 | 2026-08-15 | Serie de fixes sobre el módulo ISAK de v0.18.0, a partir de feedback de uso real. (1) El gráfico de "Adiposidad" pasa de un resumen de 2 barras (Σ6 vs Σ8) a replicar el reporte real de ISAKmetry: Σ6/Σ8 como números, un gráfico de línea con los 8 pliegues individuales en el mismo orden del PDF (nuevo components/category-line-chart.tsx) y una barra con el total Σ8 — "Muscularidad" ya coincidía, no se tocó. (2) El formulario de mediciones exigía menos de lo que debía: ahora cada grupo numérico (Medidas básicas / Datos ISAK) se completa entero o queda vacío entero — ya no se puede guardar, por ejemplo, un solo pliegue suelto de los 13 (actions/mediciones.ts, .superRefine() con CAMPOS_BASICOS/CAMPOS_ISAK). (3) Nueva columna "ISAK" en el histórico de mediciones con un badge que, al tocarlo, abre el detalle de solo lectura de esa evaluación puntual — pliegues/perímetros/tejido crudos + Σ6/Σ8/corregidos/IMO calculados (components/seguimiento/isak-detalle-modal.tsx). (4) Bug de pérdida de datos silenciosa: los inputs type="number" exigen punto como separador decimal sin importar el locale del navegador — al tipear coma (convención argentina, igual que el propio PDF de ISAKmetry: "29,5"), el campo quedaba vacío sin ningún aviso. Confirmado con una prueba directa (type=number + "18,0" → value=""). Los 19 campos numéricos de medicion-form.tsx más "Horas de sueño" (ficha-form.tsx) y "Peso autoreporte" (feedback-form.tsx, cargado por el paciente) pasan a type="text" inputMode="decimal" — la validación del servidor ya toleraba coma, no se tocó. De paso, mediciones del mismo día (columna date, sin hora) ordenaban de forma ambigua; se suma id como desempate en las 4 queries que ordenan mediciones_antropometricas. (5) El bug real detrás de "el badge ISAK nunca aparece", encontrado recién con datos reales de producción vía SQL (5 filas con los 13 campos ISAK confirmados completos, badge igual en "—"): isakCompleto() hacía Object.values(m).every(...) sobre el objeto medición completo (peso, talla, dx, observaciones, etc.), no solo los 13 campos de ISAK — bastaba con que cualquier otro campo (típicamente "observaciones", casi siempre vacío en la práctica) quedara en null para que diera false, sin importar que el ISAK estuviera perfecto. Rompía tanto el badge del histórico como las secciones IMO/Adiposidad/Muscularidad de la propia página de antropometría. Fix: isakCompleto() pasa a chequear un array explícito de las 13 claves en vez de "todo lo que tenga el objeto" — verificado ejecutando la función real contra los valores exactos de una fila de producción antes de darlo por resuelto. |
| v0.18.0 | 2026-08-15 | Módulo ISAK en antropometría: se agregan IMO (índice músculo/óseo), Adiposidad (sumatorio de 6 y 8 pliegues cutáneos) y Muscularidad (perímetros corregidos), calculados por VIMET a partir de mediciones crudas que el staff carga en un fieldset colapsable "Datos ISAK (opcional)" del formulario de mediciones — 8 pliegues cutáneos, 3 perímetros y kg de tejido muscular/óseo (migración 0034, 13 columnas nuevas nullable en mediciones_antropometricas). Las 3 fórmulas se verificaron a mano contra un reporte real de ISAKmetry (software externo que usa la nutricionista) y dan exacto. kg_tejido_muscular/kg_tejido_oseo son columnas nuevas y explícitas — no se reusa kg_musculo/kg_grasa (método distinto/ambiguo, probable bioimpedancia) para no contaminar el gráfico "Composición corporal" ya existente. Ninguno de los 4 valores derivados se persiste, se calculan on-the-fly en lib/seguimiento.ts. Nuevo componente components/bar-chart.tsx (primer gráfico de barras SVG nativo del proyecto, mismo lenguaje visual que evolution-chart.tsx), usado para Adiposidad/Muscularidad como snapshot de la evaluación ISAK más reciente; IMO sí es una serie temporal. Las 3 secciones solo se muestran en /admin/pacientes/[id]/antropometria (no en la vista del paciente) y solo si el paciente tiene al menos una medición con algún dato ISAK cargado. Verificación parcial: npm run build verde, tsc/eslint limpios, y las funciones de cálculo se probaron ejecutándolas directamente con los valores exactos del PDF (coinciden). La migración se aplicó contra el Supabase real durante la sesión. No se pudo completar una verificación visual interactiva de punta a punta (guardar + ver los 3 gráficos en pantalla) por una falla del navegador automatizado usado para probar (dejó de hidratar contenido en cualquier página tras un reinicio del dev server, incluida la home pública sin relación con este cambio) — recomendado hacer una pasada manual antes de dar la feature por completamente probada en producción. |
| v0.17.0 | 2026-08-14 | Cualquier staff (no solo admin) puede editar la foto y el teléfono de cualquier profesional (Avril o Gero, no solo el propio) desde /admin/configuracion. La RLS de UPDATE en profiles seguía en is_admin() pese a que otras tablas de contenido ya se habían relajado a is_staff() — el formulario ya se mostraba para ambos perfiles pero el guardado fallaba en silencio si quien editaba no era admin ni el dueño de la fila (migración 0032). El campo de WhatsApp deja de pedir la URL completa (wa.me/...) y pasa a un input de teléfono simple (solo dígitos), que arma el link automáticamente. La home (/) deja de leer las fotos desde datos estáticos y pasa a usar la misma fuente dinámica que /nosotros y /contacto, para que un cambio de foto se refleje en las tres páginas. Bug crítico encontrado y corregido en el camino: el formulario de "Perfil público" nunca se había mostrado en producción — la búsqueda de la fila de Avril/Gero usaba el email placeholder avril@vimet.com/gero@vimet.com de lib/config/team.ts, pero las cuentas reales usan otro email, así que la búsqueda nunca encontraba nada. Se agrega profiles.slot_publico (migración 0033), un identificador estable e independiente del email de login, para que un futuro cambio de email no vuelva a romper esto. Ambas migraciones se aplicaron contra el Supabase real en esta sesión. |
| v0.16.0 | 2026-08-14 | Fix de seguridad: la CSP bloqueaba en silencio el preview de video/GIF al subir un ejercicio propio (blob: faltaba en media-src/img-src) — sumado media-src 'self' blob: y blob: a img-src. Ejercicios de la biblioteca suman dos alternativas nuevas: link de YouTube en vez de subir un GIF (miniatura real del video + botón de play, redirige a YouTube en pestaña nueva en vez de abrir el modal in-app; migración 0030, ejercicios.youtube_url) y modo Cardio con 3 fases estructuradas — entrada en calor / trabajo principal / vuelta a la calma, cada una con valor + unidad (min/hs/km) — en vez del modelo de series/reps/descanso, que no tenía sentido para correr o bici (migración 0031, ejercicios.modo + 6 columnas nuevas en plan_ejercicios); la categoría de fase de sesión deja de pedirse para ejercicios de cardio, porque las 3 fases ya la reemplazan. La rutina de un día ahora se muestra en dos bloques separados (Fuerza / Cardio) tanto en el editor de staff como en la vista del paciente. Todos los <select> nativos del sitio (28 en total) se reemplazan por un componente Select propio (components/ui/select.tsx) para que el dropdown deje de verse con el estilo del sistema operativo — encontrado y resuelto en el camino un bug real de layout en la tabla de rutina: un width específico (w-16 etc.) combinado con la clase w-full ya presente en el estilo base de los inputs competía por la misma propiedad CSS con la misma especificidad, y ganaba la que Tailwind compilaba último en la hoja de estilos (no la que aparece último en el código) — solucionado con el modificador ! de Tailwind; y el dropdown del Select se recortaba dentro de cualquier contenedor con scroll horizontal (la tabla), por lo que pasa a renderizarse en un portal a document.body con posición calculada dinámicamente. |
| v0.15.0 | 2026-08-14 | Alta de pacientes gestionados por staff: cualquier staff (nutricionista/entrenador/admin) puede dar de alta desde /admin/pacientes un paciente que nunca va a iniciar sesión (adultos mayores o con dificultad para usar la web) — el staff carga nombre/apellido/teléfono/email opcional y gestiona todo por él; internamente se crea igual una cuenta en auth.users (password aleatoria, nunca comunicada; email sintético si no tiene uno real) porque profiles depende de esa fila para todo lo demás. Nueva columna profiles.gestionado_por_staff (migración 0029) distingue esos perfiles con un badge en el listado y en su ficha. De paso, dos fixes de permisos encontrados al probar el flujo: activar/desactivar y eliminar un paciente ya no requieren ser admin — cualquier staff puede hacerlo (eliminar sigue exigiendo que el paciente esté desactivado), tanto en la web como en el endpoint que usa la app mobile. |
| v0.14.0 | 2026-08-06 | EvolutionChart (mi-progreso, antropometría, evaluación funcional) suma tooltip interactivo: al pasar el mouse o tocar el gráfico snapea al punto más cercano y muestra fecha exacta + valor de cada serie, con línea guía y punto(s) resaltado(s). Se marca el último dato con un anillo y se agrega una tercera etiqueta de fecha en el eje X (antes solo primera/última). El SVG ahora lleva aria-label describiendo el gráfico (no tenía ninguno). Sin dependencias nuevas — mismo SVG nativo de siempre. Mismo tratamiento portado a TrendChart en la app mobile (CodeTlon/vimet-app, tooltip táctil vía responder nativo de View). |
| v0.13.0 | 2026-08-05 | Feedback de uso real (Avril): biblioteca de ejercicios rechaza nombres repetidos al subir, y borrar un ejercicio en uso en un plan ahora avisa el motivo en vez de fallar en silencio (antes borraba el GIF del storage igual, dejando el ejercicio huérfano). Planes exigen al menos un dato de nutrición y/o entrenamiento según el tipo elegido (antes solo pedían el título) y el formulario oculta la sección que no corresponde al tipo. Mediciones antropométricas, evaluaciones funcionales y feedback semanal ya no se pueden guardar completamente vacíos. En /admin/pacientes, un paciente desactivado ahora dice "Inactivo" en vez de "Pendiente" (columna nueva activado_en distingue "nunca activado" de "fue activo y se desactivó"); se suma un botón "Eliminar paciente" (solo admin, con confirmación) que limpia también sus archivos en Storage. 0019_paciente_estado.sql contra Supabase antes de este release. |
| v0.12.2 | 2026-08-05 | Código listo para vimetsalud.com.ar (robots/sitemap actualizados; falta comprar el dominio y conectarlo en Vercel). Diagnosticado y parcialmente resuelto un problema real de mails: la cuenta de Resend no tiene dominio verificado, así que cualquier mail a un destinatario que no sea la casilla de la cuenta se rechazaba con 403 — rompía el form de contacto (ya corregido) y sigue rompiendo el aviso por mail al cancelar un turno (bloqueado hasta verificar un dominio en Resend). |
| v0.12.1 | 2026-08-04 | Fix del npm run lint roto desde v0.12.0. Causa real: eslint.config.mjs usaba FlatCompat.extends(...) (patrón de create-next-app) en vez de los exports flat-nativos que eslint-config-next@16.3.0 ya trae (eslint-config-next/core-web-vitals, /typescript) — FlatCompat explotaba en una referencia circular del preset de eslint-plugin-react. Con eso resuelto apareció un segundo problema: eslint-plugin-react@7.37.5 (último publicado) no soporta ESLint 10, solo hasta ^9.7 — bajado eslint a 9.39.5 (dentro de lo que pide eslint-config-next). public/gif.worker.js (vendored, no propio) agregado a ignores. Build sin cambios. El lint, ahora que corre, destapó 12 findings reales pre-existentes (react-hooks/set-state-in-effect en 10 archivos) — quedan documentados, no se tocaron en este fix. |
| v0.12.0 | 2026-08-03 | Backend para app mobile VIMET (Expo, repo aparte CodeTlon/vimet-app): nuevas rutas app/api/mobile/* autenticadas por bearer token (lib/supabase/bearer.ts) que reusan la misma lógica de negocio que las Server Actions de la web (turnos combo, no-show sweep, cancelar-y-notificar por email, altas de staff) en vez de duplicarla — se extrajo a funciones que reciben el cliente Supabase por parámetro. /api/slots ahora acepta tanto cookies (web) como bearer token (mobile). Seguridad: upgrade a Next.js 16.3.0 + React 19.2.8 (npm audit de 25 vulnerabilidades a 0); middleware.ts se renombra a proxy.ts (convención nueva de Next 16), useFormState (react-dom) pasa a useActionState (react) en todos los forms. npm run lint queda roto por un bug de la combinación recién publicada eslint-config-next@16.3.0 + ESLint 9/10 (no afecta build ni producción). |
| v0.11.0 | 2026-07-14 | No-show automático: marcarNoAsistioVencidos() (barrido perezoso, sin cron) pasa a no_asistio los turnos pendiente cuyo horario + 15min de gracia ya pasó, corrido al cargar mis-turnos, dashboard, calendario y el detalle de turno. Chat de feedback semanal ahora muestra el nombre del autor arriba de cada mensaje (antes solo alineaba por rol, sin identificar quién escribió). Home: sacada la etiqueta de tipo (Nutrición/Entrenamiento/Plan Integral) de las cards de "Nuestros servicios". Bug crítico diagnosticado (0011_turnos_combo.sql (mergeada en v0.10.0) nunca se aplicó al proyecto Supabase real — turnos.turno_par_id no existe ahí, así que cualquier visita a /admin/turno/[id] tira 404 (el select falla con 42703 y el error se descartaba silenciosamente), y están rotos la vinculación de turnos combo al crearlos, su cancelación en par y el cambio de estado desde admin. Falta correr esa migración a mano contra la base (SQL Editor de Supabase) — no es un fix de código. De yapa, admin/turno/[id] ahora loguea el error real del select antes del notFound(), para que un futuro drift de schema no vuelva a parecer un 404 mudo. |
| v0.10.0 | 2026-07-14 | Servicios integrales (combo) reservan un turno por profesional vinculado (turnos.turno_par_id, migración 0011): la disponibilidad del wizard intersecta la agenda real de nutricionista y entrenador (antes solo chequeaba a uno de los dos y el otro no se enteraba); cancelar/actualizar un turno combo propaga el cambio al par. Fix crítico: lib/supabase/server.ts usaba el cast UnsafeUnwrappedCookies (cuelgue del codemod de Next 15) — causaba que confirmar/cancelar/completar turnos y crear un turno nuevo tiraran un crash real de Server Action ('An unexpected response was received from the server'); pasa a await cookies() en sus ~50 call sites. Módulo seguimiento: mediciones y evaluaciones funcionales ahora se editan in-place (antes solo se creaban), y los carteles de éxito/error de todos los forms (plan, ficha, objetivo, feedback, recurso, turno, evolución, medición, evaluación) se auto-ocultan a los 4s en vez de quedar pegados en pantalla. Fixes menores: slots de turno se generan en bloques de 15 min (antes 30 min dejaba huecos con turnos de 45 min), objetivos vencidos se marcan en rojo, estado de turno se muestra capitalizado y centralizado en el dashboard, badges de estado con color (faltaba lib/ en el content-scan de Tailwind), 2 decimales en % grasa/músculo, textareas sin resize manual, navbar resincroniza contraste al navegar entre rutas sin scroll real. |
| v0.9.0 | 2026-07-13 | Fix del navbar: elimina el hydration mismatch que a veces dejaba el header con fondo blanco pero links en color "transparente" al recargar el home con scroll 0 (heroDarkness arranca igual que SSR, corrección real en useLayoutEffect). Fix de AuthShell (login/registro) en pantallas medianas: el layout partido pasa a activarse en md (768px) en vez de lg (1024px) — la imagen lateral ya no desaparece en tablet (varía de 40% a 50% de ancho) y la tarjeta del form usa mx-auto (antes quedaba pegada a la izquierda). Horarios: nueva actualizarHorarioAction para editar una franja in-place; la cancelación de turnos por cambios de agenda ahora chequea cobertura contra todas las franjas activas del día (no solo la tocada), así correr un horario 1 hora ya no cancela turnos que el nuevo rango sigue cubriendo. Permisos: nutricionista/entrenador (no solo admin) ya pueden crear/editar servicios, ubicación y metodología del sitio — migración 0010_contenido_staff.sql relaja las policies RLS de is_admin() a is_staff(). Favicon claro/oscuro con el ícono corredor-en-latido de la marca, servido vía prefers-color-scheme nativo. |
| v0.8.0 | 2026-07-13 | Chat de feedback semanal (tabla feedback_mensajes, migración 0009): reemplaza el par fijo dudas/respuesta por un hilo de mensajes abierto solo durante la semana en curso, con edición del último mensaje propio. Turnos: al borrar una franja horaria, el staff ve la lista de turnos cancelados con link directo a WhatsApp por paciente (la cancelación + email por Resend ya existían). Fix del flash de "no logueado" ~1s en el navbar al cargar el home. Fotos de usuario (perfil, recursos, adjuntos de feedback) se comprimen a webp con sharp antes de subir a Storage; tope de video bajado a 40MB (sin transcodificación server-side, no viable sin ffmpeg en Vercel); 2 imágenes estáticas del home comprimidas ~83%. Fix de storage huérfano: la foto de perfil anterior se borra del bucket sitio al reemplazarla. Colores por tipo de servicio: entrenamiento en azul, plan integral en verde. |
| v0.7.0 | 2026-07-11 | Baseline de seguridad + performance percibida. Headers de seguridad en next.config.mjs (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, HSTS). Auditoría de RLS/IDOR sobre las tablas del módulo seguimiento + turnos/profiles: sin gaps, ya endurecidas en migraciones previas. Fix de open redirect en app/auth/callback/route.ts (next param ahora pasa por un allowlist de paths relativos). Progresivo: loading.tsx con skeletons (shadcn) en las páginas públicas (incluido el wizard de turno), login, registro y el flujo de auth por link; error.tsx/global-error.tsx con fallback de marca para fallas de render. |
| v0.6.1 | 2026-07-06 | Hero: clips nuevos (desktop 1280×720/24fps CRF23, 5.2MB; mobile 540×960/24fps CRF28, 3.3MB) reemplazan los placeholders en hero-training.mp4/hero-training-mobile.mp4 + poster regenerado. Contraste: scrim mobile pasa de gradiente lateral a negro sólido /90 (el texto ocupa todo el ancho ahí) y el degradé superior es más oscuro/alto en desktop para legibilidad del navbar. Contenido del hero ahora corre dentro de .container-vimet (max-w 1280px) en vez de padding suelto, alineado con el resto de las secciones. |
| v0.6.0 | 2026-07-05 | Contenido editable desde /admin/contenido (servicios, metodología, ubicación/contacto) y perfil personal de cada profesional desde /admin/configuracion — migración 0007_contenido_editable.sql (tabla contenido_sitio, columnas de perfil público en profiles, bucket sitio). Hero video con fuente mobile aparte (hero-training-mobile.mp4, 854×480 CRF27, 314KB vs 12MB) vía <source media> nativo. next.config.mjs acota qualities de imágenes. |
| v0.5.0 | 2026-07-04 | Hero home: panel derecho pasa de foto estática a video autoplay (<video autoPlay muted loop playsInline>), clip de 10s recortado (720p, CRF18 libx264, sin audio, faststart, 9MB) + poster del primer frame para evitar flash negro mientras carga. |
| v0.4.0 | 2026-06-21 | Limpieza: se quita el parche NODE_OPTIONS=--max-http-header-size=65536 de los scripts dev/start. Verificado que sobra: la sesión de auth (~3-6KB aun chunked por @supabase/ssr) está muy por debajo del default de Node de 16KB (probado con next start sin el flag: cookies hasta 15KB → 200, 431 recién a ≥16KB) y el flag solo aplicaba a dev/start local, nunca a Vercel (que no corre next start). Era cruft mal aplicado, sumado junto al bodySizeLimit:16mb de uploads en d246650. Build verde. |
| v0.3.0 | 2026-06-14 | Seguridad/upgrade: Next 14.2.35 → 15.5.19 (codemod next-async-request-api: params como Promise + cookies() con UnsafeUnwrappedCookies). Smoke E2E nuevo con Playwright (login staff + rutas /admin + ruta [id] inexistente → notFound). npm audit 5 (4 high) → 2 moderate (postcss-en-Next). npm run test:e2e |
| v0.2.0 | 2026-06-02 | Flujo de invitación para staff: /auth/confirmar (implicit + PKCE), /auth/nueva-contrasena, nuevaContrasenaAction. HashInviteHandler detecta tokens de invite en el hash y redirige automáticamente. Layout refactorizado: route group (public) con Navbar/Footer; admin y auth sin navbar pública. |
| v0.1.0 | 2026-06-02 | Página /admin/configuracion: configurar profesional (asigna rol + linkea servicios/horarios), cambiar contraseña. Registro de pacientes deja cuenta inactiva hasta activación manual desde listado de pacientes admin. |
© 2026 CodeTlon. Todos los derechos reservados. Software propietario del cliente/CodeTlon. Prohibida su copia, redistribución o reuso sin autorización escrita. Ver LICENSE.