# Portal interno INSTA Base de conocimiento para el **personal** del Instituto Santiago Tapia: la oferta educativa, los requisitos, las becas y fechas, los links de clase, las preguntas frecuentes y los datos del instituto, todo en un solo lugar y con **Magic Login**. El caso de uso es concreto: que quien está atendiendo a un interesado encuentre el dato exacto en segundos y lo diga sin equivocarse. Y que cuando un dato cambie, **dirección lo edite desde el portal** en lugar de avisar por WhatsApp. Está pensado para leerse cómodo: texto de 17 px, contrastes altos, botones grandes, tema claro y una sola columna en el teléfono. > **Es una demostración.** Los tres usuarios de acceso rápido son ficticios y los links de Google > Meet son de ejemplo. La oferta educativa, los costos, las becas, los requisitos y los datos de > contacto sí son los reales del instituto. --- ## Arrancar Requisitos: **Docker** y **Node 20+** (probado en Node 26). ```bash cd insta-portal docker compose up -d # MySQL 8 en el puerto 3307 npm install npm run db:reset # crea el esquema y siembra los datos npm start # http://localhost:4180 ``` | Comando | Qué hace | |---|---| | `npm start` | Levanta el portal en el puerto 4180 | | `npm run dev` | Igual, recargando al guardar | | `npm run db:reset` | Reconstruye el esquema y vuelve a sembrar (borra las ediciones de la demo) | | `npm run reiniciar` | Libera el puerto, siembra y arranca de nuevo, en un paso (Windows) | | `npm run test:api` | 103 comprobaciones de la API, sin navegador | | `npm test` | 51 pruebas en WebKit escritorio y en iPhone 13 | | `npm run capturas` | Genera las capturas de todas las secciones en `tests/capturas/` | | `npm run codificacion` | Avisa si algún archivo quedó con los acentos rotos | ### Variables de entorno Todas tienen valor por omisión; para correr en local no hace falta ninguna. | Variable | Por omisión | Para qué | |---|---|---| | `PORT` | `4180` | Puerto del portal | | `DB_HOST` `DB_PORT` `DB_USER` `DB_PASSWORD` `DB_NAME` | `127.0.0.1` `3307` `root` `insta_root_demo` `insta_portal` | Conexión a MySQL | | `MAGIC_LINK_MINUTOS` | `15` | Vigencia del enlace de acceso | | `SESION_MINUTOS` | `480` | Vigencia de la sesión | | `MOSTRAR_ENLACE` | `true` | Muestra el enlace en pantalla en lugar de enviarlo por correo. **Ver la advertencia de abajo.** | | `COOKIE_SEGURA` | `false` | Pon `true` detrás de HTTPS: añade `Secure` y `SameSite=Strict` | | `CONFIAR_PROXY` | `false` | Pon `true` detrás de un proxy para que la IP real llegue a la bitácora y al límite de enlaces | | `ENLACES_POR_VENTANA` `VENTANA_MINUTOS` | `60` `5` | Cuántos enlaces se pueden pedir desde un mismo origen | > ⚠️ **`MOSTRAR_ENLACE=true` deja la demo abierta a propósito.** Con esa opción, quien conozca un > correo dado de alta obtiene una sesión sin pasar por el correo. Es lo que permite que cualquiera > pruebe la demostración con un clic. Para uso real hay que ponerlo en `false` y conectar el envío > por correo. --- ## Accesos de la demostración | Rol | Correo | Ve | Edita | |---|---|---|---| | Dirección | `director@insta.demo` | 10 secciones | Todo el contenido | | Personal administrativo | `administracion@insta.demo` | 9 | Solo los links de clases | | Empleado · ventas y atención | `asesor@insta.demo` | 8 | Nada, solo consulta | Hay dos usuarios más en la base (`controlescolar@insta.demo` y `asesor.sabatino@insta.demo`) que no salen en el acceso rápido; se puede entrar con ellos escribiendo su correo. ### Cómo funciona el Magic Login 1. Escribes tu correo y el servidor crea un token de 32 bytes; en la base solo guarda su **SHA-256**. 2. El enlace se muestra en pantalla (en producción llegaría por correo) y sirve **una sola vez**. 3. Caduca a los **15 minutos**. Al canjearlo nace una sesión de 8 horas. 4. La cookie es `HttpOnly` y `SameSite`; en la base solo vive el hash del id de sesión. El canje es **atómico**: el token se reclama con un solo `UPDATE` condicional, así que dos canjes simultáneos del mismo enlace no pueden crear dos sesiones. No hay contraseñas en el sistema. --- ## Quién puede editar qué Son **19 permisos** con la forma `sección.acción`. El menú se pinta con lo que el servidor autoriza y **cada endpoint exige la misma clave**, así que no hay pantallas alcanzables escribiendo la dirección a mano: la API responde 403 y el intento queda en la bitácora. La regla es corta: **el catálogo lo edita dirección; los links de clases, dirección y administración; el resto del equipo consulta.** | Sección | Dirección | Administración | Empleado | |---|:--:|:--:|:--:| | Inicio | ● | ● | ● | | Oferta educativa | ● edita | ● lee | ● lee | | Requisitos y titulación | ● edita | ● lee | ● lee | | Becas y fechas | ● edita | ● lee | ● lee | | Links de clases | ● edita | ● **edita** | ● lee | | Preguntas frecuentes | ● edita | ● lee | ● lee | | Mensajes listos | ● edita | ● lee | ● lee | | Datos del instituto | ● edita | ● lee | ● lee | | Datos por confirmar (dentro de Oferta) | ● gestiona | ● lee | ○ | | Equipo | ● | ● | ○ | | Bitácora | ● | ○ | ○ | --- ## Las secciones | Sección | Qué tiene | |---|---| | **Inicio** | La promoción vigente, atajos a cada sección y los datos que más se dictan por teléfono. Dirección ve además los últimos cambios al contenido. | | **Oferta educativa** | Los 8 programas **en tarjetas y en tabla** (se elige con un botón y la elección se recuerda), con costo con y sin beca, duración, fecha de inicio, horarios por modalidad y estado de la validez oficial. | | **Requisitos y titulación** | Las listas de documentos por nivel, con botón para copiarlas, y las 5 formas de titularse. | | **Becas y fechas** | Los 5 esquemas de precio con sus condiciones completas, cuándo inicia cada programa, y la escala de referidos con la comparación honesta contra la beca. | | **Links de clases** | Los 27 enlaces de Google Meet agrupados por programa, con materia, día y hora, docente y grupo. Botón para entrar y para copiar el link. | | **Preguntas frecuentes** | 21 respuestas verificadas, buscables. | | **Mensajes listos** | 21 textos ya redactados para copiar y enviar por WhatsApp, filtrables por categoría. | | **Datos del instituto** | Contacto, ubicación, horarios, misión, visión y valores. | | **Equipo** | El directorio y la matriz completa de qué puede ver y editar cada rol, permiso por permiso. | | **Bitácora** | Sesiones abiertas, enlaces generados y el registro de accesos y cambios, con los intentos denegados. | ### Editar Cada sección editable trae un botón de lápiz que abre una ventana con el formulario. Todo lo que se guarda pasa por una **lista blanca de campos validados** en el servidor (`server/campos.mjs`): un precio vacío no se guarda como cero, un link que no es una dirección se rechaza con un mensaje claro, y no se puede marcar la validez oficial como confirmada sin número de acuerdo. Cada cambio queda en la bitácora con quién lo hizo. --- ## De dónde salen los datos | Fuente | Qué aportó | |---|---| | `INSTA - Base de conocimiento - Oferta Educativa.csv` | Los 8 programas: nivel, duración, fechas, costos, beneficios, horarios y promociones | | `Base de conocimiento — Instituto Santiago Tapia (INSTA).md` | Identidad, contacto, modalidades, becas, RVOE, requisitos, titulación y los datos por confirmar | | `docs/crm/01-microreporte-atencion.md` | Los errores de atención reales que los mensajes listos corrigen | **Los links de clase, las materias y los docentes son de demostración.** Todo lo demás es real. ### Lo que el portal se niega a inventar La fuente tiene huecos, y el portal los trata como huecos en lugar de rellenarlos. En **Oferta educativa** dirección y administración ven un panel de **datos por confirmar**: - **Bachillerato y las dos maestrías no tienen RVOE confirmado.** Salen marcados, con la nota de por qué el número que traían las listas viejas es incorrecto. - **Trabajo Social** figura ante la Secretaría de Educación de Guerrero y no ante Centro Educativo Puebla como el resto: queda marcado como «por revisar». - **¿La beca del 25% aplica solo el primer ciclo o toda la carrera?** La fuente dice «primer ciclo», pero en una conversación se le prometió a una interesada que quedaba congelado 3 años. Sin definir. - **Las fechas de inicio** vienen del archivo de oferta y están marcadas como pendientes de confirmar. Y lo que sí está resuelto se dice resuelto: el programa de referidos **no** se combina con nada, pero la beca del 25% **sí** se combina con el descuento de temporada — que es como el equipo ya lo ofrece por WhatsApp. --- ## Cómo está hecho ``` insta-portal/ ├─ docker-compose.yml MySQL 8 en el 3307, con volumen persistente ├─ db/ │ ├─ schema.sql 19 tablas con llaves foráneas e índices │ ├─ data.mjs todos los datos, con su fuente citada │ └─ seed.mjs reconstruye y verifica la siembra ├─ server/ │ ├─ index.mjs Express, cabeceras, estáticos y /acceso/:token │ ├─ config.mjs configuración con valores por omisión │ ├─ db.mjs pool de mysql2, todo con consultas preparadas │ ├─ auth.mjs Magic Login y sesiones (en la base solo hashes) │ ├─ rbac.mjs secciones, permisos y middlewares │ ├─ campos.mjs validación de lo que se puede editar │ ├─ bitacora.mjs registro de auditoría │ └─ rutas.mjs la API, con un permiso declarado por endpoint ├─ public/ │ ├─ index.html entrada · Magic Login │ ├─ portal.html shell del portal │ ├─ css/insta.css tokens de marca y todos los estilos │ └─ js/ ui (incluye el editor), entrada, app y las vistas ├─ scripts/ │ ├─ reiniciar.ps1 libera el puerto, siembra y arranca │ └─ revisar-codificacion.mjs detecta acentos rotos tras una edición masiva └─ tests/ ├─ api.mjs 103 comprobaciones sin navegador ├─ portal.spec.mjs 51 pruebas en WebKit └─ capturas.mjs capturas de todas las secciones ``` Sin framework de frontend y sin paso de compilación: **dos dependencias** en producción (`express` y `mysql2`). El portal es una sola página con ruteo por hash; cada sección pide sus datos a la API y se pinta sola. ### Base de datos 19 tablas, 217 filas sembradas: `roles`, `permisos`, `roles_permisos`, `usuarios`, `magic_tokens`, `sesiones`, `bitacora`, `niveles`, `modalidades`, `programas`, `programas_modalidades`, `requisitos`, `titulacion_opciones`, `promociones`, `clases`, `guiones`, `kb_articulos`, `institucional`, `discrepancias`. Todas las consultas van preparadas. Los nombres de columna de un `UPDATE` solo pueden venir de la lista blanca de cada entidad, nunca del cuerpo de la petición. ### Diseño La geometría sale del logo del instituto: una **placa** con radio grande en la esquina superior izquierda e inferior derecha, y casi recto en las otras dos. Esa forma se repite en cada superficie. | Token | Valor | De dónde | |---|---|---| | Azul | `#4275B7` | muestreado del logo | | Azul profundo | `#30517E` | muestreado del logo | | Azul panel | `#9DC2E8` | el panel interior del logo | | Amarillo | `#EFEF07` | el contorno de las letras del logo | El amarillo se usa **solo como marcador** sobre el dato más importante de cada pantalla, igual que alguien subraya la lista de precios impresa. Títulos en serif Palatino; monoespaciada para dinero, horas y números de acuerdo, que son datos que se leen dígito por dígito y se dictan por teléfono. Decisiones tomadas por el público que lo va a usar: - Texto base de **17 px** y ningún texto informativo por debajo de 14 px. - Contraste **AA** o mejor: el color de un programa o de un rol nunca se usa como color de texto, solo como fondo tenue, borde o punto. - Todo lo que se toca mide al menos **44 px** de alto. - En el teléfono, los bloques largos llegan **plegados** con un resumen, y si los abres se quedan abiertos aunque la sección se repinte al guardar un cambio. - `prefers-reduced-motion` respetado, e impresión limpia. --- ## Verificación ```bash npm run test:api # 103 comprobaciones npm test # 51 pruebas × WebKit escritorio y iPhone 13 = 102 ejecuciones ``` **Todo en verde.** Lo que cubren: - **Magic Login**: los tres accesos, correo no registrado, enlace inventado, enlace ya usado, y que **cinco canjes simultáneos del mismo enlace creen una sola sesión**. - **Permisos**: el número exacto de secciones por rol; que la API niegue con 403 los diez endpoints de escritura al empleado y los seis del catálogo a administración; que administración sí pueda crear, editar y borrar una clase; que sin sesión responda 401; y que el intento denegado aparezca en la bitácora. - **Contenido contra la fuente**: los 8 programas, los precios de Psicología y de Bachillerato, el acuerdo 20110687, los tres programas sin número de acuerdo, las 6 fotografías del bachillerato contra las 4 de licenciatura, las 5 formas de titulación, los 27 links en 8 programas, y que el único esquema no combinable sea el de referidos. - **Edición**: que dirección guarde un cambio y quede registrado quién lo hizo; que un precio vacío se rechace en lugar de guardarse como `$0`; que `"$2,500"` se guarde como 2500 y no como 2.50; que no se pueda confirmar una validez oficial sin acuerdo; que una clase sin hora se rechace; y que editar dos veces seguidas no abra dos ventanas. - **Entradas raras**: buscar `%`, `limite=50.5`, un texto de 300 000 caracteres, un JSON roto, un id que no es número, una cookie con `%` mal formado. Ninguna devuelve 500 ni cuelga la petición. - **Legibilidad y responsividad**: texto base de 17 px; tema claro incluso si el sistema pide oscuro; alturas mínimas de toque; cero desbordamiento horizontal en las 10 secciones a 390, 768, 1024 y 1440 px; ninguna sección pasa de 12 000 px de alto en el teléfono; el cajón móvil abre, navega y cierra; la ventana de edición cabe en un teléfono; `aria-current`; navegación por teclado; y `Escape` cierra el editor sin guardar. Para revisar el portal completo de un vistazo: ```bash npm run capturas # abre tests/capturas/index.html ``` --- ## Lo que falta para producción 1. **Enviar el enlace por correo** en lugar de mostrarlo en pantalla: `MOSTRAR_ENLACE=false` ya deja de exponerlo, falta conectar el proveedor de correo. 2. **HTTPS**, y con él `COOKIE_SEGURA=true` y `CONFIAR_PROXY=true`. 3. **Alta y baja de usuarios desde el portal.** Hoy el equipo se siembra en `db/data.mjs`; el permiso `equipo.ver` existe pero no hay pantalla para dar de alta a alguien. 4. **Historial de cambios por registro.** La bitácora dice quién cambió qué sección y cuándo, pero no guarda el valor anterior, así que no se puede deshacer. 5. **Aviso de edición simultánea.** Si dos personas editan el mismo programa a la vez, gana la última en guardar y la primera no se enteraría. 6. **Cerrar los datos por confirmar con dirección.** Mientras el RVOE del bachillerato y de las maestrías siga sin número, el equipo trabaja con un hueco.