Files
project.linkedcorp.net/BASE_DE_DATOS.md
T
Milton Gonzalez b2c25c4fe5 feat: version 4.14.0 -- Equipos: año del procesador con boton Obtener fecha (IA)
La IA consulta solo el nombre del procesador y devuelve el año (el mas antiguo si hay dos).
Migracion 039.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 19:07:31 -05:00

19 KiB

Base de Datos - project.linkedcorp.net

Motor

  • Motor: MariaDB
  • Base principal: project_linkedcorp
  • Host desarrollo: 192.168.222.20
  • Host producción: 192.168.222.32
  • Puerto: 3306
  • Charset: utf8mb4
  • Collation: utf8mb4_unicode_ci

Migración 034_meetings.sql (v4.4.0): aplicada en desarrollo, pendiente en producción. Crea tablas e índices de reuniones y amplía el tipo de entidad de notificaciones. DATETIME se guarda como hora civil de America/Guayaquil; la conexión usa dateStrings: true. La recurrencia se almacena como JSON de texto y se expande al consultar, sin filas por ocurrencia.

Conexiones externas (migración 038)

038_integrations.sql (aplicada en dev 2026-10-29): external_connections (URL, token cifrado AES-256-GCM), external_sources (una por destino patients/employees: tabla, modo, mapeo, filtro, campo de cambios y estado de sincronización: last_max_recno, last_changed_value), employees (importados de nómina; user_id único al crear el usuario desde nómina) y columnas en clinic_patients: external_source, external_id (único con la fuente), source_name, name_split, synced_at. Ver docs/CONEXIONES-EXTERNAS.md.

Agenda médica (migración 036, v4.7.0)

backend/database/migrations/036_clinic.sql y 037_clinic_traceability.sql: aplicadas en dev (2026-10-28/29), pendientes en producción (las aplica el actualizador). Usa utf8mb4, FKs hacia users, clinic_staff, pacientes y tipos, e índices de rangos por médico y por paciente. DATETIME se interpreta en hora civil de America/Guayaquil.

Tabla Uso
clinic_staff permiso all/own, condición de médico, especialidad, color, activo, versión
clinic_patients identificación única opcional, contacto mínimo, nacimiento, nota corta, versión
clinic_schedules tramos por médico y día de semana, lunes=0
clinic_appointment_types nombre, duración, color y estado
clinic_appointments cita, médico, paciente, tipo, estado, canal, cancelación y versión; autor fijo (created_by, created_by_kind user/ai_agent, created_by_name) y borrado lógico (deleted_at/by/by_name/reason) — migración 037
clinic_blocks bloqueo de médico, clase y recurrencia con las reglas de reuniones; created_by_name y borrado lógico (deleted_at/by/by_name)

La reserva bloquea la fila clinic_staff del médico y la del paciente antes de verificar choques; los cambios de horario y bloqueos se serializan por la misma fila del médico. La migración crea una fila inicial all para el superadmin más antiguo; el resto se habilita desde la UI. No hay migración aplicada ni conexión a BD en este cambio.

Fuente de verdad

  • La base ya existe: sí (creada en dev el 2026-09-03, usuario project_linkedcorp).
  • Archivo SQL base: backend/database/migrations/ (SQL idempotente, numerado, CREATE TABLE IF NOT EXISTS).
  • Seeds: backend/database/seeds/ (roles, catálogos, usuario admin de dev).

Tablas principales (MVP)

Tabla Contenido
roles catálogo fijo de 6 roles (superadmin, admin, it_manager, project_lead, collaborator, viewer)
users usuarios, con rol, departamento y ciclo de vida de registro (status, tokens)
system_settings clave/valor de configuración del sistema (hoy: SMTP, clave cifrada)
departments departamentos/áreas, jerarquía simple vía parent_id
project_types catálogo de tipos de proyecto
priorities catálogo de prioridades (baja/media/alta/crítica)
project_statuses catálogo de estados de proyecto
activity_statuses catálogo de estados de actividad
projects proyectos
project_members usuarios asignados a un proyecto (reemplaza "equipos" como módulo aparte)
activities actividades de un proyecto
activity_dependencies dependencias simples entre actividades ("depende de")
tasks checklist liviano dentro de una actividad ("Tareas" en la UI; renombrada desde subtasks en v1.8.0)
recurring_tasks recordatorios periódicos (ej. renovación de licencias), menú propio "Tareas recurrentes"
meetings serie de reunión en hora civil America/Guayaquil; UID iCalendar, secuencia, versión y cancelación lógica
meeting_participants / meeting_external_participants usuarios con RSVP y correos externos sin respuesta
meeting_exceptions ocurrencias canceladas, sin materializar el resto de la serie
meeting_reminders clave (meeting_id, occurrence_start) para deduplicar avisos
comments comentarios sobre proyecto o actividad (entity_type + entity_id)
attachments archivos adjuntos a proyecto o actividad
notifications notificaciones in-app por usuario
audit_log auditoría central (quién, qué, sobre qué, cuándo)
error_logs errores de backend (excepciones 500) y frontend (JS/promesas sin catch), panel "Errores del sistema" en Configuración
notes apuntes rápidos personales, menú propio "Notas"; editable solo por el dueño
note_shares con quién está compartida (solo lectura) cada nota — note_id, user_id
vault_entries repositorio de credenciales privado por usuario (menú "Contraseñas"), contraseña/notas cifradas
devices inventario de equipos (menú "Equipos"), upsert por deviceid del agente GLPI; baja lógica con status
device_cpus / device_memories / device_disks / device_volumes / device_networks / device_monitors / device_software componentes de cada equipo, reemplazados completos en cada reporte
device_accounts cuentas que reporta el agente: sesión iniciada, usuarios locales y grupos locales (kind), reemplazadas en cada reporte (migración 027)
device_changes diff entre reportes (RAM, discos, software instalado/desinstalado)
system_settings (IA) ai_api_key_enc (AES-256-GCM), ai_model, ai_edit_version (migración 031)
ai_usage consumo por llamada a la IA (user_id, action, model, input_tokens, output_tokens, cost_usd estimado, created_at) (migración 032)
user_modules acceso por módulo y usuario (user_id, module, enabled, updated_by); sin fila = habilitado (migración 030)
device_viewers usuarios con acceso de solo lectura compartido al menú Equipos (user_id, granted_by)
inventory_tokens tokens de ingesta del agente (solo hash sha256)

Relaciones clave

  • users.role_id -> roles.id, users.department_id -> departments.id
  • departments.parent_id -> departments.id (jerarquía opcional, ej. TI → Desarrollo)
  • projects.status_id -> project_statuses.id, projects.priority_id -> priorities.id, projects.project_type_id -> project_types.id, projects.owner_user_id -> users.id, projects.requesting_department_id / responsible_department_id -> departments.id
  • project_members.project_id -> projects.id, project_members.user_id -> users.id
  • activities.project_id -> projects.id, activities.status_id -> activity_statuses.id, activities.priority_id -> priorities.id, activities.responsible_user_id -> users.id
  • activity_dependencies.activity_id / depends_on_activity_id -> activities.id
  • tasks.activity_id -> activities.id, tasks.responsible_user_id -> users.id (nullable)
  • comments.entity_id / attachments.entity_id apuntan a projects.id o activities.id según entity_type (sin FK física cruzada — se valida en el servicio)

Reglas de datos

  • projects.progress_pct y activities.progress_pct son calculados por el servicio, no editables directo por el cliente: actividad con tareas = % de tareas hechas; actividad sin tareas = manual (0-100); proyecto = promedio del avance de sus actividades.
  • users.password_hash con bcrypt, nunca texto plano ni reversible.
  • users.session_version (migración 028) comienza en 1 y aumenta al restablecer clave; cada JWT de sesión debe tener la misma versión. Cambiar clave desde la aplicación conserva la sesión actual y revoca las demás.
  • user_sessions (migración 033) guarda cada sesión por public_id (el sid del JWT), usuario, IP, agente, inicio, última actividad, vencimiento y revocación. Su FK a users usa ON DELETE CASCADE. El backend limpia al iniciar sesión las filas vencidas o revocadas hace más de 30 días. Aplicar 033_user_sessions.sql antes de iniciar el backend nuevo: los JWT anteriores, sin sid, exigirán un nuevo inicio de sesión.
  • Migración 029: edit_version comienza en 1 en proyectos, comentarios, actividades, tareas, departamentos, usuarios, catálogos, notas, tareas recurrentes y equipos. El UPDATE de edición verifica la versión y la incrementa atómicamente; sin coincidencia responde 409. En system_settings, las claves company_edit_version y smtp_edit_version serializan los guardados de cada panel dentro de una transacción. Aplicar 029 antes de iniciar el backend nuevo.
  • users.status (pending_verification/pending_approval/active/invited) es el ciclo de vida de la cuenta, independiente de is_active (que sigue siendo el interruptor manual del admin para desactivar a alguien ya activo). Un usuario pending_approval rechazado se borra físicamente (no hay estado "rechazado" — se audita el email en audit_log antes de borrar).
  • invited: cuenta placeholder creada al agregar por correo a alguien que aún no es usuario desde Miembros de un proyecto (name=email, password_hash aleatorio inservible, reutiliza verification_token/verification_token_expires — 7 días en vez de 24h). Al aceptar la invitación (POST /api/auth/accept-invite) pasa directo a active sin pasar por pending_approval — a diferencia del autorregistro público, aquí quien lo invitó ya lo vetó al agregarlo a su proyecto. La fila de project_members se crea en el momento de invitar, no al aceptar, así que en cuanto acepta ya aparece como miembro sin pasos adicionales.
  • recurring_tasks: project_id/responsible_user_id son opcionales (NULL) — se puede crear directo desde "Tareas recurrentes" o promoviendo una tarea puntual de una actividad (project_id queda como referencia del proyecto de origen, la tarea puntual original NO se borra). last_reminder_sent_at se limpia a NULL cada vez que se completa un ciclo (POST /:id/complete, que suma interval_value/interval_unit a next_due_date) — es lo que evita reenviar el correo dos veces en el mismo ciclo. El envío real lo hace un job en el propio proceso del backend (setInterval cada 15 min, sin cron del SO ni cola nueva).
  • system_settings.value para smtp_pass_enc va cifrado (AES-256-GCM, ENCRYPTION_KEY del .env) — nunca en texto plano, nunca se devuelve por la API (solo configured: true/false).
  • system_settings: company_name/company_logo_filename/company_logo_mime — marca configurable (nombre y logo de la empresa, GET /api/company público, PUT solo superadmin), mismo patrón clave/valor que SMTP, sin tabla nueva. El archivo del logo vive en STORAGE_ROOT/branding/, un solo archivo a la vez (se borra el anterior al subir uno nuevo). Sin valor guardado = "LinkedCorp" y logo estático embebido (comportamiento por defecto, no rompe instalaciones existentes).
  • activities.sort_order/tasks.sort_order: orden manual (arrastrar con el mouse en la UI). 0 = nunca reordenado a mano (la migración 012 lo inicializa con el id para preservar el orden previo); el endpoint de reordenar siempre escribe posiciones 1..N, nunca 0, para que esa migración siga siendo idempotente en deploys futuros sin pisar un orden real ya guardado.
  • Borrado lógico donde aplique (users.is_active); nunca DELETE físico de usuarios activos con historial (los pending_approval rechazados sí se borran, ver arriba).
  • projects.deleted_at/deleted_by: eliminación lógica (quien puede editar el proyecto puede "eliminarlo" — se oculta de listados/dashboard, sigue en BD). Borrado físico (DELETE, arrastra actividades/comentarios/adjuntos por ON DELETE CASCADE) solo vía permanentlyDeleteProject, solo superadmin, y solo sobre un proyecto que ya tiene deleted_at (dos pasos, nunca directo).
  • activities.deleted_at/deleted_by y tasks.deleted_at/deleted_by (desde v2.5.0): antes se borraban físico sin poder recuperar; ahora es lógica igual que projects — quien tiene permiso de Escritura en el proyecto puede eliminarlas y restaurarlas (POST .../restore), sin borrado físico ni endpoint de purga definitiva (no hizo falta, a diferencia de projects).
  • project_members.role_in_project: 'owner' (dueño, se asigna solo al crear el proyecto, nunca se toca desde el endpoint de rol), 'write' (permiso de Escritura: crear/editar/eliminar (lógico) actividades y tareas del proyecto — nunca el proyecto en sí), NULL = Lectura (solo ver). Compartir un proyecto (agregar/quitar miembros, cambiar su nivel) es exclusivo del dueño (projects/service.js#canManageMembers) — ni un rol global de administración puede hacerlo por él. 'admin' es un valor legado (de un tercer nivel "administrador de proyecto" que ya no existe) que se sigue respetando como Escritura en filas viejas, pero no se asigna desde la UI. Los roles superadmin/admin/it_manager ven, editan y pueden borrar (lógico) cualquier proyecto igual que antes (soporte/supervisión) — la única acción que queda exclusiva del dueño, sin excepción ni para ellos, es compartir/dejar de compartir el proyecto.
  • Empresa asumida única (no hay tabla companies en el MVP).
  • Producción siempre usa 192.168.222.32. Desarrollo siempre usa 192.168.222.20. No mezclar.
  • error_logs: sin límite de tiempo, pero se recorta solo a los 1000 registros más recientes en cada inserción (evita crecimiento sin control si algo entra en loop de errores). Se puede vaciar del todo desde el panel ("Limpiar registro"). severity (alta/media/baja) nunca se etiqueta a mano: backend y errores de render de React = alta, errores JS sueltos = media, promesas rechazadas sin manejar = baja.
  • vault_entries (desde v2.7.0): password_enc/notes_enc guardan el resultado de encrypt() (AES-256-GCM, backend/src/utils/crypto.js, mismo mecanismo ya usado para la clave SMTP) — nunca texto plano. Privado por usuario, sin excepción (ni superadmin ve lo de otro). El acceso al menú "Contraseñas" además pide un PIN de 4 dígitos (users.vault_pin_hash, bcrypt) — el PIN es un candado de acceso, NO participa del cifrado (sería débil para eso). Cambiar/resetear el PIN (incluso por correo, vault_pin_reset_token) nunca toca vault_entries — es un cambio de candado, no una recuperación de datos. Cada vez que se revela/crea/edita/elimina una entrada, o se desbloquea el candado, queda en audit_log. Compartir una entrada con otro usuario queda para una fase 2 (no implementado).
  • vault_entries.icon_key (desde v2.7.2): ícono elegido por el usuario para la tarjeta (catálogo fijo, validado también en el backend — vault/service.js#ICON_KEYS debe coincidir siempre con el del frontend). A propósito NO se usa ningún servicio externo de favicon (revelaría a terceros qué URLs internas tiene guardadas cada usuario). NULL = sin elegir, el frontend muestra la inicial del nombre.

Riesgos o decisiones

  • Sin triggers de BD para recalcular avances: toda la lógica vive en la capa service/ del backend (más simple de depurar y de extender en fase 2).
  • risk_level es un campo simple en projects (bajo/medio/alto) — el módulo completo de Riesgos (probabilidad/impacto/mitigación) queda para fase 2, sin necesitar migrar este campo.

Archivos relacionados

  • backend/.env.example
  • backend/database/migrations/*.sql
  • backend/database/seeds/*.sql

Crear la base de datos en DESARROLLO (.20)

Ya ejecutado para este proyecto (2026-09-03):

bash /home/mgonzalez/desarrollo/project-md-templates/crear-db-dev.sh project_linkedcorp project_linkedcorp

BD project_linkedcorp y usuario project_linkedcorp@192.168.222.40 creados en 192.168.222.20. Las credenciales están en backend/.env (no versionado).

Verificar:

mysql --defaults-extra-file=~/.config/mysql/dev20-admin.cnf -e "SHOW DATABASES LIKE 'project_linkedcorp';"

Aplicar migraciones/seeds (una vez creado backend/src/scripts/migrate-db.js):

cd backend
set -a && source .env && set +a
node src/scripts/migrate-db.js
node src/scripts/seed-db.js

Setup de base de datos en producción — ejecutado el 2026-09-06

BD project_linkedcorp creada en .32 con crear-db-prod.sh (grant a 192.168.222.3). Detalle del proceso y estado real en PRODUCCION.md.

Migración en el servidor de aplicaciones (.3):

ssh deploy@192.168.222.3 -p 2203
cd /opt/apps/project.linkedcorp.net/source/backend
set -a && source /opt/apps/project.linkedcorp.net/backend/.env && set +a
node src/scripts/migrate-db.js
node src/scripts/seed-db.js          # solo catálogos/roles; sin datos de ejemplo en produccion
node src/scripts/create-admin.js "Nombre" correo@dominio   # solo la primera vez, bootstrap manual

Actualizaciones de esquema en producción

Método: archivos SQL idempotentes en backend/database/migrations/00X-nombre.sql (CREATE TABLE IF NOT EXISTS / information_schema guards), aplicados con node src/scripts/migrate-db.js. El runner lee todos los .sql, los ordena por nombre y los ejecuta todos en cada corrida; no lleva tabla de historial. Por eso 033_user_sessions.sql usa CREATE TABLE IF NOT EXISTS. Siempre backup (mysqldump --single-transaction) antes de migrar en producción.

  • Equipos (desde v2.15.0, migración 023_devices.sql): devices.deviceid UNIQUE; assigned_user_id y department_id son datos manuales (el agente nunca los toca). Hijos con ON DELETE CASCADE. device_software.is_component (migración 024) marca actualizaciones/runtimes/drivers/paquetes del sistema (según system_category del agente + nombre, ver normalizer.js#isComponent); las vistas de software lo ocultan salvo que se marque la casilla. inventory_tokens guarda el hash (token_hash, valida la ingesta) y el token cifrado (token_enc, migración 025, AES-256-GCM con ENCRYPTION_KEY) para regenerar el instalador.

Cambio de correo (migración 035, no aplicada)

email_change_requests guarda usuario, correo anterior/nuevo, estado, decisión, motivo y fechas. Los tokens de aprobación y confirmación se guardan solo como hash SHA-256. Índices únicos sobre columnas generadas impiden más de una solicitud abierta por usuario o correo nuevo; el cotejamiento utf8mb4_unicode_ci evita duplicados por mayúsculas. notifications.entity_type agrega email_change_request y conserva project, activity y meeting. El migrador reejecuta todos los archivos; la tabla se crea con IF NOT EXISTS.

  • devices.cpu_year / cpu_year_for (migración 039): año del procesador consultado a la IA (solo por el nombre del procesador); se ignora si el procesador reportado cambia.