[@ant] fix(push): обработчик push в генерируемом SW + видимые логи доставки
Корень «push отправлен, но не виден»: workboxMode GenerateSW игнорирует
sourceFiles.serviceWorker, поэтому push/notificationclick из
custom-service-worker.ts в прод не попадали — браузер push получал, но SW его
не показывал (бэкенд честно «sent to N subscription(s)»). Доказано: прод
/service-worker.js содержит 0 push-обработчиков.
- desktop: public/push-sw.js (push → showNotification, notificationclick →
focus/openWindow) + importScripts('push-sw.js') в extendGenerateSWOptions
- controller: «Доставлено»/«Канал пропущен» debug→info + сводка
«web-push → user: N подписок» — чтобы доставка была видна в логах без БД
This commit is contained in:
+3
@@ -55,6 +55,9 @@ export class WebPushChannelAdapter implements WebPushChannelPort {
|
|||||||
const title = renderTemplate(template?.subject, message) || 'Уведомление';
|
const title = renderTemplate(template?.subject, message) || 'Уведомление';
|
||||||
const body = renderTemplate(template?.body, message);
|
const body = renderTemplate(template?.body, message);
|
||||||
|
|
||||||
|
// info-уровень: видно сколько эндпоинтов адресуем (диагностика «push не пришёл»).
|
||||||
|
this.logger.log(`web-push → '${username}': ${subscriptions.length} активн. подписк(а), workflow=${message.workflowId}`);
|
||||||
|
|
||||||
let anyDelivered = false;
|
let anyDelivered = false;
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
const dto = new WebPushSubscriptionDto(new WebPushSubscriptionDomainEntity(subscription));
|
const dto = new WebPushSubscriptionDto(new WebPushSubscriptionDomainEntity(subscription));
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ export class OutboxWorkerService implements OnModuleInit {
|
|||||||
row.status = NotificationOutboxStatus.CANCELED;
|
row.status = NotificationOutboxStatus.CANCELED;
|
||||||
row.lastError = result.error;
|
row.lastError = result.error;
|
||||||
await this.outboxRepository.save(row);
|
await this.outboxRepository.save(row);
|
||||||
this.logger.debug(`Канал пропущен (неприменим к получателю): ${ctx}: ${result.error}`);
|
// info-уровень: председатель/оператор должен видеть, что и почему не ушло.
|
||||||
|
this.logger.log(`Канал пропущен (неприменим к получателю): ${ctx}: ${result.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +165,8 @@ export class OutboxWorkerService implements OnModuleInit {
|
|||||||
if (result.delivered) {
|
if (result.delivered) {
|
||||||
row.status = NotificationOutboxStatus.SENT;
|
row.status = NotificationOutboxStatus.SENT;
|
||||||
row.lastError = undefined;
|
row.lastError = undefined;
|
||||||
this.logger.debug(`Доставлено: ${ctx}`);
|
// info-уровень: каждая успешная доставка видна в логах без рытья в БД.
|
||||||
|
this.logger.log(`Доставлено: ${ctx}${result.providerResponse ? ` (${result.providerResponse})` : ''}`);
|
||||||
} else if (row.attempts >= row.maxAttempts) {
|
} else if (row.attempts >= row.maxAttempts) {
|
||||||
// Попытки исчерпаны — терминальный failed (виден/переотправляем на столе председателя).
|
// Попытки исчерпаны — терминальный failed (виден/переотправляем на столе председателя).
|
||||||
row.status = NotificationOutboxStatus.FAILED;
|
row.status = NotificationOutboxStatus.FAILED;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/*
|
||||||
|
* Push-обработчик service worker'а.
|
||||||
|
*
|
||||||
|
* Подключается в СГЕНЕРИРОВАННЫЙ Workbox'ом service-worker.js через
|
||||||
|
* `importScripts` (см. quasar.config.cjs → pwa.extendGenerateSWOptions →
|
||||||
|
* cfg.importScripts). Это единственный РАБОЧИЙ источник push-логики на проде:
|
||||||
|
* режим `workboxMode: 'GenerateSW'` игнорирует sourceFiles.serviceWorker, из-за
|
||||||
|
* чего обработчик в src-pwa/custom-service-worker.ts в прод НЕ попадал и push
|
||||||
|
* приходил в браузер, но не показывался (бэкенд при этом честно «sent»).
|
||||||
|
*
|
||||||
|
* Файл в public/ → Quasar кладёт его в корень сборки, importScripts видит как
|
||||||
|
* /push-sw.js. Это обычный SW-скрипт (не ES-модуль): только self.addEventListener.
|
||||||
|
*/
|
||||||
|
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
let data = {};
|
||||||
|
try {
|
||||||
|
if (event.data) {
|
||||||
|
// web-push шлёт JSON-строку {title, body, icon, badge, image, url, tag, ...}
|
||||||
|
data = event.data.json();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
data = { body: event.data && event.data.text() };
|
||||||
|
} catch (e2) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = data.title || 'Новое уведомление';
|
||||||
|
const options = {
|
||||||
|
body: data.body || '',
|
||||||
|
icon: data.icon || '/icons/icon-192x192.png',
|
||||||
|
badge: data.badge || '/icons/icon-192x192.png',
|
||||||
|
image: data.image,
|
||||||
|
tag: data.tag || 'general',
|
||||||
|
requireInteraction: !!data.requireInteraction,
|
||||||
|
silent: !!data.silent,
|
||||||
|
data: {
|
||||||
|
url: data.url || '/',
|
||||||
|
timestamp: data.timestamp || Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (Array.isArray(data.actions) && data.actions.length) {
|
||||||
|
options.actions = data.actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.waitUntil(self.registration.showNotification(title, options));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
const url = (event.notification.data && event.notification.data.url) || '/';
|
||||||
|
event.waitUntil(
|
||||||
|
self.clients
|
||||||
|
.matchAll({ type: 'window', includeUncontrolled: true })
|
||||||
|
.then((clients) => {
|
||||||
|
// Если вкладка приложения уже открыта — фокусируем и навигируем её.
|
||||||
|
for (const client of clients) {
|
||||||
|
if ('focus' in client) {
|
||||||
|
client.focus();
|
||||||
|
if ('navigate' in client && url !== '/') {
|
||||||
|
try {
|
||||||
|
client.navigate(url);
|
||||||
|
} catch (e) {
|
||||||
|
/* navigate доступен не всегда — не критично */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Иначе открываем новую вкладку.
|
||||||
|
if (self.clients.openWindow) return self.clients.openWindow(url);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -274,6 +274,12 @@ module.exports = configure(function (ctx) {
|
|||||||
useCredentialsForManifestTag: false,
|
useCredentialsForManifestTag: false,
|
||||||
useFilenameHashes: true, // Включаем хеширование файлов
|
useFilenameHashes: true, // Включаем хеширование файлов
|
||||||
extendGenerateSWOptions(cfg) {
|
extendGenerateSWOptions(cfg) {
|
||||||
|
// Push-обработчик. В режиме GenerateSW Quasar игнорирует
|
||||||
|
// sourceFiles.serviceWorker (custom-service-worker.ts), поэтому
|
||||||
|
// обработчик push/notificationclick подмешиваем в генерируемый SW
|
||||||
|
// через importScripts из статического public/push-sw.js. Без этого
|
||||||
|
// push доходит до браузера, но SW его не показывает.
|
||||||
|
cfg.importScripts = ['push-sw.js'];
|
||||||
// Увеличиваем максимальный размер файла для кэширования
|
// Увеличиваем максимальный размер файла для кэширования
|
||||||
cfg.maximumFileSizeToCacheInBytes = 5 * 1024 * 1024; // 5MB
|
cfg.maximumFileSizeToCacheInBytes = 5 * 1024 * 1024; // 5MB
|
||||||
// Не включаем ревизию для определенных файлов
|
// Не включаем ревизию для определенных файлов
|
||||||
|
|||||||
Reference in New Issue
Block a user