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)