codigo0/docs/ARQUITECTURA_BBDD_Y_GESTION_CONTENIDO.md

34 KiB
Raw Blame History

🏗️ 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 validated es 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ía
  • status (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 inicio
  • to (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:

  1. Cache inicial: Contenido embebido en build (fallback)
  2. Cache dinámico: Descargado desde API y guardado en IndexedDB
  3. Expiración: 7 días (configurable)
  4. Invalidación: Por versión o por tiempo
  5. 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 version incremental
  • App cachea version junto con contenido
  • Al sincronizar, compara versiones
  • Solo descarga si version remota > version local

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:

  1. Editor crea/modifica contenido → Estado: draft
  2. Editor solicita validación → Estado: pending
  3. Validador revisa → Aprobar → Estado: validated (público)
  4. Validador revisa → Rechazar → Estado: rejected (vuelve a draft)

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.ts para cargar dinámicamente
  • Modificar src/data/drugs.ts para 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.