Plataforma web para automatizar y optimizar el proceso de gestión de citas médicas del Hospital Regional Traumatológico y Quirúrgico Prof. Juan Bosch (HTQPJB) en La Vega, República Dominicana.
Trabajo de grado para optar por el título de Licenciatura en Informática — Universidad Nacional Pedro Henríquez Ureña (UNPHU), Recinto La Vega.
- Sobre el proyecto
- Características principales
- Arquitectura
- Stack tecnológico
- Evolución del stack tecnológico
- Inicio rápido
- Despliegue con TLS (HTTPS)
- Estructura del proyecto
- Casos de uso
- Roles y permisos
- Endpoints REST
- Variables de entorno
- Migraciones de base de datos
- Pruebas automatizadas
- Integración continua
- Comandos útiles
- Documentación
- Respaldos del sistema
- Operación sin internet (offline)
- Cumplimiento legal
- Autores
- Versión
El SGCM automatiza el proceso de gestión de citas que el HTQPJB realizaba previamente con registros físicos, libretas y comunicación verbal. La plataforma reemplaza esos soportes manuales por una solución digital centralizada que opera en la intranet del hospital.
Lo que el sistema NO es: una reingeniería del proceso institucional. La lógica operativa, los roles y las responsabilidades del personal se conservan.
Lo que el sistema SÍ es: la automatización de los puntos del flujo donde los soportes manuales generaban duplicaciones, demoras y descoordinación.
- Autenticación JWT con hashing bcrypt y expiración configurable.
- Control de acceso basado en roles (RBAC) en dos capas: backend y frontend.
- Calendario interactivo con FullCalendar.js, vista mensual por defecto.
- Prevención física de duplicaciones mediante índice único parcial en PostgreSQL.
- Bloqueo temporal del registro de consulta: el médico no puede registrar diagnósticos antes de la fecha de la cita.
- Vinculación de usuarios con perfiles de médico desde la UI del administrador.
- Reset de contraseña por administrador (sin requerir la contraseña anterior; auditado).
- Reportes PDF generados con WeasyPrint, con fecha de emisión, numeración secuencial y trazabilidad del usuario que los genera.
- Reportes administrativos: resumen y PDF de usuarios por rol, detalle y PDF de médicos activos con estadísticas.
- Agenda extendida para secretaria/admin con filtros por médico, rango de fechas, estado y especialidad; exportación a PDF y Excel; impresión optimizada (
@media print). - Sistema de respaldos (CU-16) con tres modalidades: local, externo (USB/UNC) y andamiaje preparado para nube (Amazon S3, Google Cloud Storage, Azure Blob).
- Gestión de catálogos administrables (CU-17): el catálogo de especialidades vive en BD, se administra desde el panel admin y se valida en backend.
- Auditoría transaccional de todas las operaciones críticas (Ley 172-13).
- Suite de pruebas automatizadas con pytest.
- CI/CD con GitHub Actions ejecutando linter, tests y construcción de imagen Docker.
- Despliegue contenedorizado con Docker Compose.
- Máscaras visuales en cédula y teléfono para mejor experiencia de usuario.
flowchart TB
Browser[Navegador del usuario]
subgraph Server["Servidor en intranet del hospital"]
Nginx[Nginx<br/>puertos 80 redirige a 443<br/>TLS + proxy + estaticos]
API[FastAPI<br/>puerto 8000<br/>backend]
DB[(PostgreSQL<br/>puerto 5432<br/>base de datos)]
end
Browser -->|HTTPS 443| Nginx
Nginx -->|/api/v1/*| API
Nginx -->|archivos HTML/JS/CSS| Browser
API <-->|SQL| DB
- Python 3.11+ — Lenguaje principal.
- FastAPI — Framework web asíncrono con OpenAPI automático.
- SQLModel — ORM combinado con validación Pydantic.
- Alembic — Migraciones versionadas del esquema.
- python-jose — Generación y validación de JWT.
- passlib + bcrypt — Hashing seguro de contraseñas.
- WeasyPrint — Generación de PDFs desde HTML/CSS.
- pytest + httpx — Pruebas automatizadas.
- PostgreSQL 15 — Con índices únicos parciales para prevenir duplicaciones.
- HTML5 + JavaScript ES6 — Vanilla, sin framework.
- Tailwind CSS — Servido localmente desde
static/vendor/tailwind/. - FullCalendar.js 6.1.15 — Calendario interactivo (también local).
- Lucide Icons — Iconografía servida desde
static/vendor/lucide/. - Fuente Inter — Archivos
.woff2locales (static/fonts/inter/).
Sin internet: todas las dependencias del frontend viajan en el repositorio. El sistema funciona completo dentro de la intranet del HTQPJB aunque el servidor no tenga salida a internet. Ver
docs/OFFLINE.md.
- Docker + Docker Compose — Orquestación de contenedores.
- Nginx — Proxy inverso, servidor de archivos estáticos y terminación TLS (puerto 80 redirige a 443).
- openssl — Generación de certificados TLS autofirmados para la intranet (
scripts/gen-cert.shyscripts/gen-cert.ps1). - GitHub Actions — Integración continua.
El stack actual no fue el original. Durante las fases de análisis y diseño, y a lo largo de las primeras iteraciones de desarrollo, el proyecto evolucionó significativamente conforme fui descubriendo limitaciones de las herramientas iniciales y conociendo alternativas más apropiadas para los requisitos del sistema. Esta sección documenta esa evolución, porque entender por qué algo cambió es tan importante como saber qué se está usando hoy.
| Componente | Idea inicial | Stack actual | Razón del cambio |
|---|---|---|---|
| Base de datos | SQLite | PostgreSQL 15 | SQLite no soporta índices únicos parciales con cláusula WHERE, característica clave del diseño para resolver el conflicto entre cancelación y reagendamiento de citas. Tampoco está pensado para concurrencia real con múltiples secretarias agendando al mismo tiempo. |
| Framework backend | Flask | FastAPI | Flask es minimalista pero requiere construir manualmente validación de datos, documentación OpenAPI y manejo asíncrono. FastAPI lo trae todo de fábrica con Pydantic, genera Swagger automáticamente y ofrece un rendimiento muy superior gracias a Starlette y al soporte nativo de async/await. |
| ORM | SQLAlchemy puro | SQLModel sobre SQLAlchemy y Pydantic | SQLModel unifica los modelos de base de datos con los esquemas de validación en una sola capa. Reduce código duplicado y mantiene la potencia de SQLAlchemy por debajo. |
| Frontend | HTML, JavaScript y Tailwind CSS | Igual, más FullCalendar.js | Se mantuvo el enfoque vanilla por su simplicidad y por evitar la complejidad de un framework para un sistema de este alcance. Se incorporó FullCalendar.js como pieza clave para la visualización del calendario interactivo. |
| Entorno de operación | Servidor local con Docker | Igual | Decisión validada y mantenida. La contenedorización facilita despliegue, reproducibilidad y portabilidad. |
| Control de versiones | Git | Git más GitHub Actions | Se añadió integración continua: cada commit ejecuta tests automatizados y construye la imagen Docker, garantizando que el código en el repositorio siempre sea desplegable. |
| Diseño visual | Draw.io | Draw.io | Se mantiene como herramienta de planificación de diagramas (DFD, casos de uso, MER). |
Algunos componentes no estaban en la planificación inicial pero se sumaron al darme cuenta de su importancia:
- Alembic para migraciones versionadas del esquema de base de datos. Sin él, cualquier cambio de modelo implicaba intervenciones manuales propensas a errores.
- WeasyPrint para generación de reportes PDF a partir de HTML y CSS, lo que permite reutilizar templates del frontend para reportes profesionales.
- python-jose y passlib (bcrypt) para JWT y hashing de contraseñas. La autenticación inicial se pensó simple, pero al estudiar la Ley 172-13 quedó claro que se requería hashing fuerte y tokens firmados.
- Nginx como proxy inverso. Originalmente se contemplaba que FastAPI sirviera todo (API y archivos estáticos), pero al separar responsabilidades se obtuvo mejor rendimiento, terminación SSL más limpia y un punto de entrada único.
- pytest y httpx para una suite de pruebas automatizadas. Esto no estaba en el plan original; surgió de la necesidad de evitar regresiones al iterar el sistema.
- GitHub Actions para CI/CD. Práctica que adopté tras enfrentar el primer fallo en pruebas que un test automatizado habría evitado.
- ruff como linter para análisis estático del código.
Porque demuestra dos cosas que considero importantes en cualquier proyecto técnico serio:
-
El stack no se eligió por moda, sino por requisitos. Cada cambio respondió a una limitación concreta encontrada durante el desarrollo o a una característica específica que el sistema necesitaba (por ejemplo, los índices únicos parciales de PostgreSQL que SQLite no soporta).
-
El proyecto evolucionó con disciplina. Cambiar de Flask a FastAPI o de SQLite a PostgreSQL en mitad del desarrollo no es trivial: implicó reescribir partes considerables del código. Fueron decisiones tomadas tempranamente, antes de que el costo del cambio fuera prohibitivo, lo cual es una habilidad fundamental en ingeniería de software.
El stack actual no es perfecto ni final. Hay áreas donde reconozco oportunidades de mejora futura (tests end-to-end con Playwright, observabilidad con Prometheus, despliegue con Kubernetes para múltiples instancias), pero corresponden a un alcance mayor al de un trabajo de grado y quedan documentadas como recomendaciones de evolución.
- Docker Engine 24+ y Docker Compose v2.
- Git 2.30+.
# 1. Clonar el repositorio
git clone https://github.com/je7remy/HTQ_Citas.git sgcm
cd sgcm
# 2. Configurar variables de entorno
cp .env.example .env
# Editar .env y configurar JWT_SECRET_KEY (mínimo 32 caracteres)
# 3. Generar el certificado TLS autofirmado (requerido por Nginx)
# Linux / macOS:
./scripts/gen-cert.sh # CN=sgcm.intranet
# o con la IP/hostname real del servidor: ./scripts/gen-cert.sh 10.0.0.15
# Windows PowerShell (requiere openssl en el PATH):
# .\scripts\gen-cert.ps1
# 4. Arrancar el sistema
docker compose up -d --build
# 5. Verificar
docker compose psAcceder a https://localhost/ en el navegador. El puerto 80 redirige automáticamente a 443. La primera vez el navegador mostrará una advertencia de certificado (esperado, ver Despliegue con TLS).
El comando python -m app.scripts.seed_db (también ejecutable en automático con SGCM_SEED=true) crea el siguiente conjunto de cuentas:
| Rol | Contraseña | |
|---|---|---|
| Administrador | admin@htqpjb.gob.do |
Admin123! |
| Secretaria | secretaria.maria@htqpjb.gob.do |
Secretaria123! |
| Secretaria | secretaria.juana@htqpjb.gob.do |
Secretaria123! |
| Secretaria | secretaria.elena@htqpjb.gob.do |
Secretaria123! |
| Secretaria | secretaria.rosa@htqpjb.gob.do |
Secretaria123! |
| Médico (Ortopedia) | dr.jperez@htqpjb.gob.do |
Medico123! |
| Médico (Medicina Interna) | dra.aramirez@htqpjb.gob.do |
Medico123! |
| Médico (Cirugía General) | dr.cgarcia@htqpjb.gob.do |
Medico123! |
| Médico (Oftalmología) | dra.lcastillo@htqpjb.gob.do |
Medico123! |
| Médico (Neurocirugía) | dr.rsantos@htqpjb.gob.do |
Medico123! |
| Médico inactivo (prueba) | dr.inactivo@htqpjb.gob.do |
Medico123! |
Importante: cambiar inmediatamente todas las contraseñas tras la primera puesta en marcha en producción.
El módulo app.db.seed puebla la base de datos del HTQPJB con datos realistas para que el sistema sea usable desde el primer arranque. Cada función es idempotente: ejecutarla dos veces no duplica datos.
| Entidad | Cantidad | Detalles |
|---|---|---|
| Usuarios | 11 | 1 admin + 4 secretarias + 5 médicos activos + 1 médico inactivo |
| Médicos | 9 (8 activos + 1 inactivo) | Cubren 11 de las 18 especialidades oficiales |
| Horarios | L-V mañana/tarde y sábados mañana | 7:00–12:00 y 14:00–17:00 entre semana; 8:00–12:00 los sábados |
| Pacientes | 40 | Cédulas dominicanas con dígito verificador real, edades 5–85, los 4 valores de sexo |
| Citas | 50 | 30% pendientes futuras, 30% atendidas pasadas, 10% canceladas, 30% próximos 3 días |
| Consultas | ≥15 | Una por cada cita atendida, con los 5 campos clínicos plausibles según especialidad |
| Auditoría | Coherente | Cada inserción del seed deja un registro en auditoria |
# Seed completo (idempotente, no duplica)
docker exec sgcm_api python -m app.scripts.seed_db
# Resetear datos y volver a sembrar (peligroso, solo dev)
docker exec sgcm_api python -m app.scripts.seed_db --reset
# Sembrar solo una sección
docker exec sgcm_api python -m app.scripts.seed_db --solo pacientesSecciones válidas para --solo: usuarios, medicos, horarios, pacientes, citas, consultas.
El contenedor sgcm_api corre docker-entrypoint.sh antes de iniciar el servidor. Si la variable de entorno SGCM_SEED=true está definida (en .env), el seed se ejecuta automáticamente en cada docker compose up. Por defecto SGCM_SEED=false para evitar que el seed se ejecute en producción.
# .env para entornos de desarrollo o demos
SGCM_SEED=trueIdempotencia y producción: ejecutar el seed en una BD ya poblada no es destructivo — cada función comprueba antes de insertar. Aun así, lo recomendado en producción es dejar
SGCM_SEED=falsey poblar manualmente solo la primera vez.
El SGCM termina TLS en Nginx. El tráfico del navegador llega cifrado por
HTTPS (puerto 443); el puerto 80 solo redirige a HTTPS. El hop interno
Nginx→FastAPI viaja por la red privada del docker compose y no se expone
al exterior.
Como el sistema opera en la intranet aislada del HTQPJB, se usa un certificado autofirmado. No hay CA pública porque el servidor no es accesible desde internet.
Linux / macOS / dentro de Docker:
./scripts/gen-cert.sh # CN=sgcm.intranet (por defecto)
./scripts/gen-cert.sh 10.0.0.15 # usar la IP real del servidor
./scripts/gen-cert.sh sgcm.hospital # o un hostname de la intranetWindows (PowerShell), requiere openssl de Git for Windows en el PATH:
.\scripts\gen-cert.ps1
.\scripts\gen-cert.ps1 -HostName 10.0.0.15Esto crea nginx/certs/sgcm.crt y nginx/certs/sgcm.key. Ambos están en
.gitignore: la llave privada nunca se versiona.
docker compose up -d --buildAcceder desde https://<host-o-ip-del-servidor>/. El puerto 80 redirige
automáticamente a 443.
Al ser autofirmado, el navegador mostrará "conexión no privada" la primera vez. Es lo normal en intranet. Dos opciones:
- Aceptar la excepción en cada equipo (rápido, válido para pruebas).
- Distribuir el
sgcm.crtcomo certificado de confianza vía GPO / directiva de dominio (recomendado en producción; lo gestiona el Departamento de Informática del HTQPJB). Así desaparece la advertencia.
El certificado dura 825 días (máximo aceptado por navegadores modernos sin CA pública). Antes de que expire, volver a correr el script del paso 1 y reiniciar Nginx:
docker compose restart nginxSi el hospital tiene una CA interna, basta colocar el certificado firmado
por esa CA como nginx/certs/sgcm.crt (con su cadena) y la llave como
nginx/certs/sgcm.key, y reiniciar Nginx. No hay que tocar default.conf.
sgcm/
├── app/ Backend Python
│ ├── api/v1/endpoints/ Endpoints REST
│ │ ├── auth.py
│ │ ├── usuarios.py CRUD + reset de password por admin
│ │ ├── pacientes.py CRUD + historial-medico
│ │ ├── medicos.py CRUD, especialidades, /buscar, /proxima-disponibilidad
│ │ ├── citas.py CRUD + agenda-extendida + feed FullCalendar
│ │ ├── consultas.py Registro de consultas (rol médico)
│ │ ├── reportes.py Citas (PDF) y agenda extendida (PDF/Excel)
│ │ ├── reportes_admin.py Reportes administrativos (usuarios y médicos)
│ │ ├── especialidades.py CU-17 — CRUD del catálogo de especialidades
│ │ ├── respaldos.py CU-16 — gestión de respaldos
│ │ └── auditoria.py
│ ├── core/ Configuración y seguridad (JWT, bcrypt)
│ ├── db/ Sesión SQLAlchemy + seeders (seed.py)
│ ├── models/ Modelos SQLModel (tablas)
│ ├── scripts/ CLI: seed_db.py (pobla datos del HTQPJB)
│ ├── services/ Lógica de negocio
│ │ ├── audit.py Auditoría transaccional
│ │ ├── citas_service.py Validaciones de disponibilidad
│ │ └── backup/ CU-16 — Strategy: local / externo / nube
│ │ ├── manager.py Orquesta pg_dump → SHA-256 → estrategia
│ │ ├── local.py
│ │ ├── externo.py
│ │ └── nube/ Stubs S3, GCS y Azure
│ ├── templates/reportes/ Templates HTML para PDFs
│ └── main.py Punto de entrada FastAPI
├── docker-entrypoint.sh Inicializa esquema y ejecuta seed si SGCM_SEED=true
├── alembic/versions/ 7 migraciones (0001-0007); la 0007 crea `especialidades`
├── frontend/
│ ├── static/
│ │ ├── css/sgcm.css Sistema de diseño (incluye `@media print`)
│ │ ├── js/app.js Módulo JS común (SGCM) + NAV_ITEMS
│ │ ├── vendor/ Tailwind, FullCalendar, Lucide, xlsx (offline)
│ │ └── fonts/inter/ Fuente Inter local (.woff2)
│ └── templates/ Páginas HTML
│ ├── login.html
│ ├── calendar.html Calendario y modales de cita
│ ├── pacientes.html
│ ├── medicos.html Médicos y modal de edición
│ ├── agenda.html Agenda del rol médico
│ ├── agenda-secretaria.html Agenda extendida con filtros, PDF/Excel/print
│ ├── usuarios.html Gestión de usuarios (admin)
│ ├── especialidades.html Gestión del catálogo de especialidades CU-17 (admin)
│ ├── reportes-usuarios.html Reportes administrativos (admin)
│ ├── respaldos.html Panel de respaldos CU-16 (admin)
│ └── auditoria.html Log de auditoría (admin)
├── nginx/ Configuración Nginx (default.conf con TLS) + certs/ (gitignored)
├── scripts/
│ ├── init.sql DDL inicial (volumen Postgres)
│ ├── gen-cert.sh Generador de cert TLS autofirmado (Linux/macOS)
│ └── gen-cert.ps1 Generador de cert TLS autofirmado (Windows)
├── docs/ Guías operativas (BACKUPS.md, OFFLINE.md)
├── tests/ Suite pytest
├── .github/workflows/ci.yml CI con GitHub Actions
├── docker-compose.yml Orquestación (incluye volumen sgcm_backups)
├── Dockerfile Imagen del backend (incluye postgresql-client)
├── requirements.txt Dependencias Python (incluye openpyxl)
└── .env.example Plantilla de variables
El sistema implementa los 17 casos de uso definidos en el análisis:
| ID | Caso de uso | Roles |
|---|---|---|
| CU-01 | Iniciar sesión | Todos |
| CU-02 | Cerrar sesión | Todos |
| CU-03 | Registrar paciente | Secretaria, Admin |
| CU-04 | Buscar paciente | Secretaria, Admin |
| CU-05 | Editar paciente | Secretaria, Admin |
| CU-06 | Agendar cita | Secretaria, Admin |
| CU-07 | Reprogramar cita | Secretaria, Admin |
| CU-08 | Cancelar cita | Secretaria, Admin |
| CU-09 | Consultar citas | Todos |
| CU-10 | Generar reporte PDF | Secretaria, Admin |
| CU-11 | Ver agenda diaria | Médico |
| CU-12 | Registrar consulta | Médico |
| CU-13 | Gestionar usuarios | Admin |
| CU-14 | Registrar médico | Admin |
| CU-15 | Consultar auditoría | Admin |
| CU-16 | Generar respaldo de la base de datos | Admin |
| CU-17 | Gestionar especialidades | Admin |
| Rol | Permisos |
|---|---|
| Secretaria | Pacientes (CRUD), citas (CRUD), reportes |
| Médico | Solo agenda propia y registro de consultas (con bloqueo temporal) |
| Administrador | Todo lo anterior, más usuarios, médicos, horarios y auditoría |
El RBAC se aplica en dos capas: el backend valida cada petición HTTP independientemente del estado del frontend, garantizando seguridad real. El frontend adapta dinámicamente la UI con data-role para mejor experiencia.
Todas las rutas viven bajo el prefijo /api/v1. La especificación OpenAPI
completa (con esquemas y ejemplos) se expone en /api/v1/docs (Swagger UI).
En la columna Rol se indica el rol mínimo aceptado:
Todos = cualquier usuario autenticado, Staff = secretaria o admin.
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| POST | /auth/login |
público | Devuelve un JWT a partir de email/contraseña |
| GET | /auth/me |
Todos | Datos del usuario autenticado actual |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /usuarios |
Admin | Lista usuarios; filtros ?rol= y ?sin_perfil_medico=true |
| POST | /usuarios |
Admin | Alta de usuario |
| PATCH | /usuarios/{id} |
Admin | Actualiza datos del usuario |
| PATCH | /usuarios/{id}/password |
Admin | Reset de contraseña sin requerir la anterior (auditado) |
| DELETE | /usuarios/{id} |
Admin | Soft delete (preserva FK en citas/auditoría) |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /medicos |
Todos | Lista médicos activos |
| GET | /medicos/especialidades |
Todos | Nombres de las especialidades activas (lectura conveniente para selects) |
| GET | /medicos/buscar?q=&incluir_inactivos= |
Todos | Autocomplete por nombre (limit 20) |
| GET | /medicos/{id}/proxima-disponibilidad |
Todos | Sugerencia de slot libre (granularidad 30 min, horizonte 30 días) |
| POST | /medicos |
Admin | Alta de perfil médico (puede vincular usuario existente) |
| POST | /medicos/con-usuario |
Admin | Crea usuario rol=medico + perfil médico en una transacción |
| PATCH | /medicos/{id} |
Admin | Actualiza perfil médico |
| GET | /medicos/{id}/horarios |
Todos | Lista horarios del médico |
| POST | /medicos/{id}/horarios |
Admin | Alta de horario |
| DELETE | /medicos/horarios/{horario_id} |
Admin | Baja de horario |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /pacientes |
Todos | Lista pacientes |
| GET | /pacientes/{id} |
Todos | Detalle |
| POST | /pacientes |
Staff | Alta (valida cédula dominicana con dígito verificador) |
| PATCH | /pacientes/{id} |
Staff | Actualización |
| GET | /pacientes/{id}/historial-medico?medico_id= |
Todos | Consultas atendidas (DESC), filtro opcional por médico |
| DELETE | /pacientes/{id} |
Staff | Baja |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /citas?desde=&hasta=&id_medico=&estado= |
Todos | Listado básico |
| GET | /citas/agenda-extendida?id_medico=&fecha_desde=&fecha_hasta=&estado=&especialidad=&busqueda_medico= |
Staff | Agenda enriquecida con conteos por estado |
| GET | /citas/calendar?start=&end=&id_medico= |
Todos | Feed en formato FullCalendar |
| POST | /citas |
Staff | Crea cita (valida E-005 / E-006) |
| PATCH | /citas/{id} |
Staff | Reprograma (revalida disponibilidad) |
| DELETE | /citas/{id} |
Staff | Cancela (libera el slot del índice parcial) |
| GET | /consultas/agenda |
Médico | Agenda del médico autenticado |
| POST | /consultas |
Médico | Registra consulta (bloqueo temporal: no antes de la fecha/hora de la cita) |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /reportes/citas.pdf?desde=&hasta=&id_medico= |
Todos | PDF de citas por rango con resumen por estado |
| GET | /reportes/agenda/pdf?id_medico=&fecha_desde=&fecha_hasta=&estado=&especialidad=&busqueda_medico= |
Staff | PDF (A4 horizontal) de la agenda extendida |
| GET | /reportes/agenda/excel?… (mismos filtros) |
Staff | Exportación .xlsx (openpyxl) |
| GET | /reportes/usuarios/resumen |
Admin | JSON con conteos por rol y estado |
| GET | /reportes/usuarios/pdf |
Admin | PDF con resumen + detalle de usuarios + estadísticas adicionales |
| GET | /reportes/medicos/detalle |
Admin | JSON con estadísticas por médico activo |
| GET | /reportes/medicos/pdf |
Admin | PDF con listado de médicos activos y resumen final |
Trazabilidad de generación. Todos los reportes (PDF y Excel) incluyen en la
cabecera la línea Generado por: <nombre del usuario autenticado>, y cada
generación queda registrada en la tabla de auditoría
(tabla_afectada='reportes', accion=CREATE, detalle='Generación de reporte: <tipo>') para saber quién extrajo información del sistema.
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /especialidades?activa=&q= |
Todos | Lista con filtros (devuelve también inactivas para que admin las reactive) |
| POST | /especialidades |
Admin | Alta de especialidad (unicidad case-insensitive, 409 si choca) |
| PATCH | /especialidades/{id} |
Admin | Renombra / desactiva / cambia descripción; el rename propaga a medicos |
| DELETE | /especialidades/{id} |
Admin | Baja sólo si no hay médicos asignados (principal o secundaria) → 409 si está en uso |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| POST | /respaldos |
Admin | Crea respaldo (`tipo=local |
| GET | /respaldos?tipo=&estado=&desde=&hasta=&limit=&offset= |
Admin | Histórico con filtros |
| GET | /respaldos/{id} |
Admin | Detalle de un respaldo |
| DELETE | /respaldos/{id} |
Admin | Elimina el registro (no el archivo físico; auditado) |
| GET | /respaldos/{id}/descargar |
Admin | Descarga el .sql (solo respaldos tipo local) |
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
| GET | /auditoria |
Admin | Bitácora paginada de operaciones críticas |
| GET | /_debug/rutas |
público | Lista las rutas registradas (útil para verificar la versión tras un rebuild) |
| GET | /health (fuera de /api/v1) |
público | Health-check |
La plantilla completa vive en .env.example. A continuación
las variables introducidas en las últimas iteraciones (las demás —JWT_*,
POSTGRES_*, BACKEND_CORS_ORIGINS— están documentadas en el propio
archivo):
| Variable | Default | Descripción |
|---|---|---|
SGCM_SEED |
false |
Si es true, el contenedor api ejecuta app.scripts.seed_db al arrancar |
SGCM_BACKUP_LOCAL_DIR |
/var/backups/sgcm |
Carpeta del servidor donde se almacenan los respaldos locales |
SGCM_BACKUP_EXTERNAL_DIR |
/mnt/backup_externo |
Punto de montaje del medio externo (USB / ruta UNC) para respaldos externos |
SGCM_BACKUP_S3_BUCKET |
(vacío) | Bucket S3 destino — andamiaje, no funcional aún |
SGCM_BACKUP_S3_REGION |
(vacío) | Región AWS — andamiaje, no funcional aún |
SGCM_BACKUP_GCS_BUCKET |
(vacío) | Bucket Google Cloud Storage — andamiaje, no funcional aún |
SGCM_BACKUP_AZURE_CONTAINER |
(vacío) | Contenedor Azure Blob — andamiaje, no funcional aún |
Nota sobre la nube: las tres estrategias (
s3,gcs,azure) están implementadas como stubs (NotImplementedErrorcon mensaje guía). El botón "Nube" de/respaldos.htmlaparece deshabilitado y los endpoints devuelvenfallidoconmensaje_errororientativo. Para activarlas hay que instalar el SDK del proveedor, completar las credenciales y rellenar el cuerpo deapp/services/backup/nube/{s3,gcs,azure}.py. La guía paso a paso está endocs/BACKUPS.md.
El esquema actual está consolidado en scripts/init.sql (se aplica al
crear el volumen sgcm_pgdata por primera vez). Para mantener historial
versionado y permitir upgrades futuros sobre BDs existentes, el proyecto
mantiene 7 migraciones Alembic reversibles en alembic/versions/:
| Revisión | Archivo | Cambio principal |
|---|---|---|
0001 |
0001_initial.py |
Esquema inicial (preexistente, desincronizado con init.sql; no se reaplica) |
0002 |
0002_pacientes_sexo_fecha_nacimiento.py |
pacientes.sexo NOT NULL (CHECK 4 valores) + fecha_nacimiento NOT NULL |
0003 |
0003_medicos_especialidades_secundarias.py |
medicos.especialidad_secundaria_1 y _2 (nullable) |
0004 |
0004_auditoria_nombre_usuario.py |
Denormaliza auditoria.nombre_usuario NOT NULL |
0005 |
0005_consultas_diagnostico_estructurado.py |
5 campos clínicos en consultas (motivo, examen físico, condiciones, tratamiento) |
0006 |
0006_respaldos.py |
Crea la tabla respaldos + 2 índices |
0007 |
0007_especialidades.py |
Crea la tabla especialidades + siembra el catálogo HTQPJB (18 entradas) |
Aplicación:
docker exec sgcm_api alembic upgrade head # despliegue normal sobre BD existente
docker exec sgcm_api alembic current # ver revisión activa
docker exec sgcm_api alembic stamp head # marcar como aplicadas cuando la BD vino de init.sql# Ejecutar todos los tests dentro del contenedor
docker exec sgcm_api pytest -v
# Con cobertura
docker exec sgcm_api pytest --cov=app --cov-report=term-missingLos tests usan SQLite en memoria con StaticPool para aislamiento y velocidad. Cubren autenticación, RBAC, CRUDs, índice único parcial, validación de cédula dominicana, bloqueo temporal del registro de consulta, vinculación de usuarios con perfiles de médico, generación de reportes y auditoría transaccional.
El workflow .github/workflows/ci.yml se ejecuta ante cada push y pull request:
- Job 1: Tests + Lint. Instala dependencias, ejecuta
ruffy la suite completa depytest. - Job 2: Docker build. Construye la imagen Docker validando que el artefacto sea desplegable.
Las versiones de WeasyPrint y pydyf están fijadas explícitamente en requirements.txt para evitar fallos derivados de cambios incompatibles entre versiones menores.
El badge de CI en la cabecera apunta a https://img.shields.io/github/actions/workflow/status/je7remy/HTQ_Citas/ci.yml. Si el repositorio es privado, shields.io no puede leer el estado y mostrará "no status". En ese caso usar el badge nativo de GitHub, que funciona si el visitante está autenticado:
[](https://github.com/je7remy/HTQ_Citas/actions/workflows/ci.yml)Como alternativa siempre disponible, la pestaña Actions del repositorio refleja el estado actual del último run.
| Acción | Comando |
|---|---|
| Arrancar el sistema | docker compose up -d |
| Detener el sistema | docker compose down |
| Ver logs en vivo | docker compose logs -f |
| Reiniciar tras cambios HTML/JS | docker compose restart nginx |
| Reiniciar tras cambios Python | docker compose restart api |
| Reconstruir tras cambios mayores | docker compose up -d --build |
| Ejecutar migraciones | docker exec sgcm_api alembic upgrade head |
| Acceder a la base de datos | docker exec -it sgcm_db psql -U sgcm_user -d sgcm_db |
| Ejecutar tests | docker exec sgcm_api pytest -v |
Las guías operativas detalladas viven bajo docs/ para no inflar este
README:
| Documento | Contenido |
|---|---|
docs/BACKUPS.md |
Flujo completo de respaldos (CU-16), montaje de disco USB en Linux, roadmap de activación de nube con snippets por SDK, restauración con psql / pg_restore, política de retención sugerida y referencia API. |
docs/OFFLINE.md |
Cómo se sirven Tailwind, FullCalendar, Lucide, xlsx y la fuente Inter desde frontend/static/vendor/, política de cache y procedimiento para actualizar una dependencia. |
La especificación OpenAPI viva del backend está siempre disponible en
http://<host>/api/v1/docs (Swagger UI) y http://<host>/api/v1/openapi.json.
⚠️ Si está actualizando desde una versión sin CU-16, ejecute estos tres pasos antes de usar las pantallas (de lo contrario el navegador recibirá errores HTTP, ya que el contenedor api todavía no tienepg_dumpni la tablarespaldos):docker compose build --no-cache api docker compose up -d docker exec sgcm_api alembic upgrade headVerifique con
curl -s http://localhost/api/v1/_debug/rutasque los endpoints/respaldos,/citas/agenda-extendiday/reportes/usuarios/resumenaparecen en la lista.
El SGCM incluye un módulo de respaldos (CU-16, panel /respaldos.html,
solo administrador) con tres modalidades:
- Local — el
.sqlse guarda enSGCM_BACKUP_LOCAL_DIR(por defecto/var/backups/sgcm) del propio servidor. Listo para usarse. - Externo — el
.sqlse copia aSGCM_BACKUP_EXTERNAL_DIR(por defecto/mnt/backup_externo), pensado para disco USB rotativo o ruta UNC. Listo para usarse cuando el medio esté montado en el host. - Nube — andamiaje con stubs para Amazon S3, Google Cloud Storage y Azure Blob Storage. El botón aparece deshabilitado hasta que se instale el SDK del proveedor y se configuren las credenciales.
Cada respaldo:
- Genera un volcado SQL con
pg_dumpcontra el contenedordb. - Calcula SHA-256 del archivo origen.
- Lo entrega al destino usando el patrón Strategy
(
app/services/backup/). - Verifica integridad re-calculando el hash en destino.
- Registra metadatos completos (usuario, tamaño, duración, estado, hash,
error si lo hubiera) en la tabla
respaldos.
| Acción | Endpoint |
|---|---|
| Crear respaldo | POST /api/v1/respaldos |
| Listar histórico | GET /api/v1/respaldos |
| Detalle | GET /api/v1/respaldos/{id} |
| Eliminar registro | DELETE /api/v1/respaldos/{id} |
Descargar .sql (solo local) |
GET /api/v1/respaldos/{id}/descargar |
Variables relevantes del .env:
SGCM_BACKUP_LOCAL_DIR=/var/backups/sgcm
SGCM_BACKUP_EXTERNAL_DIR=/mnt/backup_externo
SGCM_BACKUP_S3_BUCKET=
SGCM_BACKUP_S3_REGION=
SGCM_BACKUP_GCS_BUCKET=
SGCM_BACKUP_AZURE_CONTAINER=La guía operativa completa (montar disco USB, activar respaldo en nube,
restaurar con psql/pg_restore, política de retención sugerida) está
en docs/BACKUPS.md.
El SGCM está diseñado para correr en la intranet del HTQPJB sin necesidad de salida a internet en el servidor. Todas las dependencias del frontend (Tailwind CSS, FullCalendar 6.1.15, Lucide Icons, xlsx 0.18.5 y la fuente Inter) se sirven localmente desde Nginx:
frontend/static/
├── vendor/
│ ├── tailwind/tailwind.min.js (~440 KB)
│ ├── lucide/lucide.min.js (~350 KB)
│ ├── fullcalendar/fullcalendar.min.js (~280 KB)
│ ├── fullcalendar/locales-es.min.js (~1 KB)
│ └── xlsx/xlsx.full.min.js (~860 KB)
└── fonts/inter/
├── inter.css
├── inter-latin.woff2
└── inter-latin-ext.woff2
Procedimiento de verificación, política de cache y cómo actualizar una
dependencia en el futuro: docs/OFFLINE.md.
El sistema cumple con la Ley 172-13 sobre Protección de Datos de Carácter Personal de la República Dominicana mediante:
- Hashing bcrypt de contraseñas (nunca en texto plano).
- Comunicación cifrada HTTPS vía Nginx con certificado SSL.
- Tokens JWT firmados con clave secreta y expiración configurable.
- Auditoría transaccional inmutable de todas las operaciones críticas.
- Control de acceso basado en roles con principio de mínimo privilegio.
Cristopher Rafael Marcial — Matrícula 21-1969
Jeremy José de la Cruz Pérez — Matrícula 21-0266
Asesor: Lic. David D'Oleo
Universidad Nacional Pedro Henríquez Ureña (UNPHU) — Recinto La Vega
Facultad de Ciencias y Tecnología, Escuela de Informática
La Vega, República Dominicana — 2026
SGCM v50.1 — Mayo 2026