Neither is visible against localhost, which is why both shipped. **Install button never appeared on `/login`** — a regression from making the login a lazy route. `beforeinstallprompt` fires once per page load and is never replayed; `useInstallPrompt` attached its listener from a `useEffect`, i.e. at mount, and `InstallAppPrompt` now mounts only after an extra round-trip for its chunk. Locally that round-trip is a millisecond so the listener still won a race it should never have been in. Over a real connection the event was long gone, so a user on a slow link lost the install button entirely. The listener now lives in `src/lib/installPrompt.ts` and registers when the module evaluates — `main.tsx` imports it for its side effect before mounting React. The hook only reads from that store. Reproduced deterministically by delaying `/assets/LoginPage-*.js` by 1.5s via `route()`: absent before, present after (also at a 4s delay). `test:pwa` against the HTTPS domain now passes. **Revenue chart did not animate on a real iPhone.** Measured rather than guessed: the path animation does run in WebKit, and it triggers with the chart 100% visible at y=601..715 of an 844px viewport — so neither "broken" nor "fires too early". What fits is that iOS Safari suspends `requestAnimationFrame` during momentum scrolling while framer-motion interpolates against wall-clock time: the animation spends its 1.4s without painting a frame and snaps to the end on resume, which looks exactly like it never ran. The chart's three animations move to CSS keyframes, which keep their own timeline in the engine. The component only decides *when* (a `useInView` setting `data-ld-rev-visible`). `prefers-reduced-motion` resolves in CSS too, and still resolves to the *drawn* state — a line left at `dashoffset: 1px` with no animation would be invisible forever. Verified: `getAnimations()` returns a `CSSAnimation`, and under reduced motion the line renders complete. Not verified: no physical iPhone here, and Playwright WebKit on Windows does not reproduce iOS's rAF suspension. This is the standard mitigation and changes nothing on desktop, but on-device confirmation is still outstanding. Verified: typecheck clean; landing 13/13, PWA passed, responsive 0 findings, visual 62 screens / 0 errors, unit 39/39, e2e 33/33, admin 17/17, booking 12/12. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
AgendaMax
Aplicación web multi-tenant (SaaS) para la gestión de negocios de servicios (estética, spa, barbería, clínicas, etc.): calendario de citas con arrastrar y soltar, gestión de empleados, servicios, clientes y tickets, y un tablero con ingresos, mejores empleados, servicios más rentables y clientes top.
Tres roles: Administrador de plataforma (crea y gestiona todos los negocios desde la consola admin), Dueño (ve su negocio y el tablero) y Empleado (agenda citas que se auto-asignan a él).
Stack
- Frontend: React 18 + TypeScript + Vite + Tailwind CSS
- Calendario: FullCalendar (drag & drop, resize, day/time grid)
- Gráficas: Recharts
- Backend: Node.js + Express
- Base de datos: SQLite (vía
node:sqlite, nativo de Node — sin compilación nativa)
Requisitos
- Node.js 22.5+ (recomendado 24+). Este proyecto usa el módulo nativo
node:sqlite.
Instalación
npm install
La base de datos se siembra automáticamente con datos demo la primera vez que arranca el servidor. Para regenerarla desde cero:
# Borra la DB y vuelve a sembrar
npm run seed
# o, forzando reset desde el servidor:
$env:RESET_DB="1"; npm run dev:server # PowerShell
RESET_DB=1 npm run dev:server # bash
Desarrollo
npm run dev
Abre http://localhost:5173 (el frontend proxya /api al backend en el puerto 3000).
| Ruta | Qué es |
|---|---|
/ |
Landing pública. Con sesión abierta redirige al panel según el rol. |
/login |
Acceso. En fase demo ofrece entrar de un clic con las cuentas de abajo. |
/b/:slug |
Reserva pública del negocio, sin sesión. |
Producción
npm run build # compila TS y empaqueta el frontend en dist/
npm start # sirve la API y el frontend estático en http://localhost:3000
El acceso de un clic y el selector «Ver como…» del panel viven detrás de la constante de
build DEMO, que en un build de producción solo se enciende con VITE_DEMO_UI=1. El
Dockerfile la declara con ese valor por defecto porque el despliegue es la
demostración; pasar --build-arg VITE_DEMO_UI=0 los elimina del bundle.
Instalar AgendaMax como app
- Android/Chrome: abre la URL HTTPS y selecciona la acción de instalación mostrada por AgendaMax o por la barra de direcciones del navegador.
- iPhone/iPad Safari: usa Compartir -> Añadir a pantalla de inicio; iOS no expone el aviso de instalación dentro de la página de Android.
- La app instalada puede abrirse sin la interfaz del navegador, pero los datos del negocio siguen requiriendo conexión a internet.
- Validación local: ejecuta
npm.cmd run build, inicia producción connpm.cmd starty después ejecutanpm.cmd run test:pwa. - La validación de producción requiere el dominio HTTPS de Coolify, no una dirección IP HTTP.
npm.cmd run test:pwa usa estas variables opcionales para los flujos autenticados;
si no se definen, usa las cuentas demo locales:
[email protected]
[email protected]
PWA_PASSWORD=demo1234
Tests y auditoría visual
npm run test:e2e # 22 pruebas end-to-end de la API (login, CRUD, dashboard, auto-asignación, roles, multi-tenant)
npm run test:admin # 17 pruebas del admin (crear negocio + plantilla, aislamiento, reset-demo, delete cascade)
npm run test:landing # WebKit: ruteo landing/login, acceso de un clic y prefers-reduced-motion
npm run audit:visual # Playwright: navega 10 páginas × 4 viewports (móvil/tablet/desktop/wide),
# mide overflow horizontal, captura errores de consola y guarda capturas en screenshots/
npm run audit:responsive # WebKit: iPhone/iPad reales — auto-zoom, safe areas, objetivos táctiles y canaletas
La auditoría visual verifica responsividad en 375 / 768 / 1280 / 1536 px, detecta overflow, errores de consola y confirma que el calendario, el modal de cita y el cambio de sesión funcionan en cada tamaño.
Cuentas demo
Todas usan la contraseña demo1234. En /login se puede entrar con un clic, sin
escribirlas.
| Rol | Contraseña | Nombre | |
|---|---|---|---|
| Admin | [email protected] |
demo1234 |
Administrador |
| Dueño | [email protected] |
demo1234 |
Daniela Reyes |
| Empleado | [email protected] |
demo1234 |
Valentina Cruz |
| Empleado | [email protected] |
demo1234 |
Mateo Herrera |
| Empleado | sofía.ramí[email protected] |
demo1234 |
Sofía Ramírez |
| Empleado | [email protected] |
demo1234 |
Diego Castillo |
| Empleado | [email protected] |
demo1234 |
Isabela Torres |
| Empleado | carolina.mé[email protected] |
demo1234 |
Carolina Méndez |
En la pantalla de login aparecen botones de acceso rápido a las cuentas demo, y desde la barra lateral puedes cambiar de cuenta con “Ver como…”.
Multi-tenant y consola de administrador
AgendaMax es multi-tenant: cada negocio es un tenant aislado (todos los datos llevan business_id
y las consultas se filtran por el negocio del usuario). El administrador de plataforma gestiona
todos los negocios desde /admin:
- Resumen: KPIs globales (negocios activos, usuarios, ingresos y citas de toda la plataforma).
- Negocios: lista buscable con stats por negocio (usuarios, citas, ingresos).
- Crear negocio (2 pasos): eliges una plantilla → se cargan empleados, servicios y clientes demo con historial de citas/tickets listos para usar; defines los datos del negocio y la cuenta del dueño.
- Detalle de negocio: editar nombre/industria, cambiar plan (Prueba/Pro/Free) y estado (Activo/Suspendido), recargar datos demo con otra plantilla (preserva las cuentas de usuario) o eliminar el negocio por completo (con cascade de todos sus datos).
Plantillas disponibles: estetica-spa (Lumière), barberia (Urban Cut), clinica (Dental Sonrisa)
y blank (vacío, para configurar desde cero). Defínelas en server/lib/templates.ts.
El aislamiento entre tenants se valida: un dueño solo ve los datos de su negocio, y los empleados no pueden acceder al tablero ni a la gestión (403).
Funcionalidades
Calendario (Dueño y Empleado)
- Vistas de mes / semana / día, con indicador de hora actual.
- En móvil se abre por defecto en vista Día y se ofrece una vista Lista (agenda) — la vista Semana/Mes queda reservada para tablet/desktop para evitar columnas aplastadas.
- Arrastra para reagendar y redimensiona para cambiar duración.
- Clic en un hueco o en Nueva cita para abrir el formulario.
- Clic en una cita para editarla: cambiar servicio, especialista, cliente, hora, precio, notas.
- Marcar como completada (genera un ticket con propina y método de pago).
- Cancelar o eliminar citas.
- Filtra por empleado y por estado (programada / completada / cancelada).
Formulario de cita
- Servicio desplegable (menú del negocio), con precio y duración.
- Especialista: el empleado lo tiene auto-asignado por defecto (filtrado a quienes ofrecen el servicio).
- Cliente: buscar existente o crear uno nuevo al vuelo.
- Duración, precio y notas editables.
Tablero (Dueño)
- KPIs: ingresos del período, citas programadas, ticket promedio, ocupación y % de cancelación.
- Gráfica de ingresos diarios y distribución por categoría.
- Mejores empleados por ingreso, con rating y utilización.
- Servicios más rentables.
- Tickets más altos recientes.
- Top clientes (por gasto) y clientes frecuentes (por visitas).
- Rango de tiempo ajustable (7 / 30 / 60 días).
Gestión (Dueño)
- Empleados: crear, editar, asignar servicios, ver ingresos/citas/rating.
- Servicios: catálogo agrupado por categoría, con especialistas asignados.
- Tickets: historial de ventas con filtros por método de pago.
Clientes (Dueño y Empleado)
- Buscador por nombre / teléfono / correo.
- Ficha del cliente con historial de citas, gasto total y visitas.
Estructura
.
├── server/ # API Express + SQLite
│ ├── db.ts # esquema multi-tenant + sistema de migraciones versionado
│ ├── index.ts # app Express (auto-siembla admin + negocio demo en primer arranque)
│ ├── lib/auth.ts # middlewares: authRequired, ownerOnly, adminOnly
│ ├── lib/templates.ts # plantillas: estetica-spa, barberia, clinica, blank
│ ├── routes/ # auth, business, employees, services, clients, appointments, dashboard, admin
│ └── scripts/seed.ts# seedBusiness(businessId, template) reutilizable + script standalone
├── shared/types.ts # tipos TS compartidos cliente/servidor
├── src/ # frontend React
│ ├── components/ # AppShell, AdminShell, Modal, AppointmentModal, ui, DemoSwitcher, admin/CreateBusinessModal
│ ├── lib/ # api, auth, format
│ └── pages/ # Login, Dashboard, Calendar, Employees, Services, Clients, Tickets + admin/*
└── data/ # SQLite (generado, .gitignored)
Notas de seguridad
Es una demo. La autenticación usa un token trivial (el id del usuario) sin hashing de contraseña ni sesiones reales — no usar en producción tal cual. Antes de un despliegue real habría que añadir hashing (bcrypt), JWT firmados, validación estricta con zod, rate-limiting y HTTPS.