319 lines
7.5 KiB
Markdown
319 lines
7.5 KiB
Markdown
|
|
# 🏗️ ARQUITECTURA CONTENT ADAPTER - Sistema de Contenido Externo
|
||
|
|
|
||
|
|
**Fecha:** 2025-01-06
|
||
|
|
**Estado:** Implementación Inicial
|
||
|
|
**Principio:** ✅ ADITIVO - NO modifica código existente
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 OBJETIVO
|
||
|
|
|
||
|
|
Crear una capa de contenido externo que permita:
|
||
|
|
- ✅ Gestionar contenido sin tocar código
|
||
|
|
- ✅ Usar Content Pack JSON desde servidor
|
||
|
|
- ✅ Fallback total a datos locales
|
||
|
|
- ✅ La app funciona igual si el sistema falla
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🧱 ARQUITECTURA
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ APP PWA (Frontend) │
|
||
|
|
├─────────────────────────────────────────┤
|
||
|
|
│ ContentAdapterFactory │
|
||
|
|
│ ├── ExternalContentAdapter (NUEVO) │ ← Content Pack JSON
|
||
|
|
│ └── LocalContentAdapter (EXISTENTE) │ ← procedures.ts, drugs.ts
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
│ │
|
||
|
|
│ │
|
||
|
|
▼ ▼
|
||
|
|
┌──────────────────┐ ┌──────────────────┐
|
||
|
|
│ Content Pack JSON │ │ Datos Locales │
|
||
|
|
│ (Servidor) │ │ (Código actual) │
|
||
|
|
└──────────────────┘ └──────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📦 COMPONENTES CREADOS
|
||
|
|
|
||
|
|
### 1. ContentAdapter (`src/services/content-adapter.ts`)
|
||
|
|
|
||
|
|
**Interfaz:**
|
||
|
|
```typescript
|
||
|
|
interface ContentAdapter {
|
||
|
|
getProtocol(id: string): Procedure | null;
|
||
|
|
getDrug(id: string): Drug | null;
|
||
|
|
getAllProtocols(): Procedure[];
|
||
|
|
getAllDrugs(): Drug[];
|
||
|
|
isAvailable(): boolean;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Implementaciones:**
|
||
|
|
- `LocalContentAdapter` - Usa `procedures.ts` y `drugs.ts` (fallback)
|
||
|
|
- `ExternalContentAdapter` - Usa Content Pack JSON (si disponible)
|
||
|
|
|
||
|
|
**Factory:**
|
||
|
|
- `ContentAdapterFactory.getAdapter()` - Decide automáticamente
|
||
|
|
- Prioridad: External → Local
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2. Generador Content Pack (`backend/src/services/pack-generator.js`)
|
||
|
|
|
||
|
|
**Funcionalidad:**
|
||
|
|
- Lee contenido publicado desde PostgreSQL
|
||
|
|
- Incluye recursos multimedia asociados
|
||
|
|
- Calcula hash SHA-256
|
||
|
|
- Genera JSON optimizado
|
||
|
|
|
||
|
|
**Método principal:**
|
||
|
|
```javascript
|
||
|
|
await packGenerator.generatePack(version, options)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3. API Content Pack (`backend/src/routes/content-pack.js`)
|
||
|
|
|
||
|
|
**Endpoints:**
|
||
|
|
- `GET /api/content-pack/latest.json` - Pack más reciente
|
||
|
|
- `GET /api/content-pack/:version.json` - Pack específico
|
||
|
|
|
||
|
|
**Características:**
|
||
|
|
- ✅ Sin autenticación (público)
|
||
|
|
- ✅ Cache headers (ETag, Cache-Control)
|
||
|
|
- ✅ Generación on-the-fly si no existe archivo
|
||
|
|
- ✅ 304 Not Modified si hash coincide
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔄 FLUJO DE FUNCIONAMIENTO
|
||
|
|
|
||
|
|
### 1. App Inicia
|
||
|
|
|
||
|
|
```
|
||
|
|
App → ContentAdapterFactory.getAdapter()
|
||
|
|
→ ExternalContentAdapter (intenta cargar pack)
|
||
|
|
→ Si existe: usa pack
|
||
|
|
→ Si no existe: fallback a LocalContentAdapter
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Carga del Pack
|
||
|
|
|
||
|
|
```
|
||
|
|
ExternalContentAdapter
|
||
|
|
→ Intenta cache (localStorage)
|
||
|
|
→ Si no hay cache: descarga /api/content-pack/latest.json
|
||
|
|
→ Guarda en cache (24h)
|
||
|
|
→ Convierte ContentItem → Procedure/Drug
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Fallback Automático
|
||
|
|
|
||
|
|
```
|
||
|
|
Si ExternalContentAdapter no está disponible:
|
||
|
|
→ LocalContentAdapter (siempre disponible)
|
||
|
|
→ Usa procedures.ts y drugs.ts
|
||
|
|
→ App funciona exactamente igual
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📋 FORMATO CONTENT PACK JSON
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"metadata": {
|
||
|
|
"version": "1.0.0",
|
||
|
|
"generated_at": "2025-01-06T12:00:00Z",
|
||
|
|
"hash": "sha256:abc123...",
|
||
|
|
"total_items": 24,
|
||
|
|
"total_resources": 0
|
||
|
|
},
|
||
|
|
"content": {
|
||
|
|
"protocols": [
|
||
|
|
{
|
||
|
|
"id": "...",
|
||
|
|
"type": "protocol",
|
||
|
|
"slug": "rcp-adulto-svb",
|
||
|
|
"title": "RCP Adulto - Soporte Vital Básico",
|
||
|
|
"content": { ... },
|
||
|
|
"priority": "critical",
|
||
|
|
"status": "published",
|
||
|
|
"version": "1.0.0"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"guides": [...],
|
||
|
|
"drugs": [...],
|
||
|
|
"checklists": [...]
|
||
|
|
},
|
||
|
|
"media": {
|
||
|
|
"resources": [...],
|
||
|
|
"associations": [...]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ GARANTÍAS DE SEGURIDAD
|
||
|
|
|
||
|
|
### 1. Fallback Total
|
||
|
|
- ✅ Si el pack no existe → usa datos locales
|
||
|
|
- ✅ Si el pack falla al cargar → usa datos locales
|
||
|
|
- ✅ Si el pack está corrupto → usa datos locales
|
||
|
|
- ✅ La app NUNCA crashea por falta de pack
|
||
|
|
|
||
|
|
### 2. No Modifica Código Existente
|
||
|
|
- ✅ `procedures.ts` - NO modificado
|
||
|
|
- ✅ `drugs.ts` - NO modificado
|
||
|
|
- ✅ Componentes existentes - NO modificados
|
||
|
|
- ✅ Service Worker - NO modificado
|
||
|
|
- ✅ Rutas - NO modificadas
|
||
|
|
|
||
|
|
### 3. Compatibilidad Offline
|
||
|
|
- ✅ Pack se cachea en localStorage
|
||
|
|
- ✅ Funciona offline después de primera carga
|
||
|
|
- ✅ Cache válido por 24 horas
|
||
|
|
- ✅ Refresh manual disponible
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🚀 USO EN LA APP
|
||
|
|
|
||
|
|
### Opción 1: Usar directamente
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { getProtocol, getAllProtocols } from '@/services/content-adapter';
|
||
|
|
|
||
|
|
// Obtener protocolo (automáticamente usa pack o local)
|
||
|
|
const protocol = getProtocol('rcp-adulto-svb');
|
||
|
|
|
||
|
|
// Obtener todos (automáticamente usa pack o local)
|
||
|
|
const protocols = getAllProtocols();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Opción 2: Usar hook React
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { useContentAdapter } from '@/services/content-adapter';
|
||
|
|
|
||
|
|
function MyComponent() {
|
||
|
|
const { getProtocol, getAllProtocols } = useContentAdapter();
|
||
|
|
|
||
|
|
const protocol = getProtocol('rcp-adulto-svb');
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Opción 3: Forzar refresh
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { refreshContentPack } from '@/services/content-adapter';
|
||
|
|
|
||
|
|
// Forzar recarga del pack
|
||
|
|
await refreshContentPack();
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔧 CONFIGURACIÓN
|
||
|
|
|
||
|
|
### Backend
|
||
|
|
|
||
|
|
**Variables de entorno:**
|
||
|
|
```env
|
||
|
|
PACKS_DIR=/ruta/a/storage/packs # Opcional
|
||
|
|
```
|
||
|
|
|
||
|
|
**Estructura de directorios:**
|
||
|
|
```
|
||
|
|
backend/
|
||
|
|
├── storage/
|
||
|
|
│ └── packs/
|
||
|
|
│ ├── pack-v1.0.0.json
|
||
|
|
│ └── pack-latest.json (symlink)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Frontend
|
||
|
|
|
||
|
|
**No requiere configuración** - Funciona automáticamente
|
||
|
|
|
||
|
|
**Cache:**
|
||
|
|
- LocalStorage: `content_pack` y `content_pack_time`
|
||
|
|
- TTL: 24 horas
|
||
|
|
- Refresh manual disponible
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📊 ESTADO ACTUAL
|
||
|
|
|
||
|
|
### ✅ Implementado
|
||
|
|
|
||
|
|
- [x] ContentAdapter interface
|
||
|
|
- [x] LocalContentAdapter (fallback)
|
||
|
|
- [x] ExternalContentAdapter (pack)
|
||
|
|
- [x] ContentAdapterFactory
|
||
|
|
- [x] Generador de Content Pack
|
||
|
|
- [x] API endpoints para pack
|
||
|
|
- [x] Cache en localStorage
|
||
|
|
- [x] Conversión ContentItem → Procedure/Drug
|
||
|
|
|
||
|
|
### ⏳ Pendiente
|
||
|
|
|
||
|
|
- [ ] Integrar en componentes existentes (opcional)
|
||
|
|
- [ ] Service Worker para cache avanzado (opcional)
|
||
|
|
- [ ] IndexedDB para packs grandes (opcional)
|
||
|
|
- [ ] UI para forzar refresh (opcional)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🧪 TESTING
|
||
|
|
|
||
|
|
### Verificar que funciona
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 1. Verificar adapter disponible
|
||
|
|
import { contentAdapter } from '@/services/content-adapter';
|
||
|
|
console.log('Adapter disponible:', contentAdapter.isAvailable());
|
||
|
|
|
||
|
|
// 2. Obtener protocolo
|
||
|
|
const protocol = contentAdapter.getProtocol('rcp-adulto-svb');
|
||
|
|
console.log('Protocolo:', protocol);
|
||
|
|
|
||
|
|
// 3. Verificar fallback
|
||
|
|
// Desconectar servidor → debería usar local
|
||
|
|
```
|
||
|
|
|
||
|
|
### Verificar Content Pack
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Obtener pack
|
||
|
|
curl http://localhost:3000/api/content-pack/latest.json
|
||
|
|
|
||
|
|
# Verificar hash
|
||
|
|
# El hash debe coincidir con ETag header
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ RESTRICCIONES CUMPLIDAS
|
||
|
|
|
||
|
|
- ✅ NO modifica `src/data/procedures.ts`
|
||
|
|
- ✅ NO modifica `src/data/drugs.ts`
|
||
|
|
- ✅ NO modifica Service Worker
|
||
|
|
- ✅ NO modifica rutas existentes
|
||
|
|
- ✅ NO modifica componentes actuales
|
||
|
|
- ✅ TODO es aditivo y desacoplado
|
||
|
|
- ✅ Fallback total garantizado
|
||
|
|
- ✅ App funciona igual si falla
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Fin del Documento**
|
||
|
|
|