diff --git a/.gitea/workflows/deploy-app-frontend.yaml b/.gitea/workflows/deploy-app-frontend.yaml index 8af2be3..4ee1920 100644 --- a/.gitea/workflows/deploy-app-frontend.yaml +++ b/.gitea/workflows/deploy-app-frontend.yaml @@ -45,11 +45,11 @@ jobs: - name: deploy production run: mv -v /www/hereconnect.condev.ru/current-prod /www/hereconnect.condev.ru/old-prod && mv -v /www/hereconnect.condev.ru/build-${{env.GITHUB_SHA}}-prod /www/hereconnect.condev.ru/current-prod && rm -rf /www/hereconnect.condev.ru/old-prod -# - name: build dev -# run: cd build/frontend && bun vite build --emptyOutDir --mode development --outDir /www/hereconnect.condev.ru/build-${{env.GITHUB_SHA}}-dev/ -# -# - name: deploy dev -# run: mv -v /www/hereconnect.condev.ru/current-dev /www/hereconnect.condev.ru/old-dev && mv -v /www/hereconnect.condev.ru/build-${{env.GITHUB_SHA}}-dev /www/hereconnect.condev.ru/current-dev && rm -rf /www/hereconnect.condev.ru/old-dev + - name: build dev + run: cd build/frontend && bun vite build --emptyOutDir --mode development --outDir /www/hereconnect.condev.ru/build-${{env.GITHUB_SHA}}-dev/ + + - name: deploy dev + run: mv -v /www/hereconnect.condev.ru/current-dev /www/hereconnect.condev.ru/old-dev && mv -v /www/hereconnect.condev.ru/build-${{env.GITHUB_SHA}}-dev /www/hereconnect.condev.ru/current-dev && rm -rf /www/hereconnect.condev.ru/old-dev - name: cleanup build directories if: always() diff --git a/apps/frontend/public/service-worker.js b/apps/frontend/public/service-worker.js index 07a8b62..0c086d0 100644 --- a/apps/frontend/public/service-worker.js +++ b/apps/frontend/public/service-worker.js @@ -1,189 +1,146 @@ +const VER = 6 + +self.addEventListener('message', async (event) => { + event.waitUntil(debug(event.source, Object.assign({ answer: true }, event.data))) +}) + self.addEventListener('install', (event) => { console.log('install event:', event) // The promise that skipWaiting() returns can be safely ignored. - self.skipWaiting() + event.waitUntil(self.skipWaiting().then(() => debug(event.source, { installed: true }))) // Perform any other actions required for your // service worker to install, potentially inside // of event.waitUntil(); }) -const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent) - self.addEventListener('activate', (event) => { event.waitUntil( - clients.claim(), // Привязка нового сервис-воркера к текущим клиентам + // Привязка нового сервис-воркера к текущим клиентам + clients.claim().then(() => debug(event.source, { activated: true })), ) }) self.addEventListener('push', (event) => { - const data = event.data.json() - console.log('Push событие получено:', data.type) - - // Отображение уведомления - if (data.type === 'notification') { - event.waitUntil( - self.registration - .showNotification(data.notification.title, data.notification.options) - .then(() => { - console.log('Notification создан', data.notification.options.tag) - }) - .catch((error) => { - console.error('Notification ошибка создания', error) - }), - ) - } else if (data.type === 'cancelNotification') { - event.waitUntil( - self.registration - .getNotifications({ tag: data.notification.options.tag }) - .then((notifications) => { - notifications.forEach((notification) => { - if ( - notification.tag === data.notification.options.tag || - notification.data.payload.tag === data.notification.options.data.payload.tag // удалить, если notification.tag поддерживают все - ) { - notification.close() - console.log('Notification удален') - } - }) - }), - ) + // PushData keys structure standart https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification + let pushData = event.data.json() + if (!pushData || !pushData.notification.title) { + console.error('Received WebPush with an empty title. Received body: ', pushData) } + self.registration + .showNotification(pushData.notification.title, pushData.notification.options) + .then(function () { + // You can save to your analytics fact that push was shown + // fetch('https://your_backend_server.com/track_show?message_id=' + pushData.data.message_id); + }) }) -self.addEventListener('notificationclose', function (event) { - const dismissedNotification = event.notification - - console.log('dismissedNotification', dismissedNotification) -}) - -self.addEventListener('notificationclick', (event) => { - console.log('click to notification', event.notification) - +async function handleNotificationClick(event) { event.notification.close() - // const urlToOpen = new URL(event.notification.data.url, self.location.origin).href - + if (!event.notification.data) { + await debug(null, { + error: 'Click on WebPush with empty data, where url should be. Notification', + }) + console.error( + 'Click on WebPush with empty data, where url should be. Notification: ', + event.notification, + ) + return + } if (!event.notification.data.url) { - console.log('no urlToOpen') + await debug(null, { error: 'Click on WebPush without url. Notification' }) + console.error('Click on WebPush without url. Notification: ', event.notification) return } - function getNotificationUrl(urlToOpen) { - return urlToOpen - // return fetch(urlToOpen, { - // redirect: 'manual', - // method: 'HEAD', - // cache: 'no-cache', - // }) - // .then((response) => { - // console.log('response.url', response.url) - // console.log('response.type', response.url) - // if (response && response.type === 'opaqueredirect') { - // const location = response.headers.get('location') - // console.log('headers location', location) - // return location - // } - // return response.url - // }) - // .catch((error) => { - // console.debug('erro in getNotificationUrl', error) - // return urlToOpen - // }) - } + await openURL(event.notification.data.url) +} - // Открытие URL из данных уведомления - event.waitUntil( - Promise.all([ - getNotificationUrl(event.notification.data.url), +self.addEventListener('notificationclick', (event) => { + event.waitUntil(handleNotificationClick(event)) +}) - clients.matchAll({ type: 'window', includeUncontrolled: true }), - ]) - .then(([urlToOpen, clientList]) => { - const urlToOpenObj = new URL(urlToOpen, location.origin) - for (const client of clientList) { - console.log('client.url', client.url) - const clientUrlObj = new URL(client.url) +async function openURL(urlToOpen) { + let matchedClients + try { + const urlToOpenObj = new URL(urlToOpen, location.origin) + + matchedClients = await clients.matchAll({ type: 'window', includeUncontrolled: true }) + + try { + for (const client of matchedClients) { + const clientUrlObj = new URL(client.url) + try { if ( - urlToOpenObj.pathname + urlToOpenObj.search === + urlToOpenObj.pathname + urlToOpenObj.search !== clientUrlObj.pathname + clientUrlObj.search ) { - console.log('focus') - - return Promise.all([ - 'focus' in client && client.focus(), - client.postMessage({ - type: 'notificationClick', - tag: event.notification.tag, - payload: event.notification.payload, - url: urlToOpen, - matchUrl: true, - isIOS, - }), - ]) - } - } - - if (isIOS && clientList.length > 0) { - const client = clientList[0] - return Promise.all([ - client.focus(), - client.postMessage({ - type: 'notificationClick', - tag: event.notification.tag, - payload: event.notification.payload, - url: urlToOpen, - matchUrl: false, - isIOS, - }), - ]) - } - - if (clients.openWindow) { - console.log('open new window', urlToOpen) - - return clients.openWindow(urlToOpen).then((client) => { - if (client) { - return client.postMessage({ - type: 'notificationClick', - tag: event.notification.tag, - payload: event.notification.payload, - url: urlToOpen, - matchUrl: true, - isIOS, - }) - } else { - console.log('new window not opened') - for (const client of clientList) { - return client.postMessage({ - type: 'notificationClick', - tag: event.notification.tag, - payload: event.notification.payload, - url: urlToOpen, - matchUrl: false, - isIOS, - }) - } - } - }) - } else { - console.log('clients.openWindow not available') - - for (const client of clientList) { - return client.postMessage({ - type: 'notificationClick', - tag: event.notification.tag, - payload: event.notification.payload, - url: urlToOpen, - matchUrl: false, - isIOS, + await debug(client, { + urlToOpenObj: urlToOpenObj.pathname + urlToOpenObj.search, + clientUrlObj: clientUrlObj.pathname + clientUrlObj.search, }) + continue } - } - }) - .catch((error) => { - console.error(error) - }), - ) -}) + if (client.focused) { + return + } + + await client.focus() + return + } catch (error) { + await debug({ error: error.message }) + } + } + } catch (error) { + console.error('get client error', error) + } + } catch (error) { + console.error('openURL error', error) + } + + try { + const client = await clients.openWindow(urlToOpen) + if (!client) { + await debug(client, { error: 'client is null' }) + throw new Error('client is null') + } + } catch (error) { + await debug(matchedClients && matchedClients[0], { error: error.message }) + console.error(error) + } +} + +async function debug(client, data) { + try { + if (!client) { + const matchedClients = await clients.matchAll({ type: 'window', includeUncontrolled: true }) + for (const matchedClient of matchedClients) { + if (matchedClient.focused) { + client = matchedClient + break + } + } + if (!client && matchedClients.length) { + return Promise.all(matchedClients.map((client) => debug(client, data))) + } + } + + if (!client) { + throw new Error('not found client for debug') + } + + if (!client.postMessage) { + console.error('not found client.postMessage for debug', client) + return + } + + await client.postMessage({ + type: 'debug', + debug: Object.assign({ VER }, data || {}), + }) + } catch (error) { + console.error(error) + } +} diff --git a/apps/frontend/src/App.vue b/apps/frontend/src/App.vue index a78fb6f..35d0298 100644 --- a/apps/frontend/src/App.vue +++ b/apps/frontend/src/App.vue @@ -1,7 +1,61 @@ + + diff --git a/apps/frontend/src/components/ChatSubscribeWebPush.vue b/apps/frontend/src/components/ChatSubscribeWebPush.vue index 041ecd8..274afa9 100644 --- a/apps/frontend/src/components/ChatSubscribeWebPush.vue +++ b/apps/frontend/src/components/ChatSubscribeWebPush.vue @@ -61,6 +61,8 @@ import { isNotFoundError } from '@/api/PouchDB' import { saveUserChatSettings } from '@/api/userChatSettings' import { getQrCodeDocument } from '@/api/qrCode' +const debug: (...data: unknown[]) => void = inject('debug')! + // Проверки iOS и запуска из главного экрана const isIOS = computed( () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window && window.MSStream), @@ -99,7 +101,7 @@ async function requestNotificationPermission() { const subscription = await registration.pushManager .subscribe(await subscribeOptionsPromise) .catch(async (error) => { - console.debug('subscribe error', error) + debug('subscribe error', error) const oldSubscription = await registration.pushManager.getSubscription() if (!oldSubscription) { throw error @@ -119,26 +121,23 @@ const getPushSubscription = async () => { const permission = await registration.pushManager.permissionState( await subscribeOptionsPromise, ) - console.debug('permission', permission) - if (permission === 'prompt') { - return - } - if (permission !== 'granted') { + debug('permission', permission) + if (permission === 'denied') { throw new Error(`Разрешение на уведомления не предоставлено: ${permission}`) } const subscription = await registration.pushManager.getSubscription() if (subscription) { - console.debug('Уже есть активная подписка:', subscription) + debug('Уже есть активная подписка:', subscription) return subscription } else { - console.debug('Нет активной подписки на Push-уведомления.') + debug('Нет активной подписки на Push-уведомления.') } } else { - console.warn('Push уведомления или Service Worker не поддерживаются в этом браузере.') + debug('Push уведомления или Service Worker не поддерживаются в этом браузере.') } } catch (error) { - console.error('Произошла ошибка при получении подписки:', error) + debug('Произошла ошибка при получении подписки:', error) throw error } } @@ -151,7 +150,7 @@ const stage = ref<'receivePushSubscription' | 'createPushSubscription' | 'remove const { isPending, isError, error, mutate } = useMutation({ mutationKey: [stage], mutationFn: async () => { - console.debug('stage', stage.value) + debug({ stage: stage.value }) try { if (stage.value === 'receivePushSubscription') { const chatSettingsDocument = await getUserChatSettingsDocument({ @@ -160,13 +159,16 @@ const { isPending, isError, error, mutate } = useMutation({ user_chat_role, }).catch((error) => { if (!isNotFoundError(error)) { - console.debug('chatSettingsDocument error not found - its ok', error) + debug('chatSettingsDocument error', error) throw error } - console.debug('chatSettingsDocument error', error) + debug('chatSettingsDocument not found') // it's ok }) if (chatSettingsDocument) { - console.debug('chatSettingsDocument', chatSettingsDocument) + debug( + 'chatSettingsDocument.is_push_notification_enabled', + chatSettingsDocument.is_push_notification_enabled, + ) } if ( (chatSettingsDocument && chatSettingsDocument.is_push_notification_enabled) || @@ -186,9 +188,11 @@ const { isPending, isError, error, mutate } = useMutation({ } enabled.value = true stage.value = 'removePushSubscription' + debug('subscribed') } else { stage.value = 'createPushSubscription' enabled.value = false + debug('have to createPushSubscription') } } else { stage.value = 'createPushSubscription' @@ -211,7 +215,7 @@ const { isPending, isError, error, mutate } = useMutation({ } } catch (error) { enabled.value = false - console.debug('unknown error at stage ', stage.value, error) + debug('unknown error at stage ', stage.value, error) throw error } return null diff --git a/apps/frontend/src/main.ts b/apps/frontend/src/main.ts index ddbe4fa..9a4e0a2 100644 --- a/apps/frontend/src/main.ts +++ b/apps/frontend/src/main.ts @@ -31,7 +31,6 @@ const MyPreset = definePreset(Aura, { import App from './App.vue' import router from './router' -import { registerServiceWorker } from '@/registerServiceWorker' const app = createApp(App) @@ -61,21 +60,8 @@ app.use(PrimeVue, { }, }, }) +app.config.globalProperties.$swDebug = ref([]) app.mount('#app') -registerServiceWorker() - .then(() => { - console.log('Service Worker успешно зарегистрирован') - navigator.serviceWorker.addEventListener('message', (event) => { - console.debug('serviceWorker message', event.data) - if (event.data.type === 'notificationClick' && !event.data.matchUrl) { - const url = new URL(event.data.url, location.origin) - console.debug('go to', `${url.pathname}${url.search}${url.hash}`) - router.push(`${url.pathname}${url.search}${url.hash}`) - } - }) - }) - .catch(console.error) - window.PouchDB = PouchDB // it for debug in console only diff --git a/apps/frontend/src/registerServiceWorker.ts b/apps/frontend/src/registerServiceWorker.ts index 210fbf0..a641a31 100644 --- a/apps/frontend/src/registerServiceWorker.ts +++ b/apps/frontend/src/registerServiceWorker.ts @@ -3,9 +3,33 @@ export async function registerServiceWorker() { throw new Error('Ваш браузер не поддерживает Service Worker.') } + function requestDebugVersion() { + navigator.serviceWorker?.controller?.postMessage({ + type: 'requestDebugVersion', + }) + } + + navigator.serviceWorker.addEventListener('controllerchange', () => { + console.log('Service Worker controller has changed.') + requestDebugVersion() + }) + try { const registration = await navigator.serviceWorker.register('/service-worker.js') console.log('Service Worker зарегистрирован:', registration) + + requestDebugVersion() + + window.addEventListener( + 'focus', + () => { + navigator.serviceWorker?.controller?.postMessage({ + type: 'focused', + }) + }, + {}, + ) + return registration } catch (error) { console.error('Ошибка регистрации Service Worker:', error)