1/* Firebase Cloud Messaging service worker (push web do Escute.ai). 2 * Recebe mensagens em segundo plano e mostra a notificação. As chaves abaixo são 3 * PÃBLICAS (config web do Firebase) â não há segredo aqui. Coexiste com /sw.js 4 * (PWA) por usar escopo próprio do FCM. */ 5importScripts('https://www.gstatic.com/firebasejs/10.12.2/firebase-app-compat.js') 6importScripts('https://www.gstatic.com/firebasejs/10.12.2/firebase-messaging-compat.js') 7 8firebase.initializeApp({ 9 apiKey: 'AIzaSyDY6NY_9xcdddV5zbXW2KkI7lhSrMIQkSo', 10 authDomain: 'escute-ai.firebaseapp.com', 11 projectId: 'escute-ai', 12 storageBucket: 'escute-ai.firebasestorage.app', 13 messagingSenderId: '776325154142', 14 appId: '1:776325154142:web:6ae018f7aeeb246d777bcf', 15 measurementId: 'G-SSH42E8784', 16}) 17 18const messaging = firebase.messaging() 19 20// Convite de voz: notificação com ações. Chega data-only (o servidor omite o 21// bloco `notification` justamente para o SW poder desenhá-la com botões). 22const VOICE_INVITE_TYPE = 'voice_invite' 23 24// Mensagens em segundo plano (aba fechada/sem foco). 25messaging.onBackgroundMessage(payload => { 26 const title = payload.notification?.title || payload.data?.title || 'Escute.ai' 27 const body = payload.notification?.body || payload.data?.body || '' 28 const url = payload.fcmOptions?.link || payload.data?.path || '/home' 29 // Fase 3 (persona): push do caderno manda data.icon = avatar da voz atual â 30 // no web o Ãcone da notificação à a "foto do remetente". Genérico mantém a 31 // marca (icon-192). 32 const icon = payload.data?.icon || '/icons/icon-192.png' 33 34 if (payload.data?.type === VOICE_INVITE_TYPE && payload.data?.inviteId && payload.data?.deliveryId) { 35 const inviteId = payload.data.inviteId 36 // Identidade da AÃÃO. Sem ela o servidor não consegue distinguir este 37 // aparelho dos outros que receberam o mesmo convite, e a garantia de 38 // sessão única cai â por isso o convite sem `deliveryId` nem é desenhado. 39 const deliveryId = payload.data.deliveryId 40 self.registration.showNotification(title, { 41 body, 42 icon, 43 badge: '/icons/icon-192.png', 44 // `actions` é ignorado silenciosamente onde não há suporte â nesse caso 45 // sobra o corpo inteiro, que equivale a Atender. Degradação, não quebra. 46 actions: [ 47 { action: 'answer', title: 'Atender' }, 48 { action: 'decline', title: 'Agora não' }, 49 ], 50 data: { url, inviteId, deliveryId, voiceInvite: true }, 51 vibrate: [150, 50, 150], 52 // Sem `renotify`: a reconciliação de um envio perdido reenvia o 53 // MESMO convite, e renotify faria o segundo alertar de novo. A tag 54 // estável substitui a notificação em silêncio â um alerta, nunca dois. 55 tag: `voice-invite-${inviteId}`, 56 requireInteraction: false, 57 }) 58 return 59 } 60 61 self.registration.showNotification(title, { 62 body, 63 icon, 64 badge: '/icons/icon-192.png', 65 data: { url }, 66 vibrate: [150, 50, 150], 67 requireInteraction: false, 68 }) 69}) 70 71self.addEventListener('notificationclick', event => { 72 event.notification.close() 73 const data = event.notification.data || {} 74 const url = data.url || '/home' 75 76 // AGORA NÃO: não abre janela, não navega, não inicia nada. Só informa o 77 // servidor, que é a autoridade sobre o desfecho. Falha de rede não vira 78 // recusa local â o convite fica como está e a varredura o encerra. 79 if (data.voiceInvite && data.inviteId && data.deliveryId && event.action === 'decline') { 80 event.waitUntil( 81 fetch(`/api/agent/voice-invites/${data.inviteId}/decline`, { 82 method: 'POST', 83 headers: { 'Content-Type': 'application/json' }, 84 credentials: 'same-origin', 85 body: JSON.stringify({ platform: 'web', deliveryId: data.deliveryId }), 86 }).catch(() => undefined) 87 ) 88 return 89 } 90 91 // Qualquer outro caminho â botão Atender OU toque no corpo â abre o destino. 92 event.waitUntil( 93 self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(list => { 94 const existing = list.find(c => c.url.includes(url)) 95 if (existing) {
96 // Já há janela aberta: navega para o destino do convite em vez de só 97 // focar. Focar sem navegar deixaria a pessoa na tela em que estava, e 98 // o convite morreria sem desfecho. 99 if (data.voiceInvite && 'navigate' in existing) { 100 return existing.navigate(url).then(c => (c || existing).focus()).catch(() => existing.focus()) 101 } 102 return existing.focus() 103 } 104 return self.clients.openWindow(url) 105 }) 106 ) 107})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.