[@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 body = renderTemplate(template?.body, message);
|
||||
|
||||
// info-уровень: видно сколько эндпоинтов адресуем (диагностика «push не пришёл»).
|
||||
this.logger.log(`web-push → '${username}': ${subscriptions.length} активн. подписк(а), workflow=${message.workflowId}`);
|
||||
|
||||
let anyDelivered = false;
|
||||
for (const subscription of subscriptions) {
|
||||
const dto = new WebPushSubscriptionDto(new WebPushSubscriptionDomainEntity(subscription));
|
||||
|
||||
@@ -142,7 +142,8 @@ export class OutboxWorkerService implements OnModuleInit {
|
||||
row.status = NotificationOutboxStatus.CANCELED;
|
||||
row.lastError = result.error;
|
||||
await this.outboxRepository.save(row);
|
||||
this.logger.debug(`Канал пропущен (неприменим к получателю): ${ctx}: ${result.error}`);
|
||||
// info-уровень: председатель/оператор должен видеть, что и почему не ушло.
|
||||
this.logger.log(`Канал пропущен (неприменим к получателю): ${ctx}: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -164,7 +165,8 @@ export class OutboxWorkerService implements OnModuleInit {
|
||||
if (result.delivered) {
|
||||
row.status = NotificationOutboxStatus.SENT;
|
||||
row.lastError = undefined;
|
||||
this.logger.debug(`Доставлено: ${ctx}`);
|
||||
// info-уровень: каждая успешная доставка видна в логах без рытья в БД.
|
||||
this.logger.log(`Доставлено: ${ctx}${result.providerResponse ? ` (${result.providerResponse})` : ''}`);
|
||||
} else if (row.attempts >= row.maxAttempts) {
|
||||
// Попытки исчерпаны — терминальный 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,
|
||||
useFilenameHashes: true, // Включаем хеширование файлов
|
||||
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
|
||||
// Не включаем ревизию для определенных файлов
|
||||
|
||||
Reference in New Issue
Block a user