34 KiB
🏗️ ARQUITECTURA DE BASE DE DATOS Y GESTIÓN DE CONTENIDO - EMERGES TES
Versión: 1.0
Fecha: 2025-01-XX
Estado: Especificación Técnica - Sin Implementación
Autor: Arquitecto Senior de Software Sanitario
🎯 PROPÓSITO DEL DOCUMENTO
Este documento define LA ARQUITECTURA COMPLETA del sistema de base de datos y gestión de contenido para EMERGES TES. Especifica:
- ✅ Modelo de datos PostgreSQL (tablas, relaciones, índices)
- ✅ API REST mínima (endpoints, contratos)
- ✅ Flujo de sincronización offline-first
- ✅ Panel administrador (alcance, funcionalidades)
- ✅ Límites de modificación (qué NO tocar)
- ✅ Roadmap de implementación por fases
Este sistema permite modificar contenido clínico sin tocar código React de la app operativa.
1️⃣ PRINCIPIOS ARQUITECTÓNICOS
Principio 1: Separación Estricta
┌─────────────────────────────────────┐
│ APP OPERATIVA (PWA - React) │
│ - Solo LEE contenido (pull-only) │
│ - Funciona 100% offline │
│ - NO modifica contenido │
│ - Service Worker intacto │
└─────────────────────────────────────┘
│
│ GET /api/content/*
│ (solo lectura)
▼
┌─────────────────────────────────────┐
│ BACKEND (Node.js + PostgreSQL) │
│ - Almacena contenido versionado │
│ - API REST simple │
│ - Validación clínica │
│ - Historial completo │
└─────────────────────────────────────┘
▲
│ PUT/POST /api/content/*
│ (escritura con auth)
│
┌─────────────────────────────────────┐
│ PANEL ADMINISTRADOR (Web App) │
│ - Edita contenido │
│ - Valida clínicamente │
│ - Gestiona versiones │
│ - App separada │
└─────────────────────────────────────┘
Principio 2: Offline-First Garantizado
La app operativa:
- ✅ Cachea contenido localmente (IndexedDB)
- ✅ Funciona sin red usando cache
- ✅ Sincroniza en background cuando hay red
- ✅ Nunca falla por falta de conexión
El backend:
- ✅ Siempre disponible (alta disponibilidad)
- ✅ Versionado permite rollback
- ✅ API idempotente y estable
Principio 3: Validación Clínica Obligatoria
Todo contenido clínico:
- ✅ Debe pasar validación antes de ser público
- ✅ Solo contenido
validatedes servido a la app - ✅ Historial completo de validaciones
- ✅ Trazabilidad de quién validó y cuándo
2️⃣ MODELO DE DATOS POSTGRESQL
2.1 Esquema General
-- Esquema principal
CREATE SCHEMA IF NOT EXISTS emerges_content;
-- Extensión para UUIDs
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Extensión para JSONB (ya incluida en PostgreSQL)
2.2 Tabla: content_items (Contenido Principal)
Propósito: Almacena todos los tipos de contenido (protocolos, guías, manual, fármacos)
CREATE TABLE emerges_content.content_items (
-- Identificación (inmutable)
id VARCHAR(100) PRIMARY KEY, -- "rcp-adulto-svb", "adrenalina", etc.
type VARCHAR(50) NOT NULL, -- 'protocol' | 'guide' | 'manual' | 'drug'
level VARCHAR(50) NOT NULL, -- 'operativo' | 'formativo' | 'referencia'
-- Contenido (editable)
title VARCHAR(500) NOT NULL, -- Título del contenido
short_title VARCHAR(200), -- Título corto (opcional)
content JSONB NOT NULL, -- Contenido estructurado (varía por tipo)
content_markdown TEXT, -- Contenido Markdown (para guías/manual)
-- Metadatos de contenido
category VARCHAR(100), -- Categoría (ej: 'soporte_vital', 'cardiovascular')
subcategory VARCHAR(100), -- Subcategoría (ej: 'rcp', 'anafilaxia')
priority VARCHAR(20), -- 'critico' | 'alto' | 'medio' | 'bajo'
age_group VARCHAR(20), -- 'adulto' | 'pediatrico' | 'neonatal' | 'todos'
-- Versionado
version INTEGER NOT NULL DEFAULT 1, -- Versión actual
latest_version INTEGER NOT NULL DEFAULT 1, -- Última versión (para queries rápidas)
-- Estado y validación
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- 'draft' | 'pending' | 'validated' | 'rejected' | 'archived'
validated_by VARCHAR(100), -- ID del validador clínico
validated_at TIMESTAMP WITH TIME ZONE, -- Fecha de validación
clinical_source VARCHAR(200), -- 'ERC 2021', 'AHA 2020', 'SEMES 2022', etc.
quality_score INTEGER, -- 0-100 (opcional)
-- Revisión
reviewed_by VARCHAR(100), -- ID del revisor
reviewed_at TIMESTAMP WITH TIME ZONE, -- Fecha de revisión
-- Auditoría
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(100) NOT NULL, -- ID del creador
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_by VARCHAR(100) NOT NULL, -- ID del último editor
-- Constraints
CONSTRAINT chk_type CHECK (type IN ('protocol', 'guide', 'manual', 'drug')),
CONSTRAINT chk_level CHECK (level IN ('operativo', 'formativo', 'referencia')),
CONSTRAINT chk_status CHECK (status IN ('draft', 'pending', 'validated', 'rejected', 'archived')),
CONSTRAINT chk_priority CHECK (priority IN ('critico', 'alto', 'medio', 'bajo') OR priority IS NULL),
CONSTRAINT chk_quality_score CHECK (quality_score IS NULL OR (quality_score >= 0 AND quality_score <= 100))
);
-- Índices para performance
CREATE INDEX idx_content_items_type ON emerges_content.content_items(type);
CREATE INDEX idx_content_items_level ON emerges_content.content_items(level);
CREATE INDEX idx_content_items_status ON emerges_content.content_items(status);
CREATE INDEX idx_content_items_category ON emerges_content.content_items(category);
CREATE INDEX idx_content_items_validated_at ON emerges_content.content_items(validated_at);
CREATE INDEX idx_content_items_updated_at ON emerges_content.content_items(updated_at);
-- Índice GIN para búsqueda en JSONB
CREATE INDEX idx_content_items_content_gin ON emerges_content.content_items USING GIN (content);
-- Índice para búsqueda full-text en título
CREATE INDEX idx_content_items_title_fts ON emerges_content.content_items USING GIN (to_tsvector('spanish', title));
2.3 Tabla: content_versions (Historial de Versiones)
Propósito: Almacena todas las versiones históricas de cada contenido
CREATE TABLE emerges_content.content_versions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_id VARCHAR(100) NOT NULL, -- FK a content_items.id
version INTEGER NOT NULL, -- Número de versión
-- Snapshot del contenido en esta versión
content JSONB NOT NULL,
content_markdown TEXT,
title VARCHAR(500) NOT NULL,
-- Metadatos de la versión
status VARCHAR(20) NOT NULL,
validated_by VARCHAR(100),
validated_at TIMESTAMP WITH TIME ZONE,
clinical_source VARCHAR(200),
-- Cambios en esta versión
change_summary TEXT, -- Resumen de cambios
changed_fields TEXT[], -- Array de campos modificados
-- Auditoría
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(100) NOT NULL,
-- Constraints
CONSTRAINT fk_content_versions_content_id
FOREIGN KEY (content_id)
REFERENCES emerges_content.content_items(id)
ON DELETE CASCADE,
CONSTRAINT chk_version_status CHECK (status IN ('draft', 'pending', 'validated', 'rejected', 'archived')),
CONSTRAINT uq_content_versions_content_version UNIQUE (content_id, version)
);
-- Índices
CREATE INDEX idx_content_versions_content_id ON emerges_content.content_versions(content_id);
CREATE INDEX idx_content_versions_version ON emerges_content.content_versions(version);
CREATE INDEX idx_content_versions_created_at ON emerges_content.content_versions(created_at);
2.4 Tabla: content_relations (Relaciones entre Contenidos)
Propósito: Define relaciones entre protocolos, guías y manual
CREATE TABLE emerges_content.content_relations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
source_id VARCHAR(100) NOT NULL, -- ID del contenido origen
target_id VARCHAR(100) NOT NULL, -- ID del contenido destino
relation_type VARCHAR(50) NOT NULL, -- 'has_guide' | 'has_manual' | 'references' | 'related_to'
-- Metadatos
is_primary BOOLEAN DEFAULT false, -- Si es relación principal
order_index INTEGER, -- Orden si hay múltiples relaciones
-- Auditoría
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(100) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT fk_content_relations_source
FOREIGN KEY (source_id)
REFERENCES emerges_content.content_items(id)
ON DELETE CASCADE,
CONSTRAINT fk_content_relations_target
FOREIGN KEY (target_id)
REFERENCES emerges_content.content_items(id)
ON DELETE CASCADE,
CONSTRAINT chk_relation_type CHECK (relation_type IN ('has_guide', 'has_manual', 'references', 'related_to')),
CONSTRAINT uq_content_relations_source_target_type UNIQUE (source_id, target_id, relation_type)
);
-- Índices
CREATE INDEX idx_content_relations_source ON emerges_content.content_relations(source_id);
CREATE INDEX idx_content_relations_target ON emerges_content.content_relations(target_id);
CREATE INDEX idx_content_relations_type ON emerges_content.content_relations(relation_type);
2.5 Tabla: content_change_log (Log de Cambios)
Propósito: Registro detallado de todos los cambios realizados
CREATE TABLE emerges_content.content_change_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_id VARCHAR(100) NOT NULL,
version INTEGER NOT NULL,
-- Cambio
action VARCHAR(20) NOT NULL, -- 'create' | 'update' | 'delete' | 'validate' | 'reject' | 'archive'
field_path VARCHAR(200), -- Ruta del campo (ej: 'steps[0]', 'adultDose')
old_value JSONB, -- Valor anterior
new_value JSONB, -- Valor nuevo
-- Contexto
change_reason TEXT, -- Razón del cambio
comment TEXT, -- Comentario del editor
-- Auditoría
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(100) NOT NULL,
ip_address INET, -- IP del usuario (para auditoría)
user_agent TEXT, -- User agent (para auditoría)
-- Constraints
CONSTRAINT fk_content_change_log_content
FOREIGN KEY (content_id)
REFERENCES emerges_content.content_items(id)
ON DELETE CASCADE,
CONSTRAINT chk_change_log_action CHECK (action IN ('create', 'update', 'delete', 'validate', 'reject', 'archive'))
);
-- Índices
CREATE INDEX idx_content_change_log_content_id ON emerges_content.content_change_log(content_id);
CREATE INDEX idx_content_change_log_version ON emerges_content.content_change_log(version);
CREATE INDEX idx_content_change_log_created_at ON emerges_content.content_change_log(created_at);
CREATE INDEX idx_content_change_log_created_by ON emerges_content.content_change_log(created_by);
2.6 Tabla: users (Usuarios del Panel)
Propósito: Gestión de usuarios y roles para el panel administrador
CREATE TABLE emerges_content.users (
id VARCHAR(100) PRIMARY KEY, -- ID único (ej: "dr-juan-perez")
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
role VARCHAR(50) NOT NULL, -- 'admin_clinico' | 'editor_docente' | 'editor_clinico' | 'revisor'
-- Autenticación
password_hash VARCHAR(255) NOT NULL, -- Hash bcrypt
salt VARCHAR(255) NOT NULL,
-- Estado
is_active BOOLEAN DEFAULT true,
last_login TIMESTAMP WITH TIME ZONE,
-- Metadatos
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(100),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT chk_user_role CHECK (role IN ('admin_clinico', 'editor_docente', 'editor_clinico', 'revisor'))
);
-- Índices
CREATE INDEX idx_users_email ON emerges_content.users(email);
CREATE INDEX idx_users_role ON emerges_content.users(role);
CREATE INDEX idx_users_active ON emerges_content.users(is_active);
2.7 Tabla: content_sync_status (Estado de Sincronización)
Propósito: Trackear qué versiones de contenido han sido sincronizadas con las apps
CREATE TABLE emerges_content.content_sync_status (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_id VARCHAR(100) NOT NULL,
version INTEGER NOT NULL,
-- Sincronización
synced_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
sync_count INTEGER DEFAULT 1, -- Número de veces sincronizado
-- Constraints
CONSTRAINT fk_content_sync_status_content
FOREIGN KEY (content_id)
REFERENCES emerges_content.content_items(id)
ON DELETE CASCADE,
CONSTRAINT uq_content_sync_status_content_version UNIQUE (content_id, version)
);
-- Índices
CREATE INDEX idx_content_sync_status_content_id ON emerges_content.content_sync_status(content_id);
CREATE INDEX idx_content_sync_status_synced_at ON emerges_content.content_sync_status(synced_at);
2.8 Vistas Útiles
-- Vista: Contenido validado (solo para lectura pública)
CREATE VIEW emerges_content.v_validated_content AS
SELECT
id,
type,
level,
title,
short_title,
content,
content_markdown,
category,
subcategory,
priority,
age_group,
version,
validated_at,
clinical_source,
updated_at
FROM emerges_content.content_items
WHERE status = 'validated'
ORDER BY updated_at DESC;
-- Vista: Contenido pendiente de validación (para panel admin)
CREATE VIEW emerges_content.v_pending_validation AS
SELECT
ci.*,
u.name as created_by_name,
u.email as created_by_email
FROM emerges_content.content_items ci
LEFT JOIN emerges_content.users u ON ci.created_by = u.id
WHERE ci.status = 'pending'
ORDER BY ci.updated_at ASC;
3️⃣ API REST MÍNIMA
3.1 Principios de Diseño
La API es:
- ✅ RESTful simple
- ✅ Solo lectura para la app operativa
- ✅ Escritura solo con autenticación
- ✅ Versionada (v1)
- ✅ Documentada (OpenAPI/Swagger)
3.2 Endpoints Públicos (App Operativa)
GET /api/v1/content/protocols
Propósito: Obtener todos los protocolos validados
Query params:
level(opcional): Filtrar por nivel ('operativo')category(opcional): Filtrar por categoríastatus(opcional): Por defecto 'validated'
Response:
{
"data": [
{
"id": "rcp-adulto-svb",
"type": "protocol",
"level": "operativo",
"title": "RCP Adulto - Soporte Vital Básico",
"shortTitle": "RCP Adulto SVB",
"content": {
"steps": [...],
"warnings": [...],
"keyPoints": [...],
"equipment": [...],
"drugs": [...]
},
"version": 3,
"updatedAt": "2025-01-15T10:00:00Z"
}
],
"meta": {
"total": 5,
"version": "v1"
}
}
GET /api/v1/content/protocols/:id
Propósito: Obtener un protocolo específico
Response:
{
"data": {
"id": "rcp-adulto-svb",
"type": "protocol",
"level": "operativo",
"title": "RCP Adulto - Soporte Vital Básico",
"content": {...},
"version": 3,
"validatedAt": "2025-01-15T10:00:00Z",
"clinicalSource": "ERC 2021"
}
}
GET /api/v1/content/drugs
Propósito: Obtener todos los fármacos validados
Response: Similar a protocols
GET /api/v1/content/drugs/:id
Propósito: Obtener un fármaco específico
GET /api/v1/content/guides/:id/sections/:sectionNumber
Propósito: Obtener una sección específica de una guía
Response:
{
"data": {
"guideId": "rcp-adulto-svb",
"sectionNumber": 1,
"title": "Introducción y Contexto",
"contentMarkdown": "# SECCIÓN 1: Introducción...",
"version": 2,
"updatedAt": "2025-01-10T08:00:00Z"
}
}
GET /api/v1/content/manual/blocks/:blockId
Propósito: Obtener un bloque del manual
GET /api/v1/content/relations/:id
Propósito: Obtener relaciones de un contenido
Response:
{
"data": {
"sourceId": "rcp-adulto-svb",
"relations": [
{
"targetId": "rcp-adulto-svb",
"targetType": "guide",
"relationType": "has_guide",
"isPrimary": true
},
{
"targetId": "bloque-04-1",
"targetType": "manual",
"relationType": "has_manual",
"isPrimary": true
}
]
}
}
GET /api/v1/content/versions/:id
Propósito: Obtener historial de versiones de un contenido
Query params:
limit(opcional): Número de versiones (default: 10)
Response:
{
"data": {
"contentId": "rcp-adulto-svb",
"currentVersion": 3,
"versions": [
{
"version": 3,
"status": "validated",
"validatedAt": "2025-01-15T10:00:00Z",
"validatedBy": "dr-juan-perez",
"createdAt": "2025-01-15T09:00:00Z"
},
{
"version": 2,
"status": "validated",
"validatedAt": "2025-01-10T08:00:00Z",
"createdAt": "2025-01-10T07:00:00Z"
}
]
}
}
GET /api/v1/sync/check
Propósito: Verificar si hay actualizaciones disponibles
Query params:
lastSync(requerido): Timestamp ISO 8601 de última sincronización
Response:
{
"data": {
"hasUpdates": true,
"updatedItems": [
{
"id": "rcp-adulto-svb",
"type": "protocol",
"version": 3,
"updatedAt": "2025-01-15T10:00:00Z"
}
],
"serverTime": "2025-01-20T12:00:00Z"
}
}
3.3 Endpoints Protegidos (Panel Admin)
POST /api/v1/auth/login
Body:
{
"email": "admin@emergestes.es",
"password": "***"
}
Response:
{
"data": {
"token": "jwt-token-here",
"user": {
"id": "admin-001",
"name": "Dr. Juan Pérez",
"role": "admin_clinico"
}
}
}
PUT /api/v1/content/protocols/:id
Headers:
Authorization: Bearer {jwt-token}
Body:
{
"title": "RCP Adulto - Soporte Vital Básico (Actualizado)",
"content": {
"steps": [...],
"warnings": [...]
},
"changeReason": "Actualización según ERC 2021"
}
Response:
{
"data": {
"id": "rcp-adulto-svb",
"version": 4,
"status": "draft",
"updatedAt": "2025-01-20T12:00:00Z"
}
}
POST /api/v1/content/protocols/:id/validate
Propósito: Solicitar validación clínica
Body:
{
"clinicalSource": "ERC 2021",
"comment": "Validado según guías ERC 2021"
}
POST /api/v1/content/protocols/:id/approve
Propósito: Aprobar contenido (solo admin_clinico)
Body:
{
"qualityScore": 95,
"comment": "Contenido validado y aprobado"
}
POST /api/v1/content/protocols/:id/reject
Propósito: Rechazar contenido (solo admin_clinico)
Body:
{
"reason": "Dosis incorrecta según última guía",
"comment": "Revisar dosis de adrenalina"
}
GET /api/v1/admin/content/pending
Propósito: Obtener contenido pendiente de validación
GET /api/v1/admin/audit/log
Propósito: Obtener log de auditoría
Query params:
contentId(opcional)userId(opcional)action(opcional)from(opcional): Fecha inicioto(opcional): Fecha fin
4️⃣ FLUJO DE SINCRONIZACIÓN OFFLINE-FIRST
4.1 Arquitectura de Sincronización
┌─────────────────────────────────────────┐
│ APP OPERATIVA (PWA) │
│ │
│ 1. Al iniciar: │
│ - Cargar cache local (IndexedDB) │
│ - Verificar actualizaciones │
│ │
│ 2. Si hay red: │
│ - GET /api/v1/sync/check │
│ - Si hay updates: │
│ * Descargar contenido nuevo │
│ * Actualizar cache local │
│ * Notificar usuario (opcional) │
│ │
│ 3. Si NO hay red: │
│ - Usar cache local │
│ - Funcionar normalmente │
│ │
│ 4. En background (si hay red): │
│ - Verificar actualizaciones │
│ - Sincronizar silenciosamente │
└─────────────────────────────────────────┘
4.2 Estrategia de Cache Local
Almacenamiento: IndexedDB (navegador)
Estructura de cache:
interface CachedContent {
id: string;
type: 'protocol' | 'drug' | 'guide' | 'manual';
version: number;
content: any;
cachedAt: string; // ISO 8601
expiresAt: string; // ISO 8601 (cachedAt + 7 días)
lastSync: string; // ISO 8601
}
Reglas de cache:
- Cache inicial: Contenido embebido en build (fallback)
- Cache dinámico: Descargado desde API y guardado en IndexedDB
- Expiración: 7 días (configurable)
- Invalidación: Por versión o por tiempo
- Prioridad: Cache local > API > Fallback embebido
4.3 Flujo Detallado
Paso 1: Inicio de la App
// Pseudocódigo conceptual
async function initializeApp() {
// 1. Cargar cache local
const cachedContent = await loadFromIndexedDB();
// 2. Si hay cache válido, usarlo inmediatamente
if (cachedContent && !isExpired(cachedContent)) {
useContent(cachedContent);
} else {
// 3. Usar fallback embebido
useContent(embeddedContent);
}
// 4. Intentar sincronizar en background
if (navigator.onLine) {
syncInBackground();
}
}
Paso 2: Sincronización en Background
async function syncInBackground() {
try {
// 1. Verificar actualizaciones
const lastSync = getLastSyncTimestamp();
const response = await fetch(`/api/v1/sync/check?lastSync=${lastSync}`);
const { hasUpdates, updatedItems } = await response.json();
if (hasUpdates) {
// 2. Descargar contenido actualizado
for (const item of updatedItems) {
const content = await fetchContent(item.id, item.type);
await saveToIndexedDB(content);
}
// 3. Actualizar timestamp de sincronización
updateLastSyncTimestamp();
// 4. Notificar usuario (opcional, no intrusivo)
showUpdateNotification();
}
} catch (error) {
// Silencioso: no interrumpir uso de la app
console.error('Sync failed:', error);
}
}
Paso 3: Uso Offline
async function getContent(id: string, type: string) {
// 1. Intentar cache local
const cached = await getFromIndexedDB(id, type);
if (cached && !isExpired(cached)) {
return cached.content;
}
// 2. Si hay red, intentar API
if (navigator.onLine) {
try {
const content = await fetchFromAPI(id, type);
await saveToIndexedDB(content);
return content;
} catch (error) {
// Continuar con fallback
}
}
// 3. Fallback embebido (nunca falla)
return getEmbeddedContent(id, type);
}
4.4 Versionado de Contenido
Estrategia:
- Cada contenido tiene
versionincremental - App cachea
versionjunto con contenido - Al sincronizar, compara versiones
- Solo descarga si
versionremota >versionlocal
Ventaja: Minimiza transferencia de datos
5️⃣ PANEL ADMINISTRADOR
5.1 Alcance y Rol
El panel administrador:
- ✅ Es una aplicación web separada (no parte de la PWA)
- ✅ Permite editar TODO el contenido clínico
- ✅ Gestiona validación clínica
- ✅ Proporciona auditoría completa
- ✅ NO modifica código React de la app operativa
El panel administrador NO:
- ❌ Modifica componentes React
- ❌ Cambia rutas de la app
- ❌ Afecta al Service Worker
- ❌ Requiere redeploy de la app operativa
5.2 Funcionalidades Principales
5.2.1 Gestión de Contenido
Editor de Protocolos:
- Formulario para editar: título, pasos, advertencias, puntos clave, material, fármacos
- Validación en tiempo real
- Preview del protocolo
- Guardar como borrador o solicitar validación
Editor de Fármacos:
- Formulario para editar: dosis, vías, indicaciones, contraindicaciones, notas
- Validación de formato de dosis
- Preview del fármaco
- Guardar como borrador o solicitar validación
Editor de Guías:
- Editor Markdown con preview
- Navegación entre secciones (1-8)
- Validación de estructura
- Guardar secciones individuales
Editor de Manual:
- Editor Markdown avanzado
- Navegación por bloques
- Búsqueda y reemplazo
- Validación de referencias cruzadas
5.2.2 Validación Clínica
Panel de Validación:
- Lista de contenido pendiente
- Vista previa del contenido
- Campos para completar:
- Fuente clínica (ERC, AHA, SEMES, etc.)
- Puntuación de calidad (0-100)
- Comentarios
- Botones: Aprobar / Rechazar
Flujo:
- Editor crea/modifica contenido → Estado:
draft - Editor solicita validación → Estado:
pending - Validador revisa → Aprobar → Estado:
validated(público) - Validador revisa → Rechazar → Estado:
rejected(vuelve adraft)
5.2.3 Auditoría
Log de Cambios:
- Historial completo de todos los cambios
- Filtros: por contenido, usuario, fecha, acción
- Vista de diferencias entre versiones
- Exportación de logs
Trazabilidad:
- Quién hizo qué cambio y cuándo
- IP y user agent (para seguridad)
- Rollback a versiones anteriores
5.3 Tecnología del Panel
Stack propuesto:
- Frontend: React + TypeScript (consistente con app principal)
- UI: shadcn/ui (consistente con app principal)
- Editor Markdown: react-markdown-editor-lite
- Validación: Zod (TypeScript-first)
- Autenticación: JWT tokens
- Estado: React Query (para cache de API)
Estructura:
admin-panel/
├── src/
│ ├── pages/
│ │ ├── ProtocolsEditor.tsx
│ │ ├── DrugsEditor.tsx
│ │ ├── GuidesEditor.tsx
│ │ ├── ManualEditor.tsx
│ │ ├── Validation.tsx
│ │ └── Audit.tsx
│ ├── components/
│ │ ├── ContentEditor/
│ │ ├── Validation/
│ │ └── VersionHistory/
│ └── lib/
│ ├── api.ts
│ └── validation.ts
6️⃣ QUÉ NO TOCAR DE LA APP EXISTENTE
6.1 Componentes React
NO modificar:
- ❌
src/pages/RCP.tsx,Ictus.tsx,ViaAerea.tsx,Shock.tsx - ❌
src/components/procedures/ - ❌
src/components/drugs/ - ❌ Estructura visual y lógica de UI
SÍ permitido:
- ✅ Modificar funciones de carga de datos (de estático a dinámico)
- ✅ Añadir lógica de cache local
- ✅ Añadir sincronización en background
6.2 Service Worker
NO modificar:
- ❌
public/sw.js - ❌ Estrategia de cache
- ❌ Lógica de actualización
Razón: El Service Worker es crítico para funcionamiento offline. Cualquier cambio puede romper la app en emergencias.
6.3 Rutas y Navegación
NO modificar:
- ❌ Rutas existentes (
/rcp,/ictus,/via-aerea, etc.) - ❌ Estructura de navegación
- ❌ Deep links
Razón: Las rutas pueden estar bookmarked o compartidas. Cambiarlas rompe enlaces.
6.4 Estructura de Datos TypeScript
NO eliminar:
- ❌
src/data/procedures.ts(mantener como fallback) - ❌
src/data/drugs.ts(mantener como fallback) - ❌
src/data/guides-index.ts(mantener como fallback)
SÍ permitido:
- ✅ Modificar para cargar desde API con fallback
- ✅ Añadir lógica de cache
6.5 Build y Deploy
NO cambiar:
- ❌ Proceso de build (Vite)
- ❌ Estructura de archivos públicos
- ❌ Configuración de PWA
Razón: Cambios en build pueden afectar funcionamiento offline.
7️⃣ ROADMAP DE IMPLEMENTACIÓN
FASE 1: Infraestructura Base (4-5 semanas)
Objetivos:
- Configurar PostgreSQL
- Crear esquema de base de datos
- Implementar API REST básica
- Migrar contenido inicial
Tareas:
- Instalar y configurar PostgreSQL
- Ejecutar scripts de creación de esquema
- Crear backend Node.js básico (Express)
- Implementar endpoints GET (solo lectura)
- Script de migración: TypeScript → PostgreSQL
- Validar que contenido migrado es correcto
Criterios de éxito:
- ✅ Base de datos funcionando
- ✅ API responde correctamente
- ✅ Contenido migrado sin pérdidas
- ✅ App operativa sigue funcionando igual
FASE 2: Sincronización Offline (4-5 semanas)
Objetivos:
- Implementar carga dinámica en app
- Sistema de cache local (IndexedDB)
- Sincronización en background
- Fallback robusto
Tareas:
- Crear
src/lib/content-loader.ts - Crear
src/lib/content-cache.ts(IndexedDB) - Modificar
src/data/procedures.tspara cargar dinámicamente - Modificar
src/data/drugs.tspara cargar dinámicamente - Implementar sincronización en background
- Testing exhaustivo offline/online
- Validar que fallback funciona
Criterios de éxito:
- ✅ App carga contenido desde API
- ✅ Cache local funciona
- ✅ App funciona 100% offline
- ✅ Sincronización transparente
- ✅ Sin regresiones
FASE 3: Panel Admin MVP (6-8 semanas)
Objetivos:
- Crear panel administrador básico
- Editor de protocolos y fármacos
- Autenticación y autorización
- Guardado en base de datos
Tareas:
- Crear proyecto
admin-panel/ - Implementar autenticación JWT
- Crear editor de protocolos
- Crear editor de fármacos
- Implementar endpoints PUT/POST
- Sistema de versionado automático
- Testing del panel
Criterios de éxito:
- ✅ Panel permite editar contenido
- ✅ Cambios se guardan en BD
- ✅ Versiones se incrementan
- ✅ App consume contenido actualizado
- ✅ Autenticación funciona
FASE 4: Validación Clínica (4-5 semanas)
Objetivos:
- Sistema de roles y permisos
- Flujo de validación clínica
- Panel de validación
- Metadatos de validación
Tareas:
- Implementar RBAC
- Crear panel de validación
- Endpoints de aprobación/rechazo
- Campos de metadatos clínicos
- Notificaciones de cambios de estado
- Testing con usuarios reales
Criterios de éxito:
- ✅ Roles funcionan correctamente
- ✅ Flujo de validación completo
- ✅ Solo contenido validado es público
- ✅ Metadatos se guardan correctamente
FASE 5: Editores Completos (6-8 semanas)
Objetivos:
- Editor de guías formativas
- Editor de manual completo
- Funcionalidades avanzadas
- Migración completa
Tareas:
- Editor de guías (Markdown)
- Editor de manual (Markdown avanzado)
- Comparación de versiones (diff)
- Rollback a versiones anteriores
- Migrar todas las guías
- Migrar todo el manual
Criterios de éxito:
- ✅ Todos los editores funcionan
- ✅ Todo el contenido migrado
- ✅ Funcionalidades avanzadas disponibles
FASE 6: Auditoría y Optimización (4-5 semanas)
Objetivos:
- Sistema de auditoría completo
- Optimización de performance
- Documentación
- Mejoras de UX
Tareas:
- Panel de auditoría
- Exportación de logs
- Optimización de queries
- CDN para contenido estático
- Documentación completa
- Testing de carga
Criterios de éxito:
- ✅ Auditoría completa funcional
- ✅ Performance optimizada
- ✅ Documentación completa
📊 RESUMEN EJECUTIVO
Arquitectura
- Base de datos: PostgreSQL con esquema
emerges_content - Backend: Node.js + Express + API REST
- Panel Admin: React + TypeScript (app separada)
- Sincronización: Offline-first con IndexedDB
Modelo de Datos
- 7 tablas principales: content_items, content_versions, content_relations, content_change_log, users, content_sync_status
- Versionado completo: Historial de todas las versiones
- Validación clínica: Estados y metadatos de validación
- Auditoría: Log completo de cambios
API REST
- Endpoints públicos: Solo lectura (app operativa)
- Endpoints protegidos: Escritura con autenticación (panel admin)
- Versionado: v1
- Documentación: OpenAPI/Swagger
Sincronización
- Estrategia: Cache local > API > Fallback embebido
- Offline-first: App funciona sin red
- Background sync: Sincronización transparente
- Versionado: Solo descarga si hay actualizaciones
Panel Admin
- Alcance: Edición completa de contenido clínico
- Validación: Flujo de aprobación/rechazo
- Auditoría: Historial completo
- Separado: No modifica app operativa
Roadmap
- 6 fases: 28-36 semanas total
- Progresivo: Cada fase independiente
- Validado: Testing continuo
- Sin disrupciones: App funciona en todo momento
Este sistema permite modificar contenido clínico sin tocar código React, manteniendo funcionamiento offline-first y validación clínica obligatoria.