Ir al contenido

FAQ y troubleshooting

Esta página recopila las preguntas que tienden a aparecer cuando algo no se comporta como debería, o cuando el equipo necesita una decisión rápida sobre cómo proceder. Las respuestas están escritas para PMs, soporte y staff técnico que necesita resolver un caso concreto sin abrir el código.

El provisioning de tenants no se hace desde EMA Clinic. Es responsabilidad de EMA Vault, que orquesta la creación cross-product y emite los datos a clinic vía un POST a /api/platform/tenants con el header X-Platform-Key.

EMA Clinic recibe los datos esenciales —tenant_id, slug, country— y los registra en emashared.tenants. El tenant queda operativo cuando además existen sus credenciales OAuth en tenant_aidbox_credentials; mientras esa fila no esté presente, el handler responde con HTTP 503 hasta que Vault complete el setup correspondiente con Aidbox. El custom domain del tenant (<slug>.emahealth.io) se configura por ahora a mano vía npx wrangler domains add y queda automatizado para Wave 4.

El paciente ve servicios de otro tenant en el booking público

Sección titulada «El paciente ve servicios de otro tenant en el booking público»

Esto suele indicar un bug en publicBookingRoutes. El síntoma es que la query FHIR está cargando HealthcareService sin filtro _tag ni scope por organización, por lo que retorna recursos de cualquier tenant cargado en la misma Aidbox. La revisión obligada es src/api/public-booking.ts, en particular el bloque que lista los servicios disponibles para reserva:

// DEBE tener:
const services = await c.get('fhir').search('HealthcareService', {
_tag: `urn:ema:tenant|${tenantId}`,
});

Si este filtro está ausente, el booking público queda expuesto incluso con ORGBAC funcionando, porque ORGBAC opera sobre la URL pero esta query usa el FHIR client genérico.

Hay dos causas posibles, y conviene chequearlas en orden. La primera es que la fila correspondiente en emashared.tenant_emi_access tenga is_active = false: en ese caso el hook useEmiAccess() devuelve falso y el drawer queda escondido. Activarla manualmente es la solución hasta que Wave 4 traiga la UI de configuración en Vault. La segunda causa común es que el secret EMI_API_KEY no esté configurado en el worker, lo que hace que /api/emi/chat responda con 401. La verificación rápida es wrangler secret list. Para diagnóstico end-to-end, GET /api/emi/access debe responder { is_active: true }.

Por diseño no debería funcionar todavía: la FEA está explícitamente fuera del alcance de la versión 0.2.0. La firma actual, basada en QR (decisión F109), combina Practitioner.id, timestamp en metadata y un código de verificación público. Cumple con los requerimientos regulatorios habituales y la FEA está planificada para la versión 1.5, una vez que un cliente concreto lo demande para emitir recetas retenidas.

Una urgencia con ESI sugerido 3 fue triada como ESI 2

Sección titulada «Una urgencia con ESI sugerido 3 fue triada como ESI 2»

El override de la triagista es legítimo y esperado en muchos casos, pero exige registro. Si la fila correspondiente en triage_esi_audit no tiene final_esi=2 y override_reason poblados, el cambio no quedó completo y las métricas de calidad operacional aparecerán inconsistentes. La triagista debe ejecutar POST /api/urgencia/triage-audit con ambos campos para regularizar el caso. Una verificación rápida del triage_id en la tabla confirma si el override está bien registrado.

El reporte REM no incluye pacientes del mes anterior

Sección titulada «El reporte REM no incluye pacientes del mes anterior»

El motor REM filtra por Encounter.period.start con la lógica >= {month}-01 AND < {month+1}-01. Si un paciente fue atendido en abril pero el reporte se está solicitando con la fecha de mayo, no aparecerá: lo correcto es solicitar el REM con la fecha del mes en que ocurrió la atención, no del mes del informe.

Los precios no aplican correctamente con cobertura institucional

Sección titulada «Los precios no aplican correctamente con cobertura institucional»

La causa habitual está en datos mal cargados, en el orden siguiente. Lo primero a verificar es el campo Coverage.class: debe coincidir exactamente con uno de los tramos definidos por el plug-in del país (en el plug-in chileno, fonasa-tramo-A, fonasa-tramo-B, fonasa-tramo-C o fonasa-tramo-D); sin tramo, el copago default cae a cero. Si la cobertura es del segmento privado con bono (en Chile, Isapre), el Invoice.payment_method debe quedar identificado correctamente para que el ruteo invoque al cliente de validación correspondiente (en Chile, el cliente imed). Si los datos están bien y el monto sigue mal, queda revisar copago-engine.ts por edge cases —rounding o mismatches de rangos etarios suelen ser los responsables.

Aparece “Tenant no resuelto” al loguearse

Sección titulada «Aparece “Tenant no resuelto” al loguearse»

Tres causas explican la mayoría de estos casos:

CausaDiagnóstico
Usuario no presente en user_rolesSELECT * FROM user_roles WHERE user_id = '<uuid>' debe traer al menos una fila
tenant_id en localStorage no matcheaLimpiar localStorage del navegador
tenants.is_active = falseActivar tenant en emashared.tenants

La solución estándar consiste en que el admin del tenant ejecute INSERT INTO emashared.user_roles (user_id, tenant_id, role) con los valores apropiados.

Una clínica está viendo pacientes de otra clínica

Sección titulada «Una clínica está viendo pacientes de otra clínica»

Esto es un incidente crítico de seguridad y debe escalarse a infosec inmediatamente. La causa probable es una query FHIR sin filtro _tag o sin scope por Organization: el handler implicado debe revisarse de inmediato.

// MAL — sin filtro
const patients = await c.get('fhir').search('Patient', { name: q });
// BIEN — con scope (ORGBAC ya scope-a por URL pero el filtro es
// defense in depth para queries cross-resource)
const patients = await c.get('fhir').search('Patient', {
name: q,
_tag: `urn:ema:tenant|${tenantId}`,
});

Si la query carece del filtro pero igualmente no se observa fuga, eso indica que ORGBAC está funcionando como debe, pero la corrección sigue siendo obligatoria como defensa en profundidad. Conviene además revisar los logs de Aidbox por el mismo periodo para descartar intentos exitosos.

El JWT de Supabase expira después de aproximadamente una hora, configurable en project settings. Lo correcto es que el front-end refresque el token antes del expiry o re-autentique al usuario; si el hook useAuthContext() no implementa auto-refresh, la sesión se pierde sin aviso y el usuario debe volver a loguearse.

El layout es responsive, con tres breakpoints específicos. A 1024 px o más, el sidebar queda fijo. Entre 640 y 1024 px (tablet), el sidebar pasa a overlay y se abre por interacción. Bajo 640 px (mobile), el sidebar desaparece y se reemplaza por una barra inferior con cuatro ítems principales más overflow. Si los breakpoints no se aplican, revisar tailwind.config.js y src/client/components/layout/Layout.tsx en ese orden suele dar con el problema.

Cuando Aidbox queda inaccesible, la SPA sigue operativa para los módulos no FHIR: settings, caja y audit funcionan con normalidad. Los endpoints que requieren FHIR responden 503 con el mensaje correspondiente, lo que impide abrir fichas o consultar Encounter/Patient. Las escrituras a audit_logs y a cash_movements siguen ocurriendo, igual que el cron de tasks vencidas. Cuando Aidbox vuelve, no hay reintento automático: las DRs y tasks pendientes que hayan sido impactadas durante la caída deben regenerarse manualmente.

Aquí la degradación es más severa. La autenticación falla por completo, lo que impide crear sesiones nuevas. Las sesiones existentes con JWT válido pueden seguir leyendo FHIR (Aidbox no depende de Supabase para validar el token) pero la caja, el audit y las tasks de scheduling dejan de operar. Supabase es dependencia crítica del producto, por lo que conviene tener informados a los usuarios cuando ocurre un incidente.