19 KiB
19 KiB
📦 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
{
"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
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:
{
"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:
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 portitle
3. Media
Contiene recursos multimedia y sus asociaciones:
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
priorityyuploaded_at
4. Links
Enlaces bidireccionales entre contenido:
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
{
"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):
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:
function verifyPackIntegrity(pack: ContentPack): boolean {
const calculatedHash = calculatePackHash(pack);
return calculatedHash === pack.metadata.hash;
}
📦 GENERACIÓN DEL PACK
Desde Supabase
-- 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)
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