fix: Service Worker registration and API routing issues
- Move Service Worker to root (/sw.js) for proper scope registration - Add route to serve sw.js from root with correct content-type - Update push-notifications.js to register sw.js from root - Fix all notification API URLs to use /api/v1/ prefix - Update notificationsApi.js to use correct v1 endpoints
This commit is contained in:
parent
02f5219d75
commit
73b082aae5
@ -2,6 +2,8 @@ from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.static import serve
|
||||
import os
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
@ -14,5 +16,7 @@ urlpatterns = [
|
||||
path("search/", include("search.urls", namespace="search")),
|
||||
path("finance/", include("finance.urls", namespace="finance")),
|
||||
path("api/", include(("api.urls", "api"), namespace="api")),
|
||||
# Service Worker должен быть в корне для правильного scope
|
||||
path("sw.js", serve, {"document_root": settings.BASE_DIR, "path": "sw.js", "content_type": "application/javascript"}, name="sw"),
|
||||
path("", include("feed.urls_front", namespace="feed")),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
@ -63,7 +63,7 @@ export async function getNotifications(params = {}, ttl = 10000) {
|
||||
return dataManager.fetch(
|
||||
key,
|
||||
async () => {
|
||||
const url = new URL('/api/notifications/', window.location.origin);
|
||||
const url = new URL('/api/v1/notifications/', window.location.origin);
|
||||
Object.keys(defaultParams).forEach(k => {
|
||||
if (defaultParams[k] != null) {
|
||||
url.searchParams.set(k, String(defaultParams[k]));
|
||||
@ -113,7 +113,7 @@ export async function getUnreadCount(ttl = 5000) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function markAsRead(notificationId) {
|
||||
const url = `/api/notifications/${notificationId}/mark-read/`;
|
||||
const url = `/api/v1/notifications/${notificationId}/read/`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -137,7 +137,7 @@ export async function markAsRead(notificationId) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function markAllAsRead() {
|
||||
const url = '/api/notifications/mark-all-read/';
|
||||
const url = '/api/v1/notifications/read-all/';
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: authHeaders()
|
||||
|
||||
@ -92,8 +92,8 @@ class PushNotificationsManager {
|
||||
* @private
|
||||
*/
|
||||
async _registerServiceWorker() {
|
||||
// Service Worker должен быть в корне для максимального scope
|
||||
this.swRegistration = await navigator.serviceWorker.register('/static/sw.js', {
|
||||
// Service Worker находится в корне для правильного scope
|
||||
this.swRegistration = await navigator.serviceWorker.register('/sw.js', {
|
||||
scope: '/'
|
||||
});
|
||||
|
||||
|
||||
192
backend/sw.js
Normal file
192
backend/sw.js
Normal file
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Service Worker для обработки Web Push уведомлений
|
||||
*
|
||||
* Этот файл должен находиться в корне static/ для правильного scope.
|
||||
* Service Worker перехватывает push-события от браузера и показывает
|
||||
* нативные уведомления даже когда страница закрыта.
|
||||
*/
|
||||
|
||||
// Версия Service Worker (для обновления кэша при изменениях)
|
||||
const SW_VERSION = '1.0.0';
|
||||
|
||||
// Настройки по умолчанию для уведомлений
|
||||
const DEFAULT_NOTIFICATION_OPTIONS = {
|
||||
icon: '/static/img/logo-192.png',
|
||||
badge: '/static/img/badge-72.png',
|
||||
vibrate: [100, 50, 100],
|
||||
requireInteraction: false,
|
||||
renotify: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Событие установки Service Worker
|
||||
*/
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Service Worker installing, version:', SW_VERSION);
|
||||
// Немедленно активировать новую версию
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
/**
|
||||
* Событие активации Service Worker
|
||||
*/
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Service Worker activated, version:', SW_VERSION);
|
||||
// Захватить все клиенты без перезагрузки
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
/**
|
||||
* Обработка входящих push-уведомлений
|
||||
*
|
||||
* Формат данных с сервера:
|
||||
* {
|
||||
* "title": "Заголовок уведомления",
|
||||
* "body": "Текст уведомления",
|
||||
* "icon": "/static/img/...", // опционально
|
||||
* "badge": "/static/img/...", // опционально
|
||||
* "tag": "notification-id", // для группировки
|
||||
* "data": {
|
||||
* "url": "/path/to/page", // куда перейти при клике
|
||||
* "notification_id": 123,
|
||||
* "category": "requests",
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('[SW] Push received:', event);
|
||||
|
||||
let notificationData = {
|
||||
title: 'Новое уведомление',
|
||||
body: '',
|
||||
...DEFAULT_NOTIFICATION_OPTIONS,
|
||||
data: {}
|
||||
};
|
||||
|
||||
if (event.data) {
|
||||
try {
|
||||
const payload = event.data.json();
|
||||
console.log('[SW] Push payload:', payload);
|
||||
|
||||
notificationData = {
|
||||
...notificationData,
|
||||
title: payload.title || notificationData.title,
|
||||
body: payload.body || payload.message || '',
|
||||
icon: payload.icon || notificationData.icon,
|
||||
badge: payload.badge || notificationData.badge,
|
||||
tag: payload.tag || `notification-${Date.now()}`,
|
||||
data: payload.data || {}
|
||||
};
|
||||
|
||||
// Дополнительные опции
|
||||
if (payload.image) {
|
||||
notificationData.image = payload.image;
|
||||
}
|
||||
if (payload.actions) {
|
||||
notificationData.actions = payload.actions;
|
||||
}
|
||||
if (payload.requireInteraction !== undefined) {
|
||||
notificationData.requireInteraction = payload.requireInteraction;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// Если данные не JSON, используем как текст
|
||||
console.log('[SW] Push data is not JSON, using as text');
|
||||
notificationData.body = event.data.text();
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем уведомление
|
||||
const promiseChain = self.registration.showNotification(
|
||||
notificationData.title,
|
||||
{
|
||||
body: notificationData.body,
|
||||
icon: notificationData.icon,
|
||||
badge: notificationData.badge,
|
||||
tag: notificationData.tag,
|
||||
data: notificationData.data,
|
||||
vibrate: notificationData.vibrate,
|
||||
requireInteraction: notificationData.requireInteraction,
|
||||
renotify: notificationData.renotify,
|
||||
image: notificationData.image,
|
||||
actions: notificationData.actions,
|
||||
}
|
||||
);
|
||||
|
||||
event.waitUntil(promiseChain);
|
||||
});
|
||||
|
||||
/**
|
||||
* Обработка клика по уведомлению
|
||||
*/
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
console.log('[SW] Notification click:', event);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
// Получаем URL для перехода
|
||||
const notificationData = event.notification.data || {};
|
||||
let urlToOpen = notificationData.url || '/';
|
||||
|
||||
// Обработка действий (action buttons)
|
||||
if (event.action) {
|
||||
console.log('[SW] Action clicked:', event.action);
|
||||
// Можно добавить обработку разных действий
|
||||
if (notificationData.actions && notificationData.actions[event.action]) {
|
||||
urlToOpen = notificationData.actions[event.action].url || urlToOpen;
|
||||
}
|
||||
}
|
||||
|
||||
// Открываем или фокусируем окно
|
||||
event.waitUntil(
|
||||
self.clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then((clientList) => {
|
||||
// Ищем уже открытое окно с нашим сайтом
|
||||
for (const client of clientList) {
|
||||
const clientUrl = new URL(client.url);
|
||||
const targetUrl = new URL(urlToOpen, self.location.origin);
|
||||
|
||||
// Если найдено окно с тем же origin - фокусируем и переходим
|
||||
if (clientUrl.origin === targetUrl.origin && 'focus' in client) {
|
||||
return client.navigate(urlToOpen).then(() => client.focus());
|
||||
}
|
||||
}
|
||||
|
||||
// Если нет открытого окна - открываем новое
|
||||
if (self.clients.openWindow) {
|
||||
return self.clients.openWindow(urlToOpen);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Обработка закрытия уведомления без клика
|
||||
*/
|
||||
self.addEventListener('notificationclose', (event) => {
|
||||
console.log('[SW] Notification closed:', event.notification.tag);
|
||||
|
||||
// Можно отправить на сервер информацию о закрытии
|
||||
// Например, для аналитики
|
||||
const notificationData = event.notification.data || {};
|
||||
if (notificationData.notification_id) {
|
||||
// fetch('/api/v1/notifications/' + notificationData.notification_id + '/dismissed/', {
|
||||
// method: 'POST',
|
||||
// credentials: 'include'
|
||||
// }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Получение сообщений от основной страницы
|
||||
*/
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('[SW] Message received:', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SW] Service Worker loaded, version:', SW_VERSION);
|
||||
Loading…
x
Reference in New Issue
Block a user