616 lines
19 KiB
Markdown
616 lines
19 KiB
Markdown
# 📦 ESPECIFICACIÓN CONTENT PACK
|
|
|
|
**Versión:** 1.0.0
|
|
**Fecha:** 2025-01-06
|
|
**Estado:** Especificación Final
|
|
|
|
---
|
|
|
|
## 🎯 PROPÓSITO
|
|
|
|
El **Content Pack** es un archivo JSON que contiene todo el contenido del sistema (protocolos, guías, manuales, fármacos, recursos) en un formato optimizado para distribución offline-first.
|
|
|
|
**Características:**
|
|
- ✅ Distribución como archivo JSON único
|
|
- ✅ Cacheable localmente (IndexedDB)
|
|
- ✅ Offline-first garantizado
|
|
- ✅ Versionado semántico
|
|
- ✅ Verificación de integridad (hash)
|
|
|
|
---
|
|
|
|
## 📋 ESTRUCTURA DEL CONTENT PACK
|
|
|
|
```json
|
|
{
|
|
"metadata": {
|
|
"version": "1.0.0",
|
|
"generated_at": "2025-01-06T12:00:00Z",
|
|
"source": "supabase",
|
|
"hash": "sha256:abc123...",
|
|
"min_app_version": "1.0.0",
|
|
"max_app_version": "2.0.0"
|
|
},
|
|
"content": {
|
|
"protocols": [...],
|
|
"guides": [...],
|
|
"manuals": [...],
|
|
"drugs": [...],
|
|
"checklists": [...]
|
|
},
|
|
"media": {
|
|
"resources": [...],
|
|
"associations": [...]
|
|
},
|
|
"links": {
|
|
"protocol_to_guide": [...],
|
|
"guide_to_protocol": [...],
|
|
"manual_to_protocols": [...]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📐 DETALLE DE SECCIONES
|
|
|
|
### 1. Metadata
|
|
|
|
```typescript
|
|
interface ContentPackMetadata {
|
|
version: string; // Versión semántica del pack (ej: "1.2.3")
|
|
generated_at: string; // ISO timestamp de generación
|
|
source: string; // Origen: "supabase" | "local" | "manual"
|
|
hash: string; // Hash SHA-256 del contenido (sin metadata)
|
|
min_app_version?: string; // Versión mínima de app requerida
|
|
max_app_version?: string; // Versión máxima de app compatible
|
|
total_items: number; // Total de items de contenido
|
|
total_resources: number; // Total de recursos multimedia
|
|
generated_by?: string; // ID del generador
|
|
}
|
|
```
|
|
|
|
**Ejemplo:**
|
|
```json
|
|
{
|
|
"metadata": {
|
|
"version": "1.0.0",
|
|
"generated_at": "2025-01-06T12:00:00Z",
|
|
"source": "supabase",
|
|
"hash": "sha256:7f8e9d2c1b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d",
|
|
"min_app_version": "1.0.0",
|
|
"total_items": 45,
|
|
"total_resources": 120
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Content
|
|
|
|
Contiene arrays de ContentItems según tipo:
|
|
|
|
```typescript
|
|
interface ContentPackContent {
|
|
protocols: ContentItem[]; // Solo type: 'protocol'
|
|
guides: ContentItem[]; // Solo type: 'guide'
|
|
manuals: ContentItem[]; // Solo type: 'manual'
|
|
drugs: ContentItem[]; // Solo type: 'drug'
|
|
checklists: ContentItem[]; // Solo type: 'checklist'
|
|
}
|
|
```
|
|
|
|
**Filtrado:**
|
|
- Solo incluye items con `status: 'published'`
|
|
- Solo incluye la última versión (`version === latest_version`)
|
|
- Ordenados por `priority` (critica → baja) y luego por `title`
|
|
|
|
---
|
|
|
|
### 3. Media
|
|
|
|
Contiene recursos multimedia y sus asociaciones:
|
|
|
|
```typescript
|
|
interface ContentPackMedia {
|
|
resources: MediaResource[]; // Recursos multimedia
|
|
associations: ContentResourceAssociation[]; // Asociaciones contenido ⇄ recursos
|
|
}
|
|
```
|
|
|
|
**Filtrado:**
|
|
- Solo recursos con `status: 'published'`
|
|
- Solo asociaciones de contenido publicado
|
|
- Recursos ordenados por `priority` y `uploaded_at`
|
|
|
|
---
|
|
|
|
### 4. Links
|
|
|
|
Enlaces bidireccionales entre contenido:
|
|
|
|
```typescript
|
|
interface ContentPackLinks {
|
|
protocol_to_guide: Array<{
|
|
protocol_id: string;
|
|
guide_id: string;
|
|
}>;
|
|
guide_to_protocol: Array<{
|
|
guide_id: string;
|
|
protocol_id: string;
|
|
}>;
|
|
manual_to_protocols: Array<{
|
|
manual_id: string;
|
|
protocol_ids: string[];
|
|
}>;
|
|
manual_to_guides: Array<{
|
|
manual_id: string;
|
|
guide_ids: string[];
|
|
}>;
|
|
}
|
|
```
|
|
|
|
**Propósito:**
|
|
- Navegación rápida sin consultar BD
|
|
- Enlaces bidireccionales pre-calculados
|
|
- Optimización de búsqueda
|
|
|
|
---
|
|
|
|
## 📝 EJEMPLO COMPLETO
|
|
|
|
### Content Pack: RCP Adulto SVB
|
|
|
|
```json
|
|
{
|
|
"metadata": {
|
|
"version": "1.0.0",
|
|
"generated_at": "2025-01-06T12:00:00Z",
|
|
"source": "supabase",
|
|
"hash": "sha256:abc123...",
|
|
"min_app_version": "1.0.0",
|
|
"total_items": 1,
|
|
"total_resources": 3
|
|
},
|
|
"content": {
|
|
"protocols": [
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"type": "protocol",
|
|
"slug": "rcp-adulto-svb",
|
|
"title": "RCP Adulto - Soporte Vital Básico",
|
|
"short_title": "RCP Adulto SVB",
|
|
"description": "Protocolo de reanimación cardiopulmonar básica en adultos",
|
|
"clinical_context": "RCP",
|
|
"usage_type": "operativo",
|
|
"priority": "critica",
|
|
"status": "published",
|
|
"source_guideline": "ERC",
|
|
"source_year": 2021,
|
|
"source_url": "https://www.erc.edu/...",
|
|
"version": "1.0.0",
|
|
"latest_version": "1.0.0",
|
|
"content": {
|
|
"steps": [
|
|
{
|
|
"order": 1,
|
|
"text": "Garantizar seguridad de la escena",
|
|
"critical": true,
|
|
"time_estimate": "5-10s"
|
|
},
|
|
{
|
|
"order": 2,
|
|
"text": "Comprobar consciencia: estimular y preguntar '¿Se encuentra bien?'",
|
|
"critical": true,
|
|
"time_estimate": "5-10s"
|
|
},
|
|
{
|
|
"order": 3,
|
|
"text": "Si no responde, llamar inmediatamente al 112",
|
|
"critical": true,
|
|
"time_estimate": "10-15s"
|
|
},
|
|
{
|
|
"order": 4,
|
|
"text": "Abrir vía aérea: maniobra frente-mentón",
|
|
"critical": true,
|
|
"time_estimate": "5-10s"
|
|
},
|
|
{
|
|
"order": 5,
|
|
"text": "Comprobar respiración: VER-OÍR-SENTIR (máx. 10 segundos)",
|
|
"critical": true,
|
|
"time_estimate": "10s"
|
|
},
|
|
{
|
|
"order": 6,
|
|
"text": "Si no respira normal: iniciar RCP",
|
|
"critical": true
|
|
},
|
|
{
|
|
"order": 7,
|
|
"text": "Iniciar compresiones torácicas: 30 compresiones",
|
|
"critical": true,
|
|
"equipment": ["DEA"],
|
|
"time_estimate": "20-30s"
|
|
},
|
|
{
|
|
"order": 8,
|
|
"text": "Dar 2 ventilaciones de rescate",
|
|
"critical": true,
|
|
"equipment": ["Bolsa-mascarilla"],
|
|
"time_estimate": "5-10s"
|
|
},
|
|
{
|
|
"order": 9,
|
|
"text": "Continuar ciclos 30:2 sin interrupción",
|
|
"critical": true
|
|
},
|
|
{
|
|
"order": 10,
|
|
"text": "Solicitar DEA cuando esté disponible",
|
|
"critical": false
|
|
}
|
|
],
|
|
"checklist": {
|
|
"enabled": true,
|
|
"title": "Checklist RCP Adulto SVB",
|
|
"items": [
|
|
{
|
|
"id": "check-1",
|
|
"text": "Seguridad de la escena",
|
|
"order": 1,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-2",
|
|
"text": "Comprobación de respuesta",
|
|
"order": 2,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-3",
|
|
"text": "Apertura de vía aérea",
|
|
"order": 3,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-4",
|
|
"text": "Comprobación de respiración (<10s)",
|
|
"order": 4,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-5",
|
|
"text": "Activación de emergencias",
|
|
"order": 5,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-6",
|
|
"text": "Inicio de compresiones",
|
|
"order": 6,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-7",
|
|
"text": "Colocación del DEA",
|
|
"order": 7,
|
|
"critical": false
|
|
},
|
|
{
|
|
"id": "check-8",
|
|
"text": "Análisis y descarga si indicada",
|
|
"order": 8,
|
|
"critical": false
|
|
},
|
|
{
|
|
"id": "check-9",
|
|
"text": "RCP de alta calidad continua",
|
|
"order": 9,
|
|
"critical": true
|
|
},
|
|
{
|
|
"id": "check-10",
|
|
"text": "Reevaluación cada 2 minutos",
|
|
"order": 10,
|
|
"critical": true
|
|
}
|
|
]
|
|
},
|
|
"warnings": [
|
|
"Profundidad compresiones: 5-6 cm",
|
|
"Frecuencia: 100-120 compresiones/min",
|
|
"Permitir descompresión completa",
|
|
"Minimizar interrupciones (<10 seg)",
|
|
"Cambiar reanimador cada 2 min"
|
|
],
|
|
"key_points": [
|
|
"Compresiones de calidad salvan vidas",
|
|
"No interrumpir para pulso hasta que haya signos de vida",
|
|
"La desfibrilación precoz aumenta supervivencia"
|
|
],
|
|
"equipment": ["DEA", "Bolsa-mascarilla", "Cánula orofaríngea"],
|
|
"age_group": "adulto",
|
|
"estimated_duration": "5-10 min"
|
|
},
|
|
"related_content_ids": [
|
|
"550e8400-e29b-41d4-a716-446655440001" // Guía formativa RCP
|
|
],
|
|
"related_guide_ids": [
|
|
"550e8400-e29b-41d4-a716-446655440001"
|
|
],
|
|
"tags": ["rcp", "svb", "adulto", "emergencia", "critica"],
|
|
"category": "soporte_vital",
|
|
"created_by": "system",
|
|
"created_at": "2025-01-01T00:00:00Z",
|
|
"updated_at": "2025-01-06T12:00:00Z"
|
|
}
|
|
],
|
|
"guides": [],
|
|
"manuals": [],
|
|
"drugs": [],
|
|
"checklists": []
|
|
},
|
|
"media": {
|
|
"resources": [
|
|
{
|
|
"id": "660e8400-e29b-41d4-a716-446655440000",
|
|
"type": "image",
|
|
"file_url": "https://[project].supabase.co/storage/v1/object/public/infografias/rcp/rcp_posicion_manos_adulto.png",
|
|
"filename": "rcp_posicion_manos_adulto.png",
|
|
"path": "/assets/infografias/rcp/rcp_posicion_manos_adulto.png",
|
|
"title": "Posición de Manos - RCP Adulto",
|
|
"description": "Infografía mostrando la posición correcta de las manos para compresiones torácicas en RCP adulto",
|
|
"alt_text": "Posición de manos para compresiones torácicas RCP adulto: talón de una mano sobre el esternón, otra mano encima, brazos extendidos",
|
|
"caption": "Posición correcta de manos para compresiones torácicas en RCP adulto",
|
|
"tags": ["rcp", "adulto", "compresiones", "posicion", "operativo"],
|
|
"block": "bloque-4-soporte-vital",
|
|
"priority": "critica",
|
|
"usage_type": ["operativo"],
|
|
"width": 1200,
|
|
"height": 800,
|
|
"format": "png",
|
|
"file_size": 245678,
|
|
"status": "published",
|
|
"uploaded_by": "system",
|
|
"uploaded_at": "2025-01-05T10:00:00Z",
|
|
"updated_at": "2025-01-05T10:00:00Z"
|
|
},
|
|
{
|
|
"id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"type": "image",
|
|
"file_url": "https://[project].supabase.co/storage/v1/object/public/infografias/rcp/rcp_profundidad_compresiones.png",
|
|
"filename": "rcp_profundidad_compresiones.png",
|
|
"path": "/assets/infografias/rcp/rcp_profundidad_compresiones.png",
|
|
"title": "Profundidad de Compresiones - RCP",
|
|
"description": "Diagrama mostrando la profundidad correcta de compresiones (5-6 cm) y frecuencia (100-120/min)",
|
|
"alt_text": "Profundidad de compresiones RCP: 5-6 cm, frecuencia 100-120 por minuto",
|
|
"caption": "Profundidad y frecuencia de compresiones torácicas",
|
|
"tags": ["rcp", "compresiones", "profundidad", "frecuencia", "operativo"],
|
|
"block": "bloque-4-soporte-vital",
|
|
"priority": "critica",
|
|
"usage_type": ["operativo"],
|
|
"width": 1000,
|
|
"height": 600,
|
|
"format": "png",
|
|
"file_size": 189234,
|
|
"status": "published",
|
|
"uploaded_by": "system",
|
|
"uploaded_at": "2025-01-05T10:00:00Z",
|
|
"updated_at": "2025-01-05T10:00:00Z"
|
|
},
|
|
{
|
|
"id": "660e8400-e29b-41d4-a716-446655440002",
|
|
"type": "video",
|
|
"file_url": "https://[project].supabase.co/storage/v1/object/public/videos/rcp/rcp_adulto_svb.mp4",
|
|
"thumbnail_url": "https://[project].supabase.co/storage/v1/object/public/videos/rcp/rcp_adulto_svb_thumb.jpg",
|
|
"filename": "rcp_adulto_svb.mp4",
|
|
"path": "/assets/videos/rcp/rcp_adulto_svb.mp4",
|
|
"title": "RCP Adulto SVB - Técnica Completa",
|
|
"description": "Vídeo demostrativo de la técnica completa de RCP Adulto SVB (45 segundos)",
|
|
"alt_text": "Vídeo demostrativo RCP Adulto SVB",
|
|
"caption": "Técnica completa de RCP Adulto SVB",
|
|
"tags": ["rcp", "adulto", "svb", "video", "operativo"],
|
|
"block": "bloque-4-soporte-vital",
|
|
"priority": "critica",
|
|
"usage_type": ["operativo"],
|
|
"duration_seconds": 45,
|
|
"video_format": "mp4",
|
|
"file_size": 5242880,
|
|
"status": "published",
|
|
"uploaded_by": "system",
|
|
"uploaded_at": "2025-01-05T11:00:00Z",
|
|
"updated_at": "2025-01-05T11:00:00Z"
|
|
}
|
|
],
|
|
"associations": [
|
|
{
|
|
"id": "770e8400-e29b-41d4-a716-446655440000",
|
|
"content_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"media_resource_id": "660e8400-e29b-41d4-a716-446655440000",
|
|
"section": "pasos",
|
|
"position": 7,
|
|
"placement": "inline",
|
|
"caption": "Posición correcta de manos para compresiones",
|
|
"is_critical": true,
|
|
"priority": "critica",
|
|
"created_at": "2025-01-06T10:00:00Z"
|
|
},
|
|
{
|
|
"id": "770e8400-e29b-41d4-a716-446655440001",
|
|
"content_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"media_resource_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"section": "pasos",
|
|
"position": 7,
|
|
"placement": "after",
|
|
"caption": "Profundidad y frecuencia de compresiones",
|
|
"is_critical": true,
|
|
"priority": "critica",
|
|
"created_at": "2025-01-06T10:00:00Z"
|
|
},
|
|
{
|
|
"id": "770e8400-e29b-41d4-a716-446655440002",
|
|
"content_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"media_resource_id": "660e8400-e29b-41d4-a716-446655440002",
|
|
"section": "pasos",
|
|
"position": 0,
|
|
"placement": "before",
|
|
"caption": "Vídeo demostrativo completo",
|
|
"is_critical": true,
|
|
"priority": "critica",
|
|
"created_at": "2025-01-06T10:00:00Z"
|
|
}
|
|
]
|
|
},
|
|
"links": {
|
|
"protocol_to_guide": [
|
|
{
|
|
"protocol_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"guide_id": "550e8400-e29b-41d4-a716-446655440001"
|
|
}
|
|
],
|
|
"guide_to_protocol": [
|
|
{
|
|
"guide_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
"protocol_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
],
|
|
"manual_to_protocols": [],
|
|
"manual_to_guides": []
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 VERIFICACIÓN DE INTEGRIDAD
|
|
|
|
### Hash SHA-256
|
|
|
|
El hash se calcula sobre el contenido (sin metadata):
|
|
|
|
```typescript
|
|
function calculatePackHash(pack: ContentPack): string {
|
|
const contentToHash = {
|
|
content: pack.content,
|
|
media: pack.media,
|
|
links: pack.links
|
|
};
|
|
|
|
const contentString = JSON.stringify(contentToHash, null, 0);
|
|
const hash = sha256(contentString);
|
|
|
|
return `sha256:${hash}`;
|
|
}
|
|
```
|
|
|
|
**Verificación en app:**
|
|
```typescript
|
|
function verifyPackIntegrity(pack: ContentPack): boolean {
|
|
const calculatedHash = calculatePackHash(pack);
|
|
return calculatedHash === pack.metadata.hash;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📦 GENERACIÓN DEL PACK
|
|
|
|
### Desde Supabase
|
|
|
|
```sql
|
|
-- Query para generar Content Pack
|
|
SELECT json_build_object(
|
|
'metadata', json_build_object(
|
|
'version', '1.0.0',
|
|
'generated_at', NOW(),
|
|
'source', 'supabase',
|
|
'hash', '...', -- Calculado después
|
|
'total_items', (SELECT COUNT(*) FROM content_items WHERE status = 'published'),
|
|
'total_resources', (SELECT COUNT(*) FROM media_resources WHERE status = 'published')
|
|
),
|
|
'content', json_build_object(
|
|
'protocols', (SELECT json_agg(row_to_json(p)) FROM content_items p WHERE p.type = 'protocol' AND p.status = 'published'),
|
|
'guides', (SELECT json_agg(row_to_json(g)) FROM content_items g WHERE g.type = 'guide' AND g.status = 'published'),
|
|
'manuals', (SELECT json_agg(row_to_json(m)) FROM content_items m WHERE m.type = 'manual' AND m.status = 'published'),
|
|
'drugs', (SELECT json_agg(row_to_json(d)) FROM content_items d WHERE d.type = 'drug' AND d.status = 'published'),
|
|
'checklists', (SELECT json_agg(row_to_json(c)) FROM content_items c WHERE c.type = 'checklist' AND c.status = 'published')
|
|
),
|
|
'media', json_build_object(
|
|
'resources', (SELECT json_agg(row_to_json(r)) FROM media_resources r WHERE r.status = 'published'),
|
|
'associations', (SELECT json_agg(row_to_json(a)) FROM content_resource_associations a)
|
|
),
|
|
'links', json_build_object(
|
|
'protocol_to_guide', (SELECT json_agg(json_build_object('protocol_id', p.id, 'guide_id', g.id)) FROM content_items p, content_items g WHERE p.type = 'protocol' AND g.type = 'guide' AND g.id = ANY(p.related_guide_ids)),
|
|
'guide_to_protocol', (SELECT json_agg(json_build_object('guide_id', g.id, 'protocol_id', p.id)) FROM content_items g, content_items p WHERE g.type = 'guide' AND p.type = 'protocol' AND p.id = ANY(g.related_protocol_ids)),
|
|
'manual_to_protocols', (SELECT json_agg(json_build_object('manual_id', m.id, 'protocol_ids', m.related_protocol_ids)) FROM content_items m WHERE m.type = 'manual' AND m.related_protocol_ids IS NOT NULL)
|
|
)
|
|
) as content_pack;
|
|
```
|
|
|
|
---
|
|
|
|
## 📥 CONSUMO EN APP
|
|
|
|
### Cache Local (IndexedDB)
|
|
|
|
```typescript
|
|
interface CachedContentPack {
|
|
pack: ContentPack;
|
|
cached_at: string;
|
|
expires_at: string;
|
|
}
|
|
|
|
// Almacenar pack
|
|
async function cacheContentPack(pack: ContentPack): Promise<void> {
|
|
const cached: CachedContentPack = {
|
|
pack,
|
|
cached_at: new Date().toISOString(),
|
|
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 días
|
|
};
|
|
|
|
await db.put('content_packs', cached);
|
|
}
|
|
|
|
// Obtener pack cacheado
|
|
async function getCachedContentPack(): Promise<ContentPack | null> {
|
|
const cached = await db.get('content_packs', 'latest');
|
|
|
|
if (!cached) return null;
|
|
|
|
// Verificar expiración
|
|
if (new Date(cached.expires_at) < new Date()) {
|
|
return null;
|
|
}
|
|
|
|
// Verificar integridad
|
|
if (!verifyPackIntegrity(cached.pack)) {
|
|
return null;
|
|
}
|
|
|
|
return cached.pack;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ VALIDACIÓN
|
|
|
|
### Checklist de Validación
|
|
|
|
- [ ] Metadata completa y válida
|
|
- [ ] Hash calculado correctamente
|
|
- [ ] Solo contenido `published`
|
|
- [ ] Solo última versión de cada item
|
|
- [ ] Recursos asociados existen
|
|
- [ ] Enlaces bidireccionales correctos
|
|
- [ ] JSON válido y parseable
|
|
- [ ] Tamaño razonable (< 10MB recomendado)
|
|
|
|
---
|
|
|
|
**Fin de la Especificación**
|
|
|