# 📦 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 { 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 { 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**