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]>
195 lines
8.3 KiB
JavaScript
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);
|
|
});
|