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.
¿Cómo se agrega un tenant nuevo?
Sección titulada «¿Cómo se agrega un tenant nuevo?»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.
EMI no funciona (botón grisado)
Sección titulada «EMI no funciona (botón grisado)»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 }.
La firma electrónica avanzada no funciona
Sección titulada «La firma electrónica avanzada no funciona»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:
| Causa | Diagnóstico |
|---|---|
Usuario no presente en user_roles | SELECT * FROM user_roles WHERE user_id = '<uuid>' debe traer al menos una fila |
tenant_id en localStorage no matchea | Limpiar localStorage del navegador |
tenants.is_active = false | Activar 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 filtroconst 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.
La sesión se cierra sin previo aviso
Sección titulada «La sesión se cierra sin previo aviso»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 no se ve bien en mobile
Sección titulada «El layout no se ve bien en mobile»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.
Comportamiento ante caída de Aidbox
Sección titulada «Comportamiento ante caída de Aidbox»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.
Comportamiento ante caída de Supabase
Sección titulada «Comportamiento ante caída de Supabase»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.