Files
insta-portal/db/seed.mjs
T
Uriel JarethandClaude Opus 5 0b9c9c1dc2 Portal interno INSTA: base de conocimiento con Magic Login y permisos por rol
Base de conocimiento para el personal del Instituto Santiago Tapia: oferta
educativa (en tarjetas y en tabla), requisitos y titulacion, becas y fechas
de inscripcion, links de clases de Google Meet, preguntas frecuentes,
mensajes listos y datos del instituto.

Acceso con Magic Login: enlace de un solo uso, 15 minutos de vigencia, y en
la base solo se guarda el SHA-256 del token. El canje es atomico, asi que dos
canjes simultaneos del mismo enlace no pueden crear dos sesiones.

Tres roles con 19 permisos. El catalogo lo edita direccion; los links de
clases, direccion y administracion; el resto del equipo consulta. El menu se
pinta con lo que el servidor autoriza y cada endpoint exige la misma clave,
asi que no hay pantallas alcanzables escribiendo la direccion a mano.

Toda edicion pasa por una lista blanca de campos validados y queda en la
bitacora. Un precio vacio no se guarda como cero, un link que no es una
direccion se rechaza, y no se puede marcar una validez oficial como
confirmada sin numero de acuerdo.

Pensado para leerse comodo: texto de 17 px, contraste AA, areas de toque de
44 px, tema claro y bloques plegados en telefono.

La oferta, los costos, las becas, los requisitos y el contacto son los reales
del instituto, tomados del CSV de oferta educativa y de la base de
conocimiento. Los usuarios de acceso y los links de Meet son de demostracion.

Verificado: 103 comprobaciones de API y 51 pruebas en WebKit escritorio y
iPhone 13.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 23:20:34 -06:00

195 lines
8.3 KiB
JavaScript

// =====================================================================
// Reconstruye la base del portal desde cero: esquema + datos.
// node db/seed.mjs (o npm run db:reset)
// =====================================================================
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import mysql from 'mysql2/promise';
import { config } from '../server/config.mjs';
import * as D from './data.mjs';
const raiz = path.dirname(fileURLToPath(import.meta.url));
function log(paso, detalle = '') {
console.log(` ${paso.padEnd(34, '.')} ${detalle}`);
}
async function main() {
const cn = await mysql.createConnection({ ...config.db, multipleStatements: true, charset: 'utf8mb4' });
console.log(`\n▸ Portal INSTA · siembra de "${config.db.database}" en ${config.db.host}:${config.db.port}\n`);
const schema = await readFile(path.join(raiz, 'schema.sql'), 'utf8');
await cn.query(schema);
log('esquema reconstruido', `${schema.split('CREATE TABLE').length - 1} tablas`);
// ---- roles, permisos y usuarios -------------------------------------
const rolId = {};
for (const r of D.roles) {
const [res] = await cn.execute(
'INSERT INTO roles (clave, nombre, nivel, color, descripcion) VALUES (?,?,?,?,?)',
[r.clave, r.nombre, r.nivel, r.color, r.descripcion],
);
rolId[r.clave] = res.insertId;
}
const permisoId = {};
for (const [clave, seccion, descripcion] of D.permisos) {
const [res] = await cn.execute(
'INSERT INTO permisos (clave, seccion, descripcion) VALUES (?,?,?)',
[clave, seccion, descripcion],
);
permisoId[clave] = res.insertId;
}
let asignaciones = 0;
for (const [rolClave, lista] of Object.entries(D.rolesPermisos)) {
const claves = lista === '*' ? Object.keys(permisoId) : lista;
for (const c of claves) {
if (!permisoId[c]) throw new Error(`Permiso desconocido en rolesPermisos.${rolClave}: ${c}`);
await cn.execute('INSERT INTO roles_permisos (rol_id, permiso_id) VALUES (?,?)', [rolId[rolClave], permisoId[c]]);
asignaciones++;
}
}
log('roles y permisos', `${D.roles.length} roles · ${D.permisos.length} permisos · ${asignaciones} asignaciones`);
const usuarioId = {};
for (const u of D.usuarios) {
const [res] = await cn.execute(
'INSERT INTO usuarios (nombre, email, puesto, rol_id, iniciales, extension, es_demo) VALUES (?,?,?,?,?,?,?)',
[u.nombre, u.email, u.puesto, rolId[u.rol], u.iniciales, u.extension, u.es_demo],
);
usuarioId[u.email] = res.insertId;
}
log('usuarios', `${Object.keys(usuarioId).length} (3 en el acceso rápido)`);
// ---- oferta educativa -----------------------------------------------
const nivelId = {};
for (const n of D.niveles) {
const [res] = await cn.execute('INSERT INTO niveles (clave, nombre, orden) VALUES (?,?,?)', [n.clave, n.nombre, n.orden]);
nivelId[n.clave] = res.insertId;
}
const modalidadId = {};
for (const m of D.modalidades) {
const [res] = await cn.execute(
'INSERT INTO modalidades (clave, nombre, color, detalle) VALUES (?,?,?,?)',
[m.clave, m.nombre, m.color, m.detalle],
);
modalidadId[m.clave] = res.insertId;
}
const programaId = {};
let horarios = 0;
for (const p of D.programas) {
const [res] = await cn.execute(
`INSERT INTO programas
(nivel_id, clave, nombre, nombre_corto, duracion, fecha_inicio,
inscripcion_lista, inscripcion_beca, mensualidad_lista, mensualidad_beca, ciclo_semanas,
rvoe_acuerdo, rvoe_fecha, rvoe_autoridad, rvoe_estado, rvoe_nota, beneficios, color, orden)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
[nivelId[p.nivel], p.clave, p.nombre, p.nombre_corto, p.duracion, p.fecha_inicio,
p.inscripcion_lista, p.inscripcion_beca, p.mensualidad_lista, p.mensualidad_beca, p.ciclo_semanas,
p.rvoe_acuerdo, p.rvoe_fecha, p.rvoe_autoridad, p.rvoe_estado, p.rvoe_nota, p.beneficios, p.color, p.orden],
);
programaId[p.clave] = res.insertId;
for (const h of p.horarios) {
await cn.execute(
'INSERT INTO programas_modalidades (programa_id, modalidad_id, dias, hora_inicio, hora_fin, nota) VALUES (?,?,?,?,?,?)',
[res.insertId, modalidadId[h.modalidad], h.dias, h.hora_inicio, h.hora_fin, h.nota],
);
horarios++;
}
}
log('programas', `${Object.keys(programaId).length} programas · ${horarios} horarios`);
let reqs = 0;
for (const [nivelClave, lista] of Object.entries(D.requisitos)) {
let orden = 1;
for (const [texto, nota] of lista) {
await cn.execute('INSERT INTO requisitos (nivel_id, texto, nota, orden) VALUES (?,?,?,?)', [nivelId[nivelClave], texto, nota, orden++]);
reqs++;
}
}
let ordenTit = 1;
for (const [nombre, descripcion] of D.titulacionOpciones) {
await cn.execute('INSERT INTO titulacion_opciones (nombre, descripcion, orden) VALUES (?,?,?)', [nombre, descripcion, ordenTit++]);
}
log('requisitos y titulación', `${reqs} requisitos · ${D.titulacionOpciones.length} formas de titulación`);
// ---- becas y promociones --------------------------------------------
for (const p of D.promociones) {
await cn.execute(
'INSERT INTO promociones (clave, nombre, resumen, aplica_a, vigencia, acumulable, condiciones, orden) VALUES (?,?,?,?,?,?,?,?)',
[p.clave, p.nombre, p.resumen, p.aplica_a, p.vigencia, p.acumulable, p.condiciones, p.orden],
);
}
log('becas y promociones', `${D.promociones.length}`);
// ---- links de clases -------------------------------------------------
let ordenClase = 1;
for (const c of D.clases) {
if (!programaId[c.programa]) throw new Error(`Programa desconocido en clases: ${c.programa}`);
await cn.execute(
`INSERT INTO clases (programa_id, materia, docente, grupo, dias, hora_inicio, hora_fin, enlace, notas, orden)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
[programaId[c.programa], c.materia, c.docente, c.grupo, c.dias, c.hora_inicio, c.hora_fin, c.enlace, c.notas, ordenClase++],
);
}
log('links de clases', `${D.clases.length} en ${new Set(D.clases.map((c) => c.programa)).size} programas`);
// ---- conocimiento y contacto -----------------------------------------
for (const g of D.guiones) {
await cn.execute(
'INSERT INTO guiones (categoria, titulo, cuerpo, canal, orden) VALUES (?,?,?,?,?)',
[g.categoria, g.titulo, g.cuerpo, g.canal, g.orden],
);
}
for (const a of D.kbArticulos) {
await cn.execute(
'INSERT INTO kb_articulos (seccion, pregunta, respuesta, palabras_clave, orden) VALUES (?,?,?,?,?)',
[a.seccion, a.pregunta, a.respuesta, a.palabras_clave, a.orden],
);
}
for (const i of D.institucional) {
await cn.execute(
'INSERT INTO institucional (grupo, clave, etiqueta, valor, orden) VALUES (?,?,?,?,?)',
[i.grupo, i.clave, i.etiqueta, i.valor, i.orden],
);
}
for (const d of D.discrepancias) {
await cn.execute(
'INSERT INTO discrepancias (titulo, detalle, ambito, prioridad, estado, responsable, orden) VALUES (?,?,?,?,?,?,?)',
[d.titulo, d.detalle, d.ambito, d.prioridad, d.estado, d.responsable, d.orden],
);
}
log('conocimiento y contacto', `${D.guiones.length} mensajes · ${D.kbArticulos.length} preguntas · ${D.institucional.length} datos · ${D.discrepancias.length} por confirmar`);
await cn.execute(
"INSERT INTO bitacora (actor, rol, accion, entidad, detalle) VALUES ('sistema','','db.siembra','base','Base reconstruida desde la base de conocimiento INSTA')",
);
// ---- verificación ----------------------------------------------------
const [tablas] = await cn.query(
'SELECT TABLE_NAME AS t FROM information_schema.tables WHERE table_schema = ? ORDER BY t',
[config.db.database],
);
let total = 0;
const vacias = [];
for (const { t } of tablas) {
const [[{ n }]] = await cn.query(`SELECT COUNT(*) AS n FROM \`${t}\``);
total += n;
if (n === 0) vacias.push(t);
}
console.log(`\n ✔ ${tablas.length} tablas · ${total} filas`);
console.log(` ✔ vacías por diseño: ${vacias.join(', ') || 'ninguna'}`);
console.log('\n Accesos demo: [email protected] · [email protected] · [email protected]\n');
await cn.end();
}
main().catch((err) => {
console.error('\n✖ La siembra falló:', err.message);
process.exit(1);
});