Una auditoría marcó `<ul role="img">` en el riel de clases de fuego. El rol contradice la semántica implícita de lista, así que la combinación es inválida y el árbol de accesibilidad queda mal formado. La intención original era correcta: el riel debe anunciarse como una sola unidad con su resumen, no como "lista de cinco elementos: A, B, C, D, K", que pierde justo lo que importa, cuáles están encendidas. Lo que estaba mal era el elemento, no el rol. El riel pasa a un contenedor genérico y conserva role="img" con su etiqueta. Al revisar apareció el mismo error en dos gráficas del panel, que además duplicaban el anuncio: leían el resumen completo y después la tabla accesible entera. Ahí la representación visual se oculta al árbol y la tabla equivalente queda como única fuente. También se corrigió el botón del selector de clases, que se anunciaba "A Clase A" por tener la letra visible junto a un texto oculto con el nombre completo. Se agrega una comprobación de regresión que valida el rol contra la semántica de ul, ol, li, table, nav, main, header y footer, para que no vuelva a colarse. Suite completa en verde. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
FSPM — sitio comercial y micro CRM
En línea: https://fspm-demo.urieljareth.org · Panel en /admin/login, que
no se enlaza desde el sitio a propósito. Cómo está desplegado y cómo publicar
una versión nueva: docs/DESPLIEGUE.md.
Demostración construida por Consultoría E3 para Fire Service Plus México. Sitio público orientado a venta consultiva, con un CRM interno que registra cada envío de formulario y de widget de WhatsApp como oportunidad, con su atribución de campaña.
No es un maquetado: los formularios escriben en base de datos, el panel lee de ella, y las reglas de asignación reparten los leads entre vendedores.
De dónde salió el contenido
El sitio implementa las recomendaciones de una auditoría de marca y mercado previa. Los hallazgos que dirigieron el diseño:
| Hallazgo de la auditoría | Cómo se resuelve aquí |
|---|---|
| Propuesta demasiado amplia, ocho categorías sin rutas | Inicio con tres rutas —industria, riesgo y cumplimiento— y cinco páginas por segmento prioritario |
| El visitante debe inferir qué solución le toca | Clasificador de riesgo en el héroe: elige operación y clases de fuego, y el sitio lo enruta |
| Claims sin condiciones ni evidencia por modelo | Aislados en claimsPorVerificar y presentados como compromiso de documentación, nunca como característica |
| Sin meta description ni Open Graph | Metadatos por página, sitemap derivado de los datos, JSON-LD y tarjeta social generada |
| Conversión poco consultiva | El formulario pide lo que un ingeniero necesita para cotizar, no "solicita información" |
| Sin forma de medir efectividad comercial | Atribución de primer toque que sobrevive a la navegación, y analítica por campaña en el panel |
Los datos de producto provienen de la extracción del sitio público de FSPM. Lo que la fuente no sostiene está marcado como pendiente de verificar; nada se inventó para rellenar.
Stack
Next.js 15 App Router · React 19 · TypeScript estricto · Tailwind CSS v4 ·
Prisma 6 + SQLite · sesiones propias con jose · Playwright para pruebas.
Sin librería de gráficas: el tablero dibuja SVG a mano.
Arrancar en local
npm install
cp .env.example .env # ajusta AUTH_SECRET
npx prisma db push
npm run db:seed # datos de demostración
npm run dev
Sitio en http://localhost:3000. El panel en /admin/login — no está
enlazado desde el sitio público, es intencional.
Las credenciales del seed se imprimen al final de npm run db:seed. En la
pantalla de acceso hay además entrada rápida por perfil para la demostración,
que se apaga con DEMO_ACCESO_RAPIDO=false.
Estructura
src/app/(sitio)/ páginas públicas
src/app/admin/ panel comercial, con noindex y fuera del sitemap
src/app/api/leads/ captura: deduplica contacto, crea oportunidad, asigna
src/data/ catálogo, industrias, clases de fuego, datos de empresa
src/lib/crm/ dominio, sesiones, consultas, acciones, asignación
src/components/marca/RielClases.tsx el elemento firma del diseño
tests/ Playwright, incluidos dos proyectos WebKit
docs/CONTRATO.md sistema de diseño y reglas de contenido
docs/DESPLIEGUE.md variables, Docker y despliegue
Cómo funciona la captura
- El visitante llega con parámetros de campaña.
src/lib/utm.tsguarda el primer toque y no lo sobreescribe. - Navega. Envía el formulario en una ficha de producto, ya sin parámetros en la URL.
POST /api/leadsdeduplica el contacto por correo o teléfono normalizado, crea una oportunidad nueva —un mismo contacto acumula varias— y guarda la campaña del primer toque junto con el producto y la URL exactos.- Las reglas de asignación eligen vendedor; si ninguna coincide, se reparte por turno entre los activos.
- El widget de WhatsApp hace lo mismo antes de abrir la conversación, así el lead existe aunque la persona nunca mande el mensaje.
Pruebas
npm run test:e2e # todos los proyectos
npm run test:webkit # solo Safari e iOS
Cubren rutas y metadatos, responsividad de 320 a 1440 sin desborde horizontal, el circuito de captura con atribución, el aislamiento entre roles del panel y accesibilidad básica.
Alcance
Los datos del panel son ficticios. Los logotipos de clientes se muestran sin atribuirles testimonio ni resultado: la auditoría pide confirmar la relación antes de usarlos comercialmente. La información normativa es de referencia y no sustituye dictamen técnico ni jurídico.