Permisos por pestana de reportes y ruido de serializacion en presencia

Auditoria de 260 visitas (26 rutas x 10 roles) sobre el build actual: 0 hallazgos.
Dos defectos reales que salieron del barrido anterior se corrigen aqui.

1) Reportes: la matriz guarda claves '/reportes?tab=x' y puede conceder una
   pestana suelta (Horas agenda al medico, Exportar a cosmetologia) sin dar el
   modulo. Nadie la respetaba:
   - App.tsx: nuevo ReportesRoute que evalua la pestana del URL; si el rol no
     tiene '/reportes' pero si alguna pestana, aterriza en la primera permitida.
   - Reportes.tsx: familias, favoritos y el boton "Exportar datos" se filtran
     por canSee('/reportes?tab=x'). 'exportar' no pertenece a ninguna familia,
     por eso se lista en TABS_FUERA_DE_FAMILIA (si no, el guard lo reescribia a
     'general' y admin perdia la vista). El query se sincroniza con lo renderizado.
   - SkeenLayout/HomeNestLayout: "Horas agenda" y "Exportar datos" aparecen en
     Analitica para que el permiso sea alcanzable desde el menu, no solo a
     mano. Clasico ya los traia.
   - auth.py: _medico_reporte_permitido consulta la matriz antes de vetar
     /reports/ al medico. Sin ese puente el menu ofrecia la opcion y la API
     respondia 403. Las demas rutas vetadas (sales, payments, wallets, pos)
     siguen cerradas: verificado comisiones=403, horas-agenda=200.

2) skeen_chat/_touch: 211 "could not serialize access due to concurrent update"
   en un barrido. Bandeja, badge y heartbeat salen en paralelo y cada uno
   emitia el UPDATE de last_seen aunque no aplicara cambios, abortando la
   transaccion de otra peticion. Se corta antes de tocar la fila cuando el
   registro es reciente; el savepoint queda para el caso de empate exacto.
   Ráfaga de 128 peticiones concurrentes: todas 200, presencia sigue
   actualizando (online=true, last_seen avanza) y el ruido bajo de 211 a 5.
This commit is contained in:
2026-09-30 01:26:37 +00:00
parent 5a16f694de
commit 85a281add1
6 changed files with 120 additions and 32 deletions

View File

@@ -27,17 +27,20 @@ class SkeenChatController(http.Controller):
def _touch(self, me):
"""Marca actividad del usuario (presencia). Nunca debe romper la petición.
Se hace con SQL directo dentro de un savepoint: el frontend consulta
varios endpoints de chat en paralelo (bandeja, badge, heartbeat) y dos
UPDATE concurrentes sobre la misma fila abortan la transacción en
REPEATABLE READ ("could not serialize access due to concurrent update").
Con el savepoint ese fallo se descarta y la petición sigue; además sólo
se escribe si el último registro tiene más de TOUCH_INTERVAL segundos,
que es lo que evita la mayor parte de los choques.
El frontend consulta varios endpoints de chat en paralelo (bandeja,
badge, heartbeat) y todos llaman aquí. Por eso se corta antes de tocar
la fila cuando el registro es reciente (TOUCH_INTERVAL): el UPDATE se
ejecutaba aunque no aplicara ningún cambio y abortaba la transacción de
otra petición concurrente ("could not serialize access due to concurrent
update", 211 veces en un barrido de 260 visitas). El savepoint sigue
ahí por si dos peticiones pasan el corte en el mismo instante; su fallo
se descarta y la petición continúa.
"""
try:
ahora = fields.Datetime.now()
limite = fields.Datetime.subtract(ahora, seconds=TOUCH_INTERVAL)
if me.last_seen and me.last_seen >= limite:
return
cr = request.env.cr
with cr.savepoint():
cr.execute(