From 4441661f627b9e809635eaa73c552eb5d2effbe5 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 23 Apr 2026 10:03:24 -0300 Subject: [PATCH] =?UTF-8?q?Toast=20system=5Falert:=20agregar=20no=20catch-?= =?UTF-8?q?up=20pra=20n=C3=A3o=20empilhar=20enxurrada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: acumulando N system_alert não-lidas, o refreshAndMaybeAlert (mount / visibilitychange / polling 60s) disparava N toasts de uma vez. Comum após recarregar a página com alertas pendentes do último teste. Fix: no catch-up, mostra só a notif mais recente, com sufixo "+N outros alertas no sino" no detail se houver múltiplas. As demais são marcadas no alertedIds pra não redisparar — continuam visíveis no sininho/drawer com badge. Eventos novos via Realtime seguem aparecendo individualmente (fluxo normal — o usuário está online vendo chegar). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/composables/useNotifications.js | 38 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/composables/useNotifications.js b/src/composables/useNotifications.js index dccfc4d..41aff7f 100644 --- a/src/composables/useNotifications.js +++ b/src/composables/useNotifications.js @@ -65,16 +65,40 @@ export function useNotifications() { showToastFor(item); } - // Re-carrega notifs do DB e dispara toast pras system_alert não-lidas - // que ainda não foram vistas nesta sessão. Usado no mount, no visibilitychange - // e no polling de fallback pro caso de Realtime perder eventos. + // Re-carrega notifs do DB e dispara toast AGREGADO pras system_alert + // não-lidas ainda não vistas nesta sessão. Se tiver várias, mostra só + // a mais recente com sufixo "(+N outros alertas)" no detail pra evitar + // enxurrada de toasts ao voltar pra aba / recarregar. Eventos novos via + // Realtime continuam aparecendo individualmente (showToastFor direto). async function refreshAndMaybeAlert() { if (!ownerId) return; await store.load(ownerId); - for (const item of store.items || []) { - if (item.type !== 'system_alert') continue; - if (item.read_at || item.archived) continue; - showToastFor(item); + + const pending = (store.items || []) + .filter((i) => i.type === 'system_alert' && !i.read_at && !i.archived && !alertedIds.has(i.id)); + + if (pending.length === 0) return; + + // Marca os demais como já "alertados" nesta sessão pra não redisparar + // nos próximos ticks de polling/visibility — eles continuam no sininho. + const newest = pending[0]; // store.items já vem ordenado por created_at DESC + for (let i = 1; i < pending.length; i++) { + alertedIds.add(pending[i].id); + } + + if (pending.length > 1) { + const extra = pending.length - 1; + const suffix = ` • +${extra} outro${extra === 1 ? '' : 's'} alerta${extra === 1 ? '' : 's'} no sino`; + const patched = { + ...newest, + payload: { + ...(newest.payload || {}), + detail: `${newest.payload?.detail || ''}${suffix}` + } + }; + showToastFor(patched); + } else { + showToastFor(newest); } }